@kolisachint/hoocode-agent 0.4.91 → 0.4.93
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 +39 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts.map +1 -1
- package/dist/core/agent-frontmatter.js +3 -0
- package/dist/core/agent-frontmatter.js.map +1 -1
- package/dist/core/sdk.d.ts +6 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/docedit.d.ts +2 -0
- package/dist/core/tools/docedit.d.ts.map +1 -1
- package/dist/core/tools/docedit.js +35 -7
- package/dist/core/tools/docedit.js.map +1 -1
- package/dist/core/tools/docgrep.d.ts +28 -0
- package/dist/core/tools/docgrep.d.ts.map +1 -0
- package/dist/core/tools/docgrep.js +106 -0
- package/dist/core/tools/docgrep.js.map +1 -0
- package/dist/core/tools/docpeek.d.ts +25 -0
- package/dist/core/tools/docpeek.d.ts.map +1 -0
- package/dist/core/tools/docpeek.js +112 -0
- package/dist/core/tools/docpeek.js.map +1 -0
- package/dist/core/tools/docread.d.ts.map +1 -1
- package/dist/core/tools/docread.js +4 -15
- package/dist/core/tools/docread.js.map +1 -1
- package/dist/core/tools/docscan.d.ts +29 -0
- package/dist/core/tools/docscan.d.ts.map +1 -0
- package/dist/core/tools/docscan.js +110 -0
- package/dist/core/tools/docscan.js.map +1 -0
- package/dist/core/tools/docwrite.d.ts.map +1 -1
- package/dist/core/tools/docwrite.js +18 -8
- package/dist/core/tools/docwrite.js.map +1 -1
- package/dist/core/tools/filetools-shared.d.ts +108 -3
- package/dist/core/tools/filetools-shared.d.ts.map +1 -1
- package/dist/core/tools/filetools-shared.js +165 -5
- package/dist/core/tools/filetools-shared.js.map +1 -1
- package/dist/core/tools/index.d.ts +10 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +27 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -3
- package/dist/main.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Text } from "@kolisachint/hoocode-tui";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
4
|
+
import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
|
|
5
|
+
import { scanDocument, truncateRenderToTokenBudget } from "./filetools-shared.js";
|
|
6
|
+
import { resolveReadPath } from "./path-utils.js";
|
|
7
|
+
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
8
|
+
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
9
|
+
const docScanSchema = Type.Object({
|
|
10
|
+
path: Type.String({
|
|
11
|
+
description: "Path to the document to scan (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.",
|
|
12
|
+
}),
|
|
13
|
+
offset: Type.Optional(Type.Number({ description: "Skip the first N blocks (pagination). Default 0." })),
|
|
14
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of blocks to return. Default: all remaining." })),
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Render a scan manifest as compact, id-addressed preview lines. Each line
|
|
18
|
+
* carries the block id (feeds DocPeek/DocGrep) plus a section label, token
|
|
19
|
+
* estimate, and a short preview — structure without hydrating full content.
|
|
20
|
+
*/
|
|
21
|
+
export function renderScanView(view) {
|
|
22
|
+
const header = `document ${view.file_type} — ${view.returned}/${view.total} blocks ` +
|
|
23
|
+
`(offset ${view.offset}, ~${view.total_tokens} tok total)`;
|
|
24
|
+
const lines = [header, ""];
|
|
25
|
+
for (const block of view.blocks) {
|
|
26
|
+
const section = block.section_name ? ` ${block.section_name}#${block.section_number}` : "";
|
|
27
|
+
const preview = block.preview ? ` :: ${JSON.stringify(block.preview)}` : "";
|
|
28
|
+
lines.push(`#${block.id} [${block.block_type}]${section} ~${block.token_estimate}tok${preview}`);
|
|
29
|
+
}
|
|
30
|
+
const { text, droppedLines } = truncateRenderToTokenBudget(lines);
|
|
31
|
+
const seen = view.offset + view.returned;
|
|
32
|
+
const remaining = view.total - seen;
|
|
33
|
+
const parts = [text];
|
|
34
|
+
if (droppedLines > 0) {
|
|
35
|
+
parts.push(`\n[Truncated: ${droppedLines} more block line${droppedLines === 1 ? "" : "s"} omitted from this render. ` +
|
|
36
|
+
`Re-run with a tighter limit, or DocGrep/DocPeek to target.]`);
|
|
37
|
+
}
|
|
38
|
+
if (remaining > 0) {
|
|
39
|
+
parts.push(`\n[${remaining} more block${remaining === 1 ? "" : "s"} not shown — re-run with offset:${seen} to continue.]`);
|
|
40
|
+
}
|
|
41
|
+
return parts.join("\n");
|
|
42
|
+
}
|
|
43
|
+
function formatDocScanCall(args) {
|
|
44
|
+
const path = str(args?.path);
|
|
45
|
+
const pathDisplay = path === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg("toolOutput", "...");
|
|
46
|
+
let text = appTheme.fg("toolTitle", appTheme.bold("DocScan ")) + appTheme.fg("accent", pathDisplay);
|
|
47
|
+
const range = [];
|
|
48
|
+
if (args?.offset !== undefined)
|
|
49
|
+
range.push(`offset ${args.offset}`);
|
|
50
|
+
if (args?.limit !== undefined)
|
|
51
|
+
range.push(`limit ${args.limit}`);
|
|
52
|
+
if (range.length > 0)
|
|
53
|
+
text += appTheme.fg("muted", ` (${range.join(", ")})`);
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
function formatDocScanResult(result, options, showImages) {
|
|
57
|
+
const output = getTextOutput(result, showImages).trim();
|
|
58
|
+
if (!output)
|
|
59
|
+
return "";
|
|
60
|
+
const lines = output.split("\n");
|
|
61
|
+
const maxLines = options.expanded ? lines.length : 15;
|
|
62
|
+
const displayLines = lines.slice(0, maxLines);
|
|
63
|
+
const remaining = lines.length - maxLines;
|
|
64
|
+
let text = `\n${displayLines.map((line) => appTheme.fg("toolOutput", line)).join("\n")}`;
|
|
65
|
+
if (remaining > 0) {
|
|
66
|
+
text += `${appTheme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
67
|
+
}
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
export function createDocScanToolDefinition(cwd, options) {
|
|
71
|
+
return {
|
|
72
|
+
name: "DocScan",
|
|
73
|
+
label: "DocScan",
|
|
74
|
+
description: "Cheaply outline a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) without hydrating it: returns a paginated manifest of blocks, each with a structural-path id, type, section label, token estimate, and a short preview. This is the cheap first step of the token-sensitive loop — far smaller than a full DocRead. Pass the path ids it returns to DocPeek to hydrate just those blocks (the hydrated nodes carry the editable el_ #ids for DocEdit). Off by default; enabled with --enable-filetools.",
|
|
75
|
+
promptSnippet: "Outline a structured/binary document into a cheap, paginated block manifest",
|
|
76
|
+
promptGuidelines: [
|
|
77
|
+
"Start here for large structured/binary documents instead of a full DocRead: DocScan returns a paginated outline (structural-path block ids + previews) that is much cheaper in tokens. Page through it with offset/limit.",
|
|
78
|
+
"Flow: DocScan to see structure → DocPeek the path ids you want (hydrates them and reveals their editable el_ #ids) → DocEdit. Or jump straight in with DocGrep, which finds blocks by text and returns editable el_ #ids directly.",
|
|
79
|
+
],
|
|
80
|
+
parameters: docScanSchema,
|
|
81
|
+
async execute(_toolCallId, { path, offset, limit }, signal) {
|
|
82
|
+
if (signal?.aborted)
|
|
83
|
+
throw new Error("Operation aborted");
|
|
84
|
+
const absolutePath = resolveReadPath(path, cwd);
|
|
85
|
+
const view = await scanDocument(absolutePath, cwd, signal, {
|
|
86
|
+
offset,
|
|
87
|
+
limit,
|
|
88
|
+
timeoutSecs: options?.timeoutSecs,
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: renderScanView(view) }],
|
|
92
|
+
details: { fileType: view.file_type, returned: view.returned, total: view.total },
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
renderCall(args, _theme, context) {
|
|
96
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
97
|
+
text.setText(formatDocScanCall(args));
|
|
98
|
+
return text;
|
|
99
|
+
},
|
|
100
|
+
renderResult(result, options, _theme, context) {
|
|
101
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
102
|
+
text.setText(formatDocScanResult(result, options, context.showImages));
|
|
103
|
+
return text;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function createDocScanTool(cwd, options) {
|
|
108
|
+
return wrapToolDefinition(createDocScanToolDefinition(cwd, options));
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=docscan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docscan.js","sourceRoot":"","sources":["../../../src/core/tools/docscan.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { type ScanView, scanDocument, truncateRenderToTokenBudget } from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docScanSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to scan (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.\",\n\t}),\n\toffset: Type.Optional(Type.Number({ description: \"Skip the first N blocks (pagination). Default 0.\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of blocks to return. Default: all remaining.\" })),\n});\n\nexport type DocScanToolInput = Static<typeof docScanSchema>;\n\nexport interface DocScanToolDetails {\n\tfileType?: string;\n\treturned?: number;\n\ttotal?: number;\n}\n\nexport interface DocScanToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\n/**\n * Render a scan manifest as compact, id-addressed preview lines. Each line\n * carries the block id (feeds DocPeek/DocGrep) plus a section label, token\n * estimate, and a short preview — structure without hydrating full content.\n */\nexport function renderScanView(view: ScanView): string {\n\tconst header =\n\t\t`document ${view.file_type} — ${view.returned}/${view.total} blocks ` +\n\t\t`(offset ${view.offset}, ~${view.total_tokens} tok total)`;\n\tconst lines: string[] = [header, \"\"];\n\tfor (const block of view.blocks) {\n\t\tconst section = block.section_name ? ` ${block.section_name}#${block.section_number}` : \"\";\n\t\tconst preview = block.preview ? ` :: ${JSON.stringify(block.preview)}` : \"\";\n\t\tlines.push(`#${block.id} [${block.block_type}]${section} ~${block.token_estimate}tok${preview}`);\n\t}\n\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tconst seen = view.offset + view.returned;\n\tconst remaining = view.total - seen;\n\tconst parts = [text];\n\tif (droppedLines > 0) {\n\t\tparts.push(\n\t\t\t`\\n[Truncated: ${droppedLines} more block line${droppedLines === 1 ? \"\" : \"s\"} omitted from this render. ` +\n\t\t\t\t`Re-run with a tighter limit, or DocGrep/DocPeek to target.]`,\n\t\t);\n\t}\n\tif (remaining > 0) {\n\t\tparts.push(\n\t\t\t`\\n[${remaining} more block${remaining === 1 ? \"\" : \"s\"} not shown — re-run with offset:${seen} to continue.]`,\n\t\t);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction formatDocScanCall(args: { path?: string; offset?: number; limit?: number } | undefined): string {\n\tconst path = str(args?.path);\n\tconst pathDisplay =\n\t\tpath === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg(\"toolOutput\", \"...\");\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"DocScan \")) + appTheme.fg(\"accent\", pathDisplay);\n\tconst range: string[] = [];\n\tif (args?.offset !== undefined) range.push(`offset ${args.offset}`);\n\tif (args?.limit !== undefined) range.push(`limit ${args.limit}`);\n\tif (range.length > 0) text += appTheme.fg(\"muted\", ` (${range.join(\", \")})`);\n\treturn text;\n}\n\nfunction formatDocScanResult(\n\tresult: { content: Array<{ type: string; text?: string }> },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, showImages).trim();\n\tif (!output) return \"\";\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 15;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\treturn text;\n}\n\nexport function createDocScanToolDefinition(\n\tcwd: string,\n\toptions?: DocScanToolOptions,\n): ToolDefinition<typeof docScanSchema, DocScanToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocScan\",\n\t\tlabel: \"DocScan\",\n\t\tdescription:\n\t\t\t\"Cheaply outline a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) without hydrating it: returns a paginated manifest of blocks, each with a structural-path id, type, section label, token estimate, and a short preview. This is the cheap first step of the token-sensitive loop — far smaller than a full DocRead. Pass the path ids it returns to DocPeek to hydrate just those blocks (the hydrated nodes carry the editable el_ #ids for DocEdit). Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Outline a structured/binary document into a cheap, paginated block manifest\",\n\t\tpromptGuidelines: [\n\t\t\t\"Start here for large structured/binary documents instead of a full DocRead: DocScan returns a paginated outline (structural-path block ids + previews) that is much cheaper in tokens. Page through it with offset/limit.\",\n\t\t\t\"Flow: DocScan to see structure → DocPeek the path ids you want (hydrates them and reveals their editable el_ #ids) → DocEdit. Or jump straight in with DocGrep, which finds blocks by text and returns editable el_ #ids directly.\",\n\t\t],\n\t\tparameters: docScanSchema,\n\t\tasync execute(_toolCallId, { path, offset, limit }: DocScanToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst view = await scanDocument(absolutePath, cwd, signal, {\n\t\t\t\toffset,\n\t\t\t\tlimit,\n\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderScanView(view) }],\n\t\t\t\tdetails: { fileType: view.file_type, returned: view.returned, total: view.total },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocScanCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocScanResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocScanTool(cwd: string, options?: DocScanToolOptions): AgentTool<typeof docScanSchema> {\n\treturn wrapToolDefinition(createDocScanToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docwrite.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docwrite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"docwrite.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docwrite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAQ7D,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQlB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IACnC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAmBD,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,mBAAmB,GAC3B,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CA+DxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAE/G","sourcesContent":["import { mkdir as fsMkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Container, Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { renderEnvelopeText } from \"./docread.js\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.js\";\nimport { patchOpsSchema, reconstructDocument, StalePatchError, toPatch } from \"./filetools-shared.js\";\nimport { resolveReadPath, resolveToCwd } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docWriteSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the SOURCE document. Must have been opened with DocRead first. Left untouched.\",\n\t}),\n\tout: Type.String({\n\t\tdescription: \"Output path for the reconstructed document (relative or absolute). Created/overwritten.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocWriteToolInput = Static<typeof docWriteSchema>;\n\nexport interface DocWriteToolDetails {\n\tops?: number;\n\tout?: string;\n}\n\nexport interface DocWriteToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocWriteCall(args: { path?: string; out?: string; patch?: unknown[] } | undefined): string {\n\tconst path = str(args?.path);\n\tconst out = str(args?.out);\n\tconst pathDisplay =\n\t\tpath === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg(\"toolOutput\", \"...\");\n\tconst outDisplay =\n\t\tout === null ? invalidArgText(appTheme) : out ? shortenPath(out) : appTheme.fg(\"toolOutput\", \"...\");\n\tlet text =\n\t\tappTheme.fg(\"toolTitle\", appTheme.bold(\"DocWrite \")) +\n\t\tappTheme.fg(\"accent\", pathDisplay) +\n\t\tappTheme.fg(\"muted\", \" -> \") +\n\t\tappTheme.fg(\"accent\", outDisplay);\n\tconst ops = Array.isArray(args?.patch) ? args.patch.length : undefined;\n\tif (ops !== undefined) text += appTheme.fg(\"muted\", ` (${ops} op${ops === 1 ? \"\" : \"s\"})`);\n\treturn text;\n}\n\nexport function createDocWriteToolDefinition(\n\tcwd: string,\n\toptions?: DocWriteToolOptions,\n): ToolDefinition<typeof docWriteSchema, DocWriteToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocWrite\",\n\t\tlabel: \"DocWrite\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document and write the result to a NEW path, leaving the source untouched (save-as). Targets node ids from a DocRead extract of the source; if the cache is missing or the source changed on disk (e.g. an external script rewrote it) it is re-extracted automatically, so a separate DocRead is not required. If the patch references ids that no longer exist, the call fails and returns the current structure with fresh ids so you can re-issue. This is the canonical way to rewrite these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to produce the output — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Reconstruct a patched structured/binary document to a new path\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocWrite to save an edited document to a different file: pass the source path (opened with DocRead), an `out` path, and an id-based patch. The source is left unchanged.\",\n\t\t\t\"Keep the patch minimal and scan before you write: prefer a DocRead readonly:true glimpse to plan the change, and avoid re-running a full writable DocRead between writes — it auto-extracts the source and is token-heavy.\",\n\t\t],\n\t\tparameters: docWriteSchema,\n\t\tasync execute(_toolCallId, { path, out, patch }: DocWriteToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst outPath = resolveToCwd(out, cwd);\n\n\t\t\treturn withFileMutationQueue(outPath, async () => {\n\t\t\t\tawait fsMkdir(dirname(outPath), { recursive: true });\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), outPath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof StalePatchError) {\n\t\t\t\t\t\t// The source was rewritten out-of-band: the cache auto-refreshed\n\t\t\t\t\t\t// but the patch targets ids that no longer exist. Surface the\n\t\t\t\t\t\t// current structure so the agent can re-issue without a DocRead.\n\t\t\t\t\t\tthrow new Error(`${err.message}\\n\\n${renderEnvelopeText(err.envelope, false)}`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Wrote ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} from ${path} to ${out}.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length, out },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocWriteCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options, _theme, context) {\n\t\t\tif (!context.isError) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst output = (result.content as Array<{ type: string; text?: string }>)\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text || \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output ? `\\n${appTheme.fg(\"error\", output)}` : \"\");\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocWriteTool(cwd: string, options?: DocWriteToolOptions): AgentTool<typeof docWriteSchema> {\n\treturn wrapToolDefinition(createDocWriteToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -3,8 +3,9 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { Container, Text } from "@kolisachint/hoocode-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
|
|
6
|
+
import { renderEnvelopeText } from "./docread.js";
|
|
6
7
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
7
|
-
import {
|
|
8
|
+
import { patchOpsSchema, reconstructDocument, StalePatchError, toPatch } from "./filetools-shared.js";
|
|
8
9
|
import { resolveReadPath, resolveToCwd } from "./path-utils.js";
|
|
9
10
|
import { invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
10
11
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
@@ -35,25 +36,34 @@ export function createDocWriteToolDefinition(cwd, options) {
|
|
|
35
36
|
return {
|
|
36
37
|
name: "DocWrite",
|
|
37
38
|
label: "DocWrite",
|
|
38
|
-
description: "Apply an id-based patch to a structured/binary document and write the result to a NEW path, leaving the source untouched (save-as).
|
|
39
|
+
description: "Apply an id-based patch to a structured/binary document and write the result to a NEW path, leaving the source untouched (save-as). Targets node ids from a DocRead extract of the source; if the cache is missing or the source changed on disk (e.g. an external script rewrote it) it is re-extracted automatically, so a separate DocRead is not required. If the patch references ids that no longer exist, the call fails and returns the current structure with fresh ids so you can re-issue. This is the canonical way to rewrite these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to produce the output — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.",
|
|
39
40
|
promptSnippet: "Reconstruct a patched structured/binary document to a new path",
|
|
40
41
|
promptGuidelines: [
|
|
41
42
|
"Use DocWrite to save an edited document to a different file: pass the source path (opened with DocRead), an `out` path, and an id-based patch. The source is left unchanged.",
|
|
43
|
+
"Keep the patch minimal and scan before you write: prefer a DocRead readonly:true glimpse to plan the change, and avoid re-running a full writable DocRead between writes — it auto-extracts the source and is token-heavy.",
|
|
42
44
|
],
|
|
43
45
|
parameters: docWriteSchema,
|
|
44
46
|
async execute(_toolCallId, { path, out, patch }, signal) {
|
|
45
47
|
if (signal?.aborted)
|
|
46
48
|
throw new Error("Operation aborted");
|
|
47
49
|
const absolutePath = resolveReadPath(path, cwd);
|
|
48
|
-
if (!getExtractRecord(absolutePath)) {
|
|
49
|
-
throw new Error(`no extracted envelope for the source — run DocRead on ${path} first, then DocWrite`);
|
|
50
|
-
}
|
|
51
50
|
const outPath = resolveToCwd(out, cwd);
|
|
52
51
|
return withFileMutationQueue(outPath, async () => {
|
|
53
52
|
await fsMkdir(dirname(outPath), { recursive: true });
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
try {
|
|
54
|
+
await reconstructDocument(absolutePath, toPatch(patch), outPath, cwd, signal, {
|
|
55
|
+
timeoutSecs: options?.timeoutSecs,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof StalePatchError) {
|
|
60
|
+
// The source was rewritten out-of-band: the cache auto-refreshed
|
|
61
|
+
// but the patch targets ids that no longer exist. Surface the
|
|
62
|
+
// current structure so the agent can re-issue without a DocRead.
|
|
63
|
+
throw new Error(`${err.message}\n\n${renderEnvelopeText(err.envelope, false)}`);
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
57
67
|
return {
|
|
58
68
|
content: [
|
|
59
69
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docwrite.js","sourceRoot":"","sources":["../../../src/core/tools/docwrite.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir as fsMkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Container, Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.js\";\nimport { getExtractRecord, patchOpsSchema, reconstructDocument, toPatch } from \"./filetools-shared.js\";\nimport { resolveReadPath, resolveToCwd } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docWriteSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the SOURCE document. Must have been opened with DocRead first. Left untouched.\",\n\t}),\n\tout: Type.String({\n\t\tdescription: \"Output path for the reconstructed document (relative or absolute). Created/overwritten.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocWriteToolInput = Static<typeof docWriteSchema>;\n\nexport interface DocWriteToolDetails {\n\tops?: number;\n\tout?: string;\n}\n\nexport interface DocWriteToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocWriteCall(args: { path?: string; out?: string; patch?: unknown[] } | undefined): string {\n\tconst path = str(args?.path);\n\tconst out = str(args?.out);\n\tconst pathDisplay =\n\t\tpath === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg(\"toolOutput\", \"...\");\n\tconst outDisplay =\n\t\tout === null ? invalidArgText(appTheme) : out ? shortenPath(out) : appTheme.fg(\"toolOutput\", \"...\");\n\tlet text =\n\t\tappTheme.fg(\"toolTitle\", appTheme.bold(\"DocWrite \")) +\n\t\tappTheme.fg(\"accent\", pathDisplay) +\n\t\tappTheme.fg(\"muted\", \" -> \") +\n\t\tappTheme.fg(\"accent\", outDisplay);\n\tconst ops = Array.isArray(args?.patch) ? args.patch.length : undefined;\n\tif (ops !== undefined) text += appTheme.fg(\"muted\", ` (${ops} op${ops === 1 ? \"\" : \"s\"})`);\n\treturn text;\n}\n\nexport function createDocWriteToolDefinition(\n\tcwd: string,\n\toptions?: DocWriteToolOptions,\n): ToolDefinition<typeof docWriteSchema, DocWriteToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocWrite\",\n\t\tlabel: \"DocWrite\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document and write the result to a NEW path, leaving the source untouched (save-as). Requires a prior DocRead of the source (the patch targets node ids from that extract). This is the canonical way to rewrite these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to produce the output — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Reconstruct a patched structured/binary document to a new path\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocWrite to save an edited document to a different file: pass the source path (opened with DocRead), an `out` path, and an id-based patch. The source is left unchanged.\",\n\t\t],\n\t\tparameters: docWriteSchema,\n\t\tasync execute(_toolCallId, { path, out, patch }: DocWriteToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tif (!getExtractRecord(absolutePath)) {\n\t\t\t\tthrow new Error(`no extracted envelope for the source — run DocRead on ${path} first, then DocWrite`);\n\t\t\t}\n\t\t\tconst outPath = resolveToCwd(out, cwd);\n\n\t\t\treturn withFileMutationQueue(outPath, async () => {\n\t\t\t\tawait fsMkdir(dirname(outPath), { recursive: true });\n\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), outPath, cwd, signal, {\n\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Wrote ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} from ${path} to ${out}.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length, out },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocWriteCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options, _theme, context) {\n\t\t\tif (!context.isError) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst output = (result.content as Array<{ type: string; text?: string }>)\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text || \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output ? `\\n${appTheme.fg(\"error\", output)}` : \"\");\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocWriteTool(cwd: string, options?: DocWriteToolOptions): AgentTool<typeof docWriteSchema> {\n\treturn wrapToolDefinition(createDocWriteToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"docwrite.js","sourceRoot":"","sources":["../../../src/core/tools/docwrite.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir as fsMkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Container, Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { renderEnvelopeText } from \"./docread.js\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.js\";\nimport { patchOpsSchema, reconstructDocument, StalePatchError, toPatch } from \"./filetools-shared.js\";\nimport { resolveReadPath, resolveToCwd } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docWriteSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the SOURCE document. Must have been opened with DocRead first. Left untouched.\",\n\t}),\n\tout: Type.String({\n\t\tdescription: \"Output path for the reconstructed document (relative or absolute). Created/overwritten.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocWriteToolInput = Static<typeof docWriteSchema>;\n\nexport interface DocWriteToolDetails {\n\tops?: number;\n\tout?: string;\n}\n\nexport interface DocWriteToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocWriteCall(args: { path?: string; out?: string; patch?: unknown[] } | undefined): string {\n\tconst path = str(args?.path);\n\tconst out = str(args?.out);\n\tconst pathDisplay =\n\t\tpath === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg(\"toolOutput\", \"...\");\n\tconst outDisplay =\n\t\tout === null ? invalidArgText(appTheme) : out ? shortenPath(out) : appTheme.fg(\"toolOutput\", \"...\");\n\tlet text =\n\t\tappTheme.fg(\"toolTitle\", appTheme.bold(\"DocWrite \")) +\n\t\tappTheme.fg(\"accent\", pathDisplay) +\n\t\tappTheme.fg(\"muted\", \" -> \") +\n\t\tappTheme.fg(\"accent\", outDisplay);\n\tconst ops = Array.isArray(args?.patch) ? args.patch.length : undefined;\n\tif (ops !== undefined) text += appTheme.fg(\"muted\", ` (${ops} op${ops === 1 ? \"\" : \"s\"})`);\n\treturn text;\n}\n\nexport function createDocWriteToolDefinition(\n\tcwd: string,\n\toptions?: DocWriteToolOptions,\n): ToolDefinition<typeof docWriteSchema, DocWriteToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocWrite\",\n\t\tlabel: \"DocWrite\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document and write the result to a NEW path, leaving the source untouched (save-as). Targets node ids from a DocRead extract of the source; if the cache is missing or the source changed on disk (e.g. an external script rewrote it) it is re-extracted automatically, so a separate DocRead is not required. If the patch references ids that no longer exist, the call fails and returns the current structure with fresh ids so you can re-issue. This is the canonical way to rewrite these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to produce the output — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Reconstruct a patched structured/binary document to a new path\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocWrite to save an edited document to a different file: pass the source path (opened with DocRead), an `out` path, and an id-based patch. The source is left unchanged.\",\n\t\t\t\"Keep the patch minimal and scan before you write: prefer a DocRead readonly:true glimpse to plan the change, and avoid re-running a full writable DocRead between writes — it auto-extracts the source and is token-heavy.\",\n\t\t],\n\t\tparameters: docWriteSchema,\n\t\tasync execute(_toolCallId, { path, out, patch }: DocWriteToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst outPath = resolveToCwd(out, cwd);\n\n\t\t\treturn withFileMutationQueue(outPath, async () => {\n\t\t\t\tawait fsMkdir(dirname(outPath), { recursive: true });\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), outPath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof StalePatchError) {\n\t\t\t\t\t\t// The source was rewritten out-of-band: the cache auto-refreshed\n\t\t\t\t\t\t// but the patch targets ids that no longer exist. Surface the\n\t\t\t\t\t\t// current structure so the agent can re-issue without a DocRead.\n\t\t\t\t\t\tthrow new Error(`${err.message}\\n\\n${renderEnvelopeText(err.envelope, false)}`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Wrote ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} from ${path} to ${out}.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length, out },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocWriteCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options, _theme, context) {\n\t\t\tif (!context.isError) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst output = (result.content as Array<{ type: string; text?: string }>)\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text || \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output ? `\\n${appTheme.fg(\"error\", output)}` : \"\");\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocWriteTool(cwd: string, options?: DocWriteToolOptions): AgentTool<typeof docWriteSchema> {\n\treturn wrapToolDefinition(createDocWriteToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -130,6 +130,28 @@ export declare const patchOpsSchema: Type.TArray<Type.TUnion<[Type.TObject<{
|
|
|
130
130
|
export type PatchOpsInput = Static<typeof patchOpsSchema>;
|
|
131
131
|
/** Wrap the model-facing ops array into the binary's `{ patch: [...] }` envelope. */
|
|
132
132
|
export declare function toPatch(ops: PatchOpsInput): Patch;
|
|
133
|
+
/** Find a node by id anywhere in a (recursive) structure tree. */
|
|
134
|
+
export declare function findNodeById(nodes: DocNode[], id: string): DocNode | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Extract the target node id from a patch op pointer. Returns undefined for ops
|
|
137
|
+
* that reference a node by anchor (`add`) rather than a `/structure/<id>/...`
|
|
138
|
+
* path. Pointer shapes: `/structure/<id>`, `/structure/<id>/text`,
|
|
139
|
+
* `/structure/<id>/attrs/<name>`.
|
|
140
|
+
*/
|
|
141
|
+
export declare function patchOpNodeId(op: PatchOp): string | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Validate that every node id referenced by `ops` still exists in `structure`.
|
|
144
|
+
* Returns the ids that are missing (empty array means the patch is applicable to
|
|
145
|
+
* this extract). Used to detect when a patch was authored against a stale
|
|
146
|
+
* extract — e.g. after an external tool rewrote the document.
|
|
147
|
+
*/
|
|
148
|
+
export declare function findMissingPatchIds(ops: PatchOp[], structure: DocNode[]): string[];
|
|
149
|
+
/**
|
|
150
|
+
* Render id-addressed node lines (`#id <tag attrs> :: "text"`), the compact
|
|
151
|
+
* view the model reads and patches against. Shared by DocRead's envelope render
|
|
152
|
+
* and DocPeek's hydrated-block render so both speak the exact same dialect.
|
|
153
|
+
*/
|
|
154
|
+
export declare function renderDocNodeLines(nodes: DocNode[]): string[];
|
|
133
155
|
export interface ExtractRecord {
|
|
134
156
|
/** Absolute path of the source document. */
|
|
135
157
|
source: string;
|
|
@@ -156,11 +178,94 @@ export declare function getExtractRecord(absolutePath: string): ExtractRecord |
|
|
|
156
178
|
/** Drop any cached extraction for `absolutePath`. */
|
|
157
179
|
export declare function invalidateExtractRecord(absolutePath: string): void;
|
|
158
180
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
181
|
+
* Thrown when a patch references node ids that are absent from the current
|
|
182
|
+
* extract — typically because the document was rewritten out-of-band (e.g. by a
|
|
183
|
+
* script) after the ids were read, or the patch was authored against an older
|
|
184
|
+
* extract. Carries the freshly re-extracted envelope so the caller can surface
|
|
185
|
+
* current ids to the agent without forcing a separate DocRead.
|
|
186
|
+
*/
|
|
187
|
+
export declare class StalePatchError extends Error {
|
|
188
|
+
readonly envelope: Envelope;
|
|
189
|
+
readonly missingIds: string[];
|
|
190
|
+
constructor(envelope: Envelope, missingIds: string[]);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Return a valid cached extract for `absolutePath`, re-extracting automatically
|
|
194
|
+
* when the cache is missing or stale (e.g. the source changed on disk since the
|
|
195
|
+
* last extract). This keeps DocEdit/DocWrite usable after an out-of-band write
|
|
196
|
+
* without forcing the agent to call DocRead again.
|
|
197
|
+
*/
|
|
198
|
+
export declare function ensureExtractRecord(absolutePath: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
199
|
+
timeoutSecs?: number;
|
|
200
|
+
}): Promise<ExtractRecord>;
|
|
201
|
+
/**
|
|
202
|
+
* Apply `patch` to a document, writing the reconstructed bytes to `outPath`.
|
|
203
|
+
* Auto-extracts when the cache is missing or stale (so an out-of-band rewrite no
|
|
204
|
+
* longer forces a manual DocRead), then validates that the patch's node ids
|
|
205
|
+
* still exist in the current extract. A mismatch throws {@link StalePatchError}
|
|
206
|
+
* carrying the fresh envelope so the caller can show current ids.
|
|
162
207
|
*/
|
|
163
208
|
export declare function reconstructDocument(absolutePath: string, patch: Patch, outPath: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
164
209
|
timeoutSecs?: number;
|
|
165
210
|
}): Promise<void>;
|
|
211
|
+
/** One block in a `scan` manifest: structure + a short preview, no full content. */
|
|
212
|
+
export interface BlockManifest {
|
|
213
|
+
id: string;
|
|
214
|
+
block_type: string;
|
|
215
|
+
preview: string;
|
|
216
|
+
content_hash: string;
|
|
217
|
+
parent_id?: string | null;
|
|
218
|
+
token_estimate: number;
|
|
219
|
+
section_name: string;
|
|
220
|
+
section_number: number;
|
|
221
|
+
}
|
|
222
|
+
/** `filetools scan` output: a paginated manifest of block previews. */
|
|
223
|
+
export interface ScanView {
|
|
224
|
+
file_type: string;
|
|
225
|
+
block_count: number;
|
|
226
|
+
total_tokens: number;
|
|
227
|
+
offset: number;
|
|
228
|
+
returned: number;
|
|
229
|
+
total: number;
|
|
230
|
+
blocks: BlockManifest[];
|
|
231
|
+
}
|
|
232
|
+
/** One `grep` hit: the block id, the matching line number, and a snippet. */
|
|
233
|
+
export interface GrepMatch {
|
|
234
|
+
block_id: string;
|
|
235
|
+
line: number;
|
|
236
|
+
snippet: string;
|
|
237
|
+
writable: boolean;
|
|
238
|
+
}
|
|
239
|
+
/** `filetools grep` output: literal-substring matches across blocks. */
|
|
240
|
+
export interface GrepView {
|
|
241
|
+
pattern: string;
|
|
242
|
+
returned: number;
|
|
243
|
+
matches: GrepMatch[];
|
|
244
|
+
}
|
|
245
|
+
/** `filetools read` output: hydrated nodes for the requested blocks. */
|
|
246
|
+
export interface ReadView {
|
|
247
|
+
offset: number;
|
|
248
|
+
returned: number;
|
|
249
|
+
total: number;
|
|
250
|
+
nodes: DocNode[];
|
|
251
|
+
}
|
|
252
|
+
/** Scan a document into a paginated manifest of block previews (no hydration). */
|
|
253
|
+
export declare function scanDocument(absolutePath: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
254
|
+
offset?: number;
|
|
255
|
+
limit?: number;
|
|
256
|
+
timeoutSecs?: number;
|
|
257
|
+
}): Promise<ScanView>;
|
|
258
|
+
/** Locate blocks containing `pattern` (literal substring) without hydrating the doc. */
|
|
259
|
+
export declare function grepDocument(absolutePath: string, pattern: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
260
|
+
ignoreCase?: boolean;
|
|
261
|
+
limit?: number;
|
|
262
|
+
timeoutSecs?: number;
|
|
263
|
+
}): Promise<GrepView>;
|
|
264
|
+
/** Hydrate specific blocks by id (or a paginated slice when no ids are given). */
|
|
265
|
+
export declare function readDocumentBlocks(absolutePath: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
266
|
+
ids?: string[];
|
|
267
|
+
offset?: number;
|
|
268
|
+
limit?: number;
|
|
269
|
+
timeoutSecs?: number;
|
|
270
|
+
}): Promise<ReadView>;
|
|
166
271
|
//# sourceMappingURL=filetools-shared.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filetools-shared.d.ts","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. */\nexport const 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. */\nexport type Fidelity = \"lossless\" | \"in_place_text\" | \"read_only\";\n\nexport interface 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\nexport interface 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). */\nexport interface 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// ============================================================================\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. */\nexport function 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 * Apply `patch` to a previously-extracted document, writing the reconstructed\n * bytes to `outPath`. Requires a prior {@link extractDocument} (the stateful\n * flow): the cached envelope + sidecar carry the id-map reconstruct needs.\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 = getExtractRecord(absolutePath);\n\tif (!record) {\n\t\tthrow new Error(\n\t\t\t`no extracted envelope for ${basename(absolutePath)} — run DocRead on it first, then DocEdit/DocWrite`,\n\t\t);\n\t}\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\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"]}
|
|
1
|
+
{"version":3,"file":"filetools-shared.d.ts","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. */\nexport const 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. */\nexport type Fidelity = \"lossless\" | \"in_place_text\" | \"read_only\";\n\nexport interface 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\nexport interface 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). */\nexport interface 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. */\nexport function 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. */\nexport interface 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. */\nexport interface 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"]}
|