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
@@ -1098,6 +1098,7 @@ function loadConfig(configPath, workingDirectory) {
1098
1098
  }
1099
1099
  if (process.env.SPARKECODER_PORT) {
1100
1100
  rawConfig.server = {
1101
+ ...rawConfig.server ?? {},
1101
1102
  port: parseInt(process.env.SPARKECODER_PORT, 10),
1102
1103
  host: rawConfig.server?.host ?? "127.0.0.1"
1103
1104
  };
@@ -8580,7 +8581,7 @@ async function addLoadingReaction(channel, timestamp) {
8580
8581
  const adapter = getSlackAdapter();
8581
8582
  if (!adapter) return { ok: false, error: "slack_not_configured" };
8582
8583
  const key2 = reactionKey(channel, timestamp);
8583
- const inFlight = (async () => {
8584
+ const inFlight2 = (async () => {
8584
8585
  try {
8585
8586
  const res = await adapter.addReaction({ channel, timestamp, name: LOADING_REACTION });
8586
8587
  if (!res.ok && !REACTION_SOFT_ERRORS.has(res.error || "")) {
@@ -8592,11 +8593,11 @@ async function addLoadingReaction(channel, timestamp) {
8592
8593
  return { ok: false, error: err?.message || "unknown" };
8593
8594
  }
8594
8595
  })();
8595
- pendingAdds.set(key2, inFlight);
8596
- void inFlight.finally(() => {
8597
- if (pendingAdds.get(key2) === inFlight) pendingAdds.delete(key2);
8596
+ pendingAdds.set(key2, inFlight2);
8597
+ void inFlight2.finally(() => {
8598
+ if (pendingAdds.get(key2) === inFlight2) pendingAdds.delete(key2);
8598
8599
  });
8599
- return inFlight;
8600
+ return inFlight2;
8600
8601
  }
8601
8602
  async function removeLoadingReaction(channel, timestamp) {
8602
8603
  const adapter = getSlackAdapter();
@@ -8844,17 +8845,28 @@ async function fetchBotParticipatedInThread(channel, threadTs) {
8844
8845
  const self = await ensureSlackSelfIdentity();
8845
8846
  if (!self) return false;
8846
8847
  try {
8847
- const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${encodeURIComponent(threadTs)}&limit=200`;
8848
- const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
8849
- const data = await res.json().catch(() => ({}));
8850
- if (!data?.ok) {
8851
- console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
8852
- return false;
8848
+ let cursor = "";
8849
+ for (let page = 0; page < 10; page++) {
8850
+ const params = new URLSearchParams({ channel, ts: threadTs, limit: "200" });
8851
+ if (cursor) params.set("cursor", cursor);
8852
+ const res = await fetch(`https://slack.com/api/conversations.replies?${params.toString()}`, {
8853
+ headers: { Authorization: `Bearer ${token}` }
8854
+ });
8855
+ const data = await res.json().catch(() => ({}));
8856
+ if (!data?.ok) {
8857
+ console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
8858
+ return false;
8859
+ }
8860
+ const messages = Array.isArray(data.messages) ? data.messages : [];
8861
+ if (messages.some(
8862
+ (m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
8863
+ )) {
8864
+ return true;
8865
+ }
8866
+ cursor = String(data.response_metadata?.next_cursor || "");
8867
+ if (!cursor) break;
8853
8868
  }
8854
- const messages = Array.isArray(data.messages) ? data.messages : [];
8855
- return messages.some(
8856
- (m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
8857
- );
8869
+ return false;
8858
8870
  } catch (err) {
8859
8871
  console.warn(`[slack] conversations.replies error:`, err?.message || err);
8860
8872
  return false;
@@ -9306,7 +9318,7 @@ async function reconcileOnce(now = Date.now()) {
9306
9318
  entry2.updatedAt = Date.now();
9307
9319
  const nudged = {
9308
9320
  ...entry2.event,
9309
- 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.]
9321
+ 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.]
9310
9322
  ${entry2.event.content}`,
9311
9323
  wake: "now"
9312
9324
  };
@@ -9733,6 +9745,428 @@ var init_registry = __esm({
9733
9745
  }
9734
9746
  });
9735
9747
 
9748
+ // src/integrations/slack/files.ts
9749
+ var files_exports = {};
9750
+ __export(files_exports, {
9751
+ INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
9752
+ MAX_BYTES: () => MAX_BYTES,
9753
+ formatFileBlock: () => formatFileBlock,
9754
+ ingestSlackFiles: () => ingestSlackFiles
9755
+ });
9756
+ function inferFileName(file) {
9757
+ return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
9758
+ }
9759
+ function inferContentType(file) {
9760
+ if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
9761
+ return "application/octet-stream";
9762
+ }
9763
+ function formatBytes(n) {
9764
+ if (!Number.isFinite(n) || n <= 0) return "?";
9765
+ if (n < 1024) return `${n} B`;
9766
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
9767
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
9768
+ }
9769
+ function sleep(ms) {
9770
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
9771
+ }
9772
+ function imageMagic(bytes) {
9773
+ if (bytes.length < 12) return null;
9774
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
9775
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
9776
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
9777
+ 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";
9778
+ return null;
9779
+ }
9780
+ function requiresRasterMagic(contentType) {
9781
+ const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
9782
+ return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
9783
+ }
9784
+ function validateDownloadedBytes(bytes, declaredContentType) {
9785
+ if (!requiresRasterMagic(declaredContentType)) return null;
9786
+ const actual = imageMagic(bytes);
9787
+ if (!actual) return "invalid_image_bytes";
9788
+ return null;
9789
+ }
9790
+ async function fetchSlackPrivateFile(sourceUrl, botToken) {
9791
+ let lastError = "unknown";
9792
+ for (let attempt = 0; attempt < 3; attempt++) {
9793
+ try {
9794
+ const res = await fetch(sourceUrl, {
9795
+ headers: { Authorization: `Bearer ${botToken}` }
9796
+ });
9797
+ if (res.status === 429 || res.status >= 500) {
9798
+ lastError = `slack_fetch_${res.status}`;
9799
+ const retryAfter = Number(res.headers.get("retry-after"));
9800
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
9801
+ if (attempt < 2) {
9802
+ await sleep(waitMs);
9803
+ continue;
9804
+ }
9805
+ }
9806
+ if (!res.ok) {
9807
+ return { ok: false, error: `slack_fetch_${res.status}` };
9808
+ }
9809
+ const contentLength = Number(res.headers.get("content-length"));
9810
+ if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
9811
+ return { ok: false, error: "size_exceeded" };
9812
+ }
9813
+ const ab = await res.arrayBuffer();
9814
+ if (ab.byteLength > MAX_BYTES) {
9815
+ return { ok: false, error: "size_exceeded" };
9816
+ }
9817
+ return { ok: true, bytes: Buffer.from(ab) };
9818
+ } catch (err) {
9819
+ lastError = `slack_fetch_error:${err?.message || "unknown"}`;
9820
+ if (attempt < 2) {
9821
+ await sleep(250 * (attempt + 1));
9822
+ continue;
9823
+ }
9824
+ }
9825
+ }
9826
+ return { ok: false, error: lastError };
9827
+ }
9828
+ function withTimeout(p, ms, label) {
9829
+ return new Promise((resolve13, reject) => {
9830
+ const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
9831
+ p.then(
9832
+ (v) => {
9833
+ clearTimeout(t);
9834
+ resolve13(v);
9835
+ },
9836
+ (e) => {
9837
+ clearTimeout(t);
9838
+ reject(e);
9839
+ }
9840
+ );
9841
+ });
9842
+ }
9843
+ async function ingestOne(file, sessionId, botToken) {
9844
+ const fileName = inferFileName(file);
9845
+ const contentType = inferContentType(file);
9846
+ const declaredSize = typeof file.size === "number" ? file.size : 0;
9847
+ const base = {
9848
+ slackFileId: file.id,
9849
+ fileName,
9850
+ contentType,
9851
+ sizeBytes: declaredSize
9852
+ };
9853
+ const sourceUrl = file.url_private_download || file.url_private;
9854
+ if (!sourceUrl || typeof sourceUrl !== "string") {
9855
+ return { ...base, shortUrl: null, error: "no_source_url" };
9856
+ }
9857
+ if (declaredSize > MAX_BYTES) {
9858
+ return { ...base, shortUrl: null, error: "size_exceeded" };
9859
+ }
9860
+ let bytes;
9861
+ const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
9862
+ if (!fetched.ok) {
9863
+ return { ...base, shortUrl: null, error: fetched.error };
9864
+ }
9865
+ bytes = fetched.bytes;
9866
+ const byteError = validateDownloadedBytes(bytes, contentType);
9867
+ if (byteError) {
9868
+ console.warn(
9869
+ `[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)})`
9870
+ );
9871
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
9872
+ }
9873
+ const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
9874
+ let upload;
9875
+ try {
9876
+ upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
9877
+ } catch (err) {
9878
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
9879
+ }
9880
+ try {
9881
+ const putRes = await fetch(upload.uploadUrl, {
9882
+ method: "PUT",
9883
+ headers: { "Content-Type": contentType },
9884
+ body: bytes
9885
+ });
9886
+ if (!putRes.ok) {
9887
+ return {
9888
+ ...base,
9889
+ sizeBytes: bytes.length,
9890
+ shortUrl: null,
9891
+ error: `gcs_put_${putRes.status}`
9892
+ };
9893
+ }
9894
+ } catch (err) {
9895
+ return {
9896
+ ...base,
9897
+ sizeBytes: bytes.length,
9898
+ shortUrl: null,
9899
+ error: `gcs_put_error:${err?.message || "unknown"}`
9900
+ };
9901
+ }
9902
+ try {
9903
+ await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
9904
+ } catch (err) {
9905
+ console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
9906
+ }
9907
+ const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
9908
+ // server somehow forgot to return it (older remote-server versions).
9909
+ inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
9910
+ return {
9911
+ ...base,
9912
+ sizeBytes: bytes.length,
9913
+ shortUrl
9914
+ };
9915
+ }
9916
+ function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
9917
+ try {
9918
+ const u = new URL(uploadUrl);
9919
+ if (u.hostname.endsWith(".googleapis.com")) return null;
9920
+ return `${u.origin}/f/${fileId}`;
9921
+ } catch {
9922
+ return null;
9923
+ }
9924
+ }
9925
+ async function ingestSlackFiles(files, sessionId, options = {}) {
9926
+ if (!Array.isArray(files) || files.length === 0) return [];
9927
+ const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
9928
+ if (!isRemoteConfigured2()) {
9929
+ console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
9930
+ return files.map((f) => ({
9931
+ slackFileId: f.id,
9932
+ fileName: inferFileName(f),
9933
+ contentType: inferContentType(f),
9934
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
9935
+ shortUrl: null,
9936
+ error: "storage_unconfigured"
9937
+ }));
9938
+ }
9939
+ const botToken = getSlackBotToken();
9940
+ if (!botToken) {
9941
+ console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
9942
+ return files.map((f) => ({
9943
+ slackFileId: f.id,
9944
+ fileName: inferFileName(f),
9945
+ contentType: inferContentType(f),
9946
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
9947
+ shortUrl: null,
9948
+ error: "no_bot_token"
9949
+ }));
9950
+ }
9951
+ const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
9952
+ const startedAt = Date.now();
9953
+ const pipeline = Promise.allSettled(
9954
+ files.map(
9955
+ (f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
9956
+ if (String(err?.message || err).includes("_timeout")) {
9957
+ return {
9958
+ slackFileId: f.id,
9959
+ fileName: inferFileName(f),
9960
+ contentType: inferContentType(f),
9961
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
9962
+ shortUrl: null,
9963
+ error: "timeout"
9964
+ };
9965
+ }
9966
+ throw err;
9967
+ })
9968
+ )
9969
+ );
9970
+ const settled = await pipeline;
9971
+ const results = settled.map((s, i) => {
9972
+ if (s.status === "fulfilled") return s.value;
9973
+ const f = files[i];
9974
+ return {
9975
+ slackFileId: f.id,
9976
+ fileName: inferFileName(f),
9977
+ contentType: inferContentType(f),
9978
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
9979
+ shortUrl: null,
9980
+ error: `unexpected:${s.reason?.message || String(s.reason)}`
9981
+ };
9982
+ });
9983
+ const okCount = results.filter((r) => r.shortUrl).length;
9984
+ console.log(
9985
+ `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
9986
+ );
9987
+ return results;
9988
+ }
9989
+ function formatFileBlock(files) {
9990
+ if (!files || files.length === 0) return "";
9991
+ const lines = ["[files]"];
9992
+ for (const f of files) {
9993
+ const sizeLabel = formatBytes(f.sizeBytes);
9994
+ if (f.shortUrl) {
9995
+ lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
9996
+ } else {
9997
+ lines.push(
9998
+ ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
9999
+ );
10000
+ }
10001
+ }
10002
+ return lines.join("\n");
10003
+ }
10004
+ var MAX_BYTES, INGEST_TIMEOUT_MS;
10005
+ var init_files = __esm({
10006
+ "src/integrations/slack/files.ts"() {
10007
+ "use strict";
10008
+ init_client3();
10009
+ MAX_BYTES = 100 * 1024 * 1024;
10010
+ INGEST_TIMEOUT_MS = 2500;
10011
+ }
10012
+ });
10013
+
10014
+ // src/integrations/slack/read.ts
10015
+ var read_exports = {};
10016
+ __export(read_exports, {
10017
+ findChannelByName: () => findChannelByName,
10018
+ findUsers: () => findUsers,
10019
+ getChannelHistory: () => getChannelHistory,
10020
+ getPermalink: () => getPermalink,
10021
+ getThreadReplies: () => getThreadReplies,
10022
+ ingestMessageFiles: () => ingestMessageFiles
10023
+ });
10024
+ async function slackGet(method, params) {
10025
+ const token = getSlackBotToken();
10026
+ if (!token) return { ok: false, error: "slack_not_configured" };
10027
+ try {
10028
+ const qs = new URLSearchParams(params).toString();
10029
+ const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
10030
+ headers: { Authorization: `Bearer ${token}` }
10031
+ });
10032
+ const json = await res.json().catch(() => ({}));
10033
+ if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
10034
+ return { ok: true, json };
10035
+ } catch (err) {
10036
+ return { ok: false, error: err?.message || "unknown" };
10037
+ }
10038
+ }
10039
+ function clampLimit(n, def, max) {
10040
+ const v = typeof n === "number" && Number.isFinite(n) ? n : def;
10041
+ return String(Math.min(Math.max(Math.floor(v), 1), max));
10042
+ }
10043
+ function liteMessage(m) {
10044
+ return {
10045
+ ts: String(m?.ts ?? ""),
10046
+ threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
10047
+ user: typeof m?.user === "string" ? m.user : void 0,
10048
+ botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
10049
+ text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
10050
+ 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,
10051
+ replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
10052
+ };
10053
+ }
10054
+ async function enrichUserNames(messages) {
10055
+ const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
10056
+ const names = /* @__PURE__ */ new Map();
10057
+ await Promise.all(
10058
+ ids.map(async (id) => {
10059
+ const info = await resolveSlackUserInfo(id).catch(() => null);
10060
+ if (info?.name) names.set(id, info.name);
10061
+ })
10062
+ );
10063
+ return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
10064
+ }
10065
+ async function getChannelHistory(channel, limit) {
10066
+ const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
10067
+ if (!r.ok) return { ok: false, error: r.error };
10068
+ const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
10069
+ return { ok: true, data: await enrichUserNames(messages) };
10070
+ }
10071
+ async function getThreadReplies(channel, threadTs, limit) {
10072
+ const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
10073
+ if (!r.ok) return { ok: false, error: r.error };
10074
+ const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
10075
+ return { ok: true, data: await enrichUserNames(messages) };
10076
+ }
10077
+ async function getPermalink(channel, messageTs) {
10078
+ const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
10079
+ if (!r.ok) return { ok: false, error: r.error };
10080
+ return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
10081
+ }
10082
+ async function findChannelByName(name) {
10083
+ const clean = name.replace(/^#/, "").trim().toLowerCase();
10084
+ let cursor;
10085
+ for (let page = 0; page < 10; page++) {
10086
+ const params = {
10087
+ types: "public_channel,private_channel",
10088
+ exclude_archived: "true",
10089
+ limit: "999"
10090
+ };
10091
+ if (cursor) params.cursor = cursor;
10092
+ const r = await slackGet("conversations.list", params);
10093
+ if (!r.ok) return { ok: false, error: r.error };
10094
+ const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
10095
+ if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
10096
+ cursor = r.json.response_metadata?.next_cursor || "";
10097
+ if (!cursor) break;
10098
+ }
10099
+ return { ok: false, error: "channel_not_found" };
10100
+ }
10101
+ async function findUsers(query, max = 10) {
10102
+ const q = query.trim().toLowerCase();
10103
+ if (!q) return { ok: false, error: "empty_query" };
10104
+ if (q.includes("@")) {
10105
+ const r = await slackGet("users.lookupByEmail", { email: query.trim() });
10106
+ if (r.ok && r.json.user) {
10107
+ const u = r.json.user;
10108
+ 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 }] };
10109
+ }
10110
+ if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
10111
+ return { ok: false, error: r.error };
10112
+ }
10113
+ }
10114
+ const matches = [];
10115
+ let cursor;
10116
+ for (let page = 0; page < 10 && matches.length < max; page++) {
10117
+ const params = { limit: "1000" };
10118
+ if (cursor) params.cursor = cursor;
10119
+ const r = await slackGet("users.list", params);
10120
+ if (!r.ok) return { ok: false, error: r.error };
10121
+ for (const u of r.json.members || []) {
10122
+ if (u?.deleted || u?.is_bot) continue;
10123
+ const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
10124
+ const email = String(u?.profile?.email || "");
10125
+ if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
10126
+ matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
10127
+ if (matches.length >= max) break;
10128
+ }
10129
+ }
10130
+ cursor = r.json.response_metadata?.next_cursor || "";
10131
+ if (!cursor) break;
10132
+ }
10133
+ return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
10134
+ }
10135
+ async function getSingleMessage(channel, ts, threadTs) {
10136
+ if (threadTs) {
10137
+ const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
10138
+ if (rr2.ok && Array.isArray(rr2.json.messages)) {
10139
+ const match = rr2.json.messages.find((m) => m?.ts === ts);
10140
+ if (match) return match;
10141
+ }
10142
+ }
10143
+ const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
10144
+ if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
10145
+ const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
10146
+ if (rr.ok && Array.isArray(rr.json.messages)) {
10147
+ return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
10148
+ }
10149
+ return null;
10150
+ }
10151
+ async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
10152
+ const msg = await getSingleMessage(channel, ts, threadTs);
10153
+ if (!msg) return { ok: false, error: "message_not_found" };
10154
+ const files = Array.isArray(msg.files) ? msg.files : [];
10155
+ if (files.length === 0) return { ok: true, data: [] };
10156
+ const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
10157
+ const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
10158
+ return {
10159
+ ok: true,
10160
+ data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
10161
+ };
10162
+ }
10163
+ var init_read = __esm({
10164
+ "src/integrations/slack/read.ts"() {
10165
+ "use strict";
10166
+ init_client3();
10167
+ }
10168
+ });
10169
+
9736
10170
  // src/integrations/channels/messenger.ts
9737
10171
  async function postMessage(opts) {
9738
10172
  const adapter = getChannel(opts.channel);
@@ -9743,7 +10177,16 @@ async function postMessage(opts) {
9743
10177
  let ref;
9744
10178
  switch (opts.channel) {
9745
10179
  case "slack": {
9746
- const slackRef = { channel: "slack", slackChannel: opts.to, threadTs: opts.threadTs };
10180
+ let slackChannel2 = opts.to.trim();
10181
+ if (slackChannel2.startsWith("#")) {
10182
+ const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
10183
+ const found = await findChannelByName2(slackChannel2);
10184
+ if (!found.ok || !found.data?.id) {
10185
+ return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
10186
+ }
10187
+ slackChannel2 = found.data.id;
10188
+ }
10189
+ const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
9747
10190
  ref = slackRef;
9748
10191
  break;
9749
10192
  }
@@ -10050,6 +10493,46 @@ function buildMessengerTool() {
10050
10493
  }
10051
10494
  });
10052
10495
  }
10496
+ function buildSlackTool(opts) {
10497
+ return tool13({
10498
+ 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.",
10499
+ inputSchema: slackInputSchema,
10500
+ execute: async (input) => {
10501
+ if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
10502
+ switch (input.action) {
10503
+ case "history": {
10504
+ if (!input.channel) return { ok: false, error: "channel required" };
10505
+ return getChannelHistory(input.channel, input.limit);
10506
+ }
10507
+ case "replies": {
10508
+ if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
10509
+ return getThreadReplies(input.channel, input.threadTs, input.limit);
10510
+ }
10511
+ case "permalink": {
10512
+ if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
10513
+ return getPermalink(input.channel, input.messageTs);
10514
+ }
10515
+ case "find_channel": {
10516
+ if (!input.query) return { ok: false, error: "query (channel name) required" };
10517
+ return findChannelByName(input.query);
10518
+ }
10519
+ case "find_user": {
10520
+ if (!input.query) return { ok: false, error: "query (name or email) required" };
10521
+ return findUsers(input.query);
10522
+ }
10523
+ case "user_info": {
10524
+ if (!input.user) return { ok: false, error: "user required" };
10525
+ const info = await resolveSlackUserInfo(input.user);
10526
+ return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
10527
+ }
10528
+ case "fetch_files": {
10529
+ if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
10530
+ return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
10531
+ }
10532
+ }
10533
+ }
10534
+ });
10535
+ }
10053
10536
  function buildScheduleTool(opts) {
10054
10537
  return tool13({
10055
10538
  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.",
@@ -10131,15 +10614,18 @@ function createOrchestratorActionTools(opts) {
10131
10614
  return {
10132
10615
  agent: buildAgentTool(opts),
10133
10616
  messenger: buildMessengerTool(),
10617
+ slack: buildSlackTool(opts),
10134
10618
  schedule: buildScheduleTool(opts),
10135
10619
  webhook: buildWebhookTool(opts)
10136
10620
  };
10137
10621
  }
10138
- var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, scheduleInputSchema, webhookInputSchema;
10622
+ var AGENT_STATUS_ENUM, agentInputSchema, messengerInputSchema, slackInputSchema, scheduleInputSchema, webhookInputSchema;
10139
10623
  var init_orchestrator_actions = __esm({
10140
10624
  "src/tools/orchestrator-actions.ts"() {
10141
10625
  "use strict";
10142
10626
  init_messenger();
10627
+ init_read();
10628
+ init_client3();
10143
10629
  init_schedules_store();
10144
10630
  init_config();
10145
10631
  init_webhooks_store();
@@ -10197,6 +10683,15 @@ var init_orchestrator_actions = __esm({
10197
10683
  threadTs: z14.string().optional().describe("post + slack: reply in this thread."),
10198
10684
  subject: z14.string().optional().describe("post + email: subject (future).")
10199
10685
  });
10686
+ slackInputSchema = z14.object({
10687
+ action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
10688
+ channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
10689
+ threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
10690
+ messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
10691
+ query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
10692
+ user: z14.string().optional().describe("user_info: user id (U0123)."),
10693
+ limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
10694
+ });
10200
10695
  scheduleInputSchema = z14.object({
10201
10696
  action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
10202
10697
  // create / update
@@ -15834,226 +16329,69 @@ function verifySlackSignature(opts) {
15834
16329
  // src/server/routes/slack.ts
15835
16330
  init_client3();
15836
16331
  init_slack();
15837
-
15838
- // src/integrations/slack/files.ts
15839
- init_client3();
15840
- var MAX_BYTES = 100 * 1024 * 1024;
15841
- var INGEST_TIMEOUT_MS = 2500;
15842
- function inferFileName(file) {
15843
- return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
15844
- }
15845
- function inferContentType(file) {
15846
- if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
15847
- return "application/octet-stream";
15848
- }
15849
- function formatBytes(n) {
15850
- if (!Number.isFinite(n) || n <= 0) return "?";
15851
- if (n < 1024) return `${n} B`;
15852
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
15853
- return `${(n / 1024 / 1024).toFixed(2)} MB`;
15854
- }
15855
- function withTimeout(p, ms, label) {
15856
- return new Promise((resolve13, reject) => {
15857
- const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
15858
- p.then(
15859
- (v) => {
15860
- clearTimeout(t);
15861
- resolve13(v);
15862
- },
15863
- (e) => {
15864
- clearTimeout(t);
15865
- reject(e);
15866
- }
15867
- );
15868
- });
15869
- }
15870
- async function ingestOne(file, sessionId, botToken) {
15871
- const fileName = inferFileName(file);
15872
- const contentType = inferContentType(file);
15873
- const declaredSize = typeof file.size === "number" ? file.size : 0;
15874
- const base = {
15875
- slackFileId: file.id,
15876
- fileName,
15877
- contentType,
15878
- sizeBytes: declaredSize
15879
- };
15880
- const sourceUrl = file.url_private_download || file.url_private;
15881
- if (!sourceUrl || typeof sourceUrl !== "string") {
15882
- return { ...base, shortUrl: null, error: "no_source_url" };
15883
- }
15884
- if (declaredSize > MAX_BYTES) {
15885
- return { ...base, shortUrl: null, error: "size_exceeded" };
15886
- }
15887
- let bytes;
15888
- try {
15889
- const res = await fetch(sourceUrl, {
15890
- headers: { Authorization: `Bearer ${botToken}` }
15891
- });
15892
- if (!res.ok) {
15893
- return { ...base, shortUrl: null, error: `slack_fetch_${res.status}` };
15894
- }
15895
- const ab = await res.arrayBuffer();
15896
- if (ab.byteLength > MAX_BYTES) {
15897
- return { ...base, shortUrl: null, error: "size_exceeded" };
15898
- }
15899
- bytes = Buffer.from(ab);
15900
- } catch (err) {
15901
- return { ...base, shortUrl: null, error: `slack_fetch_error:${err?.message || "unknown"}` };
15902
- }
15903
- const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
15904
- let upload;
15905
- try {
15906
- upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
15907
- } catch (err) {
15908
- return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
15909
- }
15910
- try {
15911
- const putRes = await fetch(upload.uploadUrl, {
15912
- method: "PUT",
15913
- headers: { "Content-Type": contentType },
15914
- body: bytes
15915
- });
15916
- if (!putRes.ok) {
15917
- return {
15918
- ...base,
15919
- sizeBytes: bytes.length,
15920
- shortUrl: null,
15921
- error: `gcs_put_${putRes.status}`
15922
- };
15923
- }
15924
- } catch (err) {
15925
- return {
15926
- ...base,
15927
- sizeBytes: bytes.length,
15928
- shortUrl: null,
15929
- error: `gcs_put_error:${err?.message || "unknown"}`
15930
- };
15931
- }
15932
- try {
15933
- await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
15934
- } catch (err) {
15935
- console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
15936
- }
15937
- const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
15938
- // server somehow forgot to return it (older remote-server versions).
15939
- inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
15940
- return {
15941
- ...base,
15942
- sizeBytes: bytes.length,
15943
- shortUrl
15944
- };
15945
- }
15946
- function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
15947
- try {
15948
- const u = new URL(uploadUrl);
15949
- if (u.hostname.endsWith(".googleapis.com")) return null;
15950
- return `${u.origin}/f/${fileId}`;
15951
- } catch {
15952
- return null;
15953
- }
15954
- }
15955
- async function ingestSlackFiles(files, sessionId, options = {}) {
15956
- if (!Array.isArray(files) || files.length === 0) return [];
15957
- const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
15958
- if (!isRemoteConfigured2()) {
15959
- console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
15960
- return files.map((f) => ({
15961
- slackFileId: f.id,
15962
- fileName: inferFileName(f),
15963
- contentType: inferContentType(f),
15964
- sizeBytes: typeof f.size === "number" ? f.size : 0,
15965
- shortUrl: null,
15966
- error: "storage_unconfigured"
15967
- }));
15968
- }
15969
- const botToken = getSlackBotToken();
15970
- if (!botToken) {
15971
- console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
15972
- return files.map((f) => ({
15973
- slackFileId: f.id,
15974
- fileName: inferFileName(f),
15975
- contentType: inferContentType(f),
15976
- sizeBytes: typeof f.size === "number" ? f.size : 0,
15977
- shortUrl: null,
15978
- error: "no_bot_token"
15979
- }));
15980
- }
15981
- const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
15982
- const startedAt = Date.now();
15983
- const pipeline = Promise.allSettled(
15984
- files.map((f) => ingestOne(f, sessionId, botToken))
15985
- );
15986
- let settled;
15987
- try {
15988
- settled = await withTimeout(pipeline, timeoutMs, "ingest");
15989
- } catch (err) {
15990
- console.warn(`[slack-files] pipeline timeout after ${Date.now() - startedAt}ms (${err?.message || "timeout"})`);
15991
- return files.map((f) => ({
15992
- slackFileId: f.id,
15993
- fileName: inferFileName(f),
15994
- contentType: inferContentType(f),
15995
- sizeBytes: typeof f.size === "number" ? f.size : 0,
15996
- shortUrl: null,
15997
- error: "timeout"
15998
- }));
15999
- }
16000
- const results = settled.map((s, i) => {
16001
- if (s.status === "fulfilled") return s.value;
16002
- const f = files[i];
16003
- return {
16004
- slackFileId: f.id,
16005
- fileName: inferFileName(f),
16006
- contentType: inferContentType(f),
16007
- sizeBytes: typeof f.size === "number" ? f.size : 0,
16008
- shortUrl: null,
16009
- error: `unexpected:${s.reason?.message || String(s.reason)}`
16010
- };
16011
- });
16012
- const okCount = results.filter((r) => r.shortUrl).length;
16013
- console.log(
16014
- `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
16015
- );
16016
- return results;
16017
- }
16018
- function formatFileBlock(files) {
16019
- if (!files || files.length === 0) return "";
16020
- const lines = ["[files]"];
16021
- for (const f of files) {
16022
- const sizeLabel = formatBytes(f.sizeBytes);
16023
- if (f.shortUrl) {
16024
- lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
16025
- } else {
16026
- lines.push(
16027
- ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
16028
- );
16029
- }
16030
- }
16031
- return lines.join("\n");
16032
- }
16033
-
16034
- // src/server/routes/slack.ts
16332
+ init_files();
16035
16333
  init_webhook_events();
16036
16334
  init_inbox();
16037
16335
  var recentlyHandled = /* @__PURE__ */ new Map();
16336
+ var recentlyDeniedReplies = /* @__PURE__ */ new Map();
16337
+ var inFlight = /* @__PURE__ */ new Set();
16038
16338
  var MAX_RECENT = 1e3;
16339
+ function deliveryKey(channel, ts) {
16340
+ if (!channel || !ts) return null;
16341
+ return `${channel}\u241F${ts}`;
16342
+ }
16039
16343
  function wasHandled(channel, ts) {
16040
- if (!channel || !ts) return false;
16041
- return recentlyHandled.has(`${channel}\u241F${ts}`);
16344
+ const key2 = deliveryKey(channel, ts);
16345
+ if (!key2) return false;
16346
+ return recentlyHandled.has(key2) || inFlight.has(key2);
16347
+ }
16348
+ function markInFlight(channel, ts) {
16349
+ const key2 = deliveryKey(channel, ts);
16350
+ if (key2) inFlight.add(key2);
16351
+ }
16352
+ function clearInFlight(channel, ts) {
16353
+ const key2 = deliveryKey(channel, ts);
16354
+ if (key2) inFlight.delete(key2);
16042
16355
  }
16043
16356
  function markHandled(channel, ts) {
16044
- if (!channel || !ts) return;
16045
- const key2 = `${channel}\u241F${ts}`;
16357
+ const key2 = deliveryKey(channel, ts);
16358
+ if (!key2) return;
16359
+ inFlight.delete(key2);
16046
16360
  recentlyHandled.set(key2, Date.now());
16047
16361
  if (recentlyHandled.size > MAX_RECENT) {
16048
16362
  const oldest = recentlyHandled.keys().next().value;
16049
16363
  if (oldest) recentlyHandled.delete(oldest);
16050
16364
  }
16051
16365
  }
16366
+ function wasDeniedReplySent(channel, ts) {
16367
+ const key2 = deliveryKey(channel, ts);
16368
+ if (!key2) return false;
16369
+ return recentlyDeniedReplies.has(key2);
16370
+ }
16371
+ function markDeniedReplySent(channel, ts) {
16372
+ const key2 = deliveryKey(channel, ts);
16373
+ if (!key2) return;
16374
+ recentlyDeniedReplies.set(key2, Date.now());
16375
+ if (recentlyDeniedReplies.size > MAX_RECENT) {
16376
+ const oldest = recentlyDeniedReplies.keys().next().value;
16377
+ if (oldest) recentlyDeniedReplies.delete(oldest);
16378
+ }
16379
+ }
16052
16380
  var slack = new Hono6();
16053
16381
  slack.post("/events", async (c) => {
16054
- const signingSecret = getSlackSigningSecret();
16055
- if (!signingSecret) return c.json({ error: "slack not configured" }, 503);
16056
16382
  const rawBody = await c.req.text();
16383
+ const signingSecret = getSlackSigningSecret();
16384
+ if (!signingSecret) {
16385
+ let unsignedPayload;
16386
+ try {
16387
+ unsignedPayload = JSON.parse(rawBody);
16388
+ } catch {
16389
+ }
16390
+ if (unsignedPayload?.type === "url_verification" && typeof unsignedPayload.challenge === "string") {
16391
+ return c.json({ challenge: unsignedPayload.challenge });
16392
+ }
16393
+ return c.json({ error: "slack not configured" }, 503);
16394
+ }
16057
16395
  const verification = verifySlackSignature({
16058
16396
  signingSecret,
16059
16397
  timestampHeader: c.req.header("x-slack-request-timestamp") || null,
@@ -16107,65 +16445,79 @@ slack.post("/events", async (c) => {
16107
16445
  if (ev.type === "message" && ev.channel_type === "im" && ev.channel && (ev.thread_ts || ev.ts)) {
16108
16446
  markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
16109
16447
  }
16110
- const orchestratorId = await findOrCreateOrchestratorId();
16111
- if (orchestratorId) {
16112
- inbound.content = await normalizeSlackMentions(inbound.content);
16113
- if (ev.user) {
16114
- const info = await resolveSlackUserInfo(ev.user);
16115
- if (info?.name) {
16116
- const emailSuffix = info.email ? ` (${info.email})` : "";
16117
- const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
16118
- inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
16448
+ if (wasHandled(ev.channel, ev.ts)) {
16449
+ updateEvent(auditId, { status: "dropped", dropReason: "duplicate_delivery" });
16450
+ return c.json({ ok: true });
16451
+ }
16452
+ markInFlight(ev.channel, ev.ts);
16453
+ let routed = false;
16454
+ try {
16455
+ const orchestratorId = await findOrCreateOrchestratorId();
16456
+ if (orchestratorId) {
16457
+ inbound.content = await normalizeSlackMentions(inbound.content);
16458
+ if (ev.user) {
16459
+ const info = await resolveSlackUserInfo(ev.user);
16460
+ if (info?.name) {
16461
+ const emailSuffix = info.email ? ` (${info.email})` : "";
16462
+ const enriched = `user=${info.name} <@${ev.user}>${emailSuffix}`;
16463
+ inbound.content = inbound.content.replace(`user=${ev.user}`, () => enriched);
16464
+ }
16119
16465
  }
16120
- }
16121
- const slackFiles = Array.isArray(ev.files) ? ev.files : [];
16122
- markHandled(ev.channel, ev.ts);
16123
- if (ev.channel && ev.ts) {
16124
- void addLoadingReaction(String(ev.channel), String(ev.ts));
16125
- }
16126
- let ingestedCount = 0;
16127
- if (slackFiles.length > 0) {
16128
- try {
16129
- const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
16130
- const block = formatFileBlock(ingested);
16131
- if (block) inbound.content = `${inbound.content}
16466
+ const slackFiles = Array.isArray(ev.files) ? ev.files : [];
16467
+ if (ev.channel && ev.ts) {
16468
+ void addLoadingReaction(String(ev.channel), String(ev.ts));
16469
+ }
16470
+ let ingestedCount = 0;
16471
+ if (slackFiles.length > 0) {
16472
+ try {
16473
+ const ingested = await ingestSlackFiles(slackFiles, orchestratorId);
16474
+ const block = formatFileBlock(ingested);
16475
+ if (block) inbound.content = `${inbound.content}
16132
16476
  ${block}`;
16133
- ingestedCount = ingested.filter((f) => f.shortUrl).length;
16134
- } catch (err) {
16135
- console.warn("[slack-files] ingestion threw:", err?.message || err);
16136
- inbound.content = `${inbound.content}
16477
+ ingestedCount = ingested.filter((f) => f.shortUrl).length;
16478
+ } catch (err) {
16479
+ console.warn("[slack-files] ingestion threw:", err?.message || err);
16480
+ inbound.content = `${inbound.content}
16137
16481
  [files] (ingestion failed: ${err?.message || "unknown"})`;
16482
+ }
16138
16483
  }
16484
+ pushToInbox(orchestratorId, inbound);
16485
+ routed = true;
16486
+ markHandled(ev.channel, ev.ts);
16487
+ updateEvent(auditId, {
16488
+ status: "routed",
16489
+ sessionId: orchestratorId,
16490
+ ...slackFiles.length > 0 ? {
16491
+ // Preserve the original meta (ts, thread_ts, team,
16492
+ // event_subtype) from recordEvent above — updateEvent does a
16493
+ // shallow merge, so we have to re-include them.
16494
+ meta: {
16495
+ ts: ev.ts,
16496
+ thread_ts: ev.thread_ts,
16497
+ team: ev.team,
16498
+ event_subtype: ev.subtype,
16499
+ fileCount: slackFiles.length,
16500
+ ingestedCount
16501
+ }
16502
+ } : {}
16503
+ });
16504
+ } else {
16505
+ updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
16139
16506
  }
16140
- pushToInbox(orchestratorId, inbound);
16141
- updateEvent(auditId, {
16142
- status: "routed",
16143
- sessionId: orchestratorId,
16144
- ...slackFiles.length > 0 ? {
16145
- // Preserve the original meta (ts, thread_ts, team,
16146
- // event_subtype) from recordEvent above — updateEvent does a
16147
- // shallow merge, so we have to re-include them.
16148
- meta: {
16149
- ts: ev.ts,
16150
- thread_ts: ev.thread_ts,
16151
- team: ev.team,
16152
- event_subtype: ev.subtype,
16153
- fileCount: slackFiles.length,
16154
- ingestedCount
16155
- }
16156
- } : {}
16157
- });
16158
- } else {
16159
- updateEvent(auditId, { status: "error", error: "no orchestrator session available" });
16507
+ } finally {
16508
+ if (!routed) clearInFlight(ev.channel, ev.ts);
16160
16509
  }
16161
16510
  } else if (dropReason) {
16162
16511
  updateEvent(auditId, { status: "dropped", dropReason });
16163
16512
  const userFacingDrops = ["user_not_allowed", "channel_not_allowed", "dm_blocked"];
16164
16513
  if (userFacingDrops.includes(dropReason)) {
16165
16514
  console.log(`[slack] dropped event from user=${payload.event.user} channel=${payload.event.channel}: ${dropReason}`);
16166
- void sendDeniedReply(payload.event).catch((err) => {
16167
- console.warn(`[slack] denied-reply failed:`, err?.message || err);
16168
- });
16515
+ if (!wasDeniedReplySent(payload.event.channel, payload.event.ts)) {
16516
+ markDeniedReplySent(payload.event.channel, payload.event.ts);
16517
+ void sendDeniedReply(payload.event, dropReason).catch((err) => {
16518
+ console.warn(`[slack] denied-reply failed:`, err?.message || err);
16519
+ });
16520
+ }
16169
16521
  }
16170
16522
  }
16171
16523
  }
@@ -16190,7 +16542,7 @@ async function findOrCreateOrchestratorId() {
16190
16542
  return null;
16191
16543
  }
16192
16544
  }
16193
- async function sendDeniedReply(event) {
16545
+ async function sendDeniedReply(event, reason) {
16194
16546
  const policy = getSlackDeniedReplyPolicy();
16195
16547
  if (!policy.enabled) return;
16196
16548
  const adapter = getSlackAdapter();
@@ -16199,7 +16551,7 @@ async function sendDeniedReply(event) {
16199
16551
  const channel = String(event?.channel || "");
16200
16552
  if (!channel) return;
16201
16553
  const threadTs = event?.thread_ts || event?.ts;
16202
- const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`);
16554
+ const text = policy.template.replace(/\{user\}/g, `<@${user}>`).replace(/\{channel\}/g, `<#${channel}>`).replace(/\{reason\}/g, reason);
16203
16555
  const result = await adapter.postMessage({ channel, text, threadTs });
16204
16556
  if (!result.ok) {
16205
16557
  console.warn(`[slack] denied-reply post failed: ${result.error}`);