claude-code-telegrammer 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- claude_code_telegrammer-0.4.2/.env.example +20 -0
- claude_code_telegrammer-0.4.2/.mcp.json.example +14 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/PKG-INFO +81 -31
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/README.md +80 -30
- claude_code_telegrammer-0.4.2/bin/claude-code-telegrammer-hook +187 -0
- claude_code_telegrammer-0.4.2/bin/claude-code-telegrammer-init +104 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/pyproject.toml +1 -1
- claude_code_telegrammer-0.4.2/src/claude_code_telegrammer/__init__.py +7 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/poller.ts +50 -2
- claude_code_telegrammer-0.4.0/src/claude_code_telegrammer/__init__.py +0 -35
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.github/workflows/publish-pypi.yml +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.github/workflows/test.yml +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.gitignore +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.readthedocs.yaml +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/LICENSE +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/scitex-icon-navy-inverted.png +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/scitex-logo-blue-cropped.png +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/_static/.gitkeep +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/_templates/.gitkeep +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/api/claude_code_telegrammer.rst +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/conf.py +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/index.rst +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/installation.rst +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/quickstart.rst +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/requirements.txt +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/hooks/enforce_background_subagents.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/auto-response.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/common.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/lock.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/orochi-relay.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/state-detection.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer/_skills/claude-code-telegrammer/SKILL.md +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/PKG-INFO +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/SOURCES.txt +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/dependency_links.txt +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/entry_points.txt +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/top_level.txt +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/tests/test-state-detection.sh +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/.gitignore +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/bun.lock +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/bunfig.toml +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/dist/telegram-server.js +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/access.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/attachments.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/config.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/lock.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/log.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/store.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/telegram-api.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/tools.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/package.json +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/telegram-server.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/access.test.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/config.test.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/lock.test.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/preload.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/store.test.ts +0 -0
- {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/telegram-api.test.ts +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Claude Code Telegrammer — Environment Variables
|
|
2
|
+
# Copy to .env and fill in your values
|
|
3
|
+
|
|
4
|
+
# Required: Telegram bot token from @BotFather
|
|
5
|
+
CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN=123456789:AAH...
|
|
6
|
+
|
|
7
|
+
# Required: Comma-separated Telegram user IDs allowed to send messages
|
|
8
|
+
CLAUDE_CODE_TELEGRAMMER_TELEGRAM_ALLOWED_USERS=YOUR_TELEGRAM_USER_ID
|
|
9
|
+
|
|
10
|
+
# Optional: State directory (default: ~/.claude-code-telegrammer)
|
|
11
|
+
CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR=~/.claude-code-telegrammer
|
|
12
|
+
|
|
13
|
+
# Optional: Hostname for message metadata (default: os.hostname())
|
|
14
|
+
CLAUDE_CODE_TELEGRAMMER_TELEGRAM_HOST_NAME=
|
|
15
|
+
|
|
16
|
+
# Optional: Project identifier for message metadata (default: cwd)
|
|
17
|
+
CLAUDE_CODE_TELEGRAMMER_TELEGRAM_PROJECT=
|
|
18
|
+
|
|
19
|
+
# Optional: Agent ID for message metadata (default: 'telegram')
|
|
20
|
+
CLAUDE_CODE_TELEGRAMMER_TELEGRAM_AGENT_ID=
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"claude-code-telegrammer": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "bun",
|
|
6
|
+
"args": ["run", "/path/to/claude-code-telegrammer/ts/telegram-server.ts"],
|
|
7
|
+
"env": {
|
|
8
|
+
"CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN": "123456789:AAH...",
|
|
9
|
+
"CLAUDE_CODE_TELEGRAMMER_TELEGRAM_ALLOWED_USERS": "YOUR_TELEGRAM_USER_ID",
|
|
10
|
+
"CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR": "~/.claude-code-telegrammer"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-telegrammer
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Custom Telegram MCP server + TUI auto-responder for running Claude Code as an autonomous Telegram agent
|
|
5
5
|
Project-URL: Homepage, https://github.com/ywatanabe1989/claude-code-telegrammer
|
|
6
6
|
Project-URL: Repository, https://github.com/ywatanabe1989/claude-code-telegrammer.git
|
|
@@ -62,23 +62,63 @@ Description-Content-Type: text/markdown
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
-
## Problem
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
65
|
+
## Problem and Solution
|
|
66
|
+
|
|
67
|
+
<table>
|
|
68
|
+
<tr>
|
|
69
|
+
<th align="center">#</th>
|
|
70
|
+
<th>Problem</th>
|
|
71
|
+
<th>Solution</th>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr valign="top">
|
|
74
|
+
<td align="center">1</td>
|
|
75
|
+
<td><h4>Hardcoded paths</h4>The official plugin hardcodes <code>~/.claude/</code> as its state directory (<a href="https://github.com/anthropics/claude-code/issues/851">#851</a>), making it impossible to run multiple bots or customize where access.json lives.</td>
|
|
76
|
+
<td><h4>Configurable state directory</h4>All state (DB, lock, access config) lives under <code>CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR</code>. Run as many bots as you want, each with its own isolated state.</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr valign="top">
|
|
79
|
+
<td align="center">2</td>
|
|
80
|
+
<td><h4>409 Conflict crashes</h4>No single-instance guard — multiple sessions polling the same bot get 409 errors and crash each other (<a href="https://github.com/anthropics/claude-code/issues/1075">#1075</a>).</td>
|
|
81
|
+
<td><h4>PID-based lock</h4>Automatic single-instance enforcement via PID lock file. Second instance detects the conflict and waits instead of crashing.</td>
|
|
82
|
+
</tr>
|
|
83
|
+
<tr valign="top">
|
|
84
|
+
<td align="center">3</td>
|
|
85
|
+
<td><h4>Zombie CPU consumption</h4>After session ends, the plugin process lingers at 100% CPU — requires manual kill (<a href="https://github.com/anthropics/claude-code/issues/1146">#1146</a>).</td>
|
|
86
|
+
<td><h4>Clean shutdown</h4>Exits gracefully on stdin close, SIGTERM, or SIGINT. No zombies, no manual cleanup.</td>
|
|
87
|
+
</tr>
|
|
88
|
+
<tr valign="top">
|
|
89
|
+
<td align="center">4</td>
|
|
90
|
+
<td><h4>Only 3 basic tools</h4>The official plugin provides just send, get_updates, and set_reaction — no history, no search, no file handling, no message editing.</td>
|
|
91
|
+
<td><h4>10 MCP tools</h4>reply, react, edit_message, get_history, get_unread, mark_read, download_attachment, send_document, search_messages, get_context — everything an autonomous agent needs.</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<tr valign="top">
|
|
94
|
+
<td align="center">5</td>
|
|
95
|
+
<td><h4>No message persistence</h4>Messages vanish after delivery. No way to search past conversations, track read status, or build context from history.</td>
|
|
96
|
+
<td><h4>SQLite message store</h4>All messages persisted in WAL-mode SQLite with full-text search, reply threading (reply_to_message_id), read/replied tracking, and attachment metadata.</td>
|
|
97
|
+
</tr>
|
|
98
|
+
<tr valign="top">
|
|
99
|
+
<td align="center">6</td>
|
|
100
|
+
<td><h4>No access control for groups</h4>Basic allowlist only — no per-group policies, no hot-reload when config changes.</td>
|
|
101
|
+
<td><h4>DM + group policies</h4>Allowlist-based access control with separate DM and group chat policies via <code>access.json</code>, hot-reloaded on file change (mtime-based).</td>
|
|
102
|
+
</tr>
|
|
103
|
+
<tr valign="top">
|
|
104
|
+
<td align="center">7</td>
|
|
105
|
+
<td><h4>No attachment support</h4>Cannot download inbound files or upload documents to chats.</td>
|
|
106
|
+
<td><h4>Full attachment handling</h4>Inbound photos, documents, voice, audio, and video are auto-downloaded. Upload local files via <code>send_document</code> tool.</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr valign="top">
|
|
109
|
+
<td align="center">8</td>
|
|
110
|
+
<td><h4>Sessions stall unattended</h4>Claude Code halts at permission prompts or idle states with no way to recover — the agent just stops working.</td>
|
|
111
|
+
<td><h4>TUI Watchdog</h4>Polls GNU Screen buffer, detects TUI state via pattern matching, sends keystrokes to auto-accept prompts and re-engage on idle. Throttled with burst limits.</td>
|
|
112
|
+
</tr>
|
|
113
|
+
</table>
|
|
114
|
+
|
|
115
|
+
<p align="center"><sub><b>Table 1.</b> Eight issues with the official Telegram plugin (as of April 2026) and how claude-code-telegrammer addresses each. These problems make the official plugin unusable for production autonomous agents.</sub></p>
|
|
116
|
+
|
|
117
|
+
### Architecture
|
|
118
|
+
|
|
119
|
+
1. **Custom Telegram MCP Server** (`ts/`) -- Self-contained Bun + MCP server. 10 tools, SQLite persistence, allowlist access control, attachment handling, reaction support. Incoming messages acknowledged with 📩. Built-in responsiveness policy directs the agent to reply immediately and delegate heavy work to background subagents.
|
|
120
|
+
|
|
121
|
+
2. **TUI Watchdog** (`lib/`) -- Polls a GNU Screen session, detects Claude Code's TUI state via pattern matching, and sends keystrokes to keep the agent running unattended (auto-accepts permission prompts, re-engages on idle). Throttled with burst limits to prevent runaway responses. Orchestration handled by [scitex-agent-container](https://github.com/ywatanabe1989/scitex-agent-container).
|
|
82
122
|
|
|
83
123
|
<details>
|
|
84
124
|
<summary><strong>MCP Tools (10)</strong></summary>
|
|
@@ -98,28 +138,38 @@ Two subsystems that replace and extend the official plugin:
|
|
|
98
138
|
|
|
99
139
|
</details>
|
|
100
140
|
|
|
141
|
+
### Important: Bot Token Exclusivity
|
|
142
|
+
|
|
143
|
+
This MCP server **must be the sole consumer** of its configured Telegram bot token. The Telegram Bot API allows only one `getUpdates` long-polling connection per token.
|
|
144
|
+
|
|
145
|
+
**What happens with duplicate consumers:**
|
|
146
|
+
|
|
147
|
+
| Scenario | Symptom | Detection |
|
|
148
|
+
|----------|---------|-----------|
|
|
149
|
+
| Two pollers start simultaneously | One gets 409, the other wins silently | The loser sees `409 Conflict` in logs |
|
|
150
|
+
| Two pollers start sequentially | Both appear to work, but only one receives messages | **No error** — the other poller gets empty responses forever |
|
|
151
|
+
| Webhook active + poller | Poller gets nothing | **No error** — Telegram ignores `getUpdates` when webhook is set |
|
|
152
|
+
|
|
153
|
+
**Why 409 detection alone is insufficient:** The Telegram API does not reliably return 409 for all conflict cases. When two consumers poll sequentially (not overlapping), both connections succeed — one simply receives all messages while the other gets none, with no error. The server performs a `timeout=3` preflight check at startup to catch overlapping polls, but this cannot detect the sequential case.
|
|
154
|
+
|
|
155
|
+
**If messages aren't arriving:**
|
|
156
|
+
1. Check if another process is polling the same token: `ps aux | grep telegram-server`
|
|
157
|
+
2. Check if a webhook is set: `curl https://api.telegram.org/bot<TOKEN>/getWebhookInfo`
|
|
158
|
+
3. Use a separate bot token per component (recommended)
|
|
159
|
+
4. Or disable the other consumer
|
|
160
|
+
|
|
101
161
|
## Installation
|
|
102
162
|
|
|
103
163
|
### Prerequisites
|
|
104
164
|
|
|
105
|
-
- Python >= 3.10 and GNU Screen (for watchdog/CLI)
|
|
106
165
|
- [Bun](https://bun.sh/) >= 1.0 (for the MCP server)
|
|
166
|
+
- GNU Screen (for watchdog, optional)
|
|
107
167
|
|
|
108
168
|
### Install
|
|
109
169
|
|
|
110
|
-
```bash
|
|
111
|
-
pip install claude-code-telegrammer
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Or from source:
|
|
115
|
-
|
|
116
170
|
```bash
|
|
117
171
|
git clone https://github.com/ywatanabe1989/claude-code-telegrammer.git
|
|
118
|
-
cd claude-code-telegrammer
|
|
119
|
-
pip install -e .
|
|
120
|
-
|
|
121
|
-
# Install TypeScript dependencies for the MCP server
|
|
122
|
-
cd ts && bun install
|
|
172
|
+
cd claude-code-telegrammer/ts && bun install
|
|
123
173
|
```
|
|
124
174
|
|
|
125
175
|
## Quickstart
|
|
@@ -172,7 +222,7 @@ claude \
|
|
|
172
222
|
--dangerously-load-development-channels server:claude-code-telegrammer
|
|
173
223
|
```
|
|
174
224
|
|
|
175
|
-
|
|
225
|
+
You should see `Listening for channel messages from: server:claude-code-telegrammer` in the Claude Code TUI. Send a message from Telegram to your bot — Claude Code will receive it as a channel notification.
|
|
176
226
|
|
|
177
227
|
## Interfaces
|
|
178
228
|
|
|
@@ -29,23 +29,63 @@
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
## Problem
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
## Problem and Solution
|
|
33
|
+
|
|
34
|
+
<table>
|
|
35
|
+
<tr>
|
|
36
|
+
<th align="center">#</th>
|
|
37
|
+
<th>Problem</th>
|
|
38
|
+
<th>Solution</th>
|
|
39
|
+
</tr>
|
|
40
|
+
<tr valign="top">
|
|
41
|
+
<td align="center">1</td>
|
|
42
|
+
<td><h4>Hardcoded paths</h4>The official plugin hardcodes <code>~/.claude/</code> as its state directory (<a href="https://github.com/anthropics/claude-code/issues/851">#851</a>), making it impossible to run multiple bots or customize where access.json lives.</td>
|
|
43
|
+
<td><h4>Configurable state directory</h4>All state (DB, lock, access config) lives under <code>CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR</code>. Run as many bots as you want, each with its own isolated state.</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr valign="top">
|
|
46
|
+
<td align="center">2</td>
|
|
47
|
+
<td><h4>409 Conflict crashes</h4>No single-instance guard — multiple sessions polling the same bot get 409 errors and crash each other (<a href="https://github.com/anthropics/claude-code/issues/1075">#1075</a>).</td>
|
|
48
|
+
<td><h4>PID-based lock</h4>Automatic single-instance enforcement via PID lock file. Second instance detects the conflict and waits instead of crashing.</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr valign="top">
|
|
51
|
+
<td align="center">3</td>
|
|
52
|
+
<td><h4>Zombie CPU consumption</h4>After session ends, the plugin process lingers at 100% CPU — requires manual kill (<a href="https://github.com/anthropics/claude-code/issues/1146">#1146</a>).</td>
|
|
53
|
+
<td><h4>Clean shutdown</h4>Exits gracefully on stdin close, SIGTERM, or SIGINT. No zombies, no manual cleanup.</td>
|
|
54
|
+
</tr>
|
|
55
|
+
<tr valign="top">
|
|
56
|
+
<td align="center">4</td>
|
|
57
|
+
<td><h4>Only 3 basic tools</h4>The official plugin provides just send, get_updates, and set_reaction — no history, no search, no file handling, no message editing.</td>
|
|
58
|
+
<td><h4>10 MCP tools</h4>reply, react, edit_message, get_history, get_unread, mark_read, download_attachment, send_document, search_messages, get_context — everything an autonomous agent needs.</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<tr valign="top">
|
|
61
|
+
<td align="center">5</td>
|
|
62
|
+
<td><h4>No message persistence</h4>Messages vanish after delivery. No way to search past conversations, track read status, or build context from history.</td>
|
|
63
|
+
<td><h4>SQLite message store</h4>All messages persisted in WAL-mode SQLite with full-text search, reply threading (reply_to_message_id), read/replied tracking, and attachment metadata.</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr valign="top">
|
|
66
|
+
<td align="center">6</td>
|
|
67
|
+
<td><h4>No access control for groups</h4>Basic allowlist only — no per-group policies, no hot-reload when config changes.</td>
|
|
68
|
+
<td><h4>DM + group policies</h4>Allowlist-based access control with separate DM and group chat policies via <code>access.json</code>, hot-reloaded on file change (mtime-based).</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<tr valign="top">
|
|
71
|
+
<td align="center">7</td>
|
|
72
|
+
<td><h4>No attachment support</h4>Cannot download inbound files or upload documents to chats.</td>
|
|
73
|
+
<td><h4>Full attachment handling</h4>Inbound photos, documents, voice, audio, and video are auto-downloaded. Upload local files via <code>send_document</code> tool.</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<tr valign="top">
|
|
76
|
+
<td align="center">8</td>
|
|
77
|
+
<td><h4>Sessions stall unattended</h4>Claude Code halts at permission prompts or idle states with no way to recover — the agent just stops working.</td>
|
|
78
|
+
<td><h4>TUI Watchdog</h4>Polls GNU Screen buffer, detects TUI state via pattern matching, sends keystrokes to auto-accept prompts and re-engage on idle. Throttled with burst limits.</td>
|
|
79
|
+
</tr>
|
|
80
|
+
</table>
|
|
81
|
+
|
|
82
|
+
<p align="center"><sub><b>Table 1.</b> Eight issues with the official Telegram plugin (as of April 2026) and how claude-code-telegrammer addresses each. These problems make the official plugin unusable for production autonomous agents.</sub></p>
|
|
83
|
+
|
|
84
|
+
### Architecture
|
|
85
|
+
|
|
86
|
+
1. **Custom Telegram MCP Server** (`ts/`) -- Self-contained Bun + MCP server. 10 tools, SQLite persistence, allowlist access control, attachment handling, reaction support. Incoming messages acknowledged with 📩. Built-in responsiveness policy directs the agent to reply immediately and delegate heavy work to background subagents.
|
|
87
|
+
|
|
88
|
+
2. **TUI Watchdog** (`lib/`) -- Polls a GNU Screen session, detects Claude Code's TUI state via pattern matching, and sends keystrokes to keep the agent running unattended (auto-accepts permission prompts, re-engages on idle). Throttled with burst limits to prevent runaway responses. Orchestration handled by [scitex-agent-container](https://github.com/ywatanabe1989/scitex-agent-container).
|
|
49
89
|
|
|
50
90
|
<details>
|
|
51
91
|
<summary><strong>MCP Tools (10)</strong></summary>
|
|
@@ -65,28 +105,38 @@ Two subsystems that replace and extend the official plugin:
|
|
|
65
105
|
|
|
66
106
|
</details>
|
|
67
107
|
|
|
108
|
+
### Important: Bot Token Exclusivity
|
|
109
|
+
|
|
110
|
+
This MCP server **must be the sole consumer** of its configured Telegram bot token. The Telegram Bot API allows only one `getUpdates` long-polling connection per token.
|
|
111
|
+
|
|
112
|
+
**What happens with duplicate consumers:**
|
|
113
|
+
|
|
114
|
+
| Scenario | Symptom | Detection |
|
|
115
|
+
|----------|---------|-----------|
|
|
116
|
+
| Two pollers start simultaneously | One gets 409, the other wins silently | The loser sees `409 Conflict` in logs |
|
|
117
|
+
| Two pollers start sequentially | Both appear to work, but only one receives messages | **No error** — the other poller gets empty responses forever |
|
|
118
|
+
| Webhook active + poller | Poller gets nothing | **No error** — Telegram ignores `getUpdates` when webhook is set |
|
|
119
|
+
|
|
120
|
+
**Why 409 detection alone is insufficient:** The Telegram API does not reliably return 409 for all conflict cases. When two consumers poll sequentially (not overlapping), both connections succeed — one simply receives all messages while the other gets none, with no error. The server performs a `timeout=3` preflight check at startup to catch overlapping polls, but this cannot detect the sequential case.
|
|
121
|
+
|
|
122
|
+
**If messages aren't arriving:**
|
|
123
|
+
1. Check if another process is polling the same token: `ps aux | grep telegram-server`
|
|
124
|
+
2. Check if a webhook is set: `curl https://api.telegram.org/bot<TOKEN>/getWebhookInfo`
|
|
125
|
+
3. Use a separate bot token per component (recommended)
|
|
126
|
+
4. Or disable the other consumer
|
|
127
|
+
|
|
68
128
|
## Installation
|
|
69
129
|
|
|
70
130
|
### Prerequisites
|
|
71
131
|
|
|
72
|
-
- Python >= 3.10 and GNU Screen (for watchdog/CLI)
|
|
73
132
|
- [Bun](https://bun.sh/) >= 1.0 (for the MCP server)
|
|
133
|
+
- GNU Screen (for watchdog, optional)
|
|
74
134
|
|
|
75
135
|
### Install
|
|
76
136
|
|
|
77
|
-
```bash
|
|
78
|
-
pip install claude-code-telegrammer
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Or from source:
|
|
82
|
-
|
|
83
137
|
```bash
|
|
84
138
|
git clone https://github.com/ywatanabe1989/claude-code-telegrammer.git
|
|
85
|
-
cd claude-code-telegrammer
|
|
86
|
-
pip install -e .
|
|
87
|
-
|
|
88
|
-
# Install TypeScript dependencies for the MCP server
|
|
89
|
-
cd ts && bun install
|
|
139
|
+
cd claude-code-telegrammer/ts && bun install
|
|
90
140
|
```
|
|
91
141
|
|
|
92
142
|
## Quickstart
|
|
@@ -139,7 +189,7 @@ claude \
|
|
|
139
189
|
--dangerously-load-development-channels server:claude-code-telegrammer
|
|
140
190
|
```
|
|
141
191
|
|
|
142
|
-
|
|
192
|
+
You should see `Listening for channel messages from: server:claude-code-telegrammer` in the Claude Code TUI. Send a message from Telegram to your bot — Claude Code will receive it as a channel notification.
|
|
143
193
|
|
|
144
194
|
## Interfaces
|
|
145
195
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# claude-code-telegrammer-hook — Entry point for scitex-agent-container hooks
|
|
3
|
+
# Called by agent-container's post_start/pre_stop hooks.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# claude-code-telegrammer-hook pre-start # Write access.json (before Claude Code)
|
|
7
|
+
# claude-code-telegrammer-hook post-start # Start watchdog + send startup commands
|
|
8
|
+
# claude-code-telegrammer-hook pre-stop # Stop watchdog
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
13
|
+
|
|
14
|
+
# Expect these env vars from agent-container:
|
|
15
|
+
# AGENT_CONFIG_PATH — full path to agent YAML
|
|
16
|
+
# AGENT_SCREEN_NAME — screen session name
|
|
17
|
+
# AGENT_NAME — agent name
|
|
18
|
+
|
|
19
|
+
cmd="${1:-}"
|
|
20
|
+
|
|
21
|
+
case "$cmd" in
|
|
22
|
+
pre-start)
|
|
23
|
+
if [[ -z "${AGENT_CONFIG_PATH:-}" ]]; then
|
|
24
|
+
log_warn "AGENT_CONFIG_PATH not set, skipping pre-start"
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# 1. Map bot_token_env → CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN
|
|
29
|
+
BOT_TOKEN_ENV=$(yaml_get "$AGENT_CONFIG_PATH" "spec.telegram.bot_token_env" "")
|
|
30
|
+
if [[ -n "$BOT_TOKEN_ENV" && "$BOT_TOKEN_ENV" != "CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN" ]]; then
|
|
31
|
+
TOKEN_VAL="${!BOT_TOKEN_ENV:-}"
|
|
32
|
+
if [[ -n "$TOKEN_VAL" ]]; then
|
|
33
|
+
export CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN="$TOKEN_VAL"
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 2. Write access.json
|
|
38
|
+
AUTO_CONNECT=$(yaml_get "$AGENT_CONFIG_PATH" "spec.telegram.auto_connect" "true")
|
|
39
|
+
STATE_DIR="${CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR:-$HOME/.scitex/agent-container/telegram}"
|
|
40
|
+
|
|
41
|
+
# Resolve bot_id from token for state dir
|
|
42
|
+
if [[ -n "${CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN:-}" ]]; then
|
|
43
|
+
BOT_ID="${CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN%%:*}"
|
|
44
|
+
STATE_DIR="$HOME/.scitex/agent-container/telegram/$BOT_ID"
|
|
45
|
+
fi
|
|
46
|
+
mkdir -p "$STATE_DIR"
|
|
47
|
+
|
|
48
|
+
# Parse allowed_users via Python (yaml_get returns flat values, not JSON)
|
|
49
|
+
if [[ "$AUTO_CONNECT" == "true" || "$AUTO_CONNECT" == "True" ]]; then
|
|
50
|
+
python3 -c "
|
|
51
|
+
import yaml, json
|
|
52
|
+
with open('$AGENT_CONFIG_PATH') as f:
|
|
53
|
+
raw = yaml.safe_load(f)
|
|
54
|
+
users = raw.get('spec',{}).get('telegram',{}).get('allowed_users',[]) or []
|
|
55
|
+
users = [str(u) for u in users]
|
|
56
|
+
data = {'dmPolicy': 'allowlist', 'allowFrom': users}
|
|
57
|
+
with open('$STATE_DIR/access.json', 'w') as f:
|
|
58
|
+
json.dump(data, f, indent=2)
|
|
59
|
+
f.write('\n')
|
|
60
|
+
print('Wrote access.json with', len(users), 'allowed users to $STATE_DIR')
|
|
61
|
+
"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 3. Write .env for the MCP server
|
|
65
|
+
echo "CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN=${CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN:-}" > "$STATE_DIR/.env"
|
|
66
|
+
chmod 600 "$STATE_DIR/.env"
|
|
67
|
+
log_info "Wrote .env to $STATE_DIR"
|
|
68
|
+
|
|
69
|
+
# 4. Write MCP config JSON for --mcp-config
|
|
70
|
+
AGENT_NAME="${AGENT_NAME:-telegrammer}"
|
|
71
|
+
MCP_DIR="$HOME/.scitex/agent-container/cache/mcp-configs"
|
|
72
|
+
mkdir -p "$MCP_DIR"
|
|
73
|
+
# Find telegram-server.ts: check project root first, then relative to SCRIPT_DIR
|
|
74
|
+
TS_SERVER=""
|
|
75
|
+
for candidate in \
|
|
76
|
+
"$SCRIPT_DIR/../ts/telegram-server.ts" \
|
|
77
|
+
"$SCRIPT_DIR/ts/telegram-server.ts" \
|
|
78
|
+
"$(python3 -c 'import claude_code_telegrammer; import os; print(os.path.join(os.path.dirname(claude_code_telegrammer.__file__), "..", "..", "ts", "telegram-server.ts"))' 2>/dev/null)" \
|
|
79
|
+
; do
|
|
80
|
+
if [[ -f "$candidate" ]]; then
|
|
81
|
+
TS_SERVER="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")"
|
|
82
|
+
break
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
if [[ -z "$TS_SERVER" ]]; then
|
|
86
|
+
log_warn "telegram-server.ts not found"
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
python3 -c "
|
|
91
|
+
import yaml, json, os
|
|
92
|
+
with open('$AGENT_CONFIG_PATH') as f:
|
|
93
|
+
raw = yaml.safe_load(f)
|
|
94
|
+
users = raw.get('spec',{}).get('telegram',{}).get('allowed_users',[]) or []
|
|
95
|
+
users_str = ','.join(str(u) for u in users)
|
|
96
|
+
config = {
|
|
97
|
+
'mcpServers': {
|
|
98
|
+
'claude-code-telegrammer': {
|
|
99
|
+
'type': 'stdio',
|
|
100
|
+
'command': 'bun',
|
|
101
|
+
'args': ['run', '$TS_SERVER'],
|
|
102
|
+
'env': {
|
|
103
|
+
'CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN': os.environ.get('CLAUDE_CODE_TELEGRAMMER_TELEGRAM_BOT_TOKEN', ''),
|
|
104
|
+
'CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR': '$STATE_DIR',
|
|
105
|
+
'CLAUDE_CODE_TELEGRAMMER_TELEGRAM_ALLOWED_USERS': users_str
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
# Write to both /tmp (for --mcp-config) and workdir .mcp.json (for --dangerously-load-development-channels)
|
|
111
|
+
path = '$MCP_DIR/mcp-$AGENT_NAME.json'
|
|
112
|
+
with open(path, 'w') as f:
|
|
113
|
+
json.dump(config, f, indent=2)
|
|
114
|
+
f.write('\n')
|
|
115
|
+
print('Wrote MCP config to', path)
|
|
116
|
+
|
|
117
|
+
# Also write to workdir .mcp.json so --dangerously-load-development-channels can find it
|
|
118
|
+
import yaml as _yaml
|
|
119
|
+
with open('$AGENT_CONFIG_PATH') as f:
|
|
120
|
+
agent_cfg = _yaml.safe_load(f)
|
|
121
|
+
workdir = os.path.expanduser(agent_cfg.get('spec', {}).get('workdir', '~/proj'))
|
|
122
|
+
mcp_json_path = os.path.join(workdir, '.mcp.json')
|
|
123
|
+
# Merge with existing .mcp.json if present
|
|
124
|
+
existing = {}
|
|
125
|
+
try:
|
|
126
|
+
with open(mcp_json_path) as f:
|
|
127
|
+
existing = json.load(f)
|
|
128
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
129
|
+
pass
|
|
130
|
+
existing.setdefault('mcpServers', {})
|
|
131
|
+
existing['mcpServers']['claude-code-telegrammer'] = config['mcpServers']['claude-code-telegrammer']
|
|
132
|
+
with open(mcp_json_path, 'w') as f:
|
|
133
|
+
json.dump(existing, f, indent=2)
|
|
134
|
+
f.write('\n')
|
|
135
|
+
print('Wrote .mcp.json to', mcp_json_path)
|
|
136
|
+
"
|
|
137
|
+
;;
|
|
138
|
+
|
|
139
|
+
post-start)
|
|
140
|
+
# Send startup commands (screen must exist)
|
|
141
|
+
SESSION="${AGENT_SCREEN_NAME:-cld-telegram}"
|
|
142
|
+
if [[ -n "${AGENT_CONFIG_PATH:-}" ]]; then
|
|
143
|
+
claude-code-telegrammer-init --session "$SESSION" --config "$AGENT_CONFIG_PATH"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Start watchdog if enabled
|
|
147
|
+
if [[ -n "${AGENT_CONFIG_PATH:-}" ]]; then
|
|
148
|
+
WATCHDOG_ENABLED=$(yaml_get "$AGENT_CONFIG_PATH" "spec.watchdog.enabled" "false")
|
|
149
|
+
if [[ "$WATCHDOG_ENABLED" == "true" || "$WATCHDOG_ENABLED" == "True" ]]; then
|
|
150
|
+
INTERVAL=$(yaml_get "$AGENT_CONFIG_PATH" "spec.watchdog.interval" "1.5")
|
|
151
|
+
RESP_YN=$(yaml_get "$AGENT_CONFIG_PATH" "spec.watchdog.responses.y_n" "1")
|
|
152
|
+
RESP_YYN=$(yaml_get "$AGENT_CONFIG_PATH" "spec.watchdog.responses.y_y_n" "2")
|
|
153
|
+
RESP_WAIT=$(yaml_get "$AGENT_CONFIG_PATH" "spec.watchdog.responses.waiting" "/speak-and-call")
|
|
154
|
+
|
|
155
|
+
WATCHDOG_SESSION="${SESSION}-watchdog"
|
|
156
|
+
|
|
157
|
+
export CLAUDE_CODE_TELEGRAMMER_SESSION="$SESSION"
|
|
158
|
+
export CLAUDE_CODE_TELEGRAMMER_WATCHDOG_INTERVAL="$INTERVAL"
|
|
159
|
+
export CLAUDE_CODE_TELEGRAMMER_RESP_Y_N="$RESP_YN"
|
|
160
|
+
export CLAUDE_CODE_TELEGRAMMER_RESP_Y_Y_N="$RESP_YYN"
|
|
161
|
+
export CLAUDE_CODE_TELEGRAMMER_RESP_WAITING="$RESP_WAIT"
|
|
162
|
+
|
|
163
|
+
WATCHDOG_BIN=$(which claude-code-telegrammer-watchdog 2>/dev/null || echo "")
|
|
164
|
+
if [[ -n "$WATCHDOG_BIN" ]]; then
|
|
165
|
+
screen -dmS "$WATCHDOG_SESSION" bash -c "$WATCHDOG_BIN --session $SESSION --interval $INTERVAL"
|
|
166
|
+
log_info "Watchdog started: $WATCHDOG_SESSION"
|
|
167
|
+
else
|
|
168
|
+
log_warn "claude-code-telegrammer-watchdog not found in PATH"
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
;;
|
|
173
|
+
|
|
174
|
+
pre-stop)
|
|
175
|
+
SESSION="${AGENT_SCREEN_NAME:-cld-telegram}"
|
|
176
|
+
WATCHDOG_SESSION="${SESSION}-watchdog"
|
|
177
|
+
if screen -ls 2>/dev/null | grep -q "$WATCHDOG_SESSION"; then
|
|
178
|
+
screen -S "$WATCHDOG_SESSION" -X quit 2>/dev/null || true
|
|
179
|
+
log_info "Watchdog stopped: $WATCHDOG_SESSION"
|
|
180
|
+
fi
|
|
181
|
+
;;
|
|
182
|
+
|
|
183
|
+
*)
|
|
184
|
+
echo "Usage: claude-code-telegrammer-hook {post-start|pre-stop}"
|
|
185
|
+
exit 1
|
|
186
|
+
;;
|
|
187
|
+
esac
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# claude-code-telegrammer-init — Send startup commands to a Claude Code screen session
|
|
3
|
+
# and optionally configure Telegram access.json.
|
|
4
|
+
#
|
|
5
|
+
# Usage: claude-code-telegrammer-init --session cld-telegram --config telegram-master.yaml
|
|
6
|
+
#
|
|
7
|
+
# Requires: yq (or python3 with PyYAML as fallback)
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SESSION=""
|
|
12
|
+
CONFIG=""
|
|
13
|
+
|
|
14
|
+
usage() {
|
|
15
|
+
echo "Usage: claude-code-telegrammer-init --session <screen-session> --config <yaml-config>"
|
|
16
|
+
echo ""
|
|
17
|
+
echo "Options:"
|
|
18
|
+
echo " --session Screen session name to send commands to"
|
|
19
|
+
echo " --config YAML config file with startup_commands and telegram sections"
|
|
20
|
+
echo " --help Show this help message"
|
|
21
|
+
exit "${1:-0}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--session) SESSION="$2"; shift 2 ;;
|
|
27
|
+
--config) CONFIG="$2"; shift 2 ;;
|
|
28
|
+
--help) usage 0 ;;
|
|
29
|
+
*) echo "Unknown option: $1"; usage 1 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
if [[ -z "$SESSION" || -z "$CONFIG" ]]; then
|
|
34
|
+
echo "Error: --session and --config are required."
|
|
35
|
+
usage 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [[ ! -f "$CONFIG" ]]; then
|
|
39
|
+
echo "Error: Config file not found: $CONFIG"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Use python3 to parse YAML (always available in our environment)
|
|
44
|
+
parse_yaml() {
|
|
45
|
+
python3 -c "
|
|
46
|
+
import sys, yaml, json
|
|
47
|
+
|
|
48
|
+
with open('$CONFIG') as f:
|
|
49
|
+
raw = yaml.safe_load(f)
|
|
50
|
+
|
|
51
|
+
spec = raw.get('spec', {})
|
|
52
|
+
result = {
|
|
53
|
+
'startup_commands': spec.get('startup_commands', []) or [],
|
|
54
|
+
'telegram': spec.get('telegram', {}) or {},
|
|
55
|
+
}
|
|
56
|
+
print(json.dumps(result))
|
|
57
|
+
"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
PARSED=$(parse_yaml)
|
|
61
|
+
|
|
62
|
+
# Setup Telegram access.json if configured
|
|
63
|
+
AUTO_CONNECT=$(echo "$PARSED" | python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d.get('telegram',{}).get('auto_connect',True)).lower())")
|
|
64
|
+
ALLOWED_USERS=$(echo "$PARSED" | python3 -c "import sys,json; d=json.load(sys.stdin); users=d.get('telegram',{}).get('allowed_users',[]); print(json.dumps(users))")
|
|
65
|
+
|
|
66
|
+
if [[ "$AUTO_CONNECT" == "true" && "$ALLOWED_USERS" != "[]" ]]; then
|
|
67
|
+
ACCESS_DIR="${CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR:-$HOME/.scitex/agent-container/telegram}"
|
|
68
|
+
mkdir -p "$ACCESS_DIR"
|
|
69
|
+
python3 -c "
|
|
70
|
+
import json
|
|
71
|
+
users = json.loads('$ALLOWED_USERS')
|
|
72
|
+
data = {'dmPolicy': 'allowlist', 'allowFrom': [str(u) for u in users]}
|
|
73
|
+
with open('$ACCESS_DIR/access.json', 'w') as f:
|
|
74
|
+
json.dump(data, f, indent=2)
|
|
75
|
+
f.write('\n')
|
|
76
|
+
print('Wrote access.json with', len(users), 'allowed users')
|
|
77
|
+
"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Send startup commands with delays
|
|
81
|
+
NUM_COMMANDS=$(echo "$PARSED" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('startup_commands',[])))")
|
|
82
|
+
|
|
83
|
+
if [[ "$NUM_COMMANDS" -gt 0 ]]; then
|
|
84
|
+
echo "Sending $NUM_COMMANDS startup command(s) to session: $SESSION"
|
|
85
|
+
|
|
86
|
+
echo "$PARSED" | python3 -c "
|
|
87
|
+
import sys, json
|
|
88
|
+
d = json.load(sys.stdin)
|
|
89
|
+
for cmd in d.get('startup_commands', []):
|
|
90
|
+
delay = cmd.get('delay', 0)
|
|
91
|
+
command = cmd.get('command', '')
|
|
92
|
+
if command:
|
|
93
|
+
print(f'{delay}\t{command}')
|
|
94
|
+
" | while IFS=$'\t' read -r delay command; do
|
|
95
|
+
if [[ "$delay" -gt 0 ]]; then
|
|
96
|
+
echo " Waiting ${delay}s before sending: $command"
|
|
97
|
+
sleep "$delay"
|
|
98
|
+
fi
|
|
99
|
+
screen -S "$SESSION" -X stuff "${command}\r"
|
|
100
|
+
echo " Sent: $command"
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
echo "claude-code-telegrammer-init complete."
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-code-telegrammer"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Custom Telegram MCP server + TUI auto-responder for running Claude Code as an autonomous Telegram agent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "AGPL-3.0"
|
|
@@ -56,6 +56,39 @@ export async function startPolling(mcp: Server): Promise<void> {
|
|
|
56
56
|
log("poller", `getMe failed: ${err}`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Preflight: try a short long-poll to detect competing consumers.
|
|
60
|
+
// timeout=0 is instant and won't collide — we need timeout>0 to trigger
|
|
61
|
+
// the 409 if another consumer is already in a long-poll.
|
|
62
|
+
log("poller", "preflight: testing for competing consumers (3s)...");
|
|
63
|
+
try {
|
|
64
|
+
await tgApi("getUpdates", { offset: updateOffset, timeout: 3, limit: 1 });
|
|
65
|
+
log("poller", "preflight OK — no competing consumers detected");
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
if (errMsg.includes("409")) {
|
|
69
|
+
const fatalMsg =
|
|
70
|
+
"FATAL: 409 Conflict on startup — another process is already polling this bot token. " +
|
|
71
|
+
"Only one getUpdates consumer is allowed per token. " +
|
|
72
|
+
"Stop the other consumer first, or use a different bot token. " +
|
|
73
|
+
"Refusing to start.";
|
|
74
|
+
log("poller", fatalMsg);
|
|
75
|
+
// Notify the agent so it knows Telegram is NOT connected
|
|
76
|
+
mcp
|
|
77
|
+
.notification({
|
|
78
|
+
method: "notifications/claude/channel",
|
|
79
|
+
params: {
|
|
80
|
+
content: fatalMsg,
|
|
81
|
+
meta: { source: "telegram", type: "error" },
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.catch(() => {});
|
|
85
|
+
polling = false;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Non-409 errors are OK to proceed (e.g., network hiccup)
|
|
89
|
+
log("poller", `preflight warning: ${errMsg} (proceeding anyway)`);
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
while (polling) {
|
|
60
93
|
try {
|
|
61
94
|
const updates = await tgApi("getUpdates", {
|
|
@@ -85,8 +118,23 @@ export async function startPolling(mcp: Server): Promise<void> {
|
|
|
85
118
|
} catch (err) {
|
|
86
119
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
87
120
|
if (errMsg.includes("409")) {
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
const conflictMsg =
|
|
122
|
+
"409 Conflict — another process is polling this bot token. " +
|
|
123
|
+
"Only one getUpdates consumer is allowed per token. " +
|
|
124
|
+
"Telegram connection is DOWN. Stop the other consumer or use a different bot token.";
|
|
125
|
+
log("poller", conflictMsg);
|
|
126
|
+
mcp
|
|
127
|
+
.notification({
|
|
128
|
+
method: "notifications/claude/channel",
|
|
129
|
+
params: {
|
|
130
|
+
content: conflictMsg,
|
|
131
|
+
meta: { source: "telegram", type: "error" },
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
.catch(() => {});
|
|
135
|
+
// Stop polling — don't retry, the agent needs to fix this
|
|
136
|
+
polling = false;
|
|
137
|
+
return;
|
|
90
138
|
} else {
|
|
91
139
|
log("poller", `getUpdates error: ${errMsg}. Retrying in 3s...`);
|
|
92
140
|
await new Promise((r) => setTimeout(r, 3000));
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"""Claude Code Telegrammer -- screen-based auto-responder for Claude Code TUI."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import pathlib
|
|
6
|
-
|
|
7
|
-
from importlib.metadata import version as _get_version
|
|
8
|
-
|
|
9
|
-
__version__ = _get_version(__name__)
|
|
10
|
-
|
|
11
|
-
# Package directory (works for both editable and regular installs)
|
|
12
|
-
_PKG_DIR = pathlib.Path(__file__).resolve().parent
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_bin_path(script_name: str) -> str:
|
|
16
|
-
"""Return absolute path to an installed bash script in bin/.
|
|
17
|
-
|
|
18
|
-
Searches two locations:
|
|
19
|
-
1. <package>/bin/<script> (symlinked or copied during install)
|
|
20
|
-
2. <repo_root>/bin/<script> (editable install fallback)
|
|
21
|
-
"""
|
|
22
|
-
# Primary: inside package (symlink or copied)
|
|
23
|
-
path = _PKG_DIR / "bin" / script_name
|
|
24
|
-
if path.exists():
|
|
25
|
-
return str(path)
|
|
26
|
-
|
|
27
|
-
# Fallback: repo root (editable install, src layout)
|
|
28
|
-
repo_root = _PKG_DIR.parent.parent
|
|
29
|
-
path = repo_root / "bin" / script_name
|
|
30
|
-
if path.exists():
|
|
31
|
-
return str(path)
|
|
32
|
-
|
|
33
|
-
raise FileNotFoundError(
|
|
34
|
-
f"Script '{script_name}' not found in package bin/ or repo root bin/"
|
|
35
|
-
)
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.github/workflows/publish-pypi.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/scitex-icon-navy-inverted.png
RENAMED
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/scitex-logo-blue-cropped.png
RENAMED
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/_static/.gitkeep
RENAMED
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/_templates/.gitkeep
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/installation.rst
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/tests/test-state-detection.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/telegram-api.test.ts
RENAMED
|
File without changes
|