sparkecoder 0.1.121 → 0.1.123

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 (109) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +240 -26
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +588 -162
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -2
  7. package/dist/{index-DczYH89U.d.ts → index-Bcz0aCAR.d.ts} +104 -104
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.js +549 -123
  10. package/dist/index.js.map +1 -1
  11. package/dist/{schema-DxrKyetI.d.ts → schema-BWbWmfDQ.d.ts} +3 -3
  12. package/dist/{search-CVVfuBPZ.d.ts → search-DOzC4ojH.d.ts} +4 -4
  13. package/dist/server/index.js +549 -123
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/tools/index.d.ts +3 -3
  16. package/package.json +1 -1
  17. package/web/.next/BUILD_ID +1 -1
  18. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  19. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  20. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  21. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  22. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  37. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  81. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  89. package/web/.next/standalone/web/.next/server/app/settings.rsc +1 -1
  90. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +1 -1
  91. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  92. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  93. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  94. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  97. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  98. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  99. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  100. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  101. /package/web/.next/standalone/web/.next/static/{static/wP9z41wtqT4k-O6AlEXqw → MP4p8_EldjbZ69dONoEcM}/_buildManifest.js +0 -0
  102. /package/web/.next/standalone/web/.next/static/{static/wP9z41wtqT4k-O6AlEXqw → MP4p8_EldjbZ69dONoEcM}/_clientMiddlewareManifest.json +0 -0
  103. /package/web/.next/standalone/web/.next/static/{static/wP9z41wtqT4k-O6AlEXqw → MP4p8_EldjbZ69dONoEcM}/_ssgManifest.js +0 -0
  104. /package/web/.next/standalone/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → static/MP4p8_EldjbZ69dONoEcM}/_buildManifest.js +0 -0
  105. /package/web/.next/standalone/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → static/MP4p8_EldjbZ69dONoEcM}/_clientMiddlewareManifest.json +0 -0
  106. /package/web/.next/standalone/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → static/MP4p8_EldjbZ69dONoEcM}/_ssgManifest.js +0 -0
  107. /package/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → MP4p8_EldjbZ69dONoEcM}/_buildManifest.js +0 -0
  108. /package/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → MP4p8_EldjbZ69dONoEcM}/_clientMiddlewareManifest.json +0 -0
  109. /package/web/.next/static/{wP9z41wtqT4k-O6AlEXqw → MP4p8_EldjbZ69dONoEcM}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -2330,10 +2330,10 @@ async function resizeImageIfNeeded(buffer, mediaType) {
2330
2330
  const willConvertToJpeg = isPng && (needsShrink || buffer.length > 2 * 1024 * 1024);
2331
2331
  const outputMediaType = willConvertToJpeg || !isPng ? "image/jpeg" : "image/png";
2332
2332
  const ext = outputMediaType === "image/png" ? ".png" : ".jpg";
2333
- const cachePath = join3(cacheDir, key2 + ext);
2334
- if (existsSync3(cachePath)) {
2333
+ const cachePath2 = join3(cacheDir, key2 + ext);
2334
+ if (existsSync3(cachePath2)) {
2335
2335
  console.log(`[image-resize] Cache hit for ${width}x${height} image`);
2336
- return { buffer: readFileSync2(cachePath), mediaType: outputMediaType };
2336
+ return { buffer: readFileSync2(cachePath2), mediaType: outputMediaType };
2337
2337
  }
2338
2338
  let pipeline = sharp(buffer);
2339
2339
  if (needsResize) {
@@ -2358,7 +2358,7 @@ async function resizeImageIfNeeded(buffer, mediaType) {
2358
2358
  }
2359
2359
  finalMediaType = "image/jpeg";
2360
2360
  }
2361
- writeFileSync2(cachePath, result);
2361
+ writeFileSync2(cachePath2, result);
2362
2362
  const resultMeta = await sharp(result).metadata();
2363
2363
  console.log(
2364
2364
  `[image-resize] ${width}x${height} -> ${resultMeta.width}x${resultMeta.height} (${(buffer.length / 1024).toFixed(0)}KB -> ${(result.length / 1024).toFixed(0)}KB, ${finalMediaType})`
@@ -5415,7 +5415,7 @@ function isPathExcluded(relativePath, exclude) {
5415
5415
  }
5416
5416
  async function walkDirectory(dir, include, exclude, baseDir) {
5417
5417
  const { readdirSync: readdirSync4 } = await import("fs");
5418
- const { join: join18, relative: relative10 } = await import("path");
5418
+ const { join: join19, relative: relative10 } = await import("path");
5419
5419
  const files = [];
5420
5420
  function walk(currentDir) {
5421
5421
  let entries;
@@ -5425,7 +5425,7 @@ async function walkDirectory(dir, include, exclude, baseDir) {
5425
5425
  return;
5426
5426
  }
5427
5427
  for (const entry2 of entries) {
5428
- const fullPath = join18(currentDir, entry2.name);
5428
+ const fullPath = join19(currentDir, entry2.name);
5429
5429
  const relativePath = relative10(baseDir, fullPath);
5430
5430
  if (isPathExcluded(relativePath, exclude)) {
5431
5431
  continue;
@@ -8173,6 +8173,84 @@ function stripOrphanedToolResults(msg, removedIds) {
8173
8173
  if (parts.length === 0) return null;
8174
8174
  return { ...msg, content: parts };
8175
8175
  }
8176
+ function wrapToolsNeverThrow(tools) {
8177
+ if (!tools || typeof tools !== "object") return tools;
8178
+ const wrapped = {};
8179
+ for (const [name, t] of Object.entries(tools)) {
8180
+ if (!t || typeof t.execute !== "function") {
8181
+ wrapped[name] = t;
8182
+ continue;
8183
+ }
8184
+ const original = t.execute;
8185
+ wrapped[name] = {
8186
+ ...t,
8187
+ execute: async (input, opts) => {
8188
+ try {
8189
+ return await original.call(t, input, opts);
8190
+ } catch (err) {
8191
+ const message = err?.message ?? String(err);
8192
+ console.warn(`[tool:${name}] threw \u2014 converted to error result so the tool-call isn't orphaned:`, message);
8193
+ return {
8194
+ __error: true,
8195
+ tool: name,
8196
+ message,
8197
+ note: "Tool execution threw an exception. The agent should treat this as a failed tool call and decide how to recover."
8198
+ };
8199
+ }
8200
+ }
8201
+ };
8202
+ }
8203
+ return wrapped;
8204
+ }
8205
+ function ensureToolResultsFollowCalls(messages) {
8206
+ if (!Array.isArray(messages) || messages.length < 3) return messages;
8207
+ let mutated = false;
8208
+ const result = messages.slice();
8209
+ let i = 0;
8210
+ while (i < result.length) {
8211
+ const msg = result[i];
8212
+ if (msg?.role !== "assistant" || !Array.isArray(msg.content)) {
8213
+ i++;
8214
+ continue;
8215
+ }
8216
+ const callIds = /* @__PURE__ */ new Set();
8217
+ for (const part of msg.content) {
8218
+ if (part?.type === "tool-call" && typeof part.toolCallId === "string") {
8219
+ callIds.add(part.toolCallId);
8220
+ }
8221
+ }
8222
+ if (callIds.size === 0) {
8223
+ i++;
8224
+ continue;
8225
+ }
8226
+ let toolIdx = -1;
8227
+ for (let j = i + 1; j < result.length; j++) {
8228
+ const m = result[j];
8229
+ if (m?.role === "assistant" && Array.isArray(m.content) && m.content.some((p) => p?.type === "tool-call")) {
8230
+ break;
8231
+ }
8232
+ if (m?.role === "tool" && Array.isArray(m.content)) {
8233
+ const answersOne = m.content.some(
8234
+ (p) => p?.type === "tool-result" && typeof p.toolCallId === "string" && callIds.has(p.toolCallId)
8235
+ );
8236
+ if (answersOne) {
8237
+ toolIdx = j;
8238
+ break;
8239
+ }
8240
+ }
8241
+ }
8242
+ if (toolIdx > i + 1) {
8243
+ const [toolMsg] = result.splice(toolIdx, 1);
8244
+ result.splice(i + 1, 0, toolMsg);
8245
+ mutated = true;
8246
+ console.warn(
8247
+ `[tool-repair] Reordered tool-result message from index ${toolIdx} to ${i + 1} to immediately follow assistant tool-call(s) (${[...callIds].join(", ")}). ${toolIdx - i - 1} message(s) were wedged between them.`
8248
+ );
8249
+ }
8250
+ i++;
8251
+ }
8252
+ return mutated ? result : messages;
8253
+ }
8176
8254
  function repairToolPairing(messages) {
8177
8255
  const toolCallIds = /* @__PURE__ */ new Set();
8178
8256
  const toolResultIds = /* @__PURE__ */ new Set();
@@ -8292,6 +8370,7 @@ ${summaryContent}`
8292
8370
  ];
8293
8371
  }
8294
8372
  messages = repairToolPairing(messages);
8373
+ messages = ensureToolResultsFollowCalls(messages);
8295
8374
  messages = ensureEndsWithUserOrTool(messages);
8296
8375
  return messages;
8297
8376
  }
@@ -8486,7 +8565,7 @@ ${summaryContent}`
8486
8565
  }
8487
8566
  }
8488
8567
  async addResponseMessages(messages) {
8489
- const safe = repairToolPairing(messages);
8568
+ const safe = ensureToolResultsFollowCalls(repairToolPairing(messages));
8490
8569
  await messageQueries.addMany(this.sessionId, safe);
8491
8570
  try {
8492
8571
  const { appendTurn: appendTurn2, flattenContent: flattenContent2 } = await Promise.resolve().then(() => (init_conversation_archive(), conversation_archive_exports));
@@ -8538,6 +8617,120 @@ var init_web = __esm({
8538
8617
  }
8539
8618
  });
8540
8619
 
8620
+ // src/integrations/slack/persistence.ts
8621
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync3, renameSync } from "fs";
8622
+ import { join as join9, dirname as dirname6 } from "path";
8623
+ function cachePath() {
8624
+ return join9(ensureAppDataDirectory(), FILENAME);
8625
+ }
8626
+ function load() {
8627
+ if (loaded) return;
8628
+ loaded = true;
8629
+ const path = cachePath();
8630
+ if (!existsSync16(path)) return;
8631
+ try {
8632
+ const raw = readFileSync7(path, "utf-8");
8633
+ const parsed = JSON.parse(raw);
8634
+ if (parsed?.version !== FILE_VERSION) return;
8635
+ const now = Date.now();
8636
+ for (const [id, e] of Object.entries(parsed.users || {})) {
8637
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
8638
+ userMap.set(id, { name: e.name ?? null, expiresAt: e.expiresAt });
8639
+ }
8640
+ }
8641
+ for (const [key2, e] of Object.entries(parsed.threads || {})) {
8642
+ if (e && typeof e.expiresAt === "number" && e.expiresAt > now) {
8643
+ threadMap.set(key2, { owned: !!e.owned, expiresAt: e.expiresAt });
8644
+ }
8645
+ }
8646
+ } catch (err) {
8647
+ console.warn(`[slack] could not load ${FILENAME}:`, err?.message || err);
8648
+ }
8649
+ }
8650
+ function evictIfOversized(map, max) {
8651
+ if (map.size <= max) return;
8652
+ const sorted = [...map.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt);
8653
+ const toRemove = map.size - max;
8654
+ for (let i = 0; i < toRemove; i++) map.delete(sorted[i][0]);
8655
+ }
8656
+ function scheduleSave() {
8657
+ dirty = true;
8658
+ if (saveTimer) return;
8659
+ saveTimer = setTimeout(() => {
8660
+ saveTimer = null;
8661
+ if (dirty) saveSync();
8662
+ }, SAVE_DEBOUNCE_MS);
8663
+ saveTimer.unref?.();
8664
+ }
8665
+ function saveSync() {
8666
+ dirty = false;
8667
+ try {
8668
+ const path = cachePath();
8669
+ const dir = dirname6(path);
8670
+ if (!existsSync16(dir)) mkdirSync6(dir, { recursive: true });
8671
+ const payload = {
8672
+ version: FILE_VERSION,
8673
+ users: Object.fromEntries(userMap),
8674
+ threads: Object.fromEntries(threadMap)
8675
+ };
8676
+ const tmp = `${path}.tmp`;
8677
+ writeFileSync3(tmp, JSON.stringify(payload), "utf-8");
8678
+ renameSync(tmp, path);
8679
+ } catch (err) {
8680
+ console.warn(`[slack] could not persist ${FILENAME}:`, err?.message || err);
8681
+ }
8682
+ }
8683
+ function hookExit() {
8684
+ if (exitHooked) return;
8685
+ exitHooked = true;
8686
+ const flush2 = () => {
8687
+ if (dirty) saveSync();
8688
+ };
8689
+ process.once("beforeExit", flush2);
8690
+ process.once("SIGINT", flush2);
8691
+ process.once("SIGTERM", flush2);
8692
+ }
8693
+ function getCachedUserName(userId) {
8694
+ load();
8695
+ return userMap.get(userId);
8696
+ }
8697
+ function setCachedUserName(userId, entry2) {
8698
+ load();
8699
+ hookExit();
8700
+ userMap.set(userId, entry2);
8701
+ evictIfOversized(userMap, MAX_USERS);
8702
+ scheduleSave();
8703
+ }
8704
+ function getCachedThreadOwnership(key2) {
8705
+ load();
8706
+ return threadMap.get(key2);
8707
+ }
8708
+ function setCachedThreadOwnership(key2, entry2) {
8709
+ load();
8710
+ hookExit();
8711
+ threadMap.set(key2, entry2);
8712
+ evictIfOversized(threadMap, MAX_THREADS);
8713
+ scheduleSave();
8714
+ }
8715
+ var FILENAME, FILE_VERSION, SAVE_DEBOUNCE_MS, MAX_USERS, MAX_THREADS, loaded, userMap, threadMap, dirty, saveTimer, exitHooked;
8716
+ var init_persistence = __esm({
8717
+ "src/integrations/slack/persistence.ts"() {
8718
+ "use strict";
8719
+ init_config();
8720
+ FILENAME = "slack-cache.json";
8721
+ FILE_VERSION = 1;
8722
+ SAVE_DEBOUNCE_MS = 500;
8723
+ MAX_USERS = 5e3;
8724
+ MAX_THREADS = 5e3;
8725
+ loaded = false;
8726
+ userMap = /* @__PURE__ */ new Map();
8727
+ threadMap = /* @__PURE__ */ new Map();
8728
+ dirty = false;
8729
+ saveTimer = null;
8730
+ exitHooked = false;
8731
+ }
8732
+ });
8733
+
8541
8734
  // src/integrations/slack/client.ts
8542
8735
  function readSlackConfig() {
8543
8736
  try {
@@ -8660,13 +8853,13 @@ async function fetchSlackUserName(userId) {
8660
8853
  async function resolveSlackUserName(userId) {
8661
8854
  if (!userId) return null;
8662
8855
  const now = Date.now();
8663
- const hit = userNameCache.get(userId);
8856
+ const hit = getCachedUserName(userId);
8664
8857
  if (hit && hit.expiresAt > now) return hit.name;
8665
8858
  const inflight = userInflight.get(userId);
8666
8859
  if (inflight) return inflight;
8667
8860
  const p = (async () => {
8668
8861
  const name = await fetchSlackUserName(userId);
8669
- userNameCache.set(userId, {
8862
+ setCachedUserName(userId, {
8670
8863
  name,
8671
8864
  expiresAt: now + (name ? USER_TTL_MS : USER_FAIL_TTL_MS)
8672
8865
  });
@@ -8688,11 +8881,63 @@ async function normalizeSlackMentions(text) {
8688
8881
  }
8689
8882
  return text.replace(userMentionRe, (_full, id, label) => {
8690
8883
  if (label) return `${label} <@${id}>`;
8691
- const cached = userNameCache.get(id);
8884
+ const cached = getCachedUserName(id);
8692
8885
  const name = cached?.name;
8693
8886
  return name ? `${name} <@${id}>` : `<@${id}>`;
8694
8887
  });
8695
8888
  }
8889
+ function threadCacheKey(channel, threadTs) {
8890
+ return `${channel}\u241F${threadTs}`;
8891
+ }
8892
+ async function fetchBotParticipatedInThread(channel, threadTs) {
8893
+ const token = getSlackBotToken();
8894
+ if (!token) return false;
8895
+ const self = await ensureSlackSelfIdentity();
8896
+ if (!self) return false;
8897
+ try {
8898
+ const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${encodeURIComponent(threadTs)}&limit=200`;
8899
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
8900
+ const data = await res.json().catch(() => ({}));
8901
+ if (!data?.ok) {
8902
+ console.warn(`[slack] conversations.replies(${channel}/${threadTs}) failed: ${data?.error || `HTTP ${res.status}`}`);
8903
+ return false;
8904
+ }
8905
+ const messages = Array.isArray(data.messages) ? data.messages : [];
8906
+ return messages.some(
8907
+ (m) => self.botId && m.bot_id === self.botId || self.botUserId && m.user === self.botUserId
8908
+ );
8909
+ } catch (err) {
8910
+ console.warn(`[slack] conversations.replies error:`, err?.message || err);
8911
+ return false;
8912
+ }
8913
+ }
8914
+ async function botParticipatedInThread(channel, threadTs) {
8915
+ if (!channel || !threadTs) return false;
8916
+ const key2 = threadCacheKey(channel, threadTs);
8917
+ const now = Date.now();
8918
+ const hit = getCachedThreadOwnership(key2);
8919
+ if (hit && hit.expiresAt > now) return hit.owned;
8920
+ const inflight = threadOwnedInflight.get(key2);
8921
+ if (inflight) return inflight;
8922
+ const p = (async () => {
8923
+ const owned = await fetchBotParticipatedInThread(channel, threadTs);
8924
+ setCachedThreadOwnership(key2, {
8925
+ owned,
8926
+ expiresAt: now + (owned ? THREAD_OWNED_TTL_MS : THREAD_NEG_TTL_MS)
8927
+ });
8928
+ threadOwnedInflight.delete(key2);
8929
+ return owned;
8930
+ })();
8931
+ threadOwnedInflight.set(key2, p);
8932
+ return p;
8933
+ }
8934
+ function noteBotPostedInThread(channel, threadTs) {
8935
+ if (!channel || !threadTs) return;
8936
+ setCachedThreadOwnership(threadCacheKey(channel, threadTs), {
8937
+ owned: true,
8938
+ expiresAt: Date.now() + THREAD_OWNED_TTL_MS
8939
+ });
8940
+ }
8696
8941
  function getSlackDeniedReplyPolicy() {
8697
8942
  try {
8698
8943
  const cfg = getConfig();
@@ -8705,17 +8950,20 @@ function getSlackDeniedReplyPolicy() {
8705
8950
  return { enabled: true, template: DEFAULT_DENIED_TEMPLATE };
8706
8951
  }
8707
8952
  }
8708
- var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userNameCache, userInflight, DEFAULT_DENIED_TEMPLATE;
8953
+ var cachedSelf, selfInflight, USER_TTL_MS, USER_FAIL_TTL_MS, userInflight, THREAD_OWNED_TTL_MS, THREAD_NEG_TTL_MS, threadOwnedInflight, DEFAULT_DENIED_TEMPLATE;
8709
8954
  var init_client3 = __esm({
8710
8955
  "src/integrations/slack/client.ts"() {
8711
8956
  "use strict";
8712
8957
  init_config();
8958
+ init_persistence();
8713
8959
  cachedSelf = null;
8714
8960
  selfInflight = null;
8715
8961
  USER_TTL_MS = 60 * 60 * 1e3;
8716
8962
  USER_FAIL_TTL_MS = 5 * 60 * 1e3;
8717
- userNameCache = /* @__PURE__ */ new Map();
8718
8963
  userInflight = /* @__PURE__ */ new Map();
8964
+ THREAD_OWNED_TTL_MS = 60 * 60 * 1e3;
8965
+ THREAD_NEG_TTL_MS = 5 * 60 * 1e3;
8966
+ threadOwnedInflight = /* @__PURE__ */ new Map();
8719
8967
  DEFAULT_DENIED_TEMPLATE = "Sorry, you don't have permission to use this bot. (Contact the bot owner if you think this is a mistake.)";
8720
8968
  }
8721
8969
  });
@@ -8815,6 +9063,7 @@ var init_slack = __esm({
8815
9063
  if (!result.ok) throw new Error(`slack post failed: ${result.error}`);
8816
9064
  if (r.slackChannel && r.threadTs) {
8817
9065
  markThreadOwned(r.slackChannel, r.threadTs);
9066
+ noteBotPostedInThread(r.slackChannel, r.threadTs);
8818
9067
  }
8819
9068
  },
8820
9069
  displayLabel(ref) {
@@ -9472,8 +9721,8 @@ var init_orchestrator_actions = __esm({
9472
9721
 
9473
9722
  // src/integrations/mcp/store.ts
9474
9723
  import { nanoid as nanoid6 } from "nanoid";
9475
- import { existsSync as existsSync16, readFileSync as readFileSync7 } from "fs";
9476
- import { resolve as resolve10, join as join9 } from "path";
9724
+ import { existsSync as existsSync17, readFileSync as readFileSync8 } from "fs";
9725
+ import { resolve as resolve10, join as join10 } from "path";
9477
9726
  function readServers() {
9478
9727
  try {
9479
9728
  const cfg = getConfig();
@@ -9485,12 +9734,12 @@ function readServers() {
9485
9734
  function refreshMcpServersFromDisk() {
9486
9735
  const candidates = [
9487
9736
  resolve10(process.cwd(), "sparkecoder.config.json"),
9488
- join9(ensureAppDataDirectory(), "sparkecoder.config.json")
9737
+ join10(ensureAppDataDirectory(), "sparkecoder.config.json")
9489
9738
  ];
9490
9739
  for (const path of candidates) {
9491
- if (!existsSync16(path)) continue;
9740
+ if (!existsSync17(path)) continue;
9492
9741
  try {
9493
- const raw = JSON.parse(readFileSync7(path, "utf-8"));
9742
+ const raw = JSON.parse(readFileSync8(path, "utf-8"));
9494
9743
  const servers2 = Array.isArray(raw?.mcp?.servers) ? raw.mcp.servers : [];
9495
9744
  setMcpServers(servers2);
9496
9745
  return servers2;
@@ -10103,7 +10352,7 @@ __export(recorder_exports, {
10103
10352
  import { exec as exec5 } from "child_process";
10104
10353
  import { promisify as promisify5 } from "util";
10105
10354
  import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
10106
- import { join as join10 } from "path";
10355
+ import { join as join11 } from "path";
10107
10356
  import { tmpdir } from "os";
10108
10357
  import { nanoid as nanoid7 } from "nanoid";
10109
10358
  async function checkFfmpeg() {
@@ -10160,21 +10409,21 @@ var init_recorder = __esm({
10160
10409
  */
10161
10410
  async encode() {
10162
10411
  if (this.frames.length === 0) return null;
10163
- const workDir = join10(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
10412
+ const workDir = join11(tmpdir(), `sparkecoder-recording-${nanoid7(8)}`);
10164
10413
  await mkdir4(workDir, { recursive: true });
10165
10414
  try {
10166
10415
  for (let i = 0; i < this.frames.length; i++) {
10167
- const framePath = join10(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
10416
+ const framePath = join11(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
10168
10417
  await writeFile5(framePath, this.frames[i].data);
10169
10418
  }
10170
10419
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
10171
10420
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
10172
10421
  const clampedFps = Math.max(1, Math.min(fps, 30));
10173
- const outputPath = join10(workDir, `recording_${this.sessionId}.mp4`);
10422
+ const outputPath = join11(workDir, `recording_${this.sessionId}.mp4`);
10174
10423
  const hasFfmpeg = await checkFfmpeg();
10175
10424
  if (hasFfmpeg) {
10176
10425
  await execAsync5(
10177
- `ffmpeg -y -framerate ${clampedFps} -i "${join10(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
10426
+ `ffmpeg -y -framerate ${clampedFps} -i "${join11(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
10178
10427
  { timeout: 12e4 }
10179
10428
  );
10180
10429
  } else {
@@ -10186,7 +10435,7 @@ var init_recorder = __esm({
10186
10435
  const files = await readdir5(workDir);
10187
10436
  for (const f of files) {
10188
10437
  if (f.startsWith("frame_")) {
10189
- await unlink2(join10(workDir, f)).catch(() => {
10438
+ await unlink2(join11(workDir, f)).catch(() => {
10190
10439
  });
10191
10440
  }
10192
10441
  }
@@ -10427,7 +10676,7 @@ ${prompt}` });
10427
10676
  const config = getConfig();
10428
10677
  const userContent = this.buildUserMessageContent(options.prompt, options.attachments);
10429
10678
  if (!options.skipSaveUserMessage) {
10430
- this.context.addUserMessage(userContent);
10679
+ await this.context.addUserMessage(userContent);
10431
10680
  }
10432
10681
  await sessionQueries.updateStatus(this.session.id, "active");
10433
10682
  let systemPrompt = await buildSystemPrompt({
@@ -10453,7 +10702,8 @@ ${personality.trim()}`;
10453
10702
  }
10454
10703
  const messages = await this.context.getMessages();
10455
10704
  const tools = options.onToolProgress ? await this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
10456
- const wrappedTools = this.wrapToolsWithApproval(options, tools);
10705
+ const approvalWrapped = this.wrapToolsWithApproval(options, tools);
10706
+ const wrappedTools = wrapToolsNeverThrow(approvalWrapped);
10457
10707
  const useAnthropic = isAnthropicModel(this.session.model);
10458
10708
  const stream = streamText2({
10459
10709
  model: resolveModel(this.session.model),
@@ -10467,6 +10717,18 @@ ${personality.trim()}`;
10467
10717
  providerOptions: useAnthropic ? {
10468
10718
  anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
10469
10719
  } : void 0,
10720
+ // Run repairToolPairing before EVERY step's model call, not just the
10721
+ // first one. The AI SDK's multi-step loop can otherwise feed the model
10722
+ // a prompt containing an orphan tool-call (e.g. when a previous step's
10723
+ // tool result was lost, dropped during compaction, or the stream was
10724
+ // aborted mid-tool). Repairing in `prepareStep` guarantees no orphan
10725
+ // ever reaches the model and we never hit AI_MissingToolResultsError.
10726
+ prepareStep: async ({ messages: stepMessages }) => {
10727
+ const paired = repairToolPairing(stepMessages);
10728
+ const ordered = ensureToolResultsFollowCalls(paired);
10729
+ if (ordered === stepMessages) return {};
10730
+ return { messages: ordered };
10731
+ },
10470
10732
  onStepFinish: async (step) => {
10471
10733
  options.onStepFinish?.(step);
10472
10734
  },
@@ -10478,7 +10740,7 @@ ${personality.trim()}`;
10478
10740
  const result = await stream;
10479
10741
  const response = await result.response;
10480
10742
  const responseMessages = response.messages;
10481
- this.context.addResponseMessages(responseMessages);
10743
+ await this.context.addResponseMessages(responseMessages);
10482
10744
  };
10483
10745
  return {
10484
10746
  sessionId: this.session.id,
@@ -10492,7 +10754,7 @@ ${personality.trim()}`;
10492
10754
  */
10493
10755
  async run(options) {
10494
10756
  const config = getConfig();
10495
- this.context.addUserMessage(options.prompt);
10757
+ await this.context.addUserMessage(options.prompt);
10496
10758
  const systemPrompt = await buildSystemPrompt({
10497
10759
  workingDirectory: this.session.workingDirectory,
10498
10760
  skillsDirectories: config.resolvedSkillsDirectories,
@@ -10502,7 +10764,7 @@ ${personality.trim()}`;
10502
10764
  });
10503
10765
  const messages = await this.context.getMessages();
10504
10766
  const tools = options.onToolProgress ? await this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
10505
- const wrappedTools = this.wrapToolsWithApproval(options, tools);
10767
+ const wrappedTools = wrapToolsNeverThrow(this.wrapToolsWithApproval(options, tools));
10506
10768
  const useAnthropic = isAnthropicModel(this.session.model);
10507
10769
  const result = await generateText3({
10508
10770
  model: resolveModel(this.session.model),
@@ -10513,10 +10775,17 @@ ${personality.trim()}`;
10513
10775
  // Enable extended thinking/reasoning for models that support it
10514
10776
  providerOptions: useAnthropic ? {
10515
10777
  anthropic: getAnthropicProviderOptions(this.session.model)
10516
- } : void 0
10778
+ } : void 0,
10779
+ // Repair tool pairing before every step (see `stream()` for full rationale).
10780
+ prepareStep: async ({ messages: stepMessages }) => {
10781
+ const paired = repairToolPairing(stepMessages);
10782
+ const ordered = ensureToolResultsFollowCalls(paired);
10783
+ if (ordered === stepMessages) return {};
10784
+ return { messages: ordered };
10785
+ }
10517
10786
  });
10518
10787
  const responseMessages = result.response.messages;
10519
- this.context.addResponseMessages(responseMessages);
10788
+ await this.context.addResponseMessages(responseMessages);
10520
10789
  return {
10521
10790
  text: result.text,
10522
10791
  steps: result.steps
@@ -10690,12 +10959,20 @@ ${p.text}` : p.text;
10690
10959
  model: resolveModel(this.session.model),
10691
10960
  system: systemPrompt,
10692
10961
  messages,
10693
- tools: taskTools,
10962
+ tools: wrapToolsNeverThrow(taskTools),
10694
10963
  stopWhen: stepCountIs2(500),
10695
10964
  abortSignal: combinedAbort,
10696
10965
  providerOptions: useAnthropic ? {
10697
10966
  anthropic: getAnthropicProviderOptions(this.session.model, { toolStreaming: true })
10698
10967
  } : void 0,
10968
+ // See the matching note in `stream()` — repair tool pairing before
10969
+ // every step so we never feed the model an orphan tool-call.
10970
+ prepareStep: async ({ messages: stepMessages }) => {
10971
+ const paired = repairToolPairing(stepMessages);
10972
+ const ordered = ensureToolResultsFollowCalls(paired);
10973
+ if (ordered === stepMessages) return {};
10974
+ return { messages: ordered };
10975
+ },
10699
10976
  onStepFinish: async (step) => {
10700
10977
  options.onStepFinish?.(step);
10701
10978
  fireWebhook("task.step_finished", { iteration, text: step.text });
@@ -10929,11 +11206,11 @@ ${p.text}` : p.text;
10929
11206
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
10930
11207
  if (!isRemoteConfigured2()) return [];
10931
11208
  const { readFile: readFile13 } = await import("fs/promises");
10932
- const { join: join18, basename: basename7 } = await import("path");
11209
+ const { join: join19, basename: basename7 } = await import("path");
10933
11210
  const urls = [];
10934
11211
  for (const filePath of filePaths) {
10935
11212
  try {
10936
- const fullPath = filePath.startsWith("/") ? filePath : join18(this.session.workingDirectory, filePath);
11213
+ const fullPath = filePath.startsWith("/") ? filePath : join19(this.session.workingDirectory, filePath);
10937
11214
  const fileName = basename7(fullPath);
10938
11215
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
10939
11216
  const mimeMap = {
@@ -11138,19 +11415,19 @@ var init_session_lock = __esm({
11138
11415
  });
11139
11416
 
11140
11417
  // src/orchestrator/webhook-events.ts
11141
- import { existsSync as existsSync17, readFileSync as readFileSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync6 } from "fs";
11142
- import { dirname as dirname6, join as join11 } from "path";
11418
+ import { existsSync as existsSync18, readFileSync as readFileSync9, appendFileSync as appendFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
11419
+ import { dirname as dirname7, join as join12 } from "path";
11143
11420
  import { nanoid as nanoid9 } from "nanoid";
11144
11421
  function logFilePath() {
11145
- return join11(getAppDataDirectory(), "webhook-events.jsonl");
11422
+ return join12(getAppDataDirectory(), "webhook-events.jsonl");
11146
11423
  }
11147
11424
  function ensureLoaded() {
11148
11425
  if (cache !== null) return cache;
11149
11426
  cache = [];
11150
11427
  try {
11151
11428
  const p = logFilePath();
11152
- if (!existsSync17(p)) return cache;
11153
- const lines = readFileSync8(p, "utf-8").split("\n").filter(Boolean);
11429
+ if (!existsSync18(p)) return cache;
11430
+ const lines = readFileSync9(p, "utf-8").split("\n").filter(Boolean);
11154
11431
  for (const line of lines) {
11155
11432
  try {
11156
11433
  cache.push(JSON.parse(line));
@@ -11160,7 +11437,7 @@ function ensureLoaded() {
11160
11437
  if (cache.length > MAX_EVENTS) {
11161
11438
  cache = cache.slice(-MAX_EVENTS);
11162
11439
  try {
11163
- writeFileSync3(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
11440
+ writeFileSync4(p, cache.map((e) => JSON.stringify(e)).join("\n") + "\n");
11164
11441
  } catch {
11165
11442
  }
11166
11443
  }
@@ -11174,7 +11451,7 @@ function appendEvent(ev) {
11174
11451
  if (list.length > MAX_EVENTS) list.shift();
11175
11452
  try {
11176
11453
  const p = logFilePath();
11177
- mkdirSync6(dirname6(p), { recursive: true });
11454
+ mkdirSync7(dirname7(p), { recursive: true });
11178
11455
  appendFileSync3(p, JSON.stringify(ev) + "\n");
11179
11456
  } catch {
11180
11457
  }
@@ -11205,8 +11482,8 @@ function updateEvent(id, patch) {
11205
11482
  list[i] = { ...list[i], ...patch };
11206
11483
  try {
11207
11484
  const p = logFilePath();
11208
- mkdirSync6(dirname6(p), { recursive: true });
11209
- writeFileSync3(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
11485
+ mkdirSync7(dirname7(p), { recursive: true });
11486
+ writeFileSync4(p, list.map((e) => JSON.stringify(e)).join("\n") + "\n");
11210
11487
  } catch {
11211
11488
  }
11212
11489
  }
@@ -11238,7 +11515,7 @@ function listEvents(filter = {}) {
11238
11515
  function clearAllEvents() {
11239
11516
  cache = [];
11240
11517
  try {
11241
- writeFileSync3(logFilePath(), "");
11518
+ writeFileSync4(logFilePath(), "");
11242
11519
  } catch {
11243
11520
  }
11244
11521
  }
@@ -11728,7 +12005,7 @@ import chalk from "chalk";
11728
12005
  import ora from "ora";
11729
12006
  import "dotenv/config";
11730
12007
  import { createInterface } from "readline";
11731
- import { dirname as dirname10 } from "path";
12008
+ import { dirname as dirname11 } from "path";
11732
12009
  import { fileURLToPath as fileURLToPath5 } from "url";
11733
12010
 
11734
12011
  // src/server/index.ts
@@ -11737,8 +12014,8 @@ import { Hono as Hono10 } from "hono";
11737
12014
  import { serve } from "@hono/node-server";
11738
12015
  import { cors } from "hono/cors";
11739
12016
  import { logger } from "hono/logger";
11740
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
11741
- import { resolve as resolve12, dirname as dirname9, join as join16 } from "path";
12017
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "fs";
12018
+ import { resolve as resolve12, dirname as dirname10, join as join17 } from "path";
11742
12019
  import { spawn as spawn2 } from "child_process";
11743
12020
  import { createServer as createNetServer } from "net";
11744
12021
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -11752,9 +12029,9 @@ init_checkpoints();
11752
12029
  import { Hono } from "hono";
11753
12030
  import { zValidator } from "@hono/zod-validator";
11754
12031
  import { z as z16 } from "zod";
11755
- import { existsSync as existsSync18, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
12032
+ import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
11756
12033
  import { readdir as readdir6 } from "fs/promises";
11757
- import { join as join12, basename as basename5, extname as extname8, relative as relative9 } from "path";
12034
+ import { join as join13, basename as basename5, extname as extname8, relative as relative9 } from "path";
11758
12035
  import { nanoid as nanoid10 } from "nanoid";
11759
12036
 
11760
12037
  // src/tasks/agent-status.ts
@@ -12392,12 +12669,12 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
12392
12669
  });
12393
12670
  function getAttachmentsDir(sessionId) {
12394
12671
  const appDataDir = getAppDataDirectory();
12395
- return join12(appDataDir, "attachments", sessionId);
12672
+ return join13(appDataDir, "attachments", sessionId);
12396
12673
  }
12397
12674
  function ensureAttachmentsDir(sessionId) {
12398
12675
  const dir = getAttachmentsDir(sessionId);
12399
- if (!existsSync18(dir)) {
12400
- mkdirSync7(dir, { recursive: true });
12676
+ if (!existsSync19(dir)) {
12677
+ mkdirSync8(dir, { recursive: true });
12401
12678
  }
12402
12679
  return dir;
12403
12680
  }
@@ -12408,12 +12685,12 @@ sessions2.get("/:id/attachments", async (c) => {
12408
12685
  return c.json({ error: "Session not found" }, 404);
12409
12686
  }
12410
12687
  const dir = getAttachmentsDir(sessionId);
12411
- if (!existsSync18(dir)) {
12688
+ if (!existsSync19(dir)) {
12412
12689
  return c.json({ sessionId, attachments: [], count: 0 });
12413
12690
  }
12414
12691
  const files = readdirSync3(dir);
12415
12692
  const attachments = files.map((filename) => {
12416
- const filePath = join12(dir, filename);
12693
+ const filePath = join13(dir, filename);
12417
12694
  const stats = statSync2(filePath);
12418
12695
  return {
12419
12696
  id: filename.split("_")[0],
@@ -12448,9 +12725,9 @@ sessions2.post("/:id/attachments", async (c) => {
12448
12725
  const id = nanoid10(10);
12449
12726
  const ext = extname8(file.name) || "";
12450
12727
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
12451
- const filePath = join12(dir, safeFilename);
12728
+ const filePath = join13(dir, safeFilename);
12452
12729
  const arrayBuffer = await file.arrayBuffer();
12453
- writeFileSync4(filePath, Buffer.from(arrayBuffer));
12730
+ writeFileSync5(filePath, Buffer.from(arrayBuffer));
12454
12731
  return c.json({
12455
12732
  id,
12456
12733
  filename: file.name,
@@ -12474,13 +12751,13 @@ sessions2.post("/:id/attachments", async (c) => {
12474
12751
  const id = nanoid10(10);
12475
12752
  const ext = extname8(body.filename) || "";
12476
12753
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
12477
- const filePath = join12(dir, safeFilename);
12754
+ const filePath = join13(dir, safeFilename);
12478
12755
  let base64Data = body.data;
12479
12756
  if (base64Data.includes(",")) {
12480
12757
  base64Data = base64Data.split(",")[1];
12481
12758
  }
12482
12759
  const buffer = Buffer.from(base64Data, "base64");
12483
- writeFileSync4(filePath, buffer);
12760
+ writeFileSync5(filePath, buffer);
12484
12761
  return c.json({
12485
12762
  id,
12486
12763
  filename: body.filename,
@@ -12503,7 +12780,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
12503
12780
  return c.json({ error: "Session not found" }, 404);
12504
12781
  }
12505
12782
  const dir = getAttachmentsDir(sessionId);
12506
- if (!existsSync18(dir)) {
12783
+ if (!existsSync19(dir)) {
12507
12784
  return c.json({ error: "Attachment not found" }, 404);
12508
12785
  }
12509
12786
  const files = readdirSync3(dir);
@@ -12511,7 +12788,7 @@ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
12511
12788
  if (!file) {
12512
12789
  return c.json({ error: "Attachment not found" }, 404);
12513
12790
  }
12514
- const filePath = join12(dir, file);
12791
+ const filePath = join13(dir, file);
12515
12792
  unlinkSync2(filePath);
12516
12793
  return c.json({ success: true, id: attachmentId });
12517
12794
  });
@@ -12594,7 +12871,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
12594
12871
  const entries = await readdir6(currentDir, { withFileTypes: true });
12595
12872
  for (const entry2 of entries) {
12596
12873
  if (results.length >= limit * 2) break;
12597
- const fullPath = join12(currentDir, entry2.name);
12874
+ const fullPath = join13(currentDir, entry2.name);
12598
12875
  const relativePath = relative9(baseDir, fullPath);
12599
12876
  if (entry2.isDirectory() && IGNORED_DIRECTORIES.has(entry2.name)) {
12600
12877
  continue;
@@ -12642,7 +12919,7 @@ sessions2.get(
12642
12919
  return c.json({ error: "Session not found" }, 404);
12643
12920
  }
12644
12921
  const workingDirectory = session.workingDirectory;
12645
- if (!existsSync18(workingDirectory)) {
12922
+ if (!existsSync19(workingDirectory)) {
12646
12923
  return c.json({
12647
12924
  sessionId,
12648
12925
  workingDirectory,
@@ -12750,14 +13027,148 @@ sessions2.get("/:id/browser-recording", async (c) => {
12750
13027
 
12751
13028
  // src/server/routes/agents.ts
12752
13029
  init_db();
12753
- init_agent();
12754
- init_session_lock();
12755
- init_config();
12756
13030
  import { Hono as Hono2 } from "hono";
12757
13031
  import { zValidator as zValidator2 } from "@hono/zod-validator";
12758
13032
  import { z as z17 } from "zod";
12759
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
12760
- import { join as join13 } from "path";
13033
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
13034
+ import { join as join14 } from "path";
13035
+
13036
+ // src/agent/missing-tool-recovery.ts
13037
+ init_db();
13038
+ function extractMissingToolCallIds(error) {
13039
+ if (!error) return [];
13040
+ const e = error;
13041
+ if (Array.isArray(e.toolCallIds) && e.toolCallIds.every((x) => typeof x === "string")) {
13042
+ return e.toolCallIds;
13043
+ }
13044
+ const msg = typeof e.message === "string" ? e.message : "";
13045
+ const ids = /* @__PURE__ */ new Set();
13046
+ for (const m of msg.matchAll(/(?:tool call|tool calls)\s+([a-zA-Z0-9_\-, ]+?)(?:\.|$)/g)) {
13047
+ for (const id of m[1].split(/\s*,\s*/)) {
13048
+ const trimmed = id.trim();
13049
+ if (trimmed) ids.add(trimmed);
13050
+ }
13051
+ }
13052
+ return [...ids];
13053
+ }
13054
+ function isMissingToolResultsError(error) {
13055
+ if (!error) return false;
13056
+ const e = error;
13057
+ if (e.name === "AI_MissingToolResultsError") return true;
13058
+ if (Array.isArray(e.toolCallIds)) return true;
13059
+ return typeof e.message === "string" && /tool result.*is missing for tool call/i.test(e.message);
13060
+ }
13061
+ async function recoverFromMissingToolResults(sessionId, error) {
13062
+ const toolCallIds = extractMissingToolCallIds(error);
13063
+ if (toolCallIds.length === 0) return null;
13064
+ let history = [];
13065
+ try {
13066
+ history = await messageQueries.getModelMessages(sessionId);
13067
+ } catch (err) {
13068
+ console.warn("[missing-tool-recovery] could not load messages:", err?.message || err);
13069
+ }
13070
+ const toolInfoByCallId = /* @__PURE__ */ new Map();
13071
+ const resultMessageIndexByCallId = /* @__PURE__ */ new Map();
13072
+ const existingResultIds = /* @__PURE__ */ new Set();
13073
+ for (let idx = 0; idx < history.length; idx++) {
13074
+ const msg = history[idx];
13075
+ if (!Array.isArray(msg.content)) continue;
13076
+ for (const part of msg.content) {
13077
+ if (part?.type === "tool-call" && typeof part.toolCallId === "string") {
13078
+ if (typeof part.toolName === "string") {
13079
+ toolInfoByCallId.set(part.toolCallId, { toolName: part.toolName, callMessageIndex: idx });
13080
+ }
13081
+ }
13082
+ if (part?.type === "tool-result" && typeof part.toolCallId === "string") {
13083
+ existingResultIds.add(part.toolCallId);
13084
+ resultMessageIndexByCallId.set(part.toolCallId, idx);
13085
+ }
13086
+ }
13087
+ }
13088
+ const resolved = toolCallIds.map((id) => {
13089
+ const info = toolInfoByCallId.get(id);
13090
+ const resultIdx = resultMessageIndexByCallId.get(id);
13091
+ const wedgedRoles = info && typeof resultIdx === "number" && resultIdx > info.callMessageIndex + 1 ? history.slice(info.callMessageIndex + 1, resultIdx).map((m) => m.role) : void 0;
13092
+ return {
13093
+ toolCallId: id,
13094
+ toolName: info?.toolName ?? "unknown",
13095
+ foundInAssistantMessage: !!info,
13096
+ callMessageIndex: info?.callMessageIndex,
13097
+ resultMessageIndex: resultIdx,
13098
+ wedgedMessageRoles: wedgedRoles
13099
+ };
13100
+ });
13101
+ const stillOrphaned = toolCallIds.filter((id) => !existingResultIds.has(id));
13102
+ let syntheticToolMessageSaved = false;
13103
+ if (stillOrphaned.length > 0) {
13104
+ const syntheticParts = stillOrphaned.map((id) => ({
13105
+ type: "tool-result",
13106
+ toolCallId: id,
13107
+ toolName: toolInfoByCallId.get(id)?.toolName ?? "unknown",
13108
+ output: {
13109
+ type: "text",
13110
+ value: "[Auto-recovered: the original tool execution never produced a result (crash, timeout, or message stripped during context compaction). This synthetic placeholder lets the conversation continue.]"
13111
+ }
13112
+ }));
13113
+ try {
13114
+ await messageQueries.create(sessionId, {
13115
+ role: "tool",
13116
+ content: syntheticParts
13117
+ });
13118
+ syntheticToolMessageSaved = true;
13119
+ } catch (err) {
13120
+ console.error("[missing-tool-recovery] failed to persist synthetic tool message:", err?.message || err);
13121
+ }
13122
+ }
13123
+ const lastFewMessageRoles = history.slice(-6).map((m) => m.role);
13124
+ const first = resolved.find((r) => r.callMessageIndex !== void 0);
13125
+ let contextSnapshot;
13126
+ if (first?.callMessageIndex !== void 0) {
13127
+ const start = Math.max(0, first.callMessageIndex - 1);
13128
+ const end = Math.min(
13129
+ history.length,
13130
+ Math.max(first.callMessageIndex, first.resultMessageIndex ?? first.callMessageIndex) + 2
13131
+ );
13132
+ contextSnapshot = history.slice(start, end).map((m, offset) => ({
13133
+ index: start + offset,
13134
+ role: m.role,
13135
+ summary: summarizeContent(m.content)
13136
+ }));
13137
+ }
13138
+ const anyWedged = resolved.some((r) => r.wedgedMessageRoles && r.wedgedMessageRoles.length > 0);
13139
+ return {
13140
+ kind: "missing_tool_results",
13141
+ toolCallIds,
13142
+ resolved,
13143
+ syntheticToolMessageSaved,
13144
+ lastFewMessageRoles,
13145
+ contextSnapshot,
13146
+ hint: anyWedged ? "A non-tool message was wedged between the assistant tool-call and its tool-result (likely a Slack/system inbox event that arrived mid-turn). " + (syntheticToolMessageSaved ? "Synthetic tool-result was added as a fallback, but the proper fix (now in place) is the reordering pass that moves tool-results to immediately follow their tool-calls." : "The reordering pass should fix this on the next turn; if not, revert to the previous checkpoint.") : syntheticToolMessageSaved ? "Synthetic tool-result(s) saved. Re-send your message to continue." : "Could not auto-recover; please reset the session or revert to the previous checkpoint."
13147
+ };
13148
+ }
13149
+ function summarizeContent(content) {
13150
+ if (typeof content === "string") {
13151
+ return content.length > 160 ? content.slice(0, 160) + "\u2026" : content;
13152
+ }
13153
+ if (!Array.isArray(content)) return String(content);
13154
+ const parts = content.map((p) => {
13155
+ if (!p || typeof p !== "object") return String(p);
13156
+ if (p.type === "text") {
13157
+ const t = String(p.text ?? "");
13158
+ return `text(${t.length > 80 ? t.slice(0, 80) + "\u2026" : t})`;
13159
+ }
13160
+ if (p.type === "tool-call") return `tool-call(${p.toolName}:${p.toolCallId})`;
13161
+ if (p.type === "tool-result") return `tool-result(${p.toolName ?? "?"}:${p.toolCallId})`;
13162
+ if (p.type === "reasoning") return "reasoning";
13163
+ return p.type ?? "unknown";
13164
+ });
13165
+ return parts.join(", ");
13166
+ }
13167
+
13168
+ // src/server/routes/agents.ts
13169
+ init_agent();
13170
+ init_session_lock();
13171
+ init_config();
12761
13172
 
12762
13173
  // src/server/resumable-stream.ts
12763
13174
  import { createResumableStreamContext } from "resumable-stream/generic";
@@ -12964,12 +13375,12 @@ var rejectSchema = z17.object({
12964
13375
  var streamAbortControllers = /* @__PURE__ */ new Map();
12965
13376
  function getAttachmentsDirectory(sessionId) {
12966
13377
  const appDataDir = getAppDataDirectory();
12967
- return join13(appDataDir, "attachments", sessionId);
13378
+ return join14(appDataDir, "attachments", sessionId);
12968
13379
  }
12969
13380
  async function saveAttachmentToDisk(sessionId, attachment, index) {
12970
13381
  const attachmentsDir = getAttachmentsDirectory(sessionId);
12971
- if (!existsSync19(attachmentsDir)) {
12972
- mkdirSync8(attachmentsDir, { recursive: true });
13382
+ if (!existsSync20(attachmentsDir)) {
13383
+ mkdirSync9(attachmentsDir, { recursive: true });
12973
13384
  }
12974
13385
  let filename = attachment.filename;
12975
13386
  if (!filename) {
@@ -12987,8 +13398,8 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
12987
13398
  attachment.mediaType = resized.mediaType;
12988
13399
  attachment.data = buffer.toString("base64");
12989
13400
  }
12990
- const filePath = join13(attachmentsDir, filename);
12991
- writeFileSync5(filePath, buffer);
13401
+ const filePath = join14(attachmentsDir, filename);
13402
+ writeFileSync6(filePath, buffer);
12992
13403
  return filePath;
12993
13404
  }
12994
13405
  function stripDataUrlPrefix2(data) {
@@ -13325,7 +13736,20 @@ ${prompt}` });
13325
13736
  await writeSSE(JSON.stringify({ type: "abort" }));
13326
13737
  } else {
13327
13738
  console.error("Agent error:", error);
13328
- await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
13739
+ let debugPayload = void 0;
13740
+ if (isMissingToolResultsError(error)) {
13741
+ try {
13742
+ const recovery = await recoverFromMissingToolResults(sessionId, error);
13743
+ if (recovery) debugPayload = recovery;
13744
+ } catch (recErr) {
13745
+ console.error("[missing-tool-recovery] failed:", recErr?.message || recErr);
13746
+ }
13747
+ }
13748
+ await writeSSE(JSON.stringify({
13749
+ type: "error",
13750
+ errorText: error.message,
13751
+ ...debugPayload ? { debug: debugPayload } : {}
13752
+ }));
13329
13753
  try {
13330
13754
  await activeStreamQueries.markError(streamId);
13331
13755
  } catch {
@@ -13865,7 +14289,20 @@ agents.post(
13865
14289
  await writeSSE(JSON.stringify({ type: "abort" }));
13866
14290
  } else {
13867
14291
  console.error("Agent error:", error);
13868
- await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
14292
+ let debugPayload = void 0;
14293
+ if (isMissingToolResultsError(error)) {
14294
+ try {
14295
+ const recovery = await recoverFromMissingToolResults(session.id, error);
14296
+ if (recovery) debugPayload = recovery;
14297
+ } catch (recErr) {
14298
+ console.error("[missing-tool-recovery] failed:", recErr?.message || recErr);
14299
+ }
14300
+ }
14301
+ await writeSSE(JSON.stringify({
14302
+ type: "error",
14303
+ errorText: error.message,
14304
+ ...debugPayload ? { debug: debugPayload } : {}
14305
+ }));
13869
14306
  await activeStreamQueries.markError(streamId);
13870
14307
  }
13871
14308
  } finally {
@@ -13948,26 +14385,26 @@ init_config();
13948
14385
  import { Hono as Hono3 } from "hono";
13949
14386
  import { zValidator as zValidator3 } from "@hono/zod-validator";
13950
14387
  import { z as z18 } from "zod";
13951
- import { readFileSync as readFileSync9 } from "fs";
14388
+ import { readFileSync as readFileSync10 } from "fs";
13952
14389
  import { fileURLToPath as fileURLToPath3 } from "url";
13953
- import { dirname as dirname7, join as join14 } from "path";
14390
+ import { dirname as dirname8, join as join15 } from "path";
13954
14391
  var __filename = fileURLToPath3(import.meta.url);
13955
- var __dirname = dirname7(__filename);
14392
+ var __dirname = dirname8(__filename);
13956
14393
  var possiblePaths = [
13957
- join14(__dirname, "../package.json"),
14394
+ join15(__dirname, "../package.json"),
13958
14395
  // From dist/server -> dist/../package.json
13959
- join14(__dirname, "../../package.json"),
14396
+ join15(__dirname, "../../package.json"),
13960
14397
  // From dist/server (if nested differently)
13961
- join14(__dirname, "../../../package.json"),
14398
+ join15(__dirname, "../../../package.json"),
13962
14399
  // From src/server/routes (development)
13963
- join14(process.cwd(), "package.json")
14400
+ join15(process.cwd(), "package.json")
13964
14401
  // From current working directory
13965
14402
  ];
13966
14403
  var currentVersion = "0.0.0";
13967
14404
  var packageName = "sparkecoder";
13968
14405
  for (const packageJsonPath of possiblePaths) {
13969
14406
  try {
13970
- const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
14407
+ const packageJson = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
13971
14408
  if (packageJson.name === "sparkecoder") {
13972
14409
  currentVersion = packageJson.version || "0.0.0";
13973
14410
  packageName = packageJson.name || "sparkecoder";
@@ -14785,12 +15222,13 @@ slack.post("/events", async (c) => {
14785
15222
  if (inbound) {
14786
15223
  const isThreadReply = ev.type === "message" && ev.channel_type !== "im" && typeof ev.thread_ts === "string" && ev.thread_ts !== ev.ts;
14787
15224
  if (isThreadReply) {
14788
- const ours = isThreadOwned(ev.channel, ev.thread_ts) || await threadBelongsToUs(ev.channel, ev.thread_ts);
15225
+ const ours = isThreadOwned(ev.channel, ev.thread_ts) || await botParticipatedInThread(ev.channel, ev.thread_ts);
14789
15226
  if (!ours) {
14790
15227
  console.log(`[slack] dropping thread reply in unknown thread: channel=${ev.channel} thread=${ev.thread_ts}`);
14791
15228
  updateEvent(auditId, { status: "dropped", dropReason: "thread_not_owned" });
14792
15229
  return c.json({ ok: true });
14793
15230
  }
15231
+ markThreadOwned(ev.channel, ev.thread_ts);
14794
15232
  }
14795
15233
  if (ev.type === "app_mention" && ev.channel && (ev.thread_ts || ev.ts)) {
14796
15234
  markThreadOwned(ev.channel, ev.thread_ts || ev.ts);
@@ -14829,18 +15267,6 @@ slack.post("/events", async (c) => {
14829
15267
  }
14830
15268
  return c.json({ ok: true });
14831
15269
  });
14832
- async function threadBelongsToUs(channel, threadTs) {
14833
- try {
14834
- const sessions3 = await sessionQueries.list(500, 0);
14835
- return sessions3.some((s) => {
14836
- const slack2 = s.config?.slack;
14837
- return slack2?.channel === channel && slack2?.threadTs === threadTs;
14838
- });
14839
- } catch (err) {
14840
- console.warn("[slack] threadBelongsToUs lookup failed:", err?.message ?? err);
14841
- return false;
14842
- }
14843
- }
14844
15270
  async function findOrCreateOrchestratorId() {
14845
15271
  try {
14846
15272
  const all = await sessionQueries.list(500, 0);
@@ -15229,9 +15655,9 @@ init_skills();
15229
15655
  import { Hono as Hono9 } from "hono";
15230
15656
  import { zValidator as zValidator7 } from "@hono/zod-validator";
15231
15657
  import { z as z22 } from "zod";
15232
- import { existsSync as existsSync20, statSync as statSync3 } from "fs";
15658
+ import { existsSync as existsSync21, statSync as statSync3 } from "fs";
15233
15659
  import { readFile as readFile12, writeFile as writeFile6, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
15234
- import { resolve as resolve11, join as join15, basename as basename6, dirname as dirname8, extname as extname9 } from "path";
15660
+ import { resolve as resolve11, join as join16, basename as basename6, dirname as dirname9, extname as extname9 } from "path";
15235
15661
  var skills = new Hono9();
15236
15662
  function encodeId(filePath) {
15237
15663
  return Buffer.from(filePath, "utf-8").toString("base64url");
@@ -15290,13 +15716,13 @@ function pathToLabel(path) {
15290
15716
  if (path.includes("/.sparkecoder/skills")) return ".sparkecoder/skills";
15291
15717
  if (path.includes("/.cursor/rules")) return ".cursor/rules";
15292
15718
  if (path.includes("/.claude/skills")) return ".claude/skills";
15293
- return basename6(dirname8(path)) + "/" + basename6(path);
15719
+ return basename6(dirname9(path)) + "/" + basename6(path);
15294
15720
  }
15295
15721
  skills.get("/", async (c) => {
15296
15722
  const dirs = listAllDirectories();
15297
15723
  const allSkills = [];
15298
15724
  for (const dir of dirs) {
15299
- if (!existsSync20(dir.path)) continue;
15725
+ if (!existsSync21(dir.path)) continue;
15300
15726
  try {
15301
15727
  const list = await loadSkillsFromDirectory(dir.path, {
15302
15728
  priority: dir.priority,
@@ -15333,7 +15759,7 @@ skills.get("/", async (c) => {
15333
15759
  label: d.label,
15334
15760
  source: d.source,
15335
15761
  alwaysApply: d.alwaysApply,
15336
- exists: existsSync20(d.path),
15762
+ exists: existsSync21(d.path),
15337
15763
  writable: isWritable(d.path)
15338
15764
  })),
15339
15765
  skills: allSkills
@@ -15341,7 +15767,7 @@ skills.get("/", async (c) => {
15341
15767
  });
15342
15768
  function isWritable(dir) {
15343
15769
  try {
15344
- if (!existsSync20(dir)) return false;
15770
+ if (!existsSync21(dir)) return false;
15345
15771
  if (dir.includes("/skills/default")) return false;
15346
15772
  return true;
15347
15773
  } catch {
@@ -15350,7 +15776,7 @@ function isWritable(dir) {
15350
15776
  }
15351
15777
  skills.get("/:id", async (c) => {
15352
15778
  const filePath = decodeId(c.req.param("id"));
15353
- if (!filePath || !existsSync20(filePath)) {
15779
+ if (!filePath || !existsSync21(filePath)) {
15354
15780
  return c.json({ error: "skill not found" }, 404);
15355
15781
  }
15356
15782
  const content = await readFile12(filePath, "utf-8");
@@ -15379,8 +15805,8 @@ skills.post(
15379
15805
  const safeName = basename6(fileName).replace(/[^A-Za-z0-9._-]/g, "-");
15380
15806
  const ext = extname9(safeName).toLowerCase();
15381
15807
  const finalName = ext === ".md" || ext === ".mdc" ? safeName : `${safeName}.md`;
15382
- const filePath = join15(targetDir, finalName);
15383
- if (existsSync20(filePath)) {
15808
+ const filePath = join16(targetDir, finalName);
15809
+ if (existsSync21(filePath)) {
15384
15810
  return c.json({ error: `file already exists: ${finalName}` }, 409);
15385
15811
  }
15386
15812
  try {
@@ -15397,7 +15823,7 @@ skills.put(
15397
15823
  zValidator7("json", z22.object({ content: z22.string() })),
15398
15824
  async (c) => {
15399
15825
  const filePath = decodeId(c.req.param("id"));
15400
- if (!filePath || !existsSync20(filePath)) return c.json({ error: "skill not found" }, 404);
15826
+ if (!filePath || !existsSync21(filePath)) return c.json({ error: "skill not found" }, 404);
15401
15827
  if (filePath.includes("/skills/default")) {
15402
15828
  return c.json({ error: "built-in skills are read-only" }, 400);
15403
15829
  }
@@ -15407,7 +15833,7 @@ skills.put(
15407
15833
  );
15408
15834
  skills.delete("/:id", async (c) => {
15409
15835
  const filePath = decodeId(c.req.param("id"));
15410
- if (!filePath || !existsSync20(filePath)) return c.json({ error: "skill not found" }, 404);
15836
+ if (!filePath || !existsSync21(filePath)) return c.json({ error: "skill not found" }, 404);
15411
15837
  if (filePath.includes("/skills/default")) {
15412
15838
  return c.json({ error: "built-in skills are read-only" }, 400);
15413
15839
  }
@@ -15427,7 +15853,7 @@ skills.post(
15427
15853
  }
15428
15854
  const next = [...current, abs];
15429
15855
  setSkillsAdditionalDirectories(next);
15430
- return c.json({ ok: true, path: abs, exists: existsSync20(abs) }, 201);
15856
+ return c.json({ ok: true, path: abs, exists: existsSync21(abs) }, 201);
15431
15857
  }
15432
15858
  );
15433
15859
  skills.delete("/directories", (c) => {
@@ -15697,13 +16123,13 @@ var DEFAULT_WEB_PORT = 6969;
15697
16123
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
15698
16124
  function getWebDirectory() {
15699
16125
  try {
15700
- const currentDir = dirname9(fileURLToPath4(import.meta.url));
16126
+ const currentDir = dirname10(fileURLToPath4(import.meta.url));
15701
16127
  const webDir = resolve12(currentDir, "..", "web");
15702
- if (existsSync21(webDir) && existsSync21(join16(webDir, "package.json"))) {
16128
+ if (existsSync22(webDir) && existsSync22(join17(webDir, "package.json"))) {
15703
16129
  return webDir;
15704
16130
  }
15705
16131
  const altWebDir = resolve12(currentDir, "..", "..", "web");
15706
- if (existsSync21(altWebDir) && existsSync21(join16(altWebDir, "package.json"))) {
16132
+ if (existsSync22(altWebDir) && existsSync22(join17(altWebDir, "package.json"))) {
15707
16133
  return altWebDir;
15708
16134
  }
15709
16135
  return null;
@@ -15761,23 +16187,23 @@ async function findWebPort(preferredPort) {
15761
16187
  return { port: preferredPort, alreadyRunning: false };
15762
16188
  }
15763
16189
  function hasProductionBuild(webDir) {
15764
- const buildIdPath = join16(webDir, ".next", "BUILD_ID");
15765
- return existsSync21(buildIdPath);
16190
+ const buildIdPath = join17(webDir, ".next", "BUILD_ID");
16191
+ return existsSync22(buildIdPath);
15766
16192
  }
15767
16193
  function hasSourceFiles(webDir) {
15768
- const appDir = join16(webDir, "src", "app");
15769
- const pagesDir = join16(webDir, "src", "pages");
15770
- const rootAppDir = join16(webDir, "app");
15771
- const rootPagesDir = join16(webDir, "pages");
15772
- return existsSync21(appDir) || existsSync21(pagesDir) || existsSync21(rootAppDir) || existsSync21(rootPagesDir);
16194
+ const appDir = join17(webDir, "src", "app");
16195
+ const pagesDir = join17(webDir, "src", "pages");
16196
+ const rootAppDir = join17(webDir, "app");
16197
+ const rootPagesDir = join17(webDir, "pages");
16198
+ return existsSync22(appDir) || existsSync22(pagesDir) || existsSync22(rootAppDir) || existsSync22(rootPagesDir);
15773
16199
  }
15774
16200
  function getStandaloneServerPath(webDir) {
15775
16201
  const possiblePaths2 = [
15776
- join16(webDir, ".next", "standalone", "server.js"),
15777
- join16(webDir, ".next", "standalone", "web", "server.js")
16202
+ join17(webDir, ".next", "standalone", "server.js"),
16203
+ join17(webDir, ".next", "standalone", "web", "server.js")
15778
16204
  ];
15779
16205
  for (const serverPath of possiblePaths2) {
15780
- if (existsSync21(serverPath)) {
16206
+ if (existsSync22(serverPath)) {
15781
16207
  return serverPath;
15782
16208
  }
15783
16209
  }
@@ -15817,15 +16243,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15817
16243
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
15818
16244
  return { process: null, port: actualPort };
15819
16245
  }
15820
- const usePnpm = existsSync21(join16(webDir, "pnpm-lock.yaml"));
15821
- const useNpm = !usePnpm && existsSync21(join16(webDir, "package-lock.json"));
16246
+ const usePnpm = existsSync22(join17(webDir, "pnpm-lock.yaml"));
16247
+ const useNpm = !usePnpm && existsSync22(join17(webDir, "package-lock.json"));
15822
16248
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
15823
16249
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
15824
16250
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
15825
16251
  const runtimeConfig = { apiBaseUrl: apiUrl };
15826
- const runtimeConfigPath = join16(webDir, "runtime-config.json");
16252
+ const runtimeConfigPath = join17(webDir, "runtime-config.json");
15827
16253
  try {
15828
- writeFileSync6(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
16254
+ writeFileSync7(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
15829
16255
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
15830
16256
  } catch (err) {
15831
16257
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -15845,7 +16271,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
15845
16271
  if (standaloneServerPath) {
15846
16272
  command = "node";
15847
16273
  args = ["server.js"];
15848
- cwd = dirname9(standaloneServerPath);
16274
+ cwd = dirname10(standaloneServerPath);
15849
16275
  webEnv.PORT = String(actualPort);
15850
16276
  webEnv.HOSTNAME = "0.0.0.0";
15851
16277
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
@@ -16039,8 +16465,8 @@ async function startServer(options = {}) {
16039
16465
  if (options.workingDirectory) {
16040
16466
  config.resolvedWorkingDirectory = options.workingDirectory;
16041
16467
  }
16042
- if (!existsSync21(config.resolvedWorkingDirectory)) {
16043
- mkdirSync9(config.resolvedWorkingDirectory, { recursive: true });
16468
+ if (!existsSync22(config.resolvedWorkingDirectory)) {
16469
+ mkdirSync10(config.resolvedWorkingDirectory, { recursive: true });
16044
16470
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
16045
16471
  }
16046
16472
  if (!config.resolvedRemoteServer.url) {
@@ -16598,18 +17024,18 @@ function generateOpenAPISpec() {
16598
17024
  init_config();
16599
17025
  init_semantic();
16600
17026
  init_db();
16601
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync7, readFileSync as readFileSync10, existsSync as existsSync22, statSync as statSync4 } from "fs";
16602
- import { resolve as resolve13, join as join17 } from "path";
17027
+ import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync8, readFileSync as readFileSync11, existsSync as existsSync23, statSync as statSync4 } from "fs";
17028
+ import { resolve as resolve13, join as join18 } from "path";
16603
17029
  function getCliVersion() {
16604
- const here = dirname10(fileURLToPath5(import.meta.url));
17030
+ const here = dirname11(fileURLToPath5(import.meta.url));
16605
17031
  const candidates = [
16606
- join17(here, "..", "package.json"),
16607
- join17(here, "..", "..", "package.json"),
16608
- join17(process.cwd(), "package.json")
17032
+ join18(here, "..", "package.json"),
17033
+ join18(here, "..", "..", "package.json"),
17034
+ join18(process.cwd(), "package.json")
16609
17035
  ];
16610
17036
  for (const p of candidates) {
16611
17037
  try {
16612
- const pkg = JSON.parse(readFileSync10(p, "utf8"));
17038
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
16613
17039
  if (pkg.name === "sparkecoder" && pkg.version) return pkg.version;
16614
17040
  } catch {
16615
17041
  }
@@ -17256,8 +17682,8 @@ program.command("task").description("Run an autonomous task that completes witho
17256
17682
  let outputSchema;
17257
17683
  try {
17258
17684
  const schemaStr = options.schema;
17259
- if (existsSync22(schemaStr)) {
17260
- outputSchema = JSON.parse(readFileSync10(schemaStr, "utf-8"));
17685
+ if (existsSync23(schemaStr)) {
17686
+ outputSchema = JSON.parse(readFileSync11(schemaStr, "utf-8"));
17261
17687
  } else {
17262
17688
  outputSchema = JSON.parse(schemaStr);
17263
17689
  }
@@ -17324,19 +17750,19 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
17324
17750
  let configLocation;
17325
17751
  if (options.global) {
17326
17752
  const appDataDir = ensureAppDataDirectory();
17327
- configPath = join17(appDataDir, "sparkecoder.config.json");
17753
+ configPath = join18(appDataDir, "sparkecoder.config.json");
17328
17754
  configLocation = "global";
17329
17755
  } else {
17330
17756
  configPath = resolve13(process.cwd(), "sparkecoder.config.json");
17331
17757
  configLocation = "local";
17332
17758
  }
17333
- if (existsSync22(configPath) && !options.force) {
17759
+ if (existsSync23(configPath) && !options.force) {
17334
17760
  console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
17335
17761
  console.log(chalk.dim(` ${configPath}`));
17336
17762
  return;
17337
17763
  }
17338
17764
  const config = createDefaultConfig();
17339
- writeFileSync7(configPath, JSON.stringify(config, null, 2));
17765
+ writeFileSync8(configPath, JSON.stringify(config, null, 2));
17340
17766
  console.log(chalk.green(`\u2713 Created ${configLocation} config`));
17341
17767
  console.log(chalk.dim(` ${configPath}`));
17342
17768
  console.log(chalk.dim("Set AI_GATEWAY_API_KEY and run sparkecoder to start"));
@@ -17355,11 +17781,11 @@ program.command("slack-setup").description("Interactively configure Slack integr
17355
17781
  console.error(chalk.red("Both bot token and signing secret are required."));
17356
17782
  process.exit(1);
17357
17783
  }
17358
- const configPath = options.global ? join17(ensureAppDataDirectory(), "sparkecoder.config.json") : resolve13(process.cwd(), "sparkecoder.config.json");
17784
+ const configPath = options.global ? join18(ensureAppDataDirectory(), "sparkecoder.config.json") : resolve13(process.cwd(), "sparkecoder.config.json");
17359
17785
  let existing = {};
17360
- if (existsSync22(configPath)) {
17786
+ if (existsSync23(configPath)) {
17361
17787
  try {
17362
- existing = JSON.parse(readFileSync10(configPath, "utf-8"));
17788
+ existing = JSON.parse(readFileSync11(configPath, "utf-8"));
17363
17789
  } catch {
17364
17790
  }
17365
17791
  } else {
@@ -17371,7 +17797,7 @@ program.command("slack-setup").description("Interactively configure Slack integr
17371
17797
  signingSecret,
17372
17798
  defaultOrchestratorName: existing.slack?.defaultOrchestratorName ?? "orchestrator"
17373
17799
  };
17374
- writeFileSync7(configPath, JSON.stringify(existing, null, 2));
17800
+ writeFileSync8(configPath, JSON.stringify(existing, null, 2));
17375
17801
  console.log(chalk.green(`
17376
17802
  \u2713 Slack configured`));
17377
17803
  console.log(chalk.dim(` ${configPath}`));
@@ -17623,9 +18049,9 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
17623
18049
  }
17624
18050
  const verOut = run("cloudflared", ["--version"]).stdout?.toString().split("\n")[0] || "installed";
17625
18051
  console.log(chalk.green("\u2713"), "cloudflared:", chalk.dim(verOut));
17626
- const cfDir = join17(homedir2(), ".cloudflared");
17627
- const certPath = join17(cfDir, "cert.pem");
17628
- if (!existsSync22(certPath)) {
18052
+ const cfDir = join18(homedir2(), ".cloudflared");
18053
+ const certPath = join18(cfDir, "cert.pem");
18054
+ if (!existsSync23(certPath)) {
17629
18055
  console.log(chalk.yellow("No Cloudflare login cert found."));
17630
18056
  if (await confirm("Run `cloudflared tunnel login` now (opens a browser)?", true)) {
17631
18057
  run("cloudflared", ["tunnel", "login"], { inheritIO: true, check: true });
@@ -17669,8 +18095,8 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
17669
18095
  return;
17670
18096
  }
17671
18097
  }
17672
- const credsFile = tunnel.credentials_file || join17(cfDir, `${tunnel.id}.json`);
17673
- if (!existsSync22(credsFile)) {
18098
+ const credsFile = tunnel.credentials_file || join18(cfDir, `${tunnel.id}.json`);
18099
+ if (!existsSync23(credsFile)) {
17674
18100
  console.log(chalk.yellow(`Credentials file not found at ${credsFile}. The tunnel may still work via cert.pem.`));
17675
18101
  }
17676
18102
  let hostname = options.hostname;
@@ -17693,7 +18119,7 @@ program.command("cloudflared-setup").description("Auto-detect cloudflared + set
17693
18119
  console.log(chalk.yellow("DNS route warning:"), err.trim().split("\n").slice(-2).join(" "));
17694
18120
  }
17695
18121
  }
17696
- const configPath = join17(cfDir, "config.yml");
18122
+ const configPath = join18(cfDir, "config.yml");
17697
18123
  const configBody = `tunnel: ${tunnel.id}
17698
18124
  credentials-file: ${credsFile}
17699
18125
  ingress:
@@ -17704,14 +18130,14 @@ ingress:
17704
18130
  - service: http_status:404
17705
18131
  `;
17706
18132
  let wroteConfig = false;
17707
- if (existsSync22(configPath)) {
17708
- const existing = readFileSync10(configPath, "utf8");
18133
+ if (existsSync23(configPath)) {
18134
+ const existing = readFileSync11(configPath, "utf8");
17709
18135
  if (existing.trim() === configBody.trim()) {
17710
18136
  console.log(chalk.green("\u2713"), `config.yml already up to date: ${configPath}`);
17711
18137
  wroteConfig = true;
17712
18138
  } else if (await confirm(`A different ${configPath} exists. Overwrite (a backup will be saved)?`, false)) {
17713
18139
  copyFileSync(configPath, `${configPath}.bak.${Date.now()}`);
17714
- writeFileSync7(configPath, configBody);
18140
+ writeFileSync8(configPath, configBody);
17715
18141
  console.log(chalk.green("\u2713"), `wrote ${configPath} (previous saved as .bak.*)`);
17716
18142
  wroteConfig = true;
17717
18143
  } else {
@@ -17719,7 +18145,7 @@ ingress:
17719
18145
  console.log(chalk.cyan(configBody));
17720
18146
  }
17721
18147
  } else {
17722
- writeFileSync7(configPath, configBody);
18148
+ writeFileSync8(configPath, configBody);
17723
18149
  console.log(chalk.green("\u2713"), `wrote ${configPath}`);
17724
18150
  wroteConfig = true;
17725
18151
  }
@@ -18209,17 +18635,17 @@ program.command("request-permissions").description("Open System Settings to the
18209
18635
  });
18210
18636
  {
18211
18637
  let stateFilePath = function() {
18212
- return join17(ensureAppDataDirectory(), "recordings.json");
18638
+ return join18(ensureAppDataDirectory(), "recordings.json");
18213
18639
  }, readState = function() {
18214
18640
  const p = stateFilePath();
18215
- if (!existsSync22(p)) return [];
18641
+ if (!existsSync23(p)) return [];
18216
18642
  try {
18217
- return JSON.parse(readFileSync10(p, "utf-8"));
18643
+ return JSON.parse(readFileSync11(p, "utf-8"));
18218
18644
  } catch {
18219
18645
  return [];
18220
18646
  }
18221
18647
  }, writeState = function(rows) {
18222
- writeFileSync7(stateFilePath(), JSON.stringify(rows, null, 2), { mode: 384 });
18648
+ writeFileSync8(stateFilePath(), JSON.stringify(rows, null, 2), { mode: 384 });
18223
18649
  }, isAlive = function(pid) {
18224
18650
  try {
18225
18651
  process.kill(pid, 0);
@@ -18234,16 +18660,16 @@ program.command("request-permissions").description("Open System Settings to the
18234
18660
  const record = program.command("record").description("Start/stop screen recordings");
18235
18661
  record.command("start").description("Start a screen recording (returns id, path, pid as JSON)").option("--name <slug>", "Optional human-readable label for the recording").option("--dir <path>", "Output directory (default: ~/recordings)").action(async (opts) => {
18236
18662
  const { homedir: homedir2, platform: osPlatform } = await import("os");
18237
- const outDir = opts.dir ? resolve13(opts.dir.replace(/^~/, homedir2())) : join17(homedir2(), "recordings");
18238
- mkdirSync10(outDir, { recursive: true });
18663
+ const outDir = opts.dir ? resolve13(opts.dir.replace(/^~/, homedir2())) : join18(homedir2(), "recordings");
18664
+ mkdirSync11(outDir, { recursive: true });
18239
18665
  const id = `rec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
18240
18666
  const ext = osPlatform() === "darwin" ? "mov" : "mp4";
18241
18667
  const filename = `${id}${opts.name ? `-${String(opts.name).replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}` : ""}.${ext}`;
18242
- const path = join17(outDir, filename);
18668
+ const path = join18(outDir, filename);
18243
18669
  let cmd;
18244
18670
  let args;
18245
18671
  if (osPlatform() === "darwin") {
18246
- cmd = existsSync22("/usr/sbin/screencapture") ? "/usr/sbin/screencapture" : "screencapture";
18672
+ cmd = existsSync23("/usr/sbin/screencapture") ? "/usr/sbin/screencapture" : "screencapture";
18247
18673
  args = ["-v", "-C", "-k", path];
18248
18674
  } else if (osPlatform() === "linux") {
18249
18675
  const display = process.env.DISPLAY || ":0.0";
@@ -18346,7 +18772,7 @@ program.command("request-permissions").description("Open System Settings to the
18346
18772
  }
18347
18773
  }
18348
18774
  writeState(rows.filter((r) => r.id !== id));
18349
- const fileExists = existsSync22(row.path);
18775
+ const fileExists = existsSync23(row.path);
18350
18776
  const sizeMb = fileExists ? Math.round(statSync4(row.path).size / (1024 * 1024) * 10) / 10 : 0;
18351
18777
  console.log(JSON.stringify({
18352
18778
  id,
@@ -18381,7 +18807,7 @@ program.command("request-permissions").description("Open System Settings to the
18381
18807
  }
18382
18808
  }
18383
18809
  }
18384
- stopped.push({ id: r.id, path: r.path, ok: existsSync22(r.path) });
18810
+ stopped.push({ id: r.id, path: r.path, ok: existsSync23(r.path) });
18385
18811
  }
18386
18812
  writeState([]);
18387
18813
  console.log(JSON.stringify({ stopped }));