@integrity-labs/agt-cli 0.14.12 → 0.14.15

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.
@@ -2980,7 +2980,7 @@ var require_compile = __commonJS({
2980
2980
  const schOrFunc = root.refs[ref];
2981
2981
  if (schOrFunc)
2982
2982
  return schOrFunc;
2983
- let _sch = resolve.call(this, root, ref);
2983
+ let _sch = resolve2.call(this, root, ref);
2984
2984
  if (_sch === void 0) {
2985
2985
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2986
2986
  const { schemaId } = this.opts;
@@ -3007,7 +3007,7 @@ var require_compile = __commonJS({
3007
3007
  function sameSchemaEnv(s1, s2) {
3008
3008
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3009
3009
  }
3010
- function resolve(root, ref) {
3010
+ function resolve2(root, ref) {
3011
3011
  let sch;
3012
3012
  while (typeof (sch = this.refs[ref]) == "string")
3013
3013
  ref = sch;
@@ -3582,7 +3582,7 @@ var require_fast_uri = __commonJS({
3582
3582
  }
3583
3583
  return uri;
3584
3584
  }
3585
- function resolve(baseURI, relativeURI, options) {
3585
+ function resolve2(baseURI, relativeURI, options) {
3586
3586
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3587
3587
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3588
3588
  schemelessOptions.skipEscape = true;
@@ -3809,7 +3809,7 @@ var require_fast_uri = __commonJS({
3809
3809
  var fastUri = {
3810
3810
  SCHEMES,
3811
3811
  normalize,
3812
- resolve,
3812
+ resolve: resolve2,
3813
3813
  resolveComponent,
3814
3814
  equal,
3815
3815
  serialize,
@@ -12619,7 +12619,7 @@ var Protocol = class {
12619
12619
  return;
12620
12620
  }
12621
12621
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
12622
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
12622
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
12623
12623
  options?.signal?.throwIfAborted();
12624
12624
  }
12625
12625
  } catch (error2) {
@@ -12636,7 +12636,7 @@ var Protocol = class {
12636
12636
  */
12637
12637
  request(request, resultSchema, options) {
12638
12638
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
12639
- return new Promise((resolve, reject) => {
12639
+ return new Promise((resolve2, reject) => {
12640
12640
  const earlyReject = (error2) => {
12641
12641
  reject(error2);
12642
12642
  };
@@ -12714,7 +12714,7 @@ var Protocol = class {
12714
12714
  if (!parseResult.success) {
12715
12715
  reject(parseResult.error);
12716
12716
  } else {
12717
- resolve(parseResult.data);
12717
+ resolve2(parseResult.data);
12718
12718
  }
12719
12719
  } catch (error2) {
12720
12720
  reject(error2);
@@ -12975,12 +12975,12 @@ var Protocol = class {
12975
12975
  }
12976
12976
  } catch {
12977
12977
  }
12978
- return new Promise((resolve, reject) => {
12978
+ return new Promise((resolve2, reject) => {
12979
12979
  if (signal.aborted) {
12980
12980
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
12981
12981
  return;
12982
12982
  }
12983
- const timeoutId = setTimeout(resolve, interval);
12983
+ const timeoutId = setTimeout(resolve2, interval);
12984
12984
  signal.addEventListener("abort", () => {
12985
12985
  clearTimeout(timeoutId);
12986
12986
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -13850,12 +13850,12 @@ var StdioServerTransport = class {
13850
13850
  this.onclose?.();
13851
13851
  }
13852
13852
  send(message) {
13853
- return new Promise((resolve) => {
13853
+ return new Promise((resolve2) => {
13854
13854
  const json = serializeMessage(message);
13855
13855
  if (this._stdout.write(json)) {
13856
- resolve();
13856
+ resolve2();
13857
13857
  } else {
13858
- this._stdout.once("drain", resolve);
13858
+ this._stdout.once("drain", resolve2);
13859
13859
  }
13860
13860
  });
13861
13861
  }
@@ -13864,6 +13864,238 @@ var StdioServerTransport = class {
13864
13864
  // src/telegram-channel.ts
13865
13865
  import https from "https";
13866
13866
  import { createHash } from "crypto";
13867
+
13868
+ // src/channel-attachments.ts
13869
+ import { homedir } from "os";
13870
+ import { join, resolve, sep } from "path";
13871
+ function resolveChannelInboundDir(codeName, channelSlug) {
13872
+ const base = join(homedir(), ".augmented");
13873
+ const allowedSegment = /^[A-Za-z0-9_-]+$/;
13874
+ if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
13875
+ throw new Error(
13876
+ `Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
13877
+ );
13878
+ }
13879
+ const candidate = resolve(base, codeName, channelSlug);
13880
+ if (!isPathInside(candidate, base)) {
13881
+ throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
13882
+ }
13883
+ return candidate;
13884
+ }
13885
+ function classifyMimetype(mimetype) {
13886
+ if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
13887
+ return "attachment";
13888
+ }
13889
+ function buildSafeInboundPath(root, fileId, mimetype) {
13890
+ const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
13891
+ if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
13892
+ const ext = extensionForMimetype(mimetype);
13893
+ const candidate = resolve(root, `${safeId}${ext}`);
13894
+ if (!isPathInside(candidate, root)) {
13895
+ throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
13896
+ }
13897
+ return candidate;
13898
+ }
13899
+ function extensionForMimetype(mimetype) {
13900
+ if (!mimetype) return ".bin";
13901
+ switch (mimetype) {
13902
+ // Images
13903
+ case "image/png":
13904
+ return ".png";
13905
+ case "image/jpeg":
13906
+ case "image/jpg":
13907
+ return ".jpg";
13908
+ case "image/gif":
13909
+ return ".gif";
13910
+ case "image/webp":
13911
+ return ".webp";
13912
+ case "image/svg+xml":
13913
+ return ".svg";
13914
+ // Docs
13915
+ case "application/pdf":
13916
+ return ".pdf";
13917
+ case "text/plain":
13918
+ return ".txt";
13919
+ case "text/csv":
13920
+ return ".csv";
13921
+ case "application/json":
13922
+ return ".json";
13923
+ case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
13924
+ return ".docx";
13925
+ case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
13926
+ return ".xlsx";
13927
+ // Audio (Telegram voice notes are typically audio/ogg; regular
13928
+ // audio messages are audio/mpeg)
13929
+ case "audio/ogg":
13930
+ return ".ogg";
13931
+ case "audio/mpeg":
13932
+ return ".mp3";
13933
+ case "audio/mp4":
13934
+ return ".m4a";
13935
+ // Video
13936
+ case "video/mp4":
13937
+ return ".mp4";
13938
+ case "video/quicktime":
13939
+ return ".mov";
13940
+ default:
13941
+ return ".bin";
13942
+ }
13943
+ }
13944
+ function isPathInside(target, root) {
13945
+ const normalizedRoot = resolve(root) + sep;
13946
+ const normalizedTarget = resolve(target);
13947
+ return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
13948
+ }
13949
+ function createDownloadAllowlist(ttlMs) {
13950
+ const store = /* @__PURE__ */ new Map();
13951
+ function pruneExpired(now) {
13952
+ for (const [fileId, scopes] of store) {
13953
+ for (const [scope, expiresAt] of scopes) {
13954
+ if (now > expiresAt) scopes.delete(scope);
13955
+ }
13956
+ if (scopes.size === 0) store.delete(fileId);
13957
+ }
13958
+ }
13959
+ return {
13960
+ register(fileId, scope) {
13961
+ const now = Date.now();
13962
+ pruneExpired(now);
13963
+ const scopes = store.get(fileId) ?? /* @__PURE__ */ new Map();
13964
+ scopes.set(scope, now + ttlMs);
13965
+ store.set(fileId, scopes);
13966
+ },
13967
+ isAllowed(fileId, scope) {
13968
+ const scopes = store.get(fileId);
13969
+ if (!scopes) return false;
13970
+ const expiresAt = scopes.get(scope);
13971
+ if (!expiresAt) return false;
13972
+ if (Date.now() > expiresAt) {
13973
+ scopes.delete(scope);
13974
+ if (scopes.size === 0) store.delete(fileId);
13975
+ return false;
13976
+ }
13977
+ return true;
13978
+ }
13979
+ };
13980
+ }
13981
+ function redactAugmentedPaths(msg) {
13982
+ const homePrefix = homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13983
+ return msg.replaceAll(
13984
+ new RegExp(`${homePrefix}[\\\\/]\\.augmented(?:[\\\\/][^\\s'"\`]*)*`, "g"),
13985
+ "<augmented-path>"
13986
+ );
13987
+ }
13988
+
13989
+ // src/telegram-attachments.ts
13990
+ import { mkdirSync, writeFileSync, chmodSync, renameSync, unlinkSync } from "fs";
13991
+ function resolveTelegramInboundDir(codeName) {
13992
+ return resolveChannelInboundDir(codeName, "telegram-inbound");
13993
+ }
13994
+ var MAX_TELEGRAM_INBOUND_BYTES = 25 * 1024 * 1024;
13995
+ function classifyTelegramAttachments(message) {
13996
+ if (!message) return [];
13997
+ const out = [];
13998
+ if (Array.isArray(message.photo) && message.photo.length > 0) {
13999
+ const largest = message.photo.reduce((acc, curr) => {
14000
+ const accArea = (acc.width ?? 0) * (acc.height ?? 0);
14001
+ const currArea = (curr.width ?? 0) * (curr.height ?? 0);
14002
+ return currArea > accArea ? curr : acc;
14003
+ }, message.photo[0]);
14004
+ if (typeof largest.file_id === "string" && largest.file_id) {
14005
+ out.push({
14006
+ kind: "image",
14007
+ id: largest.file_id,
14008
+ filename: `photo-${largest.file_unique_id ?? largest.file_id}.jpg`,
14009
+ mimetype: "image/jpeg"
14010
+ });
14011
+ }
14012
+ }
14013
+ if (message.document && typeof message.document.file_id === "string" && message.document.file_id) {
14014
+ const mimetype = message.document.mime_type || "application/octet-stream";
14015
+ out.push({
14016
+ kind: classifyMimetype(mimetype),
14017
+ id: message.document.file_id,
14018
+ filename: message.document.file_name || message.document.file_id,
14019
+ mimetype
14020
+ });
14021
+ }
14022
+ if (message.voice && typeof message.voice.file_id === "string" && message.voice.file_id) {
14023
+ out.push({
14024
+ kind: "attachment",
14025
+ id: message.voice.file_id,
14026
+ filename: `voice-${message.voice.file_unique_id ?? message.voice.file_id}.ogg`,
14027
+ mimetype: message.voice.mime_type || "audio/ogg"
14028
+ });
14029
+ }
14030
+ if (message.video && typeof message.video.file_id === "string" && message.video.file_id) {
14031
+ const mimetype = message.video.mime_type || "video/mp4";
14032
+ out.push({
14033
+ kind: "attachment",
14034
+ id: message.video.file_id,
14035
+ filename: message.video.file_name || `video-${message.video.file_unique_id ?? message.video.file_id}.mp4`,
14036
+ mimetype
14037
+ });
14038
+ }
14039
+ if (message.audio && typeof message.audio.file_id === "string" && message.audio.file_id) {
14040
+ const mimetype = message.audio.mime_type || "audio/mpeg";
14041
+ out.push({
14042
+ kind: "attachment",
14043
+ id: message.audio.file_id,
14044
+ filename: message.audio.file_name || `audio-${message.audio.file_unique_id ?? message.audio.file_id}.mp3`,
14045
+ mimetype
14046
+ });
14047
+ }
14048
+ if (message.animation && typeof message.animation.file_id === "string" && message.animation.file_id) {
14049
+ const mimetype = message.animation.mime_type || "video/mp4";
14050
+ out.push({
14051
+ kind: "attachment",
14052
+ id: message.animation.file_id,
14053
+ filename: message.animation.file_name || `animation-${message.animation.file_unique_id ?? message.animation.file_id}.mp4`,
14054
+ mimetype
14055
+ });
14056
+ }
14057
+ return out;
14058
+ }
14059
+ async function downloadTelegramFile(fileId, opts) {
14060
+ const abortMeta = AbortSignal.timeout(opts.timeoutMs);
14061
+ const metadata = await opts.http.getFilePath(fileId, abortMeta);
14062
+ if (typeof metadata.file_size === "number" && metadata.file_size > opts.maxBytes) {
14063
+ throw new Error(
14064
+ `telegram file ${fileId} is ${metadata.file_size} bytes \u2014 exceeds the ${opts.maxBytes}-byte inbound cap`
14065
+ );
14066
+ }
14067
+ const abortBody = AbortSignal.timeout(opts.timeoutMs);
14068
+ const bytes = await opts.http.fetchBytes(metadata.file_path, abortBody, opts.maxBytes);
14069
+ if (bytes.byteLength > opts.maxBytes) {
14070
+ throw new Error(
14071
+ `telegram file ${fileId} downloaded ${bytes.byteLength} bytes \u2014 exceeds the ${opts.maxBytes}-byte inbound cap`
14072
+ );
14073
+ }
14074
+ const dir = resolveTelegramInboundDir(opts.codeName);
14075
+ const savedPath = buildSafeInboundPath(dir, fileId, opts.mimetype);
14076
+ if (!isPathInside(savedPath, dir)) {
14077
+ throw new Error(`refusing to write ${savedPath} outside ${dir}`);
14078
+ }
14079
+ mkdirSync(dir, { recursive: true });
14080
+ const tempPath = `${savedPath}.${process.pid}.${Date.now()}.tmp`;
14081
+ try {
14082
+ writeFileSync(tempPath, bytes, { mode: 384, flag: "wx" });
14083
+ try {
14084
+ chmodSync(tempPath, 384);
14085
+ } catch {
14086
+ }
14087
+ renameSync(tempPath, savedPath);
14088
+ } catch (err) {
14089
+ try {
14090
+ unlinkSync(tempPath);
14091
+ } catch {
14092
+ }
14093
+ throw err;
14094
+ }
14095
+ return savedPath;
14096
+ }
14097
+
14098
+ // src/telegram-channel.ts
13867
14099
  function redactId(id) {
13868
14100
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
13869
14101
  }
@@ -13879,7 +14111,7 @@ if (!BOT_TOKEN) {
13879
14111
  process.exit(1);
13880
14112
  }
13881
14113
  function telegramApiCall(method, body, timeoutMs) {
13882
- return new Promise((resolve, reject) => {
14114
+ return new Promise((resolve2, reject) => {
13883
14115
  const postData = JSON.stringify(body);
13884
14116
  const req = https.request(
13885
14117
  {
@@ -13901,7 +14133,7 @@ function telegramApiCall(method, body, timeoutMs) {
13901
14133
  });
13902
14134
  res.on("end", () => {
13903
14135
  try {
13904
- resolve(JSON.parse(data));
14136
+ resolve2(JSON.parse(data));
13905
14137
  } catch {
13906
14138
  reject(new Error("Invalid JSON from Telegram API"));
13907
14139
  }
@@ -13985,17 +14217,17 @@ var mcp = new Server(
13985
14217
  experimental: { "claude/channel": {} },
13986
14218
  tools: {}
13987
14219
  },
14220
+ // NOTE: Claude Code truncates MCP server instructions at 2048 chars.
14221
+ // Attachments rules live near the top because they get chopped otherwise
14222
+ // and the agent silently loses attachment-handling guidance.
13988
14223
  instructions: [
13989
14224
  'Messages from Telegram arrive as <channel source="telegram" chat_id="..." user="..." user_name="..." message_id="...">.',
13990
- "The user reads Telegram, not this session. The ONLY way to reach them is the telegram.reply tool \u2014 anything you put in session output is invisible to them.",
13991
- `EVERY response to a Telegram message goes through telegram.reply, passing the chat_id from the tag. This includes clarifying questions, error messages, partial answers, "I don't understand", and status updates \u2014 no exceptions.`,
13992
- 'For work that will take more than ~30 seconds, follow the standard kanban flow from CLAUDE.md: call kanban.add to track the task, then IMMEDIATELY reply via telegram.reply with "On it \u2014 tracking here: <kanban URL>" including the created task title, move the task to in_progress, do the work, and reply via telegram.reply with the result.',
13993
- "Simple lookups, one-line answers, and no-action acknowledgements do NOT need a kanban task \u2014 but still reply via telegram.reply every time.",
13994
- "When a user mentions a time without a timezone, interpret it in your Timezone from the Identity section of CLAUDE.md. Do NOT ask the user which timezone they meant \u2014 resolve it yourself against your own identity.",
13995
- "Address users by their user_name when possible; user is their numeric Telegram ID.",
13996
- "To quote-reply to a specific earlier message, pass its message_id as reply_to_message_id \u2014 otherwise omit it for a normal reply.",
13997
- "Telegram DMs are 1:1 \u2014 always reply to every inbound message unless it is obviously not addressed to you.",
13998
- 'Every inbound message gets an automatic \u{1F440} reaction as a "seen, working on it" ack. Use the telegram.react tool to add your own reactions (e.g. \u270D while clarifying, \u{1FAE1} on task complete). Only free-tier emoji are available to bots \u2014 stick to: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} and similar common reactions. Premium-only emoji fail silently.'
14225
+ "Inbound attachments: the <channel> tag's `files` attribute is a JSON-serialised array \u2014 JSON.parse it before iterating. If a file entry has a `path`, the image is ALREADY DOWNLOADED locally \u2014 Read that path directly, do NOT call telegram.download_attachment. That tool is only for entries with `file_id` but NO `path` (PDF, docx, voice, audio, video, animations): pass file_id + chat_id verbatim (never paraphrase), then Read the returned path. Single-image messages also get a top-level `image_path` convenience attribute; multi-image messages omit it. Caption (no text) arrives as the channel content. Never tell the user about internal file-handling failures that don't affect the answer.",
14226
+ "The user reads Telegram, not this session. Every response goes through telegram.reply with the chat_id from the tag \u2014 clarifying questions, errors, partial answers, status updates, all of it.",
14227
+ 'For work >30s follow CLAUDE.md kanban flow: kanban.add, reply with "On it \u2014 tracking here: <kanban URL>", move to in_progress, do the work, reply with the result. Simple lookups skip kanban but still reply.',
14228
+ "Address users by user_name; user is the numeric Telegram ID. Reply-to a specific message with reply_to_message_id, otherwise omit.",
14229
+ "Resolve ambiguous times against your own Timezone from CLAUDE.md \u2014 do not ask the user.",
14230
+ "Every inbound message gets an automatic \u{1F440} ack. Use telegram.react for your own reactions: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} (premium-only fail silently)."
13999
14231
  ].join(" ")
14000
14232
  }
14001
14233
  );
@@ -14035,6 +14267,24 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
14035
14267
  required: ["chat_id", "text"]
14036
14268
  }
14037
14269
  },
14270
+ {
14271
+ name: "telegram.download_attachment",
14272
+ description: "Download an inbound Telegram attachment on demand. Use this for non-image files (PDFs, docx, voice, video, audio, animations) that arrived via a <channel> tag \u2014 look for `file_id` in the `files` meta. Requires the `chat_id` from the same <channel> tag so the tool can verify the attachment was shared in the current conversation. Returns the local path where the file was written (inside ~/.augmented/<codeName>/telegram-inbound/). Images are auto-downloaded on receipt and don't need this tool.",
14273
+ inputSchema: {
14274
+ type: "object",
14275
+ properties: {
14276
+ file_id: {
14277
+ type: "string",
14278
+ description: "Telegram file_id (from the `file_id` field on the `files` meta array of the inbound <channel> tag)"
14279
+ },
14280
+ chat_id: {
14281
+ type: "string",
14282
+ description: "Telegram chat_id from the `chat_id` attribute of the same inbound <channel> tag that surfaced the file. The tool only downloads attachments that were shared in this chat."
14283
+ }
14284
+ },
14285
+ required: ["file_id", "chat_id"]
14286
+ }
14287
+ },
14038
14288
  {
14039
14289
  name: "telegram.react",
14040
14290
  description: "Add an emoji reaction to a Telegram message. Only free-tier emoji reactions are available to bots (Premium-only emoji fail silently). Pass an empty string or omit emoji to clear the bot's reaction on that message.",
@@ -14122,11 +14372,141 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
14122
14372
  };
14123
14373
  }
14124
14374
  }
14375
+ if (name === "telegram.download_attachment") {
14376
+ const { file_id, chat_id } = args;
14377
+ if (typeof file_id !== "string" || !file_id.trim()) {
14378
+ return {
14379
+ content: [{ type: "text", text: "telegram.download_attachment requires a string `file_id`." }],
14380
+ isError: true
14381
+ };
14382
+ }
14383
+ if (typeof chat_id !== "string" || !chat_id.trim()) {
14384
+ return {
14385
+ content: [{ type: "text", text: "telegram.download_attachment requires a string `chat_id` (from the inbound <channel> tag)." }],
14386
+ isError: true
14387
+ };
14388
+ }
14389
+ const trimmedFileId = file_id.trim();
14390
+ const trimmedChatId = chat_id.trim();
14391
+ if (ALLOWED_CHATS.size > 0 && !ALLOWED_CHATS.has(trimmedChatId)) {
14392
+ return {
14393
+ content: [{ type: "text", text: `Chat ${trimmedChatId} is not in TELEGRAM_ALLOWED_CHATS` }],
14394
+ isError: true
14395
+ };
14396
+ }
14397
+ if (!downloadAllowlist.isAllowed(trimmedFileId, trimmedChatId)) {
14398
+ return {
14399
+ content: [{
14400
+ type: "text",
14401
+ text: "Download refused: file_id was not surfaced in a recent inbound `files` meta entry for this chat. You can only download attachments the originating conversation has shared with you."
14402
+ }],
14403
+ isError: true
14404
+ };
14405
+ }
14406
+ try {
14407
+ const savedPath = await downloadTelegramFile(trimmedFileId, {
14408
+ codeName: AGENT_CODE_NAME,
14409
+ mimetype: "application/octet-stream",
14410
+ timeoutMs: TELEGRAM_DOWNLOAD_TIMEOUT_MS,
14411
+ maxBytes: MAX_TELEGRAM_INBOUND_BYTES,
14412
+ http: telegramHttp
14413
+ });
14414
+ return { content: [{ type: "text", text: `Downloaded to ${savedPath}` }] };
14415
+ } catch (err) {
14416
+ return {
14417
+ content: [{
14418
+ type: "text",
14419
+ text: `Failed to download attachment: ${redactAugmentedPaths(err.message)}`
14420
+ }],
14421
+ isError: true
14422
+ };
14423
+ }
14424
+ }
14125
14425
  throw new Error(`Unknown tool: ${name}`);
14126
14426
  });
14127
14427
  await mcp.connect(new StdioServerTransport());
14128
14428
  var LONG_POLL_SECONDS = 50;
14129
14429
  var POLL_HTTP_TIMEOUT_MS = (LONG_POLL_SECONDS + 10) * 1e3;
14430
+ var TELEGRAM_DOWNLOAD_TIMEOUT_MS = 15e3;
14431
+ var TELEGRAM_DOWNLOAD_ALLOWLIST_TTL_MS = 60 * 60 * 1e3;
14432
+ var downloadAllowlist = createDownloadAllowlist(
14433
+ TELEGRAM_DOWNLOAD_ALLOWLIST_TTL_MS
14434
+ );
14435
+ var telegramHttp = {
14436
+ async getFilePath(fileId, signal) {
14437
+ const resp = await telegramApiCall(
14438
+ "getFile",
14439
+ { file_id: fileId },
14440
+ TELEGRAM_DOWNLOAD_TIMEOUT_MS
14441
+ );
14442
+ if (signal.aborted) throw new Error("telegram getFile aborted");
14443
+ if (!resp.ok || !resp.result?.file_path) {
14444
+ throw new Error(`telegram getFile failed: ${resp.description ?? "unknown"}`);
14445
+ }
14446
+ return { file_path: resp.result.file_path, file_size: resp.result.file_size };
14447
+ },
14448
+ async fetchBytes(filePath, signal, maxBytes) {
14449
+ return new Promise((resolve2, reject) => {
14450
+ const req = https.request(
14451
+ {
14452
+ hostname: "api.telegram.org",
14453
+ port: 443,
14454
+ path: `/file/bot${BOT_TOKEN}/${filePath}`,
14455
+ method: "GET",
14456
+ family: 4,
14457
+ timeout: TELEGRAM_DOWNLOAD_TIMEOUT_MS
14458
+ },
14459
+ (res) => {
14460
+ if (res.statusCode && res.statusCode >= 400) {
14461
+ reject(new Error(`telegram file fetch returned HTTP ${res.statusCode}`));
14462
+ res.resume();
14463
+ return;
14464
+ }
14465
+ const contentLengthHeader = res.headers["content-length"];
14466
+ if (contentLengthHeader) {
14467
+ const contentLength = Number(contentLengthHeader);
14468
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
14469
+ reject(new Error(
14470
+ `telegram file fetch: Content-Length ${contentLength} exceeds ${maxBytes}-byte cap`
14471
+ ));
14472
+ req.destroy();
14473
+ return;
14474
+ }
14475
+ }
14476
+ const chunks = [];
14477
+ let total = 0;
14478
+ let aborted2 = false;
14479
+ res.on("data", (c) => {
14480
+ if (aborted2) return;
14481
+ total += c.length;
14482
+ if (total > maxBytes) {
14483
+ aborted2 = true;
14484
+ req.destroy();
14485
+ reject(new Error(
14486
+ `telegram file fetch: realized size exceeded ${maxBytes}-byte cap (>${total} bytes)`
14487
+ ));
14488
+ return;
14489
+ }
14490
+ chunks.push(c);
14491
+ });
14492
+ res.on("end", () => {
14493
+ if (!aborted2) resolve2(Buffer.concat(chunks));
14494
+ });
14495
+ res.on("error", reject);
14496
+ }
14497
+ );
14498
+ signal.addEventListener("abort", () => {
14499
+ req.destroy(new Error("telegram file fetch aborted"));
14500
+ });
14501
+ req.on("error", reject);
14502
+ req.on("timeout", () => {
14503
+ req.destroy();
14504
+ reject(new Error("telegram file fetch timeout"));
14505
+ });
14506
+ req.end();
14507
+ });
14508
+ }
14509
+ };
14130
14510
  var nextOffset = 0;
14131
14511
  var isShuttingDown = false;
14132
14512
  async function pollLoop() {
@@ -14153,7 +14533,10 @@ async function pollLoop() {
14153
14533
  for (const update of resp.result ?? []) {
14154
14534
  nextOffset = Math.max(nextOffset, update.update_id + 1);
14155
14535
  const msg = update.message;
14156
- if (!msg || typeof msg.text !== "string" || msg.text.length === 0) continue;
14536
+ if (!msg) continue;
14537
+ const classifiedAttachments = classifyTelegramAttachments(msg);
14538
+ const content = typeof msg.text === "string" && msg.text.length > 0 ? msg.text : typeof msg.caption === "string" && msg.caption.length > 0 ? msg.caption : "";
14539
+ if (content.length === 0 && classifiedAttachments.length === 0) continue;
14157
14540
  const chatId = String(msg.chat.id);
14158
14541
  if (ALLOWED_CHATS.size > 0 && !ALLOWED_CHATS.has(chatId)) continue;
14159
14542
  const userId = msg.from?.id != null ? String(msg.from.id) : "unknown";
@@ -14161,10 +14544,37 @@ async function pollLoop() {
14161
14544
  const messageId = String(msg.message_id);
14162
14545
  void setMessageReaction(chatId, messageId, ACK_EMOJI);
14163
14546
  trackPendingMessage(chatId, messageId, msg.chat.type);
14547
+ const fileMeta = [];
14548
+ for (const attachment of classifiedAttachments) {
14549
+ if (attachment.kind === "image") {
14550
+ try {
14551
+ const path = await downloadTelegramFile(attachment.id, {
14552
+ codeName: AGENT_CODE_NAME,
14553
+ mimetype: attachment.mimetype,
14554
+ timeoutMs: TELEGRAM_DOWNLOAD_TIMEOUT_MS,
14555
+ maxBytes: MAX_TELEGRAM_INBOUND_BYTES,
14556
+ http: telegramHttp
14557
+ });
14558
+ fileMeta.push({ kind: "image", file_id: attachment.id, filename: attachment.filename, mimetype: attachment.mimetype, path });
14559
+ } catch (err) {
14560
+ process.stderr.write(
14561
+ `telegram-channel(${AGENT_CODE_NAME}): image auto-download failed for ${redactId(attachment.id)}: ${redactAugmentedPaths(err.message)}
14562
+ `
14563
+ );
14564
+ downloadAllowlist.register(attachment.id, chatId);
14565
+ fileMeta.push({ kind: "attachment", file_id: attachment.id, filename: attachment.filename, mimetype: attachment.mimetype });
14566
+ }
14567
+ } else {
14568
+ downloadAllowlist.register(attachment.id, chatId);
14569
+ fileMeta.push({ kind: "attachment", file_id: attachment.id, filename: attachment.filename, mimetype: attachment.mimetype });
14570
+ }
14571
+ }
14572
+ const downloadedImages = fileMeta.filter((f) => f.kind === "image" && typeof f.path === "string");
14573
+ const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
14164
14574
  await mcp.notification({
14165
14575
  method: "notifications/claude/channel",
14166
14576
  params: {
14167
- content: msg.text,
14577
+ content,
14168
14578
  meta: {
14169
14579
  source: "telegram",
14170
14580
  chat_id: chatId,
@@ -14172,7 +14582,9 @@ async function pollLoop() {
14172
14582
  message_id: messageId,
14173
14583
  user: userId,
14174
14584
  user_name: userName,
14175
- ts: String(msg.date)
14585
+ ts: String(msg.date),
14586
+ ...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
14587
+ ...imagePath ? { image_path: imagePath } : {}
14176
14588
  }
14177
14589
  }
14178
14590
  });
@@ -14188,7 +14600,7 @@ async function pollLoop() {
14188
14600
  }
14189
14601
  }
14190
14602
  function sleep(ms) {
14191
- return new Promise((resolve) => setTimeout(resolve, ms).unref());
14603
+ return new Promise((resolve2) => setTimeout(resolve2, ms).unref());
14192
14604
  }
14193
14605
  function shutdown(reason) {
14194
14606
  if (isShuttingDown) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.14.12",
3
+ "version": "0.14.15",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {