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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `cursor-telegram-mcp watchdog` — one-shot liveness check.
3
+ *
4
+ * Meant to be run on a short interval by launchd (or cron). It hits the
5
+ * worker's /health and, if the worker is unreachable OR its poll loop looks
6
+ * wedged (pollAgeSec too high — e.g. a stale socket the abort timeout somehow
7
+ * missed), it kicks the worker's launch agent so launchd restarts it. The
8
+ * worker's own retry/backoff handles the common cases; this is the belt-and-
9
+ * suspenders layer so the bot is never silently offline while the Mac is on.
10
+ *
11
+ * Exits 0 always (a transient blip should not spam launchd error logs).
12
+ */
13
+ import { execFileSync } from "node:child_process";
14
+ import { getConfig } from "./config.js";
15
+ /** launchd label to restart; overridable so it works for legacy installs too. */
16
+ const WORKER_LABEL = process.env.TG_WORKER_LABEL?.trim() || "com.cursor-telegram.worker";
17
+ /** Restart if getUpdates has not returned successfully for this many seconds. */
18
+ const MAX_POLL_AGE_SEC = Number.parseInt(process.env.TG_WATCHDOG_MAX_POLL_AGE_SEC ?? "120", 10);
19
+ function log(msg) {
20
+ process.stderr.write(`[watchdog] ${new Date().toISOString()} ${msg}\n`);
21
+ }
22
+ function kick() {
23
+ const uid = process.getuid?.() ?? 501;
24
+ try {
25
+ execFileSync("launchctl", ["kickstart", "-k", `gui/${uid}/${WORKER_LABEL}`], {
26
+ stdio: "ignore",
27
+ });
28
+ log(`restarted ${WORKER_LABEL}`);
29
+ }
30
+ catch (err) {
31
+ log(`failed to restart ${WORKER_LABEL}: ${String(err)}`);
32
+ }
33
+ }
34
+ async function main() {
35
+ const url = `${getConfig(false).workerUrl}/health`;
36
+ const ac = new AbortController();
37
+ const timer = setTimeout(() => ac.abort(), 5000);
38
+ try {
39
+ const res = await fetch(url, { signal: ac.signal });
40
+ const body = (await res.json());
41
+ if (!body.ok || body.connected === false) {
42
+ log(`unhealthy (ok=${body.ok}, connected=${body.connected}); restarting`);
43
+ kick();
44
+ return;
45
+ }
46
+ const age = body.pollAgeSec ?? 0;
47
+ if (age > MAX_POLL_AGE_SEC) {
48
+ log(`poll loop wedged (pollAgeSec=${age} > ${MAX_POLL_AGE_SEC}); restarting`);
49
+ kick();
50
+ return;
51
+ }
52
+ log(`ok (pollAgeSec=${age})`);
53
+ }
54
+ catch (err) {
55
+ log(`worker unreachable at ${url}: ${String(err)}; restarting`);
56
+ kick();
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ }
61
+ }
62
+ await main();
package/dist/worker.js CHANGED
@@ -18,23 +18,24 @@
18
18
  *
19
19
  * API (JSON, localhost only):
20
20
  * GET /health -> { ok, connected, target, pending, commandMode, queue }
21
- * POST /notify { summary, project } -> { ok } | 429 { error, waitMs } | 503
22
- * POST /ask { question, project }-> { id } | 429 | 503
23
- * POST /mirror { question, project } -> { id, mirrored } | 429 | 503
21
+ * POST /notify { summary, project, agent? } -> { ok } | 429 { error, waitMs } | 503
22
+ * POST /ask { question, project, agent? }-> { id } | 429 | 503
23
+ * POST /mirror { question, project, agent? } -> { id, mirrored } | 429 | 503
24
24
  * GET /response/:id -> { id, status, answer?, attachments?, elapsedMin } | 404
25
25
  * optional ?waitMs=N long-polls until answered or N ms
26
26
  */
27
27
  import { createServer } from "node:http";
28
28
  import { notifyAnswered, waitForAnswer } from "./answerWaiters.js";
29
- import { readdirSync, statSync } from "node:fs";
30
- import { homedir } from "node:os";
29
+ import { readFileSync, readdirSync, statSync } from "node:fs";
30
+ import { homedir, hostname } from "node:os";
31
31
  import { delimiter, dirname, join } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
- import { getConfig } from "./config.js";
33
+ import { configDir, getConfig } from "./config.js";
34
34
  import { createStore } from "./store.js";
35
35
  import { createCommandRunner } from "./agentRunner.js";
36
- import { toPlainTelegram } from "./formatTelegram.js";
37
- import { splitInboundMessage } from "./parseInbound.js";
36
+ import { Transcript } from "./transcript.js";
37
+ import { labelPrefix, toPlainTelegram } from "./formatTelegram.js";
38
+ import { parseTargetId, splitInboundMessage } from "./parseInbound.js";
38
39
  import { splitMessage } from "./splitMessage.js";
39
40
  import { createTaskQueue } from "./taskQueue.js";
40
41
  import { TelegramClient, createStderrLogger, } from "./telegram.js";
@@ -44,6 +45,17 @@ const COMMAND_TTL_MS = 60 * 60_000;
44
45
  const UPDATE_CHECK_MS = 15_000;
45
46
  /** Directory holding this worker's source (for self-update detection). */
46
47
  const SRC_DIR = dirname(fileURLToPath(import.meta.url));
48
+ /** Worker version from package.json (one level up from src/ or dist/). */
49
+ function readVersion() {
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(join(SRC_DIR, "..", "package.json"), "utf8"));
52
+ return pkg.version ?? "unknown";
53
+ }
54
+ catch {
55
+ return "unknown";
56
+ }
57
+ }
58
+ const WORKER_VERSION = readVersion();
47
59
  /**
48
60
  * Newest mtime among the worker's TypeScript source files. Used to notice when
49
61
  * a command-mode task has edited the worker's own code, so it can relaunch on
@@ -77,6 +89,11 @@ function cleanProject(raw) {
77
89
  const s = String(raw ?? "").trim().replace(/\s+/g, " ");
78
90
  return s === "" ? "default" : s.slice(0, 48);
79
91
  }
92
+ /** Sanitize an optional per-agent label; empty string means "no agent label". */
93
+ function cleanAgent(raw) {
94
+ const s = String(raw ?? "").trim().replace(/\s+/g, " ");
95
+ return s.slice(0, 48);
96
+ }
80
97
  function sendJson(res, status, body) {
81
98
  res.writeHead(status, { "content-type": "application/json" });
82
99
  res.end(JSON.stringify(body));
@@ -108,16 +125,6 @@ function isReplyToQuestion(msg, store) {
108
125
  const pendings = store.listPending();
109
126
  return pendings.some((q) => q.sentMessageId && q.sentMessageId === msg.quotedMessageId);
110
127
  }
111
- function isCommandLike(segments) {
112
- return segments.some((s) => s.kind === "ask" ||
113
- s.kind === "plan" ||
114
- s.kind === "ask_empty" ||
115
- s.kind === "plan_empty" ||
116
- s.kind === "status" ||
117
- s.kind === "approve" ||
118
- s.kind === "reject" ||
119
- s.kind === "approval_footer");
120
- }
121
128
  async function main() {
122
129
  // Make sure the Cursor CLI (`cursor-agent`, used by command mode) is findable
123
130
  // even when launched by a GUI/launchd context with a minimal PATH.
@@ -130,18 +137,32 @@ async function main() {
130
137
  if (config.chatId === "") {
131
138
  log("TELEGRAM_CHAT_ID is not set. Run `npm run login` to find it, then set it in .env.");
132
139
  }
133
- const store = createStore();
140
+ const store = createStore({ persistPath: join(configDir(), "questions.json") });
134
141
  const taskQueue = createTaskQueue();
142
+ const startedAt = Date.now();
143
+ let lastError;
144
+ const restoredPending = store.pendingCount();
135
145
  const runner = createCommandRunner({
136
146
  apiKey: config.cursorApiKey,
137
147
  cwd: config.agentCwd,
138
148
  model: config.agentModel,
139
149
  settingSources: config.agentLoadSettings ? ["all"] : [],
150
+ rolling: config.rolling,
151
+ sessionPath: config.sessionPath,
140
152
  });
141
- const commandMode = runner.enabled;
153
+ const commandMode = runner.enabled && config.commandModeEnabled;
154
+ const transcript = new Transcript(config.transcriptPath);
142
155
  log(commandMode
143
156
  ? `Command mode ON (model ${config.agentModel}, cwd ${config.agentCwd}). /ask, /plan, or plain text to plan.`
144
- : "Command mode OFF (set CURSOR_API_KEY to text tasks to the bot).");
157
+ : runner.enabled
158
+ ? "Command mode OFF (disabled via TG_COMMAND_MODE). Inbound text will not spawn tasks."
159
+ : "Command mode OFF (set CURSOR_API_KEY to text tasks to the bot).");
160
+ if (commandMode && config.rolling) {
161
+ const info = runner.rollingInfo();
162
+ log(info
163
+ ? `Rolling thread ON (resuming ${info.agentId}). Transcript: ${config.transcriptPath}`
164
+ : `Rolling thread ON (new thread on first message). Transcript: ${config.transcriptPath}`);
165
+ }
145
166
  const client = new TelegramClient({
146
167
  botToken: config.botToken,
147
168
  logger: createStderrLogger("warn"),
@@ -208,9 +229,11 @@ async function main() {
208
229
  });
209
230
  log(`Execution ${id} -> ${session.status}`);
210
231
  if (session.status === "done") {
232
+ transcript.append({ role: "done", id, text: session.result ?? "" });
211
233
  await send(`Done (${id})\n\n${session.result ?? ""}`);
212
234
  }
213
235
  else if (session.status === "error") {
236
+ transcript.append({ role: "error", id, text: session.error ?? "unknown error" });
214
237
  await send(`Command ${id} failed:\n${session.error ?? "unknown error"}`);
215
238
  }
216
239
  }
@@ -218,15 +241,35 @@ async function main() {
218
241
  clearHeartbeat();
219
242
  }
220
243
  }
244
+ /** One line per pending question: "Q-3 [project · agent]: question…". */
245
+ function pendingLines() {
246
+ return store.listPending().map((q) => {
247
+ const head = `${q.id} ${labelPrefix(q.projectLabel, q.agentLabel)}`;
248
+ const preview = q.question.replace(/\s+/g, " ").slice(0, 80);
249
+ return `${head}: ${preview}`;
250
+ });
251
+ }
252
+ /** Full reply to /pending: the list plus how to answer a specific one. */
253
+ function formatPendingList() {
254
+ const lines = pendingLines();
255
+ if (lines.length === 0)
256
+ return "No pending questions.";
257
+ const example = store.listPending()[0]?.id ?? "Q-1";
258
+ return (`Pending questions (${lines.length}):\n${lines.join("\n")}\n\n` +
259
+ `To answer a specific one, swipe-reply its message, or prefix your answer ` +
260
+ `with its id, e.g. "${example} your answer".`);
261
+ }
221
262
  function formatStatus() {
222
263
  const lines = [];
223
264
  lines.push(`Command mode: ${commandMode ? "ON" : "OFF"}`);
224
- const pending = store.listPending();
265
+ const pending = pendingLines();
225
266
  if (pending.length === 0) {
226
267
  lines.push("Pending questions: none");
227
268
  }
228
269
  else {
229
- lines.push(`Pending questions: ${pending.length} (${pending.map((q) => q.id).join(", ")})`);
270
+ lines.push(`Pending questions: ${pending.length}`);
271
+ for (const line of pending)
272
+ lines.push(` ${line}`);
230
273
  }
231
274
  const awaiting = runner.listAwaitingApproval();
232
275
  if (awaiting.length === 0) {
@@ -244,13 +287,19 @@ async function main() {
244
287
  lines.push(`Active asks: ${activeAsks.map((s) => s.id).join(", ")}`);
245
288
  }
246
289
  lines.push(`Queued tasks: ${taskQueue.preview()}`);
290
+ if (commandMode && config.rolling) {
291
+ const info = runner.rollingInfo();
292
+ lines.push(`Rolling thread: ${info ? info.agentId : "not started yet"}`);
293
+ lines.push(`Transcript: ${config.transcriptPath}`);
294
+ }
247
295
  if (commandMode) {
248
- lines.push("Commands: /ask question, /plan task, plain text plans too");
296
+ lines.push("Commands: /ask question, /plan task, plain text plans too, /reset for a new thread");
249
297
  }
250
298
  return lines.join("\n");
251
299
  }
252
300
  async function runPlanTask(task, attachments) {
253
301
  log(`New task received; planning...`);
302
+ transcript.append({ role: "you", text: task });
254
303
  await send(`Planning your request...`);
255
304
  let heartbeat;
256
305
  const clearHeartbeat = () => {
@@ -278,14 +327,17 @@ async function main() {
278
327
  clearHeartbeat();
279
328
  log(`Plan ${session.id} -> ${session.status}`);
280
329
  if (session.status === "awaiting_approval") {
330
+ transcript.append({ role: "plan", id: session.id, text: session.plan ?? "" });
281
331
  await send(`Plan (${session.id})\n\n${session.plan ?? ""}\n\nReply YES to run it or NO to cancel.`);
282
332
  }
283
333
  else {
334
+ transcript.append({ role: "error", id: session.id, text: session.error ?? "unknown error" });
284
335
  await send(`Could not plan that (${session.id}):\n${session.error ?? "unknown error"}`);
285
336
  }
286
337
  }
287
338
  async function runAskQuestion(question, attachments) {
288
339
  log(`Ask received; answering...`);
340
+ transcript.append({ role: "you", text: `/ask ${question}` });
289
341
  await send(`Answering...`);
290
342
  let heartbeat;
291
343
  const clearHeartbeat = () => {
@@ -313,9 +365,11 @@ async function main() {
313
365
  clearHeartbeat();
314
366
  log(`Ask ${session.id} -> ${session.status}`);
315
367
  if (session.status === "done") {
368
+ transcript.append({ role: "answer", id: session.id, text: session.answer ?? "" });
316
369
  await send(`Ask (${session.id})\n\n${session.answer ?? ""}`);
317
370
  }
318
371
  else {
372
+ transcript.append({ role: "error", id: session.id, text: session.error ?? "unknown error" });
319
373
  await send(`Could not answer (${session.id}):\n${session.error ?? "unknown error"}`);
320
374
  }
321
375
  }
@@ -359,6 +413,25 @@ async function main() {
359
413
  case "status":
360
414
  await send(formatStatus());
361
415
  return;
416
+ case "pending":
417
+ await send(formatPendingList());
418
+ return;
419
+ case "reset": {
420
+ if (!commandMode)
421
+ return;
422
+ if (!config.rolling) {
423
+ await send("Rolling thread is off; nothing to reset.");
424
+ return;
425
+ }
426
+ if (isWorkerBusy()) {
427
+ await send("BUSY: finish the current work before starting a new thread.");
428
+ return;
429
+ }
430
+ runner.reset();
431
+ transcript.append({ role: "system", text: "New rolling thread started (memory cleared)." });
432
+ await send("Started a fresh thread. The next prompt begins a new conversation.");
433
+ return;
434
+ }
362
435
  case "approve": {
363
436
  if (!commandMode)
364
437
  return;
@@ -446,22 +519,75 @@ async function main() {
446
519
  const text = msg.text.trim();
447
520
  if (text === "" && attachments.length === 0)
448
521
  return;
522
+ const recordAnswer = async (matched) => {
523
+ log(`Recorded answer for ${matched.id} ${labelPrefix(matched.projectLabel, matched.agentLabel)}.`);
524
+ notifyAnswered(matched.id);
525
+ await send(`Answer recorded for ${matched.id}.`);
526
+ };
527
+ // 1) Explicit target prefix ("Q-3 ...", "#3 ...", "@Q-3 ...") wins over
528
+ // everything else, so "Q-3 yes" answers Q-3 rather than reading as a
529
+ // command. Only intercept when something is actually pending.
530
+ if (store.pendingCount() > 0) {
531
+ const target = parseTargetId(text);
532
+ if (target) {
533
+ if (!store.listPending().some((q) => q.id === target.id)) {
534
+ await send(`No pending question ${target.id}.\n\n${formatPendingList()}`);
535
+ return;
536
+ }
537
+ if (target.rest === "" && attachments.length === 0) {
538
+ await send(`What's your answer to ${target.id}? Reply: ${target.id} your answer`);
539
+ return;
540
+ }
541
+ const matched = store.matchAndAnswer(msg, { targetId: target.id, answerText: target.rest });
542
+ if (matched)
543
+ await recordAnswer(matched);
544
+ return;
545
+ }
546
+ }
547
+ // 2) Swipe-reply to a specific pending question.
449
548
  if (isReplyToQuestion(msg, store)) {
450
549
  const matched = store.matchAndAnswer(msg);
451
- if (matched) {
452
- log(`Recorded answer for ${matched.id} [${matched.projectLabel}].`);
453
- notifyAnswered(matched.id);
454
- await send(`Answer recorded for ${matched.id}.`);
455
- }
550
+ if (matched)
551
+ await recordAnswer(matched);
456
552
  return;
457
553
  }
458
- if (store.pendingCount() > 0 && !isCommandLike(splitInboundMessage(text))) {
459
- const matched = store.matchAndAnswer(msg);
460
- if (matched) {
461
- log(`Recorded answer for ${matched.id} [${matched.projectLabel}].`);
462
- notifyAnswered(matched.id);
463
- await send(`Answer recorded for ${matched.id}.`);
464
- return;
554
+ // 3) Bare reply while questions are pending. A yes/no/ok parses as an
555
+ // approve/reject COMMAND, but only when command mode is on AND a plan is
556
+ // awaiting approval. Otherwise route it to the pending question so answers
557
+ // like "Yes" are not silently swallowed.
558
+ if (store.pendingCount() > 0) {
559
+ const segs = splitInboundMessage(text);
560
+ const canApproveNow = commandMode && runner.latestAwaitingApproval() != null;
561
+ const actionableCommand = segs.some((s) => {
562
+ switch (s.kind) {
563
+ case "ask":
564
+ case "ask_empty":
565
+ case "plan":
566
+ case "plan_empty":
567
+ case "status":
568
+ case "pending":
569
+ return true;
570
+ case "reset":
571
+ return commandMode;
572
+ case "approve":
573
+ case "reject":
574
+ case "approval_footer":
575
+ return canApproveNow;
576
+ default:
577
+ return false;
578
+ }
579
+ });
580
+ if (!actionableCommand) {
581
+ if (store.pendingCount() > 1) {
582
+ // Ambiguous: don't guess at the oldest. Ask the human to target one.
583
+ await send(`You have ${store.pendingCount()} questions waiting — tell me which to answer.\n\n${formatPendingList()}`);
584
+ return;
585
+ }
586
+ const matched = store.matchAndAnswer(msg);
587
+ if (matched) {
588
+ await recordAnswer(matched);
589
+ return;
590
+ }
465
591
  }
466
592
  }
467
593
  const segments = splitInboundMessage(text);
@@ -481,7 +607,10 @@ async function main() {
481
607
  return;
482
608
  incomingChain = incomingChain
483
609
  .then(() => handleIncoming(msg))
484
- .catch((err) => log(`handleIncoming error: ${String(err)}`));
610
+ .catch((err) => {
611
+ lastError = `handleIncoming: ${String(err)}`;
612
+ log(lastError);
613
+ });
485
614
  });
486
615
  function rateGuard() {
487
616
  const elapsed = Date.now() - lastSendAt;
@@ -492,7 +621,7 @@ async function main() {
492
621
  function dedupeMirrorKey(project, question) {
493
622
  return `${project}::${question.slice(0, 200)}`;
494
623
  }
495
- async function mirrorQuestion(project, question) {
624
+ async function mirrorQuestion(project, question, agent) {
496
625
  const key = dedupeMirrorKey(project, question);
497
626
  const now = Date.now();
498
627
  const last = recentMirrors.get(key);
@@ -501,8 +630,8 @@ async function main() {
501
630
  return { id: existing?.id ?? "deduped", mirrored: false };
502
631
  }
503
632
  recentMirrors.set(key, now);
504
- const record = store.addQuestion(project, question);
505
- const text = `[${project}] ${record.id}\n${question}\n\nReply to this message to answer.`;
633
+ const record = store.addQuestion(project, question, agent);
634
+ const text = `${labelPrefix(project, agent)} ${record.id}\n${question}\n\nReply to this message to answer.`;
506
635
  const sentId = await send(text);
507
636
  if (sentId)
508
637
  store.setSentMessageId(record.id, sentId);
@@ -521,6 +650,10 @@ async function main() {
521
650
  pending: store.pendingCount(),
522
651
  commandMode,
523
652
  queue: taskQueue.length(),
653
+ version: WORKER_VERSION,
654
+ uptimeSec: Math.floor((Date.now() - startedAt) / 1000),
655
+ pollAgeSec: Math.floor(client.lastPollAgeMs() / 1000),
656
+ lastError: lastError ?? null,
524
657
  });
525
658
  }
526
659
  if (method === "POST" && path === "/notify") {
@@ -534,9 +667,10 @@ async function main() {
534
667
  const body = await readJson(req);
535
668
  const summary = String(body.summary ?? "").trim();
536
669
  const project = cleanProject(body.project);
670
+ const agent = cleanAgent(body.agent);
537
671
  if (!summary)
538
672
  return sendJson(res, 400, { error: "summary is required" });
539
- await send(`[${project}] Task complete\n\n${summary}`);
673
+ await send(`${labelPrefix(project, agent)} Task complete\n\n${summary}`);
540
674
  return sendJson(res, 200, { ok: true });
541
675
  }
542
676
  if (method === "POST" && (path === "/ask" || path === "/mirror")) {
@@ -550,9 +684,10 @@ async function main() {
550
684
  const body = await readJson(req);
551
685
  const question = String(body.question ?? "").trim();
552
686
  const project = cleanProject(body.project);
687
+ const agent = cleanAgent(body.agent);
553
688
  if (!question)
554
689
  return sendJson(res, 400, { error: "question is required" });
555
- const { id, mirrored } = await mirrorQuestion(project, question);
690
+ const { id, mirrored } = await mirrorQuestion(project, question, agent);
556
691
  return sendJson(res, 200, { id, mirrored: path === "/mirror" ? mirrored : true });
557
692
  }
558
693
  if (method === "GET" && path.startsWith("/response/")) {
@@ -616,6 +751,7 @@ async function main() {
616
751
  return sendJson(res, 404, { error: "not found" });
617
752
  }
618
753
  catch (err) {
754
+ lastError = `http: ${String(err)}`;
619
755
  return sendJson(res, 500, { error: String(err) });
620
756
  }
621
757
  });
@@ -633,6 +769,13 @@ async function main() {
633
769
  const sweep = setInterval(() => runner.sweepStale(COMMAND_TTL_MS), 10 * 60_000);
634
770
  sweep.unref?.();
635
771
  await client.connect().catch((err) => log(`Telegram connection error: ${String(err)}`));
772
+ // Startup ping: let the human know the worker is back online (opt-out via
773
+ // TG_STARTUP_PING=0). Includes restored pending-question count so a restart
774
+ // that recovered state is visible.
775
+ if (config.startupPing && client.isOpen() && config.chatId !== "") {
776
+ const restored = restoredPending > 0 ? `, restored ${restoredPending} pending` : "";
777
+ await send(`Worker online on ${hostname()} (v${WORKER_VERSION}). Command mode ${commandMode ? "ON" : "OFF"}${restored}.`);
778
+ }
636
779
  const shutdown = () => {
637
780
  log("Shutting down...");
638
781
  server.close();
@@ -643,23 +786,29 @@ async function main() {
643
786
  // Self-update: if a task edits the worker's own source, relaunch on the new
644
787
  // code once nothing is in flight. Only effective under a KeepAlive supervisor
645
788
  // (launchd `npm run worker:install`), which restarts the process after exit.
646
- const startFingerprint = sourceFingerprint();
647
- function isIdleForRestart() {
648
- return (!isWorkerBusy() &&
789
+ // Disabled when TG_SELF_UPDATE is off: the worker then never restarts itself on
790
+ // source changes, so in-flight command-mode state can't be wiped mid-task (you
791
+ // restart manually to pick up new code).
792
+ if (config.selfUpdateEnabled) {
793
+ const startFingerprint = sourceFingerprint();
794
+ const isIdleForRestart = () => !isWorkerBusy() &&
649
795
  !draining &&
650
796
  taskQueue.length() === 0 &&
651
797
  runner.listAwaitingApproval().length === 0 &&
652
798
  runner.listActiveAsks().length === 0 &&
653
- store.pendingCount() === 0);
799
+ store.pendingCount() === 0;
800
+ const updateCheck = setInterval(() => {
801
+ if (sourceFingerprint() > startFingerprint && isIdleForRestart()) {
802
+ log("Source changed and worker idle; restarting to load the latest version...");
803
+ clearInterval(updateCheck);
804
+ shutdown();
805
+ }
806
+ }, UPDATE_CHECK_MS);
807
+ updateCheck.unref?.();
808
+ }
809
+ else {
810
+ log("Self-update OFF (TG_SELF_UPDATE). Worker will not auto-restart on source changes.");
654
811
  }
655
- const updateCheck = setInterval(() => {
656
- if (sourceFingerprint() > startFingerprint && isIdleForRestart()) {
657
- log("Source changed and worker idle; restarting to load the latest version...");
658
- clearInterval(updateCheck);
659
- shutdown();
660
- }
661
- }, UPDATE_CHECK_MS);
662
- updateCheck.unref?.();
663
812
  }
664
813
  main().catch((err) => {
665
814
  log(`Fatal: ${err?.stack ?? err}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-telegram-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Manage Cursor from your phone over Telegram: an MCP server + auto-spawned local worker that notifies you, asks you questions, and (optionally) runs headless Cursor agents you text it. Local, bring-your-own-bot, runs entirely on your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",