claude-telegram-mirror 0.1.0
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.
- package/README.md +331 -0
- package/dist/bot/commands.d.ts +41 -0
- package/dist/bot/commands.d.ts.map +1 -0
- package/dist/bot/commands.js +231 -0
- package/dist/bot/commands.js.map +1 -0
- package/dist/bot/formatting.d.ts +62 -0
- package/dist/bot/formatting.d.ts.map +1 -0
- package/dist/bot/formatting.js +295 -0
- package/dist/bot/formatting.js.map +1 -0
- package/dist/bot/telegram.d.ts +93 -0
- package/dist/bot/telegram.d.ts.map +1 -0
- package/dist/bot/telegram.js +378 -0
- package/dist/bot/telegram.js.map +1 -0
- package/dist/bot/types.d.ts +28 -0
- package/dist/bot/types.d.ts.map +1 -0
- package/dist/bot/types.js +5 -0
- package/dist/bot/types.js.map +1 -0
- package/dist/bridge/daemon.d.ts +93 -0
- package/dist/bridge/daemon.d.ts.map +1 -0
- package/dist/bridge/daemon.js +626 -0
- package/dist/bridge/daemon.js.map +1 -0
- package/dist/bridge/index.d.ts +10 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +9 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/injector.d.ts +97 -0
- package/dist/bridge/injector.d.ts.map +1 -0
- package/dist/bridge/injector.js +289 -0
- package/dist/bridge/injector.js.map +1 -0
- package/dist/bridge/session.d.ts +108 -0
- package/dist/bridge/session.d.ts.map +1 -0
- package/dist/bridge/session.js +381 -0
- package/dist/bridge/session.js.map +1 -0
- package/dist/bridge/socket.d.ts +97 -0
- package/dist/bridge/socket.d.ts.map +1 -0
- package/dist/bridge/socket.js +436 -0
- package/dist/bridge/socket.js.map +1 -0
- package/dist/bridge/types.d.ts +38 -0
- package/dist/bridge/types.d.ts.map +1 -0
- package/dist/bridge/types.js +5 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +332 -0
- package/dist/cli.js.map +1 -0
- package/dist/hooks/handler.d.ts +94 -0
- package/dist/hooks/handler.d.ts.map +1 -0
- package/dist/hooks/handler.js +431 -0
- package/dist/hooks/handler.js.map +1 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/installer.d.ts +46 -0
- package/dist/hooks/installer.d.ts.map +1 -0
- package/dist/hooks/installer.js +317 -0
- package/dist/hooks/installer.js.map +1 -0
- package/dist/hooks/types.d.ts +88 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +6 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/service/doctor.d.ts +10 -0
- package/dist/service/doctor.d.ts.map +1 -0
- package/dist/service/doctor.js +424 -0
- package/dist/service/doctor.js.map +1 -0
- package/dist/service/manager.d.ts +48 -0
- package/dist/service/manager.d.ts.map +1 -0
- package/dist/service/manager.js +584 -0
- package/dist/service/manager.js.map +1 -0
- package/dist/service/setup.d.ts +10 -0
- package/dist/service/setup.d.ts.map +1 -0
- package/dist/service/setup.js +266 -0
- package/dist/service/setup.js.map +1 -0
- package/dist/utils/chunker.d.ts +24 -0
- package/dist/utils/chunker.d.ts.map +1 -0
- package/dist/utils/chunker.js +123 -0
- package/dist/utils/chunker.js.map +1 -0
- package/dist/utils/config.d.ts +48 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +154 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +28 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +88 -0
- package/postinstall.cjs +76 -0
- package/scripts/claude-wrapper.sh +122 -0
- package/scripts/doctor.sh +433 -0
- package/scripts/get-chat-id.sh +64 -0
- package/scripts/global-hooks.sh +39 -0
- package/scripts/install.sh +831 -0
- package/scripts/start-daemon.sh +49 -0
- package/scripts/telegram-hook.sh +449 -0
- package/scripts/uninstall.sh +261 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Start the Claude Code Telegram Mirror daemon
|
|
4
|
+
#
|
|
5
|
+
# This script sources environment variables from ~/.telegram-env
|
|
6
|
+
# to work around .bashrc's non-interactive shell exit.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./start-daemon.sh # Run in foreground
|
|
10
|
+
# nohup ./start-daemon.sh & # Run in background
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
# Get script directory
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
|
|
18
|
+
|
|
19
|
+
# Source environment file (bypasses .bashrc non-interactive exit)
|
|
20
|
+
if [[ -f "$HOME/.telegram-env" ]]; then
|
|
21
|
+
source "$HOME/.telegram-env"
|
|
22
|
+
elif [[ -f "$PACKAGE_DIR/.env" ]]; then
|
|
23
|
+
source "$PACKAGE_DIR/.env"
|
|
24
|
+
else
|
|
25
|
+
echo "⚠️ No environment file found."
|
|
26
|
+
echo ""
|
|
27
|
+
echo "Create ~/.telegram-env with:"
|
|
28
|
+
echo ' export TELEGRAM_BOT_TOKEN="your-token"'
|
|
29
|
+
echo ' export TELEGRAM_CHAT_ID="your-chat-id"'
|
|
30
|
+
echo ' export TELEGRAM_MIRROR=true'
|
|
31
|
+
echo ""
|
|
32
|
+
echo "Or create $PACKAGE_DIR/.env with the same exports."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Verify required variables
|
|
37
|
+
if [[ -z "$TELEGRAM_BOT_TOKEN" ]]; then
|
|
38
|
+
echo "❌ TELEGRAM_BOT_TOKEN is not set"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [[ -z "$TELEGRAM_CHAT_ID" ]]; then
|
|
43
|
+
echo "❌ TELEGRAM_CHAT_ID is not set"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Change to package directory and start
|
|
48
|
+
cd "$PACKAGE_DIR"
|
|
49
|
+
exec node dist/cli.js start
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Claude Code Telegram Hook
|
|
4
|
+
# Captures hook events and forwards to bridge daemon
|
|
5
|
+
#
|
|
6
|
+
# Usage: This script is called by Claude Code's hook system
|
|
7
|
+
# It reads JSON from stdin and forwards to the bridge
|
|
8
|
+
#
|
|
9
|
+
# The hook is ENABLED when the bridge daemon is running (socket exists).
|
|
10
|
+
# No environment variable needed - just start the bridge!
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
# Debug logging disabled by default - set TELEGRAM_HOOK_DEBUG=1 to enable
|
|
14
|
+
TELEGRAM_HOOK_DEBUG="${TELEGRAM_HOOK_DEBUG:-0}"
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
# Trap to ensure clean exit and helpful error message
|
|
19
|
+
trap 'debug_log "Script exiting with code $?"' EXIT
|
|
20
|
+
|
|
21
|
+
# Get the directory of this script
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
|
|
24
|
+
CONFIG_DIR="$HOME/.config/claude-telegram-mirror"
|
|
25
|
+
|
|
26
|
+
# Socket path for bridge communication (now in user config dir, not /tmp)
|
|
27
|
+
SOCKET_PATH="${TELEGRAM_BRIDGE_SOCKET:-$CONFIG_DIR/bridge.sock}"
|
|
28
|
+
|
|
29
|
+
# Debug logging (set TELEGRAM_HOOK_DEBUG=1 to enable)
|
|
30
|
+
debug_log() {
|
|
31
|
+
if [[ "${TELEGRAM_HOOK_DEBUG}" == "1" ]]; then
|
|
32
|
+
# Log to config dir instead of world-readable /tmp
|
|
33
|
+
mkdir -p "$CONFIG_DIR" 2>/dev/null || true
|
|
34
|
+
echo "[telegram-hook] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$CONFIG_DIR/hook-debug.log"
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
debug_log "Hook called, checking socket at $SOCKET_PATH"
|
|
39
|
+
|
|
40
|
+
# Check if bridge is running (socket exists = enabled)
|
|
41
|
+
if [[ ! -S "$SOCKET_PATH" ]]; then
|
|
42
|
+
# Bridge not running, pass through silently
|
|
43
|
+
debug_log "Bridge not running (no socket), passing through"
|
|
44
|
+
cat
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
debug_log "Bridge socket found, processing..."
|
|
49
|
+
|
|
50
|
+
# Read stdin into variable
|
|
51
|
+
INPUT=$(cat)
|
|
52
|
+
|
|
53
|
+
# If empty, just exit
|
|
54
|
+
if [[ -z "$INPUT" ]]; then
|
|
55
|
+
debug_log "Empty input, exiting"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Log raw input for debugging
|
|
60
|
+
debug_log "Raw input: $INPUT"
|
|
61
|
+
|
|
62
|
+
# Parse hook type from input (field is "hook_event_name" not "type")
|
|
63
|
+
HOOK_TYPE=$(echo "$INPUT" | jq -r '.hook_event_name // .type // empty' 2>/dev/null || echo "")
|
|
64
|
+
debug_log "Hook type: $HOOK_TYPE"
|
|
65
|
+
|
|
66
|
+
# Use Claude's native session_id from the hook input - this is the canonical session identifier
|
|
67
|
+
# This ensures all events from the same Claude session go to the same Telegram topic
|
|
68
|
+
CLAUDE_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "")
|
|
69
|
+
debug_log "Claude session_id: $CLAUDE_SESSION_ID"
|
|
70
|
+
|
|
71
|
+
# Session tracking - detect first event per Claude session
|
|
72
|
+
get_session_tracking_path() {
|
|
73
|
+
mkdir -p "$CONFIG_DIR" 2>/dev/null || true
|
|
74
|
+
# Use Claude's session_id as the key - this is stable for the entire session
|
|
75
|
+
if [[ -n "$CLAUDE_SESSION_ID" ]]; then
|
|
76
|
+
echo "$CONFIG_DIR/.session_active_${CLAUDE_SESSION_ID}"
|
|
77
|
+
else
|
|
78
|
+
# Fallback to tmux pane if no Claude session_id
|
|
79
|
+
local session_key=""
|
|
80
|
+
if [[ -n "$TMUX" ]]; then
|
|
81
|
+
session_key=$(tmux display-message -p '#{session_id}_#{window_id}_#{pane_id}' 2>/dev/null || echo "")
|
|
82
|
+
fi
|
|
83
|
+
if [[ -z "$session_key" ]]; then
|
|
84
|
+
session_key=$(tty 2>/dev/null | tr '/' '_' || echo "default")
|
|
85
|
+
fi
|
|
86
|
+
local safe_id=$(echo "$session_key" | tr -cd '[:alnum:]_')
|
|
87
|
+
echo "$CONFIG_DIR/.session_active_${safe_id}"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
is_first_event() {
|
|
92
|
+
local tracking_path=$(get_session_tracking_path)
|
|
93
|
+
if [[ -f "$tracking_path" ]]; then
|
|
94
|
+
return 1 # Not first event
|
|
95
|
+
fi
|
|
96
|
+
# Mark session as started
|
|
97
|
+
echo "$SESSION_ID" > "$tracking_path"
|
|
98
|
+
return 0 # First event
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear_session_tracking() {
|
|
102
|
+
local tracking_path=$(get_session_tracking_path)
|
|
103
|
+
rm -f "$tracking_path" 2>/dev/null || true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Use Claude's native session_id, or generate one as fallback
|
|
107
|
+
SESSION_ID="${CLAUDE_SESSION_ID:-$(date +%s)-$$}"
|
|
108
|
+
debug_log "Using session ID: $SESSION_ID"
|
|
109
|
+
|
|
110
|
+
# Get tmux info if available
|
|
111
|
+
# Extracts socket path from $TMUX env var for explicit targeting
|
|
112
|
+
get_tmux_info() {
|
|
113
|
+
if [[ -z "$TMUX" ]]; then
|
|
114
|
+
echo "{}"
|
|
115
|
+
return
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# $TMUX format: /path/to/socket,pid,index
|
|
119
|
+
# Extract socket path (everything before first comma)
|
|
120
|
+
local socket_path="${TMUX%%,*}"
|
|
121
|
+
|
|
122
|
+
local session=$(tmux display-message -p "#S" 2>/dev/null || echo "")
|
|
123
|
+
local pane=$(tmux display-message -p "#P" 2>/dev/null || echo "")
|
|
124
|
+
local window=$(tmux display-message -p "#I" 2>/dev/null || echo "")
|
|
125
|
+
|
|
126
|
+
if [[ -n "$session" && -n "$window" && -n "$pane" ]]; then
|
|
127
|
+
local target="${session}:${window}.${pane}"
|
|
128
|
+
jq -cn \
|
|
129
|
+
--arg session "$session" \
|
|
130
|
+
--arg pane "$pane" \
|
|
131
|
+
--arg target "$target" \
|
|
132
|
+
--arg socket "$socket_path" \
|
|
133
|
+
'{tmuxSession: $session, tmuxPane: $pane, tmuxTarget: $target, tmuxSocket: $socket}'
|
|
134
|
+
else
|
|
135
|
+
echo "{}"
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Send message to bridge via netcat (fast)
|
|
140
|
+
# Works on both Linux (GNU netcat) and macOS (BSD netcat)
|
|
141
|
+
send_to_bridge() {
|
|
142
|
+
local message="$1"
|
|
143
|
+
debug_log "Sending to bridge: ${message:0:100}..."
|
|
144
|
+
|
|
145
|
+
if command -v nc &> /dev/null; then
|
|
146
|
+
local nc_stderr
|
|
147
|
+
local nc_exit
|
|
148
|
+
# Try GNU netcat first (-q0 = quit after EOF), fall back to BSD netcat (no -q flag)
|
|
149
|
+
if nc -h 2>&1 | grep -q '\-q'; then
|
|
150
|
+
# GNU netcat - supports -q flag
|
|
151
|
+
nc_stderr=$(echo "$message" | nc -U -q0 "$SOCKET_PATH" 2>&1)
|
|
152
|
+
nc_exit=$?
|
|
153
|
+
else
|
|
154
|
+
# BSD netcat (macOS) - no -q flag needed, closes on EOF
|
|
155
|
+
nc_stderr=$(echo "$message" | nc -U "$SOCKET_PATH" 2>&1)
|
|
156
|
+
nc_exit=$?
|
|
157
|
+
fi
|
|
158
|
+
debug_log "nc result: exit=$nc_exit, stderr=$nc_stderr"
|
|
159
|
+
if [[ $nc_exit -ne 0 ]]; then
|
|
160
|
+
debug_log "nc FAILED! socket=$SOCKET_PATH"
|
|
161
|
+
fi
|
|
162
|
+
elif [[ -S "$SOCKET_PATH" ]]; then
|
|
163
|
+
echo "$message" > "$SOCKET_PATH" 2>/dev/null || true
|
|
164
|
+
debug_log "Used direct write to socket"
|
|
165
|
+
else
|
|
166
|
+
debug_log "No nc and no socket!"
|
|
167
|
+
fi
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# ============================================
|
|
171
|
+
# REAL-TIME TRANSCRIPT SYNC
|
|
172
|
+
# Extract new assistant text on EVERY hook event
|
|
173
|
+
# This provides real-time mirroring of Claude's responses
|
|
174
|
+
# ============================================
|
|
175
|
+
sync_new_assistant_text() {
|
|
176
|
+
local transcript_path=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null)
|
|
177
|
+
|
|
178
|
+
if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
|
|
179
|
+
return
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Use separate state file for continuous sync (different from Stop hook)
|
|
183
|
+
local state_file="$CONFIG_DIR/.sync_${SESSION_ID}"
|
|
184
|
+
local last_line=0
|
|
185
|
+
if [[ -f "$state_file" ]]; then
|
|
186
|
+
last_line=$(cat "$state_file" 2>/dev/null || echo 0)
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
local current_line=$(wc -l < "$transcript_path")
|
|
190
|
+
|
|
191
|
+
# No new lines
|
|
192
|
+
if [[ $current_line -le $last_line ]]; then
|
|
193
|
+
return
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
local new_count=$((current_line - last_line))
|
|
197
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
198
|
+
local found_text=0
|
|
199
|
+
|
|
200
|
+
# Process new lines for assistant text messages
|
|
201
|
+
while IFS= read -r line; do
|
|
202
|
+
local msg_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null)
|
|
203
|
+
|
|
204
|
+
if [[ "$msg_type" == "assistant" ]]; then
|
|
205
|
+
# Extract text content (skip tool_use blocks)
|
|
206
|
+
local text=$(echo "$line" | jq -r '.message.content[]? | select(.type == "text") | .text' 2>/dev/null)
|
|
207
|
+
|
|
208
|
+
if [[ -n "$text" && ${#text} -gt 10 ]]; then
|
|
209
|
+
debug_log "Sync: Found assistant text (${#text} chars)"
|
|
210
|
+
# Use printf + pipe to handle large content (avoids "Argument list too long")
|
|
211
|
+
local msg=$(printf '%s' "$text" | jq -Rsc \
|
|
212
|
+
--arg type "agent_response" \
|
|
213
|
+
--arg sessionId "$SESSION_ID" \
|
|
214
|
+
--arg timestamp "$timestamp" \
|
|
215
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: .}')
|
|
216
|
+
send_to_bridge "$msg"
|
|
217
|
+
found_text=1
|
|
218
|
+
fi
|
|
219
|
+
fi
|
|
220
|
+
done < <(tail -n "$new_count" "$transcript_path")
|
|
221
|
+
|
|
222
|
+
# Update state (always, even if no text found - to track line position)
|
|
223
|
+
echo "$current_line" > "$state_file"
|
|
224
|
+
|
|
225
|
+
if [[ $found_text -eq 1 ]]; then
|
|
226
|
+
debug_log "Sync: Sent new assistant text"
|
|
227
|
+
fi
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# ============================================
|
|
231
|
+
# REAL-TIME SYNC: DISABLED - causing replay issues
|
|
232
|
+
# TODO: Fix race conditions before re-enabling
|
|
233
|
+
# ============================================
|
|
234
|
+
# sync_new_assistant_text &
|
|
235
|
+
|
|
236
|
+
# Format bridge message based on hook type
|
|
237
|
+
format_message() {
|
|
238
|
+
local hook_type="$1"
|
|
239
|
+
local input="$2"
|
|
240
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
241
|
+
|
|
242
|
+
case "$hook_type" in
|
|
243
|
+
"PreToolUse")
|
|
244
|
+
local tool_name=$(echo "$input" | jq -r '.tool_name // "unknown"')
|
|
245
|
+
local tool_input=$(echo "$input" | jq -c '.tool_input // {}')
|
|
246
|
+
|
|
247
|
+
# Send tool info (not just dangerous ones - for visibility)
|
|
248
|
+
jq -cn \
|
|
249
|
+
--arg type "tool_start" \
|
|
250
|
+
--arg sessionId "$SESSION_ID" \
|
|
251
|
+
--arg timestamp "$timestamp" \
|
|
252
|
+
--arg tool "$tool_name" \
|
|
253
|
+
--argjson input "$tool_input" \
|
|
254
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: "Tool: \($tool)", metadata: {tool: $tool, input: $input}}'
|
|
255
|
+
;;
|
|
256
|
+
|
|
257
|
+
"PostToolUse")
|
|
258
|
+
local tool_name=$(echo "$input" | jq -r '.tool_name // "unknown"')
|
|
259
|
+
local tool_output=$(echo "$input" | jq -r '.tool_output // ""' | head -c 2000)
|
|
260
|
+
|
|
261
|
+
# Only send significant outputs
|
|
262
|
+
if [[ ${#tool_output} -gt 10 ]]; then
|
|
263
|
+
jq -cn \
|
|
264
|
+
--arg type "tool_result" \
|
|
265
|
+
--arg sessionId "$SESSION_ID" \
|
|
266
|
+
--arg timestamp "$timestamp" \
|
|
267
|
+
--arg tool "$tool_name" \
|
|
268
|
+
--arg output "$tool_output" \
|
|
269
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: $output, metadata: {tool: $tool}}'
|
|
270
|
+
fi
|
|
271
|
+
;;
|
|
272
|
+
|
|
273
|
+
"Notification")
|
|
274
|
+
local message=$(echo "$input" | jq -r '.message // ""')
|
|
275
|
+
local notification_type=$(echo "$input" | jq -r '.notification_type // ""')
|
|
276
|
+
local level=$(echo "$input" | jq -r '.level // "info"')
|
|
277
|
+
|
|
278
|
+
# Skip idle_prompt notifications - they're just noise
|
|
279
|
+
if [[ "$notification_type" == "idle_prompt" ]]; then
|
|
280
|
+
debug_log "Skipping idle_prompt notification"
|
|
281
|
+
return
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
if [[ -n "$message" ]]; then
|
|
285
|
+
jq -cn \
|
|
286
|
+
--arg type "agent_response" \
|
|
287
|
+
--arg sessionId "$SESSION_ID" \
|
|
288
|
+
--arg timestamp "$timestamp" \
|
|
289
|
+
--arg message "$message" \
|
|
290
|
+
--arg level "$level" \
|
|
291
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: $message, metadata: {level: $level}}'
|
|
292
|
+
fi
|
|
293
|
+
;;
|
|
294
|
+
|
|
295
|
+
"Stop")
|
|
296
|
+
local transcript_path=$(echo "$input" | jq -r '.transcript_path // ""')
|
|
297
|
+
debug_log "Stop: transcript_path='$transcript_path'"
|
|
298
|
+
debug_log "Stop: file exists=$(test -f "$transcript_path" && echo yes || echo no)"
|
|
299
|
+
|
|
300
|
+
# Extract NEW text content since last Stop
|
|
301
|
+
# Track what we've sent to avoid duplicates
|
|
302
|
+
if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
303
|
+
debug_log "Reading transcript: $transcript_path"
|
|
304
|
+
|
|
305
|
+
# Track last processed line using a state file
|
|
306
|
+
local state_file="$CONFIG_DIR/.last_line_${SESSION_ID}"
|
|
307
|
+
local last_line=0
|
|
308
|
+
if [[ -f "$state_file" ]]; then
|
|
309
|
+
last_line=$(cat "$state_file" 2>/dev/null || echo 0)
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
local current_line=$(wc -l < "$transcript_path")
|
|
313
|
+
debug_log "Last processed: $last_line, Current: $current_line"
|
|
314
|
+
|
|
315
|
+
# Only process new lines
|
|
316
|
+
if [[ $current_line -gt $last_line ]]; then
|
|
317
|
+
local new_lines=$((current_line - last_line))
|
|
318
|
+
local all_text=""
|
|
319
|
+
|
|
320
|
+
# Process only NEW lines
|
|
321
|
+
while IFS= read -r line; do
|
|
322
|
+
local text=$(echo "$line" | jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text' 2>/dev/null)
|
|
323
|
+
if [[ -n "$text" ]]; then
|
|
324
|
+
if [[ -n "$all_text" ]]; then
|
|
325
|
+
all_text="${all_text}
|
|
326
|
+
|
|
327
|
+
${text}"
|
|
328
|
+
else
|
|
329
|
+
all_text="$text"
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
done < <(tail -n "$new_lines" "$transcript_path")
|
|
333
|
+
|
|
334
|
+
debug_log "New text length: ${#all_text}"
|
|
335
|
+
debug_log "New text preview: ${all_text:0:200}"
|
|
336
|
+
|
|
337
|
+
if [[ -n "$all_text" ]]; then
|
|
338
|
+
# Use printf + pipe to handle large content (avoids "Argument list too long")
|
|
339
|
+
local agent_msg=$(printf '%s' "$all_text" | jq -Rsc \
|
|
340
|
+
--arg type "agent_response" \
|
|
341
|
+
--arg sessionId "$SESSION_ID" \
|
|
342
|
+
--arg timestamp "$timestamp" \
|
|
343
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: .}')
|
|
344
|
+
debug_log "Stop: sending agent_response: ${agent_msg:0:150}"
|
|
345
|
+
echo "$agent_msg"
|
|
346
|
+
else
|
|
347
|
+
debug_log "Stop: NO text extracted from new lines"
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Update state
|
|
351
|
+
echo "$current_line" > "$state_file"
|
|
352
|
+
else
|
|
353
|
+
debug_log "Stop: no new lines (current=$current_line, last=$last_line)"
|
|
354
|
+
fi
|
|
355
|
+
else
|
|
356
|
+
debug_log "Stop: transcript not accessible (path='$transcript_path', exists=$(test -f "$transcript_path" && echo yes || echo no))"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# DON'T send session_end on every Stop - Claude fires Stop after every turn!
|
|
360
|
+
# The session is still active. Only send a turn_complete notification.
|
|
361
|
+
# Session end should happen when user explicitly exits or connection drops.
|
|
362
|
+
local turn_msg=$(jq -cn \
|
|
363
|
+
--arg type "turn_complete" \
|
|
364
|
+
--arg sessionId "$SESSION_ID" \
|
|
365
|
+
--arg timestamp "$timestamp" \
|
|
366
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: "Turn complete"}')
|
|
367
|
+
debug_log "Stop: sending turn_complete: $turn_msg"
|
|
368
|
+
echo "$turn_msg"
|
|
369
|
+
;;
|
|
370
|
+
|
|
371
|
+
"UserPromptSubmit")
|
|
372
|
+
local prompt=$(echo "$input" | jq -r '.prompt // ""')
|
|
373
|
+
|
|
374
|
+
if [[ -n "$prompt" ]]; then
|
|
375
|
+
jq -cn \
|
|
376
|
+
--arg type "user_input" \
|
|
377
|
+
--arg sessionId "$SESSION_ID" \
|
|
378
|
+
--arg timestamp "$timestamp" \
|
|
379
|
+
--arg prompt "$prompt" \
|
|
380
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: $prompt, metadata: {source: "cli"}}'
|
|
381
|
+
fi
|
|
382
|
+
;;
|
|
383
|
+
|
|
384
|
+
"PreCompact")
|
|
385
|
+
# Fires just before context compaction (manual or auto)
|
|
386
|
+
local trigger=$(echo "$input" | jq -r '.trigger // "auto"')
|
|
387
|
+
local custom_instructions=$(echo "$input" | jq -r '.custom_instructions // ""')
|
|
388
|
+
|
|
389
|
+
debug_log "PreCompact: trigger=$trigger"
|
|
390
|
+
|
|
391
|
+
jq -cn \
|
|
392
|
+
--arg type "pre_compact" \
|
|
393
|
+
--arg sessionId "$SESSION_ID" \
|
|
394
|
+
--arg timestamp "$timestamp" \
|
|
395
|
+
--arg trigger "$trigger" \
|
|
396
|
+
--arg instructions "$custom_instructions" \
|
|
397
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: "Context compaction starting", metadata: {trigger: $trigger, custom_instructions: $instructions}}'
|
|
398
|
+
;;
|
|
399
|
+
esac
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
# Check if this is the first event of this session
|
|
403
|
+
if is_first_event; then
|
|
404
|
+
debug_log "First event of session - sending session_start"
|
|
405
|
+
|
|
406
|
+
# Get tmux info and hostname
|
|
407
|
+
TMUX_INFO=$(get_tmux_info)
|
|
408
|
+
HOSTNAME=$(hostname)
|
|
409
|
+
PROJECT_DIR=$(pwd)
|
|
410
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
411
|
+
|
|
412
|
+
# Send session_start message (compact JSON for NDJSON protocol)
|
|
413
|
+
SESSION_START=$(jq -cn \
|
|
414
|
+
--arg type "session_start" \
|
|
415
|
+
--arg sessionId "$SESSION_ID" \
|
|
416
|
+
--arg timestamp "$TIMESTAMP" \
|
|
417
|
+
--arg hostname "$HOSTNAME" \
|
|
418
|
+
--arg projectDir "$PROJECT_DIR" \
|
|
419
|
+
--argjson tmux "$TMUX_INFO" \
|
|
420
|
+
'{type: $type, sessionId: $sessionId, timestamp: $timestamp, content: "Claude Code session started", metadata: ({hostname: $hostname, projectDir: $projectDir} + $tmux)}')
|
|
421
|
+
|
|
422
|
+
send_to_bridge "$SESSION_START"
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# Clear session tracking on Stop event
|
|
426
|
+
# Note: We keep the session active even after Stop because Notification events
|
|
427
|
+
# may come after Stop and should still go to the same session thread
|
|
428
|
+
if [[ "$HOOK_TYPE" == "Stop" ]]; then
|
|
429
|
+
debug_log "Stop event received (keeping session for potential follow-up events)"
|
|
430
|
+
# Don't clear immediately - let the session timeout naturally or clear on next UserPromptSubmit
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
# Format and send message(s)
|
|
434
|
+
if [[ -n "$HOOK_TYPE" ]]; then
|
|
435
|
+
# format_message may return multiple JSON lines (e.g., Stop returns responses + session_end)
|
|
436
|
+
MESSAGES=$(format_message "$HOOK_TYPE" "$INPUT")
|
|
437
|
+
|
|
438
|
+
if [[ -n "$MESSAGES" ]]; then
|
|
439
|
+
# Send each line as a separate message
|
|
440
|
+
while IFS= read -r msg; do
|
|
441
|
+
if [[ -n "$msg" ]]; then
|
|
442
|
+
send_to_bridge "$msg"
|
|
443
|
+
fi
|
|
444
|
+
done <<< "$MESSAGES"
|
|
445
|
+
fi
|
|
446
|
+
fi
|
|
447
|
+
|
|
448
|
+
# Pass through original input for Claude Code
|
|
449
|
+
echo "$INPUT"
|