claude-code-telegrammer 0.4.0__tar.gz → 0.4.1__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 (55) hide show
  1. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/PKG-INFO +66 -18
  2. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/README.md +65 -17
  3. claude_code_telegrammer-0.4.1/bin/claude-code-telegrammer-hook +187 -0
  4. claude_code_telegrammer-0.4.1/bin/claude-code-telegrammer-init +104 -0
  5. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/pyproject.toml +1 -1
  6. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/poller.ts +7 -1
  7. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/.github/workflows/publish-pypi.yml +0 -0
  8. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/.github/workflows/test.yml +0 -0
  9. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/.gitignore +0 -0
  10. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/.readthedocs.yaml +0 -0
  11. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/LICENSE +0 -0
  12. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/scitex-icon-navy-inverted.png +0 -0
  13. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/scitex-logo-blue-cropped.png +0 -0
  14. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/_static/.gitkeep +0 -0
  15. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/_templates/.gitkeep +0 -0
  16. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/api/claude_code_telegrammer.rst +0 -0
  17. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/conf.py +0 -0
  18. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/index.rst +0 -0
  19. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/installation.rst +0 -0
  20. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/quickstart.rst +0 -0
  21. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/docs/sphinx/requirements.txt +0 -0
  22. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/hooks/enforce_background_subagents.sh +0 -0
  23. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/lib/auto-response.sh +0 -0
  24. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/lib/common.sh +0 -0
  25. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/lib/lock.sh +0 -0
  26. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/lib/orochi-relay.sh +0 -0
  27. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/lib/state-detection.sh +0 -0
  28. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer/__init__.py +0 -0
  29. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer/_skills/claude-code-telegrammer/SKILL.md +0 -0
  30. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer.egg-info/PKG-INFO +0 -0
  31. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer.egg-info/SOURCES.txt +0 -0
  32. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer.egg-info/dependency_links.txt +0 -0
  33. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer.egg-info/entry_points.txt +0 -0
  34. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/src/claude_code_telegrammer.egg-info/top_level.txt +0 -0
  35. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/tests/test-state-detection.sh +0 -0
  36. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/.gitignore +0 -0
  37. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/bun.lock +0 -0
  38. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/bunfig.toml +0 -0
  39. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/dist/telegram-server.js +0 -0
  40. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/access.ts +0 -0
  41. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/attachments.ts +0 -0
  42. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/config.ts +0 -0
  43. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/lock.ts +0 -0
  44. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/log.ts +0 -0
  45. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/store.ts +0 -0
  46. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/telegram-api.ts +0 -0
  47. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/lib/tools.ts +0 -0
  48. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/package.json +0 -0
  49. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/telegram-server.ts +0 -0
  50. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/test/access.test.ts +0 -0
  51. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/test/config.test.ts +0 -0
  52. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/test/lock.test.ts +0 -0
  53. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/test/preload.ts +0 -0
  54. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/test/store.test.ts +0 -0
  55. {claude_code_telegrammer-0.4.0 → claude_code_telegrammer-0.4.1}/ts/test/telegram-api.test.ts +0 -0
@@ -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.1
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,6 +138,14 @@ 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 enforces a single active `getUpdates` long-polling connection per token. If another component (e.g., `scitex-orochi`'s Telegram bridge) also polls the same token, **HTTP 409 Conflict** errors will occur, causing both consumers to miss messages.
144
+
145
+ **If you see 409 errors:**
146
+ - Disable the other polling component, **or**
147
+ - Use a separate bot token for each component
148
+
101
149
  ## Installation
102
150
 
103
151
  ### Prerequisites
@@ -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,6 +105,14 @@ 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 enforces a single active `getUpdates` long-polling connection per token. If another component (e.g., `scitex-orochi`'s Telegram bridge) also polls the same token, **HTTP 409 Conflict** errors will occur, causing both consumers to miss messages.
111
+
112
+ **If you see 409 errors:**
113
+ - Disable the other polling component, **or**
114
+ - Use a separate bot token for each component
115
+
68
116
  ## Installation
69
117
 
70
118
  ### Prerequisites
@@ -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.1"
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"
@@ -85,7 +85,13 @@ export async function startPolling(mcp: Server): Promise<void> {
85
85
  } catch (err) {
86
86
  const errMsg = err instanceof Error ? err.message : String(err);
87
87
  if (errMsg.includes("409")) {
88
- log("poller", "409 Conflict — another instance polling. Waiting 5s...");
88
+ log(
89
+ "poller",
90
+ "409 Conflict — another process is polling this bot token. " +
91
+ "Only one getUpdates consumer is allowed per token. " +
92
+ "If scitex-orochi's Telegram bridge is active, disable it or use a separate bot token. " +
93
+ "Retrying in 5s...",
94
+ );
89
95
  await new Promise((r) => setTimeout(r, 5000));
90
96
  } else {
91
97
  log("poller", `getUpdates error: ${errMsg}. Retrying in 3s...`);