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
@@ -1798,6 +1798,17 @@ function hookExit() {
1798
1798
  process.once("SIGINT", flush2);
1799
1799
  process.once("SIGTERM", flush2);
1800
1800
  }
1801
+ function getCachedUserName(userId) {
1802
+ load();
1803
+ return userMap.get(userId);
1804
+ }
1805
+ function setCachedUserName(userId, entry2) {
1806
+ load();
1807
+ hookExit();
1808
+ userMap.set(userId, entry2);
1809
+ evictIfOversized(userMap, MAX_USERS);
1810
+ scheduleSave();
1811
+ }
1801
1812
  function setCachedThreadOwnership(key2, entry2) {
1802
1813
  load();
1803
1814
  hookExit();
@@ -1805,7 +1816,7 @@ function setCachedThreadOwnership(key2, entry2) {
1805
1816
  evictIfOversized(threadMap, MAX_THREADS);
1806
1817
  scheduleSave();
1807
1818
  }
1808
- var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
1819
+ var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_USERS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
1809
1820
  var init_persistence = __esm({
1810
1821
  "src/integrations/slack/persistence.ts"() {
1811
1822
  "use strict";
@@ -1813,6 +1824,7 @@ var init_persistence = __esm({
1813
1824
  FILENAME = "slack-cache.json";
1814
1825
  FILE_VERSION = 1;
1815
1826
  SAVE_DEBOUNCE_MS = 500;
1827
+ MAX_USERS = 5e3;
1816
1828
  MAX_THREADS = 5e3;
1817
1829
  loaded = false;
1818
1830
  userMap = /* @__PURE__ */ new Map();
@@ -1936,6 +1948,58 @@ function getSlackAdapter() {
1936
1948
  function isSlackConfigured() {
1937
1949
  return readSlackConfig() !== void 0;
1938
1950
  }
1951
+ function getSlackBotToken() {
1952
+ return readSlackConfig()?.botToken ?? null;
1953
+ }
1954
+ async function fetchSlackUserInfo(userId) {
1955
+ const token = getSlackBotToken();
1956
+ if (!token) return null;
1957
+ try {
1958
+ const res = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, {
1959
+ headers: { Authorization: `Bearer ${token}` }
1960
+ });
1961
+ const data = await res.json().catch(() => ({}));
1962
+ if (!data?.ok) {
1963
+ console.warn(`[slack] users.info(${userId}) failed: ${data?.error || `HTTP ${res.status}`}`);
1964
+ return null;
1965
+ }
1966
+ const profile = data.user?.profile || {};
1967
+ const name = profile.display_name_normalized || profile.display_name || profile.real_name_normalized || profile.real_name || data.user?.real_name || data.user?.name || null;
1968
+ const realName = profile.real_name_normalized || profile.real_name || data.user?.real_name || null;
1969
+ const email = profile.email || null;
1970
+ return {
1971
+ name: name ? String(name) : null,
1972
+ realName: realName ? String(realName) : null,
1973
+ email: email ? String(email) : null
1974
+ };
1975
+ } catch (err) {
1976
+ console.warn(`[slack] users.info(${userId}) error:`, err?.message || err);
1977
+ return null;
1978
+ }
1979
+ }
1980
+ async function resolveSlackUserInfo(userId) {
1981
+ if (!userId) return null;
1982
+ const now = Date.now();
1983
+ const hit = getCachedUserName(userId);
1984
+ if (hit && hit.expiresAt > now) {
1985
+ return { name: hit.name, realName: hit.realName ?? null, email: hit.email ?? null };
1986
+ }
1987
+ const inflight = userInflight.get(userId);
1988
+ if (inflight) return inflight;
1989
+ const p = (async () => {
1990
+ const info = await fetchSlackUserInfo(userId);
1991
+ setCachedUserName(userId, {
1992
+ name: info?.name ?? null,
1993
+ realName: info?.realName ?? null,
1994
+ email: info?.email ?? null,
1995
+ expiresAt: now + (info?.name ? USER_TTL_MS : USER_FAIL_TTL_MS)
1996
+ });
1997
+ userInflight.delete(userId);
1998
+ return info;
1999
+ })();
2000
+ userInflight.set(userId, p);
2001
+ return p;
2002
+ }
1939
2003
  function threadCacheKey(channel, threadTs) {
1940
2004
  return `${channel}\u241F${threadTs}`;
1941
2005
  }
@@ -1946,7 +2010,7 @@ function noteBotPostedInThread(channel, threadTs) {
1946
2010
  expiresAt: Date.now() + THREAD_OWNED_TTL_MS
1947
2011
  });
1948
2012
  }
1949
- var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
2013
+ var RESULT_REACTIONS, SLACK_FETCH_ATTEMPTS, SLACK_BACKOFF_BASE_MS, SLACK_BACKOFF_CAP_MS, REACTION_SOFT_ERRORS, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS;
1950
2014
  var init_client2 = __esm({
1951
2015
  "src/integrations/slack/client.ts"() {
1952
2016
  "use strict";
@@ -1971,6 +2035,7 @@ var init_client2 = __esm({
1971
2035
  ]);
1972
2036
  USER_TTL_MS = 60 * 60 * 1e3;
1973
2037
  USER_FAIL_TTL_MS = 5 * 60 * 1e3;
2038
+ userInflight = /* @__PURE__ */ new Map();
1974
2039
  THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
1975
2040
  THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
1976
2041
  }
@@ -2278,7 +2343,7 @@ async function reconcileOnce(now = Date.now()) {
2278
2343
  entry2.updatedAt = Date.now();
2279
2344
  const nudged = {
2280
2345
  ...entry2.event,
2281
- 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.]
2346
+ 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.]
2282
2347
  ${entry2.event.content}`,
2283
2348
  wake: "now"
2284
2349
  };
@@ -2392,6 +2457,428 @@ var init_inbox_acks = __esm({
2392
2457
  }
2393
2458
  });
2394
2459
 
2460
+ // src/integrations/slack/files.ts
2461
+ var files_exports = {};
2462
+ __export(files_exports, {
2463
+ INGEST_TIMEOUT_MS: () => INGEST_TIMEOUT_MS,
2464
+ MAX_BYTES: () => MAX_BYTES,
2465
+ formatFileBlock: () => formatFileBlock,
2466
+ ingestSlackFiles: () => ingestSlackFiles
2467
+ });
2468
+ function inferFileName(file) {
2469
+ return typeof file.name === "string" && file.name || typeof file.title === "string" && file.title || `slack-file-${file.id}`;
2470
+ }
2471
+ function inferContentType(file) {
2472
+ if (typeof file.mimetype === "string" && file.mimetype) return file.mimetype;
2473
+ return "application/octet-stream";
2474
+ }
2475
+ function formatBytes(n) {
2476
+ if (!Number.isFinite(n) || n <= 0) return "?";
2477
+ if (n < 1024) return `${n} B`;
2478
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
2479
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
2480
+ }
2481
+ function sleep(ms) {
2482
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
2483
+ }
2484
+ function imageMagic(bytes) {
2485
+ if (bytes.length < 12) return null;
2486
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
2487
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
2488
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "image/gif";
2489
+ 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";
2490
+ return null;
2491
+ }
2492
+ function requiresRasterMagic(contentType) {
2493
+ const normalized = contentType.toLowerCase().split(";", 1)[0].trim();
2494
+ return normalized === "image/jpeg" || normalized === "image/jpg" || normalized === "image/png" || normalized === "image/gif" || normalized === "image/webp";
2495
+ }
2496
+ function validateDownloadedBytes(bytes, declaredContentType) {
2497
+ if (!requiresRasterMagic(declaredContentType)) return null;
2498
+ const actual = imageMagic(bytes);
2499
+ if (!actual) return "invalid_image_bytes";
2500
+ return null;
2501
+ }
2502
+ async function fetchSlackPrivateFile(sourceUrl, botToken) {
2503
+ let lastError = "unknown";
2504
+ for (let attempt = 0; attempt < 3; attempt++) {
2505
+ try {
2506
+ const res = await fetch(sourceUrl, {
2507
+ headers: { Authorization: `Bearer ${botToken}` }
2508
+ });
2509
+ if (res.status === 429 || res.status >= 500) {
2510
+ lastError = `slack_fetch_${res.status}`;
2511
+ const retryAfter = Number(res.headers.get("retry-after"));
2512
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, 2e3) : 250 * (attempt + 1);
2513
+ if (attempt < 2) {
2514
+ await sleep(waitMs);
2515
+ continue;
2516
+ }
2517
+ }
2518
+ if (!res.ok) {
2519
+ return { ok: false, error: `slack_fetch_${res.status}` };
2520
+ }
2521
+ const contentLength = Number(res.headers.get("content-length"));
2522
+ if (Number.isFinite(contentLength) && contentLength > MAX_BYTES) {
2523
+ return { ok: false, error: "size_exceeded" };
2524
+ }
2525
+ const ab = await res.arrayBuffer();
2526
+ if (ab.byteLength > MAX_BYTES) {
2527
+ return { ok: false, error: "size_exceeded" };
2528
+ }
2529
+ return { ok: true, bytes: Buffer.from(ab) };
2530
+ } catch (err) {
2531
+ lastError = `slack_fetch_error:${err?.message || "unknown"}`;
2532
+ if (attempt < 2) {
2533
+ await sleep(250 * (attempt + 1));
2534
+ continue;
2535
+ }
2536
+ }
2537
+ }
2538
+ return { ok: false, error: lastError };
2539
+ }
2540
+ function withTimeout(p, ms, label) {
2541
+ return new Promise((resolve11, reject) => {
2542
+ const t = setTimeout(() => reject(new Error(`${label}_timeout`)), ms);
2543
+ p.then(
2544
+ (v) => {
2545
+ clearTimeout(t);
2546
+ resolve11(v);
2547
+ },
2548
+ (e) => {
2549
+ clearTimeout(t);
2550
+ reject(e);
2551
+ }
2552
+ );
2553
+ });
2554
+ }
2555
+ async function ingestOne(file, sessionId, botToken) {
2556
+ const fileName = inferFileName(file);
2557
+ const contentType = inferContentType(file);
2558
+ const declaredSize = typeof file.size === "number" ? file.size : 0;
2559
+ const base = {
2560
+ slackFileId: file.id,
2561
+ fileName,
2562
+ contentType,
2563
+ sizeBytes: declaredSize
2564
+ };
2565
+ const sourceUrl = file.url_private_download || file.url_private;
2566
+ if (!sourceUrl || typeof sourceUrl !== "string") {
2567
+ return { ...base, shortUrl: null, error: "no_source_url" };
2568
+ }
2569
+ if (declaredSize > MAX_BYTES) {
2570
+ return { ...base, shortUrl: null, error: "size_exceeded" };
2571
+ }
2572
+ let bytes;
2573
+ const fetched = await fetchSlackPrivateFile(sourceUrl, botToken);
2574
+ if (!fetched.ok) {
2575
+ return { ...base, shortUrl: null, error: fetched.error };
2576
+ }
2577
+ bytes = fetched.bytes;
2578
+ const byteError = validateDownloadedBytes(bytes, contentType);
2579
+ if (byteError) {
2580
+ console.warn(
2581
+ `[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)})`
2582
+ );
2583
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: byteError };
2584
+ }
2585
+ const { storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
2586
+ let upload;
2587
+ try {
2588
+ upload = await storageQueries2.getUploadUrl(sessionId, fileName, contentType, "slack");
2589
+ } catch (err) {
2590
+ return { ...base, sizeBytes: bytes.length, shortUrl: null, error: `presign_failed:${err?.message || "unknown"}` };
2591
+ }
2592
+ try {
2593
+ const putRes = await fetch(upload.uploadUrl, {
2594
+ method: "PUT",
2595
+ headers: { "Content-Type": contentType },
2596
+ body: bytes
2597
+ });
2598
+ if (!putRes.ok) {
2599
+ return {
2600
+ ...base,
2601
+ sizeBytes: bytes.length,
2602
+ shortUrl: null,
2603
+ error: `gcs_put_${putRes.status}`
2604
+ };
2605
+ }
2606
+ } catch (err) {
2607
+ return {
2608
+ ...base,
2609
+ sizeBytes: bytes.length,
2610
+ shortUrl: null,
2611
+ error: `gcs_put_error:${err?.message || "unknown"}`
2612
+ };
2613
+ }
2614
+ try {
2615
+ await storageQueries2.updateFile(upload.fileId, { sizeBytes: bytes.length });
2616
+ } catch (err) {
2617
+ console.warn(`[slack-files] sizeBytes patch failed for ${upload.fileId}:`, err?.message || err);
2618
+ }
2619
+ const shortUrl = upload.shortUrl || // Defensive fallback: build it from the upload URL's origin if the
2620
+ // server somehow forgot to return it (older remote-server versions).
2621
+ inferShortUrlFromUploadUrl(upload.uploadUrl, upload.fileId);
2622
+ return {
2623
+ ...base,
2624
+ sizeBytes: bytes.length,
2625
+ shortUrl
2626
+ };
2627
+ }
2628
+ function inferShortUrlFromUploadUrl(uploadUrl, fileId) {
2629
+ try {
2630
+ const u = new URL(uploadUrl);
2631
+ if (u.hostname.endsWith(".googleapis.com")) return null;
2632
+ return `${u.origin}/f/${fileId}`;
2633
+ } catch {
2634
+ return null;
2635
+ }
2636
+ }
2637
+ async function ingestSlackFiles(files, sessionId, options = {}) {
2638
+ if (!Array.isArray(files) || files.length === 0) return [];
2639
+ const { isRemoteConfigured: isRemoteConfigured2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
2640
+ if (!isRemoteConfigured2()) {
2641
+ console.warn(`[slack-files] storage not configured \u2014 skipping ingestion for ${files.length} file(s)`);
2642
+ return files.map((f) => ({
2643
+ slackFileId: f.id,
2644
+ fileName: inferFileName(f),
2645
+ contentType: inferContentType(f),
2646
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2647
+ shortUrl: null,
2648
+ error: "storage_unconfigured"
2649
+ }));
2650
+ }
2651
+ const botToken = getSlackBotToken();
2652
+ if (!botToken) {
2653
+ console.warn("[slack-files] no bot token \u2014 cannot download from Slack");
2654
+ return files.map((f) => ({
2655
+ slackFileId: f.id,
2656
+ fileName: inferFileName(f),
2657
+ contentType: inferContentType(f),
2658
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2659
+ shortUrl: null,
2660
+ error: "no_bot_token"
2661
+ }));
2662
+ }
2663
+ const timeoutMs = options.timeoutMs ?? INGEST_TIMEOUT_MS;
2664
+ const startedAt = Date.now();
2665
+ const pipeline = Promise.allSettled(
2666
+ files.map(
2667
+ (f) => withTimeout(ingestOne(f, sessionId, botToken), timeoutMs, `ingest:${f.id}`).catch((err) => {
2668
+ if (String(err?.message || err).includes("_timeout")) {
2669
+ return {
2670
+ slackFileId: f.id,
2671
+ fileName: inferFileName(f),
2672
+ contentType: inferContentType(f),
2673
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2674
+ shortUrl: null,
2675
+ error: "timeout"
2676
+ };
2677
+ }
2678
+ throw err;
2679
+ })
2680
+ )
2681
+ );
2682
+ const settled = await pipeline;
2683
+ const results = settled.map((s, i) => {
2684
+ if (s.status === "fulfilled") return s.value;
2685
+ const f = files[i];
2686
+ return {
2687
+ slackFileId: f.id,
2688
+ fileName: inferFileName(f),
2689
+ contentType: inferContentType(f),
2690
+ sizeBytes: typeof f.size === "number" ? f.size : 0,
2691
+ shortUrl: null,
2692
+ error: `unexpected:${s.reason?.message || String(s.reason)}`
2693
+ };
2694
+ });
2695
+ const okCount = results.filter((r) => r.shortUrl).length;
2696
+ console.log(
2697
+ `[slack-files] ingested ${okCount}/${files.length} file(s) in ${Date.now() - startedAt}ms`
2698
+ );
2699
+ return results;
2700
+ }
2701
+ function formatFileBlock(files) {
2702
+ if (!files || files.length === 0) return "";
2703
+ const lines = ["[files]"];
2704
+ for (const f of files) {
2705
+ const sizeLabel = formatBytes(f.sizeBytes);
2706
+ if (f.shortUrl) {
2707
+ lines.push(` - ${f.fileName} (${f.contentType}, ${sizeLabel}): ${f.shortUrl}`);
2708
+ } else {
2709
+ lines.push(
2710
+ ` - ${f.fileName} (${f.contentType}, ${sizeLabel}): [ingestion failed: ${f.error || "unknown"}]`
2711
+ );
2712
+ }
2713
+ }
2714
+ return lines.join("\n");
2715
+ }
2716
+ var MAX_BYTES, INGEST_TIMEOUT_MS;
2717
+ var init_files = __esm({
2718
+ "src/integrations/slack/files.ts"() {
2719
+ "use strict";
2720
+ init_client2();
2721
+ MAX_BYTES = 100 * 1024 * 1024;
2722
+ INGEST_TIMEOUT_MS = 2500;
2723
+ }
2724
+ });
2725
+
2726
+ // src/integrations/slack/read.ts
2727
+ var read_exports = {};
2728
+ __export(read_exports, {
2729
+ findChannelByName: () => findChannelByName,
2730
+ findUsers: () => findUsers,
2731
+ getChannelHistory: () => getChannelHistory,
2732
+ getPermalink: () => getPermalink,
2733
+ getThreadReplies: () => getThreadReplies,
2734
+ ingestMessageFiles: () => ingestMessageFiles
2735
+ });
2736
+ async function slackGet(method, params) {
2737
+ const token = getSlackBotToken();
2738
+ if (!token) return { ok: false, error: "slack_not_configured" };
2739
+ try {
2740
+ const qs = new URLSearchParams(params).toString();
2741
+ const res = await fetch(`https://slack.com/api/${method}?${qs}`, {
2742
+ headers: { Authorization: `Bearer ${token}` }
2743
+ });
2744
+ const json = await res.json().catch(() => ({}));
2745
+ if (!json?.ok) return { ok: false, error: json?.error || `HTTP ${res.status}` };
2746
+ return { ok: true, json };
2747
+ } catch (err) {
2748
+ return { ok: false, error: err?.message || "unknown" };
2749
+ }
2750
+ }
2751
+ function clampLimit(n, def, max) {
2752
+ const v = typeof n === "number" && Number.isFinite(n) ? n : def;
2753
+ return String(Math.min(Math.max(Math.floor(v), 1), max));
2754
+ }
2755
+ function liteMessage(m) {
2756
+ return {
2757
+ ts: String(m?.ts ?? ""),
2758
+ threadTs: typeof m?.thread_ts === "string" ? m.thread_ts : void 0,
2759
+ user: typeof m?.user === "string" ? m.user : void 0,
2760
+ botId: typeof m?.bot_id === "string" ? m.bot_id : void 0,
2761
+ text: typeof m?.text === "string" ? m.text.slice(0, 4e3) : "",
2762
+ 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,
2763
+ replyCount: typeof m?.reply_count === "number" ? m.reply_count : void 0
2764
+ };
2765
+ }
2766
+ async function enrichUserNames(messages) {
2767
+ const ids = [...new Set(messages.map((m) => m.user).filter((u) => !!u))];
2768
+ const names = /* @__PURE__ */ new Map();
2769
+ await Promise.all(
2770
+ ids.map(async (id) => {
2771
+ const info = await resolveSlackUserInfo(id).catch(() => null);
2772
+ if (info?.name) names.set(id, info.name);
2773
+ })
2774
+ );
2775
+ return messages.map((m) => m.user && names.has(m.user) ? { ...m, userName: names.get(m.user) } : m);
2776
+ }
2777
+ async function getChannelHistory(channel, limit) {
2778
+ const r = await slackGet("conversations.history", { channel, limit: clampLimit(limit, 20, 100) });
2779
+ if (!r.ok) return { ok: false, error: r.error };
2780
+ const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
2781
+ return { ok: true, data: await enrichUserNames(messages) };
2782
+ }
2783
+ async function getThreadReplies(channel, threadTs, limit) {
2784
+ const r = await slackGet("conversations.replies", { channel, ts: threadTs, limit: clampLimit(limit, 50, 200) });
2785
+ if (!r.ok) return { ok: false, error: r.error };
2786
+ const messages = Array.isArray(r.json.messages) ? r.json.messages.map(liteMessage) : [];
2787
+ return { ok: true, data: await enrichUserNames(messages) };
2788
+ }
2789
+ async function getPermalink(channel, messageTs) {
2790
+ const r = await slackGet("chat.getPermalink", { channel, message_ts: messageTs });
2791
+ if (!r.ok) return { ok: false, error: r.error };
2792
+ return { ok: true, data: { permalink: String(r.json.permalink ?? "") } };
2793
+ }
2794
+ async function findChannelByName(name) {
2795
+ const clean = name.replace(/^#/, "").trim().toLowerCase();
2796
+ let cursor;
2797
+ for (let page = 0; page < 10; page++) {
2798
+ const params = {
2799
+ types: "public_channel,private_channel",
2800
+ exclude_archived: "true",
2801
+ limit: "999"
2802
+ };
2803
+ if (cursor) params.cursor = cursor;
2804
+ const r = await slackGet("conversations.list", params);
2805
+ if (!r.ok) return { ok: false, error: r.error };
2806
+ const match = (r.json.channels || []).find((c) => String(c?.name ?? "").toLowerCase() === clean);
2807
+ if (match) return { ok: true, data: { id: String(match.id), name: String(match.name) } };
2808
+ cursor = r.json.response_metadata?.next_cursor || "";
2809
+ if (!cursor) break;
2810
+ }
2811
+ return { ok: false, error: "channel_not_found" };
2812
+ }
2813
+ async function findUsers(query, max = 10) {
2814
+ const q = query.trim().toLowerCase();
2815
+ if (!q) return { ok: false, error: "empty_query" };
2816
+ if (q.includes("@")) {
2817
+ const r = await slackGet("users.lookupByEmail", { email: query.trim() });
2818
+ if (r.ok && r.json.user) {
2819
+ const u = r.json.user;
2820
+ 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 }] };
2821
+ }
2822
+ if (r.error && !["users_not_found", "user_not_found", "not_found"].includes(r.error)) {
2823
+ return { ok: false, error: r.error };
2824
+ }
2825
+ }
2826
+ const matches = [];
2827
+ let cursor;
2828
+ for (let page = 0; page < 10 && matches.length < max; page++) {
2829
+ const params = { limit: "1000" };
2830
+ if (cursor) params.cursor = cursor;
2831
+ const r = await slackGet("users.list", params);
2832
+ if (!r.ok) return { ok: false, error: r.error };
2833
+ for (const u of r.json.members || []) {
2834
+ if (u?.deleted || u?.is_bot) continue;
2835
+ const dn = String(u?.profile?.display_name_normalized || u?.profile?.real_name || u?.name || "");
2836
+ const email = String(u?.profile?.email || "");
2837
+ if (dn.toLowerCase().includes(q) || email.toLowerCase().includes(q)) {
2838
+ matches.push({ id: String(u.id), name: dn || String(u.name), realName: u?.profile?.real_name, email: email || void 0 });
2839
+ if (matches.length >= max) break;
2840
+ }
2841
+ }
2842
+ cursor = r.json.response_metadata?.next_cursor || "";
2843
+ if (!cursor) break;
2844
+ }
2845
+ return matches.length > 0 ? { ok: true, data: matches } : { ok: false, error: "no_match" };
2846
+ }
2847
+ async function getSingleMessage(channel, ts, threadTs) {
2848
+ if (threadTs) {
2849
+ const rr2 = await slackGet("conversations.replies", { channel, ts: threadTs, limit: "200" });
2850
+ if (rr2.ok && Array.isArray(rr2.json.messages)) {
2851
+ const match = rr2.json.messages.find((m) => m?.ts === ts);
2852
+ if (match) return match;
2853
+ }
2854
+ }
2855
+ const r = await slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: "true", limit: "1" });
2856
+ if (r.ok && Array.isArray(r.json.messages) && r.json.messages[0]) return r.json.messages[0];
2857
+ const rr = await slackGet("conversations.replies", { channel, ts, limit: "1" });
2858
+ if (rr.ok && Array.isArray(rr.json.messages)) {
2859
+ return rr.json.messages.find((m) => m?.ts === ts) || rr.json.messages[0] || null;
2860
+ }
2861
+ return null;
2862
+ }
2863
+ async function ingestMessageFiles(channel, ts, orchestratorSessionId, threadTs) {
2864
+ const msg = await getSingleMessage(channel, ts, threadTs);
2865
+ if (!msg) return { ok: false, error: "message_not_found" };
2866
+ const files = Array.isArray(msg.files) ? msg.files : [];
2867
+ if (files.length === 0) return { ok: true, data: [] };
2868
+ const { ingestSlackFiles: ingestSlackFiles2 } = await Promise.resolve().then(() => (init_files(), files_exports));
2869
+ const ingested = await ingestSlackFiles2(files, orchestratorSessionId);
2870
+ return {
2871
+ ok: true,
2872
+ data: ingested.map((f) => ({ name: f.fileName, url: f.shortUrl, error: f.error }))
2873
+ };
2874
+ }
2875
+ var init_read = __esm({
2876
+ "src/integrations/slack/read.ts"() {
2877
+ "use strict";
2878
+ init_client2();
2879
+ }
2880
+ });
2881
+
2395
2882
  // src/browser/stream-proxy.ts
2396
2883
  var stream_proxy_exports = {};
2397
2884
  __export(stream_proxy_exports, {
@@ -8528,7 +9015,16 @@ async function postMessage(opts) {
8528
9015
  let ref;
8529
9016
  switch (opts.channel) {
8530
9017
  case "slack": {
8531
- const slackRef = { channel: "slack", slackChannel: opts.to, threadTs: opts.threadTs };
9018
+ let slackChannel2 = opts.to.trim();
9019
+ if (slackChannel2.startsWith("#")) {
9020
+ const { findChannelByName: findChannelByName2 } = await Promise.resolve().then(() => (init_read(), read_exports));
9021
+ const found = await findChannelByName2(slackChannel2);
9022
+ if (!found.ok || !found.data?.id) {
9023
+ return { ok: false, error: `slack channel lookup failed: ${found.error || "channel_not_found"}` };
9024
+ }
9025
+ slackChannel2 = found.data.id;
9026
+ }
9027
+ const slackRef = { channel: "slack", slackChannel: slackChannel2, threadTs: opts.threadTs };
8532
9028
  ref = slackRef;
8533
9029
  break;
8534
9030
  }
@@ -8550,6 +9046,10 @@ function describeConfiguredChannels() {
8550
9046
  }));
8551
9047
  }
8552
9048
 
9049
+ // src/tools/orchestrator-actions.ts
9050
+ init_read();
9051
+ init_client2();
9052
+
8553
9053
  // src/orchestrator/schedules-store.ts
8554
9054
  init_db();
8555
9055
  import { nanoid as nanoid5 } from "nanoid";
@@ -8858,6 +9358,55 @@ function buildMessengerTool() {
8858
9358
  }
8859
9359
  });
8860
9360
  }
9361
+ var slackInputSchema = z14.object({
9362
+ action: z14.enum(["history", "replies", "permalink", "find_channel", "find_user", "user_info", "fetch_files"]),
9363
+ channel: z14.string().optional().describe("channel id (C0123/G0123/D0123). Required for history/replies/permalink/fetch_files."),
9364
+ threadTs: z14.string().optional().describe("replies/fetch_files: parent message ts of the thread."),
9365
+ messageTs: z14.string().optional().describe("permalink/fetch_files: ts of the target message; for thread-reply files also pass threadTs."),
9366
+ query: z14.string().optional().describe("find_channel: channel name (no #). find_user: name or email."),
9367
+ user: z14.string().optional().describe("user_info: user id (U0123)."),
9368
+ limit: z14.number().optional().describe("history/replies: max messages (history \u2264100, replies \u2264200).")
9369
+ });
9370
+ function buildSlackTool(opts) {
9371
+ return tool13({
9372
+ 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.",
9373
+ inputSchema: slackInputSchema,
9374
+ execute: async (input) => {
9375
+ if (!isSlackConfigured()) return { ok: false, error: "slack not configured" };
9376
+ switch (input.action) {
9377
+ case "history": {
9378
+ if (!input.channel) return { ok: false, error: "channel required" };
9379
+ return getChannelHistory(input.channel, input.limit);
9380
+ }
9381
+ case "replies": {
9382
+ if (!input.channel || !input.threadTs) return { ok: false, error: "channel and threadTs required" };
9383
+ return getThreadReplies(input.channel, input.threadTs, input.limit);
9384
+ }
9385
+ case "permalink": {
9386
+ if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
9387
+ return getPermalink(input.channel, input.messageTs);
9388
+ }
9389
+ case "find_channel": {
9390
+ if (!input.query) return { ok: false, error: "query (channel name) required" };
9391
+ return findChannelByName(input.query);
9392
+ }
9393
+ case "find_user": {
9394
+ if (!input.query) return { ok: false, error: "query (name or email) required" };
9395
+ return findUsers(input.query);
9396
+ }
9397
+ case "user_info": {
9398
+ if (!input.user) return { ok: false, error: "user required" };
9399
+ const info = await resolveSlackUserInfo(input.user);
9400
+ return info ? { ok: true, data: { id: input.user, ...info } } : { ok: false, error: "not_found" };
9401
+ }
9402
+ case "fetch_files": {
9403
+ if (!input.channel || !input.messageTs) return { ok: false, error: "channel and messageTs required" };
9404
+ return ingestMessageFiles(input.channel, input.messageTs, opts.orchestratorSessionId, input.threadTs);
9405
+ }
9406
+ }
9407
+ }
9408
+ });
9409
+ }
8861
9410
  var scheduleInputSchema = z14.object({
8862
9411
  action: z14.enum(["create", "list", "update", "delete", "pause", "resume"]),
8863
9412
  // create / update
@@ -8958,6 +9507,7 @@ function createOrchestratorActionTools(opts) {
8958
9507
  return {
8959
9508
  agent: buildAgentTool(opts),
8960
9509
  messenger: buildMessengerTool(),
9510
+ slack: buildSlackTool(opts),
8961
9511
  schedule: buildScheduleTool(opts),
8962
9512
  webhook: buildWebhookTool(opts)
8963
9513
  };