min-agent 0.5.1 → 0.6.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 (169) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +6 -6
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +4 -2
  6. package/skills/self-config/reference.md +4 -1
  7. package/dist/agent.js +0 -1388
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/ctx.js +0 -7
  11. package/dist/cli/commands/exec.js +0 -35
  12. package/dist/cli/commands/history.js +0 -58
  13. package/dist/cli/commands/index.js +0 -249
  14. package/dist/cli/commands/init.js +0 -18
  15. package/dist/cli/commands/mcp.js +0 -173
  16. package/dist/cli/commands/memory.js +0 -87
  17. package/dist/cli/commands/models.js +0 -21
  18. package/dist/cli/commands/permission.js +0 -12
  19. package/dist/cli/commands/rules.js +0 -33
  20. package/dist/cli/commands/sandbox.js +0 -13
  21. package/dist/cli/commands/serve.js +0 -9
  22. package/dist/cli/commands/setup.js +0 -56
  23. package/dist/cli/commands/shared.js +0 -25
  24. package/dist/cli/commands/skills.js +0 -119
  25. package/dist/cli/commands/think.js +0 -12
  26. package/dist/cli/commands/update.js +0 -7
  27. package/dist/cli/commands/write-config.js +0 -52
  28. package/dist/cli/errors.js +0 -36
  29. package/dist/cli/exec-prompt.js +0 -26
  30. package/dist/cli/option-helpers.js +0 -65
  31. package/dist/cli/program.js +0 -223
  32. package/dist/cli/setup/detect.js +0 -17
  33. package/dist/cli/setup/flags.js +0 -12
  34. package/dist/cli/setup/flow.js +0 -108
  35. package/dist/cli/setup/provider-form.js +0 -102
  36. package/dist/cli/setup/ui.js +0 -534
  37. package/dist/clipboard.js +0 -142
  38. package/dist/code-mode.js +0 -184
  39. package/dist/compaction.js +0 -644
  40. package/dist/config.js +0 -363
  41. package/dist/confirm.js +0 -172
  42. package/dist/context-window.js +0 -256
  43. package/dist/ctx-cli.js +0 -30
  44. package/dist/ctx.js +0 -80
  45. package/dist/doom-loop.js +0 -278
  46. package/dist/fetch-timeout.js +0 -152
  47. package/dist/http-approvals.js +0 -60
  48. package/dist/http.js +0 -119
  49. package/dist/instructions.js +0 -196
  50. package/dist/logger.js +0 -95
  51. package/dist/markdown.js +0 -259
  52. package/dist/mcp.js +0 -1036
  53. package/dist/memory-cli.js +0 -33
  54. package/dist/memory.js +0 -295
  55. package/dist/model-catalog.js +0 -285
  56. package/dist/ollama-model.js +0 -234
  57. package/dist/ollama-openai-bridge.js +0 -383
  58. package/dist/output.js +0 -76
  59. package/dist/paste-handler.js +0 -41
  60. package/dist/permission-cli.js +0 -40
  61. package/dist/plugins.js +0 -131
  62. package/dist/pricing.js +0 -119
  63. package/dist/provider.js +0 -63
  64. package/dist/question-format.js +0 -60
  65. package/dist/reasoning-stream.js +0 -158
  66. package/dist/sandbox-cli.js +0 -79
  67. package/dist/sandbox.js +0 -403
  68. package/dist/save-throttle.js +0 -45
  69. package/dist/scope.js +0 -23
  70. package/dist/serve/common.js +0 -425
  71. package/dist/serve/routes-chat.js +0 -367
  72. package/dist/serve/routes-mcp.js +0 -212
  73. package/dist/serve/routes-memory.js +0 -95
  74. package/dist/serve/routes-meta.js +0 -268
  75. package/dist/serve/routes-sessions.js +0 -61
  76. package/dist/serve/routes-skills.js +0 -70
  77. package/dist/serve.js +0 -138
  78. package/dist/sessions.js +0 -265
  79. package/dist/skills.js +0 -599
  80. package/dist/structured-output.js +0 -29
  81. package/dist/synthetic.js +0 -7
  82. package/dist/think-cli.js +0 -36
  83. package/dist/thinking-wire.js +0 -239
  84. package/dist/thinking.js +0 -166
  85. package/dist/title-gen.js +0 -55
  86. package/dist/token-display.js +0 -39
  87. package/dist/tool-display.js +0 -178
  88. package/dist/tool-output.js +0 -126
  89. package/dist/tools/apply_patch.js +0 -265
  90. package/dist/tools/atomic-file.js +0 -35
  91. package/dist/tools/backend.js +0 -61
  92. package/dist/tools/bash.js +0 -231
  93. package/dist/tools/code_search.js +0 -98
  94. package/dist/tools/edit.js +0 -68
  95. package/dist/tools/explore.js +0 -232
  96. package/dist/tools/glob.js +0 -40
  97. package/dist/tools/grep.js +0 -174
  98. package/dist/tools/index.js +0 -34
  99. package/dist/tools/question.js +0 -54
  100. package/dist/tools/read.js +0 -109
  101. package/dist/tools/search-searxng.js +0 -223
  102. package/dist/tools/search-serper.js +0 -189
  103. package/dist/tools/task.js +0 -165
  104. package/dist/tools/todo.js +0 -213
  105. package/dist/tools/web_fetch.js +0 -195
  106. package/dist/tools/web_search.js +0 -228
  107. package/dist/tools/write.js +0 -55
  108. package/dist/tui/App.js +0 -138
  109. package/dist/tui/ConfirmBar.js +0 -66
  110. package/dist/tui/CtxPicker.js +0 -68
  111. package/dist/tui/InputBar.js +0 -618
  112. package/dist/tui/MessageList.js +0 -718
  113. package/dist/tui/ModelPicker.js +0 -113
  114. package/dist/tui/QuestionBar.js +0 -136
  115. package/dist/tui/SessionPicker.js +0 -79
  116. package/dist/tui/Spinner.js +0 -6
  117. package/dist/tui/StatusBar.js +0 -22
  118. package/dist/tui/ThinkPicker.js +0 -75
  119. package/dist/tui/agent-runner.js +0 -223
  120. package/dist/tui/bracketed-paste.js +0 -37
  121. package/dist/tui/caret-pos.js +0 -179
  122. package/dist/tui/caret.js +0 -69
  123. package/dist/tui/click-count.js +0 -13
  124. package/dist/tui/diff-view.js +0 -61
  125. package/dist/tui/drag-state.js +0 -49
  126. package/dist/tui/hydrate.js +0 -129
  127. package/dist/tui/index.js +0 -264
  128. package/dist/tui/input-history.js +0 -125
  129. package/dist/tui/layout.js +0 -105
  130. package/dist/tui/mouse.js +0 -46
  131. package/dist/tui/overlay-input.js +0 -12
  132. package/dist/tui/paste-draft.js +0 -173
  133. package/dist/tui/prompt-queue.js +0 -24
  134. package/dist/tui/selection.js +0 -232
  135. package/dist/tui/session-switch.js +0 -28
  136. package/dist/tui/slash-commands.js +0 -129
  137. package/dist/tui/slash-handler.js +0 -615
  138. package/dist/tui/text-width.js +0 -113
  139. package/dist/tui/theme.js +0 -12
  140. package/dist/tui/token-info.js +0 -7
  141. package/dist/tui/tool-children.js +0 -19
  142. package/dist/tui/types.js +0 -1
  143. package/dist/tui/undo-stack.js +0 -14
  144. package/dist/tui/use-sgr-mouse.js +0 -29
  145. package/dist/tui-chat.js +0 -478
  146. package/dist/updater.js +0 -116
  147. package/dist/xml-search.js +0 -194
  148. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  149. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  150. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  151. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  152. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  153. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  154. package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
  155. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  156. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  157. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  158. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  159. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  160. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  161. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  162. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  163. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  164. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  165. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  166. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  167. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
  168. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  169. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -1,195 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { truncateToolOutput } from "../tool-output.js";
3
- import { assertSafeUrl, readBodyLimited } from "../http.js";
4
- import { loadConfig } from "../config.js";
5
- import { configHint, describeFetchError, resolveBackendBase } from "./backend.js";
6
- import { getEffectiveSandboxPolicy, networkDeniedMessage } from "../sandbox.js";
7
- export const FIRECRAWL_BACKEND = {
8
- envName: "MIN_AGENT_FIRECRAWL_URL",
9
- configKey: "webFetchURL",
10
- fallback: "https://fireclawl.xc.lonae.com",
11
- };
12
- const MAX_BODY_BYTES = 2 * 1024 * 1024;
13
- const DIRECT_TIMEOUT_MS = 8000;
14
- const SCRAPE_TIMEOUT_MS = 35000;
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";
16
- export const webFetchTool = tool({
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.",
18
- inputSchema: jsonSchema({
19
- type: "object",
20
- properties: {
21
- url: { type: "string", description: "The URL to fetch" },
22
- method: { type: "string", description: "HTTP method (default: GET)" },
23
- },
24
- required: ["url"],
25
- }),
26
- execute: async ({ url, method }) => {
27
- if (getEffectiveSandboxPolicy().network === "deny")
28
- return `Error: ${networkDeniedMessage()}`;
29
- const target = (url ?? "").trim();
30
- if (!target)
31
- return 'Error: "url" must be a non-empty string.';
32
- const blocked = await assertSafeUrl(target);
33
- if (blocked)
34
- return `Error: ${blocked}`;
35
- const verb = (method ?? "GET").trim().toUpperCase() || "GET";
36
- const direct = await directFetch(target, verb);
37
- // Plain text / JSON responses are what the caller asked for; return them
38
- // without paying for a scrape round trip.
39
- if (direct.kind === "ok" && !direct.html && hasContent(direct.text)) {
40
- return renderBody(direct.text, direct.bodyTruncated);
41
- }
42
- if (direct.kind === "binary") {
43
- return `Error: the response is not text (Content-Type: ${direct.contentType}). Use a different tool to download binary content.`;
44
- }
45
- // A non-GET request cannot be replayed by the scrape backend (it always
46
- // performs a GET), so report the direct outcome as-is.
47
- if (verb !== "GET") {
48
- if (direct.kind === "ok")
49
- return renderBody(direct.text, direct.bodyTruncated);
50
- return `Error: ${describeDirect(direct, target)}`;
51
- }
52
- // HTML (or an empty/JS-rendered shell): the scrape backend turns it into
53
- // markdown, which is both cheaper and more readable than raw markup.
54
- const scraped = await firecrawlFetch(target);
55
- if (scraped.kind === "ok")
56
- return renderBody(scraped.markdown, scraped.bodyTruncated);
57
- if (direct.kind === "ok" && hasContent(direct.text)) {
58
- return [
59
- `Note: returning raw HTML — the scrape backend could not convert this page (${scraped.reason}).`,
60
- renderBody(direct.text, direct.bodyTruncated),
61
- ].join("\n\n");
62
- }
63
- return [
64
- `Failed to fetch content from ${target}.`,
65
- `Direct request: ${describeDirect(direct, target)}`,
66
- `Scrape backend: ${scraped.reason}`,
67
- configHint(FIRECRAWL_BACKEND),
68
- ].join("\n");
69
- },
70
- });
71
- function renderBody(text, bodyTruncated) {
72
- const { content } = truncateToolOutput(text, { direction: "head" });
73
- // readBodyLimited cutting at MAX_BODY_BYTES is a different truncation from
74
- // truncateToolOutput's (which persists the full text), so disclose it.
75
- if (!bodyTruncated)
76
- return content;
77
- return `${content}\n\n...(response body was cut off at ${MAX_BODY_BYTES / 1024 / 1024} MB)`;
78
- }
79
- function describeDirect(outcome, url) {
80
- switch (outcome.kind) {
81
- case "ok":
82
- return `HTTP 200 but the body had almost no readable content (${url} may require JavaScript)`;
83
- case "binary":
84
- return `non-text response (${outcome.contentType})`;
85
- case "http":
86
- return `HTTP ${outcome.status}${outcome.statusText ? ` ${outcome.statusText}` : ""}`;
87
- case "network":
88
- return outcome.message;
89
- }
90
- }
91
- function isHtml(contentType) {
92
- return /html|xhtml/i.test(contentType);
93
- }
94
- /** Content types worth handing to the model as text. */
95
- function isTextual(contentType) {
96
- if (!contentType)
97
- return true; // Many APIs omit it; the NUL sniff below catches binaries.
98
- const ct = contentType.toLowerCase();
99
- if (ct.startsWith("text/"))
100
- return true;
101
- return /(json|xml|javascript|ecmascript|yaml|csv|graphql|x-www-form-urlencoded|\+json)/.test(ct);
102
- }
103
- async function directFetch(url, method) {
104
- let response;
105
- try {
106
- response = await fetch(url, {
107
- method,
108
- headers: {
109
- "User-Agent": BROWSER_UA,
110
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.9,*/*;q=0.8",
111
- },
112
- signal: AbortSignal.timeout(DIRECT_TIMEOUT_MS),
113
- });
114
- }
115
- catch (err) {
116
- return { kind: "network", message: describeFetchError(err) };
117
- }
118
- const contentType = response.headers.get("content-type") ?? "";
119
- if (!response.ok)
120
- return { kind: "http", status: response.status, statusText: response.statusText };
121
- if (!isTextual(contentType))
122
- return { kind: "binary", contentType };
123
- try {
124
- const { text, truncated } = await readBodyLimited(response, MAX_BODY_BYTES);
125
- if (text.includes("\u0000"))
126
- return { kind: "binary", contentType: contentType || "unknown (binary)" };
127
- return { kind: "ok", text, bodyTruncated: truncated, html: isHtml(contentType) || looksLikeHtml(text) };
128
- }
129
- catch (err) {
130
- return { kind: "network", message: describeFetchError(err) };
131
- }
132
- }
133
- async function firecrawlFetch(url) {
134
- const resolved = resolveBackendBase(FIRECRAWL_BACKEND, loadConfig().webFetchURL);
135
- if (!resolved.ok)
136
- return { kind: "unavailable", reason: resolved.error };
137
- let response;
138
- try {
139
- response = await fetch(`${resolved.base}/v1/scrape`, {
140
- method: "POST",
141
- headers: { "Content-Type": "application/json" },
142
- body: JSON.stringify({ url, formats: ["markdown"], onlyMainContent: true, waitFor: 3000, timeout: 30000 }),
143
- signal: AbortSignal.timeout(SCRAPE_TIMEOUT_MS),
144
- });
145
- }
146
- catch (err) {
147
- return { kind: "unavailable", reason: `could not reach the scrape backend (${describeFetchError(err)})` };
148
- }
149
- let text;
150
- let truncated;
151
- try {
152
- const body = await readBodyLimited(response, MAX_BODY_BYTES);
153
- text = body.text;
154
- truncated = body.truncated;
155
- }
156
- catch (err) {
157
- return { kind: "unavailable", reason: `could not read the scrape response (${describeFetchError(err)})` };
158
- }
159
- let data;
160
- try {
161
- data = JSON.parse(text);
162
- }
163
- catch {
164
- return {
165
- kind: "unavailable",
166
- reason: response.ok
167
- ? "the scrape backend returned a non-JSON response"
168
- : `HTTP ${response.status} from the scrape backend`,
169
- };
170
- }
171
- const record = typeof data === "object" && data !== null ? data : {};
172
- if (!response.ok) {
173
- const message = typeof record.error === "string" ? record.error : response.statusText;
174
- return {
175
- kind: "unavailable",
176
- reason: `HTTP ${response.status} from the scrape backend${message ? ` (${message})` : ""}`,
177
- };
178
- }
179
- const payload = typeof record.data === "object" && record.data !== null ? record.data : {};
180
- const markdown = typeof payload.markdown === "string" ? payload.markdown : typeof payload.content === "string" ? payload.content : "";
181
- if (!markdown.trim())
182
- return { kind: "unavailable", reason: "the scrape backend returned no content for this URL" };
183
- return { kind: "ok", markdown, bodyTruncated: truncated };
184
- }
185
- function looksLikeHtml(body) {
186
- return /^\s*(<!doctype html|<html\b|<head\b)/i.test(body);
187
- }
188
- /** Whether a body carries enough readable text to be worth returning as-is. */
189
- function hasContent(body) {
190
- if (body.trim().length < 200)
191
- return false;
192
- const withoutScripts = body.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "");
193
- const textContent = withoutScripts.replace(/<[^>]*>/g, "").trim();
194
- return textContent.length > 100;
195
- }
@@ -1,228 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { readBodyLimited } from "../http.js";
3
- import { describeFetchError } from "./backend.js";
4
- import { getEffectiveSandboxPolicy, networkDeniedMessage } from "../sandbox.js";
5
- import { runSerper } from "./search-serper.js";
6
- import { runSearxng, searxngConfigured } from "./search-searxng.js";
7
- export { SEARXNG_BACKEND } from "./search-searxng.js";
8
- export const MAX_BODY_BYTES = 2 * 1024 * 1024;
9
- const DEFAULT_MAX_RESULTS = 10;
10
- const MAX_RESULTS_LIMIT = 30;
11
- const SNIPPET_MAX_CHARS = 300;
12
- export const VALID_TIME_RANGES = new Set(["day", "week", "month", "year"]);
13
- /** ISO 639-1/2 with an optional region subtag, or SearXNG's "all". */
14
- export const LANGUAGE_RE = /^(all|[a-z]{2,3}([-_][a-zA-Z]{2,4})?)$/;
15
- const EXACT_TRACKING_PARAMS = new Set([
16
- "ref",
17
- "ref_src",
18
- "referrer",
19
- "fbclid",
20
- "gclid",
21
- "msclkid",
22
- "igshid",
23
- "mc_cid",
24
- "mc_eid",
25
- "spm",
26
- "source",
27
- ]);
28
- export function isString(v) {
29
- return typeof v === "string";
30
- }
31
- export function collapse(s) {
32
- return s.replace(/\s+/g, " ").trim();
33
- }
34
- export function clamp(s, max) {
35
- const chars = Array.from(s);
36
- if (chars.length <= max)
37
- return s;
38
- return chars.slice(0, max - 1).join("") + "…";
39
- }
40
- export function hostOf(url) {
41
- try {
42
- return new URL(url).hostname.replace(/^www\./, "");
43
- }
44
- catch {
45
- return url;
46
- }
47
- }
48
- /** Normalize a published date to YYYY-MM-DD; pass unparseable values through. */
49
- function formatPublished(raw) {
50
- if (!raw)
51
- return "";
52
- const ms = Date.parse(raw);
53
- if (Number.isNaN(ms))
54
- return collapse(raw);
55
- return new Date(ms).toISOString().slice(0, 10);
56
- }
57
- /**
58
- * Identity key for deduplication: scheme, "www.", trailing slash, fragment and
59
- * tracking parameters all describe the same page.
60
- */
61
- export function dedupeKey(raw) {
62
- try {
63
- const u = new URL(raw);
64
- const host = u.hostname.toLowerCase().replace(/^www\./, "");
65
- const path = u.pathname.replace(/\/+$/, "");
66
- const params = [...u.searchParams].filter(([k]) => !k.toLowerCase().startsWith("utm_") && !EXACT_TRACKING_PARAMS.has(k.toLowerCase()));
67
- params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
68
- const query = params.map(([k, v]) => `${k}=${v}`).join("&");
69
- return `${host}${path}${query ? `?${query}` : ""}`;
70
- }
71
- catch {
72
- return raw.trim().toLowerCase();
73
- }
74
- }
75
- export function dedupeByUrl(results) {
76
- const seen = new Set();
77
- return results.filter((r) => {
78
- const key = dedupeKey(r.url);
79
- if (seen.has(key))
80
- return false;
81
- seen.add(key);
82
- return true;
83
- });
84
- }
85
- /**
86
- * One block per result:
87
- * 1. Title
88
- * https://url
89
- * [2026-08-18 · google]
90
- * snippet on a single line
91
- * Snippets are whitespace-collapsed and clamped so the indented block
92
- * structure survives multi-line engine output.
93
- */
94
- export function formatResults(results) {
95
- return results
96
- .map((r, i) => {
97
- const lines = [`${i + 1}. ${collapse(r.title) || hostOf(r.url)}`, ` ${r.url}`];
98
- const meta = [];
99
- const date = formatPublished(r.publishedDate);
100
- if (date)
101
- meta.push(date);
102
- if (r.engine)
103
- meta.push(r.engine);
104
- if (meta.length > 0)
105
- lines.push(` [${meta.join(" · ")}]`);
106
- const snippet = clamp(collapse(r.content), SNIPPET_MAX_CHARS);
107
- if (snippet)
108
- lines.push(` ${snippet}`);
109
- return lines.join("\n");
110
- })
111
- .join("\n\n");
112
- }
113
- export function resolveMaxResults(raw, notes) {
114
- if (raw === undefined)
115
- return DEFAULT_MAX_RESULTS;
116
- const n = Math.floor(Number(raw));
117
- if (!Number.isFinite(n) || n < 1) {
118
- notes.push(`ignored invalid max_results "${raw}"; using ${DEFAULT_MAX_RESULTS}`);
119
- return DEFAULT_MAX_RESULTS;
120
- }
121
- if (n > MAX_RESULTS_LIMIT) {
122
- notes.push(`max_results capped at ${MAX_RESULTS_LIMIT}`);
123
- return MAX_RESULTS_LIMIT;
124
- }
125
- return n;
126
- }
127
- export function emptyResponse() {
128
- return {
129
- results: [],
130
- answers: [],
131
- suggestions: [],
132
- corrections: [],
133
- infoboxes: [],
134
- unresponsive_engines: [],
135
- };
136
- }
137
- /** Backends report parameter problems as {"error": "..."} — sometimes with HTTP 200. */
138
- function backendErrorMessage(data) {
139
- if (typeof data !== "object" || data === null)
140
- return "";
141
- const o = data;
142
- for (const key of ["error", "detail", "message"]) {
143
- const v = o[key];
144
- if (isString(v) && v.trim())
145
- return collapse(v);
146
- if (typeof v === "object" && v !== null) {
147
- const nested = v.message;
148
- if (isString(nested) && nested.trim())
149
- return collapse(nested);
150
- }
151
- }
152
- return "";
153
- }
154
- /**
155
- * Shared fetch scaffolding for both search backends: read the body with a
156
- * size cap, then distinguish HTTP errors, non-JSON bodies and backend-reported
157
- * errors before handing valid JSON to the backend-specific parser. Error
158
- * bodies are valid JSON even on a 4xx, so parse before branching on the
159
- * status; that way the backend's own message reaches the caller.
160
- */
161
- export async function fetchJsonOutcome(url, init, parse) {
162
- let response;
163
- try {
164
- response = await fetch(url, init);
165
- }
166
- catch (err) {
167
- return { kind: "network", message: describeFetchError(err) };
168
- }
169
- let text;
170
- let bodyTooLarge;
171
- try {
172
- const body = await readBodyLimited(response, MAX_BODY_BYTES);
173
- text = body.text;
174
- bodyTooLarge = body.truncated;
175
- }
176
- catch (err) {
177
- return { kind: "network", message: describeFetchError(err) };
178
- }
179
- let parsed;
180
- let parseFailed = false;
181
- try {
182
- parsed = JSON.parse(text);
183
- }
184
- catch {
185
- parseFailed = true;
186
- }
187
- const backendMessage = parseFailed ? "" : backendErrorMessage(parsed);
188
- if (!response.ok)
189
- return { kind: "http", status: response.status, backendMessage };
190
- if (parseFailed) {
191
- return { kind: "invalid-json", bodyTooLarge, sample: clamp(collapse(text), 100) };
192
- }
193
- // A truncated body that still parsed would be missing results silently.
194
- if (bodyTooLarge)
195
- return { kind: "invalid-json", bodyTooLarge: true, sample: "" };
196
- if (backendMessage)
197
- return { kind: "http", status: response.status, backendMessage };
198
- return { kind: "ok", data: parse(parsed) };
199
- }
200
- export const webSearchTool = tool({
201
- description: `Search the web via Google. Returns ranked results with title, URL, source and a snippet. Use this for current information, news, documentation, or anything needing up-to-date knowledge. Today's date: ${new Date().toISOString().slice(0, 10)}. When the user asks about the latest or newest state of something, use the current year in the query or set time_range (day, week, month, year) — do not append an older year from your training knowledge. A few targeted queries plus fetching the best sources is enough; then produce the user's deliverable instead of searching exhaustively.`,
202
- inputSchema: jsonSchema({
203
- type: "object",
204
- properties: {
205
- query: { type: "string", description: "The search query" },
206
- language: { type: "string", description: "Search language code, e.g. zh, en, zh-CN (default: en)" },
207
- time_range: { type: "string", description: "Time range filter: day, week, month, year" },
208
- max_results: {
209
- type: "number",
210
- description: `Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`,
211
- },
212
- },
213
- required: ["query"],
214
- }),
215
- execute: async (input) => {
216
- if (getEffectiveSandboxPolicy().network === "deny")
217
- return `Search error: ${networkDeniedMessage()}`;
218
- const query = (input.query ?? "").trim();
219
- if (!query)
220
- return 'Search error: "query" must be a non-empty string.';
221
- const notes = [];
222
- // SearXNG is only used when the user explicitly configures an instance;
223
- // otherwise Serper is used.
224
- if (searxngConfigured())
225
- return runSearxng(input, query, notes);
226
- return runSerper(input, query, notes);
227
- },
228
- });
@@ -1,55 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { mkdir, writeFile } from "fs/promises";
3
- import { existsSync } from "fs";
4
- import path from "path";
5
- import { confirm, isEditAutoApprove } from "../confirm.js";
6
- import { pathAccessError } from "../sandbox.js";
7
- import { atomicWriteFile } from "./atomic-file.js";
8
- const MAX_WRITE_BYTES = 10 * 1024 * 1024;
9
- export const writeTool = tool({
10
- description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does (unless `append` is true). Creates parent directories as needed.",
11
- inputSchema: jsonSchema({
12
- type: "object",
13
- properties: {
14
- filePath: { type: "string", description: "Path to the file to write (relative to cwd or absolute)" },
15
- content: { type: "string", description: "The content to write to the file" },
16
- append: { type: "boolean", description: "If true, append to the file instead of overwriting" },
17
- },
18
- required: ["filePath", "content"],
19
- }),
20
- execute: async ({ filePath, content, append }) => {
21
- if (Buffer.byteLength(content, "utf-8") > MAX_WRITE_BYTES) {
22
- return `Error: content exceeds ${MAX_WRITE_BYTES} bytes. Write in smaller chunks or check the content size.`;
23
- }
24
- const denied = pathAccessError(filePath, "write");
25
- if (denied)
26
- return `Error: ${denied}`;
27
- const resolved = path.resolve(process.cwd(), filePath);
28
- // Same confirmation semantics as edit: in ask mode every mutation —
29
- // create, overwrite or append — is confirmed; accept-edits skips them all.
30
- if (!isEditAutoApprove()) {
31
- const exists = existsSync(resolved);
32
- const action = append
33
- ? `Append to file: ${filePath}`
34
- : exists
35
- ? `Overwrite existing file: ${filePath}`
36
- : `Create new file: ${filePath}`;
37
- const approved = await confirm(action);
38
- if (!approved)
39
- return "Write rejected by user.";
40
- }
41
- try {
42
- await mkdir(path.dirname(resolved), { recursive: true });
43
- if (append) {
44
- await writeFile(resolved, content, { flag: "a", encoding: "utf-8" });
45
- }
46
- else {
47
- await atomicWriteFile(resolved, content);
48
- }
49
- return `Written ${Buffer.byteLength(content, "utf-8")} bytes to ${filePath}${append ? " (appended)" : ""}`;
50
- }
51
- catch (err) {
52
- return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
53
- }
54
- },
55
- });
package/dist/tui/App.js DELETED
@@ -1,138 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useState } from "react";
3
- import { Box, useInput, useStdout } from "ink";
4
- import { Spinner } from "./Spinner.js";
5
- import { InputBar } from "./InputBar.js";
6
- import { MessageList } from "./MessageList.js";
7
- import { ConfirmBar, confirmBarRows } from "./ConfirmBar.js";
8
- import { QuestionBar, questionBarRows } from "./QuestionBar.js";
9
- import { StatusBar } from "./StatusBar.js";
10
- import { ModelPicker, modelPickerRows } from "./ModelPicker.js";
11
- import { ThinkPicker, thinkPickerRows } from "./ThinkPicker.js";
12
- import { CtxPicker, ctxPickerRows } from "./CtxPicker.js";
13
- import { SessionPicker, sessionPickerRows } from "./SessionPicker.js";
14
- import { setCaretPosition } from "./caret.js";
15
- import { computeMessageMaxHeight, frameRows, INPUT_BAR_ROWS } from "./layout.js";
16
- import { isCtrlC } from "./input-history.js";
17
- export function App({ initialState, onSubmit, onConfirm, onQuestionAnswer, onModelPick, onModelCancel, onThinkPick, onThinkCancel, onCtxPick, onCtxCancel, onSessionPick, onSessionCancel, onToggleSelectionMode, onExitSelectionMode, onExit, onCopyNotice, onNotice, }) {
18
- // Ink recalculates its own layout on resize without re-rendering React, so
19
- // track terminal size ourselves to keep heights/widths in sync.
20
- const { stdout } = useStdout();
21
- const [terminal, setTerminal] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 0 });
22
- useEffect(() => {
23
- const onResize = () => setTerminal({ columns: stdout.columns || 80, rows: stdout.rows || 0 });
24
- stdout.on("resize", onResize);
25
- return () => {
26
- stdout.off("resize", onResize);
27
- };
28
- }, [stdout]);
29
- useInput((input, key) => {
30
- if (isCtrlC(input, key)) {
31
- if (initialState.confirmMessage) {
32
- onConfirm(false);
33
- return;
34
- }
35
- if (initialState.question) {
36
- onQuestionAnswer(null);
37
- return;
38
- }
39
- if (initialState.modelPicker) {
40
- onModelCancel();
41
- return;
42
- }
43
- if (initialState.thinkPicker) {
44
- onThinkCancel();
45
- return;
46
- }
47
- if (initialState.ctxPicker) {
48
- onCtxCancel();
49
- return;
50
- }
51
- if (initialState.sessionPicker) {
52
- onSessionCancel();
53
- return;
54
- }
55
- if (initialState.selectionMode) {
56
- onExitSelectionMode();
57
- return;
58
- }
59
- if (initialState.isRunning) {
60
- onExit();
61
- }
62
- return;
63
- }
64
- if (key.ctrl && input === "b") {
65
- if (initialState.confirmMessage ||
66
- initialState.question ||
67
- initialState.modelPicker ||
68
- initialState.thinkPicker ||
69
- initialState.ctxPicker ||
70
- initialState.sessionPicker)
71
- return;
72
- if (initialState.isRunning)
73
- return;
74
- if (!initialState.selectionMode)
75
- return;
76
- onToggleSelectionMode();
77
- return;
78
- }
79
- if (key.escape &&
80
- initialState.isRunning &&
81
- !initialState.confirmMessage &&
82
- !initialState.question &&
83
- !initialState.modelPicker &&
84
- !initialState.thinkPicker &&
85
- !initialState.ctxPicker &&
86
- !initialState.sessionPicker) {
87
- onExit();
88
- }
89
- });
90
- if (initialState.confirmMessage ||
91
- initialState.question ||
92
- initialState.modelPicker ||
93
- initialState.thinkPicker ||
94
- initialState.ctxPicker ||
95
- initialState.sessionPicker)
96
- setCaretPosition(null);
97
- const columns = terminal.columns;
98
- const terminalRows = terminal.rows;
99
- const layoutRows = frameRows(terminalRows);
100
- const [inputBarRows, setInputBarRows] = useState(INPUT_BAR_ROWS);
101
- const onInputBarRowsChange = useCallback((rows) => {
102
- setInputBarRows((prev) => (prev === rows ? prev : rows));
103
- }, []);
104
- const confirmActive = Boolean(initialState.confirmMessage);
105
- const questionActive = Boolean(initialState.question);
106
- const modelPickerActive = Boolean(initialState.modelPicker);
107
- const thinkPickerActive = Boolean(initialState.thinkPicker);
108
- const ctxPickerActive = Boolean(initialState.ctxPicker);
109
- const sessionPickerActive = Boolean(initialState.sessionPicker);
110
- const overlayActive = confirmActive || questionActive || modelPickerActive || thinkPickerActive || ctxPickerActive || sessionPickerActive;
111
- useEffect(() => {
112
- if (overlayActive)
113
- setInputBarRows(INPUT_BAR_ROWS);
114
- }, [overlayActive]);
115
- const footerRows = confirmActive
116
- ? confirmBarRows(initialState.confirmMessage ?? "", columns, layoutRows)
117
- : questionActive
118
- ? questionBarRows(initialState.question.prompt, initialState.question.options, columns, layoutRows)
119
- : modelPickerActive
120
- ? modelPickerRows(layoutRows)
121
- : thinkPickerActive
122
- ? thinkPickerRows()
123
- : ctxPickerActive
124
- ? ctxPickerRows()
125
- : sessionPickerActive
126
- ? sessionPickerRows(layoutRows)
127
- : inputBarRows;
128
- const spinnerVisible = initialState.isRunning &&
129
- !confirmActive &&
130
- !questionActive &&
131
- !modelPickerActive &&
132
- !thinkPickerActive &&
133
- !ctxPickerActive &&
134
- !sessionPickerActive;
135
- const spinnerRows = spinnerVisible ? 1 : 0;
136
- const messageMaxHeight = computeMessageMaxHeight(layoutRows, footerRows, spinnerRows);
137
- return (_jsxs(Box, { flexDirection: "column", width: "100%", height: layoutRows || undefined, overflow: "hidden", children: [!modelPickerActive && (_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflow: "hidden", width: "100%", children: _jsx(MessageList, { messages: initialState.messages, maxHeight: messageMaxHeight, columns: columns, onCopyNotice: onCopyNotice, selectionMode: Boolean(initialState.selectionMode), onExitSelectionMode: onExitSelectionMode, interactive: !confirmActive && !questionActive }) })), spinnerVisible && (_jsx(Box, { paddingLeft: 1, flexShrink: 0, width: "100%", children: _jsx(Spinner, { label: initialState.spinnerText || "思考中..." }) })), _jsx(StatusBar, { state: initialState, columns: columns }), initialState.confirmMessage ? (_jsx(ConfirmBar, { message: initialState.confirmMessage, onConfirm: onConfirm, columns: columns, maxRows: footerRows })) : initialState.question ? (_jsx(QuestionBar, { prompt: initialState.question.prompt, options: initialState.question.options, onAnswer: onQuestionAnswer, columns: columns, maxRows: footerRows })) : initialState.modelPicker ? (_jsx(ModelPicker, { currentModel: initialState.model, onSelect: onModelPick, onCancel: onModelCancel })) : initialState.thinkPicker ? (_jsx(ThinkPicker, { scope: initialState.thinkPicker.scope, modelId: initialState.model, onSelect: (effort) => onThinkPick(effort, initialState.thinkPicker.scope), onCancel: onThinkCancel })) : initialState.ctxPicker ? (_jsx(CtxPicker, { onSelect: onCtxPick, onCancel: onCtxCancel })) : initialState.sessionPicker ? (_jsx(SessionPicker, { currentId: initialState.sessionId, onSelect: onSessionPick, onCancel: onSessionCancel })) : (_jsx(InputBar, { onSubmit: onSubmit, disabled: initialState.isRunning || Boolean(initialState.selectionMode), placeholder: initialState.isRunning ? "按 Esc 取消运行..." : "输入消息... (Ctrl+J 换行)", columns: columns, terminalRows: layoutRows, onSelectionMode: onToggleSelectionMode, onExit: onExit, onRowsChange: onInputBarRowsChange, onNotice: onNotice }))] }));
138
- }