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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8eed6a0c1f2de97f2f16bdced1953bde78c4d1687e57a2be7686f4d824cce363
4
- data.tar.gz: 31ad0e3bf661f0883e852c015633e42e59c2265d899b3d987ea35b64dc10d2ea
3
+ metadata.gz: c881250221622ad116a1da441ca090a80fa1830f3d51b675b4a0919853d82287
4
+ data.tar.gz: 4276216066d112d8bed3e99c5d2928bd6d28ba1b44c4d2847f95f0f9447b803f
5
5
  SHA512:
6
- metadata.gz: d6ec542034f0cc10881b61d008b2a49fda43cfeeaed43565b00fde4c12ae0ca8a821311a9465a6ae697353047c942047281e5fed7d7b7d32c9ca4b406daaccac
7
- data.tar.gz: ba2ef61be533cfd99052705ab8d0fc8489596107cf8170410b7e2edf903c0de1541f698eeda2a65b7657a655ea536259547386bffc2d5db0af932bbeaff4fc76
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
- command = hook["command"]
149
- next unless command
118
+ cursor_entry = build_cursor_hook(event_name, matcher, hook)
119
+ next unless cursor_entry
150
120
 
151
- cursor_event = resolve_cursor_event(event_name, matcher)
152
- cursor_hook = { "command" => command }
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
- File.write(WRAPPER_PATH, CC_STOP_WRAPPER)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caruso
4
- VERSION = "0.7.5"
4
+ VERSION = "0.7.6"
5
5
  end
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.5
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