caruso 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e38c7a5007af96bb708d286a62909f07f1c1aa3b3aa800baa4a23f36c1a35814
4
- data.tar.gz: 18a217953c0ec7c87bc6d9476c89bbca442fde6b45aec4614e4f58c183a3a760
3
+ metadata.gz: 8f7f1e5f2c4af1c26c2592aaa0cd5be588fa105ae3e7c8faf009275e881b2800
4
+ data.tar.gz: d740248a46af8e1559f7b02fb70b2660ff907b66e1acd7522c4e7a2692255938
5
5
  SHA512:
6
- metadata.gz: 427385d4dca6b37150a1bcf487884db8979883d1e7e3d6caa353217b55609ad43dd97dfd1c85860379a6807c62eb37646eabe491540a54a683ec347e17edbc99
7
- data.tar.gz: f04abed100aef554d47572af9507c4d7701782b6321e5f48cfdee677f5cbe5ecd82dffab4e719b2328231ea8bee1a1a041c476ee3322fbdc8653c23333d81ed5
6
+ metadata.gz: f77816058f4901d123ba754005869b7e40d8f15be411df9c9fa1c96a261513a4aa420886396aa2cc32f719beaf5876b7ab102c4ee1e9105980402e4c5192a549
7
+ data.tar.gz: 7d85e8b21768c2978db8b29089920baa39a679163561eeba8ce8e012ad807068ff866d39a37e9411f1dec861232e5c19387c00daf4bb83e6e11673b294df1709
data/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-01-31
11
+
12
+ ### Added
13
+ - **Hooks Support**: Full translation of Claude Code hooks to Cursor hooks format
14
+ - Event translation: `PreToolUse` → `beforeShellExecution`, `PostToolUse` → `afterFileEdit`/`afterShellExecution`, `UserPromptSubmit` → `beforeSubmitPrompt`, `Stop` → `stop`
15
+ - Script copying with `${CLAUDE_PLUGIN_ROOT}` path rewriting
16
+ - Merges all plugin hooks into single `.cursor/hooks.json` file
17
+ - Tracks installed hooks for surgical removal on uninstall
18
+ - Skips unsupported events (`SessionStart`, `SessionEnd`, etc.) with warnings
19
+ - Skips prompt-based hooks (Cursor doesn't support LLM evaluation in hooks)
20
+ - **Commands Support**: New `CommandAdapter` writes slash commands to `.cursor/commands/`
21
+ - Commands now output as `.md` files in `.cursor/commands/caruso/<marketplace>/<plugin>/`
22
+ - Preserves frontmatter (`description`, `argument-hint`, `allowed-tools`, `model`)
23
+ - Converts bash execution markers (`!` prefix) to documentation notes
24
+ - **Plugin.json Parsing**: Reads component paths and inline configs from `.claude-plugin/plugin.json`
25
+ - Marketplace.json fields take precedence over plugin.json (conflict resolution)
26
+ - Supports inline hooks configuration
27
+ - Enables plugin-level metadata tracking
28
+ - **Comprehensive Test Coverage**: 14 new integration tests for hooks
29
+ - Two-plugin merge scenarios, cross-component plugins, marketplace removal cascade
30
+ - Plugin update idempotency, edge cases (malformed hooks, agent skipping, no-hooks plugins)
31
+ - Clean filesystem verification (orphan directory cleanup)
32
+
33
+ ### Changed
34
+ - **Dispatcher Architecture**: Refactored with class methods and step-by-step processing
35
+ - Skills → Commands → Hooks → Agents (skip with warning) → Unprocessed warnings
36
+ - Returns structured result: `{ files: [...], hooks: {...} }`
37
+ - **ConfigManager**: Now tracks installed hooks in `.caruso.local.json` for clean uninstall
38
+ - `add_plugin` accepts `hooks:` keyword argument
39
+ - `remove_plugin` returns `{ files: [...], hooks: {...} }`
40
+ - Added `get_installed_hooks` method
41
+ - **Remover**: Enhanced hook removal and orphan directory cleanup
42
+ - Removes specific hook commands from merged `.cursor/hooks.json` using tracked metadata
43
+ - Walks up directory tree removing empty dirs after file deletion
44
+ - Deletes `.cursor/hooks.json` if empty after plugin removal
45
+ - **CLI**: Updated `plugin install` and `plugin update` to pass hooks to ConfigManager
46
+
47
+ ### Fixed
48
+ - Orphan directories no longer left behind after uninstalling plugins with hook scripts
49
+ - Rubocop compliance across all source and spec files (0 offenses)
50
+
10
51
  ## [0.6.2] - 2025-12-17
11
52
 
12
53
  ### Fixed
data/impl.md ADDED
@@ -0,0 +1,111 @@
1
+ # Implementation Plan - Evolve Caruso for Claude Code Support
2
+
3
+ Refining caruso to provide robust support for Claude Code's Commands, Skills, and Hooks, adapting them specifically for the Cursor environment.
4
+
5
+ ## Goal Description
6
+
7
+ The goal is to transform caruso from a simple markdown copier into a smart adapter that bridges the gap between Claude Code's plugin architecture and Cursor's steering mechanisms. This involves:
8
+
9
+ - **Skills**: deeply adapting skills by not just copying the markdown, but also bundling associated scripts/ and making them executable.
10
+ - **Commands**: mapping Claude Code commands to Cursor's new .cursor/commands/ structure.
11
+ - **Hooks**: translating Claude Code's hooks/hooks.json events into Cursor's .cursor/hooks.json format.
12
+ - **Fetcher**: upgrading the fetcher to retrieve auxiliary files (scripts, configs) beyond just `.md` files.
13
+
14
+ ## User Review Required
15
+
16
+ > [!NOTE]
17
+ > **Script Execution Security:** We will trust the "trusted workspace" model of Cursor. This means we will automatically fetch scripts and make them executable (`chmod +x`). Users relying on Caruso are expected to audit the plugins they install, similar to how they would audit an npm package.
18
+
19
+ ## Proposed Changes
20
+
21
+ ### 1. Fetcher Upgrades (`lib/caruso/fetcher.rb`)
22
+
23
+ The current fetcher is overly focused on *.md files. We need to broaden it to fetch associated resources using an Additive Strategy.
24
+
25
+ #### [MODIFY] `fetcher.rb`
26
+
27
+ - **Additive Discovery**: In `fetch_plugin`:
28
+ - **Logic**:
29
+ 1. **Check Manifest**: Look for `skills` field in `plugin` object (string or array).
30
+ 2. **If Present**: Recursively fetch all files in those specific paths.
31
+ 3. **If Absent**: Fallback to scanning the default `skills/` directory (recursively).
32
+ 4. **Other Components**: Continue scanning for `commands/`, `agents/`, `hooks/` as before.
33
+
34
+ - **Support Resource Types**:
35
+ - `skills`: Fetch `SKILL.md` AND recursively fetch `scripts/` directories if found within the skill path.
36
+
37
+ - `hooks`: Fetch the `hooks/hooks.json` file (or inline config).
38
+ - `commands`: Fetch markdown files.
39
+ - `agents`: Fetch markdown files.
40
+
41
+ ### 2. Adapter Architecture Refactor (`lib/caruso/`)
42
+
43
+ Refactor the monolithic Adapter class into a dispatcher with specialized strategies.
44
+
45
+ #### [MODIFY] `adapter.rb`
46
+ Change `Adapter#adapt` to identify the component type and delegate to the appropriate sub-adapter.
47
+
48
+ #### [NEW] `adapters/base.rb`
49
+ Shared logic for file writing, frontmatter injection, and path sanitization.
50
+
51
+ #### [NEW] `adapters/skill_adapter.rb`
52
+ **Input:** `skills/<name>/SKILL.md` + `skills/<name>/scripts/*`
53
+ **Output:**
54
+ - `.cursor/rules/caruso/<marketplace>/<skill>/<skill>.mdc` (The rule)
55
+ - `.cursor/scripts/caruso/<marketplace>/<skill>/*` (The scripts)
56
+
57
+ **Logic:**
58
+ 1. Copy scripts to `.cursor/scripts/caruso/<marketplace>/<skill>/`.
59
+ 2. Ensure scripts are executable (`chmod +x`).
60
+ 3. **Paths:** Do NOT rewrite paths in the markdown (to avoid messiness).
61
+ 4. **Context:** Inject a location hint into the Rule's frontmatter `description` or prepended content:
62
+ ```yaml
63
+ description: Imported from <skill>. Scripts located at: .cursor/scripts/caruso/<marketplace>/<skill>/
64
+ ```
65
+
66
+ #### [NEW] `adapters/agent_adapter.rb`
67
+ **Input:** `agents/<name>.md`
68
+ **Output:** `.cursor/rules/caruso/<marketplace>/agents/<name>.mdc`
69
+ **Logic:**
70
+ - Copy content.
71
+ - Wrap as a "Persona" rule if needed, or simple markdown rule.
72
+
73
+ #### [NEW] `adapters/command_adapter.rb`
74
+ **Input:** `commands/<name>.md`
75
+ **Output:** `.cursor/commands/<name>.md`
76
+ **Logic:**
77
+ - Basic markdown copy.
78
+ - Frontmatter cleanup (remove Claude-specific fields if necessary).
79
+
80
+ #### [NEW] `adapters/hook_adapter.rb`
81
+ **Input:** `hooks/hooks.json`
82
+ **Output:** Merged/Updated `.cursor/hooks.json`
83
+ **Logic:**
84
+ - Parse source JSON.
85
+ - Map events (e.g., `PostToolUse` -> `afterFileEdit`).
86
+ - Write/Merge into project's `hooks.json`.
87
+
88
+ ### 3. CLI Updates (`lib/caruso/cli.rb`)
89
+
90
+ #### [MODIFY] `cli.rb`
91
+ - Update `install` command to handle multiple file types returned by the upgraded fetcher.
92
+ - Ensure `uninstall` cleans up the directories (scripts, etc) correctly.
93
+
94
+ ## Verification Plan
95
+
96
+ ### Automated Tests
97
+ - **Fetcher Tests**: Mock a plugin structure with `scripts/` and verify they are returned in the file list.
98
+ - **Adapter Tests**:
99
+ - Feed a `SKILL.md` + script to `SkillAdapter` and verify output structure and chmod.
100
+ - Feed a `hooks.json` and verify the event translation.
101
+
102
+ ### Manual Verification
103
+ - **Skills**: Install a plugin with a script (e.g., a "linter" skill).
104
+ - Verify file exists at `.cursor/scripts/.../lint.sh`.
105
+ - Verify it is executable.
106
+ - Verify the Rule markdown is present.
107
+ - **Commands**: Install a command plugin.
108
+ - Verify file exists at `.cursor/commands/...`.
109
+ - Test running the command in Cursor (Cmd+K `/command`).
110
+ - **Hooks**: Install a hook plugin.
111
+ - Check `.cursor/hooks.json` contains the mapped event.
@@ -1,113 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
- require "yaml"
5
- require_relative "safe_file"
6
- require_relative "path_sanitizer"
3
+ require_relative "adapters/dispatcher"
7
4
 
8
5
  module Caruso
9
6
  class Adapter
10
- attr_reader :files, :target_dir, :agent, :marketplace_name, :plugin_name
7
+ # Hook commands installed by this adapter, keyed by Cursor event name.
8
+ # Populated after adapt() is called. Used for tracking during install.
9
+ attr_reader :installed_hooks
11
10
 
11
+ # Preserving the interface for CLI compatibility
12
12
  def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
13
13
  @files = files
14
14
  @target_dir = target_dir
15
- @agent = agent
16
15
  @marketplace_name = marketplace_name
17
16
  @plugin_name = plugin_name
18
- FileUtils.mkdir_p(@target_dir)
17
+ @agent = agent
18
+ @installed_hooks = {}
19
19
  end
20
20
 
21
21
  def adapt
22
- created_files = []
23
- files.each do |file_path|
24
- content = SafeFile.read(file_path)
25
-
26
- adapted_content = inject_metadata(content, file_path)
27
- created_file = save_file(file_path, adapted_content)
28
- created_files << created_file
29
- end
30
- created_files
31
- end
32
-
33
- private
34
-
35
- def inject_metadata(content, file_path)
36
- # Check if frontmatter exists
37
- if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
38
- # If it exists, we might need to append to it or modify it
39
- # For now, we assume existing frontmatter is "good enough" but might need 'globs' for Cursor
40
- ensure_cursor_globs(content) if agent == :cursor
41
- else
42
- # No frontmatter, prepend it
43
- create_frontmatter(file_path) + content
44
- end
45
- end
46
-
47
- def ensure_cursor_globs(content)
48
- # Add required Cursor metadata fields if missing
49
- # globs: [] enables semantic search (Apply Intelligently)
50
- # alwaysApply: false means it won't apply to every chat session
51
-
52
- unless content.include?("globs:")
53
- content.sub!(/\A---\s*\n/, "---\nglobs: []\n")
54
- end
55
-
56
- unless content.include?("alwaysApply:")
57
- # Add after the first line of frontmatter
58
- content.sub!(/\A---\s*\n/, "---\nalwaysApply: false\n")
59
- end
60
-
61
- content
62
- end
63
-
64
- def create_frontmatter(file_path)
65
- filename = File.basename(file_path)
66
- <<~YAML
67
- ---
68
- description: Imported rule from #{filename}
69
- globs: []
70
- alwaysApply: false
71
- ---
72
- YAML
73
- end
74
-
75
- def save_file(original_path, content)
76
- filename = File.basename(original_path, ".*")
77
-
78
- # Rename SKILL.md to the skill name (parent directory) to avoid collisions
79
- if filename.casecmp("skill").zero?
80
- filename = File.basename(File.dirname(original_path))
81
- end
82
-
83
- extension = agent == :cursor ? ".mdc" : ".md"
84
- output_filename = "#{filename}#{extension}"
85
-
86
- # Extract component type from original path (commands/agents/skills)
87
- component_type = extract_component_type(original_path)
88
-
89
- # Build nested directory structure for Cursor
90
- # Build nested directory structure for Cursor
91
- # Structure: .cursor/rules/caruso/marketplace/plugin/component-type/file.mdc
92
- subdirs = File.join("caruso", marketplace_name, plugin_name, component_type)
93
- output_dir = File.join(@target_dir, subdirs)
94
- FileUtils.mkdir_p(output_dir)
95
- target_path = File.join(output_dir, output_filename)
96
-
97
- File.write(target_path, content)
98
- puts "Saved: #{target_path}"
99
-
100
- # Return relative path from target_dir
101
- File.join(subdirs, output_filename)
102
- end
103
-
104
- def extract_component_type(file_path)
105
- # Extract component type (commands/agents/skills) from path
106
- return "commands" if file_path.include?("/commands/")
107
- return "agents" if file_path.include?("/agents/")
108
- return "skills" if file_path.include?("/skills/")
109
-
110
- raise Caruso::Error, "Cannot determine component type from path: #{file_path}"
22
+ result = Caruso::Adapters::Dispatcher.adapt(
23
+ @files,
24
+ target_dir: @target_dir,
25
+ marketplace_name: @marketplace_name,
26
+ plugin_name: @plugin_name,
27
+ agent: @agent
28
+ )
29
+ @installed_hooks = result.is_a?(Hash) ? (result[:hooks] || {}) : {}
30
+ result.is_a?(Hash) ? result[:files] : result
111
31
  end
112
32
  end
113
33
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+ require_relative "../safe_file"
6
+ require_relative "../path_sanitizer"
7
+
8
+ module Caruso
9
+ module Adapters
10
+ class Base
11
+ attr_reader :files, :target_dir, :agent, :marketplace_name, :plugin_name
12
+
13
+ def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
14
+ @files = files
15
+ @target_dir = target_dir
16
+ @agent = agent
17
+ @marketplace_name = marketplace_name
18
+ @plugin_name = plugin_name
19
+ FileUtils.mkdir_p(@target_dir)
20
+ end
21
+
22
+ def adapt
23
+ raise NotImplementedError, "#{self.class.name}#adapt must be implemented"
24
+ end
25
+
26
+ protected
27
+
28
+ def save_file(relative_path, content, extension: nil)
29
+ filename = File.basename(relative_path, ".*")
30
+
31
+ # Preserve original extension if none provided
32
+ ext = extension || File.extname(relative_path)
33
+
34
+ # Rename SKILL.md to the skill name (parent directory) to avoid collisions
35
+ # This is specific to Skills, might move to SkillAdapter later, but keeping behavior for now
36
+ if filename.casecmp("skill").zero?
37
+ filename = File.basename(File.dirname(relative_path))
38
+ end
39
+
40
+ output_filename = "#{filename}#{ext}"
41
+
42
+ # Build nested directory structure: .cursor/rules/caruso/marketplace/plugin/component/file
43
+ # Component type is derived from the class name or passed in?
44
+ # For base, we might need a way to determine output path more flexibly.
45
+ # But sticking to current behavior:
46
+
47
+ component_type = extract_component_type(relative_path)
48
+ subdirs = File.join("caruso", marketplace_name, plugin_name, component_type)
49
+ output_dir = File.join(target_dir, subdirs)
50
+
51
+ FileUtils.mkdir_p(output_dir)
52
+ target_path = File.join(output_dir, output_filename)
53
+
54
+ File.write(target_path, content)
55
+ puts "Saved: #{target_path}"
56
+
57
+ File.join(subdirs, output_filename)
58
+ end
59
+
60
+ def extract_component_type(file_path)
61
+ # Extract component type (commands/agents/skills) from path
62
+ return "commands" if file_path.include?("/commands/")
63
+ return "agents" if file_path.include?("/agents/")
64
+ return "skills" if file_path.include?("/skills/")
65
+
66
+ # Fallback or specific handling for other types
67
+ "misc"
68
+ end
69
+
70
+ def inject_metadata(content, file_path)
71
+ if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
72
+ ensure_cursor_globs(content) if agent == :cursor
73
+ else
74
+ create_frontmatter(file_path) + content
75
+ end
76
+ end
77
+
78
+ def ensure_cursor_globs(content)
79
+ unless content.include?("globs:")
80
+ content.sub!(/\A---\s*\n/, "---\nglobs: []\n")
81
+ end
82
+
83
+ unless content.include?("alwaysApply:")
84
+ content.sub!(/\A---\s*\n/, "---\nalwaysApply: false\n")
85
+ end
86
+
87
+ content
88
+ end
89
+
90
+ def create_frontmatter(file_path)
91
+ filename = File.basename(file_path)
92
+ <<~YAML
93
+ ---
94
+ description: Imported rule from #{filename}
95
+ globs: []
96
+ alwaysApply: false
97
+ ---
98
+ YAML
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Caruso
6
+ module Adapters
7
+ class CommandAdapter < Base
8
+ def adapt
9
+ created_files = []
10
+ files.each do |file_path|
11
+ content = SafeFile.read(file_path)
12
+ adapted_content = adapt_command_content(content, file_path)
13
+
14
+ # Commands are flat .md files in .cursor/commands/
15
+ # NOT nested like rules, so we override the save behavior
16
+ created_file = save_command_file(file_path, adapted_content)
17
+
18
+ created_files << created_file
19
+ end
20
+ created_files
21
+ end
22
+
23
+ private
24
+
25
+ def adapt_command_content(content, file_path)
26
+ # Preserve or add frontmatter, but don't add Cursor-specific rule fields
27
+ if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
28
+ # Frontmatter exists - preserve command-specific fields
29
+ preserve_command_frontmatter(content, file_path)
30
+ else
31
+ # No frontmatter - create minimal description
32
+ create_command_frontmatter(file_path) + content
33
+ end
34
+ end
35
+
36
+ def preserve_command_frontmatter(content, _file_path)
37
+ # Commands support: description, argument-hint, allowed-tools, model
38
+ # We don't need globs or alwaysApply (those are for rules)
39
+ # Just return content as-is, Claude Code commands are already compatible
40
+
41
+ # Add note about bash execution if it contains ! prefix
42
+ if content.include?("!`")
43
+ add_bash_execution_note(content)
44
+ else
45
+ content
46
+ end
47
+ end
48
+
49
+ def create_command_frontmatter(file_path)
50
+ filename = File.basename(file_path)
51
+ <<~YAML
52
+ ---
53
+ description: Command from #{filename}
54
+ ---
55
+ YAML
56
+ end
57
+
58
+ def add_bash_execution_note(content)
59
+ match = content.match(/\A---\s*\n(.*?)\n---\s*\n/m)
60
+ return content unless match
61
+
62
+ note = "\n**Note:** This command originally used Claude Code's `!` prefix for bash execution. " \
63
+ "Cursor does not support this feature. The bash commands are documented below for reference.\n"
64
+
65
+ "---\n#{match[1]}\n---\n#{note}#{match.post_match}"
66
+ end
67
+
68
+ def save_command_file(relative_path, content)
69
+ filename = File.basename(relative_path, ".*")
70
+ output_filename = "#{filename}.md"
71
+
72
+ # Commands go to .cursor/commands/caruso/<marketplace>/<plugin>/
73
+ # Flat structure, no component subdirectory
74
+ subdirs = File.join("caruso", marketplace_name, plugin_name)
75
+ output_dir = File.join(".cursor", "commands", subdirs)
76
+
77
+ FileUtils.mkdir_p(output_dir)
78
+ target_path = File.join(output_dir, output_filename)
79
+
80
+ File.write(target_path, content)
81
+ puts "Saved command: #{target_path}"
82
+
83
+ # Return relative path for tracking
84
+ File.join(".cursor/commands", subdirs, output_filename)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "markdown_adapter"
4
+ require_relative "skill_adapter"
5
+ require_relative "command_adapter"
6
+ require_relative "hook_adapter"
7
+
8
+ module Caruso
9
+ module Adapters
10
+ class Dispatcher
11
+ class << self
12
+ def adapt(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
13
+ ctx = { target_dir: target_dir, marketplace_name: marketplace_name, plugin_name: plugin_name, agent: agent }
14
+ remaining = files.dup
15
+ created = []
16
+
17
+ created.concat(process_skills(remaining, ctx))
18
+ created.concat(process_commands(remaining, ctx))
19
+
20
+ hook_result = process_hooks(remaining, ctx)
21
+ created.concat(hook_result[:created])
22
+
23
+ skip_agents(remaining)
24
+ warn_unprocessed(remaining)
25
+
26
+ { files: created, hooks: hook_result[:hooks] }
27
+ end
28
+
29
+ private
30
+
31
+ def process_skills(remaining, ctx)
32
+ created = []
33
+ skill_anchors = remaining.select { |f| File.basename(f).casecmp("skill.md").zero? }
34
+
35
+ skill_anchors.each do |anchor|
36
+ skill_dir = File.dirname(anchor)
37
+ skill_cluster = remaining.select { |f| f.start_with?(skill_dir) }
38
+
39
+ adapter = SkillAdapter.new(skill_cluster, **ctx)
40
+ created.concat(adapter.adapt)
41
+ remaining.delete_if { |f| skill_cluster.include?(f) }
42
+ end
43
+
44
+ created
45
+ end
46
+
47
+ def process_commands(remaining, ctx)
48
+ commands = remaining.select { |f| f.include?("/commands/") }
49
+ return [] unless commands.any?
50
+
51
+ adapter = CommandAdapter.new(commands, **ctx)
52
+ remaining.delete_if { |f| commands.include?(f) }
53
+ adapter.adapt
54
+ end
55
+
56
+ def process_hooks(remaining, ctx)
57
+ hooks_files = remaining.select { |f| File.basename(f) =~ /hooks\.json\z/ }
58
+ return { created: [], hooks: {} } unless hooks_files.any?
59
+
60
+ adapter = HookAdapter.new(hooks_files, **ctx)
61
+ created = adapter.adapt
62
+ remaining.delete_if { |f| hooks_files.include?(f) }
63
+ { created: created, hooks: adapter.translated_hooks }
64
+ end
65
+
66
+ def skip_agents(remaining)
67
+ agents = remaining.select { |f| f.include?("/agents/") }
68
+ return unless agents.any?
69
+
70
+ puts "Skipping #{agents.size} agent(s): Agents require context isolation not available in Cursor"
71
+ remaining.delete_if { |f| agents.include?(f) }
72
+ end
73
+
74
+ def warn_unprocessed(remaining)
75
+ return unless remaining.any?
76
+
77
+ puts "Warning: #{remaining.size} file(s) could not be categorized and were skipped:"
78
+ remaining.each { |f| puts " - #{f}" }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end