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.
Files changed (58) hide show
  1. claude_code_telegrammer-0.4.2/.env.example +20 -0
  2. claude_code_telegrammer-0.4.2/.mcp.json.example +14 -0
  3. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/PKG-INFO +81 -31
  4. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/README.md +80 -30
  5. claude_code_telegrammer-0.4.2/bin/claude-code-telegrammer-hook +187 -0
  6. claude_code_telegrammer-0.4.2/bin/claude-code-telegrammer-init +104 -0
  7. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/pyproject.toml +1 -1
  8. claude_code_telegrammer-0.4.2/src/claude_code_telegrammer/__init__.py +7 -0
  9. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/poller.ts +50 -2
  10. claude_code_telegrammer-0.4.0/src/claude_code_telegrammer/__init__.py +0 -35
  11. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.github/workflows/publish-pypi.yml +0 -0
  12. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.github/workflows/test.yml +0 -0
  13. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.gitignore +0 -0
  14. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/.readthedocs.yaml +0 -0
  15. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/LICENSE +0 -0
  16. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/scitex-icon-navy-inverted.png +0 -0
  17. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/scitex-logo-blue-cropped.png +0 -0
  18. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/_static/.gitkeep +0 -0
  19. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/_templates/.gitkeep +0 -0
  20. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/api/claude_code_telegrammer.rst +0 -0
  21. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/conf.py +0 -0
  22. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/index.rst +0 -0
  23. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/installation.rst +0 -0
  24. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/quickstart.rst +0 -0
  25. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/docs/sphinx/requirements.txt +0 -0
  26. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/hooks/enforce_background_subagents.sh +0 -0
  27. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/auto-response.sh +0 -0
  28. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/common.sh +0 -0
  29. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/lock.sh +0 -0
  30. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/orochi-relay.sh +0 -0
  31. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/lib/state-detection.sh +0 -0
  32. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer/_skills/claude-code-telegrammer/SKILL.md +0 -0
  33. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/PKG-INFO +0 -0
  34. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/SOURCES.txt +0 -0
  35. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/dependency_links.txt +0 -0
  36. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/entry_points.txt +0 -0
  37. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/src/claude_code_telegrammer.egg-info/top_level.txt +0 -0
  38. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/tests/test-state-detection.sh +0 -0
  39. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/.gitignore +0 -0
  40. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/bun.lock +0 -0
  41. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/bunfig.toml +0 -0
  42. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/dist/telegram-server.js +0 -0
  43. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/access.ts +0 -0
  44. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/attachments.ts +0 -0
  45. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/config.ts +0 -0
  46. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/lock.ts +0 -0
  47. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/log.ts +0 -0
  48. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/store.ts +0 -0
  49. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/telegram-api.ts +0 -0
  50. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/lib/tools.ts +0 -0
  51. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/package.json +0 -0
  52. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/telegram-server.ts +0 -0
  53. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/access.test.ts +0 -0
  54. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/config.test.ts +0 -0
  55. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/lock.test.ts +0 -0
  56. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/preload.ts +0 -0
  57. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.2}/ts/test/store.test.ts +0 -0
  58. {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.0
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
- The official `plugin:telegram@claude-plugins-official` has several unresolved issues that make it unusable for production autonomous agents:
68
-
69
- - **[#851](https://github.com/anthropics/claude-code/issues/851)** -- `STATE_DIR` not respected; access.json path hardcoded
70
- - **[#1075](https://github.com/anthropics/claude-code/issues/1075)** -- 409 Conflict errors when multiple instances poll the same bot
71
- - **[#1146](https://github.com/anthropics/claude-code/issues/1146)** -- Zombie CPU consumption after session ends
72
-
73
- Additionally, Claude Code sessions running unattended will stall at permission prompts or idle states with no way to recover.
74
-
75
- ## Solution
76
-
77
- Two subsystems that replace and extend the official plugin:
78
-
79
- 1. **Custom Telegram MCP Server** (`ts/`) -- A self-contained MCP server that fixes all three official plugin issues: configurable state directory via `CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR`, PID-based single-instance lock, and clean shutdown on stdin close/SIGTERM. Provides 10 MCP tools (vs 3 in the official plugin), SQLite message persistence, allowlist-based access control, and inbound reaction (`message_reaction`) delivery. Incoming messages are acknowledged with a 📩 reaction.
80
-
81
- 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 and lifecycle management is handled by [scitex-agent-container](https://github.com/ywatanabe1989/scitex-agent-container).
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
- For full agent orchestration (screen sessions, watchdog, YAML configs), see [scitex-agent-container](https://github.com/ywatanabe1989/scitex-agent-container).
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
- The official `plugin:telegram@claude-plugins-official` has several unresolved issues that make it unusable for production autonomous agents:
35
-
36
- - **[#851](https://github.com/anthropics/claude-code/issues/851)** -- `STATE_DIR` not respected; access.json path hardcoded
37
- - **[#1075](https://github.com/anthropics/claude-code/issues/1075)** -- 409 Conflict errors when multiple instances poll the same bot
38
- - **[#1146](https://github.com/anthropics/claude-code/issues/1146)** -- Zombie CPU consumption after session ends
39
-
40
- Additionally, Claude Code sessions running unattended will stall at permission prompts or idle states with no way to recover.
41
-
42
- ## Solution
43
-
44
- Two subsystems that replace and extend the official plugin:
45
-
46
- 1. **Custom Telegram MCP Server** (`ts/`) -- A self-contained MCP server that fixes all three official plugin issues: configurable state directory via `CLAUDE_CODE_TELEGRAMMER_TELEGRAM_STATE_DIR`, PID-based single-instance lock, and clean shutdown on stdin close/SIGTERM. Provides 10 MCP tools (vs 3 in the official plugin), SQLite message persistence, allowlist-based access control, and inbound reaction (`message_reaction`) delivery. Incoming messages are acknowledged with a 📩 reaction.
47
-
48
- 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 and lifecycle management is handled by [scitex-agent-container](https://github.com/ywatanabe1989/scitex-agent-container).
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
- For full agent orchestration (screen sessions, watchdog, YAML configs), see [scitex-agent-container](https://github.com/ywatanabe1989/scitex-agent-container).
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.0"
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"
@@ -0,0 +1,7 @@
1
+ """Claude Code Telegrammer -- custom Telegram MCP server + TUI auto-responder for Claude Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import version as _get_version
6
+
7
+ __version__ = _get_version(__name__)
@@ -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
- log("poller", "409 Conflict — another instance polling. Waiting 5s...");
89
- await new Promise((r) => setTimeout(r, 5000));
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
- )