cursor-telegram-mcp 0.5.0 → 0.7.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.
package/README.md CHANGED
@@ -15,7 +15,8 @@ Local and bring-your-own-bot: each person installs the package, creates their
15
15
  own Telegram bot, and everything runs on their own machine. There is no shared
16
16
  server and nothing shared between users. It uses the official
17
17
  [Telegram Bot API](https://core.telegram.org/bots/api) over HTTPS - no QR, no
18
- eSIM, no SIM. Pending questions are kept in memory only.
18
+ eSIM, no SIM. Pending questions are persisted to disk, so a worker restart
19
+ restores any still-open questions instead of dropping them.
19
20
 
20
21
  ## Add to Cursor (one click)
21
22
 
@@ -31,21 +32,40 @@ Prefer it under "Plugin MCP Servers" / the Cursor marketplace? See
31
32
 
32
33
  ## Quick start
33
34
 
34
- ### 1. Configure your bot (one time)
35
+ The goal: your bot is online whenever your computer is on — no need to open
36
+ Cursor or start anything by hand.
37
+
38
+ ### 1. Install (one time)
39
+
40
+ ```bash
41
+ npm i -g cursor-telegram-mcp
42
+ ```
43
+
44
+ A global install gives the always-on service a stable location. (You can also
45
+ run everything via `npx cursor-telegram-mcp …`, but the always-on installer
46
+ needs a global install so its launch agent doesn't point at a temporary cache.)
47
+
48
+ ### 2. Configure + go always-on
35
49
 
36
50
  ```bash
37
- npx cursor-telegram-mcp setup
51
+ cursor-telegram-mcp setup
38
52
  ```
39
53
 
40
- The wizard walks you through creating a bot with [@BotFather](https://t.me/BotFather),
41
- validates the token, captures your chat id (you message the bot once), and
42
- optionally enables command mode with a Cursor API key. It saves everything to a
43
- local config file (`~/.config/cursor-telegram/config.json`, or
54
+ The wizard creates a bot with [@BotFather](https://t.me/BotFather), validates
55
+ the token, captures your chat id (you message the bot once), optionally enables
56
+ command mode with a Cursor API key, and then offers to install the **always-on
57
+ service** (macOS launchd: starts at login, restarts itself, plus a watchdog).
58
+ Config is saved to `~/.config/cursor-telegram/config.json` (or
44
59
  `%APPDATA%\cursor-telegram\config.json` on Windows).
45
60
 
46
- ### 2. Add it to Cursor
61
+ That's it text your bot to test it. Run `cursor-telegram-mcp doctor` anytime
62
+ to check status, and `cursor-telegram-mcp install` / `uninstall` to toggle the
63
+ always-on service.
47
64
 
48
- Add this to your project's `.cursor/mcp.json` (or Cursor Settings -> Tools & MCP):
65
+ ### 3. (optional) Let Cursor agents message you
66
+
67
+ Add this to a project's `.cursor/mcp.json` (or Cursor Settings -> Tools & MCP),
68
+ then reload MCP:
49
69
 
50
70
  ```json
51
71
  {
@@ -58,8 +78,8 @@ Add this to your project's `.cursor/mcp.json` (or Cursor Settings -> Tools & MCP
58
78
  }
59
79
  ```
60
80
 
61
- Reload MCP in Cursor. That's it - the MCP server auto-starts the background
62
- worker. Run `npx cursor-telegram-mcp doctor` anytime to check status.
81
+ The bot itself runs from the always-on service regardless of Cursor; this step
82
+ just lets in-IDE agents send you notifications and questions.
63
83
 
64
84
  ## Architecture
65
85
 
@@ -71,7 +91,7 @@ flowchart TD
71
91
  check -->|yes| reuse["reuse running worker"]
72
92
  spawn --> worker["worker dist/worker.js"]
73
93
  reuse --> worker
74
- worker --> store["in-memory question store"]
94
+ worker --> store["persisted question store (questions.json)"]
75
95
  worker -->|"command mode (optional)"| agent["headless Cursor agent (cursor-agent CLI)"]
76
96
  worker -->|"Bot API: sendMessage / getUpdates"| tg["Telegram"]
77
97
  tg -->|"your reply / texted task"| worker
@@ -120,15 +140,37 @@ worker treats any message that is not answering an open question as a task:
120
140
  carries out the plan and texts back a summary.
121
141
 
122
142
  You can also use `/ask <question>` (read-only Q&A) and `/plan <task>` explicitly,
123
- and send `status` to see what's running. Command mode needs the Cursor CLI
124
- (`cursor-agent`) installed:
143
+ send `status` to see what's running, and `/reset` (or `/new`) to start a fresh
144
+ conversation. Command mode needs the Cursor CLI (`cursor-agent`) installed:
125
145
 
126
146
  - macOS / Linux: `curl https://cursor.com/install -fsS | bash`
127
147
  - Windows (PowerShell): `irm 'https://cursor.com/install?win32=true' | iex`
128
148
 
129
149
  The headless agent runs locally against `TG_AGENT_CWD` (default: the directory
130
150
  the worker started in). Every task is gated: nothing changes code until you
131
- approve the plan. Sessions are in memory only.
151
+ approve the plan.
152
+
153
+ ## Rolling chat (knowledge carries across messages)
154
+
155
+ By default (`TG_ROLLING=true`) command mode keeps ONE long-lived Cursor agent
156
+ and sends every turn — plan, execute, and `/ask` — to it. That means:
157
+
158
+ - The executor remembers the plan it just made, and the next prompt remembers
159
+ the whole conversation, so you can keep building from your phone without
160
+ re-explaining context.
161
+ - The agent id is persisted to `TG_SESSION_PATH` (default
162
+ `<configDir>/rolling-session.json`), so the thread survives worker restarts
163
+ (including the self-update restart) and resumes where you left off.
164
+ - Send `/reset` (or `/new`) to drop the memory and start a fresh thread.
165
+
166
+ Set `TG_ROLLING=false` to go back to a stateless agent per message.
167
+
168
+ ### See the same chat on your computer
169
+
170
+ Every turn is also appended to a Markdown transcript at `TG_TRANSCRIPT_PATH`
171
+ (default `<TG_AGENT_CWD>/remote-chat.md`). Open it in Cursor (or any editor) to
172
+ read the same conversation you are driving from your phone. Because it lives in
173
+ the repo, git tracks its history — commit it whenever you want a snapshot.
132
174
 
133
175
  ## Keeping the worker alive
134
176
 
@@ -136,11 +178,39 @@ The worker runs only while your laptop is awake. It auto-starts with the MCP
136
178
  server and stays up across Cursor reloads, so for "laptop on the charger, manage
137
179
  from my phone" you usually need nothing else.
138
180
 
139
- If you want it to survive reboots without opening Cursor, run it under your OS
140
- service manager (launchd / systemd / Task Scheduler) calling
141
- `cursor-telegram-mcp worker`. (Note: on macOS, launchd agents cannot read files
142
- under `~/Desktop`/`~/Documents`/`~/Downloads`; install the package or project
143
- outside those folders.)
181
+ If you want it to survive reboots without opening Cursor, install it as an
182
+ always-on background service. On macOS this is one command:
183
+
184
+ ```bash
185
+ cursor-telegram-mcp install # start now + at every login (launchd)
186
+ cursor-telegram-mcp uninstall # stop and remove it
187
+ ```
188
+
189
+ `install` writes a per-user launch agent
190
+ (`~/Library/LaunchAgents/com.cursor-telegram.worker.plist`) that points at your
191
+ current Node and the installed CLI — no hardcoded paths — runs the worker under
192
+ `caffeinate` so idle sleep never blocks delivery, and restarts it if it crashes
193
+ (`KeepAlive`). Logs go to `~/Library/Logs/cursor-telegram-worker.log`. Keep the
194
+ Mac on AC power so a closed lid does not fully sleep.
195
+
196
+ `install` also sets up a **watchdog** (`com.cursor-telegram.watchdog`, every
197
+ 120s) that checks the worker's `/health` and restarts it if it is unreachable
198
+ or its poll loop has wedged. Combined with the worker's own retry/backoff (every
199
+ Bot API call has a hard abort timeout, so a stale socket after sleep/wake can't
200
+ silently hang it), the bot stays online as long as the Mac is on.
201
+
202
+ To stay online the Mac must not fully sleep. The worker runs under `caffeinate`
203
+ so idle sleep is prevented while it's up, but a closed lid on battery still
204
+ sleeps. For a machine you want always-on:
205
+
206
+ - Keep it on **AC power**.
207
+ - Optionally allow it to run with the lid closed on AC:
208
+ `sudo pmset -c sleep 0 disablesleep 1` (revert with `sudo pmset -c disablesleep 0`).
209
+
210
+ On Linux/Windows, run `cursor-telegram-mcp worker` under your own service
211
+ manager (systemd / Task Scheduler). (Note: on macOS, launchd agents cannot read
212
+ files under `~/Desktop`/`~/Documents`/`~/Downloads`; install the package or
213
+ project outside those folders.)
144
214
 
145
215
  ## Configuration
146
216
 
@@ -162,6 +232,9 @@ defaults.
162
232
  | `TG_AGENT_CWD` | worker cwd | Directory the headless command-mode agent works in. |
163
233
  | `TG_AGENT_MODEL` | `composer-2.5` | Model id for the headless agent. |
164
234
  | `TG_AGENT_LOAD_SETTINGS` | `false` | `true` to load `TG_AGENT_CWD`'s `.cursor` rules/MCP during runs. |
235
+ | `TG_ROLLING` | `true` | Keep one rolling agent thread so knowledge carries across messages. |
236
+ | `TG_TRANSCRIPT_PATH` | `<cwd>/remote-chat.md` | Markdown transcript of the rolling chat. |
237
+ | `TG_SESSION_PATH` | `<configDir>/rolling-session.json` | Where the rolling agent id is persisted. |
165
238
 
166
239
  ## CLI
167
240
 
@@ -170,6 +243,9 @@ cursor-telegram-mcp [mcp] Start the MCP server (default; Cursor runs this)
170
243
  cursor-telegram-mcp setup First-time setup: create/link your bot
171
244
  cursor-telegram-mcp login Print and save your Telegram chat id
172
245
  cursor-telegram-mcp worker Run the background worker in the foreground
246
+ cursor-telegram-mcp install Install the always-on worker + watchdog (macOS launchd)
247
+ cursor-telegram-mcp uninstall Remove the always-on worker + watchdog
248
+ cursor-telegram-mcp watchdog One-shot health check; restarts the worker if down
173
249
  cursor-telegram-mcp doctor Diagnose configuration and connectivity
174
250
  ```
175
251
 
@@ -243,8 +319,8 @@ npm run build # emit dist/ for publishing
243
319
  ## Notes & limitations
244
320
 
245
321
  - Local-only: if the laptop sleeps or the worker stops, messaging pauses until
246
- it is back. Questions are in memory, so a worker restart forgets any
247
- unanswered question (by design).
322
+ it is back. Open questions are persisted to disk and restored on the next
323
+ worker start, so a restart no longer drops unanswered questions.
248
324
  - Per user: each person creates their own bot and runs their own worker (one bot
249
325
  token can only be polled by one process).
250
326
  - Command mode requires the `cursor-agent` CLI and a Cursor API key; without
@@ -262,8 +338,12 @@ src/
262
338
  login.ts # chat-id discovery helper
263
339
  doctor.ts # diagnostics
264
340
  telegram.ts # Bot API client: long-poll getUpdates, sendText, media
265
- agentRunner.ts# command mode: headless Cursor agent (lazy @cursor/sdk)
266
- store.ts # in-memory pending-question store + reply matching
341
+ agentRunner.ts# command mode: rolling/resumable headless Cursor agent (lazy @cursor/sdk)
342
+ session.ts # persist the rolling agent id across worker restarts
343
+ transcript.ts # append-only remote-chat.md transcript of the rolling chat
344
+ store.ts # pending-question store (persisted to disk) + reply matching
345
+ install.ts # `install`/`uninstall`: generate per-user launchd worker + watchdog (macOS)
346
+ watchdog.ts # `watchdog`: one-shot /health check that restarts a wedged worker
267
347
  parseInbound.ts / splitMessage.ts / taskQueue.ts / formatTelegram.ts
268
348
  answerWaiters.ts # wake-on-answer for long-polling GET /response/:id
269
349
  .cursor/
@@ -55,15 +55,23 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
55
55
  *
56
56
  * Flow (plan-then-approve, always gated):
57
57
  * 1. You text the bot a task.
58
- * 2. `plan()` runs a read-only Cursor agent that produces a step-by-step PLAN
59
- * only (no file changes) and returns it for you to review on your phone.
60
- * 3. You reply YES -> `approve()` runs a second agent that executes the
61
- * approved plan and reports the result. Reply NO -> `reject()` drops it.
58
+ * 2. `plan()` produces a step-by-step PLAN only (no file changes) and returns
59
+ * it for you to review on your phone.
60
+ * 3. You reply YES -> `approve()` executes the approved plan and reports the
61
+ * result. Reply NO -> `reject()` drops it.
62
+ *
63
+ * Rolling thread: by default the runner keeps ONE long-lived Cursor agent and
64
+ * sends every turn (plan, execute, ask) to it, so knowledge carries across
65
+ * messages — the executor remembers the plan, and the next prompt remembers the
66
+ * whole conversation. The agent id is persisted (see session.ts) so the thread
67
+ * survives worker restarts. Set `rolling: false` to fall back to a fresh agent
68
+ * per call (the old stateless behavior).
62
69
  *
63
70
  * Everything runs locally on this machine (the laptop on the charger) against
64
- * `cwd`, using your CURSOR_API_KEY. Sessions live in memory only.
71
+ * `cwd`, using your CURSOR_API_KEY.
65
72
  */
66
73
  import { readFileSync } from "node:fs";
74
+ import { clearRollingSession, loadRollingSession, saveRollingSession, } from "./session.js";
67
75
  /**
68
76
  * Lazily load the optional `@cursor/sdk`. Command mode needs it; if the package
69
77
  * is not installed (it is an optionalDependency), surface a clear, actionable
@@ -100,7 +108,8 @@ function buildUserMessage(text, attachments) {
100
108
  return text;
101
109
  return { text, images };
102
110
  }
103
- async function runAgentPrompt(promptText, opts, attachments) {
111
+ /** Run a single prompt on a fresh, stateless agent (rolling disabled). */
112
+ async function runStatelessPrompt(promptText, opts, attachments) {
104
113
  const env_1 = { stack: [], error: void 0, hasError: false };
105
114
  try {
106
115
  const message = buildUserMessage(promptText, attachments);
@@ -163,13 +172,114 @@ export class CommandRunner {
163
172
  nextAskId = 1;
164
173
  sessions = new Map();
165
174
  askSessions = new Map();
175
+ /** Live rolling agent instance (kept across turns; created lazily). */
176
+ agent;
177
+ /** Persisted rolling agent id (may exist before `agent` is reattached). */
178
+ rollingAgentId;
179
+ /** Epoch ms the current rolling thread started. */
180
+ threadStartedAt;
181
+ /** Serializes turns so two phone messages never share one agent send. */
182
+ turnChain = Promise.resolve();
166
183
  constructor(opts) {
167
184
  this.opts = opts;
185
+ if (opts.rolling) {
186
+ const saved = loadRollingSession(opts.sessionPath);
187
+ // Only reuse a saved thread if it matches the current cwd (a different
188
+ // project should get its own conversation).
189
+ if (saved && saved.cwd === opts.cwd) {
190
+ this.rollingAgentId = saved.agentId;
191
+ this.threadStartedAt = saved.startedAt;
192
+ }
193
+ }
168
194
  }
169
195
  /** True when a Cursor API key is configured. */
170
196
  get enabled() {
171
197
  return this.opts.apiKey.trim() !== "";
172
198
  }
199
+ /** Info about the active rolling thread, if rolling is on and started. */
200
+ rollingInfo() {
201
+ if (!this.opts.rolling)
202
+ return undefined;
203
+ if (!this.rollingAgentId)
204
+ return undefined;
205
+ return { agentId: this.rollingAgentId, startedAt: this.threadStartedAt ?? 0 };
206
+ }
207
+ /**
208
+ * Get the rolling agent, creating it (first ever turn) or resuming a saved
209
+ * thread (after a worker restart). Reuses the live instance otherwise.
210
+ */
211
+ async getRollingAgent() {
212
+ if (this.agent)
213
+ return this.agent;
214
+ const { Agent } = await loadSdk();
215
+ const local = { cwd: this.opts.cwd, settingSources: this.opts.settingSources };
216
+ if (this.rollingAgentId) {
217
+ try {
218
+ this.agent = await Agent.resume(this.rollingAgentId, {
219
+ apiKey: this.opts.apiKey,
220
+ model: { id: this.opts.model },
221
+ local,
222
+ });
223
+ return this.agent;
224
+ }
225
+ catch {
226
+ // The saved thread could not be resumed (e.g. pruned); start fresh.
227
+ this.rollingAgentId = undefined;
228
+ this.threadStartedAt = undefined;
229
+ clearRollingSession(this.opts.sessionPath);
230
+ }
231
+ }
232
+ this.agent = await Agent.create({
233
+ apiKey: this.opts.apiKey,
234
+ model: { id: this.opts.model },
235
+ local,
236
+ });
237
+ this.rollingAgentId = this.agent.agentId;
238
+ this.threadStartedAt = Date.now();
239
+ saveRollingSession(this.opts.sessionPath, {
240
+ agentId: this.rollingAgentId,
241
+ model: this.opts.model,
242
+ cwd: this.opts.cwd,
243
+ startedAt: this.threadStartedAt,
244
+ });
245
+ return this.agent;
246
+ }
247
+ /**
248
+ * Run one prompt. In rolling mode this reuses the persistent agent (so the
249
+ * turn remembers all prior turns); otherwise it uses a fresh stateless agent.
250
+ * Turns are serialized to keep the single agent consistent.
251
+ */
252
+ async runTurn(promptText, attachments) {
253
+ if (!this.opts.rolling) {
254
+ return runStatelessPrompt(promptText, this.opts, attachments);
255
+ }
256
+ const run = this.turnChain.then(async () => {
257
+ const message = buildUserMessage(promptText, attachments);
258
+ const agent = await this.getRollingAgent();
259
+ const r = await agent.send(message);
260
+ await r.wait();
261
+ return { status: r.status, result: r.result };
262
+ });
263
+ // Keep the chain alive even if this turn rejects, without swallowing the
264
+ // error for the caller.
265
+ this.turnChain = run.catch(() => undefined);
266
+ return run;
267
+ }
268
+ /** Start a fresh rolling thread (drops memory of the current conversation). */
269
+ reset() {
270
+ if (this.agent) {
271
+ try {
272
+ this.agent.close();
273
+ }
274
+ catch {
275
+ // best-effort
276
+ }
277
+ }
278
+ this.agent = undefined;
279
+ this.rollingAgentId = undefined;
280
+ this.threadStartedAt = undefined;
281
+ clearRollingSession(this.opts.sessionPath);
282
+ }
173
283
  /** Most recent session waiting for a YES/NO approval, if any. */
174
284
  latestAwaitingApproval() {
175
285
  let latest;
@@ -223,7 +333,7 @@ export class CommandRunner {
223
333
  this.sessions.set(session.id, session);
224
334
  onSession?.(session);
225
335
  try {
226
- const result = await runAgentPrompt(PLAN_PROMPT(task), this.opts, attachments);
336
+ const result = await this.runTurn(PLAN_PROMPT(task), attachments);
227
337
  if (result.status === "error") {
228
338
  session.status = "error";
229
339
  session.error = coerceText(result.result) || "plan run failed";
@@ -254,7 +364,7 @@ export class CommandRunner {
254
364
  this.askSessions.set(session.id, session);
255
365
  onSession?.(session);
256
366
  try {
257
- const result = await runAgentPrompt(ASK_PROMPT(question), this.opts, attachments);
367
+ const result = await this.runTurn(ASK_PROMPT(question), attachments);
258
368
  if (result.status === "error") {
259
369
  session.status = "error";
260
370
  session.error = coerceText(result.result) || "ask run failed";
@@ -284,7 +394,7 @@ export class CommandRunner {
284
394
  session.updatedAt = Date.now();
285
395
  onExecuting?.(session);
286
396
  try {
287
- const result = await runAgentPrompt(EXECUTE_PROMPT(session.task, session.plan ?? ""), this.opts);
397
+ const result = await this.runTurn(EXECUTE_PROMPT(session.task, session.plan ?? ""));
288
398
  if (result.status === "error") {
289
399
  session.status = "error";
290
400
  session.error = coerceText(result.result) || "execution run failed";
package/dist/cli.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * cursor-telegram-mcp setup Interactive first-time setup (bot + chat id).
8
8
  * cursor-telegram-mcp login Find your chat id (token must be set).
9
9
  * cursor-telegram-mcp worker Run the background worker in the foreground.
10
+ * cursor-telegram-mcp install Install the always-on worker (macOS launchd).
11
+ * cursor-telegram-mcp uninstall Remove the always-on worker.
10
12
  * cursor-telegram-mcp doctor Diagnose configuration / connectivity.
11
13
  *
12
14
  * Each subcommand is a module whose side-effecting `main()` runs on import.
@@ -20,6 +22,8 @@ function printHelp() {
20
22
  " cursor-telegram-mcp setup First-time setup: create/link your bot",
21
23
  " cursor-telegram-mcp login Print your Telegram chat id",
22
24
  " cursor-telegram-mcp worker Run the background worker in the foreground",
25
+ " cursor-telegram-mcp install Install the always-on worker (macOS launchd)",
26
+ " cursor-telegram-mcp uninstall Remove the always-on worker",
23
27
  " cursor-telegram-mcp doctor Diagnose configuration and connectivity",
24
28
  " cursor-telegram-mcp help Show this help",
25
29
  "",
@@ -39,6 +43,13 @@ async function run() {
39
43
  case "worker":
40
44
  await import("./worker.js");
41
45
  break;
46
+ case "install":
47
+ case "uninstall":
48
+ await import("./install.js");
49
+ break;
50
+ case "watchdog":
51
+ await import("./watchdog.js");
52
+ break;
42
53
  case "setup":
43
54
  await import("./setup.js");
44
55
  break;
package/dist/config.js CHANGED
@@ -15,11 +15,21 @@
15
15
  * MCP client needs:
16
16
  * TG_WORKER_URL - where the worker listens (default http://127.0.0.1:8787).
17
17
  * TG_PROJECT - label for this project's messages (default "default").
18
+ * TG_AGENT - optional default per-agent/chat label, shown as
19
+ * [project · agent]. Overridden by the `agent` tool param.
18
20
  * TG_DEFAULT_POLL_WAIT_MS - long-poll chunk size check_human_response uses
19
21
  * when no explicit waitMs is passed (default 120000).
22
+ * TG_STARTUP_PING - set to 0/false to silence the "worker online" message the
23
+ * worker sends after it connects (default on).
20
24
  *
21
25
  * Command mode (optional, worker-side) needs:
22
26
  * CURSOR_API_KEY - enables texting tasks to the bot to run headless agents.
27
+ * TG_COMMAND_MODE - set to 0/false to hard-disable command mode even when a
28
+ * CURSOR_API_KEY is present (inbound text won't spawn tasks).
29
+ * TG_SELF_UPDATE - set to 0/false so the worker never auto-restarts itself when
30
+ * its own source changes (prevents wiping in-flight state).
31
+ * TG_AGENT_CWD - point command-mode agents at a SEPARATE repo so their edits
32
+ * never touch the worker's own source (avoids self-restarts).
23
33
  * TG_AGENT_CWD - directory the headless agent works in (default cwd).
24
34
  * TG_AGENT_MODEL - model id for the headless agent (default composer-2.5).
25
35
  * TG_AGENT_LOAD_SETTINGS - "1"/"true" to load this repo's .cursor settings.
@@ -141,6 +151,7 @@ export function getConfig(requireToken = true) {
141
151
  }
142
152
  const workerHost = strOr("TG_WORKER_HOST", "127.0.0.1");
143
153
  const workerPort = intOr("TG_WORKER_PORT", 8787);
154
+ const agentCwd = strOr("TG_AGENT_CWD", process.cwd());
144
155
  cached = {
145
156
  botToken,
146
157
  chatId: strOr("TELEGRAM_CHAT_ID", ""),
@@ -150,11 +161,18 @@ export function getConfig(requireToken = true) {
150
161
  workerPort,
151
162
  workerUrl: strOr("TG_WORKER_URL", `http://${workerHost}:${workerPort}`).replace(/\/+$/, ""),
152
163
  project: strOr("TG_PROJECT", "default"),
164
+ agentLabel: strOr("TG_AGENT", ""),
153
165
  defaultPollWaitMs: intOr("TG_DEFAULT_POLL_WAIT_MS", 120_000),
166
+ startupPing: boolOr("TG_STARTUP_PING", true),
154
167
  cursorApiKey: strOr("CURSOR_API_KEY", ""),
155
- agentCwd: strOr("TG_AGENT_CWD", process.cwd()),
168
+ commandModeEnabled: boolOr("TG_COMMAND_MODE", true),
169
+ selfUpdateEnabled: boolOr("TG_SELF_UPDATE", true),
170
+ agentCwd,
156
171
  agentModel: strOr("TG_AGENT_MODEL", "composer-2.5"),
157
172
  agentLoadSettings: boolOr("TG_AGENT_LOAD_SETTINGS", false),
173
+ rolling: boolOr("TG_ROLLING", true),
174
+ transcriptPath: strOr("TG_TRANSCRIPT_PATH", join(agentCwd, "remote-chat.md")),
175
+ sessionPath: strOr("TG_SESSION_PATH", join(configDir(), "rolling-session.json")),
158
176
  };
159
177
  return cached;
160
178
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Bracketed prefix for outgoing messages: "[project]" or "[project · agent]"
3
+ * when a per-agent/chat label is present. Lets the human tell which agent a
4
+ * question/notification came from when several chats are active.
5
+ */
6
+ export function labelPrefix(project, agent) {
7
+ return agent && agent !== "" ? `[${project} · ${agent}]` : `[${project}]`;
8
+ }
1
9
  /**
2
10
  * Convert agent Markdown to plain text suitable for Telegram (no parse_mode).
3
11
  */
package/dist/index.js CHANGED
@@ -173,9 +173,18 @@ async function main() {
173
173
  .string()
174
174
  .min(1)
175
175
  .describe("Short summary of the completed work (what changed / result)."),
176
+ agent: z
177
+ .string()
178
+ .optional()
179
+ .describe("Optional short label for THIS chat/agent (e.g. \"auth-refactor\"), shown " +
180
+ "as [project · agent] so multiple agents are distinguishable."),
176
181
  },
177
- }, async ({ summary }) => {
178
- const r = await callWorker("POST", "/notify", { summary, project: config.project });
182
+ }, async ({ summary, agent }) => {
183
+ const r = await callWorker("POST", "/notify", {
184
+ summary,
185
+ project: config.project,
186
+ agent: (agent ?? "").trim() || config.agentLabel,
187
+ });
179
188
  if (!r)
180
189
  return textResult(WORKER_DOWN, true);
181
190
  if (r.status === 200)
@@ -203,9 +212,19 @@ async function main() {
203
212
  .min(1)
204
213
  .describe("The exact question to ask the human. Be specific and include the " +
205
214
  "options/context needed to answer from a phone."),
215
+ agent: z
216
+ .string()
217
+ .optional()
218
+ .describe("Optional short label for THIS chat/agent (e.g. \"auth-refactor\"), shown " +
219
+ "as [project · agent] Q-n so the human can tell which agent is asking " +
220
+ "and route their reply. Keep it stable across the chat."),
206
221
  },
207
- }, async ({ question }) => {
208
- const r = await callWorker("POST", "/ask", { question, project: config.project });
222
+ }, async ({ question, agent }) => {
223
+ const r = await callWorker("POST", "/ask", {
224
+ question,
225
+ project: config.project,
226
+ agent: (agent ?? "").trim() || config.agentLabel,
227
+ });
209
228
  if (!r)
210
229
  return textResult(WORKER_DOWN, true);
211
230
  if (r.status === 200) {
@@ -237,6 +256,12 @@ async function main() {
237
256
  .min(1)
238
257
  .describe("The exact question to ask the human. Be specific and include the " +
239
258
  "options/context needed to answer from a phone."),
259
+ agent: z
260
+ .string()
261
+ .optional()
262
+ .describe("Optional short label for THIS chat/agent (e.g. \"auth-refactor\"), shown " +
263
+ "as [project · agent] Q-n so the human can tell which agent is asking " +
264
+ "and route their reply. Keep it stable across the chat."),
240
265
  timeoutMin: z
241
266
  .number()
242
267
  .int()
@@ -245,9 +270,13 @@ async function main() {
245
270
  .describe("Maximum minutes to wait for a reply (default: TG_RESPONSE_TIMEOUT_MIN, " +
246
271
  "usually 30). Returns timed_out if no reply within this window."),
247
272
  },
248
- }, async ({ question, timeoutMin }) => {
273
+ }, async ({ question, agent, timeoutMin }) => {
249
274
  const maxWaitMin = timeoutMin ?? config.responseTimeoutMin;
250
- const askRes = await callWorker("POST", "/ask", { question, project: config.project });
275
+ const askRes = await callWorker("POST", "/ask", {
276
+ question,
277
+ project: config.project,
278
+ agent: (agent ?? "").trim() || config.agentLabel,
279
+ });
251
280
  if (!askRes)
252
281
  return textResult(WORKER_DOWN, true);
253
282
  if (askRes.status === 503) {