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 +4 -4
- data/CHANGELOG.md +41 -0
- data/impl.md +111 -0
- data/lib/caruso/adapter.rb +16 -96
- data/lib/caruso/adapters/base.rb +102 -0
- data/lib/caruso/adapters/command_adapter.rb +88 -0
- data/lib/caruso/adapters/dispatcher.rb +83 -0
- data/lib/caruso/adapters/hook_adapter.rb +222 -0
- data/lib/caruso/adapters/markdown_adapter.rb +23 -0
- data/lib/caruso/adapters/skill_adapter.rb +87 -0
- data/lib/caruso/cli.rb +22 -6
- data/lib/caruso/config_manager.rb +30 -28
- data/lib/caruso/fetcher.rb +137 -29
- data/lib/caruso/remover.rb +73 -13
- data/lib/caruso/version.rb +1 -1
- data/package-lock.json +6 -0
- data/reference/claude_code_create_marketplaces.md +511 -0
- data/reference/claude_code_hooks.md +1137 -0
- data/reference/claude_code_plugins.md +769 -0
- data/reference/claude_code_slash_commands.md +515 -0
- data/reference/cursor_commands.md +90 -0
- data/reference/cursor_hooks.md +467 -0
- data/reference/cursor_modes.md +105 -0
- data/reference/cursor_rules.md +246 -0
- data/reference/steering_docs.md +57 -0
- data/tasks.md +22 -0
- metadata +20 -3
- data/reference/plugins_reference.md +0 -376
- /data/reference/{marketplace.md → claude_code_marketplaces.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f7f1e5f2c4af1c26c2592aaa0cd5be588fa105ae3e7c8faf009275e881b2800
|
|
4
|
+
data.tar.gz: d740248a46af8e1559f7b02fb70b2660ff907b66e1acd7522c4e7a2692255938
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/caruso/adapter.rb
CHANGED
|
@@ -1,113 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
@agent = agent
|
|
18
|
+
@installed_hooks = {}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def adapt
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|