@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
@@ -0,0 +1,2672 @@
1
+ import {
2
+ globToRegex,
3
+ globWalkPlan,
4
+ sandboxCoverage
5
+ } from "./chunk-XJRJVXJR.js";
6
+ import {
7
+ applySettingsEnvOverrides,
8
+ emptyWorkspaceProviderGate,
9
+ migrateLegacyPermissions,
10
+ resolveSettingsWithProviderGate,
11
+ settingsLayerPaths
12
+ } from "./chunk-UCFJ43OS.js";
13
+ import {
14
+ hadRemovedAuthConfiguration
15
+ } from "./chunk-HOZXKOE3.js";
16
+ import {
17
+ DEFAULT_MAX_INDEX_LINES,
18
+ MAX_INDEX_BYTES,
19
+ loadMemoryContext
20
+ } from "./chunk-EFXVSQ5J.js";
21
+ import {
22
+ createModelWindowStore
23
+ } from "./chunk-62EEAUID.js";
24
+ import {
25
+ readsSingleQuoteAsPosix,
26
+ resolveShell
27
+ } from "./chunk-W7SLYS3M.js";
28
+ import {
29
+ containsPath,
30
+ pathHoldsHome,
31
+ rootHoldsHome
32
+ } from "./chunk-4DKHLZRI.js";
33
+ import {
34
+ canonicalToolName,
35
+ canonicalizePath,
36
+ comparablePath,
37
+ getPrimaryArg,
38
+ isBookLocalSettingsPath,
39
+ isOutside,
40
+ isUnderRoot,
41
+ messagePathFor,
42
+ mutationDisplayPath,
43
+ pathOutsideWorkspaceResult,
44
+ resolveMutationPath,
45
+ resolveReadablePathDetail,
46
+ resolveWorkspacePath,
47
+ toolFailure,
48
+ toolSuccess
49
+ } from "./chunk-YIL6HTJX.js";
50
+ import {
51
+ reachesUncPath
52
+ } from "./chunk-EEX6QNXV.js";
53
+ import {
54
+ WorkspaceProviderRefusalError,
55
+ environmentModelSelections,
56
+ evaluateLegacyBaseUrl,
57
+ parseSecretReference,
58
+ providerPrefixOf,
59
+ resolveSecretFilePath,
60
+ withEnvironmentSelections
61
+ } from "./chunk-B5FC3MYZ.js";
62
+ import {
63
+ defaultTrustStorePath,
64
+ loadWorkspaceTrust
65
+ } from "./chunk-HFB74CLM.js";
66
+ import {
67
+ ADVISOR_OFF_VALUE,
68
+ DEFAULT_SETTINGS,
69
+ effortLevelSchema,
70
+ unboundedInt
71
+ } from "./chunk-REASC2QG.js";
72
+
73
+ // src/permissions.ts
74
+ import { realpathSync } from "fs";
75
+ import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
76
+
77
+ // src/async.ts
78
+ function throwIfAborted(signal) {
79
+ if (!signal?.aborted) return;
80
+ if (signal.reason instanceof Error) throw signal.reason;
81
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Operation cancelled");
82
+ }
83
+ function onAbort(signal, handler) {
84
+ if (!signal) return () => {
85
+ };
86
+ if (signal.aborted) {
87
+ handler();
88
+ return () => {
89
+ };
90
+ }
91
+ signal.addEventListener("abort", handler, { once: true });
92
+ return () => signal.removeEventListener("abort", handler);
93
+ }
94
+ async function delay(ms, signal) {
95
+ if (ms <= 0 || signal?.aborted) return;
96
+ await new Promise((resolve4) => {
97
+ const timer = setTimeout(() => {
98
+ unsubscribe();
99
+ resolve4();
100
+ }, ms);
101
+ const unsubscribe = onAbort(signal, () => {
102
+ clearTimeout(timer);
103
+ resolve4();
104
+ });
105
+ });
106
+ }
107
+ async function yieldToEventLoop(signal) {
108
+ throwIfAborted(signal);
109
+ await new Promise((resolve4) => setImmediate(resolve4));
110
+ throwIfAborted(signal);
111
+ }
112
+
113
+ // src/tools/diff.ts
114
+ function splitLines(s) {
115
+ const lines = s.split("\n");
116
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
117
+ return lines;
118
+ }
119
+ var DIFF_YIELD_CELLS = 32768;
120
+ var DIFF_YIELD_ITEMS = 1024;
121
+ function commonAffixes(a, b) {
122
+ const max = Math.min(a.length, b.length);
123
+ let prefix = 0;
124
+ while (prefix < max && a[prefix] === b[prefix]) prefix++;
125
+ let suffix = 0;
126
+ while (suffix < max - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) suffix++;
127
+ return { prefix, suffix };
128
+ }
129
+ function contextOps(a, from, count, bOffset) {
130
+ const ops = [];
131
+ for (let index = 0; index < count; index++) {
132
+ const ai = from + index;
133
+ ops.push({ kind: "ctx", text: a[ai], ai, bi: ai + bOffset });
134
+ }
135
+ return ops;
136
+ }
137
+ function editScript(a, b, dp, offset) {
138
+ const n = a.length;
139
+ const m = b.length;
140
+ const ops = [];
141
+ let i = 0;
142
+ let j = 0;
143
+ while (i < n && j < m) {
144
+ if (a[i] === b[j]) {
145
+ ops.push({ kind: "ctx", text: a[i], ai: offset + i, bi: offset + j });
146
+ i++;
147
+ j++;
148
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
149
+ ops.push({ kind: "del", text: a[i], ai: offset + i, bi: offset + j });
150
+ i++;
151
+ } else {
152
+ ops.push({ kind: "add", text: b[j], ai: offset + i, bi: offset + j });
153
+ j++;
154
+ }
155
+ }
156
+ while (i < n) {
157
+ ops.push({ kind: "del", text: a[i], ai: offset + i, bi: offset + j });
158
+ i++;
159
+ }
160
+ while (j < m) {
161
+ ops.push({ kind: "add", text: b[j], ai: offset + i, bi: offset + j });
162
+ j++;
163
+ }
164
+ return ops;
165
+ }
166
+ function hunkBounds(ops, context) {
167
+ const bounds = [];
168
+ let k = 0;
169
+ while (k < ops.length) {
170
+ if (ops[k].kind === "ctx") {
171
+ k++;
172
+ continue;
173
+ }
174
+ const start = k;
175
+ let ctxBefore = 0;
176
+ let s = start - 1;
177
+ while (s >= 0 && ops[s].kind === "ctx" && ctxBefore < context) {
178
+ s--;
179
+ ctxBefore++;
180
+ }
181
+ let e = start;
182
+ while (e < ops.length && ops[e].kind !== "ctx") e++;
183
+ let ctxAfter = 0;
184
+ while (e < ops.length && ops[e].kind === "ctx" && ctxAfter < context) {
185
+ e++;
186
+ ctxAfter++;
187
+ }
188
+ const previous = bounds.at(-1);
189
+ if (previous && s + 1 <= previous[1]) previous[1] = e;
190
+ else bounds.push([s + 1, e]);
191
+ k = e;
192
+ }
193
+ return bounds;
194
+ }
195
+ function hunkFrom(ops, start, end) {
196
+ const hunkOps = ops.slice(start, end);
197
+ return {
198
+ oldStart: (hunkOps[0]?.ai ?? 0) + 1,
199
+ newStart: (hunkOps[0]?.bi ?? 0) + 1,
200
+ lines: hunkOps.map((o) => ({ kind: o.kind, text: o.text }))
201
+ };
202
+ }
203
+ async function lineDiffAsync(oldText, newText, context = 3, signal) {
204
+ const a = splitLines(oldText);
205
+ const b = splitLines(newText);
206
+ const { prefix, suffix } = commonAffixes(a, b);
207
+ const midA = a.slice(prefix, a.length - suffix);
208
+ const midB = b.slice(prefix, b.length - suffix);
209
+ const n = midA.length;
210
+ const m = midB.length;
211
+ const dp = [];
212
+ for (let row = 0; row <= n; row++) {
213
+ dp.push(new Array(m + 1).fill(0));
214
+ if (row > 0 && row % 128 === 0) await yieldToEventLoop(signal);
215
+ }
216
+ let cellsUntilYield = DIFF_YIELD_CELLS;
217
+ for (let i = n - 1; i >= 0; i--) {
218
+ for (let j = m - 1; j >= 0; j--) {
219
+ dp[i][j] = midA[i] === midB[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
220
+ cellsUntilYield--;
221
+ if (cellsUntilYield === 0) {
222
+ cellsUntilYield = DIFF_YIELD_CELLS;
223
+ await yieldToEventLoop(signal);
224
+ }
225
+ }
226
+ }
227
+ throwIfAborted(signal);
228
+ const ops = [
229
+ ...contextOps(a, 0, prefix, 0),
230
+ ...editScript(midA, midB, dp, prefix),
231
+ ...contextOps(a, a.length - suffix, suffix, b.length - a.length)
232
+ ];
233
+ if (ops.length > DIFF_YIELD_ITEMS) await yieldToEventLoop(signal);
234
+ const hunks = [];
235
+ for (const [start, end] of hunkBounds(ops, context)) {
236
+ hunks.push(hunkFrom(ops, start, end));
237
+ await yieldToEventLoop(signal);
238
+ }
239
+ throwIfAborted(signal);
240
+ return hunks;
241
+ }
242
+ function diffStatsFromHunks(hunks) {
243
+ let addedLines = 0;
244
+ let removedLines = 0;
245
+ for (const hunk of hunks) {
246
+ for (const line of hunk.lines) {
247
+ if (line.kind === "add") addedLines++;
248
+ if (line.kind === "del") removedLines++;
249
+ }
250
+ }
251
+ return { addedLines, removedLines };
252
+ }
253
+ async function renderDiffWithStatsAsync(oldText, newText, context = 3, signal) {
254
+ const hunks = await lineDiffAsync(oldText, newText, context, signal);
255
+ if (hunks.length === 0) return { diff: "", stats: { addedLines: 0, removedLines: 0 } };
256
+ const out = [];
257
+ for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
258
+ const hunk = hunks[hunkIndex];
259
+ out.push(`@@ -${hunk.oldStart} +${hunk.newStart} @@`);
260
+ for (let lineIndex = 0; lineIndex < hunk.lines.length; lineIndex++) {
261
+ const line = hunk.lines[lineIndex];
262
+ const prefix = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
263
+ out.push(prefix + line.text);
264
+ if (lineIndex > 0 && lineIndex % DIFF_YIELD_ITEMS === 0) await yieldToEventLoop(signal);
265
+ }
266
+ if (hunkIndex > 0 && hunkIndex % 64 === 0) await yieldToEventLoop(signal);
267
+ }
268
+ return { diff: out.join("\n"), stats: diffStatsFromHunks(hunks) };
269
+ }
270
+
271
+ // src/tools/file-provenance.ts
272
+ import { createHash } from "crypto";
273
+ import { readFile, stat } from "fs/promises";
274
+ import { normalize, relative, resolve } from "path";
275
+ function workspaceIdentity(workspaceRoot) {
276
+ const root = normalize(resolve(workspaceRoot));
277
+ const stable = process.platform === "win32" ? root.toLowerCase() : root;
278
+ return createHash("sha256").update(stable).digest("hex").slice(0, 24);
279
+ }
280
+ function normalizeObservedPath(path) {
281
+ const normalized = path.replace(/\\/g, "/");
282
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
283
+ }
284
+ function observationKey(workspaceId, path) {
285
+ return `${workspaceId}:${normalizeObservedPath(path)}`;
286
+ }
287
+ function observationPathFor(workspaceRoot, absolutePath) {
288
+ const root = canonicalizePath(resolve(workspaceRoot));
289
+ return relative(root, canonicalizePath(absolutePath)).replace(/\\/g, "/");
290
+ }
291
+ function isOutline(observation) {
292
+ return observation.operation === "outline";
293
+ }
294
+ function mayReplaceObservation(current, next) {
295
+ return !current || !isOutline(next) || isOutline(current);
296
+ }
297
+ function supersedesObservation(current, next) {
298
+ if (!current) return true;
299
+ if (isOutline(current) !== isOutline(next)) return isOutline(current);
300
+ return current.timestamp <= next.timestamp;
301
+ }
302
+ function seedObservationLedger(ledger, messages) {
303
+ for (const message of messages) {
304
+ for (const observation of message.fileObservations ?? []) {
305
+ const key = observationKey(observation.workspaceId, observation.path);
306
+ if (supersedesObservation(ledger.get(key), observation)) ledger.set(key, observation);
307
+ }
308
+ }
309
+ return ledger;
310
+ }
311
+ async function observeFile(ctx, absolutePath, operation, coverage) {
312
+ const bytes = await readFile(absolutePath);
313
+ const workspaceId = workspaceIdentity(ctx.workspaceRoot);
314
+ const path = observationPathFor(ctx.workspaceRoot, absolutePath);
315
+ const observation = {
316
+ path,
317
+ workspaceId,
318
+ sha256: createHash("sha256").update(bytes).digest("hex"),
319
+ byteSize: bytes.byteLength,
320
+ lineStart: coverage?.lineStart,
321
+ lineEnd: coverage?.lineEnd,
322
+ operation,
323
+ sourceRef: ctx.currentToolTraceId ?? "runtime-tool",
324
+ timestamp: Date.now()
325
+ };
326
+ const key = observationKey(workspaceId, path);
327
+ const ledger = ctx.fileObservationLedger;
328
+ if (ledger && mayReplaceObservation(ledger.get(key), observation)) ledger.set(key, observation);
329
+ return observation;
330
+ }
331
+ async function requireFreshObservation(ctx, absolutePath, displayPath) {
332
+ const workspaceId = workspaceIdentity(ctx.workspaceRoot);
333
+ const remembered = ctx.fileObservationLedger?.get(
334
+ observationKey(workspaceId, observationPathFor(ctx.workspaceRoot, absolutePath))
335
+ );
336
+ if (!remembered || isOutline(remembered)) return void 0;
337
+ try {
338
+ const info = await stat(absolutePath);
339
+ if (!info.isFile()) return staleMessage(displayPath);
340
+ const bytes = await readFile(absolutePath);
341
+ const currentHash = createHash("sha256").update(bytes).digest("hex");
342
+ return currentHash === remembered.sha256 ? void 0 : staleMessage(displayPath);
343
+ } catch {
344
+ return staleMessage(displayPath);
345
+ }
346
+ }
347
+ function staleMessage(path) {
348
+ return `SKIPPED: ${path} changed or disappeared since it was last shown to the model. Call Read (or mention the file again) before modifying it.`;
349
+ }
350
+ function requireObservationForMutation(ctx, absolutePath, displayPath, retryVerb) {
351
+ const ledger = ctx.fileObservationLedger;
352
+ if (!ledger) return void 0;
353
+ const workspaceId = workspaceIdentity(ctx.workspaceRoot);
354
+ const remembered = ledger.get(
355
+ observationKey(workspaceId, observationPathFor(ctx.workspaceRoot, absolutePath))
356
+ );
357
+ if (remembered && !isOutline(remembered)) return void 0;
358
+ 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";
359
+ return toolFailure(
360
+ `SKIPPED: ${displayPath} ${seen}. Call Read (or mention the file) before modifying it.`,
361
+ {
362
+ code: "file_not_observed",
363
+ remediation: `Read the file first, then retry the ${retryVerb}.`
364
+ }
365
+ );
366
+ }
367
+
368
+ // src/memory-write-guard.ts
369
+ import { lstatSync, readFileSync } from "fs";
370
+ import { basename, relative as relative2, resolve as resolve2, sep } from "path";
371
+
372
+ // src/secret-detect.ts
373
+ var SECRET_PATTERNS = [
374
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
375
+ /\b(api[_-]?key|token|password|passwd|secret)\s*[:=]\s*\S{8,}/i,
376
+ /https?:\/\/[^\s/]+:[^\s/]+@/i,
377
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
378
+ /\bsk-[A-Za-z0-9_-]{20,}\b/
379
+ ];
380
+ function containsSecretPattern(text) {
381
+ return SECRET_PATTERNS.some((re) => re.test(text));
382
+ }
383
+ function looksLikeSecretOrUnfit(text) {
384
+ const trimmed = text.trim();
385
+ if (trimmed.length === 0) return "empty";
386
+ if (SECRET_PATTERNS.some((re) => re.test(trimmed))) return "looks like a secret";
387
+ const longTokens = trimmed.match(/[A-Za-z0-9+/=_-]{48,}/g) ?? [];
388
+ if (longTokens.some((token) => new Set(token).size > 20)) return "contains high-entropy text";
389
+ return null;
390
+ }
391
+
392
+ // src/memory-write-guard.ts
393
+ var TOPIC_FILE_WARN_BYTES = 4096;
394
+ var INDEX_FILE = "MEMORY.md";
395
+ var DELEGATED_TYPES = ["project", "reference"];
396
+ function refuse(code, message, remediation) {
397
+ return { ok: false, failure: toolFailure(message, { code, remediation }) };
398
+ }
399
+ var stripCr = (line) => line.replace(/\r$/, "");
400
+ function findFrontmatter(text) {
401
+ const lines = text.split("\n");
402
+ if (stripCr(lines[0] ?? "").trim() !== "---") return null;
403
+ for (let i = 1; i < lines.length; i++) {
404
+ if (stripCr(lines[i]) === "---") return { lines, close: i };
405
+ }
406
+ return null;
407
+ }
408
+ function metadataBlock({ lines, close }) {
409
+ const start = lines.findIndex((line, i) => i > 0 && i < close && /^metadata\s*:/.test(line));
410
+ if (start < 0) return null;
411
+ const rest = stripCr(lines[start]).replace(/^metadata\s*:/, "").replace(/\s+#.*$/, "").trim();
412
+ let last = start;
413
+ let indent = " ";
414
+ let sawChild = false;
415
+ for (let i = start + 1; i < close; i++) {
416
+ const line = stripCr(lines[i]);
417
+ if (line.trim() === "") continue;
418
+ if (!/^\s/.test(line)) break;
419
+ if (!sawChild) {
420
+ indent = /^\s*/.exec(line)?.[0] ?? " ";
421
+ sawChild = true;
422
+ }
423
+ last = i;
424
+ }
425
+ return {
426
+ start,
427
+ end: last + 1,
428
+ indent,
429
+ inline: rest === "" || rest.startsWith("#") ? null : rest
430
+ };
431
+ }
432
+ function scalar(raw) {
433
+ const value = raw.replace(/\s+#.*$/, "").trim();
434
+ return value.replace(/^(["'])(.*)\1$/, "$2");
435
+ }
436
+ function childKey(lines, block, key) {
437
+ for (let i = block.start + 1; i < block.end; i++) {
438
+ const line = stripCr(lines[i]);
439
+ if (!line.startsWith(block.indent)) continue;
440
+ const rest = line.slice(block.indent.length);
441
+ if (/^\s/.test(rest)) continue;
442
+ if (new RegExp(`^${key}\\s*:`).test(rest)) return i;
443
+ }
444
+ return -1;
445
+ }
446
+ function frontmatterValue(text, key) {
447
+ const fm = findFrontmatter(text);
448
+ if (!fm) return void 0;
449
+ const block = metadataBlock(fm);
450
+ if (block && block.inline === null) {
451
+ const at = childKey(fm.lines, block, key);
452
+ if (at >= 0)
453
+ return scalar(
454
+ stripCr(fm.lines[at]).slice(block.indent.length).replace(/^[^:]*:/, "")
455
+ );
456
+ }
457
+ for (let i = 1; i < fm.close; i++) {
458
+ const line = stripCr(fm.lines[i]);
459
+ if (new RegExp(`^${key}\\s*:`).test(line)) return scalar(line.replace(/^[^:]*:/, ""));
460
+ }
461
+ return void 0;
462
+ }
463
+ function yamlScalar(value) {
464
+ return /^[\w.:-]+$/.test(value) ? value : JSON.stringify(value);
465
+ }
466
+ function stamp(text, options) {
467
+ const fm = findFrontmatter(text);
468
+ if (!fm) return { text };
469
+ const { lines } = fm;
470
+ const cr = lines[0].endsWith("\r") ? "\r" : "";
471
+ let block = metadataBlock(fm);
472
+ if (block && block.inline !== null && block.inline !== "{}") {
473
+ return {
474
+ text,
475
+ warning: "The metadata block is written inline, so it was not stamped with provenance."
476
+ };
477
+ }
478
+ if (block && block.inline === "{}") {
479
+ lines[block.start] = `metadata:${cr}`;
480
+ block = { ...block, inline: null };
481
+ }
482
+ const wanted = [
483
+ {
484
+ key: "originSessionId",
485
+ value: (existing) => existing ? void 0 : options.sessionId
486
+ },
487
+ { key: "modified", value: () => (/* @__PURE__ */ new Date()).toISOString() },
488
+ ...options.driver ? [{ key: "driver", value: () => "agent" }] : []
489
+ ];
490
+ if (!block) {
491
+ const indent = " ";
492
+ const added = wanted.flatMap(({ key, value }) => {
493
+ const next = value(void 0);
494
+ return next === void 0 ? [] : [`${indent}${key}: ${yamlScalar(next)}${cr}`];
495
+ });
496
+ lines.splice(fm.close, 0, `metadata:${cr}`, ...added);
497
+ return { text: lines.join("\n") };
498
+ }
499
+ const inserts = [];
500
+ for (const { key, value } of wanted) {
501
+ const at = childKey(lines, block, key);
502
+ const line = at >= 0 ? stripCr(lines[at]) : "";
503
+ const existing = at >= 0 ? scalar(line.slice(block.indent.length).replace(/^[^:]*:/, "")) : void 0;
504
+ const next = value(existing || void 0);
505
+ if (next === void 0) continue;
506
+ if (at < 0) {
507
+ inserts.push(`${block.indent}${key}: ${yamlScalar(next)}${cr}`);
508
+ continue;
509
+ }
510
+ const comment = /(\s+#.*)$/.exec(line.slice(line.indexOf(":") + 1))?.[1] ?? "";
511
+ lines[at] = `${line.slice(0, line.indexOf(":") + 1)} ${yamlScalar(next)}${comment}${cr}`;
512
+ }
513
+ lines.splice(block.end, 0, ...inserts);
514
+ return { text: lines.join("\n") };
515
+ }
516
+ function entryTarget(line) {
517
+ return /^- \[[^\]]*\]\(([^()]+)\)/.exec(line)?.[1];
518
+ }
519
+ function normalizeLines(text) {
520
+ return text.replace(/\r\n/g, "\n").split("\n");
521
+ }
522
+ function readMemoryFile(root, name) {
523
+ const path = resolve2(root, name);
524
+ const rel = relative2(resolve2(root), path);
525
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) return null;
526
+ try {
527
+ if (!lstatSync(path).isFile()) return null;
528
+ return readFileSync(path, "utf-8");
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+ var isAgentOwned = (text) => frontmatterValue(text, "driver") === "agent";
534
+ var OWNED_BY_PERSON = "This session is driven by another agent, so it may change only memories it wrote itself (frontmatter `driver: agent`).";
535
+ function checkMemoryWrite(ctx, absPath, next, prior) {
536
+ const memory = ctx.memoryWrite;
537
+ if (!memory) {
538
+ return refuse(
539
+ "memory_write_unavailable",
540
+ "Memory is not writable in this session.",
541
+ "Only the main agent may write memory, and only with memory enabled and autoSave on."
542
+ );
543
+ }
544
+ const root = canonicalizePath(memory.root);
545
+ const rel = relative2(root, canonicalizePath(absPath)).split(sep).join("/");
546
+ const isIndex = rel.toLowerCase() === INDEX_FILE.toLowerCase();
547
+ const learnings = memory.scope === "learnings";
548
+ if (next !== null) {
549
+ let judged = next;
550
+ if (isIndex && prior !== null) {
551
+ const had = new Set(normalizeLines(prior));
552
+ judged = normalizeLines(next).filter((line) => !had.has(line)).join("\n");
553
+ }
554
+ const reason = judged.trim() === "" ? null : looksLikeSecretOrUnfit(judged);
555
+ if (reason) {
556
+ return refuse(
557
+ "memory_secret_rejected",
558
+ `Memory text ${reason}, so it was not saved.`,
559
+ "Remove the secret or high-entropy text (keys, tokens, passwords, long random strings) and write the memory again."
560
+ );
561
+ }
562
+ }
563
+ if (learnings) {
564
+ if (isIndex) {
565
+ if (prior !== null) {
566
+ const kept = new Set(next === null ? [] : normalizeLines(next));
567
+ for (const line of normalizeLines(prior)) {
568
+ if (kept.has(line)) continue;
569
+ const target = entryTarget(line);
570
+ if (target === void 0) continue;
571
+ const file = readMemoryFile(memory.root, target);
572
+ if (file !== null && !isAgentOwned(file)) {
573
+ return refuse(
574
+ "memory_owned_by_person",
575
+ `The index line for ${target} was changed or removed, but ${target} is not a memory this session wrote. ${OWNED_BY_PERSON}`,
576
+ "Keep that line as it is; add new lines for what you learned."
577
+ );
578
+ }
579
+ }
580
+ }
581
+ } else {
582
+ if (next !== null) {
583
+ const type = frontmatterValue(next, "type");
584
+ if (!type || !DELEGATED_TYPES.includes(type)) {
585
+ return refuse(
586
+ "memory_type_not_allowed",
587
+ `A memory written by this session needs frontmatter \`metadata.type\` of project or reference${type ? ` (got "${type}")` : " (none found)"}.`,
588
+ "Add a frontmatter block with metadata.type: project or reference. Preferences and feedback are a person\u2019s to record."
589
+ );
590
+ }
591
+ }
592
+ if (prior !== null && !isAgentOwned(prior)) {
593
+ return refuse(
594
+ "memory_owned_by_person",
595
+ `${rel} is not a memory this session wrote. ${OWNED_BY_PERSON}`,
596
+ "Write a new memory file instead."
597
+ );
598
+ }
599
+ }
600
+ }
601
+ if (next === null) return { ok: true, content: null, warnings: [] };
602
+ const warnings = [];
603
+ let content = next;
604
+ if (!isIndex) {
605
+ const stamped = stamp(next, { sessionId: memory.sessionId, driver: learnings });
606
+ content = stamped.text;
607
+ if (stamped.warning) warnings.push(stamped.warning);
608
+ }
609
+ if (isIndex) {
610
+ const trimmed = content.trim();
611
+ const lines = trimmed.length > 0 ? trimmed.split("\n").length : 0;
612
+ const bytes = Buffer.byteLength(trimmed);
613
+ if (lines > DEFAULT_MAX_INDEX_LINES || bytes > MAX_INDEX_BYTES) {
614
+ warnings.push(
615
+ `MEMORY.md is ${lines} lines and ${bytes} bytes; only the first ${DEFAULT_MAX_INDEX_LINES} lines or ${MAX_INDEX_BYTES} bytes load in a new session. Merge entries and keep each to one line.`
616
+ );
617
+ }
618
+ const missing = normalizeLines(content).flatMap((line) => {
619
+ const target = entryTarget(line);
620
+ return target !== void 0 && readMemoryFile(memory.root, target) === null ? [target] : [];
621
+ });
622
+ if (missing.length > 0) {
623
+ warnings.push(
624
+ `MEMORY.md points to ${[...new Set(missing)].join(", ")}, which ${missing.length === 1 ? "does" : "do"} not exist in the memory directory (yet).`
625
+ );
626
+ }
627
+ } else if (Buffer.byteLength(content) > TOPIC_FILE_WARN_BYTES) {
628
+ warnings.push(
629
+ `${rel} is ${Buffer.byteLength(content)} bytes; keep a memory file under ${TOPIC_FILE_WARN_BYTES} bytes (one fact per file).`
630
+ );
631
+ }
632
+ return { ok: true, content, warnings };
633
+ }
634
+ function noticeMemoryWrite(ctx, absPath, change) {
635
+ const name = basename(absPath);
636
+ if (name.toLowerCase() === INDEX_FILE.toLowerCase()) return;
637
+ ctx.onNotice?.(`memory ${change}: ${name}`);
638
+ }
639
+ function withMemoryWarnings(content, warnings) {
640
+ return warnings.length === 0 ? content : `${content}
641
+
642
+ ${warnings.map((w) => `Warning: ${w}`).join("\n")}`;
643
+ }
644
+
645
+ // src/config.ts
646
+ import { readFileSync as readFileSync3 } from "fs";
647
+
648
+ // src/legacy-config.ts
649
+ import { z } from "zod";
650
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
651
+ import { join } from "path";
652
+ var legacyUrlSchema = z.string().check((ctx) => {
653
+ if (!URL.canParse(ctx.value)) {
654
+ ctx.issues.push({ code: "invalid_format", format: "url", input: ctx.value });
655
+ }
656
+ });
657
+ var legacyConfigSchema = z.object({
658
+ model: z.string().optional(),
659
+ baseUrl: legacyUrlSchema.optional(),
660
+ maxTurns: unboundedInt().min(1).optional(),
661
+ maxTokens: unboundedInt().min(1e3).optional(),
662
+ autoCompactEnabled: z.boolean().optional(),
663
+ animation: z.object({
664
+ typewriterSpeed: z.number().int().min(1).max(50).default(3),
665
+ spinnerStyle: z.enum(["braille", "dots"]).default("braille")
666
+ }).optional(),
667
+ accessibility: z.object({
668
+ screenReader: z.boolean().default(false),
669
+ reducedMotion: z.boolean().default(false)
670
+ }).optional()
671
+ });
672
+ function legacyConfigPath(workspace) {
673
+ return join(workspace, ".bookrc.json");
674
+ }
675
+ function parseLegacyConfig(workspace) {
676
+ const path = legacyConfigPath(workspace);
677
+ if (reachesUncPath(path) || !existsSync(path)) return null;
678
+ try {
679
+ return legacyConfigSchema.parse(JSON.parse(readFileSync2(path, "utf-8")));
680
+ } catch (e) {
681
+ if (e instanceof SyntaxError) {
682
+ throw new Error(`Invalid JSON in config file: ${path}`);
683
+ }
684
+ throw e;
685
+ }
686
+ }
687
+ function readLegacyBaseUrl(workspace) {
688
+ try {
689
+ return parseLegacyConfig(workspace)?.baseUrl;
690
+ } catch {
691
+ return void 0;
692
+ }
693
+ }
694
+
695
+ // src/commands/effort.ts
696
+ var EFFORT_LEVELS = effortLevelSchema.options;
697
+ var EFFORT_USAGE = "Usage: /effort [low|medium|high|xhigh|max]";
698
+ function parseEffortLevel(raw, source) {
699
+ const normalized = raw.trim().toLowerCase();
700
+ if (!isEffortLevel(normalized)) {
701
+ throw new Error(`${source} must be one of: ${EFFORT_LEVELS.join(", ")} (got "${raw}")`);
702
+ }
703
+ return normalized;
704
+ }
705
+ function isEffortLevel(value) {
706
+ return EFFORT_LEVELS.includes(value);
707
+ }
708
+ function getAvailableEffortLevels(config) {
709
+ const metadata = config.modelInfo?.effort;
710
+ if (metadata === false) return null;
711
+ if (typeof metadata === "object" && metadata.levels) {
712
+ return EFFORT_LEVELS.filter((level) => metadata.levels?.includes(level));
713
+ }
714
+ return [...EFFORT_LEVELS];
715
+ }
716
+ function getEffortUnavailableError(config) {
717
+ const model = config.modelSelection ?? config.model;
718
+ const levels = getAvailableEffortLevels(config);
719
+ if (levels === null) return `Model "${model}" does not support configurable effort.`;
720
+ if (levels.length === 0) return `Model "${model}" does not expose any effort levels.`;
721
+ return void 0;
722
+ }
723
+ function updateEffortLevel(config, level, persist, apply) {
724
+ const unavailable = getEffortUnavailableError(config);
725
+ if (unavailable) return { ok: false, error: unavailable };
726
+ const levels = getAvailableEffortLevels(config) ?? [];
727
+ if (!levels.includes(level)) {
728
+ const model = config.modelSelection ?? config.model;
729
+ return {
730
+ ok: false,
731
+ error: `Effort level "${level}" is not supported by model "${model}". Available levels: ${levels.join(", ")}.`
732
+ };
733
+ }
734
+ const result = persist(level);
735
+ if (!result.ok) {
736
+ return {
737
+ ok: false,
738
+ error: `Failed to save effort level${result.error ? `: ${result.error}` : "."}`
739
+ };
740
+ }
741
+ apply(level);
742
+ return { ok: true };
743
+ }
744
+
745
+ // src/config.ts
746
+ var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
747
+ var DEFAULT_MAX_OUTPUT_TOKENS = 64e3;
748
+ function loadLegacyConfig(workspace) {
749
+ const parsed = parseLegacyConfig(workspace);
750
+ if (!parsed) return null;
751
+ console.warn(
752
+ "\u26A0 .bookrc.json is deprecated. Move your settings to .book/settings.json (project), .book/settings.local.json (local overrides), or ~/.book/settings.json (user)."
753
+ );
754
+ return {
755
+ model: parsed.model,
756
+ baseUrl: parsed.baseUrl,
757
+ maxTurns: parsed.maxTurns,
758
+ maxTokens: parsed.maxTokens,
759
+ autoCompactEnabled: parsed.autoCompactEnabled,
760
+ animation: parsed.animation,
761
+ accessibility: parsed.accessibility
762
+ };
763
+ }
764
+ function loadConfig(workspace, options) {
765
+ const settingsOverridePath = options?.settingsOverridePath;
766
+ const noSettings = options?.noSettings ?? false;
767
+ const resolvedWorkspace = workspace || process.env.BOOK_WORKSPACE || process.cwd();
768
+ const resolveLayers = () => noSettings ? { settings: structuredClone(DEFAULT_SETTINGS), providerGate: emptyWorkspaceProviderGate() } : resolveSettingsWithProviderGate(
769
+ resolvedWorkspace,
770
+ settingsOverridePath,
771
+ options?.settingsPaths
772
+ );
773
+ let { settings, providerGate } = resolveLayers();
774
+ if (!noSettings && options?.runMigrations) {
775
+ const migrated = migrateLegacyPermissions(resolvedWorkspace, void 0, settings);
776
+ if (migrated) ({ settings, providerGate } = resolveLayers());
777
+ }
778
+ settings = applySettingsEnvOverrides(settings);
779
+ const legacy = loadLegacyConfig(resolvedWorkspace);
780
+ const retry = {
781
+ maxAttempts: process.env.BOOK_RETRY_MAX_ATTEMPTS ? clampInt(process.env.BOOK_RETRY_MAX_ATTEMPTS, 0, 15) : settings.retry?.maxAttempts ?? DEFAULT_SETTINGS.retry.maxAttempts,
782
+ baseDelayMs: process.env.BOOK_RETRY_BASE_DELAY_MS ? clampInt(process.env.BOOK_RETRY_BASE_DELAY_MS, 100, 6e4) : settings.retry?.baseDelayMs ?? DEFAULT_SETTINGS.retry.baseDelayMs,
783
+ maxDelayMs: process.env.BOOK_RETRY_MAX_DELAY_MS ? clampInt(process.env.BOOK_RETRY_MAX_DELAY_MS, 100, 3e5) : settings.retry?.maxDelayMs ?? DEFAULT_SETTINGS.retry.maxDelayMs,
784
+ totalBudgetMs: process.env.BOOK_RETRY_TOTAL_BUDGET_MS ? clampInt(process.env.BOOK_RETRY_TOTAL_BUDGET_MS, 0, 6e5) : settings.retry?.totalBudgetMs ?? DEFAULT_SETTINGS.retry.totalBudgetMs,
785
+ requestTimeoutMs: process.env.BOOK_REQUEST_TIMEOUT_MS ? clampInt(process.env.BOOK_REQUEST_TIMEOUT_MS, 5e3, 6e5) : settings.retry?.requestTimeoutMs ?? DEFAULT_SETTINGS.retry.requestTimeoutMs,
786
+ streamStallTimeoutMs: process.env.BOOK_STREAM_STALL_TIMEOUT_MS ? clampInt(process.env.BOOK_STREAM_STALL_TIMEOUT_MS, 5e3, 12e4) : settings.retry?.streamStallTimeoutMs ?? DEFAULT_SETTINGS.retry.streamStallTimeoutMs,
787
+ thinkingStallTimeoutMs: process.env.BOOK_THINKING_STALL_TIMEOUT_MS ? clampInt(process.env.BOOK_THINKING_STALL_TIMEOUT_MS, 1e4, 18e5) : settings.retry?.thinkingStallTimeoutMs ?? DEFAULT_SETTINGS.retry.thinkingStallTimeoutMs,
788
+ firstDeltaStallTimeoutMs: process.env.BOOK_FIRST_DELTA_STALL_TIMEOUT_MS ? clampInt(process.env.BOOK_FIRST_DELTA_STALL_TIMEOUT_MS, 5e3, 18e5) : settings.retry?.firstDeltaStallTimeoutMs ?? DEFAULT_SETTINGS.retry.firstDeltaStallTimeoutMs,
789
+ toolRetries: process.env.BOOK_TOOL_RETRIES ? clampInt(process.env.BOOK_TOOL_RETRIES, 0, 3) : settings.retry?.toolRetries ?? DEFAULT_SETTINGS.retry.toolRetries,
790
+ watchdog: process.env.BOOK_RETRY_WATCHDOG === "1" || settings.retry?.watchdog === true,
791
+ streamReissueAttempts: process.env.BOOK_STREAM_REISSUE_ATTEMPTS ? clampInt(process.env.BOOK_STREAM_REISSUE_ATTEMPTS, 0, 10) : settings.retry?.streamReissueAttempts ?? DEFAULT_SETTINGS.retry.streamReissueAttempts,
792
+ outputCapContinuations: settings.retry?.outputCapContinuations ?? DEFAULT_SETTINGS.retry.outputCapContinuations
793
+ };
794
+ const memoryContext = settings.memory.enabled ? loadMemoryContext(resolvedWorkspace) : void 0;
795
+ const envMaxTokens = parsePositiveInt(process.env.BOOK_MAX_TOKENS, "BOOK_MAX_TOKENS");
796
+ const maxTokensExplicit = envMaxTokens !== void 0 || settings.maxTokens !== void 0 || legacy?.maxTokens !== void 0;
797
+ const effortExplicit = Boolean(
798
+ options?.effortOverride || process.env.BOOK_EFFORT || settings.effort
799
+ );
800
+ const withheldIds = new Set(providerGate.withheld);
801
+ const selectedModel = options?.modelOverride || process.env.BOOK_MODEL || settings.model;
802
+ const legacyPrefix = selectedModel || legacy?.model === void 0 ? void 0 : providerPrefixOf(legacy.model);
803
+ const withheldLegacyModel = legacy?.model !== void 0 && legacyPrefix !== void 0 && withheldIds.has(legacyPrefix) ? {
804
+ key: "model",
805
+ value: legacy.model,
806
+ providerId: legacyPrefix,
807
+ path: legacyConfigPath(resolvedWorkspace),
808
+ fallback: void 0
809
+ } : void 0;
810
+ const rawModel = selectedModel || (withheldLegacyModel ? void 0 : legacy?.model) || "gpt-4o";
811
+ const compactModel = process.env.BOOK_COMPACT_MODEL || settings.compactModel;
812
+ const advisorModel = process.env.BOOK_ADVISOR_MODEL || settings.advisorModel;
813
+ const compactEffort = settings.compactEffort;
814
+ const compactStrategy = "summary";
815
+ const shell = resolveShell({ requested: settings.shell });
816
+ const defaultApiKey = process.env.BOOK_API_KEY || "";
817
+ const legacyBaseUrlGate = legacy?.baseUrl === void 0 ? void 0 : {
818
+ baseUrl: legacy.baseUrl,
819
+ state: evaluateLegacyBaseUrl(
820
+ noSettings ? loadWorkspaceTrust(
821
+ resolvedWorkspace,
822
+ options?.settingsPaths?.trustStorePath ?? defaultTrustStorePath(options?.settingsPaths?.home)
823
+ ).workspaceProviders : settings.workspaceProviders,
824
+ legacy.baseUrl
825
+ )
826
+ };
827
+ const legacyBaseUrl = legacyBaseUrlGate?.state === "approved" ? legacyBaseUrlGate.baseUrl : void 0;
828
+ const explicitBaseUrl = process.env.BOOK_BASE_URL || legacyBaseUrl;
829
+ const defaultProviderOverride = validateProvider(process.env.BOOK_PROVIDER) || "auto";
830
+ const defaultBaseUrl = explicitBaseUrl || DEFAULT_OPENAI_BASE_URL;
831
+ const defaultMaxTokens = envMaxTokens ?? settings.maxTokens ?? legacy?.maxTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
832
+ const defaultEffort = options?.effortOverride || validateEffort(process.env.BOOK_EFFORT) || settings.effort || "high";
833
+ const defaultProvider = defaultProviderOverride;
834
+ let config = {
835
+ apiKey: defaultApiKey,
836
+ baseUrl: defaultBaseUrl,
837
+ model: rawModel,
838
+ modelSelection: rawModel,
839
+ compactModel,
840
+ advisorModel,
841
+ compactEffort,
842
+ compactStrategy,
843
+ // Undefined = unlimited. Only set when env/settings/legacy explicitly provide a value.
844
+ maxTurns: process.env.BOOK_MAX_TURNS ? parseInt(process.env.BOOK_MAX_TURNS, 10) : settings.maxTurns ?? legacy?.maxTurns,
845
+ maxTokens: defaultMaxTokens,
846
+ maxTokensExplicit,
847
+ defaultMaxTokens,
848
+ effortExplicit,
849
+ defaultEffort,
850
+ defaultApiKey,
851
+ defaultBaseUrl,
852
+ defaultBaseUrlExplicit: Boolean(process.env.BOOK_BASE_URL),
853
+ workspaceProviderGate: {
854
+ ...providerGate,
855
+ withheldSelections: withheldLegacyModel ? [...providerGate.withheldSelections, withheldLegacyModel] : providerGate.withheldSelections,
856
+ // What `--model` and the environment chose counts too: those outrank every settings layer.
857
+ unaffectedSelections: withEnvironmentSelections(
858
+ providerGate.unaffectedSelections,
859
+ environmentModelSelections(options?.modelOverride),
860
+ withheldIds
861
+ ),
862
+ legacyBaseUrl: legacyBaseUrlGate
863
+ },
864
+ defaultProvider,
865
+ autoCompactEnabled: settings.autoCompactEnabled ?? legacy?.autoCompactEnabled ?? true,
866
+ workspace: resolvedWorkspace,
867
+ animation: legacy?.animation || { typewriterSpeed: 3, spinnerStyle: "braille" },
868
+ accessibility: legacy?.accessibility || { screenReader: false, reducedMotion: false },
869
+ settings,
870
+ shell,
871
+ settingsContext: {
872
+ overridePath: settingsOverridePath,
873
+ noSettings
874
+ },
875
+ retry,
876
+ memoryContext,
877
+ effort: defaultEffort,
878
+ provider: defaultProvider,
879
+ modelWindowStore: options?.modelWindowStore ?? createModelWindowStore()
880
+ };
881
+ config = applyModelDefaults(
882
+ resolveModelProviderConfig(config, rawModel, { trustedSelection: true })
883
+ );
884
+ const rawPrefix = providerPrefixOf(rawModel);
885
+ config.modelProviderWarning = rawPrefix !== void 0 && withheldIds.has(rawPrefix) ? void 0 : describeUnresolvedProviderPrefix(settings.provider, rawModel.trim());
886
+ if (!config.apiKey && !options?.allowMissingApiKey) {
887
+ throw new Error(
888
+ "BOOK_API_KEY or provider.<id>.apiKey not set. Set BOOK_API_KEY or use {env:VAR} in settings." + (hadRemovedAuthConfiguration(
889
+ noSettings ? [] : settingsLayerPaths(resolvedWorkspace, settingsOverridePath, options?.settingsPaths),
890
+ process.env
891
+ ) ? "\n\nSubscription authentication (`book auth login`) was removed in this version, and Book authenticates with API keys only. The auth configuration still on this machine is no longer read; `book doctor` lists it and what to delete." : "")
892
+ );
893
+ }
894
+ return config;
895
+ }
896
+ function inheritedApiKey(config, baseUrl) {
897
+ if (config.defaultApiKey) return config.defaultApiKey;
898
+ return sameEndpoint(baseUrl, config.baseUrl) ? config.apiKey : "";
899
+ }
900
+ function sameEndpoint(a, b) {
901
+ const normalise = (raw) => {
902
+ const trimmed = raw.trim();
903
+ try {
904
+ const url = new URL(trimmed);
905
+ const auth = url.username || url.password ? `${url.username}:${url.password}@` : "";
906
+ return `${url.protocol}//${auth}${url.host}${url.pathname.replace(/\/+$/, "")}${url.search}`;
907
+ } catch {
908
+ return trimmed.replace(/\/+$/, "");
909
+ }
910
+ };
911
+ return normalise(a) === normalise(b);
912
+ }
913
+ function plainModelConfig(config, model) {
914
+ const baseUrl = config.defaultBaseUrl || config.baseUrl;
915
+ return {
916
+ ...config,
917
+ apiKey: inheritedApiKey(config, baseUrl),
918
+ baseUrl,
919
+ model,
920
+ modelSelection: model,
921
+ modelInfo: void 0,
922
+ provider: config.defaultProvider ?? config.provider
923
+ };
924
+ }
925
+ function isEffortChosen(config) {
926
+ return config.effortChosen ?? config.effortExplicit === true;
927
+ }
928
+ function applyModelDefaults(config) {
929
+ const maxTokens = config.maxTokensExplicit ? config.maxTokens : config.modelInfo?.maxOutputTokens ?? config.defaultMaxTokens ?? config.maxTokens;
930
+ let effort = config.effort;
931
+ if (!isEffortChosen(config)) {
932
+ if (config.modelInfo?.effort === false) {
933
+ effort = void 0;
934
+ } else if (typeof config.modelInfo?.effort === "object" && config.modelInfo.effort.default) {
935
+ effort = config.modelInfo.effort.default;
936
+ } else {
937
+ effort = config.defaultEffort ?? effort;
938
+ }
939
+ }
940
+ return { ...config, maxTokens, effort };
941
+ }
942
+ function freezeAgentConfig(config) {
943
+ return deepFreeze(config);
944
+ }
945
+ function deepFreeze(value) {
946
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
947
+ Object.freeze(value);
948
+ for (const child of Object.values(value)) deepFreeze(child);
949
+ return value;
950
+ }
951
+ function describeUnresolvedProviderPrefix(providers, rawModel) {
952
+ const slash = rawModel.indexOf("/");
953
+ if (slash <= 0 || slash === rawModel.length - 1) return void 0;
954
+ const prefix = rawModel.slice(0, slash);
955
+ const configured = Object.keys(providers);
956
+ if (configured.length === 0 || configured.includes(prefix)) return void 0;
957
+ return `Model "${rawModel}" names provider "${prefix}", which is not configured (configured: ${[...configured].sort().join(", ")}). Treating the whole id as a model name against the default endpoint. Add provider.${prefix} to settings, or use one of the configured ids.`;
958
+ }
959
+ function isWithheldWorkspaceProvider(config, providerId) {
960
+ return !Object.hasOwn(config.settings.provider, providerId) && (config.workspaceProviderGate?.withheld.includes(providerId) ?? false);
961
+ }
962
+ function resolveModelProviderConfig(config, rawModel = config.model, options = {}) {
963
+ const selection = rawModel.trim();
964
+ const providerId = providerPrefixOf(selection);
965
+ if (providerId === void 0) return plainModelConfig(config, selection);
966
+ if (providerId.length === selection.length - 1) {
967
+ throw new Error(`Invalid model "${selection}". Expected "provider/model".`);
968
+ }
969
+ const model = selection.slice(providerId.length + 1);
970
+ if (!Object.hasOwn(config.settings.provider, providerId)) {
971
+ if (!options.trustedSelection && isWithheldWorkspaceProvider(config, providerId)) {
972
+ throw new WorkspaceProviderRefusalError(providerId, selection, config.workspace);
973
+ }
974
+ return plainModelConfig(config, selection);
975
+ }
976
+ const provider = config.settings.provider[providerId];
977
+ const baseUrl = provider.baseURL ?? provider.baseUrl ?? config.defaultBaseUrl ?? config.baseUrl;
978
+ const apiKey = config.workspaceProviderGate?.declared.includes(providerId) ? resolveSecret(provider.apiKey, config.workspace) ?? "" : resolveSecret(provider.apiKey, config.workspace) ?? inheritedApiKey(config, baseUrl);
979
+ return {
980
+ ...config,
981
+ apiKey,
982
+ baseUrl,
983
+ model,
984
+ modelSelection: selection,
985
+ modelInfo: provider.models[model],
986
+ provider: provider.type
987
+ };
988
+ }
989
+ function clampEffortToCatalog(config, effort, options = {}) {
990
+ const levels = getAvailableEffortLevels(config);
991
+ if (levels === null || effort === void 0) return void 0;
992
+ const ceiling = EFFORT_LEVELS.indexOf(effort);
993
+ const clamped = levels.filter((level) => EFFORT_LEVELS.indexOf(level) <= ceiling).at(-1);
994
+ return clamped ?? (options.raiseToLowest ? levels[0] : void 0);
995
+ }
996
+ function compactModelEffort(config) {
997
+ const cap = EFFORT_LEVELS.indexOf("medium");
998
+ const target = config.compactEffort ?? (config.effort && EFFORT_LEVELS.indexOf(config.effort) > cap ? "medium" : config.effort);
999
+ return clampEffortToCatalog(config, target, {
1000
+ raiseToLowest: config.compactEffort !== void 0
1001
+ });
1002
+ }
1003
+ function resolveEffortExplicit(config, effort, chosen) {
1004
+ if (effort === void 0) return false;
1005
+ if (chosen) return true;
1006
+ const catalog = config.modelInfo?.effort;
1007
+ if (typeof catalog !== "object") return false;
1008
+ return catalog.levels ? catalog.levels.includes(effort) : catalog.default !== void 0;
1009
+ }
1010
+ function compactsOnSessionModel(config) {
1011
+ const compactModel = config.compactModel?.trim() || config.settings?.compactModel?.trim();
1012
+ return !compactModel || compactModel === config.modelSelection || compactModel === config.model;
1013
+ }
1014
+ function resolveCompactModelConfig(config) {
1015
+ const compactModel = config.compactModel?.trim() || config.settings.compactModel?.trim();
1016
+ const resolved = compactsOnSessionModel(config) ? config : applyModelDefaults(
1017
+ resolveModelProviderConfig(config, compactModel, { trustedSelection: true })
1018
+ );
1019
+ const effort = compactModelEffort(resolved);
1020
+ const chosen = resolved.compactEffort !== void 0 || isEffortChosen(resolved);
1021
+ return {
1022
+ ...resolved,
1023
+ effort,
1024
+ effortChosen: chosen,
1025
+ effortExplicit: resolveEffortExplicit(resolved, effort, chosen)
1026
+ };
1027
+ }
1028
+ function configuredAdvisorModel(config) {
1029
+ const configured = config.advisorModel?.trim() || config.settings?.advisorModel?.trim() || void 0;
1030
+ return configured?.toLowerCase() === ADVISOR_OFF_VALUE ? void 0 : configured;
1031
+ }
1032
+ function advisorModelRefusal(config, advisorModel) {
1033
+ const id = advisorModel.trim();
1034
+ if (id.startsWith("/") || id.endsWith("/")) {
1035
+ return `advisor model "${id}" is not a model id; use <provider>/<model>`;
1036
+ }
1037
+ const slash = id.indexOf("/");
1038
+ if (id === config.modelSelection || id === config.model) return void 0;
1039
+ const providers = config.settings.provider;
1040
+ if (slash > 0 && Object.hasOwn(providers, id.slice(0, slash))) return void 0;
1041
+ if (config.defaultBaseUrlExplicit) return void 0;
1042
+ const resolved = resolveModelProviderConfig(config, id, { trustedSelection: true });
1043
+ if (sameEndpoint(resolved.baseUrl, config.baseUrl) && resolved.apiKey === config.apiKey) {
1044
+ return void 0;
1045
+ }
1046
+ const configured = Object.keys(providers).sort();
1047
+ return `advisor model "${id}" names no configured provider, so its key and endpoint are ambiguous; use <provider>/<model>${configured.length ? ` (configured: ${configured.join(", ")})` : ""}`;
1048
+ }
1049
+ function resolveAdvisorModelConfig(config) {
1050
+ const advisorModel = configuredAdvisorModel(config);
1051
+ if (!advisorModel) return void 0;
1052
+ const resolved = advisorModel === config.modelSelection || advisorModel === config.model ? config : applyModelDefaults(
1053
+ resolveModelProviderConfig(config, advisorModel, { trustedSelection: true })
1054
+ );
1055
+ const capped = resolved.effort && EFFORT_LEVELS.indexOf(resolved.effort) > EFFORT_LEVELS.indexOf("high") ? "high" : resolved.effort;
1056
+ const effort = clampEffortToCatalog(resolved, capped);
1057
+ const chosen = isEffortChosen(resolved);
1058
+ return {
1059
+ ...resolved,
1060
+ effort,
1061
+ effortChosen: chosen,
1062
+ effortExplicit: resolveEffortExplicit(resolved, effort, chosen)
1063
+ };
1064
+ }
1065
+ function resolveReducerModelConfig(config) {
1066
+ const resolved = resolveCompactModelConfig(config);
1067
+ return {
1068
+ ...resolved,
1069
+ // A reducer request that dies before its first byte twice is not going to succeed
1070
+ // at that size, and runCompaction already falls back to the deterministic
1071
+ // checkpoint when the model path fails — ten attempts at five minutes each only
1072
+ // delayed that fallback. The watchdog retries 429/529 without limit, which
1073
+ // would lift this cap, so it is off for the reducer.
1074
+ retry: {
1075
+ ...resolved.retry,
1076
+ maxAttempts: Math.min(resolved.retry.maxAttempts, 2),
1077
+ watchdog: false
1078
+ }
1079
+ };
1080
+ }
1081
+ function resolveSecret(raw, workspace) {
1082
+ if (!raw) return void 0;
1083
+ const reference = parseSecretReference(raw);
1084
+ if (reference.kind === "env") return process.env[reference.name];
1085
+ if (reference.kind === "literal") return raw;
1086
+ const file = resolveSecretFilePath(reference.path, workspace);
1087
+ if (!file.readable) return void 0;
1088
+ try {
1089
+ return readFileSync3(file.path, "utf-8").trim();
1090
+ } catch {
1091
+ return void 0;
1092
+ }
1093
+ }
1094
+ function validateEffort(raw) {
1095
+ if (!raw) return void 0;
1096
+ const normalized = raw.trim().toLowerCase();
1097
+ return isEffortLevel(normalized) ? normalized : void 0;
1098
+ }
1099
+ var VALID_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "auto"]);
1100
+ function validateProvider(raw) {
1101
+ if (!raw) return void 0;
1102
+ const normalized = raw.trim().toLowerCase();
1103
+ return VALID_PROVIDERS.has(normalized) ? normalized : void 0;
1104
+ }
1105
+ function clampInt(raw, min, max) {
1106
+ const n = parseInt(raw, 10);
1107
+ if (Number.isNaN(n)) return min;
1108
+ return Math.max(min, Math.min(max, n));
1109
+ }
1110
+ function parsePositiveInt(raw, name) {
1111
+ if (raw === void 0 || raw.trim() === "") return void 0;
1112
+ const n = Number(raw);
1113
+ if (!Number.isInteger(n) || n <= 0) {
1114
+ throw new Error(`${name} must be a positive integer`);
1115
+ }
1116
+ return n;
1117
+ }
1118
+
1119
+ // src/provider-secrets.ts
1120
+ var PROVIDER_KEY_MARKER = "[redacted: provider key]";
1121
+ var MIN_PROVIDER_SECRET_LENGTH = 8;
1122
+ var PLACEHOLDER_PROVIDER_KEYS = [
1123
+ "not-needed",
1124
+ "lm-studio",
1125
+ "sk-no-key-required",
1126
+ "no-key-required",
1127
+ "no-api-key",
1128
+ "dummy-key",
1129
+ "placeholder"
1130
+ ];
1131
+ function collectProviderSecrets(config) {
1132
+ const candidates = [config.apiKey, process.env.BOOK_API_KEY];
1133
+ for (const [id, provider] of Object.entries(config.settings.provider ?? {})) {
1134
+ if (isWithheldWorkspaceProvider(config, id)) continue;
1135
+ candidates.push(resolveSecret(provider.apiKey, config.workspace));
1136
+ }
1137
+ const secrets = /* @__PURE__ */ new Set();
1138
+ for (const candidate of candidates) {
1139
+ if (typeof candidate !== "string") continue;
1140
+ const value = candidate.trim();
1141
+ if (value.length < MIN_PROVIDER_SECRET_LENGTH) continue;
1142
+ if (PLACEHOLDER_PROVIDER_KEYS.includes(value.toLowerCase())) continue;
1143
+ secrets.add(value);
1144
+ }
1145
+ return [...secrets].sort((a, b) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0));
1146
+ }
1147
+ function redactSecrets(text, secrets) {
1148
+ if (!text || secrets.length === 0) return text;
1149
+ const spans = [];
1150
+ for (const secret of secrets) {
1151
+ if (!secret) continue;
1152
+ for (let at = text.indexOf(secret); at !== -1; at = text.indexOf(secret, at + 1)) {
1153
+ spans.push([at, at + secret.length]);
1154
+ }
1155
+ }
1156
+ if (spans.length === 0) return text;
1157
+ spans.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
1158
+ let out = "";
1159
+ let cursor = 0;
1160
+ let [start, end] = spans[0];
1161
+ const flush = () => {
1162
+ out += text.slice(cursor, start) + PROVIDER_KEY_MARKER;
1163
+ cursor = end;
1164
+ };
1165
+ for (const span of spans.slice(1)) {
1166
+ if (span[0] < end) {
1167
+ end = Math.max(end, span[1]);
1168
+ continue;
1169
+ }
1170
+ flush();
1171
+ [start, end] = span;
1172
+ }
1173
+ flush();
1174
+ return out + text.slice(cursor);
1175
+ }
1176
+ function secretSafeCut(text, limit, secrets) {
1177
+ const wanted = Math.min(Math.max(1, limit), text.length);
1178
+ if (wanted >= text.length || secrets.length === 0) return wanted;
1179
+ const spanning = (cut) => {
1180
+ const found = [];
1181
+ for (const secret of secrets) {
1182
+ if (!secret) continue;
1183
+ const from = Math.max(0, cut - secret.length + 1);
1184
+ for (let at = text.indexOf(secret, from); at !== -1 && at < cut; ) {
1185
+ found.push([at, at + secret.length]);
1186
+ at = text.indexOf(secret, at + 1);
1187
+ }
1188
+ }
1189
+ return found;
1190
+ };
1191
+ let back = wanted;
1192
+ for (let spans = spanning(back); spans.length > 0; spans = spanning(back)) {
1193
+ back = Math.min(...spans.map(([start]) => start));
1194
+ }
1195
+ if (back > 0) return back;
1196
+ let forward = wanted;
1197
+ for (let spans = spanning(forward); spans.length > 0; spans = spanning(forward)) {
1198
+ forward = Math.max(...spans.map(([, end]) => end));
1199
+ }
1200
+ return forward;
1201
+ }
1202
+ function secretSafeStart(text, start, secrets) {
1203
+ let safe = Math.min(Math.max(0, start), text.length);
1204
+ if (safe === 0 || safe >= text.length || secrets.length === 0) return safe;
1205
+ for (; ; ) {
1206
+ let earliest = safe;
1207
+ for (const secret of secrets) {
1208
+ if (!secret) continue;
1209
+ const from = Math.max(0, safe - secret.length + 1);
1210
+ for (let at = text.indexOf(secret, from); at !== -1 && at < safe; ) {
1211
+ earliest = Math.min(earliest, at);
1212
+ at = text.indexOf(secret, at + 1);
1213
+ }
1214
+ }
1215
+ if (earliest === safe) return safe;
1216
+ safe = earliest;
1217
+ }
1218
+ }
1219
+ function isPlainObject(value) {
1220
+ if (typeof value !== "object" || value === null) return false;
1221
+ const proto = Object.getPrototypeOf(value);
1222
+ return proto === Object.prototype || proto === null;
1223
+ }
1224
+ function redactValue(value, secrets, ancestors) {
1225
+ if (typeof value === "string") return redactSecrets(value, secrets);
1226
+ if (typeof value !== "object" || value === null) return value;
1227
+ const isArray = Array.isArray(value);
1228
+ if (!isArray && !isPlainObject(value)) return value;
1229
+ if (ancestors.has(value)) return value;
1230
+ ancestors.add(value);
1231
+ try {
1232
+ if (isArray) {
1233
+ let copy2;
1234
+ value.forEach((item, index) => {
1235
+ const next = redactValue(item, secrets, ancestors);
1236
+ if (next === item) return;
1237
+ copy2 ??= [...value];
1238
+ copy2[index] = next;
1239
+ });
1240
+ return copy2 ?? value;
1241
+ }
1242
+ const record = value;
1243
+ let copy;
1244
+ for (const [key, item] of Object.entries(record)) {
1245
+ const next = redactValue(item, secrets, ancestors);
1246
+ if (next === item) continue;
1247
+ copy ??= { ...record };
1248
+ copy[key] = next;
1249
+ }
1250
+ return copy ?? value;
1251
+ } finally {
1252
+ ancestors.delete(value);
1253
+ }
1254
+ }
1255
+ var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["version", "toolCallId", "status"]);
1256
+ function redactToolResultSecrets(result, secrets) {
1257
+ if (secrets.length === 0) return result;
1258
+ const ancestors = /* @__PURE__ */ new Set();
1259
+ let copy;
1260
+ for (const [key, value] of Object.entries(result)) {
1261
+ if (IDENTITY_FIELDS.has(key)) continue;
1262
+ const next = redactValue(value, secrets, ancestors);
1263
+ if (next === value) continue;
1264
+ copy ??= { ...result };
1265
+ copy[key] = next;
1266
+ }
1267
+ return copy ?? result;
1268
+ }
1269
+
1270
+ // src/tools/redacted-secret-guard.ts
1271
+ function holdsProviderKey(ctx, text) {
1272
+ const secrets = ctx.providerSecrets;
1273
+ return !!secrets?.length && secrets.some((secret) => text.includes(secret));
1274
+ }
1275
+ function redactedSecretOverwrite(ctx, shownPath, existing, incoming) {
1276
+ if (!incoming.some((text) => text.includes(PROVIDER_KEY_MARKER))) return void 0;
1277
+ if (!holdsProviderKey(ctx, existing)) return void 0;
1278
+ return toolFailure(
1279
+ `${shownPath} holds a provider key, which Book shows as ${PROVIDER_KEY_MARKER}. Writing that marker back would replace the real key.`,
1280
+ {
1281
+ code: "redacted_secret_overwrite",
1282
+ remediation: "Use Edit on only the lines that need to change, without the line that shows the marker, or ask the user to make the change."
1283
+ }
1284
+ );
1285
+ }
1286
+ function markerCount(text) {
1287
+ return text.split(PROVIDER_KEY_MARKER).length - 1;
1288
+ }
1289
+ function redactedSecretAddition(ctx, shownPath, before, after) {
1290
+ if (!ctx.providerSecrets?.length) return void 0;
1291
+ if (markerCount(after) <= markerCount(before)) return void 0;
1292
+ return toolFailure(
1293
+ `Writing ${shownPath} would add ${PROVIDER_KEY_MARKER}, which stands for a real provider key that the model cannot see. Book does not write the marker to any file while a provider key is configured.`,
1294
+ {
1295
+ code: "redacted_secret_write",
1296
+ remediation: "Write a placeholder of your own instead, for example <your-api-key>, or ask the user to write that text."
1297
+ }
1298
+ );
1299
+ }
1300
+ function redactedSecretDeletion(ctx, shownPath, existing, subject = "file") {
1301
+ if (!holdsProviderKey(ctx, existing)) return void 0;
1302
+ return toolFailure(
1303
+ `${shownPath} holds a provider key, which Book shows as ${PROVIDER_KEY_MARKER}. Deleting or rewriting the ${subject} would lose the real key, and Book cannot write it back.`,
1304
+ {
1305
+ code: "redacted_secret_overwrite",
1306
+ remediation: "If the user asked for this file (or cell) to be deleted, ask them to delete it themselves. To change it, edit only the lines that need to change, with Edit or an Update File hunk that leaves the key line alone."
1307
+ }
1308
+ );
1309
+ }
1310
+ function redactedSecretCommand(ctx, command) {
1311
+ if (!ctx.providerSecrets?.length) return void 0;
1312
+ if (!command?.includes(PROVIDER_KEY_MARKER)) return void 0;
1313
+ return toolFailure(
1314
+ `The command contains ${PROVIDER_KEY_MARKER}, which stands for a real provider key that the model cannot see. A command must not write or use it.`,
1315
+ {
1316
+ code: "redacted_secret_command",
1317
+ remediation: "The marker is not the key, and Book cannot write the real key. To change a file that holds a key, edit only the lines that need to change with Edit. To delete or replace such a file, or to run a command that needs the key, ask the user. To search for or quote the marker text itself, leave out the brackets."
1318
+ }
1319
+ );
1320
+ }
1321
+
1322
+ // src/tools/mutation.ts
1323
+ import { randomUUID } from "crypto";
1324
+ import { mkdir, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "fs/promises";
1325
+ import { dirname } from "path";
1326
+ var pathLocks = /* @__PURE__ */ new Map();
1327
+ function looksBinary(bytes) {
1328
+ const sample = bytes.subarray(0, 8192);
1329
+ if (sample.includes(0)) return true;
1330
+ let controls = 0;
1331
+ for (const byte of sample) {
1332
+ if (byte < 7 || byte > 13 && byte < 32) controls++;
1333
+ }
1334
+ return sample.length > 0 && controls / sample.length > 0.1;
1335
+ }
1336
+ async function readTextSnapshot(filePath, allowMissing = false) {
1337
+ let bytes;
1338
+ try {
1339
+ bytes = await readFile2(filePath);
1340
+ } catch (error) {
1341
+ if (allowMissing && error.code === "ENOENT") {
1342
+ return {
1343
+ exists: false,
1344
+ bytes: Buffer.alloc(0),
1345
+ text: "",
1346
+ bom: false,
1347
+ lineEnding: "lf",
1348
+ mixedLineEndings: false,
1349
+ binary: false,
1350
+ mode: void 0
1351
+ };
1352
+ }
1353
+ throw error;
1354
+ }
1355
+ const bom = bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
1356
+ const body = bytes.subarray(bom ? 3 : 0);
1357
+ let binary = looksBinary(bytes);
1358
+ let decoded;
1359
+ try {
1360
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(body);
1361
+ } catch {
1362
+ binary = true;
1363
+ decoded = body.toString("utf8");
1364
+ }
1365
+ const crlfCount = (decoded.match(/\r\n/g) ?? []).length;
1366
+ const bareLfCount = (decoded.replace(/\r\n/g, "").match(/\n/g) ?? []).length;
1367
+ const bareCrCount = (decoded.replace(/\r\n/g, "").match(/\r/g) ?? []).length;
1368
+ const mixedLineEndings = crlfCount > 0 && bareLfCount > 0 || bareCrCount > 0;
1369
+ const mode = (await stat2(filePath)).mode & 511;
1370
+ return {
1371
+ exists: true,
1372
+ bytes,
1373
+ text: decoded.replace(/\r\n/g, "\n"),
1374
+ bom,
1375
+ lineEnding: crlfCount > bareLfCount ? "crlf" : "lf",
1376
+ mixedLineEndings,
1377
+ binary,
1378
+ mode
1379
+ };
1380
+ }
1381
+ function restoreTextEncoding(text, snapshot) {
1382
+ const body = snapshot.lineEnding === "crlf" ? text.replace(/\n/g, "\r\n") : text;
1383
+ return Buffer.from(`${snapshot.bom ? "\uFEFF" : ""}${body}`, "utf8");
1384
+ }
1385
+ async function writeFileAtomically(filePath, bytes, signal, mode) {
1386
+ throwIfAborted(signal);
1387
+ await mkdir(dirname(filePath), { recursive: true });
1388
+ const temporary = `${filePath}.book-tmp-${randomUUID()}`;
1389
+ try {
1390
+ await writeFile(temporary, bytes, { flag: "wx", mode });
1391
+ throwIfAborted(signal);
1392
+ await rename(temporary, filePath);
1393
+ } finally {
1394
+ await rm(temporary, { force: true }).catch(() => void 0);
1395
+ }
1396
+ }
1397
+ async function removeFileAtomically(filePath, signal) {
1398
+ throwIfAborted(signal);
1399
+ await rm(filePath, { force: false });
1400
+ }
1401
+ async function acquirePath(path) {
1402
+ const previous = pathLocks.get(path) ?? Promise.resolve();
1403
+ let release;
1404
+ const current = new Promise((resolve4) => {
1405
+ release = resolve4;
1406
+ });
1407
+ const queued = previous.then(() => current);
1408
+ pathLocks.set(path, queued);
1409
+ await previous;
1410
+ return () => {
1411
+ release();
1412
+ if (pathLocks.get(path) === queued) pathLocks.delete(path);
1413
+ };
1414
+ }
1415
+ async function withMutationLocks(paths, operation) {
1416
+ const releases = [];
1417
+ try {
1418
+ for (const path of [...new Set(paths)].sort()) releases.push(await acquirePath(path));
1419
+ return await operation();
1420
+ } finally {
1421
+ for (const release of releases.reverse()) release();
1422
+ }
1423
+ }
1424
+
1425
+ // src/tools/patch.ts
1426
+ var MAX_PATCH_BYTES = 1024 * 1024;
1427
+ var MAX_FILES = 100;
1428
+ var MAX_HUNKS = 1e3;
1429
+ function invalid(message, details) {
1430
+ return toolFailure(message, {
1431
+ code: "invalid_patch_syntax",
1432
+ remediation: "Regenerate a Codex-style patch with Begin/End markers and valid file hunks.",
1433
+ details
1434
+ });
1435
+ }
1436
+ function parseHunkHeader(line) {
1437
+ const match = line.match(/^@@(?: -(\d+)(?:,(\d+))?)?(?: \+(\d+)(?:,(\d+))?)?(?: @@.*)?$/);
1438
+ if (!match) throw new Error(`Invalid hunk header: ${line}`);
1439
+ return { oldStart: match[1] ? Number(match[1]) : void 0 };
1440
+ }
1441
+ function parsePatchUnsafe(patch) {
1442
+ if (typeof patch !== "string" || patch.length === 0)
1443
+ return invalid("patch must be a non-empty string");
1444
+ const bytes = Buffer.byteLength(patch, "utf8");
1445
+ if (bytes > MAX_PATCH_BYTES)
1446
+ return toolFailure(`Patch input exceeds ${MAX_PATCH_BYTES} bytes`, {
1447
+ code: "patch_limit_exceeded",
1448
+ remediation: "Split the change into smaller patches.",
1449
+ details: { limit: MAX_PATCH_BYTES, bytes }
1450
+ });
1451
+ const lines = patch.replace(/\r\n/g, "\n").split("\n");
1452
+ if (lines.at(-1) === "") lines.pop();
1453
+ if (lines[0] !== "*** Begin Patch" || lines.at(-1) !== "*** End Patch")
1454
+ return invalid("Patch must start with *** Begin Patch and end with *** End Patch");
1455
+ while (lines.length > 2 && lines[1] === "*** Begin Patch") lines.splice(1, 1);
1456
+ while (lines.length > 2 && lines.at(-2) === "*** End Patch") lines.splice(-2, 1);
1457
+ const operations = [];
1458
+ let index = 1;
1459
+ let hunkCount = 0;
1460
+ while (index < lines.length - 1) {
1461
+ const marker = lines[index];
1462
+ const update = marker.match(/^\*\*\* Update File: (.+)$/);
1463
+ const add = marker.match(/^\*\*\* Add File: (.+)$/);
1464
+ const del = marker.match(/^\*\*\* Delete File: (.+)$/);
1465
+ if (update) {
1466
+ const hunks = [];
1467
+ index++;
1468
+ while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
1469
+ if (!lines[index].startsWith("@@"))
1470
+ throw new Error(`Expected @@ hunk header near line ${index + 1}`);
1471
+ const header = lines[index];
1472
+ const parsedHeader = parseHunkHeader(header);
1473
+ index++;
1474
+ const hunkLines = [];
1475
+ while (index < lines.length - 1 && !lines[index].startsWith("@@") && !lines[index].startsWith("*** ")) {
1476
+ const line = lines[index++];
1477
+ if (line === "\") {
1478
+ const previous = hunkLines.at(-1);
1479
+ if (!previous)
1480
+ throw new Error(`No-newline marker has no preceding hunk line near line ${index}`);
1481
+ previous.noNewline = true;
1482
+ continue;
1483
+ }
1484
+ if (line.startsWith(" ")) hunkLines.push({ kind: "context", text: line.slice(1) });
1485
+ else if (line.startsWith("-")) hunkLines.push({ kind: "remove", text: line.slice(1) });
1486
+ else if (line.startsWith("+")) hunkLines.push({ kind: "add", text: line.slice(1) });
1487
+ else throw new Error(`Invalid hunk line near line ${index}`);
1488
+ }
1489
+ if (hunkLines.length === 0) throw new Error(`Empty hunk near line ${index}`);
1490
+ if (!hunkLines.some((line) => line.kind !== "add"))
1491
+ throw new Error(`Update hunk must contain context or removed lines near line ${index}`);
1492
+ hunks.push({ header, oldStart: parsedHeader.oldStart, lines: hunkLines });
1493
+ hunkCount++;
1494
+ }
1495
+ if (hunks.length === 0) throw new Error(`Update File ${update[1]} has no hunks`);
1496
+ operations.push({ kind: "update", path: update[1].trim(), hunks });
1497
+ continue;
1498
+ }
1499
+ if (add) {
1500
+ index++;
1501
+ const content = [];
1502
+ while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
1503
+ const line = lines[index++];
1504
+ if (!line.startsWith("+"))
1505
+ throw new Error(`Added file lines must begin with + near line ${index}`);
1506
+ content.push(line.slice(1));
1507
+ }
1508
+ operations.push({ kind: "add", path: add[1].trim(), lines: content });
1509
+ continue;
1510
+ }
1511
+ if (del) {
1512
+ index++;
1513
+ if (index < lines.length - 1 && !lines[index].startsWith("*** "))
1514
+ throw new Error(`Delete File ${del[1]} cannot contain patch lines`);
1515
+ operations.push({ kind: "delete", path: del[1].trim() });
1516
+ continue;
1517
+ }
1518
+ throw new Error(`Expected a file operation near line ${index + 1}`);
1519
+ }
1520
+ if (operations.length === 0) return invalid("Patch contains no file operations");
1521
+ if (operations.length > MAX_FILES || hunkCount > MAX_HUNKS)
1522
+ return toolFailure(`Patch exceeds limits (${MAX_FILES} files, ${MAX_HUNKS} hunks)`, {
1523
+ code: "patch_limit_exceeded",
1524
+ remediation: "Split the change into smaller patches.",
1525
+ details: { files: operations.length, hunks: hunkCount }
1526
+ });
1527
+ return { operations, bytes };
1528
+ }
1529
+ function parsePatch(patch) {
1530
+ try {
1531
+ return parsePatchUnsafe(patch);
1532
+ } catch (error) {
1533
+ return invalid(error instanceof Error ? error.message : String(error));
1534
+ }
1535
+ }
1536
+ function linesOf(text) {
1537
+ const lines = text.split("\n");
1538
+ if (lines.at(-1) === "") lines.pop();
1539
+ return lines;
1540
+ }
1541
+ function sequenceMatches(haystack, needle) {
1542
+ if (needle.length === 0) return [];
1543
+ const matches = [];
1544
+ for (let start = 0; start <= haystack.length - needle.length; start++) {
1545
+ if (needle.every((line, offset) => haystack[start + offset] === line)) matches.push(start);
1546
+ }
1547
+ return matches;
1548
+ }
1549
+ function candidateLine(haystack, line) {
1550
+ const matches = haystack.reduce((found, value, index) => {
1551
+ if (value === line) found.push(index + 1);
1552
+ return found;
1553
+ }, []);
1554
+ return matches.length === 1 ? matches[0] : void 0;
1555
+ }
1556
+ function applyHunks(text, hunks) {
1557
+ let lines = linesOf(text);
1558
+ let trailingNewline = text.endsWith("\n");
1559
+ let cursor = 0;
1560
+ for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
1561
+ const hunk = hunks[hunkIndex];
1562
+ const oldLines = hunk.lines.filter((line) => line.kind !== "add").map((line) => line.text);
1563
+ const newLines = hunk.lines.filter((line) => line.kind !== "remove").map((line) => line.text);
1564
+ const matches = sequenceMatches(lines, oldLines);
1565
+ const afterPrevious = matches.filter((start) => start >= cursor);
1566
+ const index = matches.length === 1 ? matches[0] : afterPrevious.length === 1 ? afterPrevious[0] : -1;
1567
+ if (matches.length === 0)
1568
+ return {
1569
+ text,
1570
+ mismatch: toolFailure(`Hunk ${hunkIndex + 1}: patch context not found`, {
1571
+ code: "patch_context_not_found",
1572
+ remediation: "Reread the affected range and regenerate the hunk against the latest file contents.",
1573
+ details: {
1574
+ hunkIndex: hunkIndex + 1,
1575
+ header: hunk.header,
1576
+ firstContextLine: oldLines[0]?.slice(0, 200),
1577
+ closestCandidateLine: oldLines[0] ? candidateLine(lines, oldLines[0]) : void 0,
1578
+ suggestedReadRange: hunk.oldStart !== void 0 ? {
1579
+ lineStart: Math.max(1, hunk.oldStart - 3),
1580
+ lineEnd: hunk.oldStart + oldLines.length + 3
1581
+ } : void 0
1582
+ }
1583
+ })
1584
+ };
1585
+ if (index < 0)
1586
+ return {
1587
+ text,
1588
+ mismatch: toolFailure(`Hunk ${hunkIndex + 1}: patch context is ambiguous`, {
1589
+ code: "ambiguous_patch_context",
1590
+ 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.",
1591
+ details: {
1592
+ hunkIndex: hunkIndex + 1,
1593
+ header: hunk.header,
1594
+ matches: matches.length,
1595
+ matchesAfterPreviousHunk: afterPrevious.length
1596
+ }
1597
+ })
1598
+ };
1599
+ const touchesEndOfFile = index + oldLines.length === lines.length;
1600
+ lines = [...lines.slice(0, index), ...newLines, ...lines.slice(index + oldLines.length)];
1601
+ cursor = index + newLines.length;
1602
+ if (touchesEndOfFile) {
1603
+ const oldNoNewline = hunk.lines.some((line) => line.kind !== "add" && line.noNewline);
1604
+ const newNoNewline = hunk.lines.some((line) => line.kind !== "remove" && line.noNewline);
1605
+ if (newNoNewline) trailingNewline = false;
1606
+ else if (oldNoNewline) trailingNewline = true;
1607
+ }
1608
+ }
1609
+ return { text: lines.join("\n") + (trailingNewline && lines.length > 0 ? "\n" : "") };
1610
+ }
1611
+ function mutationFromDiff(target, kind, stats) {
1612
+ return {
1613
+ kind,
1614
+ filePath: target.displayPath,
1615
+ ...target.memory ? { memory: true } : {},
1616
+ addedLines: stats.addedLines,
1617
+ removedLines: stats.removedLines
1618
+ };
1619
+ }
1620
+ function errorCodeFor(error) {
1621
+ return error instanceof Error && error.message.includes("changed or disappeared") ? "stale_file_observation" : "filesystem_error";
1622
+ }
1623
+ async function redactedSecretRefusal(ctx, entries) {
1624
+ for (const { operation, filePath, messagePath } of entries) {
1625
+ if (operation.kind === "add") continue;
1626
+ const added = operation.kind === "update" ? operation.hunks.flatMap(
1627
+ (hunk) => hunk.lines.filter((line) => line.kind === "add").map((line) => line.text)
1628
+ ) : [];
1629
+ if (operation.kind === "update" && !added.some((text) => text.includes(PROVIDER_KEY_MARKER)))
1630
+ continue;
1631
+ let existing;
1632
+ try {
1633
+ existing = (await readTextSnapshot(filePath, true)).text;
1634
+ } catch {
1635
+ continue;
1636
+ }
1637
+ const refused = operation.kind === "delete" ? redactedSecretDeletion(ctx, messagePath, existing) : redactedSecretOverwrite(ctx, messagePath, existing, added);
1638
+ if (refused) return refused;
1639
+ }
1640
+ return void 0;
1641
+ }
1642
+ async function applyPatch(args, ctx) {
1643
+ const parsed = parsePatch(args.patch);
1644
+ if (!("operations" in parsed)) return parsed;
1645
+ const paths = parsed.operations.map((operation) => operation.path);
1646
+ const resolved = parsed.operations.map((operation) => {
1647
+ const result = resolveMutationPath(ctx, operation.path);
1648
+ return result ? {
1649
+ operation,
1650
+ ...result,
1651
+ displayPath: mutationDisplayPath(result),
1652
+ messagePath: messagePathFor(result)
1653
+ } : null;
1654
+ });
1655
+ if (resolved.some((entry) => !entry))
1656
+ return pathOutsideWorkspaceResult(paths[resolved.findIndex((entry) => !entry)]);
1657
+ const entries = resolved;
1658
+ const refused = await redactedSecretRefusal(ctx, entries);
1659
+ if (refused) return refused;
1660
+ const canonicalKeys = entries.map(
1661
+ (entry) => process.platform === "win32" ? entry.canonicalPath.toLowerCase() : entry.canonicalPath
1662
+ );
1663
+ if (new Set(canonicalKeys).size !== canonicalKeys.length)
1664
+ return toolFailure("A patch may contain only one operation per file", {
1665
+ code: "patch_conflict",
1666
+ remediation: "Combine related updates into one file operation with multiple hunks."
1667
+ });
1668
+ return withMutationLocks(
1669
+ entries.map((entry) => entry.canonicalPath),
1670
+ async () => {
1671
+ const staged = [];
1672
+ const memoryWarnings = [];
1673
+ try {
1674
+ for (const entry of entries) {
1675
+ throwIfAborted(ctx.signal);
1676
+ const before = await readTextSnapshot(entry.filePath, entry.operation.kind === "add");
1677
+ if (before.binary)
1678
+ return toolFailure(`Binary file is unsupported: ${entry.messagePath}`, {
1679
+ code: "binary_file_unsupported",
1680
+ remediation: "Use a text file mutation or a dedicated binary workflow."
1681
+ });
1682
+ if (before.mixedLineEndings && entry.operation.kind === "update")
1683
+ return toolFailure(
1684
+ `Mixed line endings are unsupported for patch updates: ${entry.messagePath}`,
1685
+ {
1686
+ code: "patch_conflict",
1687
+ remediation: "Normalize the file line endings, reread it, and regenerate the patch.",
1688
+ details: { filePath: entry.messagePath, lineEnding: "mixed" }
1689
+ }
1690
+ );
1691
+ const stale = entry.operation.kind === "add" ? void 0 : await requireFreshObservation(ctx, entry.filePath, entry.messagePath);
1692
+ if (stale)
1693
+ return toolFailure(stale, {
1694
+ code: "stale_file_observation",
1695
+ remediation: "Call Read before regenerating the patch."
1696
+ });
1697
+ if (entry.operation.kind === "add") {
1698
+ if (before.exists)
1699
+ return toolFailure(`File already exists: ${entry.messagePath}`, {
1700
+ code: "patch_conflict",
1701
+ remediation: "Use Update File for an existing file."
1702
+ });
1703
+ let afterText = entry.operation.lines.join("\n") + (entry.operation.lines.length > 0 ? "\n" : "");
1704
+ if (entry.memory) {
1705
+ const checked = checkMemoryWrite(ctx, entry.filePath, afterText, null);
1706
+ if (!checked.ok) return checked.failure;
1707
+ afterText = checked.content ?? afterText;
1708
+ memoryWarnings.push(...checked.warnings);
1709
+ }
1710
+ const added = redactedSecretAddition(ctx, entry.messagePath, "", afterText);
1711
+ if (added) return added;
1712
+ const { stats } = await renderDiffWithStatsAsync("", afterText, 3, ctx.signal);
1713
+ staged.push({
1714
+ operation: entry.operation,
1715
+ absolutePath: entry.filePath,
1716
+ writePath: entry.canonicalPath,
1717
+ messagePath: entry.messagePath,
1718
+ memory: entry.memory,
1719
+ before,
1720
+ afterText,
1721
+ afterBytes: Buffer.from(afterText, "utf8"),
1722
+ mutation: mutationFromDiff(entry, "create", stats)
1723
+ });
1724
+ } else if (entry.operation.kind === "delete") {
1725
+ if (!before.exists)
1726
+ return toolFailure(`File not found: ${entry.messagePath}`, {
1727
+ code: "patch_conflict",
1728
+ remediation: "Reread the workspace and regenerate the patch."
1729
+ });
1730
+ if (entry.memory) {
1731
+ const checked = checkMemoryWrite(ctx, entry.filePath, null, before.text);
1732
+ if (!checked.ok) return checked.failure;
1733
+ }
1734
+ staged.push({
1735
+ operation: entry.operation,
1736
+ absolutePath: entry.filePath,
1737
+ writePath: entry.filePath,
1738
+ messagePath: entry.messagePath,
1739
+ memory: entry.memory,
1740
+ before,
1741
+ mutation: {
1742
+ kind: "delete",
1743
+ filePath: entry.displayPath,
1744
+ ...entry.memory ? { memory: true } : {},
1745
+ addedLines: 0,
1746
+ removedLines: linesOf(before.text).length
1747
+ }
1748
+ });
1749
+ } else {
1750
+ if (!before.exists)
1751
+ return toolFailure(`File not found: ${entry.messagePath}`, {
1752
+ code: "patch_context_not_found",
1753
+ remediation: "Reread the file and regenerate the patch."
1754
+ });
1755
+ const applied = applyHunks(before.text, entry.operation.hunks);
1756
+ if (applied.mismatch)
1757
+ return toolFailure(
1758
+ `${entry.messagePath}: ${applied.mismatch.structuredError?.message}`,
1759
+ {
1760
+ ...applied.mismatch.structuredError,
1761
+ code: applied.mismatch.structuredError?.code ?? "patch_context_not_found",
1762
+ remediation: applied.mismatch.structuredError?.remediation,
1763
+ details: {
1764
+ ...applied.mismatch.structuredError?.details,
1765
+ filePath: entry.messagePath,
1766
+ lineEnding: before.mixedLineEndings ? "mixed" : before.lineEnding,
1767
+ normalizedMatching: true
1768
+ }
1769
+ }
1770
+ );
1771
+ let afterText = applied.text;
1772
+ if (entry.memory) {
1773
+ const checked = checkMemoryWrite(ctx, entry.filePath, afterText, before.text);
1774
+ if (!checked.ok) return checked.failure;
1775
+ afterText = checked.content ?? afterText;
1776
+ memoryWarnings.push(...checked.warnings);
1777
+ }
1778
+ const added = redactedSecretAddition(ctx, entry.messagePath, before.text, afterText);
1779
+ if (added) return added;
1780
+ const { stats } = await renderDiffWithStatsAsync(before.text, afterText, 3, ctx.signal);
1781
+ staged.push({
1782
+ operation: entry.operation,
1783
+ absolutePath: entry.filePath,
1784
+ writePath: entry.canonicalPath,
1785
+ messagePath: entry.messagePath,
1786
+ memory: entry.memory,
1787
+ before,
1788
+ afterText,
1789
+ afterBytes: restoreTextEncoding(afterText, before),
1790
+ mutation: mutationFromDiff(entry, "update", stats)
1791
+ });
1792
+ }
1793
+ }
1794
+ const mutations = staged.map((file) => file.mutation);
1795
+ const changedLines = mutations.reduce(
1796
+ (total, mutation) => total + mutation.addedLines + mutation.removedLines,
1797
+ 0
1798
+ );
1799
+ const contextLines = parsed.operations.reduce(
1800
+ (total, operation) => total + (operation.kind === "update" ? operation.hunks.reduce(
1801
+ (hunkTotal, hunk) => hunkTotal + hunk.lines.filter((line) => line.kind === "context").length,
1802
+ 0
1803
+ ) : 0),
1804
+ 0
1805
+ );
1806
+ const changeBudgetWarnings = [];
1807
+ if (changedLines > 2e3) changeBudgetWarnings.push("large_change");
1808
+ if (changedLines > 100 && changedLines > Math.max(1, contextLines) * 20)
1809
+ changeBudgetWarnings.push("low_context_to_change_ratio");
1810
+ const diffs = [];
1811
+ for (const file of staged) {
1812
+ throwIfAborted(ctx.signal);
1813
+ const afterText = file.afterText ?? "";
1814
+ const { diff } = await renderDiffWithStatsAsync(
1815
+ file.before.text,
1816
+ afterText,
1817
+ 3,
1818
+ ctx.signal
1819
+ );
1820
+ if (diff) diffs.push(`--- ${file.messagePath}
1821
+ +++ ${file.messagePath}
1822
+ ${diff}`);
1823
+ }
1824
+ const committed = [];
1825
+ try {
1826
+ for (const file of staged) {
1827
+ throwIfAborted(ctx.signal);
1828
+ committed.push(file);
1829
+ if (file.operation.kind === "delete")
1830
+ await removeFileAtomically(file.absolutePath, ctx.signal);
1831
+ else
1832
+ await writeFileAtomically(
1833
+ file.writePath,
1834
+ file.afterBytes,
1835
+ ctx.signal,
1836
+ file.before.mode
1837
+ );
1838
+ const check = await readTextSnapshot(
1839
+ file.absolutePath,
1840
+ file.operation.kind === "delete"
1841
+ );
1842
+ if (file.operation.kind === "delete" ? check.exists : !check.exists || !check.bytes.equals(file.afterBytes))
1843
+ throw new Error(`Post-write verification failed for ${file.messagePath}`);
1844
+ }
1845
+ } catch (error) {
1846
+ let rollbackFailed = false;
1847
+ for (const file of committed.reverse()) {
1848
+ try {
1849
+ if (file.before.exists)
1850
+ await writeFileAtomically(
1851
+ file.writePath,
1852
+ file.before.bytes,
1853
+ void 0,
1854
+ file.before.mode
1855
+ );
1856
+ else await removeFileAtomically(file.absolutePath).catch(() => void 0);
1857
+ } catch {
1858
+ rollbackFailed = true;
1859
+ }
1860
+ }
1861
+ return toolFailure(
1862
+ `Patch commit failed${rollbackFailed ? "; rollback also failed" : " and was rolled back"}: ${error instanceof Error ? error.message : String(error)}`,
1863
+ {
1864
+ code: rollbackFailed ? "patch_rollback_failed" : "filesystem_error",
1865
+ remediation: rollbackFailed ? "Inspect the affected files and restore them manually." : "Retry after checking the workspace state."
1866
+ }
1867
+ );
1868
+ }
1869
+ const observations = [];
1870
+ for (const file of staged) {
1871
+ if (file.operation.kind !== "delete")
1872
+ observations.push(
1873
+ await observeFile(
1874
+ ctx,
1875
+ file.absolutePath,
1876
+ file.operation.kind === "add" ? "create" : "edit"
1877
+ )
1878
+ );
1879
+ }
1880
+ for (const file of staged) {
1881
+ if (file.memory) {
1882
+ noticeMemoryWrite(
1883
+ ctx,
1884
+ file.absolutePath,
1885
+ file.operation.kind === "delete" ? "deleted" : "saved"
1886
+ );
1887
+ }
1888
+ }
1889
+ const result = toolSuccess(
1890
+ withMemoryWarnings(
1891
+ diffs.join("\n") || "Patch applied successfully (no textual change)",
1892
+ memoryWarnings
1893
+ ),
1894
+ {
1895
+ data: {
1896
+ files: mutations.map((mutation) => mutation.filePath),
1897
+ changedFiles: mutations.length,
1898
+ changedLines,
1899
+ patchBytes: parsed.bytes,
1900
+ changeBudgetWarnings
1901
+ },
1902
+ artifacts: {
1903
+ fileMutation: mutations[0],
1904
+ fileMutations: mutations,
1905
+ fileObservations: observations
1906
+ },
1907
+ presentation: {
1908
+ kind: "diff",
1909
+ summary: `Applied patch to ${mutations.length} ${mutations.length === 1 ? "file" : "files"}`,
1910
+ target: mutations.length === 1 ? mutations[0].filePath : `${mutations.length} files`
1911
+ }
1912
+ }
1913
+ );
1914
+ if (!staged.every((file) => file.mutation.kind === "create")) return result;
1915
+ return {
1916
+ ...result,
1917
+ maskedPlaceholder: withMemoryWarnings(
1918
+ `${staged.map(
1919
+ (file, order) => `${order === 0 ? "Created" : "created"} ${file.messagePath} (${file.mutation.addedLines} lines, ${file.afterBytes?.byteLength ?? 0} bytes)`
1920
+ ).join("; ")}.`,
1921
+ memoryWarnings
1922
+ )
1923
+ };
1924
+ } catch (error) {
1925
+ const code = errorCodeFor(error);
1926
+ return toolFailure(error instanceof Error ? error.message : String(error), {
1927
+ code,
1928
+ remediation: "Reread the affected file and regenerate the patch."
1929
+ });
1930
+ }
1931
+ }
1932
+ );
1933
+ }
1934
+ var patchTools = [
1935
+ {
1936
+ name: "ApplyPatch",
1937
+ argumentAliases: { input: "patch" },
1938
+ 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.",
1939
+ parameters: {
1940
+ type: "object",
1941
+ properties: {
1942
+ patch: {
1943
+ type: "string",
1944
+ minLength: 1,
1945
+ maxLength: MAX_PATCH_BYTES,
1946
+ description: "Patch text beginning with *** Begin Patch and ending with *** End Patch"
1947
+ },
1948
+ baseObservation: {
1949
+ type: "string",
1950
+ maxLength: 128,
1951
+ description: "Optional short observation identifier for diagnostics; freshness is checked implicitly."
1952
+ }
1953
+ },
1954
+ required: ["patch"],
1955
+ additionalProperties: false
1956
+ },
1957
+ execute: applyPatch
1958
+ }
1959
+ ];
1960
+
1961
+ // src/tools/shell-chaining.ts
1962
+ var SHELL_CHAINING = /[;&|><`$(){}\n]/;
1963
+ var LINE_BREAK = /[\n\r\u0085\u2028\u2029]/;
1964
+ var TYPOGRAPHIC_QUOTE_RANGE = "\\u2018-\\u201E";
1965
+ var TYPOGRAPHIC_QUOTE = new RegExp(`[${TYPOGRAPHIC_QUOTE_RANGE}]`);
1966
+ var ANY_QUOTE = new RegExp(`['"${TYPOGRAPHIC_QUOTE_RANGE}]`);
1967
+ function hasCompoundShellSyntax(command, platform = process.platform, shell) {
1968
+ const text = command.trim();
1969
+ const refusesSingleQuote = platform === "win32" && !readsSingleQuoteAsPosix(shell);
1970
+ let state = "outside";
1971
+ for (let i = 0; i < text.length; i += 1) {
1972
+ const char = text[i];
1973
+ if (LINE_BREAK.test(char) || TYPOGRAPHIC_QUOTE.test(char)) return true;
1974
+ if (refusesSingleQuote && char === "'") return true;
1975
+ const next = text[i + 1];
1976
+ const quoteFollows = next === '"' || next === "'";
1977
+ if (char === "\\" && quoteFollows) return true;
1978
+ if (state === "single") {
1979
+ if (char === "'") state = "outside";
1980
+ continue;
1981
+ }
1982
+ if (state === "double") {
1983
+ if (char === "$" || char === "`") return true;
1984
+ if (char === '"') state = "outside";
1985
+ continue;
1986
+ }
1987
+ if (char === "^" && quoteFollows) return true;
1988
+ if (char === '"') state = "double";
1989
+ else if (char === "'") state = "single";
1990
+ else if (SHELL_CHAINING.test(char)) return true;
1991
+ }
1992
+ return state !== "outside";
1993
+ }
1994
+
1995
+ // src/tools/strict-rm.ts
1996
+ import { posix, win32 } from "path";
1997
+ var PLAIN_POSIX_OPERAND = /^\/[A-Za-z0-9_./@%+=:,-]*$/;
1998
+ var PLAIN_DRIVE_OPERAND = /^[A-Za-z]:[\\/](?:[A-Za-z0-9_.\\/@%+=,-]|~(?=[0-9]))*$/;
1999
+ function parseStrictRm(command, platform = process.platform) {
2000
+ const words = command.trim().split(/[ \t]+/);
2001
+ if (words[0] !== "rm" || words.length < 2) return null;
2002
+ const operands = words.slice(1);
2003
+ for (const operand of operands) {
2004
+ const drive = platform === "win32" && PLAIN_DRIVE_OPERAND.test(operand);
2005
+ if (!drive && !PLAIN_POSIX_OPERAND.test(operand)) return null;
2006
+ const spelled = comparablePath(operand, platform);
2007
+ const segments = spelled.slice(drive ? 3 : 1).split("/");
2008
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
2009
+ return null;
2010
+ }
2011
+ }
2012
+ return operands;
2013
+ }
2014
+ function isMarkdownFile(operand) {
2015
+ const name = posix.basename(operand);
2016
+ return name.length > ".md".length && !name.startsWith(".") && name.endsWith(".md");
2017
+ }
2018
+ function strictRmOperandsSpelledUnder(command, roots, platform = process.platform, respell = (operand) => operand) {
2019
+ const operands = parseStrictRm(command, platform);
2020
+ if (!operands) return null;
2021
+ const spellings = roots.map(
2022
+ (root) => posix.normalize(comparablePath(root, platform)).replace(/\/+$/, "")
2023
+ );
2024
+ const isUnder = (path) => {
2025
+ const spelled = comparablePath(path, platform);
2026
+ return spellings.some((spelling) => spelled.startsWith(`${spelling}/`));
2027
+ };
2028
+ for (const operand of operands) {
2029
+ const spelled = comparablePath(operand, platform);
2030
+ if (!isMarkdownFile(spelled) || posix.basename(spelled).toLowerCase() === "memory.md") {
2031
+ return null;
2032
+ }
2033
+ if (!isUnder(operand) && !isUnder(respell(operand))) return null;
2034
+ }
2035
+ return operands;
2036
+ }
2037
+ function strictRmOfMarkdownUnder(command, root, platform = process.platform, canonicalize = canonicalizePath) {
2038
+ const paths = platform === "win32" ? win32 : posix;
2039
+ if (!paths.isAbsolute(root)) return null;
2040
+ const canonicalRoot = canonicalize(root);
2041
+ const operands = strictRmOperandsSpelledUnder(
2042
+ command,
2043
+ [root, canonicalRoot],
2044
+ platform,
2045
+ (operand) => paths.join(canonicalize(paths.dirname(operand)), paths.basename(operand))
2046
+ );
2047
+ if (!operands) return null;
2048
+ for (const operand of operands) {
2049
+ if (isOutside(canonicalRoot, canonicalize(paths.dirname(operand)), paths)) return null;
2050
+ }
2051
+ return operands;
2052
+ }
2053
+ function strictRmMatchesGlob(command, matchesOperand, platform = process.platform) {
2054
+ const operands = parseStrictRm(command, platform);
2055
+ return operands !== null && operands.every(matchesOperand);
2056
+ }
2057
+
2058
+ // src/permissions.ts
2059
+ function isUnreadablePattern(target) {
2060
+ return typeof target === "object" && target !== null && target.target === "invalid";
2061
+ }
2062
+ var WORKSPACE_READ_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep"]);
2063
+ var PATH_RULE_TOOLS = /* @__PURE__ */ new Set([
2064
+ "Read",
2065
+ "Write",
2066
+ "Edit",
2067
+ "MultiEdit",
2068
+ "NotebookEdit"
2069
+ ]);
2070
+ var WORKSPACE_READ_JUDGING_MODES = /* @__PURE__ */ new Set([
2071
+ "default",
2072
+ "accept-edits",
2073
+ "plan"
2074
+ ]);
2075
+ var WORKSPACE_READ_REFUSAL_MODES = /* @__PURE__ */ new Set([
2076
+ ...WORKSPACE_READ_JUDGING_MODES,
2077
+ "dontAsk"
2078
+ ]);
2079
+ var WORKSPACE_READ_ONLY_GIT_TOOLS = /* @__PURE__ */ new Set([
2080
+ "GitStatus",
2081
+ "GitDiff",
2082
+ "GitLog",
2083
+ "GitBranch"
2084
+ ]);
2085
+ function parseRule(rule) {
2086
+ const parenIdx = rule.indexOf("(");
2087
+ if (parenIdx === -1) {
2088
+ return { toolName: rule.trim(), pattern: null };
2089
+ }
2090
+ const toolName = rule.slice(0, parenIdx).trim();
2091
+ const closeIdx = rule.lastIndexOf(")");
2092
+ if (closeIdx === -1 || closeIdx <= parenIdx) {
2093
+ return { toolName, pattern: null };
2094
+ }
2095
+ const pattern = rule.slice(parenIdx + 1, closeIdx).trim();
2096
+ return { toolName, pattern: pattern.length === 0 ? null : pattern };
2097
+ }
2098
+ function normalizePathArg(s) {
2099
+ if (s.startsWith("./")) return s.slice(2);
2100
+ return s;
2101
+ }
2102
+ function toolNameMatchesRule(ruleToolName, toolName) {
2103
+ if (ruleToolName === toolName) return true;
2104
+ if (!ruleToolName.startsWith("mcp__")) return false;
2105
+ const serverName = ruleToolName.slice("mcp__".length);
2106
+ return Boolean(
2107
+ serverName && !serverName.includes("__") && toolName.startsWith(`${ruleToolName}__`)
2108
+ );
2109
+ }
2110
+ var caseFoldingPathRule = (value) => value.toLowerCase();
2111
+ function ruleMatchesCandidate(rule, toolName, primaryArg, fold) {
2112
+ if (!toolNameMatchesRule(rule.toolName, toolName)) return false;
2113
+ if (rule.pattern === null) return true;
2114
+ const normalizedArg = normalizePathArg(primaryArg);
2115
+ const normalizedPattern = rule.pattern.startsWith("./") ? rule.pattern.slice(2) : rule.pattern;
2116
+ if (!fold) return globToRegex(normalizedPattern).test(normalizedArg);
2117
+ return globToRegex(caseFoldingPathRule(normalizedPattern)).test(
2118
+ caseFoldingPathRule(normalizedArg)
2119
+ );
2120
+ }
2121
+ function ruleMatches(rule, toolName, primaryArg) {
2122
+ return ruleMatchesCandidate(rule, toolName, primaryArg, false);
2123
+ }
2124
+ function isExactPattern(rule) {
2125
+ return rule.pattern !== null && !rule.pattern.includes("*");
2126
+ }
2127
+ function bashAllowRuleMatches(rule, command, platform, shell, reading) {
2128
+ if (!toolNameMatchesRule(rule.toolName, "Bash")) return false;
2129
+ if (rule.pattern === null) return true;
2130
+ if (typeof command !== "string") return false;
2131
+ const { trimmed, compound } = reading ?? readBashCommand(command, platform, shell);
2132
+ if (isExactPattern(rule)) return trimmed === rule.pattern;
2133
+ if (compound) return false;
2134
+ return ruleMatchesCandidate(rule, "Bash", trimmed, false);
2135
+ }
2136
+ function readBashCommand(command, platform, shell) {
2137
+ return { trimmed: command.trim(), compound: hasCompoundShellSyntax(command, platform, shell) };
2138
+ }
2139
+ function bashRestrictiveCandidates(command, firstLine) {
2140
+ if (typeof command !== "string") return [firstLine];
2141
+ const trimmed = command.trim();
2142
+ return [.../* @__PURE__ */ new Set([firstLine, trimmed.split("\n")[0], trimmed])];
2143
+ }
2144
+ var RULE_WILDCARD = /[*?]/;
2145
+ function ruleScopeOf(toolName, primaryArg) {
2146
+ if (!primaryArg) return void 0;
2147
+ if (toolName === "WebFetch") {
2148
+ try {
2149
+ const origin = new URL(primaryArg).origin;
2150
+ return { taken: origin, pattern: `${origin}/**` };
2151
+ } catch {
2152
+ }
2153
+ }
2154
+ return { taken: primaryArg, pattern: primaryArg };
2155
+ }
2156
+ function permissionRuleForToolCall(call) {
2157
+ const toolName = canonicalToolName(call.name);
2158
+ if (toolName === "Bash" && typeof call.arguments.command === "string") {
2159
+ const command = call.arguments.command.trim();
2160
+ if (!command || LINE_BREAK.test(command)) return void 0;
2161
+ if (!getPrimaryArg(call.arguments).trim()) return void 0;
2162
+ if (command.includes("*")) return void 0;
2163
+ return `Bash(${command})`;
2164
+ }
2165
+ if (toolName === "WebSearch") return toolName;
2166
+ const scope = ruleScopeOf(toolName, getPrimaryArg(call.arguments));
2167
+ if (!scope || RULE_WILDCARD.test(scope.taken)) return void 0;
2168
+ return `${toolName}(${scope.pattern})`;
2169
+ }
2170
+ function missingRuleReason(call) {
2171
+ if (permissionRuleForToolCall(call) !== void 0) return void 0;
2172
+ const toolName = canonicalToolName(call.name);
2173
+ if (toolName === "Bash" && typeof call.arguments.command === "string") {
2174
+ const command = call.arguments.command.trim();
2175
+ return {
2176
+ // A command that opens with a blank line has an empty first line, the line a rule holds.
2177
+ lineBreak: LINE_BREAK.test(command) || command !== "" && !getPrimaryArg(call.arguments).trim(),
2178
+ wildcard: command.includes("*")
2179
+ };
2180
+ }
2181
+ const scope = ruleScopeOf(toolName, getPrimaryArg(call.arguments));
2182
+ return scope && RULE_WILDCARD.test(scope.taken) ? { lineBreak: false, wildcard: true } : void 0;
2183
+ }
2184
+ function permissionResultOf(decision) {
2185
+ return typeof decision === "string" ? decision : decision.result;
2186
+ }
2187
+ function permissionRuleOf(decision) {
2188
+ return typeof decision === "string" ? void 0 : decision.rule;
2189
+ }
2190
+ function permissionReasonOf(decision) {
2191
+ return typeof decision === "string" ? void 0 : decision.reason;
2192
+ }
2193
+ var RULE_LADDER_MAX = 3;
2194
+ function permissionRuleLadder(call) {
2195
+ const exact = permissionRuleForToolCall(call);
2196
+ if (!exact) return [];
2197
+ if (canonicalToolName(call.name) !== "Bash") return [exact];
2198
+ const raw = call.arguments.command;
2199
+ const command = typeof raw === "string" ? raw.trim() : "";
2200
+ if (!command || SHELL_CHAINING.test(command) || ANY_QUOTE.test(command) || command.includes("?")) {
2201
+ return [exact];
2202
+ }
2203
+ const tokens = command.split(/\s+/);
2204
+ const ladder = [exact];
2205
+ for (let take = tokens.length - 1; take >= 1 && ladder.length < RULE_LADDER_MAX; take -= 1) {
2206
+ const prefix = `Bash(${tokens.slice(0, take).join(" ")} *)`;
2207
+ if (!ladder.includes(prefix)) ladder.push(prefix);
2208
+ }
2209
+ return ladder;
2210
+ }
2211
+ function permissionRuleMatchesCall(rule, call, platform = process.platform, shell) {
2212
+ const toolName = canonicalToolName(call.name);
2213
+ if (toolName === "Bash") {
2214
+ return bashAllowRuleMatches(parseRule(rule), call.arguments.command, platform, shell);
2215
+ }
2216
+ const primaryArg = getPrimaryArg(call.arguments);
2217
+ if (toolName === "WebFetch" && primaryArg) {
2218
+ try {
2219
+ return ruleMatches(parseRule(rule), toolName, new URL(primaryArg).toString());
2220
+ } catch {
2221
+ }
2222
+ }
2223
+ return ruleMatches(parseRule(rule), toolName, primaryArg);
2224
+ }
2225
+ function patchOperations(args) {
2226
+ const parsed = parsePatch(args.patch);
2227
+ return "operations" in parsed ? parsed.operations : [];
2228
+ }
2229
+ function ruleMatchesPatch(rule, paths, fold) {
2230
+ return paths.some((path) => ruleMatchesCandidate(rule, rule.toolName, path, fold));
2231
+ }
2232
+ function patchRuleSupportsOperation(rule, operation) {
2233
+ if (rule.toolName === "ApplyPatch") return true;
2234
+ if (rule.toolName === "Edit") return operation.kind === "update";
2235
+ if (rule.toolName === "Write") return operation.kind === "update" || operation.kind === "add";
2236
+ return false;
2237
+ }
2238
+ function compatiblePatchRuleMatches(rule, operations, spellingsOf, fold) {
2239
+ if (!["ApplyPatch", "Edit", "Write"].includes(rule.toolName)) return false;
2240
+ if (rule.toolName === "ApplyPatch" && rule.pattern === null) return true;
2241
+ return operations.some(
2242
+ (operation) => patchRuleSupportsOperation(rule, operation) && (rule.pattern === null || ruleMatchesPatch(rule, spellingsOf(operation.path, fold), fold))
2243
+ );
2244
+ }
2245
+ function primaryArgForRule(_toolName, args) {
2246
+ return getPrimaryArg(args);
2247
+ }
2248
+ function hasAdjudicationPolicy(settings) {
2249
+ return settings.permissions.deny.length > 0 || settings.permissions.ask.length > 0;
2250
+ }
2251
+ function hasBashAdjudicationPolicy(settings) {
2252
+ return hasBashRestriction(parseRuleLists(settings));
2253
+ }
2254
+ function hasBashRestriction(lists) {
2255
+ return [...lists.deny, ...lists.ask].some(
2256
+ ({ parsed }) => toolNameMatchesRule(parsed.toolName, "Bash")
2257
+ );
2258
+ }
2259
+ function sandboxAutoAllows(toolName, args, settings, options) {
2260
+ if (!settings.sandbox.autoAllowBashIfSandboxed) return false;
2261
+ if (canonicalToolName(toolName) !== "Bash") return false;
2262
+ if (hasAdjudicationPolicy(settings)) return false;
2263
+ const command = typeof args.command === "string" ? args.command.trim() : "";
2264
+ if (!command) return false;
2265
+ return sandboxCoverage(command, settings.sandbox, options.sandboxBackendAvailable).sandboxed;
2266
+ }
2267
+ var toPosix = (value) => value.replace(/\\/g, "/");
2268
+ function realRootOf(scope) {
2269
+ if (scope.realRoot) return scope.realRoot;
2270
+ try {
2271
+ return realpathSync.native(scope.root);
2272
+ } catch {
2273
+ return void 0;
2274
+ }
2275
+ }
2276
+ function pathArgument(toolName, args) {
2277
+ const value = toolName === "NotebookEdit" ? args.notebook_path : args.filePath;
2278
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
2279
+ }
2280
+ function rulesName(lists, toolName) {
2281
+ return [...lists.deny, ...lists.ask, ...lists.allow].some(
2282
+ ({ parsed }) => parsed.toolName === toolName
2283
+ );
2284
+ }
2285
+ function resolvedPathScope(scope) {
2286
+ const detailCache = /* @__PURE__ */ new Map();
2287
+ const rootsFor = (includeReadOnly) => ({
2288
+ workspaceRoot: scope.root,
2289
+ readOnlyRoots: includeReadOnly ? scope.readOnlyRoots : void 0,
2290
+ memorySearchRoot: scope.memorySearchRoot,
2291
+ additionalRoots: scope.additionalRoots
2292
+ });
2293
+ const holdsHomeByRoot = /* @__PURE__ */ new Map();
2294
+ const holdsHome = (root) => {
2295
+ let known = holdsHomeByRoot.get(root);
2296
+ if (known === void 0) {
2297
+ known = rootHoldsHome(root, scope.homeGuards ?? []);
2298
+ holdsHomeByRoot.set(root, known);
2299
+ }
2300
+ return known;
2301
+ };
2302
+ const guardedRoots = [scope.root, ...scope.additionalRoots ?? []].filter(holdsHome);
2303
+ const servingRootOf = (canonicalPath) => [realRootOf(scope), ...scope.additionalRoots ?? []].find(
2304
+ (root) => typeof root === "string" && root.length > 0 && isUnderRoot(canonicalPath, root)
2305
+ );
2306
+ return {
2307
+ scope,
2308
+ roots: rootsFor,
2309
+ detail: (raw, includeReadOnly) => {
2310
+ const key = `${includeReadOnly ? "r" : "w"}\0${raw}`;
2311
+ let known = detailCache.get(key);
2312
+ if (!known) {
2313
+ known = resolveReadablePathDetail(rootsFor(includeReadOnly), raw);
2314
+ detailCache.set(key, known);
2315
+ }
2316
+ return known;
2317
+ },
2318
+ isGuarded: (canonicalPath) => {
2319
+ const roots = [realRootOf(scope), ...scope.additionalRoots ?? []].filter(
2320
+ (root) => typeof root === "string" && root.length > 0
2321
+ );
2322
+ if (roots.some((root) => isBookLocalSettingsPath(canonicalPath, root, { directory: true })))
2323
+ return true;
2324
+ if (guardedRoots.length === 0) return false;
2325
+ if (!pathHoldsHome(canonicalPath, scope.homeGuards ?? [])) return false;
2326
+ const serving = servingRootOf(canonicalPath);
2327
+ return serving !== void 0 && holdsHome(serving);
2328
+ },
2329
+ /**
2330
+ * Whether a Grep or Glob *scope* can reach a guarded home. A scope is a subtree, not a file,
2331
+ * so it is guarded when the guarded home is inside it as well as when it is inside the home:
2332
+ * `Grep {path: "/home"}` reads `~/.ssh` without naming a single file under it (PR #334
2333
+ * finding 4). Compared canonically, both ways, because either side may be reached by a link.
2334
+ */
2335
+ scopeReachesHome: (canonicalScope) => {
2336
+ const homes = scope.homeGuards ?? [];
2337
+ if (homes.some((home) => containsPath(canonicalScope, home))) return true;
2338
+ return guardedRoots.some((root) => containsPath(root, canonicalScope));
2339
+ }
2340
+ };
2341
+ }
2342
+ function memoryTargetSpellings(target, canonicalRoot, restrictive, relativeOf = relative3) {
2343
+ const spellings = [toPosix(target.filePath), toPosix(target.canonicalPath)];
2344
+ if (restrictive) spellings.push(toPosix(relativeOf(canonicalRoot, target.canonicalPath)));
2345
+ return spellings;
2346
+ }
2347
+ function spellingsOfPath(paths, raw, includeReadOnly, restrictive) {
2348
+ const spellings = [];
2349
+ const { memoryWrite } = paths.scope;
2350
+ if (memoryWrite && !includeReadOnly) {
2351
+ const memory = resolveMutationPath(
2352
+ {
2353
+ workspaceRoot: paths.scope.root,
2354
+ additionalRoots: paths.scope.additionalRoots,
2355
+ memoryWrite
2356
+ },
2357
+ raw
2358
+ );
2359
+ if (memory?.memory) {
2360
+ spellings.push(
2361
+ ...memoryTargetSpellings(memory, canonicalizePath(resolve3(memoryWrite.root)), restrictive)
2362
+ );
2363
+ }
2364
+ }
2365
+ const detail = paths.detail(raw, includeReadOnly);
2366
+ if (!("path" in detail)) {
2367
+ if (restrictive) spellings.push(toPosix(raw), toPosix(resolve3(paths.scope.root, raw)));
2368
+ return [...new Set(spellings)].filter((s) => s !== "" && s !== raw);
2369
+ }
2370
+ const resolved = detail.path;
2371
+ const inWorkspace = resolveWorkspacePath(paths.scope.root, raw) !== null;
2372
+ if (inWorkspace) {
2373
+ spellings.push(
2374
+ resolved.relativePath,
2375
+ toPosix(resolved.filePath),
2376
+ toPosix(resolved.canonicalPath)
2377
+ );
2378
+ const realRoot = realRootOf(paths.scope);
2379
+ if (realRoot) spellings.push(toPosix(relative3(realRoot, resolved.canonicalPath)));
2380
+ } else {
2381
+ spellings.push(toPosix(resolved.filePath), toPosix(resolved.canonicalPath));
2382
+ if (restrictive) {
2383
+ for (const root of paths.scope.additionalRoots ?? []) {
2384
+ const fromRoot = relative3(canonicalizePath(root), resolved.canonicalPath);
2385
+ if (!fromRoot || isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep2}`)) {
2386
+ continue;
2387
+ }
2388
+ spellings.push(toPosix(fromRoot));
2389
+ }
2390
+ }
2391
+ }
2392
+ return [...new Set(spellings)].filter((spelling) => spelling !== "" && spelling !== raw);
2393
+ }
2394
+ function pathRuleSpellings(paths, toolName, args, restrictive) {
2395
+ const raw = pathArgument(toolName, args);
2396
+ return raw ? spellingsOfPath(paths, raw, toolName === "Read", restrictive) : [];
2397
+ }
2398
+ function globTarget(pattern, paths) {
2399
+ const { refusal, scopes } = globWalkPlan(pattern, paths.scope.root);
2400
+ if (refusal) return { target: "invalid", reason: refusal };
2401
+ if (scopes === null) return "outside";
2402
+ let guarded = false;
2403
+ for (const scope of scopes) {
2404
+ const detail = paths.detail(scope, false);
2405
+ if (!("path" in detail)) return "outside";
2406
+ if (paths.scopeReachesHome(detail.path.canonicalPath)) guarded = true;
2407
+ }
2408
+ return guarded ? "guarded" : "servable";
2409
+ }
2410
+ function readToolTarget(toolName, args, paths) {
2411
+ if (toolName === "Read") {
2412
+ const raw2 = pathArgument(toolName, args);
2413
+ if (!raw2) return "none";
2414
+ const detail2 = paths.detail(raw2, true);
2415
+ if (!("path" in detail2)) return "outside";
2416
+ return paths.isGuarded(detail2.path.canonicalPath) ? "guarded" : "servable";
2417
+ }
2418
+ if (toolName === "Glob") {
2419
+ const pattern = typeof args.pattern === "string" ? args.pattern.trim() : "";
2420
+ if (!pattern) return "none";
2421
+ return globTarget(pattern, paths);
2422
+ }
2423
+ const raw = typeof args.path === "string" ? args.path.trim() : "";
2424
+ if (raw === "" || raw === ".") return "servable";
2425
+ const detail = paths.detail(raw, false);
2426
+ if (!("path" in detail)) return "outside";
2427
+ if (paths.isGuarded(detail.path.canonicalPath)) return "guarded";
2428
+ return paths.scopeReachesHome(detail.path.canonicalPath) ? "guarded" : "servable";
2429
+ }
2430
+ function parseRuleLists(settings) {
2431
+ const list = (texts) => texts.map((text) => ({ text, parsed: parseRule(text) }));
2432
+ return {
2433
+ deny: list(settings.permissions.deny),
2434
+ ask: list(settings.permissions.ask),
2435
+ allow: list(settings.permissions.allow)
2436
+ };
2437
+ }
2438
+ function hasReadAdjudication(lists) {
2439
+ return [...lists.deny, ...lists.ask].some(
2440
+ ({ parsed }) => WORKSPACE_READ_TOOLS.has(parsed.toolName)
2441
+ );
2442
+ }
2443
+ var MEMORY_WRITE_TOOLS = /* @__PURE__ */ new Set([
2444
+ "Write",
2445
+ "Edit",
2446
+ "MultiEdit",
2447
+ "ApplyPatch"
2448
+ ]);
2449
+ function writesOnlyMemory(tool, args, scope) {
2450
+ if (!scope?.memoryWrite || !MEMORY_WRITE_TOOLS.has(tool)) return false;
2451
+ const roots = {
2452
+ workspaceRoot: scope.root,
2453
+ additionalRoots: scope.additionalRoots,
2454
+ memoryWrite: scope.memoryWrite
2455
+ };
2456
+ const targets = tool === "ApplyPatch" ? patchOperations(args).map((operation) => operation.path) : [pathArgument(tool, args)];
2457
+ return targets.length > 0 && targets.every(
2458
+ (target) => target !== void 0 && resolveMutationPath(roots, target)?.memory === true
2459
+ );
2460
+ }
2461
+ function removesOnlyMemory(tool, args, scope) {
2462
+ if (tool !== "Bash" || scope?.memoryWrite?.scope !== "full") return false;
2463
+ if (typeof args.command !== "string" || args.run_in_background === true) return false;
2464
+ return strictRmOfMarkdownUnder(args.command, scope.memoryWrite.root) !== null;
2465
+ }
2466
+ function evaluatePermission(toolName, args, settings, options = {}) {
2467
+ return evaluatePermissionDetail(toolName, args, settings, options).decision;
2468
+ }
2469
+ function evaluatePermissionDetail(toolName, args, settings, options = {}) {
2470
+ const scope = options.workspace;
2471
+ const paths = scope ? resolvedPathScope(scope) : void 0;
2472
+ const parsed = parseRuleLists(settings);
2473
+ if (toolName === "ApplyPatch") {
2474
+ const operations = patchOperations(args);
2475
+ const spellings = /* @__PURE__ */ new Map();
2476
+ const restrictiveSpellings = /* @__PURE__ */ new Map();
2477
+ const spellingsOf = (path, restrictive) => {
2478
+ const cache = restrictive ? restrictiveSpellings : spellings;
2479
+ let known = cache.get(path);
2480
+ if (!known) {
2481
+ known = paths ? [path, ...spellingsOfPath(paths, path, false, restrictive)] : [path];
2482
+ cache.set(path, known);
2483
+ }
2484
+ return known;
2485
+ };
2486
+ const isCompatible = (rule) => rule.toolName === "ApplyPatch" || rule.toolName === "Edit" || rule.toolName === "Write";
2487
+ for (const list of ["deny", "ask"]) {
2488
+ for (const entry of parsed[list]) {
2489
+ if (!isCompatible(entry.parsed)) continue;
2490
+ if (compatiblePatchRuleMatches(entry.parsed, operations, spellingsOf, true)) {
2491
+ return { decision: list, matchedRule: entry.text, source: list };
2492
+ }
2493
+ }
2494
+ }
2495
+ const allowRules = parsed.allow.filter(({ parsed: rule }) => isCompatible(rule));
2496
+ const firstCompatible = allowRules[0]?.text;
2497
+ if (operations.length > 0 && operations.every(
2498
+ (operation) => allowRules.some(
2499
+ ({ parsed: rule }) => patchRuleSupportsOperation(rule, operation) && (rule.pattern === null || ruleMatchesPatch(rule, spellingsOf(operation.path, false), false))
2500
+ )
2501
+ )) {
2502
+ return { decision: "allow", matchedRule: firstCompatible, source: "allow" };
2503
+ }
2504
+ if (allowRules.some(({ parsed: rule }) => rule.toolName === "ApplyPatch" && rule.pattern === null)) {
2505
+ return { decision: "allow", matchedRule: firstCompatible, source: "allow" };
2506
+ }
2507
+ if (writesOnlyMemory("ApplyPatch", args, scope)) return { decision: "allow", source: "memory" };
2508
+ return { decision: "ask", source: "default" };
2509
+ }
2510
+ const tool = canonicalToolName(toolName);
2511
+ const candidates = [getPrimaryArg(args)];
2512
+ let restrictiveCandidates = tool === "Bash" ? bashRestrictiveCandidates(args.command, candidates[0]) : candidates;
2513
+ if (scope && paths && PATH_RULE_TOOLS.has(tool) && rulesName(parsed, tool)) {
2514
+ restrictiveCandidates = [...candidates, ...pathRuleSpellings(paths, tool, args, true)];
2515
+ candidates.push(...pathRuleSpellings(paths, tool, args, false));
2516
+ }
2517
+ const matchesRule = (rule, fold) => {
2518
+ const pool = fold ? restrictiveCandidates : candidates;
2519
+ if (tool === "WebFetch") {
2520
+ try {
2521
+ const url = new URL(candidates[0]).toString();
2522
+ return ruleMatchesCandidate(rule, tool, url, false);
2523
+ } catch {
2524
+ }
2525
+ }
2526
+ return pool.some((candidate) => ruleMatchesCandidate(rule, tool, candidate, fold));
2527
+ };
2528
+ const readTarget = scope && paths && (scope.judgeReads || scope.refuseUnreachableReads) && WORKSPACE_READ_TOOLS.has(tool) ? readToolTarget(tool, args, paths) : void 0;
2529
+ const unreadablePattern = readTarget !== void 0 && isUnreadablePattern(readTarget) ? readTarget.reason : void 0;
2530
+ const outside = readTarget === "outside" ? { outsideWorkspace: true } : {};
2531
+ const platform = options.platform ?? process.platform;
2532
+ const bashReading = tool === "Bash" && typeof args.command === "string" ? readBashCommand(args.command, platform, options.shell) : void 0;
2533
+ const compoundUnderPolicy = bashReading?.compound === true && hasBashRestriction(parsed) ? { compoundUnderPolicy: true } : {};
2534
+ for (const entry of parsed.deny) {
2535
+ if (matchesRule(entry.parsed, true)) {
2536
+ return { decision: "deny", matchedRule: entry.text, source: "deny" };
2537
+ }
2538
+ }
2539
+ for (const entry of parsed.ask) {
2540
+ if (matchesRule(entry.parsed, true)) {
2541
+ const askOutside = readTarget === void 0 && scope && paths && WORKSPACE_READ_TOOLS.has(tool) && readToolTarget(tool, args, paths) === "outside" ? { outsideWorkspace: true } : outside;
2542
+ return {
2543
+ decision: "ask",
2544
+ matchedRule: entry.text,
2545
+ source: "ask",
2546
+ ...askOutside,
2547
+ ...compoundUnderPolicy
2548
+ };
2549
+ }
2550
+ }
2551
+ if (tool === "Bash") {
2552
+ for (const entry of parsed.allow) {
2553
+ if (isExactPattern(entry.parsed) && bashAllowRuleMatches(entry.parsed, args.command, platform, options.shell, bashReading)) {
2554
+ return { decision: "allow", matchedRule: entry.text, source: "allow" };
2555
+ }
2556
+ }
2557
+ if (compoundUnderPolicy.compoundUnderPolicy) {
2558
+ return { decision: "ask", source: "default", ...compoundUnderPolicy };
2559
+ }
2560
+ }
2561
+ for (const entry of parsed.allow) {
2562
+ const matched = tool === "Bash" ? bashAllowRuleMatches(entry.parsed, args.command, platform, options.shell, bashReading) : matchesRule(entry.parsed, false);
2563
+ if (matched) {
2564
+ return { decision: "allow", matchedRule: entry.text, source: "allow" };
2565
+ }
2566
+ }
2567
+ if (writesOnlyMemory(tool, args, scope) || removesOnlyMemory(tool, args, scope)) {
2568
+ return { decision: "allow", source: "memory" };
2569
+ }
2570
+ if (sandboxAutoAllows(toolName, args, settings, options)) {
2571
+ return { decision: "allow", source: "sandbox" };
2572
+ }
2573
+ if (scope?.autoAllowReads && (readTarget === "none" || readTarget === "servable" && (tool === "Read" || !hasReadAdjudication(parsed)))) {
2574
+ return { decision: "allow", source: "workspace" };
2575
+ }
2576
+ if (scope?.autoAllowReads && WORKSPACE_READ_ONLY_GIT_TOOLS.has(tool)) {
2577
+ return { decision: "allow", source: "workspace" };
2578
+ }
2579
+ if (ALWAYS_ALLOWED_TOOLS.has(tool)) {
2580
+ return { decision: "allow", source: "default" };
2581
+ }
2582
+ if (unreadablePattern !== void 0) {
2583
+ return { decision: "refuse", source: "default", unreadablePattern };
2584
+ }
2585
+ if (readTarget === "outside") {
2586
+ return { decision: "refuse", source: "default", outsideWorkspace: true };
2587
+ }
2588
+ return { decision: "ask", source: "default" };
2589
+ }
2590
+ var ALWAYS_ALLOWED_TOOLS = /* @__PURE__ */ new Set(["TodoWrite", "Advisor"]);
2591
+
2592
+ export {
2593
+ EFFORT_LEVELS,
2594
+ EFFORT_USAGE,
2595
+ parseEffortLevel,
2596
+ isEffortLevel,
2597
+ getAvailableEffortLevels,
2598
+ getEffortUnavailableError,
2599
+ updateEffortLevel,
2600
+ readLegacyBaseUrl,
2601
+ loadConfig,
2602
+ isEffortChosen,
2603
+ applyModelDefaults,
2604
+ freezeAgentConfig,
2605
+ describeUnresolvedProviderPrefix,
2606
+ resolveModelProviderConfig,
2607
+ clampEffortToCatalog,
2608
+ resolveEffortExplicit,
2609
+ compactsOnSessionModel,
2610
+ resolveCompactModelConfig,
2611
+ configuredAdvisorModel,
2612
+ advisorModelRefusal,
2613
+ resolveAdvisorModelConfig,
2614
+ resolveReducerModelConfig,
2615
+ resolveSecret,
2616
+ throwIfAborted,
2617
+ onAbort,
2618
+ delay,
2619
+ yieldToEventLoop,
2620
+ renderDiffWithStatsAsync,
2621
+ workspaceIdentity,
2622
+ normalizeObservedPath,
2623
+ observationKey,
2624
+ observationPathFor,
2625
+ supersedesObservation,
2626
+ seedObservationLedger,
2627
+ observeFile,
2628
+ requireFreshObservation,
2629
+ requireObservationForMutation,
2630
+ containsSecretPattern,
2631
+ checkMemoryWrite,
2632
+ noticeMemoryWrite,
2633
+ withMemoryWarnings,
2634
+ collectProviderSecrets,
2635
+ redactSecrets,
2636
+ secretSafeCut,
2637
+ secretSafeStart,
2638
+ redactToolResultSecrets,
2639
+ redactedSecretOverwrite,
2640
+ redactedSecretAddition,
2641
+ redactedSecretDeletion,
2642
+ redactedSecretCommand,
2643
+ readTextSnapshot,
2644
+ restoreTextEncoding,
2645
+ writeFileAtomically,
2646
+ withMutationLocks,
2647
+ parsePatch,
2648
+ applyHunks,
2649
+ patchTools,
2650
+ hasCompoundShellSyntax,
2651
+ strictRmMatchesGlob,
2652
+ WORKSPACE_READ_TOOLS,
2653
+ WORKSPACE_READ_JUDGING_MODES,
2654
+ WORKSPACE_READ_REFUSAL_MODES,
2655
+ WORKSPACE_READ_ONLY_GIT_TOOLS,
2656
+ parseRule,
2657
+ permissionRuleForToolCall,
2658
+ missingRuleReason,
2659
+ permissionResultOf,
2660
+ permissionRuleOf,
2661
+ permissionReasonOf,
2662
+ permissionRuleLadder,
2663
+ permissionRuleMatchesCall,
2664
+ primaryArgForRule,
2665
+ hasAdjudicationPolicy,
2666
+ hasBashAdjudicationPolicy,
2667
+ memoryTargetSpellings,
2668
+ evaluatePermission,
2669
+ evaluatePermissionDetail,
2670
+ ALWAYS_ALLOWED_TOOLS
2671
+ };
2672
+ //# sourceMappingURL=chunk-N2G4Q7ME.js.map