telegram-claude-mcp 1.6.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
@@ -35,29 +35,29 @@ Create directory `~/.claude/hooks/` and add these scripts:
35
35
  **~/.claude/hooks/permission-hook.sh**
36
36
  ```bash
37
37
  #!/bin/bash
38
+ # Set SESSION_NAME env var to target a specific session (for multi-session setups)
38
39
  SESSION_DIR="/tmp/telegram-claude-sessions"
39
40
 
40
- find_active_session() {
41
- local latest_file=""
42
- 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
43
48
  [ -d "$SESSION_DIR" ] || return
44
- for info_file in "$SESSION_DIR"/*.info; do
45
- [ -e "$info_file" ] || continue
46
- local pid
47
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
48
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
49
- local file_time
50
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
51
- if [ "$file_time" -gt "$latest_time" ]; then
52
- latest_time=$file_time
53
- latest_file=$info_file
54
- fi
55
- 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
+ }
56
56
  done
57
- echo "$latest_file"
57
+ echo "$latest"
58
58
  }
59
59
 
60
- INFO_FILE=$(find_active_session)
60
+ INFO_FILE=$(find_session)
61
61
  [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
62
62
 
63
63
  HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
@@ -78,33 +78,33 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" -H "Content-Type: application/json" -d "$
78
78
  **~/.claude/hooks/stop-hook.sh**
79
79
  ```bash
80
80
  #!/bin/bash
81
+ # Set SESSION_NAME env var to target a specific session (for multi-session setups)
81
82
  SESSION_DIR="/tmp/telegram-claude-sessions"
82
83
 
83
- find_active_session() {
84
- local latest_file=""
85
- 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
86
91
  [ -d "$SESSION_DIR" ] || return
87
- for info_file in "$SESSION_DIR"/*.info; do
88
- [ -e "$info_file" ] || continue
89
- local pid
90
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
91
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
92
- local file_time
93
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
94
- if [ "$file_time" -gt "$latest_time" ]; then
95
- latest_time=$file_time
96
- latest_file=$info_file
97
- fi
98
- 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
+ }
99
99
  done
100
- echo "$latest_file"
100
+ echo "$latest"
101
101
  }
102
102
 
103
103
  INPUT=$(cat)
104
104
  STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
105
105
  [ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
106
106
 
107
- INFO_FILE=$(find_active_session)
107
+ INFO_FILE=$(find_session)
108
108
  [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
109
109
 
110
110
  HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
@@ -124,29 +124,29 @@ fi
124
124
  **~/.claude/hooks/notify-hook.sh**
125
125
  ```bash
126
126
  #!/bin/bash
127
+ # Set SESSION_NAME env var to target a specific session (for multi-session setups)
127
128
  SESSION_DIR="/tmp/telegram-claude-sessions"
128
129
 
129
- find_active_session() {
130
- local latest_file=""
131
- 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
132
137
  [ -d "$SESSION_DIR" ] || return
133
- for info_file in "$SESSION_DIR"/*.info; do
134
- [ -e "$info_file" ] || continue
135
- local pid
136
- pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
137
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
138
- local file_time
139
- file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
140
- if [ "$file_time" -gt "$latest_time" ]; then
141
- latest_time=$file_time
142
- latest_file=$info_file
143
- fi
144
- 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
+ }
145
145
  done
146
- echo "$latest_file"
146
+ echo "$latest"
147
147
  }
148
148
 
149
- INFO_FILE=$(find_active_session)
149
+ INFO_FILE=$(find_session)
150
150
  [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
151
151
 
152
152
  HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
@@ -175,19 +175,19 @@ Add to `~/.claude/settings.json`:
175
175
  "PermissionRequest": [
176
176
  {
177
177
  "matcher": "*",
178
- "hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-hook.sh" }]
178
+ "hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/permission-hook.sh" }]
179
179
  }
180
180
  ],
181
181
  "Stop": [
182
182
  {
183
183
  "matcher": "*",
184
- "hooks": [{ "type": "command", "command": "~/.claude/hooks/stop-hook.sh" }]
184
+ "hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/stop-hook.sh" }]
185
185
  }
186
186
  ],
187
187
  "Notification": [
188
188
  {
189
189
  "matcher": "*",
190
- "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-hook.sh" }]
190
+ "hooks": [{ "type": "command", "command": "SESSION_NAME=default ~/.claude/hooks/notify-hook.sh" }]
191
191
  }
192
192
  ]
193
193
  },
@@ -205,6 +205,8 @@ Add to `~/.claude/settings.json`:
205
205
  }
206
206
  ```
207
207
 
208
+ **Important:** The `SESSION_NAME` in hook commands must match the `SESSION_NAME` in the MCP server env.
209
+
208
210
  ### 3. Create Telegram Bot
209
211
 
210
212
  1. Open Telegram and message [@BotFather](https://t.me/BotFather)
@@ -257,17 +259,84 @@ Claude Code Hook Scripts telegram-claude-mcp
257
259
 
258
260
  ## Multiple Sessions
259
261
 
260
- 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
261
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):
262
273
  ```json
263
274
  {
264
- "env": {
265
- "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
+ }
266
299
  }
267
300
  }
268
301
  ```
269
302
 
270
- 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
271
340
 
272
341
  ## Troubleshooting
273
342
 
package/bin/setup.js CHANGED
@@ -97,19 +97,31 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
97
97
  const STOP_HOOK = `#!/bin/bash
98
98
  #
99
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)
100
101
  #
101
102
 
102
103
  SESSION_DIR="/tmp/telegram-claude-sessions"
103
104
 
104
- 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
+
105
119
  local latest_file=""
106
120
  local latest_time=0
107
-
108
121
  [ -d "$SESSION_DIR" ] || return
109
122
 
110
123
  for info_file in "$SESSION_DIR"/*.info; do
111
124
  [ -e "$info_file" ] || continue
112
-
113
125
  local pid
114
126
  pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
115
127
  if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
@@ -121,22 +133,16 @@ find_active_session() {
121
133
  fi
122
134
  fi
123
135
  done
124
-
125
136
  echo "$latest_file"
126
137
  }
127
138
 
128
139
  INPUT=$(cat)
129
140
 
130
141
  STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
131
- if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
132
- exit 0
133
- fi
134
-
135
- INFO_FILE=$(find_active_session)
142
+ [ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
136
143
 
137
- if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
138
- exit 0
139
- fi
144
+ INFO_FILE=$(find_session)
145
+ [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
140
146
 
141
147
  HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
142
148
  HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
@@ -156,29 +162,38 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
156
162
  if [ $? -eq 0 ]; then
157
163
  DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
158
164
  REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
159
-
160
- if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
161
- echo "$RESPONSE"
162
- fi
165
+ [ "$DECISION" = "block" ] && [ -n "$REASON" ] && echo "$RESPONSE"
163
166
  fi
164
167
  `;
165
168
 
166
169
  const NOTIFY_HOOK = `#!/bin/bash
167
170
  #
168
171
  # Claude Code Notification Hook - sends notifications to Telegram
172
+ # Set SESSION_NAME env var to target a specific session (for multi-session setups)
169
173
  #
170
174
 
171
175
  SESSION_DIR="/tmp/telegram-claude-sessions"
172
176
 
173
- 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
+
174
191
  local latest_file=""
175
192
  local latest_time=0
176
-
177
193
  [ -d "$SESSION_DIR" ] || return
178
194
 
179
195
  for info_file in "$SESSION_DIR"/*.info; do
180
196
  [ -e "$info_file" ] || continue
181
-
182
197
  local pid
183
198
  pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
184
199
  if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
@@ -190,15 +205,11 @@ find_active_session() {
190
205
  fi
191
206
  fi
192
207
  done
193
-
194
208
  echo "$latest_file"
195
209
  }
196
210
 
197
- INFO_FILE=$(find_active_session)
198
-
199
- if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
200
- exit 0
201
- fi
211
+ INFO_FILE=$(find_session)
212
+ [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ] && exit 0
202
213
 
203
214
  HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
204
215
  HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
@@ -218,42 +229,46 @@ curl -s -X POST "$HOOK_URL" \\
218
229
  --max-time 10 > /dev/null 2>&1
219
230
  `;
220
231
 
221
- // Hooks configuration for Claude settings
222
- const HOOKS_CONFIG = {
223
- PermissionRequest: [
224
- {
225
- matcher: '*',
226
- hooks: [
227
- {
228
- type: 'command',
229
- command: '~/.claude/hooks/permission-hook.sh'
230
- }
231
- ]
232
- }
233
- ],
234
- Stop: [
235
- {
236
- matcher: '*',
237
- hooks: [
238
- {
239
- type: 'command',
240
- command: '~/.claude/hooks/stop-hook.sh'
241
- }
242
- ]
243
- }
244
- ],
245
- Notification: [
246
- {
247
- matcher: '*',
248
- hooks: [
249
- {
250
- type: 'command',
251
- command: '~/.claude/hooks/notify-hook.sh'
252
- }
253
- ]
254
- }
255
- ]
256
- };
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
+ }
257
272
 
258
273
  function setup() {
259
274
  console.log('\nšŸ“± telegram-claude-mcp Setup\n');
@@ -291,8 +306,8 @@ function setup() {
291
306
  }
292
307
  }
293
308
 
294
- // Merge hooks config
295
- settings.hooks = { ...settings.hooks, ...HOOKS_CONFIG };
309
+ // Merge hooks config (default session)
310
+ settings.hooks = { ...settings.hooks, ...generateHooksConfig('default') };
296
311
 
297
312
  // Add MCP server config if not present
298
313
  if (!settings.mcpServers) {
@@ -337,6 +352,18 @@ function setup() {
337
352
  console.log('4. Restart Claude Code\n');
338
353
 
339
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));
340
367
  console.log('\n✨ Setup complete! Configure your bot token and chat ID to finish.\n');
341
368
  }
342
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telegram-claude-mcp",
3
- "version": "1.6.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
@@ -351,8 +351,7 @@ export class TelegramManager {
351
351
  if (entry.type === 'assistant' && entry.message?.content) {
352
352
  const textContent = entry.message.content.find((c: any) => c.type === 'text');
353
353
  if (textContent?.text) {
354
- lastMessage = textContent.text.slice(0, 500);
355
- if (textContent.text.length > 500) lastMessage += '...';
354
+ lastMessage = this.truncateMiddle(textContent.text, 600);
356
355
  break;
357
356
  }
358
357
  }
@@ -476,6 +475,24 @@ export class TelegramManager {
476
475
  });
477
476
  }
478
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
+
479
496
  /**
480
497
  * Format tool input for display
481
498
  */