@overpod/mcp-telegram 1.19.0 → 1.20.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/CHANGELOG.md ADDED
@@ -0,0 +1,273 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.20.0] - 2026-03-31
11
+
12
+ ### Added
13
+ - **Rate limiting & retry** — automatic FLOOD_WAIT handling, network error recovery with exponential backoff (`src/rate-limiter.ts`)
14
+ - `send-message` now returns `messageId` in the response (`Message sent to @user [#12345]`), enabling send → edit workflows (closes #16)
15
+ - Rate limiter unit tests (7 tests in `src/__tests__/rate-limiter.test.ts`)
16
+
17
+ ### Changed
18
+ - `sendMessage()` return type changed from `void` to `Api.Message | Api.UpdateShortSentMessage | undefined`
19
+ - Write methods (`sendMessage`, `sendFile`, `editMessage`, `deleteMessages`) are now rate-limited with automatic retry on transient errors
20
+
21
+ ## [1.19.0] - 2026-03-30
22
+
23
+ ### Added
24
+ - Docker support for containerized deployment
25
+ - Non-blocking startup behavior
26
+ - Local QR code fallback for authentication
27
+ - Automated test infrastructure with Node.js test runner
28
+ - CI workflow to publish Docker images to GitHub Container Registry
29
+
30
+ ### Changed
31
+ - Added pnpm-lock.yaml for better dependency management
32
+
33
+ ## [1.18.0] - 2026-03-28
34
+
35
+ ### Added
36
+ - New `telegram-get-my-role` tool to check user's role in a chat
37
+ - Role information in `telegram-get-chat-members` results
38
+
39
+ ## [1.17.0] - 2026-03-28
40
+
41
+ ### Added
42
+ - Chat resolution by display name (not just ID or username)
43
+
44
+ ### Changed
45
+ - Updated documentation to replace static tool list with auto-discovery note
46
+ - Improved project structure documentation
47
+
48
+ ## [1.16.0] - 2026-03-28
49
+
50
+ ### Added
51
+ - Group management tools: invite, kick, ban, edit, leave
52
+ - Admin management capabilities
53
+
54
+ ## [1.15.0] - 2026-03-28
55
+
56
+ ### Added
57
+ - `telegram-create-group` tool for creating new groups
58
+
59
+ ### Fixed
60
+ - Documented `AUTH_KEY_DUPLICATED` error handling
61
+
62
+ ## [1.14.0] - 2026-03-28
63
+
64
+ ### Added
65
+ - SOCKS5 proxy support for Telegram connections
66
+ - MTProxy support for Telegram connections
67
+
68
+ ### Changed
69
+ - Updated Biome to 2.4.9 with new config schema
70
+ - Sorted imports for Biome compliance
71
+ - Added proxy documentation to README
72
+
73
+ ## [1.13.0] - 2026-03-26
74
+
75
+ ### Changed
76
+ - Refactored tools into modular files organized by category
77
+
78
+ ## [1.12.0] - 2026-03-26
79
+
80
+ ### Changed
81
+ - Migrated to `registerTool()` API with tool annotations
82
+
83
+ ## [1.11.1] - 2026-03-25
84
+
85
+ ### Fixed
86
+ - Sanitized unpaired UTF-16 surrogates in tool responses
87
+
88
+ ### Changed
89
+ - Upgraded TypeScript to 6.0
90
+ - Updated README with missing tools
91
+
92
+ ## [1.11.0] - 2026-03-23
93
+
94
+ ### Added
95
+ - Full reactions support: read, send multiple reactions, get detailed info
96
+
97
+ ### Changed
98
+ - Included message ID in all message-reading tool outputs
99
+
100
+ ## [1.10.1] - 2026-03-22
101
+
102
+ ### Fixed
103
+ - Message ID now included in all message-reading tool outputs
104
+
105
+ ## [1.10.0] - 2026-03-20
106
+
107
+ ### Added
108
+ - Enhanced `telegram-get-profile` with birthday, business, and premium data
109
+ - New `telegram-get-profile-photo` tool
110
+ - Global message search capability
111
+ - Enriched chat search results
112
+
113
+ ## [1.9.0] - 2026-03-18
114
+
115
+ ### Added
116
+ - Forum Topics support
117
+ - Per-topic unread count for forum groups
118
+ - Secure session storage with configurable path
119
+ - Multiple accounts support
120
+
121
+ ### Fixed
122
+ - Per-topic unread sum calculation for forum groups
123
+
124
+ ### Changed
125
+ - Updated session path and security documentation
126
+ - Upgraded GitHub Actions to v6
127
+ - Replaced Node 20 with Node 24 in CI
128
+ - Updated Biome to 2.4.7 and @types/node to 25.5.0
129
+
130
+ ## [1.8.1] - 2026-03-19
131
+
132
+ ### Fixed
133
+ - Redirected console.log to stderr to prevent MCP JSON-RPC corruption
134
+
135
+ ### Changed
136
+ - Updated dependencies (Biome 2.4.8)
137
+
138
+ ## [1.8.0] - 2026-03-18
139
+
140
+ ### Added
141
+ - Secure session storage with configurable path via SESSION_PATH environment variable
142
+
143
+ ### Changed
144
+ - Updated session path and security information in README
145
+
146
+ ## [1.7.0] - 2026-03-16
147
+
148
+ ### Added
149
+ - CI workflow to publish to GitHub Packages alongside npm
150
+ - Manual workflow dispatch trigger for publishing
151
+
152
+ ## [1.6.0] - 2026-03-16
153
+
154
+ ### Added
155
+ - Contact request management
156
+ - Block/unblock users
157
+ - Report spam functionality
158
+ - Add contact tool
159
+ - ChatGPT to list of supported clients
160
+
161
+ ### Changed
162
+ - Removed hardcoded tool counts from README and package.json
163
+ - Updated Biome to 2.4.7 and @types/node to 25.5.0
164
+
165
+ ## [1.5.0] - 2026-03-16
166
+
167
+ ### Added
168
+ - Reactions support
169
+ - Scheduled messages
170
+ - Polls creation and management
171
+ - `telegram-join-chat` tool for joining groups and channels
172
+
173
+ ### Changed
174
+ - Updated README with new tool documentation
175
+ - Increased tool count to 24
176
+
177
+ ## [1.4.0] - 2026-03-15
178
+
179
+ ### Added
180
+ - Glama.ai MCP catalog verification (glama.json)
181
+ - Smithery MCP catalog listing (smithery.yaml)
182
+ - Demo GIF and badges to README
183
+ - Hosted version link
184
+
185
+ ### Fixed
186
+ - Removed PNG file save from CLI QR login
187
+
188
+ ### Changed
189
+ - Updated README with Glama MCP server badge
190
+
191
+ ## [1.3.1] - 2026-03-12
192
+
193
+ ### Fixed
194
+ - Use `destroy()` instead of `disconnect()` to stop GramJS update loop
195
+ - Adopt QR login client directly instead of destroy+reconnect flow
196
+ - Destroy GramJS client in `logOut()` and `startQrLogin()` to stop update loop
197
+
198
+ ## [1.3.0] - 2026-03-12
199
+
200
+ ### Added
201
+ - `logOut()` method for complete Telegram session termination
202
+
203
+ ## [1.2.0] - 2026-03-11
204
+
205
+ ### Added
206
+ - `downloadMediaAsBuffer` for serverless media download
207
+ - Library exports and declaration types
208
+ - Date filters for messages
209
+ - Comprehensive README for v1.0
210
+
211
+ ### Fixed
212
+ - MIME type detection from magic bytes in `downloadMediaAsBuffer`
213
+ - Made `saveSession` resilient to file write errors in Docker
214
+
215
+ ### Changed
216
+ - Use `GetFullChannel`/`GetFullChat` for complete chat information
217
+ - Improved `telegram-login` for Claude Desktop users
218
+ - Added npm publishing support and GitHub Actions CI/CD
219
+
220
+ ## [1.1.0] - 2026-03-11
221
+
222
+ ### Added
223
+ - Contact management tools
224
+ - Chat members listing
225
+ - User profile retrieval
226
+ - Chat type filter
227
+ - Media tools (send, download, get info)
228
+ - Pin/unpin messages
229
+ - Markdown support
230
+ - Media information in messages
231
+ - Unread counts
232
+ - Mark messages as read
233
+ - Forward messages
234
+ - Edit messages
235
+ - Delete messages
236
+ - Detailed chat information
237
+ - Pagination support
238
+
239
+ ## [1.0.0] - 2026-03-10
240
+
241
+ ### Added
242
+ - Initial release: MCP server for Telegram userbot
243
+ - Basic message reading and sending
244
+ - Chat listing
245
+ - Authentication via phone number and QR code
246
+ - Session persistence
247
+ - GramJS/MTProto integration
248
+
249
+ [Unreleased]: https://github.com/overpod/mcp-telegram/compare/v1.19.0...HEAD
250
+ [1.19.0]: https://github.com/overpod/mcp-telegram/compare/v1.18.0...v1.19.0
251
+ [1.18.0]: https://github.com/overpod/mcp-telegram/compare/v1.17.0...v1.18.0
252
+ [1.17.0]: https://github.com/overpod/mcp-telegram/compare/v1.16.0...v1.17.0
253
+ [1.16.0]: https://github.com/overpod/mcp-telegram/compare/v1.15.0...v1.16.0
254
+ [1.15.0]: https://github.com/overpod/mcp-telegram/compare/v1.14.0...v1.15.0
255
+ [1.14.0]: https://github.com/overpod/mcp-telegram/compare/v1.13.0...v1.14.0
256
+ [1.13.0]: https://github.com/overpod/mcp-telegram/compare/v1.12.0...v1.13.0
257
+ [1.12.0]: https://github.com/overpod/mcp-telegram/compare/v1.11.1...v1.12.0
258
+ [1.11.1]: https://github.com/overpod/mcp-telegram/compare/v1.11.0...v1.11.1
259
+ [1.11.0]: https://github.com/overpod/mcp-telegram/compare/v1.10.1...v1.11.0
260
+ [1.10.1]: https://github.com/overpod/mcp-telegram/compare/v1.10.0...v1.10.1
261
+ [1.10.0]: https://github.com/overpod/mcp-telegram/compare/v1.9.0...v1.10.0
262
+ [1.9.0]: https://github.com/overpod/mcp-telegram/compare/v1.8.1...v1.9.0
263
+ [1.8.1]: https://github.com/overpod/mcp-telegram/compare/v1.8.0...v1.8.1
264
+ [1.8.0]: https://github.com/overpod/mcp-telegram/compare/v1.7.0...v1.8.0
265
+ [1.7.0]: https://github.com/overpod/mcp-telegram/compare/v1.6.0...v1.7.0
266
+ [1.6.0]: https://github.com/overpod/mcp-telegram/compare/v1.5.0...v1.6.0
267
+ [1.5.0]: https://github.com/overpod/mcp-telegram/compare/v1.4.0...v1.5.0
268
+ [1.4.0]: https://github.com/overpod/mcp-telegram/compare/v1.3.1...v1.4.0
269
+ [1.3.1]: https://github.com/overpod/mcp-telegram/compare/v1.3.0...v1.3.1
270
+ [1.3.0]: https://github.com/overpod/mcp-telegram/compare/v1.2.0...v1.3.0
271
+ [1.2.0]: https://github.com/overpod/mcp-telegram/compare/v1.1.0...v1.2.0
272
+ [1.1.0]: https://github.com/overpod/mcp-telegram/compare/v1.0.0...v1.1.0
273
+ [1.0.0]: https://github.com/overpod/mcp-telegram/releases/tag/v1.0.0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { RateLimiter } from "../rate-limiter.js";
4
+ describe("RateLimiter", () => {
5
+ it("should execute a function successfully", async () => {
6
+ const limiter = new RateLimiter({ maxRequestsPerSecond: 100 });
7
+ const result = await limiter.execute(async () => "success");
8
+ assert.strictEqual(result, "success");
9
+ });
10
+ it("should enforce rate limiting between requests", async () => {
11
+ const limiter = new RateLimiter({ maxRequestsPerSecond: 10 }); // 10 req/s = 100ms between requests
12
+ const start = Date.now();
13
+ await limiter.execute(async () => "first");
14
+ await limiter.execute(async () => "second");
15
+ const elapsed = Date.now() - start;
16
+ assert.ok(elapsed >= 90, `Expected at least 90ms, got ${elapsed}ms`);
17
+ });
18
+ it("should retry on FLOOD_WAIT error", async () => {
19
+ const limiter = new RateLimiter({ maxRetries: 2, maxRequestsPerSecond: 100 });
20
+ let attempts = 0;
21
+ const result = await limiter.execute(async () => {
22
+ attempts++;
23
+ if (attempts < 2) {
24
+ throw new Error("FLOOD_WAIT_1");
25
+ }
26
+ return "success after retry";
27
+ });
28
+ assert.strictEqual(result, "success after retry");
29
+ assert.strictEqual(attempts, 2);
30
+ });
31
+ it("should throw after max retries on FLOOD_WAIT", async () => {
32
+ const limiter = new RateLimiter({ maxRetries: 1, maxRequestsPerSecond: 100 });
33
+ await assert.rejects(async () => {
34
+ await limiter.execute(async () => {
35
+ throw new Error("FLOOD_WAIT_2");
36
+ });
37
+ }, {
38
+ message: /Rate limit exceeded after 1 retries/,
39
+ });
40
+ });
41
+ it("should retry on network errors with exponential backoff", async () => {
42
+ const limiter = new RateLimiter({
43
+ maxRetries: 2,
44
+ initialRetryDelay: 100,
45
+ maxRequestsPerSecond: 100,
46
+ });
47
+ let attempts = 0;
48
+ const result = await limiter.execute(async () => {
49
+ attempts++;
50
+ if (attempts < 2) {
51
+ throw new Error("TIMEOUT");
52
+ }
53
+ return "recovered";
54
+ });
55
+ assert.strictEqual(result, "recovered");
56
+ assert.strictEqual(attempts, 2);
57
+ });
58
+ it("should not retry on non-retryable errors", async () => {
59
+ const limiter = new RateLimiter({ maxRetries: 3, maxRequestsPerSecond: 100 });
60
+ let attempts = 0;
61
+ await assert.rejects(async () => {
62
+ await limiter.execute(async () => {
63
+ attempts++;
64
+ throw new Error("AUTH_KEY_UNREGISTERED");
65
+ });
66
+ }, {
67
+ message: "AUTH_KEY_UNREGISTERED",
68
+ });
69
+ assert.strictEqual(attempts, 1, "Should not retry non-retryable errors");
70
+ });
71
+ it("should handle FLOOD_WAIT with seconds parsing", async () => {
72
+ const limiter = new RateLimiter({ maxRetries: 1, maxRequestsPerSecond: 100 });
73
+ await assert.rejects(async () => {
74
+ await limiter.execute(async () => {
75
+ throw new Error("FLOOD_WAIT_1");
76
+ });
77
+ }, {
78
+ message: /Telegram requires 1s wait/,
79
+ });
80
+ });
81
+ });
package/dist/index.js CHANGED
@@ -14,7 +14,9 @@ import { registerTools } from "./tools/index.js";
14
14
  const API_ID = Number(process.env.TELEGRAM_API_ID);
15
15
  const API_HASH = process.env.TELEGRAM_API_HASH;
16
16
  if (!API_ID || !API_HASH) {
17
- console.error("[mcp-telegram] TELEGRAM_API_ID and TELEGRAM_API_HASH must be set");
17
+ console.error("[mcp-telegram] Missing TELEGRAM_API_ID and TELEGRAM_API_HASH");
18
+ console.error("Get your credentials at https://my.telegram.org/apps (API development tools)");
19
+ console.error("Set them in .env or export as environment variables");
18
20
  process.exit(1);
19
21
  }
20
22
  const telegram = new TelegramService(API_ID, API_HASH);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Rate limiter and retry logic for Telegram API calls.
3
+ * Handles FLOOD_WAIT errors and implements exponential backoff.
4
+ */
5
+ export interface RateLimiterOptions {
6
+ /** Maximum number of requests per second (default: 20) */
7
+ maxRequestsPerSecond?: number;
8
+ /** Maximum number of retry attempts (default: 3) */
9
+ maxRetries?: number;
10
+ /** Initial retry delay in milliseconds (default: 1000) */
11
+ initialRetryDelay?: number;
12
+ /** Maximum retry delay in milliseconds (default: 60000) */
13
+ maxRetryDelay?: number;
14
+ }
15
+ export declare class RateLimiter {
16
+ private lastRequestTime;
17
+ private minInterval;
18
+ private maxRetries;
19
+ private initialRetryDelay;
20
+ private maxRetryDelay;
21
+ constructor(options?: RateLimiterOptions);
22
+ /** Execute a function with rate limiting and automatic retry */
23
+ execute<T>(fn: () => Promise<T>, context?: string): Promise<T>;
24
+ private executeWithRetry;
25
+ private waitForSlot;
26
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Rate limiter and retry logic for Telegram API calls.
3
+ * Handles FLOOD_WAIT errors and implements exponential backoff.
4
+ */
5
+ export class RateLimiter {
6
+ lastRequestTime = 0;
7
+ minInterval;
8
+ maxRetries;
9
+ initialRetryDelay;
10
+ maxRetryDelay;
11
+ constructor(options = {}) {
12
+ const maxRequestsPerSecond = options.maxRequestsPerSecond ?? 20;
13
+ this.minInterval = 1000 / maxRequestsPerSecond;
14
+ this.maxRetries = options.maxRetries ?? 3;
15
+ this.initialRetryDelay = options.initialRetryDelay ?? 1000;
16
+ this.maxRetryDelay = options.maxRetryDelay ?? 60000;
17
+ }
18
+ /** Execute a function with rate limiting and automatic retry */
19
+ async execute(fn, context = "API call") {
20
+ return this.executeWithRetry(fn, context, 0);
21
+ }
22
+ async executeWithRetry(fn, context, attempt) {
23
+ await this.waitForSlot();
24
+ try {
25
+ return await fn();
26
+ }
27
+ catch (error) {
28
+ const errorMessage = error.errorMessage || error.message || String(error);
29
+ // FLOOD_WAIT — wait the exact time Telegram requires
30
+ const floodMatch = errorMessage.match(/FLOOD_WAIT[_]?(\d+)/i);
31
+ if (floodMatch) {
32
+ const waitSeconds = Number.parseInt(floodMatch[1], 10);
33
+ if (attempt >= this.maxRetries) {
34
+ throw new Error(`Rate limit exceeded after ${this.maxRetries} retries. Telegram requires ${waitSeconds}s wait. Try again later.`);
35
+ }
36
+ console.error(`[rate-limiter] FLOOD_WAIT for ${context}. Waiting ${waitSeconds}s (attempt ${attempt + 1}/${this.maxRetries})`);
37
+ await sleep(waitSeconds * 1000);
38
+ return this.executeWithRetry(fn, context, attempt + 1);
39
+ }
40
+ // Network/timeout errors — exponential backoff
41
+ if (isNetworkError(errorMessage)) {
42
+ if (attempt >= this.maxRetries) {
43
+ throw new Error(`Network error after ${this.maxRetries} retries: ${errorMessage}. Check your connection.`);
44
+ }
45
+ const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
46
+ console.error(`[rate-limiter] Network error for ${context}. Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
47
+ await sleep(delay);
48
+ return this.executeWithRetry(fn, context, attempt + 1);
49
+ }
50
+ // Temporary server errors (5xx) — exponential backoff
51
+ if (isTemporaryError(errorMessage)) {
52
+ if (attempt >= this.maxRetries) {
53
+ throw new Error(`Temporary error after ${this.maxRetries} retries: ${errorMessage}`);
54
+ }
55
+ const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
56
+ await sleep(delay);
57
+ return this.executeWithRetry(fn, context, attempt + 1);
58
+ }
59
+ // Non-retryable — throw immediately
60
+ throw error;
61
+ }
62
+ }
63
+ async waitForSlot() {
64
+ const now = Date.now();
65
+ const elapsed = now - this.lastRequestTime;
66
+ if (elapsed < this.minInterval) {
67
+ await sleep(this.minInterval - elapsed);
68
+ }
69
+ this.lastRequestTime = Date.now();
70
+ }
71
+ }
72
+ function isNetworkError(msg) {
73
+ return /TIMEOUT|ETIMEDOUT|ECONNREFUSED|ENETUNREACH|ENOTFOUND|EHOSTUNREACH|network|timed out/i.test(msg);
74
+ }
75
+ function isTemporaryError(msg) {
76
+ return /INTERNAL|^50[023]$|Internal Server Error|Service Unavailable|Bad Gateway/i.test(msg);
77
+ }
78
+ function sleep(ms) {
79
+ return new Promise((resolve) => setTimeout(resolve, ms));
80
+ }
@@ -1,3 +1,4 @@
1
+ import { Api } from "telegram/tl/index.js";
1
2
  export declare class TelegramService {
2
3
  private client;
3
4
  private apiId;
@@ -5,6 +6,7 @@ export declare class TelegramService {
5
6
  private sessionString;
6
7
  private connected;
7
8
  private sessionPath;
9
+ private rateLimiter;
8
10
  lastError: string;
9
11
  get sessionDir(): string;
10
12
  constructor(apiId: number, apiHash: string, options?: {
@@ -37,7 +39,7 @@ export declare class TelegramService {
37
39
  username?: string;
38
40
  firstName?: string;
39
41
  }>;
40
- sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<void>;
42
+ sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<Api.Message | Api.UpdateShortSentMessage | undefined>;
41
43
  sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
42
44
  downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
43
45
  downloadMediaAsBuffer(chatId: string, messageId: number): Promise<{
@@ -9,12 +9,14 @@ import { TelegramClient } from "telegram";
9
9
  import { CustomFile } from "telegram/client/uploads.js";
10
10
  import { StringSession } from "telegram/sessions/index.js";
11
11
  import { Api } from "telegram/tl/index.js";
12
+ import { RateLimiter } from "./rate-limiter.js";
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
14
  const LEGACY_SESSION_FILE = join(__dirname, "..", ".telegram-session");
14
15
  const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
15
16
  const DEFAULT_SESSION_FILE = join(DEFAULT_SESSION_DIR, "session");
16
17
  const SESSION_STRING_RE = /^[A-Za-z0-9+/=]+$/;
17
18
  const MIN_SESSION_LENGTH = 100;
19
+ const NOT_CONNECTED_ERROR = "Not connected. Run telegram-status to check or telegram-login to authenticate.";
18
20
  function resolveSessionPath(sessionPath) {
19
21
  return sessionPath ?? process.env.TELEGRAM_SESSION_PATH ?? DEFAULT_SESSION_FILE;
20
22
  }
@@ -49,6 +51,7 @@ export class TelegramService {
49
51
  sessionString = "";
50
52
  connected = false;
51
53
  sessionPath;
54
+ rateLimiter = new RateLimiter();
52
55
  lastError = "";
53
56
  get sessionDir() {
54
57
  return dirname(this.sessionPath);
@@ -136,7 +139,7 @@ export class TelegramService {
136
139
  // Auth revoked — delete invalid session
137
140
  if (msg === "AUTH_KEY_UNREGISTERED" || msg === "SESSION_REVOKED" || msg === "USER_DEACTIVATED") {
138
141
  await this.clearSession();
139
- this.lastError = "Session revoked. Re-login required.";
142
+ this.lastError = "Session revoked. Run telegram-login to re-authenticate.";
140
143
  }
141
144
  // Network error — keep session, just report
142
145
  else if (msg.includes("TIMEOUT") ||
@@ -144,7 +147,7 @@ export class TelegramService {
144
147
  msg.includes("ENETUNREACH") ||
145
148
  msg.includes("ENOTFOUND") ||
146
149
  msg.includes("network")) {
147
- this.lastError = `Network error: ${msg}. Session preserved, will retry on next call.`;
150
+ this.lastError = `Network error: ${msg}. Run telegram-status to retry connection.`;
148
151
  }
149
152
  // Unknown error
150
153
  else {
@@ -290,7 +293,7 @@ export class TelegramService {
290
293
  }
291
294
  async getMe() {
292
295
  if (!this.client || !this.connected)
293
- throw new Error("Not connected");
296
+ throw new Error(NOT_CONNECTED_ERROR);
294
297
  const me = await this.client.getMe();
295
298
  const user = me;
296
299
  return {
@@ -301,38 +304,47 @@ export class TelegramService {
301
304
  }
302
305
  async sendMessage(chatId, text, replyTo, parseMode, topicId) {
303
306
  if (!this.client || !this.connected)
304
- throw new Error("Not connected");
305
- const resolved = await this.resolvePeer(chatId);
306
- if (topicId) {
307
- // Forum topics require raw API call with InputReplyToMessage
308
- const peer = await this.client.getInputEntity(resolved);
309
- await this.client.invoke(new Api.messages.SendMessage({
310
- peer,
311
- message: text,
312
- randomId: bigInt(Math.floor(Math.random() * 1e15)),
313
- replyTo: new Api.InputReplyToMessage({
314
- replyToMsgId: replyTo ?? topicId,
315
- topMsgId: topicId,
316
- }),
317
- }));
318
- }
319
- else {
320
- await this.client.sendMessage(resolved, {
307
+ throw new Error(NOT_CONNECTED_ERROR);
308
+ return this.rateLimiter.execute(async () => {
309
+ const resolved = await this.resolvePeer(chatId);
310
+ if (topicId) {
311
+ const peer = await this.client?.getInputEntity(resolved);
312
+ const result = await this.client?.invoke(new Api.messages.SendMessage({
313
+ peer,
314
+ message: text,
315
+ randomId: bigInt(Math.floor(Math.random() * 1e15)),
316
+ replyTo: new Api.InputReplyToMessage({
317
+ replyToMsgId: replyTo ?? topicId,
318
+ topMsgId: topicId,
319
+ }),
320
+ }));
321
+ if (result instanceof Api.UpdateShortSentMessage)
322
+ return result;
323
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
324
+ const msgUpdate = result.updates.find((u) => u instanceof Api.UpdateNewMessage);
325
+ if (msgUpdate?.message instanceof Api.Message)
326
+ return msgUpdate.message;
327
+ }
328
+ return undefined;
329
+ }
330
+ return await this.client?.sendMessage(resolved, {
321
331
  message: text,
322
332
  ...(replyTo ? { replyTo } : {}),
323
333
  ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
324
334
  });
325
- }
335
+ }, `sendMessage to ${chatId}`);
326
336
  }
327
337
  async sendFile(chatId, filePath, caption) {
328
338
  if (!this.client || !this.connected)
329
- throw new Error("Not connected");
330
- const resolved = await this.resolvePeer(chatId);
331
- await this.client.sendFile(resolved, { file: filePath, caption });
339
+ throw new Error(NOT_CONNECTED_ERROR);
340
+ await this.rateLimiter.execute(async () => {
341
+ const resolved = await this.resolvePeer(chatId);
342
+ await this.client?.sendFile(resolved, { file: filePath, caption });
343
+ }, `sendFile to ${chatId}`);
332
344
  }
333
345
  async downloadMedia(chatId, messageId, downloadPath) {
334
346
  if (!this.client || !this.connected)
335
- throw new Error("Not connected");
347
+ throw new Error(NOT_CONNECTED_ERROR);
336
348
  const resolved = await this.resolvePeer(chatId);
337
349
  const messages = await this.client.getMessages(resolved, { ids: [messageId] });
338
350
  const message = messages[0];
@@ -348,7 +360,7 @@ export class TelegramService {
348
360
  }
349
361
  async downloadMediaAsBuffer(chatId, messageId) {
350
362
  if (!this.client || !this.connected)
351
- throw new Error("Not connected");
363
+ throw new Error(NOT_CONNECTED_ERROR);
352
364
  const resolved = await this.resolvePeer(chatId);
353
365
  const messages = await this.client.getMessages(resolved, { ids: [messageId] });
354
366
  const message = messages[0];
@@ -384,19 +396,19 @@ export class TelegramService {
384
396
  }
385
397
  async pinMessage(chatId, messageId, silent = false) {
386
398
  if (!this.client || !this.connected)
387
- throw new Error("Not connected");
399
+ throw new Error(NOT_CONNECTED_ERROR);
388
400
  const resolved = await this.resolvePeer(chatId);
389
401
  await this.client.pinMessage(resolved, messageId, { notify: !silent });
390
402
  }
391
403
  async unpinMessage(chatId, messageId) {
392
404
  if (!this.client || !this.connected)
393
- throw new Error("Not connected");
405
+ throw new Error(NOT_CONNECTED_ERROR);
394
406
  const resolved = await this.resolvePeer(chatId);
395
407
  await this.client.unpinMessage(resolved, messageId);
396
408
  }
397
409
  async getDialogs(limit = 20, offsetDate, filterType) {
398
410
  if (!this.client || !this.connected)
399
- throw new Error("Not connected");
411
+ throw new Error(NOT_CONNECTED_ERROR);
400
412
  const fetchLimit = filterType ? limit * 3 : limit;
401
413
  const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
402
414
  const mapped = dialogs.map((d) => {
@@ -419,7 +431,7 @@ export class TelegramService {
419
431
  }
420
432
  async getUnreadDialogs(limit = 20) {
421
433
  if (!this.client || !this.connected)
422
- throw new Error("Not connected");
434
+ throw new Error(NOT_CONNECTED_ERROR);
423
435
  const dialogs = await this.client.getDialogs({ limit: limit * 3 });
424
436
  const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
425
437
  const results = await Promise.all(unread.map(async (d) => {
@@ -460,7 +472,7 @@ export class TelegramService {
460
472
  }
461
473
  async getContactRequests(limit = 20) {
462
474
  if (!this.client || !this.connected)
463
- throw new Error("Not connected");
475
+ throw new Error(NOT_CONNECTED_ERROR);
464
476
  const dialogs = await this.client.getDialogs({ limit: limit * 5 });
465
477
  return dialogs
466
478
  .filter((d) => {
@@ -485,7 +497,7 @@ export class TelegramService {
485
497
  }
486
498
  async addContact(userId, firstName, lastName, phone) {
487
499
  if (!this.client || !this.connected)
488
- throw new Error("Not connected");
500
+ throw new Error(NOT_CONNECTED_ERROR);
489
501
  const entity = await this.client.getInputEntity(userId);
490
502
  await this.client.invoke(new Api.contacts.AddContact({
491
503
  id: entity,
@@ -496,39 +508,43 @@ export class TelegramService {
496
508
  }
497
509
  async blockUser(userId) {
498
510
  if (!this.client || !this.connected)
499
- throw new Error("Not connected");
511
+ throw new Error(NOT_CONNECTED_ERROR);
500
512
  const entity = await this.client.getInputEntity(userId);
501
513
  await this.client.invoke(new Api.contacts.Block({ id: entity }));
502
514
  }
503
515
  async reportSpam(chatId) {
504
516
  if (!this.client || !this.connected)
505
- throw new Error("Not connected");
517
+ throw new Error(NOT_CONNECTED_ERROR);
506
518
  const peer = await this.client.getInputEntity(chatId);
507
519
  await this.client.invoke(new Api.messages.ReportSpam({ peer }));
508
520
  }
509
521
  async markAsRead(chatId) {
510
522
  if (!this.client || !this.connected)
511
- throw new Error("Not connected");
523
+ throw new Error(NOT_CONNECTED_ERROR);
512
524
  await this.client.markAsRead(chatId);
513
525
  }
514
526
  async forwardMessage(fromChatId, toChatId, messageIds) {
515
527
  if (!this.client || !this.connected)
516
- throw new Error("Not connected");
528
+ throw new Error(NOT_CONNECTED_ERROR);
517
529
  const resolvedFrom = await this.resolvePeer(fromChatId);
518
530
  const resolvedTo = await this.resolvePeer(toChatId);
519
531
  await this.client.forwardMessages(resolvedTo, { messages: messageIds, fromPeer: resolvedFrom });
520
532
  }
521
533
  async editMessage(chatId, messageId, newText) {
522
534
  if (!this.client || !this.connected)
523
- throw new Error("Not connected");
524
- const resolved = await this.resolvePeer(chatId);
525
- await this.client.editMessage(resolved, { message: messageId, text: newText });
535
+ throw new Error(NOT_CONNECTED_ERROR);
536
+ await this.rateLimiter.execute(async () => {
537
+ const resolved = await this.resolvePeer(chatId);
538
+ await this.client?.editMessage(resolved, { message: messageId, text: newText });
539
+ }, `editMessage ${messageId} in ${chatId}`);
526
540
  }
527
541
  async deleteMessages(chatId, messageIds) {
528
542
  if (!this.client || !this.connected)
529
- throw new Error("Not connected");
530
- const resolved = await this.resolvePeer(chatId);
531
- await this.client.deleteMessages(resolved, messageIds, { revoke: true });
543
+ throw new Error(NOT_CONNECTED_ERROR);
544
+ await this.rateLimiter.execute(async () => {
545
+ const resolved = await this.resolvePeer(chatId);
546
+ await this.client?.deleteMessages(resolved, messageIds, { revoke: true });
547
+ }, `deleteMessages in ${chatId}`);
532
548
  }
533
549
  /**
534
550
  * Resolve a chat by ID, username, or display name.
@@ -537,7 +553,7 @@ export class TelegramService {
537
553
  // biome-ignore lint: GramJS has no proper entity union type
538
554
  async resolveChat(chatId) {
539
555
  if (!this.client)
540
- throw new Error("Not connected");
556
+ throw new Error(NOT_CONNECTED_ERROR);
541
557
  // First try direct resolve (numeric ID, username, phone)
542
558
  try {
543
559
  return await this.client.getEntity(chatId);
@@ -576,7 +592,7 @@ export class TelegramService {
576
592
  }
577
593
  async getChatInfo(chatId) {
578
594
  if (!this.client || !this.connected)
579
- throw new Error("Not connected");
595
+ throw new Error(NOT_CONNECTED_ERROR);
580
596
  const entity = await this.resolveChat(chatId);
581
597
  if (entity instanceof Api.User) {
582
598
  const parts = [entity.firstName, entity.lastName].filter(Boolean);
@@ -684,7 +700,7 @@ export class TelegramService {
684
700
  }
685
701
  async getMessages(chatId, limit = 10, offsetId, minDate, maxDate) {
686
702
  if (!this.client || !this.connected)
687
- throw new Error("Not connected");
703
+ throw new Error(NOT_CONNECTED_ERROR);
688
704
  const resolved = await this.resolvePeer(chatId);
689
705
  const opts = {
690
706
  limit,
@@ -708,7 +724,7 @@ export class TelegramService {
708
724
  }
709
725
  async searchChats(query, limit = 10) {
710
726
  if (!this.client || !this.connected)
711
- throw new Error("Not connected");
727
+ throw new Error(NOT_CONNECTED_ERROR);
712
728
  const result = await this.client.invoke(new Api.contacts.Search({ q: query, limit }));
713
729
  const chats = [];
714
730
  for (const user of result.users) {
@@ -769,7 +785,7 @@ export class TelegramService {
769
785
  }
770
786
  async searchGlobal(query, limit = 20, minDate, maxDate) {
771
787
  if (!this.client || !this.connected)
772
- throw new Error("Not connected");
788
+ throw new Error(NOT_CONNECTED_ERROR);
773
789
  const result = await this.client.invoke(new Api.messages.SearchGlobal({
774
790
  q: query,
775
791
  filter: new Api.InputMessagesFilterEmpty(),
@@ -838,7 +854,7 @@ export class TelegramService {
838
854
  }
839
855
  async searchMessages(chatId, query, limit = 20, minDate, maxDate) {
840
856
  if (!this.client || !this.connected)
841
- throw new Error("Not connected");
857
+ throw new Error(NOT_CONNECTED_ERROR);
842
858
  const resolved = await this.resolvePeer(chatId);
843
859
  const messages = await this.client.getMessages(resolved, {
844
860
  search: query,
@@ -861,7 +877,7 @@ export class TelegramService {
861
877
  }
862
878
  async getContacts(limit = 50) {
863
879
  if (!this.client || !this.connected)
864
- throw new Error("Not connected");
880
+ throw new Error(NOT_CONNECTED_ERROR);
865
881
  const result = await this.client.invoke(new Api.contacts.GetContacts({ hash: bigInt(0) }));
866
882
  if (!(result instanceof Api.contacts.Contacts))
867
883
  return [];
@@ -881,7 +897,7 @@ export class TelegramService {
881
897
  }
882
898
  async getChatMembers(chatId, limit = 50) {
883
899
  if (!this.client || !this.connected)
884
- throw new Error("Not connected");
900
+ throw new Error(NOT_CONNECTED_ERROR);
885
901
  const entity = await this.resolveChat(chatId);
886
902
  if (entity instanceof Api.Channel) {
887
903
  const result = await this.client.invoke(new Api.channels.GetParticipants({
@@ -926,7 +942,7 @@ export class TelegramService {
926
942
  }
927
943
  async getMyRole(chatId) {
928
944
  if (!this.client || !this.connected)
929
- throw new Error("Not connected");
945
+ throw new Error(NOT_CONNECTED_ERROR);
930
946
  const entity = await this.resolveChat(chatId);
931
947
  const me = await this.getMe();
932
948
  if (entity instanceof Api.Channel) {
@@ -978,7 +994,7 @@ export class TelegramService {
978
994
  }
979
995
  async getProfile(userId) {
980
996
  if (!this.client || !this.connected)
981
- throw new Error("Not connected");
997
+ throw new Error(NOT_CONNECTED_ERROR);
982
998
  const entity = await this.client.getEntity(userId);
983
999
  if (!(entity instanceof Api.User))
984
1000
  throw new Error("Entity is not a user");
@@ -1038,7 +1054,7 @@ export class TelegramService {
1038
1054
  }
1039
1055
  async downloadProfilePhoto(entityId, options) {
1040
1056
  if (!this.client || !this.connected)
1041
- throw new Error("Not connected");
1057
+ throw new Error(NOT_CONNECTED_ERROR);
1042
1058
  const entity = await this.client.getEntity(entityId);
1043
1059
  const buffer = (await this.client.downloadProfilePhoto(entity, {
1044
1060
  isBig: options?.isBig !== false,
@@ -1089,7 +1105,7 @@ export class TelegramService {
1089
1105
  }
1090
1106
  async sendReaction(chatId, messageId, emoji, addToExisting = false) {
1091
1107
  if (!this.client || !this.connected)
1092
- throw new Error("Not connected");
1108
+ throw new Error(NOT_CONNECTED_ERROR);
1093
1109
  const resolved = await this.resolvePeer(chatId);
1094
1110
  const peer = await this.client.getInputEntity(resolved);
1095
1111
  const reactionList = [];
@@ -1129,7 +1145,7 @@ export class TelegramService {
1129
1145
  }
1130
1146
  async getMessageReactions(chatId, messageId) {
1131
1147
  if (!this.client || !this.connected)
1132
- throw new Error("Not connected");
1148
+ throw new Error(NOT_CONNECTED_ERROR);
1133
1149
  const resolved = await this.resolvePeer(chatId);
1134
1150
  const peer = await this.client.getInputEntity(resolved);
1135
1151
  // First get the message to know which reactions exist
@@ -1184,7 +1200,7 @@ export class TelegramService {
1184
1200
  }
1185
1201
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
1186
1202
  if (!this.client || !this.connected)
1187
- throw new Error("Not connected");
1203
+ throw new Error(NOT_CONNECTED_ERROR);
1188
1204
  const resolved = await this.resolvePeer(chatId);
1189
1205
  await this.client.sendMessage(resolved, {
1190
1206
  message: text,
@@ -1195,7 +1211,7 @@ export class TelegramService {
1195
1211
  }
1196
1212
  async createPoll(chatId, question, answers, options) {
1197
1213
  if (!this.client || !this.connected)
1198
- throw new Error("Not connected");
1214
+ throw new Error(NOT_CONNECTED_ERROR);
1199
1215
  const peer = await this.client.getInputEntity(chatId);
1200
1216
  const pollAnswers = answers.map((text, i) => new Api.PollAnswer({
1201
1217
  text: new Api.TextWithEntities({ text, entities: [] }),
@@ -1231,7 +1247,7 @@ export class TelegramService {
1231
1247
  }
1232
1248
  async getForumTopics(chatId, limit = 100) {
1233
1249
  if (!this.client || !this.connected)
1234
- throw new Error("Not connected");
1250
+ throw new Error(NOT_CONNECTED_ERROR);
1235
1251
  const entity = await this.resolveChat(chatId);
1236
1252
  if (!(entity instanceof Api.Channel))
1237
1253
  throw new Error("Forum topics are only available in supergroups");
@@ -1260,7 +1276,7 @@ export class TelegramService {
1260
1276
  }
1261
1277
  async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
1262
1278
  if (!this.client || !this.connected)
1263
- throw new Error("Not connected");
1279
+ throw new Error(NOT_CONNECTED_ERROR);
1264
1280
  const peer = await this.client.getInputEntity(chatId);
1265
1281
  const result = await this.client.invoke(new Api.messages.GetReplies({
1266
1282
  peer,
@@ -1289,7 +1305,7 @@ export class TelegramService {
1289
1305
  /** Check if a chat entity is a forum (has topics enabled) */
1290
1306
  async isForum(chatId) {
1291
1307
  if (!this.client || !this.connected)
1292
- throw new Error("Not connected");
1308
+ throw new Error(NOT_CONNECTED_ERROR);
1293
1309
  try {
1294
1310
  const entity = await this.resolveChat(chatId);
1295
1311
  if (entity instanceof Api.Channel) {
@@ -1301,7 +1317,7 @@ export class TelegramService {
1301
1317
  }
1302
1318
  async joinChat(target) {
1303
1319
  if (!this.client)
1304
- throw new Error("Not connected");
1320
+ throw new Error(NOT_CONNECTED_ERROR);
1305
1321
  // Extract invite hash from various link formats
1306
1322
  const inviteMatch = target.match(/(?:t\.me\/\+|t\.me\/joinchat\/|tg:\/\/join\?invite=)([a-zA-Z0-9_-]+)/);
1307
1323
  if (inviteMatch) {
@@ -1332,7 +1348,7 @@ export class TelegramService {
1332
1348
  }
1333
1349
  async createGroup(options) {
1334
1350
  if (!this.client)
1335
- throw new Error("Not connected");
1351
+ throw new Error(NOT_CONNECTED_ERROR);
1336
1352
  const { title, users, supergroup = false, forum = false, description } = options;
1337
1353
  if (supergroup || forum) {
1338
1354
  // Create supergroup/channel via channels.CreateChannel
@@ -1406,7 +1422,7 @@ export class TelegramService {
1406
1422
  }
1407
1423
  async inviteToGroup(chatId, users) {
1408
1424
  if (!this.client)
1409
- throw new Error("Not connected");
1425
+ throw new Error(NOT_CONNECTED_ERROR);
1410
1426
  const entity = await this.resolveChat(chatId);
1411
1427
  const invited = [];
1412
1428
  const failed = [];
@@ -1434,7 +1450,7 @@ export class TelegramService {
1434
1450
  }
1435
1451
  async kickUser(chatId, userId) {
1436
1452
  if (!this.client)
1437
- throw new Error("Not connected");
1453
+ throw new Error(NOT_CONNECTED_ERROR);
1438
1454
  const entity = await this.resolveChat(chatId);
1439
1455
  const user = await this.client.getEntity(userId);
1440
1456
  if (!(user instanceof Api.User))
@@ -1459,7 +1475,7 @@ export class TelegramService {
1459
1475
  }
1460
1476
  async banUser(chatId, userId) {
1461
1477
  if (!this.client)
1462
- throw new Error("Not connected");
1478
+ throw new Error(NOT_CONNECTED_ERROR);
1463
1479
  const entity = await this.resolveChat(chatId);
1464
1480
  const user = await this.client.getEntity(userId);
1465
1481
  if (!(user instanceof Api.User))
@@ -1475,7 +1491,7 @@ export class TelegramService {
1475
1491
  }
1476
1492
  async unbanUser(chatId, userId) {
1477
1493
  if (!this.client)
1478
- throw new Error("Not connected");
1494
+ throw new Error(NOT_CONNECTED_ERROR);
1479
1495
  const entity = await this.resolveChat(chatId);
1480
1496
  const user = await this.client.getEntity(userId);
1481
1497
  if (!(user instanceof Api.User))
@@ -1491,7 +1507,7 @@ export class TelegramService {
1491
1507
  }
1492
1508
  async editGroup(chatId, options) {
1493
1509
  if (!this.client)
1494
- throw new Error("Not connected");
1510
+ throw new Error(NOT_CONNECTED_ERROR);
1495
1511
  const entity = await this.resolveChat(chatId);
1496
1512
  if (options.title) {
1497
1513
  if (entity instanceof Api.Channel) {
@@ -1521,7 +1537,7 @@ export class TelegramService {
1521
1537
  }
1522
1538
  async leaveGroup(chatId) {
1523
1539
  if (!this.client)
1524
- throw new Error("Not connected");
1540
+ throw new Error(NOT_CONNECTED_ERROR);
1525
1541
  const entity = await this.resolveChat(chatId);
1526
1542
  if (entity instanceof Api.Channel) {
1527
1543
  await this.client.invoke(new Api.channels.LeaveChannel({ channel: entity }));
@@ -1538,7 +1554,7 @@ export class TelegramService {
1538
1554
  }
1539
1555
  async setAdmin(chatId, userId, options) {
1540
1556
  if (!this.client)
1541
- throw new Error("Not connected");
1557
+ throw new Error(NOT_CONNECTED_ERROR);
1542
1558
  const entity = await this.resolveChat(chatId);
1543
1559
  if (!(entity instanceof Api.Channel))
1544
1560
  throw new Error("Set admin is only supported for supergroups and channels");
@@ -1564,7 +1580,7 @@ export class TelegramService {
1564
1580
  }
1565
1581
  async removeAdmin(chatId, userId) {
1566
1582
  if (!this.client)
1567
- throw new Error("Not connected");
1583
+ throw new Error(NOT_CONNECTED_ERROR);
1568
1584
  const entity = await this.resolveChat(chatId);
1569
1585
  if (!(entity instanceof Api.Channel))
1570
1586
  throw new Error("Remove admin is only supported for supergroups and channels");
@@ -16,9 +16,11 @@ export function registerMessageTools(server, telegram) {
16
16
  if (err)
17
17
  return fail(new Error(err));
18
18
  try {
19
- await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
19
+ const result = await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
20
20
  const dest = topicId ? `topic ${topicId} in ${chatId}` : chatId;
21
- return ok(`Message sent to ${dest}`);
21
+ const messageId = result?.id;
22
+ const idInfo = messageId ? ` [#${messageId}]` : "";
23
+ return ok(`Message sent to ${dest}${idInfo}`);
22
24
  }
23
25
  catch (e) {
24
26
  return fail(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "MCP server for Telegram userbot — messages, media, reactions, polls & more. Built on GramJS/MTProto.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,8 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "README.md",
17
- "LICENSE"
17
+ "LICENSE",
18
+ "CHANGELOG.md"
18
19
  ],
19
20
  "scripts": {
20
21
  "dev": "tsx watch src/index.ts",