sparkecoder 0.1.82 → 0.1.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/agent/index.js +284 -71
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +370 -144
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +358 -132
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +358 -132
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/skills/default/browser.md +30 -0
  10. package/dist/tools/index.d.ts +117 -1
  11. package/dist/tools/index.js +183 -41
  12. package/dist/tools/index.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/skills/default/browser.md +30 -0
  15. package/web/.next/BUILD_ID +1 -1
  16. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  17. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  18. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  19. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  20. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  35. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  44. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  53. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  62. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  70. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  78. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  79. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  80. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  81. package/web/.next/standalone/web/package-lock.json +3 -3
  82. /package/web/.next/standalone/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_buildManifest.js +0 -0
  83. /package/web/.next/standalone/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_clientMiddlewareManifest.json +0 -0
  84. /package/web/.next/standalone/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_ssgManifest.js +0 -0
  85. /package/web/.next/standalone/web/.next/static/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_buildManifest.js +0 -0
  86. /package/web/.next/standalone/web/.next/static/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_clientMiddlewareManifest.json +0 -0
  87. /package/web/.next/standalone/web/.next/static/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_ssgManifest.js +0 -0
  88. /package/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_buildManifest.js +0 -0
  89. /package/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_clientMiddlewareManifest.json +0 -0
  90. /package/web/.next/static/{Ne3ChQc_mw5oh4Y1Rr7qj → aCZCpTkVv_k-RisOFPegk}/_ssgManifest.js +0 -0
@@ -110,6 +110,36 @@ agent-browser wait --url "**/dashboard" # Wait for URL pattern
110
110
  agent-browser wait 2000 # Wait milliseconds
111
111
  ```
112
112
 
113
+ ### Video Recording
114
+ Record browser actions to a WebM video file. Useful for demos, bug reproductions, or proof of work.
115
+
116
+ ```bash
117
+ mkdir -p ".sparkecode-artifacts/recordings"
118
+
119
+ # Start recording (from current page — preserves login state)
120
+ agent-browser record start ".sparkecode-artifacts/recordings/demo.webm"
121
+
122
+ # Do your interactions...
123
+ agent-browser click @e3
124
+ agent-browser fill @e4 "test@example.com"
125
+ agent-browser click @e5
126
+
127
+ # Stop and save
128
+ agent-browser record stop
129
+ ```
130
+
131
+ After recording, upload the video to get a shareable URL:
132
+ ```
133
+ upload_file({ path: ".sparkecode-artifacts/recordings/demo.webm", name: "demo.webm" })
134
+ ```
135
+
136
+ You can also restart recording into a new file without closing the browser:
137
+ ```bash
138
+ agent-browser record restart ".sparkecode-artifacts/recordings/take2.webm"
139
+ ```
140
+
141
+ **Note:** In task mode, browser sessions are also automatically recorded in the background. The MP4 recording is uploaded when the task completes and appears in the `GET /tasks/:id` response as `browserRecordings` and in the `task.completed` webhook as `browserRecordingUrls`. You do NOT need to manually record in task mode — it happens for free.
142
+
113
143
  ### Sessions
114
144
  ```bash
115
145
  agent-browser sessions # List active sessions
@@ -80,15 +80,18 @@ declare function createReadFileTool(options: ReadFileToolOptions): ai.Tool<{
80
80
 
81
81
  interface TodoToolOptions {
82
82
  sessionId: string;
83
+ workingDirectory: string;
83
84
  }
84
85
  declare function createTodoTool(options: TodoToolOptions): ai.Tool<{
85
- action: "add" | "list" | "mark" | "clear";
86
+ action: "add" | "list" | "mark" | "clear" | "save_plan" | "list_plans" | "get_plan" | "delete_plan";
86
87
  status?: "completed" | "pending" | "in_progress" | "cancelled" | undefined;
87
88
  items?: {
88
89
  content: string;
89
90
  order?: number | undefined;
90
91
  }[] | undefined;
91
92
  todoId?: string | undefined;
93
+ planName?: string | undefined;
94
+ planContent?: string | undefined;
92
95
  }, {
93
96
  success: boolean;
94
97
  action: string;
@@ -103,6 +106,14 @@ declare function createTodoTool(options: TodoToolOptions): ai.Tool<{
103
106
  stats?: undefined;
104
107
  item?: undefined;
105
108
  itemsRemoved?: undefined;
109
+ planName?: undefined;
110
+ filename?: undefined;
111
+ path?: undefined;
112
+ sizeChars?: undefined;
113
+ plans?: undefined;
114
+ count?: undefined;
115
+ content?: undefined;
116
+ deleted?: undefined;
106
117
  error?: undefined;
107
118
  } | {
108
119
  success: boolean;
@@ -124,6 +135,14 @@ declare function createTodoTool(options: TodoToolOptions): ai.Tool<{
124
135
  itemsAdded?: undefined;
125
136
  item?: undefined;
126
137
  itemsRemoved?: undefined;
138
+ planName?: undefined;
139
+ filename?: undefined;
140
+ path?: undefined;
141
+ sizeChars?: undefined;
142
+ plans?: undefined;
143
+ count?: undefined;
144
+ content?: undefined;
145
+ deleted?: undefined;
127
146
  error?: undefined;
128
147
  } | {
129
148
  success: boolean;
@@ -139,6 +158,14 @@ declare function createTodoTool(options: TodoToolOptions): ai.Tool<{
139
158
  items?: undefined;
140
159
  stats?: undefined;
141
160
  itemsRemoved?: undefined;
161
+ planName?: undefined;
162
+ filename?: undefined;
163
+ path?: undefined;
164
+ sizeChars?: undefined;
165
+ plans?: undefined;
166
+ count?: undefined;
167
+ content?: undefined;
168
+ deleted?: undefined;
142
169
  error?: undefined;
143
170
  } | {
144
171
  success: boolean;
@@ -148,6 +175,87 @@ declare function createTodoTool(options: TodoToolOptions): ai.Tool<{
148
175
  items?: undefined;
149
176
  stats?: undefined;
150
177
  item?: undefined;
178
+ planName?: undefined;
179
+ filename?: undefined;
180
+ path?: undefined;
181
+ sizeChars?: undefined;
182
+ plans?: undefined;
183
+ count?: undefined;
184
+ content?: undefined;
185
+ deleted?: undefined;
186
+ error?: undefined;
187
+ } | {
188
+ success: boolean;
189
+ action: string;
190
+ planName: string;
191
+ filename: string;
192
+ path: string;
193
+ sizeChars: number;
194
+ itemsAdded?: undefined;
195
+ items?: undefined;
196
+ stats?: undefined;
197
+ item?: undefined;
198
+ itemsRemoved?: undefined;
199
+ plans?: undefined;
200
+ count?: undefined;
201
+ content?: undefined;
202
+ deleted?: undefined;
203
+ error?: undefined;
204
+ } | {
205
+ success: boolean;
206
+ action: string;
207
+ plans: {
208
+ name: string;
209
+ title: string;
210
+ filename: string;
211
+ sizeChars: number;
212
+ }[];
213
+ count: number;
214
+ itemsAdded?: undefined;
215
+ items?: undefined;
216
+ stats?: undefined;
217
+ item?: undefined;
218
+ itemsRemoved?: undefined;
219
+ planName?: undefined;
220
+ filename?: undefined;
221
+ path?: undefined;
222
+ sizeChars?: undefined;
223
+ content?: undefined;
224
+ deleted?: undefined;
225
+ error?: undefined;
226
+ } | {
227
+ success: boolean;
228
+ action: string;
229
+ planName: string;
230
+ content: string;
231
+ sizeChars: number;
232
+ itemsAdded?: undefined;
233
+ items?: undefined;
234
+ stats?: undefined;
235
+ item?: undefined;
236
+ itemsRemoved?: undefined;
237
+ filename?: undefined;
238
+ path?: undefined;
239
+ plans?: undefined;
240
+ count?: undefined;
241
+ deleted?: undefined;
242
+ error?: undefined;
243
+ } | {
244
+ success: boolean;
245
+ action: string;
246
+ planName: string;
247
+ deleted: boolean;
248
+ itemsAdded?: undefined;
249
+ items?: undefined;
250
+ stats?: undefined;
251
+ item?: undefined;
252
+ itemsRemoved?: undefined;
253
+ filename?: undefined;
254
+ path?: undefined;
255
+ sizeChars?: undefined;
256
+ plans?: undefined;
257
+ count?: undefined;
258
+ content?: undefined;
151
259
  error?: undefined;
152
260
  } | {
153
261
  success: boolean;
@@ -158,6 +266,14 @@ declare function createTodoTool(options: TodoToolOptions): ai.Tool<{
158
266
  stats?: undefined;
159
267
  item?: undefined;
160
268
  itemsRemoved?: undefined;
269
+ planName?: undefined;
270
+ filename?: undefined;
271
+ path?: undefined;
272
+ sizeChars?: undefined;
273
+ plans?: undefined;
274
+ count?: undefined;
275
+ content?: undefined;
276
+ deleted?: undefined;
161
277
  }>;
162
278
 
163
279
  interface LoadSkillToolOptions {
@@ -848,7 +848,7 @@ var init_client = __esm({
848
848
  });
849
849
 
850
850
  // src/semantic/indexer.ts
851
- import { readFileSync as readFileSync4, statSync } from "fs";
851
+ import { readFileSync as readFileSync5, statSync } from "fs";
852
852
  import { relative as relative6 } from "path";
853
853
  import { minimatch as minimatch2 } from "minimatch";
854
854
  async function getIndexStatus(workingDirectory) {
@@ -933,8 +933,8 @@ __export(semantic_search_exports, {
933
933
  });
934
934
  import { tool as tool8 } from "ai";
935
935
  import { z as z9 } from "zod";
936
- import { existsSync as existsSync12, readFileSync as readFileSync5 } from "fs";
937
- import { join as join5 } from "path";
936
+ import { existsSync as existsSync13, readFileSync as readFileSync6 } from "fs";
937
+ import { join as join6 } from "path";
938
938
  import { minimatch as minimatch3 } from "minimatch";
939
939
  function createSemanticSearchTool(options) {
940
940
  return tool8({
@@ -1001,13 +1001,13 @@ Returns matching code snippets with file paths, line numbers, and relevance scor
1001
1001
  if (language && matchLanguage !== language.toLowerCase()) {
1002
1002
  continue;
1003
1003
  }
1004
- const fullPath = join5(options.workingDirectory, filePath);
1005
- if (!existsSync12(fullPath)) {
1004
+ const fullPath = join6(options.workingDirectory, filePath);
1005
+ if (!existsSync13(fullPath)) {
1006
1006
  continue;
1007
1007
  }
1008
1008
  let snippet = "";
1009
1009
  try {
1010
- const content = readFileSync5(fullPath, "utf-8");
1010
+ const content = readFileSync6(fullPath, "utf-8");
1011
1011
  const lines = content.split("\n");
1012
1012
  const snippetLines = lines.slice(
1013
1013
  Math.max(0, startLine - 1),
@@ -2729,8 +2729,34 @@ Working directory: ${options.workingDirectory}`,
2729
2729
  init_db();
2730
2730
  import { tool as tool4 } from "ai";
2731
2731
  import { z as z5 } from "zod";
2732
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync, readFileSync as readFileSync3, appendFileSync } from "fs";
2733
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
2734
+ import { join as join4 } from "path";
2735
+ function getPlansDir(workingDirectory, sessionId) {
2736
+ return join4(workingDirectory, ".sparkecoder", "plans", sessionId);
2737
+ }
2738
+ function ensurePlansDir(workingDirectory, sessionId) {
2739
+ const dir = getPlansDir(workingDirectory, sessionId);
2740
+ if (!existsSync9(dir)) {
2741
+ mkdirSync4(dir, { recursive: true });
2742
+ }
2743
+ const gitignorePath = join4(workingDirectory, ".gitignore");
2744
+ if (existsSync9(gitignorePath)) {
2745
+ try {
2746
+ const content = readFileSync3(gitignorePath, "utf-8");
2747
+ if (!content.includes(".sparkecoder")) {
2748
+ appendFileSync(gitignorePath, "\n.sparkecoder/\n");
2749
+ }
2750
+ } catch {
2751
+ }
2752
+ }
2753
+ return dir;
2754
+ }
2755
+ function slugify(name) {
2756
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "plan";
2757
+ }
2732
2758
  var todoInputSchema = z5.object({
2733
- action: z5.enum(["add", "list", "mark", "clear"]).describe("The action to perform on the todo list"),
2759
+ action: z5.enum(["add", "list", "mark", "clear", "save_plan", "list_plans", "get_plan", "delete_plan"]).describe("The action to perform"),
2734
2760
  items: z5.array(
2735
2761
  z5.object({
2736
2762
  content: z5.string().describe("Description of the task"),
@@ -2738,27 +2764,67 @@ var todoInputSchema = z5.object({
2738
2764
  })
2739
2765
  ).optional().describe('For "add" action: Array of todo items to add'),
2740
2766
  todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
2741
- status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item')
2767
+ status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item'),
2768
+ planName: z5.string().optional().describe('For plan actions: Name of the plan (e.g. "auth-system", "db-migration")'),
2769
+ planContent: z5.string().optional().describe('For "save_plan": Full plan content as markdown with hierarchical tasks using checkboxes')
2742
2770
  });
2743
2771
  function createTodoTool(options) {
2744
2772
  return tool4({
2745
- description: `Manage your task list for the current session. Use this to:
2746
- - Break down complex tasks into smaller steps
2747
- - Track progress on multi-step operations
2748
- - Organize your work systematically
2773
+ description: `Manage your task list and persistent plans for the current session.
2749
2774
 
2750
- Available actions:
2775
+ ## Todo Actions (for tracking current work)
2751
2776
  - "add": Add one or more new todo items to the list
2752
2777
  - "list": View all current todo items and their status
2753
2778
  - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
2754
2779
  - "clear": Remove all todo items from the list
2755
2780
 
2756
- Best practices:
2757
- - Add todos before starting complex tasks
2758
- - Mark items as "in_progress" when actively working on them
2759
- - Update status as you complete each step`,
2781
+ ## Plan Actions (for complex, multi-phase work)
2782
+ - "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.
2783
+ - "list_plans": List all plans for this session
2784
+ - "get_plan": Read a specific plan by name
2785
+ - "delete_plan": Remove a plan
2786
+
2787
+ ## Plans vs Todos
2788
+ - **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.
2789
+ - **Todos** are your current focus \u2014 the immediate steps you're working on right now.
2790
+
2791
+ ## Workflow for complex tasks
2792
+ 1. Create a plan with phases and subtasks (save_plan)
2793
+ 2. Create todos from the first uncompleted phase (add)
2794
+ 3. Work through the todos, marking them as you go
2795
+ 4. When all current todos are done, update the plan (mark completed sections with [x]) and save it
2796
+ 5. Create new todos from the next uncompleted phase
2797
+ 6. Repeat until the plan is fully complete
2798
+
2799
+ ## Plan format
2800
+ Plans should be markdown with this structure:
2801
+ \`\`\`markdown
2802
+ # Plan: [Title]
2803
+
2804
+ ## Overview
2805
+ [What we're doing and why]
2806
+
2807
+ ## Phase 1: [Name] [completed]
2808
+ - [x] Task 1
2809
+ - [x] Task 2
2810
+
2811
+ ## Phase 2: [Name] [in_progress]
2812
+ - [x] Subtask 2.1
2813
+ - [ ] Subtask 2.2
2814
+ - [ ] Sub-subtask 2.2.1
2815
+ - [ ] Sub-subtask 2.2.2
2816
+ - [ ] Subtask 2.3
2817
+
2818
+ ## Phase 3: [Name] [pending]
2819
+ - [ ] Task 1
2820
+ - [ ] Task 2
2821
+
2822
+ ## Notes
2823
+ - Key decisions and context to preserve
2824
+ - Important file paths discovered
2825
+ \`\`\``,
2760
2826
  inputSchema: todoInputSchema,
2761
- execute: async ({ action, items, todoId, status }) => {
2827
+ execute: async ({ action, items, todoId, status, planName, planContent }) => {
2762
2828
  try {
2763
2829
  switch (action) {
2764
2830
  case "add": {
@@ -2826,6 +2892,81 @@ Best practices:
2826
2892
  itemsRemoved: count
2827
2893
  };
2828
2894
  }
2895
+ // ── Plan actions ─────────────────────────────────────────
2896
+ case "save_plan": {
2897
+ if (!planName) {
2898
+ return { success: false, error: 'planName is required for "save_plan"' };
2899
+ }
2900
+ if (!planContent) {
2901
+ return { success: false, error: 'planContent is required for "save_plan"' };
2902
+ }
2903
+ const dir = ensurePlansDir(options.workingDirectory, options.sessionId);
2904
+ const filename = `${slugify(planName)}.md`;
2905
+ const filePath = join4(dir, filename);
2906
+ await writeFile4(filePath, planContent, "utf-8");
2907
+ return {
2908
+ success: true,
2909
+ action: "save_plan",
2910
+ planName,
2911
+ filename,
2912
+ path: filePath,
2913
+ sizeChars: planContent.length
2914
+ };
2915
+ }
2916
+ case "list_plans": {
2917
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
2918
+ if (!existsSync9(dir)) {
2919
+ return { success: true, action: "list_plans", plans: [], count: 0 };
2920
+ }
2921
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
2922
+ const plans = [];
2923
+ for (const f of files) {
2924
+ try {
2925
+ const content = await readFile6(join4(dir, f), "utf-8");
2926
+ const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
2927
+ plans.push({
2928
+ name: f.replace(/\.md$/, ""),
2929
+ title: titleMatch?.[1]?.trim() || f.replace(/\.md$/, ""),
2930
+ filename: f,
2931
+ sizeChars: content.length
2932
+ });
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ return { success: true, action: "list_plans", plans, count: plans.length };
2937
+ }
2938
+ case "get_plan": {
2939
+ if (!planName) {
2940
+ return { success: false, error: 'planName is required for "get_plan"' };
2941
+ }
2942
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
2943
+ const filename = `${slugify(planName)}.md`;
2944
+ const filePath = join4(dir, filename);
2945
+ if (!existsSync9(filePath)) {
2946
+ return { success: false, error: `Plan not found: "${planName}" (looked for ${filename})` };
2947
+ }
2948
+ const content = await readFile6(filePath, "utf-8");
2949
+ return {
2950
+ success: true,
2951
+ action: "get_plan",
2952
+ planName,
2953
+ content,
2954
+ sizeChars: content.length
2955
+ };
2956
+ }
2957
+ case "delete_plan": {
2958
+ if (!planName) {
2959
+ return { success: false, error: 'planName is required for "delete_plan"' };
2960
+ }
2961
+ const dir = getPlansDir(options.workingDirectory, options.sessionId);
2962
+ const filename = `${slugify(planName)}.md`;
2963
+ const filePath = join4(dir, filename);
2964
+ if (!existsSync9(filePath)) {
2965
+ return { success: false, error: `Plan not found: "${planName}"` };
2966
+ }
2967
+ unlinkSync(filePath);
2968
+ return { success: true, action: "delete_plan", planName, deleted: true };
2969
+ }
2829
2970
  default:
2830
2971
  return {
2831
2972
  success: false,
@@ -2857,9 +2998,9 @@ import { z as z6 } from "zod";
2857
2998
 
2858
2999
  // src/skills/index.ts
2859
3000
  init_types();
2860
- import { readFile as readFile6, readdir } from "fs/promises";
3001
+ import { readFile as readFile7, readdir } from "fs/promises";
2861
3002
  import { resolve as resolve6, basename, extname as extname4, relative as relative4 } from "path";
2862
- import { existsSync as existsSync9 } from "fs";
3003
+ import { existsSync as existsSync10 } from "fs";
2863
3004
  import { minimatch } from "minimatch";
2864
3005
  function parseSkillFrontmatter(content) {
2865
3006
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -2937,7 +3078,7 @@ async function loadSkillsFromDirectory(directory, options = {}) {
2937
3078
  defaultLoadType = "on_demand",
2938
3079
  forceAlwaysApply = false
2939
3080
  } = options;
2940
- if (!existsSync9(directory)) {
3081
+ if (!existsSync10(directory)) {
2941
3082
  return [];
2942
3083
  }
2943
3084
  const skills = [];
@@ -2947,7 +3088,7 @@ async function loadSkillsFromDirectory(directory, options = {}) {
2947
3088
  let fileName;
2948
3089
  if (entry.isDirectory()) {
2949
3090
  const skillMdPath = resolve6(directory, entry.name, "SKILL.md");
2950
- if (existsSync9(skillMdPath)) {
3091
+ if (existsSync10(skillMdPath)) {
2951
3092
  filePath = skillMdPath;
2952
3093
  fileName = entry.name;
2953
3094
  } else {
@@ -2959,7 +3100,7 @@ async function loadSkillsFromDirectory(directory, options = {}) {
2959
3100
  } else {
2960
3101
  continue;
2961
3102
  }
2962
- const content = await readFile6(filePath, "utf-8");
3103
+ const content = await readFile7(filePath, "utf-8");
2963
3104
  const parsed = parseSkillFrontmatter(content);
2964
3105
  if (parsed) {
2965
3106
  const alwaysApply = forceAlwaysApply || parsed.metadata.alwaysApply;
@@ -3013,7 +3154,7 @@ async function loadSkillContent(skillName, directories) {
3013
3154
  if (!skill) {
3014
3155
  return null;
3015
3156
  }
3016
- const content = await readFile6(skill.filePath, "utf-8");
3157
+ const content = await readFile7(skill.filePath, "utf-8");
3017
3158
  const parsed = parseSkillFrontmatter(content);
3018
3159
  return {
3019
3160
  ...skill,
@@ -3118,7 +3259,7 @@ Once loaded, a skill's content will be available in the conversation context.`,
3118
3259
  import { tool as tool6 } from "ai";
3119
3260
  import { z as z7 } from "zod";
3120
3261
  import { resolve as resolve7, relative as relative5, isAbsolute as isAbsolute3, extname as extname5 } from "path";
3121
- import { existsSync as existsSync10 } from "fs";
3262
+ import { existsSync as existsSync11 } from "fs";
3122
3263
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
3123
3264
  var linterInputSchema = z7.object({
3124
3265
  paths: z7.array(z7.string()).optional().describe("File or directory paths to check for lint errors. If not provided, returns diagnostics for all recently touched files."),
@@ -3186,7 +3327,7 @@ Working directory: ${options.workingDirectory}`,
3186
3327
  const filesToCheck = [];
3187
3328
  for (const path of paths) {
3188
3329
  const absolutePath = isAbsolute3(path) ? path : resolve7(options.workingDirectory, path);
3189
- if (!existsSync10(absolutePath)) {
3330
+ if (!existsSync11(absolutePath)) {
3190
3331
  continue;
3191
3332
  }
3192
3333
  const stats = await stat2(absolutePath);
@@ -3679,17 +3820,17 @@ import { tool as tool9 } from "ai";
3679
3820
  import { z as z10 } from "zod";
3680
3821
  import { exec as exec4 } from "child_process";
3681
3822
  import { promisify as promisify4 } from "util";
3682
- import { readFile as readFile8, stat as stat3, readdir as readdir4 } from "fs/promises";
3823
+ import { readFile as readFile9, stat as stat3, readdir as readdir4 } from "fs/promises";
3683
3824
  import { resolve as resolve9, relative as relative8, isAbsolute as isAbsolute5 } from "path";
3684
- import { existsSync as existsSync13 } from "fs";
3825
+ import { existsSync as existsSync14 } from "fs";
3685
3826
  init_semantic();
3686
3827
 
3687
3828
  // src/tools/code-graph.ts
3688
3829
  import { tool as tool7 } from "ai";
3689
3830
  import { z as z8 } from "zod";
3690
3831
  import { resolve as resolve8, relative as relative7, isAbsolute as isAbsolute4, basename as basename3 } from "path";
3691
- import { readFile as readFile7, readdir as readdir3 } from "fs/promises";
3692
- import { existsSync as existsSync11 } from "fs";
3832
+ import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
3833
+ import { existsSync as existsSync12 } from "fs";
3693
3834
  import { fileURLToPath as fileURLToPath2 } from "url";
3694
3835
  import { execFileSync } from "child_process";
3695
3836
  var codeGraphInputSchema = z8.object({
@@ -3826,7 +3967,7 @@ async function grepForSymbol(symbol, workingDirectory) {
3826
3967
  const ext = entry.name.substring(entry.name.lastIndexOf("."));
3827
3968
  if (!SUPPORTED_EXTS.has(ext)) continue;
3828
3969
  remaining--;
3829
- const content = await readFile7(fullPath, "utf-8");
3970
+ const content = await readFile8(fullPath, "utf-8");
3830
3971
  const lines = content.split("\n");
3831
3972
  for (let i = 0; i < lines.length; i++) {
3832
3973
  if (defPattern.test(lines[i])) {
@@ -3875,7 +4016,7 @@ Working directory: ${options.workingDirectory}`,
3875
4016
  let defSymbol = null;
3876
4017
  if (filePath) {
3877
4018
  const absPath = isAbsolute4(filePath) ? filePath : resolve8(options.workingDirectory, filePath);
3878
- if (!existsSync11(absPath)) {
4019
+ if (!existsSync12(absPath)) {
3879
4020
  return { success: false, error: `File not found: ${filePath}` };
3880
4021
  }
3881
4022
  if (!isSupported(absPath)) {
@@ -3889,7 +4030,7 @@ Working directory: ${options.workingDirectory}`,
3889
4030
  defLine = defSymbol.selectionRange.start.line;
3890
4031
  defChar = defSymbol.selectionRange.start.character;
3891
4032
  } else {
3892
- const content = await readFile7(absPath, "utf-8");
4033
+ const content = await readFile8(absPath, "utf-8");
3893
4034
  const lines2 = content.split("\n");
3894
4035
  const defPattern = new RegExp(
3895
4036
  `(export|function|const|let|var|class|interface|type|enum)\\s+.*\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`
@@ -4302,7 +4443,7 @@ Keep it concise but INCLUDE THE ACTUAL DATA.`;
4302
4443
  execute: async ({ path, startLine, endLine }) => {
4303
4444
  try {
4304
4445
  const absolutePath = isAbsolute5(path) ? path : resolve9(workingDirectory, path);
4305
- if (!existsSync13(absolutePath)) {
4446
+ if (!existsSync14(absolutePath)) {
4306
4447
  return {
4307
4448
  success: false,
4308
4449
  error: `File not found: ${path}`
@@ -4315,7 +4456,7 @@ Keep it concise but INCLUDE THE ACTUAL DATA.`;
4315
4456
  error: `File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Use startLine/endLine to read portions.`
4316
4457
  };
4317
4458
  }
4318
- let content = await readFile8(absolutePath, "utf-8");
4459
+ let content = await readFile9(absolutePath, "utf-8");
4319
4460
  if (startLine !== void 0 || endLine !== void 0) {
4320
4461
  const lines = content.split("\n");
4321
4462
  const start = (startLine ?? 1) - 1;
@@ -4346,7 +4487,7 @@ Keep it concise but INCLUDE THE ACTUAL DATA.`;
4346
4487
  execute: async ({ path, recursive, maxDepth }) => {
4347
4488
  try {
4348
4489
  const absolutePath = isAbsolute5(path) ? path : resolve9(workingDirectory, path);
4349
- if (!existsSync13(absolutePath)) {
4490
+ if (!existsSync14(absolutePath)) {
4350
4491
  return {
4351
4492
  success: false,
4352
4493
  error: `Directory not found: ${path}`
@@ -4713,8 +4854,8 @@ function createTaskFailedTool(options) {
4713
4854
  // src/tools/upload-file.ts
4714
4855
  import { tool as tool12 } from "ai";
4715
4856
  import { z as z13 } from "zod";
4716
- import { readFile as readFile9, stat as stat4 } from "fs/promises";
4717
- import { join as join6, basename as basename4, extname as extname7 } from "path";
4857
+ import { readFile as readFile10, stat as stat4 } from "fs/promises";
4858
+ import { join as join7, basename as basename4, extname as extname7 } from "path";
4718
4859
  var MIME_TYPES = {
4719
4860
  ".txt": "text/plain",
4720
4861
  ".md": "text/markdown",
@@ -4756,7 +4897,7 @@ function createUploadFileTool(options) {
4756
4897
  error: "File upload is not available \u2014 remote server with GCS is not configured."
4757
4898
  };
4758
4899
  }
4759
- const fullPath = input.path.startsWith("/") ? input.path : join6(options.workingDirectory, input.path);
4900
+ const fullPath = input.path.startsWith("/") ? input.path : join7(options.workingDirectory, input.path);
4760
4901
  try {
4761
4902
  await stat4(fullPath);
4762
4903
  } catch {
@@ -4774,7 +4915,7 @@ function createUploadFileTool(options) {
4774
4915
  contentType,
4775
4916
  "general"
4776
4917
  );
4777
- const fileData = await readFile9(fullPath);
4918
+ const fileData = await readFile10(fullPath);
4778
4919
  const putRes = await fetch(uploadInfo.uploadUrl, {
4779
4920
  method: "PUT",
4780
4921
  headers: { "Content-Type": contentType },
@@ -4829,7 +4970,8 @@ async function createTools(options) {
4829
4970
  onProgress: options.onWriteFileProgress
4830
4971
  }),
4831
4972
  todo: createTodoTool({
4832
- sessionId: options.sessionId
4973
+ sessionId: options.sessionId,
4974
+ workingDirectory: options.workingDirectory
4833
4975
  }),
4834
4976
  load_skill: createLoadSkillTool({
4835
4977
  sessionId: options.sessionId,