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,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
- }