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.
Files changed (113) hide show
  1. package/dist/agent/index.js +554 -4
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +628 -274
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +624 -272
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +624 -272
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/tools/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/web/.next/BUILD_ID +1 -1
  12. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  13. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  14. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  15. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  16. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  17. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  18. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  19. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  20. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  32. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  68. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  84. package/web/.next/standalone/web/.next/server/app/settings.rsc +2 -2
  85. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  86. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  89. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  90. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  91. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  92. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +1 -1
  93. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  94. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  95. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  96. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  97. package/web/.next/standalone/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
  98. package/web/.next/standalone/web/.next/static/static/chunks/001c7ddc8dad6764.js +3 -0
  99. package/web/.next/standalone/web/runtime-config.json +2 -2
  100. package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +14 -3
  101. package/web/.next/static/chunks/001c7ddc8dad6764.js +3 -0
  102. package/web/.next/standalone/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
  103. package/web/.next/standalone/web/.next/static/static/chunks/20ca4e35e9bb3e94.js +0 -3
  104. package/web/.next/static/chunks/20ca4e35e9bb3e94.js +0 -3
  105. /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
  106. /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
  107. /package/web/.next/standalone/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
  108. /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
  109. /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
  110. /package/web/.next/standalone/web/.next/static/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_ssgManifest.js +0 -0
  111. /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_buildManifest.js +0 -0
  112. /package/web/.next/static/{QkKMkVPV-LLRD2i9PBP_Y → T0ihp-rxOYsKtonqcYLfo}/_clientMiddlewareManifest.json +0 -0
  113. /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 inFlight = (async () => {
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, inFlight);
9339
- void inFlight.finally(() => {
9340
- if (pendingAdds.get(key2) === inFlight) pendingAdds.delete(key2);
9339
+ pendingAdds.set(key2, inFlight2);
9340
+ void inFlight2.finally(() => {
9341
+ if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
9341
9342
  });
9342
- return inFlight;
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
- const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${encodeURIComponent(threadTs)}&limit=200`;
9591
- const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
9592
- const data = await res.json().catch(() => ({}));
9593
- if (!data?.ok) {
9594
- console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
9595
- return false;
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
- const messages = Array.isArray(data.messages) ? data.messages : [];
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, you can ignore it.]
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
- const slackRef = { channel: "slack", slackChannel: opts.to, threadTs: opts.threadTs };
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
- if (!channel || !ts) return false;
17012
- return recentlyHandled.has(`${channel}\u241F${ts}`);
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
- if (!channel || !ts) return;
17016
- const key2 = `${channel}\u241F${ts}`;
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
- const orchestratorId = await findOrCreateOrchestratorId();
17082
- if (orchestratorId) {
17083
- inbound.content = await normalizeSlackMentions(inbound.content);
17084
- if (ev.user) {
17085
- const info = await resolveSlackUserInfo(ev.user);
17086
- if (info?.name) {
17087
- const emailSuffix = info.email ? ` (${info.email})` : "";
17088
- const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
17089
- inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
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
- const slackFiles = Array.isArray(ev.files) ? ev.files : [];
17093
- markHandled(ev.channel, ev.ts);
17094
- if (ev.channel && ev.ts) {
17095
- void addLoadingReaction(String(ev.channel), String(ev.ts));
17096
- }
17097
- let ingestedCount = 0;
17098
- if (slackFiles.length > 0) {
17099
- try {
17100
- const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
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
- ingestedCount = ingested.filter((f) => f.shortUrl).length;
17105
- } catch (err) {
17106
- console.warn("[slack-files] ingestion threw:", err?.message || err);
17107
- inbound.content = `${inbound.content}
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
- pushToInbox(orchestratorId, inbound);
17112
- updateEvent(auditId, {
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
- void sendDeniedReply(payload.event).catch((err) => {
17138
- console.warn(`[slack] denied-reply failed:`, err?.message || err);
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(" https://<your-public-host>/api/slack/events"));
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();