sparkecoder 0.1.83 → 0.1.85

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 (136) hide show
  1. package/dist/agent/index.js +125 -22
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +532 -395
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +532 -395
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +532 -395
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/tools/index.d.ts +19 -36
  10. package/dist/tools/index.js +99 -10
  11. package/dist/tools/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/web/.next/BUILD_ID +1 -1
  14. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  15. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  16. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  17. package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
  18. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  19. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  20. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  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/page_client-reference-manifest.js +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  37. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
  43. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  44. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
  54. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  55. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
  64. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  69. package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  74. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
  77. package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
  78. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  79. package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
  80. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  81. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  82. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  83. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  85. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  86. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_02a118f9._.js → 2374f_00f7fe07._.js} +1 -1
  87. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ad08e83a._.js → 2374f_2801b766._.js} +1 -1
  88. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_0ed477f8._.js → 2374f_369747ce._.js} +1 -1
  89. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_3b51a934._.js → 2374f_60d8842c._.js} +1 -1
  90. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_acf3dfe4._.js → 2374f_806bd012._.js} +1 -1
  91. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_db3e363b._.js → 2374f_8dc0f9aa._.js} +1 -1
  92. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_f0d7e130._.js → 2374f_9adc1edb._.js} +1 -1
  93. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5ebfcf1a._.js → 2374f_b7f45fdf._.js} +1 -1
  94. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_12bad06e._.js → 2374f_c13c8f4f._.js} +1 -1
  95. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_fc992d90._.js → 2374f_cc6c6363._.js} +1 -1
  96. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_3e519469._.js → 2374f_d58d0276._.js} +1 -1
  97. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_a0f483d1._.js → 2374f_ecd2bdca._.js} +1 -1
  98. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_c1d54c16._.js → 2374f_f363c084._.js} +1 -1
  99. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_2526ca80._.js → 2374f_fdfc7f3d._.js} +1 -1
  100. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__06818a54._.js → [root-of-the-server]__25b25c9d._.js} +2 -2
  101. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__a1877334._.js → [root-of-the-server]__9d3a7cbf._.js} +4 -4
  102. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_cc5f7515._.js → web_08242997._.js} +2 -2
  103. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_2b3a5919._.js → web_123ffe97._.js} +2 -2
  104. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_38156da8._.js → web_99b01335._.js} +2 -2
  105. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  106. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  107. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  108. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  109. package/web/.next/standalone/web/.next/static/chunks/{f95d41079838994a.js → 2624c966c288fd41.js} +3 -3
  110. package/web/.next/standalone/web/.next/static/chunks/74b64476a24dd71e.css +1 -0
  111. package/web/.next/standalone/web/.next/static/{static/chunks/fc39a194539da104.js → chunks/8af263bc97c0c9ee.js} +1 -1
  112. package/web/.next/{static/chunks/2cafc7cb79454d33.js → standalone/web/.next/static/chunks/cfadc93a98190e5a.js} +1 -1
  113. package/web/.next/standalone/web/.next/static/static/chunks/{f95d41079838994a.js → 2624c966c288fd41.js} +3 -3
  114. package/web/.next/standalone/web/.next/static/static/chunks/74b64476a24dd71e.css +1 -0
  115. package/web/.next/{static/chunks/fc39a194539da104.js → standalone/web/.next/static/static/chunks/8af263bc97c0c9ee.js} +1 -1
  116. package/web/.next/standalone/web/.next/static/{chunks/2cafc7cb79454d33.js → static/chunks/cfadc93a98190e5a.js} +1 -1
  117. package/web/.next/standalone/web/src/components/ai-elements/todo-panel.tsx +194 -110
  118. package/web/.next/standalone/web/src/components/ai-elements/todo-tool.tsx +78 -1
  119. package/web/.next/standalone/web/src/components/chat-interface.tsx +15 -9
  120. package/web/.next/standalone/web/src/lib/api.ts +17 -0
  121. package/web/.next/static/chunks/{f95d41079838994a.js → 2624c966c288fd41.js} +3 -3
  122. package/web/.next/static/chunks/74b64476a24dd71e.css +1 -0
  123. package/web/.next/{standalone/web/.next/static/chunks/fc39a194539da104.js → static/chunks/8af263bc97c0c9ee.js} +1 -1
  124. package/web/.next/{standalone/web/.next/static/static/chunks/2cafc7cb79454d33.js → static/chunks/cfadc93a98190e5a.js} +1 -1
  125. package/web/.next/standalone/web/.next/static/chunks/41a5c049931b2c77.css +0 -1
  126. package/web/.next/standalone/web/.next/static/static/chunks/41a5c049931b2c77.css +0 -1
  127. package/web/.next/static/chunks/41a5c049931b2c77.css +0 -1
  128. /package/web/.next/standalone/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_buildManifest.js +0 -0
  129. /package/web/.next/standalone/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_clientMiddlewareManifest.json +0 -0
  130. /package/web/.next/standalone/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_ssgManifest.js +0 -0
  131. /package/web/.next/standalone/web/.next/static/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_buildManifest.js +0 -0
  132. /package/web/.next/standalone/web/.next/static/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_clientMiddlewareManifest.json +0 -0
  133. /package/web/.next/standalone/web/.next/static/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_ssgManifest.js +0 -0
  134. /package/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_buildManifest.js +0 -0
  135. /package/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_clientMiddlewareManifest.json +0 -0
  136. /package/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -1020,6 +1020,388 @@ var init_db = __esm({
1020
1020
  }
1021
1021
  });
1022
1022
 
1023
+ // src/tools/todo.ts
1024
+ var todo_exports = {};
1025
+ __export(todo_exports, {
1026
+ createTodoTool: () => createTodoTool,
1027
+ readSessionPlans: () => readSessionPlans
1028
+ });
1029
+ import { tool as tool4 } from "ai";
1030
+ import { z as z5 } from "zod";
1031
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
1032
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1033
+ import { join as join4 } from "path";
1034
+ function getPlansDir(workingDirectory, sessionId) {
1035
+ return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
1036
+ }
1037
+ function ensurePlansDir(workingDirectory, sessionId) {
1038
+ const dir = getPlansDir(workingDirectory, sessionId);
1039
+ if (!existsSync9(dir)) {
1040
+ mkdirSync4(dir, { recursive: true });
1041
+ }
1042
+ const gitignorePath = join4(workingDirectory, ".gitignore");
1043
+ if (existsSync9(gitignorePath)) {
1044
+ try {
1045
+ const content = readFileSync3(gitignorePath, "utf-8");
1046
+ if (!content.includes(".sparkecoder")) {
1047
+ appendFileSync(gitignorePath, "\n.sparkecoder/\n");
1048
+ }
1049
+ } catch {
1050
+ }
1051
+ }
1052
+ return dir;
1053
+ }
1054
+ function slugify(name) {
1055
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
1056
+ }
1057
+ function createTodoTool(options) {
1058
+ return tool4({
1059
+ description: `Manage your task list and persistent plans for the current session.
1060
+
1061
+ ## Todo Actions (for tracking current work)
1062
+ - "add": Add one or more new todo items to the list
1063
+ - "list": View all current todo items and their status
1064
+ - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
1065
+ - "clear": Remove all todo items from the list
1066
+
1067
+ ## Plan Actions (for complex, multi-phase work)
1068
+ - "save_plan": Create or update a named plan. When saved and no active todos exist, todos are AUTO-CREATED from the first uncompleted phase.
1069
+ - "list_plans": List all plans for this session
1070
+ - "get_plan": Read a specific plan by name
1071
+ - "delete_plan": Remove a plan
1072
+
1073
+ ## Plans vs Todos
1074
+ - **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.
1075
+ - **Todos** are your current focus \u2014 the immediate steps you're working on right now, auto-derived from plan phases.
1076
+
1077
+ ## Workflow for complex tasks
1078
+ 1. Create a plan with phases and subtasks using checkboxes (save_plan) \u2014 todos are auto-created from Phase 1
1079
+ 2. Work through the todos, marking them as you go
1080
+ 3. When all todos are done, update the plan: mark the completed phase heading with [completed] and its items with [x]
1081
+ 4. Call save_plan again with the updated content \u2014 todos are auto-created from the next uncompleted phase
1082
+ 5. Repeat until all phases are complete, then delete_plan
1083
+
1084
+ ## Auto-todo creation rules
1085
+ - Only triggers when there are NO active (pending/in_progress) todos
1086
+ - Skips phases with [completed] in the heading
1087
+ - Skips sections named Overview, Notes, Key Decisions, etc.
1088
+ - Only top-level checklist items (- [ ]) become todos \u2014 indented sub-items are task details
1089
+
1090
+ ## Plan format
1091
+ Plans should be markdown with this structure:
1092
+ \`\`\`markdown
1093
+ # Plan: [Title]
1094
+
1095
+ ## Overview
1096
+ [What we're doing and why]
1097
+
1098
+ ## Phase 1: [Name] [completed]
1099
+ - [x] Task 1
1100
+ - [x] Task 2
1101
+
1102
+ ## Phase 2: [Name] [in_progress]
1103
+ - [x] Subtask 2.1
1104
+ - [ ] Subtask 2.2
1105
+ - [ ] Sub-subtask 2.2.1
1106
+ - [ ] Sub-subtask 2.2.2
1107
+ - [ ] Subtask 2.3
1108
+
1109
+ ## Phase 3: [Name] [pending]
1110
+ - [ ] Task 1
1111
+ - [ ] Task 2
1112
+
1113
+ ## Notes
1114
+ - Key decisions and context to preserve
1115
+ - Important file paths discovered
1116
+ \`\`\``,
1117
+ inputSchema: todoInputSchema,
1118
+ execute: async ({ action, items, todoId, status, planName, planContent }) => {
1119
+ try {
1120
+ switch (action) {
1121
+ case "add": {
1122
+ if (!items || items.length === 0) {
1123
+ return {
1124
+ success: false,
1125
+ error: "No items provided. Include at least one todo item."
1126
+ };
1127
+ }
1128
+ const created = await todoQueries.createMany(options.sessionId, items);
1129
+ return {
1130
+ success: true,
1131
+ action: "add",
1132
+ itemsAdded: created.length,
1133
+ items: created.map(formatTodoItem)
1134
+ };
1135
+ }
1136
+ case "list": {
1137
+ const todos = await todoQueries.getBySession(options.sessionId);
1138
+ const stats = {
1139
+ total: todos.length,
1140
+ pending: todos.filter((t) => t.status === "pending").length,
1141
+ inProgress: todos.filter((t) => t.status === "in_progress").length,
1142
+ completed: todos.filter((t) => t.status === "completed").length,
1143
+ cancelled: todos.filter((t) => t.status === "cancelled").length
1144
+ };
1145
+ return {
1146
+ success: true,
1147
+ action: "list",
1148
+ stats,
1149
+ items: todos.map(formatTodoItem)
1150
+ };
1151
+ }
1152
+ case "mark": {
1153
+ if (!todoId) {
1154
+ return {
1155
+ success: false,
1156
+ error: 'todoId is required for "mark" action'
1157
+ };
1158
+ }
1159
+ if (!status) {
1160
+ return {
1161
+ success: false,
1162
+ error: 'status is required for "mark" action'
1163
+ };
1164
+ }
1165
+ const updated = await todoQueries.updateStatus(todoId, status);
1166
+ if (!updated) {
1167
+ return {
1168
+ success: false,
1169
+ error: `Todo item not found: ${todoId}`
1170
+ };
1171
+ }
1172
+ let planContinuation;
1173
+ if (status === "completed") {
1174
+ const allTodos = await todoQueries.getBySession(options.sessionId);
1175
+ const allDone = allTodos.every(
1176
+ (t) => t.status === "completed" || t.status === "cancelled"
1177
+ );
1178
+ if (allDone) {
1179
+ const plansDir = getPlansDir(options.workingDirectory, options.sessionId);
1180
+ if (existsSync9(plansDir)) {
1181
+ const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1182
+ for (const f of planFiles) {
1183
+ try {
1184
+ const content = await readFile6(join4(plansDir, f), "utf-8");
1185
+ if (parseNextUncompletedPhase(content) !== null) {
1186
+ planContinuation = "All todos are done but your plan has remaining phases. Update the plan now: mark the completed phase heading with [completed] and its items with [x], then call save_plan to save \u2014 new todos will be auto-created from the next phase.";
1187
+ break;
1188
+ }
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ return {
1196
+ success: true,
1197
+ action: "mark",
1198
+ item: formatTodoItem(updated),
1199
+ ...planContinuation ? { planContinuation } : {}
1200
+ };
1201
+ }
1202
+ case "clear": {
1203
+ const count = await todoQueries.clearSession(options.sessionId);
1204
+ return {
1205
+ success: true,
1206
+ action: "clear",
1207
+ itemsRemoved: count
1208
+ };
1209
+ }
1210
+ // ── Plan actions ─────────────────────────────────────────
1211
+ case "save_plan": {
1212
+ if (!planName) {
1213
+ return { success: false, error: 'planName is required for "save_plan"' };
1214
+ }
1215
+ if (!planContent) {
1216
+ return { success: false, error: 'planContent is required for "save_plan"' };
1217
+ }
1218
+ const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
1219
+ const filename = `${slugify(planName)}.md`;
1220
+ const filePath = join4(dir, filename);
1221
+ await writeFile4(filePath, planContent, "utf-8");
1222
+ const existingTodos = await todoQueries.getBySession(options.sessionId);
1223
+ const hasActiveTodos = existingTodos.some(
1224
+ (t) => t.status === "pending" || t.status === "in_progress"
1225
+ );
1226
+ let autoCreatedTodos = [];
1227
+ if (!hasActiveTodos) {
1228
+ const nextPhase = parseNextUncompletedPhase(planContent);
1229
+ if (nextPhase) {
1230
+ const created = await todoQueries.createMany(
1231
+ options.sessionId,
1232
+ nextPhase.tasks.map((task, i) => ({ content: task, order: i }))
1233
+ );
1234
+ autoCreatedTodos = created.map(formatTodoItem);
1235
+ }
1236
+ }
1237
+ return {
1238
+ success: true,
1239
+ action: "save_plan",
1240
+ planName,
1241
+ filename,
1242
+ path: filePath,
1243
+ sizeChars: planContent.length,
1244
+ ...autoCreatedTodos.length > 0 ? {
1245
+ autoCreatedTodos,
1246
+ autoCreatedFromPhase: "Created todos from the first uncompleted phase. Start working on them!"
1247
+ } : {}
1248
+ };
1249
+ }
1250
+ case "list_plans": {
1251
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
1252
+ if (!existsSync9(dir)) {
1253
+ return { success: true, action: "list_plans", plans: [], count: 0 };
1254
+ }
1255
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
1256
+ const plans = [];
1257
+ for (const f of files) {
1258
+ try {
1259
+ const content = await readFile6(join4(dir, f), "utf-8");
1260
+ const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
1261
+ plans.push({
1262
+ name: f.replace(/\.md$/, ""),
1263
+ title: titleMatch?.[1]?.trim() || f.replace(/\.md$/, ""),
1264
+ filename: f,
1265
+ sizeChars: content.length
1266
+ });
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ return { success: true, action: "list_plans", plans, count: plans.length };
1271
+ }
1272
+ case "get_plan": {
1273
+ if (!planName) {
1274
+ return { success: false, error: 'planName is required for "get_plan"' };
1275
+ }
1276
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
1277
+ const filename = `${slugify(planName)}.md`;
1278
+ const filePath = join4(dir, filename);
1279
+ if (!existsSync9(filePath)) {
1280
+ return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
1281
+ }
1282
+ const content = await readFile6(filePath, "utf-8");
1283
+ return {
1284
+ success: true,
1285
+ action: "get_plan",
1286
+ planName,
1287
+ content,
1288
+ sizeChars: content.length
1289
+ };
1290
+ }
1291
+ case "delete_plan": {
1292
+ if (!planName) {
1293
+ return { success: false, error: 'planName is required for "delete_plan"' };
1294
+ }
1295
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
1296
+ const filename = `${slugify(planName)}.md`;
1297
+ const filePath = join4(dir, filename);
1298
+ if (!existsSync9(filePath)) {
1299
+ return { success: false, error: `Plan not found: "${planName}"` };
1300
+ }
1301
+ unlinkSync(filePath);
1302
+ return { success: true, action: "delete_plan", planName, deleted: true };
1303
+ }
1304
+ default:
1305
+ return {
1306
+ success: false,
1307
+ error: `Unknown action: ${action}`
1308
+ };
1309
+ }
1310
+ } catch (error) {
1311
+ return {
1312
+ success: false,
1313
+ error: error.message
1314
+ };
1315
+ }
1316
+ }
1317
+ });
1318
+ }
1319
+ function formatTodoItem(item) {
1320
+ return {
1321
+ id: item.id,
1322
+ content: item.content,
1323
+ status: item.status,
1324
+ order: item.order,
1325
+ createdAt: item.createdAt.toISOString()
1326
+ };
1327
+ }
1328
+ function parseNextUncompletedPhase(content) {
1329
+ const lines = content.split("\n");
1330
+ let currentPhase = null;
1331
+ let currentTasks = [];
1332
+ let hasUncompletedTask = false;
1333
+ for (const line of lines) {
1334
+ const h2Match = line.match(/^##\s+(.+)/);
1335
+ if (h2Match) {
1336
+ if (currentPhase && hasUncompletedTask && currentTasks.length > 0) {
1337
+ return { phaseName: currentPhase, tasks: currentTasks };
1338
+ }
1339
+ const headingText = h2Match[1].trim();
1340
+ if (/^(overview|notes|key decisions|context|summary|references)\b/i.test(headingText)) {
1341
+ currentPhase = null;
1342
+ currentTasks = [];
1343
+ hasUncompletedTask = false;
1344
+ continue;
1345
+ }
1346
+ if (/\[completed\]/i.test(headingText)) {
1347
+ currentPhase = null;
1348
+ currentTasks = [];
1349
+ hasUncompletedTask = false;
1350
+ continue;
1351
+ }
1352
+ currentPhase = headingText.replace(/\s*\[(in_progress|pending|completed)\]\s*/gi, "").replace(/^Phase\s+\d+[:\s]*/i, "").trim();
1353
+ currentTasks = [];
1354
+ hasUncompletedTask = false;
1355
+ continue;
1356
+ }
1357
+ if (!currentPhase) continue;
1358
+ const uncheckedMatch = line.match(/^[-*]\s+\[\s\]\s+(.+)/);
1359
+ if (uncheckedMatch) {
1360
+ currentTasks.push(uncheckedMatch[1].trim());
1361
+ hasUncompletedTask = true;
1362
+ }
1363
+ }
1364
+ if (currentPhase && hasUncompletedTask && currentTasks.length > 0) {
1365
+ return { phaseName: currentPhase, tasks: currentTasks };
1366
+ }
1367
+ return null;
1368
+ }
1369
+ async function readSessionPlans(workingDirectory, sessionId) {
1370
+ const dir = getPlansDir(workingDirectory, sessionId);
1371
+ if (!existsSync9(dir)) return [];
1372
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
1373
+ if (files.length === 0) return [];
1374
+ const plans = [];
1375
+ for (const f of files) {
1376
+ try {
1377
+ const content = await readFile6(join4(dir, f), "utf-8");
1378
+ plans.push({ name: f.replace(/\.md$/, ""), content });
1379
+ } catch {
1380
+ }
1381
+ }
1382
+ return plans;
1383
+ }
1384
+ var todoInputSchema;
1385
+ var init_todo = __esm({
1386
+ "src/tools/todo.ts"() {
1387
+ "use strict";
1388
+ init_db();
1389
+ todoInputSchema = z5.object({
1390
+ action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
1391
+ items: z5.array(
1392
+ z5.object({
1393
+ content: z5.string().describe("Description of the task"),
1394
+ order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
1395
+ })
1396
+ ).optional().describe('For "add" action: Array of todo items to add'),
1397
+ todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
1398
+ status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
1399
+ planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
1400
+ planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
1401
+ });
1402
+ }
1403
+ });
1404
+
1023
1405
  // src/skills/index.ts
1024
1406
  var skills_exports = {};
1025
1407
  __export(skills_exports, {
@@ -4053,381 +4435,123 @@ Working directory: ${options.workingDirectory}`,
4053
4435
  chunkIndex: i,
4054
4436
  chunkCount,
4055
4437
  chunkStart,
4056
- isChunked: true
4057
- });
4058
- if (chunkCount > 1) {
4059
- await new Promise((resolve11) => setTimeout(resolve11, 0));
4060
- }
4061
- }
4062
- }
4063
- await backupFile(options.sessionId, options.workingDirectory, absolutePath);
4064
- const dir = dirname5(absolutePath);
4065
- if (!existsSync8(dir)) {
4066
- await mkdir3(dir, { recursive: true });
4067
- }
4068
- await writeFile3(absolutePath, content, "utf-8");
4069
- let diagnosticsOutput = "";
4070
- if (options.enableLSP !== false && isSupported(absolutePath)) {
4071
- await touchFile(absolutePath, true);
4072
- diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
4073
- }
4074
- options.onProgress?.({
4075
- path: absolutePath,
4076
- relativePath,
4077
- mode: "full",
4078
- status: "completed",
4079
- action,
4080
- totalLength: content.length
4081
- });
4082
- return {
4083
- success: true,
4084
- path: absolutePath,
4085
- relativePath,
4086
- mode: "full",
4087
- action,
4088
- bytesWritten: Buffer.byteLength(content, "utf-8"),
4089
- lineCount: content.split("\n").length,
4090
- ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
4091
- };
4092
- } else if (mode === "str_replace") {
4093
- if (old_string === void 0 || new_string === void 0) {
4094
- return {
4095
- success: false,
4096
- error: 'Both old_string and new_string are required for "str_replace" mode'
4097
- };
4098
- }
4099
- if (!existsSync8(absolutePath)) {
4100
- return {
4101
- success: false,
4102
- error: `File not found: ${path}. Use "full" mode to create new files.`
4103
- };
4104
- }
4105
- options.onProgress?.({
4106
- path: absolutePath,
4107
- relativePath,
4108
- mode: "str_replace",
4109
- status: "started",
4110
- action: "edited"
4111
- });
4112
- options.onProgress?.({
4113
- path: absolutePath,
4114
- relativePath,
4115
- mode: "str_replace",
4116
- status: "content",
4117
- oldString: old_string,
4118
- newString: new_string,
4119
- action: "edited"
4120
- });
4121
- await backupFile(options.sessionId, options.workingDirectory, absolutePath);
4122
- const currentContent = await readFile5(absolutePath, "utf-8");
4123
- if (!currentContent.includes(old_string)) {
4124
- const lines = currentContent.split("\n");
4125
- const preview = lines.slice(0, 20).join("\n");
4126
- return {
4127
- success: false,
4128
- error: "old_string not found in file. The string must match EXACTLY including whitespace.",
4129
- hint: "Check for differences in indentation, line endings, or invisible characters.",
4130
- filePreview: lines.length > 20 ? `${preview}
4131
- ... (${lines.length - 20} more lines)` : preview
4132
- };
4133
- }
4134
- const occurrences = currentContent.split(old_string).length - 1;
4135
- if (occurrences > 1) {
4136
- return {
4137
- success: false,
4138
- error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
4139
- hint: "Include surrounding lines or more specific content in old_string."
4140
- };
4141
- }
4142
- const newContent = currentContent.replace(old_string, new_string);
4143
- await writeFile3(absolutePath, newContent, "utf-8");
4144
- const oldLines = old_string.split("\n").length;
4145
- const newLines = new_string.split("\n").length;
4146
- let diagnosticsOutput = "";
4147
- if (options.enableLSP !== false && isSupported(absolutePath)) {
4148
- await touchFile(absolutePath, true);
4149
- diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
4150
- }
4151
- options.onProgress?.({
4152
- path: absolutePath,
4153
- relativePath,
4154
- mode: "str_replace",
4155
- status: "completed",
4156
- action: "edited"
4157
- });
4158
- return {
4159
- success: true,
4160
- path: absolutePath,
4161
- relativePath,
4162
- mode: "str_replace",
4163
- linesRemoved: oldLines,
4164
- linesAdded: newLines,
4165
- lineDelta: newLines - oldLines,
4166
- ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
4167
- };
4168
- }
4169
- return {
4170
- success: false,
4171
- error: `Invalid mode: ${mode}`
4172
- };
4173
- } catch (error) {
4174
- return {
4175
- success: false,
4176
- error: error.message
4177
- };
4178
- }
4179
- }
4180
- });
4181
- }
4182
-
4183
- // src/tools/todo.ts
4184
- init_db();
4185
- import { tool as tool4 } from "ai";
4186
- import { z as z5 } from "zod";
4187
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
4188
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
4189
- import { join as join4 } from "path";
4190
- function getPlansDir(workingDirectory, sessionId) {
4191
- return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
4192
- }
4193
- function ensurePlansDir(workingDirectory, sessionId) {
4194
- const dir = getPlansDir(workingDirectory, sessionId);
4195
- if (!existsSync9(dir)) {
4196
- mkdirSync4(dir, { recursive: true });
4197
- }
4198
- const gitignorePath = join4(workingDirectory, ".gitignore");
4199
- if (existsSync9(gitignorePath)) {
4200
- try {
4201
- const content = readFileSync3(gitignorePath, "utf-8");
4202
- if (!content.includes(".sparkecoder")) {
4203
- appendFileSync(gitignorePath, "\n.sparkecoder/\n");
4204
- }
4205
- } catch {
4206
- }
4207
- }
4208
- return dir;
4209
- }
4210
- function slugify(name) {
4211
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
4212
- }
4213
- var todoInputSchema = z5.object({
4214
- action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
4215
- items: z5.array(
4216
- z5.object({
4217
- content: z5.string().describe("Description of the task"),
4218
- order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
4219
- })
4220
- ).optional().describe('For "add" action: Array of todo items to add'),
4221
- todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
4222
- status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
4223
- planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
4224
- planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
4225
- });
4226
- function createTodoTool(options) {
4227
- return tool4({
4228
- description: `Manage your task list and persistent plans for the current session.
4229
-
4230
- ## Todo Actions (for tracking current work)
4231
- - "add": Add one or more new todo items to the list
4232
- - "list": View all current todo items and their status
4233
- - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
4234
- - "clear": Remove all todo items from the list
4235
-
4236
- ## Plan Actions (for complex, multi-phase work)
4237
- - "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.
4238
- - "list_plans": List all plans for this session
4239
- - "get_plan": Read a specific plan by name
4240
- - "delete_plan": Remove a plan
4241
-
4242
- ## Plans vs Todos
4243
- - **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.
4244
- - **Todos** are your current focus \u2014 the immediate steps you're working on right now.
4245
-
4246
- ## Workflow for complex tasks
4247
- 1. Create a plan with phases and subtasks (save_plan)
4248
- 2. Create todos from the first uncompleted phase (add)
4249
- 3. Work through the todos, marking them as you go
4250
- 4. When all current todos are done, update the plan (mark completed sections with [x]) and save it
4251
- 5. Create new todos from the next uncompleted phase
4252
- 6. Repeat until the plan is fully complete
4253
-
4254
- ## Plan format
4255
- Plans should be markdown with this structure:
4256
- \`\`\`markdown
4257
- # Plan: [Title]
4258
-
4259
- ## Overview
4260
- [What we're doing and why]
4261
-
4262
- ## Phase 1: [Name] [completed]
4263
- - [x] Task 1
4264
- - [x] Task 2
4265
-
4266
- ## Phase 2: [Name] [in_progress]
4267
- - [x] Subtask 2.1
4268
- - [ ] Subtask 2.2
4269
- - [ ] Sub-subtask 2.2.1
4270
- - [ ] Sub-subtask 2.2.2
4271
- - [ ] Subtask 2.3
4272
-
4273
- ## Phase 3: [Name] [pending]
4274
- - [ ] Task 1
4275
- - [ ] Task 2
4276
-
4277
- ## Notes
4278
- - Key decisions and context to preserve
4279
- - Important file paths discovered
4280
- \`\`\``,
4281
- inputSchema: todoInputSchema,
4282
- execute: async ({ action, items, todoId, status, planName, planContent }) => {
4283
- try {
4284
- switch (action) {
4285
- case "add": {
4286
- if (!items || items.length === 0) {
4287
- return {
4288
- success: false,
4289
- error: "No items provided. Include at least one todo item."
4290
- };
4291
- }
4292
- const created = await todoQueries.createMany(options.sessionId, items);
4293
- return {
4294
- success: true,
4295
- action: "add",
4296
- itemsAdded: created.length,
4297
- items: created.map(formatTodoItem)
4298
- };
4438
+ isChunked: true
4439
+ });
4440
+ if (chunkCount > 1) {
4441
+ await new Promise((resolve11) => setTimeout(resolve11, 0));
4442
+ }
4443
+ }
4299
4444
  }
4300
- case "list": {
4301
- const todos = await todoQueries.getBySession(options.sessionId);
4302
- const stats = {
4303
- total: todos.length,
4304
- pending: todos.filter((t) => t.status === "pending").length,
4305
- inProgress: todos.filter((t) => t.status === "in_progress").length,
4306
- completed: todos.filter((t) => t.status === "completed").length,
4307
- cancelled: todos.filter((t) => t.status === "cancelled").length
4308
- };
4309
- return {
4310
- success: true,
4311
- action: "list",
4312
- stats,
4313
- items: todos.map(formatTodoItem)
4314
- };
4445
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
4446
+ const dir = dirname5(absolutePath);
4447
+ if (!existsSync8(dir)) {
4448
+ await mkdir3(dir, { recursive: true });
4315
4449
  }
4316
- case "mark": {
4317
- if (!todoId) {
4318
- return {
4319
- success: false,
4320
- error: 'todoId is required for "mark" action'
4321
- };
4322
- }
4323
- if (!status) {
4324
- return {
4325
- success: false,
4326
- error: 'status is required for "mark" action'
4327
- };
4328
- }
4329
- const updated = await todoQueries.updateStatus(todoId, status);
4330
- if (!updated) {
4331
- return {
4332
- success: false,
4333
- error: `Todo item not found: ${todoId}`
4334
- };
4335
- }
4336
- return {
4337
- success: true,
4338
- action: "mark",
4339
- item: formatTodoItem(updated)
4340
- };
4450
+ await writeFile3(absolutePath, content, "utf-8");
4451
+ let diagnosticsOutput = "";
4452
+ if (options.enableLSP !== false && isSupported(absolutePath)) {
4453
+ await touchFile(absolutePath, true);
4454
+ diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
4341
4455
  }
4342
- case "clear": {
4343
- const count = await todoQueries.clearSession(options.sessionId);
4456
+ options.onProgress?.({
4457
+ path: absolutePath,
4458
+ relativePath,
4459
+ mode: "full",
4460
+ status: "completed",
4461
+ action,
4462
+ totalLength: content.length
4463
+ });
4464
+ return {
4465
+ success: true,
4466
+ path: absolutePath,
4467
+ relativePath,
4468
+ mode: "full",
4469
+ action,
4470
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
4471
+ lineCount: content.split("\n").length,
4472
+ ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
4473
+ };
4474
+ } else if (mode === "str_replace") {
4475
+ if (old_string === void 0 || new_string === void 0) {
4344
4476
  return {
4345
- success: true,
4346
- action: "clear",
4347
- itemsRemoved: count
4477
+ success: false,
4478
+ error: 'Both old_string and new_string are required for "str_replace" mode'
4348
4479
  };
4349
4480
  }
4350
- // ── Plan actions ─────────────────────────────────────────
4351
- case "save_plan": {
4352
- if (!planName) {
4353
- return { success: false, error: 'planName is required for "save_plan"' };
4354
- }
4355
- if (!planContent) {
4356
- return { success: false, error: 'planContent is required for "save_plan"' };
4357
- }
4358
- const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
4359
- const filename = `${slugify(planName)}.md`;
4360
- const filePath = join4(dir, filename);
4361
- await writeFile4(filePath, planContent, "utf-8");
4481
+ if (!existsSync8(absolutePath)) {
4362
4482
  return {
4363
- success: true,
4364
- action: "save_plan",
4365
- planName,
4366
- filename,
4367
- path: filePath,
4368
- sizeChars: planContent.length
4483
+ success: false,
4484
+ error: `File not found: ${path}. Use "full" mode to create new files.`
4369
4485
  };
4370
4486
  }
4371
- case "list_plans": {
4372
- const dir = getPlansDir(options.workingDirectory, options.sessionId);
4373
- if (!existsSync9(dir)) {
4374
- return { success: true, action: "list_plans", plans: [], count: 0 };
4375
- }
4376
- const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
4377
- const plans = [];
4378
- for (const f of files) {
4379
- try {
4380
- const content = await readFile6(join4(dir, f), "utf-8");
4381
- const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
4382
- plans.push({
4383
- name: f.replace(/\.md$/, ""),
4384
- title: titleMatch?.[1]?.trim() || f.replace(/\.md$/, ""),
4385
- filename: f,
4386
- sizeChars: content.length
4387
- });
4388
- } catch {
4389
- }
4390
- }
4391
- return { success: true, action: "list_plans", plans, count: plans.length };
4392
- }
4393
- case "get_plan": {
4394
- if (!planName) {
4395
- return { success: false, error: 'planName is required for "get_plan"' };
4396
- }
4397
- const dir = getPlansDir(options.workingDirectory, options.sessionId);
4398
- const filename = `${slugify(planName)}.md`;
4399
- const filePath = join4(dir, filename);
4400
- if (!existsSync9(filePath)) {
4401
- return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
4402
- }
4403
- const content = await readFile6(filePath, "utf-8");
4487
+ options.onProgress?.({
4488
+ path: absolutePath,
4489
+ relativePath,
4490
+ mode: "str_replace",
4491
+ status: "started",
4492
+ action: "edited"
4493
+ });
4494
+ options.onProgress?.({
4495
+ path: absolutePath,
4496
+ relativePath,
4497
+ mode: "str_replace",
4498
+ status: "content",
4499
+ oldString: old_string,
4500
+ newString: new_string,
4501
+ action: "edited"
4502
+ });
4503
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
4504
+ const currentContent = await readFile5(absolutePath, "utf-8");
4505
+ if (!currentContent.includes(old_string)) {
4506
+ const lines = currentContent.split("\n");
4507
+ const preview = lines.slice(0, 20).join("\n");
4404
4508
  return {
4405
- success: true,
4406
- action: "get_plan",
4407
- planName,
4408
- content,
4409
- sizeChars: content.length
4509
+ success: false,
4510
+ error: "old_string not found in file. The string must match EXACTLY including whitespace.",
4511
+ hint: "Check for differences in indentation, line endings, or invisible characters.",
4512
+ filePreview: lines.length > 20 ? `${preview}
4513
+ ... (${lines.length - 20} more lines)` : preview
4410
4514
  };
4411
4515
  }
4412
- case "delete_plan": {
4413
- if (!planName) {
4414
- return { success: false, error: 'planName is required for "delete_plan"' };
4415
- }
4416
- const dir = getPlansDir(options.workingDirectory, options.sessionId);
4417
- const filename = `${slugify(planName)}.md`;
4418
- const filePath = join4(dir, filename);
4419
- if (!existsSync9(filePath)) {
4420
- return { success: false, error: `Plan not found: "${planName}"` };
4421
- }
4422
- unlinkSync(filePath);
4423
- return { success: true, action: "delete_plan", planName, deleted: true };
4424
- }
4425
- default:
4516
+ const occurrences = currentContent.split(old_string).length - 1;
4517
+ if (occurrences > 1) {
4426
4518
  return {
4427
4519
  success: false,
4428
- error: `Unknown action: ${action}`
4520
+ error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
4521
+ hint: "Include surrounding lines or more specific content in old_string."
4429
4522
  };
4523
+ }
4524
+ const newContent = currentContent.replace(old_string, new_string);
4525
+ await writeFile3(absolutePath, newContent, "utf-8");
4526
+ const oldLines = old_string.split("\n").length;
4527
+ const newLines = new_string.split("\n").length;
4528
+ let diagnosticsOutput = "";
4529
+ if (options.enableLSP !== false && isSupported(absolutePath)) {
4530
+ await touchFile(absolutePath, true);
4531
+ diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
4532
+ }
4533
+ options.onProgress?.({
4534
+ path: absolutePath,
4535
+ relativePath,
4536
+ mode: "str_replace",
4537
+ status: "completed",
4538
+ action: "edited"
4539
+ });
4540
+ return {
4541
+ success: true,
4542
+ path: absolutePath,
4543
+ relativePath,
4544
+ mode: "str_replace",
4545
+ linesRemoved: oldLines,
4546
+ linesAdded: newLines,
4547
+ lineDelta: newLines - oldLines,
4548
+ ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
4549
+ };
4430
4550
  }
4551
+ return {
4552
+ success: false,
4553
+ error: `Invalid mode: ${mode}`
4554
+ };
4431
4555
  } catch (error) {
4432
4556
  return {
4433
4557
  success: false,
@@ -4437,30 +4561,9 @@ Plans should be markdown with this structure:
4437
4561
  }
4438
4562
  });
4439
4563
  }
4440
- function formatTodoItem(item) {
4441
- return {
4442
- id: item.id,
4443
- content: item.content,
4444
- status: item.status,
4445
- order: item.order,
4446
- createdAt: item.createdAt.toISOString()
4447
- };
4448
- }
4449
- async function readSessionPlans(workingDirectory, sessionId) {
4450
- const dir = getPlansDir(workingDirectory, sessionId);
4451
- if (!existsSync9(dir)) return [];
4452
- const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
4453
- if (files.length === 0) return [];
4454
- const plans = [];
4455
- for (const f of files) {
4456
- try {
4457
- const content = await readFile6(join4(dir, f), "utf-8");
4458
- plans.push({ name: f.replace(/\.md$/, ""), content });
4459
- } catch {
4460
- }
4461
- }
4462
- return plans;
4463
- }
4564
+
4565
+ // src/tools/index.ts
4566
+ init_todo();
4464
4567
 
4465
4568
  // src/tools/load-skill.ts
4466
4569
  init_skills();
@@ -6061,6 +6164,7 @@ function createUploadFileTool(options) {
6061
6164
  // src/tools/index.ts
6062
6165
  init_semantic();
6063
6166
  init_remote();
6167
+ init_todo();
6064
6168
  init_semantic_search();
6065
6169
  async function createTools(options) {
6066
6170
  const tools = {
@@ -6132,6 +6236,7 @@ init_db();
6132
6236
  // src/agent/prompts.ts
6133
6237
  init_skills();
6134
6238
  init_db();
6239
+ init_todo();
6135
6240
  import os from "os";
6136
6241
  function getSearchInstructions() {
6137
6242
  const platform3 = process.platform;
@@ -6180,7 +6285,11 @@ async function buildSystemPrompt(options) {
6180
6285
  const todos = await todoQueries.getBySession(sessionId);
6181
6286
  const todosContext = formatTodosForContext(todos);
6182
6287
  const plans = await readSessionPlans(workingDirectory, sessionId);
6183
- const plansContext = formatPlansForContext(plans);
6288
+ const allTodosDone = todos.length > 0 && todos.every(
6289
+ (t) => t.status === "completed" || t.status === "cancelled"
6290
+ );
6291
+ const hasNoTodos = todos.length === 0;
6292
+ const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
6184
6293
  const platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
6185
6294
  const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
6186
6295
  const searchInstructions = getSearchInstructions();
@@ -6213,16 +6322,19 @@ Use the **todo tool** to manage both immediate tasks AND persistent plans:
6213
6322
 
6214
6323
  **For complex, multi-phase tasks:** Create a persistent **plan** first.
6215
6324
  1. Research the codebase to understand what you're working with
6216
- 2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks
6217
- 3. Create todos from the first uncompleted phase
6218
- 4. Work through the todos
6219
- 5. When done, update the plan (mark completed phases with [x]), save it again
6220
- 6. Create new todos from the next uncompleted phase
6221
- 7. Repeat until the plan is fully complete
6222
-
6223
- 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).
6224
-
6225
- You can clear the todo list and restart it, and do multiple things inside of one session.
6325
+ 2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks using checkboxes (- [ ] for uncompleted, - [x] for completed)
6326
+ 3. Todos are **auto-created** from the first uncompleted phase when you save the plan (if no active todos exist)
6327
+ 4. Work through the todos, marking them as you go
6328
+ 5. When all todos are done, update the plan: change completed phase heading to include [completed], mark all its items with [x], then call save_plan again \u2014 new todos will be auto-created from the next uncompleted phase
6329
+ 6. Repeat until all phases are complete, then delete the plan
6330
+
6331
+ **Key details:**
6332
+ - Plans persist on disk and are always injected into your context \u2014 they survive context compaction even in very long sessions
6333
+ - You can have multiple plans active at once (e.g., one for frontend work, one for backend)
6334
+ - When you save a plan and there are no active todos, the system automatically creates todos from the first uncompleted phase (phases with [completed] in the heading are skipped)
6335
+ - Only top-level checklist items (- [ ]) become todos \u2014 indented sub-items are part of the task detail
6336
+ - Sections named Overview, Notes, Key Decisions, etc. are not treated as phases
6337
+ - You can clear the todo list and restart it, and do multiple things inside of one session
6226
6338
 
6227
6339
  ### bash Tool
6228
6340
  The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
@@ -6473,7 +6585,7 @@ function formatTodosForContext(todos) {
6473
6585
  }
6474
6586
  var MAX_PLAN_CHARS = 3e4;
6475
6587
  var MAX_TOTAL_PLANS_CHARS = 6e4;
6476
- function formatPlansForContext(plans) {
6588
+ function formatPlansForContext(plans, shouldContinue) {
6477
6589
  if (plans.length === 0) return "";
6478
6590
  let totalChars = 0;
6479
6591
  const sections = [];
@@ -6482,6 +6594,13 @@ function formatPlansForContext(plans) {
6482
6594
  sections.push("These plans persist across context compaction \u2014 they are always available.");
6483
6595
  sections.push("When you finish your current todos, check these plans for the next uncompleted phase,");
6484
6596
  sections.push("update the plan (mark completed items with [x]), then create new todos for the next phase.");
6597
+ if (shouldContinue) {
6598
+ sections.push("");
6599
+ sections.push("**>>> ACTION NEEDED: Your current todos are all done but the plan has remaining phases. To continue:**");
6600
+ sections.push("**1. Update the plan content: change the completed phase heading to include [completed] and mark its items with [x]**");
6601
+ sections.push("**2. Call save_plan with the updated content \u2014 new todos will be auto-created from the next uncompleted phase**");
6602
+ sections.push("**3. Continue working on the new todos <<<**");
6603
+ }
6485
6604
  sections.push("");
6486
6605
  for (const plan of plans) {
6487
6606
  let content = plan.content;
@@ -8219,6 +8338,24 @@ sessions.get("/:id/todos", async (c) => {
8219
8338
  } : null
8220
8339
  });
8221
8340
  });
8341
+ sessions.get("/:id/plans", async (c) => {
8342
+ const id = c.req.param("id");
8343
+ const session = await sessionQueries.getById(id);
8344
+ if (!session) {
8345
+ return c.json({ error: "Session not found" }, 404);
8346
+ }
8347
+ const { readSessionPlans: readSessionPlans2 } = await Promise.resolve().then(() => (init_todo(), todo_exports));
8348
+ const plans = await readSessionPlans2(session.workingDirectory, id);
8349
+ return c.json({
8350
+ sessionId: id,
8351
+ plans: plans.map((p) => ({
8352
+ name: p.name,
8353
+ content: p.content,
8354
+ sizeChars: p.content.length
8355
+ })),
8356
+ count: plans.length
8357
+ });
8358
+ });
8222
8359
  sessions.get("/:id/checkpoints", async (c) => {
8223
8360
  const id = c.req.param("id");
8224
8361
  const session = await sessionQueries.getById(id);