telegram-claude-mcp 1.6.1 → 2.0.2
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 +122 -0
- package/bin/daemon-ctl.js +207 -0
- package/bin/daemon.js +20 -0
- package/bin/proxy.js +22 -0
- package/hooks/stop-hook.sh +2 -0
- package/hooks-v2/notify-hook.sh +32 -0
- package/hooks-v2/permission-hook.sh +43 -0
- package/hooks-v2/stop-hook.sh +47 -0
- package/package.json +16 -5
- package/src/daemon/index.ts +415 -0
- package/src/daemon/progress-display.ts +184 -0
- package/src/daemon/progress-tracker.ts +365 -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 +69 -69
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Architecture Options for Multi-Session MCP
|
|
2
|
+
|
|
3
|
+
This document describes three approaches for handling multiple Claude Code sessions with a shared Telegram bot.
|
|
4
|
+
|
|
5
|
+
## Current Architecture (Baseline)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────┐ stdio ┌─────────────────────────────────────┐
|
|
9
|
+
│ Claude Code 1 │◄──────────────►│ MCP Server 1 (port 3333) │
|
|
10
|
+
│ │ │ ├─ TelegramManager (polling) │──►┌──────────┐
|
|
11
|
+
│ │ │ └─ HTTP Hook Server │ │ │
|
|
12
|
+
└─────────────────┘ └─────────────────────────────────────┘ │ │
|
|
13
|
+
│ Telegram │
|
|
14
|
+
┌─────────────────┐ stdio ┌─────────────────────────────────────┐ │ Bot │
|
|
15
|
+
│ Claude Code 2 │◄──────────────►│ MCP Server 2 (port 3334) │ │ │
|
|
16
|
+
│ │ │ ├─ TelegramManager (polling) │──►│ │
|
|
17
|
+
│ │ │ └─ HTTP Hook Server │ └──────────┘
|
|
18
|
+
└─────────────────┘ └─────────────────────────────────────┘
|
|
19
|
+
|
|
20
|
+
Hook scripts discover ports via /tmp/telegram-claude-sessions/*.info files
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Problems:**
|
|
24
|
+
- Multiple bot polling instances (potential rate limiting)
|
|
25
|
+
- Port collision handling complexity
|
|
26
|
+
- Session discovery via filesystem
|
|
27
|
+
- Each process manages its own state
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Option 1: Proxy Pattern (Recommended)
|
|
32
|
+
|
|
33
|
+
A singleton daemon handles all Telegram communication. Thin MCP proxies forward requests.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌─────────────────┐ stdio ┌─────────────────┐ Unix Socket ┌─────────────────────────────┐
|
|
37
|
+
│ Claude Code 1 │◄──────────────►│ MCP Proxy 1 │◄───────────────────►│ │
|
|
38
|
+
│ │ │ (session: proj1)│ │ Singleton Daemon │
|
|
39
|
+
└─────────────────┘ └─────────────────┘ │ (single process) │
|
|
40
|
+
│ │
|
|
41
|
+
┌─────────────────┐ stdio ┌─────────────────┐ Unix Socket │ ├─ TelegramManager │──►┌──────────┐
|
|
42
|
+
│ Claude Code 2 │◄──────────────►│ MCP Proxy 2 │◄───────────────────►│ │ (single polling) │ │ Telegram │
|
|
43
|
+
│ │ │ (session: proj2)│ │ ├─ SessionManager │ │ Bot │
|
|
44
|
+
└─────────────────┘ └─────────────────┘ │ │ (tracks all sessions) │ └──────────┘
|
|
45
|
+
│ └─ HTTP Hook Server │
|
|
46
|
+
┌─────────────────┐ HTTP POST │ (single port: 3333) │
|
|
47
|
+
│ Hook Scripts │────────────────────►│ │
|
|
48
|
+
└─────────────────┘ └─────────────────────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Components
|
|
52
|
+
|
|
53
|
+
**1. Singleton Daemon (`telegram-claude-daemon`)**
|
|
54
|
+
- Single long-running process
|
|
55
|
+
- Manages one TelegramManager with single bot polling
|
|
56
|
+
- Exposes Unix socket for proxy connections
|
|
57
|
+
- Exposes single HTTP port for all hooks
|
|
58
|
+
- Tracks all active sessions in memory
|
|
59
|
+
- Routes messages to correct session
|
|
60
|
+
|
|
61
|
+
**2. MCP Proxy (`telegram-claude-proxy`)**
|
|
62
|
+
- Spawned by Claude Code (stdio transport)
|
|
63
|
+
- Connects to daemon via Unix socket
|
|
64
|
+
- Registers session with unique ID
|
|
65
|
+
- Forwards MCP tool calls to daemon
|
|
66
|
+
- Receives responses from daemon
|
|
67
|
+
|
|
68
|
+
### Protocol (Daemon <-> Proxy)
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Proxy -> Daemon
|
|
72
|
+
interface ProxyMessage {
|
|
73
|
+
type: 'connect' | 'disconnect' | 'tool_call';
|
|
74
|
+
sessionId: string;
|
|
75
|
+
sessionName: string;
|
|
76
|
+
payload?: {
|
|
77
|
+
tool: string;
|
|
78
|
+
arguments: Record<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Daemon -> Proxy
|
|
83
|
+
interface DaemonMessage {
|
|
84
|
+
type: 'connected' | 'tool_result' | 'error';
|
|
85
|
+
sessionId: string;
|
|
86
|
+
payload?: {
|
|
87
|
+
result?: unknown;
|
|
88
|
+
error?: string;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Advantages
|
|
94
|
+
- Single bot polling (no rate limiting)
|
|
95
|
+
- Single port for hooks (no discovery needed)
|
|
96
|
+
- Centralized session management
|
|
97
|
+
- Clean separation of concerns
|
|
98
|
+
- Daemon can outlive individual sessions
|
|
99
|
+
|
|
100
|
+
### Disadvantages
|
|
101
|
+
- Requires daemon to be running before Claude Code starts
|
|
102
|
+
- Additional IPC complexity
|
|
103
|
+
- Single point of failure
|
|
104
|
+
|
|
105
|
+
### Implementation Files
|
|
106
|
+
```
|
|
107
|
+
server/src/
|
|
108
|
+
├── daemon/
|
|
109
|
+
│ ├── index.ts # Daemon entry point
|
|
110
|
+
│ ├── session-manager.ts # Track active sessions
|
|
111
|
+
│ └── ipc-server.ts # Unix socket server
|
|
112
|
+
├── proxy/
|
|
113
|
+
│ ├── index.ts # Proxy entry point (MCP stdio)
|
|
114
|
+
│ └── ipc-client.ts # Unix socket client
|
|
115
|
+
└── shared/
|
|
116
|
+
└── protocol.ts # Shared types and constants
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Option 2: Shared State Pattern
|
|
122
|
+
|
|
123
|
+
Keep MCP-per-session but share Telegram connection via IPC to a daemon.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
┌─────────────────┐ stdio ┌─────────────────────────────────────┐
|
|
127
|
+
│ Claude Code 1 │◄──────────────►│ MCP Server 1 │
|
|
128
|
+
│ │ │ ├─ Full MCP implementation │
|
|
129
|
+
│ │ │ ├─ HTTP Hook Server (port 3333) │───┐
|
|
130
|
+
└─────────────────┘ │ └─ IPC Client │ │
|
|
131
|
+
└─────────────────────────────────────┘ │
|
|
132
|
+
│ │
|
|
133
|
+
│ IPC (send/receive) │ HTTP
|
|
134
|
+
▼ │
|
|
135
|
+
┌─────────────────────────────────────┐ │
|
|
136
|
+
│ Telegram Daemon │ │
|
|
137
|
+
│ ├─ TelegramManager (single poll) │──►┌──────────┐
|
|
138
|
+
│ └─ Message Router │ │ Telegram │
|
|
139
|
+
└─────────────────────────────────────┘ │ Bot │
|
|
140
|
+
▲ └──────────┘
|
|
141
|
+
│ IPC (send/receive)
|
|
142
|
+
│
|
|
143
|
+
┌─────────────────┐ stdio ┌─────────────────────────────────────┐
|
|
144
|
+
│ Claude Code 2 │◄──────────────►│ MCP Server 2 │
|
|
145
|
+
│ │ │ ├─ Full MCP implementation │
|
|
146
|
+
│ │ │ ├─ HTTP Hook Server (port 3334) │
|
|
147
|
+
│ │ │ └─ IPC Client │
|
|
148
|
+
└─────────────────┘ └─────────────────────────────────────┘
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### How It Works
|
|
152
|
+
1. Each MCP server maintains its own HTTP hook server
|
|
153
|
+
2. Telegram operations are forwarded to a shared daemon
|
|
154
|
+
3. Daemon handles actual Telegram API calls
|
|
155
|
+
4. Responses routed back via IPC
|
|
156
|
+
|
|
157
|
+
### Advantages
|
|
158
|
+
- MCP servers remain mostly unchanged
|
|
159
|
+
- Gradual migration path
|
|
160
|
+
- Each session still has its own hook endpoint
|
|
161
|
+
|
|
162
|
+
### Disadvantages
|
|
163
|
+
- Still requires port management for hooks
|
|
164
|
+
- More complex message routing
|
|
165
|
+
- Partial duplication of logic
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Option 3: Session ID in Tool Calls
|
|
170
|
+
|
|
171
|
+
Single MCP server process, sessions identified by explicit IDs in every tool call.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
┌─────────────────┐ ┌─────────────────────────────┐
|
|
175
|
+
│ Claude Code 1 │──┐ │ │
|
|
176
|
+
│ │ │ Multiple stdio │ Singleton MCP Server │
|
|
177
|
+
└─────────────────┘ │ connections via │ │
|
|
178
|
+
│ wrapper/multiplexer │ ├─ TelegramManager │──►┌──────────┐
|
|
179
|
+
┌─────────────────┐ │ │ ├─ SessionManager │ │ Telegram │
|
|
180
|
+
│ Claude Code 2 │──┼──────────────────────────►│ └─ HTTP Hook Server │ │ Bot │
|
|
181
|
+
│ │ │ │ │ └──────────┘
|
|
182
|
+
└─────────────────┘ │ │ Tools include session_id: │
|
|
183
|
+
│ │ - send_message(session_id) │
|
|
184
|
+
┌─────────────────┐ │ │ - continue_chat(session_id)│
|
|
185
|
+
│ Claude Code 3 │──┘ │ │
|
|
186
|
+
└─────────────────┘ └─────────────────────────────┘
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### How It Works
|
|
190
|
+
1. Single MCP server handles all sessions
|
|
191
|
+
2. `connect` tool registers new session, returns session_id
|
|
192
|
+
3. All other tools require session_id parameter
|
|
193
|
+
4. Server routes operations by session_id
|
|
194
|
+
|
|
195
|
+
### Challenges
|
|
196
|
+
- MCP's stdio transport is 1:1 (one client per process)
|
|
197
|
+
- Would need a multiplexer/wrapper to share one MCP server
|
|
198
|
+
- Violates MCP design assumptions
|
|
199
|
+
- Complex error handling when sessions disconnect
|
|
200
|
+
|
|
201
|
+
### Advantages
|
|
202
|
+
- Simplest daemon architecture
|
|
203
|
+
- Single process for everything
|
|
204
|
+
|
|
205
|
+
### Disadvantages
|
|
206
|
+
- Requires custom MCP multiplexer
|
|
207
|
+
- Not standard MCP usage
|
|
208
|
+
- Harder to implement correctly
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Comparison Matrix
|
|
213
|
+
|
|
214
|
+
| Aspect | Current | Option 1 (Proxy) | Option 2 (Shared) | Option 3 (Session ID) |
|
|
215
|
+
|---------------------------|--------------|-------------------|-------------------|----------------------|
|
|
216
|
+
| Bot polling instances | N | 1 | 1 | 1 |
|
|
217
|
+
| Port management | Complex | Simple (1 port) | Complex | Simple (1 port) |
|
|
218
|
+
| MCP compatibility | Standard | Standard | Standard | Non-standard |
|
|
219
|
+
| Implementation effort | Done | Medium | Medium-High | High |
|
|
220
|
+
| Session isolation | Full | Full | Full | Logical only |
|
|
221
|
+
| Daemon dependency | No | Yes | Yes | Yes |
|
|
222
|
+
| Hook simplicity | Medium | Simple | Medium | Simple |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Recommended: Option 1 (Proxy Pattern)
|
|
227
|
+
|
|
228
|
+
Option 1 provides the best balance of:
|
|
229
|
+
- **Simplicity**: Single port, single polling instance
|
|
230
|
+
- **Compatibility**: Standard MCP stdio transport
|
|
231
|
+
- **Reliability**: Clean session management
|
|
232
|
+
- **Maintainability**: Clear separation between proxy and daemon
|
|
233
|
+
|
|
234
|
+
The proxy is lightweight and only handles MCP protocol translation, while the daemon manages all Telegram complexity.
|
package/README.md
CHANGED
|
@@ -353,6 +353,128 @@ Each Claude config needs a unique `SESSION_NAME` in **both** the MCP server env
|
|
|
353
353
|
- Verify hook scripts are executable: `ls -la ~/.claude/hooks/`
|
|
354
354
|
- Check Claude settings have hooks configured
|
|
355
355
|
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## v2: Daemon Architecture (Recommended for Multiple Sessions)
|
|
359
|
+
|
|
360
|
+
Version 2 introduces a singleton daemon architecture that handles all Telegram communication from one process. This solves:
|
|
361
|
+
- Multiple bot polling instances causing rate limiting
|
|
362
|
+
- Complex port discovery between sessions
|
|
363
|
+
- Session state synchronization issues
|
|
364
|
+
|
|
365
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed design documentation.
|
|
366
|
+
|
|
367
|
+
### Quick Start with Daemon
|
|
368
|
+
|
|
369
|
+
**Option A: Auto-start (recommended)**
|
|
370
|
+
|
|
371
|
+
The proxy can auto-start the daemon if you provide credentials in the MCP config:
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"mcpServers": {
|
|
376
|
+
"telegram": {
|
|
377
|
+
"command": "npx",
|
|
378
|
+
"args": ["-y", "telegram-claude-proxy"],
|
|
379
|
+
"env": {
|
|
380
|
+
"SESSION_NAME": "main",
|
|
381
|
+
"TELEGRAM_BOT_TOKEN": "YOUR_BOT_TOKEN",
|
|
382
|
+
"TELEGRAM_CHAT_ID": "YOUR_CHAT_ID"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The first Claude session will start the daemon automatically. Subsequent sessions connect to the already-running daemon.
|
|
390
|
+
|
|
391
|
+
**Option B: Manual daemon start**
|
|
392
|
+
|
|
393
|
+
Start the daemon once, then proxies connect without needing credentials:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Start daemon with credentials
|
|
397
|
+
TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=yyy telegram-claude-ctl start
|
|
398
|
+
|
|
399
|
+
# Check status
|
|
400
|
+
telegram-claude-ctl status
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
2. **Configure Claude to use the proxy** in `~/.claude/settings.json`:
|
|
404
|
+
```json
|
|
405
|
+
{
|
|
406
|
+
"hooks": {
|
|
407
|
+
"PermissionRequest": [{
|
|
408
|
+
"matcher": "*",
|
|
409
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks-v2/permission-hook.sh" }]
|
|
410
|
+
}],
|
|
411
|
+
"Stop": [{
|
|
412
|
+
"matcher": "*",
|
|
413
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks-v2/stop-hook.sh" }]
|
|
414
|
+
}],
|
|
415
|
+
"Notification": [{
|
|
416
|
+
"matcher": "*",
|
|
417
|
+
"hooks": [{ "type": "command", "command": "SESSION_NAME=main ~/.claude/hooks-v2/notify-hook.sh" }]
|
|
418
|
+
}]
|
|
419
|
+
},
|
|
420
|
+
"mcpServers": {
|
|
421
|
+
"telegram": {
|
|
422
|
+
"command": "npx",
|
|
423
|
+
"args": ["-y", "telegram-claude-proxy"],
|
|
424
|
+
"env": {
|
|
425
|
+
"SESSION_NAME": "main"
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Daemon Commands
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
# Start daemon in background
|
|
436
|
+
telegram-claude-ctl start
|
|
437
|
+
|
|
438
|
+
# Stop daemon
|
|
439
|
+
telegram-claude-ctl stop
|
|
440
|
+
|
|
441
|
+
# Restart daemon
|
|
442
|
+
telegram-claude-ctl restart
|
|
443
|
+
|
|
444
|
+
# Check status (shows active sessions)
|
|
445
|
+
telegram-claude-ctl status
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Architecture Overview
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
┌─────────────────┐ stdio ┌─────────────────┐ Unix Socket ┌─────────────────────┐
|
|
452
|
+
│ Claude Code 1 │◄──────────────►│ MCP Proxy 1 │◄───────────────────►│ │
|
|
453
|
+
└─────────────────┘ └─────────────────┘ │ Singleton Daemon │
|
|
454
|
+
│ │
|
|
455
|
+
┌─────────────────┐ stdio ┌─────────────────┐ Unix Socket │ - Single bot poll │──► Telegram
|
|
456
|
+
│ Claude Code 2 │◄──────────────►│ MCP Proxy 2 │◄───────────────────►│ - Session manager │
|
|
457
|
+
└─────────────────┘ └─────────────────┘ │ - HTTP hooks :3333 │
|
|
458
|
+
└─────────────────────┘
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Environment Variables (Daemon)
|
|
462
|
+
|
|
463
|
+
| Variable | Description | Default |
|
|
464
|
+
|----------|-------------|---------|
|
|
465
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Required |
|
|
466
|
+
| `TELEGRAM_CHAT_ID` | Your Telegram chat ID | Required |
|
|
467
|
+
| `CHAT_RESPONSE_TIMEOUT_MS` | Response timeout | 600000 (10 min) |
|
|
468
|
+
| `PERMISSION_TIMEOUT_MS` | Permission timeout | 600000 (10 min) |
|
|
469
|
+
|
|
470
|
+
### Environment Variables (Proxy/Hooks)
|
|
471
|
+
|
|
472
|
+
| Variable | Description | Default |
|
|
473
|
+
|----------|-------------|---------|
|
|
474
|
+
| `SESSION_NAME` | Session identifier | Auto-detected from CWD |
|
|
475
|
+
| `TELEGRAM_CLAUDE_PORT` | Daemon HTTP port | 3333 |
|
|
476
|
+
| `TELEGRAM_CLAUDE_HOST` | Daemon host | localhost |
|
|
477
|
+
|
|
356
478
|
## License
|
|
357
479
|
|
|
358
480
|
MIT
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Daemon Control CLI
|
|
5
|
+
*
|
|
6
|
+
* Manage the telegram-claude-daemon:
|
|
7
|
+
* daemon-ctl start - Start the daemon
|
|
8
|
+
* daemon-ctl stop - Stop the daemon
|
|
9
|
+
* daemon-ctl restart - Restart the daemon
|
|
10
|
+
* daemon-ctl status - Check daemon status
|
|
11
|
+
* daemon-ctl logs - Show daemon logs (if running with pm2 or similar)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn, execSync } from 'child_process';
|
|
15
|
+
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
|
|
19
|
+
const DAEMON_PID_FILE = '/tmp/telegram-claude-daemon.pid';
|
|
20
|
+
const DAEMON_SOCKET_PATH = '/tmp/telegram-claude-daemon.sock';
|
|
21
|
+
const DAEMON_HTTP_PORT = 3333;
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
const daemonScript = join(__dirname, '..', 'dist', 'daemon', 'index.js');
|
|
26
|
+
const daemonSrc = join(__dirname, '..', 'src', 'daemon', 'index.ts');
|
|
27
|
+
|
|
28
|
+
function getPid() {
|
|
29
|
+
if (existsSync(DAEMON_PID_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
const pid = parseInt(readFileSync(DAEMON_PID_FILE, 'utf-8').trim(), 10);
|
|
32
|
+
// Check if process is alive
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
return pid;
|
|
36
|
+
} catch {
|
|
37
|
+
// Process not running, clean up stale files
|
|
38
|
+
cleanup();
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cleanup() {
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(DAEMON_PID_FILE)) unlinkSync(DAEMON_PID_FILE);
|
|
51
|
+
if (existsSync(DAEMON_SOCKET_PATH)) unlinkSync(DAEMON_SOCKET_PATH);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getStatus() {
|
|
56
|
+
const pid = getPid();
|
|
57
|
+
if (!pid) {
|
|
58
|
+
return { running: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(`http://localhost:${DAEMON_HTTP_PORT}/status`);
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
return { running: true, pid, ...data };
|
|
65
|
+
} catch {
|
|
66
|
+
return { running: true, pid, sessions: [] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function start() {
|
|
71
|
+
const pid = getPid();
|
|
72
|
+
if (pid) {
|
|
73
|
+
console.log(`Daemon already running (PID: ${pid})`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Determine which script to run
|
|
78
|
+
let script = daemonScript;
|
|
79
|
+
let runner = 'node';
|
|
80
|
+
|
|
81
|
+
if (!existsSync(daemonScript)) {
|
|
82
|
+
if (existsSync(daemonSrc)) {
|
|
83
|
+
script = daemonSrc;
|
|
84
|
+
runner = 'bun';
|
|
85
|
+
} else {
|
|
86
|
+
console.error('Error: Daemon script not found. Run `npm run build` first.');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('Starting telegram-claude-daemon...');
|
|
92
|
+
|
|
93
|
+
const child = spawn(runner, [script], {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
env: process.env,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
child.unref();
|
|
100
|
+
|
|
101
|
+
// Wait for daemon to start
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
103
|
+
|
|
104
|
+
const newPid = getPid();
|
|
105
|
+
if (newPid) {
|
|
106
|
+
console.log(`Daemon started (PID: ${newPid})`);
|
|
107
|
+
console.log(` Socket: ${DAEMON_SOCKET_PATH}`);
|
|
108
|
+
console.log(` HTTP: http://localhost:${DAEMON_HTTP_PORT}`);
|
|
109
|
+
} else {
|
|
110
|
+
console.error('Failed to start daemon');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stop() {
|
|
116
|
+
const pid = getPid();
|
|
117
|
+
if (!pid) {
|
|
118
|
+
console.log('Daemon is not running');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`Stopping daemon (PID: ${pid})...`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
process.kill(pid, 'SIGTERM');
|
|
126
|
+
|
|
127
|
+
// Wait for process to exit
|
|
128
|
+
let attempts = 0;
|
|
129
|
+
while (attempts < 10) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
execSync('sleep 0.5');
|
|
133
|
+
attempts++;
|
|
134
|
+
} catch {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
cleanup();
|
|
140
|
+
console.log('Daemon stopped');
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error('Failed to stop daemon:', err.message);
|
|
143
|
+
cleanup();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function status() {
|
|
148
|
+
const info = await getStatus();
|
|
149
|
+
|
|
150
|
+
if (!info.running) {
|
|
151
|
+
console.log('Daemon: not running');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('Daemon: running');
|
|
156
|
+
console.log(` PID: ${info.pid}`);
|
|
157
|
+
console.log(` Version: ${info.version || 'unknown'}`);
|
|
158
|
+
console.log(` Active Sessions: ${info.activeSessions || 0}`);
|
|
159
|
+
|
|
160
|
+
if (info.sessions && info.sessions.length > 0) {
|
|
161
|
+
console.log(' Sessions:');
|
|
162
|
+
for (const session of info.sessions) {
|
|
163
|
+
console.log(` - ${session.sessionName} (${session.sessionId})`);
|
|
164
|
+
console.log(` Connected: ${session.connectedAt}`);
|
|
165
|
+
console.log(` Last Activity: ${session.lastActivity}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
const command = process.argv[2];
|
|
172
|
+
|
|
173
|
+
switch (command) {
|
|
174
|
+
case 'start':
|
|
175
|
+
await start();
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'stop':
|
|
179
|
+
stop();
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case 'restart':
|
|
183
|
+
stop();
|
|
184
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
185
|
+
await start();
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'status':
|
|
189
|
+
await status();
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
default:
|
|
193
|
+
console.log('Usage: daemon-ctl <command>');
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log('Commands:');
|
|
196
|
+
console.log(' start - Start the daemon');
|
|
197
|
+
console.log(' stop - Stop the daemon');
|
|
198
|
+
console.log(' restart - Restart the daemon');
|
|
199
|
+
console.log(' status - Check daemon status');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main().catch((err) => {
|
|
205
|
+
console.error('Error:', err.message);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
package/bin/daemon.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram Claude Daemon launcher
|
|
5
|
+
*
|
|
6
|
+
* Usage: telegram-claude-daemon
|
|
7
|
+
*
|
|
8
|
+
* Required environment variables:
|
|
9
|
+
* TELEGRAM_BOT_TOKEN - Your Telegram bot token
|
|
10
|
+
* TELEGRAM_CHAT_ID - Your Telegram chat ID
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { register } from 'node:module';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
|
|
16
|
+
// Register tsx loader for TypeScript support
|
|
17
|
+
register('tsx', pathToFileURL('./'));
|
|
18
|
+
|
|
19
|
+
// Import and run the daemon
|
|
20
|
+
import('../src/daemon/index.ts');
|
package/bin/proxy.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram Claude MCP Proxy launcher
|
|
5
|
+
*
|
|
6
|
+
* Usage: telegram-claude-proxy
|
|
7
|
+
*
|
|
8
|
+
* This connects to a running telegram-claude-daemon via Unix socket.
|
|
9
|
+
* Make sure to start the daemon first: telegram-claude-ctl start
|
|
10
|
+
*
|
|
11
|
+
* Optional environment variables:
|
|
12
|
+
* SESSION_NAME - Session identifier (default: auto-detected from CWD)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { register } from 'node:module';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
|
|
18
|
+
// Register tsx loader for TypeScript support
|
|
19
|
+
register('tsx', pathToFileURL('./'));
|
|
20
|
+
|
|
21
|
+
// Import and run the proxy
|
|
22
|
+
import('../src/proxy/index.ts');
|
package/hooks/stop-hook.sh
CHANGED
|
@@ -108,6 +108,8 @@ REASON=$(echo "$RESPONSE" | jq -r '.reason // empty')
|
|
|
108
108
|
if [ "$DECISION" = "block" ] && [ -n "$REASON" ]; then
|
|
109
109
|
# User provided instructions - continue with them
|
|
110
110
|
echo "$RESPONSE"
|
|
111
|
+
# Exit code 2 tells Claude Code to continue with the reason as instructions
|
|
112
|
+
exit 2
|
|
111
113
|
else
|
|
112
114
|
# User said done or no response - allow stop
|
|
113
115
|
exit 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Notify hook for telegram-claude-daemon (v2)
|
|
3
|
+
# Uses single daemon port instead of session discovery
|
|
4
|
+
|
|
5
|
+
DAEMON_PORT="${TELEGRAM_CLAUDE_PORT:-3333}"
|
|
6
|
+
DAEMON_HOST="${TELEGRAM_CLAUDE_HOST:-localhost}"
|
|
7
|
+
HOOK_URL="http://${DAEMON_HOST}:${DAEMON_PORT}/notify"
|
|
8
|
+
|
|
9
|
+
# Read input from stdin
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Notification"')
|
|
12
|
+
|
|
13
|
+
# Build payload
|
|
14
|
+
if [ -n "$SESSION_NAME" ]; then
|
|
15
|
+
PAYLOAD=$(jq -n \
|
|
16
|
+
--arg type "notification" \
|
|
17
|
+
--arg message "$MESSAGE" \
|
|
18
|
+
--arg session_name "$SESSION_NAME" \
|
|
19
|
+
'{type: $type, message: $message, session_name: $session_name}')
|
|
20
|
+
else
|
|
21
|
+
PAYLOAD=$(jq -n \
|
|
22
|
+
--arg type "notification" \
|
|
23
|
+
--arg message "$MESSAGE" \
|
|
24
|
+
'{type: $type, message: $message}')
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Send to daemon (non-blocking, 10 second timeout)
|
|
28
|
+
curl -s -X POST "$HOOK_URL" \
|
|
29
|
+
-H "Content-Type: application/json" \
|
|
30
|
+
-H "X-Session-Name: ${SESSION_NAME:-default}" \
|
|
31
|
+
-d "$PAYLOAD" \
|
|
32
|
+
--max-time 10 >/dev/null 2>&1
|