@sojaner/telex 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +502 -0
- package/config.example.json +7 -0
- package/dist/agents.js +94 -0
- package/dist/agents.js.map +1 -0
- package/dist/ask.js +200 -0
- package/dist/ask.js.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/setup.js +125 -0
- package/dist/setup.js.map +1 -0
- package/dist/telegram.js +205 -0
- package/dist/telegram.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# telex
|
|
2
|
+
|
|
3
|
+
An MCP server that lets local AI agents talk to you through Telegram — ask a question, offer
|
|
4
|
+
buttons, collect a typed reply, and give up gracefully when you are away.
|
|
5
|
+
|
|
6
|
+
Your agent is running a long task, hits a decision it shouldn't make alone, and you are not at
|
|
7
|
+
the keyboard. Instead of guessing or stalling, it sends you a Telegram message with two buttons
|
|
8
|
+
and waits. You tap one from your phone; the agent carries on with your answer. If you never
|
|
9
|
+
answer, the buttons come off, the message is marked stale, and the agent decides for itself
|
|
10
|
+
whether to stop or continue.
|
|
11
|
+
|
|
12
|
+
telex is local-only: it runs over stdio next to your agent, talks outbound to the Telegram Bot
|
|
13
|
+
API, and only ever messages the chats you configured. Nothing listens on a port.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌────────┐ stdio/MCP ┌───────┐ Bot API ┌──────────┐
|
|
17
|
+
│ agent │ ────────────▶ │ telex │ ──────────▶ │ Telegram │ ──▶ you
|
|
18
|
+
└────────┘ ◀────────── └───────┘ ◀──────── └──────────┘
|
|
19
|
+
the answer your tap
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm i -g @sojaner/telex
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Node 22.6 or newer. The binary is called `telex` whatever you install it from.
|
|
31
|
+
|
|
32
|
+
Pin a version, or take it straight from the GitHub release if you prefer not to go through
|
|
33
|
+
the registry:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm i -g @sojaner/telex@0.1 # a range, or any exact published version
|
|
37
|
+
npm i -g https://github.com/Sojaner/telex/releases/latest/download/telex.tgz
|
|
38
|
+
npm i -g https://github.com/Sojaner/telex/releases/download/v0.1.14/telex.tgz
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every green push to `main` bumps the patch version, publishes it to npm and attaches the same
|
|
42
|
+
tarball to a GitHub release, so both routes carry identical builds.
|
|
43
|
+
|
|
44
|
+
Or run it from a checkout:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
git clone https://github.com/Sojaner/telex && cd telex
|
|
48
|
+
corepack enable && pnpm install
|
|
49
|
+
node src/cli.ts --help # runs the TypeScript directly, no build
|
|
50
|
+
pnpm run build && pnpm link --global # or link this checkout as the global telex
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Upgrade with the same install command; uninstall with `npm rm -g @sojaner/telex`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Set up a bot
|
|
58
|
+
|
|
59
|
+
Create the bot in Telegram, then let telex do the rest:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
telex add
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It walks you through three things:
|
|
66
|
+
|
|
67
|
+
1. **The token.** Open [@BotFather](https://t.me/BotFather), send `/newbot`, pick a display name
|
|
68
|
+
and a username ending in `bot`. BotFather replies with a token like `8123456789:AAH…`. Paste
|
|
69
|
+
it in. telex validates it against `getMe` before going further.
|
|
70
|
+
2. **The chat.** telex prints your bot's link and waits. Open the chat and send it anything —
|
|
71
|
+
telex reads the chat id and your Telegram user id straight off that message, so you never
|
|
72
|
+
have to read raw `getUpdates` JSON.
|
|
73
|
+
3. **A name.** How you'll refer to this bot later (`main`, `work`, `acme-api`). The first bot
|
|
74
|
+
added becomes the default.
|
|
75
|
+
|
|
76
|
+
It writes `~/.config/telex/config.json` with mode `0600` and sends a test message so you know the
|
|
77
|
+
round trip works.
|
|
78
|
+
|
|
79
|
+
Run `telex add` again for each additional bot. One bot per project is the point — see
|
|
80
|
+
[Per-project configuration](#per-project-configuration).
|
|
81
|
+
|
|
82
|
+
### Groups
|
|
83
|
+
|
|
84
|
+
Point a bot at a group instead of a DM: add the bot to the group, run `telex add`, and send the
|
|
85
|
+
message from inside the group. Group chat ids are negative; telex picks that up automatically.
|
|
86
|
+
|
|
87
|
+
Set `allowFrom` for groups. An inline keyboard is tappable by **everyone** who can see it, and
|
|
88
|
+
telex checks the tapping user against that list — without it, any group member can answer your
|
|
89
|
+
agent's questions.
|
|
90
|
+
|
|
91
|
+
### Doing it by hand
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
telex add work --token '8123456789:AAH…' --chat-id 987654321 --allow 987654321
|
|
95
|
+
telex add team --token '8234567890:AAG…' --chat-id=-1001234567890 --allow 987654321,123456789
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Negative chat ids need `--chat-id=-100…` (with the equals sign), because a bare `-100…` looks
|
|
99
|
+
like another flag.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Managing bots
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
telex add [name] add a bot, guided; or --token … --chat-id …
|
|
107
|
+
telex list configured bots, tokens masked
|
|
108
|
+
telex set <name> [options] change token / chat / allowlist / default
|
|
109
|
+
telex remove <name> delete a bot
|
|
110
|
+
telex config [name] install the MCP registration into this project
|
|
111
|
+
telex serve run the MCP server over stdio (what agents launch)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Options for `add` and `set`:
|
|
115
|
+
|
|
116
|
+
| Flag | Meaning |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `--token <token>` | Bot token from BotFather |
|
|
119
|
+
| `--chat-id <id>` | Chat the bot writes to (`--chat-id=-100…` for groups) |
|
|
120
|
+
| `--allow <id,id>` | Telegram user ids allowed to answer; `any` clears the list |
|
|
121
|
+
| `--default` | Make this the global default bot |
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
$ telex list
|
|
125
|
+
* work chat 987654321 8123456789:******bAcD allow: 987654321
|
|
126
|
+
team chat -1001234567890 8234567890:******xYzW allow: 987654321, 123456789
|
|
127
|
+
|
|
128
|
+
* = default. Config: /home/you/.config/telex/config.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`telex list --json` prints the same thing as JSON, tokens still masked.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## The config file
|
|
136
|
+
|
|
137
|
+
`~/.config/telex/config.json`, or wherever `TELEX_CONFIG` points. `XDG_CONFIG_HOME` is honoured.
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"defaultBot": "work",
|
|
142
|
+
"bots": {
|
|
143
|
+
"work": {
|
|
144
|
+
"token": "8123456789:AAH…",
|
|
145
|
+
"chatId": 987654321,
|
|
146
|
+
"allowFrom": [987654321]
|
|
147
|
+
},
|
|
148
|
+
"team": {
|
|
149
|
+
"token": "8234567890:AAG…",
|
|
150
|
+
"chatId": -1001234567890,
|
|
151
|
+
"allowFrom": [987654321, 123456789]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Field | Required | Meaning |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `defaultBot` | no | Bot used when nothing else specifies one. Defaults to the first entry. |
|
|
160
|
+
| `bots.<name>.token` | yes | BotFather token. Keep this file at `0600`; it is a credential. |
|
|
161
|
+
| `bots.<name>.chatId` | yes | Chat the bot writes to. Negative for groups and channels. |
|
|
162
|
+
| `bots.<name>.allowFrom` | no | Telegram user ids allowed to answer. Omit to trust anyone in the chat. |
|
|
163
|
+
|
|
164
|
+
The file is shared by every project on the machine; projects select a bot from it rather than
|
|
165
|
+
keeping their own copy of your tokens.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Registering with an agent
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
cd ~/code/acme-api
|
|
173
|
+
telex config
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
asks whether this directory is the project you mean, then which agent to install for, and does
|
|
177
|
+
the install: it runs that agent's own CLI when it has one, and writes the config file itself when
|
|
178
|
+
it does not. The server command is always `telex serve`.
|
|
179
|
+
|
|
180
|
+
| Flag | Meaning |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `--agent <id>` | skip the prompts: `claude`, `gemini`, `qwen`, `codex`, `cursor`, `roo`, `vscode`, `zed`, `amp`, `opencode`, `crush` |
|
|
183
|
+
| `--scope local\|project` | for agents with both: the gitignored file or the committed one |
|
|
184
|
+
| `--print` | only show the commands and file shapes; write nothing |
|
|
185
|
+
| `-y` | don't ask about the current directory |
|
|
186
|
+
| `--json` | print just the `mcpServers` object |
|
|
187
|
+
|
|
188
|
+
Required for a non-interactive run: `--agent`, plus `--scope` for agents that have both files.
|
|
189
|
+
Writing a file merges into whatever is already there rather than replacing it, and `--scope local`
|
|
190
|
+
adds the file to `.gitignore`.
|
|
191
|
+
|
|
192
|
+
Agents that install it themselves — `telex config --agent claude` runs:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
claude mcp add --scope project telex -- telex serve # Claude Code
|
|
196
|
+
gemini mcp add --scope project telex telex serve # Gemini CLI
|
|
197
|
+
qwen mcp add --scope project telex telex serve # Qwen Code
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Agents telex configures by writing the file — same server, different shape per agent:
|
|
201
|
+
|
|
202
|
+
| Agent | File | Shape |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| Claude Code | `.mcp.json` | `{"mcpServers": {"telex": {"command": "telex", "args": ["serve"]}}}` |
|
|
205
|
+
| Cursor | `.cursor/mcp.json` | same as above |
|
|
206
|
+
| Roo Code | `.roo/mcp.json` | same as above |
|
|
207
|
+
| VS Code | `.vscode/mcp.json` | `{"servers": {"telex": {"type": "stdio", "command": "telex", "args": ["serve"]}}}` |
|
|
208
|
+
| Zed | `.zed/settings.json` | `{"context_servers": {"telex": {"source": "custom", "command": "telex", "args": ["serve"]}}}` |
|
|
209
|
+
| Amp | `.amp/settings.json` | `{"amp.mcpServers": {"telex": {"command": "telex", "args": ["serve"]}}}` |
|
|
210
|
+
| opencode | `opencode.json` | `{"mcp": {"telex": {"type": "local", "command": ["telex", "serve"]}}}` |
|
|
211
|
+
| Crush | `.crush.json` | `{"mcp": {"telex": {"type": "stdio", "command": "telex", "args": ["serve"]}}}` |
|
|
212
|
+
| Codex CLI | `.codex/config.toml` or `.codex/config.local.toml` | `[mcp_servers.telex]` / `command = "telex"` / `args = ["serve"]` |
|
|
213
|
+
|
|
214
|
+
Codex is the one with two files: `.codex/config.toml` is committed and shared with the team,
|
|
215
|
+
`.codex/config.local.toml` is your own and gitignored — `--scope project` or `--scope local`.
|
|
216
|
+
Codex only reads either for projects you have marked trusted. Anything else that
|
|
217
|
+
speaks MCP takes the `mcpServers` shape — `claude_desktop_config.json`, Continue, and the rest.
|
|
218
|
+
|
|
219
|
+
For one bot everywhere instead of one
|
|
220
|
+
per project, install at user scope: `claude mcp add --scope user telex -- telex serve`.
|
|
221
|
+
|
|
222
|
+
If `telex` is not on the agent's `PATH` — GUI apps often have a shorter `PATH` than your shell —
|
|
223
|
+
use the absolute path, or point Node at the installed entry point:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"mcpServers": {
|
|
228
|
+
"telex": {
|
|
229
|
+
"command": "node",
|
|
230
|
+
"args": ["/home/you/.npm-global/lib/node_modules/telex/dist/index.js"]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`telex config --print` prints that path for your machine.
|
|
237
|
+
|
|
238
|
+
### Per-project configuration
|
|
239
|
+
|
|
240
|
+
Give each project its own bot, so a message tells you which project it came from before you even
|
|
241
|
+
read it, and you can mute one project's bot without muting the rest.
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
cd ~/code/acme-api
|
|
245
|
+
telex add acme-api # its own bot, its own chat
|
|
246
|
+
telex config acme-api # same install, pinned to that bot
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
which threads `TELEX_BOT=acme-api` into whatever it installs:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
claude mcp add --scope project telex --env TELEX_BOT=acme-api -- telex serve
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"mcpServers": {
|
|
258
|
+
"telex": {
|
|
259
|
+
"command": "telex",
|
|
260
|
+
"args": ["serve"],
|
|
261
|
+
"env": { "TELEX_BOT": "acme-api" }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Commit that `.mcp.json` and everyone on the project gets the right routing — they each configure
|
|
268
|
+
their own bot named `acme-api` with their own token, and nothing secret goes in the repo.
|
|
269
|
+
|
|
270
|
+
Which bot a message goes to, in order:
|
|
271
|
+
|
|
272
|
+
1. The `bot` argument on the tool call, if the agent passes one.
|
|
273
|
+
2. `TELEX_BOT` from the MCP registration — the project's bot.
|
|
274
|
+
3. `defaultBot` from the config file.
|
|
275
|
+
|
|
276
|
+
So a project's agent messages its own bot without being told to, and can still reach another bot
|
|
277
|
+
deliberately (`bot: "oncall"` for something urgent, say).
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## The tools
|
|
282
|
+
|
|
283
|
+
The server exposes two tools. `send_to_user` starts the conversation:
|
|
284
|
+
|
|
285
|
+
| Parameter | Type | Meaning |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `project` | string, required | Task or project name, rendered as the bold heading. |
|
|
288
|
+
| `message` | string, required | The copy to show you, in Telegram HTML. |
|
|
289
|
+
| `options` | string[], optional | Up to 10 single-choice answers, rendered as buttons. |
|
|
290
|
+
| `expect_text` | boolean, optional | Show one *Reply* button; your next message becomes the answer. |
|
|
291
|
+
| `timeout_seconds` | number, default 300 | Deadline for the whole exchange, 5s to 24h. |
|
|
292
|
+
| `bot` | string, optional | Which configured bot to use. |
|
|
293
|
+
| `project_path` | string, required | Absolute path of the project the agent is working in. |
|
|
294
|
+
| `agent` | string, required | The agent's own name, e.g. `claude-code`. |
|
|
295
|
+
| `interval_seconds` | number, default 60 | How often the agent calls `heartbeat`. |
|
|
296
|
+
|
|
297
|
+
`options` and `expect_text` are mutually exclusive. With neither, the message is a one-way
|
|
298
|
+
notification and the call returns immediately.
|
|
299
|
+
|
|
300
|
+
**Formatting.** `message` is rendered with Telegram's HTML subset: `<b> <i> <u> <s> <code> <pre>
|
|
301
|
+
<a href=""> <blockquote>`. Literal `&`, `<` and `>` must be escaped as `& < >`.
|
|
302
|
+
Markdown is not rendered. A message with broken tags is re-sent as plain text rather than failing
|
|
303
|
+
the call.
|
|
304
|
+
|
|
305
|
+
**Results.**
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{"status": "sent", "message_id": 101}
|
|
309
|
+
{"status": "answered", "response": "Yes", "kind": "choice", "message_id": 101}
|
|
310
|
+
{"status": "timeout", "message_id": 101}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
On `timeout` the buttons are stripped and the message is marked stale, so a late tap can't answer
|
|
314
|
+
a question nobody is listening to any more. What happens next — retry, continue without you,
|
|
315
|
+
stop — is entirely the agent's call. telex has no opinion.
|
|
316
|
+
|
|
317
|
+
If you messaged the bot while the agent wasn't asking anything, the result carries those messages
|
|
318
|
+
too, delivered exactly as a heartbeat would:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{"status": "sent", "message_id": 101, "pending": [{"text": "hold off on the deploy", "received_at": "2026-09-15T18:22:04.000Z", "waited_seconds": 37}]}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Messages you send first
|
|
325
|
+
|
|
326
|
+
You can talk to the bot without being asked. An MCP server cannot wake a sleeping agent — agents
|
|
327
|
+
only act when they call a tool — so telex holds what you said until the agent checks in, and
|
|
328
|
+
edits a single receipt under your message as it moves:
|
|
329
|
+
|
|
330
|
+
| What you see | What it means |
|
|
331
|
+
|---|---|
|
|
332
|
+
| 📥 *Held for the agent's next check-in.* | telex has your message and is holding it. |
|
|
333
|
+
| ⛔ *Not accepted — no agent has checked in for this project yet.* | The server is running but no agent has identified itself. Nothing is holding your message. |
|
|
334
|
+
| 📬 *Delivered to the agent.* | A heartbeat collected it; the agent has it now. |
|
|
335
|
+
| ⌛ *Expired — the agent never picked this up.* | Three intervals passed with no check-in. Dropped. |
|
|
336
|
+
| *nothing at all* | Nothing is running for that project. The message went nowhere. |
|
|
337
|
+
|
|
338
|
+
Silence is the signal: telex only polls while its process is alive, so a message with no receipt
|
|
339
|
+
at all means no agent is there to receive it. Held messages are capped at 50 per chat, oldest
|
|
340
|
+
dropped.
|
|
341
|
+
|
|
342
|
+
### `heartbeat`
|
|
343
|
+
|
|
344
|
+
The agent's side of that. It calls this on a fixed interval for as long as it is working, and the
|
|
345
|
+
call returns immediately — it never blocks and never waits for you.
|
|
346
|
+
|
|
347
|
+
| Parameter | Type | Meaning |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| `interval_seconds` | number, default 60 | How often the agent intends to check in, 10s to 1h. |
|
|
350
|
+
| `project_path` | string, required | Absolute path of the project. |
|
|
351
|
+
| `agent` | string, required | The agent's own name. |
|
|
352
|
+
| `bot` | string, optional | Which configured bot to listen on. |
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{"messages": [{"text": "ship it", "received_at": "2026-09-15T18:22:04.000Z", "waited_seconds": 12}], "interval_seconds": 60}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
An empty `messages` array is the normal case — nothing was said, keep working. The interval does
|
|
359
|
+
double duty as a liveness signal: miss three in a row and anything waiting is marked expired and
|
|
360
|
+
dropped, so you learn the agent stopped listening instead of watching a message sit unanswered
|
|
361
|
+
forever.
|
|
362
|
+
|
|
363
|
+
### Who is calling
|
|
364
|
+
|
|
365
|
+
Every tool call carries `project_path`, `agent` and `interval_seconds`. That does three things:
|
|
366
|
+
|
|
367
|
+
- **The expiry clock starts at the first call**, not the first heartbeat. A message you send a
|
|
368
|
+
second later already has a deadline.
|
|
369
|
+
- **Messages sent before any agent has checked in are refused**, not held. The server starts with
|
|
370
|
+
your agent, but until the agent actually calls a telex tool nothing owns the bot — telex says so
|
|
371
|
+
rather than quietly stockpiling messages for an agent that may never ask.
|
|
372
|
+
- **Two projects on one bot get caught.** Each running agent records itself in
|
|
373
|
+
`~/.local/state/telex/sessions.json` (override with `TELEX_STATE`), so separate telex processes
|
|
374
|
+
can see each other. When a second one appears on the same bot you get:
|
|
375
|
+
|
|
376
|
+
> ⚠️ **Two agents are using this bot at once**
|
|
377
|
+
> `claude-code` — `/code/acme-api`
|
|
378
|
+
> `codex` — `/code/other`
|
|
379
|
+
> Telegram gives each message to only one of them, so answers will go missing.
|
|
380
|
+
|
|
381
|
+
That is not a cosmetic warning. Telegram hands each update to exactly one poller, so a shared
|
|
382
|
+
bot loses roughly half of everything you send. Give each project its own bot.
|
|
383
|
+
|
|
384
|
+
A record is live while its process exists and it has checked in within three intervals; crashed
|
|
385
|
+
and stale ones are pruned on the next call, so the warning doesn't fire for agents that are gone.
|
|
386
|
+
|
|
387
|
+
### Examples
|
|
388
|
+
|
|
389
|
+
A decision:
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
{
|
|
393
|
+
"project": "acme-api",
|
|
394
|
+
"message": "Migration <code>0042_drop_legacy_users</code> is destructive and irreversible.\nRun it against <b>production</b>?",
|
|
395
|
+
"options": ["Run it", "Skip for now", "Stop and wait for me"],
|
|
396
|
+
"timeout_seconds": 1800
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Missing information:
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"project": "acme-api",
|
|
405
|
+
"message": "What should the new endpoint be called?",
|
|
406
|
+
"expect_text": true,
|
|
407
|
+
"timeout_seconds": 600
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
A notification, no answer wanted:
|
|
412
|
+
|
|
413
|
+
```json
|
|
414
|
+
{
|
|
415
|
+
"project": "nightly",
|
|
416
|
+
"message": "✅ Test suite green, 412 passed in 3m12s.\nBranch <code>fix/token-refresh</code> is ready to merge.",
|
|
417
|
+
"bot": "alerts"
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Behaviour worth knowing
|
|
424
|
+
|
|
425
|
+
- **Long messages** are split at Telegram's limit; only the last part carries the buttons.
|
|
426
|
+
- **Long choices** — anything over 24 characters — are listed in the message body and the buttons
|
|
427
|
+
become `1`, `2`, `3`, because Telegram truncates long button labels.
|
|
428
|
+
- **Stale updates are discarded.** Messages you sent before the question was asked are never read
|
|
429
|
+
as an answer.
|
|
430
|
+
- **Slash commands are ignored** as text answers, so `/start` and friends still work.
|
|
431
|
+
- **Rate limits** are honoured: a `429` is retried after the `retry_after` Telegram asks for.
|
|
432
|
+
- **Tokens are redacted** from error messages; they appear in the API URL that failed.
|
|
433
|
+
- **Polling runs while the process does.** Once a project's bot is watched telex long-polls for
|
|
434
|
+
the life of the server, because being reachable is what makes "delivered" mean anything. Kill
|
|
435
|
+
the agent and the bot goes quiet.
|
|
436
|
+
- **Unprompted messages are held, not answered with.** A message you send while nothing is asking
|
|
437
|
+
is queued for the agent instead of being read as the answer to whatever gets asked next.
|
|
438
|
+
- **`allowFrom` covers inbound too.** Someone else in the group messaging the bot is dropped, not
|
|
439
|
+
queued.
|
|
440
|
+
- **One poller per token.** Two processes polling the same bot fight over updates; give telex its
|
|
441
|
+
own bot. It says so explicitly if it detects a conflict.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Security notes
|
|
446
|
+
|
|
447
|
+
- The config file holds bot tokens. telex writes it `0600`; keep it that way, and don't commit it.
|
|
448
|
+
- Set `allowFrom` anywhere the chat has more than you in it. Buttons are visible and tappable by
|
|
449
|
+
every member; telex authorises the tap, not just the send.
|
|
450
|
+
- A bot can only message chats it is already in, and telex only ever sends to the configured
|
|
451
|
+
`chatId`. It does not accept inbound commands or expose an HTTP endpoint.
|
|
452
|
+
- The agent chooses what to send. Treat the message body as something the agent wrote — don't
|
|
453
|
+
ask it to relay secrets you wouldn't want in a Telegram chat.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Troubleshooting
|
|
458
|
+
|
|
459
|
+
| Symptom | Cause |
|
|
460
|
+
|---|---|
|
|
461
|
+
| `no bots configured` | Run `telex add`, or point `TELEX_CONFIG` at the right file. |
|
|
462
|
+
| `Telegram rejected that token` | Paste the whole BotFather line, including the digits before the colon. |
|
|
463
|
+
| Nothing arrives, no error | Wrong `chatId`, or you never messaged the bot. Check with `telex list`. |
|
|
464
|
+
| `getUpdates conflict` | Another process is polling the same token. Give telex its own bot. |
|
|
465
|
+
| Agent can't start the server | `telex` isn't on its `PATH`; use the absolute `node …/dist/index.js` form. |
|
|
466
|
+
| Buttons do nothing | The tapping account isn't in `allowFrom`. `telex set <name> --allow <id>`. |
|
|
467
|
+
| You message the bot, nothing replies | Nothing is running for that project — start the agent. That silence is deliberate. |
|
|
468
|
+
| Messages stay "held" | The agent isn't calling `heartbeat`. It will still see them on its next tool call. |
|
|
469
|
+
| Everything is "not accepted" | The agent hasn't called any telex tool yet, so nothing owns the bot. |
|
|
470
|
+
| Warned about two agents | Two projects share one bot. `telex add <name>` and repoint one of them. |
|
|
471
|
+
| Messages expire constantly | The agent's `interval_seconds` is shorter than how often it really checks in. |
|
|
472
|
+
|
|
473
|
+
Run the server by hand to see startup errors that an agent would swallow:
|
|
474
|
+
|
|
475
|
+
```sh
|
|
476
|
+
telex serve < /dev/null
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Development
|
|
482
|
+
|
|
483
|
+
This repo uses **pnpm**. `npm install` in a checkout stops at a `preinstall` guard telling you
|
|
484
|
+
so, CI fails on a committed `package-lock.json` or `yarn.lock`, and `pnpm install --frozen-lockfile`
|
|
485
|
+
fails the build if `package.json` and `pnpm-lock.yaml` ever drift apart. The published package is
|
|
486
|
+
unaffected: the guard is stripped at pack time, so installing telex runs no scripts at all.
|
|
487
|
+
|
|
488
|
+
```sh
|
|
489
|
+
corepack enable # uses the pnpm version pinned in packageManager
|
|
490
|
+
pnpm install
|
|
491
|
+
pnpm test # node:test, no network
|
|
492
|
+
pnpm run typecheck
|
|
493
|
+
pnpm run build # tsc → dist/, normally done by CI
|
|
494
|
+
node src/cli.ts # run from source; Node strips the types
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`dist/` is not committed. CI runs the tests on Node 22 and 24 with a frozen lockfile, then bumps
|
|
498
|
+
the version, tags it, and publishes a release with the built tarball attached.
|
|
499
|
+
|
|
500
|
+
## License
|
|
501
|
+
|
|
502
|
+
MIT
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Where each agent keeps its project-scoped MCP config, and how telex gets it there. */
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
export function agents() {
|
|
6
|
+
const std = (entry) => entry;
|
|
7
|
+
return [
|
|
8
|
+
{ id: "claude", label: "Claude Code", file: ".mcp.json", path: ["mcpServers", "telex"], value: std,
|
|
9
|
+
cli: (bot) => ["claude", "mcp", "add", "--scope", "project", "telex", ...(bot ? ["--env", `TELEX_BOT=${bot}`] : []), "--", "telex", "serve"] },
|
|
10
|
+
{ id: "gemini", label: "Gemini CLI", cli: (bot) => ["gemini", "mcp", "add", "--scope", "project", ...(bot ? ["-e", `TELEX_BOT=${bot}`] : []), "telex", "telex", "serve"] },
|
|
11
|
+
{ id: "qwen", label: "Qwen Code", cli: (bot) => ["qwen", "mcp", "add", "--scope", "project", ...(bot ? ["-e", `TELEX_BOT=${bot}`] : []), "telex", "telex", "serve"] },
|
|
12
|
+
{ id: "codex", label: "Codex CLI", file: ".codex/config.toml", localFile: ".codex/config.local.toml", toml: true },
|
|
13
|
+
{ id: "cursor", label: "Cursor", file: ".cursor/mcp.json", path: ["mcpServers", "telex"], value: std },
|
|
14
|
+
{ id: "roo", label: "Roo Code", file: ".roo/mcp.json", path: ["mcpServers", "telex"], value: std },
|
|
15
|
+
{ id: "vscode", label: "VS Code", file: ".vscode/mcp.json", path: ["servers", "telex"], value: (e) => ({ type: "stdio", ...e }) },
|
|
16
|
+
{ id: "zed", label: "Zed", file: ".zed/settings.json", path: ["context_servers", "telex"], value: (e) => ({ source: "custom", ...e }) },
|
|
17
|
+
{ id: "amp", label: "Amp", file: ".amp/settings.json", path: ["amp.mcpServers", "telex"], value: std },
|
|
18
|
+
{ id: "opencode", label: "opencode", file: "opencode.json", path: ["mcp", "telex"], value: (e) => ({ type: "local", command: [e.command, ...e.args], ...(e.env ? { environment: e.env } : {}) }) },
|
|
19
|
+
{ id: "crush", label: "Crush", file: ".crush.json", path: ["mcp", "telex"], value: (e) => ({ type: "stdio", ...e }) },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
/** What the agent's file should contain, standalone — used for printing and for a fresh file. */
|
|
23
|
+
export function snippet(agent, entry) {
|
|
24
|
+
if (agent.toml)
|
|
25
|
+
return tomlBlock(entry);
|
|
26
|
+
return JSON.stringify(nest(agent.path, agent.value(entry)), null, 2);
|
|
27
|
+
}
|
|
28
|
+
export function tomlBlock(entry) {
|
|
29
|
+
const lines = [`[mcp_servers.telex]`, `command = "${entry.command}"`, `args = [${entry.args.map((a) => `"${a}"`).join(", ")}]`];
|
|
30
|
+
if (entry.env) {
|
|
31
|
+
lines.push(``, `[mcp_servers.telex.env]`, ...Object.entries(entry.env).map(([k, v]) => `${k} = "${v}"`));
|
|
32
|
+
}
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
35
|
+
const nest = (path, value) => path.reduceRight((acc, key) => ({ [key]: acc }), value);
|
|
36
|
+
/** Run the agent's own installer. Returns null when its binary is not on PATH. */
|
|
37
|
+
export function runCli(argv, cwd) {
|
|
38
|
+
const run = spawnSync(argv[0], argv.slice(1), { cwd, encoding: "utf8" });
|
|
39
|
+
if (run.error && run.error.code === "ENOENT")
|
|
40
|
+
return null;
|
|
41
|
+
const output = `${run.stdout ?? ""}${run.stderr ?? ""}`.trim();
|
|
42
|
+
return { ok: run.status === 0, how: argv.join(" "), output };
|
|
43
|
+
}
|
|
44
|
+
/** Merge the entry into the agent's config file, keeping whatever else is in there. */
|
|
45
|
+
export function writeFileConfig(agent, entry, dir, local) {
|
|
46
|
+
const rel = (local && agent.localFile) || agent.file;
|
|
47
|
+
const path = join(dir, rel);
|
|
48
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
49
|
+
const next = agent.toml ? mergeToml(existing, entry) : mergeJson(existing, agent, entry);
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
writeFileSync(path, next);
|
|
52
|
+
return { ok: true, how: `wrote ${rel}` };
|
|
53
|
+
}
|
|
54
|
+
function mergeJson(existing, agent, entry) {
|
|
55
|
+
const root = existing.trim() ? JSON.parse(existing) : {};
|
|
56
|
+
let node = root;
|
|
57
|
+
for (const key of agent.path.slice(0, -1)) {
|
|
58
|
+
if (typeof node[key] !== "object" || node[key] === null)
|
|
59
|
+
node[key] = {};
|
|
60
|
+
node = node[key];
|
|
61
|
+
}
|
|
62
|
+
node[agent.path[agent.path.length - 1]] = agent.value(entry);
|
|
63
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
64
|
+
}
|
|
65
|
+
/** Replace an existing [mcp_servers.telex] block (and its sub-tables) or append a new one. */
|
|
66
|
+
export function mergeToml(existing, entry) {
|
|
67
|
+
const lines = existing.split("\n");
|
|
68
|
+
const start = lines.findIndex((l) => l.trim() === "[mcp_servers.telex]");
|
|
69
|
+
if (start === -1) {
|
|
70
|
+
const head = existing.trim();
|
|
71
|
+
return `${head ? `${head}\n\n` : ""}${tomlBlock(entry)}\n`;
|
|
72
|
+
}
|
|
73
|
+
let end = start + 1;
|
|
74
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
75
|
+
end++;
|
|
76
|
+
while (end < lines.length && lines[end].trim().startsWith("[mcp_servers.telex.")) {
|
|
77
|
+
end++;
|
|
78
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
79
|
+
end++;
|
|
80
|
+
}
|
|
81
|
+
const merged = [...lines.slice(0, start), ...tomlBlock(entry).split("\n"), "", ...lines.slice(end)];
|
|
82
|
+
return `${merged.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
83
|
+
}
|
|
84
|
+
/** A local-only config is worthless if git picks it up anyway. */
|
|
85
|
+
export function ensureGitignored(dir, rel) {
|
|
86
|
+
const path = join(dir, ".gitignore");
|
|
87
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
88
|
+
const patterns = [rel, `/${rel}`, `${rel}/`];
|
|
89
|
+
if (existing.split("\n").some((l) => patterns.includes(l.trim())))
|
|
90
|
+
return null;
|
|
91
|
+
writeFileSync(path, `${existing.replace(/\n*$/, "")}${existing.trim() ? "\n" : ""}${rel}\n`);
|
|
92
|
+
return ".gitignore";
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.js","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"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"}
|