telegram-claude-mcp 1.0.2 → 1.1.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/hooks/notify-hook.sh +46 -0
- package/hooks/permission-hook.sh +61 -0
- package/package.json +7 -4
- package/src/index.ts +99 -1
- package/src/providers/index.ts +11 -1
- package/src/telegram.ts +204 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Claude Code Notification Hook
|
|
4
|
+
#
|
|
5
|
+
# This script receives notification events from Claude Code (stop, session events)
|
|
6
|
+
# and forwards them to the Telegram MCP server.
|
|
7
|
+
#
|
|
8
|
+
# Usage: Configure this script in your Claude Code hooks settings for Stop event
|
|
9
|
+
#
|
|
10
|
+
# Environment variables:
|
|
11
|
+
# TELEGRAM_HOOK_PORT - Port of the MCP server (default: 3333)
|
|
12
|
+
# TELEGRAM_HOOK_HOST - Host of the MCP server (default: localhost)
|
|
13
|
+
# TELEGRAM_HOOK_EVENT - Event type override (default: from input or 'stop')
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
HOOK_PORT="${TELEGRAM_HOOK_PORT:-3333}"
|
|
17
|
+
HOOK_HOST="${TELEGRAM_HOOK_HOST:-localhost}"
|
|
18
|
+
HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/notify"
|
|
19
|
+
EVENT_TYPE="${TELEGRAM_HOOK_EVENT:-stop}"
|
|
20
|
+
|
|
21
|
+
# Read the hook input from stdin
|
|
22
|
+
INPUT=$(cat)
|
|
23
|
+
|
|
24
|
+
# Extract relevant information
|
|
25
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
26
|
+
MESSAGE=$(echo "$INPUT" | jq -r '.message // .stop_reason // "Agent stopped"')
|
|
27
|
+
|
|
28
|
+
# Log for debugging
|
|
29
|
+
echo "[notify-hook] Event: $EVENT_TYPE" >&2
|
|
30
|
+
echo "[notify-hook] Message: $MESSAGE" >&2
|
|
31
|
+
|
|
32
|
+
# Build the request payload
|
|
33
|
+
PAYLOAD=$(jq -n \
|
|
34
|
+
--arg type "$EVENT_TYPE" \
|
|
35
|
+
--arg message "$MESSAGE" \
|
|
36
|
+
--arg session_id "$SESSION_ID" \
|
|
37
|
+
'{type: $type, message: $message, session_id: $session_id}')
|
|
38
|
+
|
|
39
|
+
# Send the notification (non-blocking, we don't need to wait)
|
|
40
|
+
curl -s -X POST "$HOOK_URL" \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-d "$PAYLOAD" \
|
|
43
|
+
--max-time 10 >/dev/null 2>&1 &
|
|
44
|
+
|
|
45
|
+
# Return success immediately (hook doesn't need to block)
|
|
46
|
+
echo '{"continue": true}'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Claude Code Permission Hook
|
|
4
|
+
#
|
|
5
|
+
# This script receives permission requests from Claude Code and forwards them
|
|
6
|
+
# to the Telegram MCP server for interactive approval via Telegram.
|
|
7
|
+
#
|
|
8
|
+
# Usage: Configure this script in your Claude Code hooks settings
|
|
9
|
+
#
|
|
10
|
+
# Environment variables:
|
|
11
|
+
# TELEGRAM_HOOK_PORT - Port of the MCP server (default: 3333)
|
|
12
|
+
# TELEGRAM_HOOK_HOST - Host of the MCP server (default: localhost)
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
HOOK_PORT="${TELEGRAM_HOOK_PORT:-3333}"
|
|
16
|
+
HOOK_HOST="${TELEGRAM_HOOK_HOST:-localhost}"
|
|
17
|
+
HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/permission"
|
|
18
|
+
|
|
19
|
+
# Read the hook input from stdin
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
|
|
22
|
+
# Extract tool_name and tool_input from the Claude Code hook format
|
|
23
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
24
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
25
|
+
|
|
26
|
+
# If no tool_name, this might be a different format - try to extract from the input
|
|
27
|
+
if [ -z "$TOOL_NAME" ]; then
|
|
28
|
+
# Claude Code might send the data differently, try alternative paths
|
|
29
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName // .name // empty')
|
|
30
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.toolInput // .input // .arguments // {}')
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Log for debugging (goes to stderr, not stdout)
|
|
34
|
+
echo "[permission-hook] Tool: $TOOL_NAME" >&2
|
|
35
|
+
echo "[permission-hook] Sending to: $HOOK_URL" >&2
|
|
36
|
+
|
|
37
|
+
# Build the request payload
|
|
38
|
+
PAYLOAD=$(jq -n \
|
|
39
|
+
--arg tool_name "$TOOL_NAME" \
|
|
40
|
+
--argjson tool_input "$TOOL_INPUT" \
|
|
41
|
+
'{tool_name: $tool_name, tool_input: $tool_input}')
|
|
42
|
+
|
|
43
|
+
# Send the request to the MCP server and wait for response
|
|
44
|
+
RESPONSE=$(curl -s -X POST "$HOOK_URL" \
|
|
45
|
+
-H "Content-Type: application/json" \
|
|
46
|
+
-d "$PAYLOAD" \
|
|
47
|
+
--max-time 600)
|
|
48
|
+
|
|
49
|
+
# Check if curl succeeded
|
|
50
|
+
if [ $? -ne 0 ]; then
|
|
51
|
+
echo "[permission-hook] Error: Failed to connect to hook server" >&2
|
|
52
|
+
# Return deny on error for safety
|
|
53
|
+
echo '{"hookSpecificOutput":{"decision":{"behavior":"deny","message":"Failed to connect to Telegram hook server"}}}'
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Log the response
|
|
58
|
+
echo "[permission-hook] Response: $RESPONSE" >&2
|
|
59
|
+
|
|
60
|
+
# Output the response (this goes back to Claude Code)
|
|
61
|
+
echo "$RESPONSE"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telegram-claude-mcp",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "MCP server that lets Claude message you on Telegram",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP server that lets Claude message you on Telegram with hooks support",
|
|
5
5
|
"author": "Geravant",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"mcp",
|
|
15
15
|
"claude-code",
|
|
16
16
|
"bot",
|
|
17
|
-
"messaging"
|
|
17
|
+
"messaging",
|
|
18
|
+
"hooks",
|
|
19
|
+
"permissions"
|
|
18
20
|
],
|
|
19
21
|
"type": "module",
|
|
20
22
|
"main": "src/index.ts",
|
|
@@ -23,7 +25,8 @@
|
|
|
23
25
|
},
|
|
24
26
|
"files": [
|
|
25
27
|
"src",
|
|
26
|
-
"bin"
|
|
28
|
+
"bin",
|
|
29
|
+
"hooks"
|
|
27
30
|
],
|
|
28
31
|
"scripts": {
|
|
29
32
|
"start": "node --import tsx src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A stdio-based MCP server that lets Claude message you on Telegram.
|
|
7
7
|
* Supports multiple Claude Code sessions with message tagging.
|
|
8
|
+
*
|
|
9
|
+
* Also provides an HTTP server for Claude Code hooks integration,
|
|
10
|
+
* allowing permission requests and notifications to flow through Telegram.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
14
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
15
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
-
import { TelegramManager } from './telegram.js';
|
|
16
|
+
import { TelegramManager, type HookEvent, type PermissionDecision } from './telegram.js';
|
|
14
17
|
import { loadAppConfig, validateAppConfig } from './providers/index.js';
|
|
18
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'http';
|
|
15
19
|
|
|
16
20
|
async function main() {
|
|
17
21
|
// Load configuration
|
|
@@ -35,6 +39,98 @@ async function main() {
|
|
|
35
39
|
|
|
36
40
|
telegram.start();
|
|
37
41
|
|
|
42
|
+
// Start HTTP server for hooks
|
|
43
|
+
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
44
|
+
// CORS headers
|
|
45
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
46
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
47
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
48
|
+
|
|
49
|
+
if (req.method === 'OPTIONS') {
|
|
50
|
+
res.writeHead(200);
|
|
51
|
+
res.end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (req.method !== 'POST') {
|
|
56
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read body
|
|
62
|
+
let body = '';
|
|
63
|
+
for await (const chunk of req) {
|
|
64
|
+
body += chunk;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(body);
|
|
69
|
+
const url = req.url || '/';
|
|
70
|
+
|
|
71
|
+
// Handle permission request
|
|
72
|
+
if (url === '/permission' || url === '/hooks/permission') {
|
|
73
|
+
const { tool_name, tool_input } = data;
|
|
74
|
+
|
|
75
|
+
if (!tool_name) {
|
|
76
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
77
|
+
res.end(JSON.stringify({ error: 'Missing tool_name' }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.error(`[HTTP] Permission request for tool: ${tool_name}`);
|
|
82
|
+
|
|
83
|
+
const decision = await telegram.handlePermissionRequest(
|
|
84
|
+
tool_name,
|
|
85
|
+
tool_input || {}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify({
|
|
90
|
+
hookSpecificOutput: {
|
|
91
|
+
decision: {
|
|
92
|
+
behavior: decision.behavior,
|
|
93
|
+
message: decision.message,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle notifications (stop, session events, etc.)
|
|
101
|
+
if (url === '/notify' || url === '/hooks/notify') {
|
|
102
|
+
const event: HookEvent = {
|
|
103
|
+
type: data.type || 'notification',
|
|
104
|
+
message: data.message,
|
|
105
|
+
session_id: data.session_id,
|
|
106
|
+
tool_name: data.tool_name,
|
|
107
|
+
tool_input: data.tool_input,
|
|
108
|
+
timestamp: data.timestamp || new Date().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
console.error(`[HTTP] Notification: ${event.type}`);
|
|
112
|
+
|
|
113
|
+
await telegram.sendHookNotification(event);
|
|
114
|
+
|
|
115
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
116
|
+
res.end(JSON.stringify({ success: true }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Unknown endpoint
|
|
121
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('[HTTP] Error:', error);
|
|
125
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
126
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
httpServer.listen(config.sessionPort, () => {
|
|
131
|
+
console.error(`[HTTP] Hook server listening on port ${config.sessionPort}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
38
134
|
// Track active chat sessions
|
|
39
135
|
let activeSessionId: string | null = null;
|
|
40
136
|
|
|
@@ -186,11 +282,13 @@ async function main() {
|
|
|
186
282
|
console.error('Telegram Claude MCP server ready');
|
|
187
283
|
console.error(`Session: ${config.sessionName}`);
|
|
188
284
|
console.error(`Chat ID: ${config.telegramChatId}`);
|
|
285
|
+
console.error(`Hook server: http://localhost:${config.sessionPort}`);
|
|
189
286
|
console.error('');
|
|
190
287
|
|
|
191
288
|
// Graceful shutdown
|
|
192
289
|
const shutdown = async () => {
|
|
193
290
|
console.error('\nShutting down...');
|
|
291
|
+
httpServer.close();
|
|
194
292
|
telegram.stop();
|
|
195
293
|
process.exit(0);
|
|
196
294
|
};
|
package/src/providers/index.ts
CHANGED
|
@@ -31,10 +31,20 @@ export function loadAppConfig(): AppConfig {
|
|
|
31
31
|
const chatId = process.env.TELEGRAM_CHAT_ID;
|
|
32
32
|
const sessionPort = process.env.SESSION_PORT || '3333';
|
|
33
33
|
|
|
34
|
+
// Auto-detect project name from current working directory
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
const projectName = cwd.split('/').pop() || 'unknown';
|
|
37
|
+
|
|
38
|
+
// Build session name: SESSION_NAME:projectName or just projectName
|
|
39
|
+
const baseSessionName = process.env.SESSION_NAME;
|
|
40
|
+
const sessionName = baseSessionName
|
|
41
|
+
? `${baseSessionName}:${projectName}`
|
|
42
|
+
: projectName;
|
|
43
|
+
|
|
34
44
|
return {
|
|
35
45
|
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || '',
|
|
36
46
|
telegramChatId: chatId ? parseInt(chatId, 10) : 0,
|
|
37
|
-
sessionName
|
|
47
|
+
sessionName,
|
|
38
48
|
sessionPort: parseInt(sessionPort, 10),
|
|
39
49
|
openrouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
40
50
|
openrouterModel: process.env.OPENROUTER_MODEL || 'openai/gpt-oss-120b',
|
package/src/telegram.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Handles Telegram messaging for Claude Code with multi-session support.
|
|
5
5
|
* Multiple Claude Code instances can share one Telegram chat, with messages
|
|
6
6
|
* tagged by session name and replies routed to the correct session.
|
|
7
|
+
*
|
|
8
|
+
* Also handles Claude Code hook events (permissions, notifications, etc.)
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import TelegramBot from 'node-telegram-bot-api';
|
|
@@ -28,23 +30,48 @@ interface PendingResponse {
|
|
|
28
30
|
timestamp: number;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
interface PendingPermission {
|
|
34
|
+
resolve: (decision: PermissionDecision) => void;
|
|
35
|
+
reject: (error: Error) => void;
|
|
36
|
+
messageId: number;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
toolName: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PermissionDecision {
|
|
42
|
+
behavior: 'allow' | 'deny';
|
|
43
|
+
message?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface HookEvent {
|
|
47
|
+
type: 'permission_request' | 'stop' | 'notification' | 'session_start' | 'session_end';
|
|
48
|
+
session_id?: string;
|
|
49
|
+
tool_name?: string;
|
|
50
|
+
tool_input?: Record<string, unknown>;
|
|
51
|
+
message?: string;
|
|
52
|
+
timestamp?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
export interface TelegramConfig {
|
|
32
56
|
botToken: string;
|
|
33
57
|
chatId: number;
|
|
34
58
|
sessionName: string;
|
|
35
59
|
responseTimeoutMs?: number;
|
|
60
|
+
permissionTimeoutMs?: number;
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
export class TelegramManager {
|
|
39
64
|
private bot: TelegramBot;
|
|
40
65
|
private config: TelegramConfig;
|
|
41
66
|
private pendingResponses: Map<string, PendingResponse> = new Map();
|
|
67
|
+
private pendingPermissions: Map<string, PendingPermission> = new Map();
|
|
42
68
|
private sessionStateFile: string;
|
|
43
69
|
private isRunning = false;
|
|
44
70
|
|
|
45
71
|
constructor(config: TelegramConfig) {
|
|
46
72
|
this.config = {
|
|
47
73
|
responseTimeoutMs: 180000, // 3 minutes default
|
|
74
|
+
permissionTimeoutMs: 300000, // 5 minutes for permissions
|
|
48
75
|
...config,
|
|
49
76
|
};
|
|
50
77
|
|
|
@@ -57,6 +84,7 @@ export class TelegramManager {
|
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
this.setupMessageHandler();
|
|
87
|
+
this.setupCallbackHandler();
|
|
60
88
|
this.updateSessionState({ waitingForResponse: false, messageIds: [] });
|
|
61
89
|
}
|
|
62
90
|
|
|
@@ -151,6 +179,136 @@ export class TelegramManager {
|
|
|
151
179
|
this.updateSessionState({ waitingForResponse: false, messageIds: [] });
|
|
152
180
|
}
|
|
153
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Handle a permission request from Claude Code hooks
|
|
184
|
+
* Sends a message with inline buttons and waits for user decision
|
|
185
|
+
*/
|
|
186
|
+
async handlePermissionRequest(
|
|
187
|
+
toolName: string,
|
|
188
|
+
toolInput: Record<string, unknown>
|
|
189
|
+
): Promise<PermissionDecision> {
|
|
190
|
+
// Format the tool input for display
|
|
191
|
+
const inputDisplay = this.formatToolInput(toolName, toolInput);
|
|
192
|
+
|
|
193
|
+
const message = `🔐 [${this.config.sessionName}] Permission Request\n\nTool: ${toolName}\n${inputDisplay}`;
|
|
194
|
+
|
|
195
|
+
const sent = await this.bot.sendMessage(this.config.chatId, message, {
|
|
196
|
+
reply_markup: {
|
|
197
|
+
inline_keyboard: [
|
|
198
|
+
[
|
|
199
|
+
{ text: '✅ Allow', callback_data: `allow:${sent?.message_id || 'pending'}` },
|
|
200
|
+
{ text: '❌ Deny', callback_data: `deny:${sent?.message_id || 'pending'}` },
|
|
201
|
+
],
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Update callback_data with actual message ID
|
|
207
|
+
await this.bot.editMessageReplyMarkup(
|
|
208
|
+
{
|
|
209
|
+
inline_keyboard: [
|
|
210
|
+
[
|
|
211
|
+
{ text: '✅ Allow', callback_data: `allow:${sent.message_id}` },
|
|
212
|
+
{ text: '❌ Deny', callback_data: `deny:${sent.message_id}` },
|
|
213
|
+
],
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{ chat_id: this.config.chatId, message_id: sent.message_id }
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return this.waitForPermission(sent.message_id, toolName);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Send a hook notification (non-blocking)
|
|
224
|
+
*/
|
|
225
|
+
async sendHookNotification(event: HookEvent): Promise<void> {
|
|
226
|
+
let emoji = '📢';
|
|
227
|
+
let title = 'Notification';
|
|
228
|
+
|
|
229
|
+
switch (event.type) {
|
|
230
|
+
case 'stop':
|
|
231
|
+
emoji = '🛑';
|
|
232
|
+
title = 'Agent Stopped';
|
|
233
|
+
break;
|
|
234
|
+
case 'session_start':
|
|
235
|
+
emoji = '🚀';
|
|
236
|
+
title = 'Session Started';
|
|
237
|
+
break;
|
|
238
|
+
case 'session_end':
|
|
239
|
+
emoji = '👋';
|
|
240
|
+
title = 'Session Ended';
|
|
241
|
+
break;
|
|
242
|
+
case 'notification':
|
|
243
|
+
emoji = '📢';
|
|
244
|
+
title = 'Notification';
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const message = `${emoji} [${this.config.sessionName}] ${title}\n\n${event.message || 'No details'}`;
|
|
249
|
+
await this.bot.sendMessage(this.config.chatId, message);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Format tool input for display
|
|
254
|
+
*/
|
|
255
|
+
private formatToolInput(toolName: string, input: Record<string, unknown>): string {
|
|
256
|
+
// Special formatting for common tools
|
|
257
|
+
if (toolName === 'Bash' && input.command) {
|
|
258
|
+
return `Command: \`${input.command}\``;
|
|
259
|
+
}
|
|
260
|
+
if (toolName === 'Write' && input.file_path) {
|
|
261
|
+
const content = input.content as string;
|
|
262
|
+
const preview = content?.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
263
|
+
return `File: ${input.file_path}\nContent: ${preview || '(empty)'}`;
|
|
264
|
+
}
|
|
265
|
+
if (toolName === 'Edit' && input.file_path) {
|
|
266
|
+
return `File: ${input.file_path}\nOld: ${input.old_string}\nNew: ${input.new_string}`;
|
|
267
|
+
}
|
|
268
|
+
if (toolName === 'Read' && input.file_path) {
|
|
269
|
+
return `File: ${input.file_path}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Default: JSON stringify
|
|
273
|
+
try {
|
|
274
|
+
const str = JSON.stringify(input, null, 2);
|
|
275
|
+
return str.length > 500 ? str.slice(0, 500) + '...' : str;
|
|
276
|
+
} catch {
|
|
277
|
+
return '(unable to display input)';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Wait for a permission decision with timeout
|
|
283
|
+
*/
|
|
284
|
+
private waitForPermission(messageId: number, toolName: string): Promise<PermissionDecision> {
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const key = `${messageId}`;
|
|
287
|
+
|
|
288
|
+
const timeout = setTimeout(() => {
|
|
289
|
+
this.pendingPermissions.delete(key);
|
|
290
|
+
// Default to deny on timeout for safety
|
|
291
|
+
resolve({ behavior: 'deny', message: 'Permission request timed out' });
|
|
292
|
+
}, this.config.permissionTimeoutMs);
|
|
293
|
+
|
|
294
|
+
this.pendingPermissions.set(key, {
|
|
295
|
+
resolve: (decision: PermissionDecision) => {
|
|
296
|
+
clearTimeout(timeout);
|
|
297
|
+
this.pendingPermissions.delete(key);
|
|
298
|
+
resolve(decision);
|
|
299
|
+
},
|
|
300
|
+
reject: (error: Error) => {
|
|
301
|
+
clearTimeout(timeout);
|
|
302
|
+
this.pendingPermissions.delete(key);
|
|
303
|
+
reject(error);
|
|
304
|
+
},
|
|
305
|
+
messageId,
|
|
306
|
+
timestamp: Date.now(),
|
|
307
|
+
toolName,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
154
312
|
/**
|
|
155
313
|
* Set up message handler for incoming messages
|
|
156
314
|
*/
|
|
@@ -202,6 +360,52 @@ export class TelegramManager {
|
|
|
202
360
|
});
|
|
203
361
|
}
|
|
204
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Set up callback query handler for inline keyboard buttons
|
|
365
|
+
*/
|
|
366
|
+
private setupCallbackHandler(): void {
|
|
367
|
+
this.bot.on('callback_query', async (query) => {
|
|
368
|
+
if (!query.data || !query.message) return;
|
|
369
|
+
|
|
370
|
+
const [action, messageId] = query.data.split(':');
|
|
371
|
+
const key = messageId;
|
|
372
|
+
|
|
373
|
+
console.error(`[${this.config.sessionName}] Callback query: ${action} for message ${messageId}`);
|
|
374
|
+
|
|
375
|
+
const pending = this.pendingPermissions.get(key);
|
|
376
|
+
if (!pending) {
|
|
377
|
+
// Answer the callback to remove loading state
|
|
378
|
+
await this.bot.answerCallbackQuery(query.id, { text: 'Request expired' });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const decision: PermissionDecision = {
|
|
383
|
+
behavior: action === 'allow' ? 'allow' : 'deny',
|
|
384
|
+
message: action === 'deny' ? 'Denied by user via Telegram' : undefined,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Update the message to show the decision
|
|
388
|
+
const statusEmoji = action === 'allow' ? '✅' : '❌';
|
|
389
|
+
const statusText = action === 'allow' ? 'Allowed' : 'Denied';
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await this.bot.editMessageReplyMarkup(
|
|
393
|
+
{ inline_keyboard: [] },
|
|
394
|
+
{ chat_id: query.message.chat.id, message_id: query.message.message_id }
|
|
395
|
+
);
|
|
396
|
+
await this.bot.editMessageText(
|
|
397
|
+
`${query.message.text}\n\n${statusEmoji} ${statusText}`,
|
|
398
|
+
{ chat_id: query.message.chat.id, message_id: query.message.message_id }
|
|
399
|
+
);
|
|
400
|
+
} catch {
|
|
401
|
+
// Ignore edit errors (message might be too old)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await this.bot.answerCallbackQuery(query.id, { text: `${statusText}!` });
|
|
405
|
+
pending.resolve(decision);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
205
409
|
/**
|
|
206
410
|
* Wait for a response with timeout
|
|
207
411
|
*/
|