@mariozechner/pi-coding-agent 0.38.0 → 0.39.1

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 (109) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -0
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +5 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +23 -0
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +62 -34
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts +6 -0
  12. package/dist/core/bash-executor.d.ts.map +1 -1
  13. package/dist/core/bash-executor.js +77 -0
  14. package/dist/core/bash-executor.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +1 -1
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js.map +1 -1
  18. package/dist/core/extensions/runner.d.ts +4 -3
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +44 -7
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +39 -3
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/index.d.ts +1 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/sdk.d.ts +5 -2
  30. package/dist/core/sdk.d.ts.map +1 -1
  31. package/dist/core/sdk.js +20 -12
  32. package/dist/core/sdk.js.map +1 -1
  33. package/dist/core/system-prompt.d.ts.map +1 -1
  34. package/dist/core/system-prompt.js +1 -5
  35. package/dist/core/system-prompt.js.map +1 -1
  36. package/dist/core/tools/bash.d.ts +25 -1
  37. package/dist/core/tools/bash.d.ts.map +1 -1
  38. package/dist/core/tools/bash.js +103 -73
  39. package/dist/core/tools/bash.js.map +1 -1
  40. package/dist/core/tools/edit.d.ts +17 -1
  41. package/dist/core/tools/edit.d.ts.map +1 -1
  42. package/dist/core/tools/edit.js +12 -5
  43. package/dist/core/tools/edit.js.map +1 -1
  44. package/dist/core/tools/find.d.ts +18 -1
  45. package/dist/core/tools/find.d.ts.map +1 -1
  46. package/dist/core/tools/find.js +68 -18
  47. package/dist/core/tools/find.js.map +1 -1
  48. package/dist/core/tools/grep.d.ts +15 -1
  49. package/dist/core/tools/grep.d.ts.map +1 -1
  50. package/dist/core/tools/grep.js +22 -10
  51. package/dist/core/tools/grep.js.map +1 -1
  52. package/dist/core/tools/index.d.ts +7 -7
  53. package/dist/core/tools/index.d.ts.map +1 -1
  54. package/dist/core/tools/index.js +1 -1
  55. package/dist/core/tools/index.js.map +1 -1
  56. package/dist/core/tools/ls.d.ts +21 -1
  57. package/dist/core/tools/ls.d.ts.map +1 -1
  58. package/dist/core/tools/ls.js +80 -72
  59. package/dist/core/tools/ls.js.map +1 -1
  60. package/dist/core/tools/read.d.ts +14 -0
  61. package/dist/core/tools/read.d.ts.map +1 -1
  62. package/dist/core/tools/read.js +12 -5
  63. package/dist/core/tools/read.js.map +1 -1
  64. package/dist/core/tools/write.d.ts +15 -1
  65. package/dist/core/tools/write.d.ts.map +1 -1
  66. package/dist/core/tools/write.js +9 -4
  67. package/dist/core/tools/write.js.map +1 -1
  68. package/dist/index.d.ts +3 -3
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +11 -1
  74. package/dist/main.js.map +1 -1
  75. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/assistant-message.js +7 -3
  77. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  78. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  79. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  80. package/dist/modes/interactive/components/tool-execution.js +50 -23
  81. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  82. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  83. package/dist/modes/interactive/interactive-mode.js +153 -46
  84. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  85. package/dist/modes/interactive/theme/theme.d.ts +7 -0
  86. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  87. package/dist/modes/interactive/theme/theme.js +40 -0
  88. package/dist/modes/interactive/theme/theme.js.map +1 -1
  89. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  90. package/dist/modes/rpc/rpc-mode.js +10 -0
  91. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  92. package/dist/utils/clipboard.d.ts.map +1 -1
  93. package/dist/utils/clipboard.js +35 -7
  94. package/dist/utils/clipboard.js.map +1 -1
  95. package/docs/extensions.md +101 -6
  96. package/docs/sdk.md +3 -3
  97. package/examples/extensions/README.md +2 -0
  98. package/examples/extensions/claude-rules.ts +5 -2
  99. package/examples/extensions/interactive-shell.ts +196 -0
  100. package/examples/extensions/mac-system-theme.ts +45 -0
  101. package/examples/extensions/overlay-test.ts +145 -0
  102. package/examples/extensions/pirate.ts +7 -4
  103. package/examples/extensions/preset.ts +2 -2
  104. package/examples/extensions/ssh.ts +220 -0
  105. package/examples/extensions/tool-override.ts +143 -0
  106. package/examples/extensions/with-deps/package-lock.json +2 -2
  107. package/examples/extensions/with-deps/package.json +1 -1
  108. package/examples/sdk/04-skills.ts +4 -1
  109. package/package.json +5 -5
@@ -8,7 +8,27 @@ export interface LsToolDetails {
8
8
  truncation?: TruncationResult;
9
9
  entryLimitReached?: number;
10
10
  }
11
- export declare function createLsTool(cwd: string): AgentTool<typeof lsSchema>;
11
+ /**
12
+ * Pluggable operations for the ls tool.
13
+ * Override these to delegate directory listing to remote systems (e.g., SSH).
14
+ */
15
+ export interface LsOperations {
16
+ /** Check if path exists */
17
+ exists: (absolutePath: string) => Promise<boolean> | boolean;
18
+ /** Get file/directory stats. Throws if not found. */
19
+ stat: (absolutePath: string) => Promise<{
20
+ isDirectory: () => boolean;
21
+ }> | {
22
+ isDirectory: () => boolean;
23
+ };
24
+ /** Read directory entries */
25
+ readdir: (absolutePath: string) => Promise<string[]> | string[];
26
+ }
27
+ export interface LsToolOptions {
28
+ /** Custom operations for directory listing. Default: local filesystem */
29
+ operations?: LsOperations;
30
+ }
31
+ export declare function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema>;
12
32
  /** Default ls tool using process.cwd() - for backwards compatibility */
13
33
  export declare const lsTool: AgentTool<import("@sinclair/typebox").TObject<{
14
34
  path: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -1 +1 @@
1
- {"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAK7D,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAIH,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAqHpE;AAED,wEAAwE;AACxE,eAAO,MAAM,MAAM;;;QAA8B,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport nodePath from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport function createLsTool(cwd: string): AgentTool<typeof lsSchema> {\n\treturn {\n\t\tname: \"ls\",\n\t\tlabel: \"ls\",\n\t\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tparameters: lsSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Check if path exists\n\t\t\t\t\tif (!existsSync(dirPath)) {\n\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if path is a directory\n\t\t\t\t\tconst stat = statSync(dirPath);\n\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read directory entries\n\t\t\t\t\tlet entries: string[];\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries = readdirSync(dirPath);\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Sort alphabetically (case-insensitive)\n\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t// Format entries with directory indicators\n\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\tlet entryLimitReached = false;\n\n\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\tlet suffix = \"\";\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entryStat = statSync(fullPath);\n\t\t\t\t\t\t\tif (entryStat.isDirectory()) {\n\t\t\t\t\t\t\t\tsuffix = \"/\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip entries we can't stat\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t}\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Apply byte truncation (no line limit since we already have entry limit)\n\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\tconst details: LsToolDetails = {};\n\n\t\t\t\t\t// Build notices\n\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t};\n}\n\n/** Default ls tool using process.cwd() - for backwards compatibility */\nexport const lsTool = createLsTool(process.cwd());\n"]}
1
+ {"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAK7D,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAIH,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC7D,qDAAqD;IACrD,IAAI,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC,GAAG;QAAE,WAAW,EAAE,MAAM,OAAO,CAAA;KAAE,CAAC;IACzG,6BAA6B;IAC7B,OAAO,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAChE;AAQD,MAAM,WAAW,aAAa;IAC7B,yEAAyE;IACzE,UAAU,CAAC,EAAE,YAAY,CAAC;CAC1B;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAyH7F;AAED,wEAAwE;AACxE,eAAO,MAAM,MAAM;;;QAA8B,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport nodePath from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (e.g., SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file/directory stats. Throws if not found. */\n\tstat: (absolutePath: string) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean };\n\t/** Read directory entries */\n\treaddir: (absolutePath: string) => Promise<string[]> | string[];\n}\n\nconst defaultLsOperations: LsOperations = {\n\texists: existsSync,\n\tstat: statSync,\n\treaddir: readdirSync,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nexport function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\tconst ops = options?.operations ?? defaultLsOperations;\n\n\treturn {\n\t\tname: \"ls\",\n\t\tlabel: \"ls\",\n\t\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tparameters: lsSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t\t// Check if path exists\n\t\t\t\t\t\tif (!(await ops.exists(dirPath))) {\n\t\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if path is a directory\n\t\t\t\t\t\tconst stat = await ops.stat(dirPath);\n\t\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read directory entries\n\t\t\t\t\t\tlet entries: string[];\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tentries = await ops.readdir(dirPath);\n\t\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort alphabetically (case-insensitive)\n\t\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t\t// Format entries with directory indicators\n\t\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\t\tlet entryLimitReached = false;\n\n\t\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\t\tlet suffix = \"\";\n\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst entryStat = await ops.stat(fullPath);\n\t\t\t\t\t\t\t\tif (entryStat.isDirectory()) {\n\t\t\t\t\t\t\t\t\tsuffix = \"/\";\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we can't stat\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Apply byte truncation (no line limit since we already have entry limit)\n\t\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\n\t\t\t\t\t\t// Build notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(e);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t};\n}\n\n/** Default ls tool using process.cwd() - for backwards compatibility */\nexport const lsTool = createLsTool(process.cwd());\n"]}
@@ -8,7 +8,13 @@ const lsSchema = Type.Object({
8
8
  limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
9
9
  });
10
10
  const DEFAULT_LIMIT = 500;
11
- export function createLsTool(cwd) {
11
+ const defaultLsOperations = {
12
+ exists: existsSync,
13
+ stat: statSync,
14
+ readdir: readdirSync,
15
+ };
16
+ export function createLsTool(cwd, options) {
17
+ const ops = options?.operations ?? defaultLsOperations;
12
18
  return {
13
19
  name: "ls",
14
20
  label: "ls",
@@ -22,85 +28,87 @@ export function createLsTool(cwd) {
22
28
  }
23
29
  const onAbort = () => reject(new Error("Operation aborted"));
24
30
  signal?.addEventListener("abort", onAbort, { once: true });
25
- try {
26
- const dirPath = resolveToCwd(path || ".", cwd);
27
- const effectiveLimit = limit ?? DEFAULT_LIMIT;
28
- // Check if path exists
29
- if (!existsSync(dirPath)) {
30
- reject(new Error(`Path not found: ${dirPath}`));
31
- return;
32
- }
33
- // Check if path is a directory
34
- const stat = statSync(dirPath);
35
- if (!stat.isDirectory()) {
36
- reject(new Error(`Not a directory: ${dirPath}`));
37
- return;
38
- }
39
- // Read directory entries
40
- let entries;
31
+ (async () => {
41
32
  try {
42
- entries = readdirSync(dirPath);
43
- }
44
- catch (e) {
45
- reject(new Error(`Cannot read directory: ${e.message}`));
46
- return;
47
- }
48
- // Sort alphabetically (case-insensitive)
49
- entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
50
- // Format entries with directory indicators
51
- const results = [];
52
- let entryLimitReached = false;
53
- for (const entry of entries) {
54
- if (results.length >= effectiveLimit) {
55
- entryLimitReached = true;
56
- break;
33
+ const dirPath = resolveToCwd(path || ".", cwd);
34
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
35
+ // Check if path exists
36
+ if (!(await ops.exists(dirPath))) {
37
+ reject(new Error(`Path not found: ${dirPath}`));
38
+ return;
57
39
  }
58
- const fullPath = nodePath.join(dirPath, entry);
59
- let suffix = "";
40
+ // Check if path is a directory
41
+ const stat = await ops.stat(dirPath);
42
+ if (!stat.isDirectory()) {
43
+ reject(new Error(`Not a directory: ${dirPath}`));
44
+ return;
45
+ }
46
+ // Read directory entries
47
+ let entries;
60
48
  try {
61
- const entryStat = statSync(fullPath);
62
- if (entryStat.isDirectory()) {
63
- suffix = "/";
49
+ entries = await ops.readdir(dirPath);
50
+ }
51
+ catch (e) {
52
+ reject(new Error(`Cannot read directory: ${e.message}`));
53
+ return;
54
+ }
55
+ // Sort alphabetically (case-insensitive)
56
+ entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
57
+ // Format entries with directory indicators
58
+ const results = [];
59
+ let entryLimitReached = false;
60
+ for (const entry of entries) {
61
+ if (results.length >= effectiveLimit) {
62
+ entryLimitReached = true;
63
+ break;
64
+ }
65
+ const fullPath = nodePath.join(dirPath, entry);
66
+ let suffix = "";
67
+ try {
68
+ const entryStat = await ops.stat(fullPath);
69
+ if (entryStat.isDirectory()) {
70
+ suffix = "/";
71
+ }
72
+ }
73
+ catch {
74
+ // Skip entries we can't stat
75
+ continue;
64
76
  }
77
+ results.push(entry + suffix);
65
78
  }
66
- catch {
67
- // Skip entries we can't stat
68
- continue;
79
+ signal?.removeEventListener("abort", onAbort);
80
+ if (results.length === 0) {
81
+ resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
82
+ return;
69
83
  }
70
- results.push(entry + suffix);
71
- }
72
- signal?.removeEventListener("abort", onAbort);
73
- if (results.length === 0) {
74
- resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
75
- return;
76
- }
77
- // Apply byte truncation (no line limit since we already have entry limit)
78
- const rawOutput = results.join("\n");
79
- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
80
- let output = truncation.content;
81
- const details = {};
82
- // Build notices
83
- const notices = [];
84
- if (entryLimitReached) {
85
- notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
86
- details.entryLimitReached = effectiveLimit;
87
- }
88
- if (truncation.truncated) {
89
- notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
90
- details.truncation = truncation;
84
+ // Apply byte truncation (no line limit since we already have entry limit)
85
+ const rawOutput = results.join("\n");
86
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
87
+ let output = truncation.content;
88
+ const details = {};
89
+ // Build notices
90
+ const notices = [];
91
+ if (entryLimitReached) {
92
+ notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
93
+ details.entryLimitReached = effectiveLimit;
94
+ }
95
+ if (truncation.truncated) {
96
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
97
+ details.truncation = truncation;
98
+ }
99
+ if (notices.length > 0) {
100
+ output += `\n\n[${notices.join(". ")}]`;
101
+ }
102
+ resolve({
103
+ content: [{ type: "text", text: output }],
104
+ details: Object.keys(details).length > 0 ? details : undefined,
105
+ });
91
106
  }
92
- if (notices.length > 0) {
93
- output += `\n\n[${notices.join(". ")}]`;
107
+ catch (e) {
108
+ signal?.removeEventListener("abort", onAbort);
109
+ reject(e);
94
110
  }
95
- resolve({
96
- content: [{ type: "text", text: output }],
97
- details: Object.keys(details).length > 0 ? details : undefined,
98
- });
99
- }
100
- catch (e) {
101
- signal?.removeEventListener("abort", onAbort);
102
- reject(e);
103
- }
111
+ })();
104
112
  });
105
113
  },
106
114
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport nodePath from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport function createLsTool(cwd: string): AgentTool<typeof lsSchema> {\n\treturn {\n\t\tname: \"ls\",\n\t\tlabel: \"ls\",\n\t\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tparameters: lsSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\ttry {\n\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Check if path exists\n\t\t\t\t\tif (!existsSync(dirPath)) {\n\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if path is a directory\n\t\t\t\t\tconst stat = statSync(dirPath);\n\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read directory entries\n\t\t\t\t\tlet entries: string[];\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentries = readdirSync(dirPath);\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Sort alphabetically (case-insensitive)\n\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t// Format entries with directory indicators\n\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\tlet entryLimitReached = false;\n\n\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\tlet suffix = \"\";\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entryStat = statSync(fullPath);\n\t\t\t\t\t\t\tif (entryStat.isDirectory()) {\n\t\t\t\t\t\t\t\tsuffix = \"/\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip entries we can't stat\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t}\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Apply byte truncation (no line limit since we already have entry limit)\n\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\tconst details: LsToolDetails = {};\n\n\t\t\t\t\t// Build notices\n\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t};\n}\n\n/** Default ls tool using process.cwd() - for backwards compatibility */\nexport const lsTool = createLsTool(process.cwd());\n"]}
1
+ {"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport nodePath from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (e.g., SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file/directory stats. Throws if not found. */\n\tstat: (absolutePath: string) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean };\n\t/** Read directory entries */\n\treaddir: (absolutePath: string) => Promise<string[]> | string[];\n}\n\nconst defaultLsOperations: LsOperations = {\n\texists: existsSync,\n\tstat: statSync,\n\treaddir: readdirSync,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nexport function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\tconst ops = options?.operations ?? defaultLsOperations;\n\n\treturn {\n\t\tname: \"ls\",\n\t\tlabel: \"ls\",\n\t\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tparameters: lsSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t\t// Check if path exists\n\t\t\t\t\t\tif (!(await ops.exists(dirPath))) {\n\t\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if path is a directory\n\t\t\t\t\t\tconst stat = await ops.stat(dirPath);\n\t\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read directory entries\n\t\t\t\t\t\tlet entries: string[];\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tentries = await ops.readdir(dirPath);\n\t\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort alphabetically (case-insensitive)\n\t\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t\t// Format entries with directory indicators\n\t\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\t\tlet entryLimitReached = false;\n\n\t\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\t\tlet suffix = \"\";\n\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst entryStat = await ops.stat(fullPath);\n\t\t\t\t\t\t\t\tif (entryStat.isDirectory()) {\n\t\t\t\t\t\t\t\t\tsuffix = \"/\";\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we can't stat\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Apply byte truncation (no line limit since we already have entry limit)\n\t\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\n\t\t\t\t\t\t// Build notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(e);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t};\n}\n\n/** Default ls tool using process.cwd() - for backwards compatibility */\nexport const lsTool = createLsTool(process.cwd());\n"]}
@@ -8,9 +8,23 @@ declare const readSchema: import("@sinclair/typebox").TObject<{
8
8
  export interface ReadToolDetails {
9
9
  truncation?: TruncationResult;
10
10
  }
11
+ /**
12
+ * Pluggable operations for the read tool.
13
+ * Override these to delegate file reading to remote systems (e.g., SSH).
14
+ */
15
+ export interface ReadOperations {
16
+ /** Read file contents as a Buffer */
17
+ readFile: (absolutePath: string) => Promise<Buffer>;
18
+ /** Check if file is readable (throw if not) */
19
+ access: (absolutePath: string) => Promise<void>;
20
+ /** Detect image MIME type, return null/undefined for non-images */
21
+ detectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;
22
+ }
11
23
  export interface ReadToolOptions {
12
24
  /** Whether to auto-resize images to 2000x2000 max. Default: true */
13
25
  autoResizeImages?: boolean;
26
+ /** Custom operations for file reading. Default: local filesystem */
27
+ operations?: ReadOperations;
14
28
  }
15
29
  export declare function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema>;
16
30
  /** Default read tool using process.cwd() - for backwards compatibility */
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAQ7D,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEtH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAsKnG;AAED,0EAA0E;AAC1E,eAAO,MAAM,QAAQ;;;;QAAgC,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the read operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (dimensionNote) {\n\t\t\t\t\t\t\t\t\t\ttextNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default read tool using process.cwd() - for backwards compatibility */\nexport const readTool = createReadTool(process.cwd());\n"]}
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAQ7D,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEtH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,+CAA+C;IAC/C,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,mEAAmE;IACnE,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnF;AAQD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAyKnG;AAED,0EAA0E;AAC1E,eAAO,MAAM,QAAQ;;;;QAAgC,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (e.g., SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null/undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the read operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\n\t\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\n\t\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (dimensionNote) {\n\t\t\t\t\t\t\t\t\t\ttextNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default read tool using process.cwd() - for backwards compatibility */\nexport const readTool = createReadTool(process.cwd());\n"]}
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { constants } from "fs";
3
- import { access, readFile } from "fs/promises";
3
+ import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
4
4
  import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
5
5
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
6
6
  import { resolveReadPath } from "./path-utils.js";
@@ -10,8 +10,14 @@ const readSchema = Type.Object({
10
10
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
11
11
  limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
12
12
  });
13
+ const defaultReadOperations = {
14
+ readFile: (path) => fsReadFile(path),
15
+ access: (path) => fsAccess(path, constants.R_OK),
16
+ detectImageMimeType: detectSupportedImageMimeTypeFromFile,
17
+ };
13
18
  export function createReadTool(cwd, options) {
14
19
  const autoResizeImages = options?.autoResizeImages ?? true;
20
+ const ops = options?.operations ?? defaultReadOperations;
15
21
  return {
16
22
  name: "read",
17
23
  label: "read",
@@ -38,18 +44,18 @@ export function createReadTool(cwd, options) {
38
44
  (async () => {
39
45
  try {
40
46
  // Check if file exists
41
- await access(absolutePath, constants.R_OK);
47
+ await ops.access(absolutePath);
42
48
  // Check if aborted before reading
43
49
  if (aborted) {
44
50
  return;
45
51
  }
46
- const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
52
+ const mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;
47
53
  // Read the file based on type
48
54
  let content;
49
55
  let details;
50
56
  if (mimeType) {
51
57
  // Read as image (binary)
52
- const buffer = await readFile(absolutePath);
58
+ const buffer = await ops.readFile(absolutePath);
53
59
  const base64 = buffer.toString("base64");
54
60
  if (autoResizeImages) {
55
61
  // Resize image if needed
@@ -73,7 +79,8 @@ export function createReadTool(cwd, options) {
73
79
  }
74
80
  else {
75
81
  // Read as text
76
- const textContent = await readFile(absolutePath, "utf-8");
82
+ const buffer = await ops.readFile(absolutePath);
83
+ const textContent = buffer.toString("utf-8");
77
84
  const allLines = textContent.split("\n");
78
85
  const totalFileLines = allLines.length;
79
86
  // Apply offset if specified (1-indexed to 0-indexed)
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the read operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);\n\n\t\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (dimensionNote) {\n\t\t\t\t\t\t\t\t\t\ttextNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default read tool using process.cwd() - for backwards compatibility */\nexport const readTool = createReadTool(process.cwd());\n"]}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (e.g., SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null/undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the read operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\n\t\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\n\t\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (dimensionNote) {\n\t\t\t\t\t\t\t\t\t\ttextNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default read tool using process.cwd() - for backwards compatibility */\nexport const readTool = createReadTool(process.cwd());\n"]}
@@ -3,7 +3,21 @@ declare const writeSchema: import("@sinclair/typebox").TObject<{
3
3
  path: import("@sinclair/typebox").TString;
4
4
  content: import("@sinclair/typebox").TString;
5
5
  }>;
6
- export declare function createWriteTool(cwd: string): AgentTool<typeof writeSchema>;
6
+ /**
7
+ * Pluggable operations for the write tool.
8
+ * Override these to delegate file writing to remote systems (e.g., SSH).
9
+ */
10
+ export interface WriteOperations {
11
+ /** Write content to a file */
12
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
13
+ /** Create directory (recursively) */
14
+ mkdir: (dir: string) => Promise<void>;
15
+ }
16
+ export interface WriteToolOptions {
17
+ /** Custom operations for file writing. Default: local filesystem */
18
+ operations?: WriteOperations;
19
+ }
20
+ export declare function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema>;
7
21
  /** Default write tool using process.cwd() - for backwards compatibility */
8
22
  export declare const writeTool: AgentTool<import("@sinclair/typebox").TObject<{
9
23
  path: import("@sinclair/typebox").TString;
@@ -1 +1 @@
1
- {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAM7D,QAAA,MAAM,WAAW;;;EAGf,CAAC;AAEH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,WAAW,CAAC,CA8E1E;AAED,2EAA2E;AAC3E,eAAO,MAAM,SAAS;;;QAAiC,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport function createWriteTool(cwd: string): AgentTool<typeof writeSchema> {\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tparameters: writeSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\n\t\t\treturn new Promise<{ content: Array<{ type: \"text\"; text: string }>; details: undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the write operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Create parent directories if needed\n\t\t\t\t\t\t\tawait mkdir(dir, { recursive: true });\n\n\t\t\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Write the file\n\t\t\t\t\t\t\tawait writeFile(absolutePath, content, \"utf-8\");\n\n\t\t\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default write tool using process.cwd() - for backwards compatibility */\nexport const writeTool = createWriteTool(process.cwd());\n"]}
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAM7D,QAAA,MAAM,WAAW;;;EAGf,CAAC;AAEH;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,8BAA8B;IAC9B,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,qCAAqC;IACrC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAOD,MAAM,WAAW,gBAAgB;IAChC,oEAAoE;IACpE,UAAU,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC,OAAO,WAAW,CAAC,CAgFtG;AAED,2EAA2E;AAC3E,eAAO,MAAM,SAAS;;;QAAiC,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\n/**\n * Pluggable operations for the write tool.\n * Override these to delegate file writing to remote systems (e.g., SSH).\n */\nexport interface WriteOperations {\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Create directory (recursively) */\n\tmkdir: (dir: string) => Promise<void>;\n}\n\nconst defaultWriteOperations: WriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n};\n\nexport interface WriteToolOptions {\n\t/** Custom operations for file writing. Default: local filesystem */\n\toperations?: WriteOperations;\n}\n\nexport function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {\n\tconst ops = options?.operations ?? defaultWriteOperations;\n\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tparameters: writeSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\n\t\t\treturn new Promise<{ content: Array<{ type: \"text\"; text: string }>; details: undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the write operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Create parent directories if needed\n\t\t\t\t\t\t\tawait ops.mkdir(dir);\n\n\t\t\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Write the file\n\t\t\t\t\t\t\tawait ops.writeFile(absolutePath, content);\n\n\t\t\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default write tool using process.cwd() - for backwards compatibility */\nexport const writeTool = createWriteTool(process.cwd());\n"]}
@@ -1,12 +1,17 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { mkdir, writeFile } from "fs/promises";
2
+ import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import { resolveToCwd } from "./path-utils.js";
5
5
  const writeSchema = Type.Object({
6
6
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
7
7
  content: Type.String({ description: "Content to write to the file" }),
8
8
  });
9
- export function createWriteTool(cwd) {
9
+ const defaultWriteOperations = {
10
+ writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
11
+ mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => { }),
12
+ };
13
+ export function createWriteTool(cwd, options) {
14
+ const ops = options?.operations ?? defaultWriteOperations;
10
15
  return {
11
16
  name: "write",
12
17
  label: "write",
@@ -34,13 +39,13 @@ export function createWriteTool(cwd) {
34
39
  (async () => {
35
40
  try {
36
41
  // Create parent directories if needed
37
- await mkdir(dir, { recursive: true });
42
+ await ops.mkdir(dir);
38
43
  // Check if aborted before writing
39
44
  if (aborted) {
40
45
  return;
41
46
  }
42
47
  // Write the file
43
- await writeFile(absolutePath, content, "utf-8");
48
+ await ops.writeFile(absolutePath, content);
44
49
  // Check if aborted after writing
45
50
  if (aborted) {
46
51
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"write.js","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport function createWriteTool(cwd: string): AgentTool<typeof writeSchema> {\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tparameters: writeSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\n\t\t\treturn new Promise<{ content: Array<{ type: \"text\"; text: string }>; details: undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the write operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Create parent directories if needed\n\t\t\t\t\t\t\tawait mkdir(dir, { recursive: true });\n\n\t\t\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Write the file\n\t\t\t\t\t\t\tawait writeFile(absolutePath, content, \"utf-8\");\n\n\t\t\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default write tool using process.cwd() - for backwards compatibility */\nexport const writeTool = createWriteTool(process.cwd());\n"]}
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\n/**\n * Pluggable operations for the write tool.\n * Override these to delegate file writing to remote systems (e.g., SSH).\n */\nexport interface WriteOperations {\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Create directory (recursively) */\n\tmkdir: (dir: string) => Promise<void>;\n}\n\nconst defaultWriteOperations: WriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n};\n\nexport interface WriteToolOptions {\n\t/** Custom operations for file writing. Default: local filesystem */\n\toperations?: WriteOperations;\n}\n\nexport function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {\n\tconst ops = options?.operations ?? defaultWriteOperations;\n\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tparameters: writeSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\n\t\t\treturn new Promise<{ content: Array<{ type: \"text\"; text: string }>; details: undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t// Check if already aborted\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet aborted = false;\n\n\t\t\t\t\t// Set up abort handler\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t}\n\n\t\t\t\t\t// Perform the write operation\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Create parent directories if needed\n\t\t\t\t\t\t\tawait ops.mkdir(dir);\n\n\t\t\t\t\t\t\t// Check if aborted before writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Write the file\n\t\t\t\t\t\t\tawait ops.writeFile(absolutePath, content);\n\n\t\t\t\t\t\t\t// Check if aborted after writing\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** Default write tool using process.cwd() - for backwards compatibility */\nexport const writeTool = createWriteTool(process.cwd());\n"]}