@fre4x/telegram 1.1.5 → 1.1.7

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.
Files changed (2) hide show
  1. package/dist/index.js +310 -41
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28236,9 +28236,9 @@ var StdioServerTransport = class {
28236
28236
  var import_dotenv = __toESM(require_main(), 1);
28237
28237
  import { Telegraf } from "telegraf";
28238
28238
  import { createHash } from "node:crypto";
28239
- import * as fs from "fs";
28239
+ import * as fs from "node:fs";
28240
28240
  import os from "node:os";
28241
- import * as path from "path";
28241
+ import * as path from "node:path";
28242
28242
 
28243
28243
  // ../node_modules/zod/index.js
28244
28244
  var zod_exports = {};
@@ -28587,8 +28587,107 @@ function applyPagination(items, params) {
28587
28587
  };
28588
28588
  }
28589
28589
 
28590
+ // src/channel.ts
28591
+ var MAX_CHANNEL_CONTENT_BYTES = 8192;
28592
+ var ChannelPermissionRequestNotificationSchema = external_exports3.object({
28593
+ method: external_exports3.literal("notifications/claude/channel/permission_request"),
28594
+ params: external_exports3.object({
28595
+ request_id: external_exports3.string().min(1),
28596
+ tool_name: external_exports3.string(),
28597
+ description: external_exports3.string(),
28598
+ input_preview: external_exports3.string().optional()
28599
+ })
28600
+ });
28601
+ function coerceMetaValue(value) {
28602
+ if (value === null || value === void 0) return "";
28603
+ if (typeof value === "string") return value;
28604
+ if (typeof value === "number" || typeof value === "boolean") {
28605
+ return String(value);
28606
+ }
28607
+ return JSON.stringify(value);
28608
+ }
28609
+ function buildChannelMeta(meta3) {
28610
+ const result = {};
28611
+ for (const [key, value] of Object.entries(meta3)) {
28612
+ result[key] = coerceMetaValue(value);
28613
+ }
28614
+ return result;
28615
+ }
28616
+ function truncateChannelContent(content) {
28617
+ const bytes = Buffer.byteLength(content, "utf8");
28618
+ if (bytes > MAX_CHANNEL_CONTENT_BYTES) {
28619
+ console.error(
28620
+ `[telegram:channel] dropping message (${bytes} bytes > ${MAX_CHANNEL_CONTENT_BYTES})`
28621
+ );
28622
+ return null;
28623
+ }
28624
+ return content;
28625
+ }
28626
+ async function sendChannelNotification(server2, content, meta3) {
28627
+ const safeContent = truncateChannelContent(content);
28628
+ if (!safeContent) return false;
28629
+ try {
28630
+ await server2.notification({
28631
+ method: "claude/channel",
28632
+ params: {
28633
+ content: safeContent,
28634
+ meta: meta3 ?? {}
28635
+ }
28636
+ });
28637
+ return true;
28638
+ } catch (error48) {
28639
+ console.error("[telegram:channel] failed to send notification:", error48);
28640
+ return false;
28641
+ }
28642
+ }
28643
+ async function sendPermissionVerdict(server2, requestId, behavior) {
28644
+ try {
28645
+ await server2.notification({
28646
+ method: "claude/channel/permission",
28647
+ params: {
28648
+ request_id: requestId,
28649
+ behavior
28650
+ }
28651
+ });
28652
+ return true;
28653
+ } catch (error48) {
28654
+ console.error(
28655
+ `[telegram:channel] failed to send permission verdict (${behavior}):`,
28656
+ error48
28657
+ );
28658
+ return false;
28659
+ }
28660
+ }
28661
+ function formatPermissionRequestMessage(params) {
28662
+ const preview = params.input_preview?.trim();
28663
+ const lines = [
28664
+ "Tool approval requested",
28665
+ "",
28666
+ `Tool: ${params.tool_name}`,
28667
+ params.description
28668
+ ];
28669
+ if (preview) {
28670
+ lines.push("", "Preview:", preview);
28671
+ }
28672
+ return lines.join("\n");
28673
+ }
28674
+ function parsePermissionCallbackData(data) {
28675
+ const match = /^perm:(allow|deny):([a-f0-9]{32})$/.exec(data);
28676
+ if (!match) return null;
28677
+ return {
28678
+ behavior: match[1],
28679
+ requestId: match[2]
28680
+ };
28681
+ }
28682
+ function buildPermissionCallbackData(behavior, requestId) {
28683
+ return `perm:${behavior}:${requestId}`;
28684
+ }
28685
+
28590
28686
  // src/index.ts
28591
28687
  import_dotenv.default.config();
28688
+ function isCallbackQueryUpdate(update) {
28689
+ return typeof update === "object" && update !== null && "callback_query" in update && update.callback_query !== void 0;
28690
+ }
28592
28691
  function getStatePath() {
28593
28692
  if (process.env.TELEGRAM_STATE_PATH) {
28594
28693
  return process.env.TELEGRAM_STATE_PATH;
@@ -28599,29 +28698,29 @@ function getStatePath() {
28599
28698
  var window = new JSDOM("").window;
28600
28699
  var DOMPurify = createDOMPurify(window);
28601
28700
  var IS_MOCK = process.env.MOCK === "true" || process.env.TELEGRAM_MOCK === "true";
28602
- var botToken = process.env.TELEGRAM_BOT_TOKEN;
28603
- var allowedUserId = process.env.ALLOWED_USER_ID;
28604
- var allowedRecipients = process.env.ALLOWED_RECIPIENTS?.split(",").map((id) => id.trim()) || [];
28701
+ var botToken = process.env.TELEGRAM_BOT_TOKEN || process.env.BOT_TOKEN;
28702
+ var MAX_TOKEN_REDACT_LENGTH = 256;
28703
+ function redactBotToken(message) {
28704
+ if (!botToken || !message.includes(botToken)) {
28705
+ return message;
28706
+ }
28707
+ if (botToken.length > MAX_TOKEN_REDACT_LENGTH) {
28708
+ return message.split(botToken).join("[REDACTED_TOKEN]");
28709
+ }
28710
+ const escapedToken = botToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28711
+ return message.replace(new RegExp(escapedToken, "g"), "[REDACTED_TOKEN]");
28712
+ }
28713
+ var allowedUserId = process.env.ALLOWED_USER_ID || process.env.TELEGRAM_ALLOWED_USER_ID || process.env.TELEGRAM_CHAT_ID || process.env.CHAT_ID;
28714
+ var rawAllowedRecipients = process.env.ALLOWED_RECIPIENTS || process.env.TELEGRAM_ALLOWED_RECIPIENTS;
28715
+ var allowedRecipients = rawAllowedRecipients?.split(",").map((id) => id.trim()).filter(Boolean) || [];
28605
28716
  var enableRecipientWhitelist = process.env.ENABLE_RECIPIENT_WHITELIST === "true";
28606
28717
  var originalError = console.error;
28607
28718
  console.error = (...args) => {
28608
- let message = args.map((arg) => String(arg)).join(" ");
28609
- if (botToken && message.includes(botToken)) {
28610
- message = message.replace(
28611
- new RegExp(botToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
28612
- "[REDACTED_TOKEN]"
28613
- );
28614
- }
28719
+ const message = redactBotToken(args.map((arg) => String(arg)).join(" "));
28615
28720
  originalError(message);
28616
28721
  };
28617
28722
  process.on("uncaughtException", (err) => {
28618
- let message = err.stack || err.message;
28619
- if (botToken && message.includes(botToken)) {
28620
- message = message.replace(
28621
- new RegExp(botToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
28622
- "[REDACTED_TOKEN]"
28623
- );
28624
- }
28723
+ const message = redactBotToken(err.stack || err.message);
28625
28724
  console.error("Uncaught Exception:", message);
28626
28725
  process.exit(1);
28627
28726
  });
@@ -28630,13 +28729,7 @@ process.on("unhandledRejection", (reason) => {
28630
28729
  if (reason instanceof Error) {
28631
28730
  message = reason.stack || reason.message;
28632
28731
  }
28633
- if (botToken && message.includes(botToken)) {
28634
- message = message.replace(
28635
- new RegExp(botToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
28636
- "[REDACTED_TOKEN]"
28637
- );
28638
- }
28639
- console.error("Unhandled Rejection:", message);
28732
+ console.error("Unhandled Rejection:", redactBotToken(message));
28640
28733
  });
28641
28734
  if (!IS_MOCK && !botToken) {
28642
28735
  console.error(
@@ -28730,14 +28823,16 @@ var server = new Server(
28730
28823
  capabilities: {
28731
28824
  tools: {},
28732
28825
  logging: {},
28733
- // Required by SDK for notifications/message
28734
28826
  experimental: {
28735
28827
  "claude/channel": {
28736
- description: "Telegram events are delivered via channel notifications"
28828
+ description: "Telegram events are delivered via claude/channel notifications"
28829
+ },
28830
+ "claude/channel/permission": {
28831
+ description: "Tool approval requests are relayed to Telegram for allow/deny"
28737
28832
  }
28738
28833
  }
28739
28834
  },
28740
- instructions: "Telegram MCP server. Delivers inbound Telegram messages as channel notifications via notifications/message."
28835
+ instructions: "Telegram MCP server. Inbound Telegram messages are pushed via claude/channel. Tool approvals can be relayed through Telegram when claude/channel/permission is enabled."
28741
28836
  }
28742
28837
  );
28743
28838
  var isImage = (filePath) => {
@@ -28747,6 +28842,10 @@ var isImage = (filePath) => {
28747
28842
  var messageHistory = [];
28748
28843
  var MAX_HISTORY = 100;
28749
28844
  var pollingOffset = 0;
28845
+ var channelReady = false;
28846
+ var channelPollTimer = null;
28847
+ var CHANNEL_POLL_INTERVAL_MS = 3e3;
28848
+ var pendingPermissionRequests = /* @__PURE__ */ new Map();
28750
28849
  var knownChats = /* @__PURE__ */ new Map();
28751
28850
  function loadState() {
28752
28851
  if (IS_MOCK) return;
@@ -28766,7 +28865,10 @@ function loadState() {
28766
28865
  `[telegram:state] loaded ${knownChats.size} chat(s) from ${getStatePath()}`
28767
28866
  );
28768
28867
  } catch (error48) {
28769
- console.error(`[telegram:state] failed to load ${getStatePath()}:`, error48);
28868
+ console.error(
28869
+ `[telegram:state] failed to load ${getStatePath()}:`,
28870
+ error48
28871
+ );
28770
28872
  }
28771
28873
  }
28772
28874
  function persistState() {
@@ -28777,9 +28879,16 @@ function persistState() {
28777
28879
  pollingOffset
28778
28880
  };
28779
28881
  fs.mkdirSync(path.dirname(getStatePath()), { recursive: true });
28780
- fs.writeFileSync(getStatePath(), JSON.stringify(state, null, 2), "utf-8");
28882
+ fs.writeFileSync(
28883
+ getStatePath(),
28884
+ JSON.stringify(state, null, 2),
28885
+ "utf-8"
28886
+ );
28781
28887
  } catch (error48) {
28782
- console.error(`[telegram:state] failed to persist ${getStatePath()}:`, error48);
28888
+ console.error(
28889
+ `[telegram:state] failed to persist ${getStatePath()}:`,
28890
+ error48
28891
+ );
28783
28892
  }
28784
28893
  }
28785
28894
  function reloadState() {
@@ -28797,7 +28906,10 @@ function reloadState() {
28797
28906
  pollingOffset = state.pollingOffset;
28798
28907
  }
28799
28908
  } catch (error48) {
28800
- console.error(`[telegram:state] failed to reload ${getStatePath()}:`, error48);
28909
+ console.error(
28910
+ `[telegram:state] failed to reload ${getStatePath()}:`,
28911
+ error48
28912
+ );
28801
28913
  }
28802
28914
  }
28803
28915
  function chatTitleFromTelegramChat(chat) {
@@ -28955,17 +29067,23 @@ function getActiveChats() {
28955
29067
  );
28956
29068
  }
28957
29069
  async function fetchNewUpdates() {
28958
- if (!bot || IS_MOCK) return;
29070
+ const added = [];
29071
+ if (!bot || IS_MOCK) return added;
28959
29072
  try {
28960
29073
  await ensurePollingMode();
28961
29074
  const updates = await bot.telegram.getUpdates(0, 100, pollingOffset, [
28962
29075
  "message",
28963
29076
  "edited_message",
28964
29077
  "channel_post",
28965
- "my_chat_member"
29078
+ "my_chat_member",
29079
+ "callback_query"
28966
29080
  ]);
28967
29081
  for (const update of updates) {
28968
29082
  pollingOffset = update.update_id + 1;
29083
+ if (isCallbackQueryUpdate(update)) {
29084
+ await handlePermissionCallback(update.callback_query);
29085
+ continue;
29086
+ }
28969
29087
  const msgLike = update;
28970
29088
  if (!msgLike.message && !msgLike.edited_message && !msgLike.channel_post && !msgLike.my_chat_member)
28971
29089
  continue;
@@ -28974,7 +29092,7 @@ async function fetchNewUpdates() {
28974
29092
  if (!msg?.text) continue;
28975
29093
  const senderId = msg.from?.id?.toString() ?? "unknown";
28976
29094
  const chatId = msg.chat.id.toString();
28977
- const chatTitle = msg.chat.title || msg.chat.username || msg.chat.first_name || "Private Chat";
29095
+ const chatTitle = chatTitleFromTelegramChat(msg.chat);
28978
29096
  if (messageHistory.some(
28979
29097
  (m) => m.metadata?.message_id === msg.message_id
28980
29098
  ))
@@ -28988,7 +29106,7 @@ async function fetchNewUpdates() {
28988
29106
  ALLOWED_ATTR: []
28989
29107
  });
28990
29108
  content = content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/[*_~]/g, "\\$&");
28991
- messageHistory.push({
29109
+ const entry = {
28992
29110
  senderId,
28993
29111
  chatId,
28994
29112
  chatTitle,
@@ -28999,15 +29117,140 @@ async function fetchNewUpdates() {
28999
29117
  chatType: msg.chat.type,
29000
29118
  message_id: msg.message_id
29001
29119
  }
29002
- });
29120
+ };
29121
+ messageHistory.push(entry);
29122
+ added.push(entry);
29003
29123
  if (messageHistory.length > MAX_HISTORY) {
29004
29124
  messageHistory.shift();
29005
29125
  }
29126
+ if (channelReady) {
29127
+ await pushMessageToChannel(entry);
29128
+ }
29006
29129
  }
29007
29130
  persistState();
29008
29131
  } catch (error48) {
29009
29132
  console.error("fetchNewUpdates failed:", error48);
29010
29133
  }
29134
+ return added;
29135
+ }
29136
+ async function pushMessageToChannel(entry) {
29137
+ const meta3 = buildChannelMeta({
29138
+ chat_id: entry.chatId,
29139
+ sender_id: entry.senderId,
29140
+ sender_name: entry.chatTitle ?? entry.senderId,
29141
+ message_id: entry.metadata?.message_id,
29142
+ timestamp: entry.timestamp
29143
+ });
29144
+ const sent = await sendChannelNotification(server, entry.content, meta3);
29145
+ if (sent) {
29146
+ console.error(
29147
+ `[telegram:channel] pushed message_id=${entry.metadata?.message_id ?? "unknown"} chat_id=${entry.chatId}`
29148
+ );
29149
+ }
29150
+ }
29151
+ async function relayPermissionRequestToTelegram(params) {
29152
+ pendingPermissionRequests.set(params.request_id, params);
29153
+ if (IS_MOCK || !bot) {
29154
+ console.error(
29155
+ `[telegram:channel] permission request (mock): ${params.tool_name} (${params.request_id})`
29156
+ );
29157
+ return;
29158
+ }
29159
+ const targetId = allowedUserId;
29160
+ if (!targetId) {
29161
+ console.error(
29162
+ "[telegram:channel] cannot relay permission \u2014 ALLOWED_USER_ID not set"
29163
+ );
29164
+ return;
29165
+ }
29166
+ try {
29167
+ await bot.telegram.sendMessage(
29168
+ targetId,
29169
+ formatPermissionRequestMessage(params),
29170
+ {
29171
+ reply_markup: {
29172
+ inline_keyboard: [
29173
+ [
29174
+ {
29175
+ text: "Allow",
29176
+ callback_data: buildPermissionCallbackData(
29177
+ "allow",
29178
+ params.request_id
29179
+ )
29180
+ },
29181
+ {
29182
+ text: "Deny",
29183
+ callback_data: buildPermissionCallbackData(
29184
+ "deny",
29185
+ params.request_id
29186
+ )
29187
+ }
29188
+ ]
29189
+ ]
29190
+ }
29191
+ }
29192
+ );
29193
+ } catch (error48) {
29194
+ console.error(
29195
+ "[telegram:channel] failed to relay permission request:",
29196
+ error48
29197
+ );
29198
+ }
29199
+ }
29200
+ async function handlePermissionCallback(callbackQuery) {
29201
+ if (!callbackQuery.data) return;
29202
+ const parsed = parsePermissionCallbackData(callbackQuery.data);
29203
+ if (!parsed) return;
29204
+ const currentAllowedUserId = process.env.ALLOWED_USER_ID;
29205
+ if (currentAllowedUserId && callbackQuery.from) {
29206
+ if (callbackQuery.from.id.toString() !== currentAllowedUserId) {
29207
+ console.error(
29208
+ `[telegram:channel] permission callback rejected from ${callbackQuery.from.id}`
29209
+ );
29210
+ return;
29211
+ }
29212
+ }
29213
+ if (!pendingPermissionRequests.has(parsed.requestId)) {
29214
+ console.error(
29215
+ `[telegram:channel] unknown permission request_id=${parsed.requestId}`
29216
+ );
29217
+ return;
29218
+ }
29219
+ pendingPermissionRequests.delete(parsed.requestId);
29220
+ const sent = await sendPermissionVerdict(
29221
+ server,
29222
+ parsed.requestId,
29223
+ parsed.behavior
29224
+ );
29225
+ if (sent) {
29226
+ console.error(
29227
+ `[telegram:channel] permission verdict ${parsed.behavior} for ${parsed.requestId}`
29228
+ );
29229
+ }
29230
+ if (bot && !IS_MOCK) {
29231
+ try {
29232
+ await bot.telegram.answerCbQuery(
29233
+ callbackQuery.id,
29234
+ parsed.behavior === "allow" ? "Approved" : "Denied"
29235
+ );
29236
+ } catch (error48) {
29237
+ console.error("[telegram:channel] answerCbQuery failed:", error48);
29238
+ }
29239
+ }
29240
+ }
29241
+ function startChannelPolling() {
29242
+ if (channelPollTimer || IS_MOCK || !bot) return;
29243
+ channelPollTimer = setInterval(() => {
29244
+ void fetchNewUpdates();
29245
+ }, CHANNEL_POLL_INTERVAL_MS);
29246
+ console.error(
29247
+ `[telegram:channel] background polling started (${CHANNEL_POLL_INTERVAL_MS}ms)`
29248
+ );
29249
+ }
29250
+ function stopChannelPolling() {
29251
+ if (!channelPollTimer) return;
29252
+ clearInterval(channelPollTimer);
29253
+ channelPollTimer = null;
29011
29254
  }
29012
29255
  server.setRequestHandler(ListToolsRequestSchema, async () => {
29013
29256
  return {
@@ -29129,9 +29372,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29129
29372
  const source = { source: safePath };
29130
29373
  const isPhoto = mediaType === "photo" || !mediaType && isImage(safePath);
29131
29374
  if (isPhoto) {
29132
- const sent = await bot.telegram.sendPhoto(targetId, source, {
29133
- caption: content
29134
- });
29375
+ const sent = await bot.telegram.sendPhoto(
29376
+ targetId,
29377
+ source,
29378
+ {
29379
+ caption: content
29380
+ }
29381
+ );
29135
29382
  recordKnownChatFromTelegramMessage(sent);
29136
29383
  } else {
29137
29384
  const sent = await bot.telegram.sendDocument(
@@ -29299,12 +29546,34 @@ ${formatPaginationFooter(offset, limit, activeChats.length)}` : "No active chats
29299
29546
  );
29300
29547
  }
29301
29548
  });
29549
+ server.setNotificationHandler(
29550
+ ChannelPermissionRequestNotificationSchema,
29551
+ async (notification) => {
29552
+ await relayPermissionRequestToTelegram(notification.params);
29553
+ }
29554
+ );
29555
+ server.oninitialized = () => {
29556
+ channelReady = true;
29557
+ console.error(
29558
+ "[telegram:channel] client initialized \u2014 channel push enabled"
29559
+ );
29560
+ startChannelPolling();
29561
+ };
29302
29562
  async function run() {
29303
29563
  loadState();
29304
29564
  const transport = new StdioServerTransport();
29305
29565
  await server.connect(transport);
29306
29566
  console.error("Telegram MCP Server running on stdio");
29567
+ if (!IS_MOCK && bot) {
29568
+ void discoverChatsFromApi();
29569
+ }
29307
29570
  }
29571
+ process.on("SIGINT", () => {
29572
+ stopChannelPolling();
29573
+ });
29574
+ process.on("SIGTERM", () => {
29575
+ stopChannelPolling();
29576
+ });
29308
29577
  if (process.env.NODE_ENV !== "test") {
29309
29578
  run().catch((error48) => {
29310
29579
  console.error("Fatal error in run():", error48);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fre4x/telegram",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Telegram MCP server implementing Claude Code Channels for bidirectional communication and remote permission relay.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",