telegram-claude-mcp 1.5.0 ā 1.6.1
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 +128 -58
- package/bin/setup.js +108 -73
- 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/telegram.ts +193 -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
|
|
|
@@ -34,29 +35,29 @@ Create directory `~/.claude/hooks/` and add these scripts:
|
|
|
34
35
|
**~/.claude/hooks/permission-hook.sh**
|
|
35
36
|
```bash
|
|
36
37
|
#!/bin/bash
|
|
38
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
|
|
37
39
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
find_session() {
|
|
42
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
43
|
+
local f="$SESSION_DIR/${SESSION_NAME}.info"
|
|
44
|
+
[ -f "$f" ] && { local p=$(jq -r '.pid // empty' "$f" 2>/dev/null); [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$f"; return; }
|
|
45
|
+
return
|
|
46
|
+
fi
|
|
47
|
+
local latest="" latest_time=0
|
|
42
48
|
[ -d "$SESSION_DIR" ] || return
|
|
43
|
-
for
|
|
44
|
-
[ -e "$
|
|
45
|
-
local pid
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if [ "$file_time" -gt "$latest_time" ]; then
|
|
51
|
-
latest_time=$file_time
|
|
52
|
-
latest_file=$info_file
|
|
53
|
-
fi
|
|
54
|
-
fi
|
|
49
|
+
for f in "$SESSION_DIR"/*.info; do
|
|
50
|
+
[ -e "$f" ] || continue
|
|
51
|
+
local p=$(jq -r '.pid // empty' "$f" 2>/dev/null)
|
|
52
|
+
[ -n "$p" ] && kill -0 "$p" 2>/dev/null && {
|
|
53
|
+
local t=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
|
|
54
|
+
[ "$t" -gt "$latest_time" ] && { latest_time=$t; latest=$f; }
|
|
55
|
+
}
|
|
55
56
|
done
|
|
56
|
-
echo "$
|
|
57
|
+
echo "$latest"
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
INFO_FILE=$(
|
|
60
|
+
INFO_FILE=$(find_session)
|
|
60
61
|
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
|
|
61
62
|
|
|
62
63
|
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
|
|
@@ -77,33 +78,33 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" -H "Content-Type: application/json" -d "$
|
|
|
77
78
|
**~/.claude/hooks/stop-hook.sh**
|
|
78
79
|
```bash
|
|
79
80
|
#!/bin/bash
|
|
81
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
|
|
80
82
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
find_session() {
|
|
85
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
86
|
+
local f="$SESSION_DIR/${SESSION_NAME}.info"
|
|
87
|
+
[ -f "$f" ] && { local p=$(jq -r '.pid // empty' "$f" 2>/dev/null); [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$f"; return; }
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
local latest="" latest_time=0
|
|
85
91
|
[ -d "$SESSION_DIR" ] || return
|
|
86
|
-
for
|
|
87
|
-
[ -e "$
|
|
88
|
-
local pid
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if [ "$file_time" -gt "$latest_time" ]; then
|
|
94
|
-
latest_time=$file_time
|
|
95
|
-
latest_file=$info_file
|
|
96
|
-
fi
|
|
97
|
-
fi
|
|
92
|
+
for f in "$SESSION_DIR"/*.info; do
|
|
93
|
+
[ -e "$f" ] || continue
|
|
94
|
+
local p=$(jq -r '.pid // empty' "$f" 2>/dev/null)
|
|
95
|
+
[ -n "$p" ] && kill -0 "$p" 2>/dev/null && {
|
|
96
|
+
local t=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
|
|
97
|
+
[ "$t" -gt "$latest_time" ] && { latest_time=$t; latest=$f; }
|
|
98
|
+
}
|
|
98
99
|
done
|
|
99
|
-
echo "$
|
|
100
|
+
echo "$latest"
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
INPUT=$(cat)
|
|
103
104
|
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
104
105
|
[ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
|
|
105
106
|
|
|
106
|
-
INFO_FILE=$(
|
|
107
|
+
INFO_FILE=$(find_session)
|
|
107
108
|
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
|
|
108
109
|
|
|
109
110
|
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
|
|
@@ -123,29 +124,29 @@ fi
|
|
|
123
124
|
**~/.claude/hooks/notify-hook.sh**
|
|
124
125
|
```bash
|
|
125
126
|
#!/bin/bash
|
|
127
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
|
|
126
128
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
find_session() {
|
|
131
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
132
|
+
local f="$SESSION_DIR/${SESSION_NAME}.info"
|
|
133
|
+
[ -f "$f" ] && { local p=$(jq -r '.pid // empty' "$f" 2>/dev/null); [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo "$f"; return; }
|
|
134
|
+
return
|
|
135
|
+
fi
|
|
136
|
+
local latest="" latest_time=0
|
|
131
137
|
[ -d "$SESSION_DIR" ] || return
|
|
132
|
-
for
|
|
133
|
-
[ -e "$
|
|
134
|
-
local pid
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if [ "$file_time" -gt "$latest_time" ]; then
|
|
140
|
-
latest_time=$file_time
|
|
141
|
-
latest_file=$info_file
|
|
142
|
-
fi
|
|
143
|
-
fi
|
|
138
|
+
for f in "$SESSION_DIR"/*.info; do
|
|
139
|
+
[ -e "$f" ] || continue
|
|
140
|
+
local p=$(jq -r '.pid // empty' "$f" 2>/dev/null)
|
|
141
|
+
[ -n "$p" ] && kill -0 "$p" 2>/dev/null && {
|
|
142
|
+
local t=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)
|
|
143
|
+
[ "$t" -gt "$latest_time" ] && { latest_time=$t; latest=$f; }
|
|
144
|
+
}
|
|
144
145
|
done
|
|
145
|
-
echo "$
|
|
146
|
+
echo "$latest"
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
INFO_FILE=$(
|
|
149
|
+
INFO_FILE=$(find_session)
|
|
149
150
|
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
|
|
150
151
|
|
|
151
152
|
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
|
|
@@ -174,19 +175,19 @@ Add to `~/.claude/settings.json`:
|
|
|
174
175
|
"PermissionRequest": [
|
|
175
176
|
{
|
|
176
177
|
"matcher": "*",
|
|
177
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-hook.sh" }]
|
|
178
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/permission-hook.sh" }]
|
|
178
179
|
}
|
|
179
180
|
],
|
|
180
181
|
"Stop": [
|
|
181
182
|
{
|
|
182
183
|
"matcher": "*",
|
|
183
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/stop-hook.sh" }]
|
|
184
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/stop-hook.sh" }]
|
|
184
185
|
}
|
|
185
186
|
],
|
|
186
187
|
"Notification": [
|
|
187
188
|
{
|
|
188
189
|
"matcher": "*",
|
|
189
|
-
"hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-hook.sh" }]
|
|
190
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/notify-hook.sh" }]
|
|
190
191
|
}
|
|
191
192
|
]
|
|
192
193
|
},
|
|
@@ -204,6 +205,8 @@ Add to `~/.claude/settings.json`:
|
|
|
204
205
|
}
|
|
205
206
|
```
|
|
206
207
|
|
|
208
|
+
**Important:** The `SESSION_NAME` in hook commands must match the `SESSION_NAME` in the MCP server env.
|
|
209
|
+
|
|
207
210
|
### 3. Create Telegram Bot
|
|
208
211
|
|
|
209
212
|
1. Open Telegram and message [@BotFather](https://t.me/BotFather)
|
|
@@ -256,17 +259,84 @@ Claude Code Hook Scripts telegram-claude-mcp
|
|
|
256
259
|
|
|
257
260
|
## Multiple Sessions
|
|
258
261
|
|
|
259
|
-
Run multiple Claude instances with
|
|
262
|
+
Run multiple Claude Code instances (e.g., `~/.claude` and `~/.claude-personal`) with proper message routing.
|
|
263
|
+
|
|
264
|
+
### Why This Matters
|
|
260
265
|
|
|
266
|
+
Without proper configuration, hooks from one Claude session might route to another session's MCP server. The `SESSION_NAME` environment variable ensures each session connects to its own MCP server.
|
|
267
|
+
|
|
268
|
+
### Configuration
|
|
269
|
+
|
|
270
|
+
Each Claude config needs a unique `SESSION_NAME` in **both** the MCP server env and the hook commands.
|
|
271
|
+
|
|
272
|
+
**~/.claude/settings.json** (main):
|
|
261
273
|
```json
|
|
262
274
|
{
|
|
263
|
-
"
|
|
264
|
-
"
|
|
275
|
+
"hooks": {
|
|
276
|
+
"PermissionRequest": [{
|
|
277
|
+
"matcher": "*",
|
|
278
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks/permission-hook.sh" }]
|
|
279
|
+
}],
|
|
280
|
+
"Stop": [{
|
|
281
|
+
"matcher": "*",
|
|
282
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks/stop-hook.sh" }]
|
|
283
|
+
}],
|
|
284
|
+
"Notification": [{
|
|
285
|
+
"matcher": "*",
|
|
286
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks/notify-hook.sh" }]
|
|
287
|
+
}]
|
|
288
|
+
},
|
|
289
|
+
"mcpServers": {
|
|
290
|
+
"telegram": {
|
|
291
|
+
"command": "npx",
|
|
292
|
+
"args": ["-y", "telegram-claude-mcp"],
|
|
293
|
+
"env": {
|
|
294
|
+
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
|
|
295
|
+
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID",
|
|
296
|
+
"SESSION_NAME": "main"
|
|
297
|
+
}
|
|
298
|
+
}
|
|
265
299
|
}
|
|
266
300
|
}
|
|
267
301
|
```
|
|
268
302
|
|
|
269
|
-
|
|
303
|
+
**~/.claude-personal/settings.json** (personal):
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"hooks": {
|
|
307
|
+
"PermissionRequest": [{
|
|
308
|
+
"matcher": "*",
|
|
309
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=personal ~/.claude/hooks/permission-hook.sh" }]
|
|
310
|
+
}],
|
|
311
|
+
"Stop": [{
|
|
312
|
+
"matcher": "*",
|
|
313
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=personal ~/.claude/hooks/stop-hook.sh" }]
|
|
314
|
+
}],
|
|
315
|
+
"Notification": [{
|
|
316
|
+
"matcher": "*",
|
|
317
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=personal ~/.claude/hooks/notify-hook.sh" }]
|
|
318
|
+
}]
|
|
319
|
+
},
|
|
320
|
+
"mcpServers": {
|
|
321
|
+
"telegram": {
|
|
322
|
+
"command": "npx",
|
|
323
|
+
"args": ["-y", "telegram-claude-mcp"],
|
|
324
|
+
"env": {
|
|
325
|
+
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
|
|
326
|
+
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID",
|
|
327
|
+
"SESSION_NAME": "personal"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Key Points
|
|
335
|
+
|
|
336
|
+
- **SESSION_NAME must match** - The env var in hook commands must match the MCP server's SESSION_NAME
|
|
337
|
+
- **Share hook scripts** - All configs can use the same hook scripts in `~/.claude/hooks/`
|
|
338
|
+
- **Message tagging** - Messages are tagged with `[main]` or `[personal]` so you know the source
|
|
339
|
+
- **Separate ports** - Each MCP server auto-discovers an available port
|
|
270
340
|
|
|
271
341
|
## Troubleshooting
|
|
272
342
|
|
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")
|
|
@@ -89,19 +97,31 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
|
|
|
89
97
|
const STOP_HOOK = `#!/bin/bash
|
|
90
98
|
#
|
|
91
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)
|
|
92
101
|
#
|
|
93
102
|
|
|
94
103
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
95
104
|
|
|
96
|
-
|
|
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
|
|
118
|
+
|
|
97
119
|
local latest_file=""
|
|
98
120
|
local latest_time=0
|
|
99
|
-
|
|
100
121
|
[ -d "$SESSION_DIR" ] || return
|
|
101
122
|
|
|
102
123
|
for info_file in "$SESSION_DIR"/*.info; do
|
|
103
124
|
[ -e "$info_file" ] || continue
|
|
104
|
-
|
|
105
125
|
local pid
|
|
106
126
|
pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
|
|
107
127
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
@@ -113,22 +133,16 @@ find_active_session() {
|
|
|
113
133
|
fi
|
|
114
134
|
fi
|
|
115
135
|
done
|
|
116
|
-
|
|
117
136
|
echo "$latest_file"
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
INPUT=$(cat)
|
|
121
140
|
|
|
122
141
|
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
123
|
-
|
|
124
|
-
exit 0
|
|
125
|
-
fi
|
|
142
|
+
[ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
|
|
126
143
|
|
|
127
|
-
INFO_FILE=$(
|
|
128
|
-
|
|
129
|
-
if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
|
|
130
|
-
exit 0
|
|
131
|
-
fi
|
|
144
|
+
INFO_FILE=$(find_session)
|
|
145
|
+
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
|
|
132
146
|
|
|
133
147
|
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
|
|
134
148
|
HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
|
|
@@ -148,29 +162,38 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
|
|
|
148
162
|
if [ $? -eq 0 ]; then
|
|
149
163
|
DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
|
|
150
164
|
REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
|
|
151
|
-
|
|
152
|
-
if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
|
|
153
|
-
echo "$RESPONSE"
|
|
154
|
-
fi
|
|
165
|
+
[ "$DECISION" = "block" ] && [ -n "$REASON" ] && echo "$RESPONSE"
|
|
155
166
|
fi
|
|
156
167
|
`;
|
|
157
168
|
|
|
158
169
|
const NOTIFY_HOOK = `#!/bin/bash
|
|
159
170
|
#
|
|
160
171
|
# Claude Code Notification Hook - sends notifications to Telegram
|
|
172
|
+
# Set SESSION_NAME env var to target a specific session (for multi-session setups)
|
|
161
173
|
#
|
|
162
174
|
|
|
163
175
|
SESSION_DIR="/tmp/telegram-claude-sessions"
|
|
164
176
|
|
|
165
|
-
|
|
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
|
+
|
|
166
191
|
local latest_file=""
|
|
167
192
|
local latest_time=0
|
|
168
|
-
|
|
169
193
|
[ -d "$SESSION_DIR" ] || return
|
|
170
194
|
|
|
171
195
|
for info_file in "$SESSION_DIR"/*.info; do
|
|
172
196
|
[ -e "$info_file" ] || continue
|
|
173
|
-
|
|
174
197
|
local pid
|
|
175
198
|
pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
|
|
176
199
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
@@ -182,15 +205,11 @@ find_active_session() {
|
|
|
182
205
|
fi
|
|
183
206
|
fi
|
|
184
207
|
done
|
|
185
|
-
|
|
186
208
|
echo "$latest_file"
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
INFO_FILE=$(
|
|
190
|
-
|
|
191
|
-
if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
|
|
192
|
-
exit 0
|
|
193
|
-
fi
|
|
211
|
+
INFO_FILE=$(find_session)
|
|
212
|
+
[ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
|
|
194
213
|
|
|
195
214
|
HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
|
|
196
215
|
HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
|
|
@@ -210,42 +229,46 @@ curl -s -X POST "$HOOK_URL" \\
|
|
|
210
229
|
--max-time 10 > /dev/null 2>&1
|
|
211
230
|
`;
|
|
212
231
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
232
|
+
// Generate hooks configuration for Claude settings
|
|
233
|
+
// sessionName parameter enables multi-session support
|
|
234
|
+
function generateHooksConfig(sessionName = 'default') {
|
|
235
|
+
const envPrefix = `SESSION_NAME=${sessionName}`;
|
|
236
|
+
return {
|
|
237
|
+
PermissionRequest: [
|
|
238
|
+
{
|
|
239
|
+
matcher: '*',
|
|
240
|
+
hooks: [
|
|
241
|
+
{
|
|
242
|
+
type: 'command',
|
|
243
|
+
command: `${envPrefix} ~/.claude/hooks/permission-hook.sh`
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
Stop: [
|
|
249
|
+
{
|
|
250
|
+
matcher: '*',
|
|
251
|
+
hooks: [
|
|
252
|
+
{
|
|
253
|
+
type: 'command',
|
|
254
|
+
command: `${envPrefix} ~/.claude/hooks/stop-hook.sh`
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
Notification: [
|
|
260
|
+
{
|
|
261
|
+
matcher: '*',
|
|
262
|
+
hooks: [
|
|
263
|
+
{
|
|
264
|
+
type: 'command',
|
|
265
|
+
command: `${envPrefix} ~/.claude/hooks/notify-hook.sh`
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
}
|
|
249
272
|
|
|
250
273
|
function setup() {
|
|
251
274
|
console.log('\nš± telegram-claude-mcp Setup\n');
|
|
@@ -283,8 +306,8 @@ function setup() {
|
|
|
283
306
|
}
|
|
284
307
|
}
|
|
285
308
|
|
|
286
|
-
// Merge hooks config
|
|
287
|
-
settings.hooks = { ...settings.hooks, ...
|
|
309
|
+
// Merge hooks config (default session)
|
|
310
|
+
settings.hooks = { ...settings.hooks, ...generateHooksConfig('default') };
|
|
288
311
|
|
|
289
312
|
// Add MCP server config if not present
|
|
290
313
|
if (!settings.mcpServers) {
|
|
@@ -329,6 +352,18 @@ function setup() {
|
|
|
329
352
|
console.log('4. Restart Claude Code\n');
|
|
330
353
|
|
|
331
354
|
console.log('ā'.repeat(50));
|
|
355
|
+
console.log('\nš Multi-Session Setup (optional):\n');
|
|
356
|
+
console.log('If you have multiple Claude configs (e.g., ~/.claude and ~/.claude-personal),');
|
|
357
|
+
console.log('each needs its own SESSION_NAME to avoid message routing conflicts.\n');
|
|
358
|
+
console.log('For each additional config, update its settings.json:');
|
|
359
|
+
console.log(' 1. Change SESSION_NAME in mcpServers.telegram.env');
|
|
360
|
+
console.log(' 2. Update hook commands to use that SESSION_NAME:\n');
|
|
361
|
+
console.log(' "command": "SESSION_NAME=personal ~/.claude/hooks/permission-hook.sh"\n');
|
|
362
|
+
console.log('Example for ~/.claude-personal/settings.json:');
|
|
363
|
+
console.log(' SESSION_NAME: "personal" (in mcpServers.telegram.env)');
|
|
364
|
+
console.log(' Hook commands: "SESSION_NAME=personal ~/.claude/hooks/..."');
|
|
365
|
+
|
|
366
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
332
367
|
console.log('\n⨠Setup complete! Configure your bot token and chat ID to finish.\n');
|
|
333
368
|
}
|
|
334
369
|
|
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/telegram.ts
CHANGED
|
@@ -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
|
|
@@ -262,8 +351,7 @@ export class TelegramManager {
|
|
|
262
351
|
if (entry.type === 'assistant' && entry.message?.content) {
|
|
263
352
|
const textContent = entry.message.content.find((c: any) => c.type === 'text');
|
|
264
353
|
if (textContent?.text) {
|
|
265
|
-
lastMessage = textContent.text
|
|
266
|
-
if (textContent.text.length > 500) lastMessage += '...';
|
|
354
|
+
lastMessage = this.truncateMiddle(textContent.text, 600);
|
|
267
355
|
break;
|
|
268
356
|
}
|
|
269
357
|
}
|
|
@@ -287,28 +375,124 @@ export class TelegramManager {
|
|
|
287
375
|
messageIds: [...this.getSessionState().messageIds, sent.message_id],
|
|
288
376
|
});
|
|
289
377
|
|
|
290
|
-
//
|
|
378
|
+
// Try with reminders
|
|
379
|
+
const reminderIntervalMs = 120000; // 2 minutes between reminders
|
|
380
|
+
const maxReminders = 4; // Up to 4 reminders
|
|
381
|
+
|
|
382
|
+
for (let attempt = 0; attempt <= maxReminders; attempt++) {
|
|
383
|
+
try {
|
|
384
|
+
const response = await this.waitForResponseWithTimeout(sent.message_id, reminderIntervalMs);
|
|
385
|
+
|
|
386
|
+
// Check if user wants to stop
|
|
387
|
+
const lowerResponse = response.toLowerCase().trim();
|
|
388
|
+
if (lowerResponse === 'done' || lowerResponse === 'stop' || lowerResponse === 'finish' || lowerResponse === 'ok') {
|
|
389
|
+
return {};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// User provided instructions - continue
|
|
393
|
+
return {
|
|
394
|
+
decision: 'block',
|
|
395
|
+
reason: response,
|
|
396
|
+
};
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (attempt < maxReminders) {
|
|
399
|
+
// Send reminder
|
|
400
|
+
await this.bot.sendMessage(
|
|
401
|
+
this.config.chatId,
|
|
402
|
+
`ā° [${this.config.sessionName}] Reminder: Claude is waiting for your response\n\nš¬ Reply with instructions to continue, or "done" to let Claude stop`,
|
|
403
|
+
{ reply_to_message_id: sent.message_id }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Final attempt with longer timeout
|
|
291
410
|
try {
|
|
292
|
-
|
|
411
|
+
await this.bot.sendMessage(
|
|
412
|
+
this.config.chatId,
|
|
413
|
+
`ā ļø [${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.`,
|
|
414
|
+
{ reply_to_message_id: sent.message_id }
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const response = await this.waitForResponseWithTimeout(sent.message_id, this.config.responseTimeoutMs!);
|
|
293
418
|
|
|
294
|
-
// Check if user wants to stop
|
|
295
419
|
const lowerResponse = response.toLowerCase().trim();
|
|
296
420
|
if (lowerResponse === 'done' || lowerResponse === 'stop' || lowerResponse === 'finish' || lowerResponse === 'ok') {
|
|
297
421
|
return {};
|
|
298
422
|
}
|
|
299
423
|
|
|
300
|
-
// User provided instructions - continue
|
|
301
424
|
return {
|
|
302
425
|
decision: 'block',
|
|
303
426
|
reason: response,
|
|
304
427
|
};
|
|
305
428
|
} catch (err) {
|
|
306
|
-
//
|
|
307
|
-
|
|
429
|
+
// Truly timed out - notify and allow stop
|
|
430
|
+
await this.bot.sendMessage(
|
|
431
|
+
this.config.chatId,
|
|
432
|
+
`š“ [${this.config.sessionName}] Claude stopped (no response received)\n\nStart a new conversation to continue.`
|
|
433
|
+
);
|
|
434
|
+
console.error('[Telegram] Interactive stop timeout after reminders');
|
|
308
435
|
return {};
|
|
309
436
|
}
|
|
310
437
|
}
|
|
311
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Wait for response with a specific timeout (throws on timeout)
|
|
441
|
+
*/
|
|
442
|
+
private waitForResponseWithTimeout(messageId: number, timeoutMs: number): Promise<string> {
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
const key = `${messageId}`;
|
|
445
|
+
|
|
446
|
+
// Check if already has pending response
|
|
447
|
+
const existing = this.pendingResponses.get(key);
|
|
448
|
+
if (existing) {
|
|
449
|
+
existing.resolve = resolve;
|
|
450
|
+
existing.reject = reject;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const timeout = setTimeout(() => {
|
|
455
|
+
this.pendingResponses.delete(key);
|
|
456
|
+
this.updateSessionState({ waitingForResponse: false });
|
|
457
|
+
reject(new Error('Timeout'));
|
|
458
|
+
}, timeoutMs);
|
|
459
|
+
|
|
460
|
+
this.pendingResponses.set(key, {
|
|
461
|
+
resolve: (response: string) => {
|
|
462
|
+
clearTimeout(timeout);
|
|
463
|
+
this.pendingResponses.delete(key);
|
|
464
|
+
this.updateSessionState({ waitingForResponse: false });
|
|
465
|
+
resolve(response);
|
|
466
|
+
},
|
|
467
|
+
reject: (error: Error) => {
|
|
468
|
+
clearTimeout(timeout);
|
|
469
|
+
this.pendingResponses.delete(key);
|
|
470
|
+
reject(error);
|
|
471
|
+
},
|
|
472
|
+
messageId,
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Truncate text by removing the middle, keeping beginning and end
|
|
480
|
+
* This is useful because Claude's questions are usually at the end
|
|
481
|
+
*/
|
|
482
|
+
private truncateMiddle(text: string, maxLength: number): string {
|
|
483
|
+
if (text.length <= maxLength) return text;
|
|
484
|
+
|
|
485
|
+
// Keep more at the end (where questions usually are)
|
|
486
|
+
const startLength = Math.floor(maxLength * 0.3); // 30% from start
|
|
487
|
+
const endLength = Math.floor(maxLength * 0.6); // 60% from end
|
|
488
|
+
// ~10% for the ellipsis marker
|
|
489
|
+
|
|
490
|
+
const start = text.slice(0, startLength);
|
|
491
|
+
const end = text.slice(-endLength);
|
|
492
|
+
|
|
493
|
+
return `${start}\n\n[...truncated...]\n\n${end}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
312
496
|
/**
|
|
313
497
|
* Format tool input for display
|
|
314
498
|
*/
|
|
@@ -338,37 +522,6 @@ export class TelegramManager {
|
|
|
338
522
|
}
|
|
339
523
|
}
|
|
340
524
|
|
|
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
525
|
/**
|
|
373
526
|
* Set up message handler for incoming messages
|
|
374
527
|
*/
|