telegram-claude-mcp 2.0.2 → 2.0.4

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/bin/daemon-ctl.js CHANGED
@@ -76,12 +76,13 @@ async function start() {
76
76
 
77
77
  // Determine which script to run
78
78
  let script = daemonScript;
79
- let runner = 'node';
79
+ let args = [script];
80
80
 
81
81
  if (!existsSync(daemonScript)) {
82
82
  if (existsSync(daemonSrc)) {
83
83
  script = daemonSrc;
84
- runner = 'bun';
84
+ // Use node with tsx for TypeScript support
85
+ args = ['--import', 'tsx', script];
85
86
  } else {
86
87
  console.error('Error: Daemon script not found. Run `npm run build` first.');
87
88
  process.exit(1);
@@ -90,7 +91,7 @@ async function start() {
90
91
 
91
92
  console.log('Starting telegram-claude-daemon...');
92
93
 
93
- const child = spawn(runner, [script], {
94
+ const child = spawn('node', args, {
94
95
  detached: true,
95
96
  stdio: ['ignore', 'pipe', 'pipe'],
96
97
  env: process.env,
package/bin/daemon.js CHANGED
File without changes
package/bin/proxy.js CHANGED
File without changes
package/bin/setup.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Setup script for telegram-claude-mcp hooks integration
4
+ * Setup script for telegram-claude-mcp hooks integration (v2 - Daemon Architecture)
5
5
  *
6
6
  * Usage: npx telegram-claude-mcp setup
7
7
  *
@@ -22,198 +22,101 @@ const CLAUDE_DIR = join(homedir(), '.claude');
22
22
  const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
23
23
  const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
24
24
 
25
- // Hook scripts content - with SESSION_NAME support for multi-session setups
26
- const PERMISSION_HOOK = `#!/bin/bash
27
- #
28
- # Claude Code Permission Hook - forwards permission requests to Telegram
29
- # Set SESSION_NAME env var to target a specific session (for multi-session setups)
30
- #
31
-
32
- SESSION_DIR="/tmp/telegram-claude-sessions"
33
-
34
- find_session() {
35
- if [ -n "$SESSION_NAME" ]; then
36
- local specific_file="$SESSION_DIR/\${SESSION_NAME}.info"
37
- if [ -f "$specific_file" ]; then
38
- local pid
39
- pid=$(jq -r '.pid // empty' "$specific_file" 2>/dev/null)
40
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
41
- echo "$specific_file"
42
- return
43
- fi
44
- fi
45
- return
46
- fi
25
+ // Default daemon configuration
26
+ const DAEMON_PORT = 3333;
27
+ const DAEMON_HOST = 'localhost';
47
28
 
48
- local latest_file=""
49
- local latest_time=0
50
- [ -d "$SESSION_DIR" ] || return
51
-
52
- for info_file in "$SESSION_DIR"/*.info; do
53
- [ -e "$info_file" ] || continue
54
- local pid
55
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
56
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
57
- local file_time
58
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
59
- if [ "$file_time" -gt "$latest_time" ]; then
60
- latest_time=$file_time
61
- latest_file=$info_file
62
- fi
63
- fi
64
- done
65
- echo "$latest_file"
66
- }
29
+ // Hook scripts content - v2 daemon architecture
30
+ // These hooks communicate with a single daemon process via HTTP
67
31
 
68
- INFO_FILE=$(find_session)
69
- [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
32
+ const PERMISSION_HOOK = `#!/bin/bash
33
+ # Permission hook for telegram-claude-daemon (v2)
34
+ # Sends permission requests to the daemon for Telegram approval
35
+
36
+ DAEMON_PORT="\${TELEGRAM_CLAUDE_PORT:-${DAEMON_PORT}}"
37
+ DAEMON_HOST="\${TELEGRAM_CLAUDE_HOST:-${DAEMON_HOST}}"
38
+ HOOK_URL="http://\${DAEMON_HOST}:\${DAEMON_PORT}/permission"
70
39
 
71
- HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
72
- HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
73
- HOOK_URL="http://\${HOOK_HOST}:\${HOOK_PORT}/permission"
40
+ # Auto-detect project name from current working directory
41
+ PROJECT_NAME=$(basename "$(pwd)")
42
+ BASE_SESSION="\${SESSION_NAME:-default}"
43
+ FULL_SESSION_NAME="\${BASE_SESSION}:\${PROJECT_NAME}"
74
44
 
75
45
  INPUT=$(cat)
76
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
77
- TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
78
46
 
79
- if [ -z "$TOOL_NAME" ]; then
80
- TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName // .name // empty')
81
- TOOL_INPUT=$(echo "$INPUT" | jq -c '.toolInput // .input // .arguments // {}')
82
- fi
47
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .name // empty')
48
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .toolInput // .input // .arguments // {}')
49
+
50
+ [ -z "$TOOL_NAME" ] && exit 0
83
51
 
84
52
  PAYLOAD=$(jq -n \\
85
53
  --arg tool_name "$TOOL_NAME" \\
86
54
  --argjson tool_input "$TOOL_INPUT" \\
87
- '{tool_name: $tool_name, tool_input: $tool_input}')
55
+ --arg session_name "$FULL_SESSION_NAME" \\
56
+ '{tool_name: $tool_name, tool_input: $tool_input, session_name: $session_name}')
88
57
 
89
58
  RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
90
59
  -H "Content-Type: application/json" \\
60
+ -H "X-Session-Name: $FULL_SESSION_NAME" \\
91
61
  -d "$PAYLOAD" \\
92
- --max-time 600)
62
+ --max-time 600 2>/dev/null)
93
63
 
94
- [ $? -eq 0 ] && echo "$RESPONSE"
64
+ [ $? -eq 0 ] && [ -n "$RESPONSE" ] && echo "$RESPONSE"
95
65
  `;
96
66
 
97
67
  const STOP_HOOK = `#!/bin/bash
98
- #
99
- # Claude Code Interactive Stop Hook - sends stop notification and waits for reply
100
- # Set SESSION_NAME env var to target a specific session (for multi-session setups)
101
- #
102
-
103
- SESSION_DIR="/tmp/telegram-claude-sessions"
104
-
105
- find_session() {
106
- if [ -n "$SESSION_NAME" ]; then
107
- local specific_file="$SESSION_DIR/\${SESSION_NAME}.info"
108
- if [ -f "$specific_file" ]; then
109
- local pid
110
- pid=$(jq -r '.pid // empty' "$specific_file" 2>/dev/null)
111
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
112
- echo "$specific_file"
113
- return
114
- fi
115
- fi
116
- return
117
- fi
68
+ # Stop hook for telegram-claude-daemon (v2)
69
+ # Sends stop notification and waits for user response
118
70
 
119
- local latest_file=""
120
- local latest_time=0
121
- [ -d "$SESSION_DIR" ] || return
122
-
123
- for info_file in "$SESSION_DIR"/*.info; do
124
- [ -e "$info_file" ] || continue
125
- local pid
126
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
127
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
128
- local file_time
129
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
130
- if [ "$file_time" -gt "$latest_time" ]; then
131
- latest_time=$file_time
132
- latest_file=$info_file
133
- fi
134
- fi
135
- done
136
- echo "$latest_file"
137
- }
71
+ DAEMON_PORT="\${TELEGRAM_CLAUDE_PORT:-${DAEMON_PORT}}"
72
+ DAEMON_HOST="\${TELEGRAM_CLAUDE_HOST:-${DAEMON_HOST}}"
73
+ HOOK_URL="http://\${DAEMON_HOST}:\${DAEMON_PORT}/stop"
74
+
75
+ # Auto-detect project name from current working directory
76
+ PROJECT_NAME=$(basename "$(pwd)")
77
+ BASE_SESSION="\${SESSION_NAME:-default}"
78
+ FULL_SESSION_NAME="\${BASE_SESSION}:\${PROJECT_NAME}"
138
79
 
139
80
  INPUT=$(cat)
140
81
 
82
+ # Prevent recursion
141
83
  STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
142
84
  [ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
143
85
 
144
- INFO_FILE=$(find_session)
145
- [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
146
-
147
- HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
148
- HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
149
- HOOK_URL="http://\${HOOK_HOST}:\${HOOK_PORT}/stop"
150
-
151
86
  TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
152
87
 
153
88
  PAYLOAD=$(jq -n \\
154
89
  --arg transcript_path "$TRANSCRIPT_PATH" \\
155
- '{transcript_path: $transcript_path}')
90
+ --arg session_name "$FULL_SESSION_NAME" \\
91
+ '{transcript_path: $transcript_path, session_name: $session_name}')
156
92
 
157
93
  RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
158
94
  -H "Content-Type: application/json" \\
95
+ -H "X-Session-Name: $FULL_SESSION_NAME" \\
159
96
  -d "$PAYLOAD" \\
160
- --max-time 300)
97
+ --max-time 300 2>/dev/null)
161
98
 
162
- if [ $? -eq 0 ]; then
99
+ if [ $? -eq 0 ] && [ -n "$RESPONSE" ]; then
163
100
  DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
164
- REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
165
- [ "$DECISION" = "block" ] && [ -n "$REASON" ] && echo "$RESPONSE"
101
+ if [ "$DECISION" = "block" ]; then
102
+ echo "$RESPONSE"
103
+ exit 2
104
+ fi
166
105
  fi
167
106
  `;
168
107
 
169
108
  const NOTIFY_HOOK = `#!/bin/bash
170
- #
171
- # Claude Code Notification Hook - sends notifications to Telegram
172
- # Set SESSION_NAME env var to target a specific session (for multi-session setups)
173
- #
174
-
175
- SESSION_DIR="/tmp/telegram-claude-sessions"
176
-
177
- find_session() {
178
- if [ -n "$SESSION_NAME" ]; then
179
- local specific_file="$SESSION_DIR/\${SESSION_NAME}.info"
180
- if [ -f "$specific_file" ]; then
181
- local pid
182
- pid=$(jq -r '.pid // empty' "$specific_file" 2>/dev/null)
183
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
184
- echo "$specific_file"
185
- return
186
- fi
187
- fi
188
- return
189
- fi
190
-
191
- local latest_file=""
192
- local latest_time=0
193
- [ -d "$SESSION_DIR" ] || return
194
-
195
- for info_file in "$SESSION_DIR"/*.info; do
196
- [ -e "$info_file" ] || continue
197
- local pid
198
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
199
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
200
- local file_time
201
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
202
- if [ "$file_time" -gt "$latest_time" ]; then
203
- latest_time=$file_time
204
- latest_file=$info_file
205
- fi
206
- fi
207
- done
208
- echo "$latest_file"
209
- }
109
+ # Notification hook for telegram-claude-daemon (v2)
110
+ # Sends notifications to the daemon (fire-and-forget)
210
111
 
211
- INFO_FILE=$(find_session)
212
- [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
112
+ DAEMON_PORT="\${TELEGRAM_CLAUDE_PORT:-${DAEMON_PORT}}"
113
+ DAEMON_HOST="\${TELEGRAM_CLAUDE_HOST:-${DAEMON_HOST}}"
114
+ HOOK_URL="http://\${DAEMON_HOST}:\${DAEMON_PORT}/notify"
213
115
 
214
- HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
215
- HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
216
- HOOK_URL="http://\${HOOK_HOST}:\${HOOK_PORT}/notify"
116
+ # Auto-detect project name from current working directory
117
+ PROJECT_NAME=$(basename "$(pwd)")
118
+ BASE_SESSION="\${SESSION_NAME:-default}"
119
+ FULL_SESSION_NAME="\${BASE_SESSION}:\${PROJECT_NAME}"
217
120
 
218
121
  INPUT=$(cat)
219
122
  MESSAGE=$(echo "$INPUT" | jq -r '.message // "Notification from Claude"')
@@ -221,12 +124,137 @@ MESSAGE=$(echo "$INPUT" | jq -r '.message // "Notification from Claude"')
221
124
  PAYLOAD=$(jq -n \\
222
125
  --arg type "notification" \\
223
126
  --arg message "$MESSAGE" \\
224
- '{type: $type, message: $message}')
127
+ --arg session_name "$FULL_SESSION_NAME" \\
128
+ '{type: $type, message: $message, session_name: $session_name}')
225
129
 
226
130
  curl -s -X POST "$HOOK_URL" \\
227
131
  -H "Content-Type: application/json" \\
132
+ -H "X-Session-Name: $FULL_SESSION_NAME" \\
133
+ -d "$PAYLOAD" \\
134
+ --max-time 10 >/dev/null 2>&1
135
+ `;
136
+
137
+ // Progress tracking hooks - fire-and-forget for real-time progress updates
138
+
139
+ const PROGRESS_PRE_TOOL_HOOK = `#!/bin/bash
140
+ # PreToolUse progress hook for telegram-claude-daemon
141
+ # Sends tool start events to update progress display (fire-and-forget)
142
+
143
+ DAEMON_PORT="\${TELEGRAM_CLAUDE_PORT:-${DAEMON_PORT}}"
144
+ DAEMON_HOST="\${TELEGRAM_CLAUDE_HOST:-${DAEMON_HOST}}"
145
+ HOOK_URL="http://\${DAEMON_HOST}:\${DAEMON_PORT}/progress"
146
+
147
+ # Auto-detect project name from current working directory
148
+ PROJECT_NAME=$(basename "$(pwd)")
149
+ BASE_SESSION="\${SESSION_NAME:-default}"
150
+ FULL_SESSION_NAME="\${BASE_SESSION}:\${PROJECT_NAME}"
151
+
152
+ INPUT=$(cat)
153
+
154
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .name // empty')
155
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .toolInput // .input // .arguments // {}')
156
+
157
+ [ -z "$TOOL_NAME" ] && exit 0
158
+
159
+ # Skip progress for Telegram tools (would be recursive)
160
+ case "$TOOL_NAME" in
161
+ mcp__telegram__*|send_message|continue_chat|notify_user|end_chat)
162
+ exit 0
163
+ ;;
164
+ esac
165
+
166
+ PAYLOAD=$(jq -n \\
167
+ --arg type "pre_tool" \\
168
+ --arg session_name "$FULL_SESSION_NAME" \\
169
+ --arg tool_name "$TOOL_NAME" \\
170
+ --argjson tool_input "$TOOL_INPUT" \\
171
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
172
+ '{type: $type, session_name: $session_name, tool_name: $tool_name, tool_input: $tool_input, timestamp: $timestamp}')
173
+
174
+ # Fire and forget
175
+ (curl -s -X POST "$HOOK_URL" \\
176
+ -H "Content-Type: application/json" \\
177
+ -H "X-Session-Name: $FULL_SESSION_NAME" \\
178
+ -d "$PAYLOAD" \\
179
+ --max-time 2 2>/dev/null) &
180
+
181
+ exit 0
182
+ `;
183
+
184
+ const PROGRESS_POST_TOOL_HOOK = `#!/bin/bash
185
+ # PostToolUse progress hook for telegram-claude-daemon
186
+ # Sends tool completion events to update progress display (fire-and-forget)
187
+
188
+ DAEMON_PORT="\${TELEGRAM_CLAUDE_PORT:-${DAEMON_PORT}}"
189
+ DAEMON_HOST="\${TELEGRAM_CLAUDE_HOST:-${DAEMON_HOST}}"
190
+ HOOK_URL="http://\${DAEMON_HOST}:\${DAEMON_PORT}/progress"
191
+
192
+ # Auto-detect project name from current working directory
193
+ PROJECT_NAME=$(basename "$(pwd)")
194
+ BASE_SESSION="\${SESSION_NAME:-default}"
195
+ FULL_SESSION_NAME="\${BASE_SESSION}:\${PROJECT_NAME}"
196
+
197
+ INPUT=$(cat)
198
+
199
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .name // empty')
200
+
201
+ IS_ERROR=$(echo "$INPUT" | jq -r '.is_error // .isError // false')
202
+ [ "$IS_ERROR" = "true" ] && SUCCESS="false" || SUCCESS="true"
203
+
204
+ [ -z "$TOOL_NAME" ] && exit 0
205
+
206
+ # Skip progress for Telegram tools
207
+ case "$TOOL_NAME" in
208
+ mcp__telegram__*|send_message|continue_chat|notify_user|end_chat)
209
+ exit 0
210
+ ;;
211
+ esac
212
+
213
+ PAYLOAD=$(jq -n \\
214
+ --arg type "post_tool" \\
215
+ --arg session_name "$FULL_SESSION_NAME" \\
216
+ --arg tool_name "$TOOL_NAME" \\
217
+ --argjson success "$SUCCESS" \\
218
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
219
+ '{type: $type, session_name: $session_name, tool_name: $tool_name, success: $success, timestamp: $timestamp}')
220
+
221
+ # Fire and forget
222
+ (curl -s -X POST "$HOOK_URL" \\
223
+ -H "Content-Type: application/json" \\
224
+ -H "X-Session-Name: $FULL_SESSION_NAME" \\
228
225
  -d "$PAYLOAD" \\
229
- --max-time 10 > /dev/null 2>&1
226
+ --max-time 2 2>/dev/null) &
227
+
228
+ exit 0
229
+ `;
230
+
231
+ const PROGRESS_STOP_HOOK = `#!/bin/bash
232
+ # Stop progress hook for telegram-claude-daemon
233
+ # Sends stop event to finalize progress display (fire-and-forget)
234
+
235
+ DAEMON_PORT="\${TELEGRAM_CLAUDE_PORT:-${DAEMON_PORT}}"
236
+ DAEMON_HOST="\${TELEGRAM_CLAUDE_HOST:-${DAEMON_HOST}}"
237
+ HOOK_URL="http://\${DAEMON_HOST}:\${DAEMON_PORT}/progress"
238
+
239
+ # Auto-detect project name from current working directory
240
+ PROJECT_NAME=$(basename "$(pwd)")
241
+ BASE_SESSION="\${SESSION_NAME:-default}"
242
+ FULL_SESSION_NAME="\${BASE_SESSION}:\${PROJECT_NAME}"
243
+
244
+ PAYLOAD=$(jq -n \\
245
+ --arg type "stop" \\
246
+ --arg session_name "$FULL_SESSION_NAME" \\
247
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
248
+ '{type: $type, session_name: $session_name, timestamp: $timestamp}')
249
+
250
+ # Fire and forget
251
+ (curl -s -X POST "$HOOK_URL" \\
252
+ -H "Content-Type: application/json" \\
253
+ -H "X-Session-Name: $FULL_SESSION_NAME" \\
254
+ -d "$PAYLOAD" \\
255
+ --max-time 2 2>/dev/null) &
256
+
257
+ exit 0
230
258
  `;
231
259
 
232
260
  // Generate hooks configuration for Claude settings
@@ -234,6 +262,7 @@ curl -s -X POST "$HOOK_URL" \\
234
262
  function generateHooksConfig(sessionName = 'default') {
235
263
  const envPrefix = `SESSION_NAME=${sessionName}`;
236
264
  return {
265
+ // Permission request handling
237
266
  PermissionRequest: [
238
267
  {
239
268
  matcher: '*',
@@ -245,17 +274,23 @@ function generateHooksConfig(sessionName = 'default') {
245
274
  ]
246
275
  }
247
276
  ],
277
+ // Stop hook with interactive continue option
248
278
  Stop: [
249
279
  {
250
280
  matcher: '*',
251
281
  hooks: [
252
282
  {
253
283
  type: 'command',
254
- command: `${envPrefix} ~/.claude/hooks/stop-hook.sh`
284
+ command: `${envPrefix} ~/.claude/hooks/progress-stop-hook.sh` // Progress finalization
285
+ },
286
+ {
287
+ type: 'command',
288
+ command: `${envPrefix} ~/.claude/hooks/stop-hook.sh` // Interactive stop
255
289
  }
256
290
  ]
257
291
  }
258
292
  ],
293
+ // Notification forwarding
259
294
  Notification: [
260
295
  {
261
296
  matcher: '*',
@@ -266,6 +301,30 @@ function generateHooksConfig(sessionName = 'default') {
266
301
  }
267
302
  ]
268
303
  }
304
+ ],
305
+ // Progress tracking - PreToolUse
306
+ PreToolUse: [
307
+ {
308
+ matcher: '*',
309
+ hooks: [
310
+ {
311
+ type: 'command',
312
+ command: `${envPrefix} ~/.claude/hooks/progress-pre-tool-hook.sh`
313
+ }
314
+ ]
315
+ }
316
+ ],
317
+ // Progress tracking - PostToolUse
318
+ PostToolUse: [
319
+ {
320
+ matcher: '*',
321
+ hooks: [
322
+ {
323
+ type: 'command',
324
+ command: `${envPrefix} ~/.claude/hooks/progress-post-tool-hook.sh`
325
+ }
326
+ ]
327
+ }
269
328
  ]
270
329
  };
271
330
  }
@@ -280,7 +339,7 @@ function setup() {
280
339
  console.log('✓ Created ~/.claude/hooks/');
281
340
  }
282
341
 
283
- // Write hook scripts
342
+ // Write hook scripts - core hooks
284
343
  const permissionHookPath = join(HOOKS_DIR, 'permission-hook.sh');
285
344
  writeFileSync(permissionHookPath, PERMISSION_HOOK);
286
345
  chmodSync(permissionHookPath, 0o755);
@@ -296,6 +355,22 @@ function setup() {
296
355
  chmodSync(notifyHookPath, 0o755);
297
356
  console.log('✓ Installed notify-hook.sh');
298
357
 
358
+ // Write hook scripts - progress tracking
359
+ const progressPreToolPath = join(HOOKS_DIR, 'progress-pre-tool-hook.sh');
360
+ writeFileSync(progressPreToolPath, PROGRESS_PRE_TOOL_HOOK);
361
+ chmodSync(progressPreToolPath, 0o755);
362
+ console.log('✓ Installed progress-pre-tool-hook.sh');
363
+
364
+ const progressPostToolPath = join(HOOKS_DIR, 'progress-post-tool-hook.sh');
365
+ writeFileSync(progressPostToolPath, PROGRESS_POST_TOOL_HOOK);
366
+ chmodSync(progressPostToolPath, 0o755);
367
+ console.log('✓ Installed progress-post-tool-hook.sh');
368
+
369
+ const progressStopPath = join(HOOKS_DIR, 'progress-stop-hook.sh');
370
+ writeFileSync(progressStopPath, PROGRESS_STOP_HOOK);
371
+ chmodSync(progressStopPath, 0o755);
372
+ console.log('✓ Installed progress-stop-hook.sh');
373
+
299
374
  // Update Claude settings
300
375
  let settings = {};
301
376
  if (existsSync(SETTINGS_FILE)) {
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "_comment": "Example Claude Code hooks configuration for progress tracking. Copy relevant sections to ~/.claude/settings.json or .claude/settings.json in your project.",
4
+
5
+ "hooks": {
6
+ "PreToolUse": [
7
+ {
8
+ "matcher": "*",
9
+ "command": "SESSION_NAME=${SESSION_NAME:-$(basename \"$PWD\")} /path/to/call-me/server/hooks-v2/progress-pre-tool.sh"
10
+ }
11
+ ],
12
+ "PostToolUse": [
13
+ {
14
+ "matcher": "*",
15
+ "command": "SESSION_NAME=${SESSION_NAME:-$(basename \"$PWD\")} /path/to/call-me/server/hooks-v2/progress-post-tool.sh"
16
+ }
17
+ ],
18
+ "Stop": [
19
+ {
20
+ "command": "SESSION_NAME=${SESSION_NAME:-$(basename \"$PWD\")} /path/to/call-me/server/hooks-v2/progress-stop.sh"
21
+ },
22
+ {
23
+ "_comment": "Optional: Interactive stop hook (waits for user response)",
24
+ "command": "SESSION_NAME=${SESSION_NAME:-$(basename \"$PWD\")} /path/to/call-me/server/hooks-v2/stop-hook.sh"
25
+ }
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # PostToolUse progress hook for telegram-claude-daemon
3
+ # Sends tool completion events to update progress display
4
+ #
5
+ # This hook is fire-and-forget - it doesn't block Claude's execution
6
+
7
+ DAEMON_PORT="${TELEGRAM_CLAUDE_PORT:-3333}"
8
+ DAEMON_HOST="${TELEGRAM_CLAUDE_HOST:-localhost}"
9
+ HOOK_URL="http://${DAEMON_HOST}:${DAEMON_PORT}/progress"
10
+
11
+ # Read input from stdin
12
+ INPUT=$(cat)
13
+
14
+ # Extract tool info
15
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .name // empty')
16
+ TOOL_RESULT=$(echo "$INPUT" | jq -c '.tool_result // .result // {}')
17
+
18
+ # Check if tool succeeded
19
+ IS_ERROR=$(echo "$INPUT" | jq -r '.is_error // .isError // false')
20
+ if [ "$IS_ERROR" = "true" ]; then
21
+ SUCCESS="false"
22
+ else
23
+ SUCCESS="true"
24
+ fi
25
+
26
+ # Skip if no tool name
27
+ if [ -z "$TOOL_NAME" ]; then
28
+ exit 0
29
+ fi
30
+
31
+ # Skip progress updates for Telegram tools (would be recursive)
32
+ case "$TOOL_NAME" in
33
+ mcp__telegram__*|send_message|continue_chat|notify_user|end_chat)
34
+ exit 0
35
+ ;;
36
+ esac
37
+
38
+ # Build payload
39
+ PAYLOAD=$(jq -n \
40
+ --arg type "post_tool" \
41
+ --arg session_name "${SESSION_NAME:-default}" \
42
+ --arg tool_name "$TOOL_NAME" \
43
+ --argjson success "$SUCCESS" \
44
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
45
+ '{
46
+ type: $type,
47
+ session_name: $session_name,
48
+ tool_name: $tool_name,
49
+ success: $success,
50
+ timestamp: $timestamp
51
+ }')
52
+
53
+ # Fire and forget - send in background and don't wait
54
+ (curl -s -X POST "$HOOK_URL" \
55
+ -H "Content-Type: application/json" \
56
+ -H "X-Session-Name: ${SESSION_NAME:-default}" \
57
+ -d "$PAYLOAD" \
58
+ --max-time 2 2>/dev/null) &
59
+
60
+ # Exit immediately - don't block Claude
61
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # PreToolUse progress hook for telegram-claude-daemon
3
+ # Sends tool start events to update progress display
4
+ #
5
+ # This hook is fire-and-forget - it doesn't block Claude's execution
6
+
7
+ DAEMON_PORT="${TELEGRAM_CLAUDE_PORT:-3333}"
8
+ DAEMON_HOST="${TELEGRAM_CLAUDE_HOST:-localhost}"
9
+ HOOK_URL="http://${DAEMON_HOST}:${DAEMON_PORT}/progress"
10
+
11
+ # Read input from stdin
12
+ INPUT=$(cat)
13
+
14
+ # Extract tool info
15
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .name // empty')
16
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .toolInput // .input // .arguments // {}')
17
+
18
+ # Skip if no tool name
19
+ if [ -z "$TOOL_NAME" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ # Skip progress updates for Telegram tools (would be recursive)
24
+ case "$TOOL_NAME" in
25
+ mcp__telegram__*|send_message|continue_chat|notify_user|end_chat)
26
+ exit 0
27
+ ;;
28
+ esac
29
+
30
+ # Build payload
31
+ PAYLOAD=$(jq -n \
32
+ --arg type "pre_tool" \
33
+ --arg session_name "${SESSION_NAME:-default}" \
34
+ --arg tool_name "$TOOL_NAME" \
35
+ --argjson tool_input "$TOOL_INPUT" \
36
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
37
+ '{
38
+ type: $type,
39
+ session_name: $session_name,
40
+ tool_name: $tool_name,
41
+ tool_input: $tool_input,
42
+ timestamp: $timestamp
43
+ }')
44
+
45
+ # Fire and forget - send in background and don't wait
46
+ (curl -s -X POST "$HOOK_URL" \
47
+ -H "Content-Type: application/json" \
48
+ -H "X-Session-Name: ${SESSION_NAME:-default}" \
49
+ -d "$PAYLOAD" \
50
+ --max-time 2 2>/dev/null) &
51
+
52
+ # Exit immediately - don't block Claude
53
+ exit 0
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ # Stop progress hook for telegram-claude-daemon
3
+ # Sends stop events to finalize progress display
4
+ #
5
+ # This hook is fire-and-forget - it doesn't block Claude's execution
6
+ # Note: This is separate from stop-hook.sh which handles interactive stop
7
+
8
+ DAEMON_PORT="${TELEGRAM_CLAUDE_PORT:-3333}"
9
+ DAEMON_HOST="${TELEGRAM_CLAUDE_HOST:-localhost}"
10
+ HOOK_URL="http://${DAEMON_HOST}:${DAEMON_PORT}/progress"
11
+
12
+ # Read input from stdin (if any)
13
+ INPUT=$(cat)
14
+
15
+ # Build payload
16
+ PAYLOAD=$(jq -n \
17
+ --arg type "stop" \
18
+ --arg session_name "${SESSION_NAME:-default}" \
19
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
20
+ '{
21
+ type: $type,
22
+ session_name: $session_name,
23
+ timestamp: $timestamp
24
+ }')
25
+
26
+ # Fire and forget - send in background and don't wait
27
+ (curl -s -X POST "$HOOK_URL" \
28
+ -H "Content-Type: application/json" \
29
+ -H "X-Session-Name: ${SESSION_NAME:-default}" \
30
+ -d "$PAYLOAD" \
31
+ --max-time 2 2>/dev/null) &
32
+
33
+ # Exit immediately - don't block Claude
34
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telegram-claude-mcp",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "MCP server that lets Claude message you on Telegram with hooks support",
5
5
  "author": "Geravant",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@ import { existsSync, unlinkSync, writeFileSync, mkdirSync } from 'fs';
13
13
  import { dirname } from 'path';
14
14
  import { SessionManager } from './session-manager.js';
15
15
  import { MultiTelegramManager, type HookEvent } from './telegram-multi.js';
16
+ import { ProgressTracker, type ProgressEvent } from './progress-tracker.js';
16
17
  import {
17
18
  DAEMON_SOCKET_PATH,
18
19
  DAEMON_PID_FILE,
@@ -96,6 +97,16 @@ async function main() {
96
97
 
97
98
  telegram.start();
98
99
 
100
+ // Create progress tracker
101
+ const progressTracker = new ProgressTracker(
102
+ telegram.getBot(),
103
+ telegram.getChatId(),
104
+ sessionManager
105
+ );
106
+
107
+ // Register progress tracker's callback handler for mute buttons
108
+ telegram.registerCallbackInterceptor((query) => progressTracker.handleCallback(query));
109
+
99
110
  // Track socket buffers for each connection
100
111
  const socketBuffers = new Map<net.Socket, string>();
101
112
 
@@ -223,6 +234,41 @@ async function main() {
223
234
  return;
224
235
  }
225
236
 
237
+ // Progress tracking endpoint
238
+ if (url === '/progress' || url === '/hooks/progress') {
239
+ const event = data as ProgressEvent;
240
+
241
+ console.error(`[HTTP] Progress: ${event.type} - ${event.tool_name || ''} (session: ${sessionName})`);
242
+
243
+ try {
244
+ switch (event.type) {
245
+ case 'start':
246
+ await progressTracker.handleSessionStart(sessionName);
247
+ break;
248
+ case 'pre_tool':
249
+ await progressTracker.handlePreTool(sessionName, event.tool_name!, event.tool_input);
250
+ break;
251
+ case 'post_tool':
252
+ await progressTracker.handlePostTool(sessionName, event.tool_name!, event.success ?? true);
253
+ break;
254
+ case 'stop':
255
+ await progressTracker.handleStop(sessionName);
256
+ break;
257
+ case 'notification':
258
+ await progressTracker.handleNotification(sessionName, event.tool_name || '');
259
+ break;
260
+ }
261
+
262
+ res.writeHead(200, { 'Content-Type': 'application/json' });
263
+ res.end(JSON.stringify({ ok: true }));
264
+ } catch (error) {
265
+ console.error('[HTTP] Progress error:', error);
266
+ res.writeHead(200, { 'Content-Type': 'application/json' });
267
+ res.end(JSON.stringify({ ok: true })); // Don't fail hooks
268
+ }
269
+ return;
270
+ }
271
+
226
272
  // Status endpoint
227
273
  if (url === '/status') {
228
274
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -284,9 +330,10 @@ async function main() {
284
330
  process.on('SIGINT', shutdown);
285
331
  process.on('SIGTERM', shutdown);
286
332
 
287
- // Periodic cleanup of dead sessions
333
+ // Periodic cleanup of dead sessions and progress state
288
334
  setInterval(() => {
289
335
  sessionManager.cleanup();
336
+ progressTracker.cleanup();
290
337
  }, 30000);
291
338
  }
292
339
 
@@ -54,6 +54,7 @@ export class MultiTelegramManager {
54
54
  private pendingPermissions: Map<string, PendingPermission> = new Map();
55
55
  private messageToSession: Map<number, string> = new Map(); // messageId -> sessionId
56
56
  private isRunning = false;
57
+ private callbackInterceptors: Array<(query: TelegramBot.CallbackQuery) => Promise<boolean>> = [];
57
58
 
58
59
  constructor(config: MultiTelegramConfig, sessionManager: SessionManager) {
59
60
  this.config = {
@@ -68,6 +69,28 @@ export class MultiTelegramManager {
68
69
  this.setupCallbackHandler();
69
70
  }
70
71
 
72
+ /**
73
+ * Get the underlying bot instance for advanced operations.
74
+ */
75
+ getBot(): TelegramBot {
76
+ return this.bot;
77
+ }
78
+
79
+ /**
80
+ * Get the configured chat ID.
81
+ */
82
+ getChatId(): number {
83
+ return this.config.chatId;
84
+ }
85
+
86
+ /**
87
+ * Register a callback interceptor that handles callback queries before the default handler.
88
+ * Return true to indicate the query was handled and stop further processing.
89
+ */
90
+ registerCallbackInterceptor(handler: (query: TelegramBot.CallbackQuery) => Promise<boolean>): void {
91
+ this.callbackInterceptors.push(handler);
92
+ }
93
+
71
94
  start(): void {
72
95
  this.isRunning = true;
73
96
  console.error('[MultiTelegram] Bot started');
@@ -544,6 +567,16 @@ export class MultiTelegramManager {
544
567
  this.bot.on('callback_query', async (query) => {
545
568
  if (!query.data || !query.message) return;
546
569
 
570
+ // Check interceptors first
571
+ for (const interceptor of this.callbackInterceptors) {
572
+ try {
573
+ const handled = await interceptor(query);
574
+ if (handled) return;
575
+ } catch (error) {
576
+ console.error('[MultiTelegram] Callback interceptor error:', error);
577
+ }
578
+ }
579
+
547
580
  const [action, messageId] = query.data.split(':');
548
581
  const key = messageId;
549
582