telegram-claude-mcp 1.6.0 → 2.0.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/ARCHITECTURE.md +234 -0
- package/README.md +249 -58
- package/bin/daemon-ctl.js +207 -0
- package/bin/daemon.js +20 -0
- package/bin/proxy.js +22 -0
- package/bin/setup.js +90 -63
- package/hooks-v2/notify-hook.sh +32 -0
- package/hooks-v2/permission-hook.sh +43 -0
- package/hooks-v2/stop-hook.sh +45 -0
- package/package.json +16 -5
- package/src/daemon/index.ts +415 -0
- package/src/daemon/session-manager.ts +173 -0
- package/src/daemon/telegram-multi.ts +611 -0
- package/src/proxy/index.ts +429 -0
- package/src/shared/protocol.ts +146 -0
- package/src/telegram.ts +85 -71
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram Claude MCP Proxy
|
|
5
|
+
*
|
|
6
|
+
* Thin MCP proxy that connects to the singleton daemon via Unix socket.
|
|
7
|
+
* This is what Claude Code spawns for each session.
|
|
8
|
+
*
|
|
9
|
+
* Auto-starts the daemon if it's not running.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as net from 'net';
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { existsSync, readFileSync } from 'fs';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
20
|
+
import {
|
|
21
|
+
DAEMON_SOCKET_PATH,
|
|
22
|
+
DAEMON_PID_FILE,
|
|
23
|
+
serializeMessage,
|
|
24
|
+
parseMessages,
|
|
25
|
+
generateSessionId,
|
|
26
|
+
generateRequestId,
|
|
27
|
+
type ProxyMessage,
|
|
28
|
+
type DaemonMessage,
|
|
29
|
+
type ProxyConnectMessage,
|
|
30
|
+
type ProxyDisconnectMessage,
|
|
31
|
+
type ProxyToolCallMessage,
|
|
32
|
+
} from '../shared/protocol.js';
|
|
33
|
+
|
|
34
|
+
const VERSION = '2.0.0';
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = dirname(__filename);
|
|
37
|
+
|
|
38
|
+
interface ProxyConfig {
|
|
39
|
+
sessionName: string;
|
|
40
|
+
projectPath?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadConfig(): ProxyConfig {
|
|
44
|
+
// Session name from env or derive from CWD
|
|
45
|
+
let sessionName = process.env.SESSION_NAME;
|
|
46
|
+
|
|
47
|
+
if (!sessionName) {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const projectName = cwd.split('/').pop() || 'unknown';
|
|
50
|
+
sessionName = `default:${projectName}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
sessionName,
|
|
55
|
+
projectPath: process.cwd(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if the daemon is running
|
|
61
|
+
*/
|
|
62
|
+
function isDaemonRunning(): boolean {
|
|
63
|
+
if (!existsSync(DAEMON_PID_FILE)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const pid = parseInt(readFileSync(DAEMON_PID_FILE, 'utf-8').trim(), 10);
|
|
69
|
+
process.kill(pid, 0); // Check if process exists
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start the daemon process
|
|
78
|
+
*/
|
|
79
|
+
async function startDaemon(): Promise<void> {
|
|
80
|
+
console.error('[Proxy] Daemon not running, starting it...');
|
|
81
|
+
|
|
82
|
+
// Find daemon script - check multiple locations
|
|
83
|
+
const possiblePaths = [
|
|
84
|
+
join(__dirname, '..', 'daemon', 'index.ts'), // src/proxy -> src/daemon (dev)
|
|
85
|
+
join(__dirname, '..', '..', 'src', 'daemon', 'index.ts'), // dist/proxy -> src/daemon
|
|
86
|
+
join(__dirname, '..', '..', 'daemon', 'index.js'), // dist/proxy -> dist/daemon (built)
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
let daemonScript: string | null = null;
|
|
90
|
+
for (const p of possiblePaths) {
|
|
91
|
+
if (existsSync(p)) {
|
|
92
|
+
daemonScript = p;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!daemonScript) {
|
|
98
|
+
throw new Error('Daemon script not found. Please start daemon manually: telegram-claude-ctl start');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Determine runner based on file extension
|
|
102
|
+
const isTypeScript = daemonScript.endsWith('.ts');
|
|
103
|
+
const runner = isTypeScript ? 'node' : 'node';
|
|
104
|
+
const args = isTypeScript ? ['--import', 'tsx', daemonScript] : [daemonScript];
|
|
105
|
+
|
|
106
|
+
// Spawn daemon as detached process
|
|
107
|
+
const child = spawn(runner, args, {
|
|
108
|
+
detached: true,
|
|
109
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
110
|
+
env: process.env,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
child.unref();
|
|
114
|
+
|
|
115
|
+
// Wait for daemon to be ready (socket to exist)
|
|
116
|
+
const maxWait = 10000; // 10 seconds
|
|
117
|
+
const startTime = Date.now();
|
|
118
|
+
|
|
119
|
+
while (Date.now() - startTime < maxWait) {
|
|
120
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
121
|
+
|
|
122
|
+
if (existsSync(DAEMON_SOCKET_PATH)) {
|
|
123
|
+
console.error('[Proxy] Daemon started successfully');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new Error('Daemon failed to start within timeout');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensure daemon is running, start if needed
|
|
133
|
+
*/
|
|
134
|
+
async function ensureDaemon(): Promise<void> {
|
|
135
|
+
if (isDaemonRunning() && existsSync(DAEMON_SOCKET_PATH)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if we have required env vars to start daemon
|
|
140
|
+
if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHAT_ID) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'Daemon not running and cannot auto-start: TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID required.\n' +
|
|
143
|
+
'Either start daemon manually (telegram-claude-ctl start) or provide env vars.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await startDaemon();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class DaemonClient {
|
|
151
|
+
private socket: net.Socket | null = null;
|
|
152
|
+
private buffer = '';
|
|
153
|
+
private sessionId: string;
|
|
154
|
+
private sessionName: string;
|
|
155
|
+
private projectPath?: string;
|
|
156
|
+
private connected = false;
|
|
157
|
+
private pendingRequests = new Map<
|
|
158
|
+
string,
|
|
159
|
+
{ resolve: (result: any) => void; reject: (error: Error) => void }
|
|
160
|
+
>();
|
|
161
|
+
private reconnectAttempts = 0;
|
|
162
|
+
private maxReconnectAttempts = 5;
|
|
163
|
+
|
|
164
|
+
constructor(config: ProxyConfig) {
|
|
165
|
+
this.sessionId = generateSessionId();
|
|
166
|
+
this.sessionName = config.sessionName;
|
|
167
|
+
this.projectPath = config.projectPath;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async connect(): Promise<void> {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
this.socket = net.createConnection(DAEMON_SOCKET_PATH);
|
|
173
|
+
|
|
174
|
+
this.socket.on('connect', () => {
|
|
175
|
+
console.error(`[Proxy] Connected to daemon`);
|
|
176
|
+
this.reconnectAttempts = 0;
|
|
177
|
+
|
|
178
|
+
// Send connect message
|
|
179
|
+
const msg: ProxyConnectMessage = {
|
|
180
|
+
type: 'connect',
|
|
181
|
+
sessionId: this.sessionId,
|
|
182
|
+
sessionName: this.sessionName,
|
|
183
|
+
projectPath: this.projectPath,
|
|
184
|
+
};
|
|
185
|
+
this.socket!.write(serializeMessage(msg));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
this.socket.on('data', (data) => {
|
|
189
|
+
this.buffer += data.toString();
|
|
190
|
+
const { messages, remainder } = parseMessages<DaemonMessage>(this.buffer);
|
|
191
|
+
this.buffer = remainder;
|
|
192
|
+
|
|
193
|
+
for (const msg of messages) {
|
|
194
|
+
this.handleDaemonMessage(msg, resolve, reject);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.socket.on('close', () => {
|
|
199
|
+
console.error('[Proxy] Disconnected from daemon');
|
|
200
|
+
this.connected = false;
|
|
201
|
+
|
|
202
|
+
// Reject all pending requests
|
|
203
|
+
for (const [, pending] of this.pendingRequests) {
|
|
204
|
+
pending.reject(new Error('Disconnected from daemon'));
|
|
205
|
+
}
|
|
206
|
+
this.pendingRequests.clear();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.socket.on('error', (err) => {
|
|
210
|
+
console.error('[Proxy] Socket error:', err.message);
|
|
211
|
+
if (!this.connected) {
|
|
212
|
+
reject(new Error(`Failed to connect to daemon: ${err.message}. Is the daemon running?`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private handleDaemonMessage(
|
|
219
|
+
msg: DaemonMessage,
|
|
220
|
+
connectResolve?: (value: void) => void,
|
|
221
|
+
connectReject?: (error: Error) => void
|
|
222
|
+
): void {
|
|
223
|
+
console.error(`[Proxy] Received: ${msg.type}`);
|
|
224
|
+
|
|
225
|
+
switch (msg.type) {
|
|
226
|
+
case 'connected':
|
|
227
|
+
this.connected = true;
|
|
228
|
+
console.error(`[Proxy] Session registered: ${msg.sessionId}`);
|
|
229
|
+
console.error(`[Proxy] Daemon version: ${msg.daemonVersion}`);
|
|
230
|
+
connectResolve?.();
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'tool_result': {
|
|
234
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
235
|
+
if (pending) {
|
|
236
|
+
this.pendingRequests.delete(msg.requestId);
|
|
237
|
+
if (msg.success) {
|
|
238
|
+
pending.resolve(msg.result);
|
|
239
|
+
} else {
|
|
240
|
+
pending.reject(new Error(msg.error || 'Tool call failed'));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case 'error':
|
|
247
|
+
if (msg.requestId) {
|
|
248
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
249
|
+
if (pending) {
|
|
250
|
+
this.pendingRequests.delete(msg.requestId);
|
|
251
|
+
pending.reject(new Error(msg.error));
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
console.error(`[Proxy] Daemon error: ${msg.error}`);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case 'pong':
|
|
259
|
+
console.error(`[Proxy] Daemon alive, ${msg.activeSessions} active sessions`);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async callTool(
|
|
265
|
+
tool: string,
|
|
266
|
+
args: Record<string, unknown>
|
|
267
|
+
): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> {
|
|
268
|
+
if (!this.connected || !this.socket) {
|
|
269
|
+
throw new Error('Not connected to daemon');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const requestId = generateRequestId();
|
|
273
|
+
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
this.pendingRequests.set(requestId, { resolve, reject });
|
|
276
|
+
|
|
277
|
+
const msg: ProxyToolCallMessage = {
|
|
278
|
+
type: 'tool_call',
|
|
279
|
+
sessionId: this.sessionId,
|
|
280
|
+
requestId,
|
|
281
|
+
tool,
|
|
282
|
+
arguments: args,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
this.socket!.write(serializeMessage(msg));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
disconnect(): void {
|
|
290
|
+
if (this.socket && this.connected) {
|
|
291
|
+
const msg: ProxyDisconnectMessage = {
|
|
292
|
+
type: 'disconnect',
|
|
293
|
+
sessionId: this.sessionId,
|
|
294
|
+
};
|
|
295
|
+
this.socket.write(serializeMessage(msg));
|
|
296
|
+
this.socket.end();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
const config = loadConfig();
|
|
303
|
+
|
|
304
|
+
console.error('');
|
|
305
|
+
console.error('Telegram Claude MCP Proxy v' + VERSION);
|
|
306
|
+
console.error(`Session: ${config.sessionName}`);
|
|
307
|
+
console.error('');
|
|
308
|
+
|
|
309
|
+
// Ensure daemon is running (auto-start if possible)
|
|
310
|
+
try {
|
|
311
|
+
await ensureDaemon();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('[Proxy] ' + (error as Error).message);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Connect to daemon
|
|
318
|
+
const client = new DaemonClient(config);
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await client.connect();
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('[Proxy] Failed to connect to daemon:', (error as Error).message);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create MCP server
|
|
328
|
+
const mcpServer = new Server(
|
|
329
|
+
{ name: 'telegram-claude-proxy', version: VERSION },
|
|
330
|
+
{ capabilities: { tools: {} } }
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// List available tools (same as before)
|
|
334
|
+
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
335
|
+
return {
|
|
336
|
+
tools: [
|
|
337
|
+
{
|
|
338
|
+
name: 'send_message',
|
|
339
|
+
description:
|
|
340
|
+
'Send a message to the user via Telegram and wait for their response. Use when you need user input, want to report completed work, or need discussion.',
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: 'object',
|
|
343
|
+
properties: {
|
|
344
|
+
message: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'What you want to say to the user. Be clear and concise.',
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
required: ['message'],
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'continue_chat',
|
|
354
|
+
description: 'Continue an active chat with a follow-up message and wait for response.',
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
chat_id: { type: 'string', description: 'The chat ID from send_message' },
|
|
359
|
+
message: { type: 'string', description: 'Your follow-up message' },
|
|
360
|
+
},
|
|
361
|
+
required: ['chat_id', 'message'],
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'notify_user',
|
|
366
|
+
description:
|
|
367
|
+
'Send a notification to the user without waiting for a response. Use for status updates or acknowledgments.',
|
|
368
|
+
inputSchema: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
chat_id: { type: 'string', description: 'The chat ID from send_message' },
|
|
372
|
+
message: { type: 'string', description: 'The notification message' },
|
|
373
|
+
},
|
|
374
|
+
required: ['chat_id', 'message'],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'end_chat',
|
|
379
|
+
description: 'End an active chat session with an optional closing message.',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
chat_id: { type: 'string', description: 'The chat ID from send_message' },
|
|
384
|
+
message: { type: 'string', description: 'Optional closing message' },
|
|
385
|
+
},
|
|
386
|
+
required: ['chat_id'],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Handle tool calls by forwarding to daemon
|
|
394
|
+
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
395
|
+
const { name, arguments: args } = request.params;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const result = await client.callTool(name, args as Record<string, unknown>);
|
|
399
|
+
return result;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Connect MCP server via stdio
|
|
410
|
+
const transport = new StdioServerTransport();
|
|
411
|
+
await mcpServer.connect(transport);
|
|
412
|
+
|
|
413
|
+
console.error('[Proxy] MCP server ready');
|
|
414
|
+
|
|
415
|
+
// Graceful shutdown
|
|
416
|
+
const shutdown = async () => {
|
|
417
|
+
console.error('\n[Proxy] Shutting down...');
|
|
418
|
+
client.disconnect();
|
|
419
|
+
process.exit(0);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
process.on('SIGINT', shutdown);
|
|
423
|
+
process.on('SIGTERM', shutdown);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
main().catch((error) => {
|
|
427
|
+
console.error('Fatal error:', error);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared protocol definitions for daemon <-> proxy communication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Socket path for IPC
|
|
6
|
+
export const DAEMON_SOCKET_PATH = '/tmp/telegram-claude-daemon.sock';
|
|
7
|
+
export const DAEMON_PID_FILE = '/tmp/telegram-claude-daemon.pid';
|
|
8
|
+
export const DAEMON_HTTP_PORT = 3333;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Message types from proxy to daemon
|
|
12
|
+
*/
|
|
13
|
+
export type ProxyMessageType = 'connect' | 'disconnect' | 'tool_call' | 'ping';
|
|
14
|
+
|
|
15
|
+
export interface ProxyConnectMessage {
|
|
16
|
+
type: 'connect';
|
|
17
|
+
sessionId: string;
|
|
18
|
+
sessionName: string;
|
|
19
|
+
projectPath?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProxyDisconnectMessage {
|
|
23
|
+
type: 'disconnect';
|
|
24
|
+
sessionId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ProxyToolCallMessage {
|
|
28
|
+
type: 'tool_call';
|
|
29
|
+
sessionId: string;
|
|
30
|
+
requestId: string;
|
|
31
|
+
tool: string;
|
|
32
|
+
arguments: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProxyPingMessage {
|
|
36
|
+
type: 'ping';
|
|
37
|
+
sessionId: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ProxyMessage =
|
|
41
|
+
| ProxyConnectMessage
|
|
42
|
+
| ProxyDisconnectMessage
|
|
43
|
+
| ProxyToolCallMessage
|
|
44
|
+
| ProxyPingMessage;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Message types from daemon to proxy
|
|
48
|
+
*/
|
|
49
|
+
export type DaemonMessageType = 'connected' | 'disconnected' | 'tool_result' | 'error' | 'pong';
|
|
50
|
+
|
|
51
|
+
export interface DaemonConnectedMessage {
|
|
52
|
+
type: 'connected';
|
|
53
|
+
sessionId: string;
|
|
54
|
+
daemonVersion: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DaemonDisconnectedMessage {
|
|
58
|
+
type: 'disconnected';
|
|
59
|
+
sessionId: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DaemonToolResultMessage {
|
|
63
|
+
type: 'tool_result';
|
|
64
|
+
sessionId: string;
|
|
65
|
+
requestId: string;
|
|
66
|
+
success: boolean;
|
|
67
|
+
result?: {
|
|
68
|
+
content: Array<{ type: string; text: string }>;
|
|
69
|
+
isError?: boolean;
|
|
70
|
+
};
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DaemonErrorMessage {
|
|
75
|
+
type: 'error';
|
|
76
|
+
sessionId: string;
|
|
77
|
+
requestId?: string;
|
|
78
|
+
error: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DaemonPongMessage {
|
|
82
|
+
type: 'pong';
|
|
83
|
+
sessionId: string;
|
|
84
|
+
activeSessions: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type DaemonMessage =
|
|
88
|
+
| DaemonConnectedMessage
|
|
89
|
+
| DaemonDisconnectedMessage
|
|
90
|
+
| DaemonToolResultMessage
|
|
91
|
+
| DaemonErrorMessage
|
|
92
|
+
| DaemonPongMessage;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Session state tracked by daemon
|
|
96
|
+
*/
|
|
97
|
+
export interface SessionInfo {
|
|
98
|
+
sessionId: string;
|
|
99
|
+
sessionName: string;
|
|
100
|
+
projectPath?: string;
|
|
101
|
+
connectedAt: Date;
|
|
102
|
+
lastActivity: Date;
|
|
103
|
+
activeChats: Set<string>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Serialize message for IPC (newline-delimited JSON)
|
|
108
|
+
*/
|
|
109
|
+
export function serializeMessage(msg: ProxyMessage | DaemonMessage): string {
|
|
110
|
+
return JSON.stringify(msg) + '\n';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse messages from buffer (handles partial reads)
|
|
115
|
+
*/
|
|
116
|
+
export function parseMessages<T>(buffer: string): { messages: T[]; remainder: string } {
|
|
117
|
+
const lines = buffer.split('\n');
|
|
118
|
+
const remainder = lines.pop() || ''; // Last element might be incomplete
|
|
119
|
+
const messages: T[] = [];
|
|
120
|
+
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (line.trim()) {
|
|
123
|
+
try {
|
|
124
|
+
messages.push(JSON.parse(line) as T);
|
|
125
|
+
} catch {
|
|
126
|
+
console.error('[Protocol] Failed to parse message:', line);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { messages, remainder };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate unique session ID
|
|
136
|
+
*/
|
|
137
|
+
export function generateSessionId(): string {
|
|
138
|
+
return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate unique request ID for tool calls
|
|
143
|
+
*/
|
|
144
|
+
export function generateRequestId(): string {
|
|
145
|
+
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
146
|
+
}
|