caruso 0.7.5 → 0.7.6
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/lib/caruso/adapters/hook_adapter.rb +17 -39
- data/lib/caruso/scripts/cc_stop_wrapper.sh +47 -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: c881250221622ad116a1da441ca090a80fa1830f3d51b675b4a0919853d82287
|
|
4
|
+
data.tar.gz: 4276216066d112d8bed3e99c5d2928bd6d28ba1b44c4d2847f95f0f9447b803f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebf987777a3b98062f132330f17ba646840085cd63a123c04d944997002c42c1049cb325f0eb66dfee4ecb0dfb4127b279a96e6a298aa73b1bb61f56ce7caba5
|
|
7
|
+
data.tar.gz: fe109bde2907c60ef0c15f3eaa4e4ea7fc1a4f98d2f0c05f7ceaad78b7e2ab0ea7c632fa7851497df8630aeff7adb145f7bab093059af136519cfdb80ad62e5d
|
|
@@ -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,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Translates Claude Code stop hook output to Cursor format.
|
|
3
|
+
#
|
|
4
|
+
# CC stop hooks communicate via:
|
|
5
|
+
# Exit 2 + stderr reason -> block (continue conversation)
|
|
6
|
+
# Exit 0 + {"decision":"block","reason":"..."} -> block
|
|
7
|
+
# Exit 0 + anything else -> allow stop
|
|
8
|
+
#
|
|
9
|
+
# Cursor stop hooks expect:
|
|
10
|
+
# Exit 0 + {"followup_message":"..."} -> continue with message
|
|
11
|
+
# Exit 0 + no output -> allow stop
|
|
12
|
+
#
|
|
13
|
+
# Usage: cc_stop_wrapper.sh <original-script> [args...]
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT="$1"
|
|
18
|
+
shift
|
|
19
|
+
|
|
20
|
+
STDERR_TMP=$(mktemp) || exit 1
|
|
21
|
+
trap 'rm -f "$STDERR_TMP"' EXIT
|
|
22
|
+
|
|
23
|
+
OUTPUT=$("$SCRIPT" "$@" 2>"$STDERR_TMP")
|
|
24
|
+
EXIT_CODE=$?
|
|
25
|
+
|
|
26
|
+
# CC exit 2 = block with stderr reason
|
|
27
|
+
if [ $EXIT_CODE -eq 2 ]; then
|
|
28
|
+
REASON=$(cat "$STDERR_TMP")
|
|
29
|
+
if [ -n "$REASON" ] && command -v jq >/dev/null 2>&1; then
|
|
30
|
+
jq -n --arg msg "$REASON" '{"followup_message": $msg}'
|
|
31
|
+
fi
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# CC exit 0 + JSON {"decision":"block"} -> translate to followup_message
|
|
36
|
+
if [ $EXIT_CODE -eq 0 ] && [ -n "$OUTPUT" ] && command -v jq >/dev/null 2>&1; then
|
|
37
|
+
DECISION=$(echo "$OUTPUT" | jq -r '.decision // empty' 2>/dev/null)
|
|
38
|
+
if [ "$DECISION" = "block" ]; then
|
|
39
|
+
REASON=$(echo "$OUTPUT" | jq -r '.reason // empty' 2>/dev/null)
|
|
40
|
+
[ -n "$REASON" ] && jq -n --arg msg "$REASON" '{"followup_message": $msg}'
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Pass through anything else unchanged
|
|
46
|
+
[ -n "$OUTPUT" ] && echo "$OUTPUT"
|
|
47
|
+
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.6
|
|
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
|