@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 +68 -6
- package/dist/cli/doctor.js +69 -19
- package/dist/cli/login.js +46 -14
- package/dist/cli/update.js +293 -0
- package/dist/config.js +1 -2
- package/dist/index.js +111 -10
- package/dist/setup-tools.js +23 -3
- package/dist/ui.js +237 -0
- package/dist/update-check.js +151 -0
- package/dist/version.js +76 -0
- package/package.json +2 -1
package/dist/ui.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal presentation: colour, symbols, a banner and small animations.
|
|
3
|
+
*
|
|
4
|
+
* Rules this module enforces, in order of importance:
|
|
5
|
+
*
|
|
6
|
+
* 1. **stdout is never touched.** Only stderr goes through here. stdout is the
|
|
7
|
+
* MCP JSON-RPC channel, and a single ANSI escape sequence written there
|
|
8
|
+
* corrupts the stream and gets the server dropped by the client.
|
|
9
|
+
* 2. **Degrade silently.** Colour needs a TTY, a terminal that is not dumb,
|
|
10
|
+
* and no NO_COLOR. Piped output, CI logs and `npx ... > file` get plain
|
|
11
|
+
* text with no escape codes at all.
|
|
12
|
+
* 3. **Animation is optional and cannot hang.** Every animation has a hard
|
|
13
|
+
* time cap and is skipped when `TERM=dumb`, when stderr is not a TTY, when
|
|
14
|
+
* the terminal is narrow, or when TELEGRAM_NO_ANIMATION is set.
|
|
15
|
+
*
|
|
16
|
+
* The exported surface is small on purpose: the rest of the codebase should not
|
|
17
|
+
* be threading `isTTY` checks through every message.
|
|
18
|
+
*/
|
|
19
|
+
import { SERVER_NAME, SERVER_VERSION } from "./version.js";
|
|
20
|
+
/* ------------------------------------------------------------------ */
|
|
21
|
+
/* Capability detection */
|
|
22
|
+
/* ------------------------------------------------------------------ */
|
|
23
|
+
const isWindows = process.platform === "win32";
|
|
24
|
+
function stderrIsTty() {
|
|
25
|
+
return Boolean(process.stderr.isTTY);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Colour support. `NO_COLOR` is the de-facto standard opt-out and takes
|
|
29
|
+
* priority over everything (https://no-color.org). `FORCE_COLOR` exists for the
|
|
30
|
+
* case where output is piped into a colour-aware pager.
|
|
31
|
+
*/
|
|
32
|
+
export function colorEnabled() {
|
|
33
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "")
|
|
34
|
+
return false;
|
|
35
|
+
const forced = process.env.FORCE_COLOR;
|
|
36
|
+
if (forced !== undefined && forced !== "" && forced !== "0")
|
|
37
|
+
return true;
|
|
38
|
+
if (forced === "0")
|
|
39
|
+
return false;
|
|
40
|
+
if (!stderrIsTty())
|
|
41
|
+
return false;
|
|
42
|
+
if (process.env.TERM === "dumb")
|
|
43
|
+
return false;
|
|
44
|
+
// Windows Terminal and VS Code advertise themselves; plain conhost does not
|
|
45
|
+
// reliably handle SGR, so it stays plain unless FORCE_COLOR says otherwise.
|
|
46
|
+
if (isWindows && !process.env.WT_SESSION && !process.env.TERM_PROGRAM)
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/** Whether the fancy stuff (banner frame, spinner) should run at all. */
|
|
51
|
+
export function animationEnabled() {
|
|
52
|
+
if (process.env.TELEGRAM_NO_ANIMATION && process.env.TELEGRAM_NO_ANIMATION !== "0")
|
|
53
|
+
return false;
|
|
54
|
+
if (!stderrIsTty())
|
|
55
|
+
return false;
|
|
56
|
+
if (process.env.TERM === "dumb")
|
|
57
|
+
return false;
|
|
58
|
+
const columns = process.stderr.columns ?? 0;
|
|
59
|
+
if (columns > 0 && columns < 60)
|
|
60
|
+
return false;
|
|
61
|
+
return colorEnabled();
|
|
62
|
+
}
|
|
63
|
+
function sgr(open, close = 0) {
|
|
64
|
+
return (text) => (colorEnabled() ? `\u001b[${open}m${text}\u001b[${close}m` : text);
|
|
65
|
+
}
|
|
66
|
+
/** 256-colour foreground, falling back to cyan when colour is off. */
|
|
67
|
+
function c256(code, fallback) {
|
|
68
|
+
return (text) => colorEnabled() ? `\u001b[38;5;${code}m${text}\u001b[0m` : `\u001b[${fallback}m${text}\u001b[0m`;
|
|
69
|
+
}
|
|
70
|
+
const basic = !process.env.TELEGRAM_256_COLORS;
|
|
71
|
+
/**
|
|
72
|
+
* Low-level painters, for the rare call that needs one word emphasised inside a
|
|
73
|
+
* sentence. Prefer `theme` in new code; these are aliases kept small on purpose.
|
|
74
|
+
*/
|
|
75
|
+
export const paint = {
|
|
76
|
+
bold: sgr(1, 22),
|
|
77
|
+
dim: sgr(2, 22),
|
|
78
|
+
underline: sgr(4, 24),
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The palette: cool cyan/teal for structure, warm accents for emphasis.
|
|
82
|
+
* Names are semantic (`success`, `fail`) so call sites read as intent.
|
|
83
|
+
*/
|
|
84
|
+
export const theme = {
|
|
85
|
+
bold: paint.bold,
|
|
86
|
+
dim: paint.dim,
|
|
87
|
+
underline: paint.underline,
|
|
88
|
+
border: basic ? sgr(90) : c256(244, 90),
|
|
89
|
+
title: basic ? sgr(96) : c256(51, 96),
|
|
90
|
+
label: basic ? sgr(97) : c256(252, 97),
|
|
91
|
+
accent: basic ? sgr(96) : c256(45, 96),
|
|
92
|
+
success: basic ? sgr(92) : c256(48, 92),
|
|
93
|
+
fail: basic ? sgr(91) : c256(203, 91),
|
|
94
|
+
warn: basic ? sgr(93) : c256(214, 93),
|
|
95
|
+
muted: basic ? sgr(90) : c256(242, 90),
|
|
96
|
+
};
|
|
97
|
+
/* ------------------------------------------------------------------ */
|
|
98
|
+
/* Symbols */
|
|
99
|
+
/* ------------------------------------------------------------------ */
|
|
100
|
+
// Unicode box drawing and symbols are fine on every modern terminal, but
|
|
101
|
+
// Windows conhost with a legacy code page mangles them, so ASCII is used there.
|
|
102
|
+
const unicodeSymbols = !isWindows || Boolean(process.env.WT_SESSION);
|
|
103
|
+
export const sym = {
|
|
104
|
+
ok: unicodeSymbols ? "✔" : "v",
|
|
105
|
+
fail: unicodeSymbols ? "✖" : "x",
|
|
106
|
+
warn: unicodeSymbols ? "▲" : "!",
|
|
107
|
+
arrow: unicodeSymbols ? "→" : "->",
|
|
108
|
+
bullet: unicodeSymbols ? "·" : "-",
|
|
109
|
+
bar: unicodeSymbols ? "│" : "|",
|
|
110
|
+
tl: unicodeSymbols ? "╭" : "+",
|
|
111
|
+
tr: unicodeSymbols ? "╮" : "+",
|
|
112
|
+
bl: unicodeSymbols ? "╰" : "+",
|
|
113
|
+
br: unicodeSymbols ? "╯" : "+",
|
|
114
|
+
h: unicodeSymbols ? "─" : "-",
|
|
115
|
+
};
|
|
116
|
+
/* ------------------------------------------------------------------ */
|
|
117
|
+
/* Message helpers — stderr only */
|
|
118
|
+
/* ------------------------------------------------------------------ */
|
|
119
|
+
function write(text) {
|
|
120
|
+
process.stderr.write(text);
|
|
121
|
+
}
|
|
122
|
+
/** The canonical diagnostic line. `[telegram-mcp] ...` stays greppable. */
|
|
123
|
+
export function info(message) {
|
|
124
|
+
write(`${theme.title(`[${SERVER_NAME}]`)} ${message}\n`);
|
|
125
|
+
}
|
|
126
|
+
export function muted(message) {
|
|
127
|
+
write(`${theme.muted(`[${SERVER_NAME}]`)} ${message}\n`);
|
|
128
|
+
}
|
|
129
|
+
export function success(message) {
|
|
130
|
+
write(`${theme.success(sym.ok)} ${message}\n`);
|
|
131
|
+
}
|
|
132
|
+
export function warn(message) {
|
|
133
|
+
write(`${theme.warn(`${sym.warn} ${message}`)}\n`);
|
|
134
|
+
}
|
|
135
|
+
export function error(message) {
|
|
136
|
+
write(`${theme.fail(`${sym.fail} ${message}`)}\n`);
|
|
137
|
+
}
|
|
138
|
+
export function hint(message) {
|
|
139
|
+
write(` ${theme.muted(`${sym.bullet} ${message}`)}\n`);
|
|
140
|
+
}
|
|
141
|
+
/** A blank line, but only when the terminal can afford it. */
|
|
142
|
+
export function gap() {
|
|
143
|
+
if (animationEnabled())
|
|
144
|
+
write("\n");
|
|
145
|
+
}
|
|
146
|
+
/* ------------------------------------------------------------------ */
|
|
147
|
+
/* Banner */
|
|
148
|
+
/* ------------------------------------------------------------------ */
|
|
149
|
+
/** Two-row block wordmark: readable at small widths, unlike a huge figlet. */
|
|
150
|
+
const WORDMARK = [
|
|
151
|
+
"▀█▀ █▀▀ █░░ █▀▀ █▀▀ █▀█ ▄▀█ █▀▄▀█",
|
|
152
|
+
"░█░ █▀▀ █░░ ██▄ █▄█ █▀▄ █▀█ █░▀░█",
|
|
153
|
+
];
|
|
154
|
+
/**
|
|
155
|
+
* Print the banner. Deliberately quiet on repeated runs: the full frame is for
|
|
156
|
+
* CLI commands (login, doctor, --help), while a server start uses the compact
|
|
157
|
+
* form so an agent's log does not fill up with art.
|
|
158
|
+
*/
|
|
159
|
+
export function banner(opts = {}) {
|
|
160
|
+
const version = opts.version ?? SERVER_VERSION;
|
|
161
|
+
const subtitle = opts.subtitle ?? "your Telegram account, as an MCP server";
|
|
162
|
+
if (opts.compact || !animationEnabled()) {
|
|
163
|
+
write(`${theme.title(SERVER_NAME)} ${theme.muted(version)} ${theme.muted(sym.bullet)} ${theme.label(subtitle)}\n`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const width = Math.max(...WORDMARK.map((row) => [...row].length)) + 4;
|
|
167
|
+
write(`${theme.border(`${sym.tl}${sym.h.repeat(width)}${sym.tr}`)}\n`);
|
|
168
|
+
for (const row of WORDMARK)
|
|
169
|
+
write(`${theme.border(sym.bar)} ${theme.title(row)}\n`);
|
|
170
|
+
write(`${theme.border(sym.bar)} ${theme.muted(version)} ${theme.muted(sym.bullet)} ${theme.label(subtitle)}\n`);
|
|
171
|
+
write(`${theme.border(`${sym.bl}${sym.h.repeat(width)}${sym.br}`)}\n`);
|
|
172
|
+
}
|
|
173
|
+
/* ------------------------------------------------------------------ */
|
|
174
|
+
/* Spinner */
|
|
175
|
+
/* ------------------------------------------------------------------ */
|
|
176
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
177
|
+
/** Redraw the current line in place. Only valid when animation is on. */
|
|
178
|
+
function redraw(text) {
|
|
179
|
+
write(`\u001b[2K\r${text}`);
|
|
180
|
+
}
|
|
181
|
+
function clearLine() {
|
|
182
|
+
write("\u001b[2K\r");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A one-line spinner with a hard time cap, so a stuck network call cannot leave
|
|
186
|
+
* something spinning in a log forever. `stop()` erases the line and optionally
|
|
187
|
+
* replaces it with a final message.
|
|
188
|
+
*/
|
|
189
|
+
export function spinner(text, opts = {}) {
|
|
190
|
+
const maxMs = opts.maxMs ?? 6000;
|
|
191
|
+
if (!animationEnabled()) {
|
|
192
|
+
write(`${theme.muted(`${sym.bullet} ${text}`)}\n`);
|
|
193
|
+
return { stop: () => undefined };
|
|
194
|
+
}
|
|
195
|
+
let frame = 0;
|
|
196
|
+
const started = Date.now();
|
|
197
|
+
let stopped = false;
|
|
198
|
+
const stop = (final) => {
|
|
199
|
+
if (stopped)
|
|
200
|
+
return;
|
|
201
|
+
stopped = true;
|
|
202
|
+
clearInterval(timer);
|
|
203
|
+
clearLine();
|
|
204
|
+
if (final)
|
|
205
|
+
write(`${final}\n`);
|
|
206
|
+
};
|
|
207
|
+
const draw = () => redraw(`${theme.accent(FRAMES[frame++ % FRAMES.length])} ${text}`);
|
|
208
|
+
draw();
|
|
209
|
+
const timer = setInterval(() => {
|
|
210
|
+
if (Date.now() - started > maxMs) {
|
|
211
|
+
// Give up quietly: the caller still prints its own outcome line.
|
|
212
|
+
stop();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
draw();
|
|
216
|
+
}, 90);
|
|
217
|
+
// Never let the animation be the reason the process stays alive.
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
return { stop };
|
|
220
|
+
}
|
|
221
|
+
/** A numbered step header, e.g. `[1/3] Connecting to Telegram`. */
|
|
222
|
+
export function step(index, total, text) {
|
|
223
|
+
write(`${theme.accent(`[${theme.muted(`${index}/${total}`)}]`)} ${theme.bold(text)}\n`);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The three-step onboarding footer shown once credentials are written. It is
|
|
227
|
+
* the answer to "ok, and now what?" — the most common question after setup.
|
|
228
|
+
*/
|
|
229
|
+
export function nextSteps(steps) {
|
|
230
|
+
if (!steps.length)
|
|
231
|
+
return;
|
|
232
|
+
gap();
|
|
233
|
+
write(`${theme.label(theme.bold("Next"))}\n`);
|
|
234
|
+
steps.forEach((text, i) => {
|
|
235
|
+
write(` ${theme.accent(String(i + 1))}${theme.muted(".")} ${text}\n`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "A newer version exists" notice.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists: the package is distributed through npx, where the version is
|
|
5
|
+
* resolved on every run, and through `npm i -g`, where it is not. A user who
|
|
6
|
+
* installed globally has no way to learn that a fix shipped unless they think
|
|
7
|
+
* to run `npm outdated -g`. This module prints one line to stderr when the
|
|
8
|
+
* registry has something newer.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints, all deliberate:
|
|
11
|
+
* - stderr only. stdout belongs to the JSON-RPC transport; a stray line there
|
|
12
|
+
* would make the agent drop the server.
|
|
13
|
+
* - never fatal, never blocking. The check runs after the server is already
|
|
14
|
+
* serving; a timeout, an offline machine or a broken proxy just means no
|
|
15
|
+
* notice.
|
|
16
|
+
* - cached for a day, so restarting the client five times does not mean five
|
|
17
|
+
* registry requests.
|
|
18
|
+
* - opt out with TELEGRAM_UPDATE_CHECK=0. This is a local-first tool and a
|
|
19
|
+
* user is entitled to zero outbound traffic that is not Telegram itself.
|
|
20
|
+
*/
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { PACKAGE_NAME, SERVER_VERSION, VERSION_IS_KNOWN, compareVersions } from "./version.js";
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 800;
|
|
26
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
27
|
+
function enabled() {
|
|
28
|
+
const raw = process.env.TELEGRAM_UPDATE_CHECK;
|
|
29
|
+
if (raw === undefined || raw.trim() === "")
|
|
30
|
+
return true;
|
|
31
|
+
return !/^(0|false|no|off)$/i.test(raw.trim());
|
|
32
|
+
}
|
|
33
|
+
function timeoutMs() {
|
|
34
|
+
const raw = process.env.TELEGRAM_UPDATE_CHECK_TIMEOUT_MS;
|
|
35
|
+
if (!raw)
|
|
36
|
+
return DEFAULT_TIMEOUT_MS;
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
39
|
+
return DEFAULT_TIMEOUT_MS;
|
|
40
|
+
return Math.min(n, 10_000);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Cache location. Deliberately independent of `getConfig()`: the notice must
|
|
44
|
+
* work before api_id/api_hash are set, and it must not create the state dir
|
|
45
|
+
* just to write a version number.
|
|
46
|
+
*/
|
|
47
|
+
function cachePath() {
|
|
48
|
+
const configured = process.env.TELEGRAM_STATE_DIR?.trim();
|
|
49
|
+
const base = configured
|
|
50
|
+
? path.resolve(configured.replace(/^~(?=$|[\\/])/, os.homedir()))
|
|
51
|
+
: path.join(os.homedir(), ".telegram-mcp");
|
|
52
|
+
return path.join(base, "update-check.json");
|
|
53
|
+
}
|
|
54
|
+
function readCache() {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath(), "utf8"));
|
|
57
|
+
if (typeof parsed.latest !== "string" || typeof parsed.checkedAt !== "string")
|
|
58
|
+
return null;
|
|
59
|
+
if (Date.now() - Date.parse(parsed.checkedAt) > CACHE_TTL_MS)
|
|
60
|
+
return null;
|
|
61
|
+
return { latest: parsed.latest, checkedAt: parsed.checkedAt };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeCache(latest) {
|
|
68
|
+
try {
|
|
69
|
+
const file = cachePath();
|
|
70
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
71
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
72
|
+
fs.writeFileSync(tmp, JSON.stringify({ checkedAt: new Date().toISOString(), latest }, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
73
|
+
fs.renameSync(tmp, file);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* a cache we cannot write is not a failure — it just means we ask again */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Ask the registry for `dist-tags.latest`. Returns null on any problem. */
|
|
80
|
+
async function fetchLatest() {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`, {
|
|
83
|
+
headers: { accept: "application/json" },
|
|
84
|
+
signal: AbortSignal.timeout(timeoutMs()),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
return null;
|
|
88
|
+
const body = (await res.json());
|
|
89
|
+
return typeof body.version === "string" && body.version.trim() ? body.version.trim() : null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the current state. `allowNetwork: false` reads only the cache, which
|
|
97
|
+
* is what `telegram_doctor` uses so a health check never waits on the network.
|
|
98
|
+
*/
|
|
99
|
+
export async function getUpdateInfo(opts = {}) {
|
|
100
|
+
const current = SERVER_VERSION;
|
|
101
|
+
// Without a trustworthy local version, "newer" has no meaning: skip the
|
|
102
|
+
// notice rather than telling every user to upgrade to something.
|
|
103
|
+
if (!VERSION_IS_KNOWN)
|
|
104
|
+
return { current, latest: current, updateAvailable: false, source: "unknown" };
|
|
105
|
+
if (!enabled())
|
|
106
|
+
return { current, latest: current, updateAvailable: false, source: "disabled" };
|
|
107
|
+
const cached = readCache();
|
|
108
|
+
if (cached) {
|
|
109
|
+
return {
|
|
110
|
+
current,
|
|
111
|
+
latest: cached.latest,
|
|
112
|
+
updateAvailable: compareVersions(cached.latest, current) > 0,
|
|
113
|
+
source: "cache",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (opts.allowNetwork === false) {
|
|
117
|
+
return { current, latest: current, updateAvailable: false, source: "unknown" };
|
|
118
|
+
}
|
|
119
|
+
const latest = await fetchLatest();
|
|
120
|
+
if (!latest)
|
|
121
|
+
return { current, latest: current, updateAvailable: false, source: "unknown" };
|
|
122
|
+
writeCache(latest);
|
|
123
|
+
return {
|
|
124
|
+
current,
|
|
125
|
+
latest,
|
|
126
|
+
updateAvailable: compareVersions(latest, current) > 0,
|
|
127
|
+
source: "registry",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** The single line users see. Shaped to be actionable, not just informative. */
|
|
131
|
+
export function formatNotice(info) {
|
|
132
|
+
const upgrade = process.env.npm_config_global === "true"
|
|
133
|
+
? `npm install -g ${PACKAGE_NAME}@latest`
|
|
134
|
+
: `npx -y ${PACKAGE_NAME}@latest`;
|
|
135
|
+
return (`[telegram-mcp] update available: ${info.current} -> ${info.latest}. ` +
|
|
136
|
+
`Run \`${upgrade}\`, or set TELEGRAM_UPDATE_CHECK=0 to silence this.\n`);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Fire-and-forget: called after the server is already serving, so a slow
|
|
140
|
+
* registry can never delay the first tool call.
|
|
141
|
+
*/
|
|
142
|
+
export function warnIfOutdated() {
|
|
143
|
+
void getUpdateInfo()
|
|
144
|
+
.then((info) => {
|
|
145
|
+
if (info.updateAvailable)
|
|
146
|
+
process.stderr.write(formatNotice(info));
|
|
147
|
+
})
|
|
148
|
+
.catch(() => {
|
|
149
|
+
/* the notice is best-effort by definition */
|
|
150
|
+
});
|
|
151
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place the version number comes from.
|
|
3
|
+
*
|
|
4
|
+
* `package.json` is the source of truth: npm writes the version it publishes,
|
|
5
|
+
* so reading it back means the MCP `serverInfo`, `--version` and the update
|
|
6
|
+
* check can never drift from the artifact that was actually shipped.
|
|
7
|
+
*
|
|
8
|
+
* Resolution walks up from this file, which covers every layout:
|
|
9
|
+
* <root>/dist/version.js (published / built)
|
|
10
|
+
* <root>/version.ts (tsx, dev)
|
|
11
|
+
* <root>/node_modules/@dikacode/telegram-mcp/dist/version.js
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
export const SERVER_NAME = "telegram-mcp";
|
|
17
|
+
export const PACKAGE_NAME = "@dikacode/telegram-mcp";
|
|
18
|
+
/**
|
|
19
|
+
* Last resort when package.json is unreadable. Deliberately not a plausible
|
|
20
|
+
* version: a guess here would make the update check nag about a release the
|
|
21
|
+
* user may already be running, so an unknown version disables the notice.
|
|
22
|
+
*/
|
|
23
|
+
const FALLBACK_VERSION = "0.0.0-unknown";
|
|
24
|
+
function readVersion() {
|
|
25
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
for (let i = 0; i < 6; i++) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(path.join(dir, "package.json"), "utf8");
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
31
|
+
return parsed.version.trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* keep walking: this directory is not the package root */
|
|
36
|
+
}
|
|
37
|
+
const parent = path.dirname(dir);
|
|
38
|
+
if (parent === dir)
|
|
39
|
+
break;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return FALLBACK_VERSION;
|
|
43
|
+
}
|
|
44
|
+
export const SERVER_VERSION = readVersion();
|
|
45
|
+
/** True when package.json could not be read, so the version is not trustworthy. */
|
|
46
|
+
export const VERSION_IS_KNOWN = SERVER_VERSION !== "0.0.0-unknown";
|
|
47
|
+
/** Dotted-numeric comparison, prerelease-aware, with no dependency. */
|
|
48
|
+
export function compareVersions(a, b) {
|
|
49
|
+
const split = (v) => {
|
|
50
|
+
const cleaned = v.trim().replace(/^v/i, "").split("+")[0];
|
|
51
|
+
const dash = cleaned.indexOf("-");
|
|
52
|
+
const core = dash === -1 ? cleaned : cleaned.slice(0, dash);
|
|
53
|
+
const pre = dash === -1 ? "" : cleaned.slice(dash + 1);
|
|
54
|
+
const nums = core.split(".").map((part) => {
|
|
55
|
+
const n = Number.parseInt(part, 10);
|
|
56
|
+
return Number.isFinite(n) ? n : 0;
|
|
57
|
+
});
|
|
58
|
+
while (nums.length < 3)
|
|
59
|
+
nums.push(0);
|
|
60
|
+
return { nums, pre };
|
|
61
|
+
};
|
|
62
|
+
const left = split(a);
|
|
63
|
+
const right = split(b);
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
if (left.nums[i] !== right.nums[i])
|
|
66
|
+
return left.nums[i] < right.nums[i] ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
// Same core: a release outranks any prerelease of it.
|
|
69
|
+
if (!left.pre && right.pre)
|
|
70
|
+
return 1;
|
|
71
|
+
if (left.pre && !right.pre)
|
|
72
|
+
return -1;
|
|
73
|
+
if (left.pre === right.pre)
|
|
74
|
+
return 0;
|
|
75
|
+
return left.pre < right.pre ? -1 : 1;
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dikacode/telegram-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Local MCP server that connects CLI agents (OpenCode, Codex CLI, Claude Code, ...) to a personal Telegram account over MTProto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
19
|
"login": "tsx cli/login.ts",
|
|
20
20
|
"doctor": "tsx cli/doctor.ts",
|
|
21
|
+
"preview": "node harness/preview.mjs",
|
|
21
22
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
23
|
"login:force": "tsx cli/login.ts --force",
|
|
23
24
|
"test": "npm run build && node harness/unit.mjs",
|