cursor-telegram-mcp 0.5.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/dist/cli.js ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Single CLI entry point for the published package.
4
+ *
5
+ * Usage:
6
+ * cursor-telegram-mcp Start the MCP server (what Cursor runs).
7
+ * cursor-telegram-mcp setup Interactive first-time setup (bot + chat id).
8
+ * cursor-telegram-mcp login Find your chat id (token must be set).
9
+ * cursor-telegram-mcp worker Run the background worker in the foreground.
10
+ * cursor-telegram-mcp doctor Diagnose configuration / connectivity.
11
+ *
12
+ * Each subcommand is a module whose side-effecting `main()` runs on import.
13
+ */
14
+ function printHelp() {
15
+ process.stderr.write([
16
+ "cursor-telegram-mcp - manage Cursor from your phone over Telegram",
17
+ "",
18
+ "Usage:",
19
+ " cursor-telegram-mcp [mcp] Start the MCP server (default; Cursor runs this)",
20
+ " cursor-telegram-mcp setup First-time setup: create/link your bot",
21
+ " cursor-telegram-mcp login Print your Telegram chat id",
22
+ " cursor-telegram-mcp worker Run the background worker in the foreground",
23
+ " cursor-telegram-mcp doctor Diagnose configuration and connectivity",
24
+ " cursor-telegram-mcp help Show this help",
25
+ "",
26
+ "Add to Cursor (mcp.json):",
27
+ ' { "mcpServers": { "telegram": { "command": "npx", "args": ["-y", "cursor-telegram-mcp"] } } }',
28
+ "",
29
+ ].join("\n"));
30
+ }
31
+ async function run() {
32
+ const sub = process.argv[2];
33
+ switch (sub) {
34
+ case undefined:
35
+ case "mcp":
36
+ case "start":
37
+ await import("./index.js");
38
+ break;
39
+ case "worker":
40
+ await import("./worker.js");
41
+ break;
42
+ case "setup":
43
+ await import("./setup.js");
44
+ break;
45
+ case "login":
46
+ await import("./login.js");
47
+ break;
48
+ case "doctor":
49
+ await import("./doctor.js");
50
+ break;
51
+ case "help":
52
+ case "-h":
53
+ case "--help":
54
+ printHelp();
55
+ break;
56
+ default:
57
+ process.stderr.write(`Unknown command: ${sub}\n\n`);
58
+ printHelp();
59
+ process.exit(1);
60
+ }
61
+ }
62
+ run().catch((err) => {
63
+ process.stderr.write(`Fatal: ${err?.stack ?? err}\n`);
64
+ process.exit(1);
65
+ });
66
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Central configuration for the local, single-user setup.
3
+ *
4
+ * Resolution order for every value (highest priority first):
5
+ * 1. Real environment variables (e.g. set in your Cursor `mcp.json`).
6
+ * 2. The user config file written by `setup` / `login`
7
+ * (`<configDir>/config.json`, keyed by the same names as the env vars).
8
+ * 3. A local `.env` in the working directory (developer convenience).
9
+ * 4. Built-in defaults.
10
+ *
11
+ * Worker needs:
12
+ * TELEGRAM_BOT_TOKEN - bot token from @BotFather.
13
+ * TELEGRAM_CHAT_ID - the chat to message and accept replies from.
14
+ *
15
+ * MCP client needs:
16
+ * TG_WORKER_URL - where the worker listens (default http://127.0.0.1:8787).
17
+ * TG_PROJECT - label for this project's messages (default "default").
18
+ * TG_DEFAULT_POLL_WAIT_MS - long-poll chunk size check_human_response uses
19
+ * when no explicit waitMs is passed (default 120000).
20
+ *
21
+ * Command mode (optional, worker-side) needs:
22
+ * CURSOR_API_KEY - enables texting tasks to the bot to run headless agents.
23
+ * TG_AGENT_CWD - directory the headless agent works in (default cwd).
24
+ * TG_AGENT_MODEL - model id for the headless agent (default composer-2.5).
25
+ * TG_AGENT_LOAD_SETTINGS - "1"/"true" to load this repo's .cursor settings.
26
+ * TG_ROLLING - "1"/"true" (default) to keep ONE rolling agent thread so
27
+ * knowledge carries across phone messages.
28
+ * TG_TRANSCRIPT_PATH - markdown transcript file (default <cwd>/remote-chat.md).
29
+ * TG_SESSION_PATH - rolling-session state file (default
30
+ * <configDir>/rolling-session.json).
31
+ */
32
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
33
+ import { homedir, platform } from "node:os";
34
+ import { join } from "node:path";
35
+ // Best-effort load of a local .env (developer convenience). Real env vars and
36
+ // the user config file both take precedence over anything it sets.
37
+ try {
38
+ process.loadEnvFile?.();
39
+ }
40
+ catch {
41
+ // no .env file present - rely on real environment variables / config file
42
+ }
43
+ /** Cross-platform per-user config directory for this tool. */
44
+ export function configDir() {
45
+ if (platform() === "win32") {
46
+ const appData = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
47
+ return join(appData, "cursor-telegram");
48
+ }
49
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
50
+ const base = xdg && xdg !== "" ? xdg : join(homedir(), ".config");
51
+ return join(base, "cursor-telegram");
52
+ }
53
+ /** Path to the user config file (config.json) inside the config dir. */
54
+ export function configFilePath() {
55
+ return join(configDir(), "config.json");
56
+ }
57
+ /** Read the user config file. Returns {} if missing or unreadable. */
58
+ export function readFileConfig() {
59
+ try {
60
+ const raw = readFileSync(configFilePath(), "utf8");
61
+ const parsed = JSON.parse(raw);
62
+ const out = {};
63
+ for (const [k, v] of Object.entries(parsed)) {
64
+ if (v != null)
65
+ out[k] = String(v);
66
+ }
67
+ return out;
68
+ }
69
+ catch {
70
+ return {};
71
+ }
72
+ }
73
+ /** Merge `patch` into the user config file, creating it (0600) if needed. */
74
+ export function writeFileConfig(patch) {
75
+ const dir = configDir();
76
+ mkdirSync(dir, { recursive: true });
77
+ const current = readFileConfig();
78
+ for (const [k, v] of Object.entries(patch)) {
79
+ if (v === undefined)
80
+ continue;
81
+ if (v === "")
82
+ delete current[k];
83
+ else
84
+ current[k] = v;
85
+ }
86
+ const path = configFilePath();
87
+ writeFileSync(path, JSON.stringify(current, null, 2) + "\n", "utf8");
88
+ if (platform() !== "win32") {
89
+ try {
90
+ chmodSync(path, 0o600);
91
+ }
92
+ catch {
93
+ // best-effort: secrets file permission tightening
94
+ }
95
+ }
96
+ return path;
97
+ }
98
+ let fileCfg = null;
99
+ /** Resolve a single setting: real env > config file > undefined. */
100
+ function raw(name) {
101
+ const fromEnv = process.env[name];
102
+ if (fromEnv !== undefined && fromEnv.trim() !== "")
103
+ return fromEnv;
104
+ if (fileCfg === null)
105
+ fileCfg = readFileConfig();
106
+ const fromFile = fileCfg[name];
107
+ if (fromFile !== undefined && String(fromFile).trim() !== "")
108
+ return String(fromFile);
109
+ return undefined;
110
+ }
111
+ function strOr(name, fallback) {
112
+ return (raw(name) ?? fallback).trim();
113
+ }
114
+ function intOr(name, fallback) {
115
+ const value = raw(name);
116
+ if (value === undefined)
117
+ return fallback;
118
+ const parsed = Number.parseInt(value, 10);
119
+ return Number.isFinite(parsed) ? parsed : fallback;
120
+ }
121
+ function boolOr(name, fallback) {
122
+ const value = raw(name)?.trim().toLowerCase();
123
+ if (value === undefined || value === "")
124
+ return fallback;
125
+ return value === "1" || value === "true" || value === "yes" || value === "on";
126
+ }
127
+ let cached = null;
128
+ /**
129
+ * Build (and cache) the configuration. Throws if the bot token is missing,
130
+ * unless `requireToken` is false (used by the MCP client, which only needs the
131
+ * worker URL).
132
+ */
133
+ export function getConfig(requireToken = true) {
134
+ if (cached)
135
+ return cached;
136
+ fileCfg = readFileConfig();
137
+ const botToken = strOr("TELEGRAM_BOT_TOKEN", "");
138
+ if (requireToken && botToken === "") {
139
+ throw new Error("TELEGRAM_BOT_TOKEN is not set. Run `cursor-telegram-mcp setup` (or set it in " +
140
+ 'your mcp.json env), e.g. TELEGRAM_BOT_TOKEN="123456789:ABC-DEF...".');
141
+ }
142
+ const workerHost = strOr("TG_WORKER_HOST", "127.0.0.1");
143
+ const workerPort = intOr("TG_WORKER_PORT", 8787);
144
+ cached = {
145
+ botToken,
146
+ chatId: strOr("TELEGRAM_CHAT_ID", ""),
147
+ minSendGapMs: intOr("TG_MIN_SEND_GAP_MS", 3000),
148
+ responseTimeoutMin: intOr("TG_RESPONSE_TIMEOUT_MIN", 30),
149
+ workerHost,
150
+ workerPort,
151
+ workerUrl: strOr("TG_WORKER_URL", `http://${workerHost}:${workerPort}`).replace(/\/+$/, ""),
152
+ project: strOr("TG_PROJECT", "default"),
153
+ defaultPollWaitMs: intOr("TG_DEFAULT_POLL_WAIT_MS", 120_000),
154
+ cursorApiKey: strOr("CURSOR_API_KEY", ""),
155
+ agentCwd: strOr("TG_AGENT_CWD", process.cwd()),
156
+ agentModel: strOr("TG_AGENT_MODEL", "composer-2.5"),
157
+ agentLoadSettings: boolOr("TG_AGENT_LOAD_SETTINGS", false),
158
+ };
159
+ return cached;
160
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Diagnose configuration and connectivity. Prints PASS / WARN / FAIL lines with
3
+ * actionable fixes so users can self-serve "it's not working".
4
+ */
5
+ import { spawnSync } from "node:child_process";
6
+ import { delimiter, join } from "node:path";
7
+ import { homedir, platform } from "node:os";
8
+ import { existsSync } from "node:fs";
9
+ import { configFilePath, getConfig } from "./config.js";
10
+ function line(status, msg, fix) {
11
+ process.stdout.write(`[${status}] ${msg}\n`);
12
+ if (fix)
13
+ process.stdout.write(` -> ${fix}\n`);
14
+ }
15
+ async function tgGetMe(token) {
16
+ try {
17
+ const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, { method: "POST" });
18
+ const data = (await res.json());
19
+ if (!data.ok)
20
+ return { ok: false, error: data.description ?? `HTTP ${res.status}` };
21
+ return { ok: true, username: data.result?.username };
22
+ }
23
+ catch (err) {
24
+ return { ok: false, error: String(err) };
25
+ }
26
+ }
27
+ async function workerHealth(url) {
28
+ try {
29
+ const res = await fetch(`${url}/health`);
30
+ return (await res.json());
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** Find the Cursor CLI on PATH (or in ~/.local/bin), returns the version line. */
37
+ function cursorAgentVersion() {
38
+ const candidates = ["cursor-agent", "agent"];
39
+ const localBin = join(homedir(), ".local", "bin");
40
+ if (!(process.env.PATH ?? "").split(delimiter).includes(localBin)) {
41
+ process.env.PATH = `${localBin}${delimiter}${process.env.PATH ?? ""}`;
42
+ }
43
+ for (const cmd of candidates) {
44
+ try {
45
+ const r = spawnSync(cmd, ["--version"], { encoding: "utf8" });
46
+ if (r.status === 0 && r.stdout)
47
+ return r.stdout.trim().split("\n")[0];
48
+ }
49
+ catch {
50
+ // try next candidate
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ function installHint() {
56
+ if (platform() === "win32") {
57
+ return "Install the Cursor CLI (PowerShell): irm 'https://cursor.com/install?win32=true' | iex";
58
+ }
59
+ return "Install the Cursor CLI: curl https://cursor.com/install -fsS | bash (then ensure ~/.local/bin is on PATH)";
60
+ }
61
+ async function main() {
62
+ process.stdout.write("cursor-telegram-mcp doctor\n==========================\n\n");
63
+ const config = getConfig(false);
64
+ const cfgPath = configFilePath();
65
+ if (existsSync(cfgPath))
66
+ line("PASS", `Config file present: ${cfgPath}`);
67
+ else
68
+ line("WARN", `No config file at ${cfgPath}`, "Run `cursor-telegram-mcp setup` (or set env in mcp.json).");
69
+ // Bot token.
70
+ if (!config.botToken) {
71
+ line("FAIL", "TELEGRAM_BOT_TOKEN is not set.", "Run `cursor-telegram-mcp setup`.");
72
+ }
73
+ else {
74
+ const me = await tgGetMe(config.botToken);
75
+ if (me.ok)
76
+ line("PASS", `Bot token valid (@${me.username ?? "?"}).`);
77
+ else
78
+ line("FAIL", `Bot token rejected: ${me.error}`, "Re-check the token from @BotFather; run setup again.");
79
+ }
80
+ // Chat id.
81
+ if (!config.chatId)
82
+ line("FAIL", "TELEGRAM_CHAT_ID is not set.", "Run `cursor-telegram-mcp login` and message your bot.");
83
+ else
84
+ line("PASS", `Chat id set (${config.chatId}).`);
85
+ // Worker.
86
+ const health = await workerHealth(config.workerUrl);
87
+ if (!health) {
88
+ line("WARN", `Worker not reachable at ${config.workerUrl}.`, "It auto-starts with the MCP server; or run `cursor-telegram-mcp worker`.");
89
+ }
90
+ else {
91
+ line("PASS", `Worker reachable at ${config.workerUrl}.`);
92
+ if (health.connected)
93
+ line("PASS", "Worker is connected to Telegram.");
94
+ else
95
+ line("WARN", "Worker is up but not connected to Telegram yet.", "Check the token/chat id; see the worker log.");
96
+ if (health.commandMode)
97
+ line("PASS", "Command mode is ON in the running worker.");
98
+ }
99
+ // Command mode prerequisites.
100
+ if (!config.cursorApiKey) {
101
+ line("WARN", "CURSOR_API_KEY not set (command mode disabled).", "Optional: add it via setup to text tasks to your bot.");
102
+ }
103
+ else {
104
+ line("PASS", "CURSOR_API_KEY is set.");
105
+ const version = cursorAgentVersion();
106
+ if (version)
107
+ line("PASS", `Cursor CLI found (${version}).`);
108
+ else
109
+ line("FAIL", "Cursor CLI (cursor-agent) not found on PATH.", installHint());
110
+ }
111
+ process.stdout.write("\nDone.\n");
112
+ }
113
+ main().catch((err) => {
114
+ process.stdout.write(`Doctor failed: ${err?.message ?? err}\n`);
115
+ process.exit(1);
116
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Convert agent Markdown to plain text suitable for Telegram (no parse_mode).
3
+ */
4
+ export function toPlainTelegram(text) {
5
+ let s = text;
6
+ // Fenced code blocks: drop fences, keep inner content.
7
+ s = s.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trimEnd());
8
+ // Inline code.
9
+ s = s.replace(/`([^`\n]+)`/g, "$1");
10
+ // Links: [label](url) -> label (url)
11
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
12
+ // Bold / strong.
13
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, "$1");
14
+ s = s.replace(/__([^_\n]+)__/g, "$1");
15
+ // Italic (after bold so ** is handled first).
16
+ s = s.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, "$1");
17
+ s = s.replace(/(?<!_)_([^_\n]+)_(?!_)/g, "$1");
18
+ // Strikethrough.
19
+ s = s.replace(/~~([^~\n]+)~~/g, "$1");
20
+ // ATX headings -> uppercase words (no #).
21
+ s = s.replace(/^#{1,6}\s+(.+)$/gm, (_, heading) => heading.trim().toUpperCase());
22
+ // Lone asterisks at line edges (unmatched markdown noise).
23
+ s = s.replace(/^\s*\*{1,2}\s*/gm, "");
24
+ s = s.replace(/\s*\*{1,2}\s*$/gm, "");
25
+ // Orphaned ** left in the middle of a line.
26
+ s = s.replace(/\*\*/g, "");
27
+ return s;
28
+ }