caruso 0.6.2 → 0.6.3
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/impl.md +111 -0
- data/lib/caruso/adapter.rb +10 -97
- data/lib/caruso/adapters/base.rb +102 -0
- data/lib/caruso/adapters/dispatcher.rb +53 -0
- data/lib/caruso/adapters/markdown_adapter.rb +23 -0
- data/lib/caruso/adapters/skill_adapter.rb +100 -0
- data/lib/caruso/fetcher.rb +69 -29
- data/lib/caruso/version.rb +1 -1
- 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 +17 -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: 9f6520caf8c5e8a4946369e9722ca36685a6c79b933b67336bc02b39b0fca359
|
|
4
|
+
data.tar.gz: d1b2f7b52ae5746909e173b75fc31e601417bca1ef55af889a56f802a8ae1d24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48d8c7c701c7c1a380672ac77f3025dc69c90c4a5ef7d6fd6e72e50358e69440a9c57ff022f52f94c4970faead782ba04c223246bf766045e4508bca427cf4f8
|
|
7
|
+
data.tar.gz: 293893a825850e919ca8141203f307c300e9fdba931f80aafadad15a766dd5f28ff6bd061eae53891c5a4da41f061e2b4b6bea5f0108d9b79396e63d82f49296
|
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,26 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
7
|
+
# Preserving the interface for CLI compatibility
|
|
12
8
|
def initialize(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
|
|
13
9
|
@files = files
|
|
14
10
|
@target_dir = target_dir
|
|
15
|
-
@agent = agent
|
|
16
11
|
@marketplace_name = marketplace_name
|
|
17
12
|
@plugin_name = plugin_name
|
|
18
|
-
|
|
13
|
+
@agent = agent
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
def adapt
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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}"
|
|
17
|
+
Caruso::Adapters::Dispatcher.adapt(
|
|
18
|
+
@files,
|
|
19
|
+
target_dir: @target_dir,
|
|
20
|
+
marketplace_name: @marketplace_name,
|
|
21
|
+
plugin_name: @plugin_name,
|
|
22
|
+
agent: @agent
|
|
23
|
+
)
|
|
111
24
|
end
|
|
112
25
|
end
|
|
113
26
|
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,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "markdown_adapter"
|
|
4
|
+
require_relative "skill_adapter"
|
|
5
|
+
|
|
6
|
+
module Caruso
|
|
7
|
+
module Adapters
|
|
8
|
+
class Dispatcher
|
|
9
|
+
def self.adapt(files, target_dir:, marketplace_name:, plugin_name:, agent: :cursor)
|
|
10
|
+
created_files = []
|
|
11
|
+
remaining_files = files.dup
|
|
12
|
+
|
|
13
|
+
# 1. Identify and Process Skill Clusters
|
|
14
|
+
# Find all SKILL.md files to serve as anchors
|
|
15
|
+
skill_anchors = remaining_files.select { |f| File.basename(f).casecmp("skill.md").zero? }
|
|
16
|
+
|
|
17
|
+
skill_anchors.each do |anchor|
|
|
18
|
+
skill_dir = File.dirname(anchor)
|
|
19
|
+
|
|
20
|
+
# Find all files that belong to this skill's directory (recursive)
|
|
21
|
+
skill_cluster = remaining_files.select { |f| f.start_with?(skill_dir) }
|
|
22
|
+
|
|
23
|
+
# Use SkillAdapter for this cluster
|
|
24
|
+
adapter = SkillAdapter.new(
|
|
25
|
+
skill_cluster,
|
|
26
|
+
target_dir: target_dir,
|
|
27
|
+
marketplace_name: marketplace_name,
|
|
28
|
+
plugin_name: plugin_name,
|
|
29
|
+
agent: agent
|
|
30
|
+
)
|
|
31
|
+
created_files.concat(adapter.adapt)
|
|
32
|
+
|
|
33
|
+
# Remove processed files
|
|
34
|
+
remaining_files -= skill_cluster
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 2. Process Remaining Files (Commands, Agents, etc.) via MarkdownAdapter
|
|
38
|
+
if remaining_files.any?
|
|
39
|
+
adapter = MarkdownAdapter.new(
|
|
40
|
+
remaining_files,
|
|
41
|
+
target_dir: target_dir,
|
|
42
|
+
marketplace_name: marketplace_name,
|
|
43
|
+
plugin_name: plugin_name,
|
|
44
|
+
agent: agent
|
|
45
|
+
)
|
|
46
|
+
created_files.concat(adapter.adapt)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
created_files
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Caruso
|
|
6
|
+
module Adapters
|
|
7
|
+
class MarkdownAdapter < Base
|
|
8
|
+
def adapt
|
|
9
|
+
created_files = []
|
|
10
|
+
files.each do |file_path|
|
|
11
|
+
content = SafeFile.read(file_path)
|
|
12
|
+
adapted_content = inject_metadata(content, file_path)
|
|
13
|
+
|
|
14
|
+
extension = agent == :cursor ? ".mdc" : ".md"
|
|
15
|
+
created_file = save_file(file_path, adapted_content, extension: extension)
|
|
16
|
+
|
|
17
|
+
created_files << created_file
|
|
18
|
+
end
|
|
19
|
+
created_files
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Caruso
|
|
6
|
+
module Adapters
|
|
7
|
+
class SkillAdapter < Base
|
|
8
|
+
def adapt
|
|
9
|
+
created_files = []
|
|
10
|
+
|
|
11
|
+
# Separate SKILL.md from other files (scripts, etc.)
|
|
12
|
+
skill_file = files.find { |f| File.basename(f).casecmp("skill.md").zero? }
|
|
13
|
+
other_files = files - [skill_file]
|
|
14
|
+
|
|
15
|
+
if skill_file
|
|
16
|
+
skill_root = File.dirname(skill_file)
|
|
17
|
+
skill_name = File.basename(skill_root)
|
|
18
|
+
|
|
19
|
+
# Inject script location hint
|
|
20
|
+
# Targeted script location: .cursor/scripts/caruso/<market>/<plugin>/<skill_name>/
|
|
21
|
+
script_location = ".cursor/scripts/caruso/#{marketplace_name}/#{plugin_name}/#{skill_name}/"
|
|
22
|
+
|
|
23
|
+
content = SafeFile.read(skill_file)
|
|
24
|
+
adapted_content = inject_skill_metadata(content, skill_file, script_location)
|
|
25
|
+
|
|
26
|
+
# Save as Rule (.mdc)
|
|
27
|
+
extension = agent == :cursor ? ".mdc" : ".md"
|
|
28
|
+
created_file = save_file(skill_file, adapted_content, extension: extension)
|
|
29
|
+
created_files << created_file
|
|
30
|
+
|
|
31
|
+
# Process Scripts/Assets -> .cursor/scripts/...
|
|
32
|
+
other_files.each do |file_path|
|
|
33
|
+
# Calculate path relative to the skill root
|
|
34
|
+
# e.g. source: .../skills/auth/scripts/login.sh
|
|
35
|
+
# root: .../skills/auth
|
|
36
|
+
# rel: scripts/login.sh
|
|
37
|
+
relative_path_from_root = file_path.sub(skill_root + "/", "")
|
|
38
|
+
|
|
39
|
+
created_script = save_script(file_path, skill_name, relative_path_from_root)
|
|
40
|
+
created_files << created_script
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
created_files
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def inject_skill_metadata(content, file_path, script_location)
|
|
50
|
+
# Inject script location into description
|
|
51
|
+
hint = "Scripts located at: #{script_location}"
|
|
52
|
+
|
|
53
|
+
if content.match?(/\A---\s*\n.*?\n---\s*\n/m)
|
|
54
|
+
# Update existing frontmatter
|
|
55
|
+
content.sub!(/^description: (.*)$/, "description: \\1. #{hint}")
|
|
56
|
+
ensure_cursor_globs(content)
|
|
57
|
+
else
|
|
58
|
+
# Create new frontmatter with hint
|
|
59
|
+
create_skill_frontmatter(file_path, hint) + content
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_skill_frontmatter(file_path, hint)
|
|
64
|
+
filename = File.basename(file_path)
|
|
65
|
+
<<~YAML
|
|
66
|
+
---
|
|
67
|
+
description: Imported skill from #{filename}. #{hint}
|
|
68
|
+
globs: []
|
|
69
|
+
alwaysApply: false
|
|
70
|
+
---
|
|
71
|
+
YAML
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save_script(source_path, skill_name, relative_sub_path)
|
|
75
|
+
# Construct target path in .cursor/scripts
|
|
76
|
+
# .cursor/scripts/caruso/<marketplace>/<plugin>/<skill_name>/<relative_sub_path>
|
|
77
|
+
|
|
78
|
+
scripts_root = File.join(target_dir, "..", "scripts", "caruso", marketplace_name, plugin_name, skill_name)
|
|
79
|
+
# Note: target_dir passed to adapter is usually .cursor/rules.
|
|
80
|
+
# So .. -> .cursor -> scripts
|
|
81
|
+
|
|
82
|
+
target_path = File.join(scripts_root, relative_sub_path)
|
|
83
|
+
output_dir = File.dirname(target_path)
|
|
84
|
+
|
|
85
|
+
FileUtils.mkdir_p(output_dir)
|
|
86
|
+
FileUtils.cp(source_path, target_path)
|
|
87
|
+
|
|
88
|
+
# Make executable
|
|
89
|
+
File.chmod(0755, target_path)
|
|
90
|
+
|
|
91
|
+
puts "Saved script: #{target_path}"
|
|
92
|
+
|
|
93
|
+
# Return relative path for tracking/reporting
|
|
94
|
+
# We start from .cursor (parent of target_dir) ideally?
|
|
95
|
+
# Or just return the absolute path for now?
|
|
96
|
+
target_path
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/caruso/fetcher.rb
CHANGED
|
@@ -181,12 +181,7 @@ module Caruso
|
|
|
181
181
|
return [] unless SafeDir.exist?(plugin_path)
|
|
182
182
|
|
|
183
183
|
# Start with default directories
|
|
184
|
-
files = find_steering_files(plugin_path)
|
|
185
|
-
|
|
186
|
-
# Add custom paths if specified (they supplement defaults)
|
|
187
|
-
files += find_custom_component_files(plugin_path, plugin["commands"]) if plugin["commands"]
|
|
188
|
-
files += find_custom_component_files(plugin_path, plugin["agents"]) if plugin["agents"]
|
|
189
|
-
files += find_custom_component_files(plugin_path, plugin["skills"]) if plugin["skills"]
|
|
184
|
+
files = find_steering_files(plugin_path, plugin)
|
|
190
185
|
|
|
191
186
|
files.uniq
|
|
192
187
|
end
|
|
@@ -210,18 +205,44 @@ module Caruso
|
|
|
210
205
|
end
|
|
211
206
|
end
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
glob_pattern = PathSanitizer.safe_join(plugin_path, "{commands,agents,skills}", "**", "*.md")
|
|
208
|
+
# Find all steering files based on default locations and manifest overrides
|
|
209
|
+
# RETURNS: unique list of absolute file paths to fetch
|
|
210
|
+
def find_steering_files(plugin_path, plugin_data = nil)
|
|
211
|
+
files = []
|
|
218
212
|
|
|
219
|
-
|
|
213
|
+
# 1. ALWAYS scan default directories (Additive Strategy)
|
|
214
|
+
files += glob_plugin_files(plugin_path, "commands", "**", "*.md")
|
|
215
|
+
files += glob_plugin_files(plugin_path, "agents", "**", "*.md")
|
|
216
|
+
|
|
217
|
+
# For skills, we want recursive default scan if 'skills/' exists
|
|
218
|
+
# But careful: if we scan default 'skills' recursively here, and then scan strict paths from manifest...
|
|
219
|
+
# Duplicate handling is fine via uniq.
|
|
220
|
+
default_skills_path = File.join(plugin_path, "skills")
|
|
221
|
+
if SafeDir.exist?(default_skills_path)
|
|
222
|
+
files += find_recursive_component_files(plugin_path, "skills")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# 2. Add manifest-defined paths (if present)
|
|
226
|
+
if plugin_data
|
|
227
|
+
files += find_custom_component_files(plugin_path, plugin_data["commands"]) if plugin_data["commands"]
|
|
228
|
+
files += find_custom_component_files(plugin_path, plugin_data["agents"]) if plugin_data["agents"]
|
|
229
|
+
files += find_recursive_component_files(plugin_path, plugin_data["skills"]) if plugin_data["skills"]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Filter out noise
|
|
233
|
+
files.uniq.reject do |file|
|
|
220
234
|
basename = File.basename(file).downcase
|
|
221
|
-
["readme.md", "license.md"].include?(basename)
|
|
235
|
+
["readme.md", "license.md", "plugin.json"].include?(basename) || File.directory?(file)
|
|
222
236
|
end
|
|
223
237
|
end
|
|
224
238
|
|
|
239
|
+
# Helper to glob files safely
|
|
240
|
+
def glob_plugin_files(plugin_path, *parts)
|
|
241
|
+
pattern = PathSanitizer.safe_join(plugin_path, *parts)
|
|
242
|
+
SafeDir.glob(pattern, base_dir: plugin_path)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# For Commands/Agents: typically just markdown files, flat or shallow
|
|
225
246
|
def find_custom_component_files(plugin_path, paths)
|
|
226
247
|
# Handle both string and array formats
|
|
227
248
|
paths = [paths] if paths.is_a?(String)
|
|
@@ -229,30 +250,49 @@ module Caruso
|
|
|
229
250
|
|
|
230
251
|
files = []
|
|
231
252
|
paths.each do |path|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
begin
|
|
235
|
-
full_path = PathSanitizer.sanitize_path(File.expand_path(path, plugin_path), base_dir: plugin_path)
|
|
236
|
-
rescue PathSanitizer::PathTraversalError => e
|
|
237
|
-
warn "Skipping path outside plugin directory '#{path}': #{e.message}"
|
|
238
|
-
next
|
|
239
|
-
end
|
|
253
|
+
full_path = resolve_safe_path(plugin_path, path)
|
|
254
|
+
next unless full_path
|
|
240
255
|
|
|
241
|
-
# Handle both files and directories
|
|
242
256
|
if File.file?(full_path) && full_path.end_with?(".md")
|
|
243
|
-
|
|
244
|
-
files << full_path unless ["readme.md", "license.md"].include?(basename)
|
|
257
|
+
files << full_path
|
|
245
258
|
elsif SafeDir.exist?(full_path, base_dir: plugin_path)
|
|
246
|
-
# Find all .md files in this directory
|
|
259
|
+
# Find all .md files in this directory
|
|
247
260
|
glob_pattern = PathSanitizer.safe_join(full_path, "**", "*.md")
|
|
248
|
-
SafeDir.glob(glob_pattern, base_dir: plugin_path)
|
|
249
|
-
basename = File.basename(file).downcase
|
|
250
|
-
files << file unless ["readme.md", "license.md"].include?(basename)
|
|
251
|
-
end
|
|
261
|
+
files += SafeDir.glob(glob_pattern, base_dir: plugin_path)
|
|
252
262
|
end
|
|
253
263
|
end
|
|
254
264
|
files
|
|
255
265
|
end
|
|
266
|
+
|
|
267
|
+
# For SKILLS: Recursive fetch of EVERYTHING (scripts, assets, md)
|
|
268
|
+
def find_recursive_component_files(plugin_path, paths)
|
|
269
|
+
paths = [paths] if paths.is_a?(String)
|
|
270
|
+
return [] unless paths.is_a?(Array)
|
|
271
|
+
|
|
272
|
+
files = []
|
|
273
|
+
paths.each do |path|
|
|
274
|
+
full_path = resolve_safe_path(plugin_path, path)
|
|
275
|
+
next unless full_path
|
|
276
|
+
|
|
277
|
+
if File.file?(full_path)
|
|
278
|
+
files << full_path
|
|
279
|
+
elsif SafeDir.exist?(full_path, base_dir: plugin_path)
|
|
280
|
+
# Grab EVERYTHING recursively
|
|
281
|
+
glob_pattern = PathSanitizer.safe_join(full_path, "**", "*")
|
|
282
|
+
files += SafeDir.glob(glob_pattern, base_dir: plugin_path)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
files
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def resolve_safe_path(plugin_path, relative_path)
|
|
289
|
+
begin
|
|
290
|
+
PathSanitizer.sanitize_path(File.expand_path(relative_path, plugin_path), base_dir: plugin_path)
|
|
291
|
+
rescue PathSanitizer::PathTraversalError => e
|
|
292
|
+
warn "Skipping path outside plugin directory '#{relative_path}': #{e.message}"
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
end
|
|
256
296
|
|
|
257
297
|
def local_path?
|
|
258
298
|
!@marketplace_uri.match?(/\Ahttps?:/)
|
data/lib/caruso/version.rb
CHANGED