@kolisachint/hoocode-agent 0.4.87 → 0.4.89
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 +16 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +6 -0
- 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/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +4 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/docedit.d.ts +42 -0
- package/dist/core/tools/docedit.d.ts.map +1 -0
- package/dist/core/tools/docedit.js +105 -0
- package/dist/core/tools/docedit.js.map +1 -0
- package/dist/core/tools/docread.d.ts +22 -0
- package/dist/core/tools/docread.d.ts.map +1 -0
- package/dist/core/tools/docread.js +112 -0
- package/dist/core/tools/docread.js.map +1 -0
- package/dist/core/tools/docwrite.d.ts +44 -0
- package/dist/core/tools/docwrite.d.ts.map +1 -0
- package/dist/core/tools/docwrite.js +92 -0
- package/dist/core/tools/docwrite.js.map +1 -0
- package/dist/core/tools/filetools-shared.d.ts +146 -0
- package/dist/core/tools/filetools-shared.d.ts.map +1 -0
- package/dist/core/tools/filetools-shared.js +208 -0
- package/dist/core/tools/filetools-shared.js.map +1 -0
- 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/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +29 -0
- package/dist/core/tools/read.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +24 -0
- package/dist/utils/tools-manager.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,112 @@
|
|
|
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 { extractDocument } 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 docReadSchema = Type.Object({
|
|
10
|
+
path: Type.String({
|
|
11
|
+
description: "Path to the document to extract (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.",
|
|
12
|
+
}),
|
|
13
|
+
readonly: Type.Optional(Type.Boolean({
|
|
14
|
+
description: "Analysis-only projection: strips node ids for a smaller view that CANNOT be edited. Omit (default false) when you intend to DocEdit/DocWrite afterwards.",
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
function countNodes(nodes) {
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (const node of nodes) {
|
|
20
|
+
total += 1 + (node.children ? countNodes(node.children) : 0);
|
|
21
|
+
}
|
|
22
|
+
return total;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Render the envelope as compact, id-addressed text the model edits against.
|
|
26
|
+
* Each line carries the node id so DocEdit/DocWrite patches can target it.
|
|
27
|
+
*/
|
|
28
|
+
function renderEnvelopeText(envelope) {
|
|
29
|
+
const header = `document ${envelope.source.path} [${envelope.source.type}, ${envelope.fidelity}, ` +
|
|
30
|
+
`${envelope.writable ? "writable" : "read-only"}]`;
|
|
31
|
+
const lines = [header, ""];
|
|
32
|
+
const walk = (nodes, depth) => {
|
|
33
|
+
for (const node of nodes) {
|
|
34
|
+
const indent = " ".repeat(depth);
|
|
35
|
+
const idPart = node.id ? `#${node.id} ` : "";
|
|
36
|
+
const attrs = node.attrs?.length ? ` ${node.attrs.map((a) => `${a.name}="${a.value}"`).join(" ")}` : "";
|
|
37
|
+
const text = node.text !== undefined ? ` :: ${JSON.stringify(node.text)}` : "";
|
|
38
|
+
lines.push(`${indent}${idPart}<${node.tag}${attrs}>${text}`);
|
|
39
|
+
if (node.children?.length)
|
|
40
|
+
walk(node.children, depth + 1);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(envelope.structure, 0);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
function formatDocReadCall(args) {
|
|
47
|
+
const path = str(args?.path);
|
|
48
|
+
const pathDisplay = path === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg("toolOutput", "...");
|
|
49
|
+
let text = appTheme.fg("toolTitle", appTheme.bold("DocRead ")) + appTheme.fg("accent", pathDisplay);
|
|
50
|
+
if (args?.readonly)
|
|
51
|
+
text += appTheme.fg("muted", " (readonly)");
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
function formatDocReadResult(result, options, showImages) {
|
|
55
|
+
const output = getTextOutput(result, showImages).trim();
|
|
56
|
+
if (!output)
|
|
57
|
+
return "";
|
|
58
|
+
const lines = output.split("\n");
|
|
59
|
+
const maxLines = options.expanded ? lines.length : 15;
|
|
60
|
+
const displayLines = lines.slice(0, maxLines);
|
|
61
|
+
const remaining = lines.length - maxLines;
|
|
62
|
+
let text = `\n${displayLines.map((line) => appTheme.fg("toolOutput", line)).join("\n")}`;
|
|
63
|
+
if (remaining > 0) {
|
|
64
|
+
text += `${appTheme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
65
|
+
}
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
export function createDocReadToolDefinition(cwd, options) {
|
|
69
|
+
return {
|
|
70
|
+
name: "DocRead",
|
|
71
|
+
label: "DocRead",
|
|
72
|
+
description: "Extract a structured or binary document (XML, drawio, docx/xlsx/pptx, PDF) into editable, id-addressed JSON the agent can patch losslessly. Each node has a stable #id; edit with DocEdit (in place) or DocWrite (to a new path), passing a patch that targets those ids. Off by default; enabled with --enable-filetools.",
|
|
73
|
+
promptSnippet: "Extract a structured/binary document into editable, id-addressed JSON",
|
|
74
|
+
promptGuidelines: [
|
|
75
|
+
"Use DocRead to open structured/binary documents (XML, drawio, docx/xlsx/pptx, PDF) instead of read; it returns id-addressed nodes you can patch with DocEdit/DocWrite.",
|
|
76
|
+
"DocEdit/DocWrite require a prior DocRead of the same file (the id-map is established by the extract).",
|
|
77
|
+
],
|
|
78
|
+
parameters: docReadSchema,
|
|
79
|
+
async execute(_toolCallId, { path, readonly }, signal) {
|
|
80
|
+
if (signal?.aborted)
|
|
81
|
+
throw new Error("Operation aborted");
|
|
82
|
+
const absolutePath = resolveReadPath(path, cwd);
|
|
83
|
+
const envelope = await extractDocument(absolutePath, cwd, signal, {
|
|
84
|
+
readonly,
|
|
85
|
+
timeoutSecs: options?.timeoutSecs,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: renderEnvelopeText(envelope) }],
|
|
89
|
+
details: {
|
|
90
|
+
type: envelope.source.type,
|
|
91
|
+
fidelity: envelope.fidelity,
|
|
92
|
+
writable: envelope.writable,
|
|
93
|
+
nodeCount: countNodes(envelope.structure),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
renderCall(args, _theme, context) {
|
|
98
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
99
|
+
text.setText(formatDocReadCall(args));
|
|
100
|
+
return text;
|
|
101
|
+
},
|
|
102
|
+
renderResult(result, options, _theme, context) {
|
|
103
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
104
|
+
text.setText(formatDocReadResult(result, options, context.showImages));
|
|
105
|
+
return text;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function createDocReadTool(cwd, options) {
|
|
110
|
+
return wrapToolDefinition(createDocReadToolDefinition(cwd, options));
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=docread.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docread.js","sourceRoot":"","sources":["../../../src/core/tools/docread.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AACjF,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EAA+B,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EACV,sGAAsG;KACvG,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,QAAQ,CACtB,IAAI,CAAC,OAAO,CAAC;QACZ,WAAW,EACV,0JAA0J;KAC3J,CAAC,CACF;CACD,CAAC,CAAC;AAgBH,SAAS,UAAU,CAAC,KAAgB,EAAU;IAC7C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,QAAkB,EAAU;IACvD,MAAM,MAAM,GACX,YAAY,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,QAAQ,IAAI;QACnF,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC;IACpD,MAAM,KAAK,GAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,CAAC,KAAgB,EAAE,KAAa,EAAQ,EAAE,CAAC;QACvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAClC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxG,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/E,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC;YAC7D,IAAI,IAAI,CAAC,QAAQ,EAAE,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC3D,CAAC;IAAA,CACD,CAAC;IACF,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB;AAED,SAAS,iBAAiB,CAAC,IAAuD,EAAU;IAC3F,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7B,MAAM,WAAW,GAChB,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACxG,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACpG,IAAI,IAAI,EAAE,QAAQ;QAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,mBAAmB,CAC3B,MAAyF,EACzF,OAAgC,EAChC,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAa,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/D,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACtD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;IAC1C,IAAI,IAAI,GAAG,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACzF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QACnB,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;IACnH,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,2BAA2B,CAC1C,GAAW,EACX,OAA4B,EAC2C;IACvE,OAAO;QACN,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS;QAChB,WAAW,EACV,4TAA4T;QAC7T,aAAa,EAAE,uEAAuE;QACtF,gBAAgB,EAAE;YACjB,wKAAwK;YACxK,uGAAuG;SACvG;QACD,UAAU,EAAE,aAAa;QACzB,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAoB,EAAE,MAAoB,EAAE;YACtF,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC1D,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE;gBACjE,QAAQ;gBACR,WAAW,EAAE,OAAO,EAAE,WAAW;aACjC,CAAC,CAAC;YACH,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxE,OAAO,EAAE;oBACR,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI;oBAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;oBAC3B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;oBAC3B,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;iBACzC;aACD,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;YACjC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;YACtC,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;YAC9C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,MAAa,EAAE,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAC9E,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAA4B,EAAmC;IAC7G,OAAO,kBAAkB,CAAC,2BAA2B,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CACrE","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 DocNode, type Envelope, extractDocument } 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 docReadSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription:\n\t\t\t\"Path to the document to extract (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.\",\n\t}),\n\treadonly: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Analysis-only projection: strips node ids for a smaller view that CANNOT be edited. Omit (default false) when you intend to DocEdit/DocWrite afterwards.\",\n\t\t}),\n\t),\n});\n\nexport type DocReadToolInput = Static<typeof docReadSchema>;\n\nexport interface DocReadToolDetails {\n\ttype?: string;\n\tfidelity?: string;\n\twritable?: boolean;\n\tnodeCount?: number;\n}\n\nexport interface DocReadToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\nfunction countNodes(nodes: DocNode[]): number {\n\tlet total = 0;\n\tfor (const node of nodes) {\n\t\ttotal += 1 + (node.children ? countNodes(node.children) : 0);\n\t}\n\treturn total;\n}\n\n/**\n * Render the envelope as compact, id-addressed text the model edits against.\n * Each line carries the node id so DocEdit/DocWrite patches can target it.\n */\nfunction renderEnvelopeText(envelope: Envelope): string {\n\tconst header =\n\t\t`document ${envelope.source.path} [${envelope.source.type}, ${envelope.fidelity}, ` +\n\t\t`${envelope.writable ? \"writable\" : \"read-only\"}]`;\n\tconst lines: string[] = [header, \"\"];\n\n\tconst walk = (nodes: DocNode[], depth: number): void => {\n\t\tfor (const node of nodes) {\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(envelope.structure, 0);\n\treturn lines.join(\"\\n\");\n}\n\nfunction formatDocReadCall(args: { path?: string; readonly?: boolean } | 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(\"DocRead \")) + appTheme.fg(\"accent\", pathDisplay);\n\tif (args?.readonly) text += appTheme.fg(\"muted\", \" (readonly)\");\n\treturn text;\n}\n\nfunction formatDocReadResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: DocReadToolDetails },\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 createDocReadToolDefinition(\n\tcwd: string,\n\toptions?: DocReadToolOptions,\n): ToolDefinition<typeof docReadSchema, DocReadToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocRead\",\n\t\tlabel: \"DocRead\",\n\t\tdescription:\n\t\t\t\"Extract a structured or binary document (XML, drawio, docx/xlsx/pptx, PDF) into editable, id-addressed JSON the agent can patch losslessly. Each node has a stable #id; edit with DocEdit (in place) or DocWrite (to a new path), passing a patch that targets those ids. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Extract a structured/binary document into editable, id-addressed JSON\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocRead to open structured/binary documents (XML, drawio, docx/xlsx/pptx, PDF) instead of read; it returns id-addressed nodes you can patch with DocEdit/DocWrite.\",\n\t\t\t\"DocEdit/DocWrite require a prior DocRead of the same file (the id-map is established by the extract).\",\n\t\t],\n\t\tparameters: docReadSchema,\n\t\tasync execute(_toolCallId, { path, readonly }: DocReadToolInput, 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 envelope = await extractDocument(absolutePath, cwd, signal, {\n\t\t\t\treadonly,\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: renderEnvelopeText(envelope) }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttype: envelope.source.type,\n\t\t\t\t\tfidelity: envelope.fidelity,\n\t\t\t\t\twritable: envelope.writable,\n\t\t\t\t\tnodeCount: countNodes(envelope.structure),\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(formatDocReadCall(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(formatDocReadResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocReadTool(cwd: string, options?: DocReadToolOptions): AgentTool<typeof docReadSchema> {\n\treturn wrapToolDefinition(createDocReadToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AgentTool } from "@kolisachint/hoocode-agent-core";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import type { ToolDefinition } from "../extensions/types.js";
|
|
4
|
+
declare const docWriteSchema: Type.TObject<{
|
|
5
|
+
path: Type.TString;
|
|
6
|
+
out: Type.TString;
|
|
7
|
+
patch: Type.TArray<Type.TUnion<[Type.TObject<{
|
|
8
|
+
op: Type.TLiteral<"test">;
|
|
9
|
+
path: Type.TString;
|
|
10
|
+
hash: Type.TString;
|
|
11
|
+
}>, Type.TObject<{
|
|
12
|
+
op: Type.TLiteral<"replace">;
|
|
13
|
+
path: Type.TString;
|
|
14
|
+
value: Type.TString;
|
|
15
|
+
}>, Type.TObject<{
|
|
16
|
+
op: Type.TLiteral<"add">;
|
|
17
|
+
after: Type.TOptional<Type.TString>;
|
|
18
|
+
before: Type.TOptional<Type.TString>;
|
|
19
|
+
value: Type.TObject<{
|
|
20
|
+
tag: Type.TString;
|
|
21
|
+
attrs: Type.TOptional<Type.TArray<Type.TObject<{
|
|
22
|
+
name: Type.TString;
|
|
23
|
+
value: Type.TString;
|
|
24
|
+
}>>>;
|
|
25
|
+
text: Type.TOptional<Type.TString>;
|
|
26
|
+
}>;
|
|
27
|
+
}>, Type.TObject<{
|
|
28
|
+
op: Type.TLiteral<"remove">;
|
|
29
|
+
path: Type.TString;
|
|
30
|
+
}>]>>;
|
|
31
|
+
}>;
|
|
32
|
+
export type DocWriteToolInput = Static<typeof docWriteSchema>;
|
|
33
|
+
export interface DocWriteToolDetails {
|
|
34
|
+
ops?: number;
|
|
35
|
+
out?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface DocWriteToolOptions {
|
|
38
|
+
/** Timeout (seconds) for the filetools invocation. */
|
|
39
|
+
timeoutSecs?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function createDocWriteToolDefinition(cwd: string, options?: DocWriteToolOptions): ToolDefinition<typeof docWriteSchema, DocWriteToolDetails | undefined>;
|
|
42
|
+
export declare function createDocWriteTool(cwd: string, options?: DocWriteToolOptions): AgentTool<typeof docWriteSchema>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=docwrite.d.ts.map
|
|
@@ -0,0 +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;AAO7D,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,CAuDxE;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 { 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). 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"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdir as fsMkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { Container, Text } from "@kolisachint/hoocode-tui";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
|
|
6
|
+
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
7
|
+
import { getExtractRecord, patchOpsSchema, reconstructDocument, toPatch } from "./filetools-shared.js";
|
|
8
|
+
import { resolveReadPath, resolveToCwd } from "./path-utils.js";
|
|
9
|
+
import { invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
10
|
+
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
11
|
+
const docWriteSchema = Type.Object({
|
|
12
|
+
path: Type.String({
|
|
13
|
+
description: "Path to the SOURCE document. Must have been opened with DocRead first. Left untouched.",
|
|
14
|
+
}),
|
|
15
|
+
out: Type.String({
|
|
16
|
+
description: "Output path for the reconstructed document (relative or absolute). Created/overwritten.",
|
|
17
|
+
}),
|
|
18
|
+
patch: patchOpsSchema,
|
|
19
|
+
});
|
|
20
|
+
function formatDocWriteCall(args) {
|
|
21
|
+
const path = str(args?.path);
|
|
22
|
+
const out = str(args?.out);
|
|
23
|
+
const pathDisplay = path === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg("toolOutput", "...");
|
|
24
|
+
const outDisplay = out === null ? invalidArgText(appTheme) : out ? shortenPath(out) : appTheme.fg("toolOutput", "...");
|
|
25
|
+
let text = appTheme.fg("toolTitle", appTheme.bold("DocWrite ")) +
|
|
26
|
+
appTheme.fg("accent", pathDisplay) +
|
|
27
|
+
appTheme.fg("muted", " -> ") +
|
|
28
|
+
appTheme.fg("accent", outDisplay);
|
|
29
|
+
const ops = Array.isArray(args?.patch) ? args.patch.length : undefined;
|
|
30
|
+
if (ops !== undefined)
|
|
31
|
+
text += appTheme.fg("muted", ` (${ops} op${ops === 1 ? "" : "s"})`);
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
export function createDocWriteToolDefinition(cwd, options) {
|
|
35
|
+
return {
|
|
36
|
+
name: "DocWrite",
|
|
37
|
+
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). Requires a prior DocRead of the source (the patch targets node ids from that extract). Off by default; enabled with --enable-filetools.",
|
|
39
|
+
promptSnippet: "Reconstruct a patched structured/binary document to a new path",
|
|
40
|
+
promptGuidelines: [
|
|
41
|
+
"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.",
|
|
42
|
+
],
|
|
43
|
+
parameters: docWriteSchema,
|
|
44
|
+
async execute(_toolCallId, { path, out, patch }, signal) {
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
throw new Error("Operation aborted");
|
|
47
|
+
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
|
+
const outPath = resolveToCwd(out, cwd);
|
|
52
|
+
return withFileMutationQueue(outPath, async () => {
|
|
53
|
+
await fsMkdir(dirname(outPath), { recursive: true });
|
|
54
|
+
await reconstructDocument(absolutePath, toPatch(patch), outPath, cwd, signal, {
|
|
55
|
+
timeoutSecs: options?.timeoutSecs,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Wrote ${patch.length} op${patch.length === 1 ? "" : "s"} from ${path} to ${out}.`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
details: { ops: patch.length, out },
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
renderCall(args, _theme, context) {
|
|
69
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
70
|
+
text.setText(formatDocWriteCall(args));
|
|
71
|
+
return text;
|
|
72
|
+
},
|
|
73
|
+
renderResult(result, _options, _theme, context) {
|
|
74
|
+
if (!context.isError) {
|
|
75
|
+
const component = context.lastComponent ?? new Container();
|
|
76
|
+
component.clear();
|
|
77
|
+
return component;
|
|
78
|
+
}
|
|
79
|
+
const output = result.content
|
|
80
|
+
.filter((c) => c.type === "text")
|
|
81
|
+
.map((c) => c.text || "")
|
|
82
|
+
.join("\n");
|
|
83
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
84
|
+
text.setText(output ? `\n${appTheme.fg("error", output)}` : "");
|
|
85
|
+
return text;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function createDocWriteTool(cwd, options) {
|
|
90
|
+
return wrapToolDefinition(createDocWriteToolDefinition(cwd, options));
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=docwrite.js.map
|
|
@@ -0,0 +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). 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"]}
|
|
@@ -0,0 +1,146 @@
|
|
|
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 { type Static, Type } from "typebox";
|
|
20
|
+
/** Default timeout (seconds) for a single filetools invocation. */
|
|
21
|
+
export declare const FILETOOLS_DEFAULT_TIMEOUT_SECS = 30;
|
|
22
|
+
/** How faithfully a handler can reconstruct a file after edits. */
|
|
23
|
+
export type Fidelity = "lossless" | "in_place_text" | "read_only";
|
|
24
|
+
export interface DocSource {
|
|
25
|
+
path: string;
|
|
26
|
+
/** Logical format, e.g. "xml", "drawio". */
|
|
27
|
+
type: string;
|
|
28
|
+
/** `sha256:<hex>` of the original bytes. */
|
|
29
|
+
hash: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DocAttr {
|
|
32
|
+
name: string;
|
|
33
|
+
value: string;
|
|
34
|
+
}
|
|
35
|
+
export interface DocNode {
|
|
36
|
+
id: string;
|
|
37
|
+
tag: string;
|
|
38
|
+
attrs?: DocAttr[];
|
|
39
|
+
text?: string;
|
|
40
|
+
children?: DocNode[];
|
|
41
|
+
}
|
|
42
|
+
/** The extract output handed to the model. Mirrors the Rust `Envelope`. */
|
|
43
|
+
export interface Envelope {
|
|
44
|
+
version: string;
|
|
45
|
+
source: DocSource;
|
|
46
|
+
fidelity: Fidelity;
|
|
47
|
+
writable: boolean;
|
|
48
|
+
idmap_ref?: string;
|
|
49
|
+
structure: DocNode[];
|
|
50
|
+
}
|
|
51
|
+
/** A new element for an `add` op (text-only content, v1). */
|
|
52
|
+
export interface NewElement {
|
|
53
|
+
tag: string;
|
|
54
|
+
attrs?: DocAttr[];
|
|
55
|
+
text?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One patch operation. RFC-6902 vocabulary, id-based pointers
|
|
59
|
+
* (`/structure/<id>/text`, `/structure/<id>/attrs/<name>`), per the filetools
|
|
60
|
+
* patch format.
|
|
61
|
+
*/
|
|
62
|
+
export type PatchOp = {
|
|
63
|
+
op: "test";
|
|
64
|
+
path: string;
|
|
65
|
+
hash: string;
|
|
66
|
+
} | {
|
|
67
|
+
op: "replace";
|
|
68
|
+
path: string;
|
|
69
|
+
value: string;
|
|
70
|
+
} | {
|
|
71
|
+
op: "add";
|
|
72
|
+
after?: string;
|
|
73
|
+
before?: string;
|
|
74
|
+
value: NewElement;
|
|
75
|
+
} | {
|
|
76
|
+
op: "remove";
|
|
77
|
+
path: string;
|
|
78
|
+
};
|
|
79
|
+
export interface Patch {
|
|
80
|
+
patch: PatchOp[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The model-facing patch parameter: an array of id-based RFC-6902 ops, matching
|
|
84
|
+
* the filetools patch wire format. Shared by DocEdit and DocWrite.
|
|
85
|
+
*/
|
|
86
|
+
export declare const patchOpsSchema: Type.TArray<Type.TUnion<[Type.TObject<{
|
|
87
|
+
op: Type.TLiteral<"test">;
|
|
88
|
+
path: Type.TString;
|
|
89
|
+
hash: Type.TString;
|
|
90
|
+
}>, Type.TObject<{
|
|
91
|
+
op: Type.TLiteral<"replace">;
|
|
92
|
+
path: Type.TString;
|
|
93
|
+
value: Type.TString;
|
|
94
|
+
}>, Type.TObject<{
|
|
95
|
+
op: Type.TLiteral<"add">;
|
|
96
|
+
after: Type.TOptional<Type.TString>;
|
|
97
|
+
before: Type.TOptional<Type.TString>;
|
|
98
|
+
value: Type.TObject<{
|
|
99
|
+
tag: Type.TString;
|
|
100
|
+
attrs: Type.TOptional<Type.TArray<Type.TObject<{
|
|
101
|
+
name: Type.TString;
|
|
102
|
+
value: Type.TString;
|
|
103
|
+
}>>>;
|
|
104
|
+
text: Type.TOptional<Type.TString>;
|
|
105
|
+
}>;
|
|
106
|
+
}>, Type.TObject<{
|
|
107
|
+
op: Type.TLiteral<"remove">;
|
|
108
|
+
path: Type.TString;
|
|
109
|
+
}>]>>;
|
|
110
|
+
export type PatchOpsInput = Static<typeof patchOpsSchema>;
|
|
111
|
+
/** Wrap the model-facing ops array into the binary's `{ patch: [...] }` envelope. */
|
|
112
|
+
export declare function toPatch(ops: PatchOpsInput): Patch;
|
|
113
|
+
export interface ExtractRecord {
|
|
114
|
+
/** Absolute path of the source document. */
|
|
115
|
+
source: string;
|
|
116
|
+
/** Path to the envelope JSON in the working directory. */
|
|
117
|
+
envelopePath: string;
|
|
118
|
+
/** Parsed envelope (also returned to the model on DocRead). */
|
|
119
|
+
envelope: Envelope;
|
|
120
|
+
/** The source's stat signature at extract time, to detect drift cheaply. */
|
|
121
|
+
signature: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Extract `absolutePath` to an envelope (+ sidecar) in the working directory,
|
|
125
|
+
* cache the result keyed by the source path, and return the parsed envelope.
|
|
126
|
+
*
|
|
127
|
+
* `readonly` strips ids for a smaller, analysis-only projection that cannot be
|
|
128
|
+
* reconstructed (DocRead's default-off mode).
|
|
129
|
+
*/
|
|
130
|
+
export declare function extractDocument(absolutePath: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
131
|
+
readonly?: boolean;
|
|
132
|
+
timeoutSecs?: number;
|
|
133
|
+
}): Promise<Envelope>;
|
|
134
|
+
/** Look up a cached extraction for `absolutePath`, if one is still valid. */
|
|
135
|
+
export declare function getExtractRecord(absolutePath: string): ExtractRecord | undefined;
|
|
136
|
+
/** Drop any cached extraction for `absolutePath`. */
|
|
137
|
+
export declare function invalidateExtractRecord(absolutePath: string): void;
|
|
138
|
+
/**
|
|
139
|
+
* Apply `patch` to a previously-extracted document, writing the reconstructed
|
|
140
|
+
* bytes to `outPath`. Requires a prior {@link extractDocument} (the stateful
|
|
141
|
+
* flow): the cached envelope + sidecar carry the id-map reconstruct needs.
|
|
142
|
+
*/
|
|
143
|
+
export declare function reconstructDocument(absolutePath: string, patch: Patch, outPath: string, cwd: string, signal: AbortSignal | undefined, options?: {
|
|
144
|
+
timeoutSecs?: number;
|
|
145
|
+
}): Promise<void>;
|
|
146
|
+
//# sourceMappingURL=filetools-shared.d.ts.map
|
|
@@ -0,0 +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// 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"]}
|