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.
- package/dist/agent/index.js +125 -22
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +532 -395
- package/dist/cli.js.map +1 -1
- package/dist/index.js +532 -395
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +532 -395
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +19 -36
- package/dist/tools/index.js +99 -10
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_02a118f9._.js → 2374f_00f7fe07._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ad08e83a._.js → 2374f_2801b766._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_0ed477f8._.js → 2374f_369747ce._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_3b51a934._.js → 2374f_60d8842c._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_acf3dfe4._.js → 2374f_806bd012._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_db3e363b._.js → 2374f_8dc0f9aa._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_f0d7e130._.js → 2374f_9adc1edb._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5ebfcf1a._.js → 2374f_b7f45fdf._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_12bad06e._.js → 2374f_c13c8f4f._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_fc992d90._.js → 2374f_cc6c6363._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_3e519469._.js → 2374f_d58d0276._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_a0f483d1._.js → 2374f_ecd2bdca._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_c1d54c16._.js → 2374f_f363c084._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_2526ca80._.js → 2374f_fdfc7f3d._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__06818a54._.js → [root-of-the-server]__25b25c9d._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__a1877334._.js → [root-of-the-server]__9d3a7cbf._.js} +4 -4
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_cc5f7515._.js → web_08242997._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_2b3a5919._.js → web_123ffe97._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_38156da8._.js → web_99b01335._.js} +2 -2
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/{f95d41079838994a.js → 2624c966c288fd41.js} +3 -3
- package/web/.next/standalone/web/.next/static/chunks/74b64476a24dd71e.css +1 -0
- package/web/.next/standalone/web/.next/static/{static/chunks/fc39a194539da104.js → chunks/8af263bc97c0c9ee.js} +1 -1
- package/web/.next/{static/chunks/2cafc7cb79454d33.js → standalone/web/.next/static/chunks/cfadc93a98190e5a.js} +1 -1
- package/web/.next/standalone/web/.next/static/static/chunks/{f95d41079838994a.js → 2624c966c288fd41.js} +3 -3
- package/web/.next/standalone/web/.next/static/static/chunks/74b64476a24dd71e.css +1 -0
- package/web/.next/{static/chunks/fc39a194539da104.js → standalone/web/.next/static/static/chunks/8af263bc97c0c9ee.js} +1 -1
- package/web/.next/standalone/web/.next/static/{chunks/2cafc7cb79454d33.js → static/chunks/cfadc93a98190e5a.js} +1 -1
- package/web/.next/standalone/web/src/components/ai-elements/todo-panel.tsx +194 -110
- package/web/.next/standalone/web/src/components/ai-elements/todo-tool.tsx +78 -1
- package/web/.next/standalone/web/src/components/chat-interface.tsx +15 -9
- package/web/.next/standalone/web/src/lib/api.ts +17 -0
- package/web/.next/static/chunks/{f95d41079838994a.js → 2624c966c288fd41.js} +3 -3
- package/web/.next/static/chunks/74b64476a24dd71e.css +1 -0
- package/web/.next/{standalone/web/.next/static/chunks/fc39a194539da104.js → static/chunks/8af263bc97c0c9ee.js} +1 -1
- package/web/.next/{standalone/web/.next/static/static/chunks/2cafc7cb79454d33.js → static/chunks/cfadc93a98190e5a.js} +1 -1
- package/web/.next/standalone/web/.next/static/chunks/41a5c049931b2c77.css +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/41a5c049931b2c77.css +0 -1
- package/web/.next/static/chunks/41a5c049931b2c77.css +0 -1
- /package/web/.next/standalone/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_ssgManifest.js +0 -0
- /package/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_buildManifest.js +0 -0
- /package/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{aCZCpTkVv_k-RisOFPegk → J0gen1p9aNjUNIU1NDO5h}/_ssgManifest.js +0 -0
package/dist/server/index.js
CHANGED
|
@@ -1020,6 +1020,388 @@ var init_config = __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, {
|
|
@@ -4058,381 +4440,123 @@ Working directory: ${options.workingDirectory}`,
|
|
|
4058
4440
|
chunkIndex: i,
|
|
4059
4441
|
chunkCount,
|
|
4060
4442
|
chunkStart,
|
|
4061
|
-
isChunked: true
|
|
4062
|
-
});
|
|
4063
|
-
if (chunkCount > 1) {
|
|
4064
|
-
await new Promise((resolve11) => setTimeout(resolve11, 0));
|
|
4065
|
-
}
|
|
4066
|
-
}
|
|
4067
|
-
}
|
|
4068
|
-
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
4069
|
-
const dir = dirname5(absolutePath);
|
|
4070
|
-
if (!existsSync8(dir)) {
|
|
4071
|
-
await mkdir3(dir, { recursive: true });
|
|
4072
|
-
}
|
|
4073
|
-
await writeFile3(absolutePath, content, "utf-8");
|
|
4074
|
-
let diagnosticsOutput = "";
|
|
4075
|
-
if (options.enableLSP !== false && isSupported(absolutePath)) {
|
|
4076
|
-
await touchFile(absolutePath, true);
|
|
4077
|
-
diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
|
|
4078
|
-
}
|
|
4079
|
-
options.onProgress?.({
|
|
4080
|
-
path: absolutePath,
|
|
4081
|
-
relativePath,
|
|
4082
|
-
mode: "full",
|
|
4083
|
-
status: "completed",
|
|
4084
|
-
action,
|
|
4085
|
-
totalLength: content.length
|
|
4086
|
-
});
|
|
4087
|
-
return {
|
|
4088
|
-
success: true,
|
|
4089
|
-
path: absolutePath,
|
|
4090
|
-
relativePath,
|
|
4091
|
-
mode: "full",
|
|
4092
|
-
action,
|
|
4093
|
-
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
4094
|
-
lineCount: content.split("\n").length,
|
|
4095
|
-
...diagnosticsOutput && { diagnostics: diagnosticsOutput }
|
|
4096
|
-
};
|
|
4097
|
-
} else if (mode === "str_replace") {
|
|
4098
|
-
if (old_string === void 0 || new_string === void 0) {
|
|
4099
|
-
return {
|
|
4100
|
-
success: false,
|
|
4101
|
-
error: 'Both old_string and new_string are required for "str_replace" mode'
|
|
4102
|
-
};
|
|
4103
|
-
}
|
|
4104
|
-
if (!existsSync8(absolutePath)) {
|
|
4105
|
-
return {
|
|
4106
|
-
success: false,
|
|
4107
|
-
error: `File not found: ${path}. Use "full" mode to create new files.`
|
|
4108
|
-
};
|
|
4109
|
-
}
|
|
4110
|
-
options.onProgress?.({
|
|
4111
|
-
path: absolutePath,
|
|
4112
|
-
relativePath,
|
|
4113
|
-
mode: "str_replace",
|
|
4114
|
-
status: "started",
|
|
4115
|
-
action: "edited"
|
|
4116
|
-
});
|
|
4117
|
-
options.onProgress?.({
|
|
4118
|
-
path: absolutePath,
|
|
4119
|
-
relativePath,
|
|
4120
|
-
mode: "str_replace",
|
|
4121
|
-
status: "content",
|
|
4122
|
-
oldString: old_string,
|
|
4123
|
-
newString: new_string,
|
|
4124
|
-
action: "edited"
|
|
4125
|
-
});
|
|
4126
|
-
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
4127
|
-
const currentContent = await readFile5(absolutePath, "utf-8");
|
|
4128
|
-
if (!currentContent.includes(old_string)) {
|
|
4129
|
-
const lines = currentContent.split("\n");
|
|
4130
|
-
const preview = lines.slice(0, 20).join("\n");
|
|
4131
|
-
return {
|
|
4132
|
-
success: false,
|
|
4133
|
-
error: "old_string not found in file. The string must match EXACTLY including whitespace.",
|
|
4134
|
-
hint: "Check for differences in indentation, line endings, or invisible characters.",
|
|
4135
|
-
filePreview: lines.length > 20 ? `${preview}
|
|
4136
|
-
... (${lines.length - 20} more lines)` : preview
|
|
4137
|
-
};
|
|
4138
|
-
}
|
|
4139
|
-
const occurrences = currentContent.split(old_string).length - 1;
|
|
4140
|
-
if (occurrences > 1) {
|
|
4141
|
-
return {
|
|
4142
|
-
success: false,
|
|
4143
|
-
error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
|
|
4144
|
-
hint: "Include surrounding lines or more specific content in old_string."
|
|
4145
|
-
};
|
|
4146
|
-
}
|
|
4147
|
-
const newContent = currentContent.replace(old_string, new_string);
|
|
4148
|
-
await writeFile3(absolutePath, newContent, "utf-8");
|
|
4149
|
-
const oldLines = old_string.split("\n").length;
|
|
4150
|
-
const newLines = new_string.split("\n").length;
|
|
4151
|
-
let diagnosticsOutput = "";
|
|
4152
|
-
if (options.enableLSP !== false && isSupported(absolutePath)) {
|
|
4153
|
-
await touchFile(absolutePath, true);
|
|
4154
|
-
diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
|
|
4155
|
-
}
|
|
4156
|
-
options.onProgress?.({
|
|
4157
|
-
path: absolutePath,
|
|
4158
|
-
relativePath,
|
|
4159
|
-
mode: "str_replace",
|
|
4160
|
-
status: "completed",
|
|
4161
|
-
action: "edited"
|
|
4162
|
-
});
|
|
4163
|
-
return {
|
|
4164
|
-
success: true,
|
|
4165
|
-
path: absolutePath,
|
|
4166
|
-
relativePath,
|
|
4167
|
-
mode: "str_replace",
|
|
4168
|
-
linesRemoved: oldLines,
|
|
4169
|
-
linesAdded: newLines,
|
|
4170
|
-
lineDelta: newLines - oldLines,
|
|
4171
|
-
...diagnosticsOutput && { diagnostics: diagnosticsOutput }
|
|
4172
|
-
};
|
|
4173
|
-
}
|
|
4174
|
-
return {
|
|
4175
|
-
success: false,
|
|
4176
|
-
error: `Invalid mode: ${mode}`
|
|
4177
|
-
};
|
|
4178
|
-
} catch (error) {
|
|
4179
|
-
return {
|
|
4180
|
-
success: false,
|
|
4181
|
-
error: error.message
|
|
4182
|
-
};
|
|
4183
|
-
}
|
|
4184
|
-
}
|
|
4185
|
-
});
|
|
4186
|
-
}
|
|
4187
|
-
|
|
4188
|
-
// src/tools/todo.ts
|
|
4189
|
-
init_db();
|
|
4190
|
-
import { tool as tool4 } from "ai";
|
|
4191
|
-
import { z as z5 } from "zod";
|
|
4192
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
|
|
4193
|
-
import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
4194
|
-
import { join as join4 } from "path";
|
|
4195
|
-
function getPlansDir(workingDirectory, sessionId) {
|
|
4196
|
-
return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
|
|
4197
|
-
}
|
|
4198
|
-
function ensurePlansDir(workingDirectory, sessionId) {
|
|
4199
|
-
const dir = getPlansDir(workingDirectory, sessionId);
|
|
4200
|
-
if (!existsSync9(dir)) {
|
|
4201
|
-
mkdirSync4(dir, { recursive: true });
|
|
4202
|
-
}
|
|
4203
|
-
const gitignorePath = join4(workingDirectory, ".gitignore");
|
|
4204
|
-
if (existsSync9(gitignorePath)) {
|
|
4205
|
-
try {
|
|
4206
|
-
const content = readFileSync3(gitignorePath, "utf-8");
|
|
4207
|
-
if (!content.includes(".sparkecoder")) {
|
|
4208
|
-
appendFileSync(gitignorePath, "\n.sparkecoder/\n");
|
|
4209
|
-
}
|
|
4210
|
-
} catch {
|
|
4211
|
-
}
|
|
4212
|
-
}
|
|
4213
|
-
return dir;
|
|
4214
|
-
}
|
|
4215
|
-
function slugify(name) {
|
|
4216
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
|
|
4217
|
-
}
|
|
4218
|
-
var todoInputSchema = z5.object({
|
|
4219
|
-
action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
|
|
4220
|
-
items: z5.array(
|
|
4221
|
-
z5.object({
|
|
4222
|
-
content: z5.string().describe("Description of the task"),
|
|
4223
|
-
order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
|
|
4224
|
-
})
|
|
4225
|
-
).optional().describe('For "add" action: Array of todo items to add'),
|
|
4226
|
-
todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
|
|
4227
|
-
status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
|
|
4228
|
-
planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
|
|
4229
|
-
planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
|
|
4230
|
-
});
|
|
4231
|
-
function createTodoTool(options) {
|
|
4232
|
-
return tool4({
|
|
4233
|
-
description: `Manage your task list and persistent plans for the current session.
|
|
4234
|
-
|
|
4235
|
-
## Todo Actions (for tracking current work)
|
|
4236
|
-
- "add": Add one or more new todo items to the list
|
|
4237
|
-
- "list": View all current todo items and their status
|
|
4238
|
-
- "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
|
|
4239
|
-
- "clear": Remove all todo items from the list
|
|
4240
|
-
|
|
4241
|
-
## Plan Actions (for complex, multi-phase work)
|
|
4242
|
-
- "save_plan": Create or update a named plan \u2014 a persistent markdown document with hierarchical tasks, subtasks, and notes. Plans survive context compaction and are always available.
|
|
4243
|
-
- "list_plans": List all plans for this session
|
|
4244
|
-
- "get_plan": Read a specific plan by name
|
|
4245
|
-
- "delete_plan": Remove a plan
|
|
4246
|
-
|
|
4247
|
-
## Plans vs Todos
|
|
4248
|
-
- **Plans** are the big picture \u2014 the full spec with phases, subtasks, notes, and decisions. They persist on disk and are always injected into your context, even after old messages are summarized.
|
|
4249
|
-
- **Todos** are your current focus \u2014 the immediate steps you're working on right now.
|
|
4250
|
-
|
|
4251
|
-
## Workflow for complex tasks
|
|
4252
|
-
1. Create a plan with phases and subtasks (save_plan)
|
|
4253
|
-
2. Create todos from the first uncompleted phase (add)
|
|
4254
|
-
3. Work through the todos, marking them as you go
|
|
4255
|
-
4. When all current todos are done, update the plan (mark completed sections with [x]) and save it
|
|
4256
|
-
5. Create new todos from the next uncompleted phase
|
|
4257
|
-
6. Repeat until the plan is fully complete
|
|
4258
|
-
|
|
4259
|
-
## Plan format
|
|
4260
|
-
Plans should be markdown with this structure:
|
|
4261
|
-
\`\`\`markdown
|
|
4262
|
-
# Plan: [Title]
|
|
4263
|
-
|
|
4264
|
-
## Overview
|
|
4265
|
-
[What we're doing and why]
|
|
4266
|
-
|
|
4267
|
-
## Phase 1: [Name] [completed]
|
|
4268
|
-
- [x] Task 1
|
|
4269
|
-
- [x] Task 2
|
|
4270
|
-
|
|
4271
|
-
## Phase 2: [Name] [in_progress]
|
|
4272
|
-
- [x] Subtask 2.1
|
|
4273
|
-
- [ ] Subtask 2.2
|
|
4274
|
-
- [ ] Sub-subtask 2.2.1
|
|
4275
|
-
- [ ] Sub-subtask 2.2.2
|
|
4276
|
-
- [ ] Subtask 2.3
|
|
4277
|
-
|
|
4278
|
-
## Phase 3: [Name] [pending]
|
|
4279
|
-
- [ ] Task 1
|
|
4280
|
-
- [ ] Task 2
|
|
4281
|
-
|
|
4282
|
-
## Notes
|
|
4283
|
-
- Key decisions and context to preserve
|
|
4284
|
-
- Important file paths discovered
|
|
4285
|
-
\`\`\``,
|
|
4286
|
-
inputSchema: todoInputSchema,
|
|
4287
|
-
execute: async ({ action, items, todoId, status, planName, planContent }) => {
|
|
4288
|
-
try {
|
|
4289
|
-
switch (action) {
|
|
4290
|
-
case "add": {
|
|
4291
|
-
if (!items || items.length === 0) {
|
|
4292
|
-
return {
|
|
4293
|
-
success: false,
|
|
4294
|
-
error: "No items provided. Include at least one todo item."
|
|
4295
|
-
};
|
|
4296
|
-
}
|
|
4297
|
-
const created = await todoQueries.createMany(options.sessionId, items);
|
|
4298
|
-
return {
|
|
4299
|
-
success: true,
|
|
4300
|
-
action: "add",
|
|
4301
|
-
itemsAdded: created.length,
|
|
4302
|
-
items: created.map(formatTodoItem)
|
|
4303
|
-
};
|
|
4443
|
+
isChunked: true
|
|
4444
|
+
});
|
|
4445
|
+
if (chunkCount > 1) {
|
|
4446
|
+
await new Promise((resolve11) => setTimeout(resolve11, 0));
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4304
4449
|
}
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
pending: todos.filter((t) => t.status === "pending").length,
|
|
4310
|
-
inProgress: todos.filter((t) => t.status === "in_progress").length,
|
|
4311
|
-
completed: todos.filter((t) => t.status === "completed").length,
|
|
4312
|
-
cancelled: todos.filter((t) => t.status === "cancelled").length
|
|
4313
|
-
};
|
|
4314
|
-
return {
|
|
4315
|
-
success: true,
|
|
4316
|
-
action: "list",
|
|
4317
|
-
stats,
|
|
4318
|
-
items: todos.map(formatTodoItem)
|
|
4319
|
-
};
|
|
4450
|
+
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
4451
|
+
const dir = dirname5(absolutePath);
|
|
4452
|
+
if (!existsSync8(dir)) {
|
|
4453
|
+
await mkdir3(dir, { recursive: true });
|
|
4320
4454
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
};
|
|
4327
|
-
}
|
|
4328
|
-
if (!status) {
|
|
4329
|
-
return {
|
|
4330
|
-
success: false,
|
|
4331
|
-
error: 'status is required for "mark" action'
|
|
4332
|
-
};
|
|
4333
|
-
}
|
|
4334
|
-
const updated = await todoQueries.updateStatus(todoId, status);
|
|
4335
|
-
if (!updated) {
|
|
4336
|
-
return {
|
|
4337
|
-
success: false,
|
|
4338
|
-
error: `Todo item not found: ${todoId}`
|
|
4339
|
-
};
|
|
4340
|
-
}
|
|
4341
|
-
return {
|
|
4342
|
-
success: true,
|
|
4343
|
-
action: "mark",
|
|
4344
|
-
item: formatTodoItem(updated)
|
|
4345
|
-
};
|
|
4455
|
+
await writeFile3(absolutePath, content, "utf-8");
|
|
4456
|
+
let diagnosticsOutput = "";
|
|
4457
|
+
if (options.enableLSP !== false && isSupported(absolutePath)) {
|
|
4458
|
+
await touchFile(absolutePath, true);
|
|
4459
|
+
diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
|
|
4346
4460
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4461
|
+
options.onProgress?.({
|
|
4462
|
+
path: absolutePath,
|
|
4463
|
+
relativePath,
|
|
4464
|
+
mode: "full",
|
|
4465
|
+
status: "completed",
|
|
4466
|
+
action,
|
|
4467
|
+
totalLength: content.length
|
|
4468
|
+
});
|
|
4469
|
+
return {
|
|
4470
|
+
success: true,
|
|
4471
|
+
path: absolutePath,
|
|
4472
|
+
relativePath,
|
|
4473
|
+
mode: "full",
|
|
4474
|
+
action,
|
|
4475
|
+
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
4476
|
+
lineCount: content.split("\n").length,
|
|
4477
|
+
...diagnosticsOutput && { diagnostics: diagnosticsOutput }
|
|
4478
|
+
};
|
|
4479
|
+
} else if (mode === "str_replace") {
|
|
4480
|
+
if (old_string === void 0 || new_string === void 0) {
|
|
4349
4481
|
return {
|
|
4350
|
-
success:
|
|
4351
|
-
|
|
4352
|
-
itemsRemoved: count
|
|
4482
|
+
success: false,
|
|
4483
|
+
error: 'Both old_string and new_string are required for "str_replace" mode'
|
|
4353
4484
|
};
|
|
4354
4485
|
}
|
|
4355
|
-
|
|
4356
|
-
case "save_plan": {
|
|
4357
|
-
if (!planName) {
|
|
4358
|
-
return { success: false, error: 'planName is required for "save_plan"' };
|
|
4359
|
-
}
|
|
4360
|
-
if (!planContent) {
|
|
4361
|
-
return { success: false, error: 'planContent is required for "save_plan"' };
|
|
4362
|
-
}
|
|
4363
|
-
const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
|
|
4364
|
-
const filename = `${slugify(planName)}.md`;
|
|
4365
|
-
const filePath = join4(dir, filename);
|
|
4366
|
-
await writeFile4(filePath, planContent, "utf-8");
|
|
4486
|
+
if (!existsSync8(absolutePath)) {
|
|
4367
4487
|
return {
|
|
4368
|
-
success:
|
|
4369
|
-
|
|
4370
|
-
planName,
|
|
4371
|
-
filename,
|
|
4372
|
-
path: filePath,
|
|
4373
|
-
sizeChars: planContent.length
|
|
4488
|
+
success: false,
|
|
4489
|
+
error: `File not found: ${path}. Use "full" mode to create new files.`
|
|
4374
4490
|
};
|
|
4375
4491
|
}
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
}
|
|
4398
|
-
case "get_plan": {
|
|
4399
|
-
if (!planName) {
|
|
4400
|
-
return { success: false, error: 'planName is required for "get_plan"' };
|
|
4401
|
-
}
|
|
4402
|
-
const dir = getPlansDir(options.workingDirectory, options.sessionId);
|
|
4403
|
-
const filename = `${slugify(planName)}.md`;
|
|
4404
|
-
const filePath = join4(dir, filename);
|
|
4405
|
-
if (!existsSync9(filePath)) {
|
|
4406
|
-
return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
|
|
4407
|
-
}
|
|
4408
|
-
const content = await readFile6(filePath, "utf-8");
|
|
4492
|
+
options.onProgress?.({
|
|
4493
|
+
path: absolutePath,
|
|
4494
|
+
relativePath,
|
|
4495
|
+
mode: "str_replace",
|
|
4496
|
+
status: "started",
|
|
4497
|
+
action: "edited"
|
|
4498
|
+
});
|
|
4499
|
+
options.onProgress?.({
|
|
4500
|
+
path: absolutePath,
|
|
4501
|
+
relativePath,
|
|
4502
|
+
mode: "str_replace",
|
|
4503
|
+
status: "content",
|
|
4504
|
+
oldString: old_string,
|
|
4505
|
+
newString: new_string,
|
|
4506
|
+
action: "edited"
|
|
4507
|
+
});
|
|
4508
|
+
await backupFile(options.sessionId, options.workingDirectory, absolutePath);
|
|
4509
|
+
const currentContent = await readFile5(absolutePath, "utf-8");
|
|
4510
|
+
if (!currentContent.includes(old_string)) {
|
|
4511
|
+
const lines = currentContent.split("\n");
|
|
4512
|
+
const preview = lines.slice(0, 20).join("\n");
|
|
4409
4513
|
return {
|
|
4410
|
-
success:
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4514
|
+
success: false,
|
|
4515
|
+
error: "old_string not found in file. The string must match EXACTLY including whitespace.",
|
|
4516
|
+
hint: "Check for differences in indentation, line endings, or invisible characters.",
|
|
4517
|
+
filePreview: lines.length > 20 ? `${preview}
|
|
4518
|
+
... (${lines.length - 20} more lines)` : preview
|
|
4415
4519
|
};
|
|
4416
4520
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
return { success: false, error: 'planName is required for "delete_plan"' };
|
|
4420
|
-
}
|
|
4421
|
-
const dir = getPlansDir(options.workingDirectory, options.sessionId);
|
|
4422
|
-
const filename = `${slugify(planName)}.md`;
|
|
4423
|
-
const filePath = join4(dir, filename);
|
|
4424
|
-
if (!existsSync9(filePath)) {
|
|
4425
|
-
return { success: false, error: `Plan not found: "${planName}"` };
|
|
4426
|
-
}
|
|
4427
|
-
unlinkSync(filePath);
|
|
4428
|
-
return { success: true, action: "delete_plan", planName, deleted: true };
|
|
4429
|
-
}
|
|
4430
|
-
default:
|
|
4521
|
+
const occurrences = currentContent.split(old_string).length - 1;
|
|
4522
|
+
if (occurrences > 1) {
|
|
4431
4523
|
return {
|
|
4432
4524
|
success: false,
|
|
4433
|
-
error: `
|
|
4525
|
+
error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
|
|
4526
|
+
hint: "Include surrounding lines or more specific content in old_string."
|
|
4434
4527
|
};
|
|
4528
|
+
}
|
|
4529
|
+
const newContent = currentContent.replace(old_string, new_string);
|
|
4530
|
+
await writeFile3(absolutePath, newContent, "utf-8");
|
|
4531
|
+
const oldLines = old_string.split("\n").length;
|
|
4532
|
+
const newLines = new_string.split("\n").length;
|
|
4533
|
+
let diagnosticsOutput = "";
|
|
4534
|
+
if (options.enableLSP !== false && isSupported(absolutePath)) {
|
|
4535
|
+
await touchFile(absolutePath, true);
|
|
4536
|
+
diagnosticsOutput = await formatDiagnosticsOutput(absolutePath);
|
|
4537
|
+
}
|
|
4538
|
+
options.onProgress?.({
|
|
4539
|
+
path: absolutePath,
|
|
4540
|
+
relativePath,
|
|
4541
|
+
mode: "str_replace",
|
|
4542
|
+
status: "completed",
|
|
4543
|
+
action: "edited"
|
|
4544
|
+
});
|
|
4545
|
+
return {
|
|
4546
|
+
success: true,
|
|
4547
|
+
path: absolutePath,
|
|
4548
|
+
relativePath,
|
|
4549
|
+
mode: "str_replace",
|
|
4550
|
+
linesRemoved: oldLines,
|
|
4551
|
+
linesAdded: newLines,
|
|
4552
|
+
lineDelta: newLines - oldLines,
|
|
4553
|
+
...diagnosticsOutput && { diagnostics: diagnosticsOutput }
|
|
4554
|
+
};
|
|
4435
4555
|
}
|
|
4556
|
+
return {
|
|
4557
|
+
success: false,
|
|
4558
|
+
error: `Invalid mode: ${mode}`
|
|
4559
|
+
};
|
|
4436
4560
|
} catch (error) {
|
|
4437
4561
|
return {
|
|
4438
4562
|
success: false,
|
|
@@ -4442,30 +4566,9 @@ Plans should be markdown with this structure:
|
|
|
4442
4566
|
}
|
|
4443
4567
|
});
|
|
4444
4568
|
}
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
content: item.content,
|
|
4449
|
-
status: item.status,
|
|
4450
|
-
order: item.order,
|
|
4451
|
-
createdAt: item.createdAt.toISOString()
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
async function readSessionPlans(workingDirectory, sessionId) {
|
|
4455
|
-
const dir = getPlansDir(workingDirectory, sessionId);
|
|
4456
|
-
if (!existsSync9(dir)) return [];
|
|
4457
|
-
const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
4458
|
-
if (files.length === 0) return [];
|
|
4459
|
-
const plans = [];
|
|
4460
|
-
for (const f of files) {
|
|
4461
|
-
try {
|
|
4462
|
-
const content = await readFile6(join4(dir, f), "utf-8");
|
|
4463
|
-
plans.push({ name: f.replace(/\.md$/, ""), content });
|
|
4464
|
-
} catch {
|
|
4465
|
-
}
|
|
4466
|
-
}
|
|
4467
|
-
return plans;
|
|
4468
|
-
}
|
|
4569
|
+
|
|
4570
|
+
// src/tools/index.ts
|
|
4571
|
+
init_todo();
|
|
4469
4572
|
|
|
4470
4573
|
// src/tools/load-skill.ts
|
|
4471
4574
|
init_skills();
|
|
@@ -6066,6 +6169,7 @@ function createUploadFileTool(options) {
|
|
|
6066
6169
|
// src/tools/index.ts
|
|
6067
6170
|
init_semantic();
|
|
6068
6171
|
init_remote();
|
|
6172
|
+
init_todo();
|
|
6069
6173
|
init_semantic_search();
|
|
6070
6174
|
async function createTools(options) {
|
|
6071
6175
|
const tools = {
|
|
@@ -6137,6 +6241,7 @@ init_db();
|
|
|
6137
6241
|
// src/agent/prompts.ts
|
|
6138
6242
|
init_skills();
|
|
6139
6243
|
init_db();
|
|
6244
|
+
init_todo();
|
|
6140
6245
|
import os from "os";
|
|
6141
6246
|
function getSearchInstructions() {
|
|
6142
6247
|
const platform3 = process.platform;
|
|
@@ -6185,7 +6290,11 @@ async function buildSystemPrompt(options) {
|
|
|
6185
6290
|
const todos = await todoQueries.getBySession(sessionId);
|
|
6186
6291
|
const todosContext = formatTodosForContext(todos);
|
|
6187
6292
|
const plans = await readSessionPlans(workingDirectory, sessionId);
|
|
6188
|
-
const
|
|
6293
|
+
const allTodosDone = todos.length > 0 && todos.every(
|
|
6294
|
+
(t) => t.status === "completed" || t.status === "cancelled"
|
|
6295
|
+
);
|
|
6296
|
+
const hasNoTodos = todos.length === 0;
|
|
6297
|
+
const plansContext = formatPlansForContext(plans, allTodosDone || hasNoTodos);
|
|
6189
6298
|
const platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
|
|
6190
6299
|
const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
6191
6300
|
const searchInstructions = getSearchInstructions();
|
|
@@ -6218,16 +6327,19 @@ Use the **todo tool** to manage both immediate tasks AND persistent plans:
|
|
|
6218
6327
|
|
|
6219
6328
|
**For complex, multi-phase tasks:** Create a persistent **plan** first.
|
|
6220
6329
|
1. Research the codebase to understand what you're working with
|
|
6221
|
-
2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks
|
|
6222
|
-
3.
|
|
6223
|
-
4. Work through the todos
|
|
6224
|
-
5. When done, update the plan
|
|
6225
|
-
6.
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
Plans persist on disk and are always injected into your context \u2014 they survive context compaction even in very long sessions
|
|
6229
|
-
|
|
6230
|
-
|
|
6330
|
+
2. Create a plan with save_plan \u2014 a structured markdown document with phases and subtasks using checkboxes (- [ ] for uncompleted, - [x] for completed)
|
|
6331
|
+
3. Todos are **auto-created** from the first uncompleted phase when you save the plan (if no active todos exist)
|
|
6332
|
+
4. Work through the todos, marking them as you go
|
|
6333
|
+
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
|
|
6334
|
+
6. Repeat until all phases are complete, then delete the plan
|
|
6335
|
+
|
|
6336
|
+
**Key details:**
|
|
6337
|
+
- Plans persist on disk and are always injected into your context \u2014 they survive context compaction even in very long sessions
|
|
6338
|
+
- You can have multiple plans active at once (e.g., one for frontend work, one for backend)
|
|
6339
|
+
- 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)
|
|
6340
|
+
- Only top-level checklist items (- [ ]) become todos \u2014 indented sub-items are part of the task detail
|
|
6341
|
+
- Sections named Overview, Notes, Key Decisions, etc. are not treated as phases
|
|
6342
|
+
- You can clear the todo list and restart it, and do multiple things inside of one session
|
|
6231
6343
|
|
|
6232
6344
|
### bash Tool
|
|
6233
6345
|
The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
|
|
@@ -6478,7 +6590,7 @@ function formatTodosForContext(todos) {
|
|
|
6478
6590
|
}
|
|
6479
6591
|
var MAX_PLAN_CHARS = 3e4;
|
|
6480
6592
|
var MAX_TOTAL_PLANS_CHARS = 6e4;
|
|
6481
|
-
function formatPlansForContext(plans) {
|
|
6593
|
+
function formatPlansForContext(plans, shouldContinue) {
|
|
6482
6594
|
if (plans.length === 0) return "";
|
|
6483
6595
|
let totalChars = 0;
|
|
6484
6596
|
const sections = [];
|
|
@@ -6487,6 +6599,13 @@ function formatPlansForContext(plans) {
|
|
|
6487
6599
|
sections.push("These plans persist across context compaction \u2014 they are always available.");
|
|
6488
6600
|
sections.push("When you finish your current todos, check these plans for the next uncompleted phase,");
|
|
6489
6601
|
sections.push("update the plan (mark completed items with [x]), then create new todos for the next phase.");
|
|
6602
|
+
if (shouldContinue) {
|
|
6603
|
+
sections.push("");
|
|
6604
|
+
sections.push("**>>> ACTION NEEDED: Your current todos are all done but the plan has remaining phases. To continue:**");
|
|
6605
|
+
sections.push("**1. Update the plan content: change the completed phase heading to include [completed] and mark its items with [x]**");
|
|
6606
|
+
sections.push("**2. Call save_plan with the updated content \u2014 new todos will be auto-created from the next uncompleted phase**");
|
|
6607
|
+
sections.push("**3. Continue working on the new todos <<<**");
|
|
6608
|
+
}
|
|
6490
6609
|
sections.push("");
|
|
6491
6610
|
for (const plan of plans) {
|
|
6492
6611
|
let content = plan.content;
|
|
@@ -8204,6 +8323,24 @@ sessions.get("/:id/todos", async (c) => {
|
|
|
8204
8323
|
} : null
|
|
8205
8324
|
});
|
|
8206
8325
|
});
|
|
8326
|
+
sessions.get("/:id/plans", async (c) => {
|
|
8327
|
+
const id = c.req.param("id");
|
|
8328
|
+
const session = await sessionQueries.getById(id);
|
|
8329
|
+
if (!session) {
|
|
8330
|
+
return c.json({ error: "Session not found" }, 404);
|
|
8331
|
+
}
|
|
8332
|
+
const { readSessionPlans: readSessionPlans2 } = await Promise.resolve().then(() => (init_todo(), todo_exports));
|
|
8333
|
+
const plans = await readSessionPlans2(session.workingDirectory, id);
|
|
8334
|
+
return c.json({
|
|
8335
|
+
sessionId: id,
|
|
8336
|
+
plans: plans.map((p) => ({
|
|
8337
|
+
name: p.name,
|
|
8338
|
+
content: p.content,
|
|
8339
|
+
sizeChars: p.content.length
|
|
8340
|
+
})),
|
|
8341
|
+
count: plans.length
|
|
8342
|
+
});
|
|
8343
|
+
});
|
|
8207
8344
|
sessions.get("/:id/checkpoints", async (c) => {
|
|
8208
8345
|
const id = c.req.param("id");
|
|
8209
8346
|
const session = await sessionQueries.getById(id);
|