sparkecoder 0.1.139 → 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.d.ts +3 -3
- package/dist/agent/index.js +661 -10
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +756 -294
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/{index-BAsQWqZj.d.ts → index-Cl_eUatM.d.ts} +96 -96
- package/dist/index.d.ts +5 -5
- package/dist/index.js +752 -292
- package/dist/index.js.map +1 -1
- package/dist/{schema-Dz-wABVY.d.ts → schema-BSz4MzhJ.d.ts} +3 -3
- package/dist/{search-CVVfuBPZ.d.ts → search-DOzC4ojH.d.ts} +4 -4
- package/dist/server/index.js +752 -292
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +3 -3
- 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/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{rY6TfiRfMLnxUYYyNit1Q → 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
|
};
|
|
@@ -8395,6 +8396,105 @@ var init_cap_image_count = __esm({
|
|
|
8395
8396
|
}
|
|
8396
8397
|
});
|
|
8397
8398
|
|
|
8399
|
+
// src/utils/sanitize-images.ts
|
|
8400
|
+
function hasImageMagic(bytes) {
|
|
8401
|
+
if (bytes.length < 12) return false;
|
|
8402
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return true;
|
|
8403
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return true;
|
|
8404
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return true;
|
|
8405
|
+
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 true;
|
|
8406
|
+
return false;
|
|
8407
|
+
}
|
|
8408
|
+
function extractBase64(part) {
|
|
8409
|
+
const raw = typeof part?.data === "string" ? part.data : typeof part?.image === "string" ? part.image : null;
|
|
8410
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
8411
|
+
if (/^https?:\/\//i.test(raw)) return null;
|
|
8412
|
+
const dataUrl = raw.match(/^data:[^;,]+;base64,([\s\S]*)$/);
|
|
8413
|
+
return dataUrl ? dataUrl[1] : raw;
|
|
8414
|
+
}
|
|
8415
|
+
function isInvalidImagePart(part) {
|
|
8416
|
+
if (!part || typeof part !== "object") return false;
|
|
8417
|
+
const t = part.type;
|
|
8418
|
+
if (t !== "image" && t !== "image-data" && t !== "media") return false;
|
|
8419
|
+
const mt = part.mediaType;
|
|
8420
|
+
const b64 = extractBase64(part);
|
|
8421
|
+
if (b64 === null) {
|
|
8422
|
+
return typeof mt === "string" && mt.startsWith("image/") && !SUPPORTED_IMAGE_TYPES.includes(mt);
|
|
8423
|
+
}
|
|
8424
|
+
let bytes;
|
|
8425
|
+
try {
|
|
8426
|
+
bytes = Buffer.from(b64, "base64");
|
|
8427
|
+
} catch {
|
|
8428
|
+
return true;
|
|
8429
|
+
}
|
|
8430
|
+
if (bytes.length === 0) return true;
|
|
8431
|
+
return !hasImageMagic(bytes);
|
|
8432
|
+
}
|
|
8433
|
+
function placeholder() {
|
|
8434
|
+
return { type: "text", text: INVALID_IMAGE_PLACEHOLDER };
|
|
8435
|
+
}
|
|
8436
|
+
function sanitizeInvalidImages(messages) {
|
|
8437
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
8438
|
+
let mutated = false;
|
|
8439
|
+
let dropped = 0;
|
|
8440
|
+
const out = messages.slice();
|
|
8441
|
+
for (let i = 0; i < out.length; i++) {
|
|
8442
|
+
const msg = out[i];
|
|
8443
|
+
if (!Array.isArray(msg.content)) continue;
|
|
8444
|
+
let contentCloned = false;
|
|
8445
|
+
const ensureCloned = () => {
|
|
8446
|
+
if (contentCloned) return;
|
|
8447
|
+
out[i] = { ...msg, content: [...msg.content] };
|
|
8448
|
+
contentCloned = true;
|
|
8449
|
+
};
|
|
8450
|
+
const content = () => out[i].content;
|
|
8451
|
+
for (let j = 0; j < content().length; j++) {
|
|
8452
|
+
const part = content()[j];
|
|
8453
|
+
if (isInvalidImagePart(part)) {
|
|
8454
|
+
ensureCloned();
|
|
8455
|
+
out[i].content[j] = placeholder();
|
|
8456
|
+
mutated = true;
|
|
8457
|
+
dropped++;
|
|
8458
|
+
continue;
|
|
8459
|
+
}
|
|
8460
|
+
if (part && typeof part === "object" && part.type === "tool-result" && part.output && part.output.type === "content" && Array.isArray(part.output.value)) {
|
|
8461
|
+
const innerValue = part.output.value;
|
|
8462
|
+
let innerMutated = false;
|
|
8463
|
+
const newValue = innerValue.slice();
|
|
8464
|
+
for (let k = 0; k < newValue.length; k++) {
|
|
8465
|
+
if (isInvalidImagePart(newValue[k])) {
|
|
8466
|
+
newValue[k] = placeholder();
|
|
8467
|
+
innerMutated = true;
|
|
8468
|
+
dropped++;
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
if (innerMutated) {
|
|
8472
|
+
ensureCloned();
|
|
8473
|
+
out[i].content[j] = {
|
|
8474
|
+
...part,
|
|
8475
|
+
output: { ...part.output, value: newValue }
|
|
8476
|
+
};
|
|
8477
|
+
mutated = true;
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
if (mutated) {
|
|
8483
|
+
console.warn(
|
|
8484
|
+
`[sanitize-images] Replaced ${dropped} invalid image part(s) with a text placeholder (non-image bytes / unsupported type) to avoid provider 400 "invalid image" errors.`
|
|
8485
|
+
);
|
|
8486
|
+
}
|
|
8487
|
+
return mutated ? out : messages;
|
|
8488
|
+
}
|
|
8489
|
+
var SUPPORTED_IMAGE_TYPES, INVALID_IMAGE_PLACEHOLDER;
|
|
8490
|
+
var init_sanitize_images = __esm({
|
|
8491
|
+
"src/utils/sanitize-images.ts"() {
|
|
8492
|
+
"use strict";
|
|
8493
|
+
SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
8494
|
+
INVALID_IMAGE_PLACEHOLDER = "[invalid image omitted \u2014 the data was not a valid jpeg/png/gif/webp]";
|
|
8495
|
+
}
|
|
8496
|
+
});
|
|
8497
|
+
|
|
8398
8498
|
// src/agent/model-limits.ts
|
|
8399
8499
|
function getModelLimits(modelId) {
|
|
8400
8500
|
const normalized = modelId.trim().toLowerCase();
|
|
@@ -8710,6 +8810,7 @@ var init_context = __esm({
|
|
|
8710
8810
|
init_prompts();
|
|
8711
8811
|
init_sanitize_messages();
|
|
8712
8812
|
init_cap_image_count();
|
|
8813
|
+
init_sanitize_images();
|
|
8713
8814
|
init_model_limits();
|
|
8714
8815
|
TOOL_OUTPUT_TRIM_CHARS = 400;
|
|
8715
8816
|
COMPACTABLE_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8759,6 +8860,7 @@ ${summaryContent}`
|
|
|
8759
8860
|
messages = repairToolPairing(messages);
|
|
8760
8861
|
messages = ensureToolResultsFollowCalls(messages);
|
|
8761
8862
|
messages = ensureEndsWithUserOrTool(messages);
|
|
8863
|
+
messages = sanitizeInvalidImages(messages);
|
|
8762
8864
|
messages = capImageCount(messages);
|
|
8763
8865
|
messages = this.enforceHardCap(messages);
|
|
8764
8866
|
return messages;
|
|
@@ -8770,28 +8872,35 @@ ${summaryContent}`
|
|
|
8770
8872
|
* repairs tool pairing so dropping can't orphan a tool result.
|
|
8771
8873
|
*/
|
|
8772
8874
|
enforceHardCap(messages) {
|
|
8773
|
-
const {
|
|
8774
|
-
const
|
|
8875
|
+
const { rollingTarget } = getModelLimits(this.modelId);
|
|
8876
|
+
const MAX_MESSAGES = 120;
|
|
8877
|
+
const tokenCeiling = Math.max(2e4, Math.floor(rollingTarget * 1.5));
|
|
8775
8878
|
const tokens = messages.map((m) => this.messageTokens(m));
|
|
8776
8879
|
let total = tokens.reduce((a, b) => a + b, 0);
|
|
8777
|
-
if (total <= ceiling) return messages;
|
|
8778
8880
|
const hasLeadingSummary = messages.length > 0 && messages[0].role === "system";
|
|
8779
8881
|
const firstBody = hasLeadingSummary ? 1 : 0;
|
|
8780
8882
|
const lastIndex = messages.length - 1;
|
|
8883
|
+
const bodyCount = messages.length - firstBody;
|
|
8884
|
+
if (bodyCount <= MAX_MESSAGES && total <= tokenCeiling) return messages;
|
|
8781
8885
|
let start = firstBody;
|
|
8782
|
-
|
|
8886
|
+
const countDropTarget = messages.length - MAX_MESSAGES;
|
|
8887
|
+
while (start < countDropTarget && start < lastIndex) {
|
|
8888
|
+
total -= tokens[start];
|
|
8889
|
+
start += 1;
|
|
8890
|
+
}
|
|
8891
|
+
while (start < lastIndex && total > tokenCeiling) {
|
|
8783
8892
|
total -= tokens[start];
|
|
8784
8893
|
start += 1;
|
|
8785
8894
|
}
|
|
8786
8895
|
let out = hasLeadingSummary ? [messages[0], ...messages.slice(start)] : messages.slice(start);
|
|
8787
|
-
if (total >
|
|
8896
|
+
if (total > tokenCeiling) {
|
|
8788
8897
|
out = out.map((m) => hardTruncateMessageText(m));
|
|
8789
8898
|
}
|
|
8790
8899
|
out = repairToolPairing(out);
|
|
8791
8900
|
out = ensureToolResultsFollowCalls(out);
|
|
8792
8901
|
out = ensureEndsWithUserOrTool(out);
|
|
8793
8902
|
console.warn(
|
|
8794
|
-
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${
|
|
8903
|
+
`[Context] hard cap engaged for ${this.modelId}: trimmed ${messages.length}\u2192${out.length} msgs (ceiling ${tokenCeiling} est-tokens / ${MAX_MESSAGES} msgs).`
|
|
8795
8904
|
);
|
|
8796
8905
|
return out;
|
|
8797
8906
|
}
|
|
@@ -9215,7 +9324,7 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
9215
9324
|
const adapter = getSlackAdapter();
|
|
9216
9325
|
if (!adapter) return { ok: false, error: "slack_not_configured" };
|
|
9217
9326
|
const key2 = reactionKey(channel, timestamp);
|
|
9218
|
-
const
|
|
9327
|
+
const inFlight2 = (async () => {
|
|
9219
9328
|
try {
|
|
9220
9329
|
const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
|
|
9221
9330
|
if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
|
|
@@ -9227,11 +9336,11 @@ async function addLoadingReaction(channel, timestamp) {
|
|
|
9227
9336
|
return { ok: false, error: err?.message || "unknown" };
|
|
9228
9337
|
}
|
|
9229
9338
|
})();
|
|
9230
|
-
pendingAdds.set(key2,
|
|
9231
|
-
void
|
|
9232
|
-
if (pendingAdds.get(key2) ===
|
|
9339
|
+
pendingAdds.set(key2, inFlight2);
|
|
9340
|
+
void inFlight2.finally(() => {
|
|
9341
|
+
if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
|
|
9233
9342
|
});
|
|
9234
|
-
return
|
|
9343
|
+
return inFlight2;
|
|
9235
9344
|
}
|
|
9236
9345
|
async function removeLoadingReaction(channel, timestamp) {
|
|
9237
9346
|
const adapter = getSlackAdapter();
|
|
@@ -9479,17 +9588,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
|
|
|
9479
9588
|
const self = await ensureSlackSelfIdentity();
|
|
9480
9589
|
if (!self) return false;
|
|
9481
9590
|
try {
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
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;
|
|
9488
9611
|
}
|
|
9489
|
-
|
|
9490
|
-
return messages.some(
|
|
9491
|
-
(m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
|
|
9492
|
-
);
|
|
9612
|
+
return false;
|
|
9493
9613
|
} catch (err) {
|
|
9494
9614
|
console.warn(`[slack] conversations.replies error:`, err?.message || err);
|
|
9495
9615
|
return false;
|
|
@@ -9941,7 +10061,7 @@ async function reconcileOnce(now = Date.now()) {
|
|
|
9941
10061
|
entry2.updatedAt = Date.now();
|
|
9942
10062
|
const nudged = {
|
|
9943
10063
|
...entry2.event,
|
|
9944
|
-
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.]
|
|
9945
10065
|
${entry2.event.content}`,
|
|
9946
10066
|
wake: "now"
|
|
9947
10067
|
};
|
|
@@ -10349,22 +10469,444 @@ function getChannel(id) {
|
|
|
10349
10469
|
function listChannels() {
|
|
10350
10470
|
return Array.from(channels.values());
|
|
10351
10471
|
}
|
|
10352
|
-
function listOutboundChannels() {
|
|
10353
|
-
return listChannels().filter((c) => c.canSend());
|
|
10472
|
+
function listOutboundChannels() {
|
|
10473
|
+
return listChannels().filter((c) => c.canSend());
|
|
10474
|
+
}
|
|
10475
|
+
var channels;
|
|
10476
|
+
var init_registry = __esm({
|
|
10477
|
+
"src/integrations/channels/registry.ts"() {
|
|
10478
|
+
"use strict";
|
|
10479
|
+
init_web();
|
|
10480
|
+
init_slack();
|
|
10481
|
+
init_system();
|
|
10482
|
+
init_schedule();
|
|
10483
|
+
init_webhook();
|
|
10484
|
+
channels = /* @__PURE__ */ new Map();
|
|
10485
|
+
for (const c of [webChannel, slackChannel, systemChannel, scheduleChannel, webhookChannel]) {
|
|
10486
|
+
channels.set(c.id, c);
|
|
10487
|
+
}
|
|
10488
|
+
}
|
|
10489
|
+
});
|
|
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
|
+
};
|
|
10354
10905
|
}
|
|
10355
|
-
var
|
|
10356
|
-
|
|
10357
|
-
"src/integrations/channels/registry.ts"() {
|
|
10906
|
+
var init_read = __esm({
|
|
10907
|
+
"src/integrations/slack/read.ts"() {
|
|
10358
10908
|
"use strict";
|
|
10359
|
-
|
|
10360
|
-
init_slack();
|
|
10361
|
-
init_system();
|
|
10362
|
-
init_schedule();
|
|
10363
|
-
init_webhook();
|
|
10364
|
-
channels = /* @__PURE__ */ new Map();
|
|
10365
|
-
for (const c of [webChannel, slackChannel, systemChannel, scheduleChannel, webhookChannel]) {
|
|
10366
|
-
channels.set(c.id, c);
|
|
10367
|
-
}
|
|
10909
|
+
init_client3();
|
|
10368
10910
|
}
|
|
10369
10911
|
});
|
|
10370
10912
|
|
|
@@ -10378,7 +10920,16 @@ async function postMessage(opts) {
|
|
|
10378
10920
|
let ref;
|
|
10379
10921
|
switch (opts.channel) {
|
|
10380
10922
|
case "slack": {
|
|
10381
|
-
|
|
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 };
|
|
10382
10933
|
ref = slackRef;
|
|
10383
10934
|
break;
|
|
10384
10935
|
}
|
|
@@ -10685,6 +11236,46 @@ function buildMessengerTool() {
|
|
|
10685
11236
|
}
|
|
10686
11237
|
});
|
|
10687
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
|
+
}
|
|
10688
11279
|
function buildScheduleTool(opts) {
|
|
10689
11280
|
return tool13({
|
|
10690
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.",
|
|
@@ -10766,15 +11357,18 @@ function createOrchestratorActionTools(opts) {
|
|
|
10766
11357
|
return {
|
|
10767
11358
|
agent: buildAgentTool(opts),
|
|
10768
11359
|
messenger: buildMessengerTool(),
|
|
11360
|
+
slack: buildSlackTool(opts),
|
|
10769
11361
|
schedule: buildScheduleTool(opts),
|
|
10770
11362
|
webhook: buildWebhookTool(opts)
|
|
10771
11363
|
};
|
|
10772
11364
|
}
|
|
10773
|
-
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
11365
|
+
var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
|
|
10774
11366
|
var init_orchestrator_actions = __esm({
|
|
10775
11367
|
"src/tools/orchestrator-actions.ts"() {
|
|
10776
11368
|
"use strict";
|
|
10777
11369
|
init_messenger();
|
|
11370
|
+
init_read();
|
|
11371
|
+
init_client3();
|
|
10778
11372
|
init_schedules_store();
|
|
10779
11373
|
init_config();
|
|
10780
11374
|
init_webhooks_store();
|
|
@@ -10832,6 +11426,15 @@ var init_orchestrator_actions = __esm({
|
|
|
10832
11426
|
threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
|
|
10833
11427
|
subject: z14.string().optional().describe("post + email: subject (future).")
|
|
10834
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
|
+
});
|
|
10835
11438
|
scheduleInputSchema = z14.object({
|
|
10836
11439
|
action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
|
|
10837
11440
|
// create / update
|
|
@@ -16697,226 +17300,69 @@ function verifySlackSignature(opts) {
|
|
|
16697
17300
|
// src/server/routes/slack.ts
|
|
16698
17301
|
init_client3();
|
|
16699
17302
|
init_slack();
|
|
16700
|
-
|
|
16701
|
-
// src/integrations/slack/files.ts
|
|
16702
|
-
init_client3();
|
|
16703
|
-
var MAX_BYTES = 100 * 1024 * 1024;
|
|
16704
|
-
var INGEST_TIMEOUT_MS = 2500;
|
|
16705
|
-
function inferFileName(file) {
|
|
16706
|
-
return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
|
|
16707
|
-
}
|
|
16708
|
-
function inferContentType(file) {
|
|
16709
|
-
if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
|
|
16710
|
-
return "application/octet-stream";
|
|
16711
|
-
}
|
|
16712
|
-
function formatBytes(n) {
|
|
16713
|
-
if (!Number.isFinite(n) || n <= 0) return "?";
|
|
16714
|
-
if (n < 1024) return `${n} B`;
|
|
16715
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
16716
|
-
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
16717
|
-
}
|
|
16718
|
-
function withTimeout(p, ms, label) {
|
|
16719
|
-
return new Promise((resolve14, reject) => {
|
|
16720
|
-
const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
|
|
16721
|
-
p.then(
|
|
16722
|
-
(v) => {
|
|
16723
|
-
clearTimeout(t);
|
|
16724
|
-
resolve14(v);
|
|
16725
|
-
},
|
|
16726
|
-
(e) => {
|
|
16727
|
-
clearTimeout(t);
|
|
16728
|
-
reject(e);
|
|
16729
|
-
}
|
|
16730
|
-
);
|
|
16731
|
-
});
|
|
16732
|
-
}
|
|
16733
|
-
async function ingestOne(file, sessionId, botToken) {
|
|
16734
|
-
const fileName = inferFileName(file);
|
|
16735
|
-
const contentType = inferContentType(file);
|
|
16736
|
-
const declaredSize = typeof file.size === "number" ? file.size : 0;
|
|
16737
|
-
const base = {
|
|
16738
|
-
slackFileId: file.id,
|
|
16739
|
-
fileName,
|
|
16740
|
-
contentType,
|
|
16741
|
-
sizeBytes: declaredSize
|
|
16742
|
-
};
|
|
16743
|
-
const sourceUrl = file.url_private_download || file.url_private;
|
|
16744
|
-
if (!sourceUrl || typeof sourceUrl !== "string") {
|
|
16745
|
-
return { ...base, shortUrl: null, error: "no_source_url" };
|
|
16746
|
-
}
|
|
16747
|
-
if (declaredSize > MAX_BYTES) {
|
|
16748
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
16749
|
-
}
|
|
16750
|
-
let bytes;
|
|
16751
|
-
try {
|
|
16752
|
-
const res = await fetch(sourceUrl, {
|
|
16753
|
-
headers: { Authorization: `Bearer ${botToken}` }
|
|
16754
|
-
});
|
|
16755
|
-
if (!res.ok) {
|
|
16756
|
-
return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
|
|
16757
|
-
}
|
|
16758
|
-
const ab = await res.arrayBuffer();
|
|
16759
|
-
if (ab.byteLength > MAX_BYTES) {
|
|
16760
|
-
return { ...base, shortUrl: null, error: "size_exceeded" };
|
|
16761
|
-
}
|
|
16762
|
-
bytes = Buffer.from(ab);
|
|
16763
|
-
} catch (err) {
|
|
16764
|
-
return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
|
|
16765
|
-
}
|
|
16766
|
-
const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
16767
|
-
let upload;
|
|
16768
|
-
try {
|
|
16769
|
-
upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
|
|
16770
|
-
} catch (err) {
|
|
16771
|
-
return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
|
|
16772
|
-
}
|
|
16773
|
-
try {
|
|
16774
|
-
const putRes = await fetch(upload.uploadUrl, {
|
|
16775
|
-
method: "PUT",
|
|
16776
|
-
headers: { "Content-Type": contentType },
|
|
16777
|
-
body: bytes
|
|
16778
|
-
});
|
|
16779
|
-
if (!putRes.ok) {
|
|
16780
|
-
return {
|
|
16781
|
-
...base,
|
|
16782
|
-
sizeBytes: bytes.length,
|
|
16783
|
-
shortUrl: null,
|
|
16784
|
-
error: `gcs_put_${putRes.status}`
|
|
16785
|
-
};
|
|
16786
|
-
}
|
|
16787
|
-
} catch (err) {
|
|
16788
|
-
return {
|
|
16789
|
-
...base,
|
|
16790
|
-
sizeBytes: bytes.length,
|
|
16791
|
-
shortUrl: null,
|
|
16792
|
-
error: `gcs_put_error:${err?.message || "unknown"}`
|
|
16793
|
-
};
|
|
16794
|
-
}
|
|
16795
|
-
try {
|
|
16796
|
-
await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
|
|
16797
|
-
} catch (err) {
|
|
16798
|
-
console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
|
|
16799
|
-
}
|
|
16800
|
-
const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
|
|
16801
|
-
// server somehow forgot to return it (older remote-server versions).
|
|
16802
|
-
inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
|
|
16803
|
-
return {
|
|
16804
|
-
...base,
|
|
16805
|
-
sizeBytes: bytes.length,
|
|
16806
|
-
shortUrl
|
|
16807
|
-
};
|
|
16808
|
-
}
|
|
16809
|
-
function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
|
|
16810
|
-
try {
|
|
16811
|
-
const u = new URL(uploadUrl);
|
|
16812
|
-
if (u.hostname.endsWith(".googleapis.com")) return null;
|
|
16813
|
-
return `${u.origin}/f/${fileId}`;
|
|
16814
|
-
} catch {
|
|
16815
|
-
return null;
|
|
16816
|
-
}
|
|
16817
|
-
}
|
|
16818
|
-
async function ingestSlackFiles(files, sessionId, options = {}) {
|
|
16819
|
-
if (!Array.isArray(files) || files.length === 0) return [];
|
|
16820
|
-
const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
16821
|
-
if (!isRemoteConfigured2()) {
|
|
16822
|
-
console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
|
|
16823
|
-
return files.map((f) => ({
|
|
16824
|
-
slackFileId: f.id,
|
|
16825
|
-
fileName: inferFileName(f),
|
|
16826
|
-
contentType: inferContentType(f),
|
|
16827
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16828
|
-
shortUrl: null,
|
|
16829
|
-
error: "storage_unconfigured"
|
|
16830
|
-
}));
|
|
16831
|
-
}
|
|
16832
|
-
const botToken = getSlackBotToken();
|
|
16833
|
-
if (!botToken) {
|
|
16834
|
-
console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
|
|
16835
|
-
return files.map((f) => ({
|
|
16836
|
-
slackFileId: f.id,
|
|
16837
|
-
fileName: inferFileName(f),
|
|
16838
|
-
contentType: inferContentType(f),
|
|
16839
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16840
|
-
shortUrl: null,
|
|
16841
|
-
error: "no_bot_token"
|
|
16842
|
-
}));
|
|
16843
|
-
}
|
|
16844
|
-
const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
|
|
16845
|
-
const startedAt = Date.now();
|
|
16846
|
-
const pipeline = Promise.allSettled(
|
|
16847
|
-
files.map((f) => ingestOne(f, sessionId, botToken))
|
|
16848
|
-
);
|
|
16849
|
-
let settled;
|
|
16850
|
-
try {
|
|
16851
|
-
settled = await withTimeout(pipeline, timeoutMs, "ingest");
|
|
16852
|
-
} catch (err) {
|
|
16853
|
-
console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
|
|
16854
|
-
return files.map((f) => ({
|
|
16855
|
-
slackFileId: f.id,
|
|
16856
|
-
fileName: inferFileName(f),
|
|
16857
|
-
contentType: inferContentType(f),
|
|
16858
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16859
|
-
shortUrl: null,
|
|
16860
|
-
error: "timeout"
|
|
16861
|
-
}));
|
|
16862
|
-
}
|
|
16863
|
-
const results = settled.map((s, i) => {
|
|
16864
|
-
if (s.status === "fulfilled") return s.value;
|
|
16865
|
-
const f = files[i];
|
|
16866
|
-
return {
|
|
16867
|
-
slackFileId: f.id,
|
|
16868
|
-
fileName: inferFileName(f),
|
|
16869
|
-
contentType: inferContentType(f),
|
|
16870
|
-
sizeBytes: typeof f.size === "number" ? f.size : 0,
|
|
16871
|
-
shortUrl: null,
|
|
16872
|
-
error: `unexpected:${s.reason?.message || String(s.reason)}`
|
|
16873
|
-
};
|
|
16874
|
-
});
|
|
16875
|
-
const okCount = results.filter((r) => r.shortUrl).length;
|
|
16876
|
-
console.log(
|
|
16877
|
-
`[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
|
|
16878
|
-
);
|
|
16879
|
-
return results;
|
|
16880
|
-
}
|
|
16881
|
-
function formatFileBlock(files) {
|
|
16882
|
-
if (!files || files.length === 0) return "";
|
|
16883
|
-
const lines = ["[files]"];
|
|
16884
|
-
for (const f of files) {
|
|
16885
|
-
const sizeLabel = formatBytes(f.sizeBytes);
|
|
16886
|
-
if (f.shortUrl) {
|
|
16887
|
-
lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
|
|
16888
|
-
} else {
|
|
16889
|
-
lines.push(
|
|
16890
|
-
` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
|
|
16891
|
-
);
|
|
16892
|
-
}
|
|
16893
|
-
}
|
|
16894
|
-
return lines.join("\n");
|
|
16895
|
-
}
|
|
16896
|
-
|
|
16897
|
-
// src/server/routes/slack.ts
|
|
17303
|
+
init_files();
|
|
16898
17304
|
init_webhook_events();
|
|
16899
17305
|
init_inbox();
|
|
16900
17306
|
var recentlyHandled = /* @__PURE__ */ new Map();
|
|
17307
|
+
var recentlyDeniedReplies = /* @__PURE__ */ new Map();
|
|
17308
|
+
var inFlight = /* @__PURE__ */ new Set();
|
|
16901
17309
|
var MAX_RECENT = 1e3;
|
|
17310
|
+
function deliveryKey(channel, ts) {
|
|
17311
|
+
if (!channel || !ts) return null;
|
|
17312
|
+
return `${channel}\u241F${ts}`;
|
|
17313
|
+
}
|
|
16902
17314
|
function wasHandled(channel, ts) {
|
|
16903
|
-
|
|
16904
|
-
|
|
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);
|
|
16905
17326
|
}
|
|
16906
17327
|
function markHandled(channel, ts) {
|
|
16907
|
-
|
|
16908
|
-
|
|
17328
|
+
const key2 = deliveryKey(channel, ts);
|
|
17329
|
+
if (!key2) return;
|
|
17330
|
+
inFlight.delete(key2);
|
|
16909
17331
|
recentlyHandled.set(key2, Date.now());
|
|
16910
17332
|
if (recentlyHandled.size > MAX_RECENT) {
|
|
16911
17333
|
const oldest = recentlyHandled.keys().next().value;
|
|
16912
17334
|
if (oldest) recentlyHandled.delete(oldest);
|
|
16913
17335
|
}
|
|
16914
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
|
+
}
|
|
16915
17351
|
var slack = new Hono6();
|
|
16916
17352
|
slack.post("/events", async (c) => {
|
|
16917
|
-
const signingSecret = getSlackSigningSecret();
|
|
16918
|
-
if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
|
|
16919
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
|
+
}
|
|
16920
17366
|
const verification = verifySlackSignature({
|
|
16921
17367
|
signingSecret,
|
|
16922
17368
|
timestampHeader: c.req.header("x-slack-request-timestamp") || null,
|
|
@@ -16970,65 +17416,79 @@ slack.post("/events", async (c) => {
|
|
|
16970
17416
|
if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
|
|
16971
17417
|
markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
|
|
16972
17418
|
}
|
|
16973
|
-
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
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
|
+
}
|
|
16982
17436
|
}
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
|
|
16988
|
-
|
|
16989
|
-
|
|
16990
|
-
|
|
16991
|
-
|
|
16992
|
-
|
|
16993
|
-
const block = formatFileBlock(ingested);
|
|
16994
|
-
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}
|
|
16995
17447
|
${block}`;
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
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}
|
|
17000
17452
|
[files] (ingestion failed: ${err?.message || "unknown"})`;
|
|
17453
|
+
}
|
|
17001
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" });
|
|
17002
17477
|
}
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
status: "routed",
|
|
17006
|
-
sessionId: orchestratorId,
|
|
17007
|
-
...slackFiles.length > 0 ? {
|
|
17008
|
-
// Preserve the original meta (ts, thread_ts, team,
|
|
17009
|
-
// event_subtype) from recordEvent above — updateEvent does a
|
|
17010
|
-
// shallow merge, so we have to re-include them.
|
|
17011
|
-
meta: {
|
|
17012
|
-
ts: ev.ts,
|
|
17013
|
-
thread_ts: ev.thread_ts,
|
|
17014
|
-
team: ev.team,
|
|
17015
|
-
event_subtype: ev.subtype,
|
|
17016
|
-
fileCount: slackFiles.length,
|
|
17017
|
-
ingestedCount
|
|
17018
|
-
}
|
|
17019
|
-
} : {}
|
|
17020
|
-
});
|
|
17021
|
-
} else {
|
|
17022
|
-
updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
|
|
17478
|
+
} finally {
|
|
17479
|
+
if (!routed) clearInFlight(ev.channel, ev.ts);
|
|
17023
17480
|
}
|
|
17024
17481
|
} else if (dropReason) {
|
|
17025
17482
|
updateEvent(auditId, { status: "dropped", dropReason });
|
|
17026
17483
|
const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
|
|
17027
17484
|
if (userFacingDrops.includes(dropReason)) {
|
|
17028
17485
|
console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
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
|
+
}
|
|
17032
17492
|
}
|
|
17033
17493
|
}
|
|
17034
17494
|
}
|
|
@@ -17053,7 +17513,7 @@ async function findOrCreateOrchestratorId() {
|
|
|
17053
17513
|
return null;
|
|
17054
17514
|
}
|
|
17055
17515
|
}
|
|
17056
|
-
async function sendDeniedReply(event) {
|
|
17516
|
+
async function sendDeniedReply(event, reason) {
|
|
17057
17517
|
const policy = getSlackDeniedReplyPolicy();
|
|
17058
17518
|
if (!policy.enabled) return;
|
|
17059
17519
|
const adapter = getSlackAdapter();
|
|
@@ -17062,7 +17522,7 @@ async function sendDeniedReply(event) {
|
|
|
17062
17522
|
const channel = String(event?.channel || "");
|
|
17063
17523
|
if (!channel) return;
|
|
17064
17524
|
const threadTs = event?.thread_ts || event?.ts;
|
|
17065
|
-
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);
|
|
17066
17526
|
const result = await adapter.postMessage({ channel, text, threadTs });
|
|
17067
17527
|
if (!result.ok) {
|
|
17068
17528
|
console.warn(`[slack] denied-reply post failed: ${result.error}`);
|
|
@@ -19668,6 +20128,8 @@ program.command("slack-setup").description("Interactively configure Slack integr
|
|
|
19668
20128
|
signingSecret,
|
|
19669
20129
|
defaultOrchestratorName: existing.slack?.defaultOrchestratorName ?? "orchestrator"
|
|
19670
20130
|
};
|
|
20131
|
+
const webhookToken = existing.webhooks?.token;
|
|
20132
|
+
const eventsPath = typeof webhookToken === "string" && webhookToken ? `/w/${webhookToken}/slack/events` : "/api/slack/events";
|
|
19671
20133
|
writeFileSync9(configPath, JSON.stringify(existing, null, 2));
|
|
19672
20134
|
console.log(chalk.green(`
|
|
19673
20135
|
\u2713 Slack configured`));
|
|
@@ -19676,8 +20138,8 @@ program.command("slack-setup").description("Interactively configure Slack integr
|
|
|
19676
20138
|
console.log(chalk.bold("Next steps:"));
|
|
19677
20139
|
console.log(" 1. Make sure your sparkecoder is reachable from the internet (sparkecoder cloudflared-setup).");
|
|
19678
20140
|
console.log(" 2. In your Slack app config, set Event Subscriptions request URL to:");
|
|
19679
|
-
console.log(chalk.cyan(
|
|
19680
|
-
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");
|
|
19681
20143
|
console.log(" 4. Reinstall the app to your workspace.");
|
|
19682
20144
|
} catch (err) {
|
|
19683
20145
|
rl.close();
|