@integrity-labs/agt-cli 0.14.12 → 0.14.14
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/dist/bin/agt.js +51 -29
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-NSHSUWZQ.js → chunk-VWCF6BOZ.js} +93 -12
- package/dist/chunk-VWCF6BOZ.js.map +1 -0
- package/dist/lib/manager-worker.js +246 -34
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/slack-channel.js +508 -41
- package/mcp/telegram-channel.js +439 -27
- package/package.json +1 -1
- package/dist/chunk-NSHSUWZQ.js.map +0 -1
package/mcp/telegram-channel.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
13853
|
+
return new Promise((resolve2) => {
|
|
13854
13854
|
const json = serializeMessage(message);
|
|
13855
13855
|
if (this._stdout.write(json)) {
|
|
13856
|
-
|
|
13856
|
+
resolve2();
|
|
13857
13857
|
} else {
|
|
13858
|
-
this._stdout.once("drain",
|
|
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((
|
|
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
|
-
|
|
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
|
-
"
|
|
13991
|
-
|
|
13992
|
-
'For work
|
|
13993
|
-
"
|
|
13994
|
-
"
|
|
13995
|
-
"
|
|
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
|
|
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
|
|
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((
|
|
14603
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms).unref());
|
|
14192
14604
|
}
|
|
14193
14605
|
function shutdown(reason) {
|
|
14194
14606
|
if (isShuttingDown) return;
|