@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.
- package/dist/bin/agt.js +51 -29
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-NSHSUWZQ.js → chunk-LGEQOVFU.js} +101 -15
- package/dist/chunk-LGEQOVFU.js.map +1 -0
- package/dist/lib/manager-worker.js +246 -34
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/index.js +113 -0
- 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/slack-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 = resolve3.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 resolve3(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 resolve3(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: resolve3,
|
|
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((resolve3) => setTimeout(resolve3, 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((resolve3, 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
|
+
resolve3(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((resolve3, 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(resolve3, interval);
|
|
12984
12984
|
signal.addEventListener("abort", () => {
|
|
12985
12985
|
clearTimeout(timeoutId);
|
|
12986
12986
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -13875,21 +13875,129 @@ var StdioServerTransport = class {
|
|
|
13875
13875
|
this.onclose?.();
|
|
13876
13876
|
}
|
|
13877
13877
|
send(message) {
|
|
13878
|
-
return new Promise((
|
|
13878
|
+
return new Promise((resolve3) => {
|
|
13879
13879
|
const json = serializeMessage(message);
|
|
13880
13880
|
if (this._stdout.write(json)) {
|
|
13881
|
-
|
|
13881
|
+
resolve3();
|
|
13882
13882
|
} else {
|
|
13883
|
-
this._stdout.once("drain",
|
|
13883
|
+
this._stdout.once("drain", resolve3);
|
|
13884
13884
|
}
|
|
13885
13885
|
});
|
|
13886
13886
|
}
|
|
13887
13887
|
};
|
|
13888
13888
|
|
|
13889
13889
|
// src/slack-channel.ts
|
|
13890
|
-
import { readFileSync, statSync } from "fs";
|
|
13891
|
-
import { basename, resolve } from "path";
|
|
13892
|
-
import { homedir } from "os";
|
|
13890
|
+
import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, chmodSync } from "fs";
|
|
13891
|
+
import { basename, join as join2, resolve as resolve2 } from "path";
|
|
13892
|
+
import { homedir as homedir2 } from "os";
|
|
13893
|
+
|
|
13894
|
+
// src/slack-thread-store.ts
|
|
13895
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
13896
|
+
import { dirname } from "path";
|
|
13897
|
+
var FILE_VERSION = 1;
|
|
13898
|
+
var DEFAULT_TTL_DAYS = 30;
|
|
13899
|
+
var DEFAULT_MIN_INTERVAL_MS = 1e3;
|
|
13900
|
+
function loadThreadStore(filePath, opts = {}) {
|
|
13901
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
13902
|
+
const ttlDays = opts.ttlDays ?? DEFAULT_TTL_DAYS;
|
|
13903
|
+
const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
|
|
13904
|
+
let raw;
|
|
13905
|
+
try {
|
|
13906
|
+
raw = readFileSync(filePath, "utf-8");
|
|
13907
|
+
} catch {
|
|
13908
|
+
return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
|
|
13909
|
+
}
|
|
13910
|
+
let parsed;
|
|
13911
|
+
try {
|
|
13912
|
+
parsed = JSON.parse(raw);
|
|
13913
|
+
} catch (err) {
|
|
13914
|
+
opts.onError?.(
|
|
13915
|
+
`slack-thread-store: malformed JSON at ${filePath} (${err.message}) \u2014 starting with empty tracking state`
|
|
13916
|
+
);
|
|
13917
|
+
return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
|
|
13918
|
+
}
|
|
13919
|
+
if (!isThreadStoreShape(parsed)) {
|
|
13920
|
+
opts.onError?.(
|
|
13921
|
+
`slack-thread-store: unexpected shape at ${filePath} \u2014 starting with empty tracking state`
|
|
13922
|
+
);
|
|
13923
|
+
return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
|
|
13924
|
+
}
|
|
13925
|
+
const threads = /* @__PURE__ */ new Map();
|
|
13926
|
+
let pruned = 0;
|
|
13927
|
+
for (const [key, entry] of Object.entries(parsed.threads)) {
|
|
13928
|
+
if (!isThreadEntry(entry)) continue;
|
|
13929
|
+
const seenTs = Date.parse(entry.last_seen_at);
|
|
13930
|
+
if (Number.isFinite(seenTs) && now.getTime() - seenTs > ttlMs) {
|
|
13931
|
+
pruned++;
|
|
13932
|
+
continue;
|
|
13933
|
+
}
|
|
13934
|
+
threads.set(key, { involvement: entry.involvement, last_seen_at: entry.last_seen_at });
|
|
13935
|
+
}
|
|
13936
|
+
return { threads, pruned };
|
|
13937
|
+
}
|
|
13938
|
+
function serializeThreadStore(threads) {
|
|
13939
|
+
const out = { version: FILE_VERSION, threads: {} };
|
|
13940
|
+
for (const [key, entry] of threads) out.threads[key] = entry;
|
|
13941
|
+
return JSON.stringify(out, null, 2);
|
|
13942
|
+
}
|
|
13943
|
+
function createThreadPersister(opts) {
|
|
13944
|
+
const interval = opts.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
13945
|
+
let lastWriteAt = 0;
|
|
13946
|
+
let pendingTimer = null;
|
|
13947
|
+
let pendingSnapshot = null;
|
|
13948
|
+
const writeNow = (snap) => {
|
|
13949
|
+
try {
|
|
13950
|
+
mkdirSync(dirname(opts.filePath), { recursive: true });
|
|
13951
|
+
writeFileSync(opts.filePath, serializeThreadStore(snap), "utf-8");
|
|
13952
|
+
lastWriteAt = Date.now();
|
|
13953
|
+
} catch (err) {
|
|
13954
|
+
opts.onError?.(
|
|
13955
|
+
`slack-thread-store: failed to persist ${opts.filePath}: ${err.message}`
|
|
13956
|
+
);
|
|
13957
|
+
}
|
|
13958
|
+
};
|
|
13959
|
+
return {
|
|
13960
|
+
schedule(threads) {
|
|
13961
|
+
pendingSnapshot = new Map(threads);
|
|
13962
|
+
const sinceLast = Date.now() - lastWriteAt;
|
|
13963
|
+
if (sinceLast >= interval) {
|
|
13964
|
+
const snap = pendingSnapshot;
|
|
13965
|
+
pendingSnapshot = null;
|
|
13966
|
+
writeNow(snap);
|
|
13967
|
+
return;
|
|
13968
|
+
}
|
|
13969
|
+
if (pendingTimer) return;
|
|
13970
|
+
pendingTimer = setTimeout(() => {
|
|
13971
|
+
pendingTimer = null;
|
|
13972
|
+
const snap = pendingSnapshot;
|
|
13973
|
+
pendingSnapshot = null;
|
|
13974
|
+
if (snap) writeNow(snap);
|
|
13975
|
+
}, interval - sinceLast);
|
|
13976
|
+
pendingTimer.unref?.();
|
|
13977
|
+
},
|
|
13978
|
+
flush(threads) {
|
|
13979
|
+
if (pendingTimer) {
|
|
13980
|
+
clearTimeout(pendingTimer);
|
|
13981
|
+
pendingTimer = null;
|
|
13982
|
+
pendingSnapshot = null;
|
|
13983
|
+
}
|
|
13984
|
+
writeNow(new Map(threads));
|
|
13985
|
+
},
|
|
13986
|
+
dispose() {
|
|
13987
|
+
if (pendingTimer) clearTimeout(pendingTimer);
|
|
13988
|
+
pendingTimer = null;
|
|
13989
|
+
pendingSnapshot = null;
|
|
13990
|
+
}
|
|
13991
|
+
};
|
|
13992
|
+
}
|
|
13993
|
+
function isThreadStoreShape(value) {
|
|
13994
|
+
return typeof value === "object" && value !== null && "threads" in value && typeof value.threads === "object" && value.threads !== null;
|
|
13995
|
+
}
|
|
13996
|
+
function isThreadEntry(value) {
|
|
13997
|
+
if (!value || typeof value !== "object") return false;
|
|
13998
|
+
const v = value;
|
|
13999
|
+
return (v.involvement === "started" || v.involvement === "mentioned") && typeof v.last_seen_at === "string";
|
|
14000
|
+
}
|
|
13893
14001
|
|
|
13894
14002
|
// src/safe-async.ts
|
|
13895
14003
|
async function runOrRetry(fn, opts) {
|
|
@@ -13908,6 +14016,102 @@ async function runOrRetry(fn, opts) {
|
|
|
13908
14016
|
}
|
|
13909
14017
|
}
|
|
13910
14018
|
|
|
14019
|
+
// src/channel-attachments.ts
|
|
14020
|
+
import { homedir } from "os";
|
|
14021
|
+
import { join, resolve, sep } from "path";
|
|
14022
|
+
function resolveChannelInboundDir(codeName, channelSlug) {
|
|
14023
|
+
const base = join(homedir(), ".augmented");
|
|
14024
|
+
const allowedSegment = /^[A-Za-z0-9_-]+$/;
|
|
14025
|
+
if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
|
|
14026
|
+
throw new Error(
|
|
14027
|
+
`Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
|
|
14028
|
+
);
|
|
14029
|
+
}
|
|
14030
|
+
const candidate = resolve(base, codeName, channelSlug);
|
|
14031
|
+
if (!isPathInside(candidate, base)) {
|
|
14032
|
+
throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
|
|
14033
|
+
}
|
|
14034
|
+
return candidate;
|
|
14035
|
+
}
|
|
14036
|
+
function classifyMimetype(mimetype) {
|
|
14037
|
+
if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
|
|
14038
|
+
return "attachment";
|
|
14039
|
+
}
|
|
14040
|
+
function buildSafeInboundPath(root, fileId, mimetype) {
|
|
14041
|
+
const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14042
|
+
if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
|
|
14043
|
+
const ext = extensionForMimetype(mimetype);
|
|
14044
|
+
const candidate = resolve(root, `${safeId}${ext}`);
|
|
14045
|
+
if (!isPathInside(candidate, root)) {
|
|
14046
|
+
throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
|
|
14047
|
+
}
|
|
14048
|
+
return candidate;
|
|
14049
|
+
}
|
|
14050
|
+
function extensionForMimetype(mimetype) {
|
|
14051
|
+
if (!mimetype) return ".bin";
|
|
14052
|
+
switch (mimetype) {
|
|
14053
|
+
// Images
|
|
14054
|
+
case "image/png":
|
|
14055
|
+
return ".png";
|
|
14056
|
+
case "image/jpeg":
|
|
14057
|
+
case "image/jpg":
|
|
14058
|
+
return ".jpg";
|
|
14059
|
+
case "image/gif":
|
|
14060
|
+
return ".gif";
|
|
14061
|
+
case "image/webp":
|
|
14062
|
+
return ".webp";
|
|
14063
|
+
case "image/svg+xml":
|
|
14064
|
+
return ".svg";
|
|
14065
|
+
// Docs
|
|
14066
|
+
case "application/pdf":
|
|
14067
|
+
return ".pdf";
|
|
14068
|
+
case "text/plain":
|
|
14069
|
+
return ".txt";
|
|
14070
|
+
case "text/csv":
|
|
14071
|
+
return ".csv";
|
|
14072
|
+
case "application/json":
|
|
14073
|
+
return ".json";
|
|
14074
|
+
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
14075
|
+
return ".docx";
|
|
14076
|
+
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
14077
|
+
return ".xlsx";
|
|
14078
|
+
// Audio (Telegram voice notes are typically audio/ogg; regular
|
|
14079
|
+
// audio messages are audio/mpeg)
|
|
14080
|
+
case "audio/ogg":
|
|
14081
|
+
return ".ogg";
|
|
14082
|
+
case "audio/mpeg":
|
|
14083
|
+
return ".mp3";
|
|
14084
|
+
case "audio/mp4":
|
|
14085
|
+
return ".m4a";
|
|
14086
|
+
// Video
|
|
14087
|
+
case "video/mp4":
|
|
14088
|
+
return ".mp4";
|
|
14089
|
+
case "video/quicktime":
|
|
14090
|
+
return ".mov";
|
|
14091
|
+
default:
|
|
14092
|
+
return ".bin";
|
|
14093
|
+
}
|
|
14094
|
+
}
|
|
14095
|
+
function isPathInside(target, root) {
|
|
14096
|
+
const normalizedRoot = resolve(root) + sep;
|
|
14097
|
+
const normalizedTarget = resolve(target);
|
|
14098
|
+
return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
|
|
14099
|
+
}
|
|
14100
|
+
|
|
14101
|
+
// src/slack-inbound-files.ts
|
|
14102
|
+
function classifySlackFile(file) {
|
|
14103
|
+
if (!file || typeof file.id !== "string" || !file.id) return null;
|
|
14104
|
+
const mimetype = typeof file.mimetype === "string" ? file.mimetype : "application/octet-stream";
|
|
14105
|
+
const filename = typeof file.name === "string" && file.name ? file.name : file.id;
|
|
14106
|
+
return { kind: classifyMimetype(mimetype), id: file.id, filename, mimetype };
|
|
14107
|
+
}
|
|
14108
|
+
function resolveSlackInboundDir(codeName) {
|
|
14109
|
+
return resolveChannelInboundDir(codeName, "slack-inbound");
|
|
14110
|
+
}
|
|
14111
|
+
function buildSafeInboundPath2(codeName, fileId, mimetype) {
|
|
14112
|
+
return buildSafeInboundPath(resolveSlackInboundDir(codeName), fileId, mimetype);
|
|
14113
|
+
}
|
|
14114
|
+
|
|
13911
14115
|
// src/slack-response-mode.ts
|
|
13912
14116
|
var MODES = [
|
|
13913
14117
|
"mention_only",
|
|
@@ -13988,6 +14192,37 @@ function clearPendingMessage(channel, threadTs) {
|
|
|
13988
14192
|
}
|
|
13989
14193
|
}
|
|
13990
14194
|
var trackedThreads = /* @__PURE__ */ new Map();
|
|
14195
|
+
var THREAD_STORE_PATH = resolveThreadStorePath();
|
|
14196
|
+
var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAYS);
|
|
14197
|
+
var threadPersister = null;
|
|
14198
|
+
function resolveThreadStorePath() {
|
|
14199
|
+
if (!AGENT_CODE_NAME) return null;
|
|
14200
|
+
return join2(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
|
|
14201
|
+
}
|
|
14202
|
+
function parseTtlDays(raw) {
|
|
14203
|
+
if (!raw) return void 0;
|
|
14204
|
+
const n = Number(raw);
|
|
14205
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
14206
|
+
}
|
|
14207
|
+
function buildThreadKey(channel, threadTs) {
|
|
14208
|
+
if (!channel || !threadTs) return null;
|
|
14209
|
+
return `${channel}:${threadTs}`;
|
|
14210
|
+
}
|
|
14211
|
+
function rememberThread(channel, threadTs, involvement, opts = {}) {
|
|
14212
|
+
const key = buildThreadKey(channel, threadTs);
|
|
14213
|
+
if (!key) return;
|
|
14214
|
+
const existing = trackedThreads.get(key);
|
|
14215
|
+
const next = {
|
|
14216
|
+
involvement: opts.override || !existing ? involvement : existing.involvement,
|
|
14217
|
+
last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
14218
|
+
};
|
|
14219
|
+
trackedThreads.set(key, next);
|
|
14220
|
+
if (isShuttingDown) {
|
|
14221
|
+
threadPersister?.flush(trackedThreads);
|
|
14222
|
+
} else {
|
|
14223
|
+
threadPersister?.schedule(trackedThreads);
|
|
14224
|
+
}
|
|
14225
|
+
}
|
|
13991
14226
|
if (!BOT_TOKEN || !APP_TOKEN) {
|
|
13992
14227
|
console.error(
|
|
13993
14228
|
"slack-channel: Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN. Cannot start."
|
|
@@ -14001,18 +14236,17 @@ var mcp = new Server(
|
|
|
14001
14236
|
experimental: { "claude/channel": {} },
|
|
14002
14237
|
tools: {}
|
|
14003
14238
|
},
|
|
14239
|
+
// NOTE: Claude Code truncates MCP server instructions at 2048 chars.
|
|
14240
|
+
// Attachments rules live near the top because they get chopped otherwise
|
|
14241
|
+
// and the agent silently loses attachment-handling guidance.
|
|
14004
14242
|
instructions: [
|
|
14005
14243
|
'Messages from Slack arrive as <channel source="slack" user="<slack-id>" user_name="<display-name>" channel="..." thread_ts="...">.',
|
|
14006
|
-
"
|
|
14007
|
-
"
|
|
14008
|
-
"
|
|
14009
|
-
"
|
|
14010
|
-
"
|
|
14011
|
-
"
|
|
14012
|
-
"Messages with auto_followed=true are from threads you previously participated in.",
|
|
14013
|
-
"For auto-followed messages, use relevance judgment: only reply if you have something useful to add.",
|
|
14014
|
-
"Do NOT reply to every auto-followed message \u2014 skip if the conversation has moved on, the message is directed at someone else, or your input would not add value.",
|
|
14015
|
-
"To deliver a file (PDF, image, report), save it under your project dir and call slack.upload_file with path, channel, and thread_ts."
|
|
14244
|
+
"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 slack.download_attachment. That tool is only for entries with `file_id` but NO `path` (PDF, docx, csv, etc.): pass file_id + channel 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. Never tell the user about internal file-handling failures that don't affect the answer.",
|
|
14245
|
+
"Reply via slack.reply passing channel and thread_ts from the tag. Always include thread_ts on threaded replies so the response lands in the same thread.",
|
|
14246
|
+
"Address users by user_name, never by raw user ID. In multi-participant threads the CURRENT speaker is the one on the latest <channel> tag \u2014 don't conflate with earlier participants.",
|
|
14247
|
+
"Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly.",
|
|
14248
|
+
'auto_followed="true" messages are from threads you previously joined: reply only if you have something useful to add. Skip if the conversation moved on, the message is directed at someone else, or your input would not add value.',
|
|
14249
|
+
"To deliver a file, save it under your project dir and call slack.upload_file with path, channel, thread_ts."
|
|
14016
14250
|
].join(" ")
|
|
14017
14251
|
}
|
|
14018
14252
|
);
|
|
@@ -14072,6 +14306,24 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
14072
14306
|
required: ["channel", "thread_ts"]
|
|
14073
14307
|
}
|
|
14074
14308
|
},
|
|
14309
|
+
{
|
|
14310
|
+
name: "slack.download_attachment",
|
|
14311
|
+
description: "Download an inbound Slack attachment on demand. Use this for non-image files (PDFs, docx, csv, etc.) that arrived via a <channel> tag \u2014 look for `file_id` in the `files` meta. Requires the `channel` attribute 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>/slack-inbound/). Images are auto-downloaded on receipt and don't need this tool.",
|
|
14312
|
+
inputSchema: {
|
|
14313
|
+
type: "object",
|
|
14314
|
+
properties: {
|
|
14315
|
+
file_id: {
|
|
14316
|
+
type: "string",
|
|
14317
|
+
description: "Slack file id (from the `file_id` field on the `files` meta array of the inbound <channel> tag)"
|
|
14318
|
+
},
|
|
14319
|
+
channel: {
|
|
14320
|
+
type: "string",
|
|
14321
|
+
description: "Slack channel ID from the `channel` attribute of the same inbound <channel> tag that surfaced the file. The tool only downloads attachments that were shared in this channel."
|
|
14322
|
+
}
|
|
14323
|
+
},
|
|
14324
|
+
required: ["file_id", "channel"]
|
|
14325
|
+
}
|
|
14326
|
+
},
|
|
14075
14327
|
{
|
|
14076
14328
|
name: "slack.upload_file",
|
|
14077
14329
|
description: "Upload a file from the agent project dir to a Slack channel or thread. Use this for PDFs, images, reports, and any binary deliverables the agent generates. Path must be inside the agent's project directory.",
|
|
@@ -14136,12 +14388,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
14136
14388
|
};
|
|
14137
14389
|
}
|
|
14138
14390
|
if (THREAD_AUTO_FOLLOW !== "off") {
|
|
14139
|
-
const
|
|
14140
|
-
|
|
14141
|
-
if (!trackedThreads.has(trackKey)) {
|
|
14142
|
-
trackedThreads.set(trackKey, thread_ts ? "mentioned" : "started");
|
|
14143
|
-
}
|
|
14144
|
-
}
|
|
14391
|
+
const trackTs = thread_ts ?? data.ts ?? void 0;
|
|
14392
|
+
rememberThread(channel, trackTs, thread_ts ? "mentioned" : "started");
|
|
14145
14393
|
}
|
|
14146
14394
|
return { content: [{ type: "text", text: "sent" }] };
|
|
14147
14395
|
} catch (err) {
|
|
@@ -14248,8 +14496,8 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
14248
14496
|
isError: true
|
|
14249
14497
|
};
|
|
14250
14498
|
}
|
|
14251
|
-
const allowedRoot =
|
|
14252
|
-
const resolvedPath =
|
|
14499
|
+
const allowedRoot = resolve2(homedir2(), ".augmented", AGENT_CODE_NAME, "project") + "/";
|
|
14500
|
+
const resolvedPath = resolve2(path);
|
|
14253
14501
|
if (!resolvedPath.startsWith(allowedRoot)) {
|
|
14254
14502
|
return {
|
|
14255
14503
|
content: [{
|
|
@@ -14270,7 +14518,7 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
14270
14518
|
};
|
|
14271
14519
|
}
|
|
14272
14520
|
size = stat.size;
|
|
14273
|
-
bytes =
|
|
14521
|
+
bytes = readFileSync2(resolvedPath);
|
|
14274
14522
|
} catch (err) {
|
|
14275
14523
|
return {
|
|
14276
14524
|
content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
|
|
@@ -14339,8 +14587,186 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
14339
14587
|
};
|
|
14340
14588
|
}
|
|
14341
14589
|
}
|
|
14590
|
+
if (name === "slack.download_attachment") {
|
|
14591
|
+
const { file_id, channel } = args;
|
|
14592
|
+
if (typeof file_id !== "string" || !file_id.trim()) {
|
|
14593
|
+
return {
|
|
14594
|
+
content: [{ type: "text", text: "slack.download_attachment requires a string `file_id`." }],
|
|
14595
|
+
isError: true
|
|
14596
|
+
};
|
|
14597
|
+
}
|
|
14598
|
+
if (typeof channel !== "string" || !channel.trim()) {
|
|
14599
|
+
return {
|
|
14600
|
+
content: [{ type: "text", text: "slack.download_attachment requires a string `channel` (from the inbound <channel> tag)." }],
|
|
14601
|
+
isError: true
|
|
14602
|
+
};
|
|
14603
|
+
}
|
|
14604
|
+
if (!AGENT_CODE_NAME) {
|
|
14605
|
+
return {
|
|
14606
|
+
content: [{
|
|
14607
|
+
type: "text",
|
|
14608
|
+
text: "Download refused: AGT_AGENT_CODE_NAME not set in the MCP server env. Reprovision the slack channel so the download tool can scope paths."
|
|
14609
|
+
}],
|
|
14610
|
+
isError: true
|
|
14611
|
+
};
|
|
14612
|
+
}
|
|
14613
|
+
const trimmedFileId = file_id.trim();
|
|
14614
|
+
const trimmedChannel = channel.trim();
|
|
14615
|
+
if (!isDownloadableFileId(trimmedFileId, trimmedChannel)) {
|
|
14616
|
+
return {
|
|
14617
|
+
content: [{
|
|
14618
|
+
type: "text",
|
|
14619
|
+
text: "Download refused: file_id was not surfaced in a recent inbound `files` meta entry for this channel. You can only download attachments the originating conversation has shared with you."
|
|
14620
|
+
}],
|
|
14621
|
+
isError: true
|
|
14622
|
+
};
|
|
14623
|
+
}
|
|
14624
|
+
try {
|
|
14625
|
+
const savedPath = await downloadSlackFile(trimmedFileId, AGENT_CODE_NAME);
|
|
14626
|
+
return { content: [{ type: "text", text: `Downloaded to ${savedPath}` }] };
|
|
14627
|
+
} catch (err) {
|
|
14628
|
+
return {
|
|
14629
|
+
content: [{
|
|
14630
|
+
type: "text",
|
|
14631
|
+
text: `Failed to download attachment: ${redactAugmentedPaths(err.message)}`
|
|
14632
|
+
}],
|
|
14633
|
+
isError: true
|
|
14634
|
+
};
|
|
14635
|
+
}
|
|
14636
|
+
}
|
|
14342
14637
|
throw new Error(`Unknown tool: ${name}`);
|
|
14343
14638
|
});
|
|
14639
|
+
var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
|
|
14640
|
+
var SLACK_DOWNLOAD_TIMEOUT_MS = 15e3;
|
|
14641
|
+
var DOWNLOAD_ALLOWLIST_TTL_MS = 60 * 60 * 1e3;
|
|
14642
|
+
var downloadAllowlist = /* @__PURE__ */ new Map();
|
|
14643
|
+
function pruneExpiredAllowlist(now) {
|
|
14644
|
+
for (const [fileId, entry] of downloadAllowlist) {
|
|
14645
|
+
if (now > entry.expiresAt) downloadAllowlist.delete(fileId);
|
|
14646
|
+
}
|
|
14647
|
+
}
|
|
14648
|
+
function registerDownloadableFileId(fileId, channel) {
|
|
14649
|
+
const now = Date.now();
|
|
14650
|
+
pruneExpiredAllowlist(now);
|
|
14651
|
+
downloadAllowlist.set(fileId, {
|
|
14652
|
+
channel,
|
|
14653
|
+
expiresAt: now + DOWNLOAD_ALLOWLIST_TTL_MS
|
|
14654
|
+
});
|
|
14655
|
+
}
|
|
14656
|
+
function isDownloadableFileId(fileId, channel) {
|
|
14657
|
+
const entry = downloadAllowlist.get(fileId);
|
|
14658
|
+
if (!entry) return false;
|
|
14659
|
+
if (Date.now() > entry.expiresAt) {
|
|
14660
|
+
downloadAllowlist.delete(fileId);
|
|
14661
|
+
return false;
|
|
14662
|
+
}
|
|
14663
|
+
return entry.channel === channel;
|
|
14664
|
+
}
|
|
14665
|
+
function redactAugmentedPaths(msg) {
|
|
14666
|
+
return msg.replaceAll(
|
|
14667
|
+
new RegExp(`${homedir2().replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}/\\.augmented/[^\\s'"\`]*`, "g"),
|
|
14668
|
+
"<augmented-path>"
|
|
14669
|
+
);
|
|
14670
|
+
}
|
|
14671
|
+
async function downloadSlackFile(fileId, codeName) {
|
|
14672
|
+
const infoRes = await fetch(
|
|
14673
|
+
`https://slack.com/api/files.info?file=${encodeURIComponent(fileId)}`,
|
|
14674
|
+
{
|
|
14675
|
+
headers: { Authorization: `Bearer ${BOT_TOKEN}` },
|
|
14676
|
+
signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
|
|
14677
|
+
}
|
|
14678
|
+
);
|
|
14679
|
+
const infoData = await infoRes.json();
|
|
14680
|
+
if (!infoData.ok || !infoData.file) {
|
|
14681
|
+
throw new Error(`files.info failed: ${infoData.error ?? "unknown"}`);
|
|
14682
|
+
}
|
|
14683
|
+
const file = infoData.file;
|
|
14684
|
+
if (typeof file.size === "number" && file.size > MAX_INBOUND_FILE_BYTES) {
|
|
14685
|
+
throw new Error(
|
|
14686
|
+
`file ${fileId} is ${file.size} bytes \u2014 exceeds the ${MAX_INBOUND_FILE_BYTES}-byte inbound cap`
|
|
14687
|
+
);
|
|
14688
|
+
}
|
|
14689
|
+
const url = file.url_private_download ?? file.url_private;
|
|
14690
|
+
if (!url) throw new Error(`file ${fileId} has no url_private`);
|
|
14691
|
+
const res = await fetch(url, {
|
|
14692
|
+
headers: { Authorization: `Bearer ${BOT_TOKEN}` },
|
|
14693
|
+
signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
|
|
14694
|
+
});
|
|
14695
|
+
if (!res.ok) {
|
|
14696
|
+
throw new Error(`download failed: HTTP ${res.status}`);
|
|
14697
|
+
}
|
|
14698
|
+
const contentLengthHeader = res.headers.get("content-length");
|
|
14699
|
+
if (contentLengthHeader) {
|
|
14700
|
+
const contentLength = Number(contentLengthHeader);
|
|
14701
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_INBOUND_FILE_BYTES) {
|
|
14702
|
+
throw new Error(
|
|
14703
|
+
`file ${fileId} transport reports ${contentLength} bytes \u2014 exceeds the ${MAX_INBOUND_FILE_BYTES}-byte inbound cap`
|
|
14704
|
+
);
|
|
14705
|
+
}
|
|
14706
|
+
}
|
|
14707
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
14708
|
+
if (bytes.byteLength > MAX_INBOUND_FILE_BYTES) {
|
|
14709
|
+
throw new Error(
|
|
14710
|
+
`file ${fileId} downloaded ${bytes.byteLength} bytes \u2014 exceeds the ${MAX_INBOUND_FILE_BYTES}-byte inbound cap`
|
|
14711
|
+
);
|
|
14712
|
+
}
|
|
14713
|
+
const savedPath = buildSafeInboundPath2(codeName, fileId, file.mimetype);
|
|
14714
|
+
const dir = resolveSlackInboundDir(codeName);
|
|
14715
|
+
if (!isPathInside(savedPath, dir)) {
|
|
14716
|
+
throw new Error(`refusing to write ${savedPath} outside ${dir}`);
|
|
14717
|
+
}
|
|
14718
|
+
mkdirSync2(dir, { recursive: true });
|
|
14719
|
+
writeFileSync2(savedPath, bytes, { mode: 384 });
|
|
14720
|
+
try {
|
|
14721
|
+
chmodSync(savedPath, 384);
|
|
14722
|
+
} catch {
|
|
14723
|
+
}
|
|
14724
|
+
return savedPath;
|
|
14725
|
+
}
|
|
14726
|
+
async function buildInboundFileMeta(rawFiles, codeName, channel) {
|
|
14727
|
+
if (!Array.isArray(rawFiles) || rawFiles.length === 0) return [];
|
|
14728
|
+
if (!channel) return [];
|
|
14729
|
+
const out = [];
|
|
14730
|
+
for (const raw of rawFiles) {
|
|
14731
|
+
const classified = classifySlackFile(
|
|
14732
|
+
raw ?? null
|
|
14733
|
+
);
|
|
14734
|
+
if (!classified) continue;
|
|
14735
|
+
if (classified.kind === "image" && codeName) {
|
|
14736
|
+
try {
|
|
14737
|
+
const path = await downloadSlackFile(classified.id, codeName);
|
|
14738
|
+
out.push({
|
|
14739
|
+
kind: "image",
|
|
14740
|
+
file_id: classified.id,
|
|
14741
|
+
filename: classified.filename,
|
|
14742
|
+
mimetype: classified.mimetype,
|
|
14743
|
+
path
|
|
14744
|
+
});
|
|
14745
|
+
} catch (err) {
|
|
14746
|
+
process.stderr.write(
|
|
14747
|
+
`slack-channel: image auto-download failed for ${classified.id}: ${redactAugmentedPaths(err.message)}
|
|
14748
|
+
`
|
|
14749
|
+
);
|
|
14750
|
+
registerDownloadableFileId(classified.id, channel);
|
|
14751
|
+
out.push({
|
|
14752
|
+
kind: "attachment",
|
|
14753
|
+
file_id: classified.id,
|
|
14754
|
+
filename: classified.filename,
|
|
14755
|
+
mimetype: classified.mimetype
|
|
14756
|
+
});
|
|
14757
|
+
}
|
|
14758
|
+
} else {
|
|
14759
|
+
registerDownloadableFileId(classified.id, channel);
|
|
14760
|
+
out.push({
|
|
14761
|
+
kind: "attachment",
|
|
14762
|
+
file_id: classified.id,
|
|
14763
|
+
filename: classified.filename,
|
|
14764
|
+
mimetype: classified.mimetype
|
|
14765
|
+
});
|
|
14766
|
+
}
|
|
14767
|
+
}
|
|
14768
|
+
return out;
|
|
14769
|
+
}
|
|
14344
14770
|
await mcp.connect(new StdioServerTransport());
|
|
14345
14771
|
var botUserId = null;
|
|
14346
14772
|
async function getBotUserId() {
|
|
@@ -14429,21 +14855,22 @@ async function connectSocketMode() {
|
|
|
14429
14855
|
}
|
|
14430
14856
|
const isDirectMessage = evt.channel?.startsWith("D");
|
|
14431
14857
|
const isThreadReply = !!evt.thread_ts && evt.thread_ts !== evt.ts;
|
|
14432
|
-
const
|
|
14433
|
-
|
|
14434
|
-
|
|
14858
|
+
const trackTs = evt.thread_ts ?? evt.ts ?? "";
|
|
14859
|
+
const threadKey = buildThreadKey(evt.channel, trackTs);
|
|
14860
|
+
if (ALLOWED_USERS.size > 0 && evt.user && !ALLOWED_USERS.has(evt.user)) return;
|
|
14861
|
+
if (evt.type === "app_mention") {
|
|
14862
|
+
rememberThread(evt.channel, trackTs, "mentioned");
|
|
14435
14863
|
}
|
|
14436
14864
|
if (evt.type === "message" && evt.channel && !isDirectMessage) {
|
|
14437
14865
|
if (isThreadReply) {
|
|
14438
14866
|
if (THREAD_AUTO_FOLLOW === "off") return;
|
|
14439
|
-
const threadInvolvement = trackedThreads.get(threadKey);
|
|
14867
|
+
const threadInvolvement = threadKey ? trackedThreads.get(threadKey)?.involvement : void 0;
|
|
14440
14868
|
if (!threadInvolvement) return;
|
|
14441
14869
|
if (THREAD_AUTO_FOLLOW === "started" && threadInvolvement !== "started") return;
|
|
14442
14870
|
} else {
|
|
14443
14871
|
if (!channelMessageShouldRespond(evt.text ?? "", CHANNEL_RESPONSE_MODE)) return;
|
|
14444
14872
|
}
|
|
14445
14873
|
}
|
|
14446
|
-
if (ALLOWED_USERS.size > 0 && evt.user && !ALLOWED_USERS.has(evt.user)) return;
|
|
14447
14874
|
const text = evt.text ?? "";
|
|
14448
14875
|
const channel = evt.channel ?? "";
|
|
14449
14876
|
const ts = evt.ts ?? "";
|
|
@@ -14460,11 +14887,16 @@ async function connectSocketMode() {
|
|
|
14460
14887
|
}).catch(() => {
|
|
14461
14888
|
});
|
|
14462
14889
|
}
|
|
14463
|
-
const isAutoFollowed = evt.type === "message" && isThreadReply && trackedThreads.has(threadKey);
|
|
14890
|
+
const isAutoFollowed = evt.type === "message" && isThreadReply && !!threadKey && trackedThreads.has(threadKey);
|
|
14464
14891
|
if (channel && ts) {
|
|
14465
14892
|
trackPendingMessage(channel, threadTs, ts);
|
|
14466
14893
|
}
|
|
14467
14894
|
const userName = await resolveUserName(user);
|
|
14895
|
+
const fileMeta = await buildInboundFileMeta(evt.files, AGENT_CODE_NAME, channel);
|
|
14896
|
+
const downloadedImages = fileMeta.filter(
|
|
14897
|
+
(f) => f.kind === "image" && typeof f.path === "string"
|
|
14898
|
+
);
|
|
14899
|
+
const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
|
|
14468
14900
|
await mcp.notification({
|
|
14469
14901
|
method: "notifications/claude/channel",
|
|
14470
14902
|
params: {
|
|
@@ -14475,7 +14907,11 @@ async function connectSocketMode() {
|
|
|
14475
14907
|
channel,
|
|
14476
14908
|
thread_ts: threadTs,
|
|
14477
14909
|
event_type: evt.type,
|
|
14478
|
-
...isAutoFollowed ? { auto_followed: true } : {}
|
|
14910
|
+
...isAutoFollowed ? { auto_followed: "true" } : {},
|
|
14911
|
+
// Only set these when we actually have attachments to avoid
|
|
14912
|
+
// bloating every notification with empty metadata.
|
|
14913
|
+
...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
|
|
14914
|
+
...imagePath ? { image_path: imagePath } : {}
|
|
14479
14915
|
}
|
|
14480
14916
|
}
|
|
14481
14917
|
});
|
|
@@ -14498,6 +14934,14 @@ async function connectSocketMode() {
|
|
|
14498
14934
|
function shutdown(reason, exitCode = 0) {
|
|
14499
14935
|
if (isShuttingDown) return;
|
|
14500
14936
|
isShuttingDown = true;
|
|
14937
|
+
try {
|
|
14938
|
+
threadPersister?.flush(trackedThreads);
|
|
14939
|
+
} catch {
|
|
14940
|
+
}
|
|
14941
|
+
try {
|
|
14942
|
+
threadPersister?.dispose();
|
|
14943
|
+
} catch {
|
|
14944
|
+
}
|
|
14501
14945
|
process.stderr.write(`slack-channel: ${reason} \u2014 closing Socket Mode and exiting
|
|
14502
14946
|
`);
|
|
14503
14947
|
try {
|
|
@@ -14516,6 +14960,29 @@ process.on("unhandledRejection", (reason) => {
|
|
|
14516
14960
|
process.stderr.write(`slack-channel: unhandledRejection (continuing): ${msg}
|
|
14517
14961
|
`);
|
|
14518
14962
|
});
|
|
14963
|
+
if (THREAD_STORE_PATH) {
|
|
14964
|
+
const threadStoreLabel = "slack-tracked-threads.json";
|
|
14965
|
+
const redact = (msg) => msg.replaceAll(THREAD_STORE_PATH, threadStoreLabel);
|
|
14966
|
+
const { threads, pruned } = loadThreadStore(THREAD_STORE_PATH, {
|
|
14967
|
+
ttlDays: THREAD_STORE_TTL_DAYS,
|
|
14968
|
+
onError: (msg) => process.stderr.write(`${redact(msg)}
|
|
14969
|
+
`)
|
|
14970
|
+
});
|
|
14971
|
+
for (const [key, entry] of threads) trackedThreads.set(key, entry);
|
|
14972
|
+
threadPersister = createThreadPersister({
|
|
14973
|
+
filePath: THREAD_STORE_PATH,
|
|
14974
|
+
onError: (msg) => process.stderr.write(`${redact(msg)}
|
|
14975
|
+
`)
|
|
14976
|
+
});
|
|
14977
|
+
process.stderr.write(
|
|
14978
|
+
`slack-channel: hydrated ${trackedThreads.size} tracked thread(s)${pruned > 0 ? ` (pruned ${pruned} stale)` : ""} from ${threadStoreLabel}
|
|
14979
|
+
`
|
|
14980
|
+
);
|
|
14981
|
+
} else {
|
|
14982
|
+
process.stderr.write(
|
|
14983
|
+
"slack-channel: AGT_AGENT_CODE_NAME not set \u2014 running without thread-follow persistence\n"
|
|
14984
|
+
);
|
|
14985
|
+
}
|
|
14519
14986
|
botUserId = await getBotUserId();
|
|
14520
14987
|
process.stderr.write(`slack-channel: Bot user ID: ${botUserId}
|
|
14521
14988
|
`);
|