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.
- package/README.md +32 -18
- package/dist/cli.js +538 -12
- package/docs/API.md +25 -8
- package/package.json +6 -6
- package/skills/self-config/SKILL.md +7 -3
- package/skills/self-config/reference.md +8 -4
- package/dist/agent.js +0 -1342
- package/dist/assistant-stream.js +0 -131
- package/dist/cli/commands/chat.js +0 -13
- package/dist/cli/commands/exec.js +0 -35
- package/dist/cli/commands/history.js +0 -58
- package/dist/cli/commands/index.js +0 -241
- package/dist/cli/commands/init.js +0 -18
- package/dist/cli/commands/mcp.js +0 -173
- package/dist/cli/commands/memory.js +0 -87
- package/dist/cli/commands/models.js +0 -21
- package/dist/cli/commands/permission.js +0 -12
- package/dist/cli/commands/rules.js +0 -33
- package/dist/cli/commands/sandbox.js +0 -13
- package/dist/cli/commands/serve.js +0 -9
- package/dist/cli/commands/setup.js +0 -4
- package/dist/cli/commands/shared.js +0 -16
- package/dist/cli/commands/skills.js +0 -119
- package/dist/cli/commands/think.js +0 -12
- package/dist/cli/commands/update.js +0 -7
- package/dist/cli/commands/write-config.js +0 -52
- package/dist/cli/errors.js +0 -36
- package/dist/cli/exec-prompt.js +0 -26
- package/dist/cli/option-helpers.js +0 -65
- package/dist/cli/program.js +0 -217
- package/dist/clipboard.js +0 -142
- package/dist/code-mode.js +0 -184
- package/dist/compaction.js +0 -644
- package/dist/config.js +0 -470
- package/dist/confirm.js +0 -172
- package/dist/context-window.js +0 -246
- package/dist/doom-loop.js +0 -278
- package/dist/fetch-timeout.js +0 -152
- package/dist/http-approvals.js +0 -60
- package/dist/http.js +0 -119
- package/dist/instructions.js +0 -196
- package/dist/logger.js +0 -95
- package/dist/markdown.js +0 -259
- package/dist/mcp.js +0 -1036
- package/dist/memory-cli.js +0 -33
- package/dist/memory.js +0 -295
- package/dist/model-catalog.js +0 -285
- package/dist/output.js +0 -76
- package/dist/paste-handler.js +0 -41
- package/dist/permission-cli.js +0 -40
- package/dist/plugins.js +0 -131
- package/dist/pricing.js +0 -119
- package/dist/provider.js +0 -63
- package/dist/question-format.js +0 -60
- package/dist/reasoning-stream.js +0 -158
- package/dist/sandbox-cli.js +0 -79
- package/dist/sandbox.js +0 -403
- package/dist/save-throttle.js +0 -45
- package/dist/scope.js +0 -23
- package/dist/serve/common.js +0 -425
- package/dist/serve/routes-chat.js +0 -367
- package/dist/serve/routes-mcp.js +0 -212
- package/dist/serve/routes-memory.js +0 -95
- package/dist/serve/routes-meta.js +0 -233
- package/dist/serve/routes-sessions.js +0 -61
- package/dist/serve/routes-skills.js +0 -70
- package/dist/serve.js +0 -138
- package/dist/sessions.js +0 -265
- package/dist/skills.js +0 -599
- package/dist/structured-output.js +0 -29
- package/dist/synthetic.js +0 -7
- package/dist/think-cli.js +0 -36
- package/dist/thinking-wire.js +0 -228
- package/dist/thinking.js +0 -142
- package/dist/title-gen.js +0 -55
- package/dist/token-display.js +0 -39
- package/dist/tool-display.js +0 -178
- package/dist/tool-output.js +0 -126
- package/dist/tools/apply_patch.js +0 -265
- package/dist/tools/atomic-file.js +0 -35
- package/dist/tools/backend.js +0 -61
- package/dist/tools/bash.js +0 -231
- package/dist/tools/code_search.js +0 -98
- package/dist/tools/edit.js +0 -68
- package/dist/tools/explore.js +0 -232
- package/dist/tools/glob.js +0 -40
- package/dist/tools/grep.js +0 -174
- package/dist/tools/index.js +0 -34
- package/dist/tools/question.js +0 -54
- package/dist/tools/read.js +0 -109
- package/dist/tools/search-searxng.js +0 -223
- package/dist/tools/search-serper.js +0 -189
- package/dist/tools/task.js +0 -165
- package/dist/tools/todo.js +0 -213
- package/dist/tools/web_fetch.js +0 -195
- package/dist/tools/web_search.js +0 -228
- package/dist/tools/write.js +0 -55
- package/dist/tui/App.js +0 -126
- package/dist/tui/ConfirmBar.js +0 -66
- package/dist/tui/InputBar.js +0 -616
- package/dist/tui/MessageList.js +0 -718
- package/dist/tui/ModelPicker.js +0 -113
- package/dist/tui/QuestionBar.js +0 -136
- package/dist/tui/SessionPicker.js +0 -79
- package/dist/tui/Spinner.js +0 -6
- package/dist/tui/StatusBar.js +0 -22
- package/dist/tui/ThinkPicker.js +0 -77
- package/dist/tui/agent-runner.js +0 -223
- package/dist/tui/bracketed-paste.js +0 -37
- package/dist/tui/caret-pos.js +0 -179
- package/dist/tui/caret.js +0 -69
- package/dist/tui/click-count.js +0 -13
- package/dist/tui/diff-view.js +0 -61
- package/dist/tui/drag-state.js +0 -49
- package/dist/tui/hydrate.js +0 -129
- package/dist/tui/index.js +0 -258
- package/dist/tui/input-history.js +0 -125
- package/dist/tui/layout.js +0 -105
- package/dist/tui/mouse.js +0 -46
- package/dist/tui/overlay-input.js +0 -12
- package/dist/tui/paste-draft.js +0 -173
- package/dist/tui/prompt-queue.js +0 -24
- package/dist/tui/selection.js +0 -232
- package/dist/tui/session-switch.js +0 -28
- package/dist/tui/slash-commands.js +0 -123
- package/dist/tui/slash-handler.js +0 -589
- package/dist/tui/text-width.js +0 -113
- package/dist/tui/theme.js +0 -12
- package/dist/tui/token-info.js +0 -7
- package/dist/tui/tool-children.js +0 -19
- package/dist/tui/types.js +0 -1
- package/dist/tui/undo-stack.js +0 -14
- package/dist/tui/use-sgr-mouse.js +0 -29
- package/dist/tui-chat.js +0 -456
- package/dist/updater.js +0 -116
- package/dist/xml-search.js +0 -194
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
- package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
package/dist/tool-output.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/backend.js
DELETED
|
@@ -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
|
-
}
|