Class: KairosMcp::PluginProjector
- Inherits:
-
Object
- Object
- KairosMcp::PluginProjector
- Defined in:
- lib/kairos_mcp/plugin_projector.rb
Overview
Projects SkillSet plugin artifacts to Claude Code plugin/project structure.
Dual-mode:
:project (default) — writes to .claude/skills/, .claude/agents/, .claude/settings.json
:plugin — writes to plugin root skills/, agents/, hooks/hooks.json
Design: log/skillset_plugin_projection_design_v2.2_20260404.md
Defined Under Namespace
Classes: CoincidenceRefused, InstructionModeTooLarge
Constant Summary collapse
- SEED_SKILLS =
%w[kairos-chain].freeze
- PROJECTED_BY =
'kairos-chain'- SAFE_NAME_PATTERN =
/\A[a-zA-Z0-9][a-zA-Z0-9_-]*\z/- ALLOWED_HOOK_COMMANDS =
/\Akairos-/- INSTRUCTION_MODE_MARKER_BEGIN =
'<!-- BEGIN kairos-chain:instruction-mode _projected_by=kairos-chain -->'- INSTRUCTION_MODE_MARKER_END =
'<!-- END kairos-chain:instruction-mode -->'- INSTRUCTION_MODE_REL_PATH =
'kairos/instruction_mode.md'- INSTRUCTION_MODE_SIZE_WARN =
150 * 1024
- INSTRUCTION_MODE_SIZE_REFUSE =
256 * 1024
Instance Attribute Summary collapse
-
#data_dir ⇒ Object
readonly
Returns the value of attribute data_dir.
-
#mode ⇒ Object
readonly
Returns the value of attribute mode.
-
#output_root ⇒ Object
readonly
Returns the value of attribute output_root.
-
#project_root ⇒ Object
readonly
Returns the value of attribute project_root.
Instance Method Summary collapse
-
#initialize(project_root, mode: :auto, data_dir: nil) ⇒ PluginProjector
constructor
Construct a PluginProjector.
-
#instruction_mode_status ⇒ Object
Status summary for the instruction mode projection.
-
#project!(enabled_skillsets, knowledge_entries: []) ⇒ Object
Main entry: project all SkillSet plugin artifacts + L1 knowledge meta skill.
-
#project_if_changed!(enabled_skillsets, knowledge_entries: []) ⇒ Object
Digest-based no-op: skip projection if nothing changed.
-
#project_instruction_mode!(mode_name, body, mode_version: nil) ⇒ Hash
Project the active instruction mode body.
-
#remove_projected_instruction_mode! ⇒ Hash
Remove the projected instruction mode artifact and CLAUDE.md region.
-
#status ⇒ Object
Status summary for MCP tool.
-
#verify ⇒ Object
Verify projected files match manifest.
Constructor Details
#initialize(project_root, mode: :auto, data_dir: nil) ⇒ PluginProjector
Construct a PluginProjector.
42 43 44 45 46 47 48 49 50 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 42 def initialize(project_root, mode: :auto, data_dir: nil) @project_root = project_root @data_dir = data_dir || File.join(project_root, '.kairos') enforce_no_coincidence! @mode = resolve_mode(mode) @output_root = @mode == :plugin ? project_root : File.join(project_root, '.claude') @manifest_path = File.join(@data_dir, 'projection_manifest.json') @instruction_mode_manifest_path = File.join(@data_dir, 'instruction_mode_manifest.json') end |
Instance Attribute Details
#data_dir ⇒ Object (readonly)
Returns the value of attribute data_dir.
30 31 32 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 30 def data_dir @data_dir end |
#mode ⇒ Object (readonly)
Returns the value of attribute mode.
30 31 32 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 30 def mode @mode end |
#output_root ⇒ Object (readonly)
Returns the value of attribute output_root.
30 31 32 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 30 def output_root @output_root end |
#project_root ⇒ Object (readonly)
Returns the value of attribute project_root.
30 31 32 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 30 def project_root @project_root end |
Instance Method Details
#instruction_mode_status ⇒ Object
Status summary for the instruction mode projection.
178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 178 def instruction_mode_status manifest = load_instruction_mode_manifest { mode: @mode, active: !manifest.empty?, mode_name: manifest['mode_name'], mode_version: manifest['mode_version'], artifact_path: manifest['artifact_path'], artifact_size: manifest['artifact_size'], region_present: manifest['region_present'], projected_at: manifest['projected_at'] } end |
#project!(enabled_skillsets, knowledge_entries: []) ⇒ Object
Main entry: project all SkillSet plugin artifacts + L1 knowledge meta skill
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 60 def project!(enabled_skillsets, knowledge_entries: []) previous_manifest = load_manifest current_outputs = {} merged_hooks = @mode == :plugin ? load_seed_hooks : { 'hooks' => {} } enabled_skillsets.each do |ss| next unless ss.has_plugin? plugin_dir = File.join(ss.path, 'plugin') project_skill!(ss, plugin_dir, current_outputs) project_agents!(ss, plugin_dir, current_outputs) collect_hooks!(ss, plugin_dir, merged_hooks) end (knowledge_entries, current_outputs) write_merged_hooks!(merged_hooks, current_outputs) cleanup_stale!(previous_manifest, current_outputs) save_manifest(current_outputs, enabled_skillsets, knowledge_entries) end |
#project_if_changed!(enabled_skillsets, knowledge_entries: []) ⇒ Object
Digest-based no-op: skip projection if nothing changed
81 82 83 84 85 86 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 81 def project_if_changed!(enabled_skillsets, knowledge_entries: []) digest = compute_source_digest(enabled_skillsets, knowledge_entries) return false if digest == load_manifest.dig('source_digest') project!(enabled_skillsets, knowledge_entries: knowledge_entries) true end |
#project_instruction_mode!(mode_name, body, mode_version: nil) ⇒ Hash
Project the active instruction mode body.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 127 def project_instruction_mode!(mode_name, body, mode_version: nil) raise ArgumentError, "unsafe mode name: #{mode_name.inspect}" unless safe_name?(mode_name) size = body.bytesize raise InstructionModeTooLarge.new(size, INSTRUCTION_MODE_SIZE_REFUSE) if size > INSTRUCTION_MODE_SIZE_REFUSE warn "[PluginProjector] WARNING: instruction mode body is #{size} bytes (warn threshold #{INSTRUCTION_MODE_SIZE_WARN})" if size > INSTRUCTION_MODE_SIZE_WARN artifact_path = File.join(@output_root, INSTRUCTION_MODE_REL_PATH) raise "instruction mode artifact path outside output_root: #{artifact_path}" unless safe_path?(artifact_path) FileUtils.mkdir_p(File.dirname(artifact_path)) atomic_write(artifact_path, body) region_written = merge_instruction_mode_region!(mode_name, mode_version, artifact_path) save_instruction_mode_manifest( 'mode_name' => mode_name, 'mode_version' => mode_version, 'artifact_path' => artifact_path, 'artifact_size' => size, 'artifact_digest' => Digest::SHA256.hexdigest(body), 'region_present' => region_written, 'projected_at' => Time.now.utc.iso8601 ) { artifact_path: artifact_path, region_written: region_written, size_bytes: size } end |
#remove_projected_instruction_mode! ⇒ Hash
Remove the projected instruction mode artifact and CLAUDE.md region.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 158 def remove_projected_instruction_mode! manifest = load_instruction_mode_manifest artifact_path = manifest['artifact_path'] || File.join(@output_root, INSTRUCTION_MODE_REL_PATH) artifact_removed = false if File.exist?(artifact_path) && safe_path?(artifact_path) FileUtils.rm_f(artifact_path) parent = File.dirname(artifact_path) FileUtils.rmdir(parent) if Dir.exist?(parent) && Dir.empty?(parent) artifact_removed = true end region_removed = remove_instruction_mode_region! save_instruction_mode_manifest(nil) # clear { artifact_removed: artifact_removed, region_removed: region_removed } end |
#status ⇒ Object
Status summary for MCP tool
89 90 91 92 93 94 95 96 97 98 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 89 def status manifest = load_manifest { mode: @mode, output_root: @output_root, projected_at: manifest['projected_at'], source_digest: manifest['source_digest'], output_count: manifest.fetch('outputs', {}).size } end |
#verify ⇒ Object
Verify projected files match manifest
101 102 103 104 105 106 107 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 101 def verify manifest = load_manifest outputs = manifest.fetch('outputs', {}) missing = outputs.keys.reject { |f| File.exist?(f) } orphaned = find_orphaned_files(outputs) { valid: missing.empty? && orphaned.empty?, missing: missing, orphaned: orphaned } end |