cursor-telegram-mcp 0.6.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
@@ -32,21 +32,40 @@ Prefer it under "Plugin MCP Servers" / the Cursor marketplace? See
32
32
 
33
33
  ## Quick start
34
34
 
35
- ### 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
36
49
 
37
50
  ```bash
38
- npx cursor-telegram-mcp setup
51
+ cursor-telegram-mcp setup
39
52
  ```
40
53
 
41
- The wizard walks you through creating a bot with [@BotFather](https://t.me/BotFather),
42
- validates the token, captures your chat id (you message the bot once), and
43
- optionally enables command mode with a Cursor API key. It saves everything to a
44
- 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
45
59
  `%APPDATA%\cursor-telegram\config.json` on Windows).
46
60
 
47
- ### 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.
64
+
65
+ ### 3. (optional) Let Cursor agents message you
48
66
 
49
- Add this to your project's `.cursor/mcp.json` (or Cursor Settings -> Tools & MCP):
67
+ Add this to a project's `.cursor/mcp.json` (or Cursor Settings -> Tools & MCP),
68
+ then reload MCP:
50
69
 
51
70
  ```json
52
71
  {
@@ -59,8 +78,8 @@ Add this to your project's `.cursor/mcp.json` (or Cursor Settings -> Tools & MCP
59
78
  }
60
79
  ```
61
80
 
62
- Reload MCP in Cursor. That's it - the MCP server auto-starts the background
63
- 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.
64
83
 
65
84
  ## Architecture
66
85
 
@@ -174,6 +193,20 @@ current Node and the installed CLI — no hardcoded paths — runs the worker un
174
193
  (`KeepAlive`). Logs go to `~/Library/Logs/cursor-telegram-worker.log`. Keep the
175
194
  Mac on AC power so a closed lid does not fully sleep.
176
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
+
177
210
  On Linux/Windows, run `cursor-telegram-mcp worker` under your own service
178
211
  manager (systemd / Task Scheduler). (Note: on macOS, launchd agents cannot read
179
212
  files under `~/Desktop`/`~/Documents`/`~/Downloads`; install the package or
@@ -210,8 +243,9 @@ cursor-telegram-mcp [mcp] Start the MCP server (default; Cursor runs this)
210
243
  cursor-telegram-mcp setup First-time setup: create/link your bot
211
244
  cursor-telegram-mcp login Print and save your Telegram chat id
212
245
  cursor-telegram-mcp worker Run the background worker in the foreground
213
- cursor-telegram-mcp install Install the always-on worker (macOS launchd)
214
- cursor-telegram-mcp uninstall Remove the always-on worker
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
215
249
  cursor-telegram-mcp doctor Diagnose configuration and connectivity
216
250
  ```
217
251
 
@@ -308,7 +342,8 @@ src/
308
342
  session.ts # persist the rolling agent id across worker restarts
309
343
  transcript.ts # append-only remote-chat.md transcript of the rolling chat
310
344
  store.ts # pending-question store (persisted to disk) + reply matching
311
- install.ts # `install`/`uninstall`: generate a per-user launchd agent (macOS)
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
312
347
  parseInbound.ts / splitMessage.ts / taskQueue.ts / formatTelegram.ts
313
348
  answerWaiters.ts # wake-on-answer for long-polling GET /response/:id
314
349
  .cursor/
package/dist/cli.js CHANGED
@@ -47,6 +47,9 @@ async function run() {
47
47
  case "uninstall":
48
48
  await import("./install.js");
49
49
  break;
50
+ case "watchdog":
51
+ await import("./watchdog.js");
52
+ break;
50
53
  case "setup":
51
54
  await import("./setup.js");
52
55
  break;
package/dist/install.js CHANGED
@@ -12,14 +12,17 @@
12
12
  */
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
15
- import { homedir, platform } from "node:os";
16
- import { dirname, join } from "node:path";
15
+ import { homedir, platform, tmpdir } from "node:os";
16
+ import { dirname, join, sep } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  const LABEL = "com.cursor-telegram.worker";
19
+ const WATCHDOG_LABEL = "com.cursor-telegram.watchdog";
19
20
  /** Older label used by the in-repo dev scripts; booted out on install to avoid a double-run. */
20
21
  const LEGACY_LABEL = "com.cursor-remote-chat.worker";
21
- function plistPath() {
22
- return join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
22
+ /** How often (seconds) the watchdog checks worker /health. */
23
+ const WATCHDOG_INTERVAL_SEC = 120;
24
+ function plistPath(label = LABEL) {
25
+ return join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
23
26
  }
24
27
  function logPaths() {
25
28
  const dir = join(homedir(), "Library", "Logs");
@@ -38,6 +41,21 @@ function xmlEscape(s) {
38
41
  function cliEntry() {
39
42
  return fileURLToPath(import.meta.url).replace(/install\.js$/, "cli.js");
40
43
  }
44
+ /**
45
+ * Whether this package lives in a stable location. `npx -y cursor-telegram-mcp`
46
+ * runs from an ephemeral cache that npm may garbage-collect, which would leave
47
+ * the launch agent pointing at a path that disappears. A global install
48
+ * (`npm i -g`) is durable. We refuse to install a launch agent from an
49
+ * ephemeral path unless explicitly forced.
50
+ */
51
+ function isDurableInstall() {
52
+ const p = fileURLToPath(import.meta.url);
53
+ if (p.includes(`${sep}_npx${sep}`))
54
+ return false;
55
+ if (p.startsWith(tmpdir()))
56
+ return false;
57
+ return true;
58
+ }
41
59
  function buildProgramArguments() {
42
60
  const node = process.execPath;
43
61
  const cli = cliEntry();
@@ -89,6 +107,45 @@ ${argXml}
89
107
  </plist>
90
108
  `;
91
109
  }
110
+ function buildWatchdogPlist() {
111
+ const node = process.execPath;
112
+ const cli = cliEntry();
113
+ const nodeDir = dirname(process.execPath);
114
+ const pathEnv = [nodeDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].join(":");
115
+ const { out, err } = logPaths();
116
+ return `<?xml version="1.0" encoding="UTF-8"?>
117
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
118
+ <plist version="1.0">
119
+ <dict>
120
+ <key>Label</key>
121
+ <string>${WATCHDOG_LABEL}</string>
122
+ <key>ProgramArguments</key>
123
+ <array>
124
+ <string>${xmlEscape(node)}</string>
125
+ <string>${xmlEscape(cli)}</string>
126
+ <string>watchdog</string>
127
+ </array>
128
+ <key>EnvironmentVariables</key>
129
+ <dict>
130
+ <key>PATH</key>
131
+ <string>${xmlEscape(pathEnv)}</string>
132
+ <key>TG_WORKER_LABEL</key>
133
+ <string>${LABEL}</string>
134
+ </dict>
135
+ <key>RunAtLoad</key>
136
+ <true/>
137
+ <key>StartInterval</key>
138
+ <integer>${WATCHDOG_INTERVAL_SEC}</integer>
139
+ <key>ProcessType</key>
140
+ <string>Background</string>
141
+ <key>StandardOutPath</key>
142
+ <string>${xmlEscape(out)}</string>
143
+ <key>StandardErrorPath</key>
144
+ <string>${xmlEscape(err)}</string>
145
+ </dict>
146
+ </plist>
147
+ `;
148
+ }
92
149
  function domain() {
93
150
  return `gui/${process.getuid?.() ?? 501}`;
94
151
  }
@@ -108,8 +165,22 @@ function requireMac() {
108
165
  process.exit(1);
109
166
  }
110
167
  }
111
- function install() {
168
+ export function runInstall() {
112
169
  requireMac();
170
+ if (!isDurableInstall() && process.env.TG_FORCE_INSTALL !== "1") {
171
+ process.stderr.write([
172
+ "This looks like a temporary (npx) copy, which npm may delete later —",
173
+ "an always-on launch agent must point at a stable install.",
174
+ "",
175
+ "Install it globally first, then run install:",
176
+ " npm i -g cursor-telegram-mcp",
177
+ " cursor-telegram-mcp install",
178
+ "",
179
+ "(Set TG_FORCE_INSTALL=1 to override, not recommended.)",
180
+ "",
181
+ ].join("\n"));
182
+ process.exit(1);
183
+ }
113
184
  const path = plistPath();
114
185
  mkdirSync(dirname(path), { recursive: true });
115
186
  mkdirSync(join(homedir(), "Library", "Logs"), { recursive: true });
@@ -118,9 +189,16 @@ function install() {
118
189
  launchctl(["bootout", domain(), path]);
119
190
  launchctl(["bootout", domain(), join(homedir(), "Library", "LaunchAgents", `${LEGACY_LABEL}.plist`)]);
120
191
  execFileSync("launchctl", ["bootstrap", domain(), path], { stdio: "inherit" });
192
+ // Watchdog: restarts the worker if it ever goes unreachable or its poll loop
193
+ // wedges, so the bot is never silently offline while the Mac is on.
194
+ const wdPath = plistPath(WATCHDOG_LABEL);
195
+ writeFileSync(wdPath, buildWatchdogPlist(), "utf8");
196
+ launchctl(["bootout", domain(), wdPath]);
197
+ execFileSync("launchctl", ["bootstrap", domain(), wdPath], { stdio: "inherit" });
121
198
  const { out } = logPaths();
122
199
  process.stdout.write([
123
200
  `Installed always-on worker: ${path}`,
201
+ `Installed watchdog (every ${WATCHDOG_INTERVAL_SEC}s): ${wdPath}`,
124
202
  `Logs: ${out}`,
125
203
  "",
126
204
  "It will start now and at every login. To stop it:",
@@ -130,13 +208,24 @@ function install() {
130
208
  "",
131
209
  ].join("\n"));
132
210
  }
133
- function uninstall() {
211
+ export function runUninstall() {
134
212
  requireMac();
213
+ const wdPath = plistPath(WATCHDOG_LABEL);
214
+ launchctl(["bootout", domain(), wdPath]);
215
+ if (existsSync(wdPath))
216
+ rmSync(wdPath);
135
217
  const path = plistPath();
136
218
  launchctl(["bootout", domain(), path]);
137
219
  if (existsSync(path))
138
220
  rmSync(path);
139
- process.stdout.write("Uninstalled always-on worker.\n");
221
+ process.stdout.write("Uninstalled always-on worker and watchdog.\n");
222
+ }
223
+ // Only act when invoked directly as the `install`/`uninstall` subcommand, so
224
+ // other modules (e.g. setup) can import runInstall() without side effects.
225
+ const sub = process.argv[2];
226
+ if (sub === "install" || sub === "uninstall") {
227
+ if (sub === "uninstall")
228
+ runUninstall();
229
+ else
230
+ runInstall();
140
231
  }
141
- const action = process.argv[2] === "uninstall" ? uninstall : install;
142
- action();
package/dist/setup.js CHANGED
@@ -9,7 +9,9 @@
9
9
  */
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import { stdin, stdout } from "node:process";
12
+ import { platform } from "node:os";
12
13
  import { configFilePath, readFileConfig, writeFileConfig } from "./config.js";
14
+ import { runInstall } from "./install.js";
13
15
  async function tg(token, method, params) {
14
16
  const res = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
15
17
  method: "POST",
@@ -101,21 +103,39 @@ async function main() {
101
103
  out("Skipped. Notifications and questions still work; enable later by re-running setup.");
102
104
  }
103
105
  out("");
104
- out("Done. Config saved to:");
106
+ out("Config saved to:");
105
107
  out(` ${configFilePath()}`);
106
108
  out("");
107
- out("Add this to your project's .cursor/mcp.json (or Cursor Settings -> MCP):");
109
+ out("Step 4 (recommended): keep your bot online whenever your computer is on.");
110
+ out("This runs a tiny background worker that starts at login and restarts");
111
+ out("itself if it ever stops - no need to open Cursor or run anything.");
112
+ if (platform() === "darwin") {
113
+ const yes = (await rl.question("Install the always-on service now? [Y/n]: ")).trim().toLowerCase();
114
+ if (yes === "" || yes === "y" || yes === "yes") {
115
+ try {
116
+ runInstall();
117
+ }
118
+ catch (err) {
119
+ out(`Could not install the service automatically: ${String(err)}`);
120
+ out("You can run it later with: cursor-telegram-mcp install");
121
+ }
122
+ }
123
+ else {
124
+ out("Skipped. Enable it later with: cursor-telegram-mcp install");
125
+ }
126
+ }
127
+ else {
128
+ out("On Linux/Windows, run `cursor-telegram-mcp worker` under your service");
129
+ out("manager (systemd / Task Scheduler) so it starts at boot.");
130
+ }
131
+ out("");
132
+ out("Optional: to let Cursor agents message you, add this to a project's");
133
+ out(".cursor/mcp.json (or Cursor Settings -> MCP), then reload MCP:");
108
134
  out("");
109
- out(' {');
110
- out(' "mcpServers": {');
111
- out(' "telegram": {');
112
- out(' "command": "npx",');
113
- out(' "args": ["-y", "cursor-telegram-mcp"]');
114
- out(' }');
115
- out(' }');
116
- out(' }');
135
+ out(' { "mcpServers": { "telegram": { "command": "npx",');
136
+ out(' "args": ["-y", "cursor-telegram-mcp"] } } }');
117
137
  out("");
118
- out("Then reload MCP in Cursor. The worker auto-starts with the MCP server.");
138
+ out("You're set - text your bot to test it.");
119
139
  }
120
140
  finally {
121
141
  rl.close();
package/dist/telegram.js CHANGED
@@ -59,6 +59,8 @@ export class TelegramClient {
59
59
  stopping = false;
60
60
  offset = 0;
61
61
  me = null;
62
+ /** Last time getUpdates returned successfully (ms epoch); liveness signal. */
63
+ lastPollOkAt = Date.now();
62
64
  constructor(opts) {
63
65
  this.opts = opts;
64
66
  this.logger = opts.logger ?? silentLogger();
@@ -70,23 +72,46 @@ export class TelegramClient {
70
72
  isOpen() {
71
73
  return this.open;
72
74
  }
75
+ /**
76
+ * Milliseconds since getUpdates last returned successfully. A large value
77
+ * while `open` means the poll loop is stuck (e.g. a stale socket after the
78
+ * Mac slept) even though the connection looks up — used by /health and the
79
+ * watchdog to detect a silently-offline worker.
80
+ */
81
+ lastPollAgeMs() {
82
+ return Date.now() - this.lastPollOkAt;
83
+ }
73
84
  username() {
74
85
  return this.me?.username;
75
86
  }
76
87
  onIncoming(handler) {
77
88
  this.handlers.push(handler);
78
89
  }
79
- async call(method, params) {
80
- const res = await fetch(`${this.base}/${method}`, {
81
- method: "POST",
82
- headers: { "content-type": "application/json" },
83
- body: JSON.stringify(params ?? {}),
84
- });
85
- const data = (await res.json());
86
- if (!data.ok) {
87
- throw new Error(`Telegram ${method} failed: ${data.description ?? `HTTP ${res.status}`}`);
90
+ /**
91
+ * Call a Bot API method with a hard client-side timeout. Without this, a
92
+ * socket left half-open by a sleep/wake could make `fetch` hang forever and
93
+ * silently take the bot offline. On timeout the call rejects (AbortError) and
94
+ * the caller retries.
95
+ */
96
+ async call(method, params, timeoutMs = 20_000) {
97
+ const ac = new AbortController();
98
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
99
+ try {
100
+ const res = await fetch(`${this.base}/${method}`, {
101
+ method: "POST",
102
+ headers: { "content-type": "application/json" },
103
+ body: JSON.stringify(params ?? {}),
104
+ signal: ac.signal,
105
+ });
106
+ const data = (await res.json());
107
+ if (!data.ok) {
108
+ throw new Error(`Telegram ${method} failed: ${data.description ?? `HTTP ${res.status}`}`);
109
+ }
110
+ return data.result;
111
+ }
112
+ finally {
113
+ clearTimeout(timer);
88
114
  }
89
- return data.result;
90
115
  }
91
116
  async connect() {
92
117
  await mkdir(this.mediaDir, { recursive: true });
@@ -100,13 +125,19 @@ export class TelegramClient {
100
125
  sweep.unref?.();
101
126
  }
102
127
  async pollLoop() {
128
+ // Abort getUpdates a bit after the server-side long-poll window so a dead
129
+ // socket (e.g. after sleep/wake) can't wedge the loop indefinitely.
130
+ const pollTimeoutMs = (GETUPDATES_LONG_POLL_SEC + 15) * 1000;
131
+ let backoffMs = 1000;
103
132
  while (!this.stopping) {
104
133
  try {
105
134
  const updates = await this.call("getUpdates", {
106
135
  offset: this.offset,
107
136
  timeout: GETUPDATES_LONG_POLL_SEC,
108
137
  allowed_updates: ["message"],
109
- });
138
+ }, pollTimeoutMs);
139
+ this.lastPollOkAt = Date.now();
140
+ backoffMs = 1000;
110
141
  for (const update of updates) {
111
142
  this.offset = update.update_id + 1;
112
143
  const msg = await this.toIncoming(update.message);
@@ -118,8 +149,9 @@ export class TelegramClient {
118
149
  catch (err) {
119
150
  if (this.stopping)
120
151
  break;
121
- this.logger.warn(`getUpdates error (retrying): ${String(err)}`);
122
- await new Promise((r) => setTimeout(r, 3000));
152
+ this.logger.warn(`getUpdates error (retry in ${backoffMs}ms): ${String(err)}`);
153
+ await new Promise((r) => setTimeout(r, backoffMs));
154
+ backoffMs = Math.min(backoffMs * 2, 30_000);
123
155
  }
124
156
  }
125
157
  }
@@ -145,10 +177,18 @@ export class TelegramClient {
145
177
  throw new Error(`Attachment too large (${file.file_size} bytes, max ${this.maxAttachmentBytes})`);
146
178
  }
147
179
  const url = `https://api.telegram.org/file/bot${this.opts.botToken}/${file.file_path}`;
148
- const res = await fetch(url);
149
- if (!res.ok)
150
- throw new Error(`Download failed: HTTP ${res.status}`);
151
- const buf = Buffer.from(await res.arrayBuffer());
180
+ const ac = new AbortController();
181
+ const timer = setTimeout(() => ac.abort(), 60_000);
182
+ let buf;
183
+ try {
184
+ const res = await fetch(url, { signal: ac.signal });
185
+ if (!res.ok)
186
+ throw new Error(`Download failed: HTTP ${res.status}`);
187
+ buf = Buffer.from(await res.arrayBuffer());
188
+ }
189
+ finally {
190
+ clearTimeout(timer);
191
+ }
152
192
  if (buf.length > this.maxAttachmentBytes) {
153
193
  throw new Error(`Attachment too large (${buf.length} bytes, max ${this.maxAttachmentBytes})`);
154
194
  }
@@ -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
@@ -652,6 +652,7 @@ async function main() {
652
652
  queue: taskQueue.length(),
653
653
  version: WORKER_VERSION,
654
654
  uptimeSec: Math.floor((Date.now() - startedAt) / 1000),
655
+ pollAgeSec: Math.floor(client.lastPollAgeMs() / 1000),
655
656
  lastError: lastError ?? null,
656
657
  });
657
658
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-telegram-mcp",
3
- "version": "0.6.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",