@letrquan/book 0.2.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/CHANGELOG.md +2504 -0
- package/LICENSE +123 -0
- package/README.md +1383 -0
- package/dist/app-MOXXWEOY.js +18945 -0
- package/dist/app-MOXXWEOY.js.map +1 -0
- package/dist/chunk-24AXE6SP.js +12 -0
- package/dist/chunk-24AXE6SP.js.map +1 -0
- package/dist/chunk-3U5IBM24.js +213 -0
- package/dist/chunk-3U5IBM24.js.map +1 -0
- package/dist/chunk-574C6MQR.js +743 -0
- package/dist/chunk-574C6MQR.js.map +1 -0
- package/dist/chunk-5GDZ22YP.js +449 -0
- package/dist/chunk-5GDZ22YP.js.map +1 -0
- package/dist/chunk-5KLVH3PY.js +167 -0
- package/dist/chunk-5KLVH3PY.js.map +1 -0
- package/dist/chunk-5RDRAO4B.js +92 -0
- package/dist/chunk-5RDRAO4B.js.map +1 -0
- package/dist/chunk-7N4J557C.js +10 -0
- package/dist/chunk-7N4J557C.js.map +1 -0
- package/dist/chunk-A7JLBK2Y.js +3506 -0
- package/dist/chunk-A7JLBK2Y.js.map +1 -0
- package/dist/chunk-ANSJPYGI.js +84 -0
- package/dist/chunk-ANSJPYGI.js.map +1 -0
- package/dist/chunk-BTZZUCLI.js +264 -0
- package/dist/chunk-BTZZUCLI.js.map +1 -0
- package/dist/chunk-C7D4NJ2D.js +217 -0
- package/dist/chunk-C7D4NJ2D.js.map +1 -0
- package/dist/chunk-CAGOB2N4.js +208 -0
- package/dist/chunk-CAGOB2N4.js.map +1 -0
- package/dist/chunk-DGMQEI6Y.js +98 -0
- package/dist/chunk-DGMQEI6Y.js.map +1 -0
- package/dist/chunk-E75IPST5.js +289 -0
- package/dist/chunk-E75IPST5.js.map +1 -0
- package/dist/chunk-EYGD6X2T.js +180 -0
- package/dist/chunk-EYGD6X2T.js.map +1 -0
- package/dist/chunk-GTVB4HEM.js +765 -0
- package/dist/chunk-GTVB4HEM.js.map +1 -0
- package/dist/chunk-J7MIZ5CA.js +1907 -0
- package/dist/chunk-J7MIZ5CA.js.map +1 -0
- package/dist/chunk-KWHQ2XXL.js +626 -0
- package/dist/chunk-KWHQ2XXL.js.map +1 -0
- package/dist/chunk-L6LK2QIQ.js +657 -0
- package/dist/chunk-L6LK2QIQ.js.map +1 -0
- package/dist/chunk-MAQXCUR4.js +1007 -0
- package/dist/chunk-MAQXCUR4.js.map +1 -0
- package/dist/chunk-S4XL7HOM.js +402 -0
- package/dist/chunk-S4XL7HOM.js.map +1 -0
- package/dist/chunk-URLQAXTR.js +60 -0
- package/dist/chunk-URLQAXTR.js.map +1 -0
- package/dist/chunk-V4BOYR52.js +3504 -0
- package/dist/chunk-V4BOYR52.js.map +1 -0
- package/dist/chunk-VZQXX3WW.js +210 -0
- package/dist/chunk-VZQXX3WW.js.map +1 -0
- package/dist/chunk-WKBO5R4O.js +34 -0
- package/dist/chunk-WKBO5R4O.js.map +1 -0
- package/dist/chunk-XIFFTATU.js +15753 -0
- package/dist/chunk-XIFFTATU.js.map +1 -0
- package/dist/command-approvals-6Y57VCWZ.js +28 -0
- package/dist/command-approvals-6Y57VCWZ.js.map +1 -0
- package/dist/context-ZC5IFEAX.js +25 -0
- package/dist/context-ZC5IFEAX.js.map +1 -0
- package/dist/hook-approvals-CTUFKEJT.js +22 -0
- package/dist/hook-approvals-CTUFKEJT.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2582 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive-assets-CCAAWVKH.js +31 -0
- package/dist/interactive-assets-CCAAWVKH.js.map +1 -0
- package/dist/job-runner.d.ts +2 -0
- package/dist/job-runner.js +235 -0
- package/dist/job-runner.js.map +1 -0
- package/dist/loader-4IDS2RIE.js +15 -0
- package/dist/loader-4IDS2RIE.js.map +1 -0
- package/dist/mcp-approvals-WNPH65BK.js +18 -0
- package/dist/mcp-approvals-WNPH65BK.js.map +1 -0
- package/dist/mcp-config-YIJIES2C.js +16 -0
- package/dist/mcp-config-YIJIES2C.js.map +1 -0
- package/dist/model-window-store-YFVNXCIP.js +27 -0
- package/dist/model-window-store-YFVNXCIP.js.map +1 -0
- package/dist/openai-compatible-RHZUBHV6.js +10 -0
- package/dist/openai-compatible-RHZUBHV6.js.map +1 -0
- package/dist/permission-approvals-76PVREFD.js +14 -0
- package/dist/permission-approvals-76PVREFD.js.map +1 -0
- package/dist/permissions-CHCKTFQQ.js +28 -0
- package/dist/permissions-CHCKTFQQ.js.map +1 -0
- package/dist/sandbox-CNVAQDA7.js +23 -0
- package/dist/sandbox-CNVAQDA7.js.map +1 -0
- package/dist/scrollback-LUKLIBVI.js +136 -0
- package/dist/scrollback-LUKLIBVI.js.map +1 -0
- package/dist/sdk.d.ts +4574 -0
- package/dist/sdk.js +665 -0
- package/dist/sdk.js.map +1 -0
- package/dist/settings-loader-M2QP5TX2.js +21 -0
- package/dist/settings-loader-M2QP5TX2.js.map +1 -0
- package/dist/settings-removed-JWWPT4WA.js +19 -0
- package/dist/settings-removed-JWWPT4WA.js.map +1 -0
- package/dist/shell-selection-V6QAYXPX.js +17 -0
- package/dist/shell-selection-V6QAYXPX.js.map +1 -0
- package/package.json +143 -0
- package/patches/ink+6.8.0.patch +13 -0
- package/scripts/apply-ink-patch.mjs +29 -0
- package/scripts/ink-patch.mjs +59 -0
- package/scripts/verify-ink-patch.mjs +11 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalToolName,
|
|
3
|
+
getPrimaryArg,
|
|
4
|
+
observeFile,
|
|
5
|
+
requireFreshObservation,
|
|
6
|
+
toolFailure,
|
|
7
|
+
toolSuccess
|
|
8
|
+
} from "./chunk-5GDZ22YP.js";
|
|
9
|
+
import {
|
|
10
|
+
globToRegex,
|
|
11
|
+
sandboxCoverage
|
|
12
|
+
} from "./chunk-EYGD6X2T.js";
|
|
13
|
+
|
|
14
|
+
// src/async.ts
|
|
15
|
+
function throwIfAborted(signal) {
|
|
16
|
+
if (!signal?.aborted) return;
|
|
17
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
18
|
+
throw new Error(typeof signal.reason === "string" ? signal.reason : "Operation cancelled");
|
|
19
|
+
}
|
|
20
|
+
function onAbort(signal, handler) {
|
|
21
|
+
if (!signal) return () => {
|
|
22
|
+
};
|
|
23
|
+
if (signal.aborted) {
|
|
24
|
+
handler();
|
|
25
|
+
return () => {
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
29
|
+
return () => signal.removeEventListener("abort", handler);
|
|
30
|
+
}
|
|
31
|
+
async function delay(ms, signal) {
|
|
32
|
+
if (ms <= 0 || signal?.aborted) return;
|
|
33
|
+
await new Promise((resolve2) => {
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
unsubscribe();
|
|
36
|
+
resolve2();
|
|
37
|
+
}, ms);
|
|
38
|
+
const unsubscribe = onAbort(signal, () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve2();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function yieldToEventLoop(signal) {
|
|
45
|
+
throwIfAborted(signal);
|
|
46
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/tools/diff.ts
|
|
51
|
+
function splitLines(s) {
|
|
52
|
+
const lines = s.split("\n");
|
|
53
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
var DIFF_YIELD_CELLS = 32768;
|
|
57
|
+
var DIFF_YIELD_ITEMS = 1024;
|
|
58
|
+
function commonAffixes(a, b) {
|
|
59
|
+
const max = Math.min(a.length, b.length);
|
|
60
|
+
let prefix = 0;
|
|
61
|
+
while (prefix < max && a[prefix] === b[prefix]) prefix++;
|
|
62
|
+
let suffix = 0;
|
|
63
|
+
while (suffix < max - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) suffix++;
|
|
64
|
+
return { prefix, suffix };
|
|
65
|
+
}
|
|
66
|
+
function contextOps(a, from, count, bOffset) {
|
|
67
|
+
const ops = [];
|
|
68
|
+
for (let index = 0; index < count; index++) {
|
|
69
|
+
const ai = from + index;
|
|
70
|
+
ops.push({ kind: "ctx", text: a[ai], ai, bi: ai + bOffset });
|
|
71
|
+
}
|
|
72
|
+
return ops;
|
|
73
|
+
}
|
|
74
|
+
function editScript(a, b, dp, offset) {
|
|
75
|
+
const n = a.length;
|
|
76
|
+
const m = b.length;
|
|
77
|
+
const ops = [];
|
|
78
|
+
let i = 0;
|
|
79
|
+
let j = 0;
|
|
80
|
+
while (i < n && j < m) {
|
|
81
|
+
if (a[i] === b[j]) {
|
|
82
|
+
ops.push({ kind: "ctx", text: a[i], ai: offset + i, bi: offset + j });
|
|
83
|
+
i++;
|
|
84
|
+
j++;
|
|
85
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
86
|
+
ops.push({ kind: "del", text: a[i], ai: offset + i, bi: offset + j });
|
|
87
|
+
i++;
|
|
88
|
+
} else {
|
|
89
|
+
ops.push({ kind: "add", text: b[j], ai: offset + i, bi: offset + j });
|
|
90
|
+
j++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
while (i < n) {
|
|
94
|
+
ops.push({ kind: "del", text: a[i], ai: offset + i, bi: offset + j });
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
while (j < m) {
|
|
98
|
+
ops.push({ kind: "add", text: b[j], ai: offset + i, bi: offset + j });
|
|
99
|
+
j++;
|
|
100
|
+
}
|
|
101
|
+
return ops;
|
|
102
|
+
}
|
|
103
|
+
function hunkBounds(ops, context) {
|
|
104
|
+
const bounds = [];
|
|
105
|
+
let k = 0;
|
|
106
|
+
while (k < ops.length) {
|
|
107
|
+
if (ops[k].kind === "ctx") {
|
|
108
|
+
k++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const start = k;
|
|
112
|
+
let ctxBefore = 0;
|
|
113
|
+
let s = start - 1;
|
|
114
|
+
while (s >= 0 && ops[s].kind === "ctx" && ctxBefore < context) {
|
|
115
|
+
s--;
|
|
116
|
+
ctxBefore++;
|
|
117
|
+
}
|
|
118
|
+
let e = start;
|
|
119
|
+
while (e < ops.length && ops[e].kind !== "ctx") e++;
|
|
120
|
+
let ctxAfter = 0;
|
|
121
|
+
while (e < ops.length && ops[e].kind === "ctx" && ctxAfter < context) {
|
|
122
|
+
e++;
|
|
123
|
+
ctxAfter++;
|
|
124
|
+
}
|
|
125
|
+
bounds.push([s + 1, e]);
|
|
126
|
+
k = e;
|
|
127
|
+
}
|
|
128
|
+
return bounds;
|
|
129
|
+
}
|
|
130
|
+
function hunkFrom(ops, start, end) {
|
|
131
|
+
const hunkOps = ops.slice(start, end);
|
|
132
|
+
return {
|
|
133
|
+
oldStart: (hunkOps[0]?.ai ?? 0) + 1,
|
|
134
|
+
newStart: (hunkOps[0]?.bi ?? 0) + 1,
|
|
135
|
+
lines: hunkOps.map((o) => ({ kind: o.kind, text: o.text }))
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function lineDiffAsync(oldText, newText, context = 3, signal) {
|
|
139
|
+
const a = splitLines(oldText);
|
|
140
|
+
const b = splitLines(newText);
|
|
141
|
+
const { prefix, suffix } = commonAffixes(a, b);
|
|
142
|
+
const midA = a.slice(prefix, a.length - suffix);
|
|
143
|
+
const midB = b.slice(prefix, b.length - suffix);
|
|
144
|
+
const n = midA.length;
|
|
145
|
+
const m = midB.length;
|
|
146
|
+
const dp = [];
|
|
147
|
+
for (let row = 0; row <= n; row++) {
|
|
148
|
+
dp.push(new Array(m + 1).fill(0));
|
|
149
|
+
if (row > 0 && row % 128 === 0) await yieldToEventLoop(signal);
|
|
150
|
+
}
|
|
151
|
+
let cellsUntilYield = DIFF_YIELD_CELLS;
|
|
152
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
153
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
154
|
+
dp[i][j] = midA[i] === midB[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
155
|
+
cellsUntilYield--;
|
|
156
|
+
if (cellsUntilYield === 0) {
|
|
157
|
+
cellsUntilYield = DIFF_YIELD_CELLS;
|
|
158
|
+
await yieldToEventLoop(signal);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throwIfAborted(signal);
|
|
163
|
+
const ops = [
|
|
164
|
+
...contextOps(a, 0, prefix, 0),
|
|
165
|
+
...editScript(midA, midB, dp, prefix),
|
|
166
|
+
...contextOps(a, a.length - suffix, suffix, b.length - a.length)
|
|
167
|
+
];
|
|
168
|
+
if (ops.length > DIFF_YIELD_ITEMS) await yieldToEventLoop(signal);
|
|
169
|
+
const hunks = [];
|
|
170
|
+
for (const [start, end] of hunkBounds(ops, context)) {
|
|
171
|
+
hunks.push(hunkFrom(ops, start, end));
|
|
172
|
+
await yieldToEventLoop(signal);
|
|
173
|
+
}
|
|
174
|
+
throwIfAborted(signal);
|
|
175
|
+
return hunks;
|
|
176
|
+
}
|
|
177
|
+
function diffStatsFromHunks(hunks) {
|
|
178
|
+
let addedLines = 0;
|
|
179
|
+
let removedLines = 0;
|
|
180
|
+
for (const hunk of hunks) {
|
|
181
|
+
for (const line of hunk.lines) {
|
|
182
|
+
if (line.kind === "add") addedLines++;
|
|
183
|
+
if (line.kind === "del") removedLines++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { addedLines, removedLines };
|
|
187
|
+
}
|
|
188
|
+
async function renderDiffWithStatsAsync(oldText, newText, context = 3, signal) {
|
|
189
|
+
const hunks = await lineDiffAsync(oldText, newText, context, signal);
|
|
190
|
+
if (hunks.length === 0) return { diff: "", stats: { addedLines: 0, removedLines: 0 } };
|
|
191
|
+
const out = [];
|
|
192
|
+
for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
|
|
193
|
+
const hunk = hunks[hunkIndex];
|
|
194
|
+
out.push(`@@ -${hunk.oldStart} +${hunk.newStart} @@`);
|
|
195
|
+
for (let lineIndex = 0; lineIndex < hunk.lines.length; lineIndex++) {
|
|
196
|
+
const line = hunk.lines[lineIndex];
|
|
197
|
+
const prefix = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
198
|
+
out.push(prefix + line.text);
|
|
199
|
+
if (lineIndex > 0 && lineIndex % DIFF_YIELD_ITEMS === 0) await yieldToEventLoop(signal);
|
|
200
|
+
}
|
|
201
|
+
if (hunkIndex > 0 && hunkIndex % 64 === 0) await yieldToEventLoop(signal);
|
|
202
|
+
}
|
|
203
|
+
return { diff: out.join("\n"), stats: diffStatsFromHunks(hunks) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/tools/path-utils.ts
|
|
207
|
+
import { existsSync, realpathSync } from "fs";
|
|
208
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
209
|
+
function isOutside(root, candidate) {
|
|
210
|
+
const rel = relative(root, candidate);
|
|
211
|
+
return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
212
|
+
}
|
|
213
|
+
function nearestExistingPath(inputPath) {
|
|
214
|
+
let candidate = inputPath;
|
|
215
|
+
while (!existsSync(candidate)) {
|
|
216
|
+
const parent = dirname(candidate);
|
|
217
|
+
if (parent === candidate) return null;
|
|
218
|
+
candidate = parent;
|
|
219
|
+
}
|
|
220
|
+
return candidate;
|
|
221
|
+
}
|
|
222
|
+
function resolveWorkspacePath(workspaceRoot, inputPath) {
|
|
223
|
+
const lexicalRoot = resolve(workspaceRoot);
|
|
224
|
+
const filePath = resolve(isAbsolute(inputPath) ? inputPath : resolve(lexicalRoot, inputPath));
|
|
225
|
+
if (isOutside(lexicalRoot, filePath)) return null;
|
|
226
|
+
const existingPath = nearestExistingPath(filePath);
|
|
227
|
+
if (!existingPath) return null;
|
|
228
|
+
let realRoot;
|
|
229
|
+
let realExisting;
|
|
230
|
+
try {
|
|
231
|
+
realRoot = realpathSync.native(lexicalRoot);
|
|
232
|
+
realExisting = realpathSync.native(existingPath);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (isOutside(realRoot, realExisting)) return null;
|
|
237
|
+
const canonicalPath = resolve(realExisting, relative(existingPath, filePath));
|
|
238
|
+
if (isOutside(realRoot, canonicalPath)) return null;
|
|
239
|
+
const rel = relative(lexicalRoot, filePath);
|
|
240
|
+
return { filePath, canonicalPath, relativePath: rel.replace(/\\/g, "/") };
|
|
241
|
+
}
|
|
242
|
+
function pathOutsideWorkspaceResult(inputPath) {
|
|
243
|
+
return toolFailure(`Path outside workspace: ${inputPath}`, {
|
|
244
|
+
code: "path_outside_workspace"
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/tools/mutation.ts
|
|
249
|
+
import { randomUUID } from "crypto";
|
|
250
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
251
|
+
import { dirname as dirname2 } from "path";
|
|
252
|
+
var pathLocks = /* @__PURE__ */ new Map();
|
|
253
|
+
function looksBinary(bytes) {
|
|
254
|
+
const sample = bytes.subarray(0, 8192);
|
|
255
|
+
if (sample.includes(0)) return true;
|
|
256
|
+
let controls = 0;
|
|
257
|
+
for (const byte of sample) {
|
|
258
|
+
if (byte < 7 || byte > 13 && byte < 32) controls++;
|
|
259
|
+
}
|
|
260
|
+
return sample.length > 0 && controls / sample.length > 0.1;
|
|
261
|
+
}
|
|
262
|
+
async function readTextSnapshot(filePath, allowMissing = false) {
|
|
263
|
+
let bytes;
|
|
264
|
+
try {
|
|
265
|
+
bytes = await readFile(filePath);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (allowMissing && error.code === "ENOENT") {
|
|
268
|
+
return {
|
|
269
|
+
exists: false,
|
|
270
|
+
bytes: Buffer.alloc(0),
|
|
271
|
+
text: "",
|
|
272
|
+
bom: false,
|
|
273
|
+
lineEnding: "lf",
|
|
274
|
+
mixedLineEndings: false,
|
|
275
|
+
binary: false,
|
|
276
|
+
mode: void 0
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
|
|
282
|
+
const body = bytes.subarray(bom ? 3 : 0);
|
|
283
|
+
let binary = looksBinary(bytes);
|
|
284
|
+
let decoded;
|
|
285
|
+
try {
|
|
286
|
+
decoded = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
287
|
+
} catch {
|
|
288
|
+
binary = true;
|
|
289
|
+
decoded = body.toString("utf8");
|
|
290
|
+
}
|
|
291
|
+
const crlfCount = (decoded.match(/\r\n/g) ?? []).length;
|
|
292
|
+
const bareLfCount = (decoded.replace(/\r\n/g, "").match(/\n/g) ?? []).length;
|
|
293
|
+
const bareCrCount = (decoded.replace(/\r\n/g, "").match(/\r/g) ?? []).length;
|
|
294
|
+
const mixedLineEndings = crlfCount > 0 && bareLfCount > 0 || bareCrCount > 0;
|
|
295
|
+
const mode = (await stat(filePath)).mode & 511;
|
|
296
|
+
return {
|
|
297
|
+
exists: true,
|
|
298
|
+
bytes,
|
|
299
|
+
text: decoded.replace(/\r\n/g, "\n"),
|
|
300
|
+
bom,
|
|
301
|
+
lineEnding: crlfCount > bareLfCount ? "crlf" : "lf",
|
|
302
|
+
mixedLineEndings,
|
|
303
|
+
binary,
|
|
304
|
+
mode
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function restoreTextEncoding(text, snapshot) {
|
|
308
|
+
const body = snapshot.lineEnding === "crlf" ? text.replace(/\n/g, "\r\n") : text;
|
|
309
|
+
return Buffer.from(`${snapshot.bom ? "\uFEFF" : ""}${body}`, "utf8");
|
|
310
|
+
}
|
|
311
|
+
async function writeFileAtomically(filePath, bytes, signal, mode) {
|
|
312
|
+
throwIfAborted(signal);
|
|
313
|
+
await mkdir(dirname2(filePath), { recursive: true });
|
|
314
|
+
const temporary = `${filePath}.book-tmp-${randomUUID()}`;
|
|
315
|
+
try {
|
|
316
|
+
await writeFile(temporary, bytes, { flag: "wx", mode });
|
|
317
|
+
throwIfAborted(signal);
|
|
318
|
+
await rename(temporary, filePath);
|
|
319
|
+
} finally {
|
|
320
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function removeFileAtomically(filePath, signal) {
|
|
324
|
+
throwIfAborted(signal);
|
|
325
|
+
await rm(filePath, { force: false });
|
|
326
|
+
}
|
|
327
|
+
async function acquirePath(path) {
|
|
328
|
+
const previous = pathLocks.get(path) ?? Promise.resolve();
|
|
329
|
+
let release;
|
|
330
|
+
const current = new Promise((resolve2) => {
|
|
331
|
+
release = resolve2;
|
|
332
|
+
});
|
|
333
|
+
const queued = previous.then(() => current);
|
|
334
|
+
pathLocks.set(path, queued);
|
|
335
|
+
await previous;
|
|
336
|
+
return () => {
|
|
337
|
+
release();
|
|
338
|
+
if (pathLocks.get(path) === queued) pathLocks.delete(path);
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async function withMutationLocks(paths, operation) {
|
|
342
|
+
const releases = [];
|
|
343
|
+
try {
|
|
344
|
+
for (const path of [...new Set(paths)].sort()) releases.push(await acquirePath(path));
|
|
345
|
+
return await operation();
|
|
346
|
+
} finally {
|
|
347
|
+
for (const release of releases.reverse()) release();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/tools/patch.ts
|
|
352
|
+
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
353
|
+
var MAX_FILES = 100;
|
|
354
|
+
var MAX_HUNKS = 1e3;
|
|
355
|
+
function invalid(message, details) {
|
|
356
|
+
return toolFailure(message, {
|
|
357
|
+
code: "invalid_patch_syntax",
|
|
358
|
+
remediation: "Regenerate a Codex-style patch with Begin/End markers and valid file hunks.",
|
|
359
|
+
details
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function parseHunkHeader(line) {
|
|
363
|
+
const match = line.match(/^@@(?: -(\d+)(?:,(\d+))?)?(?: \+(\d+)(?:,(\d+))?)?(?: @@.*)?$/);
|
|
364
|
+
if (!match) throw new Error(`Invalid hunk header: ${line}`);
|
|
365
|
+
return { oldStart: match[1] ? Number(match[1]) : void 0 };
|
|
366
|
+
}
|
|
367
|
+
function parsePatchUnsafe(patch) {
|
|
368
|
+
if (typeof patch !== "string" || patch.length === 0)
|
|
369
|
+
return invalid("patch must be a non-empty string");
|
|
370
|
+
const bytes = Buffer.byteLength(patch, "utf8");
|
|
371
|
+
if (bytes > MAX_PATCH_BYTES)
|
|
372
|
+
return toolFailure(`Patch input exceeds ${MAX_PATCH_BYTES} bytes`, {
|
|
373
|
+
code: "patch_limit_exceeded",
|
|
374
|
+
remediation: "Split the change into smaller patches.",
|
|
375
|
+
details: { limit: MAX_PATCH_BYTES, bytes }
|
|
376
|
+
});
|
|
377
|
+
const lines = patch.replace(/\r\n/g, "\n").split("\n");
|
|
378
|
+
if (lines.at(-1) === "") lines.pop();
|
|
379
|
+
if (lines[0] !== "*** Begin Patch" || lines.at(-1) !== "*** End Patch")
|
|
380
|
+
return invalid("Patch must start with *** Begin Patch and end with *** End Patch");
|
|
381
|
+
const operations = [];
|
|
382
|
+
let index = 1;
|
|
383
|
+
let hunkCount = 0;
|
|
384
|
+
while (index < lines.length - 1) {
|
|
385
|
+
const marker = lines[index];
|
|
386
|
+
const update = marker.match(/^\*\*\* Update File: (.+)$/);
|
|
387
|
+
const add = marker.match(/^\*\*\* Add File: (.+)$/);
|
|
388
|
+
const del = marker.match(/^\*\*\* Delete File: (.+)$/);
|
|
389
|
+
if (update) {
|
|
390
|
+
const hunks = [];
|
|
391
|
+
index++;
|
|
392
|
+
while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
|
|
393
|
+
if (!lines[index].startsWith("@@"))
|
|
394
|
+
throw new Error(`Expected @@ hunk header near line ${index + 1}`);
|
|
395
|
+
const header = lines[index];
|
|
396
|
+
const parsedHeader = parseHunkHeader(header);
|
|
397
|
+
index++;
|
|
398
|
+
const hunkLines = [];
|
|
399
|
+
while (index < lines.length - 1 && !lines[index].startsWith("@@") && !lines[index].startsWith("*** ")) {
|
|
400
|
+
const line = lines[index++];
|
|
401
|
+
if (line === "\") {
|
|
402
|
+
const previous = hunkLines.at(-1);
|
|
403
|
+
if (!previous)
|
|
404
|
+
throw new Error(`No-newline marker has no preceding hunk line near line ${index}`);
|
|
405
|
+
previous.noNewline = true;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (line.startsWith(" ")) hunkLines.push({ kind: "context", text: line.slice(1) });
|
|
409
|
+
else if (line.startsWith("-")) hunkLines.push({ kind: "remove", text: line.slice(1) });
|
|
410
|
+
else if (line.startsWith("+")) hunkLines.push({ kind: "add", text: line.slice(1) });
|
|
411
|
+
else throw new Error(`Invalid hunk line near line ${index}`);
|
|
412
|
+
}
|
|
413
|
+
if (hunkLines.length === 0) throw new Error(`Empty hunk near line ${index}`);
|
|
414
|
+
if (!hunkLines.some((line) => line.kind !== "add"))
|
|
415
|
+
throw new Error(`Update hunk must contain context or removed lines near line ${index}`);
|
|
416
|
+
hunks.push({ header, oldStart: parsedHeader.oldStart, lines: hunkLines });
|
|
417
|
+
hunkCount++;
|
|
418
|
+
}
|
|
419
|
+
if (hunks.length === 0) throw new Error(`Update File ${update[1]} has no hunks`);
|
|
420
|
+
operations.push({ kind: "update", path: update[1].trim(), hunks });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (add) {
|
|
424
|
+
index++;
|
|
425
|
+
const content = [];
|
|
426
|
+
while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
|
|
427
|
+
const line = lines[index++];
|
|
428
|
+
if (!line.startsWith("+"))
|
|
429
|
+
throw new Error(`Added file lines must begin with + near line ${index}`);
|
|
430
|
+
content.push(line.slice(1));
|
|
431
|
+
}
|
|
432
|
+
operations.push({ kind: "add", path: add[1].trim(), lines: content });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (del) {
|
|
436
|
+
index++;
|
|
437
|
+
if (index < lines.length - 1 && !lines[index].startsWith("*** "))
|
|
438
|
+
throw new Error(`Delete File ${del[1]} cannot contain patch lines`);
|
|
439
|
+
operations.push({ kind: "delete", path: del[1].trim() });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`Expected a file operation near line ${index + 1}`);
|
|
443
|
+
}
|
|
444
|
+
if (operations.length === 0) return invalid("Patch contains no file operations");
|
|
445
|
+
if (operations.length > MAX_FILES || hunkCount > MAX_HUNKS)
|
|
446
|
+
return toolFailure(`Patch exceeds limits (${MAX_FILES} files, ${MAX_HUNKS} hunks)`, {
|
|
447
|
+
code: "patch_limit_exceeded",
|
|
448
|
+
remediation: "Split the change into smaller patches.",
|
|
449
|
+
details: { files: operations.length, hunks: hunkCount }
|
|
450
|
+
});
|
|
451
|
+
return { operations, bytes };
|
|
452
|
+
}
|
|
453
|
+
function parsePatch(patch) {
|
|
454
|
+
try {
|
|
455
|
+
return parsePatchUnsafe(patch);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return invalid(error instanceof Error ? error.message : String(error));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function linesOf(text) {
|
|
461
|
+
const lines = text.split("\n");
|
|
462
|
+
if (lines.at(-1) === "") lines.pop();
|
|
463
|
+
return lines;
|
|
464
|
+
}
|
|
465
|
+
function findUniqueSequence(haystack, needle) {
|
|
466
|
+
if (needle.length === 0) return { index: 0, count: 0 };
|
|
467
|
+
let index = -1;
|
|
468
|
+
let count = 0;
|
|
469
|
+
for (let start = 0; start <= haystack.length - needle.length; start++) {
|
|
470
|
+
if (needle.every((line, offset) => haystack[start + offset] === line)) {
|
|
471
|
+
index = start;
|
|
472
|
+
count++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return { index, count };
|
|
476
|
+
}
|
|
477
|
+
function candidateLine(haystack, line) {
|
|
478
|
+
const matches = haystack.reduce((found, value, index) => {
|
|
479
|
+
if (value === line) found.push(index + 1);
|
|
480
|
+
return found;
|
|
481
|
+
}, []);
|
|
482
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
483
|
+
}
|
|
484
|
+
function applyHunks(text, hunks) {
|
|
485
|
+
let lines = linesOf(text);
|
|
486
|
+
let trailingNewline = text.endsWith("\n");
|
|
487
|
+
for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
|
|
488
|
+
const hunk = hunks[hunkIndex];
|
|
489
|
+
const oldLines = hunk.lines.filter((line) => line.kind !== "add").map((line) => line.text);
|
|
490
|
+
const newLines = hunk.lines.filter((line) => line.kind !== "remove").map((line) => line.text);
|
|
491
|
+
const match = findUniqueSequence(lines, oldLines);
|
|
492
|
+
if (match.count === 0)
|
|
493
|
+
return {
|
|
494
|
+
text,
|
|
495
|
+
mismatch: toolFailure(`Hunk ${hunkIndex + 1}: patch context not found`, {
|
|
496
|
+
code: "patch_context_not_found",
|
|
497
|
+
remediation: "Reread the affected range and regenerate the hunk against the latest file contents.",
|
|
498
|
+
details: {
|
|
499
|
+
hunkIndex: hunkIndex + 1,
|
|
500
|
+
header: hunk.header,
|
|
501
|
+
firstContextLine: oldLines[0]?.slice(0, 200),
|
|
502
|
+
closestCandidateLine: oldLines[0] ? candidateLine(lines, oldLines[0]) : void 0,
|
|
503
|
+
suggestedReadRange: hunk.oldStart !== void 0 ? {
|
|
504
|
+
lineStart: Math.max(1, hunk.oldStart - 3),
|
|
505
|
+
lineEnd: hunk.oldStart + oldLines.length + 3
|
|
506
|
+
} : void 0
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
};
|
|
510
|
+
if (match.count > 1)
|
|
511
|
+
return {
|
|
512
|
+
text,
|
|
513
|
+
mismatch: toolFailure(`Hunk ${hunkIndex + 1}: patch context is ambiguous`, {
|
|
514
|
+
code: "ambiguous_patch_context",
|
|
515
|
+
remediation: "Reread a narrower range and include more exact context lines.",
|
|
516
|
+
details: { hunkIndex: hunkIndex + 1, header: hunk.header, matches: match.count }
|
|
517
|
+
})
|
|
518
|
+
};
|
|
519
|
+
const touchesEndOfFile = match.index + oldLines.length === lines.length;
|
|
520
|
+
lines = [
|
|
521
|
+
...lines.slice(0, match.index),
|
|
522
|
+
...newLines,
|
|
523
|
+
...lines.slice(match.index + oldLines.length)
|
|
524
|
+
];
|
|
525
|
+
if (touchesEndOfFile) {
|
|
526
|
+
const oldNoNewline = hunk.lines.some((line) => line.kind !== "add" && line.noNewline);
|
|
527
|
+
const newNoNewline = hunk.lines.some((line) => line.kind !== "remove" && line.noNewline);
|
|
528
|
+
if (newNoNewline) trailingNewline = false;
|
|
529
|
+
else if (oldNoNewline) trailingNewline = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { text: lines.join("\n") + (trailingNewline && lines.length > 0 ? "\n" : "") };
|
|
533
|
+
}
|
|
534
|
+
function mutationFromDiff(relativePath, kind, stats) {
|
|
535
|
+
return {
|
|
536
|
+
kind,
|
|
537
|
+
filePath: relativePath,
|
|
538
|
+
addedLines: stats.addedLines,
|
|
539
|
+
removedLines: stats.removedLines
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function errorCodeFor(error) {
|
|
543
|
+
return error instanceof Error && error.message.includes("changed or disappeared") ? "stale_file_observation" : "filesystem_error";
|
|
544
|
+
}
|
|
545
|
+
async function applyPatch(args, ctx) {
|
|
546
|
+
const parsed = parsePatch(args.patch);
|
|
547
|
+
if (!("operations" in parsed)) return parsed;
|
|
548
|
+
const paths = parsed.operations.map((operation) => operation.path);
|
|
549
|
+
const resolved = parsed.operations.map((operation) => {
|
|
550
|
+
const result = resolveWorkspacePath(ctx.workspaceRoot, operation.path);
|
|
551
|
+
return result ? { operation, ...result } : null;
|
|
552
|
+
});
|
|
553
|
+
if (resolved.some((entry) => !entry))
|
|
554
|
+
return pathOutsideWorkspaceResult(paths[resolved.findIndex((entry) => !entry)]);
|
|
555
|
+
const entries = resolved;
|
|
556
|
+
const canonicalKeys = entries.map(
|
|
557
|
+
(entry) => process.platform === "win32" ? entry.canonicalPath.toLowerCase() : entry.canonicalPath
|
|
558
|
+
);
|
|
559
|
+
if (new Set(canonicalKeys).size !== canonicalKeys.length)
|
|
560
|
+
return toolFailure("A patch may contain only one operation per file", {
|
|
561
|
+
code: "patch_conflict",
|
|
562
|
+
remediation: "Combine related updates into one file operation with multiple hunks."
|
|
563
|
+
});
|
|
564
|
+
return withMutationLocks(
|
|
565
|
+
entries.map((entry) => entry.canonicalPath),
|
|
566
|
+
async () => {
|
|
567
|
+
const staged = [];
|
|
568
|
+
try {
|
|
569
|
+
for (const entry of entries) {
|
|
570
|
+
throwIfAborted(ctx.signal);
|
|
571
|
+
const before = await readTextSnapshot(entry.filePath, entry.operation.kind === "add");
|
|
572
|
+
if (before.binary)
|
|
573
|
+
return toolFailure(`Binary file is unsupported: ${entry.relativePath}`, {
|
|
574
|
+
code: "binary_file_unsupported",
|
|
575
|
+
remediation: "Use a text file mutation or a dedicated binary workflow."
|
|
576
|
+
});
|
|
577
|
+
if (before.mixedLineEndings && entry.operation.kind === "update")
|
|
578
|
+
return toolFailure(
|
|
579
|
+
`Mixed line endings are unsupported for patch updates: ${entry.relativePath}`,
|
|
580
|
+
{
|
|
581
|
+
code: "patch_conflict",
|
|
582
|
+
remediation: "Normalize the file line endings, reread it, and regenerate the patch.",
|
|
583
|
+
details: { filePath: entry.relativePath, lineEnding: "mixed" }
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
const stale = entry.operation.kind === "add" ? void 0 : await requireFreshObservation(ctx, entry.filePath, entry.relativePath);
|
|
587
|
+
if (stale)
|
|
588
|
+
return toolFailure(stale, {
|
|
589
|
+
code: "stale_file_observation",
|
|
590
|
+
remediation: "Call Read before regenerating the patch."
|
|
591
|
+
});
|
|
592
|
+
if (entry.operation.kind === "add") {
|
|
593
|
+
if (before.exists)
|
|
594
|
+
return toolFailure(`File already exists: ${entry.relativePath}`, {
|
|
595
|
+
code: "patch_conflict",
|
|
596
|
+
remediation: "Use Update File for an existing file."
|
|
597
|
+
});
|
|
598
|
+
const afterText = entry.operation.lines.join("\n") + (entry.operation.lines.length > 0 ? "\n" : "");
|
|
599
|
+
const { stats } = await renderDiffWithStatsAsync("", afterText, 3, ctx.signal);
|
|
600
|
+
staged.push({
|
|
601
|
+
operation: entry.operation,
|
|
602
|
+
absolutePath: entry.filePath,
|
|
603
|
+
writePath: entry.canonicalPath,
|
|
604
|
+
relativePath: entry.relativePath,
|
|
605
|
+
before,
|
|
606
|
+
afterText,
|
|
607
|
+
afterBytes: Buffer.from(afterText, "utf8"),
|
|
608
|
+
mutation: mutationFromDiff(entry.relativePath, "create", stats)
|
|
609
|
+
});
|
|
610
|
+
} else if (entry.operation.kind === "delete") {
|
|
611
|
+
if (!before.exists)
|
|
612
|
+
return toolFailure(`File not found: ${entry.relativePath}`, {
|
|
613
|
+
code: "patch_conflict",
|
|
614
|
+
remediation: "Reread the workspace and regenerate the patch."
|
|
615
|
+
});
|
|
616
|
+
staged.push({
|
|
617
|
+
operation: entry.operation,
|
|
618
|
+
absolutePath: entry.filePath,
|
|
619
|
+
writePath: entry.filePath,
|
|
620
|
+
relativePath: entry.relativePath,
|
|
621
|
+
before,
|
|
622
|
+
mutation: {
|
|
623
|
+
kind: "delete",
|
|
624
|
+
filePath: entry.relativePath,
|
|
625
|
+
addedLines: 0,
|
|
626
|
+
removedLines: linesOf(before.text).length
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
} else {
|
|
630
|
+
if (!before.exists)
|
|
631
|
+
return toolFailure(`File not found: ${entry.relativePath}`, {
|
|
632
|
+
code: "patch_context_not_found",
|
|
633
|
+
remediation: "Reread the file and regenerate the patch."
|
|
634
|
+
});
|
|
635
|
+
const applied = applyHunks(before.text, entry.operation.hunks);
|
|
636
|
+
if (applied.mismatch)
|
|
637
|
+
return toolFailure(
|
|
638
|
+
`${entry.relativePath}: ${applied.mismatch.structuredError?.message}`,
|
|
639
|
+
{
|
|
640
|
+
...applied.mismatch.structuredError,
|
|
641
|
+
code: applied.mismatch.structuredError?.code ?? "patch_context_not_found",
|
|
642
|
+
remediation: applied.mismatch.structuredError?.remediation,
|
|
643
|
+
details: {
|
|
644
|
+
...applied.mismatch.structuredError?.details,
|
|
645
|
+
filePath: entry.relativePath,
|
|
646
|
+
lineEnding: before.mixedLineEndings ? "mixed" : before.lineEnding,
|
|
647
|
+
normalizedMatching: true
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
const afterText = applied.text;
|
|
652
|
+
const { stats } = await renderDiffWithStatsAsync(before.text, afterText, 3, ctx.signal);
|
|
653
|
+
staged.push({
|
|
654
|
+
operation: entry.operation,
|
|
655
|
+
absolutePath: entry.filePath,
|
|
656
|
+
writePath: entry.canonicalPath,
|
|
657
|
+
relativePath: entry.relativePath,
|
|
658
|
+
before,
|
|
659
|
+
afterText,
|
|
660
|
+
afterBytes: restoreTextEncoding(afterText, before),
|
|
661
|
+
mutation: mutationFromDiff(entry.relativePath, "update", stats)
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const mutations = staged.map((file) => file.mutation);
|
|
666
|
+
const changedLines = mutations.reduce(
|
|
667
|
+
(total, mutation) => total + mutation.addedLines + mutation.removedLines,
|
|
668
|
+
0
|
|
669
|
+
);
|
|
670
|
+
const contextLines = parsed.operations.reduce(
|
|
671
|
+
(total, operation) => total + (operation.kind === "update" ? operation.hunks.reduce(
|
|
672
|
+
(hunkTotal, hunk) => hunkTotal + hunk.lines.filter((line) => line.kind === "context").length,
|
|
673
|
+
0
|
|
674
|
+
) : 0),
|
|
675
|
+
0
|
|
676
|
+
);
|
|
677
|
+
const changeBudgetWarnings = [];
|
|
678
|
+
if (changedLines > 2e3) changeBudgetWarnings.push("large_change");
|
|
679
|
+
if (changedLines > 100 && changedLines > Math.max(1, contextLines) * 20)
|
|
680
|
+
changeBudgetWarnings.push("low_context_to_change_ratio");
|
|
681
|
+
const diffs = [];
|
|
682
|
+
for (const file of staged) {
|
|
683
|
+
throwIfAborted(ctx.signal);
|
|
684
|
+
const afterText = file.afterText ?? "";
|
|
685
|
+
const { diff } = await renderDiffWithStatsAsync(
|
|
686
|
+
file.before.text,
|
|
687
|
+
afterText,
|
|
688
|
+
3,
|
|
689
|
+
ctx.signal
|
|
690
|
+
);
|
|
691
|
+
if (diff) diffs.push(`--- ${file.relativePath}
|
|
692
|
+
+++ ${file.relativePath}
|
|
693
|
+
${diff}`);
|
|
694
|
+
}
|
|
695
|
+
const committed = [];
|
|
696
|
+
try {
|
|
697
|
+
for (const file of staged) {
|
|
698
|
+
throwIfAborted(ctx.signal);
|
|
699
|
+
committed.push(file);
|
|
700
|
+
if (file.operation.kind === "delete")
|
|
701
|
+
await removeFileAtomically(file.absolutePath, ctx.signal);
|
|
702
|
+
else
|
|
703
|
+
await writeFileAtomically(
|
|
704
|
+
file.writePath,
|
|
705
|
+
file.afterBytes,
|
|
706
|
+
ctx.signal,
|
|
707
|
+
file.before.mode
|
|
708
|
+
);
|
|
709
|
+
const check = await readTextSnapshot(
|
|
710
|
+
file.absolutePath,
|
|
711
|
+
file.operation.kind === "delete"
|
|
712
|
+
);
|
|
713
|
+
if (file.operation.kind === "delete" ? check.exists : !check.exists || !check.bytes.equals(file.afterBytes))
|
|
714
|
+
throw new Error(`Post-write verification failed for ${file.relativePath}`);
|
|
715
|
+
}
|
|
716
|
+
} catch (error) {
|
|
717
|
+
let rollbackFailed = false;
|
|
718
|
+
for (const file of committed.reverse()) {
|
|
719
|
+
try {
|
|
720
|
+
if (file.before.exists)
|
|
721
|
+
await writeFileAtomically(
|
|
722
|
+
file.writePath,
|
|
723
|
+
file.before.bytes,
|
|
724
|
+
void 0,
|
|
725
|
+
file.before.mode
|
|
726
|
+
);
|
|
727
|
+
else await removeFileAtomically(file.absolutePath).catch(() => void 0);
|
|
728
|
+
} catch {
|
|
729
|
+
rollbackFailed = true;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return toolFailure(
|
|
733
|
+
`Patch commit failed${rollbackFailed ? "; rollback also failed" : " and was rolled back"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
734
|
+
{
|
|
735
|
+
code: rollbackFailed ? "patch_rollback_failed" : "filesystem_error",
|
|
736
|
+
remediation: rollbackFailed ? "Inspect the affected files and restore them manually." : "Retry after checking the workspace state."
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const observations = [];
|
|
741
|
+
for (const file of staged) {
|
|
742
|
+
if (file.operation.kind !== "delete")
|
|
743
|
+
observations.push(
|
|
744
|
+
await observeFile(
|
|
745
|
+
ctx,
|
|
746
|
+
file.absolutePath,
|
|
747
|
+
file.operation.kind === "add" ? "create" : "edit"
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const result = toolSuccess(
|
|
752
|
+
diffs.join("\n") || "Patch applied successfully (no textual change)",
|
|
753
|
+
{
|
|
754
|
+
data: {
|
|
755
|
+
files: mutations.map((mutation) => mutation.filePath),
|
|
756
|
+
changedFiles: mutations.length,
|
|
757
|
+
changedLines,
|
|
758
|
+
patchBytes: parsed.bytes,
|
|
759
|
+
changeBudgetWarnings
|
|
760
|
+
},
|
|
761
|
+
artifacts: {
|
|
762
|
+
fileMutation: mutations[0],
|
|
763
|
+
fileMutations: mutations,
|
|
764
|
+
fileObservations: observations
|
|
765
|
+
},
|
|
766
|
+
presentation: {
|
|
767
|
+
kind: "diff",
|
|
768
|
+
summary: `Applied patch to ${mutations.length} ${mutations.length === 1 ? "file" : "files"}`,
|
|
769
|
+
target: mutations.length === 1 ? mutations[0].filePath : `${mutations.length} files`
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
return result;
|
|
774
|
+
} catch (error) {
|
|
775
|
+
const code = errorCodeFor(error);
|
|
776
|
+
return toolFailure(error instanceof Error ? error.message : String(error), {
|
|
777
|
+
code,
|
|
778
|
+
remediation: "Reread the affected file and regenerate the patch."
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
var patchTools = [
|
|
785
|
+
{
|
|
786
|
+
name: "ApplyPatch",
|
|
787
|
+
argumentAliases: { input: "patch" },
|
|
788
|
+
description: "Apply a Codex-style patch with Update File, Add File, or Delete File operations across one or more files atomically. Read the targets first; use exact contextual hunks, and regenerate the patch after a context mismatch instead of resending it unchanged.",
|
|
789
|
+
parameters: {
|
|
790
|
+
type: "object",
|
|
791
|
+
properties: {
|
|
792
|
+
patch: {
|
|
793
|
+
type: "string",
|
|
794
|
+
minLength: 1,
|
|
795
|
+
maxLength: MAX_PATCH_BYTES,
|
|
796
|
+
description: "Patch text beginning with *** Begin Patch and ending with *** End Patch"
|
|
797
|
+
},
|
|
798
|
+
baseObservation: {
|
|
799
|
+
type: "string",
|
|
800
|
+
maxLength: 128,
|
|
801
|
+
description: "Optional short observation identifier for diagnostics; freshness is checked implicitly."
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
required: ["patch"],
|
|
805
|
+
additionalProperties: false
|
|
806
|
+
},
|
|
807
|
+
execute: applyPatch
|
|
808
|
+
}
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
// src/permissions.ts
|
|
812
|
+
function parseRule(rule) {
|
|
813
|
+
const parenIdx = rule.indexOf("(");
|
|
814
|
+
if (parenIdx === -1) {
|
|
815
|
+
return { toolName: rule.trim(), pattern: null };
|
|
816
|
+
}
|
|
817
|
+
const toolName = rule.slice(0, parenIdx).trim();
|
|
818
|
+
const closeIdx = rule.lastIndexOf(")");
|
|
819
|
+
if (closeIdx === -1 || closeIdx <= parenIdx) {
|
|
820
|
+
return { toolName, pattern: null };
|
|
821
|
+
}
|
|
822
|
+
const pattern = rule.slice(parenIdx + 1, closeIdx).trim();
|
|
823
|
+
return { toolName, pattern: pattern.length === 0 ? null : pattern };
|
|
824
|
+
}
|
|
825
|
+
function normalizePathArg(s) {
|
|
826
|
+
if (s.startsWith("./")) return s.slice(2);
|
|
827
|
+
return s;
|
|
828
|
+
}
|
|
829
|
+
function toolNameMatchesRule(ruleToolName, toolName) {
|
|
830
|
+
if (ruleToolName === toolName) return true;
|
|
831
|
+
if (!ruleToolName.startsWith("mcp__")) return false;
|
|
832
|
+
const serverName = ruleToolName.slice("mcp__".length);
|
|
833
|
+
return Boolean(
|
|
834
|
+
serverName && !serverName.includes("__") && toolName.startsWith(`${ruleToolName}__`)
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function ruleMatches(rule, toolName, primaryArg) {
|
|
838
|
+
if (!toolNameMatchesRule(rule.toolName, toolName)) return false;
|
|
839
|
+
if (rule.pattern === null) return true;
|
|
840
|
+
const normalizedArg = normalizePathArg(primaryArg);
|
|
841
|
+
const normalizedPattern = rule.pattern.startsWith("./") ? rule.pattern.slice(2) : rule.pattern;
|
|
842
|
+
return globToRegex(normalizedPattern).test(normalizedArg);
|
|
843
|
+
}
|
|
844
|
+
function permissionRuleForToolCall(call) {
|
|
845
|
+
const toolName = canonicalToolName(call.name);
|
|
846
|
+
const primaryArg = getPrimaryArg(call.arguments);
|
|
847
|
+
if (toolName === "WebSearch") return toolName;
|
|
848
|
+
if (toolName === "WebFetch" && primaryArg) {
|
|
849
|
+
try {
|
|
850
|
+
const url = new URL(primaryArg);
|
|
851
|
+
return `${toolName}(${url.origin}/**)`;
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return primaryArg ? `${toolName}(${primaryArg})` : toolName;
|
|
856
|
+
}
|
|
857
|
+
function permissionResultOf(decision) {
|
|
858
|
+
return typeof decision === "string" ? decision : decision.result;
|
|
859
|
+
}
|
|
860
|
+
function permissionRuleOf(decision) {
|
|
861
|
+
return typeof decision === "string" ? void 0 : decision.rule;
|
|
862
|
+
}
|
|
863
|
+
var RULE_LADDER_MAX = 3;
|
|
864
|
+
var SHELL_CHAINING = /[;&|><`$(){}\n]/;
|
|
865
|
+
function permissionRuleLadder(call) {
|
|
866
|
+
const exact = permissionRuleForToolCall(call);
|
|
867
|
+
if (canonicalToolName(call.name) !== "Bash") return [exact];
|
|
868
|
+
const command = getPrimaryArg(call.arguments).trim();
|
|
869
|
+
if (!command || SHELL_CHAINING.test(command)) return [exact];
|
|
870
|
+
const tokens = command.split(/\s+/);
|
|
871
|
+
const ladder = [exact];
|
|
872
|
+
for (let take = tokens.length - 1; take >= 1 && ladder.length < RULE_LADDER_MAX; take -= 1) {
|
|
873
|
+
const prefix = `Bash(${tokens.slice(0, take).join(" ")} *)`;
|
|
874
|
+
if (!ladder.includes(prefix)) ladder.push(prefix);
|
|
875
|
+
}
|
|
876
|
+
return ladder;
|
|
877
|
+
}
|
|
878
|
+
function permissionRuleMatchesCall(rule, call) {
|
|
879
|
+
const toolName = canonicalToolName(call.name);
|
|
880
|
+
const primaryArg = getPrimaryArg(call.arguments);
|
|
881
|
+
if (toolName === "WebFetch" && primaryArg) {
|
|
882
|
+
try {
|
|
883
|
+
return ruleMatches(parseRule(rule), toolName, new URL(primaryArg).toString());
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return ruleMatches(parseRule(rule), toolName, primaryArg);
|
|
888
|
+
}
|
|
889
|
+
function patchOperations(args) {
|
|
890
|
+
const parsed = parsePatch(args.patch);
|
|
891
|
+
return "operations" in parsed ? parsed.operations : [];
|
|
892
|
+
}
|
|
893
|
+
function ruleMatchesPatch(rule, path) {
|
|
894
|
+
return ruleMatches(rule, rule.toolName, path);
|
|
895
|
+
}
|
|
896
|
+
function patchRuleSupportsOperation(rule, operation) {
|
|
897
|
+
if (rule.toolName === "ApplyPatch") return true;
|
|
898
|
+
if (rule.toolName === "Edit") return operation.kind === "update";
|
|
899
|
+
if (rule.toolName === "Write") return operation.kind === "update" || operation.kind === "add";
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
function compatiblePatchRuleMatches(rule, operations) {
|
|
903
|
+
if (!["ApplyPatch", "Edit", "Write"].includes(rule.toolName)) return false;
|
|
904
|
+
if (rule.toolName === "ApplyPatch" && rule.pattern === null) return true;
|
|
905
|
+
return operations.some(
|
|
906
|
+
(operation) => patchRuleSupportsOperation(rule, operation) && (rule.pattern === null || ruleMatchesPatch(rule, operation.path))
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
function primaryArgForRule(_toolName, args) {
|
|
910
|
+
return getPrimaryArg(args);
|
|
911
|
+
}
|
|
912
|
+
function hasAdjudicationPolicy(settings) {
|
|
913
|
+
return settings.permissions.deny.length > 0 || settings.permissions.ask.length > 0;
|
|
914
|
+
}
|
|
915
|
+
function sandboxAutoAllows(toolName, args, settings, options) {
|
|
916
|
+
if (!settings.sandbox.autoAllowBashIfSandboxed) return false;
|
|
917
|
+
if (canonicalToolName(toolName) !== "Bash") return false;
|
|
918
|
+
if (hasAdjudicationPolicy(settings)) return false;
|
|
919
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
920
|
+
if (!command) return false;
|
|
921
|
+
return sandboxCoverage(command, settings.sandbox, options.sandboxBackendAvailable).sandboxed;
|
|
922
|
+
}
|
|
923
|
+
function evaluatePermission(toolName, args, settings, options = {}) {
|
|
924
|
+
return evaluatePermissionDetail(toolName, args, settings, options).decision;
|
|
925
|
+
}
|
|
926
|
+
function evaluatePermissionDetail(toolName, args, settings, options = {}) {
|
|
927
|
+
const { deny, ask, allow } = settings.permissions;
|
|
928
|
+
if (toolName === "ApplyPatch") {
|
|
929
|
+
const operations = patchOperations(args);
|
|
930
|
+
const compatible = (ruleStr) => {
|
|
931
|
+
const rule = parseRule(ruleStr);
|
|
932
|
+
return rule.toolName === "ApplyPatch" || rule.toolName === "Edit" || rule.toolName === "Write";
|
|
933
|
+
};
|
|
934
|
+
for (const ruleStr of deny) {
|
|
935
|
+
if (!compatible(ruleStr)) continue;
|
|
936
|
+
const rule = parseRule(ruleStr);
|
|
937
|
+
if (compatiblePatchRuleMatches(rule, operations))
|
|
938
|
+
return { decision: "deny", matchedRule: ruleStr, source: "deny" };
|
|
939
|
+
}
|
|
940
|
+
for (const ruleStr of ask) {
|
|
941
|
+
if (!compatible(ruleStr)) continue;
|
|
942
|
+
const rule = parseRule(ruleStr);
|
|
943
|
+
if (compatiblePatchRuleMatches(rule, operations))
|
|
944
|
+
return { decision: "ask", matchedRule: ruleStr, source: "ask" };
|
|
945
|
+
}
|
|
946
|
+
const allowRules = allow.filter(compatible).map(parseRule);
|
|
947
|
+
if (operations.length > 0 && operations.every(
|
|
948
|
+
(operation) => allowRules.some(
|
|
949
|
+
(rule) => patchRuleSupportsOperation(rule, operation) && (rule.pattern === null || ruleMatchesPatch(rule, operation.path))
|
|
950
|
+
)
|
|
951
|
+
)) {
|
|
952
|
+
return { decision: "allow", matchedRule: allow.find(compatible), source: "allow" };
|
|
953
|
+
}
|
|
954
|
+
if (allowRules.some((rule) => rule.toolName === "ApplyPatch" && rule.pattern === null)) {
|
|
955
|
+
return { decision: "allow", matchedRule: allow.find(compatible), source: "allow" };
|
|
956
|
+
}
|
|
957
|
+
return { decision: "ask", source: "default" };
|
|
958
|
+
}
|
|
959
|
+
const call = { id: "permission-evaluation", name: toolName, arguments: args };
|
|
960
|
+
for (const ruleStr of deny) {
|
|
961
|
+
if (permissionRuleMatchesCall(ruleStr, call)) {
|
|
962
|
+
return { decision: "deny", matchedRule: ruleStr, source: "deny" };
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const ruleStr of ask) {
|
|
966
|
+
if (permissionRuleMatchesCall(ruleStr, call)) {
|
|
967
|
+
return { decision: "ask", matchedRule: ruleStr, source: "ask" };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
for (const ruleStr of allow) {
|
|
971
|
+
if (permissionRuleMatchesCall(ruleStr, call)) {
|
|
972
|
+
return { decision: "allow", matchedRule: ruleStr, source: "allow" };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (sandboxAutoAllows(toolName, args, settings, options)) {
|
|
976
|
+
return { decision: "allow", source: "sandbox" };
|
|
977
|
+
}
|
|
978
|
+
return { decision: "ask", source: "default" };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export {
|
|
982
|
+
throwIfAborted,
|
|
983
|
+
onAbort,
|
|
984
|
+
delay,
|
|
985
|
+
yieldToEventLoop,
|
|
986
|
+
renderDiffWithStatsAsync,
|
|
987
|
+
resolveWorkspacePath,
|
|
988
|
+
pathOutsideWorkspaceResult,
|
|
989
|
+
readTextSnapshot,
|
|
990
|
+
restoreTextEncoding,
|
|
991
|
+
writeFileAtomically,
|
|
992
|
+
withMutationLocks,
|
|
993
|
+
parsePatch,
|
|
994
|
+
applyHunks,
|
|
995
|
+
patchTools,
|
|
996
|
+
parseRule,
|
|
997
|
+
permissionRuleForToolCall,
|
|
998
|
+
permissionResultOf,
|
|
999
|
+
permissionRuleOf,
|
|
1000
|
+
permissionRuleLadder,
|
|
1001
|
+
permissionRuleMatchesCall,
|
|
1002
|
+
primaryArgForRule,
|
|
1003
|
+
hasAdjudicationPolicy,
|
|
1004
|
+
evaluatePermission,
|
|
1005
|
+
evaluatePermissionDetail
|
|
1006
|
+
};
|
|
1007
|
+
//# sourceMappingURL=chunk-MAQXCUR4.js.map
|