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/cli.js CHANGED
@@ -1049,6 +1049,388 @@ var init_config = __esm({
1049
1049
  }
1050
1050
  });
1051
1051
 
1052
+ // src/tools/todo.ts
1053
+ var todo_exports = {};
1054
+ __export(todo_exports, {
1055
+ createTodoTool: () => createTodoTool,
1056
+ readSessionPlans: () => readSessionPlans
1057
+ });
1058
+ import { tool as tool4 } from "ai";
1059
+ import { z as z5 } from "zod";
1060
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
1061
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1062
+ import { join as join4 } from "path";
1063
+ function getPlansDir(workingDirectory, sessionId) {
1064
+ return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
1065
+ }
1066
+ function ensurePlansDir(workingDirectory, sessionId) {
1067
+ const dir = getPlansDir(workingDirectory, sessionId);
1068
+ if (!existsSync9(dir)) {
1069
+ mkdirSync4(dir, { recursive: true });
1070
+ }
1071
+ const gitignorePath = join4(workingDirectory, ".gitignore");
1072
+ if (existsSync9(gitignorePath)) {
1073
+ try {
1074
+ const content = readFileSync3(gitignorePath, "utf-8");
1075
+ if (!content.includes(".sparkecoder")) {
1076
+ appendFileSync(gitignorePath, "\n.sparkecoder/\n");
1077
+ }
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ return dir;
1082
+ }
1083
+ function slugify(name) {
1084
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
1085
+ }
1086
+ function createTodoTool(options) {
1087
+ return tool4({
1088
+ description: `Manage your task list and persistent plans for the current session.
1089
+
1090
+ ## Todo Actions (for tracking current work)
1091
+ - "add": Add one or more new todo items to the list
1092
+ - "list": View all current todo items and their status
1093
+ - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
1094
+ - "clear": Remove all todo items from the list
1095
+
1096
+ ## Plan Actions (for complex, multi-phase work)
1097
+ - "save_plan": Create or update a named plan. When saved and no active todos exist, todos are AUTO-CREATED from the first uncompleted phase.
1098
+ - "list_plans": List all plans for this session
1099
+ - "get_plan": Read a specific plan by name
1100
+ - "delete_plan": Remove a plan
1101
+
1102
+ ## Plans vs Todos
1103
+ - **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.
1104
+ - **Todos** are your current focus \u2014 the immediate steps you're working on right now, auto-derived from plan phases.
1105
+
1106
+ ## Workflow for complex tasks
1107
+ 1. Create a plan with phases and subtasks using checkboxes (save_plan) \u2014 todos are auto-created from Phase 1
1108
+ 2. Work through the todos, marking them as you go
1109
+ 3. When all todos are done, update the plan: mark the completed phase heading with [completed] and its items with [x]
1110
+ 4. Call save_plan again with the updated content \u2014 todos are auto-created from the next uncompleted phase
1111
+ 5. Repeat until all phases are complete, then delete_plan
1112
+
1113
+ ## Auto-todo creation rules
1114
+ - Only triggers when there are NO active (pending/in_progress) todos
1115
+ - Skips phases with [completed] in the heading
1116
+ - Skips sections named Overview, Notes, Key Decisions, etc.
1117
+ - Only top-level checklist items (- [ ]) become todos \u2014 indented sub-items are task details
1118
+
1119
+ ## Plan format
1120
+ Plans should be markdown with this structure:
1121
+ \`\`\`markdown
1122
+ # Plan: [Title]
1123
+
1124
+ ## Overview
1125
+ [What we're doing and why]
1126
+
1127
+ ## Phase 1: [Name] [completed]
1128
+ - [x] Task 1
1129
+ - [x] Task 2
1130
+
1131
+ ## Phase 2: [Name] [in_progress]
1132
+ - [x] Subtask 2.1
1133
+ - [ ] Subtask 2.2
1134
+ - [ ] Sub-subtask 2.2.1
1135
+ - [ ] Sub-subtask 2.2.2
1136
+ - [ ] Subtask 2.3
1137
+
1138
+ ## Phase 3: [Name] [pending]
1139
+ - [ ] Task 1
1140
+ - [ ] Task 2
1141
+
1142
+ ## Notes
1143
+ - Key decisions and context to preserve
1144
+ - Important file paths discovered
1145
+ \`\`\``,
1146
+ inputSchema: todoInputSchema,
1147
+ execute: async ({ action, items, todoId, status, planName, planContent }) => {
1148
+ try {
1149
+ switch (action) {
1150
+ case "add": {
1151
+ if (!items || items.length === 0) {
1152
+ return {
1153
+ success: false,
1154
+ error: "No items provided. Include at least one todo item."
1155
+ };
1156
+ }
1157
+ const created = await todoQueries.createMany(options.sessionId, items);
1158
+ return {
1159
+ success: true,
1160
+ action: "add",
1161
+ itemsAdded: created.length,
1162
+ items: created.map(formatTodoItem)
1163
+ };
1164
+ }
1165
+ case "list": {
1166
+ const todos = await todoQueries.getBySession(options.sessionId);
1167
+ const stats = {
1168
+ total: todos.length,
1169
+ pending: todos.filter((t) => t.status === "pending").length,
1170
+ inProgress: todos.filter((t) => t.status === "in_progress").length,
1171
+ completed: todos.filter((t) => t.status === "completed").length,
1172
+ cancelled: todos.filter((t) => t.status === "cancelled").length
1173
+ };
1174
+ return {
1175
+ success: true,
1176
+ action: "list",
1177
+ stats,
1178
+ items: todos.map(formatTodoItem)
1179
+ };
1180
+ }
1181
+ case "mark": {
1182
+ if (!todoId) {
1183
+ return {
1184
+ success: false,
1185
+ error: 'todoId is required for "mark" action'
1186
+ };
1187
+ }
1188
+ if (!status) {
1189
+ return {
1190
+ success: false,
1191
+ error: 'status is required for "mark" action'
1192
+ };
1193
+ }
1194
+ const updated = await todoQueries.updateStatus(todoId, status);
1195
+ if (!updated) {
1196
+ return {
1197
+ success: false,
1198
+ error: `Todo item not found: ${todoId}`
1199
+ };
1200
+ }
1201
+ let planContinuation;
1202
+ if (status === "completed") {
1203
+ const allTodos = await todoQueries.getBySession(options.sessionId);
1204
+ const allDone = allTodos.every(
1205
+ (t) => t.status === "completed" || t.status === "cancelled"
1206
+ );
1207
+ if (allDone) {
1208
+ const plansDir = getPlansDir(options.workingDirectory, options.sessionId);
1209
+ if (existsSync9(plansDir)) {
1210
+ const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
1211
+ for (const f of planFiles) {
1212
+ try {
1213
+ const content = await readFile6(join4(plansDir, f), "utf-8");
1214
+ if (parseNextUncompletedPhase(content) !== null) {
1215
+ 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.";
1216
+ break;
1217
+ }
1218
+ } catch {
1219
+ }
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ return {
1225
+ success: true,
1226
+ action: "mark",
1227
+ item: formatTodoItem(updated),
1228
+ ...planContinuation ? { planContinuation } : {}
1229
+ };
1230
+ }
1231
+ case "clear": {
1232
+ const count = await todoQueries.clearSession(options.sessionId);
1233
+ return {
1234
+ success: true,
1235
+ action: "clear",
1236
+ itemsRemoved: count
1237
+ };
1238
+ }
1239
+ // ── Plan actions ─────────────────────────────────────────
1240
+ case "save_plan": {
1241
+ if (!planName) {
1242
+ return { success: false, error: 'planName is required for "save_plan"' };
1243
+ }
1244
+ if (!planContent) {
1245
+ return { success: false, error: 'planContent is required for "save_plan"' };
1246
+ }
1247
+ const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
1248
+ const filename = `${slugify(planName)}.md`;
1249
+ const filePath = join4(dir, filename);
1250
+ await writeFile4(filePath, planContent, "utf-8");
1251
+ const existingTodos = await todoQueries.getBySession(options.sessionId);
1252
+ const hasActiveTodos = existingTodos.some(
1253
+ (t) => t.status === "pending" || t.status === "in_progress"
1254
+ );
1255
+ let autoCreatedTodos = [];
1256
+ if (!hasActiveTodos) {
1257
+ const nextPhase = parseNextUncompletedPhase(planContent);
1258
+ if (nextPhase) {
1259
+ const created = await todoQueries.createMany(
1260
+ options.sessionId,
1261
+ nextPhase.tasks.map((task, i) => ({ content: task, order: i }))
1262
+ );
1263
+ autoCreatedTodos = created.map(formatTodoItem);
1264
+ }
1265
+ }
1266
+ return {
1267
+ success: true,
1268
+ action: "save_plan",
1269
+ planName,
1270
+ filename,
1271
+ path: filePath,
1272
+ sizeChars: planContent.length,
1273
+ ...autoCreatedTodos.length > 0 ? {
1274
+ autoCreatedTodos,
1275
+ autoCreatedFromPhase: "Created todos from the first uncompleted phase. Start working on them!"
1276
+ } : {}
1277
+ };
1278
+ }
1279
+ case "list_plans": {
1280
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
1281
+ if (!existsSync9(dir)) {
1282
+ return { success: true, action: "list_plans", plans: [], count: 0 };
1283
+ }
1284
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
1285
+ const plans = [];
1286
+ for (const f of files) {
1287
+ try {
1288
+ const content = await readFile6(join4(dir, f), "utf-8");
1289
+ const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
1290
+ plans.push({
1291
+ name: f.replace(/\.md$/, ""),
1292
+ title: titleMatch?.[1]?.trim() || f.replace(/\.md$/, ""),
1293
+ filename: f,
1294
+ sizeChars: content.length
1295
+ });
1296
+ } catch {
1297
+ }
1298
+ }
1299
+ return { success: true, action: "list_plans", plans, count: plans.length };
1300
+ }
1301
+ case "get_plan": {
1302
+ if (!planName) {
1303
+ return { success: false, error: 'planName is required for "get_plan"' };
1304
+ }
1305
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
1306
+ const filename = `${slugify(planName)}.md`;
1307
+ const filePath = join4(dir, filename);
1308
+ if (!existsSync9(filePath)) {
1309
+ return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
1310
+ }
1311
+ const content = await readFile6(filePath, "utf-8");
1312
+ return {
1313
+ success: true,
1314
+ action: "get_plan",
1315
+ planName,
1316
+ content,
1317
+ sizeChars: content.length
1318
+ };
1319
+ }
1320
+ case "delete_plan": {
1321
+ if (!planName) {
1322
+ return { success: false, error: 'planName is required for "delete_plan"' };
1323
+ }
1324
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
1325
+ const filename = `${slugify(planName)}.md`;
1326
+ const filePath = join4(dir, filename);
1327
+ if (!existsSync9(filePath)) {
1328
+ return { success: false, error: `Plan not found: "${planName}"` };
1329
+ }
1330
+ unlinkSync(filePath);
1331
+ return { success: true, action: "delete_plan", planName, deleted: true };
1332
+ }
1333
+ default:
1334
+ return {
1335
+ success: false,
1336
+ error: `Unknown action: ${action}`
1337
+ };
1338
+ }
1339
+ } catch (error) {
1340
+ return {
1341
+ success: false,
1342
+ error: error.message
1343
+ };
1344
+ }
1345
+ }
1346
+ });
1347
+ }
1348
+ function formatTodoItem(item) {
1349
+ return {
1350
+ id: item.id,
1351
+ content: item.content,
1352
+ status: item.status,
1353
+ order: item.order,
1354
+ createdAt: item.createdAt.toISOString()
1355
+ };
1356
+ }
1357
+ function parseNextUncompletedPhase(content) {
1358
+ const lines = content.split("\n");
1359
+ let currentPhase = null;
1360
+ let currentTasks = [];
1361
+ let hasUncompletedTask = false;
1362
+ for (const line of lines) {
1363
+ const h2Match = line.match(/^##\s+(.+)/);
1364
+ if (h2Match) {
1365
+ if (currentPhase && hasUncompletedTask && currentTasks.length > 0) {
1366
+ return { phaseName: currentPhase, tasks: currentTasks };
1367
+ }
1368
+ const headingText = h2Match[1].trim();
1369
+ if (/^(overview|notes|key decisions|context|summary|references)\b/i.test(headingText)) {
1370
+ currentPhase = null;
1371
+ currentTasks = [];
1372
+ hasUncompletedTask = false;
1373
+ continue;
1374
+ }
1375
+ if (/\[completed\]/i.test(headingText)) {
1376
+ currentPhase = null;
1377
+ currentTasks = [];
1378
+ hasUncompletedTask = false;
1379
+ continue;
1380
+ }
1381
+ currentPhase = headingText.replace(/\s*\[(in_progress|pending|completed)\]\s*/gi, "").replace(/^Phase\s+\d+[:\s]*/i, "").trim();
1382
+ currentTasks = [];
1383
+ hasUncompletedTask = false;
1384
+ continue;
1385
+ }
1386
+ if (!currentPhase) continue;
1387
+ const uncheckedMatch = line.match(/^[-*]\s+\[\s\]\s+(.+)/);
1388
+ if (uncheckedMatch) {
1389
+ currentTasks.push(uncheckedMatch[1].trim());
1390
+ hasUncompletedTask = true;
1391
+ }
1392
+ }
1393
+ if (currentPhase && hasUncompletedTask && currentTasks.length > 0) {
1394
+ return { phaseName: currentPhase, tasks: currentTasks };
1395
+ }
1396
+ return null;
1397
+ }
1398
+ async function readSessionPlans(workingDirectory, sessionId) {
1399
+ const dir = getPlansDir(workingDirectory, sessionId);
1400
+ if (!existsSync9(dir)) return [];
1401
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
1402
+ if (files.length === 0) return [];
1403
+ const plans = [];
1404
+ for (const f of files) {
1405
+ try {
1406
+ const content = await readFile6(join4(dir, f), "utf-8");
1407
+ plans.push({ name: f.replace(/\.md$/, ""), content });
1408
+ } catch {
1409
+ }
1410
+ }
1411
+ return plans;
1412
+ }
1413
+ var todoInputSchema;
1414
+ var init_todo = __esm({
1415
+ "src/tools/todo.ts"() {
1416
+ "use strict";
1417
+ init_db();
1418
+ todoInputSchema = z5.object({
1419
+ action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
1420
+ items: z5.array(
1421
+ z5.object({
1422
+ content: z5.string().describe("Description of the task"),
1423
+ order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
1424
+ })
1425
+ ).optional().describe('For "add" action: Array of todo items to add'),
1426
+ todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
1427
+ status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
1428
+ planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
1429
+ planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
1430
+ });
1431
+ }
1432
+ });
1433
+
1052
1434
  // src/skills/index.ts
1053
1435
  var skills_exports = {};
1054
1436
  __export(skills_exports, {
@@ -4836,381 +5218,123 @@ Working directory: ${options.workingDirectory}`,
4836
5218
  chunkIndex: i,
4837
5219
  chunkCount,
4838
5220
  chunkStart,
4839
- isChunked: true
4840
- });
4841
- if (chunkCount > 1) {
4842
- await new Promise((resolve12) => setTimeout(resolve12, 0));
4843
- }
4844
- }
4845
- }
4846
- await backupFile(options.sessionId, options.workingDirectory, absolutePath);
4847
- const dir = dirname5(absolutePath);
4848
- if (!existsSync8(dir)) {
4849
- await mkdir3(dir, { recursive: true });
4850
- }
4851
- await writeFile3(absolutePath, content, "utf-8");
4852
- let diagnosticsOutput = "";
4853
- if (options.enableLSP !== false && isSupported(absolutePath)) {
4854
- await touchFile(absolutePath, true);
4855
- diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
4856
- }
4857
- options.onProgress?.({
4858
- path: absolutePath,
4859
- relativePath,
4860
- mode: "full",
4861
- status: "completed",
4862
- action,
4863
- totalLength: content.length
4864
- });
4865
- return {
4866
- success: true,
4867
- path: absolutePath,
4868
- relativePath,
4869
- mode: "full",
4870
- action,
4871
- bytesWritten: Buffer.byteLength(content, "utf-8"),
4872
- lineCount: content.split("\n").length,
4873
- ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
4874
- };
4875
- } else if (mode === "str_replace") {
4876
- if (old_string === void 0 || new_string === void 0) {
4877
- return {
4878
- success: false,
4879
- error: 'Both old_string and new_string are required for "str_replace" mode'
4880
- };
4881
- }
4882
- if (!existsSync8(absolutePath)) {
4883
- return {
4884
- success: false,
4885
- error: `File not found: ${path}. Use "full" mode to create new files.`
4886
- };
4887
- }
4888
- options.onProgress?.({
4889
- path: absolutePath,
4890
- relativePath,
4891
- mode: "str_replace",
4892
- status: "started",
4893
- action: "edited"
4894
- });
4895
- options.onProgress?.({
4896
- path: absolutePath,
4897
- relativePath,
4898
- mode: "str_replace",
4899
- status: "content",
4900
- oldString: old_string,
4901
- newString: new_string,
4902
- action: "edited"
4903
- });
4904
- await backupFile(options.sessionId, options.workingDirectory, absolutePath);
4905
- const currentContent = await readFile5(absolutePath, "utf-8");
4906
- if (!currentContent.includes(old_string)) {
4907
- const lines = currentContent.split("\n");
4908
- const preview = lines.slice(0, 20).join("\n");
4909
- return {
4910
- success: false,
4911
- error: "old_string not found in file. The string must match EXACTLY including whitespace.",
4912
- hint: "Check for differences in indentation, line endings, or invisible characters.",
4913
- filePreview: lines.length > 20 ? `${preview}
4914
- ... (${lines.length - 20} more lines)` : preview
4915
- };
4916
- }
4917
- const occurrences = currentContent.split(old_string).length - 1;
4918
- if (occurrences > 1) {
4919
- return {
4920
- success: false,
4921
- error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
4922
- hint: "Include surrounding lines or more specific content in old_string."
4923
- };
4924
- }
4925
- const newContent = currentContent.replace(old_string, new_string);
4926
- await writeFile3(absolutePath, newContent, "utf-8");
4927
- const oldLines = old_string.split("\n").length;
4928
- const newLines = new_string.split("\n").length;
4929
- let diagnosticsOutput = "";
4930
- if (options.enableLSP !== false && isSupported(absolutePath)) {
4931
- await touchFile(absolutePath, true);
4932
- diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
4933
- }
4934
- options.onProgress?.({
4935
- path: absolutePath,
4936
- relativePath,
4937
- mode: "str_replace",
4938
- status: "completed",
4939
- action: "edited"
4940
- });
4941
- return {
4942
- success: true,
4943
- path: absolutePath,
4944
- relativePath,
4945
- mode: "str_replace",
4946
- linesRemoved: oldLines,
4947
- linesAdded: newLines,
4948
- lineDelta: newLines - oldLines,
4949
- ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
4950
- };
4951
- }
4952
- return {
4953
- success: false,
4954
- error: `Invalid mode: ${mode}`
4955
- };
4956
- } catch (error) {
4957
- return {
4958
- success: false,
4959
- error: error.message
4960
- };
4961
- }
4962
- }
4963
- });
4964
- }
4965
-
4966
- // src/tools/todo.ts
4967
- init_db();
4968
- import { tool as tool4 } from "ai";
4969
- import { z as z5 } from "zod";
4970
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
4971
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
4972
- import { join as join4 } from "path";
4973
- function getPlansDir(workingDirectory, sessionId) {
4974
- return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
4975
- }
4976
- function ensurePlansDir(workingDirectory, sessionId) {
4977
- const dir = getPlansDir(workingDirectory, sessionId);
4978
- if (!existsSync9(dir)) {
4979
- mkdirSync4(dir, { recursive: true });
4980
- }
4981
- const gitignorePath = join4(workingDirectory, ".gitignore");
4982
- if (existsSync9(gitignorePath)) {
4983
- try {
4984
- const content = readFileSync3(gitignorePath, "utf-8");
4985
- if (!content.includes(".sparkecoder")) {
4986
- appendFileSync(gitignorePath, "\n.sparkecoder/\n");
4987
- }
4988
- } catch {
4989
- }
4990
- }
4991
- return dir;
4992
- }
4993
- function slugify(name) {
4994
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
4995
- }
4996
- var todoInputSchema = z5.object({
4997
- action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
4998
- items: z5.array(
4999
- z5.object({
5000
- content: z5.string().describe("Description of the task"),
5001
- order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
5002
- })
5003
- ).optional().describe('For "add" action: Array of todo items to add'),
5004
- todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
5005
- status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
5006
- planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
5007
- planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
5008
- });
5009
- function createTodoTool(options) {
5010
- return tool4({
5011
- description: `Manage your task list and persistent plans for the current session.
5012
-
5013
- ## Todo Actions (for tracking current work)
5014
- - "add": Add one or more new todo items to the list
5015
- - "list": View all current todo items and their status
5016
- - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
5017
- - "clear": Remove all todo items from the list
5018
-
5019
- ## Plan Actions (for complex, multi-phase work)
5020
- - "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.
5021
- - "list_plans": List all plans for this session
5022
- - "get_plan": Read a specific plan by name
5023
- - "delete_plan": Remove a plan
5024
-
5025
- ## Plans vs Todos
5026
- - **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.
5027
- - **Todos** are your current focus \u2014 the immediate steps you're working on right now.
5028
-
5029
- ## Workflow for complex tasks
5030
- 1. Create a plan with phases and subtasks (save_plan)
5031
- 2. Create todos from the first uncompleted phase (add)
5032
- 3. Work through the todos, marking them as you go
5033
- 4. When all current todos are done, update the plan (mark completed sections with [x]) and save it
5034
- 5. Create new todos from the next uncompleted phase
5035
- 6. Repeat until the plan is fully complete
5036
-
5037
- ## Plan format
5038
- Plans should be markdown with this structure:
5039
- \`\`\`markdown
5040
- # Plan: [Title]
5041
-
5042
- ## Overview
5043
- [What we're doing and why]
5044
-
5045
- ## Phase 1: [Name] [completed]
5046
- - [x] Task 1
5047
- - [x] Task 2
5048
-
5049
- ## Phase 2: [Name] [in_progress]
5050
- - [x] Subtask 2.1
5051
- - [ ] Subtask 2.2
5052
- - [ ] Sub-subtask 2.2.1
5053
- - [ ] Sub-subtask 2.2.2
5054
- - [ ] Subtask 2.3
5055
-
5056
- ## Phase 3: [Name] [pending]
5057
- - [ ] Task 1
5058
- - [ ] Task 2
5059
-
5060
- ## Notes
5061
- - Key decisions and context to preserve
5062
- - Important file paths discovered
5063
- \`\`\``,
5064
- inputSchema: todoInputSchema,
5065
- execute: async ({ action, items, todoId, status, planName, planContent }) => {
5066
- try {
5067
- switch (action) {
5068
- case "add": {
5069
- if (!items || items.length === 0) {
5070
- return {
5071
- success: false,
5072
- error: "No items provided. Include at least one todo item."
5073
- };
5074
- }
5075
- const created = await todoQueries.createMany(options.sessionId, items);
5076
- return {
5077
- success: true,
5078
- action: "add",
5079
- itemsAdded: created.length,
5080
- items: created.map(formatTodoItem)
5081
- };
5221
+ isChunked: true
5222
+ });
5223
+ if (chunkCount > 1) {
5224
+ await new Promise((resolve12) => setTimeout(resolve12, 0));
5225
+ }
5226
+ }
5082
5227
  }
5083
- case "list": {
5084
- const todos = await todoQueries.getBySession(options.sessionId);
5085
- const stats = {
5086
- total: todos.length,
5087
- pending: todos.filter((t) => t.status === "pending").length,
5088
- inProgress: todos.filter((t) => t.status === "in_progress").length,
5089
- completed: todos.filter((t) => t.status === "completed").length,
5090
- cancelled: todos.filter((t) => t.status === "cancelled").length
5091
- };
5092
- return {
5093
- success: true,
5094
- action: "list",
5095
- stats,
5096
- items: todos.map(formatTodoItem)
5097
- };
5228
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
5229
+ const dir = dirname5(absolutePath);
5230
+ if (!existsSync8(dir)) {
5231
+ await mkdir3(dir, { recursive: true });
5098
5232
  }
5099
- case "mark": {
5100
- if (!todoId) {
5101
- return {
5102
- success: false,
5103
- error: 'todoId is required for "mark" action'
5104
- };
5105
- }
5106
- if (!status) {
5107
- return {
5108
- success: false,
5109
- error: 'status is required for "mark" action'
5110
- };
5111
- }
5112
- const updated = await todoQueries.updateStatus(todoId, status);
5113
- if (!updated) {
5114
- return {
5115
- success: false,
5116
- error: `Todo item not found: ${todoId}`
5117
- };
5118
- }
5119
- return {
5120
- success: true,
5121
- action: "mark",
5122
- item: formatTodoItem(updated)
5123
- };
5233
+ await writeFile3(absolutePath, content, "utf-8");
5234
+ let diagnosticsOutput = "";
5235
+ if (options.enableLSP !== false && isSupported(absolutePath)) {
5236
+ await touchFile(absolutePath, true);
5237
+ diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
5124
5238
  }
5125
- case "clear": {
5126
- const count = await todoQueries.clearSession(options.sessionId);
5239
+ options.onProgress?.({
5240
+ path: absolutePath,
5241
+ relativePath,
5242
+ mode: "full",
5243
+ status: "completed",
5244
+ action,
5245
+ totalLength: content.length
5246
+ });
5247
+ return {
5248
+ success: true,
5249
+ path: absolutePath,
5250
+ relativePath,
5251
+ mode: "full",
5252
+ action,
5253
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
5254
+ lineCount: content.split("\n").length,
5255
+ ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
5256
+ };
5257
+ } else if (mode === "str_replace") {
5258
+ if (old_string === void 0 || new_string === void 0) {
5127
5259
  return {
5128
- success: true,
5129
- action: "clear",
5130
- itemsRemoved: count
5260
+ success: false,
5261
+ error: 'Both old_string and new_string are required for "str_replace" mode'
5131
5262
  };
5132
5263
  }
5133
- // ── Plan actions ─────────────────────────────────────────
5134
- case "save_plan": {
5135
- if (!planName) {
5136
- return { success: false, error: 'planName is required for "save_plan"' };
5137
- }
5138
- if (!planContent) {
5139
- return { success: false, error: 'planContent is required for "save_plan"' };
5140
- }
5141
- const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
5142
- const filename = `${slugify(planName)}.md`;
5143
- const filePath = join4(dir, filename);
5144
- await writeFile4(filePath, planContent, "utf-8");
5264
+ if (!existsSync8(absolutePath)) {
5145
5265
  return {
5146
- success: true,
5147
- action: "save_plan",
5148
- planName,
5149
- filename,
5150
- path: filePath,
5151
- sizeChars: planContent.length
5266
+ success: false,
5267
+ error: `File not found: ${path}. Use "full" mode to create new files.`
5152
5268
  };
5153
5269
  }
5154
- case "list_plans": {
5155
- const dir = getPlansDir(options.workingDirectory, options.sessionId);
5156
- if (!existsSync9(dir)) {
5157
- return { success: true, action: "list_plans", plans: [], count: 0 };
5158
- }
5159
- const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
5160
- const plans = [];
5161
- for (const f of files) {
5162
- try {
5163
- const content = await readFile6(join4(dir, f), "utf-8");
5164
- const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
5165
- plans.push({
5166
- name: f.replace(/\.md$/, ""),
5167
- title: titleMatch?.[1]?.trim() || f.replace(/\.md$/, ""),
5168
- filename: f,
5169
- sizeChars: content.length
5170
- });
5171
- } catch {
5172
- }
5173
- }
5174
- return { success: true, action: "list_plans", plans, count: plans.length };
5175
- }
5176
- case "get_plan": {
5177
- if (!planName) {
5178
- return { success: false, error: 'planName is required for "get_plan"' };
5179
- }
5180
- const dir = getPlansDir(options.workingDirectory, options.sessionId);
5181
- const filename = `${slugify(planName)}.md`;
5182
- const filePath = join4(dir, filename);
5183
- if (!existsSync9(filePath)) {
5184
- return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
5185
- }
5186
- const content = await readFile6(filePath, "utf-8");
5270
+ options.onProgress?.({
5271
+ path: absolutePath,
5272
+ relativePath,
5273
+ mode: "str_replace",
5274
+ status: "started",
5275
+ action: "edited"
5276
+ });
5277
+ options.onProgress?.({
5278
+ path: absolutePath,
5279
+ relativePath,
5280
+ mode: "str_replace",
5281
+ status: "content",
5282
+ oldString: old_string,
5283
+ newString: new_string,
5284
+ action: "edited"
5285
+ });
5286
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
5287
+ const currentContent = await readFile5(absolutePath, "utf-8");
5288
+ if (!currentContent.includes(old_string)) {
5289
+ const lines = currentContent.split("\n");
5290
+ const preview = lines.slice(0, 20).join("\n");
5187
5291
  return {
5188
- success: true,
5189
- action: "get_plan",
5190
- planName,
5191
- content,
5192
- sizeChars: content.length
5292
+ success: false,
5293
+ error: "old_string not found in file. The string must match EXACTLY including whitespace.",
5294
+ hint: "Check for differences in indentation, line endings, or invisible characters.",
5295
+ filePreview: lines.length > 20 ? `${preview}
5296
+ ... (${lines.length - 20} more lines)` : preview
5193
5297
  };
5194
5298
  }
5195
- case "delete_plan": {
5196
- if (!planName) {
5197
- return { success: false, error: 'planName is required for "delete_plan"' };
5198
- }
5199
- const dir = getPlansDir(options.workingDirectory, options.sessionId);
5200
- const filename = `${slugify(planName)}.md`;
5201
- const filePath = join4(dir, filename);
5202
- if (!existsSync9(filePath)) {
5203
- return { success: false, error: `Plan not found: "${planName}"` };
5204
- }
5205
- unlinkSync(filePath);
5206
- return { success: true, action: "delete_plan", planName, deleted: true };
5207
- }
5208
- default:
5299
+ const occurrences = currentContent.split(old_string).length - 1;
5300
+ if (occurrences > 1) {
5209
5301
  return {
5210
5302
  success: false,
5211
- error: `Unknown action: ${action}`
5303
+ error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
5304
+ hint: "Include surrounding lines or more specific content in old_string."
5212
5305
  };
5306
+ }
5307
+ const newContent = currentContent.replace(old_string, new_string);
5308
+ await writeFile3(absolutePath, newContent, "utf-8");
5309
+ const oldLines = old_string.split("\n").length;
5310
+ const newLines = new_string.split("\n").length;
5311
+ let diagnosticsOutput = "";
5312
+ if (options.enableLSP !== false && isSupported(absolutePath)) {
5313
+ await touchFile(absolutePath, true);
5314
+ diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
5315
+ }
5316
+ options.onProgress?.({
5317
+ path: absolutePath,
5318
+ relativePath,
5319
+ mode: "str_replace",
5320
+ status: "completed",
5321
+ action: "edited"
5322
+ });
5323
+ return {
5324
+ success: true,
5325
+ path: absolutePath,
5326
+ relativePath,
5327
+ mode: "str_replace",
5328
+ linesRemoved: oldLines,
5329
+ linesAdded: newLines,
5330
+ lineDelta: newLines - oldLines,
5331
+ ...diagnosticsOutput && { diagnostics: diagnosticsOutput }
5332
+ };
5213
5333
  }
5334
+ return {
5335
+ success: false,
5336
+ error: `Invalid mode: ${mode}`
5337
+ };
5214
5338
  } catch (error) {
5215
5339
  return {
5216
5340
  success: false,
@@ -5220,30 +5344,9 @@ Plans should be markdown with this structure:
5220
5344
  }
5221
5345
  });
5222
5346
  }
5223
- function formatTodoItem(item) {
5224
- return {
5225
- id: item.id,
5226
- content: item.content,
5227
- status: item.status,
5228
- order: item.order,
5229
- createdAt: item.createdAt.toISOString()
5230
- };
5231
- }
5232
- async function readSessionPlans(workingDirectory, sessionId) {
5233
- const dir = getPlansDir(workingDirectory, sessionId);
5234
- if (!existsSync9(dir)) return [];
5235
- const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
5236
- if (files.length === 0) return [];
5237
- const plans = [];
5238
- for (const f of files) {
5239
- try {
5240
- const content = await readFile6(join4(dir, f), "utf-8");
5241
- plans.push({ name: f.replace(/\.md$/, ""), content });
5242
- } catch {
5243
- }
5244
- }
5245
- return plans;
5246
- }
5347
+
5348
+ // src/tools/index.ts
5349
+ init_todo();
5247
5350
 
5248
5351
  // src/tools/load-skill.ts
5249
5352
  init_skills();
@@ -6844,6 +6947,7 @@ function createUploadFileTool(options) {
6844
6947
  // src/tools/index.ts
6845
6948
  init_semantic();
6846
6949
  init_remote();
6950
+ init_todo();
6847
6951
  init_semantic_search();
6848
6952
  async function createTools(options) {
6849
6953
  const tools = {
@@ -6915,6 +7019,7 @@ init_db();
6915
7019
  // src/agent/prompts.ts
6916
7020
  init_skills();
6917
7021
  init_db();
7022
+ init_todo();
6918
7023
  import os from "os";
6919
7024
  function getSearchInstructions() {
6920
7025
  const platform3 = process.platform;
@@ -6963,7 +7068,11 @@ async function buildSystemPrompt(options) {
6963
7068
  const todos = await todoQueries.getBySession(sessionId);
6964
7069
  const todosContext = formatTodosForContext(todos);
6965
7070
  const plans = await readSessionPlans(workingDirectory, sessionId);
6966
- const plansContext = formatPlansForContext(plans);
7071
+ const allTodosDone = todos.length > 0 && todos.every(
7072
+ (t) => t.status === "completed" || t.status === "cancelled"
7073
+ );
7074
+ const hasNoTodos = todos.length === 0;
7075
+ const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
6967
7076
  const platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
6968
7077
  const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
6969
7078
  const searchInstructions = getSearchInstructions();
@@ -6996,16 +7105,19 @@ Use the **todo tool** to manage both immediate tasks AND persistent plans:
6996
7105
 
6997
7106
  **For complex, multi-phase tasks:** Create a persistent **plan** first.
6998
7107
  1. Research the codebase to understand what you're working with
6999
- 2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks
7000
- 3. Create todos from the first uncompleted phase
7001
- 4. Work through the todos
7002
- 5. When done, update the plan (mark completed phases with [x]), save it again
7003
- 6. Create new todos from the next uncompleted phase
7004
- 7. Repeat until the plan is fully complete
7005
-
7006
- 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).
7007
-
7008
- You can clear the todo list and restart it, and do multiple things inside of one session.
7108
+ 2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks using checkboxes (- [ ] for uncompleted, - [x] for completed)
7109
+ 3. Todos are **auto-created** from the first uncompleted phase when you save the plan (if no active todos exist)
7110
+ 4. Work through the todos, marking them as you go
7111
+ 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
7112
+ 6. Repeat until all phases are complete, then delete the plan
7113
+
7114
+ **Key details:**
7115
+ - Plans persist on disk and are always injected into your context \u2014 they survive context compaction even in very long sessions
7116
+ - You can have multiple plans active at once (e.g., one for frontend work, one for backend)
7117
+ - 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)
7118
+ - Only top-level checklist items (- [ ]) become todos \u2014 indented sub-items are part of the task detail
7119
+ - Sections named Overview, Notes, Key Decisions, etc. are not treated as phases
7120
+ - You can clear the todo list and restart it, and do multiple things inside of one session
7009
7121
 
7010
7122
  ### bash Tool
7011
7123
  The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
@@ -7256,7 +7368,7 @@ function formatTodosForContext(todos) {
7256
7368
  }
7257
7369
  var MAX_PLAN_CHARS = 3e4;
7258
7370
  var MAX_TOTAL_PLANS_CHARS = 6e4;
7259
- function formatPlansForContext(plans) {
7371
+ function formatPlansForContext(plans, shouldContinue) {
7260
7372
  if (plans.length === 0) return "";
7261
7373
  let totalChars = 0;
7262
7374
  const sections = [];
@@ -7265,6 +7377,13 @@ function formatPlansForContext(plans) {
7265
7377
  sections.push("These plans persist across context compaction \u2014 they are always available.");
7266
7378
  sections.push("When you finish your current todos, check these plans for the next uncompleted phase,");
7267
7379
  sections.push("update the plan (mark completed items with [x]), then create new todos for the next phase.");
7380
+ if (shouldContinue) {
7381
+ sections.push("");
7382
+ sections.push("**>>> ACTION NEEDED: Your current todos are all done but the plan has remaining phases. To continue:**");
7383
+ sections.push("**1. Update the plan content: change the completed phase heading to include [completed] and mark its items with [x]**");
7384
+ sections.push("**2. Call save_plan with the updated content \u2014 new todos will be auto-created from the next uncompleted phase**");
7385
+ sections.push("**3. Continue working on the new todos <<<**");
7386
+ }
7268
7387
  sections.push("");
7269
7388
  for (const plan of plans) {
7270
7389
  let content = plan.content;
@@ -8982,6 +9101,24 @@ sessions.get("/:id/todos", async (c) => {
8982
9101
  } : null
8983
9102
  });
8984
9103
  });
9104
+ sessions.get("/:id/plans", async (c) => {
9105
+ const id = c.req.param("id");
9106
+ const session = await sessionQueries.getById(id);
9107
+ if (!session) {
9108
+ return c.json({ error: "Session not found" }, 404);
9109
+ }
9110
+ const { readSessionPlans: readSessionPlans2 } = await Promise.resolve().then(() => (init_todo(), todo_exports));
9111
+ const plans = await readSessionPlans2(session.workingDirectory, id);
9112
+ return c.json({
9113
+ sessionId: id,
9114
+ plans: plans.map((p) => ({
9115
+ name: p.name,
9116
+ content: p.content,
9117
+ sizeChars: p.content.length
9118
+ })),
9119
+ count: plans.length
9120
+ });
9121
+ });
8985
9122
  sessions.get("/:id/checkpoints", async (c) => {
8986
9123
  const id = c.req.param("id");
8987
9124
  const session = await sessionQueries.getById(id);