@dikacode/telegram-mcp 1.0.0 → 1.0.2

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
@@ -11,7 +11,6 @@
11
11
 
12
12
  <p align="center">
13
13
  <a href="https://www.npmjs.com/package/@dikacode/telegram-mcp"><img alt="npm" src="https://img.shields.io/npm/v/@dikacode/telegram-mcp?style=flat-square&color=5eead4&label=npm"></a>
14
- <a href="https://www.npmjs.com/package/@dikacode/telegram-mcp"><img alt="downloads" src="https://img.shields.io/npm/dm/@dikacode/telegram-mcp?style=flat-square&color=38bdf8"></a>
15
14
  <a href="https://github.com/dikaglorya/telegram-mcp/actions/workflows/ci.yml"><img alt="ci" src="https://github.com/dikaglorya/telegram-mcp/actions/workflows/ci.yml/badge.svg"></a>
16
15
  <img alt="node" src="https://img.shields.io/badge/node-%E2%89%A5%2020-0ea5e9?style=flat-square">
17
16
  <img alt="tools" src="https://img.shields.io/badge/tools-69-a78bfa?style=flat-square">
@@ -78,6 +77,7 @@ The server speaks MCP over stdio, so the last command is only a manual check. Th
78
77
  - [Architecture](#architecture)
79
78
  - [Requirements](#requirements)
80
79
  - [Install](#install)
80
+ - [Updating](#updating)
81
81
  - [Credentials](#credentials)
82
82
  - [First login — from chat](#first-login--from-chat)
83
83
  - [Connect your agent](#connect-your-agent)
@@ -150,7 +150,7 @@ telegram-mcp
150
150
  }
151
151
  ```
152
152
 
153
- Pin a version if you want reproducible installs: `npx -y @dikacode/telegram-mcp@1.0.0`.
153
+ Pin a version if you want reproducible installs: `npx -y @dikacode/telegram-mcp@1.0.2`.
154
154
 
155
155
  ### 2. Clone and link
156
156
 
@@ -170,7 +170,7 @@ npm link # optional: makes `telegram-mcp` available on PATH
170
170
  Download the tarball attached to the latest release and install it globally:
171
171
 
172
172
  ```bash
173
- npm install -g https://github.com/dikaglorya/telegram-mcp/releases/download/v1.0.0/dikacode-telegram-mcp-1.0.0.tgz
173
+ npm install -g https://github.com/dikaglorya/telegram-mcp/releases/download/v1.0.2/dikacode-telegram-mcp-1.0.2.tgz
174
174
  telegram-mcp
175
175
  ```
176
176
 
@@ -186,6 +186,59 @@ npx -y github:dikaglorya/telegram-mcp
186
186
 
187
187
  npm fetches the repository, installs dependencies, runs the `prepare` build, and starts the launcher.
188
188
 
189
+ ## Updating
190
+
191
+ One command, whichever way it was installed:
192
+
193
+ ```bash
194
+ telegram-mcp update
195
+ ```
196
+
197
+ It works out how *this* copy got here and runs the right thing — `npm install
198
+ -g @latest` for a global install (reusing a custom `--prefix`), `git pull
199
+ --ff-only` plus a rebuild for a checkout, and a plain restart instruction for an
200
+ npx run, where nothing on disk needs changing. `--check` reports and installs
201
+ nothing.
202
+
203
+ ```text
204
+ telegram-mcp 1.0.2 · update
205
+
206
+ installed 1.0.1 /home/you/.local/lib/node_modules/@dikacode/telegram-mcp
207
+ latest 1.0.2
208
+ install global npm package
209
+
210
+ ▲ 1.0.1 -> 1.0.2 available
211
+ ```
212
+
213
+ | Installed with | Behaviour |
214
+ | --- | --- |
215
+ | `npx -y @dikacode/telegram-mcp` (unpinned) | **Automatic.** npm resolves `latest` on every start, so the next client restart picks up a new release. |
216
+ | `npx -y @dikacode/telegram-mcp@1.0.2` (pinned) | **Never.** That is the point of pinning. |
217
+ | `npm install -g @dikacode/telegram-mcp` | **`telegram-mcp update`**, or `npm update -g @dikacode/telegram-mcp`. |
218
+ | Release tarball | **Manual.** Reinstall from the newer tarball. |
219
+ | `npx -y github:dikaglorya/telegram-mcp` | **Not automatic.** npm caches the fetched repo; clear the cache or pin a ref (`#main`) to move forward. |
220
+
221
+ Check what you are running:
222
+
223
+ ```bash
224
+ telegram-mcp --version
225
+ telegram-mcp update --check # newer release available? installs nothing
226
+ npm outdated -g @dikacode/telegram-mcp # global installs only
227
+ ```
228
+
229
+ The server also tells you. When a newer release exists it prints one line to
230
+ stderr at startup, after the MCP transport is already serving:
231
+
232
+ ```
233
+ [telegram-mcp] update available: 1.0.1 -> 1.0.2. Run `npm install -g @dikacode/telegram-mcp@latest`, or set TELEGRAM_UPDATE_CHECK=0 to silence this.
234
+ ```
235
+
236
+ That check is a single request to the npm registry with an 800 ms timeout,
237
+ cached for 24 hours, and it never blocks startup or writes to stdout. If you
238
+ want zero outbound traffic that is not Telegram itself, set
239
+ `TELEGRAM_UPDATE_CHECK=0`. `telegram_doctor` shows the version it knows about
240
+ without touching the network.
241
+
189
242
  ## Credentials
190
243
 
191
244
  1. Open [my.telegram.org/apps](https://my.telegram.org/apps) and log in with your phone number.
@@ -477,6 +530,8 @@ All settings are environment variables. The server also loads a `.env` next to t
477
530
  | `TELEGRAM_EXPORT_DIR` | `~/.telegram-mcp/exports` | History exports. |
478
531
  | `TELEGRAM_AUDIT_LOG` | `~/.telegram-mcp/audit.log` | Call log. |
479
532
  | `TELEGRAM_MAX_DOWNLOAD_BYTES` | `0` (unlimited) | Hard cap for one download. A non-numeric value is refused rather than treated as unlimited. |
533
+ | `TELEGRAM_UPDATE_CHECK` | `true` | Set to `0` to stop the startup check for a newer npm release. |
534
+ | `TELEGRAM_UPDATE_CHECK_TIMEOUT_MS` | `800` | Timeout for that check, capped at 10 s. |
480
535
  | `TELEGRAM_MCP_NODE` | current Node | Launcher override, for exotic Node installs. |
481
536
 
482
537
  `~` and `~\` are both expanded. Relative paths resolve against the current working directory.
@@ -494,6 +549,9 @@ All settings are environment variables. The server also loads a `.env` next to t
494
549
  | `Cannot find module .../dist/index.js` | The package was installed without building. Run `npm run build`, or use the release tarball. |
495
550
  | Login works, then the next run asks again | `TELEGRAM_SESSION_PATH` changed between runs. Keep it stable, or leave it unset. |
496
551
  | Window/terminal closes when started by hand | Expected: the server speaks MCP on stdio. Start it through a client, or use `npm run login` for the standalone CLI. |
552
+ | `update available: …` on every start | A newer release is on npm. Run `telegram-mcp update`, or set `TELEGRAM_UPDATE_CHECK=0` if you pin versions on purpose. |
553
+ | `telegram-mcp update` finds no upgrade command | The copy lives in a `node_modules` tree npm does not manage as a global prefix (npx cache, or a project dependency). Restart it as `npx -y @dikacode/telegram-mcp@latest`, or upgrade the project dependency. |
554
+ | `telegram-mcp --version` prints nothing | You are on `1.0.0`, which predates the flag. Upgrade to `1.0.1` or newer. |
497
555
 
498
556
  ## Development
499
557
 
@@ -503,7 +561,7 @@ The repository ships with a unit suite and a real MCP harness that spawns the bu
503
561
  npm install
504
562
  npm run build
505
563
 
506
- npm run test:unit # 64 offline checks
564
+ npm run test:unit # 72 offline checks
507
565
  npm run test:mcp # read-only end-to-end pass
508
566
  npm run test:mcp:write # + self-cleaning writes
509
567
  npm run test:destructive # + disposable chats (opt-in, flood-limited)
@@ -529,7 +587,7 @@ The harness also supports `--json`, `--only=tool_a,tool_b`, `--cleanup`, `--recl
529
587
  ```
530
588
  telegram-mcp/
531
589
  ├── bin/telegram-mcp.js portable launcher (finds Node, then dist/index.js)
532
- ├── cli/ standalone login and doctor commands
590
+ ├── cli/ standalone login, doctor and update commands
533
591
  ├── assets/ animated README art (SVG, no scripts)
534
592
  ├── harness/ unit checks + end-to-end MCP harness
535
593
  ├── index.ts stdio entrypoint
@@ -556,7 +614,8 @@ The TypeScript is built with `tsc` to `dist/`. There is no bundler; the publishe
556
614
 
557
615
  ## Release
558
616
 
559
- **v1.0.0** is the first public release.
617
+ **v1.0.2** is the current release, published to npm as
618
+ [`@dikacode/telegram-mcp`](https://www.npmjs.com/package/@dikacode/telegram-mcp).
560
619
 
561
620
  - 69 MCP tools, all registered with strict input schemas.
562
621
  - In-chat login, including two-step verification.
@@ -564,6 +623,9 @@ The TypeScript is built with `tsc` to `dist/`. There is no bundler; the publishe
564
623
  - Read-back verification for state-changing tools.
565
624
  - Optional allowlist, audited calls, redacted secrets.
566
625
  - Cross-platform launcher; no hardcoded paths.
626
+ - `--version` / `--help`, and a startup notice when a newer release exists.
627
+ - `telegram-mcp update` upgrades whichever layout is installed; `--check`
628
+ reports without touching anything.
567
629
  - End-to-end harness with paging, policy probes and artifact cleanup.
568
630
 
569
631
  See [CHANGELOG.md](https://github.com/dikaglorya/telegram-mcp/blob/main/CHANGELOG.md) for the full release notes. Release artifacts are on the [releases page](https://github.com/dikaglorya/telegram-mcp/releases).
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Doctor: validate config, session presence, permissions, live connectivity.
4
- * Prints pass/fail. Never prints secrets.
4
+ * Never prints secrets.
5
+ *
6
+ * Output is a framed report on stdout when run as a CLI — this is a human
7
+ * command, not an MCP server, so stdout is free. Under `--quiet` (or when
8
+ * piped) it prints one line per check with no colour, which is what CI wants.
5
9
  */
6
10
  import "../env.js";
7
11
  import fs from "node:fs";
@@ -9,8 +13,14 @@ import path from "node:path";
9
13
  import { ensureDirs, getConfig, hasSession, readSessionString, redact } from "../config.js";
10
14
  import { lockFile } from "../perms.js";
11
15
  import { getClient, disconnect } from "../telegram.js";
12
- function line(okFlag, label, detail = "") {
13
- process.stdout.write(`[${okFlag ? "PASS" : "FAIL"}] ${label}${detail ? ` — ${detail}` : ""}\n`);
16
+ import { banner, colorEnabled, gap, paint, sym, theme } from "../ui.js";
17
+ const out = (text) => void process.stdout.write(text);
18
+ const rows = [];
19
+ function record(pass, label, detail = "") {
20
+ rows.push({ pass, label, detail });
21
+ }
22
+ function icon(pass) {
23
+ return pass ? theme.success(sym.ok) : theme.fail(sym.fail);
14
24
  }
15
25
  async function main() {
16
26
  let failures = 0;
@@ -19,49 +29,55 @@ async function main() {
19
29
  config = getConfig();
20
30
  }
21
31
  catch (err) {
22
- line(false, "config", err instanceof Error ? err.message : String(err));
32
+ record(false, "config", err instanceof Error ? err.message : String(err));
33
+ report();
23
34
  process.exit(1);
24
35
  }
25
36
  ensureDirs(config);
26
- line(true, "config", `api_id=${config.apiId}, allowAll=${config.allowAllChats}, allowlist=${config.allowlist.length}`);
27
- line(fs.existsSync(path.dirname(config.sessionPath)), "state dir", path.dirname(config.sessionPath));
37
+ record(true, "config", `api_id=${config.apiId}, allowAll=${config.allowAllChats}, allowlist=${config.allowlist.length}`);
38
+ record(fs.existsSync(path.dirname(config.sessionPath)), "state dir", path.dirname(config.sessionPath));
28
39
  const has = hasSession(config);
29
- line(has, "session present", config.sessionPath);
40
+ record(has, "session present", config.sessionPath);
30
41
  if (!has)
31
42
  failures++;
32
43
  if (has) {
33
44
  if (process.platform === "win32") {
34
- line(true, "session permissions", "skipped on Windows (inherits user profile ACLs)");
45
+ record(true, "session permissions", "skipped on Windows (inherits user profile ACLs)");
35
46
  }
36
47
  else {
37
48
  try {
38
49
  const mode = fs.statSync(config.sessionPath).mode & 0o777;
39
50
  const good = (mode & 0o077) === 0;
40
- line(good, "session permissions", `mode=${mode.toString(8)}`);
41
- if (!good) {
51
+ if (good) {
52
+ record(true, "session permissions", `mode=${mode.toString(8)}`);
53
+ }
54
+ else {
42
55
  lockFile(config.sessionPath);
43
- line(true, "session permissions", "repaired to 600");
56
+ const fixed = (fs.statSync(config.sessionPath).mode & 0o777 & 0o077) === 0;
57
+ record(fixed, "session permissions", fixed ? "repaired to 600" : `mode=${mode.toString(8)}`);
58
+ if (!fixed)
59
+ failures++;
44
60
  }
45
61
  }
46
62
  catch (err) {
47
- line(false, "session permissions", err instanceof Error ? err.message : String(err));
63
+ record(false, "session permissions", err instanceof Error ? err.message : String(err));
48
64
  failures++;
49
65
  }
50
66
  }
51
67
  try {
52
- line(readSessionString(config).length > 0, "session readable");
68
+ record(readSessionString(config).length > 0, "session readable");
53
69
  }
54
70
  catch (err) {
55
- line(false, "session readable", err instanceof Error ? err.message : String(err));
71
+ record(false, "session readable", err instanceof Error ? err.message : String(err));
56
72
  failures++;
57
73
  }
58
74
  try {
59
75
  const client = await getClient();
60
76
  const authorized = await client.isUserAuthorized();
61
- line(authorized, "telegram authorized");
77
+ record(authorized, "telegram authorized");
62
78
  if (authorized) {
63
79
  const me = (await client.getMe());
64
- line(true, "account", `id=${String(me.id ?? "")} username=${me.username ?? "none"}`);
80
+ record(true, "account", `id=${String(me.id ?? "")} username=${me.username ?? "none"}`);
65
81
  }
66
82
  else {
67
83
  failures++;
@@ -69,14 +85,48 @@ async function main() {
69
85
  await disconnect();
70
86
  }
71
87
  catch (err) {
72
- line(false, "telegram connectivity", redact(err instanceof Error ? err.message : String(err)));
88
+ record(false, "telegram connectivity", redact(err instanceof Error ? err.message : String(err)));
73
89
  failures++;
74
90
  }
75
91
  }
76
- process.stdout.write(failures === 0 ? "\nall checks passed.\n" : `\n${failures} check(s) failed.\n`);
92
+ report(failures);
77
93
  process.exit(failures === 0 ? 0 : 1);
78
94
  }
95
+ function report(failures) {
96
+ const plain = !colorEnabled() || process.argv.includes("--quiet");
97
+ if (!plain) {
98
+ banner({ subtitle: "health check", compact: true });
99
+ gap();
100
+ }
101
+ const width = Math.max(...rows.map((r) => r.label.length));
102
+ for (const row of rows) {
103
+ if (plain) {
104
+ out(`[${row.pass ? "PASS" : "FAIL"}] ${row.label}${row.detail ? ` - ${row.detail}` : ""}\n`);
105
+ }
106
+ else {
107
+ const detail = row.detail ? ` ${theme.muted(row.detail)}` : "";
108
+ // padEnd measures code units; the icon is one glyph, so this aligns.
109
+ out(` ${icon(row.pass)} ${theme.label(row.label.padEnd(width))}${detail}\n`);
110
+ }
111
+ }
112
+ if (failures === undefined)
113
+ return;
114
+ if (plain) {
115
+ out(failures === 0 ? "\nall checks passed.\n" : `\n${failures} check(s) failed.\n`);
116
+ return;
117
+ }
118
+ gap();
119
+ out(failures === 0
120
+ ? ` ${theme.success(`${sym.ok} ${paint.bold("all checks passed")}`)}\n`
121
+ : ` ${theme.fail(`${sym.fail} ${failures} check(s) failed`)}\n`);
122
+ if (failures !== 0 && hasHint()) {
123
+ out(` ${theme.muted(`${sym.bullet} run ${paint.bold("telegram-mcp login")} to issue a fresh session`)}\n`);
124
+ }
125
+ }
126
+ function hasHint() {
127
+ return rows.some((r) => !r.pass && /session|authorized/i.test(r.label));
128
+ }
79
129
  main().catch((err) => {
80
- process.stderr.write(`[doctor] fatal: ${redact(err instanceof Error ? err.message : String(err))}\n`);
130
+ process.stderr.write(`${theme.fail(`[doctor] fatal: ${redact(err instanceof Error ? err.message : String(err))}`)}\n`);
81
131
  process.exit(1);
82
132
  });
package/dist/cli/login.js CHANGED
@@ -15,6 +15,7 @@ import { TelegramClient } from "telegram";
15
15
  import { StringSession } from "telegram/sessions/index.js";
16
16
  import { ensureDirs, getConfig, hasSession, readSessionString, writeSessionString, redact, } from "../config.js";
17
17
  import { createLogger } from "../logger.js";
18
+ import { banner, gap, hint, nextSteps, paint, spinner, step, success, sym, theme, warn } from "../ui.js";
18
19
  function ask(question) {
19
20
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
20
21
  return new Promise((resolve) => rl.question(question, (answer) => {
@@ -22,6 +23,9 @@ function ask(question) {
22
23
  resolve(answer.trim());
23
24
  }));
24
25
  }
26
+ function askPlain(question) {
27
+ return ask(`${theme.accent(`${sym.arrow} `)}${theme.label(question)} `);
28
+ }
25
29
  /**
26
30
  * Ask a question without echoing the answer. readline's `terminal` mode masks
27
31
  * the prompt on POSIX and Windows alike; it falls back to a plain read when
@@ -57,6 +61,9 @@ function askSecret(question) {
57
61
  async function main() {
58
62
  const config = getConfig();
59
63
  ensureDirs(config);
64
+ banner({ subtitle: "interactive login", compact: true });
65
+ gap();
66
+ step(1, 3, "Checking the existing session");
60
67
  // Re-running login silently mints a NEW auth key for the same account. Telegram
61
68
  // then treats the previous key as AUTH_KEY_DUPLICATED and every client holding
62
69
  // it stops working. Refuse to overwrite a session that still works.
@@ -65,25 +72,35 @@ async function main() {
65
72
  connectionRetries: 2,
66
73
  baseLogger: createLogger(),
67
74
  });
75
+ const check = spinner("verifying the saved session");
68
76
  try {
69
77
  await probe.connect();
70
78
  const ok = await probe.isUserAuthorized();
71
79
  if (ok) {
72
80
  const me = (await probe.getMe());
73
- process.stderr.write(`[login] already authorized as ${me.username ?? String(me.id ?? "")} — nothing to do.\n` +
74
- `[login] session: ${config.sessionPath}\n` +
75
- `[login] pass --force to overwrite it with a fresh auth key.\n`);
81
+ check.stop(`${theme.success(sym.ok)} already authorized as ${theme.label(String(me.username ?? me.id ?? ""))}`);
82
+ gap();
83
+ hint(`session kept at ${config.sessionPath}`);
84
+ hint(`pass ${theme.label("--force")} to overwrite it with a fresh auth key`);
76
85
  await probe.disconnect().catch(() => undefined);
77
86
  return;
78
87
  }
88
+ check.stop(`${theme.warn(`${sym.warn} saved session is no longer authorized`)}`);
79
89
  }
80
90
  catch {
91
+ check.stop(`${theme.warn(`${sym.warn} saved session is unusable — issuing a fresh one`)}`);
81
92
  /* dead or duplicated session — fall through and replace it */
82
93
  }
83
94
  await probe.disconnect().catch(() => undefined);
84
- process.stderr.write(`[login] existing session is unusable, issuing a fresh one\n`);
85
95
  }
86
- process.stderr.write(`[login] session will be stored at ${config.sessionPath}\n`);
96
+ else {
97
+ hint(hasSession(config) ? `overwriting the session because of ${theme.label("--force")}` : "no session on disk yet");
98
+ }
99
+ gap();
100
+ step(2, 3, "Signing in to Telegram");
101
+ hint(`the session will be stored at ${config.sessionPath}`);
102
+ if (!process.stdin.isTTY)
103
+ hint(`stdin is not a terminal — prompts may not echo correctly`);
87
104
  const existing = hasSession(config) ? readSessionString(config) : "";
88
105
  const session = new StringSession(existing);
89
106
  const client = new TelegramClient(session, config.apiId, config.apiHash, {
@@ -97,28 +114,40 @@ async function main() {
97
114
  phoneNumber: async () => {
98
115
  const stored = process.env.TELEGRAM_PHONE?.trim();
99
116
  if (stored) {
100
- process.stderr.write(`[login] using phone from .env\n`);
117
+ hint(`using ${theme.label("TELEGRAM_PHONE")} from .env`);
101
118
  return stored;
102
119
  }
103
- return ask("Phone number (international, e.g. +15551234567): ");
120
+ return askPlain("Phone number (international, e.g. +15551234567)");
121
+ },
122
+ phoneCode: async () => {
123
+ hint("Telegram is sending a code to your other devices");
124
+ return askPlain("Login code");
125
+ },
126
+ password: async () => {
127
+ hint("two-step verification is on for this account");
128
+ return askSecret("2FA password (leave blank if none)");
104
129
  },
105
- phoneCode: async () => ask("Login code from Telegram: "),
106
- password: async () => askSecret("2FA password (leave blank if none)"),
107
130
  onError: (err) => {
108
131
  // The update loop keeps pinging after we disconnect; those late
109
132
  // rejections are noise, not auth failures.
110
133
  const msg = err instanceof Error ? err.message : String(err);
111
134
  if (msg === "TIMEOUT" || msg.includes("TIMEOUT"))
112
135
  return;
113
- process.stderr.write(`[login] auth error: ${redact(msg)}\n`);
136
+ warn(`auth error: ${redact(msg)}`);
114
137
  },
115
138
  });
116
139
  const me = (await client.getMe());
117
140
  const serialized = client.session.save();
118
141
  writeSessionString(config, serialized);
119
- process.stderr.write(`[login] authorized as id=${String(me.id ?? "")} username=${me.username ?? "none"}\n`);
120
- process.stderr.write(`[login] session saved to ${config.sessionPath}\n`);
121
- process.stderr.write("[login] you can close this and start the MCP server.\n");
142
+ gap();
143
+ step(3, 3, "Saving");
144
+ success(`authorized as ${paint.bold(String(me.username ?? me.id ?? ""))}`);
145
+ hint(`session saved to ${config.sessionPath} ${theme.muted("(0600)")}`);
146
+ nextSteps([
147
+ `start your MCP client — it spawns ${theme.label("telegram-mcp")} from the config you already added`,
148
+ `or run ${theme.label("telegram-mcp doctor")} to confirm everything is healthy`,
149
+ ]);
150
+ gap();
122
151
  }
123
152
  finally {
124
153
  // gramJS fires the update-ping loop the moment we connect. Disconnecting
@@ -136,6 +165,9 @@ async function main() {
136
165
  }
137
166
  main().catch((err) => {
138
167
  const msg = err instanceof Error ? err.message : String(err);
139
- process.stderr.write(`[login] failed: ${redact(msg)}\n`);
168
+ process.stderr.write(`${theme.fail(`${sym.fail} login failed: ${redact(msg)}`)}\n`);
169
+ if (/not set|invalid/i.test(msg)) {
170
+ process.stderr.write(` ${theme.muted(`${sym.bullet} get api_id/api_hash from `)}${theme.underline("https://my.telegram.org/apps")}\n`);
171
+ }
140
172
  process.exit(1);
141
173
  });