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 +336 -0
- package/hooks/stop-hook.sh +93 -0
- package/package.json +3 -2
- package/src/index.ts +13 -0
- package/src/telegram.ts +68 -0
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.
|
|
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
|
*/
|