@kolisachint/hoocode-agent 0.4.89 → 0.4.91
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 +4 -0
- package/dist/core/tools/docedit.d.ts.map +1 -1
- package/dist/core/tools/docedit.js +1 -1
- package/dist/core/tools/docedit.js.map +1 -1
- package/dist/core/tools/docread.d.ts +10 -0
- package/dist/core/tools/docread.d.ts.map +1 -1
- package/dist/core/tools/docread.js +18 -6
- package/dist/core/tools/docread.js.map +1 -1
- package/dist/core/tools/docwrite.d.ts.map +1 -1
- package/dist/core/tools/docwrite.js +1 -1
- package/dist/core/tools/docwrite.js.map +1 -1
- package/dist/core/tools/filetools-shared.d.ts +20 -0
- package/dist/core/tools/filetools-shared.d.ts.map +1 -1
- package/dist/core/tools/filetools-shared.js +35 -0
- package/dist/core/tools/filetools-shared.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +9 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docedit.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docedit.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;EAKjB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IAClC,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CA4EtE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC,OAAO,aAAa,CAAC,CAE5G","sourcesContent":["import { mkdtempSync, renameSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } 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 { APP_NAME } from \"../../config.js\";\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 {\n\textractDocument,\n\tgetExtractRecord,\n\tinvalidateExtractRecord,\n\tpatchOpsSchema,\n\treconstructDocument,\n\ttoPatch,\n} from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docEditSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to edit in place. Must have been opened with DocRead first.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocEditToolInput = Static<typeof docEditSchema>;\n\nexport interface DocEditToolDetails {\n\tops?: number;\n}\n\nexport interface DocEditToolOptions {\n\t/** Timeout (seconds) for the filetools invocations. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocEditCall(args: { path?: string; patch?: unknown[] } | 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(\"DocEdit \")) + appTheme.fg(\"accent\", pathDisplay);\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 createDocEditToolDefinition(\n\tcwd: string,\n\toptions?: DocEditToolOptions,\n): ToolDefinition<typeof docEditSchema, DocEditToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocEdit\",\n\t\tlabel: \"DocEdit\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Requires a prior DocRead of the same file (the patch targets node ids from that extract). Untouched bytes are preserved exactly; the file is then re-extracted so ids stay current for further edits. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Patch a structured/binary document in place by node id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocEdit to modify a document opened with DocRead; pass a patch of id-based ops (replace/add/remove) targeting the #ids from the extract. It edits in place and re-extracts.\",\n\t\t],\n\t\tparameters: docEditSchema,\n\t\tasync execute(_toolCallId, { path, patch }: DocEditToolInput, 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(\n\t\t\t\t\t`no extracted envelope for ${basename(absolutePath)} — run DocRead on it first, then DocEdit`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Reconstruct to a temp file, then atomically swap it into place so a\n\t\t\t\t// failed reconstruct never leaves a half-written document behind.\n\t\t\t\tconst stageDir = mkdtempSync(join(tmpdir(), `${APP_NAME}-docedit-`));\n\t\t\t\tconst stagePath = join(stageDir, basename(absolutePath));\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), stagePath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t\trenameSync(stagePath, absolutePath);\n\t\t\t\t} finally {\n\t\t\t\t\trmSync(stageDir, { recursive: true, force: true });\n\t\t\t\t}\n\n\t\t\t\t// The source changed, so the cached id-map is stale. Re-extract to\n\t\t\t\t// refresh ids for subsequent edits (best-effort; a failure here does\n\t\t\t\t// not undo the successful write).\n\t\t\t\tinvalidateExtractRecord(absolutePath);\n\t\t\t\ttry {\n\t\t\t\t\tawait extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });\n\t\t\t\t} catch {\n\t\t\t\t\t// Re-extract failure leaves no cached record; the next DocEdit will\n\t\t\t\t\t// require a fresh DocRead, which is the correct fallback.\n\t\t\t\t}\n\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: `Applied ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} to ${path} (re-extracted; ids refreshed).`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length },\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(formatDocEditCall(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 createDocEditTool(cwd: string, options?: DocEditToolOptions): AgentTool<typeof docEditSchema> {\n\treturn wrapToolDefinition(createDocEditToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"docedit.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docedit.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;EAKjB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IAClC,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CA4EtE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC,OAAO,aAAa,CAAC,CAE5G","sourcesContent":["import { mkdtempSync, renameSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } 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 { APP_NAME } from \"../../config.js\";\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 {\n\textractDocument,\n\tgetExtractRecord,\n\tinvalidateExtractRecord,\n\tpatchOpsSchema,\n\treconstructDocument,\n\ttoPatch,\n} from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docEditSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to edit in place. Must have been opened with DocRead first.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocEditToolInput = Static<typeof docEditSchema>;\n\nexport interface DocEditToolDetails {\n\tops?: number;\n}\n\nexport interface DocEditToolOptions {\n\t/** Timeout (seconds) for the filetools invocations. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocEditCall(args: { path?: string; patch?: unknown[] } | 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(\"DocEdit \")) + appTheme.fg(\"accent\", pathDisplay);\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 createDocEditToolDefinition(\n\tcwd: string,\n\toptions?: DocEditToolOptions,\n): ToolDefinition<typeof docEditSchema, DocEditToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocEdit\",\n\t\tlabel: \"DocEdit\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Requires a prior DocRead of the same file (the patch targets node ids from that extract). Untouched bytes are preserved exactly; the file is then re-extracted so ids stay current for further edits. This is the canonical way to edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to rewrite them — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Patch a structured/binary document in place by node id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocEdit to modify a document opened with DocRead; pass a patch of id-based ops (replace/add/remove) targeting the #ids from the extract. It edits in place and re-extracts.\",\n\t\t],\n\t\tparameters: docEditSchema,\n\t\tasync execute(_toolCallId, { path, patch }: DocEditToolInput, 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(\n\t\t\t\t\t`no extracted envelope for ${basename(absolutePath)} — run DocRead on it first, then DocEdit`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Reconstruct to a temp file, then atomically swap it into place so a\n\t\t\t\t// failed reconstruct never leaves a half-written document behind.\n\t\t\t\tconst stageDir = mkdtempSync(join(tmpdir(), `${APP_NAME}-docedit-`));\n\t\t\t\tconst stagePath = join(stageDir, basename(absolutePath));\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), stagePath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t\trenameSync(stagePath, absolutePath);\n\t\t\t\t} finally {\n\t\t\t\t\trmSync(stageDir, { recursive: true, force: true });\n\t\t\t\t}\n\n\t\t\t\t// The source changed, so the cached id-map is stale. Re-extract to\n\t\t\t\t// refresh ids for subsequent edits (best-effort; a failure here does\n\t\t\t\t// not undo the successful write).\n\t\t\t\tinvalidateExtractRecord(absolutePath);\n\t\t\t\ttry {\n\t\t\t\t\tawait extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });\n\t\t\t\t} catch {\n\t\t\t\t\t// Re-extract failure leaves no cached record; the next DocEdit will\n\t\t\t\t\t// require a fresh DocRead, which is the correct fallback.\n\t\t\t\t}\n\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: `Applied ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} to ${path} (re-extracted; ids refreshed).`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length },\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(formatDocEditCall(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 createDocEditTool(cwd: string, options?: DocEditToolOptions): AgentTool<typeof docEditSchema> {\n\treturn wrapToolDefinition(createDocEditToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -29,7 +29,7 @@ export function createDocEditToolDefinition(cwd, options) {
|
|
|
29
29
|
return {
|
|
30
30
|
name: "DocEdit",
|
|
31
31
|
label: "DocEdit",
|
|
32
|
-
description: "Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Requires a prior DocRead of the same file (the patch targets node ids from that extract). Untouched bytes are preserved exactly; the file is then re-extracted so ids stay current for further edits. Off by default; enabled with --enable-filetools.",
|
|
32
|
+
description: "Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Requires a prior DocRead of the same file (the patch targets node ids from that extract). Untouched bytes are preserved exactly; the file is then re-extracted so ids stay current for further edits. This is the canonical way to edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to rewrite them — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.",
|
|
33
33
|
promptSnippet: "Patch a structured/binary document in place by node id",
|
|
34
34
|
promptGuidelines: [
|
|
35
35
|
"Use DocEdit to modify a document opened with DocRead; pass a patch of id-based ops (replace/add/remove) targeting the #ids from the extract. It edits in place and re-extracts.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docedit.js","sourceRoot":"","sources":["../../../src/core/tools/docedit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACN,eAAe,EACf,gBAAgB,EAChB,uBAAuB,EACvB,cAAc,EACd,mBAAmB,EACnB,OAAO,GACP,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,kFAAkF;KAC/F,CAAC;IACF,KAAK,EAAE,cAAc;CACrB,CAAC,CAAC;AAaH,SAAS,iBAAiB,CAAC,IAAsD,EAAU;IAC1F,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,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACvE,IAAI,GAAG,KAAK,SAAS;QAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;IAC3F,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,sUAAsU;QACvU,aAAa,EAAE,wDAAwD;QACvE,gBAAgB,EAAE;YACjB,iLAAiL;SACjL;QACD,UAAU,EAAE,aAAa;QACzB,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAoB,EAAE,MAAoB,EAAE;YACnF,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC1D,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAChD,IAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CACd,6BAA6B,QAAQ,CAAC,YAAY,CAAC,4CAA0C,CAC7F,CAAC;YACH,CAAC;YAED,OAAO,qBAAqB,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC;gBACtD,sEAAsE;gBACtE,kEAAkE;gBAClE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,GAAG,QAAQ,WAAW,CAAC,CAAC,CAAC;gBACrE,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;gBACzD,IAAI,CAAC;oBACJ,MAAM,mBAAmB,CAAC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE;wBAC/E,WAAW,EAAE,OAAO,EAAE,WAAW;qBACjC,CAAC,CAAC;oBACH,UAAU,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;gBACrC,CAAC;wBAAS,CAAC;oBACV,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpD,CAAC;gBAED,mEAAmE;gBACnE,qEAAqE;gBACrE,kCAAkC;gBAClC,uBAAuB,CAAC,YAAY,CAAC,CAAC;gBACtC,IAAI,CAAC;oBACJ,MAAM,eAAe,CAAC,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;gBACzF,CAAC;gBAAC,MAAM,CAAC;oBACR,oEAAoE;oBACpE,0DAA0D;gBAC3D,CAAC;gBAED,OAAO;oBACN,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,WAAW,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,iCAAiC;yBAC5G;qBACD;oBACD,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE;iBAC9B,CAAC;YAAA,CACF,CAAC,CAAC;QAAA,CACH;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,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;YAC/C,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACtB,MAAM,SAAS,GAAI,OAAO,CAAC,aAAuC,IAAI,IAAI,SAAS,EAAE,CAAC;gBACtF,SAAS,CAAC,KAAK,EAAE,CAAC;gBAClB,OAAO,SAAS,CAAC;YAClB,CAAC;YACD,MAAM,MAAM,GAAI,MAAM,CAAC,OAAkD;iBACvE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;iBAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;iBACxB,IAAI,CAAC,IAAI,CAAC,CAAC;YACb,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAChE,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 { mkdtempSync, renameSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } 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 { APP_NAME } from \"../../config.js\";\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 {\n\textractDocument,\n\tgetExtractRecord,\n\tinvalidateExtractRecord,\n\tpatchOpsSchema,\n\treconstructDocument,\n\ttoPatch,\n} from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docEditSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to edit in place. Must have been opened with DocRead first.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocEditToolInput = Static<typeof docEditSchema>;\n\nexport interface DocEditToolDetails {\n\tops?: number;\n}\n\nexport interface DocEditToolOptions {\n\t/** Timeout (seconds) for the filetools invocations. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocEditCall(args: { path?: string; patch?: unknown[] } | 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(\"DocEdit \")) + appTheme.fg(\"accent\", pathDisplay);\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 createDocEditToolDefinition(\n\tcwd: string,\n\toptions?: DocEditToolOptions,\n): ToolDefinition<typeof docEditSchema, DocEditToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocEdit\",\n\t\tlabel: \"DocEdit\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Requires a prior DocRead of the same file (the patch targets node ids from that extract). Untouched bytes are preserved exactly; the file is then re-extracted so ids stay current for further edits. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Patch a structured/binary document in place by node id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocEdit to modify a document opened with DocRead; pass a patch of id-based ops (replace/add/remove) targeting the #ids from the extract. It edits in place and re-extracts.\",\n\t\t],\n\t\tparameters: docEditSchema,\n\t\tasync execute(_toolCallId, { path, patch }: DocEditToolInput, 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(\n\t\t\t\t\t`no extracted envelope for ${basename(absolutePath)} — run DocRead on it first, then DocEdit`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Reconstruct to a temp file, then atomically swap it into place so a\n\t\t\t\t// failed reconstruct never leaves a half-written document behind.\n\t\t\t\tconst stageDir = mkdtempSync(join(tmpdir(), `${APP_NAME}-docedit-`));\n\t\t\t\tconst stagePath = join(stageDir, basename(absolutePath));\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), stagePath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t\trenameSync(stagePath, absolutePath);\n\t\t\t\t} finally {\n\t\t\t\t\trmSync(stageDir, { recursive: true, force: true });\n\t\t\t\t}\n\n\t\t\t\t// The source changed, so the cached id-map is stale. Re-extract to\n\t\t\t\t// refresh ids for subsequent edits (best-effort; a failure here does\n\t\t\t\t// not undo the successful write).\n\t\t\t\tinvalidateExtractRecord(absolutePath);\n\t\t\t\ttry {\n\t\t\t\t\tawait extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });\n\t\t\t\t} catch {\n\t\t\t\t\t// Re-extract failure leaves no cached record; the next DocEdit will\n\t\t\t\t\t// require a fresh DocRead, which is the correct fallback.\n\t\t\t\t}\n\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: `Applied ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} to ${path} (re-extracted; ids refreshed).`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length },\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(formatDocEditCall(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 createDocEditTool(cwd: string, options?: DocEditToolOptions): AgentTool<typeof docEditSchema> {\n\treturn wrapToolDefinition(createDocEditToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"docedit.js","sourceRoot":"","sources":["../../../src/core/tools/docedit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EACN,eAAe,EACf,gBAAgB,EAChB,uBAAuB,EACvB,cAAc,EACd,mBAAmB,EACnB,OAAO,GACP,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,kFAAkF;KAC/F,CAAC;IACF,KAAK,EAAE,cAAc;CACrB,CAAC,CAAC;AAaH,SAAS,iBAAiB,CAAC,IAAsD,EAAU;IAC1F,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,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACvE,IAAI,GAAG,KAAK,SAAS;QAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;IAC3F,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,khBAAghB;QACjhB,aAAa,EAAE,wDAAwD;QACvE,gBAAgB,EAAE;YACjB,iLAAiL;SACjL;QACD,UAAU,EAAE,aAAa;QACzB,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAoB,EAAE,MAAoB,EAAE;YACnF,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC1D,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAChD,IAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CACd,6BAA6B,QAAQ,CAAC,YAAY,CAAC,4CAA0C,CAC7F,CAAC;YACH,CAAC;YAED,OAAO,qBAAqB,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC;gBACtD,sEAAsE;gBACtE,kEAAkE;gBAClE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,GAAG,QAAQ,WAAW,CAAC,CAAC,CAAC;gBACrE,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;gBACzD,IAAI,CAAC;oBACJ,MAAM,mBAAmB,CAAC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE;wBAC/E,WAAW,EAAE,OAAO,EAAE,WAAW;qBACjC,CAAC,CAAC;oBACH,UAAU,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;gBACrC,CAAC;wBAAS,CAAC;oBACV,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpD,CAAC;gBAED,mEAAmE;gBACnE,qEAAqE;gBACrE,kCAAkC;gBAClC,uBAAuB,CAAC,YAAY,CAAC,CAAC;gBACtC,IAAI,CAAC;oBACJ,MAAM,eAAe,CAAC,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;gBACzF,CAAC;gBAAC,MAAM,CAAC;oBACR,oEAAoE;oBACpE,0DAA0D;gBAC3D,CAAC;gBAED,OAAO;oBACN,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,WAAW,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,iCAAiC;yBAC5G;qBACD;oBACD,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE;iBAC9B,CAAC;YAAA,CACF,CAAC,CAAC;QAAA,CACH;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,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;YAC/C,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACtB,MAAM,SAAS,GAAI,OAAO,CAAC,aAAuC,IAAI,IAAI,SAAS,EAAE,CAAC;gBACtF,SAAS,CAAC,KAAK,EAAE,CAAC;gBAClB,OAAO,SAAS,CAAC;YAClB,CAAC;YACD,MAAM,MAAM,GAAI,MAAM,CAAC,OAAkD;iBACvE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;iBAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;iBACxB,IAAI,CAAC,IAAI,CAAC,CAAC;YACb,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAChE,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 { mkdtempSync, renameSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } 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 { APP_NAME } from \"../../config.js\";\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 {\n\textractDocument,\n\tgetExtractRecord,\n\tinvalidateExtractRecord,\n\tpatchOpsSchema,\n\treconstructDocument,\n\ttoPatch,\n} from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docEditSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to edit in place. Must have been opened with DocRead first.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocEditToolInput = Static<typeof docEditSchema>;\n\nexport interface DocEditToolDetails {\n\tops?: number;\n}\n\nexport interface DocEditToolOptions {\n\t/** Timeout (seconds) for the filetools invocations. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocEditCall(args: { path?: string; patch?: unknown[] } | 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(\"DocEdit \")) + appTheme.fg(\"accent\", pathDisplay);\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 createDocEditToolDefinition(\n\tcwd: string,\n\toptions?: DocEditToolOptions,\n): ToolDefinition<typeof docEditSchema, DocEditToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocEdit\",\n\t\tlabel: \"DocEdit\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Requires a prior DocRead of the same file (the patch targets node ids from that extract). Untouched bytes are preserved exactly; the file is then re-extracted so ids stay current for further edits. This is the canonical way to edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to rewrite them — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Patch a structured/binary document in place by node id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocEdit to modify a document opened with DocRead; pass a patch of id-based ops (replace/add/remove) targeting the #ids from the extract. It edits in place and re-extracts.\",\n\t\t],\n\t\tparameters: docEditSchema,\n\t\tasync execute(_toolCallId, { path, patch }: DocEditToolInput, 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(\n\t\t\t\t\t`no extracted envelope for ${basename(absolutePath)} — run DocRead on it first, then DocEdit`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Reconstruct to a temp file, then atomically swap it into place so a\n\t\t\t\t// failed reconstruct never leaves a half-written document behind.\n\t\t\t\tconst stageDir = mkdtempSync(join(tmpdir(), `${APP_NAME}-docedit-`));\n\t\t\t\tconst stagePath = join(stageDir, basename(absolutePath));\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), stagePath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t\trenameSync(stagePath, absolutePath);\n\t\t\t\t} finally {\n\t\t\t\t\trmSync(stageDir, { recursive: true, force: true });\n\t\t\t\t}\n\n\t\t\t\t// The source changed, so the cached id-map is stale. Re-extract to\n\t\t\t\t// refresh ids for subsequent edits (best-effort; a failure here does\n\t\t\t\t// not undo the successful write).\n\t\t\t\tinvalidateExtractRecord(absolutePath);\n\t\t\t\ttry {\n\t\t\t\t\tawait extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });\n\t\t\t\t} catch {\n\t\t\t\t\t// Re-extract failure leaves no cached record; the next DocEdit will\n\t\t\t\t\t// require a fresh DocRead, which is the correct fallback.\n\t\t\t\t}\n\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: `Applied ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} to ${path} (re-extracted; ids refreshed).`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length },\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(formatDocEditCall(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 createDocEditTool(cwd: string, options?: DocEditToolOptions): AgentTool<typeof docEditSchema> {\n\treturn wrapToolDefinition(createDocEditToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@kolisachint/hoocode-agent-core";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import type { ToolDefinition } from "../extensions/types.js";
|
|
4
|
+
import { type Envelope } from "./filetools-shared.js";
|
|
4
5
|
declare const docReadSchema: Type.TObject<{
|
|
5
6
|
path: Type.TString;
|
|
6
7
|
readonly: Type.TOptional<Type.TBoolean>;
|
|
@@ -16,6 +17,15 @@ export interface DocReadToolOptions {
|
|
|
16
17
|
/** Timeout (seconds) for the filetools invocation. */
|
|
17
18
|
timeoutSecs?: number;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Render the envelope as compact, id-addressed text the model edits against.
|
|
22
|
+
* Each line carries the node id so DocEdit/DocWrite patches can target it.
|
|
23
|
+
*
|
|
24
|
+
* The filetools binary has no pagination, so a dense document can project into
|
|
25
|
+
* a huge dump. We truncate the rendered view to a token budget and append an
|
|
26
|
+
* actionable notice instead of flooding the model context.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderEnvelopeText(envelope: Envelope, readonly: boolean | undefined): string;
|
|
19
29
|
export declare function createDocReadToolDefinition(cwd: string, options?: DocReadToolOptions): ToolDefinition<typeof docReadSchema, DocReadToolDetails | undefined>;
|
|
20
30
|
export declare function createDocReadTool(cwd: string, options?: DocReadToolOptions): AgentTool<typeof docReadSchema>;
|
|
21
31
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docread.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docread.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"docread.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docread.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AACtF,OAAO,EAGN,KAAK,QAAQ,EAGb,MAAM,uBAAuB,CAAC;AAK/B,QAAA,MAAM,aAAa;;;EAWjB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAUD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CA6B5F;AA6BD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAwCtE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC,OAAO,aAAa,CAAC,CAE5G","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 {\n\tDOCREAD_MAX_RENDER_TOKENS,\n\ttype DocNode,\n\ttype Envelope,\n\textractDocument,\n\ttruncateRenderToTokenBudget,\n} 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 *\n * The filetools binary has no pagination, so a dense document can project into\n * a huge dump. We truncate the rendered view to a token budget and append an\n * actionable notice instead of flooding the model context.\n */\nexport function renderEnvelopeText(envelope: Envelope, readonly: boolean | undefined): 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\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tif (droppedLines === 0) return text;\n\n\tconst hint = readonly\n\t\t? \"narrow to a smaller or more targeted file\"\n\t\t: \"re-run with readonly:true for a smaller analysis-only view, or target a smaller file\";\n\treturn (\n\t\t`${text}\\n\\n[Truncated: document exceeds the ~${DOCREAD_MAX_RENDER_TOKENS} token render budget; ` +\n\t\t`${droppedLines} more node line${droppedLines === 1 ? \"\" : \"s\"} omitted. The ids shown are still valid for ` +\n\t\t`DocEdit/DocWrite — ${hint}.]`\n\t);\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. This is the canonical way to read and edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to parse or rewrite them — that loses the lossless id-map and corrupts formatting. 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. Never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to parse or edit these formats — that loses the lossless id-map and corrupts the file.\",\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, readonly) }],\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"]}
|
|
@@ -2,7 +2,7 @@ import { Text } from "@kolisachint/hoocode-tui";
|
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
4
4
|
import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
|
|
5
|
-
import { extractDocument } from "./filetools-shared.js";
|
|
5
|
+
import { DOCREAD_MAX_RENDER_TOKENS, extractDocument, truncateRenderToTokenBudget, } from "./filetools-shared.js";
|
|
6
6
|
import { resolveReadPath } from "./path-utils.js";
|
|
7
7
|
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
8
8
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
@@ -24,8 +24,12 @@ function countNodes(nodes) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Render the envelope as compact, id-addressed text the model edits against.
|
|
26
26
|
* Each line carries the node id so DocEdit/DocWrite patches can target it.
|
|
27
|
+
*
|
|
28
|
+
* The filetools binary has no pagination, so a dense document can project into
|
|
29
|
+
* a huge dump. We truncate the rendered view to a token budget and append an
|
|
30
|
+
* actionable notice instead of flooding the model context.
|
|
27
31
|
*/
|
|
28
|
-
function renderEnvelopeText(envelope) {
|
|
32
|
+
export function renderEnvelopeText(envelope, readonly) {
|
|
29
33
|
const header = `document ${envelope.source.path} [${envelope.source.type}, ${envelope.fidelity}, ` +
|
|
30
34
|
`${envelope.writable ? "writable" : "read-only"}]`;
|
|
31
35
|
const lines = [header, ""];
|
|
@@ -41,7 +45,15 @@ function renderEnvelopeText(envelope) {
|
|
|
41
45
|
}
|
|
42
46
|
};
|
|
43
47
|
walk(envelope.structure, 0);
|
|
44
|
-
|
|
48
|
+
const { text, droppedLines } = truncateRenderToTokenBudget(lines);
|
|
49
|
+
if (droppedLines === 0)
|
|
50
|
+
return text;
|
|
51
|
+
const hint = readonly
|
|
52
|
+
? "narrow to a smaller or more targeted file"
|
|
53
|
+
: "re-run with readonly:true for a smaller analysis-only view, or target a smaller file";
|
|
54
|
+
return (`${text}\n\n[Truncated: document exceeds the ~${DOCREAD_MAX_RENDER_TOKENS} token render budget; ` +
|
|
55
|
+
`${droppedLines} more node line${droppedLines === 1 ? "" : "s"} omitted. The ids shown are still valid for ` +
|
|
56
|
+
`DocEdit/DocWrite — ${hint}.]`);
|
|
45
57
|
}
|
|
46
58
|
function formatDocReadCall(args) {
|
|
47
59
|
const path = str(args?.path);
|
|
@@ -69,10 +81,10 @@ export function createDocReadToolDefinition(cwd, options) {
|
|
|
69
81
|
return {
|
|
70
82
|
name: "DocRead",
|
|
71
83
|
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.",
|
|
84
|
+
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. This is the canonical way to read and edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to parse or rewrite them — that loses the lossless id-map and corrupts formatting. Off by default; enabled with --enable-filetools.",
|
|
73
85
|
promptSnippet: "Extract a structured/binary document into editable, id-addressed JSON",
|
|
74
86
|
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.",
|
|
87
|
+
"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. Never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to parse or edit these formats — that loses the lossless id-map and corrupts the file.",
|
|
76
88
|
"DocEdit/DocWrite require a prior DocRead of the same file (the id-map is established by the extract).",
|
|
77
89
|
],
|
|
78
90
|
parameters: docReadSchema,
|
|
@@ -85,7 +97,7 @@ export function createDocReadToolDefinition(cwd, options) {
|
|
|
85
97
|
timeoutSecs: options?.timeoutSecs,
|
|
86
98
|
});
|
|
87
99
|
return {
|
|
88
|
-
content: [{ type: "text", text: renderEnvelopeText(envelope) }],
|
|
100
|
+
content: [{ type: "text", text: renderEnvelopeText(envelope, readonly) }],
|
|
89
101
|
details: {
|
|
90
102
|
type: envelope.source.type,
|
|
91
103
|
fidelity: envelope.fidelity,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docread.js","sourceRoot":"","sources":["../../../src/core/tools/docread.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 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"]}
|
|
1
|
+
{"version":3,"file":"docread.js","sourceRoot":"","sources":["../../../src/core/tools/docread.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 {\n\tDOCREAD_MAX_RENDER_TOKENS,\n\ttype DocNode,\n\ttype Envelope,\n\textractDocument,\n\ttruncateRenderToTokenBudget,\n} 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 *\n * The filetools binary has no pagination, so a dense document can project into\n * a huge dump. We truncate the rendered view to a token budget and append an\n * actionable notice instead of flooding the model context.\n */\nexport function renderEnvelopeText(envelope: Envelope, readonly: boolean | undefined): 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\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tif (droppedLines === 0) return text;\n\n\tconst hint = readonly\n\t\t? \"narrow to a smaller or more targeted file\"\n\t\t: \"re-run with readonly:true for a smaller analysis-only view, or target a smaller file\";\n\treturn (\n\t\t`${text}\\n\\n[Truncated: document exceeds the ~${DOCREAD_MAX_RENDER_TOKENS} token render budget; ` +\n\t\t`${droppedLines} more node line${droppedLines === 1 ? \"\" : \"s\"} omitted. The ids shown are still valid for ` +\n\t\t`DocEdit/DocWrite — ${hint}.]`\n\t);\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. This is the canonical way to read and edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to parse or rewrite them — that loses the lossless id-map and corrupts formatting. 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. Never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to parse or edit these formats — that loses the lossless id-map and corrupts the file.\",\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, readonly) }],\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"]}
|
|
@@ -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;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"]}
|
|
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). 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"]}
|
|
@@ -35,7 +35,7 @@ export function createDocWriteToolDefinition(cwd, options) {
|
|
|
35
35
|
return {
|
|
36
36
|
name: "DocWrite",
|
|
37
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.",
|
|
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). 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
39
|
promptSnippet: "Reconstruct a patched structured/binary document to a new path",
|
|
40
40
|
promptGuidelines: [
|
|
41
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.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docwrite.js","sourceRoot":"","sources":["../../../src/core/tools/docwrite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACvG,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,wFAAwF;KACrG,CAAC;IACF,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC;QAChB,WAAW,EAAE,yFAAyF;KACtG,CAAC;IACF,KAAK,EAAE,cAAc;CACrB,CAAC,CAAC;AAcH,SAAS,kBAAkB,CAAC,IAAoE,EAAU;IACzG,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,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,MAAM,UAAU,GACf,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACrG,IAAI,IAAI,GACP,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACpD,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC;QAClC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACvE,IAAI,GAAG,KAAK,SAAS;QAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;IAC3F,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,4BAA4B,CAC3C,GAAW,EACX,OAA6B,EAC4C;IACzE,OAAO;QACN,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,UAAU;QACjB,WAAW,EACV,
|
|
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"]}
|
|
@@ -19,6 +19,26 @@
|
|
|
19
19
|
import { type Static, Type } from "typebox";
|
|
20
20
|
/** Default timeout (seconds) for a single filetools invocation. */
|
|
21
21
|
export declare const FILETOOLS_DEFAULT_TIMEOUT_SECS = 30;
|
|
22
|
+
/**
|
|
23
|
+
* Soft token ceiling for a single DocRead render. The filetools binary has no
|
|
24
|
+
* pagination, so a dense file (e.g. a large spreadsheet) can project into a
|
|
25
|
+
* huge id-addressed dump that floods the model context and burns tokens. We
|
|
26
|
+
* cannot make the extract itself smaller without the binary's help, so DocRead
|
|
27
|
+
* truncates the rendered view to roughly this budget and tells the model how to
|
|
28
|
+
* narrow it (readonly projection, a smaller/targeted file, or direct edits).
|
|
29
|
+
*/
|
|
30
|
+
export declare const DOCREAD_MAX_RENDER_TOKENS = 10000;
|
|
31
|
+
/** Rough token estimate (chars/4), matching the agent's compaction heuristic. */
|
|
32
|
+
export declare function estimateTextTokens(text: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Truncate rendered envelope lines to roughly `maxTokens`, keeping whole lines.
|
|
35
|
+
* Returns the kept text plus how many lines were dropped (0 when nothing was
|
|
36
|
+
* truncated).
|
|
37
|
+
*/
|
|
38
|
+
export declare function truncateRenderToTokenBudget(lines: string[], maxTokens?: number): {
|
|
39
|
+
text: string;
|
|
40
|
+
droppedLines: number;
|
|
41
|
+
};
|
|
22
42
|
/** How faithfully a handler can reconstruct a file after edits. */
|
|
23
43
|
export type Fidelity = "lossless" | "in_place_text" | "read_only";
|
|
24
44
|
export interface DocSource {
|
|
@@ -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// 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// ============================================================================\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"]}
|
|
@@ -26,6 +26,41 @@ import { ensureTool } from "../../utils/tools-manager.js";
|
|
|
26
26
|
import { execCommand } from "../exec.js";
|
|
27
27
|
/** Default timeout (seconds) for a single filetools invocation. */
|
|
28
28
|
export const FILETOOLS_DEFAULT_TIMEOUT_SECS = 30;
|
|
29
|
+
/**
|
|
30
|
+
* Soft token ceiling for a single DocRead render. The filetools binary has no
|
|
31
|
+
* pagination, so a dense file (e.g. a large spreadsheet) can project into a
|
|
32
|
+
* huge id-addressed dump that floods the model context and burns tokens. We
|
|
33
|
+
* cannot make the extract itself smaller without the binary's help, so DocRead
|
|
34
|
+
* truncates the rendered view to roughly this budget and tells the model how to
|
|
35
|
+
* narrow it (readonly projection, a smaller/targeted file, or direct edits).
|
|
36
|
+
*/
|
|
37
|
+
export const DOCREAD_MAX_RENDER_TOKENS = 10000;
|
|
38
|
+
/** Rough token estimate (chars/4), matching the agent's compaction heuristic. */
|
|
39
|
+
export function estimateTextTokens(text) {
|
|
40
|
+
return Math.ceil(text.length / 4);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Truncate rendered envelope lines to roughly `maxTokens`, keeping whole lines.
|
|
44
|
+
* Returns the kept text plus how many lines were dropped (0 when nothing was
|
|
45
|
+
* truncated).
|
|
46
|
+
*/
|
|
47
|
+
export function truncateRenderToTokenBudget(lines, maxTokens = DOCREAD_MAX_RENDER_TOKENS) {
|
|
48
|
+
const full = lines.join("\n");
|
|
49
|
+
if (estimateTextTokens(full) <= maxTokens) {
|
|
50
|
+
return { text: full, droppedLines: 0 };
|
|
51
|
+
}
|
|
52
|
+
const budgetChars = maxTokens * 4;
|
|
53
|
+
const kept = [];
|
|
54
|
+
let used = 0;
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const next = used + line.length + 1; // + newline
|
|
57
|
+
if (next > budgetChars && kept.length > 0)
|
|
58
|
+
break;
|
|
59
|
+
kept.push(line);
|
|
60
|
+
used = next;
|
|
61
|
+
}
|
|
62
|
+
return { text: kept.join("\n"), droppedLines: lines.length - kept.length };
|
|
63
|
+
}
|
|
29
64
|
// ----------------------------------------------------------------------------
|
|
30
65
|
// TypeBox schema for the model-facing patch input (shared by DocEdit/DocWrite)
|
|
31
66
|
// ----------------------------------------------------------------------------
|