min-agent 0.5.0 → 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 (156) hide show
  1. package/README.md +32 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +25 -8
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +7 -3
  6. package/skills/self-config/reference.md +8 -4
  7. package/dist/agent.js +0 -1342
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/exec.js +0 -35
  11. package/dist/cli/commands/history.js +0 -58
  12. package/dist/cli/commands/index.js +0 -241
  13. package/dist/cli/commands/init.js +0 -18
  14. package/dist/cli/commands/mcp.js +0 -173
  15. package/dist/cli/commands/memory.js +0 -87
  16. package/dist/cli/commands/models.js +0 -21
  17. package/dist/cli/commands/permission.js +0 -12
  18. package/dist/cli/commands/rules.js +0 -33
  19. package/dist/cli/commands/sandbox.js +0 -13
  20. package/dist/cli/commands/serve.js +0 -9
  21. package/dist/cli/commands/setup.js +0 -4
  22. package/dist/cli/commands/shared.js +0 -16
  23. package/dist/cli/commands/skills.js +0 -119
  24. package/dist/cli/commands/think.js +0 -12
  25. package/dist/cli/commands/update.js +0 -7
  26. package/dist/cli/commands/write-config.js +0 -52
  27. package/dist/cli/errors.js +0 -36
  28. package/dist/cli/exec-prompt.js +0 -26
  29. package/dist/cli/option-helpers.js +0 -65
  30. package/dist/cli/program.js +0 -217
  31. package/dist/clipboard.js +0 -142
  32. package/dist/code-mode.js +0 -184
  33. package/dist/compaction.js +0 -644
  34. package/dist/config.js +0 -470
  35. package/dist/confirm.js +0 -172
  36. package/dist/context-window.js +0 -246
  37. package/dist/doom-loop.js +0 -278
  38. package/dist/fetch-timeout.js +0 -152
  39. package/dist/http-approvals.js +0 -60
  40. package/dist/http.js +0 -119
  41. package/dist/instructions.js +0 -196
  42. package/dist/logger.js +0 -95
  43. package/dist/markdown.js +0 -259
  44. package/dist/mcp.js +0 -1036
  45. package/dist/memory-cli.js +0 -33
  46. package/dist/memory.js +0 -295
  47. package/dist/model-catalog.js +0 -285
  48. package/dist/output.js +0 -76
  49. package/dist/paste-handler.js +0 -41
  50. package/dist/permission-cli.js +0 -40
  51. package/dist/plugins.js +0 -131
  52. package/dist/pricing.js +0 -119
  53. package/dist/provider.js +0 -63
  54. package/dist/question-format.js +0 -60
  55. package/dist/reasoning-stream.js +0 -158
  56. package/dist/sandbox-cli.js +0 -79
  57. package/dist/sandbox.js +0 -403
  58. package/dist/save-throttle.js +0 -45
  59. package/dist/scope.js +0 -23
  60. package/dist/serve/common.js +0 -425
  61. package/dist/serve/routes-chat.js +0 -367
  62. package/dist/serve/routes-mcp.js +0 -212
  63. package/dist/serve/routes-memory.js +0 -95
  64. package/dist/serve/routes-meta.js +0 -233
  65. package/dist/serve/routes-sessions.js +0 -61
  66. package/dist/serve/routes-skills.js +0 -70
  67. package/dist/serve.js +0 -138
  68. package/dist/sessions.js +0 -265
  69. package/dist/skills.js +0 -599
  70. package/dist/structured-output.js +0 -29
  71. package/dist/synthetic.js +0 -7
  72. package/dist/think-cli.js +0 -36
  73. package/dist/thinking-wire.js +0 -228
  74. package/dist/thinking.js +0 -142
  75. package/dist/title-gen.js +0 -55
  76. package/dist/token-display.js +0 -39
  77. package/dist/tool-display.js +0 -178
  78. package/dist/tool-output.js +0 -126
  79. package/dist/tools/apply_patch.js +0 -265
  80. package/dist/tools/atomic-file.js +0 -35
  81. package/dist/tools/backend.js +0 -61
  82. package/dist/tools/bash.js +0 -231
  83. package/dist/tools/code_search.js +0 -98
  84. package/dist/tools/edit.js +0 -68
  85. package/dist/tools/explore.js +0 -232
  86. package/dist/tools/glob.js +0 -40
  87. package/dist/tools/grep.js +0 -174
  88. package/dist/tools/index.js +0 -34
  89. package/dist/tools/question.js +0 -54
  90. package/dist/tools/read.js +0 -109
  91. package/dist/tools/search-searxng.js +0 -223
  92. package/dist/tools/search-serper.js +0 -189
  93. package/dist/tools/task.js +0 -165
  94. package/dist/tools/todo.js +0 -213
  95. package/dist/tools/web_fetch.js +0 -195
  96. package/dist/tools/web_search.js +0 -228
  97. package/dist/tools/write.js +0 -55
  98. package/dist/tui/App.js +0 -126
  99. package/dist/tui/ConfirmBar.js +0 -66
  100. package/dist/tui/InputBar.js +0 -616
  101. package/dist/tui/MessageList.js +0 -718
  102. package/dist/tui/ModelPicker.js +0 -113
  103. package/dist/tui/QuestionBar.js +0 -136
  104. package/dist/tui/SessionPicker.js +0 -79
  105. package/dist/tui/Spinner.js +0 -6
  106. package/dist/tui/StatusBar.js +0 -22
  107. package/dist/tui/ThinkPicker.js +0 -77
  108. package/dist/tui/agent-runner.js +0 -223
  109. package/dist/tui/bracketed-paste.js +0 -37
  110. package/dist/tui/caret-pos.js +0 -179
  111. package/dist/tui/caret.js +0 -69
  112. package/dist/tui/click-count.js +0 -13
  113. package/dist/tui/diff-view.js +0 -61
  114. package/dist/tui/drag-state.js +0 -49
  115. package/dist/tui/hydrate.js +0 -129
  116. package/dist/tui/index.js +0 -258
  117. package/dist/tui/input-history.js +0 -125
  118. package/dist/tui/layout.js +0 -105
  119. package/dist/tui/mouse.js +0 -46
  120. package/dist/tui/overlay-input.js +0 -12
  121. package/dist/tui/paste-draft.js +0 -173
  122. package/dist/tui/prompt-queue.js +0 -24
  123. package/dist/tui/selection.js +0 -232
  124. package/dist/tui/session-switch.js +0 -28
  125. package/dist/tui/slash-commands.js +0 -123
  126. package/dist/tui/slash-handler.js +0 -589
  127. package/dist/tui/text-width.js +0 -113
  128. package/dist/tui/theme.js +0 -12
  129. package/dist/tui/token-info.js +0 -7
  130. package/dist/tui/tool-children.js +0 -19
  131. package/dist/tui/types.js +0 -1
  132. package/dist/tui/undo-stack.js +0 -14
  133. package/dist/tui/use-sgr-mouse.js +0 -29
  134. package/dist/tui-chat.js +0 -456
  135. package/dist/updater.js +0 -116
  136. package/dist/xml-search.js +0 -194
  137. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  138. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  139. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  140. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  141. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  142. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  143. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  144. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  145. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  146. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  147. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  148. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  149. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  150. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  151. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  152. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  153. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  154. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  155. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  156. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -1,126 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
2
- import path from "path";
3
- import { randomBytes } from "crypto";
4
- import { getConfigDir } from "./config.js";
5
- /** Default limits (aligned with common agent tooling practice). */
6
- export const TOOL_OUTPUT_MAX_LINES = 2000;
7
- export const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
8
- const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
9
- const CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
10
- let lastCleanup = 0;
11
- function toolOutputDir() {
12
- return path.join(getConfigDir(), "tool-output");
13
- }
14
- function cleanupOldToolOutputs(dir) {
15
- const now = Date.now();
16
- if (now - lastCleanup < CLEANUP_INTERVAL_MS)
17
- return;
18
- lastCleanup = now;
19
- if (!existsSync(dir))
20
- return;
21
- try {
22
- for (const f of readdirSync(dir)) {
23
- if (!f.startsWith("tool-") || !f.endsWith(".txt"))
24
- continue;
25
- const p = path.join(dir, f);
26
- try {
27
- if (now - statSync(p).mtimeMs > RETENTION_MS)
28
- unlinkSync(p);
29
- }
30
- catch {
31
- /* ignore */
32
- }
33
- }
34
- }
35
- catch {
36
- /* ignore */
37
- }
38
- }
39
- /** Write full text to ~/.min-agent/tool-output/ and return absolute path. */
40
- export function writeFullToolOutput(fullText) {
41
- const dir = toolOutputDir();
42
- mkdirSync(dir, { recursive: true });
43
- cleanupOldToolOutputs(dir);
44
- const name = `tool-${Date.now()}-${randomBytes(4).toString("hex")}.txt`;
45
- const filePath = path.join(dir, name);
46
- writeFileSync(filePath, fullText, "utf-8");
47
- return filePath;
48
- }
49
- const hint = (filePath) => `The tool output was truncated. Full output saved to: ${filePath}\nUse the read tool with startLine/endLine, or grep, to inspect further.`;
50
- /** Keep start or end of text within line/byte limits (good for shell logs / files). */
51
- function preview(text, maxLines, maxBytes, direction, lines, totalBytes) {
52
- if (lines.length <= maxLines && totalBytes <= maxBytes) {
53
- return { text, cut: false };
54
- }
55
- const out = [];
56
- let bytes = 0;
57
- const pushLine = (i, first) => {
58
- const size = Buffer.byteLength(lines[i], "utf-8") + (first ? 0 : 1);
59
- if (bytes + size > maxBytes)
60
- return false;
61
- if (direction === "head")
62
- out.push(lines[i]);
63
- else
64
- out.unshift(lines[i]);
65
- bytes += size;
66
- return true;
67
- };
68
- if (direction === "head") {
69
- for (let i = 0; i < lines.length && out.length < maxLines; i++) {
70
- if (!pushLine(i, out.length === 0)) {
71
- if (out.length === 0) {
72
- const buf = Buffer.from(lines[i], "utf-8");
73
- let end = Math.min(maxBytes, buf.length);
74
- while (end > 0 && (buf[end] & 0xc0) === 0x80)
75
- end--;
76
- out.push(buf.subarray(0, end).toString("utf-8"));
77
- }
78
- break;
79
- }
80
- }
81
- }
82
- else {
83
- for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
84
- if (!pushLine(i, out.length === 0)) {
85
- if (out.length === 0) {
86
- const buf = Buffer.from(lines[i], "utf-8");
87
- let start = buf.length - maxBytes;
88
- if (start < 0)
89
- start = 0;
90
- while (start < buf.length && (buf[start] & 0xc0) === 0x80)
91
- start++;
92
- out.unshift(buf.subarray(start).toString("utf-8"));
93
- }
94
- break;
95
- }
96
- }
97
- }
98
- return { text: out.join("\n"), cut: true };
99
- }
100
- /** Keep end of text within line/byte limits (good for shell logs). */
101
- export function tailPreview(text, maxLines, maxBytes) {
102
- return preview(text, maxLines, maxBytes, "tail", text.split("\n"), Buffer.byteLength(text, "utf-8"));
103
- }
104
- /** Keep start of text within line/byte limits (good for files / HTTP bodies). */
105
- export function headPreview(text, maxLines, maxBytes) {
106
- return preview(text, maxLines, maxBytes, "head", text.split("\n"), Buffer.byteLength(text, "utf-8"));
107
- }
108
- /**
109
- * If text exceeds limits, write full text to disk and return a preview + path hint.
110
- * Otherwise returns the original string.
111
- */
112
- export function truncateToolOutput(text, options = {}) {
113
- const maxLines = options.maxLines ?? TOOL_OUTPUT_MAX_LINES;
114
- const maxBytes = options.maxBytes ?? TOOL_OUTPUT_MAX_BYTES;
115
- const direction = options.direction ?? "head";
116
- const lines = text.split("\n");
117
- const totalBytes = Buffer.byteLength(text, "utf-8");
118
- if (lines.length <= maxLines && totalBytes <= maxBytes) {
119
- return { content: text, truncated: false };
120
- }
121
- const filePath = writeFullToolOutput(text);
122
- const pv = preview(text, maxLines, maxBytes, direction, lines, totalBytes);
123
- const header = "...output truncated...\n\n";
124
- const content = direction === "tail" ? `${header}${hint(filePath)}\n\n${pv.text}` : `${pv.text}\n\n${header}${hint(filePath)}`;
125
- return { content, truncated: true, outputPath: filePath };
126
- }
@@ -1,265 +0,0 @@
1
- import { tool, jsonSchema } from "ai";
2
- import { readFileSync, existsSync } from "fs";
3
- import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
4
- import path from "path";
5
- import { confirm, isEditAutoApprove } from "../confirm.js";
6
- import { pathAccessError } from "../sandbox.js";
7
- import { atomicWriteFile, tempPathFor } from "./atomic-file.js";
8
- const OFFSET_TOLERANCE = 3;
9
- export function parseUnifiedDiff(diff) {
10
- const files = [];
11
- let current = null;
12
- let currentHunk = null;
13
- let pendingHeader = null;
14
- let remainingOld = 0;
15
- let remainingNew = 0;
16
- const lines = diff.replace(/\r\n/g, "\n").split("\n");
17
- for (const raw of lines) {
18
- if (currentHunk === null && raw.startsWith("--- ")) {
19
- pendingHeader = raw.slice(4);
20
- continue;
21
- }
22
- if (currentHunk === null && pendingHeader !== null && raw.startsWith("+++ ")) {
23
- const target = raw.slice(4);
24
- if (current)
25
- files.push(current);
26
- current = { path: stripPrefix(target), isNew: pendingHeader === "/dev/null", hunks: [] };
27
- currentHunk = null;
28
- pendingHeader = null;
29
- continue;
30
- }
31
- if (currentHunk === null && raw.startsWith("@@ ")) {
32
- const m = raw.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
33
- if (m && current) {
34
- currentHunk = {
35
- oldStart: parseInt(m[1], 10),
36
- oldCount: m[2] ? parseInt(m[2], 10) : 1,
37
- newStart: parseInt(m[3], 10),
38
- newCount: m[4] ? parseInt(m[4], 10) : 1,
39
- lines: [],
40
- };
41
- current.hunks.push(currentHunk);
42
- remainingOld = currentHunk.oldCount;
43
- remainingNew = currentHunk.newCount;
44
- }
45
- continue;
46
- }
47
- if (currentHunk && current) {
48
- if (raw.startsWith("+")) {
49
- currentHunk.lines.push({ type: "add", text: raw.slice(1) });
50
- remainingNew--;
51
- }
52
- else if (raw.startsWith("-")) {
53
- currentHunk.lines.push({ type: "delete", text: raw.slice(1) });
54
- remainingOld--;
55
- }
56
- else if (raw.startsWith(" ")) {
57
- currentHunk.lines.push({ type: "context", text: raw.slice(1) });
58
- remainingOld--;
59
- remainingNew--;
60
- }
61
- if (remainingOld <= 0 && remainingNew <= 0)
62
- currentHunk = null;
63
- }
64
- }
65
- if (current)
66
- files.push(current);
67
- return files;
68
- }
69
- function stripPrefix(p) {
70
- const trimmed = p.trim();
71
- if (trimmed === "/dev/null")
72
- return trimmed;
73
- if (trimmed.startsWith("a/"))
74
- return trimmed.slice(2);
75
- if (trimmed.startsWith("b/"))
76
- return trimmed.slice(2);
77
- return trimmed;
78
- }
79
- function findMatch(lines, start, block) {
80
- if (block.length === 0)
81
- return Math.max(0, Math.min(start, lines.length));
82
- for (let pos = Math.max(0, start - OFFSET_TOLERANCE); pos <= Math.min(lines.length - block.length, start + OFFSET_TOLERANCE); pos++) {
83
- let ok = true;
84
- for (let i = 0; i < block.length; i++) {
85
- if (lines[pos + i] !== block[i]) {
86
- ok = false;
87
- break;
88
- }
89
- }
90
- if (ok)
91
- return pos;
92
- }
93
- return -1;
94
- }
95
- function applyHunk(lines, hunk) {
96
- const block = hunk.lines.filter((l) => l.type !== "add").map((l) => l.text);
97
- const pos = findMatch(lines, hunk.oldStart - 1, block);
98
- if (pos === -1) {
99
- return { error: `第 ${hunk.oldStart} 行附近的 hunk 未找到匹配(需要匹配 ${block.length} 行)` };
100
- }
101
- const result = [...lines.slice(0, pos)];
102
- let src = pos;
103
- for (const l of hunk.lines) {
104
- if (l.type === "context") {
105
- result.push(lines[src]);
106
- src++;
107
- }
108
- else if (l.type === "delete") {
109
- src++;
110
- }
111
- else {
112
- result.push(l.text);
113
- }
114
- }
115
- result.push(...lines.slice(src));
116
- return { lines: result };
117
- }
118
- /** 在内存中应用全部文件;成功后返回各文件新内容(不写盘)。 */
119
- export function buildPatchedFiles(files, cwd = process.cwd()) {
120
- const contents = [];
121
- for (const file of files) {
122
- const target = path.resolve(cwd, file.path);
123
- const exists = existsSync(target);
124
- if (file.isNew && !exists) {
125
- const lines = [];
126
- for (const hunk of file.hunks) {
127
- const r = applyHunk(lines, hunk);
128
- if ("error" in r)
129
- return { ok: false, error: `${file.path}: ${r.error}` };
130
- lines.length = 0;
131
- lines.push(...r.lines);
132
- }
133
- contents.push({ file, content: lines.join("\n") + (lines.length > 0 ? "\n" : ""), original: null });
134
- continue;
135
- }
136
- if (!exists)
137
- return { ok: false, error: `${file.path}: 文件不存在` };
138
- let content;
139
- try {
140
- content = readFileSync(target, "utf-8");
141
- }
142
- catch (err) {
143
- return { ok: false, error: `${file.path}: 读取失败 ${err instanceof Error ? err.message : String(err)}` };
144
- }
145
- const lines = content.split("\n");
146
- for (const hunk of file.hunks) {
147
- const r = applyHunk(lines, hunk);
148
- if ("error" in r)
149
- return { ok: false, error: `${file.path}: ${r.error}` };
150
- lines.length = 0;
151
- lines.push(...r.lines);
152
- }
153
- contents.push({ file, content: lines.join("\n"), original: content });
154
- }
155
- return { ok: true, contents };
156
- }
157
- function errMessage(err) {
158
- return err instanceof Error ? err.message : String(err);
159
- }
160
- /**
161
- * 事务化写盘:先把所有新内容写入目标旁的临时文件,再统一 rename 就位。
162
- * 第一阶段失败时工作区零改动;rename 中途失败时回滚已就位的文件。
163
- */
164
- async function commitPatchedFiles(contents) {
165
- const cwd = process.cwd();
166
- const staged = [];
167
- // 漂移检查:确认弹窗期间文件可能被用户或其他进程改动。
168
- for (const c of contents) {
169
- const target = path.resolve(cwd, c.file.path);
170
- if (c.original === null) {
171
- if (existsSync(target))
172
- return { ok: false, error: `${c.file.path}: 文件已存在,未应用任何更改` };
173
- continue;
174
- }
175
- let current;
176
- try {
177
- current = await readFile(target, "utf-8");
178
- }
179
- catch (err) {
180
- return { ok: false, error: `${c.file.path}: 读取失败 ${errMessage(err)}` };
181
- }
182
- if (current !== c.original) {
183
- return { ok: false, error: `${c.file.path}: 文件在确认期间已被修改,未应用任何更改` };
184
- }
185
- }
186
- for (const c of contents) {
187
- const target = path.resolve(cwd, c.file.path);
188
- const tmp = tempPathFor(target);
189
- try {
190
- await mkdir(path.dirname(target), { recursive: true });
191
- await writeFile(tmp, c.content, "utf-8");
192
- }
193
- catch (err) {
194
- for (const s of staged)
195
- await unlink(s.tmp).catch(() => { });
196
- await unlink(tmp).catch(() => { });
197
- return { ok: false, error: `${c.file.path}: 写入失败 ${errMessage(err)}` };
198
- }
199
- staged.push({ rel: c.file.path, target, tmp, original: c.original });
200
- }
201
- const renamed = [];
202
- for (const s of staged) {
203
- try {
204
- await rename(s.tmp, s.target);
205
- renamed.push(s);
206
- }
207
- catch (err) {
208
- for (const d of [...renamed].reverse()) {
209
- try {
210
- if (d.original !== null)
211
- await atomicWriteFile(d.target, d.original);
212
- else
213
- await unlink(d.target).catch(() => { });
214
- }
215
- catch { }
216
- }
217
- for (const s2 of staged.slice(renamed.length))
218
- await unlink(s2.tmp).catch(() => { });
219
- return { ok: false, error: `${s.rel}: 写入失败 ${errMessage(err)},已回滚其他文件` };
220
- }
221
- }
222
- return { ok: true };
223
- }
224
- export const applyPatchTool = tool({
225
- description: "Apply a unified diff to the working tree. Use this for precise multi-file edits or when you have a generated diff. " +
226
- "Supports standard unified diff format (---/+++ headers, @@ hunks, context/delete/add lines). " +
227
- "New files are created when the diff targets /dev/null. Conflicts are reported without partial writes.",
228
- inputSchema: jsonSchema({
229
- type: "object",
230
- properties: {
231
- diff: { type: "string", description: "The unified diff text to apply" },
232
- },
233
- required: ["diff"],
234
- }),
235
- execute: async ({ diff }) => {
236
- const parsed = parseUnifiedDiff(diff);
237
- if (parsed.length === 0)
238
- return "Error: 无法解析 diff(缺少 ---/+++ 文件头)";
239
- // 先鉴权再读盘:任何目标路径不可写都直接拒绝,不触碰文件内容。
240
- for (const f of parsed) {
241
- const denied = pathAccessError(f.path, "write");
242
- if (denied)
243
- return `Error: ${denied}`;
244
- }
245
- const built = buildPatchedFiles(parsed);
246
- if (!built.ok)
247
- return `Error: ${built.error}`;
248
- const summary = built.contents
249
- .map((c) => {
250
- const del = c.file.hunks.reduce((s, h) => s + h.lines.filter((l) => l.type === "delete").length, 0);
251
- const add = c.file.hunks.reduce((s, h) => s + h.lines.filter((l) => l.type === "add").length, 0);
252
- return ` ${c.file.path} ${c.file.isNew ? "(new)" : ""} -${del}/+${add}`;
253
- })
254
- .join("\n");
255
- if (!isEditAutoApprove()) {
256
- const approved = await confirm(`Apply patch to ${built.contents.length} file(s)?\n${summary}`);
257
- if (!approved)
258
- return "Patch rejected by user.";
259
- }
260
- const committed = await commitPatchedFiles(built.contents);
261
- if (!committed.ok)
262
- return `Error: ${committed.error}`;
263
- return `Applied patch:\n${summary}`;
264
- },
265
- });
@@ -1,35 +0,0 @@
1
- import { writeFile, rename, unlink } from "fs/promises";
2
- import { writeFileSync, renameSync, unlinkSync } from "fs";
3
- import { randomBytes } from "crypto";
4
- import path from "path";
5
- /** Temp file path living next to the target so the rename stays on the same filesystem. */
6
- export function tempPathFor(target) {
7
- return path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
8
- }
9
- /** Write content to a sibling temp file, then atomically rename it over the target. */
10
- export async function atomicWriteFile(target, content) {
11
- const tmp = tempPathFor(target);
12
- try {
13
- await writeFile(tmp, content, "utf-8");
14
- await rename(tmp, target);
15
- }
16
- catch (err) {
17
- await unlink(tmp).catch(() => { });
18
- throw err;
19
- }
20
- }
21
- /** Sync variant for call sites that must stay synchronous (config/session stores). */
22
- export function atomicWriteFileSync(target, content) {
23
- const tmp = tempPathFor(target);
24
- try {
25
- writeFileSync(tmp, content, "utf-8");
26
- renameSync(tmp, target);
27
- }
28
- catch (err) {
29
- try {
30
- unlinkSync(tmp);
31
- }
32
- catch { }
33
- throw err;
34
- }
35
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Shared plumbing for the HTTP-backed tools (`search_web`, `web_fetch`):
3
- * base-URL resolution, consistent "how to configure this" copy, and fetch
4
- * error description. Kept separate so both tools report problems the same way.
5
- */
6
- /** Human-readable pointer to both configuration channels. */
7
- export function configHint(spec) {
8
- return `Configure a working instance via "${spec.configKey}" in ~/.min-agent/config.json or the ${spec.envName} env var.`;
9
- }
10
- /**
11
- * Normalize a user-supplied base URL: trim whitespace and trailing slashes so
12
- * callers can always append "/path" without producing a double slash.
13
- * Returns an error string for anything that is not an absolute http(s) URL.
14
- */
15
- export function normalizeBaseURL(raw, source) {
16
- const trimmed = raw.trim();
17
- if (trimmed === "")
18
- return { ok: false, error: `${source} is empty` };
19
- let parsed;
20
- try {
21
- parsed = new URL(trimmed);
22
- }
23
- catch {
24
- return { ok: false, error: `${source} is not a valid absolute URL: ${trimmed}` };
25
- }
26
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
27
- return { ok: false, error: `${source} must use http or https, got "${parsed.protocol}"` };
28
- }
29
- // Keep any path prefix (instances behind a reverse proxy subpath) but drop
30
- // query/hash and trailing slashes.
31
- parsed.search = "";
32
- parsed.hash = "";
33
- return { ok: true, base: parsed.toString().replace(/\/+$/, "") };
34
- }
35
- /**
36
- * Resolve a backend base URL. The environment variable wins over the config
37
- * file so a single run can be redirected from the shell without editing
38
- * config.json; the built-in default is the last resort.
39
- */
40
- export function resolveBackendBase(spec, configValue) {
41
- const env = process.env[spec.envName];
42
- if (env?.trim())
43
- return normalizeBaseURL(env, spec.envName);
44
- if (configValue?.trim())
45
- return normalizeBaseURL(configValue, `"${spec.configKey}" in config.json`);
46
- return normalizeBaseURL(spec.fallback, "the built-in default backend URL");
47
- }
48
- /** Turn a thrown fetch error into a short, actionable sentence. */
49
- export function describeFetchError(err) {
50
- if (err instanceof Error) {
51
- // AbortSignal.timeout rejects with a TimeoutError DOMException.
52
- if (err.name === "TimeoutError" || err.name === "AbortError")
53
- return "the request timed out";
54
- const cause = err.cause;
55
- const causeCode = typeof cause === "object" && cause !== null ? cause.code : undefined;
56
- if (typeof causeCode === "string")
57
- return `${err.message} (${causeCode})`;
58
- return err.message;
59
- }
60
- return String(err);
61
- }