telegram-claude-mcp 2.0.1 → 2.0.3

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
File without changes
package/bin/daemon.js CHANGED
File without changes
package/bin/proxy.js CHANGED
File without changes
package/bin/setup.js CHANGED
@@ -162,7 +162,11 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
162
162
  if [ $? -eq 0 ]; then
163
163
  DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
164
164
  REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
165
- [ "$DECISION" = "block" ] && [ -n "$REASON" ] && echo "$RESPONSE"
165
+ if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
166
+ echo "$RESPONSE"
167
+ # Exit code 2 tells Claude Code to continue with the reason as instructions
168
+ exit 2
169
+ fi
166
170
  fi
167
171
  `;
168
172
 
@@ -108,6 +108,8 @@ REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
108
108
  if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
109
109
  # User provided instructions - continue with them
110
110
  echo "$RESPONSE"
111
+ # Exit code 2 tells Claude Code to continue with the reason as instructions
112
+ exit 2
111
113
  else
112
114
  # User said done or no response - allow stop
113
115
  exit 0
@@ -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
@@ -41,5 +41,7 @@ if [ $? -eq 0 ] && [ -n "$RESPONSE" ]; then
41
41
  DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
42
42
  if [ "$DECISION" = "block" ]; then
43
43
  echo "$RESPONSE"
44
+ # Exit code 2 tells Claude Code to continue with the reason as instructions
45
+ exit 2
44
46
  fi
45
47
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telegram-claude-mcp",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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
 
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Progress Display Utilities
3
+ *
4
+ * Visual rendering for progress tracking in Telegram messages.
5
+ * Inspired by DnD-Books telegram-bot ProgressIndicator.
6
+ */
7
+
8
+ export enum ProgressStage {
9
+ IDLE = 'idle',
10
+ THINKING = 'thinking',
11
+ TOOL_QUEUED = 'queued',
12
+ TOOL_RUNNING = 'running',
13
+ TOOL_COMPLETE = 'complete',
14
+ WAITING_USER = 'waiting',
15
+ FINISHED = 'finished',
16
+ }
17
+
18
+ interface StageDisplay {
19
+ emoji: string;
20
+ label: string;
21
+ progress: number;
22
+ }
23
+
24
+ export const stageDisplay: Record<ProgressStage, StageDisplay> = {
25
+ [ProgressStage.IDLE]: { emoji: '💤', label: 'Idle', progress: 0 },
26
+ [ProgressStage.THINKING]: { emoji: '🧠', label: 'Thinking', progress: 10 },
27
+ [ProgressStage.TOOL_QUEUED]: { emoji: '📋', label: 'Tool queued', progress: 20 },
28
+ [ProgressStage.TOOL_RUNNING]: { emoji: '🔧', label: 'Executing', progress: 50 },
29
+ [ProgressStage.TOOL_COMPLETE]: { emoji: '✅', label: 'Tool done', progress: 80 },
30
+ [ProgressStage.WAITING_USER]: { emoji: '💬', label: 'Waiting for you', progress: 90 },
31
+ [ProgressStage.FINISHED]: { emoji: '🏁', label: 'Finished', progress: 100 },
32
+ };
33
+
34
+ /**
35
+ * Build a visual progress bar using block characters.
36
+ */
37
+ export function buildProgressBar(percent: number): string {
38
+ const total = 10;
39
+ const filled = Math.floor(percent / 10);
40
+ const empty = total - filled;
41
+
42
+ const filledChar = '▓';
43
+ const emptyChar = '░';
44
+
45
+ return `[${filledChar.repeat(filled)}${emptyChar.repeat(empty)}] ${percent}%`;
46
+ }
47
+
48
+ /**
49
+ * Format tool name for display.
50
+ */
51
+ export function formatToolName(toolName: string): string {
52
+ // Shorten common tool names
53
+ const shortNames: Record<string, string> = {
54
+ 'Read': 'Read',
55
+ 'Write': 'Write',
56
+ 'Edit': 'Edit',
57
+ 'Bash': 'Bash',
58
+ 'Glob': 'Glob',
59
+ 'Grep': 'Grep',
60
+ 'Task': 'Task',
61
+ 'WebFetch': 'Web',
62
+ 'WebSearch': 'Search',
63
+ 'TodoWrite': 'Todo',
64
+ 'AskUserQuestion': 'Ask',
65
+ };
66
+
67
+ return shortNames[toolName] || toolName;
68
+ }
69
+
70
+ /**
71
+ * Format elapsed time for display.
72
+ */
73
+ export function formatElapsedTime(startTime: number): string {
74
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
75
+
76
+ if (elapsed < 60) {
77
+ return `${elapsed}s`;
78
+ }
79
+
80
+ const minutes = Math.floor(elapsed / 60);
81
+ const seconds = elapsed % 60;
82
+ return `${minutes}m ${seconds}s`;
83
+ }
84
+
85
+ /**
86
+ * Build tool statistics summary.
87
+ */
88
+ export function buildToolStats(toolCounts: Map<string, number>): string {
89
+ if (toolCounts.size === 0) return '';
90
+
91
+ const entries = Array.from(toolCounts.entries())
92
+ .sort((a, b) => b[1] - a[1])
93
+ .slice(0, 5) // Top 5 tools
94
+ .map(([tool, count]) => `${formatToolName(tool)} (${count})`)
95
+ .join(', ');
96
+
97
+ return entries;
98
+ }
99
+
100
+ export interface ProgressMessageOptions {
101
+ sessionName: string;
102
+ stage: ProgressStage;
103
+ currentTool: string | null;
104
+ toolStack: string[];
105
+ startTime: number;
106
+ toolsExecuted: number;
107
+ toolCounts: Map<string, number>;
108
+ showDetails?: boolean;
109
+ }
110
+
111
+ /**
112
+ * Build the complete progress message text.
113
+ */
114
+ export function buildProgressMessage(options: ProgressMessageOptions): string {
115
+ const {
116
+ sessionName,
117
+ stage,
118
+ currentTool,
119
+ toolStack,
120
+ startTime,
121
+ toolsExecuted,
122
+ toolCounts,
123
+ showDetails = true,
124
+ } = options;
125
+
126
+ const display = stageDisplay[stage];
127
+ const elapsed = formatElapsedTime(startTime);
128
+
129
+ let text = `[${sessionName}] Working...\n\n`;
130
+
131
+ // Current stage with emoji
132
+ text += `${display.emoji} *${display.label}*`;
133
+
134
+ // Current tool (if any)
135
+ if (currentTool) {
136
+ const toolDisplay = toolStack.length > 1
137
+ ? toolStack.map(formatToolName).join(' > ')
138
+ : formatToolName(currentTool);
139
+ text += `: \`${toolDisplay}\``;
140
+ }
141
+ text += '\n\n';
142
+
143
+ // Progress bar
144
+ text += buildProgressBar(display.progress) + '\n\n';
145
+
146
+ // Stats line
147
+ text += `⏱ ${elapsed}`;
148
+ if (toolsExecuted > 0) {
149
+ text += ` | Tools: ${toolsExecuted}`;
150
+ }
151
+
152
+ // Detailed tool breakdown (for finished state)
153
+ if (showDetails && stage === ProgressStage.FINISHED && toolCounts.size > 0) {
154
+ text += '\n\n---\n';
155
+ text += `Tools used: ${buildToolStats(toolCounts)}`;
156
+ }
157
+
158
+ return text;
159
+ }
160
+
161
+ /**
162
+ * Build a completion message.
163
+ */
164
+ export function buildCompletionMessage(options: {
165
+ sessionName: string;
166
+ startTime: number;
167
+ toolsExecuted: number;
168
+ toolCounts: Map<string, number>;
169
+ }): string {
170
+ const { sessionName, startTime, toolsExecuted, toolCounts } = options;
171
+ const elapsed = formatElapsedTime(startTime);
172
+
173
+ let text = `[${sessionName}] Task Complete\n\n`;
174
+ text += `🏁 *Finished*\n\n`;
175
+ text += buildProgressBar(100) + '\n\n';
176
+ text += `⏱ ${elapsed} | Tools: ${toolsExecuted}`;
177
+
178
+ if (toolCounts.size > 0) {
179
+ text += '\n\n---\n';
180
+ text += `Tools used: ${buildToolStats(toolCounts)}`;
181
+ }
182
+
183
+ return text;
184
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Progress Tracker
3
+ *
4
+ * Manages per-session progress state and updates Telegram messages
5
+ * in-place using editMessageText.
6
+ */
7
+
8
+ import TelegramBot from 'node-telegram-bot-api';
9
+ import type { SessionManager } from './session-manager.js';
10
+ import {
11
+ ProgressStage,
12
+ buildProgressMessage,
13
+ buildCompletionMessage,
14
+ } from './progress-display.js';
15
+
16
+ export interface SessionProgress {
17
+ sessionId: string;
18
+ sessionName: string;
19
+
20
+ // Message tracking
21
+ progressMessageId: number | null;
22
+ lastUpdateTime: number;
23
+
24
+ // Progress state
25
+ stage: ProgressStage;
26
+ currentTool: string | null;
27
+ toolStack: string[];
28
+
29
+ // Statistics
30
+ startTime: number;
31
+ toolsExecuted: number;
32
+ toolCounts: Map<string, number>;
33
+
34
+ // Settings
35
+ isActive: boolean;
36
+ showProgress: boolean;
37
+ }
38
+
39
+ export interface ProgressEvent {
40
+ type: 'pre_tool' | 'post_tool' | 'notification' | 'stop' | 'start';
41
+ session_name: string;
42
+ timestamp: string;
43
+ tool_name?: string;
44
+ tool_input?: Record<string, unknown>;
45
+ success?: boolean;
46
+ error?: string;
47
+ transcript_path?: string;
48
+ }
49
+
50
+ export class ProgressTracker {
51
+ private sessions: Map<string, SessionProgress> = new Map();
52
+ private bot: TelegramBot;
53
+ private chatId: number;
54
+ private sessionManager: SessionManager;
55
+
56
+ // Rate limiting
57
+ private lastUpdate: Map<string, number> = new Map();
58
+ private pendingUpdates: Map<string, NodeJS.Timeout> = new Map();
59
+ private readonly MIN_UPDATE_INTERVAL = 1000; // 1 second minimum between updates
60
+
61
+ constructor(bot: TelegramBot, chatId: number, sessionManager: SessionManager) {
62
+ this.bot = bot;
63
+ this.chatId = chatId;
64
+ this.sessionManager = sessionManager;
65
+ }
66
+
67
+ /**
68
+ * Get or create progress state for a session.
69
+ */
70
+ private getOrCreateProgress(sessionName: string): SessionProgress {
71
+ let progress = this.sessions.get(sessionName);
72
+
73
+ if (!progress) {
74
+ const session = this.sessionManager.getByName(sessionName);
75
+ progress = {
76
+ sessionId: session?.sessionId || sessionName,
77
+ sessionName,
78
+ progressMessageId: null,
79
+ lastUpdateTime: 0,
80
+ stage: ProgressStage.IDLE,
81
+ currentTool: null,
82
+ toolStack: [],
83
+ startTime: Date.now(),
84
+ toolsExecuted: 0,
85
+ toolCounts: new Map(),
86
+ isActive: true,
87
+ showProgress: true,
88
+ };
89
+ this.sessions.set(sessionName, progress);
90
+ }
91
+
92
+ return progress;
93
+ }
94
+
95
+ /**
96
+ * Handle session start event.
97
+ */
98
+ async handleSessionStart(sessionName: string): Promise<void> {
99
+ const progress = this.getOrCreateProgress(sessionName);
100
+ progress.startTime = Date.now();
101
+ progress.stage = ProgressStage.THINKING;
102
+ progress.isActive = true;
103
+ progress.toolsExecuted = 0;
104
+ progress.toolCounts.clear();
105
+ progress.toolStack = [];
106
+ progress.currentTool = null;
107
+
108
+ await this.sendOrUpdateProgressMessage(progress);
109
+ }
110
+
111
+ /**
112
+ * Handle PreToolUse hook event.
113
+ */
114
+ async handlePreTool(sessionName: string, toolName: string, toolInput?: Record<string, unknown>): Promise<void> {
115
+ const progress = this.getOrCreateProgress(sessionName);
116
+
117
+ // Reset start time if this is the first tool
118
+ if (progress.stage === ProgressStage.IDLE) {
119
+ progress.startTime = Date.now();
120
+ }
121
+
122
+ // Push tool onto stack (for nested tool calls)
123
+ progress.toolStack.push(toolName);
124
+ progress.currentTool = toolName;
125
+ progress.stage = ProgressStage.TOOL_RUNNING;
126
+ progress.isActive = true;
127
+
128
+ await this.sendOrUpdateProgressMessage(progress);
129
+ }
130
+
131
+ /**
132
+ * Handle PostToolUse hook event.
133
+ */
134
+ async handlePostTool(sessionName: string, toolName: string, success: boolean = true): Promise<void> {
135
+ const progress = this.getOrCreateProgress(sessionName);
136
+
137
+ // Pop tool from stack
138
+ const poppedTool = progress.toolStack.pop();
139
+
140
+ // Update statistics
141
+ progress.toolsExecuted++;
142
+ const currentCount = progress.toolCounts.get(toolName) || 0;
143
+ progress.toolCounts.set(toolName, currentCount + 1);
144
+
145
+ // Update state
146
+ if (progress.toolStack.length > 0) {
147
+ // Still have parent tools running
148
+ progress.currentTool = progress.toolStack[progress.toolStack.length - 1];
149
+ progress.stage = ProgressStage.TOOL_RUNNING;
150
+ } else {
151
+ // All tools complete
152
+ progress.currentTool = null;
153
+ progress.stage = ProgressStage.TOOL_COMPLETE;
154
+ }
155
+
156
+ await this.sendOrUpdateProgressMessage(progress);
157
+ }
158
+
159
+ /**
160
+ * Handle Stop hook event.
161
+ */
162
+ async handleStop(sessionName: string): Promise<void> {
163
+ const progress = this.sessions.get(sessionName);
164
+
165
+ if (!progress) {
166
+ return;
167
+ }
168
+
169
+ progress.stage = ProgressStage.FINISHED;
170
+ progress.currentTool = null;
171
+ progress.toolStack = [];
172
+ progress.isActive = false;
173
+
174
+ // Send final completion message
175
+ await this.sendCompletionMessage(progress);
176
+
177
+ // Clean up pending updates
178
+ const pendingTimeout = this.pendingUpdates.get(sessionName);
179
+ if (pendingTimeout) {
180
+ clearTimeout(pendingTimeout);
181
+ this.pendingUpdates.delete(sessionName);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Handle notification event.
187
+ */
188
+ async handleNotification(sessionName: string, message: string): Promise<void> {
189
+ const progress = this.getOrCreateProgress(sessionName);
190
+
191
+ // Just update the timestamp, don't change stage
192
+ progress.lastUpdateTime = Date.now();
193
+
194
+ // Optionally could update the message with notification info
195
+ }
196
+
197
+ /**
198
+ * Toggle progress display for a session.
199
+ */
200
+ setShowProgress(sessionName: string, show: boolean): void {
201
+ const progress = this.sessions.get(sessionName);
202
+ if (progress) {
203
+ progress.showProgress = show;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Send or update the progress message with rate limiting.
209
+ */
210
+ private async sendOrUpdateProgressMessage(progress: SessionProgress): Promise<void> {
211
+ if (!progress.showProgress) {
212
+ return;
213
+ }
214
+
215
+ const now = Date.now();
216
+ const lastTime = this.lastUpdate.get(progress.sessionName) || 0;
217
+
218
+ if (now - lastTime < this.MIN_UPDATE_INTERVAL) {
219
+ // Schedule update for later if not already scheduled
220
+ if (!this.pendingUpdates.has(progress.sessionName)) {
221
+ const timeout = setTimeout(async () => {
222
+ this.pendingUpdates.delete(progress.sessionName);
223
+ await this.doUpdateProgressMessage(progress);
224
+ }, this.MIN_UPDATE_INTERVAL - (now - lastTime));
225
+
226
+ this.pendingUpdates.set(progress.sessionName, timeout);
227
+ }
228
+ return;
229
+ }
230
+
231
+ await this.doUpdateProgressMessage(progress);
232
+ }
233
+
234
+ /**
235
+ * Actually send or update the progress message.
236
+ */
237
+ private async doUpdateProgressMessage(progress: SessionProgress): Promise<void> {
238
+ this.lastUpdate.set(progress.sessionName, Date.now());
239
+
240
+ const messageText = buildProgressMessage({
241
+ sessionName: progress.sessionName,
242
+ stage: progress.stage,
243
+ currentTool: progress.currentTool,
244
+ toolStack: progress.toolStack,
245
+ startTime: progress.startTime,
246
+ toolsExecuted: progress.toolsExecuted,
247
+ toolCounts: progress.toolCounts,
248
+ showDetails: false,
249
+ });
250
+
251
+ try {
252
+ if (progress.progressMessageId) {
253
+ // Edit existing message
254
+ await this.bot.editMessageText(messageText, {
255
+ chat_id: this.chatId,
256
+ message_id: progress.progressMessageId,
257
+ parse_mode: 'Markdown',
258
+ });
259
+ } else {
260
+ // Send new message with mute button
261
+ const sent = await this.bot.sendMessage(this.chatId, messageText, {
262
+ parse_mode: 'Markdown',
263
+ reply_markup: {
264
+ inline_keyboard: [[
265
+ { text: '🔕 Mute', callback_data: `progress_mute:${progress.sessionName}` },
266
+ ]],
267
+ },
268
+ });
269
+ progress.progressMessageId = sent.message_id;
270
+ }
271
+ } catch (error) {
272
+ // Message might have been deleted or content unchanged
273
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
274
+
275
+ // If message was deleted or not found, create a new one
276
+ if (errorMessage.includes('message to edit not found') ||
277
+ errorMessage.includes('message is not modified')) {
278
+ // Content unchanged is fine, just skip
279
+ if (!errorMessage.includes('message is not modified')) {
280
+ progress.progressMessageId = null;
281
+ // Don't retry immediately to avoid spam
282
+ }
283
+ } else {
284
+ console.error('[ProgressTracker] Error updating message:', errorMessage);
285
+ }
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Send final completion message.
291
+ */
292
+ private async sendCompletionMessage(progress: SessionProgress): Promise<void> {
293
+ const messageText = buildCompletionMessage({
294
+ sessionName: progress.sessionName,
295
+ startTime: progress.startTime,
296
+ toolsExecuted: progress.toolsExecuted,
297
+ toolCounts: progress.toolCounts,
298
+ });
299
+
300
+ try {
301
+ if (progress.progressMessageId) {
302
+ // Edit existing message to show completion
303
+ await this.bot.editMessageText(messageText, {
304
+ chat_id: this.chatId,
305
+ message_id: progress.progressMessageId,
306
+ parse_mode: 'Markdown',
307
+ reply_markup: { inline_keyboard: [] }, // Remove mute button
308
+ });
309
+ } else {
310
+ // Send new completion message
311
+ await this.bot.sendMessage(this.chatId, messageText, {
312
+ parse_mode: 'Markdown',
313
+ });
314
+ }
315
+ } catch (error) {
316
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
317
+ if (!errorMessage.includes('message is not modified')) {
318
+ console.error('[ProgressTracker] Error sending completion:', errorMessage);
319
+ }
320
+ }
321
+
322
+ // Clean up session progress
323
+ this.sessions.delete(progress.sessionName);
324
+ }
325
+
326
+ /**
327
+ * Handle callback query for mute button.
328
+ */
329
+ async handleCallback(query: TelegramBot.CallbackQuery): Promise<boolean> {
330
+ if (!query.data?.startsWith('progress_mute:')) {
331
+ return false;
332
+ }
333
+
334
+ const sessionName = query.data.replace('progress_mute:', '');
335
+ this.setShowProgress(sessionName, false);
336
+
337
+ // Remove the progress message
338
+ const progress = this.sessions.get(sessionName);
339
+ if (progress?.progressMessageId && query.message) {
340
+ try {
341
+ await this.bot.deleteMessage(this.chatId, progress.progressMessageId);
342
+ } catch {
343
+ // Ignore deletion errors
344
+ }
345
+ progress.progressMessageId = null;
346
+ }
347
+
348
+ await this.bot.answerCallbackQuery(query.id, { text: 'Progress muted' });
349
+ return true;
350
+ }
351
+
352
+ /**
353
+ * Clean up inactive sessions.
354
+ */
355
+ cleanup(): void {
356
+ const now = Date.now();
357
+ const maxAge = 30 * 60 * 1000; // 30 minutes
358
+
359
+ for (const [sessionName, progress] of this.sessions) {
360
+ if (!progress.isActive && now - progress.lastUpdateTime > maxAge) {
361
+ this.sessions.delete(sessionName);
362
+ }
363
+ }
364
+ }
365
+ }
@@ -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
 
package/src/telegram.ts CHANGED
@@ -417,9 +417,12 @@ export class TelegramManager {
417
417
  }
418
418
 
419
419
  // User provided instructions - continue
420
+ // Include multiple fields for compatibility with different Claude Code versions
420
421
  return {
421
422
  decision: 'block',
422
423
  reason: response,
424
+ continue: true,
425
+ stopReason: response,
423
426
  };
424
427
  } catch (err) {
425
428
  clearInterval(reminderInterval);