telegram-claude-mcp 1.4.1 ā 1.6.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 +9 -6
- package/bin/setup.js +18 -10
- package/hooks/notify-hook.sh +24 -8
- package/hooks/permission-hook.sh +24 -8
- package/hooks/stop-hook.sh +25 -4
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/providers/index.ts +3 -1
- package/src/telegram.ts +176 -40
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Then:
|
|
|
20
20
|
|
|
21
21
|
- **Permission buttons** - Allow/Deny tool usage from Telegram
|
|
22
22
|
- **Interactive stop** - Reply to continue Claude's work after it stops
|
|
23
|
+
- **Auto-retry with reminders** - Get reminders every 2 minutes if you miss a message
|
|
23
24
|
- **Notifications** - Get notified about Claude events
|
|
24
25
|
- **Multi-session** - Run multiple Claude instances with message tagging
|
|
25
26
|
|
|
@@ -225,12 +226,14 @@ Replace `YOUR_BOT_TOKEN` and `YOUR_CHAT_ID` in `~/.claude/settings.json`
|
|
|
225
226
|
|
|
226
227
|
## Environment Variables
|
|
227
228
|
|
|
228
|
-
| Variable | Description |
|
|
229
|
-
|
|
230
|
-
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather |
|
|
231
|
-
| `TELEGRAM_CHAT_ID` | Your Telegram chat ID |
|
|
232
|
-
| `SESSION_NAME` | Session identifier (for multi-instance) |
|
|
233
|
-
| `SESSION_PORT` | Preferred HTTP port
|
|
229
|
+
| Variable | Description | Default |
|
|
230
|
+
|----------|-------------|---------|
|
|
231
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Required |
|
|
232
|
+
| `TELEGRAM_CHAT_ID` | Your Telegram chat ID | Required |
|
|
233
|
+
| `SESSION_NAME` | Session identifier (for multi-instance) | "default" |
|
|
234
|
+
| `SESSION_PORT` | Preferred HTTP port (auto-finds if busy) | 3333 |
|
|
235
|
+
| `CHAT_RESPONSE_TIMEOUT_MS` | Timeout for message responses | 600000 (10 min) |
|
|
236
|
+
| `PERMISSION_TIMEOUT_MS` | Timeout for permission decisions | 600000 (10 min) |
|
|
234
237
|
|
|
235
238
|
## How It Works
|
|
236
239
|
|
package/bin/setup.js
CHANGED
|
@@ -22,23 +22,35 @@ 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
|
|
25
|
+
// Hook scripts content - with SESSION_NAME support for multi-session setups
|
|
26
26
|
const PERMISSION_HOOK = `#!/bin/bash
|
|
27
27
|
#
|
|
28
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)
|
|
29
30
|
#
|
|
30
31
|
|
|
31
32
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
32
33
|
|
|
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
|
|
47
|
+
|
|
34
48
|
local latest_file=""
|
|
35
49
|
local latest_time=0
|
|
36
|
-
|
|
37
50
|
[ -d "$SESSION_DIR" ] || return
|
|
38
51
|
|
|
39
52
|
for info_file in "$SESSION_DIR"/*.info; do
|
|
40
53
|
[ -e "$info_file" ] || continue
|
|
41
|
-
|
|
42
54
|
local pid
|
|
43
55
|
pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
|
|
44
56
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
@@ -50,15 +62,11 @@ find_active_session() {
|
|
|
50
62
|
fi
|
|
51
63
|
fi
|
|
52
64
|
done
|
|
53
|
-
|
|
54
65
|
echo "$latest_file"
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
INFO_FILE=$(
|
|
58
|
-
|
|
59
|
-
if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
|
|
60
|
-
exit 0
|
|
61
|
-
fi
|
|
68
|
+
INFO_FILE=$(find_session)
|
|
69
|
+
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
|
|
62
70
|
|
|
63
71
|
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
|
|
64
72
|
HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
|
package/hooks/notify-hook.sh
CHANGED
|
@@ -5,25 +5,41 @@
|
|
|
5
5
|
# This script receives notification events from Claude Code (stop, session events)
|
|
6
6
|
# and forwards them to the Telegram MCP server.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups).
|
|
9
|
+
# Without SESSION_NAME, falls back to the most recently started session.
|
|
9
10
|
#
|
|
10
11
|
|
|
11
12
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
12
13
|
EVENT_TYPE="${TELEGRAM_HOOK_EVENT:-stop}"
|
|
13
14
|
|
|
14
|
-
# Function to find
|
|
15
|
-
|
|
15
|
+
# Function to find session info file
|
|
16
|
+
# If SESSION_NAME is set, use that specific session
|
|
17
|
+
# Otherwise, find the most recent active session
|
|
18
|
+
find_session() {
|
|
19
|
+
# If SESSION_NAME is set, use specific session
|
|
20
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
21
|
+
local specific_file="$SESSION_DIR/${SESSION_NAME}.info"
|
|
22
|
+
if [ -f "$specific_file" ]; then
|
|
23
|
+
local pid
|
|
24
|
+
pid=$(jq -r '.pid // empty' "$specific_file" 2>/dev/null)
|
|
25
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
26
|
+
echo "$specific_file"
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
echo "[notify-hook] Session '$SESSION_NAME' not found or not running" >&2
|
|
31
|
+
return
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Fallback: find most recent active session
|
|
16
35
|
local latest_file=""
|
|
17
36
|
local latest_time=0
|
|
18
37
|
|
|
19
|
-
# Check if directory exists
|
|
20
38
|
[ -d "$SESSION_DIR" ] || return
|
|
21
39
|
|
|
22
40
|
for info_file in "$SESSION_DIR"/*.info; do
|
|
23
|
-
# Skip if no matches (glob didn't expand)
|
|
24
41
|
[ -e "$info_file" ] || continue
|
|
25
42
|
|
|
26
|
-
# Check if the process is still running
|
|
27
43
|
local pid
|
|
28
44
|
pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
|
|
29
45
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
@@ -39,8 +55,8 @@ find_active_session() {
|
|
|
39
55
|
echo "$latest_file"
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
# Find
|
|
43
|
-
INFO_FILE=$(
|
|
58
|
+
# Find session info
|
|
59
|
+
INFO_FILE=$(find_session)
|
|
44
60
|
|
|
45
61
|
if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
|
|
46
62
|
echo "[notify-hook] No active session found, skipping notification" >&2
|
package/hooks/permission-hook.sh
CHANGED
|
@@ -5,24 +5,40 @@
|
|
|
5
5
|
# This script receives permission requests from Claude Code and forwards them
|
|
6
6
|
# to the Telegram MCP server for interactive approval via Telegram.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups).
|
|
9
|
+
# Without SESSION_NAME, falls back to the most recently started session.
|
|
9
10
|
#
|
|
10
11
|
|
|
11
12
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
12
13
|
|
|
13
|
-
# Function to find
|
|
14
|
-
|
|
14
|
+
# Function to find session info file
|
|
15
|
+
# If SESSION_NAME is set, use that specific session
|
|
16
|
+
# Otherwise, find the most recent active session
|
|
17
|
+
find_session() {
|
|
18
|
+
# If SESSION_NAME is set, use specific session
|
|
19
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
20
|
+
local specific_file="$SESSION_DIR/${SESSION_NAME}.info"
|
|
21
|
+
if [ -f "$specific_file" ]; then
|
|
22
|
+
local pid
|
|
23
|
+
pid=$(jq -r '.pid // empty' "$specific_file" 2>/dev/null)
|
|
24
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
25
|
+
echo "$specific_file"
|
|
26
|
+
return
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
echo "[permission-hook] Session '$SESSION_NAME' not found or not running" >&2
|
|
30
|
+
return
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Fallback: find most recent active session
|
|
15
34
|
local latest_file=""
|
|
16
35
|
local latest_time=0
|
|
17
36
|
|
|
18
|
-
# Check if directory exists
|
|
19
37
|
[ -d "$SESSION_DIR" ] || return
|
|
20
38
|
|
|
21
39
|
for info_file in "$SESSION_DIR"/*.info; do
|
|
22
|
-
# Skip if no matches (glob didn't expand)
|
|
23
40
|
[ -e "$info_file" ] || continue
|
|
24
41
|
|
|
25
|
-
# Check if the process is still running
|
|
26
42
|
local pid
|
|
27
43
|
pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
|
|
28
44
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
@@ -38,8 +54,8 @@ find_active_session() {
|
|
|
38
54
|
echo "$latest_file"
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
# Find
|
|
42
|
-
INFO_FILE=$(
|
|
57
|
+
# Find session info
|
|
58
|
+
INFO_FILE=$(find_session)
|
|
43
59
|
|
|
44
60
|
if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
|
|
45
61
|
echo "[permission-hook] No active session found" >&2
|
package/hooks/stop-hook.sh
CHANGED
|
@@ -6,11 +6,32 @@
|
|
|
6
6
|
# If you reply with instructions, Claude continues working on them.
|
|
7
7
|
# If you reply "done" or don't reply, Claude stops.
|
|
8
8
|
#
|
|
9
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups).
|
|
10
|
+
# Without SESSION_NAME, falls back to the most recently started session.
|
|
11
|
+
#
|
|
9
12
|
|
|
10
13
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
11
14
|
|
|
12
|
-
# Function to find
|
|
13
|
-
|
|
15
|
+
# Function to find session info file
|
|
16
|
+
# If SESSION_NAME is set, use that specific session
|
|
17
|
+
# Otherwise, find the most recent active session
|
|
18
|
+
find_session() {
|
|
19
|
+
# If SESSION_NAME is set, use specific session
|
|
20
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
21
|
+
local specific_file="$SESSION_DIR/${SESSION_NAME}.info"
|
|
22
|
+
if [ -f "$specific_file" ]; then
|
|
23
|
+
local pid
|
|
24
|
+
pid=$(jq -r '.pid // empty' "$specific_file" 2>/dev/null)
|
|
25
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
26
|
+
echo "$specific_file"
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
echo "[stop-hook] Session '$SESSION_NAME' not found or not running" >&2
|
|
31
|
+
return
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Fallback: find most recent active session
|
|
14
35
|
local latest_file=""
|
|
15
36
|
local latest_time=0
|
|
16
37
|
|
|
@@ -44,8 +65,8 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
|
|
44
65
|
exit 0
|
|
45
66
|
fi
|
|
46
67
|
|
|
47
|
-
# Find
|
|
48
|
-
INFO_FILE=$(
|
|
68
|
+
# Find session info
|
|
69
|
+
INFO_FILE=$(find_session)
|
|
49
70
|
|
|
50
71
|
if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
|
|
51
72
|
echo "[stop-hook] No active session found" >&2
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/providers/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface AppConfig {
|
|
|
25
25
|
|
|
26
26
|
// Chat settings
|
|
27
27
|
responseTimeoutMs: number;
|
|
28
|
+
permissionTimeoutMs: number;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function loadAppConfig(): AppConfig {
|
|
@@ -48,7 +49,8 @@ export function loadAppConfig(): AppConfig {
|
|
|
48
49
|
sessionPort: parseInt(sessionPort, 10),
|
|
49
50
|
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
50
51
|
openrouterModel: process.env.OPENROUTER_MODEL || 'openai/gpt-oss-120b',
|
|
51
|
-
responseTimeoutMs: parseInt(process.env.CHAT_RESPONSE_TIMEOUT_MS || '
|
|
52
|
+
responseTimeoutMs: parseInt(process.env.CHAT_RESPONSE_TIMEOUT_MS || '600000', 10),
|
|
53
|
+
permissionTimeoutMs: parseInt(process.env.PERMISSION_TIMEOUT_MS || '600000', 10),
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
package/src/telegram.ts
CHANGED
|
@@ -70,8 +70,8 @@ export class TelegramManager {
|
|
|
70
70
|
|
|
71
71
|
constructor(config: TelegramConfig) {
|
|
72
72
|
this.config = {
|
|
73
|
-
responseTimeoutMs:
|
|
74
|
-
permissionTimeoutMs:
|
|
73
|
+
responseTimeoutMs: 600000, // 10 minutes default
|
|
74
|
+
permissionTimeoutMs: 600000, // 10 minutes for permissions
|
|
75
75
|
...config,
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -182,6 +182,7 @@ export class TelegramManager {
|
|
|
182
182
|
/**
|
|
183
183
|
* Handle a permission request from Claude Code hooks
|
|
184
184
|
* Sends a message with inline buttons and waits for user decision
|
|
185
|
+
* Includes auto-retry with reminders if user doesn't respond
|
|
185
186
|
*/
|
|
186
187
|
async handlePermissionRequest(
|
|
187
188
|
toolName: string,
|
|
@@ -208,7 +209,94 @@ export class TelegramManager {
|
|
|
208
209
|
{ chat_id: this.config.chatId, message_id: sent.message_id }
|
|
209
210
|
);
|
|
210
211
|
|
|
211
|
-
|
|
212
|
+
// Try with reminders
|
|
213
|
+
const reminderIntervalMs = 120000; // 2 minutes between reminders
|
|
214
|
+
const maxReminders = 4; // Up to 4 reminders (total ~10 min with initial wait)
|
|
215
|
+
|
|
216
|
+
for (let attempt = 0; attempt <= maxReminders; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
const decision = await this.waitForPermissionWithTimeout(sent.message_id, toolName, reminderIntervalMs);
|
|
219
|
+
return decision;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (attempt < maxReminders) {
|
|
222
|
+
// Send reminder
|
|
223
|
+
await this.bot.sendMessage(
|
|
224
|
+
this.config.chatId,
|
|
225
|
+
`ā° [${this.config.sessionName}] Reminder: Still waiting for permission\n\nTool: ${toolName}\n\nš Please respond to the message above`,
|
|
226
|
+
{ reply_to_message_id: sent.message_id }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Final attempt: ask if they want to retry
|
|
233
|
+
const retryMsg = await this.bot.sendMessage(
|
|
234
|
+
this.config.chatId,
|
|
235
|
+
`ā ļø [${this.config.sessionName}] Permission request timed out\n\nTool: ${toolName}\n\nClick below to respond now:`,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await this.bot.editMessageReplyMarkup(
|
|
239
|
+
{
|
|
240
|
+
inline_keyboard: [
|
|
241
|
+
[
|
|
242
|
+
{ text: 'ā
Allow Now', callback_data: `allow:${sent.message_id}` },
|
|
243
|
+
{ text: 'ā Deny', callback_data: `deny:${sent.message_id}` },
|
|
244
|
+
],
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{ chat_id: this.config.chatId, message_id: retryMsg.message_id }
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// One more wait with the original timeout
|
|
251
|
+
try {
|
|
252
|
+
return await this.waitForPermissionWithTimeout(sent.message_id, toolName, this.config.permissionTimeoutMs!);
|
|
253
|
+
} catch {
|
|
254
|
+
// Truly timed out
|
|
255
|
+
return { behavior: 'deny', message: 'Permission request timed out after multiple reminders' };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Wait for permission with a specific timeout (throws on timeout)
|
|
261
|
+
*/
|
|
262
|
+
private waitForPermissionWithTimeout(
|
|
263
|
+
messageId: number,
|
|
264
|
+
toolName: string,
|
|
265
|
+
timeoutMs: number
|
|
266
|
+
): Promise<PermissionDecision> {
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const key = `${messageId}`;
|
|
269
|
+
|
|
270
|
+
// Check if already resolved
|
|
271
|
+
const existing = this.pendingPermissions.get(key);
|
|
272
|
+
if (existing) {
|
|
273
|
+
// Already waiting, just update the callbacks
|
|
274
|
+
existing.resolve = resolve;
|
|
275
|
+
existing.reject = reject;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const timeout = setTimeout(() => {
|
|
280
|
+
this.pendingPermissions.delete(key);
|
|
281
|
+
reject(new Error('Timeout'));
|
|
282
|
+
}, timeoutMs);
|
|
283
|
+
|
|
284
|
+
this.pendingPermissions.set(key, {
|
|
285
|
+
resolve: (decision: PermissionDecision) => {
|
|
286
|
+
clearTimeout(timeout);
|
|
287
|
+
this.pendingPermissions.delete(key);
|
|
288
|
+
resolve(decision);
|
|
289
|
+
},
|
|
290
|
+
reject: (error: Error) => {
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
this.pendingPermissions.delete(key);
|
|
293
|
+
reject(error);
|
|
294
|
+
},
|
|
295
|
+
messageId,
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
toolName,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
212
300
|
}
|
|
213
301
|
|
|
214
302
|
/**
|
|
@@ -245,6 +333,7 @@ export class TelegramManager {
|
|
|
245
333
|
* Handle interactive stop - send message and wait for user to reply with instructions
|
|
246
334
|
* Returns { decision: "block", reason: "..." } if user wants to continue
|
|
247
335
|
* Returns {} if user is done
|
|
336
|
+
* Includes auto-retry with reminders if user doesn't respond
|
|
248
337
|
*/
|
|
249
338
|
async handleInteractiveStop(transcriptPath?: string): Promise<Record<string, unknown>> {
|
|
250
339
|
// Try to get last assistant message from transcript
|
|
@@ -287,28 +376,106 @@ export class TelegramManager {
|
|
|
287
376
|
messageIds: [...this.getSessionState().messageIds, sent.message_id],
|
|
288
377
|
});
|
|
289
378
|
|
|
290
|
-
//
|
|
379
|
+
// Try with reminders
|
|
380
|
+
const reminderIntervalMs = 120000; // 2 minutes between reminders
|
|
381
|
+
const maxReminders = 4; // Up to 4 reminders
|
|
382
|
+
|
|
383
|
+
for (let attempt = 0; attempt <= maxReminders; attempt++) {
|
|
384
|
+
try {
|
|
385
|
+
const response = await this.waitForResponseWithTimeout(sent.message_id, reminderIntervalMs);
|
|
386
|
+
|
|
387
|
+
// Check if user wants to stop
|
|
388
|
+
const lowerResponse = response.toLowerCase().trim();
|
|
389
|
+
if (lowerResponse === 'done' || lowerResponse === 'stop' || lowerResponse === 'finish' || lowerResponse === 'ok') {
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// User provided instructions - continue
|
|
394
|
+
return {
|
|
395
|
+
decision: 'block',
|
|
396
|
+
reason: response,
|
|
397
|
+
};
|
|
398
|
+
} catch (err) {
|
|
399
|
+
if (attempt < maxReminders) {
|
|
400
|
+
// Send reminder
|
|
401
|
+
await this.bot.sendMessage(
|
|
402
|
+
this.config.chatId,
|
|
403
|
+
`ā° [${this.config.sessionName}] Reminder: Claude is waiting for your response\n\nš¬ Reply with instructions to continue, or "done" to let Claude stop`,
|
|
404
|
+
{ reply_to_message_id: sent.message_id }
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Final attempt with longer timeout
|
|
291
411
|
try {
|
|
292
|
-
|
|
412
|
+
await this.bot.sendMessage(
|
|
413
|
+
this.config.chatId,
|
|
414
|
+
`ā ļø [${this.config.sessionName}] Last chance! Claude will stop in ${Math.round(this.config.responseTimeoutMs! / 60000)} minutes if no response.\n\nš¬ Reply now to continue working.`,
|
|
415
|
+
{ reply_to_message_id: sent.message_id }
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const response = await this.waitForResponseWithTimeout(sent.message_id, this.config.responseTimeoutMs!);
|
|
293
419
|
|
|
294
|
-
// Check if user wants to stop
|
|
295
420
|
const lowerResponse = response.toLowerCase().trim();
|
|
296
421
|
if (lowerResponse === 'done' || lowerResponse === 'stop' || lowerResponse === 'finish' || lowerResponse === 'ok') {
|
|
297
422
|
return {};
|
|
298
423
|
}
|
|
299
424
|
|
|
300
|
-
// User provided instructions - continue
|
|
301
425
|
return {
|
|
302
426
|
decision: 'block',
|
|
303
427
|
reason: response,
|
|
304
428
|
};
|
|
305
429
|
} catch (err) {
|
|
306
|
-
//
|
|
307
|
-
|
|
430
|
+
// Truly timed out - notify and allow stop
|
|
431
|
+
await this.bot.sendMessage(
|
|
432
|
+
this.config.chatId,
|
|
433
|
+
`š“ [${this.config.sessionName}] Claude stopped (no response received)\n\nStart a new conversation to continue.`
|
|
434
|
+
);
|
|
435
|
+
console.error('[Telegram] Interactive stop timeout after reminders');
|
|
308
436
|
return {};
|
|
309
437
|
}
|
|
310
438
|
}
|
|
311
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Wait for response with a specific timeout (throws on timeout)
|
|
442
|
+
*/
|
|
443
|
+
private waitForResponseWithTimeout(messageId: number, timeoutMs: number): Promise<string> {
|
|
444
|
+
return new Promise((resolve, reject) => {
|
|
445
|
+
const key = `${messageId}`;
|
|
446
|
+
|
|
447
|
+
// Check if already has pending response
|
|
448
|
+
const existing = this.pendingResponses.get(key);
|
|
449
|
+
if (existing) {
|
|
450
|
+
existing.resolve = resolve;
|
|
451
|
+
existing.reject = reject;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const timeout = setTimeout(() => {
|
|
456
|
+
this.pendingResponses.delete(key);
|
|
457
|
+
this.updateSessionState({ waitingForResponse: false });
|
|
458
|
+
reject(new Error('Timeout'));
|
|
459
|
+
}, timeoutMs);
|
|
460
|
+
|
|
461
|
+
this.pendingResponses.set(key, {
|
|
462
|
+
resolve: (response: string) => {
|
|
463
|
+
clearTimeout(timeout);
|
|
464
|
+
this.pendingResponses.delete(key);
|
|
465
|
+
this.updateSessionState({ waitingForResponse: false });
|
|
466
|
+
resolve(response);
|
|
467
|
+
},
|
|
468
|
+
reject: (error: Error) => {
|
|
469
|
+
clearTimeout(timeout);
|
|
470
|
+
this.pendingResponses.delete(key);
|
|
471
|
+
reject(error);
|
|
472
|
+
},
|
|
473
|
+
messageId,
|
|
474
|
+
timestamp: Date.now(),
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
312
479
|
/**
|
|
313
480
|
* Format tool input for display
|
|
314
481
|
*/
|
|
@@ -338,37 +505,6 @@ export class TelegramManager {
|
|
|
338
505
|
}
|
|
339
506
|
}
|
|
340
507
|
|
|
341
|
-
/**
|
|
342
|
-
* Wait for a permission decision with timeout
|
|
343
|
-
*/
|
|
344
|
-
private waitForPermission(messageId: number, toolName: string): Promise<PermissionDecision> {
|
|
345
|
-
return new Promise((resolve, reject) => {
|
|
346
|
-
const key = `${messageId}`;
|
|
347
|
-
|
|
348
|
-
const timeout = setTimeout(() => {
|
|
349
|
-
this.pendingPermissions.delete(key);
|
|
350
|
-
// Default to deny on timeout for safety
|
|
351
|
-
resolve({ behavior: 'deny', message: 'Permission request timed out' });
|
|
352
|
-
}, this.config.permissionTimeoutMs);
|
|
353
|
-
|
|
354
|
-
this.pendingPermissions.set(key, {
|
|
355
|
-
resolve: (decision: PermissionDecision) => {
|
|
356
|
-
clearTimeout(timeout);
|
|
357
|
-
this.pendingPermissions.delete(key);
|
|
358
|
-
resolve(decision);
|
|
359
|
-
},
|
|
360
|
-
reject: (error: Error) => {
|
|
361
|
-
clearTimeout(timeout);
|
|
362
|
-
this.pendingPermissions.delete(key);
|
|
363
|
-
reject(error);
|
|
364
|
-
},
|
|
365
|
-
messageId,
|
|
366
|
-
timestamp: Date.now(),
|
|
367
|
-
toolName,
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
508
|
/**
|
|
373
509
|
* Set up message handler for incoming messages
|
|
374
510
|
*/
|