sparkecoder 0.1.82 → 0.1.83

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 (90) hide show
  1. package/dist/agent/index.js +284 -71
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +370 -144
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +358 -132
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +358 -132
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/skills/default/browser.md +30 -0
  10. package/dist/tools/index.d.ts +117 -1
  11. package/dist/tools/index.js +183 -41
  12. package/dist/tools/index.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/skills/default/browser.md +30 -0
  15. package/web/.next/BUILD_ID +1 -1
  16. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  17. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  18. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  19. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  20. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  35. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  44. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  53. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  62. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  70. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  78. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  79. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  80. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  81. package/web/.next/standalone/web/package-lock.json +3 -3
  82. /package/web/.next/standalone/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_buildManifest.js +0 -0
  83. /package/web/.next/standalone/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_clientMiddlewareManifest.json +0 -0
  84. /package/web/.next/standalone/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_ssgManifest.js +0 -0
  85. /package/web/.next/standalone/web/.next/static/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_buildManifest.js +0 -0
  86. /package/web/.next/standalone/web/.next/static/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_clientMiddlewareManifest.json +0 -0
  87. /package/web/.next/standalone/web/.next/static/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_ssgManifest.js +0 -0
  88. /package/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_buildManifest.js +0 -0
  89. /package/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_clientMiddlewareManifest.json +0 -0
  90. /package/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_ssgManifest.js +0 -0
@@ -854,27 +854,38 @@ function requiresApproval(toolName, sessionConfig) {
854
854
  return false;
855
855
  }
856
856
  function loadStoredAuthKey() {
857
- const keysPath = join(getAppDataDirectory(), AUTH_KEY_FILE);
858
- if (!existsSync(keysPath)) {
859
- return null;
860
- }
861
- try {
862
- const content = readFileSync(keysPath, "utf-8");
863
- const data = JSON.parse(content);
864
- return data.authKey || null;
865
- } catch {
866
- return null;
857
+ const locations = [
858
+ join(process.cwd(), ".sparkecoder", AUTH_KEY_FILE),
859
+ join(getAppDataDirectory(), AUTH_KEY_FILE)
860
+ ];
861
+ for (const keysPath of locations) {
862
+ if (!existsSync(keysPath)) continue;
863
+ try {
864
+ const content = readFileSync(keysPath, "utf-8");
865
+ const data = JSON.parse(content);
866
+ if (data.authKey) return data.authKey;
867
+ } catch {
868
+ }
867
869
  }
870
+ return null;
868
871
  }
869
872
  function saveAuthKey(authKey3, userId) {
870
- const appDir = ensureAppDataDirectory();
871
- const keysPath = join(appDir, AUTH_KEY_FILE);
872
873
  const data = {
873
874
  authKey: authKey3,
874
875
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
875
876
  userId
876
877
  };
877
- writeFileSync(keysPath, JSON.stringify(data, null, 2), { mode: 384 });
878
+ const json = JSON.stringify(data, null, 2);
879
+ const appDir = ensureAppDataDirectory();
880
+ writeFileSync(join(appDir, AUTH_KEY_FILE), json, { mode: 384 });
881
+ try {
882
+ const workspaceAuthDir = join(process.cwd(), ".sparkecoder");
883
+ if (!existsSync(workspaceAuthDir)) {
884
+ mkdirSync(workspaceAuthDir, { recursive: true });
885
+ }
886
+ writeFileSync(join(workspaceAuthDir, AUTH_KEY_FILE), json, { mode: 384 });
887
+ } catch {
888
+ }
878
889
  }
879
890
  async function registerWithRemoteServer(serverUrl, name) {
880
891
  const response = await fetch(`${serverUrl}/auth/register`, {
@@ -1023,9 +1034,9 @@ __export(skills_exports, {
1023
1034
  loadSkillContent: () => loadSkillContent,
1024
1035
  loadSkillsFromDirectory: () => loadSkillsFromDirectory
1025
1036
  });
1026
- import { readFile as readFile6, readdir } from "fs/promises";
1037
+ import { readFile as readFile7, readdir } from "fs/promises";
1027
1038
  import { resolve as resolve6, basename, extname as extname4, relative as relative4 } from "path";
1028
- import { existsSync as existsSync9 } from "fs";
1039
+ import { existsSync as existsSync10 } from "fs";
1029
1040
  import { minimatch } from "minimatch";
1030
1041
  function parseSkillFrontmatter(content) {
1031
1042
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -1103,7 +1114,7 @@ async function loadSkillsFromDirectory(directory, options = {}) {
1103
1114
  defaultLoadType = "on_demand",
1104
1115
  forceAlwaysApply = false
1105
1116
  } = options;
1106
- if (!existsSync9(directory)) {
1117
+ if (!existsSync10(directory)) {
1107
1118
  return [];
1108
1119
  }
1109
1120
  const skills = [];
@@ -1113,7 +1124,7 @@ async function loadSkillsFromDirectory(directory, options = {}) {
1113
1124
  let fileName;
1114
1125
  if (entry.isDirectory()) {
1115
1126
  const skillMdPath = resolve6(directory, entry.name, "SKILL.md");
1116
- if (existsSync9(skillMdPath)) {
1127
+ if (existsSync10(skillMdPath)) {
1117
1128
  filePath = skillMdPath;
1118
1129
  fileName = entry.name;
1119
1130
  } else {
@@ -1125,7 +1136,7 @@ async function loadSkillsFromDirectory(directory, options = {}) {
1125
1136
  } else {
1126
1137
  continue;
1127
1138
  }
1128
- const content = await readFile6(filePath, "utf-8");
1139
+ const content = await readFile7(filePath, "utf-8");
1129
1140
  const parsed = parseSkillFrontmatter(content);
1130
1141
  if (parsed) {
1131
1142
  const alwaysApply = forceAlwaysApply || parsed.metadata.alwaysApply;
@@ -1204,7 +1215,7 @@ async function loadAllSkillsFromDiscovered(discovered) {
1204
1215
  const onDemandSkills = allSkills.filter((s) => !s.alwaysApply && s.loadType !== "always");
1205
1216
  const alwaysWithContent = await Promise.all(
1206
1217
  alwaysSkills.map(async (skill) => {
1207
- const content = await readFile6(skill.filePath, "utf-8");
1218
+ const content = await readFile7(skill.filePath, "utf-8");
1208
1219
  const parsed = parseSkillFrontmatter(content);
1209
1220
  return {
1210
1221
  ...skill,
@@ -1241,7 +1252,7 @@ async function getGlobMatchedSkills(skills, activeFiles, workingDirectory) {
1241
1252
  });
1242
1253
  const matchedWithContent = await Promise.all(
1243
1254
  matchedSkills.map(async (skill) => {
1244
- const content = await readFile6(skill.filePath, "utf-8");
1255
+ const content = await readFile7(skill.filePath, "utf-8");
1245
1256
  const parsed = parseSkillFrontmatter(content);
1246
1257
  return {
1247
1258
  ...skill,
@@ -1253,10 +1264,10 @@ async function getGlobMatchedSkills(skills, activeFiles, workingDirectory) {
1253
1264
  return matchedWithContent;
1254
1265
  }
1255
1266
  async function loadAgentsMd(agentsMdPath) {
1256
- if (!agentsMdPath || !existsSync9(agentsMdPath)) {
1267
+ if (!agentsMdPath || !existsSync10(agentsMdPath)) {
1257
1268
  return null;
1258
1269
  }
1259
- const content = await readFile6(agentsMdPath, "utf-8");
1270
+ const content = await readFile7(agentsMdPath, "utf-8");
1260
1271
  return content;
1261
1272
  }
1262
1273
  async function loadSkillContent(skillName, directories) {
@@ -1267,7 +1278,7 @@ async function loadSkillContent(skillName, directories) {
1267
1278
  if (!skill) {
1268
1279
  return null;
1269
1280
  }
1270
- const content = await readFile6(skill.filePath, "utf-8");
1281
+ const content = await readFile7(skill.filePath, "utf-8");
1271
1282
  const parsed = parseSkillFrontmatter(content);
1272
1283
  return {
1273
1284
  ...skill,
@@ -1528,7 +1539,7 @@ var init_client = __esm({
1528
1539
  });
1529
1540
 
1530
1541
  // src/semantic/indexer.ts
1531
- import { readFileSync as readFileSync4, statSync } from "fs";
1542
+ import { readFileSync as readFileSync5, statSync } from "fs";
1532
1543
  import { relative as relative6 } from "path";
1533
1544
  import { minimatch as minimatch2 } from "minimatch";
1534
1545
  async function getIndexStatus(workingDirectory) {
@@ -1613,8 +1624,8 @@ __export(semantic_search_exports, {
1613
1624
  });
1614
1625
  import { tool as tool8 } from "ai";
1615
1626
  import { z as z9 } from "zod";
1616
- import { existsSync as existsSync12, readFileSync as readFileSync5 } from "fs";
1617
- import { join as join5 } from "path";
1627
+ import { existsSync as existsSync13, readFileSync as readFileSync6 } from "fs";
1628
+ import { join as join6 } from "path";
1618
1629
  import { minimatch as minimatch3 } from "minimatch";
1619
1630
  function createSemanticSearchTool(options) {
1620
1631
  return tool8({
@@ -1681,13 +1692,13 @@ Returns matching code snippets with file paths, line numbers, and relevance scor
1681
1692
  if (language && matchLanguage !== language.toLowerCase()) {
1682
1693
  continue;
1683
1694
  }
1684
- const fullPath = join5(options.workingDirectory, filePath);
1685
- if (!existsSync12(fullPath)) {
1695
+ const fullPath = join6(options.workingDirectory, filePath);
1696
+ if (!existsSync13(fullPath)) {
1686
1697
  continue;
1687
1698
  }
1688
1699
  let snippet = "";
1689
1700
  try {
1690
- const content = readFileSync5(fullPath, "utf-8");
1701
+ const content = readFileSync6(fullPath, "utf-8");
1691
1702
  const lines = content.split("\n");
1692
1703
  const snippetLines = lines.slice(
1693
1704
  Math.max(0, startLine - 1),
@@ -1755,7 +1766,7 @@ async function sendWebhook(url, event) {
1755
1766
  try {
1756
1767
  const controller = new AbortController();
1757
1768
  const timeout = setTimeout(() => controller.abort(), 5e3);
1758
- await fetch(url, {
1769
+ const response = await fetch(url, {
1759
1770
  method: "POST",
1760
1771
  headers: {
1761
1772
  "Content-Type": "application/json",
@@ -1765,7 +1776,12 @@ async function sendWebhook(url, event) {
1765
1776
  signal: controller.signal
1766
1777
  });
1767
1778
  clearTimeout(timeout);
1768
- } catch {
1779
+ if (!response.ok) {
1780
+ console.warn(`[WEBHOOK] ${event.type} to ${url} returned HTTP ${response.status}`);
1781
+ }
1782
+ } catch (err) {
1783
+ const reason = err.name === "AbortError" ? "timeout (5s)" : err.message;
1784
+ console.warn(`[WEBHOOK] ${event.type} to ${url} failed: ${reason}`);
1769
1785
  }
1770
1786
  }
1771
1787
  var init_webhook = __esm({
@@ -1982,8 +1998,8 @@ __export(recorder_exports, {
1982
1998
  });
1983
1999
  import { exec as exec5 } from "child_process";
1984
2000
  import { promisify as promisify5 } from "util";
1985
- import { writeFile as writeFile4, mkdir as mkdir4, readFile as readFile10, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
1986
- import { join as join7 } from "path";
2001
+ import { writeFile as writeFile5, mkdir as mkdir4, readFile as readFile11, unlink as unlink2, readdir as readdir5, rm } from "fs/promises";
2002
+ import { join as join8 } from "path";
1987
2003
  import { tmpdir } from "os";
1988
2004
  import { nanoid as nanoid3 } from "nanoid";
1989
2005
  async function checkFfmpeg() {
@@ -2040,21 +2056,21 @@ var init_recorder = __esm({
2040
2056
  */
2041
2057
  async encode() {
2042
2058
  if (this.frames.length === 0) return null;
2043
- const workDir = join7(tmpdir(), `sparkecoder-recording-${nanoid3(8)}`);
2059
+ const workDir = join8(tmpdir(), `sparkecoder-recording-${nanoid3(8)}`);
2044
2060
  await mkdir4(workDir, { recursive: true });
2045
2061
  try {
2046
2062
  for (let i = 0; i < this.frames.length; i++) {
2047
- const framePath = join7(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
2048
- await writeFile4(framePath, this.frames[i].data);
2063
+ const framePath = join8(workDir, `frame_${String(i).padStart(6, "0")}.jpg`);
2064
+ await writeFile5(framePath, this.frames[i].data);
2049
2065
  }
2050
2066
  const duration = (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1e3;
2051
2067
  const fps = duration > 0 ? Math.round(this.frames.length / duration) : 10;
2052
2068
  const clampedFps = Math.max(1, Math.min(fps, 30));
2053
- const outputPath = join7(workDir, `recording_${this.sessionId}.mp4`);
2069
+ const outputPath = join8(workDir, `recording_${this.sessionId}.mp4`);
2054
2070
  const hasFfmpeg = await checkFfmpeg();
2055
2071
  if (hasFfmpeg) {
2056
2072
  await execAsync5(
2057
- `ffmpeg -y -framerate ${clampedFps} -i "${join7(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
2073
+ `ffmpeg -y -framerate ${clampedFps} -i "${join8(workDir, "frame_%06d.jpg")}" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
2058
2074
  { timeout: 12e4 }
2059
2075
  );
2060
2076
  } else {
@@ -2062,11 +2078,11 @@ var init_recorder = __esm({
2062
2078
  await cleanup(workDir);
2063
2079
  return null;
2064
2080
  }
2065
- const outputBuf = await readFile10(outputPath);
2081
+ const outputBuf = await readFile11(outputPath);
2066
2082
  const files = await readdir5(workDir);
2067
2083
  for (const f of files) {
2068
2084
  if (f.startsWith("frame_")) {
2069
- await unlink2(join7(workDir, f)).catch(() => {
2085
+ await unlink2(join8(workDir, f)).catch(() => {
2070
2086
  });
2071
2087
  }
2072
2088
  }
@@ -2093,8 +2109,8 @@ import { Hono as Hono6 } from "hono";
2093
2109
  import { serve } from "@hono/node-server";
2094
2110
  import { cors } from "hono/cors";
2095
2111
  import { logger } from "hono/logger";
2096
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
2097
- import { resolve as resolve10, dirname as dirname7, join as join11 } from "path";
2112
+ import { existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
2113
+ import { resolve as resolve10, dirname as dirname7, join as join12 } from "path";
2098
2114
  import { spawn as spawn2 } from "child_process";
2099
2115
  import { createServer as createNetServer } from "net";
2100
2116
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -2104,9 +2120,9 @@ init_db();
2104
2120
  import { Hono } from "hono";
2105
2121
  import { zValidator } from "@hono/zod-validator";
2106
2122
  import { z as z15 } from "zod";
2107
- import { existsSync as existsSync14, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, readdirSync, statSync as statSync2, unlinkSync } from "fs";
2123
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
2108
2124
  import { readdir as readdir6 } from "fs/promises";
2109
- import { join as join8, basename as basename5, extname as extname8, relative as relative9 } from "path";
2125
+ import { join as join9, basename as basename5, extname as extname8, relative as relative9 } from "path";
2110
2126
  import { nanoid as nanoid5 } from "nanoid";
2111
2127
 
2112
2128
  // src/agent/index.ts
@@ -4173,8 +4189,34 @@ Working directory: ${options.workingDirectory}`,
4173
4189
  init_db();
4174
4190
  import { tool as tool4 } from "ai";
4175
4191
  import { z as z5 } from "zod";
4192
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
4193
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
4194
+ import { join as join4 } from "path";
4195
+ function getPlansDir(workingDirectory, sessionId) {
4196
+ return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
4197
+ }
4198
+ function ensurePlansDir(workingDirectory, sessionId) {
4199
+ const dir = getPlansDir(workingDirectory, sessionId);
4200
+ if (!existsSync9(dir)) {
4201
+ mkdirSync4(dir, { recursive: true });
4202
+ }
4203
+ const gitignorePath = join4(workingDirectory, ".gitignore");
4204
+ if (existsSync9(gitignorePath)) {
4205
+ try {
4206
+ const content = readFileSync3(gitignorePath, "utf-8");
4207
+ if (!content.includes(".sparkecoder")) {
4208
+ appendFileSync(gitignorePath, "\n.sparkecoder/\n");
4209
+ }
4210
+ } catch {
4211
+ }
4212
+ }
4213
+ return dir;
4214
+ }
4215
+ function slugify(name) {
4216
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
4217
+ }
4176
4218
  var todoInputSchema = z5.object({
4177
- action: z5.enum(["add", "list", "mark", "clear"]).describe("The action to perform on the todo list"),
4219
+ action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
4178
4220
  items: z5.array(
4179
4221
  z5.object({
4180
4222
  content: z5.string().describe("Description of the task"),
@@ -4182,27 +4224,67 @@ var todoInputSchema = z5.object({
4182
4224
  })
4183
4225
  ).optional().describe('For "add" action: Array of todo items to add'),
4184
4226
  todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
4185
- status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item')
4227
+ status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
4228
+ planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
4229
+ planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
4186
4230
  });
4187
4231
  function createTodoTool(options) {
4188
4232
  return tool4({
4189
- description: `Manage your task list for the current session. Use this to:
4190
- - Break down complex tasks into smaller steps
4191
- - Track progress on multi-step operations
4192
- - Organize your work systematically
4233
+ description: `Manage your task list and persistent plans for the current session.
4193
4234
 
4194
- Available actions:
4235
+ ## Todo Actions (for tracking current work)
4195
4236
  - "add": Add one or more new todo items to the list
4196
4237
  - "list": View all current todo items and their status
4197
4238
  - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
4198
4239
  - "clear": Remove all todo items from the list
4199
4240
 
4200
- Best practices:
4201
- - Add todos before starting complex tasks
4202
- - Mark items as "in_progress" when actively working on them
4203
- - Update status as you complete each step`,
4241
+ ## Plan Actions (for complex, multi-phase work)
4242
+ - "save_plan": Create or update a named plan \u2014 a persistent markdown document with hierarchical tasks, subtasks, and notes. Plans survive context compaction and are always available.
4243
+ - "list_plans": List all plans for this session
4244
+ - "get_plan": Read a specific plan by name
4245
+ - "delete_plan": Remove a plan
4246
+
4247
+ ## Plans vs Todos
4248
+ - **Plans** are the big picture \u2014 the full spec with phases, subtasks, notes, and decisions. They persist on disk and are always injected into your context, even after old messages are summarized.
4249
+ - **Todos** are your current focus \u2014 the immediate steps you're working on right now.
4250
+
4251
+ ## Workflow for complex tasks
4252
+ 1. Create a plan with phases and subtasks (save_plan)
4253
+ 2. Create todos from the first uncompleted phase (add)
4254
+ 3. Work through the todos, marking them as you go
4255
+ 4. When all current todos are done, update the plan (mark completed sections with [x]) and save it
4256
+ 5. Create new todos from the next uncompleted phase
4257
+ 6. Repeat until the plan is fully complete
4258
+
4259
+ ## Plan format
4260
+ Plans should be markdown with this structure:
4261
+ \`\`\`markdown
4262
+ # Plan: [Title]
4263
+
4264
+ ## Overview
4265
+ [What we're doing and why]
4266
+
4267
+ ## Phase 1: [Name] [completed]
4268
+ - [x] Task 1
4269
+ - [x] Task 2
4270
+
4271
+ ## Phase 2: [Name] [in_progress]
4272
+ - [x] Subtask 2.1
4273
+ - [ ] Subtask 2.2
4274
+ - [ ] Sub-subtask 2.2.1
4275
+ - [ ] Sub-subtask 2.2.2
4276
+ - [ ] Subtask 2.3
4277
+
4278
+ ## Phase 3: [Name] [pending]
4279
+ - [ ] Task 1
4280
+ - [ ] Task 2
4281
+
4282
+ ## Notes
4283
+ - Key decisions and context to preserve
4284
+ - Important file paths discovered
4285
+ \`\`\``,
4204
4286
  inputSchema: todoInputSchema,
4205
- execute: async ({ action, items, todoId, status }) => {
4287
+ execute: async ({ action, items, todoId, status, planName, planContent }) => {
4206
4288
  try {
4207
4289
  switch (action) {
4208
4290
  case "add": {
@@ -4270,6 +4352,81 @@ Best practices:
4270
4352
  itemsRemoved: count
4271
4353
  };
4272
4354
  }
4355
+ // ── Plan actions ─────────────────────────────────────────
4356
+ case "save_plan": {
4357
+ if (!planName) {
4358
+ return { success: false, error: 'planName is required for "save_plan"' };
4359
+ }
4360
+ if (!planContent) {
4361
+ return { success: false, error: 'planContent is required for "save_plan"' };
4362
+ }
4363
+ const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
4364
+ const filename = `${slugify(planName)}.md`;
4365
+ const filePath = join4(dir, filename);
4366
+ await writeFile4(filePath, planContent, "utf-8");
4367
+ return {
4368
+ success: true,
4369
+ action: "save_plan",
4370
+ planName,
4371
+ filename,
4372
+ path: filePath,
4373
+ sizeChars: planContent.length
4374
+ };
4375
+ }
4376
+ case "list_plans": {
4377
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
4378
+ if (!existsSync9(dir)) {
4379
+ return { success: true, action: "list_plans", plans: [], count: 0 };
4380
+ }
4381
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
4382
+ const plans = [];
4383
+ for (const f of files) {
4384
+ try {
4385
+ const content = await readFile6(join4(dir, f), "utf-8");
4386
+ const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
4387
+ plans.push({
4388
+ name: f.replace(/\.md$/, ""),
4389
+ title: titleMatch?.[1]?.trim() || f.replace(/\.md$/, ""),
4390
+ filename: f,
4391
+ sizeChars: content.length
4392
+ });
4393
+ } catch {
4394
+ }
4395
+ }
4396
+ return { success: true, action: "list_plans", plans, count: plans.length };
4397
+ }
4398
+ case "get_plan": {
4399
+ if (!planName) {
4400
+ return { success: false, error: 'planName is required for "get_plan"' };
4401
+ }
4402
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
4403
+ const filename = `${slugify(planName)}.md`;
4404
+ const filePath = join4(dir, filename);
4405
+ if (!existsSync9(filePath)) {
4406
+ return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
4407
+ }
4408
+ const content = await readFile6(filePath, "utf-8");
4409
+ return {
4410
+ success: true,
4411
+ action: "get_plan",
4412
+ planName,
4413
+ content,
4414
+ sizeChars: content.length
4415
+ };
4416
+ }
4417
+ case "delete_plan": {
4418
+ if (!planName) {
4419
+ return { success: false, error: 'planName is required for "delete_plan"' };
4420
+ }
4421
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
4422
+ const filename = `${slugify(planName)}.md`;
4423
+ const filePath = join4(dir, filename);
4424
+ if (!existsSync9(filePath)) {
4425
+ return { success: false, error: `Plan not found: "${planName}"` };
4426
+ }
4427
+ unlinkSync(filePath);
4428
+ return { success: true, action: "delete_plan", planName, deleted: true };
4429
+ }
4273
4430
  default:
4274
4431
  return {
4275
4432
  success: false,
@@ -4294,6 +4451,21 @@ function formatTodoItem(item) {
4294
4451
  createdAt: item.createdAt.toISOString()
4295
4452
  };
4296
4453
  }
4454
+ async function readSessionPlans(workingDirectory, sessionId) {
4455
+ const dir = getPlansDir(workingDirectory, sessionId);
4456
+ if (!existsSync9(dir)) return [];
4457
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
4458
+ if (files.length === 0) return [];
4459
+ const plans = [];
4460
+ for (const f of files) {
4461
+ try {
4462
+ const content = await readFile6(join4(dir, f), "utf-8");
4463
+ plans.push({ name: f.replace(/\.md$/, ""), content });
4464
+ } catch {
4465
+ }
4466
+ }
4467
+ return plans;
4468
+ }
4297
4469
 
4298
4470
  // src/tools/load-skill.ts
4299
4471
  init_skills();
@@ -4383,7 +4555,7 @@ Once loaded, a skill's content will be available in the conversation context.`,
4383
4555
  import { tool as tool6 } from "ai";
4384
4556
  import { z as z7 } from "zod";
4385
4557
  import { resolve as resolve7, relative as relative5, isAbsolute as isAbsolute3, extname as extname5 } from "path";
4386
- import { existsSync as existsSync10 } from "fs";
4558
+ import { existsSync as existsSync11 } from "fs";
4387
4559
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
4388
4560
  var linterInputSchema = z7.object({
4389
4561
  paths: z7.array(z7.string()).optional().describe("File or directory paths to check for lint errors. If not provided, returns diagnostics for all recently touched files."),
@@ -4451,7 +4623,7 @@ Working directory: ${options.workingDirectory}`,
4451
4623
  const filesToCheck = [];
4452
4624
  for (const path of paths) {
4453
4625
  const absolutePath = isAbsolute3(path) ? path : resolve7(options.workingDirectory, path);
4454
- if (!existsSync10(absolutePath)) {
4626
+ if (!existsSync11(absolutePath)) {
4455
4627
  continue;
4456
4628
  }
4457
4629
  const stats = await stat2(absolutePath);
@@ -4763,17 +4935,17 @@ import { tool as tool9 } from "ai";
4763
4935
  import { z as z10 } from "zod";
4764
4936
  import { exec as exec4 } from "child_process";
4765
4937
  import { promisify as promisify4 } from "util";
4766
- import { readFile as readFile8, stat as stat3, readdir as readdir4 } from "fs/promises";
4938
+ import { readFile as readFile9, stat as stat3, readdir as readdir4 } from "fs/promises";
4767
4939
  import { resolve as resolve9, relative as relative8, isAbsolute as isAbsolute5 } from "path";
4768
- import { existsSync as existsSync13 } from "fs";
4940
+ import { existsSync as existsSync14 } from "fs";
4769
4941
  init_semantic();
4770
4942
 
4771
4943
  // src/tools/code-graph.ts
4772
4944
  import { tool as tool7 } from "ai";
4773
4945
  import { z as z8 } from "zod";
4774
4946
  import { resolve as resolve8, relative as relative7, isAbsolute as isAbsolute4, basename as basename3 } from "path";
4775
- import { readFile as readFile7, readdir as readdir3 } from "fs/promises";
4776
- import { existsSync as existsSync11 } from "fs";
4947
+ import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
4948
+ import { existsSync as existsSync12 } from "fs";
4777
4949
  import { fileURLToPath as fileURLToPath2 } from "url";
4778
4950
  import { execFileSync } from "child_process";
4779
4951
  var codeGraphInputSchema = z8.object({
@@ -4910,7 +5082,7 @@ async function grepForSymbol(symbol, workingDirectory) {
4910
5082
  const ext = entry.name.substring(entry.name.lastIndexOf("."));
4911
5083
  if (!SUPPORTED_EXTS.has(ext)) continue;
4912
5084
  remaining--;
4913
- const content = await readFile7(fullPath, "utf-8");
5085
+ const content = await readFile8(fullPath, "utf-8");
4914
5086
  const lines = content.split("\n");
4915
5087
  for (let i = 0; i < lines.length; i++) {
4916
5088
  if (defPattern.test(lines[i])) {
@@ -4959,7 +5131,7 @@ Working directory: ${options.workingDirectory}`,
4959
5131
  let defSymbol = null;
4960
5132
  if (filePath) {
4961
5133
  const absPath = isAbsolute4(filePath) ? filePath : resolve8(options.workingDirectory, filePath);
4962
- if (!existsSync11(absPath)) {
5134
+ if (!existsSync12(absPath)) {
4963
5135
  return { success: false, error: `File not found: ${filePath}` };
4964
5136
  }
4965
5137
  if (!isSupported(absPath)) {
@@ -4973,7 +5145,7 @@ Working directory: ${options.workingDirectory}`,
4973
5145
  defLine = defSymbol.selectionRange.start.line;
4974
5146
  defChar = defSymbol.selectionRange.start.character;
4975
5147
  } else {
4976
- const content = await readFile7(absPath, "utf-8");
5148
+ const content = await readFile8(absPath, "utf-8");
4977
5149
  const lines2 = content.split("\n");
4978
5150
  const defPattern = new RegExp(
4979
5151
  `(export|function|const|let|var|class|interface|type|enum)\\s+.*\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`
@@ -5386,7 +5558,7 @@ Keep it concise but INCLUDE THE ACTUAL DATA.`;
5386
5558
  execute: async ({ path, startLine, endLine }) => {
5387
5559
  try {
5388
5560
  const absolutePath = isAbsolute5(path) ? path : resolve9(workingDirectory, path);
5389
- if (!existsSync13(absolutePath)) {
5561
+ if (!existsSync14(absolutePath)) {
5390
5562
  return {
5391
5563
  success: false,
5392
5564
  error: `File not found: ${path}`
@@ -5399,7 +5571,7 @@ Keep it concise but INCLUDE THE ACTUAL DATA.`;
5399
5571
  error: `File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Use startLine/endLine to read portions.`
5400
5572
  };
5401
5573
  }
5402
- let content = await readFile8(absolutePath, "utf-8");
5574
+ let content = await readFile9(absolutePath, "utf-8");
5403
5575
  if (startLine !== void 0 || endLine !== void 0) {
5404
5576
  const lines = content.split("\n");
5405
5577
  const start = (startLine ?? 1) - 1;
@@ -5430,7 +5602,7 @@ Keep it concise but INCLUDE THE ACTUAL DATA.`;
5430
5602
  execute: async ({ path, recursive, maxDepth }) => {
5431
5603
  try {
5432
5604
  const absolutePath = isAbsolute5(path) ? path : resolve9(workingDirectory, path);
5433
- if (!existsSync13(absolutePath)) {
5605
+ if (!existsSync14(absolutePath)) {
5434
5606
  return {
5435
5607
  success: false,
5436
5608
  error: `Directory not found: ${path}`
@@ -5797,8 +5969,8 @@ function createTaskFailedTool(options) {
5797
5969
  // src/tools/upload-file.ts
5798
5970
  import { tool as tool12 } from "ai";
5799
5971
  import { z as z13 } from "zod";
5800
- import { readFile as readFile9, stat as stat4 } from "fs/promises";
5801
- import { join as join6, basename as basename4, extname as extname7 } from "path";
5972
+ import { readFile as readFile10, stat as stat4 } from "fs/promises";
5973
+ import { join as join7, basename as basename4, extname as extname7 } from "path";
5802
5974
  var MIME_TYPES = {
5803
5975
  ".txt": "text/plain",
5804
5976
  ".md": "text/markdown",
@@ -5840,7 +6012,7 @@ function createUploadFileTool(options) {
5840
6012
  error: "File upload is not available \u2014 remote server with GCS is not configured."
5841
6013
  };
5842
6014
  }
5843
- const fullPath = input.path.startsWith("/") ? input.path : join6(options.workingDirectory, input.path);
6015
+ const fullPath = input.path.startsWith("/") ? input.path : join7(options.workingDirectory, input.path);
5844
6016
  try {
5845
6017
  await stat4(fullPath);
5846
6018
  } catch {
@@ -5858,7 +6030,7 @@ function createUploadFileTool(options) {
5858
6030
  contentType,
5859
6031
  "general"
5860
6032
  );
5861
- const fileData = await readFile9(fullPath);
6033
+ const fileData = await readFile10(fullPath);
5862
6034
  const putRes = await fetch(uploadInfo.uploadUrl, {
5863
6035
  method: "PUT",
5864
6036
  headers: { "Content-Type": contentType },
@@ -5913,7 +6085,8 @@ async function createTools(options) {
5913
6085
  onProgress: options.onWriteFileProgress
5914
6086
  }),
5915
6087
  todo: createTodoTool({
5916
- sessionId: options.sessionId
6088
+ sessionId: options.sessionId,
6089
+ workingDirectory: options.workingDirectory
5917
6090
  }),
5918
6091
  load_skill: createLoadSkillTool({
5919
6092
  sessionId: options.sessionId,
@@ -6011,6 +6184,8 @@ async function buildSystemPrompt(options) {
6011
6184
  }
6012
6185
  const todos = await todoQueries.getBySession(sessionId);
6013
6186
  const todosContext = formatTodosForContext(todos);
6187
+ const plans = await readSessionPlans(workingDirectory, sessionId);
6188
+ const plansContext = formatPlansForContext(plans);
6014
6189
  const platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
6015
6190
  const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
6016
6191
  const searchInstructions = getSearchInstructions();
@@ -6027,7 +6202,7 @@ You have access to powerful tools for:
6027
6202
  - **read_file**: Read file contents to understand code and context
6028
6203
  - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
6029
6204
  - **linter**: Check files for type errors and lint issues (TypeScript, JavaScript, TSX, JSX)
6030
- - **todo**: Manage your task list to track progress on complex operations
6205
+ - **todo**: Manage your task list AND persistent plans for complex multi-phase operations
6031
6206
  - **load_skill**: Load specialized knowledge documents for specific tasks
6032
6207
  - **explore_agent**: Explore agent for semantic discovery - for exploratory questions and finding code by meaning
6033
6208
  - **code_graph**: Inspect a symbol's type hierarchy and usage graph via the TypeScript language server
@@ -6036,9 +6211,23 @@ You have access to powerful tools for:
6036
6211
 
6037
6212
  IMPORTANT: If you have zero context of where you are working, always explore it first to understand the structure before doing things for the user.
6038
6213
 
6039
- Use the TODO tool to manage your task list to track progress on complex operations. Always ask the user what they want to do specifically before doing it, and make a plan.
6040
- Step 1 of the plan should be researching files and understanding the components/structure of what you're working on (if you don't already have context), then after u have done that, plan out the rest of the tasks u need to do.
6041
- You can clear the todo and restart it, and do multiple things inside of one session.
6214
+ ### Planning & Task Management
6215
+ Use the **todo tool** to manage both immediate tasks AND persistent plans:
6216
+
6217
+ **For simple tasks (< 5 steps):** Just use regular todos (add/mark/clear).
6218
+
6219
+ **For complex, multi-phase tasks:** Create a persistent **plan** first.
6220
+ 1. Research the codebase to understand what you're working with
6221
+ 2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks
6222
+ 3. Create todos from the first uncompleted phase
6223
+ 4. Work through the todos
6224
+ 5. When done, update the plan (mark completed phases with [x]), save it again
6225
+ 6. Create new todos from the next uncompleted phase
6226
+ 7. Repeat until the plan is fully complete
6227
+
6228
+ Plans persist on disk and are always injected into your context \u2014 they survive context compaction even in very long sessions. You can have multiple plans active at once (e.g., one for frontend, one for backend).
6229
+
6230
+ You can clear the todo list and restart it, and do multiple things inside of one session.
6042
6231
 
6043
6232
  ### bash Tool
6044
6233
  The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
@@ -6262,6 +6451,8 @@ ${onDemandSkillsContext}
6262
6451
  ## Current Task List
6263
6452
  ${todosContext}
6264
6453
 
6454
+ ${plansContext}
6455
+
6265
6456
  ${customInstructions ? `## Custom Instructions
6266
6457
  ${customInstructions}` : ""}
6267
6458
 
@@ -6285,6 +6476,37 @@ function formatTodosForContext(todos) {
6285
6476
  }
6286
6477
  return lines.join("\n");
6287
6478
  }
6479
+ var MAX_PLAN_CHARS = 3e4;
6480
+ var MAX_TOTAL_PLANS_CHARS = 6e4;
6481
+ function formatPlansForContext(plans) {
6482
+ if (plans.length === 0) return "";
6483
+ let totalChars = 0;
6484
+ const sections = [];
6485
+ sections.push(`## Persistent Plans (${plans.length})`);
6486
+ sections.push("");
6487
+ sections.push("These plans persist across context compaction \u2014 they are always available.");
6488
+ sections.push("When you finish your current todos, check these plans for the next uncompleted phase,");
6489
+ sections.push("update the plan (mark completed items with [x]), then create new todos for the next phase.");
6490
+ sections.push("");
6491
+ for (const plan of plans) {
6492
+ let content = plan.content;
6493
+ if (content.length > MAX_PLAN_CHARS) {
6494
+ content = content.slice(0, MAX_PLAN_CHARS) + `
6495
+
6496
+ ... [plan truncated \u2014 ${content.length - MAX_PLAN_CHARS} chars omitted. Use get_plan to read the full plan.]`;
6497
+ }
6498
+ if (totalChars + content.length > MAX_TOTAL_PLANS_CHARS) {
6499
+ sections.push(`### \u{1F4CB} Plan: ${plan.name} [truncated \u2014 use get_plan("${plan.name}") to read]`);
6500
+ continue;
6501
+ }
6502
+ sections.push(`### \u{1F4CB} Plan: ${plan.name}`);
6503
+ sections.push("");
6504
+ sections.push(content);
6505
+ sections.push("");
6506
+ totalChars += content.length;
6507
+ }
6508
+ return sections.join("\n");
6509
+ }
6288
6510
  function buildTaskPromptAddendum(outputSchema) {
6289
6511
  return `
6290
6512
  ## Task Mode
@@ -6356,7 +6578,7 @@ Before calling \`complete_task\`, you MUST verify your work completely. Do not j
6356
6578
  - **load_skill**: Load specialized skills/knowledge relevant to the task. Check what skills are available and use them.
6357
6579
  - **explore_agent**: Use for codebase exploration and understanding before making changes.
6358
6580
  - **code_graph**: Use to understand type hierarchies, references, and impact before refactoring.
6359
- - **todo**: Track your progress on multi-step tasks so you don't miss steps.
6581
+ - **todo**: Track your progress on multi-step tasks so you don't miss steps. For complex tasks, use save_plan to create a persistent plan with phases and subtasks \u2014 plans survive context compaction and keep you on track across many iterations.
6360
6582
  - **bash**: Full shell access \u2014 run builds, tests, dev servers, open browsers, curl endpoints, anything.
6361
6583
  - **upload_file**: Upload files (screenshots, reports, exports) to cloud storage. Use this to include screenshots of completed work in your task result \u2014 visual proof is very helpful.
6362
6584
 
@@ -7109,12 +7331,14 @@ ${prompt}` });
7109
7331
  const config = getConfig();
7110
7332
  const maxIterations = options.taskConfig.maxIterations ?? 50;
7111
7333
  const webhookUrl = options.taskConfig.webhookUrl;
7334
+ const parentTaskId = options.taskConfig.parentTaskId;
7112
7335
  const fireWebhook = (type, data) => {
7113
7336
  if (!webhookUrl) return;
7114
7337
  sendWebhook(webhookUrl, {
7115
7338
  type,
7116
7339
  taskId: this.session.id,
7117
7340
  sessionId: this.session.id,
7341
+ ...parentTaskId ? { parentTaskId } : {},
7118
7342
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7119
7343
  data
7120
7344
  });
@@ -7307,14 +7531,14 @@ ${taskAddendum}`;
7307
7531
  for (const step of resultSteps) {
7308
7532
  if (step.toolCalls) {
7309
7533
  for (const tc of step.toolCalls) {
7310
- options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.args });
7311
- fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.args });
7534
+ options.onToolCall?.({ toolCallId: tc.toolCallId, toolName: tc.toolName, input: tc.input });
7535
+ fireWebhook("task.tool_call", { iteration, toolName: tc.toolName, toolCallId: tc.toolCallId, input: tc.input });
7312
7536
  }
7313
7537
  }
7314
7538
  if (step.toolResults) {
7315
7539
  for (const tr of step.toolResults) {
7316
- options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.result });
7317
- fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.result });
7540
+ options.onToolResult?.({ toolCallId: tr.toolCallId, toolName: tr.toolName, output: tr.output });
7541
+ fireWebhook("task.tool_result", { iteration, toolName: tr.toolName, toolCallId: tr.toolCallId, output: tr.output });
7318
7542
  }
7319
7543
  }
7320
7544
  }
@@ -7406,14 +7630,14 @@ ${taskAddendum}`;
7406
7630
  const result = await recorder.encode();
7407
7631
  recorder.clear();
7408
7632
  if (!result) return [];
7409
- const { readFile: readFile11, unlink: unlink3 } = await import("fs/promises");
7633
+ const { readFile: readFile12, unlink: unlink3 } = await import("fs/promises");
7410
7634
  const uploadInfo = await storageQueries2.getUploadUrl(
7411
7635
  this.session.id,
7412
7636
  `browser-recording-${Date.now()}.mp4`,
7413
7637
  "video/mp4",
7414
7638
  "browser-recording"
7415
7639
  );
7416
- const fileData = await readFile11(result.path);
7640
+ const fileData = await readFile12(result.path);
7417
7641
  await fetch(uploadInfo.uploadUrl, {
7418
7642
  method: "PUT",
7419
7643
  headers: { "Content-Type": "video/mp4" },
@@ -7439,12 +7663,12 @@ ${taskAddendum}`;
7439
7663
  try {
7440
7664
  const { isRemoteConfigured: isRemoteConfigured2, storageQueries: storageQueries2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
7441
7665
  if (!isRemoteConfigured2()) return [];
7442
- const { readFile: readFile11 } = await import("fs/promises");
7443
- const { join: join12, basename: basename6 } = await import("path");
7666
+ const { readFile: readFile12 } = await import("fs/promises");
7667
+ const { join: join13, basename: basename6 } = await import("path");
7444
7668
  const urls = [];
7445
7669
  for (const filePath of filePaths) {
7446
7670
  try {
7447
- const fullPath = filePath.startsWith("/") ? filePath : join12(this.session.workingDirectory, filePath);
7671
+ const fullPath = filePath.startsWith("/") ? filePath : join13(this.session.workingDirectory, filePath);
7448
7672
  const fileName = basename6(fullPath);
7449
7673
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
7450
7674
  const mimeMap = {
@@ -7469,7 +7693,7 @@ ${taskAddendum}`;
7469
7693
  contentType,
7470
7694
  "task-output"
7471
7695
  );
7472
- const fileData = await readFile11(fullPath);
7696
+ const fileData = await readFile12(fullPath);
7473
7697
  await fetch(uploadInfo.uploadUrl, {
7474
7698
  method: "PUT",
7475
7699
  headers: { "Content-Type": contentType },
@@ -8074,12 +8298,12 @@ sessions.get("/:id/diff/:filePath", async (c) => {
8074
8298
  });
8075
8299
  function getAttachmentsDir(sessionId) {
8076
8300
  const appDataDir = getAppDataDirectory();
8077
- return join8(appDataDir, "attachments", sessionId);
8301
+ return join9(appDataDir, "attachments", sessionId);
8078
8302
  }
8079
8303
  function ensureAttachmentsDir(sessionId) {
8080
8304
  const dir = getAttachmentsDir(sessionId);
8081
- if (!existsSync14(dir)) {
8082
- mkdirSync4(dir, { recursive: true });
8305
+ if (!existsSync15(dir)) {
8306
+ mkdirSync5(dir, { recursive: true });
8083
8307
  }
8084
8308
  return dir;
8085
8309
  }
@@ -8090,12 +8314,12 @@ sessions.get("/:id/attachments", async (c) => {
8090
8314
  return c.json({ error: "Session not found" }, 404);
8091
8315
  }
8092
8316
  const dir = getAttachmentsDir(sessionId);
8093
- if (!existsSync14(dir)) {
8317
+ if (!existsSync15(dir)) {
8094
8318
  return c.json({ sessionId, attachments: [], count: 0 });
8095
8319
  }
8096
- const files = readdirSync(dir);
8320
+ const files = readdirSync2(dir);
8097
8321
  const attachments = files.map((filename) => {
8098
- const filePath = join8(dir, filename);
8322
+ const filePath = join9(dir, filename);
8099
8323
  const stats = statSync2(filePath);
8100
8324
  return {
8101
8325
  id: filename.split("_")[0],
@@ -8130,7 +8354,7 @@ sessions.post("/:id/attachments", async (c) => {
8130
8354
  const id = nanoid5(10);
8131
8355
  const ext = extname8(file.name) || "";
8132
8356
  const safeFilename = `${id}_${basename5(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
8133
- const filePath = join8(dir, safeFilename);
8357
+ const filePath = join9(dir, safeFilename);
8134
8358
  const arrayBuffer = await file.arrayBuffer();
8135
8359
  writeFileSync3(filePath, Buffer.from(arrayBuffer));
8136
8360
  return c.json({
@@ -8156,7 +8380,7 @@ sessions.post("/:id/attachments", async (c) => {
8156
8380
  const id = nanoid5(10);
8157
8381
  const ext = extname8(body.filename) || "";
8158
8382
  const safeFilename = `${id}_${basename5(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
8159
- const filePath = join8(dir, safeFilename);
8383
+ const filePath = join9(dir, safeFilename);
8160
8384
  let base64Data = body.data;
8161
8385
  if (base64Data.includes(",")) {
8162
8386
  base64Data = base64Data.split(",")[1];
@@ -8185,16 +8409,16 @@ sessions.delete("/:id/attachments/:attachmentId", async (c) => {
8185
8409
  return c.json({ error: "Session not found" }, 404);
8186
8410
  }
8187
8411
  const dir = getAttachmentsDir(sessionId);
8188
- if (!existsSync14(dir)) {
8412
+ if (!existsSync15(dir)) {
8189
8413
  return c.json({ error: "Attachment not found" }, 404);
8190
8414
  }
8191
- const files = readdirSync(dir);
8415
+ const files = readdirSync2(dir);
8192
8416
  const file = files.find((f) => f.startsWith(attachmentId + "_"));
8193
8417
  if (!file) {
8194
8418
  return c.json({ error: "Attachment not found" }, 404);
8195
8419
  }
8196
- const filePath = join8(dir, file);
8197
- unlinkSync(filePath);
8420
+ const filePath = join9(dir, file);
8421
+ unlinkSync2(filePath);
8198
8422
  return c.json({ success: true, id: attachmentId });
8199
8423
  });
8200
8424
  var filesQuerySchema = z15.object({
@@ -8276,7 +8500,7 @@ async function listWorkspaceFiles(baseDir, currentDir, query, limit, results = [
8276
8500
  const entries = await readdir6(currentDir, { withFileTypes: true });
8277
8501
  for (const entry of entries) {
8278
8502
  if (results.length >= limit * 2) break;
8279
- const fullPath = join8(currentDir, entry.name);
8503
+ const fullPath = join9(currentDir, entry.name);
8280
8504
  const relativePath = relative9(baseDir, fullPath);
8281
8505
  if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) {
8282
8506
  continue;
@@ -8324,7 +8548,7 @@ sessions.get(
8324
8548
  return c.json({ error: "Session not found" }, 404);
8325
8549
  }
8326
8550
  const workingDirectory = session.workingDirectory;
8327
- if (!existsSync14(workingDirectory)) {
8551
+ if (!existsSync15(workingDirectory)) {
8328
8552
  return c.json({
8329
8553
  sessionId,
8330
8554
  workingDirectory,
@@ -8435,8 +8659,8 @@ init_db();
8435
8659
  import { Hono as Hono2 } from "hono";
8436
8660
  import { zValidator as zValidator2 } from "@hono/zod-validator";
8437
8661
  import { z as z16 } from "zod";
8438
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
8439
- import { join as join9 } from "path";
8662
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
8663
+ import { join as join10 } from "path";
8440
8664
  init_config();
8441
8665
 
8442
8666
  // src/server/resumable-stream.ts
@@ -8642,12 +8866,12 @@ var rejectSchema = z16.object({
8642
8866
  var streamAbortControllers = /* @__PURE__ */ new Map();
8643
8867
  function getAttachmentsDirectory(sessionId) {
8644
8868
  const appDataDir = getAppDataDirectory();
8645
- return join9(appDataDir, "attachments", sessionId);
8869
+ return join10(appDataDir, "attachments", sessionId);
8646
8870
  }
8647
8871
  async function saveAttachmentToDisk(sessionId, attachment, index) {
8648
8872
  const attachmentsDir = getAttachmentsDirectory(sessionId);
8649
- if (!existsSync15(attachmentsDir)) {
8650
- mkdirSync5(attachmentsDir, { recursive: true });
8873
+ if (!existsSync16(attachmentsDir)) {
8874
+ mkdirSync6(attachmentsDir, { recursive: true });
8651
8875
  }
8652
8876
  let filename = attachment.filename;
8653
8877
  if (!filename) {
@@ -8665,7 +8889,7 @@ async function saveAttachmentToDisk(sessionId, attachment, index) {
8665
8889
  attachment.mediaType = resized.mediaType;
8666
8890
  attachment.data = buffer.toString("base64");
8667
8891
  }
8668
- const filePath = join9(attachmentsDir, filename);
8892
+ const filePath = join10(attachmentsDir, filename);
8669
8893
  writeFileSync4(filePath, buffer);
8670
8894
  return filePath;
8671
8895
  }
@@ -9606,26 +9830,26 @@ init_config();
9606
9830
  import { Hono as Hono3 } from "hono";
9607
9831
  import { zValidator as zValidator3 } from "@hono/zod-validator";
9608
9832
  import { z as z17 } from "zod";
9609
- import { readFileSync as readFileSync6 } from "fs";
9833
+ import { readFileSync as readFileSync7 } from "fs";
9610
9834
  import { fileURLToPath as fileURLToPath3 } from "url";
9611
- import { dirname as dirname6, join as join10 } from "path";
9835
+ import { dirname as dirname6, join as join11 } from "path";
9612
9836
  var __filename = fileURLToPath3(import.meta.url);
9613
9837
  var __dirname = dirname6(__filename);
9614
9838
  var possiblePaths = [
9615
- join10(__dirname, "../package.json"),
9839
+ join11(__dirname, "../package.json"),
9616
9840
  // From dist/server -> dist/../package.json
9617
- join10(__dirname, "../../package.json"),
9841
+ join11(__dirname, "../../package.json"),
9618
9842
  // From dist/server (if nested differently)
9619
- join10(__dirname, "../../../package.json"),
9843
+ join11(__dirname, "../../../package.json"),
9620
9844
  // From src/server/routes (development)
9621
- join10(process.cwd(), "package.json")
9845
+ join11(process.cwd(), "package.json")
9622
9846
  // From current working directory
9623
9847
  ];
9624
9848
  var currentVersion = "0.0.0";
9625
9849
  var packageName = "sparkecoder";
9626
9850
  for (const packageJsonPath of possiblePaths) {
9627
9851
  try {
9628
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
9852
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
9629
9853
  if (packageJson.name === "sparkecoder") {
9630
9854
  currentVersion = packageJson.version || "0.0.0";
9631
9855
  packageName = packageJson.name || "sparkecoder";
@@ -10195,6 +10419,7 @@ tasks.post(
10195
10419
  type: "task.failed",
10196
10420
  taskId,
10197
10421
  sessionId: taskId,
10422
+ ...taskConfig.parentTaskId ? { parentTaskId: taskConfig.parentTaskId } : {},
10198
10423
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10199
10424
  data: { status: "failed", error: errorMsg }
10200
10425
  });
@@ -10284,6 +10509,7 @@ tasks.post("/:id/cancel", async (c) => {
10284
10509
  type: "task.failed",
10285
10510
  taskId: id,
10286
10511
  sessionId: id,
10512
+ ...task.parentTaskId ? { parentTaskId: task.parentTaskId } : {},
10287
10513
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10288
10514
  data: { status: "failed", error: "Task cancelled by user" }
10289
10515
  });
@@ -10382,11 +10608,11 @@ function getWebDirectory() {
10382
10608
  try {
10383
10609
  const currentDir = dirname7(fileURLToPath4(import.meta.url));
10384
10610
  const webDir = resolve10(currentDir, "..", "web");
10385
- if (existsSync16(webDir) && existsSync16(join11(webDir, "package.json"))) {
10611
+ if (existsSync17(webDir) && existsSync17(join12(webDir, "package.json"))) {
10386
10612
  return webDir;
10387
10613
  }
10388
10614
  const altWebDir = resolve10(currentDir, "..", "..", "web");
10389
- if (existsSync16(altWebDir) && existsSync16(join11(altWebDir, "package.json"))) {
10615
+ if (existsSync17(altWebDir) && existsSync17(join12(altWebDir, "package.json"))) {
10390
10616
  return altWebDir;
10391
10617
  }
10392
10618
  return null;
@@ -10444,23 +10670,23 @@ async function findWebPort(preferredPort) {
10444
10670
  return { port: preferredPort, alreadyRunning: false };
10445
10671
  }
10446
10672
  function hasProductionBuild(webDir) {
10447
- const buildIdPath = join11(webDir, ".next", "BUILD_ID");
10448
- return existsSync16(buildIdPath);
10673
+ const buildIdPath = join12(webDir, ".next", "BUILD_ID");
10674
+ return existsSync17(buildIdPath);
10449
10675
  }
10450
10676
  function hasSourceFiles(webDir) {
10451
- const appDir = join11(webDir, "src", "app");
10452
- const pagesDir = join11(webDir, "src", "pages");
10453
- const rootAppDir = join11(webDir, "app");
10454
- const rootPagesDir = join11(webDir, "pages");
10455
- return existsSync16(appDir) || existsSync16(pagesDir) || existsSync16(rootAppDir) || existsSync16(rootPagesDir);
10677
+ const appDir = join12(webDir, "src", "app");
10678
+ const pagesDir = join12(webDir, "src", "pages");
10679
+ const rootAppDir = join12(webDir, "app");
10680
+ const rootPagesDir = join12(webDir, "pages");
10681
+ return existsSync17(appDir) || existsSync17(pagesDir) || existsSync17(rootAppDir) || existsSync17(rootPagesDir);
10456
10682
  }
10457
10683
  function getStandaloneServerPath(webDir) {
10458
10684
  const possiblePaths2 = [
10459
- join11(webDir, ".next", "standalone", "server.js"),
10460
- join11(webDir, ".next", "standalone", "web", "server.js")
10685
+ join12(webDir, ".next", "standalone", "server.js"),
10686
+ join12(webDir, ".next", "standalone", "web", "server.js")
10461
10687
  ];
10462
10688
  for (const serverPath of possiblePaths2) {
10463
- if (existsSync16(serverPath)) {
10689
+ if (existsSync17(serverPath)) {
10464
10690
  return serverPath;
10465
10691
  }
10466
10692
  }
@@ -10500,13 +10726,13 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
10500
10726
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
10501
10727
  return { process: null, port: actualPort };
10502
10728
  }
10503
- const usePnpm = existsSync16(join11(webDir, "pnpm-lock.yaml"));
10504
- const useNpm = !usePnpm && existsSync16(join11(webDir, "package-lock.json"));
10729
+ const usePnpm = existsSync17(join12(webDir, "pnpm-lock.yaml"));
10730
+ const useNpm = !usePnpm && existsSync17(join12(webDir, "package-lock.json"));
10505
10731
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
10506
10732
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
10507
10733
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
10508
10734
  const runtimeConfig = { apiBaseUrl: apiUrl };
10509
- const runtimeConfigPath = join11(webDir, "runtime-config.json");
10735
+ const runtimeConfigPath = join12(webDir, "runtime-config.json");
10510
10736
  try {
10511
10737
  writeFileSync5(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
10512
10738
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
@@ -10699,8 +10925,8 @@ async function startServer(options = {}) {
10699
10925
  if (options.workingDirectory) {
10700
10926
  config.resolvedWorkingDirectory = options.workingDirectory;
10701
10927
  }
10702
- if (!existsSync16(config.resolvedWorkingDirectory)) {
10703
- mkdirSync6(config.resolvedWorkingDirectory, { recursive: true });
10928
+ if (!existsSync17(config.resolvedWorkingDirectory)) {
10929
+ mkdirSync7(config.resolvedWorkingDirectory, { recursive: true });
10704
10930
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
10705
10931
  }
10706
10932
  if (!config.resolvedRemoteServer.url) {