@quantiya/codevibe-claude-plugin 1.0.6 → 1.0.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevibe-claude",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Sync Claude Code sessions with iOS mobile app via AWS backend. Control Claude Code from your iPhone with real-time bidirectional synchronization.",
5
5
  "author": {
6
6
  "name": "CodeVibe Team"
@@ -39,92 +39,71 @@ SENT_UUIDS_FILE="${CODEVIBE_TMPDIR}/codevibe-claude-sent-uuids-${SESSION_ID}.txt
39
39
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
40
40
  log "DEBUG" "Reading transcript for assistant responses: $TRANSCRIPT_PATH"
41
41
 
42
- # Get the last user prompt UUID to know where assistant messages start
43
- LAST_USER_UUID=$(grep '"type":"user"' "$TRANSCRIPT_PATH" | tail -1 | jq -r '.uuid // empty')
44
-
45
- if [ -n "$LAST_USER_UUID" ]; then
46
- log "DEBUG" "Last user prompt UUID: $LAST_USER_UUID"
47
-
48
- # Track assistant messages in current turn (chained by parentUuid)
49
- declare -a CURRENT_TURN_UUIDS
50
- CURRENT_TURN_UUIDS=("$LAST_USER_UUID")
51
-
52
- # First pass: build the complete UUID chain including both assistant and user (tool_result) messages
53
- # This is needed because the chain goes: user_prompt -> assistant -> user(tool_result) -> assistant -> ...
54
- while IFS= read -r line; do
55
- PARENT_UUID=$(echo "$line" | jq -r '.parentUuid // empty')
56
- MESSAGE_UUID=$(echo "$line" | jq -r '.uuid // empty')
57
-
58
- # Check if parent is in current turn - if so, add this UUID to the chain
59
- for turn_uuid in "${CURRENT_TURN_UUIDS[@]}"; do
60
- if [ "$PARENT_UUID" = "$turn_uuid" ]; then
61
- CURRENT_TURN_UUIDS+=("$MESSAGE_UUID")
62
- break
63
- fi
64
- done
65
- done < "$TRANSCRIPT_PATH"
66
-
67
- log "DEBUG" "Built UUID chain with ${#CURRENT_TURN_UUIDS[@]} entries"
68
-
69
- # Second pass: extract and send assistant text messages that are in the chain
70
- while IFS= read -r line; do
71
- MESSAGE_UUID=$(echo "$line" | jq -r '.uuid // empty')
72
-
73
- # Check if this message is in our turn chain
74
- IS_CURRENT_TURN=false
75
- for turn_uuid in "${CURRENT_TURN_UUIDS[@]}"; do
76
- if [ "$MESSAGE_UUID" = "$turn_uuid" ]; then
77
- IS_CURRENT_TURN=true
78
- break
79
- fi
80
- done
81
-
82
- if [ "$IS_CURRENT_TURN" = false ]; then
83
- continue
84
- fi
85
-
86
- # Skip if this message UUID was already sent
87
- if [ -f "$SENT_UUIDS_FILE" ] && grep -q "^${MESSAGE_UUID}$" "$SENT_UUIDS_FILE"; then
88
- log "DEBUG" "Skipping already sent message UUID: $MESSAGE_UUID"
89
- continue
90
- fi
91
-
92
- MESSAGE_CONTENT=$(echo "$line" | jq -r '.message.content // empty')
42
+ # Load already-sent UUIDs for filtering
43
+ SENT_UUIDS_CONTENT=""
44
+ if [ -f "$SENT_UUIDS_FILE" ]; then
45
+ SENT_UUIDS_CONTENT=$(cat "$SENT_UUIDS_FILE")
46
+ fi
93
47
 
94
- if [ -z "$MESSAGE_CONTENT" ] || [ "$MESSAGE_CONTENT" = "null" ]; then
48
+ # Single jq invocation: find last user UUID, build chain, extract unsent assistant text
49
+ # This replaces two slow bash while-loops that spawned jq per line (O(n) jq processes → 1)
50
+ ASSISTANT_MESSAGES=$(jq -r --slurp --arg sent "$SENT_UUIDS_CONTENT" '
51
+ # Find last user prompt UUID
52
+ (map(select(.type == "user")) | last | .uuid // empty) as $lastUserUuid |
53
+ if ($lastUserUuid | length) == 0 then empty
54
+ else
55
+ # Build UUID chain from last user prompt
56
+ reduce .[] as $msg (
57
+ [$lastUserUuid];
58
+ if (. | any(. == ($msg.parentUuid // ""))) then
59
+ . + [($msg.uuid // "")]
60
+ else . end
61
+ ) as $chain |
62
+ # Parse sent UUIDs
63
+ ($sent | split("\n") | map(select(. != ""))) as $sentList |
64
+ # Filter: assistant messages in chain, not sent, with text content
65
+ .[] |
66
+ select(.type == "assistant") |
67
+ select(.uuid as $u | $chain | any(. == $u)) |
68
+ select(.uuid as $u | $sentList | any(. == $u) | not) |
69
+ {
70
+ uuid: .uuid,
71
+ text: ([.message.content[]? | select(.type == "text") | .text // empty] | join(" "))
72
+ } |
73
+ select(.text | length > 0) |
74
+ "\(.uuid)\t\(.text)"
75
+ end
76
+ ' "$TRANSCRIPT_PATH" 2>/dev/null)
77
+
78
+ if [ -n "$ASSISTANT_MESSAGES" ]; then
79
+ while IFS=$'\t' read -r MSG_UUID MSG_TEXT; do
80
+ if [ -z "$MSG_UUID" ] || [ -z "$MSG_TEXT" ]; then
95
81
  continue
96
82
  fi
97
83
 
98
- # Extract text content from assistant message
99
- TEXT_CONTENT=$(echo "$line" | jq -r '.message.content[] | select(.type == "text") | .text // empty' | tr '\n' ' ')
100
-
101
- if [ -n "$TEXT_CONTENT" ] && [ "$TEXT_CONTENT" != "null" ]; then
102
- # Create ASSISTANT_RESPONSE event
103
- ASSISTANT_PAYLOAD=$(jq -n \
104
- --arg session_id "$SESSION_ID" \
105
- --arg content "$TEXT_CONTENT" \
106
- --arg hook_event_name "PermissionRequest" \
107
- --arg type "ASSISTANT_RESPONSE" \
108
- '{
109
- session_id: $session_id,
110
- hook_event_name: $hook_event_name,
111
- type: $type,
112
- content: $content
113
- }')
114
-
115
- log "DEBUG" "Sending assistant response from PermissionRequest: ${TEXT_CONTENT:0:100}..."
116
-
117
- send_to_mcp "event" "$ASSISTANT_PAYLOAD" "$SESSION_ID"
118
-
119
- if [ $? -eq 0 ]; then
120
- log "INFO" "Assistant response sent successfully from PermissionRequest hook"
121
- # Track that this message UUID was sent
122
- echo "$MESSAGE_UUID" >> "$SENT_UUIDS_FILE"
123
- else
124
- log "ERROR" "Failed to send assistant response from PermissionRequest hook"
125
- fi
84
+ ASSISTANT_PAYLOAD=$(jq -n \
85
+ --arg session_id "$SESSION_ID" \
86
+ --arg content "$MSG_TEXT" \
87
+ --arg hook_event_name "PermissionRequest" \
88
+ --arg type "ASSISTANT_RESPONSE" \
89
+ '{
90
+ session_id: $session_id,
91
+ hook_event_name: $hook_event_name,
92
+ type: $type,
93
+ content: $content
94
+ }')
95
+
96
+ log "DEBUG" "Sending assistant response from PermissionRequest: ${MSG_TEXT:0:100}..."
97
+
98
+ send_to_mcp "event" "$ASSISTANT_PAYLOAD" "$SESSION_ID"
99
+
100
+ if [ $? -eq 0 ]; then
101
+ log "INFO" "Assistant response sent successfully from PermissionRequest hook"
102
+ echo "$MSG_UUID" >> "$SENT_UUIDS_FILE"
103
+ else
104
+ log "ERROR" "Failed to send assistant response from PermissionRequest hook"
126
105
  fi
127
- done < <(grep '"type":"assistant"' "$TRANSCRIPT_PATH")
106
+ done <<< "$ASSISTANT_MESSAGES"
128
107
  fi
129
108
  else
130
109
  log "WARN" "Transcript path not found or file doesn't exist: $TRANSCRIPT_PATH"
package/hooks/stop.sh CHANGED
@@ -31,123 +31,111 @@ fi
31
31
  log "DEBUG" "Reading transcript: $TRANSCRIPT_PATH"
32
32
 
33
33
  # Parse transcript and extract events since last user prompt
34
- # Strategy:
35
- # 1. Read all JSONL lines
36
- # 2. Find last user prompt (type=user with prompt content)
37
- # 3. Extract all assistant messages after that prompt
38
- # 4. Send each as separate event to MCP server
39
-
40
- # Get the last user prompt UUID to know where assistant messages start
41
- LAST_USER_UUID=$(grep '"type":"user"' "$TRANSCRIPT_PATH" | tail -1 | jq -r '.uuid // empty')
42
-
43
- if [ -z "$LAST_USER_UUID" ]; then
44
- log "WARN" "No user prompt found in transcript"
45
- exit 0
46
- fi
47
-
48
- log "DEBUG" "Last user prompt UUID: $LAST_USER_UUID"
49
-
50
- # Extract all assistant messages after the last user prompt
51
- # We'll read the file and process assistant messages
34
+ # Single jq --slurp invocation: finds last user UUID, builds parent chain,
35
+ # extracts assistant text + tool_use events. ~200x faster than per-line bash+jq loops.
52
36
  EVENTS_SENT=0
53
37
 
54
38
  # Track sent message UUIDs to avoid duplicates (shared with PermissionRequest hook)
55
39
  SENT_UUIDS_FILE="${CODEVIBE_TMPDIR}/codevibe-claude-sent-uuids-${SESSION_ID}.txt"
56
40
 
57
- # Track messages in current turn (chained by parentUuid)
58
- # Includes both assistant and user (tool_result) messages to follow the complete chain
59
- declare -a CURRENT_TURN_UUIDS
60
- CURRENT_TURN_UUIDS=("$LAST_USER_UUID")
61
-
62
- # First pass: build the complete UUID chain including both assistant and user (tool_result) messages
63
- # This is needed because the chain goes: user_prompt -> assistant -> user(tool_result) -> assistant -> ...
64
- while IFS= read -r line; do
65
- PARENT_UUID=$(echo "$line" | jq -r '.parentUuid // empty')
66
- MESSAGE_UUID=$(echo "$line" | jq -r '.uuid // empty')
67
-
68
- # Check if parent is in current turn - if so, add this UUID to the chain
69
- for turn_uuid in "${CURRENT_TURN_UUIDS[@]}"; do
70
- if [ "$PARENT_UUID" = "$turn_uuid" ]; then
71
- CURRENT_TURN_UUIDS+=("$MESSAGE_UUID")
72
- break
73
- fi
74
- done
75
- done < "$TRANSCRIPT_PATH"
76
-
77
- log "DEBUG" "Built UUID chain with ${#CURRENT_TURN_UUIDS[@]} entries"
78
-
79
- # Second pass: extract and send assistant messages that are in the chain
80
- while IFS= read -r line; do
81
- MESSAGE_UUID=$(echo "$line" | jq -r '.uuid // empty')
41
+ # Load already-sent UUIDs for filtering
42
+ SENT_UUIDS_CONTENT=""
43
+ if [ -f "$SENT_UUIDS_FILE" ]; then
44
+ SENT_UUIDS_CONTENT=$(cat "$SENT_UUIDS_FILE")
45
+ fi
82
46
 
83
- # Check if this message is in our turn chain
84
- IS_CURRENT_TURN=false
85
- for turn_uuid in "${CURRENT_TURN_UUIDS[@]}"; do
86
- if [ "$MESSAGE_UUID" = "$turn_uuid" ]; then
87
- IS_CURRENT_TURN=true
88
- break
47
+ # Single jq invocation: find last user UUID, build chain, extract assistant messages
48
+ # This replaces two slow bash while-loops that spawned jq per line (O(n) jq processes → 1)
49
+ # Output format: tab-separated lines: uuid\ttype\tcontent
50
+ # type is "text" for assistant text, "tool_use" for tool uses
51
+ TRANSCRIPT_EVENTS=$(jq -r --slurp --arg sent "$SENT_UUIDS_CONTENT" '
52
+ # Find last user prompt UUID
53
+ (map(select(.type == "user")) | last | .uuid // empty) as $lastUserUuid |
54
+ if ($lastUserUuid | length) == 0 then empty
55
+ else
56
+ # Build UUID chain from last user prompt
57
+ reduce .[] as $msg (
58
+ [$lastUserUuid];
59
+ if (. | any(. == ($msg.parentUuid // ""))) then
60
+ . + [($msg.uuid // "")]
61
+ else . end
62
+ ) as $chain |
63
+ # Parse sent UUIDs
64
+ ($sent | split("\n") | map(select(. != ""))) as $sentList |
65
+ # Filter: assistant messages in chain
66
+ .[] |
67
+ select(.type == "assistant") |
68
+ select(.uuid as $u | $chain | any(. == $u)) |
69
+ . as $msg |
70
+ # Emit text content
71
+ (
72
+ ($msg.uuid) as $uuid |
73
+ ($sentList | any(. == $uuid)) as $alreadySent |
74
+ if $alreadySent then
75
+ "\($uuid)\talready_sent\t"
76
+ else
77
+ # Text content
78
+ ([$msg.message.content[]? | select(.type == "text") | .text // empty] | join(" ")) as $text |
79
+ if ($text | length) > 0 then
80
+ "\($uuid)\ttext\t\($text)"
81
+ else empty end
82
+ end
83
+ ),
84
+ # Emit tool_use content (not affected by sent UUIDs)
85
+ (
86
+ $msg.message.content[]? | select(.type == "tool_use") |
87
+ "\($msg.uuid)\ttool_use\t\(. | tojson)"
88
+ )
89
+ end
90
+ ' "$TRANSCRIPT_PATH" 2>/dev/null)
91
+
92
+ log "DEBUG" "Transcript parsing complete"
93
+
94
+ if [ -n "$TRANSCRIPT_EVENTS" ]; then
95
+ while IFS=$'\t' read -r MSG_UUID MSG_TYPE MSG_CONTENT; do
96
+ if [ -z "$MSG_UUID" ]; then
97
+ continue
89
98
  fi
90
- done
91
-
92
- if [ "$IS_CURRENT_TURN" = false ]; then
93
- continue
94
- fi
95
99
 
96
- # Skip if this message UUID was already sent (by PermissionRequest hook)
97
- if [ -f "$SENT_UUIDS_FILE" ] && grep -q "^${MESSAGE_UUID}$" "$SENT_UUIDS_FILE"; then
98
- log "DEBUG" "Skipping already sent message UUID: $MESSAGE_UUID"
99
- continue
100
- fi
101
-
102
- MESSAGE_CONTENT=$(echo "$line" | jq -r '.message.content // empty')
103
-
104
- if [ -z "$MESSAGE_CONTENT" ] || [ "$MESSAGE_CONTENT" = "null" ]; then
105
- continue
106
- fi
107
-
108
- # Extract text content from assistant message
109
- TEXT_CONTENT=$(echo "$line" | jq -r '.message.content[] | select(.type == "text") | .text // empty' | tr '\n' ' ')
110
-
111
- if [ -n "$TEXT_CONTENT" ] && [ "$TEXT_CONTENT" != "null" ]; then
112
- # Create ASSISTANT_RESPONSE event (SESSION_ID already extracted at top)
113
- EVENT_PAYLOAD=$(jq -n \
114
- --arg session_id "$SESSION_ID" \
115
- --arg content "$TEXT_CONTENT" \
116
- --arg hook_event_name "Stop" \
117
- --arg type "ASSISTANT_RESPONSE" \
118
- '{
119
- session_id: $session_id,
120
- hook_event_name: $hook_event_name,
121
- type: $type,
122
- content: $content
123
- }')
124
-
125
- log "DEBUG" "Sending assistant response: ${TEXT_CONTENT:0:100}..."
126
-
127
- send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
128
-
129
- if [ $? -eq 0 ]; then
130
- EVENTS_SENT=$((EVENTS_SENT + 1))
131
- log "INFO" "Assistant response sent successfully"
132
- # Track that this message UUID was sent
133
- echo "$MESSAGE_UUID" >> "$SENT_UUIDS_FILE"
134
- else
135
- log "ERROR" "Failed to send assistant response"
100
+ if [ "$MSG_TYPE" = "already_sent" ]; then
101
+ log "DEBUG" "Skipping already sent message UUID: $MSG_UUID"
102
+ EVENTS_SENT=$((EVENTS_SENT + 1)) # Count as sent to prevent fallback duplicate
103
+ continue
136
104
  fi
137
- fi
138
105
 
139
- # Extract tool use content from assistant message
140
- TOOL_USES=$(echo "$line" | jq -c '.message.content[] | select(.type == "tool_use")')
106
+ if [ "$MSG_TYPE" = "text" ] && [ -n "$MSG_CONTENT" ]; then
107
+ EVENT_PAYLOAD=$(jq -n \
108
+ --arg session_id "$SESSION_ID" \
109
+ --arg content "$MSG_CONTENT" \
110
+ --arg hook_event_name "Stop" \
111
+ --arg type "ASSISTANT_RESPONSE" \
112
+ '{
113
+ session_id: $session_id,
114
+ hook_event_name: $hook_event_name,
115
+ type: $type,
116
+ content: $content
117
+ }')
118
+
119
+ log "DEBUG" "Sending assistant response: ${MSG_CONTENT:0:100}..."
120
+
121
+ send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
122
+
123
+ if [ $? -eq 0 ]; then
124
+ EVENTS_SENT=$((EVENTS_SENT + 1))
125
+ log "INFO" "Assistant response sent successfully"
126
+ echo "$MSG_UUID" >> "$SENT_UUIDS_FILE"
127
+ else
128
+ log "ERROR" "Failed to send assistant response"
129
+ fi
130
+ fi
141
131
 
142
- if [ -n "$TOOL_USES" ]; then
143
- echo "$TOOL_USES" | while IFS= read -r tool_use; do
144
- TOOL_NAME=$(echo "$tool_use" | jq -r '.name // empty')
145
- TOOL_INPUT=$(echo "$tool_use" | jq -c '.input // {}')
132
+ if [ "$MSG_TYPE" = "tool_use" ] && [ -n "$MSG_CONTENT" ]; then
133
+ TOOL_NAME=$(echo "$MSG_CONTENT" | jq -r '.name // empty')
134
+ TOOL_INPUT=$(echo "$MSG_CONTENT" | jq -c '.input // {}')
146
135
 
147
136
  if [ -n "$TOOL_NAME" ]; then
148
137
  # Check if this is an interactive prompt (AskUserQuestion)
149
138
  if [ "$TOOL_NAME" = "AskUserQuestion" ]; then
150
- # Extract question text from input
151
139
  QUESTION_TEXT=$(echo "$TOOL_INPUT" | jq -r '.questions[0].question // empty')
152
140
 
153
141
  if [ -n "$QUESTION_TEXT" ]; then
@@ -181,7 +169,7 @@ while IFS= read -r line; do
181
169
  fi
182
170
  fi
183
171
 
184
- # Send regular TOOL_USE event for all tools (including AskUserQuestion for context)
172
+ # Send regular TOOL_USE event for all tools
185
173
  TOOL_EVENT_PAYLOAD=$(jq -n \
186
174
  --arg session_id "$SESSION_ID" \
187
175
  --arg hook_event_name "Stop" \
@@ -209,10 +197,10 @@ while IFS= read -r line; do
209
197
  log "ERROR" "Failed to send tool use event"
210
198
  fi
211
199
  fi
212
- done
213
- fi
200
+ fi
214
201
 
215
- done < <(grep '"type":"assistant"' "$TRANSCRIPT_PATH")
202
+ done <<< "$TRANSCRIPT_EVENTS"
203
+ fi
216
204
 
217
205
  # Fallback: if no events were extracted from transcript but last_assistant_message is available,
218
206
  # send it directly. This handles the race condition where the Stop hook fires before the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-claude-plugin",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Mobile companion for Claude Code - monitor and control your Claude Code sessions from your iPhone with CodeVibe",
5
5
  "main": "dist/server.js",
6
6
  "bin": {