@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 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 `&amp; &lt; &gt;`.
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
@@ -0,0 +1,7 @@
1
+ {
2
+ "defaultBot": "main",
3
+ "bots": {
4
+ "main": { "token": "123456:ABC-DEF-your-bot-token", "chatId": 987654321, "allowFrom": [987654321] },
5
+ "alerts": { "token": "234567:GHI-JKL-another-bot-token", "chatId": 987654321 }
6
+ }
7
+ }
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":"AAAA,yFAAyF;AACzF,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAmB1C,MAAM,UAAU,MAAM;IACpB,MAAM,GAAG,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,KAAK,CAAC;IACpC,OAAO;QACL,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG;YAChG,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,aAAa,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAChJ,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAC1K,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QACrK,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,oBAAoB,EAAE,SAAS,EAAE,0BAA0B,EAAE,IAAI,EAAE,IAAI,EAAE;QAClH,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE;QACtG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE;QAClG,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE;QACjI,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,iBAAiB,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE;QACvI,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,gBAAgB,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE;QACtG,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;QAClM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE;KACtH,CAAC;AACJ,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,OAAO,CAAC,KAAY,EAAE,KAAY;IAChD,IAAI,KAAK,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAK,EAAE,KAAK,CAAC,KAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,MAAM,KAAK,GAAG,CAAC,qBAAqB,EAAE,cAAc,KAAK,CAAC,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChI,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,yBAAyB,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,IAAc,EAAE,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAU,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,CAA4B,CAAC;AAI7I,kFAAkF;AAClF,MAAM,UAAU,MAAM,CAAC,IAAc,EAAE,GAAW;IAChD,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACzE,IAAI,GAAG,CAAC,KAAK,IAAK,GAAG,CAAC,KAA+B,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrF,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;IAC/D,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;AAC/D,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,eAAe,CAAC,KAAY,EAAE,KAAY,EAAE,GAAW,EAAE,KAAc;IACrF,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,IAAK,CAAC;IACtD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5B,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACzF,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,GAAG,EAAE,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,KAAY,EAAE,KAAY;IAC7D,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAA6B,CAAC,CAAC,CAAC,EAAE,CAAC;IACtF,IAAI,IAAI,GAAG,IAAI,CAAC;IAChB,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,IAAI,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACxE,IAAI,GAAG,IAAI,CAAC,GAAG,CAA4B,CAAC;IAC9C,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,IAAK,CAAC,KAAK,CAAC,IAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAM,CAAC,KAAK,CAAC,CAAC;IAChE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,KAAY;IACtD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,qBAAqB,CAAC,CAAC;IACzE,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC7B,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;IAC7D,CAAC;IACD,IAAI,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC;IACpB,OAAO,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAAE,GAAG,EAAE,CAAC;IAC/D,OAAO,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACjF,GAAG,EAAE,CAAC;QACN,OAAO,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAAE,GAAG,EAAE,CAAC;IACjE,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACpG,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC;AACvE,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,GAAW;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpE,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;IAC7C,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/E,aAAa,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC;IAC7F,OAAO,YAAY,CAAC;AACtB,CAAC"}