@letrquan/book 0.3.0 → 0.4.0-beta.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 (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -1,1339 +0,0 @@
1
- import {
2
- globToRegex,
3
- globWalkPlan,
4
- sandboxCoverage
5
- } from "./chunk-JTPOI3KU.js";
6
- import {
7
- containsPath,
8
- pathHoldsHome,
9
- rootHoldsHome
10
- } from "./chunk-525CAZPY.js";
11
- import {
12
- canonicalizePath,
13
- isBookLocalSettingsPath,
14
- isUnderRoot,
15
- pathOutsideWorkspaceResult,
16
- resolveMutationPath,
17
- resolveReadablePathDetail,
18
- resolveWorkspacePath
19
- } from "./chunk-X66I2NQO.js";
20
- import {
21
- canonicalToolName,
22
- getPrimaryArg,
23
- toolFailure,
24
- toolSuccess
25
- } from "./chunk-224FJZLH.js";
26
-
27
- // src/permissions.ts
28
- import { realpathSync } from "fs";
29
- import { isAbsolute, relative as relative2, sep } from "path";
30
-
31
- // src/async.ts
32
- function throwIfAborted(signal) {
33
- if (!signal?.aborted) return;
34
- if (signal.reason instanceof Error) throw signal.reason;
35
- throw new Error(typeof signal.reason === "string" ? signal.reason : "Operation cancelled");
36
- }
37
- function onAbort(signal, handler) {
38
- if (!signal) return () => {
39
- };
40
- if (signal.aborted) {
41
- handler();
42
- return () => {
43
- };
44
- }
45
- signal.addEventListener("abort", handler, { once: true });
46
- return () => signal.removeEventListener("abort", handler);
47
- }
48
- async function delay(ms, signal) {
49
- if (ms <= 0 || signal?.aborted) return;
50
- await new Promise((resolve2) => {
51
- const timer = setTimeout(() => {
52
- unsubscribe();
53
- resolve2();
54
- }, ms);
55
- const unsubscribe = onAbort(signal, () => {
56
- clearTimeout(timer);
57
- resolve2();
58
- });
59
- });
60
- }
61
- async function yieldToEventLoop(signal) {
62
- throwIfAborted(signal);
63
- await new Promise((resolve2) => setImmediate(resolve2));
64
- throwIfAborted(signal);
65
- }
66
-
67
- // src/tools/diff.ts
68
- function splitLines(s) {
69
- const lines = s.split("\n");
70
- if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
71
- return lines;
72
- }
73
- var DIFF_YIELD_CELLS = 32768;
74
- var DIFF_YIELD_ITEMS = 1024;
75
- function commonAffixes(a, b) {
76
- const max = Math.min(a.length, b.length);
77
- let prefix = 0;
78
- while (prefix < max && a[prefix] === b[prefix]) prefix++;
79
- let suffix = 0;
80
- while (suffix < max - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) suffix++;
81
- return { prefix, suffix };
82
- }
83
- function contextOps(a, from, count, bOffset) {
84
- const ops = [];
85
- for (let index = 0; index < count; index++) {
86
- const ai = from + index;
87
- ops.push({ kind: "ctx", text: a[ai], ai, bi: ai + bOffset });
88
- }
89
- return ops;
90
- }
91
- function editScript(a, b, dp, offset) {
92
- const n = a.length;
93
- const m = b.length;
94
- const ops = [];
95
- let i = 0;
96
- let j = 0;
97
- while (i < n && j < m) {
98
- if (a[i] === b[j]) {
99
- ops.push({ kind: "ctx", text: a[i], ai: offset + i, bi: offset + j });
100
- i++;
101
- j++;
102
- } else if (dp[i + 1][j] >= dp[i][j + 1]) {
103
- ops.push({ kind: "del", text: a[i], ai: offset + i, bi: offset + j });
104
- i++;
105
- } else {
106
- ops.push({ kind: "add", text: b[j], ai: offset + i, bi: offset + j });
107
- j++;
108
- }
109
- }
110
- while (i < n) {
111
- ops.push({ kind: "del", text: a[i], ai: offset + i, bi: offset + j });
112
- i++;
113
- }
114
- while (j < m) {
115
- ops.push({ kind: "add", text: b[j], ai: offset + i, bi: offset + j });
116
- j++;
117
- }
118
- return ops;
119
- }
120
- function hunkBounds(ops, context) {
121
- const bounds = [];
122
- let k = 0;
123
- while (k < ops.length) {
124
- if (ops[k].kind === "ctx") {
125
- k++;
126
- continue;
127
- }
128
- const start = k;
129
- let ctxBefore = 0;
130
- let s = start - 1;
131
- while (s >= 0 && ops[s].kind === "ctx" && ctxBefore < context) {
132
- s--;
133
- ctxBefore++;
134
- }
135
- let e = start;
136
- while (e < ops.length && ops[e].kind !== "ctx") e++;
137
- let ctxAfter = 0;
138
- while (e < ops.length && ops[e].kind === "ctx" && ctxAfter < context) {
139
- e++;
140
- ctxAfter++;
141
- }
142
- const previous = bounds.at(-1);
143
- if (previous && s + 1 <= previous[1]) previous[1] = e;
144
- else bounds.push([s + 1, e]);
145
- k = e;
146
- }
147
- return bounds;
148
- }
149
- function hunkFrom(ops, start, end) {
150
- const hunkOps = ops.slice(start, end);
151
- return {
152
- oldStart: (hunkOps[0]?.ai ?? 0) + 1,
153
- newStart: (hunkOps[0]?.bi ?? 0) + 1,
154
- lines: hunkOps.map((o) => ({ kind: o.kind, text: o.text }))
155
- };
156
- }
157
- async function lineDiffAsync(oldText, newText, context = 3, signal) {
158
- const a = splitLines(oldText);
159
- const b = splitLines(newText);
160
- const { prefix, suffix } = commonAffixes(a, b);
161
- const midA = a.slice(prefix, a.length - suffix);
162
- const midB = b.slice(prefix, b.length - suffix);
163
- const n = midA.length;
164
- const m = midB.length;
165
- const dp = [];
166
- for (let row = 0; row <= n; row++) {
167
- dp.push(new Array(m + 1).fill(0));
168
- if (row > 0 && row % 128 === 0) await yieldToEventLoop(signal);
169
- }
170
- let cellsUntilYield = DIFF_YIELD_CELLS;
171
- for (let i = n - 1; i >= 0; i--) {
172
- for (let j = m - 1; j >= 0; j--) {
173
- dp[i][j] = midA[i] === midB[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
174
- cellsUntilYield--;
175
- if (cellsUntilYield === 0) {
176
- cellsUntilYield = DIFF_YIELD_CELLS;
177
- await yieldToEventLoop(signal);
178
- }
179
- }
180
- }
181
- throwIfAborted(signal);
182
- const ops = [
183
- ...contextOps(a, 0, prefix, 0),
184
- ...editScript(midA, midB, dp, prefix),
185
- ...contextOps(a, a.length - suffix, suffix, b.length - a.length)
186
- ];
187
- if (ops.length > DIFF_YIELD_ITEMS) await yieldToEventLoop(signal);
188
- const hunks = [];
189
- for (const [start, end] of hunkBounds(ops, context)) {
190
- hunks.push(hunkFrom(ops, start, end));
191
- await yieldToEventLoop(signal);
192
- }
193
- throwIfAborted(signal);
194
- return hunks;
195
- }
196
- function diffStatsFromHunks(hunks) {
197
- let addedLines = 0;
198
- let removedLines = 0;
199
- for (const hunk of hunks) {
200
- for (const line of hunk.lines) {
201
- if (line.kind === "add") addedLines++;
202
- if (line.kind === "del") removedLines++;
203
- }
204
- }
205
- return { addedLines, removedLines };
206
- }
207
- async function renderDiffWithStatsAsync(oldText, newText, context = 3, signal) {
208
- const hunks = await lineDiffAsync(oldText, newText, context, signal);
209
- if (hunks.length === 0) return { diff: "", stats: { addedLines: 0, removedLines: 0 } };
210
- const out = [];
211
- for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
212
- const hunk = hunks[hunkIndex];
213
- out.push(`@@ -${hunk.oldStart} +${hunk.newStart} @@`);
214
- for (let lineIndex = 0; lineIndex < hunk.lines.length; lineIndex++) {
215
- const line = hunk.lines[lineIndex];
216
- const prefix = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
217
- out.push(prefix + line.text);
218
- if (lineIndex > 0 && lineIndex % DIFF_YIELD_ITEMS === 0) await yieldToEventLoop(signal);
219
- }
220
- if (hunkIndex > 0 && hunkIndex % 64 === 0) await yieldToEventLoop(signal);
221
- }
222
- return { diff: out.join("\n"), stats: diffStatsFromHunks(hunks) };
223
- }
224
-
225
- // src/tools/file-provenance.ts
226
- import { createHash } from "crypto";
227
- import { readFile, stat } from "fs/promises";
228
- import { normalize, relative, resolve } from "path";
229
- function workspaceIdentity(workspaceRoot) {
230
- const root = normalize(resolve(workspaceRoot));
231
- const stable = process.platform === "win32" ? root.toLowerCase() : root;
232
- return createHash("sha256").update(stable).digest("hex").slice(0, 24);
233
- }
234
- function normalizeObservedPath(path) {
235
- const normalized = path.replace(/\\/g, "/");
236
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
237
- }
238
- function observationKey(workspaceId, path) {
239
- return `${workspaceId}:${normalizeObservedPath(path)}`;
240
- }
241
- function observationPathFor(workspaceRoot, absolutePath) {
242
- const root = canonicalizePath(resolve(workspaceRoot));
243
- return relative(root, canonicalizePath(absolutePath)).replace(/\\/g, "/");
244
- }
245
- function isOutline(observation) {
246
- return observation.operation === "outline";
247
- }
248
- function mayReplaceObservation(current, next) {
249
- return !current || !isOutline(next) || isOutline(current);
250
- }
251
- function supersedesObservation(current, next) {
252
- if (!current) return true;
253
- if (isOutline(current) !== isOutline(next)) return isOutline(current);
254
- return current.timestamp <= next.timestamp;
255
- }
256
- function seedObservationLedger(ledger, messages) {
257
- for (const message of messages) {
258
- for (const observation of message.fileObservations ?? []) {
259
- const key = observationKey(observation.workspaceId, observation.path);
260
- if (supersedesObservation(ledger.get(key), observation)) ledger.set(key, observation);
261
- }
262
- }
263
- return ledger;
264
- }
265
- async function observeFile(ctx, absolutePath, operation, coverage) {
266
- const bytes = await readFile(absolutePath);
267
- const workspaceId = workspaceIdentity(ctx.workspaceRoot);
268
- const path = observationPathFor(ctx.workspaceRoot, absolutePath);
269
- const observation = {
270
- path,
271
- workspaceId,
272
- sha256: createHash("sha256").update(bytes).digest("hex"),
273
- byteSize: bytes.byteLength,
274
- lineStart: coverage?.lineStart,
275
- lineEnd: coverage?.lineEnd,
276
- operation,
277
- sourceRef: ctx.currentToolTraceId ?? "runtime-tool",
278
- timestamp: Date.now()
279
- };
280
- const key = observationKey(workspaceId, path);
281
- const ledger = ctx.fileObservationLedger;
282
- if (ledger && mayReplaceObservation(ledger.get(key), observation)) ledger.set(key, observation);
283
- return observation;
284
- }
285
- async function requireFreshObservation(ctx, absolutePath, relativePath) {
286
- const workspaceId = workspaceIdentity(ctx.workspaceRoot);
287
- const normalizedPath = relativePath.replace(/\\/g, "/");
288
- const remembered = ctx.fileObservationLedger?.get(
289
- observationKey(workspaceId, observationPathFor(ctx.workspaceRoot, absolutePath))
290
- );
291
- if (!remembered || isOutline(remembered)) return void 0;
292
- try {
293
- const info = await stat(absolutePath);
294
- if (!info.isFile()) return staleMessage(normalizedPath);
295
- const bytes = await readFile(absolutePath);
296
- const currentHash = createHash("sha256").update(bytes).digest("hex");
297
- return currentHash === remembered.sha256 ? void 0 : staleMessage(normalizedPath);
298
- } catch {
299
- return staleMessage(normalizedPath);
300
- }
301
- }
302
- function staleMessage(path) {
303
- return `SKIPPED: ${path} changed or disappeared since it was last shown to the model. Call Read (or mention the file again) before modifying it.`;
304
- }
305
- function requireObservationForMutation(ctx, absolutePath, relativePath, retryVerb) {
306
- const ledger = ctx.fileObservationLedger;
307
- if (!ledger) return void 0;
308
- const workspaceId = workspaceIdentity(ctx.workspaceRoot);
309
- const normalizedPath = relativePath.replace(/\\/g, "/");
310
- const remembered = ledger.get(
311
- observationKey(workspaceId, observationPathFor(ctx.workspaceRoot, absolutePath))
312
- );
313
- if (remembered && !isOutline(remembered)) return void 0;
314
- const seen = remembered ? "has only been outlined in this session, and an outline is not the file's content" : "has not been read in this session";
315
- return toolFailure(
316
- `SKIPPED: ${normalizedPath} ${seen}. Call Read (or mention the file) before modifying it.`,
317
- {
318
- code: "file_not_observed",
319
- remediation: `Read the file first, then retry the ${retryVerb}.`
320
- }
321
- );
322
- }
323
-
324
- // src/tools/mutation.ts
325
- import { randomUUID } from "crypto";
326
- import { mkdir, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "fs/promises";
327
- import { dirname } from "path";
328
- var pathLocks = /* @__PURE__ */ new Map();
329
- function looksBinary(bytes) {
330
- const sample = bytes.subarray(0, 8192);
331
- if (sample.includes(0)) return true;
332
- let controls = 0;
333
- for (const byte of sample) {
334
- if (byte < 7 || byte > 13 && byte < 32) controls++;
335
- }
336
- return sample.length > 0 && controls / sample.length > 0.1;
337
- }
338
- async function readTextSnapshot(filePath, allowMissing = false) {
339
- let bytes;
340
- try {
341
- bytes = await readFile2(filePath);
342
- } catch (error) {
343
- if (allowMissing && error.code === "ENOENT") {
344
- return {
345
- exists: false,
346
- bytes: Buffer.alloc(0),
347
- text: "",
348
- bom: false,
349
- lineEnding: "lf",
350
- mixedLineEndings: false,
351
- binary: false,
352
- mode: void 0
353
- };
354
- }
355
- throw error;
356
- }
357
- const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
358
- const body = bytes.subarray(bom ? 3 : 0);
359
- let binary = looksBinary(bytes);
360
- let decoded;
361
- try {
362
- decoded = new TextDecoder("utf-8", { fatal: true }).decode(body);
363
- } catch {
364
- binary = true;
365
- decoded = body.toString("utf8");
366
- }
367
- const crlfCount = (decoded.match(/\r\n/g) ?? []).length;
368
- const bareLfCount = (decoded.replace(/\r\n/g, "").match(/\n/g) ?? []).length;
369
- const bareCrCount = (decoded.replace(/\r\n/g, "").match(/\r/g) ?? []).length;
370
- const mixedLineEndings = crlfCount > 0 && bareLfCount > 0 || bareCrCount > 0;
371
- const mode = (await stat2(filePath)).mode & 511;
372
- return {
373
- exists: true,
374
- bytes,
375
- text: decoded.replace(/\r\n/g, "\n"),
376
- bom,
377
- lineEnding: crlfCount > bareLfCount ? "crlf" : "lf",
378
- mixedLineEndings,
379
- binary,
380
- mode
381
- };
382
- }
383
- function restoreTextEncoding(text, snapshot) {
384
- const body = snapshot.lineEnding === "crlf" ? text.replace(/\n/g, "\r\n") : text;
385
- return Buffer.from(`${snapshot.bom ? "\uFEFF" : ""}${body}`, "utf8");
386
- }
387
- async function writeFileAtomically(filePath, bytes, signal, mode) {
388
- throwIfAborted(signal);
389
- await mkdir(dirname(filePath), { recursive: true });
390
- const temporary = `${filePath}.book-tmp-${randomUUID()}`;
391
- try {
392
- await writeFile(temporary, bytes, { flag: "wx", mode });
393
- throwIfAborted(signal);
394
- await rename(temporary, filePath);
395
- } finally {
396
- await rm(temporary, { force: true }).catch(() => void 0);
397
- }
398
- }
399
- async function removeFileAtomically(filePath, signal) {
400
- throwIfAborted(signal);
401
- await rm(filePath, { force: false });
402
- }
403
- async function acquirePath(path) {
404
- const previous = pathLocks.get(path) ?? Promise.resolve();
405
- let release;
406
- const current = new Promise((resolve2) => {
407
- release = resolve2;
408
- });
409
- const queued = previous.then(() => current);
410
- pathLocks.set(path, queued);
411
- await previous;
412
- return () => {
413
- release();
414
- if (pathLocks.get(path) === queued) pathLocks.delete(path);
415
- };
416
- }
417
- async function withMutationLocks(paths, operation) {
418
- const releases = [];
419
- try {
420
- for (const path of [...new Set(paths)].sort()) releases.push(await acquirePath(path));
421
- return await operation();
422
- } finally {
423
- for (const release of releases.reverse()) release();
424
- }
425
- }
426
-
427
- // src/tools/patch.ts
428
- var MAX_PATCH_BYTES = 1024 * 1024;
429
- var MAX_FILES = 100;
430
- var MAX_HUNKS = 1e3;
431
- function invalid(message, details) {
432
- return toolFailure(message, {
433
- code: "invalid_patch_syntax",
434
- remediation: "Regenerate a Codex-style patch with Begin/End markers and valid file hunks.",
435
- details
436
- });
437
- }
438
- function parseHunkHeader(line) {
439
- const match = line.match(/^@@(?: -(\d+)(?:,(\d+))?)?(?: \+(\d+)(?:,(\d+))?)?(?: @@.*)?$/);
440
- if (!match) throw new Error(`Invalid hunk header: ${line}`);
441
- return { oldStart: match[1] ? Number(match[1]) : void 0 };
442
- }
443
- function parsePatchUnsafe(patch) {
444
- if (typeof patch !== "string" || patch.length === 0)
445
- return invalid("patch must be a non-empty string");
446
- const bytes = Buffer.byteLength(patch, "utf8");
447
- if (bytes > MAX_PATCH_BYTES)
448
- return toolFailure(`Patch input exceeds ${MAX_PATCH_BYTES} bytes`, {
449
- code: "patch_limit_exceeded",
450
- remediation: "Split the change into smaller patches.",
451
- details: { limit: MAX_PATCH_BYTES, bytes }
452
- });
453
- const lines = patch.replace(/\r\n/g, "\n").split("\n");
454
- if (lines.at(-1) === "") lines.pop();
455
- if (lines[0] !== "*** Begin Patch" || lines.at(-1) !== "*** End Patch")
456
- return invalid("Patch must start with *** Begin Patch and end with *** End Patch");
457
- while (lines.length > 2 && lines[1] === "*** Begin Patch") lines.splice(1, 1);
458
- while (lines.length > 2 && lines.at(-2) === "*** End Patch") lines.splice(-2, 1);
459
- const operations = [];
460
- let index = 1;
461
- let hunkCount = 0;
462
- while (index < lines.length - 1) {
463
- const marker = lines[index];
464
- const update = marker.match(/^\*\*\* Update File: (.+)$/);
465
- const add = marker.match(/^\*\*\* Add File: (.+)$/);
466
- const del = marker.match(/^\*\*\* Delete File: (.+)$/);
467
- if (update) {
468
- const hunks = [];
469
- index++;
470
- while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
471
- if (!lines[index].startsWith("@@"))
472
- throw new Error(`Expected @@ hunk header near line ${index + 1}`);
473
- const header = lines[index];
474
- const parsedHeader = parseHunkHeader(header);
475
- index++;
476
- const hunkLines = [];
477
- while (index < lines.length - 1 && !lines[index].startsWith("@@") && !lines[index].startsWith("*** ")) {
478
- const line = lines[index++];
479
- if (line === "\") {
480
- const previous = hunkLines.at(-1);
481
- if (!previous)
482
- throw new Error(`No-newline marker has no preceding hunk line near line ${index}`);
483
- previous.noNewline = true;
484
- continue;
485
- }
486
- if (line.startsWith(" ")) hunkLines.push({ kind: "context", text: line.slice(1) });
487
- else if (line.startsWith("-")) hunkLines.push({ kind: "remove", text: line.slice(1) });
488
- else if (line.startsWith("+")) hunkLines.push({ kind: "add", text: line.slice(1) });
489
- else throw new Error(`Invalid hunk line near line ${index}`);
490
- }
491
- if (hunkLines.length === 0) throw new Error(`Empty hunk near line ${index}`);
492
- if (!hunkLines.some((line) => line.kind !== "add"))
493
- throw new Error(`Update hunk must contain context or removed lines near line ${index}`);
494
- hunks.push({ header, oldStart: parsedHeader.oldStart, lines: hunkLines });
495
- hunkCount++;
496
- }
497
- if (hunks.length === 0) throw new Error(`Update File ${update[1]} has no hunks`);
498
- operations.push({ kind: "update", path: update[1].trim(), hunks });
499
- continue;
500
- }
501
- if (add) {
502
- index++;
503
- const content = [];
504
- while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
505
- const line = lines[index++];
506
- if (!line.startsWith("+"))
507
- throw new Error(`Added file lines must begin with + near line ${index}`);
508
- content.push(line.slice(1));
509
- }
510
- operations.push({ kind: "add", path: add[1].trim(), lines: content });
511
- continue;
512
- }
513
- if (del) {
514
- index++;
515
- if (index < lines.length - 1 && !lines[index].startsWith("*** "))
516
- throw new Error(`Delete File ${del[1]} cannot contain patch lines`);
517
- operations.push({ kind: "delete", path: del[1].trim() });
518
- continue;
519
- }
520
- throw new Error(`Expected a file operation near line ${index + 1}`);
521
- }
522
- if (operations.length === 0) return invalid("Patch contains no file operations");
523
- if (operations.length > MAX_FILES || hunkCount > MAX_HUNKS)
524
- return toolFailure(`Patch exceeds limits (${MAX_FILES} files, ${MAX_HUNKS} hunks)`, {
525
- code: "patch_limit_exceeded",
526
- remediation: "Split the change into smaller patches.",
527
- details: { files: operations.length, hunks: hunkCount }
528
- });
529
- return { operations, bytes };
530
- }
531
- function parsePatch(patch) {
532
- try {
533
- return parsePatchUnsafe(patch);
534
- } catch (error) {
535
- return invalid(error instanceof Error ? error.message : String(error));
536
- }
537
- }
538
- function linesOf(text) {
539
- const lines = text.split("\n");
540
- if (lines.at(-1) === "") lines.pop();
541
- return lines;
542
- }
543
- function sequenceMatches(haystack, needle) {
544
- if (needle.length === 0) return [];
545
- const matches = [];
546
- for (let start = 0; start <= haystack.length - needle.length; start++) {
547
- if (needle.every((line, offset) => haystack[start + offset] === line)) matches.push(start);
548
- }
549
- return matches;
550
- }
551
- function candidateLine(haystack, line) {
552
- const matches = haystack.reduce((found, value, index) => {
553
- if (value === line) found.push(index + 1);
554
- return found;
555
- }, []);
556
- return matches.length === 1 ? matches[0] : void 0;
557
- }
558
- function applyHunks(text, hunks) {
559
- let lines = linesOf(text);
560
- let trailingNewline = text.endsWith("\n");
561
- let cursor = 0;
562
- for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
563
- const hunk = hunks[hunkIndex];
564
- const oldLines = hunk.lines.filter((line) => line.kind !== "add").map((line) => line.text);
565
- const newLines = hunk.lines.filter((line) => line.kind !== "remove").map((line) => line.text);
566
- const matches = sequenceMatches(lines, oldLines);
567
- const afterPrevious = matches.filter((start) => start >= cursor);
568
- const index = matches.length === 1 ? matches[0] : afterPrevious.length === 1 ? afterPrevious[0] : -1;
569
- if (matches.length === 0)
570
- return {
571
- text,
572
- mismatch: toolFailure(`Hunk ${hunkIndex + 1}: patch context not found`, {
573
- code: "patch_context_not_found",
574
- remediation: "Reread the affected range and regenerate the hunk against the latest file contents.",
575
- details: {
576
- hunkIndex: hunkIndex + 1,
577
- header: hunk.header,
578
- firstContextLine: oldLines[0]?.slice(0, 200),
579
- closestCandidateLine: oldLines[0] ? candidateLine(lines, oldLines[0]) : void 0,
580
- suggestedReadRange: hunk.oldStart !== void 0 ? {
581
- lineStart: Math.max(1, hunk.oldStart - 3),
582
- lineEnd: hunk.oldStart + oldLines.length + 3
583
- } : void 0
584
- }
585
- })
586
- };
587
- if (index < 0)
588
- return {
589
- text,
590
- mismatch: toolFailure(`Hunk ${hunkIndex + 1}: patch context is ambiguous`, {
591
- code: "ambiguous_patch_context",
592
- remediation: "Add surrounding lines, such as the enclosing function signature, until the hunk occurs once in the file or once after the previous hunk, then regenerate the patch.",
593
- details: {
594
- hunkIndex: hunkIndex + 1,
595
- header: hunk.header,
596
- matches: matches.length,
597
- matchesAfterPreviousHunk: afterPrevious.length
598
- }
599
- })
600
- };
601
- const touchesEndOfFile = index + oldLines.length === lines.length;
602
- lines = [...lines.slice(0, index), ...newLines, ...lines.slice(index + oldLines.length)];
603
- cursor = index + newLines.length;
604
- if (touchesEndOfFile) {
605
- const oldNoNewline = hunk.lines.some((line) => line.kind !== "add" && line.noNewline);
606
- const newNoNewline = hunk.lines.some((line) => line.kind !== "remove" && line.noNewline);
607
- if (newNoNewline) trailingNewline = false;
608
- else if (oldNoNewline) trailingNewline = true;
609
- }
610
- }
611
- return { text: lines.join("\n") + (trailingNewline && lines.length > 0 ? "\n" : "") };
612
- }
613
- function mutationFromDiff(relativePath, kind, stats) {
614
- return {
615
- kind,
616
- filePath: relativePath,
617
- addedLines: stats.addedLines,
618
- removedLines: stats.removedLines
619
- };
620
- }
621
- function errorCodeFor(error) {
622
- return error instanceof Error && error.message.includes("changed or disappeared") ? "stale_file_observation" : "filesystem_error";
623
- }
624
- async function applyPatch(args, ctx) {
625
- const parsed = parsePatch(args.patch);
626
- if (!("operations" in parsed)) return parsed;
627
- const paths = parsed.operations.map((operation) => operation.path);
628
- const resolved = parsed.operations.map((operation) => {
629
- const result = resolveMutationPath(ctx, operation.path);
630
- return result ? { operation, ...result } : null;
631
- });
632
- if (resolved.some((entry) => !entry))
633
- return pathOutsideWorkspaceResult(paths[resolved.findIndex((entry) => !entry)]);
634
- const entries = resolved;
635
- const canonicalKeys = entries.map(
636
- (entry) => process.platform === "win32" ? entry.canonicalPath.toLowerCase() : entry.canonicalPath
637
- );
638
- if (new Set(canonicalKeys).size !== canonicalKeys.length)
639
- return toolFailure("A patch may contain only one operation per file", {
640
- code: "patch_conflict",
641
- remediation: "Combine related updates into one file operation with multiple hunks."
642
- });
643
- return withMutationLocks(
644
- entries.map((entry) => entry.canonicalPath),
645
- async () => {
646
- const staged = [];
647
- try {
648
- for (const entry of entries) {
649
- throwIfAborted(ctx.signal);
650
- const before = await readTextSnapshot(entry.filePath, entry.operation.kind === "add");
651
- if (before.binary)
652
- return toolFailure(`Binary file is unsupported: ${entry.relativePath}`, {
653
- code: "binary_file_unsupported",
654
- remediation: "Use a text file mutation or a dedicated binary workflow."
655
- });
656
- if (before.mixedLineEndings && entry.operation.kind === "update")
657
- return toolFailure(
658
- `Mixed line endings are unsupported for patch updates: ${entry.relativePath}`,
659
- {
660
- code: "patch_conflict",
661
- remediation: "Normalize the file line endings, reread it, and regenerate the patch.",
662
- details: { filePath: entry.relativePath, lineEnding: "mixed" }
663
- }
664
- );
665
- const stale = entry.operation.kind === "add" ? void 0 : await requireFreshObservation(ctx, entry.filePath, entry.relativePath);
666
- if (stale)
667
- return toolFailure(stale, {
668
- code: "stale_file_observation",
669
- remediation: "Call Read before regenerating the patch."
670
- });
671
- if (entry.operation.kind === "add") {
672
- if (before.exists)
673
- return toolFailure(`File already exists: ${entry.relativePath}`, {
674
- code: "patch_conflict",
675
- remediation: "Use Update File for an existing file."
676
- });
677
- const afterText = entry.operation.lines.join("\n") + (entry.operation.lines.length > 0 ? "\n" : "");
678
- const { stats } = await renderDiffWithStatsAsync("", afterText, 3, ctx.signal);
679
- staged.push({
680
- operation: entry.operation,
681
- absolutePath: entry.filePath,
682
- writePath: entry.canonicalPath,
683
- relativePath: entry.relativePath,
684
- before,
685
- afterText,
686
- afterBytes: Buffer.from(afterText, "utf8"),
687
- mutation: mutationFromDiff(entry.relativePath, "create", stats)
688
- });
689
- } else if (entry.operation.kind === "delete") {
690
- if (!before.exists)
691
- return toolFailure(`File not found: ${entry.relativePath}`, {
692
- code: "patch_conflict",
693
- remediation: "Reread the workspace and regenerate the patch."
694
- });
695
- staged.push({
696
- operation: entry.operation,
697
- absolutePath: entry.filePath,
698
- writePath: entry.filePath,
699
- relativePath: entry.relativePath,
700
- before,
701
- mutation: {
702
- kind: "delete",
703
- filePath: entry.relativePath,
704
- addedLines: 0,
705
- removedLines: linesOf(before.text).length
706
- }
707
- });
708
- } else {
709
- if (!before.exists)
710
- return toolFailure(`File not found: ${entry.relativePath}`, {
711
- code: "patch_context_not_found",
712
- remediation: "Reread the file and regenerate the patch."
713
- });
714
- const applied = applyHunks(before.text, entry.operation.hunks);
715
- if (applied.mismatch)
716
- return toolFailure(
717
- `${entry.relativePath}: ${applied.mismatch.structuredError?.message}`,
718
- {
719
- ...applied.mismatch.structuredError,
720
- code: applied.mismatch.structuredError?.code ?? "patch_context_not_found",
721
- remediation: applied.mismatch.structuredError?.remediation,
722
- details: {
723
- ...applied.mismatch.structuredError?.details,
724
- filePath: entry.relativePath,
725
- lineEnding: before.mixedLineEndings ? "mixed" : before.lineEnding,
726
- normalizedMatching: true
727
- }
728
- }
729
- );
730
- const afterText = applied.text;
731
- const { stats } = await renderDiffWithStatsAsync(before.text, afterText, 3, ctx.signal);
732
- staged.push({
733
- operation: entry.operation,
734
- absolutePath: entry.filePath,
735
- writePath: entry.canonicalPath,
736
- relativePath: entry.relativePath,
737
- before,
738
- afterText,
739
- afterBytes: restoreTextEncoding(afterText, before),
740
- mutation: mutationFromDiff(entry.relativePath, "update", stats)
741
- });
742
- }
743
- }
744
- const mutations = staged.map((file) => file.mutation);
745
- const changedLines = mutations.reduce(
746
- (total, mutation) => total + mutation.addedLines + mutation.removedLines,
747
- 0
748
- );
749
- const contextLines = parsed.operations.reduce(
750
- (total, operation) => total + (operation.kind === "update" ? operation.hunks.reduce(
751
- (hunkTotal, hunk) => hunkTotal + hunk.lines.filter((line) => line.kind === "context").length,
752
- 0
753
- ) : 0),
754
- 0
755
- );
756
- const changeBudgetWarnings = [];
757
- if (changedLines > 2e3) changeBudgetWarnings.push("large_change");
758
- if (changedLines > 100 && changedLines > Math.max(1, contextLines) * 20)
759
- changeBudgetWarnings.push("low_context_to_change_ratio");
760
- const diffs = [];
761
- for (const file of staged) {
762
- throwIfAborted(ctx.signal);
763
- const afterText = file.afterText ?? "";
764
- const { diff } = await renderDiffWithStatsAsync(
765
- file.before.text,
766
- afterText,
767
- 3,
768
- ctx.signal
769
- );
770
- if (diff) diffs.push(`--- ${file.relativePath}
771
- +++ ${file.relativePath}
772
- ${diff}`);
773
- }
774
- const committed = [];
775
- try {
776
- for (const file of staged) {
777
- throwIfAborted(ctx.signal);
778
- committed.push(file);
779
- if (file.operation.kind === "delete")
780
- await removeFileAtomically(file.absolutePath, ctx.signal);
781
- else
782
- await writeFileAtomically(
783
- file.writePath,
784
- file.afterBytes,
785
- ctx.signal,
786
- file.before.mode
787
- );
788
- const check = await readTextSnapshot(
789
- file.absolutePath,
790
- file.operation.kind === "delete"
791
- );
792
- if (file.operation.kind === "delete" ? check.exists : !check.exists || !check.bytes.equals(file.afterBytes))
793
- throw new Error(`Post-write verification failed for ${file.relativePath}`);
794
- }
795
- } catch (error) {
796
- let rollbackFailed = false;
797
- for (const file of committed.reverse()) {
798
- try {
799
- if (file.before.exists)
800
- await writeFileAtomically(
801
- file.writePath,
802
- file.before.bytes,
803
- void 0,
804
- file.before.mode
805
- );
806
- else await removeFileAtomically(file.absolutePath).catch(() => void 0);
807
- } catch {
808
- rollbackFailed = true;
809
- }
810
- }
811
- return toolFailure(
812
- `Patch commit failed${rollbackFailed ? "; rollback also failed" : " and was rolled back"}: ${error instanceof Error ? error.message : String(error)}`,
813
- {
814
- code: rollbackFailed ? "patch_rollback_failed" : "filesystem_error",
815
- remediation: rollbackFailed ? "Inspect the affected files and restore them manually." : "Retry after checking the workspace state."
816
- }
817
- );
818
- }
819
- const observations = [];
820
- for (const file of staged) {
821
- if (file.operation.kind !== "delete")
822
- observations.push(
823
- await observeFile(
824
- ctx,
825
- file.absolutePath,
826
- file.operation.kind === "add" ? "create" : "edit"
827
- )
828
- );
829
- }
830
- const result = toolSuccess(
831
- diffs.join("\n") || "Patch applied successfully (no textual change)",
832
- {
833
- data: {
834
- files: mutations.map((mutation) => mutation.filePath),
835
- changedFiles: mutations.length,
836
- changedLines,
837
- patchBytes: parsed.bytes,
838
- changeBudgetWarnings
839
- },
840
- artifacts: {
841
- fileMutation: mutations[0],
842
- fileMutations: mutations,
843
- fileObservations: observations
844
- },
845
- presentation: {
846
- kind: "diff",
847
- summary: `Applied patch to ${mutations.length} ${mutations.length === 1 ? "file" : "files"}`,
848
- target: mutations.length === 1 ? mutations[0].filePath : `${mutations.length} files`
849
- }
850
- }
851
- );
852
- if (!staged.every((file) => file.mutation.kind === "create")) return result;
853
- return {
854
- ...result,
855
- maskedPlaceholder: `${staged.map(
856
- (file, order) => `${order === 0 ? "Created" : "created"} ${file.relativePath} (${file.mutation.addedLines} lines, ${file.afterBytes?.byteLength ?? 0} bytes)`
857
- ).join("; ")}.`
858
- };
859
- } catch (error) {
860
- const code = errorCodeFor(error);
861
- return toolFailure(error instanceof Error ? error.message : String(error), {
862
- code,
863
- remediation: "Reread the affected file and regenerate the patch."
864
- });
865
- }
866
- }
867
- );
868
- }
869
- var patchTools = [
870
- {
871
- name: "ApplyPatch",
872
- argumentAliases: { input: "patch" },
873
- 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.",
874
- parameters: {
875
- type: "object",
876
- properties: {
877
- patch: {
878
- type: "string",
879
- minLength: 1,
880
- maxLength: MAX_PATCH_BYTES,
881
- description: "Patch text beginning with *** Begin Patch and ending with *** End Patch"
882
- },
883
- baseObservation: {
884
- type: "string",
885
- maxLength: 128,
886
- description: "Optional short observation identifier for diagnostics; freshness is checked implicitly."
887
- }
888
- },
889
- required: ["patch"],
890
- additionalProperties: false
891
- },
892
- execute: applyPatch
893
- }
894
- ];
895
-
896
- // src/permissions.ts
897
- function isUnreadablePattern(target) {
898
- return typeof target === "object" && target !== null && target.target === "invalid";
899
- }
900
- var WORKSPACE_READ_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep"]);
901
- var PATH_RULE_TOOLS = /* @__PURE__ */ new Set([
902
- "Read",
903
- "Write",
904
- "Edit",
905
- "MultiEdit",
906
- "NotebookEdit"
907
- ]);
908
- var WORKSPACE_READ_JUDGING_MODES = /* @__PURE__ */ new Set([
909
- "default",
910
- "accept-edits",
911
- "plan"
912
- ]);
913
- var WORKSPACE_READ_REFUSAL_MODES = /* @__PURE__ */ new Set([
914
- ...WORKSPACE_READ_JUDGING_MODES,
915
- "dontAsk"
916
- ]);
917
- var WORKSPACE_READ_ONLY_GIT_TOOLS = /* @__PURE__ */ new Set([
918
- "GitStatus",
919
- "GitDiff",
920
- "GitLog",
921
- "GitBranch"
922
- ]);
923
- function parseRule(rule) {
924
- const parenIdx = rule.indexOf("(");
925
- if (parenIdx === -1) {
926
- return { toolName: rule.trim(), pattern: null };
927
- }
928
- const toolName = rule.slice(0, parenIdx).trim();
929
- const closeIdx = rule.lastIndexOf(")");
930
- if (closeIdx === -1 || closeIdx <= parenIdx) {
931
- return { toolName, pattern: null };
932
- }
933
- const pattern = rule.slice(parenIdx + 1, closeIdx).trim();
934
- return { toolName, pattern: pattern.length === 0 ? null : pattern };
935
- }
936
- function normalizePathArg(s) {
937
- if (s.startsWith("./")) return s.slice(2);
938
- return s;
939
- }
940
- function toolNameMatchesRule(ruleToolName, toolName) {
941
- if (ruleToolName === toolName) return true;
942
- if (!ruleToolName.startsWith("mcp__")) return false;
943
- const serverName = ruleToolName.slice("mcp__".length);
944
- return Boolean(
945
- serverName && !serverName.includes("__") && toolName.startsWith(`${ruleToolName}__`)
946
- );
947
- }
948
- var caseFoldingPathRule = (value) => value.toLowerCase();
949
- function ruleMatchesCandidate(rule, toolName, primaryArg, fold) {
950
- if (!toolNameMatchesRule(rule.toolName, toolName)) return false;
951
- if (rule.pattern === null) return true;
952
- const normalizedArg = normalizePathArg(primaryArg);
953
- const normalizedPattern = rule.pattern.startsWith("./") ? rule.pattern.slice(2) : rule.pattern;
954
- if (!fold) return globToRegex(normalizedPattern).test(normalizedArg);
955
- return globToRegex(caseFoldingPathRule(normalizedPattern)).test(
956
- caseFoldingPathRule(normalizedArg)
957
- );
958
- }
959
- function ruleMatches(rule, toolName, primaryArg) {
960
- return ruleMatchesCandidate(rule, toolName, primaryArg, false);
961
- }
962
- function permissionRuleForToolCall(call) {
963
- const toolName = canonicalToolName(call.name);
964
- const primaryArg = getPrimaryArg(call.arguments);
965
- if (toolName === "WebSearch") return toolName;
966
- if (toolName === "WebFetch" && primaryArg) {
967
- try {
968
- const url = new URL(primaryArg);
969
- return `${toolName}(${url.origin}/**)`;
970
- } catch {
971
- }
972
- }
973
- if (!primaryArg) return void 0;
974
- return `${toolName}(${primaryArg})`;
975
- }
976
- function permissionResultOf(decision) {
977
- return typeof decision === "string" ? decision : decision.result;
978
- }
979
- function permissionRuleOf(decision) {
980
- return typeof decision === "string" ? void 0 : decision.rule;
981
- }
982
- function permissionReasonOf(decision) {
983
- return typeof decision === "string" ? void 0 : decision.reason;
984
- }
985
- var RULE_LADDER_MAX = 3;
986
- var SHELL_CHAINING = /[;&|><`$(){}\n]/;
987
- function permissionRuleLadder(call) {
988
- const exact = permissionRuleForToolCall(call);
989
- if (!exact) return [];
990
- if (canonicalToolName(call.name) !== "Bash") return [exact];
991
- const command = getPrimaryArg(call.arguments).trim();
992
- if (!command || SHELL_CHAINING.test(command)) return [exact];
993
- const tokens = command.split(/\s+/);
994
- const ladder = [exact];
995
- for (let take = tokens.length - 1; take >= 1 && ladder.length < RULE_LADDER_MAX; take -= 1) {
996
- const prefix = `Bash(${tokens.slice(0, take).join(" ")} *)`;
997
- if (!ladder.includes(prefix)) ladder.push(prefix);
998
- }
999
- return ladder;
1000
- }
1001
- function permissionRuleMatchesCall(rule, call) {
1002
- const toolName = canonicalToolName(call.name);
1003
- const primaryArg = getPrimaryArg(call.arguments);
1004
- if (toolName === "WebFetch" && primaryArg) {
1005
- try {
1006
- return ruleMatches(parseRule(rule), toolName, new URL(primaryArg).toString());
1007
- } catch {
1008
- }
1009
- }
1010
- return ruleMatches(parseRule(rule), toolName, primaryArg);
1011
- }
1012
- function patchOperations(args) {
1013
- const parsed = parsePatch(args.patch);
1014
- return "operations" in parsed ? parsed.operations : [];
1015
- }
1016
- function ruleMatchesPatch(rule, paths, fold) {
1017
- return paths.some((path) => ruleMatchesCandidate(rule, rule.toolName, path, fold));
1018
- }
1019
- function patchRuleSupportsOperation(rule, operation) {
1020
- if (rule.toolName === "ApplyPatch") return true;
1021
- if (rule.toolName === "Edit") return operation.kind === "update";
1022
- if (rule.toolName === "Write") return operation.kind === "update" || operation.kind === "add";
1023
- return false;
1024
- }
1025
- function compatiblePatchRuleMatches(rule, operations, spellingsOf, fold) {
1026
- if (!["ApplyPatch", "Edit", "Write"].includes(rule.toolName)) return false;
1027
- if (rule.toolName === "ApplyPatch" && rule.pattern === null) return true;
1028
- return operations.some(
1029
- (operation) => patchRuleSupportsOperation(rule, operation) && (rule.pattern === null || ruleMatchesPatch(rule, spellingsOf(operation.path, fold), fold))
1030
- );
1031
- }
1032
- function primaryArgForRule(_toolName, args) {
1033
- return getPrimaryArg(args);
1034
- }
1035
- function hasAdjudicationPolicy(settings) {
1036
- return settings.permissions.deny.length > 0 || settings.permissions.ask.length > 0;
1037
- }
1038
- function sandboxAutoAllows(toolName, args, settings, options) {
1039
- if (!settings.sandbox.autoAllowBashIfSandboxed) return false;
1040
- if (canonicalToolName(toolName) !== "Bash") return false;
1041
- if (hasAdjudicationPolicy(settings)) return false;
1042
- const command = typeof args.command === "string" ? args.command.trim() : "";
1043
- if (!command) return false;
1044
- return sandboxCoverage(command, settings.sandbox, options.sandboxBackendAvailable).sandboxed;
1045
- }
1046
- var toPosix = (value) => value.replace(/\\/g, "/");
1047
- function realRootOf(scope) {
1048
- if (scope.realRoot) return scope.realRoot;
1049
- try {
1050
- return realpathSync.native(scope.root);
1051
- } catch {
1052
- return void 0;
1053
- }
1054
- }
1055
- function pathArgument(toolName, args) {
1056
- const value = toolName === "NotebookEdit" ? args.notebook_path : args.filePath;
1057
- return typeof value === "string" && value.trim() !== "" ? value : void 0;
1058
- }
1059
- function rulesName(lists, toolName) {
1060
- return [...lists.deny, ...lists.ask, ...lists.allow].some(
1061
- ({ parsed }) => parsed.toolName === toolName
1062
- );
1063
- }
1064
- function resolvedPathScope(scope) {
1065
- const detailCache = /* @__PURE__ */ new Map();
1066
- const rootsFor = (includeReadOnly) => ({
1067
- workspaceRoot: scope.root,
1068
- readOnlyRoots: includeReadOnly ? scope.readOnlyRoots : void 0,
1069
- additionalRoots: scope.additionalRoots
1070
- });
1071
- const holdsHomeByRoot = /* @__PURE__ */ new Map();
1072
- const holdsHome = (root) => {
1073
- let known = holdsHomeByRoot.get(root);
1074
- if (known === void 0) {
1075
- known = rootHoldsHome(root, scope.homeGuards ?? []);
1076
- holdsHomeByRoot.set(root, known);
1077
- }
1078
- return known;
1079
- };
1080
- const guardedRoots = [scope.root, ...scope.additionalRoots ?? []].filter(holdsHome);
1081
- const servingRootOf = (canonicalPath) => [realRootOf(scope), ...scope.additionalRoots ?? []].find(
1082
- (root) => typeof root === "string" && root.length > 0 && isUnderRoot(canonicalPath, root)
1083
- );
1084
- return {
1085
- scope,
1086
- roots: rootsFor,
1087
- detail: (raw, includeReadOnly) => {
1088
- const key = `${includeReadOnly ? "r" : "w"}\0${raw}`;
1089
- let known = detailCache.get(key);
1090
- if (!known) {
1091
- known = resolveReadablePathDetail(rootsFor(includeReadOnly), raw);
1092
- detailCache.set(key, known);
1093
- }
1094
- return known;
1095
- },
1096
- isGuarded: (canonicalPath) => {
1097
- const roots = [realRootOf(scope), ...scope.additionalRoots ?? []].filter(
1098
- (root) => typeof root === "string" && root.length > 0
1099
- );
1100
- if (roots.some((root) => isBookLocalSettingsPath(canonicalPath, root, { directory: true })))
1101
- return true;
1102
- if (guardedRoots.length === 0) return false;
1103
- if (!pathHoldsHome(canonicalPath, scope.homeGuards ?? [])) return false;
1104
- const serving = servingRootOf(canonicalPath);
1105
- return serving !== void 0 && holdsHome(serving);
1106
- },
1107
- /**
1108
- * Whether a Grep or Glob *scope* can reach a guarded home. A scope is a subtree, not a file,
1109
- * so it is guarded when the guarded home is inside it as well as when it is inside the home:
1110
- * `Grep {path: "/home"}` reads `~/.ssh` without naming a single file under it (PR #334
1111
- * finding 4). Compared canonically, both ways, because either side may be reached by a link.
1112
- */
1113
- scopeReachesHome: (canonicalScope) => {
1114
- const homes = scope.homeGuards ?? [];
1115
- if (homes.some((home) => containsPath(canonicalScope, home))) return true;
1116
- return guardedRoots.some((root) => containsPath(root, canonicalScope));
1117
- }
1118
- };
1119
- }
1120
- function spellingsOfPath(paths, raw, includeReadOnly, restrictive) {
1121
- const spellings = [];
1122
- const detail = paths.detail(raw, includeReadOnly);
1123
- if (!("path" in detail)) return [];
1124
- const resolved = detail.path;
1125
- const inWorkspace = resolveWorkspacePath(paths.scope.root, raw) !== null;
1126
- if (inWorkspace) {
1127
- spellings.push(
1128
- resolved.relativePath,
1129
- toPosix(resolved.filePath),
1130
- toPosix(resolved.canonicalPath)
1131
- );
1132
- const realRoot = realRootOf(paths.scope);
1133
- if (realRoot) spellings.push(toPosix(relative2(realRoot, resolved.canonicalPath)));
1134
- } else {
1135
- spellings.push(toPosix(resolved.filePath), toPosix(resolved.canonicalPath));
1136
- if (restrictive) {
1137
- for (const root of paths.scope.additionalRoots ?? []) {
1138
- const fromRoot = relative2(canonicalizePath(root), resolved.canonicalPath);
1139
- if (!fromRoot || isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) {
1140
- continue;
1141
- }
1142
- spellings.push(toPosix(fromRoot));
1143
- }
1144
- }
1145
- }
1146
- return [...new Set(spellings)].filter((spelling) => spelling !== "" && spelling !== raw);
1147
- }
1148
- function pathRuleSpellings(paths, toolName, args, restrictive) {
1149
- const raw = pathArgument(toolName, args);
1150
- return raw ? spellingsOfPath(paths, raw, toolName === "Read", restrictive) : [];
1151
- }
1152
- function globTarget(pattern, paths) {
1153
- const { refusal, scopes } = globWalkPlan(pattern, paths.scope.root);
1154
- if (refusal) return { target: "invalid", reason: refusal };
1155
- if (scopes === null) return "outside";
1156
- let guarded = false;
1157
- for (const scope of scopes) {
1158
- const detail = paths.detail(scope, false);
1159
- if (!("path" in detail)) return "outside";
1160
- if (paths.scopeReachesHome(detail.path.canonicalPath)) guarded = true;
1161
- }
1162
- return guarded ? "guarded" : "servable";
1163
- }
1164
- function readToolTarget(toolName, args, paths) {
1165
- if (toolName === "Read") {
1166
- const raw2 = pathArgument(toolName, args);
1167
- if (!raw2) return "none";
1168
- const detail2 = paths.detail(raw2, true);
1169
- if (!("path" in detail2)) return detail2.reason === "excluded" ? "hidden" : "outside";
1170
- return paths.isGuarded(detail2.path.canonicalPath) ? "guarded" : "servable";
1171
- }
1172
- if (toolName === "Glob") {
1173
- const pattern = typeof args.pattern === "string" ? args.pattern.trim() : "";
1174
- if (!pattern) return "none";
1175
- return globTarget(pattern, paths);
1176
- }
1177
- const raw = typeof args.path === "string" ? args.path.trim() : "";
1178
- if (raw === "" || raw === ".") return "servable";
1179
- const detail = paths.detail(raw, false);
1180
- if (!("path" in detail)) return detail.reason === "excluded" ? "hidden" : "outside";
1181
- if (paths.isGuarded(detail.path.canonicalPath)) return "guarded";
1182
- return paths.scopeReachesHome(detail.path.canonicalPath) ? "guarded" : "servable";
1183
- }
1184
- function parseRuleLists(settings) {
1185
- const list = (texts) => texts.map((text) => ({ text, parsed: parseRule(text) }));
1186
- return {
1187
- deny: list(settings.permissions.deny),
1188
- ask: list(settings.permissions.ask),
1189
- allow: list(settings.permissions.allow)
1190
- };
1191
- }
1192
- function hasReadAdjudication(lists) {
1193
- return [...lists.deny, ...lists.ask].some(
1194
- ({ parsed }) => WORKSPACE_READ_TOOLS.has(parsed.toolName)
1195
- );
1196
- }
1197
- function evaluatePermission(toolName, args, settings, options = {}) {
1198
- return evaluatePermissionDetail(toolName, args, settings, options).decision;
1199
- }
1200
- function evaluatePermissionDetail(toolName, args, settings, options = {}) {
1201
- const scope = options.workspace;
1202
- const paths = scope ? resolvedPathScope(scope) : void 0;
1203
- const parsed = parseRuleLists(settings);
1204
- if (toolName === "ApplyPatch") {
1205
- const operations = patchOperations(args);
1206
- const spellings = /* @__PURE__ */ new Map();
1207
- const restrictiveSpellings = /* @__PURE__ */ new Map();
1208
- const spellingsOf = (path, restrictive) => {
1209
- const cache = restrictive ? restrictiveSpellings : spellings;
1210
- let known = cache.get(path);
1211
- if (!known) {
1212
- known = paths ? [path, ...spellingsOfPath(paths, path, false, restrictive)] : [path];
1213
- cache.set(path, known);
1214
- }
1215
- return known;
1216
- };
1217
- const isCompatible = (rule) => rule.toolName === "ApplyPatch" || rule.toolName === "Edit" || rule.toolName === "Write";
1218
- for (const list of ["deny", "ask"]) {
1219
- for (const entry of parsed[list]) {
1220
- if (!isCompatible(entry.parsed)) continue;
1221
- if (compatiblePatchRuleMatches(entry.parsed, operations, spellingsOf, true)) {
1222
- return { decision: list, matchedRule: entry.text, source: list };
1223
- }
1224
- }
1225
- }
1226
- const allowRules = parsed.allow.filter(({ parsed: rule }) => isCompatible(rule));
1227
- const firstCompatible = allowRules[0]?.text;
1228
- if (operations.length > 0 && operations.every(
1229
- (operation) => allowRules.some(
1230
- ({ parsed: rule }) => patchRuleSupportsOperation(rule, operation) && (rule.pattern === null || ruleMatchesPatch(rule, spellingsOf(operation.path, false), false))
1231
- )
1232
- )) {
1233
- return { decision: "allow", matchedRule: firstCompatible, source: "allow" };
1234
- }
1235
- if (allowRules.some(({ parsed: rule }) => rule.toolName === "ApplyPatch" && rule.pattern === null)) {
1236
- return { decision: "allow", matchedRule: firstCompatible, source: "allow" };
1237
- }
1238
- return { decision: "ask", source: "default" };
1239
- }
1240
- const tool = canonicalToolName(toolName);
1241
- const candidates = [getPrimaryArg(args)];
1242
- let restrictiveCandidates = candidates;
1243
- if (scope && paths && PATH_RULE_TOOLS.has(tool) && rulesName(parsed, tool)) {
1244
- restrictiveCandidates = [...candidates, ...pathRuleSpellings(paths, tool, args, true)];
1245
- candidates.push(...pathRuleSpellings(paths, tool, args, false));
1246
- }
1247
- const matchesRule = (rule, fold) => {
1248
- const pool = fold ? restrictiveCandidates : candidates;
1249
- if (tool === "WebFetch") {
1250
- try {
1251
- const url = new URL(candidates[0]).toString();
1252
- return ruleMatchesCandidate(rule, tool, url, false);
1253
- } catch {
1254
- }
1255
- }
1256
- return pool.some((candidate) => ruleMatchesCandidate(rule, tool, candidate, fold));
1257
- };
1258
- const readTarget = scope && paths && (scope.judgeReads || scope.refuseUnreachableReads) && WORKSPACE_READ_TOOLS.has(tool) ? readToolTarget(tool, args, paths) : void 0;
1259
- const unreadablePattern = readTarget !== void 0 && isUnreadablePattern(readTarget) ? readTarget.reason : void 0;
1260
- const outside = readTarget === "outside" ? { outsideWorkspace: true } : {};
1261
- for (const entry of parsed.deny) {
1262
- if (matchesRule(entry.parsed, true)) {
1263
- return { decision: "deny", matchedRule: entry.text, source: "deny" };
1264
- }
1265
- }
1266
- for (const entry of parsed.ask) {
1267
- if (matchesRule(entry.parsed, true)) {
1268
- return { decision: "ask", matchedRule: entry.text, source: "ask", ...outside };
1269
- }
1270
- }
1271
- for (const entry of parsed.allow) {
1272
- if (matchesRule(entry.parsed, false)) {
1273
- return { decision: "allow", matchedRule: entry.text, source: "allow" };
1274
- }
1275
- }
1276
- if (sandboxAutoAllows(toolName, args, settings, options)) {
1277
- return { decision: "allow", source: "sandbox" };
1278
- }
1279
- if (scope?.autoAllowReads && (readTarget === "none" || readTarget === "servable" && (tool === "Read" || !hasReadAdjudication(parsed)))) {
1280
- return { decision: "allow", source: "workspace" };
1281
- }
1282
- if (scope?.autoAllowReads && WORKSPACE_READ_ONLY_GIT_TOOLS.has(tool)) {
1283
- return { decision: "allow", source: "workspace" };
1284
- }
1285
- if (ALWAYS_ALLOWED_TOOLS.has(tool)) {
1286
- return { decision: "allow", source: "default" };
1287
- }
1288
- if (unreadablePattern !== void 0) {
1289
- return { decision: "refuse", source: "default", unreadablePattern };
1290
- }
1291
- if (readTarget === "outside") {
1292
- return { decision: "refuse", source: "default", outsideWorkspace: true };
1293
- }
1294
- if (readTarget === "hidden") {
1295
- return { decision: "refuse", source: "default", excludedPath: true };
1296
- }
1297
- return { decision: "ask", source: "default" };
1298
- }
1299
- var ALWAYS_ALLOWED_TOOLS = /* @__PURE__ */ new Set(["MemorySave", "TodoWrite"]);
1300
-
1301
- export {
1302
- throwIfAborted,
1303
- onAbort,
1304
- delay,
1305
- yieldToEventLoop,
1306
- renderDiffWithStatsAsync,
1307
- workspaceIdentity,
1308
- normalizeObservedPath,
1309
- observationKey,
1310
- supersedesObservation,
1311
- seedObservationLedger,
1312
- observeFile,
1313
- requireFreshObservation,
1314
- requireObservationForMutation,
1315
- readTextSnapshot,
1316
- restoreTextEncoding,
1317
- writeFileAtomically,
1318
- withMutationLocks,
1319
- parsePatch,
1320
- applyHunks,
1321
- patchTools,
1322
- WORKSPACE_READ_TOOLS,
1323
- WORKSPACE_READ_JUDGING_MODES,
1324
- WORKSPACE_READ_REFUSAL_MODES,
1325
- WORKSPACE_READ_ONLY_GIT_TOOLS,
1326
- parseRule,
1327
- permissionRuleForToolCall,
1328
- permissionResultOf,
1329
- permissionRuleOf,
1330
- permissionReasonOf,
1331
- permissionRuleLadder,
1332
- permissionRuleMatchesCall,
1333
- primaryArgForRule,
1334
- hasAdjudicationPolicy,
1335
- evaluatePermission,
1336
- evaluatePermissionDetail,
1337
- ALWAYS_ALLOWED_TOOLS
1338
- };
1339
- //# sourceMappingURL=chunk-J62VZYOB.js.map