@kolisachint/hoocode-agent 0.4.163 → 0.4.165

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 (164) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/cli/args.d.ts +0 -6
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +0 -22
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  7. package/dist/core/agent-frontmatter.js +0 -6
  8. package/dist/core/agent-frontmatter.js.map +1 -1
  9. package/dist/core/agent-session-services.d.ts +0 -2
  10. package/dist/core/agent-session-services.d.ts.map +1 -1
  11. package/dist/core/agent-session-services.js +0 -2
  12. package/dist/core/agent-session-services.js.map +1 -1
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +0 -2
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/embsearch/client.d.ts +18 -2
  17. package/dist/core/embsearch/client.d.ts.map +1 -1
  18. package/dist/core/embsearch/client.js +20 -3
  19. package/dist/core/embsearch/client.js.map +1 -1
  20. package/dist/core/embsearch/embsearch-service.d.ts +18 -1
  21. package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
  22. package/dist/core/embsearch/embsearch-service.js +52 -4
  23. package/dist/core/embsearch/embsearch-service.js.map +1 -1
  24. package/dist/core/sdk.d.ts +0 -16
  25. package/dist/core/sdk.d.ts.map +1 -1
  26. package/dist/core/sdk.js +0 -2
  27. package/dist/core/sdk.js.map +1 -1
  28. package/dist/core/search/eval-compare.d.ts +57 -0
  29. package/dist/core/search/eval-compare.d.ts.map +1 -0
  30. package/dist/core/search/eval-compare.js +114 -0
  31. package/dist/core/search/eval-compare.js.map +1 -0
  32. package/dist/core/search/eval-gold.d.ts +47 -0
  33. package/dist/core/search/eval-gold.d.ts.map +1 -0
  34. package/dist/core/search/eval-gold.js +172 -0
  35. package/dist/core/search/eval-gold.js.map +1 -0
  36. package/dist/core/search/eval-harness.d.ts +140 -0
  37. package/dist/core/search/eval-harness.d.ts.map +1 -0
  38. package/dist/core/search/eval-harness.js +225 -0
  39. package/dist/core/search/eval-harness.js.map +1 -0
  40. package/dist/core/search/eval.d.ts +66 -8
  41. package/dist/core/search/eval.d.ts.map +1 -1
  42. package/dist/core/search/eval.js +67 -12
  43. package/dist/core/search/eval.js.map +1 -1
  44. package/dist/core/search/hybrid-search.d.ts +16 -0
  45. package/dist/core/search/hybrid-search.d.ts.map +1 -1
  46. package/dist/core/search/hybrid-search.js +54 -2
  47. package/dist/core/search/hybrid-search.js.map +1 -1
  48. package/dist/core/search/mode.d.ts +21 -5
  49. package/dist/core/search/mode.d.ts.map +1 -1
  50. package/dist/core/search/mode.js +23 -10
  51. package/dist/core/search/mode.js.map +1 -1
  52. package/dist/core/search/rerank.d.ts.map +1 -1
  53. package/dist/core/search/rerank.js +70 -11
  54. package/dist/core/search/rerank.js.map +1 -1
  55. package/dist/core/search/rrf.d.ts +19 -6
  56. package/dist/core/search/rrf.d.ts.map +1 -1
  57. package/dist/core/search/rrf.js +19 -6
  58. package/dist/core/search/rrf.js.map +1 -1
  59. package/dist/core/search/types.d.ts +11 -1
  60. package/dist/core/search/types.d.ts.map +1 -1
  61. package/dist/core/search/types.js.map +1 -1
  62. package/dist/core/settings-defaults.d.ts +0 -3
  63. package/dist/core/settings-defaults.d.ts.map +1 -1
  64. package/dist/core/settings-defaults.js +0 -3
  65. package/dist/core/settings-defaults.js.map +1 -1
  66. package/dist/core/settings-manager.d.ts +0 -6
  67. package/dist/core/settings-manager.d.ts.map +1 -1
  68. package/dist/core/settings-manager.js +0 -24
  69. package/dist/core/settings-manager.js.map +1 -1
  70. package/dist/core/settings-types.d.ts +0 -3
  71. package/dist/core/settings-types.d.ts.map +1 -1
  72. package/dist/core/settings-types.js.map +1 -1
  73. package/dist/core/tools/bash.d.ts.map +1 -1
  74. package/dist/core/tools/bash.js +3 -1
  75. package/dist/core/tools/bash.js.map +1 -1
  76. package/dist/core/tools/edit.d.ts.map +1 -1
  77. package/dist/core/tools/edit.js +5 -5
  78. package/dist/core/tools/edit.js.map +1 -1
  79. package/dist/core/tools/find.d.ts.map +1 -1
  80. package/dist/core/tools/find.js +8 -10
  81. package/dist/core/tools/find.js.map +1 -1
  82. package/dist/core/tools/grep.d.ts.map +1 -1
  83. package/dist/core/tools/grep.js +4 -4
  84. package/dist/core/tools/grep.js.map +1 -1
  85. package/dist/core/tools/index.d.ts +0 -20
  86. package/dist/core/tools/index.d.ts.map +1 -1
  87. package/dist/core/tools/index.js +0 -13
  88. package/dist/core/tools/index.js.map +1 -1
  89. package/dist/core/tools/ls.d.ts.map +1 -1
  90. package/dist/core/tools/ls.js +2 -2
  91. package/dist/core/tools/ls.js.map +1 -1
  92. package/dist/core/tools/read.d.ts.map +1 -1
  93. package/dist/core/tools/read.js +10 -8
  94. package/dist/core/tools/read.js.map +1 -1
  95. package/dist/core/tools/search.d.ts.map +1 -1
  96. package/dist/core/tools/search.js +6 -2
  97. package/dist/core/tools/search.js.map +1 -1
  98. package/dist/main.d.ts.map +1 -1
  99. package/dist/main.js +0 -30
  100. package/dist/main.js.map +1 -1
  101. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  102. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/settings-selector.js +2 -2
  104. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  105. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.js +0 -18
  107. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/dist/utils/tools-manager.d.ts +1 -1
  109. package/dist/utils/tools-manager.d.ts.map +1 -1
  110. package/dist/utils/tools-manager.js +1 -49
  111. package/dist/utils/tools-manager.js.map +1 -1
  112. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  113. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  114. package/examples/extensions/sandbox/package.json +1 -1
  115. package/examples/extensions/with-deps/package.json +1 -1
  116. package/package.json +7 -4
  117. package/dist/core/tools/browser/browser-continue.d.ts +0 -24
  118. package/dist/core/tools/browser/browser-continue.d.ts.map +0 -1
  119. package/dist/core/tools/browser/browser-continue.js +0 -84
  120. package/dist/core/tools/browser/browser-continue.js.map +0 -1
  121. package/dist/core/tools/browser/browser-run.d.ts +0 -47
  122. package/dist/core/tools/browser/browser-run.d.ts.map +0 -1
  123. package/dist/core/tools/browser/browser-run.js +0 -377
  124. package/dist/core/tools/browser/browser-run.js.map +0 -1
  125. package/dist/core/tools/browser/browsertools-shared.d.ts +0 -177
  126. package/dist/core/tools/browser/browsertools-shared.d.ts.map +0 -1
  127. package/dist/core/tools/browser/browsertools-shared.js +0 -335
  128. package/dist/core/tools/browser/browsertools-shared.js.map +0 -1
  129. package/dist/core/tools/browser/index.d.ts +0 -8
  130. package/dist/core/tools/browser/index.d.ts.map +0 -1
  131. package/dist/core/tools/browser/index.js +0 -8
  132. package/dist/core/tools/browser/index.js.map +0 -1
  133. package/dist/core/tools/doc/docedit.d.ts +0 -43
  134. package/dist/core/tools/doc/docedit.d.ts.map +0 -1
  135. package/dist/core/tools/doc/docedit.js +0 -133
  136. package/dist/core/tools/doc/docedit.js.map +0 -1
  137. package/dist/core/tools/doc/docgrep.d.ts +0 -27
  138. package/dist/core/tools/doc/docgrep.d.ts.map +0 -1
  139. package/dist/core/tools/doc/docgrep.js +0 -106
  140. package/dist/core/tools/doc/docgrep.js.map +0 -1
  141. package/dist/core/tools/doc/docpeek.d.ts +0 -24
  142. package/dist/core/tools/doc/docpeek.d.ts.map +0 -1
  143. package/dist/core/tools/doc/docpeek.js +0 -112
  144. package/dist/core/tools/doc/docpeek.js.map +0 -1
  145. package/dist/core/tools/doc/docread.d.ts +0 -31
  146. package/dist/core/tools/doc/docread.d.ts.map +0 -1
  147. package/dist/core/tools/doc/docread.js +0 -113
  148. package/dist/core/tools/doc/docread.js.map +0 -1
  149. package/dist/core/tools/doc/docscan.d.ts +0 -28
  150. package/dist/core/tools/doc/docscan.d.ts.map +0 -1
  151. package/dist/core/tools/doc/docscan.js +0 -110
  152. package/dist/core/tools/doc/docscan.js.map +0 -1
  153. package/dist/core/tools/doc/docwrite.d.ts +0 -43
  154. package/dist/core/tools/doc/docwrite.d.ts.map +0 -1
  155. package/dist/core/tools/doc/docwrite.js +0 -102
  156. package/dist/core/tools/doc/docwrite.js.map +0 -1
  157. package/dist/core/tools/doc/index.d.ts +0 -11
  158. package/dist/core/tools/doc/index.d.ts.map +0 -1
  159. package/dist/core/tools/doc/index.js +0 -11
  160. package/dist/core/tools/doc/index.js.map +0 -1
  161. package/dist/core/tools/filetools-shared.d.ts +0 -268
  162. package/dist/core/tools/filetools-shared.d.ts.map +0 -1
  163. package/dist/core/tools/filetools-shared.js +0 -403
  164. package/dist/core/tools/filetools-shared.js.map +0 -1
@@ -1,403 +0,0 @@
1
- /**
2
- * Shared plumbing for the `DocRead` / `DocEdit` / `DocWrite` tools.
3
- *
4
- * All three shell out to the `filetools` binary (extract / reconstruct
5
- * subcommands, resolved/downloaded via {@link ensureTool}) to losslessly
6
- * project structured/binary documents (XML, drawio, OOXML, PDF) into editable,
7
- * id-addressed JSON and reconstruct them after id-based patches.
8
- *
9
- * Unlike webtools, the filetools CLI is file-oriented, not stdout-oriented:
10
- * `extract` writes the envelope JSON to `--out` and the sidecar id-map next to
11
- * it, emitting only a human status line on stderr. This module therefore:
12
- * - owns a per-process working directory where envelopes + sidecars live,
13
- * - runs extract/reconstruct and reads the resulting files back,
14
- * - keeps a small cache mapping a source file to its extracted envelope +
15
- * sidecar, so a DocRead can be followed by a DocEdit/DocWrite (the stateful
16
- * extract -> patch -> reconstruct flow), and
17
- * - exposes the locked JSON wire types mirroring the Rust `model.rs`/`patch.rs`.
18
- */
19
- import { createHash } from "node:crypto";
20
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
21
- import { tmpdir } from "node:os";
22
- import { basename, join } from "node:path";
23
- import { Type } from "typebox";
24
- import { APP_NAME } from "../../config.js";
25
- import { ensureTool } from "../../utils/tools-manager.js";
26
- import { execCommand } from "../exec.js";
27
- /** Default timeout (seconds) for a single filetools invocation. */
28
- const FILETOOLS_DEFAULT_TIMEOUT_SECS = 30;
29
- /**
30
- * Soft token ceiling for a single DocRead render. The filetools binary has no
31
- * pagination, so a dense file (e.g. a large spreadsheet) can project into a
32
- * huge id-addressed dump that floods the model context and burns tokens. We
33
- * cannot make the extract itself smaller without the binary's help, so DocRead
34
- * truncates the rendered view to roughly this budget and tells the model how to
35
- * narrow it (readonly projection, a smaller/targeted file, or direct edits).
36
- */
37
- export const DOCREAD_MAX_RENDER_TOKENS = 10000;
38
- /** Rough token estimate (chars/4), matching the agent's compaction heuristic. */
39
- export function estimateTextTokens(text) {
40
- return Math.ceil(text.length / 4);
41
- }
42
- /**
43
- * Truncate rendered envelope lines to roughly `maxTokens`, keeping whole lines.
44
- * Returns the kept text plus how many lines were dropped (0 when nothing was
45
- * truncated).
46
- */
47
- export function truncateRenderToTokenBudget(lines, maxTokens = DOCREAD_MAX_RENDER_TOKENS) {
48
- const full = lines.join("\n");
49
- if (estimateTextTokens(full) <= maxTokens) {
50
- return { text: full, droppedLines: 0 };
51
- }
52
- const budgetChars = maxTokens * 4;
53
- const kept = [];
54
- let used = 0;
55
- for (const line of lines) {
56
- const next = used + line.length + 1; // + newline
57
- if (next > budgetChars && kept.length > 0)
58
- break;
59
- kept.push(line);
60
- used = next;
61
- }
62
- return { text: kept.join("\n"), droppedLines: lines.length - kept.length };
63
- }
64
- // ----------------------------------------------------------------------------
65
- // TypeBox schema for the model-facing patch input (shared by DocEdit/DocWrite)
66
- // ----------------------------------------------------------------------------
67
- const attrSchema = Type.Object({
68
- name: Type.String(),
69
- value: Type.String(),
70
- });
71
- const newElementSchema = Type.Object({
72
- tag: Type.String({ description: 'Element tag name, e.g. "w:p" or "mxCell".' }),
73
- attrs: Type.Optional(Type.Array(attrSchema, { description: "Attributes in document order." })),
74
- text: Type.Optional(Type.String({ description: "Inline text content (text-only elements, v1)." })),
75
- });
76
- const patchOpSchema = Type.Union([
77
- Type.Object({
78
- op: Type.Literal("test"),
79
- path: Type.String({ description: "Pointer `/structure/<id>` (or /text, /attrs/<name>) to guard." }),
80
- hash: Type.String({ description: "Expected content hash of the target node." }),
81
- }, { description: "Optimistic guard: assert the target node's content hash before mutating." }),
82
- Type.Object({
83
- op: Type.Literal("replace"),
84
- path: Type.String({
85
- description: "`/structure/<id>/text` for element text, or `/structure/<id>/attrs/<name>` for an attribute.",
86
- }),
87
- value: Type.String({ description: "New text or attribute value." }),
88
- }, { description: "Replace an element's text or an attribute value." }),
89
- Type.Object({
90
- op: Type.Literal("add"),
91
- after: Type.Optional(Type.String({ description: "Anchor node id to insert AFTER." })),
92
- before: Type.Optional(Type.String({ description: "Anchor node id to insert BEFORE." })),
93
- value: newElementSchema,
94
- }, { description: "Insert a new element next to an anchor. Provide exactly one of `after`/`before`." }),
95
- Type.Object({
96
- op: Type.Literal("remove"),
97
- path: Type.String({ description: "Pointer `/structure/<id>` of the element to delete." }),
98
- }, { description: "Delete an element and all its bytes." }),
99
- ]);
100
- /**
101
- * The model-facing patch parameter: an array of id-based RFC-6902 ops, matching
102
- * the filetools patch wire format. Shared by DocEdit and DocWrite.
103
- */
104
- export const patchOpsSchema = Type.Array(patchOpSchema, {
105
- description: "Ordered id-based patch ops (test/replace/add/remove) targeting node ids from a prior DocRead. Applied atomically.",
106
- });
107
- /** Wrap the model-facing ops array into the binary's `{ patch: [...] }` envelope. */
108
- export function toPatch(ops) {
109
- return { patch: ops };
110
- }
111
- /** Find a node by id anywhere in a (recursive) structure tree. */
112
- export function findNodeById(nodes, id) {
113
- for (const node of nodes) {
114
- if (node.id === id)
115
- return node;
116
- if (node.children) {
117
- const hit = findNodeById(node.children, id);
118
- if (hit)
119
- return hit;
120
- }
121
- }
122
- return undefined;
123
- }
124
- /**
125
- * Extract the target node id from a patch op pointer. Returns undefined for ops
126
- * that reference a node by anchor (`add`) rather than a `/structure/<id>/...`
127
- * path. Pointer shapes: `/structure/<id>`, `/structure/<id>/text`,
128
- * `/structure/<id>/attrs/<name>`.
129
- */
130
- export function patchOpNodeId(op) {
131
- if (op.op === "add")
132
- return op.after ?? op.before;
133
- const parts = op.path.split("/");
134
- // ["", "structure", "<id>", ...]
135
- return parts[1] === "structure" ? parts[2] : undefined;
136
- }
137
- /**
138
- * Validate that every node id referenced by `ops` still exists in `structure`.
139
- * Returns the ids that are missing (empty array means the patch is applicable to
140
- * this extract). Used to detect when a patch was authored against a stale
141
- * extract — e.g. after an external tool rewrote the document.
142
- */
143
- export function findMissingPatchIds(ops, structure) {
144
- const missing = [];
145
- for (const op of ops) {
146
- const id = patchOpNodeId(op);
147
- if (id && !findNodeById(structure, id))
148
- missing.push(id);
149
- }
150
- return missing;
151
- }
152
- /**
153
- * Render id-addressed node lines (`#id <tag attrs> :: "text"`), the compact
154
- * view the model reads and patches against. Shared by DocRead's envelope render
155
- * and DocPeek's hydrated-block render so both speak the exact same dialect.
156
- */
157
- export function renderDocNodeLines(nodes) {
158
- const lines = [];
159
- const walk = (ns, depth) => {
160
- for (const node of ns) {
161
- const indent = " ".repeat(depth);
162
- const idPart = node.id ? `#${node.id} ` : "";
163
- const attrs = node.attrs?.length ? ` ${node.attrs.map((a) => `${a.name}="${a.value}"`).join(" ")}` : "";
164
- const text = node.text !== undefined ? ` :: ${JSON.stringify(node.text)}` : "";
165
- lines.push(`${indent}${idPart}<${node.tag}${attrs}>${text}`);
166
- if (node.children?.length)
167
- walk(node.children, depth + 1);
168
- }
169
- };
170
- walk(nodes, 0);
171
- return lines;
172
- }
173
- // ============================================================================
174
- // Binary runner + working directory
175
- // ============================================================================
176
- const BINARY_MISSING_MESSAGE = "filetools binary unavailable and could not be downloaded — the document tools require the `filetools` CLI on PATH or a published release for this platform";
177
- /** Lazily-created per-process working directory for envelopes + sidecars. */
178
- let workDir;
179
- function getWorkDir() {
180
- if (workDir)
181
- return workDir;
182
- const base = join(tmpdir(), `${APP_NAME}-filetools`);
183
- mkdirSync(base, { recursive: true });
184
- workDir = mkdtempSync(join(base, "doc-"));
185
- return workDir;
186
- }
187
- /** Short, filesystem-safe key for a source path (used to name its subdir). */
188
- function pathKey(absolutePath) {
189
- return createHash("sha256").update(absolutePath).digest("hex").slice(0, 16);
190
- }
191
- async function resolveBinary() {
192
- const binaryPath = await ensureTool("filetools", true);
193
- if (!binaryPath)
194
- throw new Error(BINARY_MISSING_MESSAGE);
195
- return binaryPath;
196
- }
197
- async function runFiletools(binaryPath, subcommand, args, cwd, signal, timeoutSecs) {
198
- if (signal?.aborted)
199
- throw new Error("Operation aborted");
200
- const spawnTimeoutMs = (timeoutSecs + 5) * 1000;
201
- const result = await execCommand(binaryPath, [subcommand, ...args], cwd, { signal, timeout: spawnTimeoutMs });
202
- if (signal?.aborted)
203
- throw new Error("Operation aborted");
204
- if (result.killed)
205
- throw new Error(`filetools ${subcommand} timed out after ${timeoutSecs}s`);
206
- if (result.code !== 0) {
207
- const stderr = result.stderr.trim();
208
- throw new Error(stderr || `filetools ${subcommand} exited with code ${result.code}`);
209
- }
210
- // Status goes to stderr; callers read the produced files, not stdout.
211
- return result.stderr.trim();
212
- }
213
- const records = new Map();
214
- function statSignature(absolutePath) {
215
- try {
216
- const st = statSync(absolutePath);
217
- return `${st.mtimeMs}:${st.size}`;
218
- }
219
- catch {
220
- return "absent";
221
- }
222
- }
223
- /**
224
- * Extract `absolutePath` to an envelope (+ sidecar) in the working directory,
225
- * cache the result keyed by the source path, and return the parsed envelope.
226
- *
227
- * `readonly` strips ids for a smaller, analysis-only projection that cannot be
228
- * reconstructed (DocRead's default-off mode).
229
- */
230
- export async function extractDocument(absolutePath, cwd, signal, options) {
231
- const binaryPath = await resolveBinary();
232
- const dir = join(getWorkDir(), pathKey(absolutePath));
233
- mkdirSync(dir, { recursive: true });
234
- const envelopePath = join(dir, "envelope.json");
235
- const args = ["--input", absolutePath, "--out", envelopePath];
236
- if (options?.readonly)
237
- args.push("--readonly");
238
- await runFiletools(binaryPath, "extract", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);
239
- const envelope = readEnvelope(envelopePath);
240
- if (!options?.readonly) {
241
- records.set(absolutePath, {
242
- source: absolutePath,
243
- envelopePath,
244
- envelope,
245
- signature: statSignature(absolutePath),
246
- });
247
- }
248
- return envelope;
249
- }
250
- function readEnvelope(envelopePath) {
251
- let raw;
252
- try {
253
- raw = readFileSync(envelopePath, "utf8");
254
- }
255
- catch {
256
- throw new Error("filetools extract produced no envelope");
257
- }
258
- try {
259
- return JSON.parse(raw);
260
- }
261
- catch {
262
- throw new Error("filetools extract produced a malformed envelope");
263
- }
264
- }
265
- /** Look up a cached extraction for `absolutePath`, if one is still valid. */
266
- function getExtractRecord(absolutePath) {
267
- const record = records.get(absolutePath);
268
- if (!record)
269
- return undefined;
270
- // Drop a stale record if the source changed since extract; reconstruct would
271
- // fail the binary's hash-drift guard anyway, but a clearer error is better.
272
- if (record.signature !== statSignature(absolutePath)) {
273
- records.delete(absolutePath);
274
- return undefined;
275
- }
276
- return record;
277
- }
278
- /** Drop any cached extraction for `absolutePath`. */
279
- export function invalidateExtractRecord(absolutePath) {
280
- records.delete(absolutePath);
281
- }
282
- /**
283
- * Thrown when a patch references node ids that are absent from the current
284
- * extract — typically because the document was rewritten out-of-band (e.g. by a
285
- * script) after the ids were read, or the patch was authored against an older
286
- * extract. Carries the freshly re-extracted envelope so the caller can surface
287
- * current ids to the agent without forcing a separate DocRead.
288
- */
289
- export class StalePatchError extends Error {
290
- envelope;
291
- missingIds;
292
- constructor(envelope, missingIds) {
293
- super(`patch references ${missingIds.length} node id${missingIds.length === 1 ? "" : "s"} that no longer exist ` +
294
- `in ${basename(envelope.source.path)} (${missingIds.slice(0, 5).join(", ")}` +
295
- `${missingIds.length > 5 ? ", …" : ""}). The document was re-extracted; re-issue the patch against the ids below.`);
296
- this.name = "StalePatchError";
297
- this.envelope = envelope;
298
- this.missingIds = missingIds;
299
- }
300
- }
301
- /**
302
- * Return a valid cached extract for `absolutePath`, re-extracting automatically
303
- * when the cache is missing or stale (e.g. the source changed on disk since the
304
- * last extract). This keeps DocEdit/DocWrite usable after an out-of-band write
305
- * without forcing the agent to call DocRead again.
306
- */
307
- export async function ensureExtractRecord(absolutePath, cwd, signal, options) {
308
- const existing = getExtractRecord(absolutePath);
309
- if (existing)
310
- return existing;
311
- invalidateExtractRecord(absolutePath);
312
- await extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });
313
- const record = getExtractRecord(absolutePath);
314
- if (!record) {
315
- throw new Error(`failed to extract ${basename(absolutePath)} — the document tools could not read it`);
316
- }
317
- return record;
318
- }
319
- /**
320
- * Apply `patch` to a document, writing the reconstructed bytes to `outPath`.
321
- * Auto-extracts when the cache is missing or stale (so an out-of-band rewrite no
322
- * longer forces a manual DocRead), then validates that the patch's node ids
323
- * still exist in the current extract. A mismatch throws {@link StalePatchError}
324
- * carrying the fresh envelope so the caller can show current ids.
325
- */
326
- export async function reconstructDocument(absolutePath, patch, outPath, cwd, signal, options) {
327
- const record = await ensureExtractRecord(absolutePath, cwd, signal, options);
328
- if (!record.envelope.writable) {
329
- throw new Error(`${basename(absolutePath)} is read-only (fidelity ${record.envelope.fidelity}); it cannot be edited`);
330
- }
331
- const missingIds = findMissingPatchIds(patch.patch, record.envelope.structure);
332
- if (missingIds.length > 0) {
333
- throw new StalePatchError(record.envelope, missingIds);
334
- }
335
- const binaryPath = await resolveBinary();
336
- const patchPath = join(getWorkDir(), pathKey(absolutePath), "patch.json");
337
- writeFileSync(patchPath, JSON.stringify(patch), "utf8");
338
- try {
339
- await runFiletools(binaryPath, "reconstruct", ["--envelope", record.envelopePath, "--patch", patchPath, "--out", outPath, "--original", absolutePath], cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);
340
- }
341
- finally {
342
- rmSync(patchPath, { force: true });
343
- }
344
- }
345
- /**
346
- * Run a stdout-oriented filetools subcommand (scan/grep/read) and parse its
347
- * single pretty-JSON object. These do not write files or populate the extract
348
- * cache, so they are safe to interleave with a pending DocEdit/DocWrite.
349
- */
350
- async function runFiletoolsJson(subcommand, args, cwd, signal, timeoutSecs) {
351
- const binaryPath = await resolveBinary();
352
- if (signal?.aborted)
353
- throw new Error("Operation aborted");
354
- const spawnTimeoutMs = (timeoutSecs + 5) * 1000;
355
- const result = await execCommand(binaryPath, [subcommand, ...args], cwd, { signal, timeout: spawnTimeoutMs });
356
- if (signal?.aborted)
357
- throw new Error("Operation aborted");
358
- if (result.killed)
359
- throw new Error(`filetools ${subcommand} timed out after ${timeoutSecs}s`);
360
- if (result.code !== 0) {
361
- const stderr = result.stderr.trim();
362
- throw new Error(stderr || `filetools ${subcommand} exited with code ${result.code}`);
363
- }
364
- const stdout = result.stdout.trim();
365
- if (!stdout)
366
- throw new Error(`filetools ${subcommand} produced no output`);
367
- try {
368
- return JSON.parse(stdout);
369
- }
370
- catch {
371
- throw new Error(`filetools ${subcommand} produced malformed JSON output`);
372
- }
373
- }
374
- /** Scan a document into a paginated manifest of block previews (no hydration). */
375
- export async function scanDocument(absolutePath, cwd, signal, options) {
376
- const args = ["--input", absolutePath];
377
- if (options?.offset !== undefined)
378
- args.push("--offset", String(options.offset));
379
- if (options?.limit !== undefined)
380
- args.push("--limit", String(options.limit));
381
- return runFiletoolsJson("scan", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);
382
- }
383
- /** Locate blocks containing `pattern` (literal substring) without hydrating the doc. */
384
- export async function grepDocument(absolutePath, pattern, cwd, signal, options) {
385
- const args = ["--input", absolutePath, "--pattern", pattern];
386
- if (options?.ignoreCase)
387
- args.push("--ignore-case");
388
- if (options?.limit !== undefined)
389
- args.push("--limit", String(options.limit));
390
- return runFiletoolsJson("grep", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);
391
- }
392
- /** Hydrate specific blocks by id (or a paginated slice when no ids are given). */
393
- export async function readDocumentBlocks(absolutePath, cwd, signal, options) {
394
- const args = ["--input", absolutePath];
395
- for (const id of options?.ids ?? [])
396
- args.push("--id", id);
397
- if (options?.offset !== undefined)
398
- args.push("--offset", String(options.offset));
399
- if (options?.limit !== undefined)
400
- args.push("--limit", String(options.limit));
401
- return runFiletoolsJson("read", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);
402
- }
403
- //# sourceMappingURL=filetools-shared.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"filetools-shared.js","sourceRoot":"","sources":["../../../src/core/tools/filetools-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared plumbing for the `DocRead` / `DocEdit` / `DocWrite` tools.\n *\n * All three shell out to the `filetools` binary (extract / reconstruct\n * subcommands, resolved/downloaded via {@link ensureTool}) to losslessly\n * project structured/binary documents (XML, drawio, OOXML, PDF) into editable,\n * id-addressed JSON and reconstruct them after id-based patches.\n *\n * Unlike webtools, the filetools CLI is file-oriented, not stdout-oriented:\n * `extract` writes the envelope JSON to `--out` and the sidecar id-map next to\n * it, emitting only a human status line on stderr. This module therefore:\n * - owns a per-process working directory where envelopes + sidecars live,\n * - runs extract/reconstruct and reads the resulting files back,\n * - keeps a small cache mapping a source file to its extracted envelope +\n * sidecar, so a DocRead can be followed by a DocEdit/DocWrite (the stateful\n * extract -> patch -> reconstruct flow), and\n * - exposes the locked JSON wire types mirroring the Rust `model.rs`/`patch.rs`.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { type Static, Type } from \"typebox\";\nimport { APP_NAME } from \"../../config.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { execCommand } from \"../exec.js\";\n\n/** Default timeout (seconds) for a single filetools invocation. */\nconst FILETOOLS_DEFAULT_TIMEOUT_SECS = 30;\n\n/**\n * Soft token ceiling for a single DocRead render. The filetools binary has no\n * pagination, so a dense file (e.g. a large spreadsheet) can project into a\n * huge id-addressed dump that floods the model context and burns tokens. We\n * cannot make the extract itself smaller without the binary's help, so DocRead\n * truncates the rendered view to roughly this budget and tells the model how to\n * narrow it (readonly projection, a smaller/targeted file, or direct edits).\n */\nexport const DOCREAD_MAX_RENDER_TOKENS = 10000;\n\n/** Rough token estimate (chars/4), matching the agent's compaction heuristic. */\nexport function estimateTextTokens(text: string): number {\n\treturn Math.ceil(text.length / 4);\n}\n\n/**\n * Truncate rendered envelope lines to roughly `maxTokens`, keeping whole lines.\n * Returns the kept text plus how many lines were dropped (0 when nothing was\n * truncated).\n */\nexport function truncateRenderToTokenBudget(\n\tlines: string[],\n\tmaxTokens: number = DOCREAD_MAX_RENDER_TOKENS,\n): { text: string; droppedLines: number } {\n\tconst full = lines.join(\"\\n\");\n\tif (estimateTextTokens(full) <= maxTokens) {\n\t\treturn { text: full, droppedLines: 0 };\n\t}\n\tconst budgetChars = maxTokens * 4;\n\tconst kept: string[] = [];\n\tlet used = 0;\n\tfor (const line of lines) {\n\t\tconst next = used + line.length + 1; // + newline\n\t\tif (next > budgetChars && kept.length > 0) break;\n\t\tkept.push(line);\n\t\tused = next;\n\t}\n\treturn { text: kept.join(\"\\n\"), droppedLines: lines.length - kept.length };\n}\n\n// ============================================================================\n// Wire types (locked against `filetools` model.rs / patch.rs)\n// ============================================================================\n\n/** How faithfully a handler can reconstruct a file after edits. */\ntype Fidelity = \"lossless\" | \"in_place_text\" | \"read_only\";\n\ninterface DocSource {\n\tpath: string;\n\t/** Logical format, e.g. \"xml\", \"drawio\". */\n\ttype: string;\n\t/** `sha256:<hex>` of the original bytes. */\n\thash: string;\n}\n\ninterface DocAttr {\n\tname: string;\n\tvalue: string;\n}\n\nexport interface DocNode {\n\tid: string;\n\ttag: string;\n\tattrs?: DocAttr[];\n\ttext?: string;\n\tchildren?: DocNode[];\n}\n\n/** The extract output handed to the model. Mirrors the Rust `Envelope`. */\nexport interface Envelope {\n\tversion: string;\n\tsource: DocSource;\n\tfidelity: Fidelity;\n\twritable: boolean;\n\tidmap_ref?: string;\n\tstructure: DocNode[];\n}\n\n/** A new element for an `add` op (text-only content, v1). */\ninterface NewElement {\n\ttag: string;\n\tattrs?: DocAttr[];\n\ttext?: string;\n}\n\n/**\n * One patch operation. RFC-6902 vocabulary, id-based pointers\n * (`/structure/<id>/text`, `/structure/<id>/attrs/<name>`), per the filetools\n * patch format.\n */\nexport type PatchOp =\n\t| { op: \"test\"; path: string; hash: string }\n\t| { op: \"replace\"; path: string; value: string }\n\t| { op: \"add\"; after?: string; before?: string; value: NewElement }\n\t| { op: \"remove\"; path: string };\n\nexport interface Patch {\n\tpatch: PatchOp[];\n}\n\n// ----------------------------------------------------------------------------\n// TypeBox schema for the model-facing patch input (shared by DocEdit/DocWrite)\n// ----------------------------------------------------------------------------\n\nconst attrSchema = Type.Object({\n\tname: Type.String(),\n\tvalue: Type.String(),\n});\n\nconst newElementSchema = Type.Object({\n\ttag: Type.String({ description: 'Element tag name, e.g. \"w:p\" or \"mxCell\".' }),\n\tattrs: Type.Optional(Type.Array(attrSchema, { description: \"Attributes in document order.\" })),\n\ttext: Type.Optional(Type.String({ description: \"Inline text content (text-only elements, v1).\" })),\n});\n\nconst patchOpSchema = Type.Union([\n\tType.Object(\n\t\t{\n\t\t\top: Type.Literal(\"test\"),\n\t\t\tpath: Type.String({ description: \"Pointer `/structure/<id>` (or /text, /attrs/<name>) to guard.\" }),\n\t\t\thash: Type.String({ description: \"Expected content hash of the target node.\" }),\n\t\t},\n\t\t{ description: \"Optimistic guard: assert the target node's content hash before mutating.\" },\n\t),\n\tType.Object(\n\t\t{\n\t\t\top: Type.Literal(\"replace\"),\n\t\t\tpath: Type.String({\n\t\t\t\tdescription: \"`/structure/<id>/text` for element text, or `/structure/<id>/attrs/<name>` for an attribute.\",\n\t\t\t}),\n\t\t\tvalue: Type.String({ description: \"New text or attribute value.\" }),\n\t\t},\n\t\t{ description: \"Replace an element's text or an attribute value.\" },\n\t),\n\tType.Object(\n\t\t{\n\t\t\top: Type.Literal(\"add\"),\n\t\t\tafter: Type.Optional(Type.String({ description: \"Anchor node id to insert AFTER.\" })),\n\t\t\tbefore: Type.Optional(Type.String({ description: \"Anchor node id to insert BEFORE.\" })),\n\t\t\tvalue: newElementSchema,\n\t\t},\n\t\t{ description: \"Insert a new element next to an anchor. Provide exactly one of `after`/`before`.\" },\n\t),\n\tType.Object(\n\t\t{\n\t\t\top: Type.Literal(\"remove\"),\n\t\t\tpath: Type.String({ description: \"Pointer `/structure/<id>` of the element to delete.\" }),\n\t\t},\n\t\t{ description: \"Delete an element and all its bytes.\" },\n\t),\n]);\n\n/**\n * The model-facing patch parameter: an array of id-based RFC-6902 ops, matching\n * the filetools patch wire format. Shared by DocEdit and DocWrite.\n */\nexport const patchOpsSchema = Type.Array(patchOpSchema, {\n\tdescription:\n\t\t\"Ordered id-based patch ops (test/replace/add/remove) targeting node ids from a prior DocRead. Applied atomically.\",\n});\n\nexport type PatchOpsInput = Static<typeof patchOpsSchema>;\n\n/** Wrap the model-facing ops array into the binary's `{ patch: [...] }` envelope. */\nexport function toPatch(ops: PatchOpsInput): Patch {\n\treturn { patch: ops as PatchOp[] };\n}\n\n/** Find a node by id anywhere in a (recursive) structure tree. */\nexport function findNodeById(nodes: DocNode[], id: string): DocNode | undefined {\n\tfor (const node of nodes) {\n\t\tif (node.id === id) return node;\n\t\tif (node.children) {\n\t\t\tconst hit = findNodeById(node.children, id);\n\t\t\tif (hit) return hit;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Extract the target node id from a patch op pointer. Returns undefined for ops\n * that reference a node by anchor (`add`) rather than a `/structure/<id>/...`\n * path. Pointer shapes: `/structure/<id>`, `/structure/<id>/text`,\n * `/structure/<id>/attrs/<name>`.\n */\nexport function patchOpNodeId(op: PatchOp): string | undefined {\n\tif (op.op === \"add\") return op.after ?? op.before;\n\tconst parts = op.path.split(\"/\");\n\t// [\"\", \"structure\", \"<id>\", ...]\n\treturn parts[1] === \"structure\" ? parts[2] : undefined;\n}\n\n/**\n * Validate that every node id referenced by `ops` still exists in `structure`.\n * Returns the ids that are missing (empty array means the patch is applicable to\n * this extract). Used to detect when a patch was authored against a stale\n * extract — e.g. after an external tool rewrote the document.\n */\nexport function findMissingPatchIds(ops: PatchOp[], structure: DocNode[]): string[] {\n\tconst missing: string[] = [];\n\tfor (const op of ops) {\n\t\tconst id = patchOpNodeId(op);\n\t\tif (id && !findNodeById(structure, id)) missing.push(id);\n\t}\n\treturn missing;\n}\n\n/**\n * Render id-addressed node lines (`#id <tag attrs> :: \"text\"`), the compact\n * view the model reads and patches against. Shared by DocRead's envelope render\n * and DocPeek's hydrated-block render so both speak the exact same dialect.\n */\nexport function renderDocNodeLines(nodes: DocNode[]): string[] {\n\tconst lines: string[] = [];\n\tconst walk = (ns: DocNode[], depth: number): void => {\n\t\tfor (const node of ns) {\n\t\t\tconst indent = \" \".repeat(depth);\n\t\t\tconst idPart = node.id ? `#${node.id} ` : \"\";\n\t\t\tconst attrs = node.attrs?.length ? ` ${node.attrs.map((a) => `${a.name}=\"${a.value}\"`).join(\" \")}` : \"\";\n\t\t\tconst text = node.text !== undefined ? ` :: ${JSON.stringify(node.text)}` : \"\";\n\t\t\tlines.push(`${indent}${idPart}<${node.tag}${attrs}>${text}`);\n\t\t\tif (node.children?.length) walk(node.children, depth + 1);\n\t\t}\n\t};\n\twalk(nodes, 0);\n\treturn lines;\n}\n\n// ============================================================================\n// Binary runner + working directory\n// ============================================================================\n\nconst BINARY_MISSING_MESSAGE =\n\t\"filetools binary unavailable and could not be downloaded — the document tools require the `filetools` CLI on PATH or a published release for this platform\";\n\n/** Lazily-created per-process working directory for envelopes + sidecars. */\nlet workDir: string | undefined;\nfunction getWorkDir(): string {\n\tif (workDir) return workDir;\n\tconst base = join(tmpdir(), `${APP_NAME}-filetools`);\n\tmkdirSync(base, { recursive: true });\n\tworkDir = mkdtempSync(join(base, \"doc-\"));\n\treturn workDir;\n}\n\n/** Short, filesystem-safe key for a source path (used to name its subdir). */\nfunction pathKey(absolutePath: string): string {\n\treturn createHash(\"sha256\").update(absolutePath).digest(\"hex\").slice(0, 16);\n}\n\nasync function resolveBinary(): Promise<string> {\n\tconst binaryPath = await ensureTool(\"filetools\", true);\n\tif (!binaryPath) throw new Error(BINARY_MISSING_MESSAGE);\n\treturn binaryPath;\n}\n\nasync function runFiletools(\n\tbinaryPath: string,\n\tsubcommand: \"extract\" | \"reconstruct\",\n\targs: string[],\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\ttimeoutSecs: number,\n): Promise<string> {\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\tconst spawnTimeoutMs = (timeoutSecs + 5) * 1000;\n\tconst result = await execCommand(binaryPath, [subcommand, ...args], cwd, { signal, timeout: spawnTimeoutMs });\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\tif (result.killed) throw new Error(`filetools ${subcommand} timed out after ${timeoutSecs}s`);\n\tif (result.code !== 0) {\n\t\tconst stderr = result.stderr.trim();\n\t\tthrow new Error(stderr || `filetools ${subcommand} exited with code ${result.code}`);\n\t}\n\t// Status goes to stderr; callers read the produced files, not stdout.\n\treturn result.stderr.trim();\n}\n\n// ============================================================================\n// Extraction cache (source file -> extracted envelope + sidecar)\n// ============================================================================\n\nexport interface ExtractRecord {\n\t/** Absolute path of the source document. */\n\tsource: string;\n\t/** Path to the envelope JSON in the working directory. */\n\tenvelopePath: string;\n\t/** Parsed envelope (also returned to the model on DocRead). */\n\tenvelope: Envelope;\n\t/** The source's stat signature at extract time, to detect drift cheaply. */\n\tsignature: string;\n}\n\nconst records = new Map<string, ExtractRecord>();\n\nfunction statSignature(absolutePath: string): string {\n\ttry {\n\t\tconst st = statSync(absolutePath);\n\t\treturn `${st.mtimeMs}:${st.size}`;\n\t} catch {\n\t\treturn \"absent\";\n\t}\n}\n\n/**\n * Extract `absolutePath` to an envelope (+ sidecar) in the working directory,\n * cache the result keyed by the source path, and return the parsed envelope.\n *\n * `readonly` strips ids for a smaller, analysis-only projection that cannot be\n * reconstructed (DocRead's default-off mode).\n */\nexport async function extractDocument(\n\tabsolutePath: string,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\toptions?: { readonly?: boolean; timeoutSecs?: number },\n): Promise<Envelope> {\n\tconst binaryPath = await resolveBinary();\n\tconst dir = join(getWorkDir(), pathKey(absolutePath));\n\tmkdirSync(dir, { recursive: true });\n\tconst envelopePath = join(dir, \"envelope.json\");\n\n\tconst args = [\"--input\", absolutePath, \"--out\", envelopePath];\n\tif (options?.readonly) args.push(\"--readonly\");\n\tawait runFiletools(binaryPath, \"extract\", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);\n\n\tconst envelope = readEnvelope(envelopePath);\n\tif (!options?.readonly) {\n\t\trecords.set(absolutePath, {\n\t\t\tsource: absolutePath,\n\t\t\tenvelopePath,\n\t\t\tenvelope,\n\t\t\tsignature: statSignature(absolutePath),\n\t\t});\n\t}\n\treturn envelope;\n}\n\nfunction readEnvelope(envelopePath: string): Envelope {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(envelopePath, \"utf8\");\n\t} catch {\n\t\tthrow new Error(\"filetools extract produced no envelope\");\n\t}\n\ttry {\n\t\treturn JSON.parse(raw) as Envelope;\n\t} catch {\n\t\tthrow new Error(\"filetools extract produced a malformed envelope\");\n\t}\n}\n\n/** Look up a cached extraction for `absolutePath`, if one is still valid. */\nfunction getExtractRecord(absolutePath: string): ExtractRecord | undefined {\n\tconst record = records.get(absolutePath);\n\tif (!record) return undefined;\n\t// Drop a stale record if the source changed since extract; reconstruct would\n\t// fail the binary's hash-drift guard anyway, but a clearer error is better.\n\tif (record.signature !== statSignature(absolutePath)) {\n\t\trecords.delete(absolutePath);\n\t\treturn undefined;\n\t}\n\treturn record;\n}\n\n/** Drop any cached extraction for `absolutePath`. */\nexport function invalidateExtractRecord(absolutePath: string): void {\n\trecords.delete(absolutePath);\n}\n\n/**\n * Thrown when a patch references node ids that are absent from the current\n * extract — typically because the document was rewritten out-of-band (e.g. by a\n * script) after the ids were read, or the patch was authored against an older\n * extract. Carries the freshly re-extracted envelope so the caller can surface\n * current ids to the agent without forcing a separate DocRead.\n */\nexport class StalePatchError extends Error {\n\treadonly envelope: Envelope;\n\treadonly missingIds: string[];\n\tconstructor(envelope: Envelope, missingIds: string[]) {\n\t\tsuper(\n\t\t\t`patch references ${missingIds.length} node id${missingIds.length === 1 ? \"\" : \"s\"} that no longer exist ` +\n\t\t\t\t`in ${basename(envelope.source.path)} (${missingIds.slice(0, 5).join(\", \")}` +\n\t\t\t\t`${missingIds.length > 5 ? \", …\" : \"\"}). The document was re-extracted; re-issue the patch against the ids below.`,\n\t\t);\n\t\tthis.name = \"StalePatchError\";\n\t\tthis.envelope = envelope;\n\t\tthis.missingIds = missingIds;\n\t}\n}\n\n/**\n * Return a valid cached extract for `absolutePath`, re-extracting automatically\n * when the cache is missing or stale (e.g. the source changed on disk since the\n * last extract). This keeps DocEdit/DocWrite usable after an out-of-band write\n * without forcing the agent to call DocRead again.\n */\nexport async function ensureExtractRecord(\n\tabsolutePath: string,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\toptions?: { timeoutSecs?: number },\n): Promise<ExtractRecord> {\n\tconst existing = getExtractRecord(absolutePath);\n\tif (existing) return existing;\n\tinvalidateExtractRecord(absolutePath);\n\tawait extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });\n\tconst record = getExtractRecord(absolutePath);\n\tif (!record) {\n\t\tthrow new Error(`failed to extract ${basename(absolutePath)} — the document tools could not read it`);\n\t}\n\treturn record;\n}\n\n/**\n * Apply `patch` to a document, writing the reconstructed bytes to `outPath`.\n * Auto-extracts when the cache is missing or stale (so an out-of-band rewrite no\n * longer forces a manual DocRead), then validates that the patch's node ids\n * still exist in the current extract. A mismatch throws {@link StalePatchError}\n * carrying the fresh envelope so the caller can show current ids.\n */\nexport async function reconstructDocument(\n\tabsolutePath: string,\n\tpatch: Patch,\n\toutPath: string,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\toptions?: { timeoutSecs?: number },\n): Promise<void> {\n\tconst record = await ensureExtractRecord(absolutePath, cwd, signal, options);\n\tif (!record.envelope.writable) {\n\t\tthrow new Error(\n\t\t\t`${basename(absolutePath)} is read-only (fidelity ${record.envelope.fidelity}); it cannot be edited`,\n\t\t);\n\t}\n\tconst missingIds = findMissingPatchIds(patch.patch, record.envelope.structure);\n\tif (missingIds.length > 0) {\n\t\tthrow new StalePatchError(record.envelope, missingIds);\n\t}\n\n\tconst binaryPath = await resolveBinary();\n\tconst patchPath = join(getWorkDir(), pathKey(absolutePath), \"patch.json\");\n\twriteFileSync(patchPath, JSON.stringify(patch), \"utf8\");\n\ttry {\n\t\tawait runFiletools(\n\t\t\tbinaryPath,\n\t\t\t\"reconstruct\",\n\t\t\t[\"--envelope\", record.envelopePath, \"--patch\", patchPath, \"--out\", outPath, \"--original\", absolutePath],\n\t\t\tcwd,\n\t\t\tsignal,\n\t\t\toptions?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS,\n\t\t);\n\t} finally {\n\t\trmSync(patchPath, { force: true });\n\t}\n}\n\n// ============================================================================\n// Discovery commands (scan / grep / read) — the token-sensitive loop\n// ============================================================================\n//\n// Unlike extract/reconstruct (which write files and the caller reads back),\n// scan/grep/read print a single pretty-JSON object to stdout and never touch\n// the extract cache: they are read-only projections used to navigate a document\n// cheaply before (optionally) editing it. The shapes below are locked against\n// the filetools `ScanView` / `GrepView` / `ReadView` serializers.\n\n/** One block in a `scan` manifest: structure + a short preview, no full content. */\ninterface BlockManifest {\n\tid: string;\n\tblock_type: string;\n\tpreview: string;\n\tcontent_hash: string;\n\tparent_id?: string | null;\n\ttoken_estimate: number;\n\tsection_name: string;\n\tsection_number: number;\n}\n\n/** `filetools scan` output: a paginated manifest of block previews. */\nexport interface ScanView {\n\tfile_type: string;\n\tblock_count: number;\n\ttotal_tokens: number;\n\toffset: number;\n\treturned: number;\n\ttotal: number;\n\tblocks: BlockManifest[];\n}\n\n/** One `grep` hit: the block id, the matching line number, and a snippet. */\ninterface GrepMatch {\n\tblock_id: string;\n\tline: number;\n\tsnippet: string;\n\twritable: boolean;\n}\n\n/** `filetools grep` output: literal-substring matches across blocks. */\nexport interface GrepView {\n\tpattern: string;\n\treturned: number;\n\tmatches: GrepMatch[];\n}\n\n/** `filetools read` output: hydrated nodes for the requested blocks. */\nexport interface ReadView {\n\toffset: number;\n\treturned: number;\n\ttotal: number;\n\tnodes: DocNode[];\n}\n\n/**\n * Run a stdout-oriented filetools subcommand (scan/grep/read) and parse its\n * single pretty-JSON object. These do not write files or populate the extract\n * cache, so they are safe to interleave with a pending DocEdit/DocWrite.\n */\nasync function runFiletoolsJson<T>(\n\tsubcommand: \"scan\" | \"grep\" | \"read\",\n\targs: string[],\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\ttimeoutSecs: number,\n): Promise<T> {\n\tconst binaryPath = await resolveBinary();\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\tconst spawnTimeoutMs = (timeoutSecs + 5) * 1000;\n\tconst result = await execCommand(binaryPath, [subcommand, ...args], cwd, { signal, timeout: spawnTimeoutMs });\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\tif (result.killed) throw new Error(`filetools ${subcommand} timed out after ${timeoutSecs}s`);\n\tif (result.code !== 0) {\n\t\tconst stderr = result.stderr.trim();\n\t\tthrow new Error(stderr || `filetools ${subcommand} exited with code ${result.code}`);\n\t}\n\tconst stdout = result.stdout.trim();\n\tif (!stdout) throw new Error(`filetools ${subcommand} produced no output`);\n\ttry {\n\t\treturn JSON.parse(stdout) as T;\n\t} catch {\n\t\tthrow new Error(`filetools ${subcommand} produced malformed JSON output`);\n\t}\n}\n\n/** Scan a document into a paginated manifest of block previews (no hydration). */\nexport async function scanDocument(\n\tabsolutePath: string,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\toptions?: { offset?: number; limit?: number; timeoutSecs?: number },\n): Promise<ScanView> {\n\tconst args = [\"--input\", absolutePath];\n\tif (options?.offset !== undefined) args.push(\"--offset\", String(options.offset));\n\tif (options?.limit !== undefined) args.push(\"--limit\", String(options.limit));\n\treturn runFiletoolsJson<ScanView>(\"scan\", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);\n}\n\n/** Locate blocks containing `pattern` (literal substring) without hydrating the doc. */\nexport async function grepDocument(\n\tabsolutePath: string,\n\tpattern: string,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\toptions?: { ignoreCase?: boolean; limit?: number; timeoutSecs?: number },\n): Promise<GrepView> {\n\tconst args = [\"--input\", absolutePath, \"--pattern\", pattern];\n\tif (options?.ignoreCase) args.push(\"--ignore-case\");\n\tif (options?.limit !== undefined) args.push(\"--limit\", String(options.limit));\n\treturn runFiletoolsJson<GrepView>(\"grep\", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);\n}\n\n/** Hydrate specific blocks by id (or a paginated slice when no ids are given). */\nexport async function readDocumentBlocks(\n\tabsolutePath: string,\n\tcwd: string,\n\tsignal: AbortSignal | undefined,\n\toptions?: { ids?: string[]; offset?: number; limit?: number; timeoutSecs?: number },\n): Promise<ReadView> {\n\tconst args = [\"--input\", absolutePath];\n\tfor (const id of options?.ids ?? []) args.push(\"--id\", id);\n\tif (options?.offset !== undefined) args.push(\"--offset\", String(options.offset));\n\tif (options?.limit !== undefined) args.push(\"--limit\", String(options.limit));\n\treturn runFiletoolsJson<ReadView>(\"read\", args, cwd, signal, options?.timeoutSecs ?? FILETOOLS_DEFAULT_TIMEOUT_SECS);\n}\n"]}