charness 3.1.0 → 3.2.0

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 (138) hide show
  1. package/.env.example +8 -7
  2. package/DEVELOPMENT.md +12 -6
  3. package/MIGRATION_1_TO_2.md +1 -1
  4. package/MIGRATION_RENAME_TO_CHARNESS.md +133 -0
  5. package/README.md +75 -581
  6. package/SETUP.md +539 -0
  7. package/dist/adapters/claudeCliAdapter.d.ts +1 -1
  8. package/dist/adapters/claudeCliAdapter.js +1 -1
  9. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  10. package/dist/adapters/claudeJsonStreamAdapter.js +2 -1
  11. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  12. package/dist/adapters/openCodeAdapter.js +1 -1
  13. package/dist/bot.d.ts.map +1 -1
  14. package/dist/bot.js +7 -6
  15. package/dist/bot.js.map +1 -1
  16. package/dist/cli/bot.d.ts +4 -2
  17. package/dist/cli/bot.d.ts.map +1 -1
  18. package/dist/cli/bot.js +11 -3
  19. package/dist/cli/bot.js.map +1 -1
  20. package/dist/cli/botEntry.js +3 -1
  21. package/dist/cli/botEntry.js.map +1 -1
  22. package/dist/cli/claudeProfiles.d.ts +3 -3
  23. package/dist/cli/claudeProfiles.js +4 -4
  24. package/dist/cli/claudeProfiles.js.map +1 -1
  25. package/dist/cli/envLoader.d.ts +6 -5
  26. package/dist/cli/envLoader.d.ts.map +1 -1
  27. package/dist/cli/envLoader.js +8 -27
  28. package/dist/cli/envLoader.js.map +1 -1
  29. package/dist/cli/hot.d.ts +2 -2
  30. package/dist/cli/hot.d.ts.map +1 -1
  31. package/dist/cli/hot.js +26 -16
  32. package/dist/cli/hot.js.map +1 -1
  33. package/dist/cli.js +12 -12
  34. package/dist/cli.js.map +1 -1
  35. package/dist/connectors/jira/adf.d.ts +21 -0
  36. package/dist/connectors/jira/adf.d.ts.map +1 -1
  37. package/dist/connectors/jira/adf.js +39 -0
  38. package/dist/connectors/jira/adf.js.map +1 -1
  39. package/dist/connectors/jira/answerSink.d.ts +20 -3
  40. package/dist/connectors/jira/answerSink.d.ts.map +1 -1
  41. package/dist/connectors/jira/answerSink.js +126 -13
  42. package/dist/connectors/jira/answerSink.js.map +1 -1
  43. package/dist/connectors/jira/client.d.ts +4 -0
  44. package/dist/connectors/jira/client.d.ts.map +1 -1
  45. package/dist/connectors/jira/client.js +3 -0
  46. package/dist/connectors/jira/client.js.map +1 -1
  47. package/dist/connectors/jira/config.d.ts +10 -1
  48. package/dist/connectors/jira/config.d.ts.map +1 -1
  49. package/dist/connectors/jira/config.js +11 -2
  50. package/dist/connectors/jira/config.js.map +1 -1
  51. package/dist/connectors/jira/connector.d.ts +7 -1
  52. package/dist/connectors/jira/connector.d.ts.map +1 -1
  53. package/dist/connectors/jira/connector.js +23 -4
  54. package/dist/connectors/jira/connector.js.map +1 -1
  55. package/dist/connectors/jira/contextLedger.d.ts +6 -0
  56. package/dist/connectors/jira/contextLedger.d.ts.map +1 -1
  57. package/dist/connectors/jira/contextLedger.js +16 -0
  58. package/dist/connectors/jira/contextLedger.js.map +1 -1
  59. package/dist/connectors/jira/contextPreamble.d.ts.map +1 -1
  60. package/dist/connectors/jira/contextPreamble.js +6 -3
  61. package/dist/connectors/jira/contextPreamble.js.map +1 -1
  62. package/dist/connectors/jira/inbound.d.ts +60 -7
  63. package/dist/connectors/jira/inbound.d.ts.map +1 -1
  64. package/dist/connectors/jira/inbound.js +169 -16
  65. package/dist/connectors/jira/inbound.js.map +1 -1
  66. package/dist/connectors/jira/issueBlocks.d.ts +6 -1
  67. package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
  68. package/dist/connectors/jira/issueBlocks.js +5 -1
  69. package/dist/connectors/jira/issueBlocks.js.map +1 -1
  70. package/dist/connectors/jira/prompt.d.ts +2 -0
  71. package/dist/connectors/jira/prompt.d.ts.map +1 -1
  72. package/dist/connectors/jira/prompt.js +16 -0
  73. package/dist/connectors/jira/prompt.js.map +1 -1
  74. package/dist/connectors/jira/trigger.d.ts +12 -3
  75. package/dist/connectors/jira/trigger.d.ts.map +1 -1
  76. package/dist/connectors/jira/trigger.js +8 -1
  77. package/dist/connectors/jira/trigger.js.map +1 -1
  78. package/dist/connectors/jira/triggerLog.d.ts +10 -3
  79. package/dist/connectors/jira/triggerLog.d.ts.map +1 -1
  80. package/dist/connectors/jira/triggerLog.js +18 -2
  81. package/dist/connectors/jira/triggerLog.js.map +1 -1
  82. package/dist/index.js +1 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/installManager.d.ts +16 -2
  85. package/dist/installManager.d.ts.map +1 -1
  86. package/dist/installManager.js +42 -12
  87. package/dist/installManager.js.map +1 -1
  88. package/dist/instanceLock.d.ts.map +1 -1
  89. package/dist/instanceLock.js +10 -8
  90. package/dist/instanceLock.js.map +1 -1
  91. package/dist/legacyMigration.d.ts +69 -0
  92. package/dist/legacyMigration.d.ts.map +1 -0
  93. package/dist/legacyMigration.js +338 -0
  94. package/dist/legacyMigration.js.map +1 -0
  95. package/dist/legacyNames.d.ts +107 -0
  96. package/dist/legacyNames.d.ts.map +1 -0
  97. package/dist/legacyNames.js +230 -0
  98. package/dist/legacyNames.js.map +1 -0
  99. package/dist/platform/answerSink.d.ts +5 -0
  100. package/dist/platform/answerSink.d.ts.map +1 -1
  101. package/dist/platform/answerSink.js.map +1 -1
  102. package/dist/requests/answerRequest.d.ts +2 -0
  103. package/dist/requests/answerRequest.d.ts.map +1 -1
  104. package/dist/requests/answerRequest.js +3 -1
  105. package/dist/requests/answerRequest.js.map +1 -1
  106. package/dist/requests/answerStyle.d.ts +11 -0
  107. package/dist/requests/answerStyle.d.ts.map +1 -0
  108. package/dist/requests/answerStyle.js +14 -0
  109. package/dist/requests/answerStyle.js.map +1 -0
  110. package/dist/requests/answerTail.d.ts.map +1 -1
  111. package/dist/requests/answerTail.js +2 -0
  112. package/dist/requests/answerTail.js.map +1 -1
  113. package/dist/requests/requestHeader.d.ts.map +1 -1
  114. package/dist/requests/requestHeader.js +3 -0
  115. package/dist/requests/requestHeader.js.map +1 -1
  116. package/dist/requests/requestLedger.js +1 -1
  117. package/dist/requests/requestLedger.js.map +1 -1
  118. package/dist/requests/types.d.ts +3 -1
  119. package/dist/requests/types.d.ts.map +1 -1
  120. package/dist/scheduler/mcpSurface.d.ts +9 -1
  121. package/dist/scheduler/mcpSurface.d.ts.map +1 -1
  122. package/dist/scheduler/mcpSurface.js +45 -12
  123. package/dist/scheduler/mcpSurface.js.map +1 -1
  124. package/dist/state.d.ts +3 -1
  125. package/dist/state.d.ts.map +1 -1
  126. package/dist/state.js +5 -2
  127. package/dist/state.js.map +1 -1
  128. package/dist/utils/claudeProfiles.d.ts +7 -0
  129. package/dist/utils/claudeProfiles.d.ts.map +1 -1
  130. package/dist/utils/claudeProfiles.js +19 -0
  131. package/dist/utils/claudeProfiles.js.map +1 -1
  132. package/dist/utils/openCodeCompactPlugin.d.ts +11 -3
  133. package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
  134. package/dist/utils/openCodeCompactPlugin.js +37 -5
  135. package/dist/utils/openCodeCompactPlugin.js.map +1 -1
  136. package/examples/.env.example +7 -7
  137. package/examples/docker-compose.yml +15 -14
  138. package/package.json +3 -1
package/README.md CHANGED
@@ -1,13 +1,36 @@
1
1
  <h1 align="center">Charness</h1>
2
2
 
3
- <p align="center"><em>formerly TelegramCode</em></p>
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/charness"><img src="https://img.shields.io/npm/v/charness?logo=npm" alt="npm version"></a>
5
+ <a href="https://www.npmjs.com/package/charness"><img src="https://img.shields.io/node/v/charness?logo=node.js" alt="Node.js version"></a>
6
+ <a href="https://github.com/olosegres/charness/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/charness" alt="license"></a>
7
+ </p>
8
+
9
+ <h2 align="center">Run multiple Claude Code / OpenCode agents and connect them to your Telegram or Jira.</h2>
4
10
 
5
- <h2 align="center">Telegram bot/group as a terminal for OpenCode, Claude Code — every thread like a terminal tab + scheduling, voice control</h2>
11
+ - **In Telegram** you control your agents through a bot or a group, so it becomes a terminal for OpenCode and Claude Code — every thread is like a terminal tab, plus scheduling and voice control.
12
+ - **In Jira** it starts an agent when you assign a ticket to a special user: the agent reads the description, comments and attachments and does the work according to the ticket's status — like a real employee.
13
+
14
+ <p align="center">It runs fine on your laptop. But it is called a <b>c</b>loud <b>harness</b> for a reason — put it on a server and your agents keep working while the laptop sleeps.</p>
6
15
 
7
16
  <p align="center">
8
17
  <img src="docs/images/hero-v2.png" alt="Charness — all your agents in Telegram; chat, code, ship; powerful slash commands" width="900" />
9
18
  </p>
10
19
 
20
+ ## What it does
21
+
22
+ ### Common (any surface)
23
+
24
+ These hold whichever surface you drive it from — the same core serves all of them:
25
+
26
+ - **Agent backends** — OpenCode (native server API over HTTP+SSE) and Claude Code (three backends — json-stream, per-turn and tmux)
27
+ - **Sleeping agents** — a Claude json-stream topic's process is stopped when idle (or after every turn in per-turn mode) and the next message resumes the same conversation by itself, prompt cache still warm
28
+ - **Several Claude accounts** — keep more than one Claude subscription signed in side by side and choose per topic which one a session runs on (`/accounts`); every account's usage windows are shown next to it. See [Several accounts](#several-accounts-accounts)
29
+ - **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits` turns waiting off per topic or skips a single resume. With a **fallback chain** (`/limits`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
30
+ - **Restart-safe** — agents run as external processes (tmux, `opencode serve`) that the bot re-adopts after a restart instead of killing them; a prompt sent while a session boots is buffered and replayed. See [Restart behaviour](#restart-behaviour)
31
+
32
+ ### Telegram
33
+
11
34
  Charness turns Telegram into a terminal for running agentic CLIs. Open as
12
35
  many tabs as you need — even two on the same project for parallel work — and
13
36
  drive the agents from your phone or tablet, voice messages included.
@@ -16,13 +39,9 @@ Think **OpenClaw for vibe coding**, driven entirely from Telegram: no complex
16
39
  setup, no extra dashboards — direct access to your own **OpenCode** /
17
40
  **Claude Code** running on your machine or server.
18
41
 
19
- ### Features
20
-
21
42
  - **Simple setup & management** — install the CLI, put your bot token in one `.env`, bind a topic to a folder, and run; everything after is managed from Telegram — no dashboards
22
43
  - **Multi-thread / topic** — one topic per directory with your agent; two topics can share one folder for parallel work — run almost unlimited agents in parallel
23
44
  - **Two chat surfaces** — forum-group topics and the owner's bot DM, both served at once by default, with tabs on either
24
- - **Agent backends** — OpenCode (native server API over HTTP+SSE) and Claude Code (three backends — json-stream, per-turn and tmux)
25
- - **Sleeping agents** — a Claude json-stream topic's process is stopped when idle (or after every turn in per-turn mode) and the next message resumes the same conversation by itself, prompt cache still warm
26
45
  - **Raw terminal** — `/terminal` binds a topic to a real `$SHELL` in the project folder
27
46
  - **Notifications** — when the agent asks a question, the bot pins that message so even a muted topic notifies you: the pin pierces mute, giving exactly one notification per question (unpinned once you answer)
28
47
  - **Scheduled & self-driving runs** — `/schedule` arms cron / one-shot / N-times jobs per topic, and the agent can schedule *itself* via injected MCP tools: nightly reviews, recurring reports, a "finish this tomorrow at 9" hand-off. Each job fires as a fresh session with your prompt; restart-safe, with one catch-up for a run missed while the bot was down
@@ -34,9 +53,8 @@ setup, no extra dashboards — direct access to your own **OpenCode** /
34
53
  - **Time-aware prompts** — `/timestamps on` prepends each forwarded prompt's send time (local-offset ISO), so a days-long session knows what "yesterday" or "2 days ago" means; agent-facing only, per topic
35
54
  - **Reply context** — reply to a message in a topic and the quoted text is folded into the prompt the agent receives, so it sees what you point at without re-pasting; works for text, voice and files (a photo, a document, an album…), both agents; a reply TO a photo, file or voice note names it, with the path of the bot's saved copy when it has one
36
55
  - **Your timezone** — `/timezone Europe/Moscow` once, and schedules fire at your wall-clock time while the agent is told what time it actually is; no server-clock guessing
37
- - **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits` turns waiting off per topic or skips a single resume. With a **fallback chain** (`/limits`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
38
56
 
39
- ## Two surfaces: group topics, bot DM, or both
57
+ #### Two surfaces: group topics, bot DM, or both
40
58
 
41
59
  The bot serves two Telegram surfaces:
42
60
 
@@ -50,289 +68,22 @@ The bot serves two Telegram surfaces:
50
68
  the reply grows in place in one message instead of arriving as a flood of
51
69
  separate messages.
52
70
 
53
- ## Where to run the bot server
54
-
55
- The bot runs anywhere the agent CLIs run:
56
-
57
- - **Your own working machine** — the simplest start: launch `telegramcode`
58
- from your projects parent folder and your laptop/desktop becomes the
59
- backend. The catch: agents are only reachable while the machine is on and
60
- awake.
61
- - **A remote server (cloud VPS/VDS or bare metal)** — the more convenient
62
- setup: organize your working environment there once (git + your repos,
63
- Claude Code / OpenCode, `telegramcode`) and you get an always-on dev box you
64
- drive from any device through Telegram — phone, tablet, voice. When picking
65
- one, a bare-metal server without virtualization beats a virtualized VPS/VDS:
66
- its NVMe drives are usually much faster, which matters once you also build
67
- and run a dev server there, not just the agents. Classic terminal access to
68
- the very same Claude sessions stays available over SSH — run plain `claude`
69
- in the project folder: it reads the same `~/.claude/projects/<cwd-slug>/`
70
- session store the bot uses, so a session started in a Telegram thread can
71
- be picked up in the terminal and vice versa.
72
-
73
- ## Quick Start
74
-
75
- Five steps: install the CLI, create a bot, (optionally) set up a group, launch
76
- from your projects folder, bind a topic to an agent.
71
+ ## Quick start
77
72
 
78
- > **[manual]** tags a step you must do by hand in the Telegram app — an agent
79
- > running on the host can't do it for you. Everything unmarked is a host-side
80
- > command (or file edit) that can be automated.
73
+ 1. `npm install -g charness` (needs Node ≥ 22.12).
74
+ 2. Put your bot token (from [@BotFather](https://t.me/BotFather)) into `~/.config/charness/.env` as
75
+ `TELEGRAM_BOT_TOKEN=…`.
76
+ 3. Run `charness` from the folder that holds your projects: `cd ~/projects && charness`.
77
+ 4. In your Telegram group (or the bot's DM): `/bind <subfolder>` in a topic, then `/claude` or `/opencode` and
78
+ talk to the agent — text or voice.
81
79
 
82
- ### 1. Install the CLI
80
+ Full setup — Telegram, Jira, environment, updating: [SETUP.md](SETUP.md)
83
81
 
84
- ```bash
85
- npm install -g charness # needs Node ≥ 22.12
86
- ```
87
-
88
- This registers the `telegramcode` command (it keeps the project's former name;
89
- `charness` works too). Coming from the old `telegramcode` npm package (2.x)?
90
- Run `npm uninstall -g telegramcode` first: both install the same commands. The
91
- config, `DATA_DIR` and sessions carry over as they are.
92
-
93
- Prefer the whole bot in a container, isolated from the host? Use
94
- [Run with Docker](#run-with-docker) instead — the bot and group steps are the
95
- same. To hack on the bot itself, see [Run from source](#run-from-source).
96
-
97
- ### 2. Create the bot
98
-
99
- **[manual]** In Telegram, with @BotFather:
100
-
101
- 1. Message [@BotFather](https://t.me/BotFather), send `/newbot`, follow prompts.
102
- 2. Save the token (`123456789:ABCdef...`).
103
- 3. **Disable privacy mode** — same chat: `/setprivacy` → pick this bot →
104
- `Disable`. Without this the bot only sees `/commands`, not free-form text.
105
-
106
- <p><img src="docs/images/01-botfather.png" width="360" alt="@BotFather: /newbot to get the token, then /setprivacy → this bot → Disable" /></p>
107
-
108
- Put the token where the bot reads it — the global config `.env`:
109
-
110
- ```bash
111
- mkdir -p ~/.config/telegramcode
112
- $EDITOR ~/.config/telegramcode/.env
113
- ```
82
+ ## Reference
114
83
 
115
- ```dotenv
116
- # ~/.config/telegramcode/.env — the one place your keys live
117
- TELEGRAM_BOT_TOKEN=123456789:ABCdef...
118
- # GROQ_API_KEY=... # optional: free voice transcription
119
- # OWNER_USER_ID=... # set to drive the bot from its DM (step 3)
120
- ```
121
-
122
- All keys the bot needs live in this one file — the bot token and the optional
123
- `GROQ_API_KEY` for voice sit side by side, no separate stores. (Agent
124
- provider auth is the exception, and lives inside the agents themselves:
125
- `claude login` for Claude, `/connect` or OpenCode's own config for OpenCode —
126
- see [Environment Variables](#environment-variables).) The wrapper loads env
127
- from two locations, in order — a global base and an optional per-project
128
- override:
129
-
130
- 1. `~/.config/telegramcode/.env` — base, set once, used everywhere.
131
- 2. `$PWD/.env` — per-project tweaks (last write wins).
132
-
133
- The full annotated template is `.env.example` (shipped with the package and in
134
- the repo).
135
-
136
- ### 3. Set up a group (optional) or DM
137
-
138
- The bot serves a **forum supergroup**, the **owner's private chat (DM)**, or
139
- both at once (`CHAT_MODE`, default `both`). The group is the nicest UX — a
140
- visible topic list, per-topic names/icons, quick agent switching — but it is
141
- **optional**: with `OWNER_USER_ID` set you can run entirely from the bot's DM.
142
-
143
- **To use a group (recommended)** — **[manual]**, all in the Telegram client:
144
-
145
- 1. In a Telegram client: `New Group` → name it → add the bot.
146
- 2. Open group settings → enable **Topics** (Forum mode).
147
- 3. Promote the bot to admin with these rights:
148
- - `Manage Topics` (required — bind/create threads)
149
- - `Delete Messages` (for `/clear_messages`)
150
- - `Pin Messages` (per-thread status banner + question alerts; `/doctor`
151
- warns if missing, but the bot still runs without it)
152
- 4. **Remove the bot from the group and add it again.** Telegram caches the
153
- privacy-mode flag on join; without re-adding, the bot keeps private-mode and
154
- ignores free-form messages (see
155
- [Troubleshooting → Bot doesn't see text](#bot-doesnt-see-text)).
156
-
157
- <p>
158
- <img src="docs/images/02-enable-topics.png" width="360" alt="Group settings → Topics toggle enabled (Forum mode)" />
159
- <img src="docs/images/03-promote-admin.png" width="360" alt="Bot admin rights: Manage Topics, Delete Messages, Pin Messages ticked" />
160
- </p>
161
-
162
- The group **id is automatic** — leave `ALLOWED_GROUP_ID` empty and the bot
163
- auto-pairs with the first forum supergroup an admin talks to it from (the
164
- `-100…` id is saved to `state.json`; re-point later with `/pair`). To pin a
165
- specific id by hand, set `ALLOWED_GROUP_ID` to the numeric value (a group
166
- **name** is not accepted) — that also disables auto-pairing. **Access control:**
167
- whoever is a creator/administrator of that group may drive the bot — there is no
168
- separate allow-list.
169
-
170
- **To use the DM instead (or as well):** **[manual]** send `/start` to
171
- [@userinfobot](https://t.me/userinfobot) for your numeric user id, then add it
172
- (host-side) to the same `.env`:
84
+ ### Commands (Telegram)
173
85
 
174
- ```dotenv
175
- OWNER_USER_ID=987654321
176
- ```
177
-
178
- With the default `CHAT_MODE=both`, an unset `OWNER_USER_ID` just leaves the DM
179
- surface inert (group-only); set it and the DM lights up alongside the group. Use
180
- `CHAT_MODE=dm` to skip the group entirely.
181
-
182
- ### 4. Launch from your projects folder
183
-
184
- ```bash
185
- cd ~/projects && telegramcode
186
- ```
187
-
188
- That `$PWD` — the parent folder holding your repositories — is the folder the
189
- bot works in; each topic binds to a subfolder under it. There's nothing else to
190
- configure for it. A single-instance lockfile (`$DATA_DIR/instance.lock`) stops a
191
- second bot starting under the same user; stale locks (after `kill -9`) are
192
- reclaimed automatically.
193
-
194
- ### 5. Bind a topic and talk to an agent
195
-
196
- This step is **[manual]** — it all happens inside the Telegram app.
197
-
198
- **In the group:**
199
-
200
- 1. Open the group; in the General topic send `/ls` — the bot lists subfolders
201
- under the work root.
202
- 2. Create a topic (the `+` button).
203
- 3. In the new topic: `/bind <subdir>` (auto-bound if the topic name matches a
204
- subdir).
205
- 4. `/claude` or `/opencode` → talk to the agent (text or voice).
206
-
207
- <p><img src="docs/images/04-bind-and-run.png" width="360" alt="A topic bound with /bind, /claude started, and the agent replying" /></p>
208
-
209
- **In the DM:** the same commands, one thread per agent — `/ls`,
210
- `/bind <subdir>`, then `/claude` or `/opencode`.
211
-
212
- Then: `/quit` ends the session; bare `/bind` inspects/reconfigures the binding;
213
- `/clear_messages` deletes the topic's bot messages.
214
-
215
- > **Continue the same session from a terminal.** No wrapper needed: run
216
- > `claude --dangerously-skip-permissions` in the project folder. It is the same
217
- > binary and the same `~/.claude/projects/<cwd-slug>/` session store the bot
218
- > uses, so a thread started in Telegram can be picked up with `claude --resume`
219
- > and vice versa.
220
-
221
- ### Run with Docker
222
-
223
- The whole bot runs in one container: the bot itself, tmux, every agent it starts (Claude Code in any backend,
224
- `/terminal`, OpenCode and its server) and the bot's MCP server. Everything works as on a host; the host only
225
- lends the container the folders it mounts, so the bot and its agents see nothing else of the machine.
226
-
227
- ```
228
- host container telegramcode-<instance>
229
- your instance env file (tokens) ── ro ──▶ the bot (hot mode) + a restart loop
230
- DATA_DIR, WORK_ROOT ──────▶ tmux, the agents, OpenCode, the bot MCP
231
- a clone of this repository ──────▶ /opt/telegramcode
232
- the agents' home folder ──────▶ /home/telegramcode: their Claude / OpenCode
233
- login, sessions and CLI installs
234
- ```
235
-
236
- 1. **Image and checkout.** Clone the repository for the container (a full clone of its own, not a worktree and
237
- not the checkout a host instance runs from) and build the image from it:
238
-
239
- ```bash
240
- git clone https://github.com/olosegres/charness /srv/telegramcode/checkout
241
- cd /srv/telegramcode/checkout && docker build -t telegramcode -f docker/Dockerfile .
242
- ```
243
-
244
- The image holds only system tools (tmux, git, ffmpeg, ripgrep, …) and the yarn the checkout's `package.json`
245
- names (build from the checkout's root, which is how the build reads it; rebuild after a yarn bump so no
246
- container has to download it). On first start the container installs the checkout's dependencies for the
247
- image and builds it; the bot installs Claude Code and OpenCode into the agents' home.
248
- 2. **Instance env file**, as for a host install (`TELEGRAM_BOT_TOKEN`, `DATA_DIR`, `WORK_ROOT`, …). It stays on
249
- the host and is mounted read-only. Every host path it names (`DATA_DIR`, `WORK_ROOT`, `TMUX_TMPDIR`, …) is
250
- mounted at the SAME path, so `state.json`, sessions and file paths stay valid inside and an instance can move
251
- between host and container unchanged.
252
- 3. **Run** (or use `examples/docker-compose.yml`):
253
-
254
- ```bash
255
- docker run -d --name telegramcode-main --hostname telegramcode-main \
256
- --restart unless-stopped --stop-timeout 60 \
257
- -v /srv/telegramcode/checkout:/opt/telegramcode \
258
- -v /srv/telegramcode/home:/home/telegramcode \
259
- -v /srv/telegramcode/data:/srv/telegramcode/data \
260
- -v /srv/projects:/srv/projects \
261
- -v /srv/telegramcode/instance.env:/srv/telegramcode/instance.env:ro \
262
- -e ENV_FILE=/srv/telegramcode/instance.env \
263
- telegramcode
264
- ```
265
-
266
- No port is published: Telegram is long polling, and the bot's MCP server and the OpenCode server listen on the
267
- container's own loopback. Keep the `--hostname` fixed and unique per instance: the instance lock tells its own
268
- restarted container from another machine by it.
269
- 4. **Log the agents in once**, with the account the agents should use (a separate one is best):
270
- `docker exec -it telegramcode-main claude`, then `/login` (or `/login` in a topic). OpenCode: `/connect` in a
271
- topic, or `docker exec -it telegramcode-main opencode auth login`. Never copy your own `~/.claude` login in:
272
- a second holder of its refresh token can log the first one out. For git, put a `.gitconfig` and an SSH key of
273
- their own into the agents' home.
274
-
275
- Running it:
276
-
277
- - **Restart the bot only** (agents keep running and are adopted): `docker exec telegramcode-main
278
- telegramcode-restart-bot`. A crashed bot is restarted by the container after 20 s.
279
- - **Update the bot**: `docker exec telegramcode-main /opt/telegramcode/scripts/self-update.sh` (by hand or from
280
- a timer on the host); hot mode picks the change up. A change to `src/cli.ts`, `src/cli/hot.ts` or `nodemon.json`
281
- needs the bot restart above. **Update Claude Code / OpenCode**: they update themselves in the agents' home, or
282
- `docker exec telegramcode-main npm install -g @anthropic-ai/claude-code` / `… opencode-ai`.
283
- - **A container restart is a reboot**: every agent process ends; each conversation resumes with its next message.
284
- - **Rootless Docker** (recommended): the container's root is your user, so everything written on the mounts is
285
- yours. **Rootful Docker**: add `--user "$(id -u):$(id -g)"` and give that user the agents' home.
286
- - **Moving a host instance in**: stop it first (the lock refuses a lock written on another host), mount its
287
- folders at their paths, and copy its conversations' folders from `~/.claude/projects/` into the agents' home
288
- `.claude/projects/` — without them each conversation starts a fresh session on its next message.
289
- - **What the agents can read**: everything mounted, the instance env file with its tokens, `DATA_DIR` and the
290
- checkout included. The container keeps the rest of the host out of reach; running the agents as a second user
291
- inside the container would close that gap later.
292
-
293
- ### Run from source
294
-
295
- To hack on the bot itself:
296
-
297
- ```bash
298
- git clone https://github.com/olosegres/charness && cd charness
299
- yarn install && yarn build
300
- npm install -g . # registers the `telegramcode` (and `charness`) command
301
- ```
302
-
303
- Shortcut: `yarn install-link` runs `yarn install && yarn build && npm link` in
304
- one shot — `npm link` symlinks the global `telegramcode` to this folder, so
305
- later rebuilds (`yarn build`) are picked up without re-installing. Config and
306
- launch are identical to steps 2–5 above.
307
-
308
- ## Required files structure
309
-
310
- Launch `telegramcode` from the parent folder that holds your project repos —
311
- that `$PWD` is the work root, and every topic binds to a subfolder under it:
312
-
313
- ```
314
- $PWD=/home/user/src ← run the bot here ($PWD = work root)
315
- ├── projectA/ ← Topic "projectA" (claude)
316
- ├── projectB/ ← Topic "projB-frontend" (claude)
317
- │ ← Topic "projB-backend" (opencode) ← one folder, two topics
318
- │ ← Topic "projB-refactor" (claude)
319
- └── projectC/ ← Topic "projectC" (opencode)
320
-
321
- Telegram forum supergroup
322
- ├── Bot is admin with can_manage_topics + can_delete_messages + can_pin_messages
323
- └── Routes each topic by (chatId, threadId) → remembered in state.json
324
- ```
325
-
326
- **What the bot remembers (`state.json`).** So a restart is seamless, the bot
327
- saves which topic is bound to which folder, each topic's live agent session (to
328
- re-attach it after a reboot), your per-topic preferences (agent, model,
329
- reasoning effort, language, display verbosity), scheduled prompts, and the
330
- paired group id. This file lives in `DATA_DIR` (default `~/.telegramCode`) and
331
- is written safely, so a crash can't corrupt it.
332
-
333
- ## Commands
334
-
335
- ### In any topic
86
+ #### In any topic
336
87
 
337
88
  Most commands work in any topic; a binding (`/bind`) is required only to
338
89
  actually start an agent or terminal in the folder.
@@ -340,12 +91,12 @@ actually start an agent or terminal in the folder.
340
91
  | Command | Description |
341
92
  |---|---|
342
93
  | `/claude`, `/opencode`, `/oc` | Start agent in this topic's bound folder |
343
- | `/terminal` | Open a raw `$SHELL` in the bound folder — see [Raw terminal](#raw-terminal-terminal) |
94
+ | `/terminal` | Open a raw `$SHELL` in the bound folder — see [Raw terminal](#raw-terminal-terminal-telegram) |
344
95
  | `/claude_mode` | Switch this topic's Claude backend (json-stream ⇄ per-turn ⇄ tmux-scrape); bare shows a picker — see [Claude Code backends](#claude-code-backends-claude_mode) |
345
96
  | `/model` | Switch model through a two-level picker: providers, then that provider's models (10 per page). On OpenCode the first row is 🕘 Recently used — a flat list of the models picked before, across providers (bot-wide, most recent first, up to 10), one tap to switch; it appears once something has been picked. Each provider row has a 🙈 to hide it from the picker (bot-wide, persisted; 👁 brings it back) — `/model <provider/model>` still reaches a hidden provider. If OpenCode is waiting on an old provider's retry, the next prompt interrupts that wait and starts with the selected model instead of sitting queued behind it |
346
97
  | `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working); in **General** the pick becomes the default for all topics. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in. **OpenCode providers**: the connected ones are listed with a «🔌 Disconnect <provider>» button and a 🔑 (connect again) each (the disconnect asks, then removes the stored credentials; a provider OpenCode enables from an environment variable, e.g. `OPENROUTER_API_KEY`, stays active — the reply says so and points at hiding it in `/model`); «➕ OpenCode provider» asks for the provider id and continues with the connect flow (OAuth methods when available, otherwise an API key whose message is deleted from Telegram). What happens when an account hits a usage limit is set in `/limits`. Text forms: `/accounts add <name>`, `/accounts use <name>`, `/accounts login <name>`, `/accounts remove <name>`, `/accounts autoswitch on\|off`. `/account`, `/connect [provider [key]]` and `/disconnect [provider]` still work as hidden aliases (the menu lists only `/accounts`). See [Several accounts](#several-accounts-accounts) |
347
98
  | `/effort` | Set reasoning effort (per-thread) via inline buttons. Claude: native `/effort` levels (`low…ultracode`). OpenCode: the current model's variants, applied per-prompt. No env configuration |
348
- | `/verbosity` | Output-verbosity macro (`minimal\|short\|full`): sets the thinking, tool-results and sub-agent display prefs at once; `/thinking`, `/tool_results`, `/subagent` point-override afterwards. Mixed prefs show as "custom" in the picker. The picker's second row (or `/verbosity stream\|stream_answers\|answers`) sets what the topic SHOWS — "Stream only" (default), "Stream + answers" (the stream plus the agent's pinned answers) or "Answers only" (the pinned answers, no stream; an agent that keeps writing after its final answer is reminded once to send what matters); a switch applies from the next message, and `/status` names the view |
99
+ | `/verbosity` | Output-verbosity macro (`minimal\|short\|full`): sets the thinking, tool-results and sub-agent display prefs at once; `/thinking`, `/tool_results`, `/subagent` point-override afterwards. Mixed prefs show as "custom" in the picker. The picker's second row (or `/verbosity stream\|stream_answers\|answers`) sets what the topic SHOWS — "Stream only" (default), "Stream + answers" (the stream plus the agent's pinned answers) or "Answers only" (the pinned answers, no stream; an agent that keeps writing after its final answer is reminded once to send what matters); the answers are written like for a manager or QA — brief and to the point; a switch applies from the next message, and `/status` names the view |
349
100
  | `/thinking` | Chain-of-thought display: `full` keeps the reasoning, `short` collapses to "💭 thought for Ns", `minimal` keeps only the live working cue |
350
101
  | `/tool_results` | Completed tool-call output: `full` whole body, `short` capped (15 lines / 1200 chars), `minimal` transient 🔧 status only |
351
102
  | `/subagent` | Sub-agent transcript: status-only with a ticking elapsed counter (`minimal`/`short`), or streamed "🤖 ⤷" chunks (`full`) |
@@ -358,8 +109,8 @@ actually start an agent or terminal in the folder.
358
109
  | `/c`, `/y`, `/n` | Ctrl+C / "y" / "n" |
359
110
  | `/enter`, `/up`, `/down`, `/tab` | tmux key passthrough |
360
111
  | `/esc`, `/escape` | Send a raw Escape — interrupt the current turn / dismiss a selector |
361
- | `/schedule` | Schedule a prompt in free text — the agent parses the time and owns the job; see [Scheduler](#scheduler-schedule) |
362
- | `/reminders` | Reminders set with buttons only — the bot posts and pins them itself, with no agent involved; works in any topic, bound or not. See [Reminders](#reminders-reminders) |
112
+ | `/schedule` | Schedule a prompt in free text — the agent parses the time and owns the job; see [Scheduler](#scheduler-schedule-telegram) |
113
+ | `/reminders` | Reminders set with buttons only — the bot posts and pins them itself, with no agent involved; works in any topic, bound or not. See [Reminders](#reminders-reminders-telegram) |
363
114
  | `/clear_messages` | Delete bot messages in this topic (up to 48h, Telegram limit) |
364
115
  | `/clear` | Forwarded to the agent (Claude wipes context; OpenCode plain text) — not a bot command anymore. Also purges the topic's file-intake dir |
365
116
  | `/compact` | Compact the agent's context — a real, confirmed compaction on OpenCode and on the default (stream) Claude backend; the tmux Claude backend has the literal command forwarded (its TUI compacts); terminal: not supported |
@@ -369,7 +120,7 @@ actually start an agent or terminal in the folder.
369
120
  | `/bind` | Bare: current binding + folder picker, with «leave current dir» (the old `/unbind`) and «create new folder» buttons |
370
121
  | `/mcp` | List MCP servers active for this thread |
371
122
 
372
- ### Info / ops (work anywhere)
123
+ #### Info / ops (work anywhere)
373
124
 
374
125
  | Command | Description |
375
126
  |---|---|
@@ -387,13 +138,13 @@ actually start an agent or terminal in the folder.
387
138
  | `/timezone` | Set the timezone every clock the bot touches speaks in — schedules, agent time context, time renders. `/timezone Europe/Moscow`, `/timezone +04:00`, `/timezone auto` (host zone); bare opens a region → zone picker. Instance-wide, not per chat |
388
139
  | `/pair` | Bind this forum supergroup to the bot (re-point auto-pairing) — works from any topic of the target group; a numeric `ALLOWED_GROUP_ID` env locks pairing |
389
140
 
390
- ### General-only
141
+ #### General-only
391
142
 
392
143
  | Command | Description |
393
144
  |---|---|
394
145
  | `/quit-all`, `/quitall` | End every active agent in every bound topic (also releases their session ids) |
395
146
 
396
- ### Natural language
147
+ #### Natural language
397
148
 
398
149
  In a bound topic you can also type:
399
150
  - `claude fix the bug` / `opencode add tests`
@@ -407,7 +158,7 @@ then 15 s) — no need to re-record the voice note.
407
158
  `/terminal` is never started from a natural-language phrase — only the
408
159
  explicit command opens a shell.
409
160
 
410
- ## Claude Code backends (`/claude_mode`)
161
+ ### Claude Code backends (`/claude_mode`)
411
162
 
412
163
  "Claude Code" in a topic is one user-facing choice with three interchangeable
413
164
  backends:
@@ -496,7 +247,7 @@ so one account's usage limit does not stop every topic.
496
247
  percentage is USED. A reset is shown as a time from
497
248
  now (`🔄 in 2d 1h`; the two largest units, minutes only under an hour); an
498
249
  unknown 5h reset shows the weekly one, and with no reset at all only the
499
- percentage is shown. `telegramcode claude-profiles` prints
250
+ percentage is shown. `charness claude-profiles` prints
500
251
  the same relative form; its `--json` and the MCP tool keep ISO instants. The bot reads the figures with
501
252
  each profile's own login every 10 minutes, when `/accounts` or `/limits` opens and when an
502
253
  agent asks; a json-stream session's own rate-limit events keep them current
@@ -528,9 +279,9 @@ so one account's usage limit does not stop every topic.
528
279
  any other model → the shared weekly window and the session window). It then
529
280
  starts the child with `CLAUDE_CONFIG_DIR=<configDir>` (`null` = the default
530
281
  profile: set nothing). The same answer from a shell, for a worker without
531
- MCP: `telegramcode claude-profiles [--best] [--model <name>] [--json]` —
282
+ MCP: `charness claude-profiles [--best] [--model <name>] [--json]` —
532
283
  `--best` prints just the directory (an empty line for the default), so
533
- `CLAUDE_CONFIG_DIR="$(telegramcode claude-profiles --best --model fable)"`
284
+ `CLAUDE_CONFIG_DIR="$(charness claude-profiles --best --model fable)"`
534
285
  works as is; it reads the bot's cache off `DATA_DIR/state.json`.
535
286
 
536
287
  ### Usage limits and the fallback models (`/limits`)
@@ -592,7 +343,7 @@ instance-wide, ordered list of **fallback models** (where to go), top = preferre
592
343
  — the switch notice names the reset time; switch back by hand (`/accounts`,
593
344
  `/model`, `/claude`, `/opencode`).
594
345
 
595
- ## Scheduler (`/schedule`)
346
+ ### Scheduler (`/schedule`) (Telegram)
596
347
 
597
348
  A topic can have scheduled prompts: at fire time the bot posts the prompt
598
349
  into the topic, pins the announcement (pins accumulate as run history),
@@ -612,7 +363,7 @@ with the thread's last-used backend.
612
363
  them (an expired one-shot is dropped). Reminders keep running — see
613
364
  below. Run history: `DATA_DIR/scheduler-runs.jsonl`.
614
365
 
615
- ## Reminders (`/reminders`)
366
+ ### Reminders (`/reminders`) (Telegram)
616
367
 
617
368
  A reminder is the bot's own notification, not a job for an agent: at the due
618
369
  time the bot posts your text into the topic and pins it — and the pin is what
@@ -647,7 +398,7 @@ a single catch-up.
647
398
  Reminders are yours, not the agent's: the scheduling tools an agent can call
648
399
  do not see them, so it can neither list nor delete them.
649
400
 
650
- ## Raw terminal (`/terminal`)
401
+ ### Raw terminal (`/terminal`) (Telegram)
651
402
 
652
403
  `/terminal` binds the topic to a real interactive `$SHELL` (in tmux) in the
653
404
  bound folder — a third backend alongside the agents, mutually exclusive
@@ -659,7 +410,7 @@ re-adopted. Known v1 limitation: full-screen TUIs (vim, htop, less) render
659
410
  messy; normal commands, builds, and logs stream cleanly. The shell comes
660
411
  from the `SHELL` env (fallback `/bin/bash`).
661
412
 
662
- ## File intake
413
+ ### File intake (Telegram)
663
414
 
664
415
  Send a file to a bound topic with an active agent and the bot hands it to
665
416
  the agent:
@@ -675,9 +426,9 @@ the agent:
675
426
  bare `/clear` purges the topic's intake dir (the agent's context is gone,
676
427
  so the files are useless); files older than 30 days are swept daily.
677
428
 
678
- ## Observability
429
+ ### Observability
679
430
 
680
- - **Output trace** — ON by default for every thread: each incoming update,
431
+ - **Output trace** (Telegram) — ON by default for every thread: each incoming update,
681
432
  adapter emit, and outgoing Bot API call (with outcome, incl. 429 details)
682
433
  is recorded into hourly buckets `DATA_DIR/output-trace-*.jsonl` (pruned
683
434
  after 6h). `/trace off all` turns it off durably; bare `/trace` reports
@@ -686,11 +437,11 @@ the agent:
686
437
  - **Console tee** — the bot's stdout/stderr is mirrored to
687
438
  `DATA_DIR/bot-console-*.log` (same hourly buckets, same 6h prune), so
688
439
  boot logs stay readable post-incident without the operator's terminal.
689
- - **`/timestamps on`** — prepends each forwarded prompt's send time
440
+ - **`/timestamps on`** (Telegram) — prepends each forwarded prompt's send time
690
441
  (local-offset ISO) so a days-long session knows what "yesterday" means.
691
442
  Agent-facing only, default off, persisted per topic.
692
443
 
693
- ## Timezone
444
+ ### Timezone (Telegram)
694
445
 
695
446
  The bot runs on the host clock, which on a typical server is UTC. `/timezone`
696
447
  declares yours ONCE, instance-wide, and every clock the bot touches follows:
@@ -715,219 +466,7 @@ The current zone and its wall clock also show in `/status`. Nothing is stored
715
466
  until you set one, so an install that never runs `/timezone` behaves exactly as
716
467
  it did before.
717
468
 
718
- ## Environment Variables
719
-
720
- ### Set Before Launch
721
-
722
- | Variable | Description |
723
- |---|---|
724
- | `TELEGRAM_BOT_TOKEN` | Token from @BotFather |
725
-
726
- Start from the parent folder containing your projects: `cd ~/projects && telegramcode`.
727
- That `$PWD` is the folder the bot works in — nothing else to set for it.
728
-
729
- **Access control.** There is no user allow-list. Whoever is a **creator or
730
- administrator of the served forum group** may talk to the agent — read live from
731
- Telegram (`getChatAdministrators`) and cached for 1h. Promote someone in the
732
- group to grant access; demote/remove them to revoke it — the bot subscribes to
733
- `chat_member` updates, so an admin change invalidates the cache and takes
734
- effect immediately (the 1h TTL is only the fallback). The bot must be a group
735
- admin itself (it already needs that to create topics and pin). Anonymous admins
736
- can't be matched from their messages, so post non-anonymously. The DM surface
737
- (when enabled) is gated separately: only the configured `OWNER_USER_ID` may
738
- talk to the bot there.
739
-
740
- > **Security model — group admin (or the DM owner) ⇒ shell on the host.** The agents run with
741
- > permission checks disabled (`--dangerously-skip-permissions` for Claude,
742
- > auto-approve for OpenCode) and `/terminal` opens a raw `$SHELL` in the bound
743
- > folder, so anyone who can talk to the bot can execute arbitrary commands as
744
- > the bot's OS user. Only promote people you would trust with SSH access to
745
- > that machine, and keep the served group itself private.
746
-
747
- ### External Optional
748
-
749
- | Variable | Default | Description |
750
- |---|---|---|
751
- | `ALLOWED_GROUP_ID` | (auto-pair) | Numeric forum supergroup id (`-100…`). Leave empty to auto-pair with the first forum group a group **admin/creator** contacts the bot from (id is saved to `state.json`; re-point with `/pair`). A **name** is not accepted. A numeric value disables auto-pairing |
752
- | `DATA_DIR` | `~/.telegramCode` | Per-instance state. **Mandatory** if you run two bots on the same host — otherwise both share `state.json` and `mcp.json` and corrupt each other |
753
- | `CHAT_MODE` | `both` | Which surface(s) this instance serves: `group`, `dm`, or `both` — see [Two surfaces](#two-surfaces-group-topics-bot-dm-or-both) |
754
- | `OWNER_USER_ID` | — | **Optional.** Numeric Telegram user id of the owner — leave it unset to run group-only (the DM surface stays inert; a group works fully without it). Set it only to enable the owner-DM surface; **required** solely for `CHAT_MODE=dm` |
755
- | `GROQ_API_KEY` | — | Recommended for voice transcription. Without it, voice messages are not transcribed unless you intentionally configure the OpenAI fallback |
756
-
757
- **Localized in 12 languages** — the bot UI (commands, buttons, notices) speaks
758
- 中文, English, Français, ქართული, Deutsch, हिन्दी, 日本語, Português, Русский,
759
- Español, Українська, Oʻzbekcha. The language is automatic per Telegram chat:
760
- explicit `/language <locale>` override wins, then the sender's Telegram client
761
- language, then the last supported Telegram locale seen for that chat, then
762
- English.
763
- Supported locales: `en`, `de`, `fr`, `es`, `pt`, `ru`, `zh`, `ja`, `hi`, `uz`,
764
- `ka`, `uk`. Bare `/language` opens a single-message inline picker with each language
765
- shown by its own name (endonym), sorted A→Z by English name, and a `🌐 Auto` button
766
- (all 12 fit at once — no pagination). Tapping a language (or `🌐 Auto`) applies it, then the menu
767
- disappears and a short confirmation stays in the chosen language;
768
- `/language auto` returns a DM/group to automatic selection.
769
-
770
- Agent provider/auth setup is normally done inside the agents themselves:
771
- `claude login` for Claude CLI and `/connect <provider-id>` (for example,
772
- `/connect openrouter`) or OpenCode's own config/plugins for OpenCode. No provider
773
- API key env var is required by the bot for text sessions. For OpenCode, install
774
- any third-party provider plugins or authentication resolvers before launch if
775
- your chosen providers need them.
776
-
777
- ### Advanced / Not Normally Needed
778
-
779
- | Variable | Default | Set only when |
780
- |---|---|---|
781
- | `WORK_ROOT` | `$PWD` | You cannot control the process cwd; normal launch uses `cd <projects-parent> && telegramcode` |
782
- | `OPENCODE_URL` | `http://localhost:4096` | You use a custom OpenCode port or external server; the port must differ per instance |
783
- | `OPENCODE_USERNAME` | `opencode` | `OPENCODE_PASSWORD` is set for a protected OpenCode server |
784
- | `OPENCODE_PASSWORD` | — | Connecting to a protected OpenCode server |
785
- | `OPENCODE_ALLOW_REMOTE` | — | `OPENCODE_URL` intentionally points outside loopback |
786
- | `OPENCODE_BIN` | (auto) | The `opencode` binary is not on PATH or you use a fork |
787
- | `CLAUDE_BIN` | (auto) | The `claude` binary is not on PATH due to nvm/asdf/systemd PATH differences |
788
- | `OPENAI_API_KEY` | — | You intentionally use OpenAI Whisper as the voice fallback instead of Groq |
789
- | `ANTHROPIC_API_KEY` | — | A custom MCP/OpenCode plugin/auth resolver explicitly reads it; not needed for normal Claude CLI auth. The json-stream Claude backend strips it from the agent's env (subscription billing) |
790
- | `SHELL` | `/bin/bash` | You want `/terminal` to open a different shell than your login shell (host env var, not set by the bot) |
791
- | `SCHEDULER_MCP_PORT` | an OS-chosen free loopback port | You need a stable, explicitly chosen port; it must differ from this instance's `OPENCODE_URL` port |
792
- | `CLAUDE_SCRAPE_DEBUG` | off | You are debugging Claude tmux scraping and need full RAW/FILTERED chunks |
793
- | `AGENT_IDLE_MINUTES` | `55` | A json-stream Claude topic idle for this long is compacted (when `/compact_on_idle` allows) and its process stopped; the session sleeps and the next message resumes it — shorten it only for a test; a non-positive or non-numeric value keeps the default |
794
- | `REQUEST_BACKSTOP_MINUTES` | `90` | A request (a Jira hand-over, a message in an answers-only topic) with nothing seen working on it for this long is re-posted to its session — shorten it only for a test; a non-positive or non-numeric value keeps the default |
795
- | `TMUX_SOCKET_NAME` | — (the default tmux server) | The instance must run its agents on its OWN tmux server (`tmux -L <name>`; any plain name except `default`). Recommended for a second instance on one host: at boot an instance kills every agent session on its server that its own state does not know |
796
- | `ENV_FILE` | — | Absolute path of the ONLY env file to read — no `~/.config/telegramcode/.env`, no `$PWD/.env`; its values win over the inherited environment. Set it in the launching environment, never inside an env file (the start refuses that). `scripts/run-isolated.sh <env-file>` starts an instance with a clean environment plus this variable |
797
- | `CONNECTORS` | `telegram` | Experimental: the surfaces this instance serves, as a comma list — see [Jira connector](#jira-connector-experimental). An instance that lists `jira` refuses to start on Node below 22.12 and without `ENV_FILE`, `TMUX_SOCKET_NAME` and `DATA_DIR/jira.json`, and — with Telegram off — with a `TELEGRAM_BOT_TOKEN` or any `ATLASSIAN_*` variable in its environment. Any instance refuses to start on a `DATA_DIR` that holds conversations of a surface it does not serve |
798
-
799
- > `WORK_DIR` (1.x) is retired. Use the wrapper from the desired parent folder
800
- > instead of carrying the old env forward.
801
-
802
- ## Jira connector (experimental)
803
-
804
- The same core can serve **Jira Cloud issues** instead of (or next to) Telegram topics: an issue assigned to
805
- a dedicated AI account becomes a conversation with an agent, and the agent answers in comments.
806
-
807
- ```
808
- a person assigns an issue to the AI account (in a trigger status)
809
- → the instance polls Jira as the AI account and opens a request
810
- → the agent works in the project's folder; its streamed output never reaches Jira
811
- → it answers through the answer_request tool:
812
- progress → a comment, the issue stays with the AI
813
- question → a comment, the issue goes back to the person who handed it over
814
- final → a comment, the issue goes back to that person
815
- → an agent that stops answering is reminded; when the reminders give up, a
816
- comment says a person has to look, and the issue goes back
817
- ```
818
-
819
- - **One issue = one conversation = one agent session**, bound to the folder its project maps to. The agent
820
- reads the issue's text as information, never as instructions.
821
- - **The agent gets the whole issue.** Every request's prompt carries the issue's fields, the full description,
822
- its sub-tasks or an epic's children, links and remote links, an attachment list and ALL comments (oldest
823
- first, restricted ones marked) — with a trusted group, minus what outsiders wrote (see below). A screenshot
824
- or video pasted into the description or a comment shows up as a placeholder where it sat, naming its
825
- attachment. Nothing is cut: a comment over 10 000 characters, or any text
826
- that would not fit the prompt, is written whole to a file in the conversation's folder under `DATA_DIR/files`
827
- and the prompt says where. A later hand-over to the same conversation carries only what changed since the
828
- agent's last prompt (new and edited comments, deleted ones named, the rest listed as unchanged); a fresh
829
- session, or one whose context was compacted, gets the whole issue again.
830
- - **Who gets the answer.** Each hand-over is a request from its sender. Two people handing one issue over in
831
- turn get two answers, each their own comment; the first closing answer hands the issue back to its sender,
832
- the second finds the issue no longer the AI's and leaves the assignee alone. The same person handing over
833
- again replaces their earlier open request; the agent answers only what the new hand-over adds (if it
834
- already finished the first, a short pointer to that answer).
835
- - **Budget.** An issue handed over more than `runBudgetPer24h` times in 24 hours is handed back with a notice
836
- and opens no request.
837
- - **Who can steer it.** With `trustedGroup` (see Setup) only the members of ONE Jira group, the AI account and
838
- Jira's apps and automations count. A hand-over by anyone else — another person, a service-desk customer —
839
- opens no request: one notice on the issue names the group and the issue goes back to that person. Text
840
- written or edited by anyone else (a comment, the description, the summary, labels and extra fields, a
841
- file's name and content) is replaced in the prompt by a `[withheld: …]` marker, so the agent knows there
842
- is something and can ask its requester. Without `trustedGroup` everyone who can edit or comment steers the
843
- agent, and the instance logs one line saying so.
844
-
845
- ### Setup
846
-
847
- 1. Create a Jira account for the AI (its API token is the instance's only secret) and note its account id.
848
- 2. Recommended: create or choose a Jira group of the people whose words may steer the AI (for example
849
- `ai-trusted`), and give the AI account the *Browse users and groups* global permission so that it can read
850
- the group. Then the agent reads only the text and files of its members and of Jira's apps and automations,
851
- and only their hand-overs start it.
852
- 3. An instance of its own: a `DATA_DIR`, a `WORK_ROOT` with one folder per project, a private tmux server, and
853
- an env file that is the ONLY configuration it reads:
854
-
855
- ```bash
856
- # /path/to/jira-instance.env
857
- CONNECTORS=jira
858
- DATA_DIR=/path/to/jira-instance/data
859
- WORK_ROOT=/path/to/jira-instance/work
860
- TMUX_SOCKET_NAME=jira-instance
861
- OPENCODE_URL=http://localhost:4097 # a port of its own — the default 4096 is refused (other instances use it)
862
- JIRA_AI_API_TOKEN=... # referenced by jira.json below, never written there
863
- ```
864
-
865
- 4. `DATA_DIR/jira.json` — the allowlist, the trusted group and the credentials (`${VAR}` expands from the env file; an unknown
866
- key is an error):
867
-
868
- ```json
869
- {
870
- "site": "example.atlassian.net",
871
- "email": "ai-account@example.com",
872
- "apiToken": "${JIRA_AI_API_TOKEN}",
873
- "accountId": "<the AI account's id>",
874
- "projects": {
875
- "PROJ": { "folder": "proj", "triggerStatuses": ["In Progress"], "extraFields": ["customfield_10042"] }
876
- },
877
- "trustedGroup": "ai-trusted",
878
- "pollIntervalSeconds": 90,
879
- "runBudgetPer24h": 5,
880
- "model": "opus",
881
- "effort": "high"
882
- }
883
- ```
884
-
885
- `trustedGroup` (optional, but without it everyone who can edit or comment steers the agent) is the group's
886
- NAME — one group per instance; a team with several trust circles runs a second instance. The instance
887
- resolves it to the group's id at start, so renaming the group while it runs keeps working, and reads its
888
- members again whenever its last read is older than 5 minutes: a removed member loses the AI's trust within
889
- that time. List only projects where only your team can write, and where no automation acts on text or
890
- actions from outside it — an e-mail handler, a portal form, a copy from another project, a rule that
891
- hands an issue to the AI when someone outside the team acts: an app's or automation's text and hand-over
892
- are trusted, so such a rule brings that outside text, or that person's hand-over, to the agent. Nor list a
893
- project where anyone outside the team holds the *Edit all comments* permission: Jira Cloud keeps no history
894
- of a comment's edits, so a comment is judged by its author and its LAST editor only — an outsider's edit of a
895
- member's comment, followed by a member's own edit, reads as trusted.
896
-
897
- `projects` maps each served project key to its folder under `WORK_ROOT` and the status names in which an
898
- assigned issue is a request; the agent loads that folder's project memory (`CLAUDE.md`, `.claude/`, … in it
899
- and in every parent) like any Claude Code session, while your user-level Claude setup stays out of its
900
- sessions — except that for a folder under HOME that walk reads `~/.claude/CLAUDE.md` back in.
901
- `pollIntervalSeconds` is 10–600 (default 90);
902
- `runBudgetPer24h` defaults to 5; `model` and `effort` set the sessions' Claude model and reasoning effort
903
- (optional; absent → `opus` and `high`, either key overrides only its own default). `extraFields` (optional,
904
- none by default) names fields by id (`customfield_…`, or a system one like `duedate`) the agent should see next to the standard
905
- ones, shown under their site name; an id the site does not list is logged at start and left out. `agentBinaries` (optional, host runtime) maps a tool name to the absolute path of an executable — for
906
- example `{ "ffmpeg": "/opt/tools/ffmpeg" }` — which is checked at start and put on the agents' PATH. `adapter` is `claude-json-stream` (the default — the agent process is stopped when an issue
907
- idles) or `claude-per-turn` (stopped after every answer); either way the issue's conversation sleeps between
908
- requests and the next request resumes it.
909
-
910
- 5. Start it with a clean environment, so nothing of the calling shell reaches it:
911
-
912
- ```bash
913
- scripts/run-isolated.sh /path/to/jira-instance.env
914
- ```
915
-
916
- The start refuses a missing `ENV_FILE`, `TMUX_SOCKET_NAME` or `jira.json`, an `OPENCODE_URL` on the default
917
- port, a `TELEGRAM_BOT_TOKEN` or any `ATLASSIAN_*` variable when Telegram is off, a token that belongs to
918
- another account, an unknown project or status, a `trustedGroup` the site does not have or the AI account
919
- may not read, and a `DATA_DIR` holding another surface's state. An unreachable Jira does not refuse the start —
920
- polling backs off and resumes.
921
-
922
- The agent sessions of a Jira instance run without the user-level Claude settings, hooks and skills, with the
923
- bot's MCP server only, without the native question tool, and with an environment reduced to a short allowlist
924
- — the Jira token is not in the agent's environment. The agent still runs as the instance's OS user and can read
925
- the instance's env file and `DATA_DIR`, so run the instance under an OS user that holds nothing else, with an AI
926
- account that sees only the allowlisted projects. **A production Jira account runs only in the Docker container**
927
- ([Run with Docker](#run-with-docker)): there the agent can still read the instance's env file and `DATA_DIR`,
928
- but nothing of the host outside the container's mounts. Module details: `src/connectors/jira/README.md`.
929
-
930
- ## Bot-injected agent tools
469
+ ### Bot-injected agent tools
931
470
 
932
471
  The bot injects its own `telegramBot` MCP server into every bot-started session
933
472
  — for Claude via a generated `--mcp-config`, for OpenCode via runtime
@@ -939,9 +478,6 @@ agent even when two clients use the same JSON-RPC request id. It exposes:
939
478
 
940
479
  - `schedule_create` / `schedule_list` / `schedule_cancel` — the agent-side
941
480
  scheduling API behind `/schedule`;
942
- - `jira_get_attachment` — Jira conversations only: saves the original of one attachment of the conversation's
943
- own issue (any size) and returns its path, so the agent can read an image or run `ffmpeg` on a video (with a
944
- trusted group, only a file uploaded by a trusted account);
945
481
  - `compact_conversation` — lets the agent compact its OWN session when you ask it
946
482
  to in plain words ("compact our conversation"). It arms the compaction and
947
483
  returns immediately; the bot runs the real compaction the moment the current
@@ -992,52 +528,7 @@ Third-party OpenCode MCP servers, including a separate `telegram-mcp`, belong to
992
528
  the OpenCode configuration of the Linux user that runs the bot. They are not
993
529
  injected by Charness; run `opencode mcp list` as that user to verify them.
994
530
 
995
- ## Two instances on one host
996
-
997
- Two host instances, or a host instance next to a container: each pair of variables below must differ to avoid
998
- silent corruption. Two containers need only different tokens, groups, `DATA_DIR`s, container names and hostnames: their
999
- ports and tmux servers are their own.
1000
-
1001
- | What | Why it must differ |
1002
- |---|---|
1003
- | `TELEGRAM_BOT_TOKEN` | Telegram routes updates to a single long-poller per token; sharing → message loss |
1004
- | `ALLOWED_GROUP_ID` | The bot gates by group id; sharing → cross-instance leakage |
1005
- | `WORK_ROOT` | Each instance manages its subtree; sharing → tmux name collisions |
1006
- | `DATA_DIR` | `state.json` / `mcp.json` / `threads/` per instance; sharing → corrupted JSON |
1007
- | `OPENCODE_URL` port | OpenCode server binds the port; second start fails with `EADDRINUSE` and you'd silently share sessions |
1008
- | `SCHEDULER_MCP_PORT` | Optional stable scheduler-MCP port; if set, it must differ from that instance's `OPENCODE_URL` port |
1009
- | `TMUX_SOCKET_NAME` | Optional, recommended: each instance's boot kills the agent sessions on its tmux server that its own state does not know — on one shared server, the other instance's |
1010
-
1011
- ## Updating a deployment (self-update, no root)
1012
-
1013
- `scripts/self-update.sh` refreshes a checkout in place, running as the
1014
- checkout's owning user — no root, no cross-user copying. It is safe to run
1015
- blindly (e.g. from cron):
1016
-
1017
- - fast-forward only: it skips silently when the tree has tracked changes, a
1018
- merge/rebase is in progress, or local history diverged from the upstream —
1019
- a dev clone that is ahead of origin is never touched;
1020
- - `yarn install --immutable` runs only when `yarn.lock` / `package.json`
1021
- changed;
1022
- - a hot-mode instance (`telegramcode hot`) rebuilds and restarts the worker
1023
- by itself (after a dependency install the script touches `tsconfig.json`
1024
- so `tsc -w` recompiles with the new modules); a non-hot instance gets
1025
- `yarn build` plus a restart notice — the script never kills processes;
1026
- - changes to the hot supervisor itself (`src/cli.ts`, `src/cli/hot.ts`,
1027
- `nodemon.json`) are flagged with a warning: those need a manual
1028
- `telegramcode hot` restart (nodemon reloads only the worker).
1029
-
1030
- For unattended updates give the deploying user read access to the repo (e.g.
1031
- a read-only GitLab deploy key on a passphrase-less SSH key) and add a cron
1032
- entry:
1033
-
1034
- ```cron
1035
- */10 * * * * /path/to/telegramcode/scripts/self-update.sh >> "$HOME/.local/state/telegramcode/self-update.log" 2>&1
1036
- ```
1037
-
1038
- (create the log directory once: `mkdir -p ~/.local/state/telegramcode`).
1039
-
1040
- ## Restart behaviour
531
+ ### Restart behaviour
1041
532
 
1042
533
  Bot restarts are designed to be invisible: agents run in external
1043
534
  processes (`tmux`, `opencode serve`), so the bot re-adopts them instead
@@ -1102,7 +593,7 @@ thread not found` from a send triggers binding cleanup. Closed topics
1102
593
  are detected by `TOPIC_CLOSED` errors and surface a friendly message
1103
594
  asking the user to reopen.
1104
595
 
1105
- In the Docker container (see [Run with Docker](#run-with-docker)) the same holds for a bot restart
596
+ In the Docker container (see [Run with Docker](SETUP.md#run-with-docker)) the same holds for a bot restart
1106
597
  inside the container; a restart of the container itself ends every agent process, and each conversation
1107
598
  resumes with its next message.
1108
599
 
@@ -1111,9 +602,9 @@ The default `control-group` mode kills tmux and `opencode serve` children,
1111
602
  which defeats restart/reattach. Process restarts intentionally leave agent
1112
603
  sessions alive; use `/quit` or `/quit-all` when you want to terminate them.
1113
604
 
1114
- ## Troubleshooting
605
+ ### Troubleshooting
1115
606
 
1116
- ### Bot doesn't see text
607
+ #### Bot doesn't see text (Telegram)
1117
608
 
1118
609
  You disabled privacy mode in @BotFather **after** adding the bot. The
1119
610
  flag is cached on join. Fix:
@@ -1124,7 +615,7 @@ flag is cached on join. Fix:
1124
615
 
1125
616
  Verify with `/doctor` — the privacy line should report it as disabled.
1126
617
 
1127
- ### `/bind` says "folder not found"
618
+ #### `/bind` says "folder not found" (Telegram)
1128
619
 
1129
620
  The folder you launched from doesn't contain a subfolder with that name (or
1130
621
  it's a symlink pointing outside it). Run `/ls` to see what's available, or fix
@@ -1132,19 +623,19 @@ the host path. Path-traversal attempts (`../`, absolute paths,
1132
623
  NUL bytes, NFC-vs-NFD mismatches) are rejected by design — see
1133
624
  `validateSubdir` in `src/validation.ts`.
1134
625
 
1135
- ### "Missing right" / `/clear_messages` does nothing
626
+ #### "Missing right" / `/clear_messages` does nothing (Telegram)
1136
627
 
1137
628
  The bot needs admin rights `can_delete_messages` and `can_pin_messages`
1138
629
  in addition to `can_manage_topics`. Open group settings → admins →
1139
630
  the bot → tick the missing boxes. `/doctor` shows the exact gap.
1140
631
 
1141
- ### Two instances on the same machine fight each other
632
+ #### Two instances on the same machine fight each other
1142
633
 
1143
634
  You forgot to set distinct `DATA_DIR` and `OPENCODE_URL`. The bot logs
1144
635
  `[startup] DATA_DIR=<path>` on boot — if both instances log the same
1145
- path, fix `.env` and `docker compose down && up -d`.
636
+ path, give each its own `DATA_DIR` and `OPENCODE_URL` port and restart both.
1146
637
 
1147
- ### Claude opens a session picker instead of resuming
638
+ #### Claude opens a session picker instead of resuming
1148
639
 
1149
640
  The 1.x adapter invoked `claude --resume` with no UUID and relied on the
1150
641
  interactive picker, which doesn't survive a non-interactive pty. 2.0
@@ -1154,12 +645,12 @@ anyway, your local claude CLI is older than 2.1.x — upgrade with
1154
645
  `npm i -g @anthropic-ai/claude-code` (the bot also auto-installs on
1155
646
  first use, see `src/installManager.ts`).
1156
647
 
1157
- ### OpenCode "address already in use"
648
+ #### OpenCode "address already in use"
1158
649
 
1159
650
  Two instances on the same host with the same `OPENCODE_URL`. Pick a
1160
651
  different port for the second instance (e.g. `4097`).
1161
652
 
1162
- ## FAQ
653
+ ### FAQ
1163
654
 
1164
655
  **How do I run Claude Code from my phone?**
1165
656
  Install Charness on your own machine or VPS, create a Telegram bot, and bind a forum topic (or the bot's DM) to a project folder. Start a Claude Code session in that topic and you drive it entirely from Telegram: type prompts, send voice notes, or drop in files, and the agent's output streams back into the same topic. Your phone never runs the agent; it stays on your machine.
@@ -1168,7 +659,7 @@ Install Charness on your own machine or VPS, create a Telegram bot, and bind a f
1168
659
  Yes. The bot only makes outbound connections to Telegram, so nothing listens for inbound traffic and you never expose a port. OpenCode runs as a local HTTP server that the bot talks to on loopback, and you reach it from anywhere through Telegram.
1169
660
 
1170
661
  **How do I run a coding agent on a VPS and drive it from Telegram?**
1171
- Set up your VPS once with git, your repos, the agent CLIs, and `telegramcode`, then launch the bot from your projects folder. You get an always-on dev box you drive from any device: phone, tablet, or voice. Because the agent lives on the server, it keeps working even when your laptop sleeps, and the same sessions are still reachable over plain SSH.
662
+ Set up your VPS once with git, your repos, the agent CLIs, and `charness`, then launch the bot from your projects folder. You get an always-on dev box you drive from any device: phone, tablet, or voice. Because the agent lives on the server, it keeps working even when your laptop sleeps, and the same sessions are still reachable over plain SSH.
1172
663
 
1173
664
  **Does my session survive a restart?**
1174
665
  Yes, both a bot restart and an agent-server restart. State is persisted to disk; on restart the bot re-attaches to the running Claude session in tmux or re-connects to OpenCode, and an in-flight turn is delivered once it is back. A prompt sent during startup is buffered and replayed, never dropped.
@@ -1179,19 +670,22 @@ Yes. Each topic picks its backend independently: OpenCode over its native HTTP+S
1179
670
  **Can I talk to the agent by voice?**
1180
671
  Yes. Send a voice message to a topic and it is transcribed with Whisper (via Groq or OpenAI) and forwarded to the agent as a normal prompt, so you can vibe code hands-free from your phone.
1181
672
 
1182
- ## Documentation
673
+ ### Documentation
1183
674
 
675
+ - **[SETUP.md](SETUP.md)** — installing and configuring: Telegram, Jira, environment, updating; Docker as an alternative to npm.
1184
676
  - **[DEVELOPMENT.md](DEVELOPMENT.md)** — architecture and local development (build, test, hot-reload, Docker dev loop).
1185
677
  - **[MIGRATION_1_TO_2.md](MIGRATION_1_TO_2.md)** — upgrading a 1.x install to 2.0.
1186
678
 
1187
- ## The story behind it
679
+ ### The story behind it
1188
680
 
1189
681
  I started running Claude Code on my own machine, and I kept wanting to check on it after I stepped away from the desk. Back then there was no way to reach it from a phone, so I put together a small bot that launched the agent and let me talk to it over Telegram. It ran on my own computer, which had to stay on the whole time.
1190
682
 
1191
683
  Then I moved the whole thing onto a server that never sleeps, added OpenCode next to Claude Code, and it grew into what it is now: voice control, file intake, scheduling, a raw terminal, and sessions that survive restarts. These days I run it every day and drive everything from my phone or tablet, no laptop required.
1192
684
 
685
+ The project was called TelegramCode before it became Charness: its npm package was `telegramcode` up to 2.0.3 and is `charness` since 3.0.0.
686
+
1193
687
  Full write-up: [Vibe-coding by voice, from my phone](https://dev.to/olosegres/vibe-coding-by-voice-from-my-phone-claude-code-and-opencode-over-telegram-j75).
1194
688
 
1195
- ## License
689
+ ### License
1196
690
 
1197
691
  MIT