@fre4x/telegram 1.1.5 → 1.1.6

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 +303 -38
  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;
@@ -28600,28 +28699,27 @@ 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
28701
  var botToken = process.env.TELEGRAM_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
+ }
28603
28713
  var allowedUserId = process.env.ALLOWED_USER_ID;
28604
28714
  var allowedRecipients = process.env.ALLOWED_RECIPIENTS?.split(",").map((id) => id.trim()) || [];
28605
28715
  var enableRecipientWhitelist = process.env.ENABLE_RECIPIENT_WHITELIST === "true";
28606
28716
  var originalError = console.error;
28607
28717
  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
- }
28718
+ const message = redactBotToken(args.map((arg) => String(arg)).join(" "));
28615
28719
  originalError(message);
28616
28720
  };
28617
28721
  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
- }
28722
+ const message = redactBotToken(err.stack || err.message);
28625
28723
  console.error("Uncaught Exception:", message);
28626
28724
  process.exit(1);
28627
28725
  });
@@ -28630,13 +28728,7 @@ process.on("unhandledRejection", (reason) => {
28630
28728
  if (reason instanceof Error) {
28631
28729
  message = reason.stack || reason.message;
28632
28730
  }
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);
28731
+ console.error("Unhandled Rejection:", redactBotToken(message));
28640
28732
  });
28641
28733
  if (!IS_MOCK && !botToken) {
28642
28734
  console.error(
@@ -28730,14 +28822,16 @@ var server = new Server(
28730
28822
  capabilities: {
28731
28823
  tools: {},
28732
28824
  logging: {},
28733
- // Required by SDK for notifications/message
28734
28825
  experimental: {
28735
28826
  "claude/channel": {
28736
- description: "Telegram events are delivered via channel notifications"
28827
+ description: "Telegram events are delivered via claude/channel notifications"
28828
+ },
28829
+ "claude/channel/permission": {
28830
+ description: "Tool approval requests are relayed to Telegram for allow/deny"
28737
28831
  }
28738
28832
  }
28739
28833
  },
28740
- instructions: "Telegram MCP server. Delivers inbound Telegram messages as channel notifications via notifications/message."
28834
+ 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
28835
  }
28742
28836
  );
28743
28837
  var isImage = (filePath) => {
@@ -28747,6 +28841,10 @@ var isImage = (filePath) => {
28747
28841
  var messageHistory = [];
28748
28842
  var MAX_HISTORY = 100;
28749
28843
  var pollingOffset = 0;
28844
+ var channelReady = false;
28845
+ var channelPollTimer = null;
28846
+ var CHANNEL_POLL_INTERVAL_MS = 3e3;
28847
+ var pendingPermissionRequests = /* @__PURE__ */ new Map();
28750
28848
  var knownChats = /* @__PURE__ */ new Map();
28751
28849
  function loadState() {
28752
28850
  if (IS_MOCK) return;
@@ -28766,7 +28864,10 @@ function loadState() {
28766
28864
  `[telegram:state] loaded ${knownChats.size} chat(s) from ${getStatePath()}`
28767
28865
  );
28768
28866
  } catch (error48) {
28769
- console.error(`[telegram:state] failed to load ${getStatePath()}:`, error48);
28867
+ console.error(
28868
+ `[telegram:state] failed to load ${getStatePath()}:`,
28869
+ error48
28870
+ );
28770
28871
  }
28771
28872
  }
28772
28873
  function persistState() {
@@ -28777,9 +28878,16 @@ function persistState() {
28777
28878
  pollingOffset
28778
28879
  };
28779
28880
  fs.mkdirSync(path.dirname(getStatePath()), { recursive: true });
28780
- fs.writeFileSync(getStatePath(), JSON.stringify(state, null, 2), "utf-8");
28881
+ fs.writeFileSync(
28882
+ getStatePath(),
28883
+ JSON.stringify(state, null, 2),
28884
+ "utf-8"
28885
+ );
28781
28886
  } catch (error48) {
28782
- console.error(`[telegram:state] failed to persist ${getStatePath()}:`, error48);
28887
+ console.error(
28888
+ `[telegram:state] failed to persist ${getStatePath()}:`,
28889
+ error48
28890
+ );
28783
28891
  }
28784
28892
  }
28785
28893
  function reloadState() {
@@ -28797,7 +28905,10 @@ function reloadState() {
28797
28905
  pollingOffset = state.pollingOffset;
28798
28906
  }
28799
28907
  } catch (error48) {
28800
- console.error(`[telegram:state] failed to reload ${getStatePath()}:`, error48);
28908
+ console.error(
28909
+ `[telegram:state] failed to reload ${getStatePath()}:`,
28910
+ error48
28911
+ );
28801
28912
  }
28802
28913
  }
28803
28914
  function chatTitleFromTelegramChat(chat) {
@@ -28955,17 +29066,23 @@ function getActiveChats() {
28955
29066
  );
28956
29067
  }
28957
29068
  async function fetchNewUpdates() {
28958
- if (!bot || IS_MOCK) return;
29069
+ const added = [];
29070
+ if (!bot || IS_MOCK) return added;
28959
29071
  try {
28960
29072
  await ensurePollingMode();
28961
29073
  const updates = await bot.telegram.getUpdates(0, 100, pollingOffset, [
28962
29074
  "message",
28963
29075
  "edited_message",
28964
29076
  "channel_post",
28965
- "my_chat_member"
29077
+ "my_chat_member",
29078
+ "callback_query"
28966
29079
  ]);
28967
29080
  for (const update of updates) {
28968
29081
  pollingOffset = update.update_id + 1;
29082
+ if (isCallbackQueryUpdate(update)) {
29083
+ await handlePermissionCallback(update.callback_query);
29084
+ continue;
29085
+ }
28969
29086
  const msgLike = update;
28970
29087
  if (!msgLike.message && !msgLike.edited_message && !msgLike.channel_post && !msgLike.my_chat_member)
28971
29088
  continue;
@@ -28974,7 +29091,7 @@ async function fetchNewUpdates() {
28974
29091
  if (!msg?.text) continue;
28975
29092
  const senderId = msg.from?.id?.toString() ?? "unknown";
28976
29093
  const chatId = msg.chat.id.toString();
28977
- const chatTitle = msg.chat.title || msg.chat.username || msg.chat.first_name || "Private Chat";
29094
+ const chatTitle = chatTitleFromTelegramChat(msg.chat);
28978
29095
  if (messageHistory.some(
28979
29096
  (m) => m.metadata?.message_id === msg.message_id
28980
29097
  ))
@@ -28988,7 +29105,7 @@ async function fetchNewUpdates() {
28988
29105
  ALLOWED_ATTR: []
28989
29106
  });
28990
29107
  content = content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/[*_~]/g, "\\$&");
28991
- messageHistory.push({
29108
+ const entry = {
28992
29109
  senderId,
28993
29110
  chatId,
28994
29111
  chatTitle,
@@ -28999,15 +29116,140 @@ async function fetchNewUpdates() {
28999
29116
  chatType: msg.chat.type,
29000
29117
  message_id: msg.message_id
29001
29118
  }
29002
- });
29119
+ };
29120
+ messageHistory.push(entry);
29121
+ added.push(entry);
29003
29122
  if (messageHistory.length > MAX_HISTORY) {
29004
29123
  messageHistory.shift();
29005
29124
  }
29125
+ if (channelReady) {
29126
+ await pushMessageToChannel(entry);
29127
+ }
29006
29128
  }
29007
29129
  persistState();
29008
29130
  } catch (error48) {
29009
29131
  console.error("fetchNewUpdates failed:", error48);
29010
29132
  }
29133
+ return added;
29134
+ }
29135
+ async function pushMessageToChannel(entry) {
29136
+ const meta3 = buildChannelMeta({
29137
+ chat_id: entry.chatId,
29138
+ sender_id: entry.senderId,
29139
+ sender_name: entry.chatTitle ?? entry.senderId,
29140
+ message_id: entry.metadata?.message_id,
29141
+ timestamp: entry.timestamp
29142
+ });
29143
+ const sent = await sendChannelNotification(server, entry.content, meta3);
29144
+ if (sent) {
29145
+ console.error(
29146
+ `[telegram:channel] pushed message_id=${entry.metadata?.message_id ?? "unknown"} chat_id=${entry.chatId}`
29147
+ );
29148
+ }
29149
+ }
29150
+ async function relayPermissionRequestToTelegram(params) {
29151
+ pendingPermissionRequests.set(params.request_id, params);
29152
+ if (IS_MOCK || !bot) {
29153
+ console.error(
29154
+ `[telegram:channel] permission request (mock): ${params.tool_name} (${params.request_id})`
29155
+ );
29156
+ return;
29157
+ }
29158
+ const targetId = allowedUserId;
29159
+ if (!targetId) {
29160
+ console.error(
29161
+ "[telegram:channel] cannot relay permission \u2014 ALLOWED_USER_ID not set"
29162
+ );
29163
+ return;
29164
+ }
29165
+ try {
29166
+ await bot.telegram.sendMessage(
29167
+ targetId,
29168
+ formatPermissionRequestMessage(params),
29169
+ {
29170
+ reply_markup: {
29171
+ inline_keyboard: [
29172
+ [
29173
+ {
29174
+ text: "Allow",
29175
+ callback_data: buildPermissionCallbackData(
29176
+ "allow",
29177
+ params.request_id
29178
+ )
29179
+ },
29180
+ {
29181
+ text: "Deny",
29182
+ callback_data: buildPermissionCallbackData(
29183
+ "deny",
29184
+ params.request_id
29185
+ )
29186
+ }
29187
+ ]
29188
+ ]
29189
+ }
29190
+ }
29191
+ );
29192
+ } catch (error48) {
29193
+ console.error(
29194
+ "[telegram:channel] failed to relay permission request:",
29195
+ error48
29196
+ );
29197
+ }
29198
+ }
29199
+ async function handlePermissionCallback(callbackQuery) {
29200
+ if (!callbackQuery.data) return;
29201
+ const parsed = parsePermissionCallbackData(callbackQuery.data);
29202
+ if (!parsed) return;
29203
+ const currentAllowedUserId = process.env.ALLOWED_USER_ID;
29204
+ if (currentAllowedUserId && callbackQuery.from) {
29205
+ if (callbackQuery.from.id.toString() !== currentAllowedUserId) {
29206
+ console.error(
29207
+ `[telegram:channel] permission callback rejected from ${callbackQuery.from.id}`
29208
+ );
29209
+ return;
29210
+ }
29211
+ }
29212
+ if (!pendingPermissionRequests.has(parsed.requestId)) {
29213
+ console.error(
29214
+ `[telegram:channel] unknown permission request_id=${parsed.requestId}`
29215
+ );
29216
+ return;
29217
+ }
29218
+ pendingPermissionRequests.delete(parsed.requestId);
29219
+ const sent = await sendPermissionVerdict(
29220
+ server,
29221
+ parsed.requestId,
29222
+ parsed.behavior
29223
+ );
29224
+ if (sent) {
29225
+ console.error(
29226
+ `[telegram:channel] permission verdict ${parsed.behavior} for ${parsed.requestId}`
29227
+ );
29228
+ }
29229
+ if (bot && !IS_MOCK) {
29230
+ try {
29231
+ await bot.telegram.answerCbQuery(
29232
+ callbackQuery.id,
29233
+ parsed.behavior === "allow" ? "Approved" : "Denied"
29234
+ );
29235
+ } catch (error48) {
29236
+ console.error("[telegram:channel] answerCbQuery failed:", error48);
29237
+ }
29238
+ }
29239
+ }
29240
+ function startChannelPolling() {
29241
+ if (channelPollTimer || IS_MOCK || !bot) return;
29242
+ channelPollTimer = setInterval(() => {
29243
+ void fetchNewUpdates();
29244
+ }, CHANNEL_POLL_INTERVAL_MS);
29245
+ console.error(
29246
+ `[telegram:channel] background polling started (${CHANNEL_POLL_INTERVAL_MS}ms)`
29247
+ );
29248
+ }
29249
+ function stopChannelPolling() {
29250
+ if (!channelPollTimer) return;
29251
+ clearInterval(channelPollTimer);
29252
+ channelPollTimer = null;
29011
29253
  }
29012
29254
  server.setRequestHandler(ListToolsRequestSchema, async () => {
29013
29255
  return {
@@ -29129,9 +29371,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
29129
29371
  const source = { source: safePath };
29130
29372
  const isPhoto = mediaType === "photo" || !mediaType && isImage(safePath);
29131
29373
  if (isPhoto) {
29132
- const sent = await bot.telegram.sendPhoto(targetId, source, {
29133
- caption: content
29134
- });
29374
+ const sent = await bot.telegram.sendPhoto(
29375
+ targetId,
29376
+ source,
29377
+ {
29378
+ caption: content
29379
+ }
29380
+ );
29135
29381
  recordKnownChatFromTelegramMessage(sent);
29136
29382
  } else {
29137
29383
  const sent = await bot.telegram.sendDocument(
@@ -29299,12 +29545,31 @@ ${formatPaginationFooter(offset, limit, activeChats.length)}` : "No active chats
29299
29545
  );
29300
29546
  }
29301
29547
  });
29548
+ server.setNotificationHandler(
29549
+ ChannelPermissionRequestNotificationSchema,
29550
+ async (notification) => {
29551
+ await relayPermissionRequestToTelegram(notification.params);
29552
+ }
29553
+ );
29554
+ server.oninitialized = () => {
29555
+ channelReady = true;
29556
+ console.error(
29557
+ "[telegram:channel] client initialized \u2014 channel push enabled"
29558
+ );
29559
+ startChannelPolling();
29560
+ };
29302
29561
  async function run() {
29303
29562
  loadState();
29304
29563
  const transport = new StdioServerTransport();
29305
29564
  await server.connect(transport);
29306
29565
  console.error("Telegram MCP Server running on stdio");
29307
29566
  }
29567
+ process.on("SIGINT", () => {
29568
+ stopChannelPolling();
29569
+ });
29570
+ process.on("SIGTERM", () => {
29571
+ stopChannelPolling();
29572
+ });
29308
29573
  if (process.env.NODE_ENV !== "test") {
29309
29574
  run().catch((error48) => {
29310
29575
  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.6",
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",