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 +104 -24
- package/dist/agentRunner.js +119 -9
- package/dist/cli.js +11 -0
- package/dist/config.js +19 -1
- package/dist/formatTelegram.js +8 -0
- package/dist/index.js +35 -6
- package/dist/install.js +231 -0
- package/dist/parseInbound.js +35 -3
- package/dist/setup.js +31 -11
- package/dist/store.js +97 -12
- package/dist/telegram.js +57 -17
- package/dist/watchdog.js +62 -0
- package/dist/worker.js +203 -54
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
cursor-telegram-mcp setup
|
|
38
52
|
```
|
|
39
53
|
|
|
40
|
-
The wizard
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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["
|
|
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
|
-
|
|
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.
|
|
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,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
247
|
-
|
|
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
|
-
|
|
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/
|
package/dist/agentRunner.js
CHANGED
|
@@ -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()`
|
|
59
|
-
*
|
|
60
|
-
* 3. You reply YES -> `approve()`
|
|
61
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/formatTelegram.js
CHANGED
|
@@ -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", {
|
|
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", {
|
|
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", {
|
|
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) {
|