apex-code 0.3.0 → 0.5.0

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 (213) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/client/transcript.d.ts.map +1 -1
  3. package/dist/client/transcript.js +1 -1
  4. package/dist/client/transcript.js.map +1 -1
  5. package/dist/core/agent-session-services.d.ts.map +1 -1
  6. package/dist/core/agent-session-services.js +2 -1
  7. package/dist/core/agent-session-services.js.map +1 -1
  8. package/dist/core/delegation/runtime.d.ts.map +1 -1
  9. package/dist/core/delegation/runtime.js +2 -2
  10. package/dist/core/delegation/runtime.js.map +1 -1
  11. package/dist/core/durable-state/daemon.d.ts.map +1 -1
  12. package/dist/core/durable-state/daemon.js +1 -1
  13. package/dist/core/durable-state/daemon.js.map +1 -1
  14. package/dist/core/extensions/types.d.ts +1 -1
  15. package/dist/core/extensions/types.d.ts.map +1 -1
  16. package/dist/core/extensions/types.js.map +1 -1
  17. package/dist/core/keybindings.d.ts +1 -1
  18. package/dist/core/keybindings.d.ts.map +1 -1
  19. package/dist/core/keybindings.js.map +1 -1
  20. package/dist/core/mcp/mcp-tool.d.ts.map +1 -1
  21. package/dist/core/mcp/mcp-tool.js +2 -4
  22. package/dist/core/mcp/mcp-tool.js.map +1 -1
  23. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
  24. package/dist/core/mcp/oauth/authorize.js +7 -2
  25. package/dist/core/mcp/oauth/authorize.js.map +1 -1
  26. package/dist/core/mcp/runtime.d.ts +2 -2
  27. package/dist/core/mcp/runtime.d.ts.map +1 -1
  28. package/dist/core/mcp/runtime.js +8 -3
  29. package/dist/core/mcp/runtime.js.map +1 -1
  30. package/dist/core/permissions/store.d.ts +7 -2
  31. package/dist/core/permissions/store.d.ts.map +1 -1
  32. package/dist/core/permissions/store.js +6 -1
  33. package/dist/core/permissions/store.js.map +1 -1
  34. package/dist/core/project-resources.d.ts +12 -0
  35. package/dist/core/project-resources.d.ts.map +1 -1
  36. package/dist/core/project-resources.js +19 -0
  37. package/dist/core/project-resources.js.map +1 -1
  38. package/dist/core/provider-composer.d.ts.map +1 -1
  39. package/dist/core/provider-composer.js +10 -1
  40. package/dist/core/provider-composer.js.map +1 -1
  41. package/dist/core/resource-loader.d.ts +6 -0
  42. package/dist/core/resource-loader.d.ts.map +1 -1
  43. package/dist/core/resource-loader.js +9 -3
  44. package/dist/core/resource-loader.js.map +1 -1
  45. package/dist/core/sdk.d.ts +1 -1
  46. package/dist/core/sdk.d.ts.map +1 -1
  47. package/dist/core/sdk.js +3 -3
  48. package/dist/core/sdk.js.map +1 -1
  49. package/dist/core/settings-manager.d.ts +16 -3
  50. package/dist/core/settings-manager.d.ts.map +1 -1
  51. package/dist/core/settings-manager.js +39 -16
  52. package/dist/core/settings-manager.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +11 -7
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +37 -46
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/delegate.d.ts.map +1 -1
  58. package/dist/core/tools/delegate.js +2 -2
  59. package/dist/core/tools/delegate.js.map +1 -1
  60. package/dist/core/tools/edit.d.ts.map +1 -1
  61. package/dist/core/tools/edit.js +3 -3
  62. package/dist/core/tools/edit.js.map +1 -1
  63. package/dist/core/tools/find.d.ts.map +1 -1
  64. package/dist/core/tools/find.js +4 -4
  65. package/dist/core/tools/find.js.map +1 -1
  66. package/dist/core/tools/grep.d.ts.map +1 -1
  67. package/dist/core/tools/grep.js +4 -4
  68. package/dist/core/tools/grep.js.map +1 -1
  69. package/dist/core/tools/ls.d.ts.map +1 -1
  70. package/dist/core/tools/ls.js +4 -4
  71. package/dist/core/tools/ls.js.map +1 -1
  72. package/dist/core/tools/path-utils.d.ts +9 -0
  73. package/dist/core/tools/path-utils.d.ts.map +1 -1
  74. package/dist/core/tools/path-utils.js +235 -10
  75. package/dist/core/tools/path-utils.js.map +1 -1
  76. package/dist/core/tools/read.d.ts.map +1 -1
  77. package/dist/core/tools/read.js +11 -8
  78. package/dist/core/tools/read.js.map +1 -1
  79. package/dist/core/tools/write.d.ts.map +1 -1
  80. package/dist/core/tools/write.js +7 -8
  81. package/dist/core/tools/write.js.map +1 -1
  82. package/dist/core/workspace/state.d.ts.map +1 -1
  83. package/dist/core/workspace/state.js +1 -1
  84. package/dist/core/workspace/state.js.map +1 -1
  85. package/dist/extensions/llama/ui.d.ts.map +1 -1
  86. package/dist/extensions/llama/ui.js +3 -2
  87. package/dist/extensions/llama/ui.js.map +1 -1
  88. package/dist/main.d.ts.map +1 -1
  89. package/dist/main.js +3 -1
  90. package/dist/main.js.map +1 -1
  91. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  92. package/dist/modes/interactive/components/assistant-message.js +0 -10
  93. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  94. package/dist/modes/interactive/components/autocomplete-source.d.ts +8 -0
  95. package/dist/modes/interactive/components/autocomplete-source.d.ts.map +1 -0
  96. package/dist/modes/interactive/components/autocomplete-source.js +23 -0
  97. package/dist/modes/interactive/components/autocomplete-source.js.map +1 -0
  98. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/bash-execution.js +6 -4
  100. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  101. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  103. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  104. package/dist/modes/interactive/components/composer-dock.d.ts +12 -0
  105. package/dist/modes/interactive/components/composer-dock.d.ts.map +1 -0
  106. package/dist/modes/interactive/components/composer-dock.js +24 -0
  107. package/dist/modes/interactive/components/composer-dock.js.map +1 -0
  108. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/config-selector.js +11 -9
  110. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  111. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  112. package/dist/modes/interactive/components/diff.js +2 -2
  113. package/dist/modes/interactive/components/diff.js.map +1 -1
  114. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/extension-editor.js +9 -9
  116. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  117. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  118. package/dist/modes/interactive/components/extension-input.js +9 -5
  119. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  120. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/extension-selector.js +11 -10
  122. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  123. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/first-time-setup.js +6 -6
  125. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  126. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/footer.js +8 -3
  128. package/dist/modes/interactive/components/footer.js.map +1 -1
  129. package/dist/modes/interactive/components/keybinding-hints.d.ts +21 -0
  130. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  131. package/dist/modes/interactive/components/keybinding-hints.js +34 -0
  132. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  133. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  134. package/dist/modes/interactive/components/login-dialog.js +27 -23
  135. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  136. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  137. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  138. package/dist/modes/interactive/components/model-selector.js +14 -18
  139. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  140. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/oauth-selector.js +7 -6
  142. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  143. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/permission-preview.js +3 -3
  145. package/dist/modes/interactive/components/permission-preview.js.map +1 -1
  146. package/dist/modes/interactive/components/prompt-input.d.ts +12 -0
  147. package/dist/modes/interactive/components/prompt-input.d.ts.map +1 -0
  148. package/dist/modes/interactive/components/prompt-input.js +18 -0
  149. package/dist/modes/interactive/components/prompt-input.js.map +1 -0
  150. package/dist/modes/interactive/components/scoped-models-selector.d.ts +1 -1
  151. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/scoped-models-selector.js +4 -3
  153. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  154. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/session-selector.js +26 -19
  156. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  157. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/settings-selector.js +39 -6
  159. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  160. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/settings-submenu.js +8 -6
  162. package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
  163. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  164. package/dist/modes/interactive/components/thinking-selector.js +10 -5
  165. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  166. package/dist/modes/interactive/components/tool-execution.d.ts +1 -11
  167. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/tool-execution.js +9 -38
  169. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  170. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/tool-panel.js +7 -2
  172. package/dist/modes/interactive/components/tool-panel.js.map +1 -1
  173. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/tree-selector.js +9 -5
  175. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  176. package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/trust-selector.js +11 -11
  178. package/dist/modes/interactive/components/trust-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  180. package/dist/modes/interactive/components/user-message-selector.js +2 -2
  181. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  182. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/user-message.js +6 -3
  184. package/dist/modes/interactive/components/user-message.js.map +1 -1
  185. package/dist/modes/interactive/interactive-mode.d.ts +7 -2
  186. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  187. package/dist/modes/interactive/interactive-mode.js +65 -65
  188. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  189. package/dist/modes/print-mode.d.ts.map +1 -1
  190. package/dist/modes/print-mode.js +49 -23
  191. package/dist/modes/print-mode.js.map +1 -1
  192. package/dist/tsconfig.json +3 -0
  193. package/dist/utils/clipboard-image.d.ts.map +1 -1
  194. package/dist/utils/clipboard-image.js +1 -1
  195. package/dist/utils/clipboard-image.js.map +1 -1
  196. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  197. package/dist/utils/syntax-highlight.js +22 -22
  198. package/dist/utils/syntax-highlight.js.map +1 -1
  199. package/docs/json.md +36 -0
  200. package/docs/models.md +1 -1
  201. package/docs/settings.md +1 -1
  202. package/examples/extensions/gondolin/package-lock.json +3 -3
  203. package/examples/extensions/minimal-mode.ts +6 -6
  204. package/examples/extensions/sandbox/package-lock.json +4 -4
  205. package/examples/extensions/sandbox/package.json +1 -1
  206. package/examples/sdk/10-settings.ts +2 -2
  207. package/examples/sdk/README.md +1 -1
  208. package/npm-shrinkwrap.json +19 -34
  209. package/package.json +6 -6
  210. package/dist/modes/interactive/components/message-spine.d.ts +0 -14
  211. package/dist/modes/interactive/components/message-spine.d.ts.map +0 -1
  212. package/dist/modes/interactive/components/message-spine.js +0 -19
  213. package/dist/modes/interactive/components/message-spine.js.map +0 -1
@@ -4,7 +4,7 @@ import { Text } from "@earendil-works/pi-tui";
4
4
  import { spawn } from "child_process";
5
5
  import path from "path";
6
6
  import { Type } from "typebox";
7
- import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
7
+ import { formatHiddenLines, previewLineCount } from "../../modes/interactive/components/keybinding-hints.js";
8
8
  import { ensureTool } from "../../utils/tools-manager.js";
9
9
  import { getPreparedPathOperation } from "../permissions/operations.js";
10
10
  import { createPathPermissionSpec } from "./path-permission.js";
@@ -52,12 +52,12 @@ function formatGrepResult(result, options, theme, showImages) {
52
52
  let text = "";
53
53
  if (output) {
54
54
  const lines = output.split("\n");
55
- const maxLines = options.expanded ? lines.length : 15;
55
+ const maxLines = options.expanded ? lines.length : previewLineCount(lines.length, 15);
56
56
  const displayLines = lines.slice(0, maxLines);
57
- const remaining = lines.length - maxLines;
57
+ const remaining = lines.length - displayLines.length;
58
58
  text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
59
59
  if (remaining > 0) {
60
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
60
+ text += `\n${formatHiddenLines(remaining, "more")}`;
61
61
  }
62
62
  }
63
63
  const matchLimit = result.details?.matchLimitReached;
@@ -1 +1 @@
1
- {"version":3,"file":"grep.js","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile as fsReadFile, stat as fsStat } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport { spawn } from \"child_process\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { ensureTool } from \"../../utils/tools-manager.ts\";\nimport type { ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getPreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.ts\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nexport const grepToolSystemPromptContribution = {\n\tsnippet: \"Search file contents for patterns (respects .gitignore)\",\n\tguidelines: [],\n} as const;\n\nexport type GrepToolInput = Static<typeof grepSchema>;\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\t/** Matches ripgrep found before the limit was applied. Present only when truncated. */\n\ttotalMatches?: number;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\n/**\n * Pluggable operations for the grep tool.\n * Override these to delegate search to remote systems (for example SSH).\n */\nexport interface GrepOperations {\n\t/** Check if path is a directory. Throws if path does not exist. */\n\tisDirectory: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Read file contents for context lines */\n\treadFile: (absolutePath: string) => Promise<string> | string;\n}\n\nconst defaultGrepOperations: GrepOperations = {\n\tisDirectory: async (p) => (await fsStat(p)).isDirectory(),\n\treadFile: (p) => fsReadFile(p, \"utf-8\"),\n};\n\nexport interface GrepToolOptions {\n\t/** Custom operations for grep. Default: local filesystem plus ripgrep */\n\toperations?: GrepOperations;\n}\n\nfunction formatGrepCall(\n\targs: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,\n\ttheme: Theme,\n): string {\n\tconst pattern = str(args?.pattern);\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst glob = str(args?.glob);\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\" \" +\n\t\t(pattern === null ? invalidArg : theme.fg(\"accent\", `/${pattern || \"\"}/`)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${path === null ? invalidArg : path}`);\n\tif (glob) text += theme.fg(\"toolOutput\", ` (${glob})`);\n\tif (limit !== undefined) text += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\treturn text;\n}\n\nfunction formatGrepResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GrepToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\tconst matchLimit = result.details?.matchLimitReached;\n\tconst truncation = result.details?.truncation;\n\tconst linesTruncated = result.details?.linesTruncated;\n\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (matchLimit) warnings.push(`${matchLimit} matches limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\tif (linesTruncated) warnings.push(\"some lines truncated\");\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createGrepToolDefinition(\n\tcwd: string,\n\toptions?: GrepToolOptions,\n): ApexToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\t\tpromptSnippet: grepToolSystemPromptContribution.snippet,\n\t\tparameters: grepSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"allow\",\n\t\t\t\tverb: \"Search\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\tinput: {\n\t\t\t\tpattern: string;\n\t\t\t\tpath?: string;\n\t\t\t\tglob?: string;\n\t\t\t\tignoreCase?: boolean;\n\t\t\t\tliteral?: boolean;\n\t\t\t\tcontext?: number;\n\t\t\t\tlimit?: number;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst { pattern, path: searchDir, glob, ignoreCase, literal, context, limit } = input;\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\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (!settled) {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tfn();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst rgPath = await ensureTool(\"rg\");\n\t\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst prepared = getPreparedPathOperation(input);\n\t\t\t\t\t\tconst searchPath = prepared ? prepared.path.value : resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst ops = customOps ?? defaultGrepOperations;\n\t\t\t\t\t\tlet isDirectory: boolean;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tisDirectory = await ops.isDirectory(searchPath);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\t\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\t\tconst getFileLines = async (filePath: string): Promise<string[]> => {\n\t\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst content = await ops.readFile(filePath);\n\t\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn lines;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\t\t\t\t\t\tif (ignoreCase) args.push(\"--ignore-case\");\n\t\t\t\t\t\tif (literal) args.push(\"--fixed-strings\");\n\t\t\t\t\t\tif (glob) args.push(\"--glob\", glob);\n\t\t\t\t\t\targs.push(\"--\", pattern, searchPath);\n\n\t\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\t\tlet totalMatches = 0;\n\t\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\t\tlet aborted = false;\n\t\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst stopChild = () => {\n\t\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\t\tstopChild();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {\n\t\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\t\tconst lines = await getFileLines(filePath);\n\t\t\t\t\t\t\tif (!lines.length) return [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\t\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\t\t\t\t\t\t\t\t// Truncate long lines so grep output stays compact.\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tif (isMatchLine) block.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t\telse block.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn block;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Collect matches during streaming, then format them after rg exits.\n\t\t\t\t\t\tconst matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];\n\t\t\t\t\t\t// Once the limit is hit we stop collecting but let ripgrep run to the end,\n\t\t\t\t\t\t// because its closing `summary` event carries the true match count and\n\t\t\t\t\t\t// nothing else can produce one -- an exact total requires a full scan. The\n\t\t\t\t\t\t// tail of an rg scan is far cheaper than the extra model round trip an\n\t\t\t\t\t\t// agent spends re-running with a bigger limit to learn what it missed.\n\t\t\t\t\t\t// Lines past the limit skip `JSON.parse` unless they are that summary.\n\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\tif (!line.trim()) return;\n\t\t\t\t\t\t\tconst collecting = matchCount < effectiveLimit;\n\t\t\t\t\t\t\tif (!collecting && !line.includes('\"type\":\"summary\"')) return;\n\t\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"summary\") {\n\t\t\t\t\t\t\t\tconst summarized = event.data?.stats?.matches;\n\t\t\t\t\t\t\t\tif (typeof summarized === \"number\") totalMatches = summarized;\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\t\t// Belt and braces. File content cannot currently spoof the substring\n\t\t\t\t\t\t\t\t// gate above, because ripgrep escapes quotes inside JSON strings, but\n\t\t\t\t\t\t\t\t// that is ripgrep's encoding guarantee rather than this function's.\n\t\t\t\t\t\t\t\t// Re-asserting the cap keeps the row limit true of this code alone.\n\t\t\t\t\t\t\t\tif (!collecting) return;\n\t\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\t\t\t\t\t\t\t\tconst lineText = event.data?.lines?.text;\n\t\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\")\n\t\t\t\t\t\t\t\t\tmatches.push({ filePath, lineNumber, lineText });\n\t\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) matchLimitReached = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tchild.on(\"close\", async (code) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (code !== 0 && code !== 1) {\n\t\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t\t);\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// Format matches after streaming finishes so custom readFile() backends can be async.\n\t\t\t\t\t\t\tfor (const match of matches) {\n\t\t\t\t\t\t\t\tif (contextValue === 0 && match.lineText !== undefined) {\n\t\t\t\t\t\t\t\t\tconst relativePath = formatPath(match.filePath);\n\t\t\t\t\t\t\t\t\tconst sanitized = match.lineText\n\t\t\t\t\t\t\t\t\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t\t\t\t\t\t\t\t\t.replace(/\\r/g, \"\")\n\t\t\t\t\t\t\t\t\t\t.replace(/\\n$/, \"\");\n\t\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\t\toutputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst block = await formatBlock(match.filePath, match.lineNumber);\n\t\t\t\t\t\t\t\t\toutputLines.push(...block);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\t\t// Apply byte truncation. There is no line limit here because the match limit already capped rows.\n\t\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\t\t\t\t\t\t\t// Build actionable notices for truncation and match limits.\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\t\t// `summary.stats.matches` counts every match ripgrep found, which is\n\t\t\t\t\t\t\t\t// what the agent needs to judge whether the truncated list is\n\t\t\t\t\t\t\t\t// representative. It falls back to the shown count only if ripgrep\n\t\t\t\t\t\t\t\t// exited without a summary, which would make \"N of N\" the honest\n\t\t\t\t\t\t\t\t// answer rather than an invented larger number.\n\t\t\t\t\t\t\t\tconst total = Math.max(totalMatches, matchCount);\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${matchCount} of ${total} total matches shown. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t\t\tdetails.totalMatches = total;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (notices.length > 0) output += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {\n\treturn wrapToolDefinition(createGrepToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"grep.js","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile as fsReadFile, stat as fsStat } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport { spawn } from \"child_process\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { formatHiddenLines, previewLineCount } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { ensureTool } from \"../../utils/tools-manager.ts\";\nimport type { ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getPreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.ts\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nexport const grepToolSystemPromptContribution = {\n\tsnippet: \"Search file contents for patterns (respects .gitignore)\",\n\tguidelines: [],\n} as const;\n\nexport type GrepToolInput = Static<typeof grepSchema>;\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\t/** Matches ripgrep found before the limit was applied. Present only when truncated. */\n\ttotalMatches?: number;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\n/**\n * Pluggable operations for the grep tool.\n * Override these to delegate search to remote systems (for example SSH).\n */\nexport interface GrepOperations {\n\t/** Check if path is a directory. Throws if path does not exist. */\n\tisDirectory: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Read file contents for context lines */\n\treadFile: (absolutePath: string) => Promise<string> | string;\n}\n\nconst defaultGrepOperations: GrepOperations = {\n\tisDirectory: async (p) => (await fsStat(p)).isDirectory(),\n\treadFile: (p) => fsReadFile(p, \"utf-8\"),\n};\n\nexport interface GrepToolOptions {\n\t/** Custom operations for grep. Default: local filesystem plus ripgrep */\n\toperations?: GrepOperations;\n}\n\nfunction formatGrepCall(\n\targs: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,\n\ttheme: Theme,\n): string {\n\tconst pattern = str(args?.pattern);\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst glob = str(args?.glob);\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\" \" +\n\t\t(pattern === null ? invalidArg : theme.fg(\"accent\", `/${pattern || \"\"}/`)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${path === null ? invalidArg : path}`);\n\tif (glob) text += theme.fg(\"toolOutput\", ` (${glob})`);\n\tif (limit !== undefined) text += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\treturn text;\n}\n\nfunction formatGrepResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GrepToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : previewLineCount(lines.length, 15);\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - displayLines.length;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `\\n${formatHiddenLines(remaining, \"more\")}`;\n\t\t}\n\t}\n\n\tconst matchLimit = result.details?.matchLimitReached;\n\tconst truncation = result.details?.truncation;\n\tconst linesTruncated = result.details?.linesTruncated;\n\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (matchLimit) warnings.push(`${matchLimit} matches limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\tif (linesTruncated) warnings.push(\"some lines truncated\");\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createGrepToolDefinition(\n\tcwd: string,\n\toptions?: GrepToolOptions,\n): ApexToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\t\tpromptSnippet: grepToolSystemPromptContribution.snippet,\n\t\tparameters: grepSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"allow\",\n\t\t\t\tverb: \"Search\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\tinput: {\n\t\t\t\tpattern: string;\n\t\t\t\tpath?: string;\n\t\t\t\tglob?: string;\n\t\t\t\tignoreCase?: boolean;\n\t\t\t\tliteral?: boolean;\n\t\t\t\tcontext?: number;\n\t\t\t\tlimit?: number;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst { pattern, path: searchDir, glob, ignoreCase, literal, context, limit } = input;\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\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (!settled) {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tfn();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst rgPath = await ensureTool(\"rg\");\n\t\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst prepared = getPreparedPathOperation(input);\n\t\t\t\t\t\tconst searchPath = prepared ? prepared.path.value : resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst ops = customOps ?? defaultGrepOperations;\n\t\t\t\t\t\tlet isDirectory: boolean;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tisDirectory = await ops.isDirectory(searchPath);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\t\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\t\tconst getFileLines = async (filePath: string): Promise<string[]> => {\n\t\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst content = await ops.readFile(filePath);\n\t\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn lines;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\t\t\t\t\t\tif (ignoreCase) args.push(\"--ignore-case\");\n\t\t\t\t\t\tif (literal) args.push(\"--fixed-strings\");\n\t\t\t\t\t\tif (glob) args.push(\"--glob\", glob);\n\t\t\t\t\t\targs.push(\"--\", pattern, searchPath);\n\n\t\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\t\tlet totalMatches = 0;\n\t\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\t\tlet aborted = false;\n\t\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst stopChild = () => {\n\t\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\t\tstopChild();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {\n\t\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\t\tconst lines = await getFileLines(filePath);\n\t\t\t\t\t\t\tif (!lines.length) return [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\t\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\t\t\t\t\t\t\t\t// Truncate long lines so grep output stays compact.\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tif (isMatchLine) block.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t\telse block.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn block;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Collect matches during streaming, then format them after rg exits.\n\t\t\t\t\t\tconst matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];\n\t\t\t\t\t\t// Once the limit is hit we stop collecting but let ripgrep run to the end,\n\t\t\t\t\t\t// because its closing `summary` event carries the true match count and\n\t\t\t\t\t\t// nothing else can produce one -- an exact total requires a full scan. The\n\t\t\t\t\t\t// tail of an rg scan is far cheaper than the extra model round trip an\n\t\t\t\t\t\t// agent spends re-running with a bigger limit to learn what it missed.\n\t\t\t\t\t\t// Lines past the limit skip `JSON.parse` unless they are that summary.\n\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\tif (!line.trim()) return;\n\t\t\t\t\t\t\tconst collecting = matchCount < effectiveLimit;\n\t\t\t\t\t\t\tif (!collecting && !line.includes('\"type\":\"summary\"')) return;\n\t\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"summary\") {\n\t\t\t\t\t\t\t\tconst summarized = event.data?.stats?.matches;\n\t\t\t\t\t\t\t\tif (typeof summarized === \"number\") totalMatches = summarized;\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\t\t// Belt and braces. File content cannot currently spoof the substring\n\t\t\t\t\t\t\t\t// gate above, because ripgrep escapes quotes inside JSON strings, but\n\t\t\t\t\t\t\t\t// that is ripgrep's encoding guarantee rather than this function's.\n\t\t\t\t\t\t\t\t// Re-asserting the cap keeps the row limit true of this code alone.\n\t\t\t\t\t\t\t\tif (!collecting) return;\n\t\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\t\t\t\t\t\t\t\tconst lineText = event.data?.lines?.text;\n\t\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\")\n\t\t\t\t\t\t\t\t\tmatches.push({ filePath, lineNumber, lineText });\n\t\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) matchLimitReached = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tchild.on(\"close\", async (code) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (code !== 0 && code !== 1) {\n\t\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t\t);\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// Format matches after streaming finishes so custom readFile() backends can be async.\n\t\t\t\t\t\t\tfor (const match of matches) {\n\t\t\t\t\t\t\t\tif (contextValue === 0 && match.lineText !== undefined) {\n\t\t\t\t\t\t\t\t\tconst relativePath = formatPath(match.filePath);\n\t\t\t\t\t\t\t\t\tconst sanitized = match.lineText\n\t\t\t\t\t\t\t\t\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t\t\t\t\t\t\t\t\t.replace(/\\r/g, \"\")\n\t\t\t\t\t\t\t\t\t\t.replace(/\\n$/, \"\");\n\t\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\t\toutputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst block = await formatBlock(match.filePath, match.lineNumber);\n\t\t\t\t\t\t\t\t\toutputLines.push(...block);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\t\t// Apply byte truncation. There is no line limit here because the match limit already capped rows.\n\t\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\t\t\t\t\t\t\t// Build actionable notices for truncation and match limits.\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\t\t// `summary.stats.matches` counts every match ripgrep found, which is\n\t\t\t\t\t\t\t\t// what the agent needs to judge whether the truncated list is\n\t\t\t\t\t\t\t\t// representative. It falls back to the shown count only if ripgrep\n\t\t\t\t\t\t\t\t// exited without a summary, which would make \"N of N\" the honest\n\t\t\t\t\t\t\t\t// answer rather than an invented larger number.\n\t\t\t\t\t\t\t\tconst total = Math.max(totalMatches, matchCount);\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${matchCount} of ${total} total matches shown. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t\t\tdetails.totalMatches = total;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (notices.length > 0) output += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {\n\treturn wrapToolDefinition(createGrepToolDefinition(cwd, options));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAK5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAKxD,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAEH,eAAO,MAAM,8BAA8B;;;CAGjC,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIlD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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,wDAAwD;IACxD,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;AA6CD,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,aAAa,GACrB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,aAAa,GAAG,SAAS,CAAC,CAwIhE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAE7F","sourcesContent":["import { readdir as fsReaddir, stat as fsStat } from \"node:fs/promises\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getPreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { pathExists, resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, renderToolPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport const lsToolSystemPromptContribution = {\n\tsnippet: \"List directory contents\",\n\tguidelines: [],\n} as const;\n\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\t/** Entries in the directory before the limit was applied. Present only when truncated. */\n\ttotalEntries?: number;\n\tentryLimitReached?: number;\n}\n\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (for example SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file or 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: pathExists,\n\tstat: fsStat,\n\treaddir: fsReaddir,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nfunction formatLsCall(args: { path?: string; limit?: number } | undefined, theme: Theme, cwd: string): string {\n\tconst limit = args?.limit;\n\tconst pathDisplay = renderToolPath(str(args?.path), theme, cwd, { emptyFallback: \".\" });\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${pathDisplay}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 20;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\tcwd: string,\n\toptions?: LsToolOptions,\n): ApexToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = options?.operations ?? defaultLsOperations;\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\tpromptSnippet: lsToolSystemPromptContribution.snippet,\n\t\tparameters: lsSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"allow\",\n\t\t\t\tverb: \"List\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(_toolCallId, input: { path?: string; limit?: number }, signal?: AbortSignal, _onUpdate?, _ctx?) {\n\t\t\tconst { path, limit } = input;\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 prepared = getPreparedPathOperation(input);\n\t\t\t\t\t\tconst dirPath = prepared ? prepared.path.value : 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\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\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()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\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\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\t// The whole listing was read before capping, so the total is already in\n\t\t\t\t\t\t\t// hand. Reporting it is what lets the agent decide whether it has seen\n\t\t\t\t\t\t\t// everything instead of spending a second call to find out.\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${results.length} of ${entries.length} total entries shown. Use limit=${effectiveLimit * 2} for more`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t\tdetails.totalEntries = entries.length;\n\t\t\t\t\t\t}\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\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\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsCall(args, theme, context.cwd));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAK5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAKxD,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAEH,eAAO,MAAM,8BAA8B;;;CAGjC,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIlD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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,wDAAwD;IACxD,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;AA6CD,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,aAAa,GACrB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,aAAa,GAAG,SAAS,CAAC,CAwIhE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAE7F","sourcesContent":["import { readdir as fsReaddir, stat as fsStat } from \"node:fs/promises\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { formatHiddenLines, previewLineCount } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getPreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { pathExists, resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, renderToolPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport const lsToolSystemPromptContribution = {\n\tsnippet: \"List directory contents\",\n\tguidelines: [],\n} as const;\n\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\t/** Entries in the directory before the limit was applied. Present only when truncated. */\n\ttotalEntries?: number;\n\tentryLimitReached?: number;\n}\n\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (for example SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file or 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: pathExists,\n\tstat: fsStat,\n\treaddir: fsReaddir,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nfunction formatLsCall(args: { path?: string; limit?: number } | undefined, theme: Theme, cwd: string): string {\n\tconst limit = args?.limit;\n\tconst pathDisplay = renderToolPath(str(args?.path), theme, cwd, { emptyFallback: \".\" });\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${pathDisplay}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : previewLineCount(lines.length, 20);\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - displayLines.length;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `\\n${formatHiddenLines(remaining, \"more\")}`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\tcwd: string,\n\toptions?: LsToolOptions,\n): ApexToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = options?.operations ?? defaultLsOperations;\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\tpromptSnippet: lsToolSystemPromptContribution.snippet,\n\t\tparameters: lsSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"allow\",\n\t\t\t\tverb: \"List\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(_toolCallId, input: { path?: string; limit?: number }, signal?: AbortSignal, _onUpdate?, _ctx?) {\n\t\t\tconst { path, limit } = input;\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 prepared = getPreparedPathOperation(input);\n\t\t\t\t\t\tconst dirPath = prepared ? prepared.path.value : 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\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\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()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\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\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\t// The whole listing was read before capping, so the total is already in\n\t\t\t\t\t\t\t// hand. Reporting it is what lets the agent decide whether it has seen\n\t\t\t\t\t\t\t// everything instead of spending a second call to find out.\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${results.length} of ${entries.length} total entries shown. Use limit=${effectiveLimit * 2} for more`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t\tdetails.totalEntries = entries.length;\n\t\t\t\t\t\t}\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\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\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsCall(args, theme, context.cwd));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(cwd, options));\n}\n"]}
@@ -2,7 +2,7 @@ import { readdir as fsReaddir, stat as fsStat } from "node:fs/promises";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import nodePath from "path";
4
4
  import { Type } from "typebox";
5
- import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
5
+ import { formatHiddenLines, previewLineCount } from "../../modes/interactive/components/keybinding-hints.js";
6
6
  import { getPreparedPathOperation } from "../permissions/operations.js";
7
7
  import { createPathPermissionSpec } from "./path-permission.js";
8
8
  import { pathExists, resolveToCwd } from "./path-utils.js";
@@ -37,12 +37,12 @@ function formatLsResult(result, options, theme, showImages) {
37
37
  let text = "";
38
38
  if (output) {
39
39
  const lines = output.split("\n");
40
- const maxLines = options.expanded ? lines.length : 20;
40
+ const maxLines = options.expanded ? lines.length : previewLineCount(lines.length, 20);
41
41
  const displayLines = lines.slice(0, maxLines);
42
- const remaining = lines.length - maxLines;
42
+ const remaining = lines.length - displayLines.length;
43
43
  text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
44
44
  if (remaining > 0) {
45
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
45
+ text += `\n${formatHiddenLines(remaining, "more")}`;
46
46
  }
47
47
  }
48
48
  const entryLimit = result.details?.entryLimitReached;
@@ -1 +1 @@
1
- {"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdir as fsReaddir, stat as fsStat } from \"node:fs/promises\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getPreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { pathExists, resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, renderToolPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport const lsToolSystemPromptContribution = {\n\tsnippet: \"List directory contents\",\n\tguidelines: [],\n} as const;\n\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\t/** Entries in the directory before the limit was applied. Present only when truncated. */\n\ttotalEntries?: number;\n\tentryLimitReached?: number;\n}\n\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (for example SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file or 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: pathExists,\n\tstat: fsStat,\n\treaddir: fsReaddir,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nfunction formatLsCall(args: { path?: string; limit?: number } | undefined, theme: Theme, cwd: string): string {\n\tconst limit = args?.limit;\n\tconst pathDisplay = renderToolPath(str(args?.path), theme, cwd, { emptyFallback: \".\" });\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${pathDisplay}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 20;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\tcwd: string,\n\toptions?: LsToolOptions,\n): ApexToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = options?.operations ?? defaultLsOperations;\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\tpromptSnippet: lsToolSystemPromptContribution.snippet,\n\t\tparameters: lsSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"allow\",\n\t\t\t\tverb: \"List\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(_toolCallId, input: { path?: string; limit?: number }, signal?: AbortSignal, _onUpdate?, _ctx?) {\n\t\t\tconst { path, limit } = input;\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 prepared = getPreparedPathOperation(input);\n\t\t\t\t\t\tconst dirPath = prepared ? prepared.path.value : 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\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\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()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\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\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\t// The whole listing was read before capping, so the total is already in\n\t\t\t\t\t\t\t// hand. Reporting it is what lets the agent decide whether it has seen\n\t\t\t\t\t\t\t// everything instead of spending a second call to find out.\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${results.length} of ${entries.length} total entries shown. Use limit=${effectiveLimit * 2} for more`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t\tdetails.totalEntries = entries.length;\n\t\t\t\t\t\t}\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\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\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsCall(args, theme, context.cwd));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE9C,OAAO,QAAQ,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,wDAAwD,CAAC;AAG7G,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAExE,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAyB,YAAY,EAAE,MAAM,eAAe,CAAC;AAEnG,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC,CAAC;IACnG,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,oDAAoD,EAAE,CAAC,CAAC;CACxG,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,8BAA8B,GAAG;IAC7C,OAAO,EAAE,yBAAyB;IAClC,UAAU,EAAE,EAAE;CACL,CAAC;AAIX,MAAM,aAAa,GAAG,GAAG,CAAC;AAsB1B,MAAM,mBAAmB,GAAiB;IACzC,MAAM,EAAE,UAAU;IAClB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;CAClB,CAAC;AAOF,SAAS,YAAY,CAAC,IAAmD,EAAE,KAAY,EAAE,GAAW,EAAU;IAC7G,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC;IAC1B,MAAM,WAAW,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,CAAC;IACxF,IAAI,IAAI,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;IACvE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,WAAW,KAAK,GAAG,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,cAAc,CACtB,MAGC,EACD,OAAgC,EAChC,KAAY,EACZ,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,MAAM,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACtF,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC;QACrD,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACnF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,KAAK,iBAAiB,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,CAAC;QACrD,CAAC;IACF,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC;IAC9C,IAAI,UAAU,IAAI,UAAU,EAAE,SAAS,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,UAAU;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,gBAAgB,CAAC,CAAC;QAC7D,IAAI,UAAU,EAAE,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC,QAAQ,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAC1G,IAAI,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,sBAAsB,CACrC,GAAW,EACX,OAAuB,EAC0C;IACjE,MAAM,GAAG,GAAG,OAAO,EAAE,UAAU,IAAI,mBAAmB,CAAC;IACvD,OAAO;QACN,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,8IAA8I,aAAa,eAAe,iBAAiB,GAAG,IAAI,8BAA8B;QAC7O,aAAa,EAAE,8BAA8B,CAAC,OAAO;QACrD,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE;YACT,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;YAClC,UAAU,EAAE,wBAAwB,CAAC;gBACpC,GAAG;gBACH,eAAe,EAAE,OAAO;gBACxB,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI;aAChC,CAAC;YACF,OAAO,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE;YACvD,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE;SACjD;QACD,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,KAAwC,EAAE,MAAoB,EAAE,SAAU,EAAE,IAAK,EAAE;YAC7G,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;YAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;gBACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;oBACvC,OAAO;gBACR,CAAC;gBAED,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;gBAC7D,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;gBAE3D,CAAC,KAAK,IAAI,EAAE,CAAC;oBACZ,IAAI,CAAC;wBACJ,MAAM,QAAQ,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;wBACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;wBAChF,MAAM,cAAc,GAAG,KAAK,IAAI,aAAa,CAAC;wBAE9C,wBAAwB;wBACxB,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;4BAClC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC,CAAC;4BAChD,OAAO;wBACR,CAAC;wBAED,gCAAgC;wBAChC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;wBACrC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;4BACzB,MAAM,CAAC,IAAI,KAAK,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC,CAAC;4BACjD,OAAO;wBACR,CAAC;wBAED,0BAA0B;wBAC1B,IAAI,OAAiB,CAAC;wBACtB,IAAI,CAAC;4BACJ,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;wBACtC,CAAC;wBAAC,OAAO,CAAM,EAAE,CAAC;4BACjB,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;4BACzD,OAAO;wBACR,CAAC;wBAED,yCAAyC;wBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;wBAEvE,4CAA4C;wBAC5C,MAAM,OAAO,GAAa,EAAE,CAAC;wBAC7B,IAAI,iBAAiB,GAAG,KAAK,CAAC;wBAC9B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;4BAC7B,IAAI,OAAO,CAAC,MAAM,IAAI,cAAc,EAAE,CAAC;gCACtC,iBAAiB,GAAG,IAAI,CAAC;gCACzB,MAAM;4BACP,CAAC;4BAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;4BAC/C,IAAI,MAAM,GAAG,EAAE,CAAC;4BAChB,IAAI,CAAC;gCACJ,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gCAC3C,IAAI,SAAS,CAAC,WAAW,EAAE;oCAAE,MAAM,GAAG,GAAG,CAAC;4BAC3C,CAAC;4BAAC,MAAM,CAAC;gCACR,+BAA+B;gCAC/B,SAAS;4BACV,CAAC;4BACD,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;wBAC9B,CAAC;wBAED,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;wBAE9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;4BAC1B,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;4BACxF,OAAO;wBACR,CAAC;wBAED,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;wBACrC,gGAAgG;wBAChG,MAAM,UAAU,GAAG,YAAY,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;wBAClF,IAAI,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC;wBAChC,MAAM,OAAO,GAAkB,EAAE,CAAC;wBAClC,4DAA4D;wBAC5D,MAAM,OAAO,GAAa,EAAE,CAAC;wBAC7B,IAAI,iBAAiB,EAAE,CAAC;4BACvB,wEAAwE;4BACxE,uEAAuE;4BACvE,4DAA4D;4BAC5D,OAAO,CAAC,IAAI,CACX,GAAG,OAAO,CAAC,MAAM,OAAO,OAAO,CAAC,MAAM,mCAAmC,cAAc,GAAG,CAAC,WAAW,CACtG,CAAC;4BACF,OAAO,CAAC,iBAAiB,GAAG,cAAc,CAAC;4BAC3C,OAAO,CAAC,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;wBACvC,CAAC;wBACD,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;4BAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;4BAC/D,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC;wBACjC,CAAC;wBACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BACxB,MAAM,IAAI,QAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;wBACzC,CAAC;wBAED,OAAO,CAAC;4BACP,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4BACzC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;yBAC9D,CAAC,CAAC;oBACJ,CAAC;oBAAC,OAAO,CAAM,EAAE,CAAC;wBACjB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;wBAC9C,MAAM,CAAC,CAAC,CAAC,CAAC;oBACX,CAAC;gBAAA,CACD,CAAC,EAAE,CAAC;YAAA,CACL,CAAC,CAAC;QAAA,CACH;QACD,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAChC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACrD,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;YAC7C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,MAAa,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAChF,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,OAAuB,EAA8B;IAC9F,OAAO,kBAAkB,CAAC,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CAChE","sourcesContent":["import { readdir as fsReaddir, stat as fsStat } from \"node:fs/promises\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport type { AgentTool } from \"apex-code-agent-core\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { formatHiddenLines, previewLineCount } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getPreparedPathOperation } from \"../permissions/operations.ts\";\nimport type { ApexToolDefinition } from \"./contract.ts\";\nimport { createPathPermissionSpec } from \"./path-permission.ts\";\nimport { pathExists, resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, renderToolPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport const lsToolSystemPromptContribution = {\n\tsnippet: \"List directory contents\",\n\tguidelines: [],\n} as const;\n\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\t/** Entries in the directory before the limit was applied. Present only when truncated. */\n\ttotalEntries?: number;\n\tentryLimitReached?: number;\n}\n\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (for example SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file or 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: pathExists,\n\tstat: fsStat,\n\treaddir: fsReaddir,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nfunction formatLsCall(args: { path?: string; limit?: number } | undefined, theme: Theme, cwd: string): string {\n\tconst limit = args?.limit;\n\tconst pathDisplay = renderToolPath(str(args?.path), theme, cwd, { emptyFallback: \".\" });\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${pathDisplay}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : previewLineCount(lines.length, 20);\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - displayLines.length;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `\\n${formatHiddenLines(remaining, \"more\")}`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\tcwd: string,\n\toptions?: LsToolOptions,\n): ApexToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = options?.operations ?? defaultLsOperations;\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\tpromptSnippet: lsToolSystemPromptContribution.snippet,\n\t\tparameters: lsSchema,\n\t\tcontract: {\n\t\t\tcapabilities: new Set([\"fs.read\"]),\n\t\t\tpermission: createPathPermissionSpec({\n\t\t\t\tcwd,\n\t\t\t\tdefaultBehavior: \"allow\",\n\t\t\t\tverb: \"List\",\n\t\t\t\tgetPath: (params) => params.path,\n\t\t\t}),\n\t\t\tcontext: { resultRecoverable: true, deferSchema: true },\n\t\t\tevidence: { emits: new Set(), capture: () => [] },\n\t\t},\n\t\tasync execute(_toolCallId, input: { path?: string; limit?: number }, signal?: AbortSignal, _onUpdate?, _ctx?) {\n\t\t\tconst { path, limit } = input;\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 prepared = getPreparedPathOperation(input);\n\t\t\t\t\t\tconst dirPath = prepared ? prepared.path.value : 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\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\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()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\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\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\t// The whole listing was read before capping, so the total is already in\n\t\t\t\t\t\t\t// hand. Reporting it is what lets the agent decide whether it has seen\n\t\t\t\t\t\t\t// everything instead of spending a second call to find out.\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${results.length} of ${entries.length} total entries shown. Use limit=${effectiveLimit * 2} for more`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t\tdetails.totalEntries = entries.length;\n\t\t\t\t\t\t}\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\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\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsCall(args, theme, context.cwd));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(cwd, options));\n}\n"]}
@@ -22,6 +22,15 @@ export declare class PreparedTargetChangedError extends Error {
22
22
  * replaced parent or a pre-placed symlink is never followed.
23
23
  */
24
24
  export declare function writePreparedPath(operation: PreparedPathOperation, content: string | Buffer): void;
25
+ /**
26
+ * Atomic publish for an ungated write, where no prepared operation exists to pin the target.
27
+ *
28
+ * `writePreparedPath` is the gated path and the one that matters; this is its counterpart for
29
+ * a tool invoked outside the permission gate, so that neither route can leave a half-written
30
+ * file. It makes no containment claim: with no authorization there is no authorized target to
31
+ * hold the write to, and the no-follow walk would have nothing to check against.
32
+ */
33
+ export declare function writePathAtomically(filePath: string, content: string | Buffer): void;
25
34
  export declare function pathExists(filePath: string): Promise<boolean>;
26
35
  export declare function expandPath(filePath: string): string;
27
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-utils.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAI1E,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAazF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,qBAAqB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B5F;AAED,iFAAiF;AACjF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AA2GxD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsClG;AA0BD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAWlE;AAMD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAgCrE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCzF","sourcesContent":["import {\n\taccessSync,\n\tcloseSync,\n\tconstants,\n\texistsSync,\n\tfstatSync,\n\tftruncateSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\trealpathSync,\n\tstatSync,\n\twriteSync,\n} from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, parse, sep } from \"node:path\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { PreparedPathOperation } from \"../permissions/operations.ts\";\n\nconst NARROW_NO_BREAK_SPACE = \"\\u202F\";\n\nexport function preparePathOperation(filePath: string, cwd: string): PreparedPathOperation {\n\tconst canonicalPath = resolveToCwd(filePath, cwd);\n\tif (!isAbsolute(canonicalPath)) throw new Error(\"Canonical path must be absolute\");\n\ttry {\n\t\tconst stats = statSync(canonicalPath);\n\t\treturn {\n\t\t\tkind: \"path-existing\",\n\t\t\tpath: { kind: \"canonical-path\", value: canonicalPath },\n\t\t\tidentity: { device: stats.dev, inode: stats.ino },\n\t\t};\n\t} catch {\n\t\treturn { kind: \"path-new\", path: { kind: \"canonical-path\", value: canonicalPath } };\n\t}\n}\n\n/**\n * Read the authorized target through a no-follow final-component open, verifying\n * the descriptor identity captured at authorization before any byte is returned.\n * A replaced alias or an injected symlink never matches and the read aborts.\n */\nexport function readPreparedPath(operation: PreparedPathOperation, maxBytes?: number): Buffer {\n\tif (operation.kind !== \"path-existing\") {\n\t\tthrow new PreparedTargetChangedError(\"Authorized read target did not exist when permission was checked\");\n\t}\n\treturn withTargetDirectory(operation, { createMissingDirectories: false }, (parent, name) => {\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(parent, name, constants.O_RDONLY | constants.O_NOFOLLOW);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized read target changed before execution\", { cause: error });\n\t\t}\n\t\ttry {\n\t\t\tconst stats = fstatSync(fd);\n\t\t\tif (stats.dev !== operation.identity.device || stats.ino !== operation.identity.inode) {\n\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized read target changed before execution\");\n\t\t\t}\n\t\t\tconst length = maxBytes === undefined ? stats.size : Math.min(stats.size, maxBytes);\n\t\t\tconst buffer = Buffer.alloc(length);\n\t\t\tlet offset = 0;\n\t\t\twhile (offset < buffer.length) {\n\t\t\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, null);\n\t\t\t\tif (read === 0) break;\n\t\t\t\toffset += read;\n\t\t\t}\n\t\t\treturn offset === buffer.length ? buffer : buffer.subarray(0, offset);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t});\n}\n\n/** The authorized target changed between permission evaluation and execution. */\nexport class PreparedTargetChangedError extends Error {}\n\nconst PROC_FD = \"/proc/self/fd\";\nlet procFdAvailable: boolean | undefined;\n\n/** /proc lets an already-open directory fd act as the parent of the next open, so a swapped intermediate directory cannot be traversed. */\nfunction useProcFd(): boolean {\n\tif (procFdAvailable === undefined) {\n\t\ttry {\n\t\t\tprocFdAvailable = existsSync(`${PROC_FD}/self`);\n\t\t} catch {\n\t\t\tprocFdAvailable = false;\n\t\t}\n\t}\n\treturn procFdAvailable;\n}\n\ninterface OpenDirectory {\n\tfd: number;\n\t/** Fallback parent path for platforms without /proc/self/fd. */\n\tpath: string;\n}\n\nfunction openUnder(parent: OpenDirectory, name: string, flags: number, mode?: number): number {\n\tif (useProcFd()) return openSync(`${PROC_FD}/${parent.fd}/${name}`, flags, mode);\n\treturn openSync(join(parent.path, name), flags, mode);\n}\n\nfunction mkdirUnder(parent: OpenDirectory, name: string): void {\n\tif (useProcFd()) mkdirSync(`${PROC_FD}/${parent.fd}/${name}`);\n\telse mkdirSync(join(parent.path, name));\n}\n\nfunction openChildDirectory(parent: OpenDirectory, name: string): OpenDirectory {\n\tconst flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;\n\tconst fd = openUnder(parent, name, flags);\n\tcloseSync(parent.fd);\n\treturn { fd, path: join(parent.path, name) };\n}\n\nfunction writeAll(fd: number, content: string | Buffer): void {\n\tconst buffer = typeof content === \"string\" ? Buffer.from(content, \"utf-8\") : content;\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n}\n\nfunction isMissingEntryError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === \"object\" && error !== null && \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\"\n\t);\n}\n\n/**\n * Walk every directory component of the authorized path no-follow, holding one\n * directory fd across the walk, then run the caller's final-component step against\n * that walked parent. Any intermediate that is not a real directory — a swapped\n * symlink, a removed component — aborts before the target is touched.\n */\nfunction withTargetDirectory<T>(\n\toperation: PreparedPathOperation,\n\toptions: { createMissingDirectories: boolean },\n\trun: (parent: OpenDirectory, name: string) => T,\n): T {\n\tconst root = parse(operation.path.value).root;\n\tconst components = operation.path.value\n\t\t.slice(root.length)\n\t\t.split(sep)\n\t\t.filter((component) => component.length > 0);\n\tconst name = components[components.length - 1];\n\tif (name === undefined) throw new PreparedTargetChangedError(\"Authorized path has no final component\");\n\tlet parent: OpenDirectory = { fd: openSync(root, constants.O_RDONLY | constants.O_DIRECTORY), path: root };\n\tconst closeParent = (): void => {\n\t\ttry {\n\t\t\tcloseSync(parent.fd);\n\t\t} catch {}\n\t};\n\tfor (const component of components.slice(0, -1)) {\n\t\ttry {\n\t\t\tparent = openChildDirectory(parent, component);\n\t\t} catch (error) {\n\t\t\tif (options.createMissingDirectories && isMissingEntryError(error)) {\n\t\t\t\ttry {\n\t\t\t\t\tmkdirUnder(parent, component);\n\t\t\t\t\tparent = openChildDirectory(parent, component);\n\t\t\t\t\tcontinue;\n\t\t\t\t} catch (mkdirError) {\n\t\t\t\t\tcloseParent();\n\t\t\t\t\tthrow new PreparedTargetChangedError(`Authorized write parent \"${component}\" changed before execution`, {\n\t\t\t\t\t\tcause: mkdirError,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tcloseParent();\n\t\t\tthrow new PreparedTargetChangedError(`Authorized path component \"${component}\" changed before execution`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t}\n\ttry {\n\t\treturn run(parent, name);\n\t} finally {\n\t\tcloseParent();\n\t}\n}\n\n/**\n * Execute an authorized write against the prepared target.\n *\n * - `path-existing`: the final component is opened `O_NOFOLLOW`, its descriptor\n * identity is compared with the identity captured at authorization, and only\n * then is the file truncated and written through that descriptor. A swapped\n * alias, an injected symlink, or a redirected parent never matches and the\n * write aborts before any byte changes.\n * - `path-new`: missing parents are created no-follow under the walked directory\n * descriptor, and the file is created `O_CREAT | O_EXCL | O_NOFOLLOW`, so a\n * replaced parent or a pre-placed symlink is never followed.\n */\nexport function writePreparedPath(operation: PreparedPathOperation, content: string | Buffer): void {\n\twithTargetDirectory(operation, { createMissingDirectories: operation.kind === \"path-new\" }, (parent, name) => {\n\t\tif (operation.kind === \"path-existing\") {\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);\n\t\t\t} catch (error) {\n\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst stats = fstatSync(fd);\n\t\t\t\tif (stats.dev !== operation.identity.device || stats.ino !== operation.identity.inode) {\n\t\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\");\n\t\t\t\t}\n\t\t\t\tftruncateSync(fd, 0);\n\t\t\t\twriteAll(fd, content);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(\n\t\t\t\tparent,\n\t\t\t\tname,\n\t\t\t\tconstants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,\n\t\t\t\t0o600,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized new-file target changed before execution\", { cause: error });\n\t\t}\n\t\ttry {\n\t\t\twriteAll(fd, content);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t});\n}\n\nfunction tryMacOSScreenshotPath(filePath: string): string {\n\treturn filePath.replace(/ (AM|PM)\\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);\n}\n\nfunction tryNFDVariant(filePath: string): string {\n\t// macOS stores filenames in NFD (decomposed) form, try converting user input to NFD\n\treturn filePath.normalize(\"NFD\");\n}\n\nfunction tryCurlyQuoteVariant(filePath: string): string {\n\t// macOS uses U+2019 (right single quotation mark) in screenshot names like \"Capture d'écran\"\n\t// Users typically type U+0027 (straight apostrophe)\n\treturn filePath.replace(/'/g, \"\\u2019\");\n}\n\nfunction fileExists(filePath: string): boolean {\n\ttry {\n\t\taccessSync(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport async function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function expandPath(filePath: string): string {\n\treturn normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n}\n\n/**\n * Resolve a path relative to the given cwd, handling ~ expansion and absolute\n * paths. The result is the single target used by both permission matching and\n * filesystem execution.\n */\nexport function resolveToCwd(filePath: string, cwd: string): string {\n\tconst requested = resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n\ttry {\n\t\treturn realpathSync(requested);\n\t} catch {\n\t\ttry {\n\t\t\treturn joinCanonicalParent(requested);\n\t\t} catch {\n\t\t\treturn requested;\n\t\t}\n\t}\n}\n\nfunction joinCanonicalParent(requested: string): string {\n\treturn join(realpathSync(dirname(requested)), basename(requested));\n}\n\nexport function resolveReadPath(filePath: string, cwd: string): string {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (fileExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && fileExists(amPmVariant)) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && fileExists(nfdVariant)) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && fileExists(curlyVariant)) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n\nexport async function resolveReadPathAsync(filePath: string, cwd: string): Promise<string> {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (await pathExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && (await pathExists(amPmVariant))) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && (await pathExists(nfdVariant))) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && (await pathExists(curlyVariant))) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n"]}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-utils.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAgB,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAIxF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAazF;AAwBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,qBAAqB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5F;AAED,iFAAiF;AACjF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAkPxD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgDlG;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwCpF;AA0BD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAWlE;AAMD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAgCrE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCzF","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Stats } from \"node:fs\";\nimport {\n\taccessSync,\n\tcloseSync,\n\tconstants,\n\texistsSync,\n\tfchmodSync,\n\tfstatSync,\n\tftruncateSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\trealpathSync,\n\trenameSync,\n\tstatSync,\n\tunlinkSync,\n\twriteSync,\n} from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, parse, sep } from \"node:path\";\nimport { normalizePath, resolvePath } from \"../../utils/paths.ts\";\nimport type { FileIdentity, PreparedPathOperation } from \"../permissions/operations.ts\";\n\nconst NARROW_NO_BREAK_SPACE = \"\\u202F\";\n\nexport function preparePathOperation(filePath: string, cwd: string): PreparedPathOperation {\n\tconst canonicalPath = resolveToCwd(filePath, cwd);\n\tif (!isAbsolute(canonicalPath)) throw new Error(\"Canonical path must be absolute\");\n\ttry {\n\t\tconst stats = statSync(canonicalPath);\n\t\treturn {\n\t\t\tkind: \"path-existing\",\n\t\t\tpath: { kind: \"canonical-path\", value: canonicalPath },\n\t\t\tidentity: { device: stats.dev, inode: stats.ino },\n\t\t};\n\t} catch {\n\t\treturn { kind: \"path-new\", path: { kind: \"canonical-path\", value: canonicalPath } };\n\t}\n}\n\n/**\n * Verify a descriptor's identity against the one captured at authorization.\n *\n * A zero inode is not an identity. POSIX reserves inode 0 for \"no file\", and Node reports 0\n * on Windows when the file index is unavailable for the handle. Comparing two of them\n * succeeds trivially, which would turn this check -- the only thing standing between an\n * authorized target and one swapped underneath it -- into a no-op at exactly the moment the\n * platform could not say what we are holding. An unverifiable target is not an authorized\n * one, so refuse rather than proceed.\n *\n * The mismatched case keeps its own wording: \"changed\" and \"could not be verified\" are\n * different facts, and an operator reading the refusal should be able to tell them apart.\n */\nfunction verifyPreparedIdentity(subject: \"read\" | \"write\", stats: Stats, identity: FileIdentity): void {\n\tif (stats.ino === 0 || identity.inode === 0) {\n\t\tthrow new PreparedTargetChangedError(`Authorized ${subject} target identity could not be verified`);\n\t}\n\tif (stats.dev !== identity.device || stats.ino !== identity.inode) {\n\t\tthrow new PreparedTargetChangedError(`Authorized ${subject} target changed before execution`);\n\t}\n}\n\n/**\n * Read the authorized target through a no-follow final-component open, verifying\n * the descriptor identity captured at authorization before any byte is returned.\n * A replaced alias or an injected symlink never matches and the read aborts.\n */\nexport function readPreparedPath(operation: PreparedPathOperation, maxBytes?: number): Buffer {\n\tif (operation.kind !== \"path-existing\") {\n\t\tthrow new PreparedTargetChangedError(\"Authorized read target did not exist when permission was checked\");\n\t}\n\treturn withTargetDirectory(operation, { createMissingDirectories: false }, (parent, name) => {\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(parent, name, constants.O_RDONLY | constants.O_NOFOLLOW);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized read target changed before execution\", { cause: error });\n\t\t}\n\t\ttry {\n\t\t\tconst stats = fstatSync(fd);\n\t\t\tverifyPreparedIdentity(\"read\", stats, operation.identity);\n\t\t\tconst length = maxBytes === undefined ? stats.size : Math.min(stats.size, maxBytes);\n\t\t\tconst buffer = Buffer.alloc(length);\n\t\t\tlet offset = 0;\n\t\t\twhile (offset < buffer.length) {\n\t\t\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, null);\n\t\t\t\tif (read === 0) break;\n\t\t\t\toffset += read;\n\t\t\t}\n\t\t\treturn offset === buffer.length ? buffer : buffer.subarray(0, offset);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t});\n}\n\n/** The authorized target changed between permission evaluation and execution. */\nexport class PreparedTargetChangedError extends Error {}\n\nconst PROC_FD = \"/proc/self/fd\";\nlet procFdAvailable: boolean | undefined;\n\n/** /proc lets an already-open directory fd act as the parent of the next open, so a swapped intermediate directory cannot be traversed. */\nfunction useProcFd(): boolean {\n\tif (procFdAvailable === undefined) {\n\t\ttry {\n\t\t\tprocFdAvailable = existsSync(`${PROC_FD}/self`);\n\t\t} catch {\n\t\t\tprocFdAvailable = false;\n\t\t}\n\t}\n\treturn procFdAvailable;\n}\n\ninterface OpenDirectory {\n\tfd: number;\n\t/** Fallback parent path for platforms without /proc/self/fd. */\n\tpath: string;\n}\n\nfunction openUnder(parent: OpenDirectory, name: string, flags: number, mode?: number): number {\n\tif (useProcFd()) return openSync(`${PROC_FD}/${parent.fd}/${name}`, flags, mode);\n\treturn openSync(join(parent.path, name), flags, mode);\n}\n\nfunction renameUnder(parent: OpenDirectory, from: string, to: string): void {\n\tif (useProcFd()) renameSync(`${PROC_FD}/${parent.fd}/${from}`, `${PROC_FD}/${parent.fd}/${to}`);\n\telse renameSync(join(parent.path, from), join(parent.path, to));\n}\n\nfunction unlinkUnder(parent: OpenDirectory, name: string): void {\n\ttry {\n\t\tif (useProcFd()) unlinkSync(`${PROC_FD}/${parent.fd}/${name}`);\n\t\telse unlinkSync(join(parent.path, name));\n\t} catch {}\n}\n\n/**\n * The name of the sibling a publish stages into.\n *\n * It carries enough of the destination to be recognizable if a crash ever strands one, and\n * a pid and a random suffix so two publishes in one directory cannot collide. The base is\n * truncated to fit: a destination already at the filesystem's per-component limit would\n * otherwise produce a staging name past it, and fail a write that truncate-in-place handled.\n * 255 is the limit on every filesystem this runs on that has one at all.\n */\nfunction stagingName(name: string): string {\n\tconst suffix = `.apex-${process.pid}-${randomUUID().slice(0, 8)}`;\n\tconst budget = 255 - suffix.length - 1;\n\treturn `.${name.slice(0, Math.max(1, budget))}${suffix}`;\n}\n\n/**\n * Write `content` to a sibling of `name` and rename it over `name`.\n *\n * The rename is the publish. Until it runs, `name` still refers to the old inode with all\n * of its old bytes, so a crash, a full disk, or a kill mid-write destroys nothing. The\n * caller has already proved that `name` refers to the authorized target; this only decides\n * how the bytes arrive.\n *\n * `mode` is the destination's, captured from the descriptor the identity check used, so a\n * publish does not quietly reset a file's permissions to the temp file's. Ownership is not\n * carried: a rename installs a file this process created, so a target owned by someone else\n * and merely writable by us changes owner. That is inherent to publishing by rename and is\n * the same trade the session store already makes.\n *\n * One guarantee narrows, and it is worth stating plainly rather than leaving for a reader to\n * derive. ADR 0029's identity check still runs and still aborts on a mismatch, but it now\n * proves the name referred to the authorized file at the moment of the check rather than at\n * the moment of the write. Writing through the checked descriptor could not be redirected by\n * a later swap; a rename can be, because it resolves the name again. The window is between\n * the identity check and the rename, both anchored to the same validated parent descriptor,\n * and exploiting it needs a local attacker placing a file at that exact name in a directory\n * the call was already authorized to write. That is traded for crash safety, which is not a\n * race: it is what every interrupted write did.\n *\n * Windows refuses a rename onto a destination another handle holds open, and an ordinary read\n * handle is enough to trigger it. An earlier version of this comment called that an acceptable\n * trade. Three-OS CI disproved it: the case is an editor, a watcher, or a language server with\n * the file open, which is the common case rather than an exotic one, and failing the write\n * there is not a trade a user opted into. It falls back to `writeInPlace` instead, so Windows\n * gets atomic publish whenever it can and a working write when it cannot.\n */\nfunction publishByRename(parent: OpenDirectory, name: string, content: string | Buffer, mode: number): boolean {\n\tconst temporary = stagingName(name);\n\tlet fd: number;\n\ttry {\n\t\tfd = openUnder(\n\t\t\tparent,\n\t\t\ttemporary,\n\t\t\tconstants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,\n\t\t\tmode,\n\t\t);\n\t} catch (error) {\n\t\tthrow new PreparedTargetChangedError(\"Authorized write could not stage its replacement\", { cause: error });\n\t}\n\ttry {\n\t\twriteAll(fd, content);\n\t\t// The create mode passes through umask; the destination's mode does not.\n\t\tfchmodSync(fd, mode);\n\t\tcloseSync(fd);\n\t} catch (error) {\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {}\n\t\tunlinkUnder(parent, temporary);\n\t\tthrow error;\n\t}\n\ttry {\n\t\trenameUnder(parent, temporary, name);\n\t\treturn true;\n\t} catch (error) {\n\t\tunlinkUnder(parent, temporary);\n\t\tif (isWindowsSharingViolation(error)) return false;\n\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t}\n}\n\n/**\n * Windows refuses to rename onto a destination another handle holds open, and an ordinary\n * read handle is enough. An editor, a watcher, or a language server with the file open is\n * the common case, not an exotic one, so a rename that cannot land there has to fall back\n * rather than fail the write.\n *\n * Three OS CI found this. The Linux and macOS jobs pass, because POSIX renames over an open\n * file happily.\n */\nfunction isWindowsSharingViolation(error: unknown): boolean {\n\tif (process.platform !== \"win32\") return false;\n\tconst code = (error as { code?: unknown } | null)?.code;\n\treturn code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n}\n\n/**\n * The pre-rename publish, kept for the one case that cannot use a rename.\n *\n * This is the truncate-then-write window the rename exists to close, so it runs only when\n * Windows has refused the rename. Crash safety is lost for that write; the alternative is\n * refusing to write a file something else has open, which is worse and is not a trade a\n * user opted into. The identity check is re-run against a freshly opened descriptor, so the\n * weaker durability does not come with a weaker target guarantee.\n */\nfunction writeInPlace(parent: OpenDirectory, name: string, content: string | Buffer, identity?: FileIdentity): void {\n\tlet fd: number;\n\ttry {\n\t\tfd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);\n\t} catch (error) {\n\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t}\n\ttry {\n\t\tif (identity) {\n\t\t\tverifyPreparedIdentity(\"write\", fstatSync(fd), identity);\n\t\t}\n\t\tftruncateSync(fd, 0);\n\t\twriteAll(fd, content);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n}\n\nfunction mkdirUnder(parent: OpenDirectory, name: string): void {\n\tif (useProcFd()) mkdirSync(`${PROC_FD}/${parent.fd}/${name}`);\n\telse mkdirSync(join(parent.path, name));\n}\n\nfunction openChildDirectory(parent: OpenDirectory, name: string): OpenDirectory {\n\tconst flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;\n\tconst fd = openUnder(parent, name, flags);\n\tcloseSync(parent.fd);\n\treturn { fd, path: join(parent.path, name) };\n}\n\nfunction writeAll(fd: number, content: string | Buffer): void {\n\tconst buffer = typeof content === \"string\" ? Buffer.from(content, \"utf-8\") : content;\n\tlet written = 0;\n\twhile (written < buffer.length) {\n\t\twritten += writeSync(fd, buffer, written, buffer.length - written);\n\t}\n}\n\nfunction isMissingEntryError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === \"object\" && error !== null && \"code\" in error && (error as { code?: unknown }).code === \"ENOENT\"\n\t);\n}\n\n/**\n * Walk every directory component of the authorized path no-follow, holding one\n * directory fd across the walk, then run the caller's final-component step against\n * that walked parent. Any intermediate that is not a real directory — a swapped\n * symlink, a removed component — aborts before the target is touched.\n */\nfunction withTargetDirectory<T>(\n\toperation: PreparedPathOperation,\n\toptions: { createMissingDirectories: boolean },\n\trun: (parent: OpenDirectory, name: string) => T,\n): T {\n\tconst root = parse(operation.path.value).root;\n\tconst components = operation.path.value\n\t\t.slice(root.length)\n\t\t.split(sep)\n\t\t.filter((component) => component.length > 0);\n\tconst name = components[components.length - 1];\n\tif (name === undefined) throw new PreparedTargetChangedError(\"Authorized path has no final component\");\n\tlet parent: OpenDirectory = { fd: openSync(root, constants.O_RDONLY | constants.O_DIRECTORY), path: root };\n\tconst closeParent = (): void => {\n\t\ttry {\n\t\t\tcloseSync(parent.fd);\n\t\t} catch {}\n\t};\n\tfor (const component of components.slice(0, -1)) {\n\t\ttry {\n\t\t\tparent = openChildDirectory(parent, component);\n\t\t} catch (error) {\n\t\t\tif (options.createMissingDirectories && isMissingEntryError(error)) {\n\t\t\t\ttry {\n\t\t\t\t\tmkdirUnder(parent, component);\n\t\t\t\t\tparent = openChildDirectory(parent, component);\n\t\t\t\t\tcontinue;\n\t\t\t\t} catch (mkdirError) {\n\t\t\t\t\tcloseParent();\n\t\t\t\t\tthrow new PreparedTargetChangedError(`Authorized write parent \"${component}\" changed before execution`, {\n\t\t\t\t\t\tcause: mkdirError,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tcloseParent();\n\t\t\tthrow new PreparedTargetChangedError(`Authorized path component \"${component}\" changed before execution`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t}\n\ttry {\n\t\treturn run(parent, name);\n\t} finally {\n\t\tcloseParent();\n\t}\n}\n\n/**\n * Execute an authorized write against the prepared target.\n *\n * - `path-existing`: the final component is opened `O_NOFOLLOW`, its descriptor\n * identity is compared with the identity captured at authorization, and only\n * then is the file truncated and written through that descriptor. A swapped\n * alias, an injected symlink, or a redirected parent never matches and the\n * write aborts before any byte changes.\n * - `path-new`: missing parents are created no-follow under the walked directory\n * descriptor, and the file is created `O_CREAT | O_EXCL | O_NOFOLLOW`, so a\n * replaced parent or a pre-placed symlink is never followed.\n */\nexport function writePreparedPath(operation: PreparedPathOperation, content: string | Buffer): void {\n\twithTargetDirectory(operation, { createMissingDirectories: operation.kind === \"path-new\" }, (parent, name) => {\n\t\tif (operation.kind === \"path-existing\") {\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openUnder(parent, name, constants.O_WRONLY | constants.O_NOFOLLOW);\n\t\t\t} catch (error) {\n\t\t\t\tthrow new PreparedTargetChangedError(\"Authorized write target changed before execution\", { cause: error });\n\t\t\t}\n\t\t\tlet mode: number;\n\t\t\ttry {\n\t\t\t\tconst stats = fstatSync(fd);\n\t\t\t\tverifyPreparedIdentity(\"write\", stats, operation.identity);\n\t\t\t\tmode = stats.mode & 0o7777;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tif (!publishByRename(parent, name, content, mode)) {\n\t\t\t\twriteInPlace(parent, name, content, operation.identity);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// The exclusive create is what proves the name was still absent, which is the whole\n\t\t// authorization for a new-file write. It reserves the name; the rename below then\n\t\t// replaces that empty placeholder, so the name never holds a half-written file.\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openUnder(\n\t\t\t\tparent,\n\t\t\t\tname,\n\t\t\t\tconstants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,\n\t\t\t\t0o600,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow new PreparedTargetChangedError(\"Authorized new-file target changed before execution\", { cause: error });\n\t\t}\n\t\tcloseSync(fd);\n\t\ttry {\n\t\t\t// The reservation this just created is ours, so there is no prior identity to\n\t\t\t// re-check if Windows refuses the rename onto it.\n\t\t\tif (!publishByRename(parent, name, content, 0o600)) {\n\t\t\t\twriteInPlace(parent, name, content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tunlinkUnder(parent, name);\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\n/**\n * Atomic publish for an ungated write, where no prepared operation exists to pin the target.\n *\n * `writePreparedPath` is the gated path and the one that matters; this is its counterpart for\n * a tool invoked outside the permission gate, so that neither route can leave a half-written\n * file. It makes no containment claim: with no authorization there is no authorized target to\n * hold the write to, and the no-follow walk would have nothing to check against.\n */\nexport function writePathAtomically(filePath: string, content: string | Buffer): void {\n\tconst temporary = join(dirname(filePath), stagingName(basename(filePath)));\n\t// Only an existing destination has a mode worth carrying. For a new file there is\n\t// nothing to capture, and chmod'ing to the 0o666 open default would force past the\n\t// caller's umask and publish a world-writable file, which `writeFile` never did.\n\tlet destinationMode: number | undefined;\n\ttry {\n\t\tdestinationMode = statSync(filePath).mode & 0o7777;\n\t} catch {}\n\tconst fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, destinationMode ?? 0o666);\n\ttry {\n\t\twriteAll(fd, content);\n\t\tif (destinationMode !== undefined) fchmodSync(fd, destinationMode);\n\t\tcloseSync(fd);\n\t} catch (error) {\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {}\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {}\n\t\tthrow error;\n\t}\n\ttry {\n\t\trenameSync(temporary, filePath);\n\t} catch (error) {\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {}\n\t\t// Same Windows sharing case as the gated path, and the same answer: a file something\n\t\t// else has open is still a file the caller asked to write.\n\t\tif (!isWindowsSharingViolation(error)) throw error;\n\t\tconst fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT, destinationMode ?? 0o666);\n\t\ttry {\n\t\t\tftruncateSync(fd, 0);\n\t\t\twriteAll(fd, content);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t}\n}\n\nfunction tryMacOSScreenshotPath(filePath: string): string {\n\treturn filePath.replace(/ (AM|PM)\\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);\n}\n\nfunction tryNFDVariant(filePath: string): string {\n\t// macOS stores filenames in NFD (decomposed) form, try converting user input to NFD\n\treturn filePath.normalize(\"NFD\");\n}\n\nfunction tryCurlyQuoteVariant(filePath: string): string {\n\t// macOS uses U+2019 (right single quotation mark) in screenshot names like \"Capture d'écran\"\n\t// Users typically type U+0027 (straight apostrophe)\n\treturn filePath.replace(/'/g, \"\\u2019\");\n}\n\nfunction fileExists(filePath: string): boolean {\n\ttry {\n\t\taccessSync(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport async function pathExists(filePath: string): Promise<boolean> {\n\ttry {\n\t\tawait access(filePath, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function expandPath(filePath: string): string {\n\treturn normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n}\n\n/**\n * Resolve a path relative to the given cwd, handling ~ expansion and absolute\n * paths. The result is the single target used by both permission matching and\n * filesystem execution.\n */\nexport function resolveToCwd(filePath: string, cwd: string): string {\n\tconst requested = resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });\n\ttry {\n\t\treturn realpathSync(requested);\n\t} catch {\n\t\ttry {\n\t\t\treturn joinCanonicalParent(requested);\n\t\t} catch {\n\t\t\treturn requested;\n\t\t}\n\t}\n}\n\nfunction joinCanonicalParent(requested: string): string {\n\treturn join(realpathSync(dirname(requested)), basename(requested));\n}\n\nexport function resolveReadPath(filePath: string, cwd: string): string {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (fileExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && fileExists(amPmVariant)) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && fileExists(nfdVariant)) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && fileExists(curlyVariant)) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n\nexport async function resolveReadPathAsync(filePath: string, cwd: string): Promise<string> {\n\tconst resolved = resolveToCwd(filePath, cwd);\n\n\tif (await pathExists(resolved)) {\n\t\treturn resolved;\n\t}\n\n\t// Try macOS AM/PM variant (narrow no-break space before AM/PM)\n\tconst amPmVariant = tryMacOSScreenshotPath(resolved);\n\tif (amPmVariant !== resolved && (await pathExists(amPmVariant))) {\n\t\treturn amPmVariant;\n\t}\n\n\t// Try NFD variant (macOS stores filenames in NFD form)\n\tconst nfdVariant = tryNFDVariant(resolved);\n\tif (nfdVariant !== resolved && (await pathExists(nfdVariant))) {\n\t\treturn nfdVariant;\n\t}\n\n\t// Try curly quote variant (macOS uses U+2019 in screenshot names)\n\tconst curlyVariant = tryCurlyQuoteVariant(resolved);\n\tif (curlyVariant !== resolved && (await pathExists(curlyVariant))) {\n\t\treturn curlyVariant;\n\t}\n\n\t// Try combined NFD + curly quote (for French macOS screenshots like \"Capture d'écran\")\n\tconst nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);\n\tif (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) {\n\t\treturn nfdCurlyVariant;\n\t}\n\n\treturn resolved;\n}\n"]}