@integrity-labs/agt-cli 0.14.10 → 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-RMTMETCK.js → chunk-VWCF6BOZ.js} +103 -13
- 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 +573 -43
- package/mcp/telegram-channel.js +439 -27
- package/package.json +1 -1
- package/dist/chunk-RMTMETCK.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"));
|
|
@@ -13769,6 +13769,31 @@ var Server = class extends Protocol {
|
|
|
13769
13769
|
}
|
|
13770
13770
|
};
|
|
13771
13771
|
|
|
13772
|
+
// src/slack-inbound-filter.ts
|
|
13773
|
+
var ALLOWED_MESSAGE_SUBTYPES = /* @__PURE__ */ new Set([
|
|
13774
|
+
void 0,
|
|
13775
|
+
"file_share",
|
|
13776
|
+
"thread_broadcast"
|
|
13777
|
+
]);
|
|
13778
|
+
function decideSlackMessageForward(evt) {
|
|
13779
|
+
if (evt.type !== "message") {
|
|
13780
|
+
return { forward: true };
|
|
13781
|
+
}
|
|
13782
|
+
if (!ALLOWED_MESSAGE_SUBTYPES.has(evt.subtype)) {
|
|
13783
|
+
return { forward: false, reason: "subtype" };
|
|
13784
|
+
}
|
|
13785
|
+
if (!evt.user) {
|
|
13786
|
+
return { forward: false, reason: "no_user" };
|
|
13787
|
+
}
|
|
13788
|
+
const hasText = typeof evt.text === "string" && evt.text.trim().length > 0;
|
|
13789
|
+
const hasFiles = Array.isArray(evt.files) && evt.files.length > 0;
|
|
13790
|
+
const hasBlocks = Array.isArray(evt.blocks) && evt.blocks.length > 0;
|
|
13791
|
+
if (!hasText && !hasFiles && !hasBlocks) {
|
|
13792
|
+
return { forward: false, reason: "empty" };
|
|
13793
|
+
}
|
|
13794
|
+
return { forward: true };
|
|
13795
|
+
}
|
|
13796
|
+
|
|
13772
13797
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
13773
13798
|
import process2 from "process";
|
|
13774
13799
|
|
|
@@ -13850,21 +13875,242 @@ var StdioServerTransport = class {
|
|
|
13850
13875
|
this.onclose?.();
|
|
13851
13876
|
}
|
|
13852
13877
|
send(message) {
|
|
13853
|
-
return new Promise((
|
|
13878
|
+
return new Promise((resolve3) => {
|
|
13854
13879
|
const json = serializeMessage(message);
|
|
13855
13880
|
if (this._stdout.write(json)) {
|
|
13856
|
-
|
|
13881
|
+
resolve3();
|
|
13857
13882
|
} else {
|
|
13858
|
-
this._stdout.once("drain",
|
|
13883
|
+
this._stdout.once("drain", resolve3);
|
|
13859
13884
|
}
|
|
13860
13885
|
});
|
|
13861
13886
|
}
|
|
13862
13887
|
};
|
|
13863
13888
|
|
|
13864
13889
|
// src/slack-channel.ts
|
|
13865
|
-
import { readFileSync, statSync } from "fs";
|
|
13866
|
-
import { basename, resolve } from "path";
|
|
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
|
+
}
|
|
14001
|
+
|
|
14002
|
+
// src/safe-async.ts
|
|
14003
|
+
async function runOrRetry(fn, opts) {
|
|
14004
|
+
try {
|
|
14005
|
+
await fn();
|
|
14006
|
+
} catch (err) {
|
|
14007
|
+
opts.onError(err);
|
|
14008
|
+
if (!opts.shouldRetry()) return;
|
|
14009
|
+
const schedule = opts.scheduleRetry ?? ((f, ms) => {
|
|
14010
|
+
const t = setTimeout(f, ms);
|
|
14011
|
+
t.unref?.();
|
|
14012
|
+
});
|
|
14013
|
+
schedule(() => {
|
|
14014
|
+
void runOrRetry(fn, opts);
|
|
14015
|
+
}, opts.retryDelayMs);
|
|
14016
|
+
}
|
|
14017
|
+
}
|
|
14018
|
+
|
|
14019
|
+
// src/channel-attachments.ts
|
|
13867
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
|
+
}
|
|
13868
14114
|
|
|
13869
14115
|
// src/slack-response-mode.ts
|
|
13870
14116
|
var MODES = [
|
|
@@ -13946,6 +14192,37 @@ function clearPendingMessage(channel, threadTs) {
|
|
|
13946
14192
|
}
|
|
13947
14193
|
}
|
|
13948
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
|
+
}
|
|
13949
14226
|
if (!BOT_TOKEN || !APP_TOKEN) {
|
|
13950
14227
|
console.error(
|
|
13951
14228
|
"slack-channel: Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN. Cannot start."
|
|
@@ -13959,18 +14236,17 @@ var mcp = new Server(
|
|
|
13959
14236
|
experimental: { "claude/channel": {} },
|
|
13960
14237
|
tools: {}
|
|
13961
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.
|
|
13962
14242
|
instructions: [
|
|
13963
14243
|
'Messages from Slack arrive as <channel source="slack" user="<slack-id>" user_name="<display-name>" channel="..." thread_ts="...">.',
|
|
13964
|
-
"
|
|
13965
|
-
"
|
|
13966
|
-
"
|
|
13967
|
-
"
|
|
13968
|
-
"
|
|
13969
|
-
"
|
|
13970
|
-
"Messages with auto_followed=true are from threads you previously participated in.",
|
|
13971
|
-
"For auto-followed messages, use relevance judgment: only reply if you have something useful to add.",
|
|
13972
|
-
"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.",
|
|
13973
|
-
"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."
|
|
13974
14250
|
].join(" ")
|
|
13975
14251
|
}
|
|
13976
14252
|
);
|
|
@@ -14030,6 +14306,24 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
14030
14306
|
required: ["channel", "thread_ts"]
|
|
14031
14307
|
}
|
|
14032
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
|
+
},
|
|
14033
14327
|
{
|
|
14034
14328
|
name: "slack.upload_file",
|
|
14035
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.",
|
|
@@ -14094,12 +14388,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
14094
14388
|
};
|
|
14095
14389
|
}
|
|
14096
14390
|
if (THREAD_AUTO_FOLLOW !== "off") {
|
|
14097
|
-
const
|
|
14098
|
-
|
|
14099
|
-
if (!trackedThreads.has(trackKey)) {
|
|
14100
|
-
trackedThreads.set(trackKey, thread_ts ? "mentioned" : "started");
|
|
14101
|
-
}
|
|
14102
|
-
}
|
|
14391
|
+
const trackTs = thread_ts ?? data.ts ?? void 0;
|
|
14392
|
+
rememberThread(channel, trackTs, thread_ts ? "mentioned" : "started");
|
|
14103
14393
|
}
|
|
14104
14394
|
return { content: [{ type: "text", text: "sent" }] };
|
|
14105
14395
|
} catch (err) {
|
|
@@ -14206,8 +14496,8 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
14206
14496
|
isError: true
|
|
14207
14497
|
};
|
|
14208
14498
|
}
|
|
14209
|
-
const allowedRoot =
|
|
14210
|
-
const resolvedPath =
|
|
14499
|
+
const allowedRoot = resolve2(homedir2(), ".augmented", AGENT_CODE_NAME, "project") + "/";
|
|
14500
|
+
const resolvedPath = resolve2(path);
|
|
14211
14501
|
if (!resolvedPath.startsWith(allowedRoot)) {
|
|
14212
14502
|
return {
|
|
14213
14503
|
content: [{
|
|
@@ -14228,7 +14518,7 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
14228
14518
|
};
|
|
14229
14519
|
}
|
|
14230
14520
|
size = stat.size;
|
|
14231
|
-
bytes =
|
|
14521
|
+
bytes = readFileSync2(resolvedPath);
|
|
14232
14522
|
} catch (err) {
|
|
14233
14523
|
return {
|
|
14234
14524
|
content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
|
|
@@ -14297,8 +14587,186 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
14297
14587
|
};
|
|
14298
14588
|
}
|
|
14299
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
|
+
}
|
|
14300
14637
|
throw new Error(`Unknown tool: ${name}`);
|
|
14301
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
|
+
}
|
|
14302
14770
|
await mcp.connect(new StdioServerTransport());
|
|
14303
14771
|
var botUserId = null;
|
|
14304
14772
|
async function getBotUserId() {
|
|
@@ -14333,6 +14801,16 @@ async function resolveUserName(userId) {
|
|
|
14333
14801
|
}
|
|
14334
14802
|
var currentWs = null;
|
|
14335
14803
|
var isShuttingDown = false;
|
|
14804
|
+
function connectSocketModeSafely() {
|
|
14805
|
+
void runOrRetry(connectSocketMode, {
|
|
14806
|
+
retryDelayMs: 1e4,
|
|
14807
|
+
onError: (err) => process.stderr.write(
|
|
14808
|
+
`slack-channel: Socket Mode connect failed: ${err.message} \u2014 retrying in 10s
|
|
14809
|
+
`
|
|
14810
|
+
),
|
|
14811
|
+
shouldRetry: () => !isShuttingDown
|
|
14812
|
+
});
|
|
14813
|
+
}
|
|
14336
14814
|
async function connectSocketMode() {
|
|
14337
14815
|
if (isShuttingDown) return;
|
|
14338
14816
|
const res = await fetch("https://slack.com/api/apps.connections.open", {
|
|
@@ -14344,7 +14822,7 @@ async function connectSocketMode() {
|
|
|
14344
14822
|
if (!data.ok || !data.url) {
|
|
14345
14823
|
process.stderr.write(`slack-channel: Socket Mode connection failed: ${data.error}
|
|
14346
14824
|
`);
|
|
14347
|
-
if (!isShuttingDown) setTimeout(
|
|
14825
|
+
if (!isShuttingDown) setTimeout(connectSocketModeSafely, 1e4).unref?.();
|
|
14348
14826
|
return;
|
|
14349
14827
|
}
|
|
14350
14828
|
const ws = new WebSocket(data.url);
|
|
@@ -14369,23 +14847,30 @@ async function connectSocketMode() {
|
|
|
14369
14847
|
const evt = msg.payload.event;
|
|
14370
14848
|
if (evt.user === botUserId) return;
|
|
14371
14849
|
if (evt.type !== "app_mention" && evt.type !== "message") return;
|
|
14850
|
+
const filterDecision = decideSlackMessageForward(evt);
|
|
14851
|
+
if (!filterDecision.forward) {
|
|
14852
|
+
process.stderr.write(`slack-channel: dropped message event (reason=${filterDecision.reason}, subtype=${evt.subtype ?? "none"}, ts=${evt.ts ?? "n/a"})
|
|
14853
|
+
`);
|
|
14854
|
+
return;
|
|
14855
|
+
}
|
|
14372
14856
|
const isDirectMessage = evt.channel?.startsWith("D");
|
|
14373
14857
|
const isThreadReply = !!evt.thread_ts && evt.thread_ts !== evt.ts;
|
|
14374
|
-
const
|
|
14375
|
-
|
|
14376
|
-
|
|
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");
|
|
14377
14863
|
}
|
|
14378
14864
|
if (evt.type === "message" && evt.channel && !isDirectMessage) {
|
|
14379
14865
|
if (isThreadReply) {
|
|
14380
14866
|
if (THREAD_AUTO_FOLLOW === "off") return;
|
|
14381
|
-
const threadInvolvement = trackedThreads.get(threadKey);
|
|
14867
|
+
const threadInvolvement = threadKey ? trackedThreads.get(threadKey)?.involvement : void 0;
|
|
14382
14868
|
if (!threadInvolvement) return;
|
|
14383
14869
|
if (THREAD_AUTO_FOLLOW === "started" && threadInvolvement !== "started") return;
|
|
14384
14870
|
} else {
|
|
14385
14871
|
if (!channelMessageShouldRespond(evt.text ?? "", CHANNEL_RESPONSE_MODE)) return;
|
|
14386
14872
|
}
|
|
14387
14873
|
}
|
|
14388
|
-
if (ALLOWED_USERS.size > 0 && evt.user && !ALLOWED_USERS.has(evt.user)) return;
|
|
14389
14874
|
const text = evt.text ?? "";
|
|
14390
14875
|
const channel = evt.channel ?? "";
|
|
14391
14876
|
const ts = evt.ts ?? "";
|
|
@@ -14402,11 +14887,16 @@ async function connectSocketMode() {
|
|
|
14402
14887
|
}).catch(() => {
|
|
14403
14888
|
});
|
|
14404
14889
|
}
|
|
14405
|
-
const isAutoFollowed = evt.type === "message" && isThreadReply && trackedThreads.has(threadKey);
|
|
14890
|
+
const isAutoFollowed = evt.type === "message" && isThreadReply && !!threadKey && trackedThreads.has(threadKey);
|
|
14406
14891
|
if (channel && ts) {
|
|
14407
14892
|
trackPendingMessage(channel, threadTs, ts);
|
|
14408
14893
|
}
|
|
14409
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;
|
|
14410
14900
|
await mcp.notification({
|
|
14411
14901
|
method: "notifications/claude/channel",
|
|
14412
14902
|
params: {
|
|
@@ -14417,7 +14907,11 @@ async function connectSocketMode() {
|
|
|
14417
14907
|
channel,
|
|
14418
14908
|
thread_ts: threadTs,
|
|
14419
14909
|
event_type: evt.type,
|
|
14420
|
-
...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 } : {}
|
|
14421
14915
|
}
|
|
14422
14916
|
}
|
|
14423
14917
|
});
|
|
@@ -14430,7 +14924,7 @@ async function connectSocketMode() {
|
|
|
14430
14924
|
if (currentWs === ws) currentWs = null;
|
|
14431
14925
|
if (isShuttingDown) return;
|
|
14432
14926
|
process.stderr.write("slack-channel: Socket Mode disconnected, reconnecting...\n");
|
|
14433
|
-
setTimeout(
|
|
14927
|
+
setTimeout(connectSocketModeSafely, 5e3).unref?.();
|
|
14434
14928
|
};
|
|
14435
14929
|
ws.onerror = (err) => {
|
|
14436
14930
|
process.stderr.write(`slack-channel: WebSocket error: ${err}
|
|
@@ -14440,6 +14934,14 @@ async function connectSocketMode() {
|
|
|
14440
14934
|
function shutdown(reason, exitCode = 0) {
|
|
14441
14935
|
if (isShuttingDown) return;
|
|
14442
14936
|
isShuttingDown = true;
|
|
14937
|
+
try {
|
|
14938
|
+
threadPersister?.flush(trackedThreads);
|
|
14939
|
+
} catch {
|
|
14940
|
+
}
|
|
14941
|
+
try {
|
|
14942
|
+
threadPersister?.dispose();
|
|
14943
|
+
} catch {
|
|
14944
|
+
}
|
|
14443
14945
|
process.stderr.write(`slack-channel: ${reason} \u2014 closing Socket Mode and exiting
|
|
14444
14946
|
`);
|
|
14445
14947
|
try {
|
|
@@ -14453,7 +14955,35 @@ process.stdin.on("end", () => shutdown("stdin ended"));
|
|
|
14453
14955
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
14454
14956
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
14455
14957
|
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
14958
|
+
process.on("unhandledRejection", (reason) => {
|
|
14959
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
14960
|
+
process.stderr.write(`slack-channel: unhandledRejection (continuing): ${msg}
|
|
14961
|
+
`);
|
|
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
|
+
}
|
|
14456
14986
|
botUserId = await getBotUserId();
|
|
14457
14987
|
process.stderr.write(`slack-channel: Bot user ID: ${botUserId}
|
|
14458
14988
|
`);
|
|
14459
|
-
|
|
14989
|
+
connectSocketModeSafely();
|