telegram-claude-mcp 1.2.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/setup.js ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Setup script for telegram-claude-mcp hooks integration
5
+ *
6
+ * Usage: npx telegram-claude-mcp setup
7
+ *
8
+ * This will:
9
+ * 1. Copy hook scripts to ~/.claude/hooks/
10
+ * 2. Update Claude settings with hooks configuration
11
+ * 3. Display instructions for completing setup
12
+ */
13
+
14
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, chmodSync } from 'fs';
15
+ import { join, dirname } from 'path';
16
+ import { homedir } from 'os';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+
21
+ const CLAUDE_DIR = join(homedir(), '.claude');
22
+ const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
23
+ const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
24
+
25
+ // Hook scripts content
26
+ const PERMISSION_HOOK = `#!/bin/bash
27
+ #
28
+ # Claude Code Permission Hook - forwards permission requests to Telegram
29
+ #
30
+
31
+ SESSION_DIR="/tmp/telegram-claude-sessions"
32
+
33
+ find_active_session() {
34
+ local latest_file=""
35
+ local latest_time=0
36
+
37
+ [ -d "$SESSION_DIR" ] || return
38
+
39
+ for info_file in "$SESSION_DIR"/*.info; do
40
+ [ -e "$info_file" ] || continue
41
+
42
+ local pid
43
+ pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
44
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
45
+ local file_time
46
+ file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
47
+ if [ "$file_time" -gt "$latest_time" ]; then
48
+ latest_time=$file_time
49
+ latest_file=$info_file
50
+ fi
51
+ fi
52
+ done
53
+
54
+ echo "$latest_file"
55
+ }
56
+
57
+ INFO_FILE=$(find_active_session)
58
+
59
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
60
+ exit 0
61
+ fi
62
+
63
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
64
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
65
+ HOOK_URL="http://\${HOOK_HOST}:\${HOOK_PORT}/permission"
66
+
67
+ INPUT=$(cat)
68
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
69
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
70
+
71
+ if [ -z "$TOOL_NAME" ]; then
72
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName // .name // empty')
73
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.toolInput // .input // .arguments // {}')
74
+ fi
75
+
76
+ PAYLOAD=$(jq -n \\
77
+ --arg tool_name "$TOOL_NAME" \\
78
+ --argjson tool_input "$TOOL_INPUT" \\
79
+ '{tool_name: $tool_name, tool_input: $tool_input}')
80
+
81
+ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
82
+ -H "Content-Type: application/json" \\
83
+ -d "$PAYLOAD" \\
84
+ --max-time 600)
85
+
86
+ [ $? -eq 0 ] && echo "$RESPONSE"
87
+ `;
88
+
89
+ const STOP_HOOK = `#!/bin/bash
90
+ #
91
+ # Claude Code Interactive Stop Hook - sends stop notification and waits for reply
92
+ #
93
+
94
+ SESSION_DIR="/tmp/telegram-claude-sessions"
95
+
96
+ find_active_session() {
97
+ local latest_file=""
98
+ local latest_time=0
99
+
100
+ [ -d "$SESSION_DIR" ] || return
101
+
102
+ for info_file in "$SESSION_DIR"/*.info; do
103
+ [ -e "$info_file" ] || continue
104
+
105
+ local pid
106
+ pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
107
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
108
+ local file_time
109
+ file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
110
+ if [ "$file_time" -gt "$latest_time" ]; then
111
+ latest_time=$file_time
112
+ latest_file=$info_file
113
+ fi
114
+ fi
115
+ done
116
+
117
+ echo "$latest_file"
118
+ }
119
+
120
+ INPUT=$(cat)
121
+
122
+ STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
123
+ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
124
+ exit 0
125
+ fi
126
+
127
+ INFO_FILE=$(find_active_session)
128
+
129
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
130
+ exit 0
131
+ fi
132
+
133
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
134
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
135
+ HOOK_URL="http://\${HOOK_HOST}:\${HOOK_PORT}/stop"
136
+
137
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
138
+
139
+ PAYLOAD=$(jq -n \\
140
+ --arg transcript_path "$TRANSCRIPT_PATH" \\
141
+ '{transcript_path: $transcript_path}')
142
+
143
+ RESPONSE=$(curl -s -X POST "$HOOK_URL" \\
144
+ -H "Content-Type: application/json" \\
145
+ -d "$PAYLOAD" \\
146
+ --max-time 300)
147
+
148
+ if [ $? -eq 0 ]; then
149
+ DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
150
+ REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
151
+
152
+ if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
153
+ echo "$RESPONSE"
154
+ fi
155
+ fi
156
+ `;
157
+
158
+ const NOTIFY_HOOK = `#!/bin/bash
159
+ #
160
+ # Claude Code Notification Hook - sends notifications to Telegram
161
+ #
162
+
163
+ SESSION_DIR="/tmp/telegram-claude-sessions"
164
+
165
+ find_active_session() {
166
+ local latest_file=""
167
+ local latest_time=0
168
+
169
+ [ -d "$SESSION_DIR" ] || return
170
+
171
+ for info_file in "$SESSION_DIR"/*.info; do
172
+ [ -e "$info_file" ] || continue
173
+
174
+ local pid
175
+ pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
176
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
177
+ local file_time
178
+ file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
179
+ if [ "$file_time" -gt "$latest_time" ]; then
180
+ latest_time=$file_time
181
+ latest_file=$info_file
182
+ fi
183
+ fi
184
+ done
185
+
186
+ echo "$latest_file"
187
+ }
188
+
189
+ INFO_FILE=$(find_active_session)
190
+
191
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
192
+ exit 0
193
+ fi
194
+
195
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
196
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
197
+ HOOK_URL="http://\${HOOK_HOST}:\${HOOK_PORT}/notify"
198
+
199
+ INPUT=$(cat)
200
+ MESSAGE=$(echo "$INPUT" | jq -r '.message // "Notification from Claude"')
201
+
202
+ PAYLOAD=$(jq -n \\
203
+ --arg type "notification" \\
204
+ --arg message "$MESSAGE" \\
205
+ '{type: $type, message: $message}')
206
+
207
+ curl -s -X POST "$HOOK_URL" \\
208
+ -H "Content-Type: application/json" \\
209
+ -d "$PAYLOAD" \\
210
+ --max-time 10 > /dev/null 2>&1
211
+ `;
212
+
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
+ };
249
+
250
+ function setup() {
251
+ console.log('\nšŸ“± telegram-claude-mcp Setup\n');
252
+ console.log('This will configure Telegram notifications for Claude Code.\n');
253
+
254
+ // Create hooks directory
255
+ if (!existsSync(HOOKS_DIR)) {
256
+ mkdirSync(HOOKS_DIR, { recursive: true });
257
+ console.log('āœ“ Created ~/.claude/hooks/');
258
+ }
259
+
260
+ // Write hook scripts
261
+ const permissionHookPath = join(HOOKS_DIR, 'permission-hook.sh');
262
+ writeFileSync(permissionHookPath, PERMISSION_HOOK);
263
+ chmodSync(permissionHookPath, 0o755);
264
+ console.log('āœ“ Installed permission-hook.sh');
265
+
266
+ const stopHookPath = join(HOOKS_DIR, 'stop-hook.sh');
267
+ writeFileSync(stopHookPath, STOP_HOOK);
268
+ chmodSync(stopHookPath, 0o755);
269
+ console.log('āœ“ Installed stop-hook.sh');
270
+
271
+ const notifyHookPath = join(HOOKS_DIR, 'notify-hook.sh');
272
+ writeFileSync(notifyHookPath, NOTIFY_HOOK);
273
+ chmodSync(notifyHookPath, 0o755);
274
+ console.log('āœ“ Installed notify-hook.sh');
275
+
276
+ // Update Claude settings
277
+ let settings = {};
278
+ if (existsSync(SETTINGS_FILE)) {
279
+ try {
280
+ settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8'));
281
+ } catch {
282
+ console.log('⚠ Could not parse existing settings.json, creating new one');
283
+ }
284
+ }
285
+
286
+ // Merge hooks config
287
+ settings.hooks = { ...settings.hooks, ...HOOKS_CONFIG };
288
+
289
+ // Add MCP server config if not present
290
+ if (!settings.mcpServers) {
291
+ settings.mcpServers = {};
292
+ }
293
+
294
+ if (!settings.mcpServers.telegram) {
295
+ settings.mcpServers.telegram = {
296
+ command: 'npx',
297
+ args: ['-y', 'telegram-claude-mcp'],
298
+ env: {
299
+ TELEGRAM_BOT_TOKEN: 'YOUR_BOT_TOKEN',
300
+ TELEGRAM_CHAT_ID: 'YOUR_CHAT_ID',
301
+ SESSION_NAME: 'default'
302
+ }
303
+ };
304
+ console.log('āœ“ Added telegram MCP server config (needs bot token)');
305
+ }
306
+
307
+ writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
308
+ console.log('āœ“ Updated ~/.claude/settings.json with hooks');
309
+
310
+ // Print next steps
311
+ console.log('\n' + '─'.repeat(50));
312
+ console.log('\nšŸ“‹ Next Steps:\n');
313
+
314
+ console.log('1. Create a Telegram bot:');
315
+ console.log(' - Open Telegram and message @BotFather');
316
+ console.log(' - Send /newbot and follow the prompts');
317
+ console.log(' - Copy the bot token\n');
318
+
319
+ console.log('2. Get your Chat ID:');
320
+ console.log(' - Start a chat with your new bot');
321
+ console.log(' - Send any message');
322
+ console.log(' - Visit: https://api.telegram.org/bot<TOKEN>/getUpdates');
323
+ console.log(' - Find "chat":{"id": YOUR_CHAT_ID}\n');
324
+
325
+ console.log('3. Update ~/.claude/settings.json:');
326
+ console.log(' - Set TELEGRAM_BOT_TOKEN to your bot token');
327
+ console.log(' - Set TELEGRAM_CHAT_ID to your chat ID\n');
328
+
329
+ console.log('4. Restart Claude Code\n');
330
+
331
+ console.log('─'.repeat(50));
332
+ console.log('\n✨ Setup complete! Configure your bot token and chat ID to finish.\n');
333
+ }
334
+
335
+ // Run setup
336
+ setup();
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ #
3
+ # Claude Code Interactive Stop Hook
4
+ #
5
+ # When Claude stops, this sends a message to Telegram and waits for your reply.
6
+ # If you reply with instructions, Claude continues working on them.
7
+ # If you reply "done" or don't reply, Claude stops.
8
+ #
9
+
10
+ SESSION_DIR="/tmp/telegram-claude-sessions"
11
+
12
+ # Function to find the most recent active session
13
+ find_active_session() {
14
+ local latest_file=""
15
+ local latest_time=0
16
+
17
+ [ -d "$SESSION_DIR" ] || return
18
+
19
+ for info_file in "$SESSION_DIR"/*.info; do
20
+ [ -e "$info_file" ] || continue
21
+
22
+ local pid
23
+ pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
24
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
25
+ local file_time
26
+ file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
27
+ if [ "$file_time" -gt "$latest_time" ]; then
28
+ latest_time=$file_time
29
+ latest_file=$info_file
30
+ fi
31
+ fi
32
+ done
33
+
34
+ echo "$latest_file"
35
+ }
36
+
37
+ # Read hook input
38
+ INPUT=$(cat)
39
+
40
+ # Check if stop_hook_active - if true, we already continued once, don't loop
41
+ STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
42
+ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
43
+ echo "[stop-hook] Already continued once, allowing stop" >&2
44
+ exit 0
45
+ fi
46
+
47
+ # Find active session
48
+ INFO_FILE=$(find_active_session)
49
+
50
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
51
+ echo "[stop-hook] No active session found" >&2
52
+ exit 0
53
+ fi
54
+
55
+ # Read port from session info
56
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
57
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
58
+ HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/stop"
59
+
60
+ echo "[stop-hook] Using session: $INFO_FILE (port $HOOK_PORT)" >&2
61
+
62
+ # Extract transcript path to get context
63
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
64
+
65
+ # Build request payload
66
+ PAYLOAD=$(jq -n \
67
+ --arg transcript_path "$TRANSCRIPT_PATH" \
68
+ '{transcript_path: $transcript_path}')
69
+
70
+ # Send to server and wait for response (blocking - waits for user reply)
71
+ RESPONSE=$(curl -s -X POST "$HOOK_URL" \
72
+ -H "Content-Type: application/json" \
73
+ -d "$PAYLOAD" \
74
+ --max-time 300)
75
+
76
+ if [ $? -ne 0 ]; then
77
+ echo "[stop-hook] Failed to connect to server" >&2
78
+ exit 0
79
+ fi
80
+
81
+ echo "[stop-hook] Response: $RESPONSE" >&2
82
+
83
+ # Check if user wants to continue
84
+ DECISION=$(echo "$RESPONSE" | jq -r '.decision // empty')
85
+ REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
86
+
87
+ if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
88
+ # User provided instructions - continue with them
89
+ echo "$RESPONSE"
90
+ else
91
+ # User said done or no response - allow stop
92
+ exit 0
93
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telegram-claude-mcp",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server that lets Claude message you on Telegram with hooks support",
5
5
  "author": "Geravant",
6
6
  "license": "MIT",
@@ -21,7 +21,8 @@
21
21
  "type": "module",
22
22
  "main": "src/index.ts",
23
23
  "bin": {
24
- "telegram-claude-mcp": "./bin/run.js"
24
+ "telegram-claude-mcp": "./bin/run.js",
25
+ "telegram-claude-setup": "./bin/setup.js"
25
26
  },
26
27
  "files": [
27
28
  "src",
package/src/index.ts CHANGED
@@ -190,6 +190,19 @@ async function main() {
190
190
  return;
191
191
  }
192
192
 
193
+ // Handle interactive stop (waits for user reply)
194
+ if (url === '/stop' || url === '/hooks/stop') {
195
+ const { transcript_path } = data;
196
+
197
+ console.error(`[HTTP] Interactive stop request`);
198
+
199
+ const result = await telegram.handleInteractiveStop(transcript_path);
200
+
201
+ res.writeHead(200, { 'Content-Type': 'application/json' });
202
+ res.end(JSON.stringify(result));
203
+ return;
204
+ }
205
+
193
206
  // Unknown endpoint
194
207
  res.writeHead(404, { 'Content-Type': 'application/json' });
195
208
  res.end(JSON.stringify({ error: 'Not found' }));
package/src/telegram.ts CHANGED
@@ -241,6 +241,74 @@ export class TelegramManager {
241
241
  await this.bot.sendMessage(this.config.chatId, message);
242
242
  }
243
243
 
244
+ /**
245
+ * Handle interactive stop - send message and wait for user to reply with instructions
246
+ * Returns { decision: "block", reason: "..." } if user wants to continue
247
+ * Returns {} if user is done
248
+ */
249
+ async handleInteractiveStop(transcriptPath?: string): Promise<Record<string, unknown>> {
250
+ // Try to get last assistant message from transcript
251
+ let lastMessage = 'Claude has finished working.';
252
+ if (transcriptPath) {
253
+ try {
254
+ const fs = await import('fs');
255
+ if (fs.existsSync(transcriptPath)) {
256
+ const content = fs.readFileSync(transcriptPath, 'utf-8');
257
+ const lines = content.trim().split('\n');
258
+ // Find last assistant message
259
+ for (let i = lines.length - 1; i >= 0; i--) {
260
+ try {
261
+ const entry = JSON.parse(lines[i]);
262
+ if (entry.type === 'assistant' && entry.message?.content) {
263
+ const textContent = entry.message.content.find((c: any) => c.type === 'text');
264
+ if (textContent?.text) {
265
+ lastMessage = textContent.text.slice(0, 500);
266
+ if (textContent.text.length > 500) lastMessage += '...';
267
+ break;
268
+ }
269
+ }
270
+ } catch {
271
+ // Skip invalid JSON lines
272
+ }
273
+ }
274
+ }
275
+ } catch (err) {
276
+ console.error('[Telegram] Error reading transcript:', err);
277
+ }
278
+ }
279
+
280
+ const message = `šŸ [${this.config.sessionName}] Claude stopped\n\n${lastMessage}\n\nšŸ’¬ Reply with instructions to continue, or "done" to finish.`;
281
+
282
+ const sent = await this.bot.sendMessage(this.config.chatId, message);
283
+
284
+ // Update session state
285
+ this.updateSessionState({
286
+ waitingForResponse: true,
287
+ messageIds: [...this.getSessionState().messageIds, sent.message_id],
288
+ });
289
+
290
+ // Wait for response with longer timeout for interactive stop
291
+ try {
292
+ const response = await this.waitForResponse(sent.message_id);
293
+
294
+ // Check if user wants to stop
295
+ const lowerResponse = response.toLowerCase().trim();
296
+ if (lowerResponse === 'done' || lowerResponse === 'stop' || lowerResponse === 'finish' || lowerResponse === 'ok') {
297
+ return {};
298
+ }
299
+
300
+ // User provided instructions - continue
301
+ return {
302
+ decision: 'block',
303
+ reason: response,
304
+ };
305
+ } catch (err) {
306
+ // Timeout or error - allow stop
307
+ console.error('[Telegram] Interactive stop timeout or error:', err);
308
+ return {};
309
+ }
310
+ }
311
+
244
312
  /**
245
313
  * Format tool input for display
246
314
  */