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: 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
-
#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) ⇒ PluginProjector
constructor
A new instance of 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) ⇒ PluginProjector
Returns a new instance of PluginProjector.
32 33 34 35 36 37 38 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 32 def initialize(project_root, mode: :auto) @project_root = project_root @mode = resolve_mode(mode) @output_root = @mode == :plugin ? project_root : File.join(project_root, '.claude') @manifest_path = File.join(project_root, '.kairos', 'projection_manifest.json') @instruction_mode_manifest_path = File.join(project_root, '.kairos', 'instruction_mode_manifest.json') end |
Instance Attribute Details
#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.
159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 159 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
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 41 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
62 63 64 65 66 67 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 62 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.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 108 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.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 139 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
70 71 72 73 74 75 76 77 78 79 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 70 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
82 83 84 85 86 87 88 |
# File 'lib/kairos_mcp/plugin_projector.rb', line 82 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 |