@kolisachint/hoocode-agent 0.4.92 → 0.4.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  7. package/dist/core/agent-frontmatter.js +3 -0
  8. package/dist/core/agent-frontmatter.js.map +1 -1
  9. package/dist/core/sdk.d.ts +6 -4
  10. package/dist/core/sdk.d.ts.map +1 -1
  11. package/dist/core/sdk.js +1 -1
  12. package/dist/core/sdk.js.map +1 -1
  13. package/dist/core/settings-manager.d.ts.map +1 -1
  14. package/dist/core/settings-manager.js.map +1 -1
  15. package/dist/core/tools/docedit.d.ts.map +1 -1
  16. package/dist/core/tools/docedit.js +1 -0
  17. package/dist/core/tools/docedit.js.map +1 -1
  18. package/dist/core/tools/docgrep.d.ts +28 -0
  19. package/dist/core/tools/docgrep.d.ts.map +1 -0
  20. package/dist/core/tools/docgrep.js +106 -0
  21. package/dist/core/tools/docgrep.js.map +1 -0
  22. package/dist/core/tools/docpeek.d.ts +25 -0
  23. package/dist/core/tools/docpeek.d.ts.map +1 -0
  24. package/dist/core/tools/docpeek.js +112 -0
  25. package/dist/core/tools/docpeek.js.map +1 -0
  26. package/dist/core/tools/docread.d.ts.map +1 -1
  27. package/dist/core/tools/docread.js +4 -15
  28. package/dist/core/tools/docread.js.map +1 -1
  29. package/dist/core/tools/docscan.d.ts +29 -0
  30. package/dist/core/tools/docscan.d.ts.map +1 -0
  31. package/dist/core/tools/docscan.js +110 -0
  32. package/dist/core/tools/docscan.js.map +1 -0
  33. package/dist/core/tools/docwrite.d.ts.map +1 -1
  34. package/dist/core/tools/docwrite.js +1 -0
  35. package/dist/core/tools/docwrite.js.map +1 -1
  36. package/dist/core/tools/filetools-shared.d.ts +66 -0
  37. package/dist/core/tools/filetools-shared.d.ts.map +1 -1
  38. package/dist/core/tools/filetools-shared.js +79 -0
  39. package/dist/core/tools/filetools-shared.js.map +1 -1
  40. package/dist/core/tools/index.d.ts +10 -1
  41. package/dist/core/tools/index.d.ts.map +1 -1
  42. package/dist/core/tools/index.js +27 -0
  43. package/dist/core/tools/index.js.map +1 -1
  44. package/dist/main.d.ts.map +1 -1
  45. package/dist/main.js +4 -3
  46. package/dist/main.js.map +1 -1
  47. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  48. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  49. package/examples/extensions/sandbox/package.json +1 -1
  50. package/examples/extensions/with-deps/package.json +1 -1
  51. package/package.json +4 -4
@@ -0,0 +1,106 @@
1
+ import { Text } from "@kolisachint/hoocode-tui";
2
+ import { Type } from "typebox";
3
+ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
4
+ import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
5
+ import { grepDocument, truncateRenderToTokenBudget } from "./filetools-shared.js";
6
+ import { resolveReadPath } from "./path-utils.js";
7
+ import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
8
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
9
+ const docGrepSchema = Type.Object({
10
+ path: Type.String({
11
+ description: "Path to the document to search (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.",
12
+ }),
13
+ pattern: Type.String({
14
+ description: "Literal substring to match per line (NOT a regex).",
15
+ }),
16
+ ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive matching. Default false." })),
17
+ limit: Type.Optional(Type.Number({ description: "Stop after N matches. Default: all matches." })),
18
+ });
19
+ /**
20
+ * Render grep matches as `#block_id:line :: "snippet"` lines. Each block id
21
+ * feeds straight back into DocPeek (to hydrate) or a DocEdit patch.
22
+ */
23
+ export function renderGrepView(view) {
24
+ const header = `grep ${JSON.stringify(view.pattern)} — ${view.returned} match${view.returned === 1 ? "" : "es"}`;
25
+ if (view.matches.length === 0)
26
+ return header;
27
+ const lines = [header, ""];
28
+ for (const m of view.matches) {
29
+ const flag = m.writable ? "" : " (read-only)";
30
+ lines.push(`#${m.block_id}:${m.line}${flag} :: ${JSON.stringify(m.snippet)}`);
31
+ }
32
+ const { text, droppedLines } = truncateRenderToTokenBudget(lines);
33
+ if (droppedLines === 0)
34
+ return text;
35
+ return (`${text}\n\n[Truncated: ${droppedLines} more match line${droppedLines === 1 ? "" : "s"} omitted; ` +
36
+ `re-run with a smaller limit or a more specific pattern.]`);
37
+ }
38
+ function formatDocGrepCall(args) {
39
+ const path = str(args?.path);
40
+ const pathDisplay = path === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg("toolOutput", "...");
41
+ let text = appTheme.fg("toolTitle", appTheme.bold("DocGrep "));
42
+ const pattern = str(args?.pattern);
43
+ if (pattern)
44
+ text += `${appTheme.fg("accent", JSON.stringify(pattern))} ${appTheme.fg("muted", "in")} `;
45
+ text += appTheme.fg("accent", pathDisplay);
46
+ if (args?.ignoreCase)
47
+ text += appTheme.fg("muted", " (i)");
48
+ return text;
49
+ }
50
+ function formatDocGrepResult(result, options, showImages) {
51
+ const output = getTextOutput(result, showImages).trim();
52
+ if (!output)
53
+ return "";
54
+ const lines = output.split("\n");
55
+ const maxLines = options.expanded ? lines.length : 15;
56
+ const displayLines = lines.slice(0, maxLines);
57
+ const remaining = lines.length - maxLines;
58
+ let text = `\n${displayLines.map((line) => appTheme.fg("toolOutput", line)).join("\n")}`;
59
+ if (remaining > 0) {
60
+ text += `${appTheme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
61
+ }
62
+ return text;
63
+ }
64
+ export function createDocGrepToolDefinition(cwd, options) {
65
+ return {
66
+ name: "DocGrep",
67
+ label: "DocGrep",
68
+ description: "Locate blocks in a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) by literal text, WITHOUT hydrating the whole document. Returns each match as an editable el_ node #id, line number, and snippet. Those el_ #ids target a DocEdit/DocWrite patch directly — DocGrep is the fast path from 'find text' to 'edit it' (far cheaper than a full DocRead). Off by default; enabled with --enable-filetools.",
69
+ promptSnippet: "Find blocks in a structured/binary document by literal text, without hydrating it",
70
+ promptGuidelines: [
71
+ "Use DocGrep to find where something is in a large structured/binary document instead of reading it all: it returns editable el_ node #ids + snippets for a literal substring match. Patch those el_ #ids straight with DocEdit/DocWrite.",
72
+ "DocGrep's el_ #ids are the edit id-space; they are NOT DocPeek's structural-path ids. To read more context around a match, DocScan/DocPeek by structural path instead.",
73
+ "pattern is a literal substring (not a regex); pass ignoreCase for case-insensitive matching.",
74
+ "Works across all supported formats including spreadsheet cell values (xlsx) and slide text (pptx); patch a matched el_ #id with DocEdit, or read more context with DocPeek.",
75
+ ],
76
+ parameters: docGrepSchema,
77
+ async execute(_toolCallId, { path, pattern, ignoreCase, limit }, signal) {
78
+ if (signal?.aborted)
79
+ throw new Error("Operation aborted");
80
+ const absolutePath = resolveReadPath(path, cwd);
81
+ const view = await grepDocument(absolutePath, pattern, cwd, signal, {
82
+ ignoreCase,
83
+ limit,
84
+ timeoutSecs: options?.timeoutSecs,
85
+ });
86
+ return {
87
+ content: [{ type: "text", text: renderGrepView(view) }],
88
+ details: { pattern: view.pattern, matches: view.returned },
89
+ };
90
+ },
91
+ renderCall(args, _theme, context) {
92
+ const text = context.lastComponent ?? new Text("", 0, 0);
93
+ text.setText(formatDocGrepCall(args));
94
+ return text;
95
+ },
96
+ renderResult(result, options, _theme, context) {
97
+ const text = context.lastComponent ?? new Text("", 0, 0);
98
+ text.setText(formatDocGrepResult(result, options, context.showImages));
99
+ return text;
100
+ },
101
+ };
102
+ }
103
+ export function createDocGrepTool(cwd, options) {
104
+ return wrapToolDefinition(createDocGrepToolDefinition(cwd, options));
105
+ }
106
+ //# sourceMappingURL=docgrep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docgrep.js","sourceRoot":"","sources":["../../../src/core/tools/docgrep.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 GrepView, grepDocument, truncateRenderToTokenBudget } from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docGrepSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription:\n\t\t\t\"Path to the document to search (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.\",\n\t}),\n\tpattern: Type.String({\n\t\tdescription: \"Literal substring to match per line (NOT a regex).\",\n\t}),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive matching. Default false.\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Stop after N matches. Default: all matches.\" })),\n});\n\nexport type DocGrepToolInput = Static<typeof docGrepSchema>;\n\nexport interface DocGrepToolDetails {\n\tpattern?: string;\n\tmatches?: number;\n}\n\nexport interface DocGrepToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\n/**\n * Render grep matches as `#block_id:line :: \"snippet\"` lines. Each block id\n * feeds straight back into DocPeek (to hydrate) or a DocEdit patch.\n */\nexport function renderGrepView(view: GrepView): string {\n\tconst header = `grep ${JSON.stringify(view.pattern)} — ${view.returned} match${view.returned === 1 ? \"\" : \"es\"}`;\n\tif (view.matches.length === 0) return header;\n\tconst lines: string[] = [header, \"\"];\n\tfor (const m of view.matches) {\n\t\tconst flag = m.writable ? \"\" : \" (read-only)\";\n\t\tlines.push(`#${m.block_id}:${m.line}${flag} :: ${JSON.stringify(m.snippet)}`);\n\t}\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tif (droppedLines === 0) return text;\n\treturn (\n\t\t`${text}\\n\\n[Truncated: ${droppedLines} more match line${droppedLines === 1 ? \"\" : \"s\"} omitted; ` +\n\t\t`re-run with a smaller limit or a more specific pattern.]`\n\t);\n}\n\nfunction formatDocGrepCall(args: { path?: string; pattern?: string; ignoreCase?: 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(\"DocGrep \"));\n\tconst pattern = str(args?.pattern);\n\tif (pattern) text += `${appTheme.fg(\"accent\", JSON.stringify(pattern))} ${appTheme.fg(\"muted\", \"in\")} `;\n\ttext += appTheme.fg(\"accent\", pathDisplay);\n\tif (args?.ignoreCase) text += appTheme.fg(\"muted\", \" (i)\");\n\treturn text;\n}\n\nfunction formatDocGrepResult(\n\tresult: { content: Array<{ type: string; text?: string }> },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, showImages).trim();\n\tif (!output) return \"\";\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 15;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\treturn text;\n}\n\nexport function createDocGrepToolDefinition(\n\tcwd: string,\n\toptions?: DocGrepToolOptions,\n): ToolDefinition<typeof docGrepSchema, DocGrepToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocGrep\",\n\t\tlabel: \"DocGrep\",\n\t\tdescription:\n\t\t\t\"Locate blocks in a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) by literal text, WITHOUT hydrating the whole document. Returns each match as an editable el_ node #id, line number, and snippet. Those el_ #ids target a DocEdit/DocWrite patch directly — DocGrep is the fast path from 'find text' to 'edit it' (far cheaper than a full DocRead). Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Find blocks in a structured/binary document by literal text, without hydrating it\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocGrep to find where something is in a large structured/binary document instead of reading it all: it returns editable el_ node #ids + snippets for a literal substring match. Patch those el_ #ids straight with DocEdit/DocWrite.\",\n\t\t\t\"DocGrep's el_ #ids are the edit id-space; they are NOT DocPeek's structural-path ids. To read more context around a match, DocScan/DocPeek by structural path instead.\",\n\t\t\t\"pattern is a literal substring (not a regex); pass ignoreCase for case-insensitive matching.\",\n\t\t\t\"Works across all supported formats including spreadsheet cell values (xlsx) and slide text (pptx); patch a matched el_ #id with DocEdit, or read more context with DocPeek.\",\n\t\t],\n\t\tparameters: docGrepSchema,\n\t\tasync execute(_toolCallId, { path, pattern, ignoreCase, limit }: DocGrepToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst view = await grepDocument(absolutePath, pattern, cwd, signal, {\n\t\t\t\tignoreCase,\n\t\t\t\tlimit,\n\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderGrepView(view) }],\n\t\t\t\tdetails: { pattern: view.pattern, matches: view.returned },\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(formatDocGrepCall(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(formatDocGrepResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocGrepTool(cwd: string, options?: DocGrepToolOptions): AgentTool<typeof docGrepSchema> {\n\treturn wrapToolDefinition(createDocGrepToolDefinition(cwd, options));\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import type { AgentTool } from "@kolisachint/hoocode-agent-core";
2
+ import { type Static, Type } from "typebox";
3
+ import type { ToolDefinition } from "../extensions/types.js";
4
+ import { type ReadView } from "./filetools-shared.js";
5
+ declare const docPeekSchema: Type.TObject<{
6
+ path: Type.TString;
7
+ id: Type.TOptional<Type.TArray<Type.TString>>;
8
+ offset: Type.TOptional<Type.TNumber>;
9
+ limit: Type.TOptional<Type.TNumber>;
10
+ }>;
11
+ export type DocPeekToolInput = Static<typeof docPeekSchema>;
12
+ export interface DocPeekToolDetails {
13
+ returned?: number;
14
+ total?: number;
15
+ }
16
+ export interface DocPeekToolOptions {
17
+ /** Timeout (seconds) for the filetools invocation. */
18
+ timeoutSecs?: number;
19
+ }
20
+ /** Render hydrated read blocks using the same id-addressed node dialect as DocRead. */
21
+ export declare function renderReadView(view: ReadView): string;
22
+ export declare function createDocPeekToolDefinition(cwd: string, options?: DocPeekToolOptions): ToolDefinition<typeof docPeekSchema, DocPeekToolDetails | undefined>;
23
+ export declare function createDocPeekTool(cwd: string, options?: DocPeekToolOptions): AgentTool<typeof docPeekSchema>;
24
+ export {};
25
+ //# sourceMappingURL=docpeek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docpeek.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docpeek.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,EACN,KAAK,QAAQ,EAIb,MAAM,uBAAuB,CAAC;AAK/B,QAAA,MAAM,aAAa;;;;;EAYjB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAmBrD;AAuCD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAuCtE;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\ttype ReadView,\n\treadDocumentBlocks,\n\trenderDocNodeLines,\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 docPeekSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to read (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.\",\n\t}),\n\tid: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Block id(s) to hydrate — use the EXACT ids returned by a prior DocScan (e.g. 'node[title:0]', 'paragraph[3]', 'page[1]'); do NOT hand-construct sub-ranges (an invented 'rows[0-2]' returns nothing — pass the exact 'rows[0-99]' DocScan emitted). These are DocScan's path ids, not DocGrep's el_ node ids. Omit to read a paginated slice of all blocks.\",\n\t\t}),\n\t),\n\toffset: Type.Optional(Type.Number({ description: \"Pagination start when no ids are given. Default 0.\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Pagination size when no ids are given. Default: all remaining.\" })),\n});\n\nexport type DocPeekToolInput = Static<typeof docPeekSchema>;\n\nexport interface DocPeekToolDetails {\n\treturned?: number;\n\ttotal?: number;\n}\n\nexport interface DocPeekToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\n/** Render hydrated read blocks using the same id-addressed node dialect as DocRead. */\nexport function renderReadView(view: ReadView): string {\n\tconst header = `document — ${view.returned}/${view.total} blocks (offset ${view.offset})`;\n\tconst lines: string[] = [header, \"\", ...renderDocNodeLines(view.nodes)];\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tconst seen = view.offset + view.returned;\n\tconst remaining = view.total - seen;\n\tconst parts = [text];\n\tif (droppedLines > 0) {\n\t\tparts.push(\n\t\t\t`\\n[Truncated: ${droppedLines} more node line${droppedLines === 1 ? \"\" : \"s\"} omitted; ` +\n\t\t\t\t`request fewer ids or a smaller limit.]`,\n\t\t);\n\t}\n\tif (remaining > 0) {\n\t\tparts.push(\n\t\t\t`\\n[${remaining} more block${remaining === 1 ? \"\" : \"s\"} not shown — re-run with offset:${seen} to continue.]`,\n\t\t);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction formatDocPeekCall(\n\targs: { path?: string; id?: string[]; offset?: number; limit?: number } | undefined,\n): 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(\"DocPeek \")) + appTheme.fg(\"accent\", pathDisplay);\n\tconst ids = Array.isArray(args?.id) ? args.id.length : 0;\n\tif (ids > 0) {\n\t\ttext += appTheme.fg(\"muted\", ` (${ids} id${ids === 1 ? \"\" : \"s\"})`);\n\t} else {\n\t\tconst range: string[] = [];\n\t\tif (args?.offset !== undefined) range.push(`offset ${args.offset}`);\n\t\tif (args?.limit !== undefined) range.push(`limit ${args.limit}`);\n\t\tif (range.length > 0) text += appTheme.fg(\"muted\", ` (${range.join(\", \")})`);\n\t}\n\treturn text;\n}\n\nfunction formatDocPeekResult(\n\tresult: { content: Array<{ type: string; text?: string }> },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, showImages).trim();\n\tif (!output) return \"\";\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 15;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\treturn text;\n}\n\nexport function createDocPeekToolDefinition(\n\tcwd: string,\n\toptions?: DocPeekToolOptions,\n): ToolDefinition<typeof docPeekSchema, DocPeekToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocPeek\",\n\t\tlabel: \"DocPeek\",\n\t\tdescription:\n\t\t\t\"Hydrate specific blocks of a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) by structural-path id — the read half of the token-sensitive loop. Pass the path ids from a prior DocScan (e.g. node[title:0]) to read only those blocks (or omit ids to page through all with offset/limit), instead of dumping the whole document with DocRead. The hydrated nodes carry the editable el_ #ids, which you then patch with DocEdit/DocWrite. Read-only. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Read only specific blocks of a structured/binary document by id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocPeek to read just the blocks you need (by structural-path id from DocScan, e.g. node[title:0]) instead of a full DocRead — it is much cheaper in tokens. Omit ids and use offset/limit to page through a large document.\",\n\t\t\t\"Pass the EXACT ids DocScan returned; DocPeek does not accept invented sub-ranges (e.g. rows[0-2] when DocScan emitted rows[0-99]).\",\n\t\t\t\"DocPeek takes DocScan's path ids, NOT DocGrep's el_ node ids. The nodes it returns carry the editable el_ #ids — patch those with DocEdit/DocWrite (which auto-extract). If you already have an el_ id from DocGrep, go straight to DocEdit; you do not need DocPeek.\",\n\t\t\t\"Works across all supported formats including spreadsheet cells (hydrating a sheet's rows[a-b] block returns the cell values) and slide text (pptx).\",\n\t\t],\n\t\tparameters: docPeekSchema,\n\t\tasync execute(_toolCallId, { path, id, offset, limit }: DocPeekToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst view = await readDocumentBlocks(absolutePath, cwd, signal, {\n\t\t\t\tids: id,\n\t\t\t\toffset,\n\t\t\t\tlimit,\n\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderReadView(view) }],\n\t\t\t\tdetails: { returned: view.returned, total: view.total },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocPeekCall(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(formatDocPeekResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocPeekTool(cwd: string, options?: DocPeekToolOptions): AgentTool<typeof docPeekSchema> {\n\treturn wrapToolDefinition(createDocPeekToolDefinition(cwd, options));\n}\n"]}
@@ -0,0 +1,112 @@
1
+ import { Text } from "@kolisachint/hoocode-tui";
2
+ import { Type } from "typebox";
3
+ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
4
+ import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
5
+ import { readDocumentBlocks, renderDocNodeLines, truncateRenderToTokenBudget, } from "./filetools-shared.js";
6
+ import { resolveReadPath } from "./path-utils.js";
7
+ import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
8
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
9
+ const docPeekSchema = Type.Object({
10
+ path: Type.String({
11
+ description: "Path to the document to read (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.",
12
+ }),
13
+ id: Type.Optional(Type.Array(Type.String(), {
14
+ description: "Block id(s) to hydrate — use the EXACT ids returned by a prior DocScan (e.g. 'node[title:0]', 'paragraph[3]', 'page[1]'); do NOT hand-construct sub-ranges (an invented 'rows[0-2]' returns nothing — pass the exact 'rows[0-99]' DocScan emitted). These are DocScan's path ids, not DocGrep's el_ node ids. Omit to read a paginated slice of all blocks.",
15
+ })),
16
+ offset: Type.Optional(Type.Number({ description: "Pagination start when no ids are given. Default 0." })),
17
+ limit: Type.Optional(Type.Number({ description: "Pagination size when no ids are given. Default: all remaining." })),
18
+ });
19
+ /** Render hydrated read blocks using the same id-addressed node dialect as DocRead. */
20
+ export function renderReadView(view) {
21
+ const header = `document — ${view.returned}/${view.total} blocks (offset ${view.offset})`;
22
+ const lines = [header, "", ...renderDocNodeLines(view.nodes)];
23
+ const { text, droppedLines } = truncateRenderToTokenBudget(lines);
24
+ const seen = view.offset + view.returned;
25
+ const remaining = view.total - seen;
26
+ const parts = [text];
27
+ if (droppedLines > 0) {
28
+ parts.push(`\n[Truncated: ${droppedLines} more node line${droppedLines === 1 ? "" : "s"} omitted; ` +
29
+ `request fewer ids or a smaller limit.]`);
30
+ }
31
+ if (remaining > 0) {
32
+ parts.push(`\n[${remaining} more block${remaining === 1 ? "" : "s"} not shown — re-run with offset:${seen} to continue.]`);
33
+ }
34
+ return parts.join("\n");
35
+ }
36
+ function formatDocPeekCall(args) {
37
+ const path = str(args?.path);
38
+ const pathDisplay = path === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg("toolOutput", "...");
39
+ let text = appTheme.fg("toolTitle", appTheme.bold("DocPeek ")) + appTheme.fg("accent", pathDisplay);
40
+ const ids = Array.isArray(args?.id) ? args.id.length : 0;
41
+ if (ids > 0) {
42
+ text += appTheme.fg("muted", ` (${ids} id${ids === 1 ? "" : "s"})`);
43
+ }
44
+ else {
45
+ const range = [];
46
+ if (args?.offset !== undefined)
47
+ range.push(`offset ${args.offset}`);
48
+ if (args?.limit !== undefined)
49
+ range.push(`limit ${args.limit}`);
50
+ if (range.length > 0)
51
+ text += appTheme.fg("muted", ` (${range.join(", ")})`);
52
+ }
53
+ return text;
54
+ }
55
+ function formatDocPeekResult(result, options, showImages) {
56
+ const output = getTextOutput(result, showImages).trim();
57
+ if (!output)
58
+ return "";
59
+ const lines = output.split("\n");
60
+ const maxLines = options.expanded ? lines.length : 15;
61
+ const displayLines = lines.slice(0, maxLines);
62
+ const remaining = lines.length - maxLines;
63
+ let text = `\n${displayLines.map((line) => appTheme.fg("toolOutput", line)).join("\n")}`;
64
+ if (remaining > 0) {
65
+ text += `${appTheme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
66
+ }
67
+ return text;
68
+ }
69
+ export function createDocPeekToolDefinition(cwd, options) {
70
+ return {
71
+ name: "DocPeek",
72
+ label: "DocPeek",
73
+ description: "Hydrate specific blocks of a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) by structural-path id — the read half of the token-sensitive loop. Pass the path ids from a prior DocScan (e.g. node[title:0]) to read only those blocks (or omit ids to page through all with offset/limit), instead of dumping the whole document with DocRead. The hydrated nodes carry the editable el_ #ids, which you then patch with DocEdit/DocWrite. Read-only. Off by default; enabled with --enable-filetools.",
74
+ promptSnippet: "Read only specific blocks of a structured/binary document by id",
75
+ promptGuidelines: [
76
+ "Use DocPeek to read just the blocks you need (by structural-path id from DocScan, e.g. node[title:0]) instead of a full DocRead — it is much cheaper in tokens. Omit ids and use offset/limit to page through a large document.",
77
+ "Pass the EXACT ids DocScan returned; DocPeek does not accept invented sub-ranges (e.g. rows[0-2] when DocScan emitted rows[0-99]).",
78
+ "DocPeek takes DocScan's path ids, NOT DocGrep's el_ node ids. The nodes it returns carry the editable el_ #ids — patch those with DocEdit/DocWrite (which auto-extract). If you already have an el_ id from DocGrep, go straight to DocEdit; you do not need DocPeek.",
79
+ "Works across all supported formats including spreadsheet cells (hydrating a sheet's rows[a-b] block returns the cell values) and slide text (pptx).",
80
+ ],
81
+ parameters: docPeekSchema,
82
+ async execute(_toolCallId, { path, id, offset, limit }, signal) {
83
+ if (signal?.aborted)
84
+ throw new Error("Operation aborted");
85
+ const absolutePath = resolveReadPath(path, cwd);
86
+ const view = await readDocumentBlocks(absolutePath, cwd, signal, {
87
+ ids: id,
88
+ offset,
89
+ limit,
90
+ timeoutSecs: options?.timeoutSecs,
91
+ });
92
+ return {
93
+ content: [{ type: "text", text: renderReadView(view) }],
94
+ details: { returned: view.returned, total: view.total },
95
+ };
96
+ },
97
+ renderCall(args, _theme, context) {
98
+ const text = context.lastComponent ?? new Text("", 0, 0);
99
+ text.setText(formatDocPeekCall(args));
100
+ return text;
101
+ },
102
+ renderResult(result, options, _theme, context) {
103
+ const text = context.lastComponent ?? new Text("", 0, 0);
104
+ text.setText(formatDocPeekResult(result, options, context.showImages));
105
+ return text;
106
+ },
107
+ };
108
+ }
109
+ export function createDocPeekTool(cwd, options) {
110
+ return wrapToolDefinition(createDocPeekToolDefinition(cwd, options));
111
+ }
112
+ //# sourceMappingURL=docpeek.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docpeek.js","sourceRoot":"","sources":["../../../src/core/tools/docpeek.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\ttype ReadView,\n\treadDocumentBlocks,\n\trenderDocNodeLines,\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 docPeekSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to read (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.\",\n\t}),\n\tid: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Block id(s) to hydrate — use the EXACT ids returned by a prior DocScan (e.g. 'node[title:0]', 'paragraph[3]', 'page[1]'); do NOT hand-construct sub-ranges (an invented 'rows[0-2]' returns nothing — pass the exact 'rows[0-99]' DocScan emitted). These are DocScan's path ids, not DocGrep's el_ node ids. Omit to read a paginated slice of all blocks.\",\n\t\t}),\n\t),\n\toffset: Type.Optional(Type.Number({ description: \"Pagination start when no ids are given. Default 0.\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Pagination size when no ids are given. Default: all remaining.\" })),\n});\n\nexport type DocPeekToolInput = Static<typeof docPeekSchema>;\n\nexport interface DocPeekToolDetails {\n\treturned?: number;\n\ttotal?: number;\n}\n\nexport interface DocPeekToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\n/** Render hydrated read blocks using the same id-addressed node dialect as DocRead. */\nexport function renderReadView(view: ReadView): string {\n\tconst header = `document — ${view.returned}/${view.total} blocks (offset ${view.offset})`;\n\tconst lines: string[] = [header, \"\", ...renderDocNodeLines(view.nodes)];\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tconst seen = view.offset + view.returned;\n\tconst remaining = view.total - seen;\n\tconst parts = [text];\n\tif (droppedLines > 0) {\n\t\tparts.push(\n\t\t\t`\\n[Truncated: ${droppedLines} more node line${droppedLines === 1 ? \"\" : \"s\"} omitted; ` +\n\t\t\t\t`request fewer ids or a smaller limit.]`,\n\t\t);\n\t}\n\tif (remaining > 0) {\n\t\tparts.push(\n\t\t\t`\\n[${remaining} more block${remaining === 1 ? \"\" : \"s\"} not shown — re-run with offset:${seen} to continue.]`,\n\t\t);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction formatDocPeekCall(\n\targs: { path?: string; id?: string[]; offset?: number; limit?: number } | undefined,\n): 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(\"DocPeek \")) + appTheme.fg(\"accent\", pathDisplay);\n\tconst ids = Array.isArray(args?.id) ? args.id.length : 0;\n\tif (ids > 0) {\n\t\ttext += appTheme.fg(\"muted\", ` (${ids} id${ids === 1 ? \"\" : \"s\"})`);\n\t} else {\n\t\tconst range: string[] = [];\n\t\tif (args?.offset !== undefined) range.push(`offset ${args.offset}`);\n\t\tif (args?.limit !== undefined) range.push(`limit ${args.limit}`);\n\t\tif (range.length > 0) text += appTheme.fg(\"muted\", ` (${range.join(\", \")})`);\n\t}\n\treturn text;\n}\n\nfunction formatDocPeekResult(\n\tresult: { content: Array<{ type: string; text?: string }> },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, showImages).trim();\n\tif (!output) return \"\";\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 15;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\treturn text;\n}\n\nexport function createDocPeekToolDefinition(\n\tcwd: string,\n\toptions?: DocPeekToolOptions,\n): ToolDefinition<typeof docPeekSchema, DocPeekToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocPeek\",\n\t\tlabel: \"DocPeek\",\n\t\tdescription:\n\t\t\t\"Hydrate specific blocks of a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) by structural-path id — the read half of the token-sensitive loop. Pass the path ids from a prior DocScan (e.g. node[title:0]) to read only those blocks (or omit ids to page through all with offset/limit), instead of dumping the whole document with DocRead. The hydrated nodes carry the editable el_ #ids, which you then patch with DocEdit/DocWrite. Read-only. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Read only specific blocks of a structured/binary document by id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocPeek to read just the blocks you need (by structural-path id from DocScan, e.g. node[title:0]) instead of a full DocRead — it is much cheaper in tokens. Omit ids and use offset/limit to page through a large document.\",\n\t\t\t\"Pass the EXACT ids DocScan returned; DocPeek does not accept invented sub-ranges (e.g. rows[0-2] when DocScan emitted rows[0-99]).\",\n\t\t\t\"DocPeek takes DocScan's path ids, NOT DocGrep's el_ node ids. The nodes it returns carry the editable el_ #ids — patch those with DocEdit/DocWrite (which auto-extract). If you already have an el_ id from DocGrep, go straight to DocEdit; you do not need DocPeek.\",\n\t\t\t\"Works across all supported formats including spreadsheet cells (hydrating a sheet's rows[a-b] block returns the cell values) and slide text (pptx).\",\n\t\t],\n\t\tparameters: docPeekSchema,\n\t\tasync execute(_toolCallId, { path, id, offset, limit }: DocPeekToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst view = await readDocumentBlocks(absolutePath, cwd, signal, {\n\t\t\t\tids: id,\n\t\t\t\toffset,\n\t\t\t\tlimit,\n\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderReadView(view) }],\n\t\t\t\tdetails: { returned: view.returned, total: view.total },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocPeekCall(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(formatDocPeekResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocPeekTool(cwd: string, options?: DocPeekToolOptions): AgentTool<typeof docPeekSchema> {\n\treturn wrapToolDefinition(createDocPeekToolDefinition(cwd, options));\n}\n"]}
@@ -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;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"]}
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,EAIb,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,CAiB5F;AA6BD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAyCtE;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\trenderDocNodeLines,\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, \"\", ...renderDocNodeLines(envelope.structure)];\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\"Flow: scan first, edit second. To understand a document, start with DocRead readonly:true an analysis-only projection that strips node ids and is much cheaper in tokens. Only do a full (writable) DocRead when you actually intend to edit, since that view carries the whole id-map and is token-heavy.\",\n\t\t\t\"Read once, then edit. A writable DocRead establishes the id-map; DocEdit/DocWrite re-extract on their own, so do not re-run DocRead between edits. If a patch targets stale ids the edit fails and returns the current structure with fresh ids to re-issue against.\",\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 { DOCREAD_MAX_RENDER_TOKENS, extractDocument, truncateRenderToTokenBudget, } from "./filetools-shared.js";
5
+ import { DOCREAD_MAX_RENDER_TOKENS, extractDocument, renderDocNodeLines, 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";
@@ -32,19 +32,7 @@ function countNodes(nodes) {
32
32
  export function renderEnvelopeText(envelope, readonly) {
33
33
  const header = `document ${envelope.source.path} [${envelope.source.type}, ${envelope.fidelity}, ` +
34
34
  `${envelope.writable ? "writable" : "read-only"}]`;
35
- const lines = [header, ""];
36
- const walk = (nodes, depth) => {
37
- for (const node of nodes) {
38
- const indent = " ".repeat(depth);
39
- const idPart = node.id ? `#${node.id} ` : "";
40
- const attrs = node.attrs?.length ? ` ${node.attrs.map((a) => `${a.name}="${a.value}"`).join(" ")}` : "";
41
- const text = node.text !== undefined ? ` :: ${JSON.stringify(node.text)}` : "";
42
- lines.push(`${indent}${idPart}<${node.tag}${attrs}>${text}`);
43
- if (node.children?.length)
44
- walk(node.children, depth + 1);
45
- }
46
- };
47
- walk(envelope.structure, 0);
35
+ const lines = [header, "", ...renderDocNodeLines(envelope.structure)];
48
36
  const { text, droppedLines } = truncateRenderToTokenBudget(lines);
49
37
  if (droppedLines === 0)
50
38
  return text;
@@ -85,7 +73,8 @@ export function createDocReadToolDefinition(cwd, options) {
85
73
  promptSnippet: "Extract a structured/binary document into editable, id-addressed JSON",
86
74
  promptGuidelines: [
87
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. 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.",
88
- "DocEdit/DocWrite require a prior DocRead of the same file (the id-map is established by the extract).",
76
+ "Flow: scan first, edit second. To understand a document, start with DocRead readonly:true an analysis-only projection that strips node ids and is much cheaper in tokens. Only do a full (writable) DocRead when you actually intend to edit, since that view carries the whole id-map and is token-heavy.",
77
+ "Read once, then edit. A writable DocRead establishes the id-map; DocEdit/DocWrite re-extract on their own, so do not re-run DocRead between edits. If a patch targets stale ids the edit fails and returns the current structure with fresh ids to re-issue against.",
89
78
  ],
90
79
  parameters: docReadSchema,
91
80
  async execute(_toolCallId, { path, readonly }, signal) {
@@ -1 +1 @@
1
- {"version":3,"file":"docread.js","sourceRoot":"","sources":["../../../src/core/tools/docread.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AACjF,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EACN,yBAAyB,EAGzB,eAAe,EACf,2BAA2B,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EACV,sGAAsG;KACvG,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,QAAQ,CACtB,IAAI,CAAC,OAAO,CAAC;QACZ,WAAW,EACV,0JAA0J;KAC3J,CAAC,CACF;CACD,CAAC,CAAC;AAgBH,SAAS,UAAU,CAAC,KAAgB,EAAU;IAC7C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAkB,EAAE,QAA6B,EAAU;IAC7F,MAAM,MAAM,GACX,YAAY,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,QAAQ,IAAI;QACnF,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC;IACpD,MAAM,KAAK,GAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,CAAC,KAAgB,EAAE,KAAa,EAAQ,EAAE,CAAC;QACvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAClC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxG,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/E,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC;YAC7D,IAAI,IAAI,CAAC,QAAQ,EAAE,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC3D,CAAC;IAAA,CACD,CAAC;IACF,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAE5B,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,2BAA2B,CAAC,KAAK,CAAC,CAAC;IAClE,IAAI,YAAY,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpC,MAAM,IAAI,GAAG,QAAQ;QACpB,CAAC,CAAC,2CAA2C;QAC7C,CAAC,CAAC,sFAAsF,CAAC;IAC1F,OAAO,CACN,GAAG,IAAI,yCAAyC,yBAAyB,wBAAwB;QACjG,GAAG,YAAY,kBAAkB,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,8CAA8C;QAC5G,wBAAsB,IAAI,IAAI,CAC9B,CAAC;AAAA,CACF;AAED,SAAS,iBAAiB,CAAC,IAAuD,EAAU;IAC3F,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7B,MAAM,WAAW,GAChB,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACxG,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACpG,IAAI,IAAI,EAAE,QAAQ;QAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,mBAAmB,CAC3B,MAAyF,EACzF,OAAgC,EAChC,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAa,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/D,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACtD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;IAC1C,IAAI,IAAI,GAAG,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACzF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QACnB,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;IACnH,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,2BAA2B,CAC1C,GAAW,EACX,OAA4B,EAC2C;IACvE,OAAO;QACN,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS;QAChB,WAAW,EACV,yhBAAuhB;QACxhB,aAAa,EAAE,uEAAuE;QACtF,gBAAgB,EAAE;YACjB,+UAA6U;YAC7U,uGAAuG;SACvG;QACD,UAAU,EAAE,aAAa;QACzB,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAoB,EAAE,MAAoB,EAAE;YACtF,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC1D,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE;gBACjE,QAAQ;gBACR,WAAW,EAAE,OAAO,EAAE,WAAW;aACjC,CAAC,CAAC;YACH,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAClF,OAAO,EAAE;oBACR,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI;oBAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;oBAC3B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;oBAC3B,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;iBACzC;aACD,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;YACjC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;YACtC,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;YAC9C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,MAAa,EAAE,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAC9E,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAA4B,EAAmC;IAC7G,OAAO,kBAAkB,CAAC,2BAA2B,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CACrE","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport {\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
+ {"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\trenderDocNodeLines,\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, \"\", ...renderDocNodeLines(envelope.structure)];\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\"Flow: scan first, edit second. To understand a document, start with DocRead readonly:true — an analysis-only projection that strips node ids and is much cheaper in tokens. Only do a full (writable) DocRead when you actually intend to edit, since that view carries the whole id-map and is token-heavy.\",\n\t\t\t\"Read once, then edit. A writable DocRead establishes the id-map; DocEdit/DocWrite re-extract on their own, so do not re-run DocRead between edits. If a patch targets stale ids the edit fails and returns the current structure with fresh ids to re-issue against.\",\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"]}
@@ -0,0 +1,29 @@
1
+ import type { AgentTool } from "@kolisachint/hoocode-agent-core";
2
+ import { type Static, Type } from "typebox";
3
+ import type { ToolDefinition } from "../extensions/types.js";
4
+ import { type ScanView } from "./filetools-shared.js";
5
+ declare const docScanSchema: Type.TObject<{
6
+ path: Type.TString;
7
+ offset: Type.TOptional<Type.TNumber>;
8
+ limit: Type.TOptional<Type.TNumber>;
9
+ }>;
10
+ export type DocScanToolInput = Static<typeof docScanSchema>;
11
+ export interface DocScanToolDetails {
12
+ fileType?: string;
13
+ returned?: number;
14
+ total?: number;
15
+ }
16
+ export interface DocScanToolOptions {
17
+ /** Timeout (seconds) for the filetools invocation. */
18
+ timeoutSecs?: number;
19
+ }
20
+ /**
21
+ * Render a scan manifest as compact, id-addressed preview lines. Each line
22
+ * carries the block id (feeds DocPeek/DocGrep) plus a section label, token
23
+ * estimate, and a short preview — structure without hydrating full content.
24
+ */
25
+ export declare function renderScanView(view: ScanView): string;
26
+ export declare function createDocScanToolDefinition(cwd: string, options?: DocScanToolOptions): ToolDefinition<typeof docScanSchema, DocScanToolDetails | undefined>;
27
+ export declare function createDocScanTool(cwd: string, options?: DocScanToolOptions): AgentTool<typeof docScanSchema>;
28
+ export {};
29
+ //# sourceMappingURL=docscan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docscan.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docscan.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,EAAE,KAAK,QAAQ,EAA6C,MAAM,uBAAuB,CAAC;AAKjG,QAAA,MAAM,aAAa;;;;EAMjB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CA2BrD;AAgCD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAoCtE;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 { type ScanView, scanDocument, truncateRenderToTokenBudget } from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docScanSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to scan (relative or absolute). XML, drawio, OOXML (docx/xlsx/pptx), or PDF.\",\n\t}),\n\toffset: Type.Optional(Type.Number({ description: \"Skip the first N blocks (pagination). Default 0.\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of blocks to return. Default: all remaining.\" })),\n});\n\nexport type DocScanToolInput = Static<typeof docScanSchema>;\n\nexport interface DocScanToolDetails {\n\tfileType?: string;\n\treturned?: number;\n\ttotal?: number;\n}\n\nexport interface DocScanToolOptions {\n\t/** Timeout (seconds) for the filetools invocation. */\n\ttimeoutSecs?: number;\n}\n\n/**\n * Render a scan manifest as compact, id-addressed preview lines. Each line\n * carries the block id (feeds DocPeek/DocGrep) plus a section label, token\n * estimate, and a short preview — structure without hydrating full content.\n */\nexport function renderScanView(view: ScanView): string {\n\tconst header =\n\t\t`document ${view.file_type} — ${view.returned}/${view.total} blocks ` +\n\t\t`(offset ${view.offset}, ~${view.total_tokens} tok total)`;\n\tconst lines: string[] = [header, \"\"];\n\tfor (const block of view.blocks) {\n\t\tconst section = block.section_name ? ` ${block.section_name}#${block.section_number}` : \"\";\n\t\tconst preview = block.preview ? ` :: ${JSON.stringify(block.preview)}` : \"\";\n\t\tlines.push(`#${block.id} [${block.block_type}]${section} ~${block.token_estimate}tok${preview}`);\n\t}\n\n\tconst { text, droppedLines } = truncateRenderToTokenBudget(lines);\n\tconst seen = view.offset + view.returned;\n\tconst remaining = view.total - seen;\n\tconst parts = [text];\n\tif (droppedLines > 0) {\n\t\tparts.push(\n\t\t\t`\\n[Truncated: ${droppedLines} more block line${droppedLines === 1 ? \"\" : \"s\"} omitted from this render. ` +\n\t\t\t\t`Re-run with a tighter limit, or DocGrep/DocPeek to target.]`,\n\t\t);\n\t}\n\tif (remaining > 0) {\n\t\tparts.push(\n\t\t\t`\\n[${remaining} more block${remaining === 1 ? \"\" : \"s\"} not shown — re-run with offset:${seen} to continue.]`,\n\t\t);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nfunction formatDocScanCall(args: { path?: string; offset?: number; limit?: number } | undefined): string {\n\tconst path = str(args?.path);\n\tconst pathDisplay =\n\t\tpath === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg(\"toolOutput\", \"...\");\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"DocScan \")) + appTheme.fg(\"accent\", pathDisplay);\n\tconst range: string[] = [];\n\tif (args?.offset !== undefined) range.push(`offset ${args.offset}`);\n\tif (args?.limit !== undefined) range.push(`limit ${args.limit}`);\n\tif (range.length > 0) text += appTheme.fg(\"muted\", ` (${range.join(\", \")})`);\n\treturn text;\n}\n\nfunction formatDocScanResult(\n\tresult: { content: Array<{ type: string; text?: string }> },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, showImages).trim();\n\tif (!output) return \"\";\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 15;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\treturn text;\n}\n\nexport function createDocScanToolDefinition(\n\tcwd: string,\n\toptions?: DocScanToolOptions,\n): ToolDefinition<typeof docScanSchema, DocScanToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocScan\",\n\t\tlabel: \"DocScan\",\n\t\tdescription:\n\t\t\t\"Cheaply outline a structured/binary document (XML, drawio, docx/xlsx/pptx, PDF) without hydrating it: returns a paginated manifest of blocks, each with a structural-path id, type, section label, token estimate, and a short preview. This is the cheap first step of the token-sensitive loop — far smaller than a full DocRead. Pass the path ids it returns to DocPeek to hydrate just those blocks (the hydrated nodes carry the editable el_ #ids for DocEdit). Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Outline a structured/binary document into a cheap, paginated block manifest\",\n\t\tpromptGuidelines: [\n\t\t\t\"Start here for large structured/binary documents instead of a full DocRead: DocScan returns a paginated outline (structural-path block ids + previews) that is much cheaper in tokens. Page through it with offset/limit.\",\n\t\t\t\"Flow: DocScan to see structure → DocPeek the path ids you want (hydrates them and reveals their editable el_ #ids) → DocEdit. Or jump straight in with DocGrep, which finds blocks by text and returns editable el_ #ids directly.\",\n\t\t],\n\t\tparameters: docScanSchema,\n\t\tasync execute(_toolCallId, { path, offset, limit }: DocScanToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\tconst view = await scanDocument(absolutePath, cwd, signal, {\n\t\t\t\toffset,\n\t\t\t\tlimit,\n\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderScanView(view) }],\n\t\t\t\tdetails: { fileType: view.file_type, returned: view.returned, total: view.total },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocScanCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocScanResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocScanTool(cwd: string, options?: DocScanToolOptions): AgentTool<typeof docScanSchema> {\n\treturn wrapToolDefinition(createDocScanToolDefinition(cwd, options));\n}\n"]}