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 +48 -13
- package/dist/cli.js +3 -0
- package/dist/install.js +98 -9
- package/dist/setup.js +31 -11
- package/dist/telegram.js +57 -17
- package/dist/watchdog.js +62 -0
- package/dist/worker.js +1 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
51
|
+
cursor-telegram-mcp setup
|
|
39
52
|
```
|
|
40
53
|
|
|
41
|
-
The wizard
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
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
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
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("
|
|
106
|
+
out("Config saved to:");
|
|
105
107
|
out(` ${configFilePath()}`);
|
|
106
108
|
out("");
|
|
107
|
-
out("
|
|
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('
|
|
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("
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 (
|
|
122
|
-
await new Promise((r) => setTimeout(r,
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
}
|
package/dist/watchdog.js
ADDED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-telegram-mcp",
|
|
3
|
-
"version": "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",
|