sparkecoder 0.1.140 → 0.1.141
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/agent/index.js +554 -4
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +628 -274
- package/dist/cli.js.map +1 -1
- package/dist/index.js +624 -272
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +624 -272
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +1 -1
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/.next/static/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/runtime-config.json +2 -2
- package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +14 -3
- package/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
- package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
- package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +0 -3
- package/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
- /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -1099,6 +1099,7 @@ function loadConfig(configPath, workingDirectory) {
|
|
|
1099
1099
|
}
|
|
1100
1100
|
if (process.env.SPARKECODER_PORT) {
|
|
1101
1101
|
rawConfig.server = {
|
|
1102
|
+
...rawConfig.server ?? {},
|
|
1102
1103
|
port: parseInt(process.env.SPARKECODER_PORT, 10),
|
|
1103
1104
|
host: rawConfig.server?.host ?? "127.0.0.1"
|
|
1104
1105
|
};
|
|
@@ -9323,7 +9324,7 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
9323
9324
|
const adapter = getSlackAdapter();
|
|
9324
9325
|
if (!adapter) return { ok: false, error: "slack_not_configured" };
|
|
9325
9326
|
const key2 = reactionKey(channel, timestamp);
|
|
9326
|
-
const
|
|
9327
|
+
const inFlight2 = (async () => {
|
|
9327
9328
|
try {
|
|
9328
9329
|
const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
|
|
9329
9330
|
if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
|
|
@@ -9335,11 +9336,11 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
9335
9336
|
return { ok: false, error: err?.message || "unknown" };
|
|
9336
9337
|
}
|
|
9337
9338
|
})();
|
|
9338
|
-
pendingAdds.set(key2,
|
|
9339
|
-
void
|
|
9340
|
-
if (pendingAdds.get(key2) ===
|
|
9339
|
+
pendingAdds.set(key2, inFlight2);
|
|
9340
|
+
void inFlight2.finally(() => {
|
|
9341
|
+
if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
|
|
9341
9342
|
});
|
|
9342
|
-
return
|
|
9343
|
+
return inFlight2;
|
|
9343
9344
|
}
|
|
9344
9345
|
async function removeLoadingReaction(channel, timestamp) {
|
|
9345
9346
|
const adapter = getSlackAdapter();
|
|
@@ -9587,17 +9588,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
|
|
|
9587
9588
|
const self = await ensureSlackSelfIdentity();
|
|
9588
9589
|
if (!self) return false;
|
|
9589
9590
|
try {
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9591
|
+
let cursor = "";
|
|
9592
|
+
for (let page = 0; page < 10; page++) {
|
|
9593
|
+
const params = new URLSearchParams({ channel, ts: threadTs, limit: "200" });
|
|
9594
|
+
if (cursor) params.set("cursor", cursor);
|
|
9595
|
+
const res = await fetch(`https://slack.com/api/conversations.replies?${params.toString()}`, {
|
|
9596
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
9597
|
+
});
|
|
9598
|
+
const data = await res.json().catch(() => ({}));
|
|
9599
|
+
if (!data?.ok) {
|
|
9600
|
+
console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
|
|
9601
|
+
return false;
|
|
9602
|
+
}
|
|
9603
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
9604
|
+
if (messages.some(
|
|
9605
|
+
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
9606
|
+
)) {
|
|
9607
|
+
return true;
|
|
9608
|
+
}
|
|
9609
|
+
cursor = String(data.response_metadata?.next_cursor || "");
|
|
9610
|
+
if (!cursor) break;
|
|
9596
9611
|
}
|
|
9597
|
-
|
|
9598
|
-
return messages.some(
|
|
9599
|
-
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
9600
|
-
);
|
|
9612
|
+
return false;
|
|
9601
9613
|
} catch (err) {
|
|
9602
9614
|
console.warn(`[slack] conversations.replies error:`, err?.message || err);
|
|
9603
9615
|
return false;
|
|
@@ -10049,7 +10061,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
10049
10061
|
entry2.updatedAt = Date.now();
|
|
10050
10062
|
const nudged = {
|
|
10051
10063
|
...entry2.event,
|
|
10052
|
-
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no reply,
|
|
10064
|
+
content: `[REPLAY attempt ${entry2.attempts}/${MAX_ATTEMPTS} \u2014 you received this but have not yet replied to it or marked it handled. Respond now on the originating channel; if it genuinely needs no substantive reply, post a brief acknowledgement.]
|
|
10053
10065
|
${entry2.event.content}`,
|
|
10054
10066
|
wake: "now"
|
|
10055
10067
|
};
|
|
@@ -10476,6 +10488,428 @@ var init_registry = __esm({
|
|
|
10476
10488
|
}
|
|
10477
10489
|
});
|
|
10478
10490
|
|
|
10491
|
+
// src/integrations/slack/files.ts
|
|
10492
|
+
var files_exports = {};
|
|
10493
|
+
__export(files_exports, {
|
|
10494
|
+
INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
|
|
10495
|
+
MAX_BYTES: () => MAX_BYTES,
|
|
10496
|
+
formatFileBlock: () => formatFileBlock,
|
|
10497
|
+
ingestSlackFiles: () => ingestSlackFiles
|
|
10498
|
+
});
|
|
10499
|
+
function inferFileName(file) {
|
|
10500
|
+
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
10501
|
+
}
|
|
10502
|
+
function inferContentType(file) {
|
|
10503
|
+
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
10504
|
+
return "application/octet-stream";
|
|
10505
|
+
}
|
|
10506
|
+
function formatBytes(n) {
|
|
10507
|
+
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
10508
|
+
if (n < 1024) return `${n} B`;
|
|
10509
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
10510
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
10511
|
+
}
|
|
10512
|
+
function sleep2(ms) {
|
|
10513
|
+
return new Promise((resolve14) => setTimeout(resolve14, ms));
|
|
10514
|
+
}
|
|
10515
|
+
function imageMagic(bytes) {
|
|
10516
|
+
if (bytes.length < 12) return null;
|
|
10517
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
10518
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
|
|
10519
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
|
|
10520
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
|
|
10521
|
+
return null;
|
|
10522
|
+
}
|
|
10523
|
+
function requiresRasterMagic(contentType) {
|
|
10524
|
+
const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
|
|
10525
|
+
return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
|
|
10526
|
+
}
|
|
10527
|
+
function validateDownloadedBytes(bytes, declaredContentType) {
|
|
10528
|
+
if (!requiresRasterMagic(declaredContentType)) return null;
|
|
10529
|
+
const actual = imageMagic(bytes);
|
|
10530
|
+
if (!actual) return "invalid_image_bytes";
|
|
10531
|
+
return null;
|
|
10532
|
+
}
|
|
10533
|
+
async function fetchSlackPrivateFile(sourceUrl, botToken) {
|
|
10534
|
+
let lastError = "unknown";
|
|
10535
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
10536
|
+
try {
|
|
10537
|
+
const res = await fetch(sourceUrl, {
|
|
10538
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
10539
|
+
});
|
|
10540
|
+
if (res.status === 429 || res.status >= 500) {
|
|
10541
|
+
lastError = `slack_fetch_${res.status}`;
|
|
10542
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
10543
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
|
|
10544
|
+
if (attempt < 2) {
|
|
10545
|
+
await sleep2(waitMs);
|
|
10546
|
+
continue;
|
|
10547
|
+
}
|
|
10548
|
+
}
|
|
10549
|
+
if (!res.ok) {
|
|
10550
|
+
return { ok: false, error: `slack_fetch_${res.status}` };
|
|
10551
|
+
}
|
|
10552
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
10553
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
|
|
10554
|
+
return { ok: false, error: "size_exceeded" };
|
|
10555
|
+
}
|
|
10556
|
+
const ab = await res.arrayBuffer();
|
|
10557
|
+
if (ab.byteLength > MAX_BYTES) {
|
|
10558
|
+
return { ok: false, error: "size_exceeded" };
|
|
10559
|
+
}
|
|
10560
|
+
return { ok: true, bytes: Buffer.from(ab) };
|
|
10561
|
+
} catch (err) {
|
|
10562
|
+
lastError = `slack_fetch_error:${err?.message || "unknown"}`;
|
|
10563
|
+
if (attempt < 2) {
|
|
10564
|
+
await sleep2(250 * (attempt + 1));
|
|
10565
|
+
continue;
|
|
10566
|
+
}
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
return { ok: false, error: lastError };
|
|
10570
|
+
}
|
|
10571
|
+
function withTimeout(p, ms, label) {
|
|
10572
|
+
return new Promise((resolve14, reject) => {
|
|
10573
|
+
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
10574
|
+
p.then(
|
|
10575
|
+
(v) => {
|
|
10576
|
+
clearTimeout(t);
|
|
10577
|
+
resolve14(v);
|
|
10578
|
+
},
|
|
10579
|
+
(e) => {
|
|
10580
|
+
clearTimeout(t);
|
|
10581
|
+
reject(e);
|
|
10582
|
+
}
|
|
10583
|
+
);
|
|
10584
|
+
});
|
|
10585
|
+
}
|
|
10586
|
+
async function ingestOne(file, sessionId, botToken) {
|
|
10587
|
+
const fileName = inferFileName(file);
|
|
10588
|
+
const contentType = inferContentType(file);
|
|
10589
|
+
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
10590
|
+
const base = {
|
|
10591
|
+
slackFileId: file.id,
|
|
10592
|
+
fileName,
|
|
10593
|
+
contentType,
|
|
10594
|
+
sizeBytes: declaredSize
|
|
10595
|
+
};
|
|
10596
|
+
const sourceUrl = file.url_private_download || file.url_private;
|
|
10597
|
+
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
10598
|
+
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
10599
|
+
}
|
|
10600
|
+
if (declaredSize > MAX_BYTES) {
|
|
10601
|
+
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
10602
|
+
}
|
|
10603
|
+
let bytes;
|
|
10604
|
+
const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
|
|
10605
|
+
if (!fetched.ok) {
|
|
10606
|
+
return { ...base, shortUrl: null, error: fetched.error };
|
|
10607
|
+
}
|
|
10608
|
+
bytes = fetched.bytes;
|
|
10609
|
+
const byteError = validateDownloadedBytes(bytes, contentType);
|
|
10610
|
+
if (byteError) {
|
|
10611
|
+
console.warn(
|
|
10612
|
+
`[slack-files] refusing to upload ${fileName}: Slack metadata says ${contentType}, but downloaded bytes are not a supported image (${bytes.slice(0, 32).toString("utf8").replace(/\s+/g, " ").slice(0, 32)})`
|
|
10613
|
+
);
|
|
10614
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
|
|
10615
|
+
}
|
|
10616
|
+
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
10617
|
+
let upload;
|
|
10618
|
+
try {
|
|
10619
|
+
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
10620
|
+
} catch (err) {
|
|
10621
|
+
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
10622
|
+
}
|
|
10623
|
+
try {
|
|
10624
|
+
const putRes = await fetch(upload.uploadUrl, {
|
|
10625
|
+
method: "PUT",
|
|
10626
|
+
headers: { "Content-Type": contentType },
|
|
10627
|
+
body: bytes
|
|
10628
|
+
});
|
|
10629
|
+
if (!putRes.ok) {
|
|
10630
|
+
return {
|
|
10631
|
+
...base,
|
|
10632
|
+
sizeBytes: bytes.length,
|
|
10633
|
+
shortUrl: null,
|
|
10634
|
+
error: `gcs_put_${putRes.status}`
|
|
10635
|
+
};
|
|
10636
|
+
}
|
|
10637
|
+
} catch (err) {
|
|
10638
|
+
return {
|
|
10639
|
+
...base,
|
|
10640
|
+
sizeBytes: bytes.length,
|
|
10641
|
+
shortUrl: null,
|
|
10642
|
+
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
10643
|
+
};
|
|
10644
|
+
}
|
|
10645
|
+
try {
|
|
10646
|
+
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
10647
|
+
} catch (err) {
|
|
10648
|
+
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
10649
|
+
}
|
|
10650
|
+
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
10651
|
+
// server somehow forgot to return it (older remote-server versions).
|
|
10652
|
+
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
10653
|
+
return {
|
|
10654
|
+
...base,
|
|
10655
|
+
sizeBytes: bytes.length,
|
|
10656
|
+
shortUrl
|
|
10657
|
+
};
|
|
10658
|
+
}
|
|
10659
|
+
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
10660
|
+
try {
|
|
10661
|
+
const u = new URL(uploadUrl);
|
|
10662
|
+
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
10663
|
+
return `${u.origin}/f/${fileId}`;
|
|
10664
|
+
} catch {
|
|
10665
|
+
return null;
|
|
10666
|
+
}
|
|
10667
|
+
}
|
|
10668
|
+
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
10669
|
+
if (!Array.isArray(files) || files.length === 0) return [];
|
|
10670
|
+
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
10671
|
+
if (!isRemoteConfigured2()) {
|
|
10672
|
+
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
10673
|
+
return files.map((f) => ({
|
|
10674
|
+
slackFileId: f.id,
|
|
10675
|
+
fileName: inferFileName(f),
|
|
10676
|
+
contentType: inferContentType(f),
|
|
10677
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
10678
|
+
shortUrl: null,
|
|
10679
|
+
error: "storage_unconfigured"
|
|
10680
|
+
}));
|
|
10681
|
+
}
|
|
10682
|
+
const botToken = getSlackBotToken();
|
|
10683
|
+
if (!botToken) {
|
|
10684
|
+
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
10685
|
+
return files.map((f) => ({
|
|
10686
|
+
slackFileId: f.id,
|
|
10687
|
+
fileName: inferFileName(f),
|
|
10688
|
+
contentType: inferContentType(f),
|
|
10689
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
10690
|
+
shortUrl: null,
|
|
10691
|
+
error: "no_bot_token"
|
|
10692
|
+
}));
|
|
10693
|
+
}
|
|
10694
|
+
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
10695
|
+
const startedAt = Date.now();
|
|
10696
|
+
const pipeline = Promise.allSettled(
|
|
10697
|
+
files.map(
|
|
10698
|
+
(f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
|
|
10699
|
+
if (String(err?.message || err).includes("_timeout")) {
|
|
10700
|
+
return {
|
|
10701
|
+
slackFileId: f.id,
|
|
10702
|
+
fileName: inferFileName(f),
|
|
10703
|
+
contentType: inferContentType(f),
|
|
10704
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
10705
|
+
shortUrl: null,
|
|
10706
|
+
error: "timeout"
|
|
10707
|
+
};
|
|
10708
|
+
}
|
|
10709
|
+
throw err;
|
|
10710
|
+
})
|
|
10711
|
+
)
|
|
10712
|
+
);
|
|
10713
|
+
const settled = await pipeline;
|
|
10714
|
+
const results = settled.map((s, i) => {
|
|
10715
|
+
if (s.status === "fulfilled") return s.value;
|
|
10716
|
+
const f = files[i];
|
|
10717
|
+
return {
|
|
10718
|
+
slackFileId: f.id,
|
|
10719
|
+
fileName: inferFileName(f),
|
|
10720
|
+
contentType: inferContentType(f),
|
|
10721
|
+
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
10722
|
+
shortUrl: null,
|
|
10723
|
+
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
10724
|
+
};
|
|
10725
|
+
});
|
|
10726
|
+
const okCount = results.filter((r) => r.shortUrl).length;
|
|
10727
|
+
console.log(
|
|
10728
|
+
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
10729
|
+
);
|
|
10730
|
+
return results;
|
|
10731
|
+
}
|
|
10732
|
+
function formatFileBlock(files) {
|
|
10733
|
+
if (!files || files.length === 0) return "";
|
|
10734
|
+
const lines = ["[files]"];
|
|
10735
|
+
for (const f of files) {
|
|
10736
|
+
const sizeLabel = formatBytes(f.sizeBytes);
|
|
10737
|
+
if (f.shortUrl) {
|
|
10738
|
+
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
10739
|
+
} else {
|
|
10740
|
+
lines.push(
|
|
10741
|
+
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
10742
|
+
);
|
|
10743
|
+
}
|
|
10744
|
+
}
|
|
10745
|
+
return lines.join("\n");
|
|
10746
|
+
}
|
|
10747
|
+
var MAX_BYTES, INGEST_TIMEOUT_MS;
|
|
10748
|
+
var init_files = __esm({
|
|
10749
|
+
"src/integrations/slack/files.ts"() {
|
|
10750
|
+
"use strict";
|
|
10751
|
+
init_client3();
|
|
10752
|
+
MAX_BYTES = 100 * 1024 * 1024;
|
|
10753
|
+
INGEST_TIMEOUT_MS = 2500;
|
|
10754
|
+
}
|
|
10755
|
+
});
|
|
10756
|
+
|
|
10757
|
+
// src/integrations/slack/read.ts
|
|
10758
|
+
var read_exports = {};
|
|
10759
|
+
__export(read_exports, {
|
|
10760
|
+
findChannelByName: () => findChannelByName,
|
|
10761
|
+
findUsers: () => findUsers,
|
|
10762
|
+
getChannelHistory: () => getChannelHistory,
|
|
10763
|
+
getPermalink: () => getPermalink,
|
|
10764
|
+
getThreadReplies: () => getThreadReplies,
|
|
10765
|
+
ingestMessageFiles: () => ingestMessageFiles
|
|
10766
|
+
});
|
|
10767
|
+
async function slackGet(method, params) {
|
|
10768
|
+
const token = getSlackBotToken();
|
|
10769
|
+
if (!token) return { ok: false, error: "slack_not_configured" };
|
|
10770
|
+
try {
|
|
10771
|
+
const qs = new URLSearchParams(params).toString();
|
|
10772
|
+
const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
|
|
10773
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
10774
|
+
});
|
|
10775
|
+
const json = await res.json().catch(() => ({}));
|
|
10776
|
+
if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
|
|
10777
|
+
return { ok: true, json };
|
|
10778
|
+
} catch (err) {
|
|
10779
|
+
return { ok: false, error: err?.message || "unknown" };
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
function clampLimit(n, def, max) {
|
|
10783
|
+
const v = typeof n === "number" && Number.isFinite(n) ? n : def;
|
|
10784
|
+
return String(Math.min(Math.max(Math.floor(v), 1), max));
|
|
10785
|
+
}
|
|
10786
|
+
function liteMessage(m) {
|
|
10787
|
+
return {
|
|
10788
|
+
ts: String(m?.ts ?? ""),
|
|
10789
|
+
threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
|
|
10790
|
+
user: typeof m?.user === "string" ? m.user : void 0,
|
|
10791
|
+
botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
|
|
10792
|
+
text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
|
|
10793
|
+
files: Array.isArray(m?.files) ? m.files.map((f) => ({ id: String(f?.id ?? ""), name: f?.name || f?.title, mimetype: f?.mimetype, size: typeof f?.size === "number" ? f.size : void 0 })) : void 0,
|
|
10794
|
+
replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
|
|
10795
|
+
};
|
|
10796
|
+
}
|
|
10797
|
+
async function enrichUserNames(messages) {
|
|
10798
|
+
const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
|
|
10799
|
+
const names = /* @__PURE__ */ new Map();
|
|
10800
|
+
await Promise.all(
|
|
10801
|
+
ids.map(async (id) => {
|
|
10802
|
+
const info = await resolveSlackUserInfo(id).catch(() => null);
|
|
10803
|
+
if (info?.name) names.set(id, info.name);
|
|
10804
|
+
})
|
|
10805
|
+
);
|
|
10806
|
+
return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
|
|
10807
|
+
}
|
|
10808
|
+
async function getChannelHistory(channel, limit) {
|
|
10809
|
+
const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
|
|
10810
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10811
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10812
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10813
|
+
}
|
|
10814
|
+
async function getThreadReplies(channel, threadTs, limit) {
|
|
10815
|
+
const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
|
|
10816
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10817
|
+
const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
|
|
10818
|
+
return { ok: true, data: await enrichUserNames(messages) };
|
|
10819
|
+
}
|
|
10820
|
+
async function getPermalink(channel, messageTs) {
|
|
10821
|
+
const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
|
|
10822
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10823
|
+
return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
|
|
10824
|
+
}
|
|
10825
|
+
async function findChannelByName(name) {
|
|
10826
|
+
const clean = name.replace(/^#/, "").trim().toLowerCase();
|
|
10827
|
+
let cursor;
|
|
10828
|
+
for (let page = 0; page < 10; page++) {
|
|
10829
|
+
const params = {
|
|
10830
|
+
types: "public_channel,private_channel",
|
|
10831
|
+
exclude_archived: "true",
|
|
10832
|
+
limit: "999"
|
|
10833
|
+
};
|
|
10834
|
+
if (cursor) params.cursor = cursor;
|
|
10835
|
+
const r = await slackGet("conversations.list", params);
|
|
10836
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10837
|
+
const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
|
|
10838
|
+
if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
|
|
10839
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10840
|
+
if (!cursor) break;
|
|
10841
|
+
}
|
|
10842
|
+
return { ok: false, error: "channel_not_found" };
|
|
10843
|
+
}
|
|
10844
|
+
async function findUsers(query, max = 10) {
|
|
10845
|
+
const q = query.trim().toLowerCase();
|
|
10846
|
+
if (!q) return { ok: false, error: "empty_query" };
|
|
10847
|
+
if (q.includes("@")) {
|
|
10848
|
+
const r = await slackGet("users.lookupByEmail", { email: query.trim() });
|
|
10849
|
+
if (r.ok && r.json.user) {
|
|
10850
|
+
const u = r.json.user;
|
|
10851
|
+
return { ok: true, data: [{ id: String(u.id), name: u.profile?.display_name || u.real_name || u.name, realName: u.real_name, email: u.profile?.email }] };
|
|
10852
|
+
}
|
|
10853
|
+
if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
|
|
10854
|
+
return { ok: false, error: r.error };
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
const matches = [];
|
|
10858
|
+
let cursor;
|
|
10859
|
+
for (let page = 0; page < 10 && matches.length < max; page++) {
|
|
10860
|
+
const params = { limit: "1000" };
|
|
10861
|
+
if (cursor) params.cursor = cursor;
|
|
10862
|
+
const r = await slackGet("users.list", params);
|
|
10863
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
10864
|
+
for (const u of r.json.members || []) {
|
|
10865
|
+
if (u?.deleted || u?.is_bot) continue;
|
|
10866
|
+
const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
|
|
10867
|
+
const email = String(u?.profile?.email || "");
|
|
10868
|
+
if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
|
|
10869
|
+
matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
|
|
10870
|
+
if (matches.length >= max) break;
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10873
|
+
cursor = r.json.response_metadata?.next_cursor || "";
|
|
10874
|
+
if (!cursor) break;
|
|
10875
|
+
}
|
|
10876
|
+
return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
|
|
10877
|
+
}
|
|
10878
|
+
async function getSingleMessage(channel, ts, threadTs) {
|
|
10879
|
+
if (threadTs) {
|
|
10880
|
+
const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
|
|
10881
|
+
if (rr2.ok && Array.isArray(rr2.json.messages)) {
|
|
10882
|
+
const match = rr2.json.messages.find((m) => m?.ts === ts);
|
|
10883
|
+
if (match) return match;
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
|
|
10887
|
+
if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
|
|
10888
|
+
const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
|
|
10889
|
+
if (rr.ok && Array.isArray(rr.json.messages)) {
|
|
10890
|
+
return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
|
|
10891
|
+
}
|
|
10892
|
+
return null;
|
|
10893
|
+
}
|
|
10894
|
+
async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
|
|
10895
|
+
const msg = await getSingleMessage(channel, ts, threadTs);
|
|
10896
|
+
if (!msg) return { ok: false, error: "message_not_found" };
|
|
10897
|
+
const files = Array.isArray(msg.files) ? msg.files : [];
|
|
10898
|
+
if (files.length === 0) return { ok: true, data: [] };
|
|
10899
|
+
const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
|
|
10900
|
+
const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
|
|
10901
|
+
return {
|
|
10902
|
+
ok: true,
|
|
10903
|
+
data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
|
|
10904
|
+
};
|
|
10905
|
+
}
|
|
10906
|
+
var init_read = __esm({
|
|
10907
|
+
"src/integrations/slack/read.ts"() {
|
|
10908
|
+
"use strict";
|
|
10909
|
+
init_client3();
|
|
10910
|
+
}
|
|
10911
|
+
});
|
|
10912
|
+
|
|
10479
10913
|
// src/integrations/channels/messenger.ts
|
|
10480
10914
|
async function postMessage(opts) {
|
|
10481
10915
|
const adapter = getChannel(opts.channel);
|
|
@@ -10486,7 +10920,16 @@ async function postMessage(opts) {
|
|
|
10486
10920
|
let ref;
|
|
10487
10921
|
switch (opts.channel) {
|
|
10488
10922
|
case "slack": {
|
|
10489
|
-
|
|
10923
|
+
let slackChannel2 = opts.to.trim();
|
|
10924
|
+
if (slackChannel2.startsWith("#")) {
|
|
10925
|
+
const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
|
|
10926
|
+
const found = await findChannelByName2(slackChannel2);
|
|
10927
|
+
if (!found.ok || !found.data?.id) {
|
|
10928
|
+
return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
|
|
10929
|
+
}
|
|
10930
|
+
slackChannel2 = found.data.id;
|
|
10931
|
+
}
|
|
10932
|
+
const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
|
|
10490
10933
|
ref = slackRef;
|
|
10491
10934
|
break;
|
|
10492
10935
|
}
|
|
@@ -10793,6 +11236,46 @@ function buildMessengerTool() {
|
|
|
10793
11236
|
}
|
|
10794
11237
|
});
|
|
10795
11238
|
}
|
|
11239
|
+
function buildSlackTool(opts) {
|
|
11240
|
+
return tool13({
|
|
11241
|
+
description: "Read from Slack like a human would. Actions: history (channel[, limit] \u2192 recent messages, newest first, with sender names + file metadata), replies (channel, threadTs[, limit] \u2192 full thread), permalink (channel, messageTs \u2192 shareable link), find_channel (query=name \u2192 channel id), find_user (query=name|email \u2192 matching members), user_info (user=U0123 \u2192 name/email), fetch_files (channel, messageTs[, threadTs] \u2192 re-uploads that message's attachments and returns fetchable URLs you can curl). Use this to scroll back, read context, and open files others posted before replying.",
|
|
11242
|
+
inputSchema: slackInputSchema,
|
|
11243
|
+
execute: async (input) => {
|
|
11244
|
+
if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
|
|
11245
|
+
switch (input.action) {
|
|
11246
|
+
case "history": {
|
|
11247
|
+
if (!input.channel) return { ok: false, error: "channel required" };
|
|
11248
|
+
return getChannelHistory(input.channel, input.limit);
|
|
11249
|
+
}
|
|
11250
|
+
case "replies": {
|
|
11251
|
+
if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
|
|
11252
|
+
return getThreadReplies(input.channel, input.threadTs, input.limit);
|
|
11253
|
+
}
|
|
11254
|
+
case "permalink": {
|
|
11255
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
11256
|
+
return getPermalink(input.channel, input.messageTs);
|
|
11257
|
+
}
|
|
11258
|
+
case "find_channel": {
|
|
11259
|
+
if (!input.query) return { ok: false, error: "query (channel name) required" };
|
|
11260
|
+
return findChannelByName(input.query);
|
|
11261
|
+
}
|
|
11262
|
+
case "find_user": {
|
|
11263
|
+
if (!input.query) return { ok: false, error: "query (name or email) required" };
|
|
11264
|
+
return findUsers(input.query);
|
|
11265
|
+
}
|
|
11266
|
+
case "user_info": {
|
|
11267
|
+
if (!input.user) return { ok: false, error: "user required" };
|
|
11268
|
+
const info = await resolveSlackUserInfo(input.user);
|
|
11269
|
+
return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
|
|
11270
|
+
}
|
|
11271
|
+
case "fetch_files": {
|
|
11272
|
+
if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
|
|
11273
|
+
return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
});
|
|
11278
|
+
}
|
|
10796
11279
|
function buildScheduleTool(opts) {
|
|
10797
11280
|
return tool13({
|
|
10798
11281
|
description: "Recurring prompts. Actions: create (required: name, cron, prompt), list, update (required: id; any of name/cron/prompt/enabled/replyChannel), delete (required: id), pause (required: id), resume (required: id). Cron is standard 5-field syntax.",
|
|
@@ -10874,15 +11357,18 @@ function createOrchestratorActionTools(opts) {
|
|
|
10874
11357
|
return {
|
|
10875
11358
|
agent: buildAgentTool(opts),
|
|
10876
11359
|
messenger: buildMessengerTool(),
|
|
11360
|
+
slack: buildSlackTool(opts),
|
|
10877
11361
|
schedule: buildScheduleTool(opts),
|
|
10878
11362
|
webhook: buildWebhookTool(opts)
|
|
10879
11363
|
};
|
|
10880
11364
|
}
|
|
10881
|
-
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
11365
|
+
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10882
11366
|
var init_orchestrator_actions = __esm({
|
|
10883
11367
|
"src/tools/orchestrator-actions.ts"() {
|
|
10884
11368
|
"use strict";
|
|
10885
11369
|
init_messenger();
|
|
11370
|
+
init_read();
|
|
11371
|
+
init_client3();
|
|
10886
11372
|
init_schedules_store();
|
|
10887
11373
|
init_config();
|
|
10888
11374
|
init_webhooks_store();
|
|
@@ -10940,6 +11426,15 @@ var init_orchestrator_actions = __esm({
|
|
|
10940
11426
|
threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
|
|
10941
11427
|
subject: z14.string().optional().describe("post + email: subject (future).")
|
|
10942
11428
|
});
|
|
11429
|
+
slackInputSchema = z14.object({
|
|
11430
|
+
action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
|
|
11431
|
+
channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
|
|
11432
|
+
threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
|
|
11433
|
+
messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
|
|
11434
|
+
query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
|
|
11435
|
+
user: z14.string().optional().describe("user_info: user id (U0123)."),
|
|
11436
|
+
limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
|
|
11437
|
+
});
|
|
10943
11438
|
scheduleInputSchema = z14.object({
|
|
10944
11439
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
10945
11440
|
// create / update
|
|
@@ -16805,226 +17300,69 @@ function verifySlackSignature(opts) {
|
|
|
16805
17300
|
// src/server/routes/slack.ts
|
|
16806
17301
|
init_client3();
|
|
16807
17302
|
init_slack();
|
|
16808
|
-
|
|
16809
|
-
// src/integrations/slack/files.ts
|
|
16810
|
-
init_client3();
|
|
16811
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
16812
|
-
var INGEST_TIMEOUT_MS = 2500;
|
|
16813
|
-
function inferFileName(file) {
|
|
16814
|
-
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
16815
|
-
}
|
|
16816
|
-
function inferContentType(file) {
|
|
16817
|
-
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
16818
|
-
return "application/octet-stream";
|
|
16819
|
-
}
|
|
16820
|
-
function formatBytes(n) {
|
|
16821
|
-
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
16822
|
-
if (n < 1024) return `${n} B`;
|
|
16823
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
16824
|
-
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
16825
|
-
}
|
|
16826
|
-
function withTimeout(p, ms, label) {
|
|
16827
|
-
return new Promise((resolve14, reject) => {
|
|
16828
|
-
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
16829
|
-
p.then(
|
|
16830
|
-
(v) => {
|
|
16831
|
-
clearTimeout(t);
|
|
16832
|
-
resolve14(v);
|
|
16833
|
-
},
|
|
16834
|
-
(e) => {
|
|
16835
|
-
clearTimeout(t);
|
|
16836
|
-
reject(e);
|
|
16837
|
-
}
|
|
16838
|
-
);
|
|
16839
|
-
});
|
|
16840
|
-
}
|
|
16841
|
-
async function ingestOne(file, sessionId, botToken) {
|
|
16842
|
-
const fileName = inferFileName(file);
|
|
16843
|
-
const contentType = inferContentType(file);
|
|
16844
|
-
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
16845
|
-
const base = {
|
|
16846
|
-
slackFileId: file.id,
|
|
16847
|
-
fileName,
|
|
16848
|
-
contentType,
|
|
16849
|
-
sizeBytes: declaredSize
|
|
16850
|
-
};
|
|
16851
|
-
const sourceUrl = file.url_private_download || file.url_private;
|
|
16852
|
-
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
16853
|
-
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
16854
|
-
}
|
|
16855
|
-
if (declaredSize > MAX_BYTES) {
|
|
16856
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
16857
|
-
}
|
|
16858
|
-
let bytes;
|
|
16859
|
-
try {
|
|
16860
|
-
const res = await fetch(sourceUrl, {
|
|
16861
|
-
headers: { Authorization: `Bearer ${botToken}` }
|
|
16862
|
-
});
|
|
16863
|
-
if (!res.ok) {
|
|
16864
|
-
return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
|
|
16865
|
-
}
|
|
16866
|
-
const ab = await res.arrayBuffer();
|
|
16867
|
-
if (ab.byteLength > MAX_BYTES) {
|
|
16868
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
16869
|
-
}
|
|
16870
|
-
bytes = Buffer.from(ab);
|
|
16871
|
-
} catch (err) {
|
|
16872
|
-
return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
|
|
16873
|
-
}
|
|
16874
|
-
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
16875
|
-
let upload;
|
|
16876
|
-
try {
|
|
16877
|
-
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
16878
|
-
} catch (err) {
|
|
16879
|
-
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
16880
|
-
}
|
|
16881
|
-
try {
|
|
16882
|
-
const putRes = await fetch(upload.uploadUrl, {
|
|
16883
|
-
method: "PUT",
|
|
16884
|
-
headers: { "Content-Type": contentType },
|
|
16885
|
-
body: bytes
|
|
16886
|
-
});
|
|
16887
|
-
if (!putRes.ok) {
|
|
16888
|
-
return {
|
|
16889
|
-
...base,
|
|
16890
|
-
sizeBytes: bytes.length,
|
|
16891
|
-
shortUrl: null,
|
|
16892
|
-
error: `gcs_put_${putRes.status}`
|
|
16893
|
-
};
|
|
16894
|
-
}
|
|
16895
|
-
} catch (err) {
|
|
16896
|
-
return {
|
|
16897
|
-
...base,
|
|
16898
|
-
sizeBytes: bytes.length,
|
|
16899
|
-
shortUrl: null,
|
|
16900
|
-
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
16901
|
-
};
|
|
16902
|
-
}
|
|
16903
|
-
try {
|
|
16904
|
-
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
16905
|
-
} catch (err) {
|
|
16906
|
-
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
16907
|
-
}
|
|
16908
|
-
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
16909
|
-
// server somehow forgot to return it (older remote-server versions).
|
|
16910
|
-
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
16911
|
-
return {
|
|
16912
|
-
...base,
|
|
16913
|
-
sizeBytes: bytes.length,
|
|
16914
|
-
shortUrl
|
|
16915
|
-
};
|
|
16916
|
-
}
|
|
16917
|
-
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
16918
|
-
try {
|
|
16919
|
-
const u = new URL(uploadUrl);
|
|
16920
|
-
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
16921
|
-
return `${u.origin}/f/${fileId}`;
|
|
16922
|
-
} catch {
|
|
16923
|
-
return null;
|
|
16924
|
-
}
|
|
16925
|
-
}
|
|
16926
|
-
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
16927
|
-
if (!Array.isArray(files) || files.length === 0) return [];
|
|
16928
|
-
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
16929
|
-
if (!isRemoteConfigured2()) {
|
|
16930
|
-
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
16931
|
-
return files.map((f) => ({
|
|
16932
|
-
slackFileId: f.id,
|
|
16933
|
-
fileName: inferFileName(f),
|
|
16934
|
-
contentType: inferContentType(f),
|
|
16935
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16936
|
-
shortUrl: null,
|
|
16937
|
-
error: "storage_unconfigured"
|
|
16938
|
-
}));
|
|
16939
|
-
}
|
|
16940
|
-
const botToken = getSlackBotToken();
|
|
16941
|
-
if (!botToken) {
|
|
16942
|
-
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
16943
|
-
return files.map((f) => ({
|
|
16944
|
-
slackFileId: f.id,
|
|
16945
|
-
fileName: inferFileName(f),
|
|
16946
|
-
contentType: inferContentType(f),
|
|
16947
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16948
|
-
shortUrl: null,
|
|
16949
|
-
error: "no_bot_token"
|
|
16950
|
-
}));
|
|
16951
|
-
}
|
|
16952
|
-
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
16953
|
-
const startedAt = Date.now();
|
|
16954
|
-
const pipeline = Promise.allSettled(
|
|
16955
|
-
files.map((f) => ingestOne(f, sessionId, botToken))
|
|
16956
|
-
);
|
|
16957
|
-
let settled;
|
|
16958
|
-
try {
|
|
16959
|
-
settled = await withTimeout(pipeline, timeoutMs, "ingest");
|
|
16960
|
-
} catch (err) {
|
|
16961
|
-
console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
|
|
16962
|
-
return files.map((f) => ({
|
|
16963
|
-
slackFileId: f.id,
|
|
16964
|
-
fileName: inferFileName(f),
|
|
16965
|
-
contentType: inferContentType(f),
|
|
16966
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16967
|
-
shortUrl: null,
|
|
16968
|
-
error: "timeout"
|
|
16969
|
-
}));
|
|
16970
|
-
}
|
|
16971
|
-
const results = settled.map((s, i) => {
|
|
16972
|
-
if (s.status === "fulfilled") return s.value;
|
|
16973
|
-
const f = files[i];
|
|
16974
|
-
return {
|
|
16975
|
-
slackFileId: f.id,
|
|
16976
|
-
fileName: inferFileName(f),
|
|
16977
|
-
contentType: inferContentType(f),
|
|
16978
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16979
|
-
shortUrl: null,
|
|
16980
|
-
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
16981
|
-
};
|
|
16982
|
-
});
|
|
16983
|
-
const okCount = results.filter((r) => r.shortUrl).length;
|
|
16984
|
-
console.log(
|
|
16985
|
-
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
16986
|
-
);
|
|
16987
|
-
return results;
|
|
16988
|
-
}
|
|
16989
|
-
function formatFileBlock(files) {
|
|
16990
|
-
if (!files || files.length === 0) return "";
|
|
16991
|
-
const lines = ["[files]"];
|
|
16992
|
-
for (const f of files) {
|
|
16993
|
-
const sizeLabel = formatBytes(f.sizeBytes);
|
|
16994
|
-
if (f.shortUrl) {
|
|
16995
|
-
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
16996
|
-
} else {
|
|
16997
|
-
lines.push(
|
|
16998
|
-
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
16999
|
-
);
|
|
17000
|
-
}
|
|
17001
|
-
}
|
|
17002
|
-
return lines.join("\n");
|
|
17003
|
-
}
|
|
17004
|
-
|
|
17005
|
-
// src/server/routes/slack.ts
|
|
17303
|
+
init_files();
|
|
17006
17304
|
init_webhook_events();
|
|
17007
17305
|
init_inbox();
|
|
17008
17306
|
var recentlyHandled = /* @__PURE__ */ new Map();
|
|
17307
|
+
var recentlyDeniedReplies = /* @__PURE__ */ new Map();
|
|
17308
|
+
var inFlight = /* @__PURE__ */ new Set();
|
|
17009
17309
|
var MAX_RECENT = 1e3;
|
|
17310
|
+
function deliveryKey(channel, ts) {
|
|
17311
|
+
if (!channel || !ts) return null;
|
|
17312
|
+
return `${channel}\u241F${ts}`;
|
|
17313
|
+
}
|
|
17010
17314
|
function wasHandled(channel, ts) {
|
|
17011
|
-
|
|
17012
|
-
|
|
17315
|
+
const key2 = deliveryKey(channel, ts);
|
|
17316
|
+
if (!key2) return false;
|
|
17317
|
+
return recentlyHandled.has(key2) || inFlight.has(key2);
|
|
17318
|
+
}
|
|
17319
|
+
function markInFlight(channel, ts) {
|
|
17320
|
+
const key2 = deliveryKey(channel, ts);
|
|
17321
|
+
if (key2) inFlight.add(key2);
|
|
17322
|
+
}
|
|
17323
|
+
function clearInFlight(channel, ts) {
|
|
17324
|
+
const key2 = deliveryKey(channel, ts);
|
|
17325
|
+
if (key2) inFlight.delete(key2);
|
|
17013
17326
|
}
|
|
17014
17327
|
function markHandled(channel, ts) {
|
|
17015
|
-
|
|
17016
|
-
|
|
17328
|
+
const key2 = deliveryKey(channel, ts);
|
|
17329
|
+
if (!key2) return;
|
|
17330
|
+
inFlight.delete(key2);
|
|
17017
17331
|
recentlyHandled.set(key2, Date.now());
|
|
17018
17332
|
if (recentlyHandled.size > MAX_RECENT) {
|
|
17019
17333
|
const oldest = recentlyHandled.keys().next().value;
|
|
17020
17334
|
if (oldest) recentlyHandled.delete(oldest);
|
|
17021
17335
|
}
|
|
17022
17336
|
}
|
|
17337
|
+
function wasDeniedReplySent(channel, ts) {
|
|
17338
|
+
const key2 = deliveryKey(channel, ts);
|
|
17339
|
+
if (!key2) return false;
|
|
17340
|
+
return recentlyDeniedReplies.has(key2);
|
|
17341
|
+
}
|
|
17342
|
+
function markDeniedReplySent(channel, ts) {
|
|
17343
|
+
const key2 = deliveryKey(channel, ts);
|
|
17344
|
+
if (!key2) return;
|
|
17345
|
+
recentlyDeniedReplies.set(key2, Date.now());
|
|
17346
|
+
if (recentlyDeniedReplies.size > MAX_RECENT) {
|
|
17347
|
+
const oldest = recentlyDeniedReplies.keys().next().value;
|
|
17348
|
+
if (oldest) recentlyDeniedReplies.delete(oldest);
|
|
17349
|
+
}
|
|
17350
|
+
}
|
|
17023
17351
|
var slack = new Hono6();
|
|
17024
17352
|
slack.post("/events", async (c) => {
|
|
17025
|
-
const signingSecret = getSlackSigningSecret();
|
|
17026
|
-
if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
|
|
17027
17353
|
const rawBody = await c.req.text();
|
|
17354
|
+
const signingSecret = getSlackSigningSecret();
|
|
17355
|
+
if (!signingSecret) {
|
|
17356
|
+
let unsignedPayload;
|
|
17357
|
+
try {
|
|
17358
|
+
unsignedPayload = JSON.parse(rawBody);
|
|
17359
|
+
} catch {
|
|
17360
|
+
}
|
|
17361
|
+
if (unsignedPayload?.type === "url_verification" && typeof unsignedPayload.challenge === "string") {
|
|
17362
|
+
return c.json({ challenge: unsignedPayload.challenge });
|
|
17363
|
+
}
|
|
17364
|
+
return c.json({ error: "slack not configured" }, 503);
|
|
17365
|
+
}
|
|
17028
17366
|
const verification = verifySlackSignature({
|
|
17029
17367
|
signingSecret,
|
|
17030
17368
|
timestampHeader: c.req.header("x-slack-request-timestamp") || null,
|
|
@@ -17078,65 +17416,79 @@ slack.post("/events", async (c) => {
|
|
|
17078
17416
|
if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
|
|
17079
17417
|
markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
|
|
17080
17418
|
}
|
|
17081
|
-
|
|
17082
|
-
|
|
17083
|
-
|
|
17084
|
-
|
|
17085
|
-
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17419
|
+
if (wasHandled(ev.channel, ev.ts)) {
|
|
17420
|
+
updateEvent(auditId, { status: "dropped", dropReason: "duplicate_delivery" });
|
|
17421
|
+
return c.json({ ok: true });
|
|
17422
|
+
}
|
|
17423
|
+
markInFlight(ev.channel, ev.ts);
|
|
17424
|
+
let routed = false;
|
|
17425
|
+
try {
|
|
17426
|
+
const orchestratorId = await findOrCreateOrchestratorId();
|
|
17427
|
+
if (orchestratorId) {
|
|
17428
|
+
inbound.content = await normalizeSlackMentions(inbound.content);
|
|
17429
|
+
if (ev.user) {
|
|
17430
|
+
const info = await resolveSlackUserInfo(ev.user);
|
|
17431
|
+
if (info?.name) {
|
|
17432
|
+
const emailSuffix = info.email ? ` (${info.email})` : "";
|
|
17433
|
+
const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
|
|
17434
|
+
inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
|
|
17435
|
+
}
|
|
17090
17436
|
}
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
const block = formatFileBlock(ingested);
|
|
17102
|
-
if (block) inbound.content = `${inbound.content}
|
|
17437
|
+
const slackFiles = Array.isArray(ev.files) ? ev.files : [];
|
|
17438
|
+
if (ev.channel && ev.ts) {
|
|
17439
|
+
void addLoadingReaction(String(ev.channel), String(ev.ts));
|
|
17440
|
+
}
|
|
17441
|
+
let ingestedCount = 0;
|
|
17442
|
+
if (slackFiles.length > 0) {
|
|
17443
|
+
try {
|
|
17444
|
+
const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
|
|
17445
|
+
const block = formatFileBlock(ingested);
|
|
17446
|
+
if (block) inbound.content = `${inbound.content}
|
|
17103
17447
|
${block}`;
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
|
|
17448
|
+
ingestedCount = ingested.filter((f) => f.shortUrl).length;
|
|
17449
|
+
} catch (err) {
|
|
17450
|
+
console.warn("[slack-files] ingestion threw:", err?.message || err);
|
|
17451
|
+
inbound.content = `${inbound.content}
|
|
17108
17452
|
[files] (ingestion failed: ${err?.message || "unknown"})`;
|
|
17453
|
+
}
|
|
17109
17454
|
}
|
|
17455
|
+
pushToInbox(orchestratorId, inbound);
|
|
17456
|
+
routed = true;
|
|
17457
|
+
markHandled(ev.channel, ev.ts);
|
|
17458
|
+
updateEvent(auditId, {
|
|
17459
|
+
status: "routed",
|
|
17460
|
+
sessionId: orchestratorId,
|
|
17461
|
+
...slackFiles.length > 0 ? {
|
|
17462
|
+
// Preserve the original meta (ts, thread_ts, team,
|
|
17463
|
+
// event_subtype) from recordEvent above — updateEvent does a
|
|
17464
|
+
// shallow merge, so we have to re-include them.
|
|
17465
|
+
meta: {
|
|
17466
|
+
ts: ev.ts,
|
|
17467
|
+
thread_ts: ev.thread_ts,
|
|
17468
|
+
team: ev.team,
|
|
17469
|
+
event_subtype: ev.subtype,
|
|
17470
|
+
fileCount: slackFiles.length,
|
|
17471
|
+
ingestedCount
|
|
17472
|
+
}
|
|
17473
|
+
} : {}
|
|
17474
|
+
});
|
|
17475
|
+
} else {
|
|
17476
|
+
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
17110
17477
|
}
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
status: "routed",
|
|
17114
|
-
sessionId: orchestratorId,
|
|
17115
|
-
...slackFiles.length > 0 ? {
|
|
17116
|
-
// Preserve the original meta (ts, thread_ts, team,
|
|
17117
|
-
// event_subtype) from recordEvent above — updateEvent does a
|
|
17118
|
-
// shallow merge, so we have to re-include them.
|
|
17119
|
-
meta: {
|
|
17120
|
-
ts: ev.ts,
|
|
17121
|
-
thread_ts: ev.thread_ts,
|
|
17122
|
-
team: ev.team,
|
|
17123
|
-
event_subtype: ev.subtype,
|
|
17124
|
-
fileCount: slackFiles.length,
|
|
17125
|
-
ingestedCount
|
|
17126
|
-
}
|
|
17127
|
-
} : {}
|
|
17128
|
-
});
|
|
17129
|
-
} else {
|
|
17130
|
-
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
17478
|
+
} finally {
|
|
17479
|
+
if (!routed) clearInFlight(ev.channel, ev.ts);
|
|
17131
17480
|
}
|
|
17132
17481
|
} else if (dropReason) {
|
|
17133
17482
|
updateEvent(auditId, { status: "dropped", dropReason });
|
|
17134
17483
|
const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
|
|
17135
17484
|
if (userFacingDrops.includes(dropReason)) {
|
|
17136
17485
|
console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17486
|
+
if (!wasDeniedReplySent(payload.event.channel, payload.event.ts)) {
|
|
17487
|
+
markDeniedReplySent(payload.event.channel, payload.event.ts);
|
|
17488
|
+
void sendDeniedReply(payload.event, dropReason).catch((err) => {
|
|
17489
|
+
console.warn(`[slack] denied-reply failed:`, err?.message || err);
|
|
17490
|
+
});
|
|
17491
|
+
}
|
|
17140
17492
|
}
|
|
17141
17493
|
}
|
|
17142
17494
|
}
|
|
@@ -17161,7 +17513,7 @@ async function findOrCreateOrchestratorId() {
|
|
|
17161
17513
|
return null;
|
|
17162
17514
|
}
|
|
17163
17515
|
}
|
|
17164
|
-
async function sendDeniedReply(event) {
|
|
17516
|
+
async function sendDeniedReply(event, reason) {
|
|
17165
17517
|
const policy = getSlackDeniedReplyPolicy();
|
|
17166
17518
|
if (!policy.enabled) return;
|
|
17167
17519
|
const adapter = getSlackAdapter();
|
|
@@ -17170,7 +17522,7 @@ async function sendDeniedReply(event) {
|
|
|
17170
17522
|
const channel = String(event?.channel || "");
|
|
17171
17523
|
if (!channel) return;
|
|
17172
17524
|
const threadTs = event?.thread_ts || event?.ts;
|
|
17173
|
-
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`);
|
|
17525
|
+
const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`).replace(/\{reason\}/g, reason);
|
|
17174
17526
|
const result = await adapter.postMessage({ channel, text, threadTs });
|
|
17175
17527
|
if (!result.ok) {
|
|
17176
17528
|
console.warn(`[slack] denied-reply post failed: ${result.error}`);
|
|
@@ -19776,6 +20128,8 @@ program.command("slack-setup").description("Interactively configure Slack integr
|
|
|
19776
20128
|
signingSecret,
|
|
19777
20129
|
defaultOrchestratorName: existing.slack?.defaultOrchestratorName ?? "orchestrator"
|
|
19778
20130
|
};
|
|
20131
|
+
const webhookToken = existing.webhooks?.token;
|
|
20132
|
+
const eventsPath = typeof webhookToken === "string" && webhookToken ? `/w/${webhookToken}/slack/events` : "/api/slack/events";
|
|
19779
20133
|
writeFileSync9(configPath, JSON.stringify(existing, null, 2));
|
|
19780
20134
|
console.log(chalk.green(`
|
|
19781
20135
|
\u2713 Slack configured`));
|
|
@@ -19784,8 +20138,8 @@ program.command("slack-setup").description("Interactively configure Slack integr
|
|
|
19784
20138
|
console.log(chalk.bold("Next steps:"));
|
|
19785
20139
|
console.log(" 1. Make sure your sparkecoder is reachable from the internet (sparkecoder cloudflared-setup).");
|
|
19786
20140
|
console.log(" 2. In your Slack app config, set Event Subscriptions request URL to:");
|
|
19787
|
-
console.log(chalk.cyan(
|
|
19788
|
-
console.log(" 3. Subscribe to bot events: app_mention, message.im");
|
|
20141
|
+
console.log(chalk.cyan(` https://<your-public-host>${eventsPath}`));
|
|
20142
|
+
console.log(" 3. Subscribe to bot events: app_mention, message.im, message.mpim, message.channels, message.groups");
|
|
19789
20143
|
console.log(" 4. Reinstall the app to your workspace.");
|
|
19790
20144
|
} catch (err) {
|
|
19791
20145
|
rl.close();
|