@quantiya/codevibe-claude-plugin 1.0.6 → 1.0.8

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.8",
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,74 @@ 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 | @base64)"
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_B64; do
80
+ if [ -z "$MSG_UUID" ] || [ -z "$MSG_TEXT_B64" ]; 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
+ # Decode base64-encoded text content (preserves newlines in markdown)
85
+ MSG_TEXT=$(echo "$MSG_TEXT_B64" | base64 -d 2>/dev/null || echo "$MSG_TEXT_B64")
86
+
87
+ ASSISTANT_PAYLOAD=$(jq -n \
88
+ --arg session_id "$SESSION_ID" \
89
+ --arg content "$MSG_TEXT" \
90
+ --arg hook_event_name "PermissionRequest" \
91
+ --arg type "ASSISTANT_RESPONSE" \
92
+ '{
93
+ session_id: $session_id,
94
+ hook_event_name: $hook_event_name,
95
+ type: $type,
96
+ content: $content
97
+ }')
98
+
99
+ log "DEBUG" "Sending assistant response from PermissionRequest: ${MSG_TEXT:0:100}..."
100
+
101
+ send_to_mcp "event" "$ASSISTANT_PAYLOAD" "$SESSION_ID"
102
+
103
+ if [ $? -eq 0 ]; then
104
+ log "INFO" "Assistant response sent successfully from PermissionRequest hook"
105
+ echo "$MSG_UUID" >> "$SENT_UUIDS_FILE"
106
+ else
107
+ log "ERROR" "Failed to send assistant response from PermissionRequest hook"
126
108
  fi
127
- done < <(grep '"type":"assistant"' "$TRANSCRIPT_PATH")
109
+ done <<< "$ASSISTANT_MESSAGES"
128
110
  fi
129
111
  else
130
112
  log "WARN" "Transcript path not found or file doesn't exist: $TRANSCRIPT_PATH"
package/hooks/stop.sh CHANGED
@@ -31,123 +31,116 @@ 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 base64-encoded assistant text, "tool_use" for JSON tool uses
51
+ # Text is base64-encoded because it may contain newlines which break line-by-line read
52
+ TRANSCRIPT_EVENTS=$(jq -r --slurp --arg sent "$SENT_UUIDS_CONTENT" '
53
+ # Find last user prompt UUID
54
+ (map(select(.type == "user")) | last | .uuid // empty) as $lastUserUuid |
55
+ if ($lastUserUuid | length) == 0 then empty
56
+ else
57
+ # Build UUID chain from last user prompt
58
+ reduce .[] as $msg (
59
+ [$lastUserUuid];
60
+ if (. | any(. == ($msg.parentUuid // ""))) then
61
+ . + [($msg.uuid // "")]
62
+ else . end
63
+ ) as $chain |
64
+ # Parse sent UUIDs
65
+ ($sent | split("\n") | map(select(. != ""))) as $sentList |
66
+ # Filter: assistant messages in chain
67
+ .[] |
68
+ select(.type == "assistant") |
69
+ select(.uuid as $u | $chain | any(. == $u)) |
70
+ . as $msg |
71
+ # Emit text content (base64-encoded to preserve newlines)
72
+ (
73
+ ($msg.uuid) as $uuid |
74
+ ($sentList | any(. == $uuid)) as $alreadySent |
75
+ if $alreadySent then
76
+ "\($uuid)\talready_sent\t"
77
+ else
78
+ ([$msg.message.content[]? | select(.type == "text") | .text // empty] | join(" ")) as $text |
79
+ if ($text | length) > 0 then
80
+ "\($uuid)\ttext\t\($text | @base64)"
81
+ else empty end
82
+ end
83
+ ),
84
+ # Emit tool_use content as JSON (tojson escapes newlines, safe for line read)
85
+ (
86
+ $msg.message.content[]? | select(.type == "tool_use") |
87
+ "\($msg.uuid)\ttool_use\t\(. | tojson | @base64)"
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
+ # Decode base64-encoded text content
108
+ DECODED_CONTENT=$(echo "$MSG_CONTENT" | base64 -d 2>/dev/null || echo "$MSG_CONTENT")
109
+
110
+ EVENT_PAYLOAD=$(jq -n \
111
+ --arg session_id "$SESSION_ID" \
112
+ --arg content "$DECODED_CONTENT" \
113
+ --arg hook_event_name "Stop" \
114
+ --arg type "ASSISTANT_RESPONSE" \
115
+ '{
116
+ session_id: $session_id,
117
+ hook_event_name: $hook_event_name,
118
+ type: $type,
119
+ content: $content
120
+ }')
121
+
122
+ log "DEBUG" "Sending assistant response: ${DECODED_CONTENT:0:100}..."
123
+
124
+ send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
125
+
126
+ if [ $? -eq 0 ]; then
127
+ EVENTS_SENT=$((EVENTS_SENT + 1))
128
+ log "INFO" "Assistant response sent successfully"
129
+ echo "$MSG_UUID" >> "$SENT_UUIDS_FILE"
130
+ else
131
+ log "ERROR" "Failed to send assistant response"
132
+ fi
133
+ fi
141
134
 
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 // {}')
135
+ if [ "$MSG_TYPE" = "tool_use" ] && [ -n "$MSG_CONTENT" ]; then
136
+ # Decode base64-encoded tool_use JSON
137
+ DECODED_TOOL=$(echo "$MSG_CONTENT" | base64 -d 2>/dev/null || echo "$MSG_CONTENT")
138
+ TOOL_NAME=$(echo "$DECODED_TOOL" | jq -r '.name // empty')
139
+ TOOL_INPUT=$(echo "$DECODED_TOOL" | jq -c '.input // {}')
146
140
 
147
141
  if [ -n "$TOOL_NAME" ]; then
148
142
  # Check if this is an interactive prompt (AskUserQuestion)
149
143
  if [ "$TOOL_NAME" = "AskUserQuestion" ]; then
150
- # Extract question text from input
151
144
  QUESTION_TEXT=$(echo "$TOOL_INPUT" | jq -r '.questions[0].question // empty')
152
145
 
153
146
  if [ -n "$QUESTION_TEXT" ]; then
@@ -181,7 +174,7 @@ while IFS= read -r line; do
181
174
  fi
182
175
  fi
183
176
 
184
- # Send regular TOOL_USE event for all tools (including AskUserQuestion for context)
177
+ # Send regular TOOL_USE event for all tools
185
178
  TOOL_EVENT_PAYLOAD=$(jq -n \
186
179
  --arg session_id "$SESSION_ID" \
187
180
  --arg hook_event_name "Stop" \
@@ -209,10 +202,10 @@ while IFS= read -r line; do
209
202
  log "ERROR" "Failed to send tool use event"
210
203
  fi
211
204
  fi
212
- done
213
- fi
205
+ fi
214
206
 
215
- done < <(grep '"type":"assistant"' "$TRANSCRIPT_PATH")
207
+ done <<< "$TRANSCRIPT_EVENTS"
208
+ fi
216
209
 
217
210
  # Fallback: if no events were extracted from transcript but last_assistant_message is available,
218
211
  # 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.8",
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": {