min-agent 0.3.0 → 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 (120) hide show
  1. package/README.md +111 -28
  2. package/dist/agent.js +1119 -256
  3. package/dist/cli/commands/chat.js +10 -0
  4. package/dist/cli/commands/exec.js +32 -0
  5. package/dist/cli/commands/history.js +58 -0
  6. package/dist/cli/commands/index.js +224 -0
  7. package/dist/cli/commands/init.js +18 -0
  8. package/dist/cli/commands/mcp.js +173 -0
  9. package/dist/cli/commands/memory.js +69 -0
  10. package/dist/cli/commands/models.js +21 -0
  11. package/dist/cli/commands/permission.js +12 -0
  12. package/dist/cli/commands/rules.js +33 -0
  13. package/dist/cli/commands/sandbox.js +13 -0
  14. package/dist/cli/commands/serve.js +9 -0
  15. package/dist/cli/commands/setup.js +4 -0
  16. package/dist/cli/commands/shared.js +16 -0
  17. package/dist/cli/commands/skills.js +119 -0
  18. package/dist/cli/commands/update.js +7 -0
  19. package/dist/cli/commands/write-config.js +30 -0
  20. package/dist/cli/errors.js +36 -0
  21. package/dist/cli/exec-prompt.js +26 -0
  22. package/dist/cli/option-helpers.js +53 -0
  23. package/dist/cli/program.js +180 -0
  24. package/dist/cli.js +5 -888
  25. package/dist/code-mode.js +32 -14
  26. package/dist/compaction.js +347 -160
  27. package/dist/config.js +119 -10
  28. package/dist/confirm.js +56 -9
  29. package/dist/context-window.js +107 -39
  30. package/dist/doom-loop.js +264 -29
  31. package/dist/fetch-timeout.js +152 -0
  32. package/dist/http-approvals.js +60 -0
  33. package/dist/instructions.js +21 -0
  34. package/dist/logger.js +33 -4
  35. package/dist/markdown.js +37 -11
  36. package/dist/mcp.js +328 -30
  37. package/dist/memory.js +97 -56
  38. package/dist/output.js +7 -5
  39. package/dist/permission-cli.js +43 -0
  40. package/dist/plugins.js +46 -8
  41. package/dist/pricing.js +4 -4
  42. package/dist/provider.js +23 -6
  43. package/dist/question-format.js +60 -0
  44. package/dist/sandbox-cli.js +82 -0
  45. package/dist/sandbox.js +403 -0
  46. package/dist/save-throttle.js +45 -0
  47. package/dist/serve/common.js +404 -0
  48. package/dist/serve/routes-chat.js +347 -0
  49. package/dist/serve/routes-mcp.js +212 -0
  50. package/dist/serve/routes-memory.js +66 -0
  51. package/dist/serve/routes-meta.js +205 -0
  52. package/dist/serve/routes-sessions.js +61 -0
  53. package/dist/serve/routes-skills.js +70 -0
  54. package/dist/serve.js +33 -883
  55. package/dist/sessions.js +53 -9
  56. package/dist/skills.js +82 -18
  57. package/dist/title-gen.js +8 -2
  58. package/dist/token-display.js +36 -0
  59. package/dist/tool-display.js +5 -0
  60. package/dist/tool-output.js +1 -3
  61. package/dist/tools/apply_patch.js +85 -11
  62. package/dist/tools/atomic-file.js +35 -0
  63. package/dist/tools/backend.js +2 -2
  64. package/dist/tools/bash.js +57 -19
  65. package/dist/tools/code_search.js +7 -1
  66. package/dist/tools/edit.js +11 -10
  67. package/dist/tools/explore.js +74 -14
  68. package/dist/tools/glob.js +4 -0
  69. package/dist/tools/grep.js +17 -10
  70. package/dist/tools/index.js +6 -21
  71. package/dist/tools/question.js +28 -9
  72. package/dist/tools/read.js +6 -4
  73. package/dist/tools/search-searxng.js +223 -0
  74. package/dist/tools/search-serper.js +189 -0
  75. package/dist/tools/task.js +84 -30
  76. package/dist/tools/todo.js +120 -19
  77. package/dist/tools/web_fetch.js +11 -3
  78. package/dist/tools/web_search.js +66 -556
  79. package/dist/tools/write.js +23 -6
  80. package/dist/tui/App.js +63 -14
  81. package/dist/tui/ConfirmBar.js +45 -13
  82. package/dist/tui/InputBar.js +150 -35
  83. package/dist/tui/MessageList.js +266 -125
  84. package/dist/tui/ModelPicker.js +8 -3
  85. package/dist/tui/QuestionBar.js +51 -19
  86. package/dist/tui/SessionPicker.js +79 -0
  87. package/dist/tui/StatusBar.js +8 -14
  88. package/dist/tui/agent-runner.js +142 -22
  89. package/dist/tui/caret-pos.js +48 -5
  90. package/dist/tui/caret.js +1 -1
  91. package/dist/tui/click-count.js +13 -0
  92. package/dist/tui/drag-state.js +8 -3
  93. package/dist/tui/hydrate.js +129 -0
  94. package/dist/tui/index.js +42 -13
  95. package/dist/tui/input-history.js +92 -11
  96. package/dist/tui/layout.js +75 -4
  97. package/dist/tui/prompt-queue.js +24 -0
  98. package/dist/tui/selection.js +113 -21
  99. package/dist/tui/session-switch.js +28 -0
  100. package/dist/tui/slash-commands.js +22 -6
  101. package/dist/tui/slash-handler.js +233 -58
  102. package/dist/tui/text-width.js +38 -16
  103. package/dist/tui/token-info.js +7 -0
  104. package/dist/tui/tool-children.js +19 -0
  105. package/dist/tui/undo-stack.js +1 -1
  106. package/dist/tui/use-sgr-mouse.js +3 -1
  107. package/dist/tui-chat.js +276 -40
  108. package/dist/updater.js +88 -29
  109. package/dist/xml-search.js +194 -0
  110. package/docs/API.md +257 -25
  111. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  112. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  113. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  114. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  115. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  116. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  117. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  118. package/package.json +6 -1
  119. package/skills/self-config/SKILL.md +90 -0
  120. package/skills/self-config/reference.md +149 -0
@@ -1,12 +1,111 @@
1
1
  import { tool, jsonSchema } from "ai";
2
+ import { isSyntheticMessage } from "../synthetic.js";
2
3
  export const TODO_STATUSES = ["pending", "in_progress", "done", "cancelled"];
3
- /** Create an isolated todo tool for one agent run. */
4
+ const GOAL_MAX_CHARS = 4000;
5
+ export function emptyTaskState() {
6
+ return { goal: "", todos: [], nextId: 1 };
7
+ }
8
+ export function resetTaskState(state) {
9
+ state.goal = "";
10
+ state.todos.length = 0;
11
+ state.nextId = 1;
12
+ }
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,
52
+ };
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)
62
+ .join("\n");
63
+ }
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);
77
+ return;
78
+ }
79
+ }
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
+ }
4
104
  export function createTodoTool(opts) {
5
105
  const silent = opts?.silent ?? false;
6
- let todos = [];
7
- let nextId = 1;
106
+ const store = opts?.store ?? emptyTaskState();
8
107
  const formatTodos = () => {
9
- if (todos.length === 0)
108
+ if (store.todos.length === 0)
10
109
  return "No tasks.";
11
110
  const icons = {
12
111
  pending: "○",
@@ -14,18 +113,19 @@ export function createTodoTool(opts) {
14
113
  done: "●",
15
114
  cancelled: "✕",
16
115
  };
17
- return todos
18
- .map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`)
19
- .join("\n");
116
+ return store.todos.map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`).join("\n");
20
117
  };
21
118
  const printTodos = () => {
22
- if (silent || todos.length === 0)
119
+ if (silent || store.todos.length === 0)
23
120
  return;
24
121
  console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
25
- for (const t of todos) {
26
- const icon = t.status === "done" ? "\x1b[32m●\x1b[0m"
27
- : t.status === "in_progress" ? "\x1b[33m◐\x1b[0m"
28
- : t.status === "cancelled" ? "\x1b[90m✕\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"
29
129
  : "\x1b[90m○\x1b[0m";
30
130
  const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
31
131
  const reset = dim ? "\x1b[0m" : "";
@@ -34,8 +134,9 @@ export function createTodoTool(opts) {
34
134
  console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
35
135
  };
36
136
  return tool({
37
- description: `Create or update tasks to track progress. Use this FREQUENTLY to:
38
- - Plan multi-step work by creating tasks upfront
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
39
140
  - Mark tasks as in_progress when starting them
40
141
  - Mark tasks as done when completed
41
142
  - Give the user visibility into your progress
@@ -65,7 +166,7 @@ You can mix creates and updates in one call.`,
65
166
  const errors = [];
66
167
  for (const item of items) {
67
168
  if (item.id != null) {
68
- const existing = todos.find((t) => t.id === item.id);
169
+ const existing = store.todos.find((t) => t.id === item.id);
69
170
  if (!existing) {
70
171
  errors.push(`task #${item.id} not found`);
71
172
  continue;
@@ -77,10 +178,12 @@ You can mix creates and updates in one call.`,
77
178
  }
78
179
  existing.status = item.status;
79
180
  }
181
+ if (item.text)
182
+ existing.text = item.text;
80
183
  }
81
184
  else if (item.text) {
82
- todos.push({
83
- id: nextId++,
185
+ store.todos.push({
186
+ id: store.nextId++,
84
187
  text: item.text,
85
188
  status: TODO_STATUSES.includes(item.status) ? item.status : "pending",
86
189
  });
@@ -94,5 +197,3 @@ You can mix creates and updates in one call.`,
94
197
  },
95
198
  });
96
199
  }
97
- /** Shared placeholder — callers in code mode replace it with a per-run instance. */
98
- export const todoTool = createTodoTool();
@@ -3,6 +3,7 @@ import { truncateToolOutput } from "../tool-output.js";
3
3
  import { assertSafeUrl, readBodyLimited } from "../http.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { configHint, describeFetchError, resolveBackendBase } from "./backend.js";
6
+ import { getEffectiveSandboxPolicy, networkDeniedMessage } from "../sandbox.js";
6
7
  export const FIRECRAWL_BACKEND = {
7
8
  envName: "MIN_AGENT_FIRECRAWL_URL",
8
9
  configKey: "webFetchURL",
@@ -13,7 +14,7 @@ const DIRECT_TIMEOUT_MS = 8000;
13
14
  const SCRAPE_TIMEOUT_MS = 35000;
14
15
  const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
15
16
  export const webFetchTool = tool({
16
- description: "Fetch content from a URL. Use this to access web pages, APIs, or any HTTP resource. Returns the response body as text, or markdown for HTML pages (JavaScript-rendered pages go through a scrape backend).",
17
+ description: "Fetch content from a URL. Use this to access web pages, APIs, or any HTTP resource. Returns the response body as text, or markdown for HTML pages (JavaScript-rendered pages go through a scrape backend). Do not re-fetch URLs you already have; extract what you need and produce the user's deliverable.",
17
18
  inputSchema: jsonSchema({
18
19
  type: "object",
19
20
  properties: {
@@ -23,6 +24,8 @@ export const webFetchTool = tool({
23
24
  required: ["url"],
24
25
  }),
25
26
  execute: async ({ url, method }) => {
27
+ if (getEffectiveSandboxPolicy().network === "deny")
28
+ return `Error: ${networkDeniedMessage()}`;
26
29
  const target = (url ?? "").trim();
27
30
  if (!target)
28
31
  return 'Error: "url" must be a non-empty string.';
@@ -160,13 +163,18 @@ async function firecrawlFetch(url) {
160
163
  catch {
161
164
  return {
162
165
  kind: "unavailable",
163
- reason: response.ok ? "the scrape backend returned a non-JSON response" : `HTTP ${response.status} from the scrape backend`,
166
+ reason: response.ok
167
+ ? "the scrape backend returned a non-JSON response"
168
+ : `HTTP ${response.status} from the scrape backend`,
164
169
  };
165
170
  }
166
171
  const record = typeof data === "object" && data !== null ? data : {};
167
172
  if (!response.ok) {
168
173
  const message = typeof record.error === "string" ? record.error : response.statusText;
169
- return { kind: "unavailable", reason: `HTTP ${response.status} from the scrape backend${message ? ` (${message})` : ""}` };
174
+ return {
175
+ kind: "unavailable",
176
+ reason: `HTTP ${response.status} from the scrape backend${message ? ` (${message})` : ""}`,
177
+ };
170
178
  }
171
179
  const payload = typeof record.data === "object" && record.data !== null ? record.data : {};
172
180
  const markdown = typeof payload.markdown === "string" ? payload.markdown : typeof payload.content === "string" ? payload.content : "";