@overpod/mcp-telegram 1.19.0 → 1.21.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,278 @@
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.21.0] - 2026-04-01
11
+
12
+ ### Added
13
+ - `TelegramService.getClient()` — public accessor for the underlying GramJS `TelegramClient` instance, enabling event handlers like `NewMessage` for real-time listeners (#17)
14
+
15
+ ## [1.20.0] - 2026-03-31
16
+
17
+ ### Added
18
+ - **Rate limiting & retry** — automatic FLOOD_WAIT handling, network error recovery with exponential backoff (`src/rate-limiter.ts`)
19
+ - `send-message` now returns `messageId` in the response (`Message sent to @user [#12345]`), enabling send → edit workflows (closes #16)
20
+ - Rate limiter unit tests (7 tests in `src/__tests__/rate-limiter.test.ts`)
21
+
22
+ ### Changed
23
+ - `sendMessage()` return type changed from `void` to `Api.Message | Api.UpdateShortSentMessage | undefined`
24
+ - Write methods (`sendMessage`, `sendFile`, `editMessage`, `deleteMessages`) are now rate-limited with automatic retry on transient errors
25
+
26
+ ## [1.19.0] - 2026-03-30
27
+
28
+ ### Added
29
+ - Docker support for containerized deployment
30
+ - Non-blocking startup behavior
31
+ - Local QR code fallback for authentication
32
+ - Automated test infrastructure with Node.js test runner
33
+ - CI workflow to publish Docker images to GitHub Container Registry
34
+
35
+ ### Changed
36
+ - Added pnpm-lock.yaml for better dependency management
37
+
38
+ ## [1.18.0] - 2026-03-28
39
+
40
+ ### Added
41
+ - New `telegram-get-my-role` tool to check user's role in a chat
42
+ - Role information in `telegram-get-chat-members` results
43
+
44
+ ## [1.17.0] - 2026-03-28
45
+
46
+ ### Added
47
+ - Chat resolution by display name (not just ID or username)
48
+
49
+ ### Changed
50
+ - Updated documentation to replace static tool list with auto-discovery note
51
+ - Improved project structure documentation
52
+
53
+ ## [1.16.0] - 2026-03-28
54
+
55
+ ### Added
56
+ - Group management tools: invite, kick, ban, edit, leave
57
+ - Admin management capabilities
58
+
59
+ ## [1.15.0] - 2026-03-28
60
+
61
+ ### Added
62
+ - `telegram-create-group` tool for creating new groups
63
+
64
+ ### Fixed
65
+ - Documented `AUTH_KEY_DUPLICATED` error handling
66
+
67
+ ## [1.14.0] - 2026-03-28
68
+
69
+ ### Added
70
+ - SOCKS5 proxy support for Telegram connections
71
+ - MTProxy support for Telegram connections
72
+
73
+ ### Changed
74
+ - Updated Biome to 2.4.9 with new config schema
75
+ - Sorted imports for Biome compliance
76
+ - Added proxy documentation to README
77
+
78
+ ## [1.13.0] - 2026-03-26
79
+
80
+ ### Changed
81
+ - Refactored tools into modular files organized by category
82
+
83
+ ## [1.12.0] - 2026-03-26
84
+
85
+ ### Changed
86
+ - Migrated to `registerTool()` API with tool annotations
87
+
88
+ ## [1.11.1] - 2026-03-25
89
+
90
+ ### Fixed
91
+ - Sanitized unpaired UTF-16 surrogates in tool responses
92
+
93
+ ### Changed
94
+ - Upgraded TypeScript to 6.0
95
+ - Updated README with missing tools
96
+
97
+ ## [1.11.0] - 2026-03-23
98
+
99
+ ### Added
100
+ - Full reactions support: read, send multiple reactions, get detailed info
101
+
102
+ ### Changed
103
+ - Included message ID in all message-reading tool outputs
104
+
105
+ ## [1.10.1] - 2026-03-22
106
+
107
+ ### Fixed
108
+ - Message ID now included in all message-reading tool outputs
109
+
110
+ ## [1.10.0] - 2026-03-20
111
+
112
+ ### Added
113
+ - Enhanced `telegram-get-profile` with birthday, business, and premium data
114
+ - New `telegram-get-profile-photo` tool
115
+ - Global message search capability
116
+ - Enriched chat search results
117
+
118
+ ## [1.9.0] - 2026-03-18
119
+
120
+ ### Added
121
+ - Forum Topics support
122
+ - Per-topic unread count for forum groups
123
+ - Secure session storage with configurable path
124
+ - Multiple accounts support
125
+
126
+ ### Fixed
127
+ - Per-topic unread sum calculation for forum groups
128
+
129
+ ### Changed
130
+ - Updated session path and security documentation
131
+ - Upgraded GitHub Actions to v6
132
+ - Replaced Node 20 with Node 24 in CI
133
+ - Updated Biome to 2.4.7 and @types/node to 25.5.0
134
+
135
+ ## [1.8.1] - 2026-03-19
136
+
137
+ ### Fixed
138
+ - Redirected console.log to stderr to prevent MCP JSON-RPC corruption
139
+
140
+ ### Changed
141
+ - Updated dependencies (Biome 2.4.8)
142
+
143
+ ## [1.8.0] - 2026-03-18
144
+
145
+ ### Added
146
+ - Secure session storage with configurable path via SESSION_PATH environment variable
147
+
148
+ ### Changed
149
+ - Updated session path and security information in README
150
+
151
+ ## [1.7.0] - 2026-03-16
152
+
153
+ ### Added
154
+ - CI workflow to publish to GitHub Packages alongside npm
155
+ - Manual workflow dispatch trigger for publishing
156
+
157
+ ## [1.6.0] - 2026-03-16
158
+
159
+ ### Added
160
+ - Contact request management
161
+ - Block/unblock users
162
+ - Report spam functionality
163
+ - Add contact tool
164
+ - ChatGPT to list of supported clients
165
+
166
+ ### Changed
167
+ - Removed hardcoded tool counts from README and package.json
168
+ - Updated Biome to 2.4.7 and @types/node to 25.5.0
169
+
170
+ ## [1.5.0] - 2026-03-16
171
+
172
+ ### Added
173
+ - Reactions support
174
+ - Scheduled messages
175
+ - Polls creation and management
176
+ - `telegram-join-chat` tool for joining groups and channels
177
+
178
+ ### Changed
179
+ - Updated README with new tool documentation
180
+ - Increased tool count to 24
181
+
182
+ ## [1.4.0] - 2026-03-15
183
+
184
+ ### Added
185
+ - Glama.ai MCP catalog verification (glama.json)
186
+ - Smithery MCP catalog listing (smithery.yaml)
187
+ - Demo GIF and badges to README
188
+ - Hosted version link
189
+
190
+ ### Fixed
191
+ - Removed PNG file save from CLI QR login
192
+
193
+ ### Changed
194
+ - Updated README with Glama MCP server badge
195
+
196
+ ## [1.3.1] - 2026-03-12
197
+
198
+ ### Fixed
199
+ - Use `destroy()` instead of `disconnect()` to stop GramJS update loop
200
+ - Adopt QR login client directly instead of destroy+reconnect flow
201
+ - Destroy GramJS client in `logOut()` and `startQrLogin()` to stop update loop
202
+
203
+ ## [1.3.0] - 2026-03-12
204
+
205
+ ### Added
206
+ - `logOut()` method for complete Telegram session termination
207
+
208
+ ## [1.2.0] - 2026-03-11
209
+
210
+ ### Added
211
+ - `downloadMediaAsBuffer` for serverless media download
212
+ - Library exports and declaration types
213
+ - Date filters for messages
214
+ - Comprehensive README for v1.0
215
+
216
+ ### Fixed
217
+ - MIME type detection from magic bytes in `downloadMediaAsBuffer`
218
+ - Made `saveSession` resilient to file write errors in Docker
219
+
220
+ ### Changed
221
+ - Use `GetFullChannel`/`GetFullChat` for complete chat information
222
+ - Improved `telegram-login` for Claude Desktop users
223
+ - Added npm publishing support and GitHub Actions CI/CD
224
+
225
+ ## [1.1.0] - 2026-03-11
226
+
227
+ ### Added
228
+ - Contact management tools
229
+ - Chat members listing
230
+ - User profile retrieval
231
+ - Chat type filter
232
+ - Media tools (send, download, get info)
233
+ - Pin/unpin messages
234
+ - Markdown support
235
+ - Media information in messages
236
+ - Unread counts
237
+ - Mark messages as read
238
+ - Forward messages
239
+ - Edit messages
240
+ - Delete messages
241
+ - Detailed chat information
242
+ - Pagination support
243
+
244
+ ## [1.0.0] - 2026-03-10
245
+
246
+ ### Added
247
+ - Initial release: MCP server for Telegram userbot
248
+ - Basic message reading and sending
249
+ - Chat listing
250
+ - Authentication via phone number and QR code
251
+ - Session persistence
252
+ - GramJS/MTProto integration
253
+
254
+ [Unreleased]: https://github.com/overpod/mcp-telegram/compare/v1.19.0...HEAD
255
+ [1.19.0]: https://github.com/overpod/mcp-telegram/compare/v1.18.0...v1.19.0
256
+ [1.18.0]: https://github.com/overpod/mcp-telegram/compare/v1.17.0...v1.18.0
257
+ [1.17.0]: https://github.com/overpod/mcp-telegram/compare/v1.16.0...v1.17.0
258
+ [1.16.0]: https://github.com/overpod/mcp-telegram/compare/v1.15.0...v1.16.0
259
+ [1.15.0]: https://github.com/overpod/mcp-telegram/compare/v1.14.0...v1.15.0
260
+ [1.14.0]: https://github.com/overpod/mcp-telegram/compare/v1.13.0...v1.14.0
261
+ [1.13.0]: https://github.com/overpod/mcp-telegram/compare/v1.12.0...v1.13.0
262
+ [1.12.0]: https://github.com/overpod/mcp-telegram/compare/v1.11.1...v1.12.0
263
+ [1.11.1]: https://github.com/overpod/mcp-telegram/compare/v1.11.0...v1.11.1
264
+ [1.11.0]: https://github.com/overpod/mcp-telegram/compare/v1.10.1...v1.11.0
265
+ [1.10.1]: https://github.com/overpod/mcp-telegram/compare/v1.10.0...v1.10.1
266
+ [1.10.0]: https://github.com/overpod/mcp-telegram/compare/v1.9.0...v1.10.0
267
+ [1.9.0]: https://github.com/overpod/mcp-telegram/compare/v1.8.1...v1.9.0
268
+ [1.8.1]: https://github.com/overpod/mcp-telegram/compare/v1.8.0...v1.8.1
269
+ [1.8.0]: https://github.com/overpod/mcp-telegram/compare/v1.7.0...v1.8.0
270
+ [1.7.0]: https://github.com/overpod/mcp-telegram/compare/v1.6.0...v1.7.0
271
+ [1.6.0]: https://github.com/overpod/mcp-telegram/compare/v1.5.0...v1.6.0
272
+ [1.5.0]: https://github.com/overpod/mcp-telegram/compare/v1.4.0...v1.5.0
273
+ [1.4.0]: https://github.com/overpod/mcp-telegram/compare/v1.3.1...v1.4.0
274
+ [1.3.1]: https://github.com/overpod/mcp-telegram/compare/v1.3.0...v1.3.1
275
+ [1.3.0]: https://github.com/overpod/mcp-telegram/compare/v1.2.0...v1.3.0
276
+ [1.2.0]: https://github.com/overpod/mcp-telegram/compare/v1.1.0...v1.2.0
277
+ [1.1.0]: https://github.com/overpod/mcp-telegram/compare/v1.0.0...v1.1.0
278
+ [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,5 @@
1
+ import { TelegramClient } from "telegram";
2
+ import { Api } from "telegram/tl/index.js";
1
3
  export declare class TelegramService {
2
4
  private client;
3
5
  private apiId;
@@ -5,8 +7,10 @@ export declare class TelegramService {
5
7
  private sessionString;
6
8
  private connected;
7
9
  private sessionPath;
10
+ private rateLimiter;
8
11
  lastError: string;
9
12
  get sessionDir(): string;
13
+ getClient(): TelegramClient | null;
10
14
  constructor(apiId: number, apiHash: string, options?: {
11
15
  sessionPath?: string;
12
16
  });
@@ -37,7 +41,7 @@ export declare class TelegramService {
37
41
  username?: string;
38
42
  firstName?: string;
39
43
  }>;
40
- sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<void>;
44
+ sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<Api.Message | Api.UpdateShortSentMessage | undefined>;
41
45
  sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
42
46
  downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
43
47
  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,10 +51,14 @@ 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);
55
58
  }
59
+ getClient() {
60
+ return this.client;
61
+ }
56
62
  constructor(apiId, apiHash, options) {
57
63
  this.apiId = apiId;
58
64
  this.apiHash = apiHash;
@@ -136,7 +142,7 @@ export class TelegramService {
136
142
  // Auth revoked — delete invalid session
137
143
  if (msg === "AUTH_KEY_UNREGISTERED" || msg === "SESSION_REVOKED" || msg === "USER_DEACTIVATED") {
138
144
  await this.clearSession();
139
- this.lastError = "Session revoked. Re-login required.";
145
+ this.lastError = "Session revoked. Run telegram-login to re-authenticate.";
140
146
  }
141
147
  // Network error — keep session, just report
142
148
  else if (msg.includes("TIMEOUT") ||
@@ -144,7 +150,7 @@ export class TelegramService {
144
150
  msg.includes("ENETUNREACH") ||
145
151
  msg.includes("ENOTFOUND") ||
146
152
  msg.includes("network")) {
147
- this.lastError = `Network error: ${msg}. Session preserved, will retry on next call.`;
153
+ this.lastError = `Network error: ${msg}. Run telegram-status to retry connection.`;
148
154
  }
149
155
  // Unknown error
150
156
  else {
@@ -290,7 +296,7 @@ export class TelegramService {
290
296
  }
291
297
  async getMe() {
292
298
  if (!this.client || !this.connected)
293
- throw new Error("Not connected");
299
+ throw new Error(NOT_CONNECTED_ERROR);
294
300
  const me = await this.client.getMe();
295
301
  const user = me;
296
302
  return {
@@ -301,38 +307,47 @@ export class TelegramService {
301
307
  }
302
308
  async sendMessage(chatId, text, replyTo, parseMode, topicId) {
303
309
  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, {
310
+ throw new Error(NOT_CONNECTED_ERROR);
311
+ return this.rateLimiter.execute(async () => {
312
+ const resolved = await this.resolvePeer(chatId);
313
+ if (topicId) {
314
+ const peer = await this.client?.getInputEntity(resolved);
315
+ const result = await this.client?.invoke(new Api.messages.SendMessage({
316
+ peer,
317
+ message: text,
318
+ randomId: bigInt(Math.floor(Math.random() * 1e15)),
319
+ replyTo: new Api.InputReplyToMessage({
320
+ replyToMsgId: replyTo ?? topicId,
321
+ topMsgId: topicId,
322
+ }),
323
+ }));
324
+ if (result instanceof Api.UpdateShortSentMessage)
325
+ return result;
326
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
327
+ const msgUpdate = result.updates.find((u) => u instanceof Api.UpdateNewMessage);
328
+ if (msgUpdate?.message instanceof Api.Message)
329
+ return msgUpdate.message;
330
+ }
331
+ return undefined;
332
+ }
333
+ return await this.client?.sendMessage(resolved, {
321
334
  message: text,
322
335
  ...(replyTo ? { replyTo } : {}),
323
336
  ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
324
337
  });
325
- }
338
+ }, `sendMessage to ${chatId}`);
326
339
  }
327
340
  async sendFile(chatId, filePath, caption) {
328
341
  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 });
342
+ throw new Error(NOT_CONNECTED_ERROR);
343
+ await this.rateLimiter.execute(async () => {
344
+ const resolved = await this.resolvePeer(chatId);
345
+ await this.client?.sendFile(resolved, { file: filePath, caption });
346
+ }, `sendFile to ${chatId}`);
332
347
  }
333
348
  async downloadMedia(chatId, messageId, downloadPath) {
334
349
  if (!this.client || !this.connected)
335
- throw new Error("Not connected");
350
+ throw new Error(NOT_CONNECTED_ERROR);
336
351
  const resolved = await this.resolvePeer(chatId);
337
352
  const messages = await this.client.getMessages(resolved, { ids: [messageId] });
338
353
  const message = messages[0];
@@ -348,7 +363,7 @@ export class TelegramService {
348
363
  }
349
364
  async downloadMediaAsBuffer(chatId, messageId) {
350
365
  if (!this.client || !this.connected)
351
- throw new Error("Not connected");
366
+ throw new Error(NOT_CONNECTED_ERROR);
352
367
  const resolved = await this.resolvePeer(chatId);
353
368
  const messages = await this.client.getMessages(resolved, { ids: [messageId] });
354
369
  const message = messages[0];
@@ -384,19 +399,19 @@ export class TelegramService {
384
399
  }
385
400
  async pinMessage(chatId, messageId, silent = false) {
386
401
  if (!this.client || !this.connected)
387
- throw new Error("Not connected");
402
+ throw new Error(NOT_CONNECTED_ERROR);
388
403
  const resolved = await this.resolvePeer(chatId);
389
404
  await this.client.pinMessage(resolved, messageId, { notify: !silent });
390
405
  }
391
406
  async unpinMessage(chatId, messageId) {
392
407
  if (!this.client || !this.connected)
393
- throw new Error("Not connected");
408
+ throw new Error(NOT_CONNECTED_ERROR);
394
409
  const resolved = await this.resolvePeer(chatId);
395
410
  await this.client.unpinMessage(resolved, messageId);
396
411
  }
397
412
  async getDialogs(limit = 20, offsetDate, filterType) {
398
413
  if (!this.client || !this.connected)
399
- throw new Error("Not connected");
414
+ throw new Error(NOT_CONNECTED_ERROR);
400
415
  const fetchLimit = filterType ? limit * 3 : limit;
401
416
  const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
402
417
  const mapped = dialogs.map((d) => {
@@ -419,7 +434,7 @@ export class TelegramService {
419
434
  }
420
435
  async getUnreadDialogs(limit = 20) {
421
436
  if (!this.client || !this.connected)
422
- throw new Error("Not connected");
437
+ throw new Error(NOT_CONNECTED_ERROR);
423
438
  const dialogs = await this.client.getDialogs({ limit: limit * 3 });
424
439
  const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
425
440
  const results = await Promise.all(unread.map(async (d) => {
@@ -460,7 +475,7 @@ export class TelegramService {
460
475
  }
461
476
  async getContactRequests(limit = 20) {
462
477
  if (!this.client || !this.connected)
463
- throw new Error("Not connected");
478
+ throw new Error(NOT_CONNECTED_ERROR);
464
479
  const dialogs = await this.client.getDialogs({ limit: limit * 5 });
465
480
  return dialogs
466
481
  .filter((d) => {
@@ -485,7 +500,7 @@ export class TelegramService {
485
500
  }
486
501
  async addContact(userId, firstName, lastName, phone) {
487
502
  if (!this.client || !this.connected)
488
- throw new Error("Not connected");
503
+ throw new Error(NOT_CONNECTED_ERROR);
489
504
  const entity = await this.client.getInputEntity(userId);
490
505
  await this.client.invoke(new Api.contacts.AddContact({
491
506
  id: entity,
@@ -496,39 +511,43 @@ export class TelegramService {
496
511
  }
497
512
  async blockUser(userId) {
498
513
  if (!this.client || !this.connected)
499
- throw new Error("Not connected");
514
+ throw new Error(NOT_CONNECTED_ERROR);
500
515
  const entity = await this.client.getInputEntity(userId);
501
516
  await this.client.invoke(new Api.contacts.Block({ id: entity }));
502
517
  }
503
518
  async reportSpam(chatId) {
504
519
  if (!this.client || !this.connected)
505
- throw new Error("Not connected");
520
+ throw new Error(NOT_CONNECTED_ERROR);
506
521
  const peer = await this.client.getInputEntity(chatId);
507
522
  await this.client.invoke(new Api.messages.ReportSpam({ peer }));
508
523
  }
509
524
  async markAsRead(chatId) {
510
525
  if (!this.client || !this.connected)
511
- throw new Error("Not connected");
526
+ throw new Error(NOT_CONNECTED_ERROR);
512
527
  await this.client.markAsRead(chatId);
513
528
  }
514
529
  async forwardMessage(fromChatId, toChatId, messageIds) {
515
530
  if (!this.client || !this.connected)
516
- throw new Error("Not connected");
531
+ throw new Error(NOT_CONNECTED_ERROR);
517
532
  const resolvedFrom = await this.resolvePeer(fromChatId);
518
533
  const resolvedTo = await this.resolvePeer(toChatId);
519
534
  await this.client.forwardMessages(resolvedTo, { messages: messageIds, fromPeer: resolvedFrom });
520
535
  }
521
536
  async editMessage(chatId, messageId, newText) {
522
537
  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 });
538
+ throw new Error(NOT_CONNECTED_ERROR);
539
+ await this.rateLimiter.execute(async () => {
540
+ const resolved = await this.resolvePeer(chatId);
541
+ await this.client?.editMessage(resolved, { message: messageId, text: newText });
542
+ }, `editMessage ${messageId} in ${chatId}`);
526
543
  }
527
544
  async deleteMessages(chatId, messageIds) {
528
545
  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 });
546
+ throw new Error(NOT_CONNECTED_ERROR);
547
+ await this.rateLimiter.execute(async () => {
548
+ const resolved = await this.resolvePeer(chatId);
549
+ await this.client?.deleteMessages(resolved, messageIds, { revoke: true });
550
+ }, `deleteMessages in ${chatId}`);
532
551
  }
533
552
  /**
534
553
  * Resolve a chat by ID, username, or display name.
@@ -537,7 +556,7 @@ export class TelegramService {
537
556
  // biome-ignore lint: GramJS has no proper entity union type
538
557
  async resolveChat(chatId) {
539
558
  if (!this.client)
540
- throw new Error("Not connected");
559
+ throw new Error(NOT_CONNECTED_ERROR);
541
560
  // First try direct resolve (numeric ID, username, phone)
542
561
  try {
543
562
  return await this.client.getEntity(chatId);
@@ -576,7 +595,7 @@ export class TelegramService {
576
595
  }
577
596
  async getChatInfo(chatId) {
578
597
  if (!this.client || !this.connected)
579
- throw new Error("Not connected");
598
+ throw new Error(NOT_CONNECTED_ERROR);
580
599
  const entity = await this.resolveChat(chatId);
581
600
  if (entity instanceof Api.User) {
582
601
  const parts = [entity.firstName, entity.lastName].filter(Boolean);
@@ -684,7 +703,7 @@ export class TelegramService {
684
703
  }
685
704
  async getMessages(chatId, limit = 10, offsetId, minDate, maxDate) {
686
705
  if (!this.client || !this.connected)
687
- throw new Error("Not connected");
706
+ throw new Error(NOT_CONNECTED_ERROR);
688
707
  const resolved = await this.resolvePeer(chatId);
689
708
  const opts = {
690
709
  limit,
@@ -708,7 +727,7 @@ export class TelegramService {
708
727
  }
709
728
  async searchChats(query, limit = 10) {
710
729
  if (!this.client || !this.connected)
711
- throw new Error("Not connected");
730
+ throw new Error(NOT_CONNECTED_ERROR);
712
731
  const result = await this.client.invoke(new Api.contacts.Search({ q: query, limit }));
713
732
  const chats = [];
714
733
  for (const user of result.users) {
@@ -769,7 +788,7 @@ export class TelegramService {
769
788
  }
770
789
  async searchGlobal(query, limit = 20, minDate, maxDate) {
771
790
  if (!this.client || !this.connected)
772
- throw new Error("Not connected");
791
+ throw new Error(NOT_CONNECTED_ERROR);
773
792
  const result = await this.client.invoke(new Api.messages.SearchGlobal({
774
793
  q: query,
775
794
  filter: new Api.InputMessagesFilterEmpty(),
@@ -838,7 +857,7 @@ export class TelegramService {
838
857
  }
839
858
  async searchMessages(chatId, query, limit = 20, minDate, maxDate) {
840
859
  if (!this.client || !this.connected)
841
- throw new Error("Not connected");
860
+ throw new Error(NOT_CONNECTED_ERROR);
842
861
  const resolved = await this.resolvePeer(chatId);
843
862
  const messages = await this.client.getMessages(resolved, {
844
863
  search: query,
@@ -861,7 +880,7 @@ export class TelegramService {
861
880
  }
862
881
  async getContacts(limit = 50) {
863
882
  if (!this.client || !this.connected)
864
- throw new Error("Not connected");
883
+ throw new Error(NOT_CONNECTED_ERROR);
865
884
  const result = await this.client.invoke(new Api.contacts.GetContacts({ hash: bigInt(0) }));
866
885
  if (!(result instanceof Api.contacts.Contacts))
867
886
  return [];
@@ -881,7 +900,7 @@ export class TelegramService {
881
900
  }
882
901
  async getChatMembers(chatId, limit = 50) {
883
902
  if (!this.client || !this.connected)
884
- throw new Error("Not connected");
903
+ throw new Error(NOT_CONNECTED_ERROR);
885
904
  const entity = await this.resolveChat(chatId);
886
905
  if (entity instanceof Api.Channel) {
887
906
  const result = await this.client.invoke(new Api.channels.GetParticipants({
@@ -926,7 +945,7 @@ export class TelegramService {
926
945
  }
927
946
  async getMyRole(chatId) {
928
947
  if (!this.client || !this.connected)
929
- throw new Error("Not connected");
948
+ throw new Error(NOT_CONNECTED_ERROR);
930
949
  const entity = await this.resolveChat(chatId);
931
950
  const me = await this.getMe();
932
951
  if (entity instanceof Api.Channel) {
@@ -978,7 +997,7 @@ export class TelegramService {
978
997
  }
979
998
  async getProfile(userId) {
980
999
  if (!this.client || !this.connected)
981
- throw new Error("Not connected");
1000
+ throw new Error(NOT_CONNECTED_ERROR);
982
1001
  const entity = await this.client.getEntity(userId);
983
1002
  if (!(entity instanceof Api.User))
984
1003
  throw new Error("Entity is not a user");
@@ -1038,7 +1057,7 @@ export class TelegramService {
1038
1057
  }
1039
1058
  async downloadProfilePhoto(entityId, options) {
1040
1059
  if (!this.client || !this.connected)
1041
- throw new Error("Not connected");
1060
+ throw new Error(NOT_CONNECTED_ERROR);
1042
1061
  const entity = await this.client.getEntity(entityId);
1043
1062
  const buffer = (await this.client.downloadProfilePhoto(entity, {
1044
1063
  isBig: options?.isBig !== false,
@@ -1089,7 +1108,7 @@ export class TelegramService {
1089
1108
  }
1090
1109
  async sendReaction(chatId, messageId, emoji, addToExisting = false) {
1091
1110
  if (!this.client || !this.connected)
1092
- throw new Error("Not connected");
1111
+ throw new Error(NOT_CONNECTED_ERROR);
1093
1112
  const resolved = await this.resolvePeer(chatId);
1094
1113
  const peer = await this.client.getInputEntity(resolved);
1095
1114
  const reactionList = [];
@@ -1129,7 +1148,7 @@ export class TelegramService {
1129
1148
  }
1130
1149
  async getMessageReactions(chatId, messageId) {
1131
1150
  if (!this.client || !this.connected)
1132
- throw new Error("Not connected");
1151
+ throw new Error(NOT_CONNECTED_ERROR);
1133
1152
  const resolved = await this.resolvePeer(chatId);
1134
1153
  const peer = await this.client.getInputEntity(resolved);
1135
1154
  // First get the message to know which reactions exist
@@ -1184,7 +1203,7 @@ export class TelegramService {
1184
1203
  }
1185
1204
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
1186
1205
  if (!this.client || !this.connected)
1187
- throw new Error("Not connected");
1206
+ throw new Error(NOT_CONNECTED_ERROR);
1188
1207
  const resolved = await this.resolvePeer(chatId);
1189
1208
  await this.client.sendMessage(resolved, {
1190
1209
  message: text,
@@ -1195,7 +1214,7 @@ export class TelegramService {
1195
1214
  }
1196
1215
  async createPoll(chatId, question, answers, options) {
1197
1216
  if (!this.client || !this.connected)
1198
- throw new Error("Not connected");
1217
+ throw new Error(NOT_CONNECTED_ERROR);
1199
1218
  const peer = await this.client.getInputEntity(chatId);
1200
1219
  const pollAnswers = answers.map((text, i) => new Api.PollAnswer({
1201
1220
  text: new Api.TextWithEntities({ text, entities: [] }),
@@ -1231,7 +1250,7 @@ export class TelegramService {
1231
1250
  }
1232
1251
  async getForumTopics(chatId, limit = 100) {
1233
1252
  if (!this.client || !this.connected)
1234
- throw new Error("Not connected");
1253
+ throw new Error(NOT_CONNECTED_ERROR);
1235
1254
  const entity = await this.resolveChat(chatId);
1236
1255
  if (!(entity instanceof Api.Channel))
1237
1256
  throw new Error("Forum topics are only available in supergroups");
@@ -1260,7 +1279,7 @@ export class TelegramService {
1260
1279
  }
1261
1280
  async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
1262
1281
  if (!this.client || !this.connected)
1263
- throw new Error("Not connected");
1282
+ throw new Error(NOT_CONNECTED_ERROR);
1264
1283
  const peer = await this.client.getInputEntity(chatId);
1265
1284
  const result = await this.client.invoke(new Api.messages.GetReplies({
1266
1285
  peer,
@@ -1289,7 +1308,7 @@ export class TelegramService {
1289
1308
  /** Check if a chat entity is a forum (has topics enabled) */
1290
1309
  async isForum(chatId) {
1291
1310
  if (!this.client || !this.connected)
1292
- throw new Error("Not connected");
1311
+ throw new Error(NOT_CONNECTED_ERROR);
1293
1312
  try {
1294
1313
  const entity = await this.resolveChat(chatId);
1295
1314
  if (entity instanceof Api.Channel) {
@@ -1301,7 +1320,7 @@ export class TelegramService {
1301
1320
  }
1302
1321
  async joinChat(target) {
1303
1322
  if (!this.client)
1304
- throw new Error("Not connected");
1323
+ throw new Error(NOT_CONNECTED_ERROR);
1305
1324
  // Extract invite hash from various link formats
1306
1325
  const inviteMatch = target.match(/(?:t\.me\/\+|t\.me\/joinchat\/|tg:\/\/join\?invite=)([a-zA-Z0-9_-]+)/);
1307
1326
  if (inviteMatch) {
@@ -1332,7 +1351,7 @@ export class TelegramService {
1332
1351
  }
1333
1352
  async createGroup(options) {
1334
1353
  if (!this.client)
1335
- throw new Error("Not connected");
1354
+ throw new Error(NOT_CONNECTED_ERROR);
1336
1355
  const { title, users, supergroup = false, forum = false, description } = options;
1337
1356
  if (supergroup || forum) {
1338
1357
  // Create supergroup/channel via channels.CreateChannel
@@ -1406,7 +1425,7 @@ export class TelegramService {
1406
1425
  }
1407
1426
  async inviteToGroup(chatId, users) {
1408
1427
  if (!this.client)
1409
- throw new Error("Not connected");
1428
+ throw new Error(NOT_CONNECTED_ERROR);
1410
1429
  const entity = await this.resolveChat(chatId);
1411
1430
  const invited = [];
1412
1431
  const failed = [];
@@ -1434,7 +1453,7 @@ export class TelegramService {
1434
1453
  }
1435
1454
  async kickUser(chatId, userId) {
1436
1455
  if (!this.client)
1437
- throw new Error("Not connected");
1456
+ throw new Error(NOT_CONNECTED_ERROR);
1438
1457
  const entity = await this.resolveChat(chatId);
1439
1458
  const user = await this.client.getEntity(userId);
1440
1459
  if (!(user instanceof Api.User))
@@ -1459,7 +1478,7 @@ export class TelegramService {
1459
1478
  }
1460
1479
  async banUser(chatId, userId) {
1461
1480
  if (!this.client)
1462
- throw new Error("Not connected");
1481
+ throw new Error(NOT_CONNECTED_ERROR);
1463
1482
  const entity = await this.resolveChat(chatId);
1464
1483
  const user = await this.client.getEntity(userId);
1465
1484
  if (!(user instanceof Api.User))
@@ -1475,7 +1494,7 @@ export class TelegramService {
1475
1494
  }
1476
1495
  async unbanUser(chatId, userId) {
1477
1496
  if (!this.client)
1478
- throw new Error("Not connected");
1497
+ throw new Error(NOT_CONNECTED_ERROR);
1479
1498
  const entity = await this.resolveChat(chatId);
1480
1499
  const user = await this.client.getEntity(userId);
1481
1500
  if (!(user instanceof Api.User))
@@ -1491,7 +1510,7 @@ export class TelegramService {
1491
1510
  }
1492
1511
  async editGroup(chatId, options) {
1493
1512
  if (!this.client)
1494
- throw new Error("Not connected");
1513
+ throw new Error(NOT_CONNECTED_ERROR);
1495
1514
  const entity = await this.resolveChat(chatId);
1496
1515
  if (options.title) {
1497
1516
  if (entity instanceof Api.Channel) {
@@ -1521,7 +1540,7 @@ export class TelegramService {
1521
1540
  }
1522
1541
  async leaveGroup(chatId) {
1523
1542
  if (!this.client)
1524
- throw new Error("Not connected");
1543
+ throw new Error(NOT_CONNECTED_ERROR);
1525
1544
  const entity = await this.resolveChat(chatId);
1526
1545
  if (entity instanceof Api.Channel) {
1527
1546
  await this.client.invoke(new Api.channels.LeaveChannel({ channel: entity }));
@@ -1538,7 +1557,7 @@ export class TelegramService {
1538
1557
  }
1539
1558
  async setAdmin(chatId, userId, options) {
1540
1559
  if (!this.client)
1541
- throw new Error("Not connected");
1560
+ throw new Error(NOT_CONNECTED_ERROR);
1542
1561
  const entity = await this.resolveChat(chatId);
1543
1562
  if (!(entity instanceof Api.Channel))
1544
1563
  throw new Error("Set admin is only supported for supergroups and channels");
@@ -1564,7 +1583,7 @@ export class TelegramService {
1564
1583
  }
1565
1584
  async removeAdmin(chatId, userId) {
1566
1585
  if (!this.client)
1567
- throw new Error("Not connected");
1586
+ throw new Error(NOT_CONNECTED_ERROR);
1568
1587
  const entity = await this.resolveChat(chatId);
1569
1588
  if (!(entity instanceof Api.Channel))
1570
1589
  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.21.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",