caruso 0.7.5 → 0.7.7
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/AGENTS.md +8 -12
- data/lib/caruso/adapters/command_adapter.rb +5 -10
- data/lib/caruso/adapters/hook_adapter.rb +17 -39
- data/lib/caruso/scripts/cc_stop_wrapper.sh +62 -0
- data/lib/caruso/stop_hook_translator.rb +67 -0
- data/lib/caruso/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af75d0db21770fa0559c36964cbca4cd51e80f8c96eb8097a1d41f0281ce70e3
|
|
4
|
+
data.tar.gz: 12bff57c9d9891b5abc54d2d2dd4a854ceb34faf8e3c5ad026ac3c51a0705944
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cadbe6b5022e9ae58c79d8cf3fdc9750e71f10f3d6c90f920fbb3e2be18da127977e14d725a4b36ec629cfc69008cd4c8a58203492f6748185557bf0f3567fe3
|
|
7
|
+
data.tar.gz: 616a955c75e3c1e4aff04a18f282248dbf677d01ce85be4f224db2e6b08418e59fcd4b1ba9db0dda8f485e8f9684b127153f0df44c917571683c782cb45f697b
|
data/AGENTS.md
CHANGED
|
@@ -6,21 +6,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
6
6
|
|
|
7
7
|
Caruso is a Ruby gem CLI that bridges the gap between AI coding assistants. It fetches "steering documentation" (commands, agents, skills) from Claude Code Marketplaces and converts them to formats compatible with other IDEs, currently Cursor.
|
|
8
8
|
|
|
9
|
-
## Source of Truth:
|
|
9
|
+
## Source of Truth: Official Documentation
|
|
10
10
|
|
|
11
|
-
**IMPORTANT:** The
|
|
11
|
+
**IMPORTANT:** The authoritative sources for Claude Code and Cursor specifications are the official docs. Reference links are in `/Users/philipp/code/caruso/reference/`:
|
|
12
12
|
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
- `plugins_reference.md` - Component configuration fields and metadata
|
|
13
|
+
- `claude_code.md` - Links to official Claude Code docs (hooks, plugins, marketplaces, skills)
|
|
14
|
+
- `cursor.md` - Links to official Cursor docs (hooks, modes, rules, commands)
|
|
16
15
|
|
|
17
|
-
**
|
|
18
|
-
- Marketplace.json schema and plugin metadata format
|
|
19
|
-
- Component configuration fields (`commands`, `agents`, `skills`, `hooks`, `mcpServers`)
|
|
20
|
-
- Expected directory structures and file patterns
|
|
21
|
-
- Metadata requirements and validation rules
|
|
22
|
-
|
|
23
|
-
When implementing features or fixing bugs related to marketplace compatibility, **always consult these reference files first**. If the implementation conflicts with the reference docs, the reference docs are correct and the code should be updated to match.
|
|
16
|
+
When implementing features or fixing bugs, **always fetch the latest docs from these official URLs**. If the implementation conflicts with the official docs, the docs are correct and the code should be updated to match.
|
|
24
17
|
|
|
25
18
|
## Development Commands
|
|
26
19
|
|
|
@@ -271,6 +264,9 @@ Results are deduplicated with `.uniq`.
|
|
|
271
264
|
|
|
272
265
|
Version is managed in `lib/caruso/version.rb`.
|
|
273
266
|
|
|
267
|
+
# Rules
|
|
268
|
+
- **NEVER force-push.** No `git push --force`, `git push --force-with-lease`, or `git push origin <tag> --force`. If a tag or commit was pushed wrong, fix forward with a new version.
|
|
269
|
+
|
|
274
270
|
# Memory
|
|
275
271
|
- The goal is a clean, correct, consistent implementation. Never implement fallbacks that hide errors or engage in defensive programming.
|
|
276
272
|
- **Idempotency**: Removal commands (`marketplace remove`, `plugin uninstall`) are designed to be idempotent. They exit successfully (0) if the target does not exist. This is intentional for automation friendliness and is NOT considered "hiding errors".
|
|
@@ -40,9 +40,9 @@ module Caruso
|
|
|
40
40
|
# We don't need globs or alwaysApply (those are for rules)
|
|
41
41
|
# Just return content as-is, Claude Code commands are already compatible
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Convert ```! auto-execute blocks to ```bash with run instruction
|
|
44
44
|
if content.include?("`!")
|
|
45
|
-
|
|
45
|
+
convert_auto_execute_blocks(content)
|
|
46
46
|
else
|
|
47
47
|
content
|
|
48
48
|
end
|
|
@@ -57,14 +57,9 @@ module Caruso
|
|
|
57
57
|
YAML
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
note = "\n**Note:** This command originally used Claude Code's `!` prefix for bash execution. " \
|
|
65
|
-
"Cursor does not support this feature. The bash commands are documented below for reference.\n"
|
|
66
|
-
|
|
67
|
-
"---\n#{match[1]}\n---\n#{note}#{match.post_match}"
|
|
60
|
+
def convert_auto_execute_blocks(content)
|
|
61
|
+
# Replace ```! with ```bash and add instruction to run
|
|
62
|
+
content.gsub("```!\n", "```bash\n")
|
|
68
63
|
end
|
|
69
64
|
|
|
70
65
|
def save_command_file(relative_path, content)
|
|
@@ -32,37 +32,7 @@ module Caruso
|
|
|
32
32
|
STOP_EVENTS = %w[stop subagentStop].freeze
|
|
33
33
|
|
|
34
34
|
WRAPPER_PATH = File.join(".cursor", "hooks", "caruso", "_cc_stop_wrapper.sh").freeze
|
|
35
|
-
|
|
36
|
-
# Translates Claude Code stop hook output to Cursor format.
|
|
37
|
-
# CC: {"decision":"block","reason":"..."} or exit 2 with stderr
|
|
38
|
-
# Cursor: {"followup_message":"..."}
|
|
39
|
-
CC_STOP_WRAPPER = <<~'BASH'
|
|
40
|
-
#!/bin/bash
|
|
41
|
-
set -uo pipefail
|
|
42
|
-
SCRIPT="$1"
|
|
43
|
-
shift
|
|
44
|
-
STDERR_TMP=$(mktemp) || exit 1
|
|
45
|
-
trap 'rm -f "$STDERR_TMP"' EXIT
|
|
46
|
-
OUTPUT=$("$SCRIPT" "$@" 2>"$STDERR_TMP")
|
|
47
|
-
EXIT_CODE=$?
|
|
48
|
-
if [ $EXIT_CODE -eq 2 ]; then
|
|
49
|
-
REASON=$(cat "$STDERR_TMP")
|
|
50
|
-
if [ -n "$REASON" ] && command -v jq >/dev/null 2>&1; then
|
|
51
|
-
jq -n --arg msg "$REASON" '{"followup_message": $msg}'
|
|
52
|
-
fi
|
|
53
|
-
exit 0
|
|
54
|
-
fi
|
|
55
|
-
if [ $EXIT_CODE -eq 0 ] && [ -n "$OUTPUT" ] && command -v jq >/dev/null 2>&1; then
|
|
56
|
-
DECISION=$(echo "$OUTPUT" | jq -r '.decision // empty' 2>/dev/null)
|
|
57
|
-
if [ "$DECISION" = "block" ]; then
|
|
58
|
-
REASON=$(echo "$OUTPUT" | jq -r '.reason // empty' 2>/dev/null)
|
|
59
|
-
[ -n "$REASON" ] && jq -n --arg msg "$REASON" '{"followup_message": $msg}'
|
|
60
|
-
exit 0
|
|
61
|
-
fi
|
|
62
|
-
fi
|
|
63
|
-
[ -n "$OUTPUT" ] && echo "$OUTPUT"
|
|
64
|
-
exit $EXIT_CODE
|
|
65
|
-
BASH
|
|
35
|
+
WRAPPER_SOURCE = File.expand_path("../scripts/cc_stop_wrapper.sh", __dir__).freeze
|
|
66
36
|
|
|
67
37
|
# Contains translated hook commands keyed by event (for clean uninstall tracking).
|
|
68
38
|
attr_reader :translated_hooks
|
|
@@ -145,20 +115,28 @@ module Caruso
|
|
|
145
115
|
next
|
|
146
116
|
end
|
|
147
117
|
|
|
148
|
-
|
|
149
|
-
next unless
|
|
118
|
+
cursor_entry = build_cursor_hook(event_name, matcher, hook)
|
|
119
|
+
next unless cursor_entry
|
|
150
120
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
cursor_hook["timeout"] = hook["timeout"] if hook["timeout"]
|
|
154
|
-
cursor_hook["loop_limit"] = nil if STOP_EVENTS.include?(cursor_event)
|
|
155
|
-
(hooks[cursor_event] ||= []) << cursor_hook
|
|
121
|
+
event, entry = cursor_entry
|
|
122
|
+
(hooks[event] ||= []) << entry
|
|
156
123
|
end
|
|
157
124
|
end
|
|
158
125
|
|
|
159
126
|
{ hooks: hooks, skipped_prompts: skipped }
|
|
160
127
|
end
|
|
161
128
|
|
|
129
|
+
def build_cursor_hook(event_name, matcher, hook)
|
|
130
|
+
command = hook["command"]
|
|
131
|
+
return unless command
|
|
132
|
+
|
|
133
|
+
cursor_event = resolve_cursor_event(event_name, matcher)
|
|
134
|
+
cursor_hook = { "command" => command }
|
|
135
|
+
cursor_hook["timeout"] = hook["timeout"] if hook["timeout"]
|
|
136
|
+
cursor_hook["loop_limit"] = nil if STOP_EVENTS.include?(cursor_event)
|
|
137
|
+
[cursor_event, cursor_hook]
|
|
138
|
+
end
|
|
139
|
+
|
|
162
140
|
def warn_skipped(skipped_events, skipped_prompts)
|
|
163
141
|
if skipped_events.any?
|
|
164
142
|
unique_skipped = skipped_events.uniq
|
|
@@ -244,7 +222,7 @@ module Caruso
|
|
|
244
222
|
|
|
245
223
|
def install_wrapper_script
|
|
246
224
|
FileUtils.mkdir_p(File.dirname(WRAPPER_PATH))
|
|
247
|
-
|
|
225
|
+
FileUtils.cp(WRAPPER_SOURCE, WRAPPER_PATH)
|
|
248
226
|
File.chmod(0o755, WRAPPER_PATH)
|
|
249
227
|
puts "Installed stop hook wrapper: #{WRAPPER_PATH}"
|
|
250
228
|
WRAPPER_PATH
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Translates Claude Code stop hook I/O to Cursor format.
|
|
3
|
+
#
|
|
4
|
+
# INPUT translation (stdin):
|
|
5
|
+
# Cursor may pass transcript_path: null. CC hooks that read the transcript
|
|
6
|
+
# would bail out. We provide a minimal dummy transcript so the hook can
|
|
7
|
+
# proceed past transcript gates (completion promise won't match, but the
|
|
8
|
+
# core loop/block logic still works).
|
|
9
|
+
#
|
|
10
|
+
# OUTPUT translation (stdout):
|
|
11
|
+
# CC: exit 2 + stderr reason -> block
|
|
12
|
+
# CC: exit 0 + {"decision":"block","reason":"..."} -> block
|
|
13
|
+
# Cursor: exit 0 + {"followup_message":"..."} -> continue with message
|
|
14
|
+
# Cursor: exit 0 + no output -> allow stop
|
|
15
|
+
#
|
|
16
|
+
# Usage: cc_stop_wrapper.sh <original-script> [args...]
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT="$1"
|
|
21
|
+
shift
|
|
22
|
+
|
|
23
|
+
STDERR_TMP=$(mktemp) || exit 1
|
|
24
|
+
FAKE_TRANSCRIPT=""
|
|
25
|
+
trap 'rm -f "$STDERR_TMP" "$FAKE_TRANSCRIPT"' EXIT
|
|
26
|
+
|
|
27
|
+
# Read and patch stdin: ensure transcript_path points to a readable file
|
|
28
|
+
INPUT=$(cat)
|
|
29
|
+
if [ -n "$INPUT" ] && command -v jq >/dev/null 2>&1; then
|
|
30
|
+
TP=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
31
|
+
if [ -z "$TP" ] || [ "$TP" = "null" ] || [ ! -f "$TP" ]; then
|
|
32
|
+
FAKE_TRANSCRIPT=$(mktemp) || exit 1
|
|
33
|
+
echo '{"role":"assistant","message":{"content":[{"type":"text","text":""}]}}' > "$FAKE_TRANSCRIPT"
|
|
34
|
+
INPUT=$(echo "$INPUT" | jq --arg tp "$FAKE_TRANSCRIPT" '.transcript_path = $tp')
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
OUTPUT=$(echo "$INPUT" | "$SCRIPT" "$@" 2>"$STDERR_TMP")
|
|
39
|
+
EXIT_CODE=$?
|
|
40
|
+
|
|
41
|
+
# CC exit 2 = block with stderr reason
|
|
42
|
+
if [ $EXIT_CODE -eq 2 ]; then
|
|
43
|
+
REASON=$(cat "$STDERR_TMP")
|
|
44
|
+
if [ -n "$REASON" ] && command -v jq >/dev/null 2>&1; then
|
|
45
|
+
jq -n --arg msg "$REASON" '{"followup_message": $msg}'
|
|
46
|
+
fi
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# CC exit 0 + JSON {"decision":"block"} -> translate to followup_message
|
|
51
|
+
if [ $EXIT_CODE -eq 0 ] && [ -n "$OUTPUT" ] && command -v jq >/dev/null 2>&1; then
|
|
52
|
+
DECISION=$(echo "$OUTPUT" | jq -r '.decision // empty' 2>/dev/null)
|
|
53
|
+
if [ "$DECISION" = "block" ]; then
|
|
54
|
+
REASON=$(echo "$OUTPUT" | jq -r '.reason // empty' 2>/dev/null)
|
|
55
|
+
[ -n "$REASON" ] && jq -n --arg msg "$REASON" '{"followup_message": $msg}'
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Pass through anything else unchanged
|
|
61
|
+
[ -n "$OUTPUT" ] && echo "$OUTPUT"
|
|
62
|
+
exit $EXIT_CODE
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Caruso
|
|
7
|
+
# Translates Claude Code stop hook output to Cursor format.
|
|
8
|
+
#
|
|
9
|
+
# Claude Code stop hooks communicate via:
|
|
10
|
+
# - Exit 2 + stderr message → block (continue the conversation)
|
|
11
|
+
# - Exit 0 + {"decision":"block","reason":"..."} → block
|
|
12
|
+
# - Exit 0 + no decision → allow stop
|
|
13
|
+
#
|
|
14
|
+
# Cursor stop hooks expect:
|
|
15
|
+
# - Exit 0 + {"followup_message":"..."} → continue with message
|
|
16
|
+
# - Exit 0 + no output → allow stop
|
|
17
|
+
class StopHookTranslator
|
|
18
|
+
def self.translate(stdout:, stderr:, exit_code:)
|
|
19
|
+
new(stdout: stdout, stderr: stderr, exit_code: exit_code).translate
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(stdout:, stderr:, exit_code:)
|
|
23
|
+
@stdout = stdout.to_s.strip
|
|
24
|
+
@stderr = stderr.to_s.strip
|
|
25
|
+
@exit_code = exit_code
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def translate
|
|
29
|
+
return translate_exit2 if @exit_code == 2
|
|
30
|
+
return translate_json_decision if @exit_code.zero? && block_decision?
|
|
31
|
+
|
|
32
|
+
# Pass through: non-blocking exit 0 or any other exit code
|
|
33
|
+
{ stdout: @stdout, exit_code: @exit_code }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def translate_exit2
|
|
39
|
+
if @stderr.empty?
|
|
40
|
+
{ stdout: "", exit_code: 0 }
|
|
41
|
+
else
|
|
42
|
+
{ stdout: JSON.generate({ "followup_message" => @stderr }), exit_code: 0 }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def translate_json_decision
|
|
47
|
+
reason = parsed_output&.fetch("reason", nil).to_s
|
|
48
|
+
if reason.empty?
|
|
49
|
+
{ stdout: "", exit_code: 0 }
|
|
50
|
+
else
|
|
51
|
+
{ stdout: JSON.generate({ "followup_message" => reason }), exit_code: 0 }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def block_decision?
|
|
56
|
+
parsed_output&.fetch("decision", nil) == "block"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parsed_output
|
|
60
|
+
return @parsed_output if defined?(@parsed_output)
|
|
61
|
+
|
|
62
|
+
@parsed_output = @stdout.empty? ? nil : JSON.parse(@stdout)
|
|
63
|
+
rescue JSON::ParserError
|
|
64
|
+
@parsed_output = nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/caruso/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: caruso
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philipp Comans
|
|
@@ -163,6 +163,8 @@ files:
|
|
|
163
163
|
- lib/caruso/remover.rb
|
|
164
164
|
- lib/caruso/safe_dir.rb
|
|
165
165
|
- lib/caruso/safe_file.rb
|
|
166
|
+
- lib/caruso/scripts/cc_stop_wrapper.sh
|
|
167
|
+
- lib/caruso/stop_hook_translator.rb
|
|
166
168
|
- lib/caruso/version.rb
|
|
167
169
|
- package-lock.json
|
|
168
170
|
- reference/claude_code.md
|