min-agent 0.2.1 → 0.4.0

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 (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
@@ -1,14 +1,24 @@
1
1
  import { tool, jsonSchema, streamText, stepCountIs } from "ai";
2
2
  import { resolveModel } from "../provider.js";
3
3
  import { createTools } from "./index.js";
4
- import { getMcpTools } from "../mcp.js";
5
- import { getSkillsTool, getSkills } from "../skills.js";
4
+ import { getMcpTools, getMcpCatalogTools } from "../mcp.js";
5
+ import { attachSkills } from "../skills.js";
6
6
  import { loadPluginTools } from "../plugins.js";
7
7
  import { DoomLoopDetector } from "../doom-loop.js";
8
+ import { log } from "../logger.js";
9
+ import { getEffectiveConfig } from "../config.js";
8
10
  import { stripThinkingFromAssistantText } from "../assistant-stream.js";
11
+ import { unwrapXmlSearchTags } from "../xml-search.js";
9
12
  import { truncateToolOutput } from "../tool-output.js";
13
+ import { createTodoTool } from "./todo.js";
10
14
  const SUB_AGENT_MAX_STEPS = 15;
11
- const SUB_AGENT_SYSTEM = `You are a focused sub-agent executing a specific task. Complete the task thoroughly and return a clear result.
15
+ function resolveSubAgentSteps(explicit) {
16
+ const configured = getEffectiveConfig().agent?.subAgentMaxSteps;
17
+ const value = explicit ?? configured ?? SUB_AGENT_MAX_STEPS;
18
+ return Number.isFinite(value) && value >= 1 ? Math.floor(value) : SUB_AGENT_MAX_STEPS;
19
+ }
20
+ function subAgentSystem() {
21
+ return `You are a focused sub-agent executing a specific task. Complete the task thoroughly and return a clear result.
12
22
 
13
23
  Rules:
14
24
  - Focus only on the assigned task
@@ -19,7 +29,16 @@ Rules:
19
29
  Working directory: ${process.cwd()}
20
30
  Platform: ${process.platform}
21
31
  Date: ${new Date().toDateString()}`;
22
- export function createTaskTool(modelId) {
32
+ }
33
+ let listener = null;
34
+ export function setSubAgentListener(fn) {
35
+ listener = fn;
36
+ }
37
+ function logSub(line) {
38
+ if (!listener)
39
+ console.error(line);
40
+ }
41
+ export function createTaskTool(modelId, abortSignal, onUsage, context = {}) {
23
42
  return tool({
24
43
  description: `Launch a sub-agent to execute a task independently. The sub-agent has its own context and tools. Use this for:
25
44
  - Parallel execution: call multiple tasks at once for independent work
@@ -36,44 +55,56 @@ Call multiple tasks in parallel when the work is independent.`,
36
55
  },
37
56
  required: ["description", "prompt"],
38
57
  }),
39
- execute: async ({ description, prompt }) => {
40
- console.log(`\x1b[90m ┌─ Sub-agent: ${description}\x1b[0m`);
58
+ execute: async ({ description, prompt }, options) => {
59
+ const parentToolCallId = options?.toolCallId;
60
+ logSub(`\x1b[90m ┌─ Sub-agent: ${description}\x1b[0m`);
41
61
  try {
42
- const result = await runSubAgent(prompt, modelId);
43
- console.log(`\x1b[90m └─ ✓ Done\x1b[0m`);
44
- return truncateToolOutput(result, { direction: "head" }).content;
62
+ const result = await runSubAgent(prompt, modelId, abortSignal, parentToolCallId, context);
63
+ if (result.usage)
64
+ onUsage?.(result.usage);
65
+ logSub(`\x1b[90m └─ ✓ Done\x1b[0m`);
66
+ return truncateToolOutput(result.text, { direction: "tail" }).content;
45
67
  }
46
68
  catch (err) {
47
- console.log(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
48
- return `Sub-agent error: ${err.message}`;
69
+ if (abortSignal?.aborted)
70
+ return "Sub-agent cancelled by user.";
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ log("error", `sub-agent failed: ${message}`);
73
+ logSub(`\x1b[90m └─ ✗ Failed: ${message}\x1b[0m`);
74
+ return `Sub-agent error: ${message}`;
49
75
  }
50
76
  },
51
77
  });
52
78
  }
53
- async function runSubAgent(prompt, modelId) {
79
+ async function runSubAgent(prompt, modelId, abortSignal, parentToolCallId, context = {}) {
54
80
  const model = resolveModel(modelId);
55
- // Build tools for sub-agent (no task tool to prevent recursion)
56
81
  const builtinTools = createTools();
82
+ builtinTools.todo = createTodoTool({ silent: true });
57
83
  const mcpTools = getMcpTools();
84
+ const catalogTools = getMcpCatalogTools();
58
85
  const pluginTools = await loadPluginTools();
59
- const skills = getSkills();
60
- const allTools = { ...builtinTools, ...pluginTools };
61
- for (const [id, t] of Object.entries(mcpTools))
62
- allTools[id] = t;
63
- if (skills.length > 0)
64
- allTools["skill"] = getSkillsTool();
65
- // Remove task tool from sub-agent to prevent infinite recursion
66
- delete allTools["task"];
86
+ const allTools = { ...builtinTools, ...pluginTools, ...mcpTools, ...catalogTools };
87
+ const skillsPrompt = attachSkills(allTools);
88
+ // Remove task/explore tools from sub-agent to prevent infinite recursion
89
+ delete allTools.task;
90
+ delete allTools.explore;
67
91
  const messages = [{ role: "user", content: prompt }];
68
- const doomLoop = new DoomLoopDetector();
92
+ // Sharing the parent guard means the child cannot restart the research budget.
93
+ const doomLoop = context.loopGuard ?? new DoomLoopDetector();
94
+ const maxSteps = resolveSubAgentSteps(context.maxSteps);
95
+ const controller = new AbortController();
96
+ const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
69
97
  const result = streamText({
70
98
  model,
71
- system: SUB_AGENT_SYSTEM,
99
+ system: skillsPrompt ? `${subAgentSystem()}\n\n${skillsPrompt}` : subAgentSystem(),
72
100
  messages,
73
101
  tools: allTools,
74
- stopWhen: stepCountIs(SUB_AGENT_MAX_STEPS),
102
+ stopWhen: stepCountIs(maxSteps),
75
103
  maxRetries: 2,
76
- onError() { },
104
+ abortSignal: signal,
105
+ onError({ error }) {
106
+ log("warn", `sub-agent step failed (will retry): ${String(error)}`);
107
+ },
77
108
  });
78
109
  let assistantText = "";
79
110
  for await (const event of result.fullStream) {
@@ -81,18 +112,54 @@ async function runSubAgent(prompt, modelId) {
81
112
  case "text-delta":
82
113
  assistantText += event.text;
83
114
  break;
84
- case "tool-call":
85
- if (doomLoop.record(event.toolName, event.input)) {
86
- return assistantText + "\n\n[Sub-agent stopped: doom loop detected]";
115
+ case "tool-call": {
116
+ const action = doomLoop.observe(event.toolName, event.input);
117
+ if (action === "halt" || action === "cap") {
118
+ controller.abort();
119
+ const note = action === "halt"
120
+ ? "[Sub-agent stopped: repeated the same action too many times]"
121
+ : "[Sub-agent stopped: too many web searches. Summarize what was found.]";
122
+ return { text: unwrapXmlSearchTags(assistantText) + "\n\n" + note };
87
123
  }
88
- console.log(`\x1b[90m │ ${event.toolName}\x1b[0m`);
124
+ listener?.({ parentToolCallId, name: event.toolName, input: event.input, phase: "call" });
125
+ logSub(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
89
126
  break;
90
- case "tool-result":
127
+ }
128
+ case "tool-result": {
129
+ listener?.({
130
+ parentToolCallId,
131
+ name: event.toolName,
132
+ output: event.output,
133
+ phase: "result",
134
+ });
135
+ const action = doomLoop.observeResult(event.toolName, event.output);
136
+ if (action === "cap") {
137
+ controller.abort();
138
+ return {
139
+ text: unwrapXmlSearchTags(assistantText) +
140
+ "\n\n[Sub-agent stopped: research budget spent. Summarize what was found.]",
141
+ };
142
+ }
143
+ if (context.shouldStop?.()) {
144
+ log("warn", "sub-agent stopped: parent budget reached");
145
+ controller.abort();
146
+ return {
147
+ text: unwrapXmlSearchTags(assistantText) + "\n\n[Sub-agent stopped: run budget reached]",
148
+ };
149
+ }
91
150
  break;
151
+ }
92
152
  case "error":
93
- return assistantText + `\n\n[Sub-agent error: ${event.error}]`;
153
+ log("error", `sub-agent stream error: ${String(event.error)}`);
154
+ controller.abort();
155
+ return { text: unwrapXmlSearchTags(assistantText) + `\n\n[Sub-agent error: ${event.error}]` };
94
156
  }
95
157
  }
96
- const cleaned = stripThinkingFromAssistantText(assistantText);
97
- return cleaned || "(sub-agent produced no output)";
158
+ const cleaned = unwrapXmlSearchTags(stripThinkingFromAssistantText(assistantText));
159
+ let usage;
160
+ try {
161
+ usage = await result.usage;
162
+ }
163
+ catch { }
164
+ return { text: cleaned || "(sub-agent produced no output)", usage };
98
165
  }
@@ -1,44 +1,142 @@
1
1
  import { tool, jsonSchema } from "ai";
2
- let todos = [];
3
- let nextId = 1;
4
- export function getTodos() {
5
- return todos;
2
+ import { isSyntheticMessage } from "../synthetic.js";
3
+ export const TODO_STATUSES = ["pending", "in_progress", "done", "cancelled"];
4
+ const GOAL_MAX_CHARS = 4000;
5
+ export function emptyTaskState() {
6
+ return { goal: "", todos: [], nextId: 1 };
6
7
  }
7
- export function resetTodos() {
8
- todos = [];
9
- nextId = 1;
8
+ export function resetTaskState(state) {
9
+ state.goal = "";
10
+ state.todos.length = 0;
11
+ state.nextId = 1;
10
12
  }
11
- function formatTodos() {
12
- if (todos.length === 0)
13
- return "No tasks.";
14
- const icons = {
15
- pending: "○",
16
- in_progress: "◐",
17
- done: "●",
18
- cancelled: "✕",
13
+ export function copyTaskState(from, into) {
14
+ if (!from) {
15
+ resetTaskState(into);
16
+ return;
17
+ }
18
+ into.goal = from.goal;
19
+ into.todos = from.todos.map((t) => ({ ...t }));
20
+ into.nextId = from.nextId;
21
+ }
22
+ function isTodoStatus(value) {
23
+ return typeof value === "string" && TODO_STATUSES.includes(value);
24
+ }
25
+ export function parseTaskState(raw) {
26
+ if (!raw || typeof raw !== "object")
27
+ return undefined;
28
+ const rec = raw;
29
+ const todos = [];
30
+ if (Array.isArray(rec.todos)) {
31
+ for (const item of rec.todos) {
32
+ if (!item || typeof item !== "object")
33
+ continue;
34
+ const t = item;
35
+ if (typeof t.id !== "number" || typeof t.text !== "string")
36
+ continue;
37
+ todos.push({
38
+ id: t.id,
39
+ text: t.text,
40
+ status: isTodoStatus(t.status) ? t.status : "pending",
41
+ });
42
+ }
43
+ }
44
+ const maxId = todos.reduce((m, t) => Math.max(m, t.id), 0);
45
+ const nextId = typeof rec.nextId === "number" && Number.isFinite(rec.nextId) && rec.nextId > maxId
46
+ ? Math.floor(rec.nextId)
47
+ : maxId + 1;
48
+ return {
49
+ goal: typeof rec.goal === "string" ? rec.goal : "",
50
+ todos,
51
+ nextId,
19
52
  };
20
- return todos
21
- .map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`)
53
+ }
54
+ function messageText(msg) {
55
+ if (typeof msg.content === "string")
56
+ return msg.content;
57
+ if (!Array.isArray(msg.content))
58
+ return "";
59
+ return msg.content
60
+ .filter((p) => p.type === "text" && typeof p.text === "string")
61
+ .map((p) => p.text)
22
62
  .join("\n");
23
63
  }
24
- function printTodos() {
25
- if (todos.length === 0)
64
+ /** Keep the first real user request so compaction cannot drop the original goal. */
65
+ export function captureGoal(state, messages) {
66
+ if (state.goal.trim())
67
+ return;
68
+ for (const msg of messages) {
69
+ if (msg.role !== "user")
70
+ continue;
71
+ if (isSyntheticMessage(msg))
72
+ continue;
73
+ const text = messageText(msg).trim();
74
+ if (!text)
75
+ continue;
76
+ state.goal = text.slice(0, GOAL_MAX_CHARS);
26
77
  return;
27
- console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
28
- for (const t of todos) {
29
- const icon = t.status === "done" ? "\x1b[32m●\x1b[0m"
30
- : t.status === "in_progress" ? "\x1b[33m◐\x1b[0m"
31
- : t.status === "cancelled" ? "\x1b[90m✕\x1b[0m"
32
- : "\x1b[90m○\x1b[0m";
33
- const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
34
- const reset = dim ? "\x1b[0m" : "";
35
- console.log(`\x1b[90m │\x1b[0m ${icon} ${dim}#${t.id} ${t.text}${reset}`);
36
78
  }
37
- console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
38
79
  }
39
- export const todoTool = tool({
40
- description: `Create or update tasks to track progress. Use this FREQUENTLY to:
41
- - Plan multi-step work by creating tasks upfront
80
+ export function formatTaskStatePrompt(state) {
81
+ if (!state.goal && state.todos.length === 0)
82
+ return "";
83
+ const icons = {
84
+ pending: "pending",
85
+ in_progress: "in_progress",
86
+ done: "done",
87
+ cancelled: "cancelled",
88
+ };
89
+ const lines = [
90
+ "## Current Task",
91
+ "This block is the durable task state. It survives context compaction. Keep it accurate with the todo tool.",
92
+ ];
93
+ if (state.goal) {
94
+ lines.push("", "### Original goal", state.goal);
95
+ }
96
+ if (state.todos.length > 0) {
97
+ lines.push("", "### Task list");
98
+ for (const t of state.todos) {
99
+ lines.push(`- [${icons[t.status]}] #${t.id} ${t.text}`);
100
+ }
101
+ }
102
+ return lines.join("\n");
103
+ }
104
+ export function createTodoTool(opts) {
105
+ const silent = opts?.silent ?? false;
106
+ const store = opts?.store ?? emptyTaskState();
107
+ const formatTodos = () => {
108
+ if (store.todos.length === 0)
109
+ return "No tasks.";
110
+ const icons = {
111
+ pending: "○",
112
+ in_progress: "◐",
113
+ done: "●",
114
+ cancelled: "✕",
115
+ };
116
+ return store.todos.map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`).join("\n");
117
+ };
118
+ const printTodos = () => {
119
+ if (silent || store.todos.length === 0)
120
+ return;
121
+ console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
122
+ for (const t of store.todos) {
123
+ const icon = t.status === "done"
124
+ ? "\x1b[32m●\x1b[0m"
125
+ : t.status === "in_progress"
126
+ ? "\x1b[33m◐\x1b[0m"
127
+ : t.status === "cancelled"
128
+ ? "\x1b[90m✕\x1b[0m"
129
+ : "\x1b[90m○\x1b[0m";
130
+ const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
131
+ const reset = dim ? "\x1b[0m" : "";
132
+ console.log(`\x1b[90m │\x1b[0m ${icon} ${dim}#${t.id} ${t.text}${reset}`);
133
+ }
134
+ console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
135
+ };
136
+ return tool({
137
+ description: `Create or update tasks to track progress. The list persists across context compaction and auto-continue. Use this FREQUENTLY to:
138
+ - Plan multi-step work by creating tasks upfront (research, then the deliverable)
139
+ - Mark research tasks done before more searching — then write the output
42
140
  - Mark tasks as in_progress when starting them
43
141
  - Mark tasks as done when completed
44
142
  - Give the user visibility into your progress
@@ -46,43 +144,56 @@ export const todoTool = tool({
46
144
  To create new tasks: provide items with "text" and optionally "status" (defaults to "pending").
47
145
  To update existing tasks: provide items with "id" and "status".
48
146
  You can mix creates and updates in one call.`,
49
- inputSchema: jsonSchema({
50
- type: "object",
51
- properties: {
52
- todos: {
53
- type: "array",
54
- description: "List of tasks to create or update",
55
- items: {
56
- type: "object",
57
- properties: {
58
- text: { type: "string", description: "Task description (for new tasks)" },
59
- status: { type: "string", description: "Status: pending, in_progress, done, cancelled" },
60
- id: { type: "number", description: "Task ID (for updating existing tasks)" },
147
+ inputSchema: jsonSchema({
148
+ type: "object",
149
+ properties: {
150
+ todos: {
151
+ type: "array",
152
+ description: "List of tasks to create or update",
153
+ items: {
154
+ type: "object",
155
+ properties: {
156
+ text: { type: "string", description: "Task description (for new tasks)" },
157
+ status: { type: "string", description: "Status: pending, in_progress, done, cancelled" },
158
+ id: { type: "number", description: "Task ID (for updating existing tasks)" },
159
+ },
61
160
  },
62
161
  },
63
162
  },
64
- },
65
- required: ["todos"],
66
- }),
67
- execute: async ({ todos: items }) => {
68
- for (const item of items) {
69
- if (item.id) {
70
- // Update existing
71
- const existing = todos.find((t) => t.id === item.id);
72
- if (existing && item.status) {
73
- existing.status = item.status;
163
+ required: ["todos"],
164
+ }),
165
+ execute: async ({ todos: items }) => {
166
+ const errors = [];
167
+ for (const item of items) {
168
+ if (item.id != null) {
169
+ const existing = store.todos.find((t) => t.id === item.id);
170
+ if (!existing) {
171
+ errors.push(`task #${item.id} not found`);
172
+ continue;
173
+ }
174
+ if (item.status) {
175
+ if (!TODO_STATUSES.includes(item.status)) {
176
+ errors.push(`invalid status "${item.status}" for task #${item.id}`);
177
+ continue;
178
+ }
179
+ existing.status = item.status;
180
+ }
181
+ if (item.text)
182
+ existing.text = item.text;
183
+ }
184
+ else if (item.text) {
185
+ store.todos.push({
186
+ id: store.nextId++,
187
+ text: item.text,
188
+ status: TODO_STATUSES.includes(item.status) ? item.status : "pending",
189
+ });
190
+ }
191
+ else {
192
+ errors.push("item needs either an id (update) or text (create)");
74
193
  }
75
194
  }
76
- else if (item.text) {
77
- // Create new
78
- todos.push({
79
- id: nextId++,
80
- text: item.text,
81
- status: item.status ?? "pending",
82
- });
83
- }
84
- }
85
- printTodos();
86
- return formatTodos();
87
- },
88
- });
195
+ printTodos();
196
+ return errors.length > 0 ? `${formatTodos()}\n\nWarnings: ${errors.join("; ")}` : formatTodos();
197
+ },
198
+ });
199
+ }