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 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
- find_active_session() {
40
- local latest_file=""
41
- local latest_time=0
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 info_file in "$SESSION_DIR"/*.info; do
44
- [ -e "$info_file" ] || continue
45
- local pid
46
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
47
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
48
- local file_time
49
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
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 "$latest_file"
57
+ echo "$latest"
57
58
  }
58
59
 
59
- INFO_FILE=$(find_active_session)
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
- find_active_session() {
83
- local latest_file=""
84
- local latest_time=0
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 info_file in "$SESSION_DIR"/*.info; do
87
- [ -e "$info_file" ] || continue
88
- local pid
89
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
90
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
91
- local file_time
92
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
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 "$latest_file"
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=$(find_active_session)
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
- find_active_session() {
129
- local latest_file=""
130
- local latest_time=0
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 info_file in "$SESSION_DIR"/*.info; do
133
- [ -e "$info_file" ] || continue
134
- local pid
135
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
136
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
137
- local file_time
138
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
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 "$latest_file"
146
+ echo "$latest"
146
147
  }
147
148
 
148
- INFO_FILE=$(find_active_session)
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 different session names:
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
- "env": {
264
- "SESSION_NAME": "project-a"
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
- Messages are tagged with `[project-a]` so you know which instance sent them.
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
- find_active_session() {
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=$(find_active_session)
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
- find_active_session() {
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
- if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
124
- exit 0
125
- fi
142
+ [ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
126
143
 
127
- INFO_FILE=$(find_active_session)
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
- find_active_session() {
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=$(find_active_session)
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
- // Hooks configuration for Claude settings
214
- const HOOKS_CONFIG = {
215
- PermissionRequest: [
216
- {
217
- matcher: '*',
218
- hooks: [
219
- {
220
- type: 'command',
221
- command: '~/.claude/hooks/permission-hook.sh'
222
- }
223
- ]
224
- }
225
- ],
226
- Stop: [
227
- {
228
- matcher: '*',
229
- hooks: [
230
- {
231
- type: 'command',
232
- command: '~/.claude/hooks/stop-hook.sh'
233
- }
234
- ]
235
- }
236
- ],
237
- Notification: [
238
- {
239
- matcher: '*',
240
- hooks: [
241
- {
242
- type: 'command',
243
- command: '~/.claude/hooks/notify-hook.sh'
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, ...HOOKS_CONFIG };
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
 
@@ -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
- # The script auto-discovers the MCP server port from session info files.
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 the most recent active session
15
- find_active_session() {
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 active session info
43
- INFO_FILE=$(find_active_session)
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
@@ -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
- # The script auto-discovers the MCP server port from session info files.
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 the most recent active session
14
- find_active_session() {
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 active session info
42
- INFO_FILE=$(find_active_session)
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
@@ -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 the most recent active session
13
- find_active_session() {
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 active session
48
- INFO_FILE=$(find_active_session)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telegram-claude-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "MCP server that lets Claude message you on Telegram with hooks support",
5
5
  "author": "Geravant",
6
6
  "license": "MIT",
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
- return this.waitForPermission(sent.message_id, toolName);
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.slice(0, 500);
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
- // Wait for response with longer timeout for interactive stop
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
- const response = await this.waitForResponse(sent.message_id);
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
- // Timeout or error - allow stop
307
- console.error('[Telegram] Interactive stop timeout or error:', err);
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
  */