@oh-my-pi/pi-coding-agent 7.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (503) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/docs/hooks.md +2 -2
  4. package/docs/sdk.md +1 -1
  5. package/package.json +12 -11
  6. package/scripts/format-prompts.ts +143 -0
  7. package/scripts/generate-template.ts +1 -1
  8. package/src/cli/args.ts +3 -3
  9. package/src/cli/config-cli.ts +4 -4
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/list-models.ts +2 -2
  12. package/src/cli/plugin-cli.ts +3 -3
  13. package/src/cli/session-picker.ts +2 -2
  14. package/src/cli/setup-cli.ts +2 -2
  15. package/src/cli/stats-cli.ts +1 -1
  16. package/src/cli/update-cli.ts +2 -2
  17. package/src/{core → config}/keybindings.ts +1 -1
  18. package/src/{core → config}/model-registry.ts +1 -1
  19. package/src/{core → config}/model-resolver.ts +3 -3
  20. package/src/{core → config}/prompt-templates.ts +2 -2
  21. package/src/{core → config}/settings-manager.ts +6 -6
  22. package/src/{core/cursor/exec-bridge.ts → cursor.ts} +4 -4
  23. package/src/discovery/agents-md.ts +4 -4
  24. package/src/discovery/builtin.ts +17 -17
  25. package/src/discovery/claude.ts +12 -12
  26. package/src/discovery/cline.ts +6 -6
  27. package/src/discovery/codex.ts +21 -21
  28. package/src/discovery/cursor.ts +9 -9
  29. package/src/discovery/gemini.ts +9 -9
  30. package/src/discovery/github.ts +6 -6
  31. package/src/discovery/helpers.ts +4 -4
  32. package/src/discovery/index.ts +16 -16
  33. package/src/discovery/mcp-json.ts +4 -4
  34. package/src/discovery/ssh.ts +4 -4
  35. package/src/discovery/vscode.ts +4 -4
  36. package/src/discovery/windsurf.ts +6 -6
  37. package/src/{core/tools/exa → exa}/company.ts +2 -3
  38. package/src/{core/tools/exa → exa}/index.ts +2 -2
  39. package/src/{core/tools/exa → exa}/linkedin.ts +2 -3
  40. package/src/{core/tools/exa → exa}/mcp-client.ts +2 -2
  41. package/src/{core/tools/exa → exa}/render.ts +3 -3
  42. package/src/{core/tools/exa → exa}/researcher.ts +1 -1
  43. package/src/{core/tools/exa → exa}/search.ts +2 -10
  44. package/src/{core/tools/exa → exa}/websets.ts +1 -1
  45. package/src/{core → exec}/bash-executor.ts +22 -6
  46. package/src/{core → export}/custom-share.ts +1 -1
  47. package/src/{core/export-html → export/html}/index.ts +3 -3
  48. package/src/{core → export}/ttsr.ts +2 -2
  49. package/src/{core → extensibility}/custom-commands/bundled/review/index.ts +4 -4
  50. package/src/{core → extensibility}/custom-commands/loader.ts +3 -3
  51. package/src/{core → extensibility}/custom-commands/types.ts +1 -1
  52. package/src/{core → extensibility}/custom-tools/loader.ts +9 -9
  53. package/src/{core → extensibility}/custom-tools/types.ts +6 -6
  54. package/src/{core → extensibility}/custom-tools/wrapper.ts +1 -1
  55. package/src/{core → extensibility}/extensions/loader.ts +8 -8
  56. package/src/{core → extensibility}/extensions/runner.ts +3 -3
  57. package/src/{core → extensibility}/extensions/types.ts +15 -15
  58. package/src/{core → extensibility}/extensions/wrapper.ts +1 -1
  59. package/src/{core → extensibility}/hooks/index.ts +1 -1
  60. package/src/{core → extensibility}/hooks/loader.ts +7 -7
  61. package/src/{core → extensibility}/hooks/runner.ts +4 -4
  62. package/src/{core → extensibility}/hooks/types.ts +9 -9
  63. package/src/{core → extensibility}/plugins/doctor.ts +1 -1
  64. package/src/{core → extensibility}/plugins/installer.ts +2 -3
  65. package/src/{core → extensibility}/plugins/paths.ts +1 -1
  66. package/src/{core → extensibility}/skills.ts +8 -48
  67. package/src/{core → extensibility}/slash-commands.ts +6 -6
  68. package/src/index.ts +127 -128
  69. package/src/internal-urls/agent-protocol.ts +126 -0
  70. package/src/internal-urls/artifact-protocol.ts +93 -0
  71. package/src/internal-urls/index.ts +28 -0
  72. package/src/internal-urls/json-query.ts +126 -0
  73. package/src/internal-urls/router.ts +69 -0
  74. package/src/internal-urls/rule-protocol.ts +56 -0
  75. package/src/internal-urls/skill-protocol.ts +112 -0
  76. package/src/internal-urls/types.ts +48 -0
  77. package/src/{core/python-executor.ts → ipy/executor.ts} +51 -11
  78. package/src/{core/python-gateway-coordinator.ts → ipy/gateway-coordinator.ts} +41 -325
  79. package/src/{core/python-kernel.ts → ipy/kernel.ts} +38 -10
  80. package/src/ipy/prelude.ts +3 -0
  81. package/src/{core/tools/lsp → lsp}/client.ts +7 -6
  82. package/src/{core/tools/lsp → lsp}/clients/biome-client.ts +1 -1
  83. package/src/{core/tools/lsp → lsp}/clients/index.ts +1 -1
  84. package/src/{core/tools/lsp → lsp}/clients/lsp-linter-client.ts +4 -4
  85. package/src/{core/tools/lsp → lsp}/config.ts +1 -1
  86. package/src/{core/tools/lsp → lsp}/index.ts +16 -15
  87. package/src/{core/tools/lsp → lsp}/render.ts +2 -2
  88. package/src/{core/tools/lsp → lsp}/types.ts +14 -16
  89. package/src/{core/tools/lsp → lsp}/utils.ts +1 -1
  90. package/src/main.ts +12 -12
  91. package/src/{core/mcp → mcp}/config.ts +8 -8
  92. package/src/{core/mcp → mcp}/loader.ts +5 -6
  93. package/src/{core/mcp → mcp}/manager.ts +2 -2
  94. package/src/{core/mcp → mcp}/tool-bridge.ts +35 -6
  95. package/src/{core/mcp → mcp}/tool-cache.ts +1 -1
  96. package/src/{core/mcp → mcp}/transports/http.ts +7 -1
  97. package/src/{core/mcp → mcp}/transports/stdio.ts +1 -1
  98. package/src/{core/mcp → mcp}/types.ts +1 -1
  99. package/src/migrations.ts +2 -2
  100. package/src/modes/{interactive/components → components}/armin.ts +1 -1
  101. package/src/modes/{interactive/components → components}/assistant-message.ts +1 -1
  102. package/src/modes/{interactive/components → components}/bash-execution.ts +37 -29
  103. package/src/modes/{interactive/components → components}/bordered-loader.ts +1 -1
  104. package/src/modes/{interactive/components → components}/branch-summary-message.ts +2 -2
  105. package/src/modes/{interactive/components → components}/compaction-summary-message.ts +2 -2
  106. package/src/modes/{interactive/components → components}/custom-message.ts +3 -3
  107. package/src/modes/{interactive/components → components}/diff.ts +1 -1
  108. package/src/modes/{interactive/components → components}/dynamic-border.ts +1 -1
  109. package/src/modes/{interactive/components → components}/extensions/extension-dashboard.ts +3 -3
  110. package/src/modes/{interactive/components → components}/extensions/extension-list.ts +2 -2
  111. package/src/modes/{interactive/components → components}/extensions/inspector-panel.ts +1 -1
  112. package/src/modes/{interactive/components → components}/extensions/state-manager.ts +11 -17
  113. package/src/modes/{interactive/components → components}/extensions/types.ts +1 -1
  114. package/src/modes/{interactive/components → components}/footer.ts +3 -3
  115. package/src/modes/{interactive/components → components}/history-search.ts +2 -2
  116. package/src/modes/{interactive/components → components}/hook-editor.ts +1 -1
  117. package/src/modes/{interactive/components → components}/hook-input.ts +1 -1
  118. package/src/modes/{interactive/components → components}/hook-message.ts +3 -3
  119. package/src/modes/{interactive/components → components}/hook-selector.ts +1 -1
  120. package/src/modes/{interactive/components → components}/keybinding-hints.ts +2 -2
  121. package/src/modes/{interactive/components → components}/login-dialog.ts +1 -1
  122. package/src/modes/{interactive/components → components}/model-selector.ts +5 -5
  123. package/src/modes/{interactive/components → components}/oauth-selector.ts +2 -2
  124. package/src/modes/{interactive/components → components}/plugin-settings.ts +3 -3
  125. package/src/modes/{interactive/components → components}/python-execution.ts +35 -24
  126. package/src/modes/{interactive/components → components}/queue-mode-selector.ts +1 -1
  127. package/src/modes/{interactive/components → components}/read-tool-group.ts +2 -2
  128. package/src/modes/{interactive/components → components}/session-selector.ts +3 -3
  129. package/src/modes/{interactive/components → components}/settings-defs.ts +2 -2
  130. package/src/modes/{interactive/components → components}/settings-selector.ts +2 -2
  131. package/src/modes/{interactive/components → components}/show-images-selector.ts +1 -1
  132. package/src/modes/{interactive/components → components}/status-line/segments.ts +2 -2
  133. package/src/modes/{interactive/components → components}/status-line/separators.ts +1 -1
  134. package/src/modes/{interactive/components → components}/status-line/types.ts +2 -2
  135. package/src/modes/{interactive/components → components}/status-line-segment-editor.ts +2 -2
  136. package/src/modes/{interactive/components → components}/status-line.ts +3 -3
  137. package/src/modes/{interactive/components → components}/theme-selector.ts +1 -1
  138. package/src/modes/{interactive/components → components}/thinking-selector.ts +1 -1
  139. package/src/modes/{interactive/components → components}/todo-display.ts +3 -4
  140. package/src/modes/{interactive/components → components}/todo-reminder.ts +2 -2
  141. package/src/modes/{interactive/components → components}/tool-execution.ts +8 -8
  142. package/src/modes/{interactive/components → components}/tree-selector.ts +3 -3
  143. package/src/modes/{interactive/components → components}/ttsr-notification.ts +2 -2
  144. package/src/modes/{interactive/components → components}/user-message-selector.ts +1 -1
  145. package/src/modes/{interactive/components → components}/user-message.ts +1 -1
  146. package/src/modes/{interactive/components → components}/welcome.ts +2 -2
  147. package/src/modes/{interactive/controllers → controllers}/command-controller.ts +32 -30
  148. package/src/modes/{interactive/controllers → controllers}/event-controller.ts +9 -9
  149. package/src/modes/{interactive/controllers → controllers}/extension-ui-controller.ts +8 -8
  150. package/src/modes/{interactive/controllers → controllers}/input-controller.ts +6 -6
  151. package/src/modes/{interactive/controllers → controllers}/selector-controller.ts +16 -16
  152. package/src/modes/index.ts +1 -1
  153. package/src/modes/{interactive/interactive-mode.ts → interactive-mode.ts} +14 -14
  154. package/src/modes/print-mode.ts +1 -1
  155. package/src/modes/rpc/rpc-client.ts +3 -3
  156. package/src/modes/rpc/rpc-mode.ts +3 -3
  157. package/src/modes/rpc/rpc-types.ts +3 -3
  158. package/src/modes/{interactive/theme → theme}/theme.ts +1 -1
  159. package/src/modes/{interactive/types.ts → types.ts} +8 -9
  160. package/src/modes/{interactive/utils → utils}/ui-helpers.ts +20 -27
  161. package/src/{core/tools/patch → patch}/applicator.ts +1 -1
  162. package/src/{core/tools/patch → patch}/diff.ts +1 -1
  163. package/src/{core/tools/patch → patch}/index.ts +31 -36
  164. package/src/{core/tools/patch → patch}/shared.ts +9 -6
  165. package/src/prompts/agents/explore.md +83 -46
  166. package/src/prompts/agents/init.md +9 -4
  167. package/src/prompts/agents/plan.md +8 -7
  168. package/src/prompts/agents/reviewer.md +36 -18
  169. package/src/prompts/agents/task.md +4 -4
  170. package/src/prompts/compaction/branch-summary-preamble.md +0 -1
  171. package/src/prompts/review-request.md +0 -1
  172. package/src/prompts/system/custom-system-prompt.md +2 -14
  173. package/src/prompts/system/file-operations.md +0 -2
  174. package/src/prompts/system/system-prompt.md +147 -138
  175. package/src/prompts/system/web-search.md +26 -0
  176. package/src/prompts/tools/ask.md +31 -24
  177. package/src/prompts/tools/bash.md +20 -17
  178. package/src/prompts/tools/calculator.md +9 -5
  179. package/src/prompts/tools/fetch.md +16 -0
  180. package/src/prompts/tools/find.md +15 -5
  181. package/src/prompts/tools/gemini-image.md +21 -6
  182. package/src/prompts/tools/grep.md +28 -12
  183. package/src/prompts/tools/lsp.md +35 -14
  184. package/src/prompts/tools/patch.md +39 -41
  185. package/src/prompts/tools/python.md +59 -76
  186. package/src/prompts/tools/read.md +23 -22
  187. package/src/prompts/tools/replace.md +19 -12
  188. package/src/prompts/tools/ssh.md +21 -28
  189. package/src/prompts/tools/task.md +54 -44
  190. package/src/prompts/tools/todo-write.md +52 -163
  191. package/src/prompts/tools/web-search.md +16 -9
  192. package/src/prompts/tools/write.md +13 -2
  193. package/src/{core/sdk.ts → sdk.ts} +65 -34
  194. package/src/{core → session}/agent-session.ts +45 -37
  195. package/src/{core → session}/agent-storage.ts +2 -2
  196. package/src/session/artifacts.ts +110 -0
  197. package/src/{core → session}/auth-storage.ts +1 -1
  198. package/src/{core → session}/compaction/branch-summarization.ts +5 -5
  199. package/src/{core → session}/compaction/compaction.ts +6 -6
  200. package/src/{core → session}/compaction/utils.ts +3 -3
  201. package/src/{core → session}/history-storage.ts +1 -1
  202. package/src/{core → session}/messages.ts +6 -8
  203. package/src/{core → session}/session-manager.ts +2 -2
  204. package/src/{core → session}/storage-migration.ts +2 -2
  205. package/src/session/streaming-output.ts +177 -0
  206. package/src/{core/ssh → ssh}/connection-manager.ts +1 -1
  207. package/src/{core/ssh → ssh}/ssh-executor.ts +19 -4
  208. package/src/{core/ssh → ssh}/sshfs-mount.ts +1 -1
  209. package/src/{core/system-prompt.ts → system-prompt.ts} +8 -37
  210. package/src/{core/tools/task → task}/agents.ts +8 -8
  211. package/src/{core/tools/task → task}/commands.ts +5 -6
  212. package/src/{core/tools/task → task}/discovery.ts +3 -3
  213. package/src/{core/tools/task → task}/executor.ts +34 -44
  214. package/src/{core/tools/task → task}/index.ts +206 -50
  215. package/src/{core/tools/task → task}/render.ts +80 -23
  216. package/src/{core/tools/task → task}/subprocess-tool-registry.ts +1 -1
  217. package/src/task/template.ts +47 -0
  218. package/src/{core/tools/task → task}/types.ts +19 -27
  219. package/src/{core/tools/task → task}/worker-protocol.ts +8 -4
  220. package/src/{core/tools/task → task}/worker.ts +34 -29
  221. package/src/task/worktree.ts +166 -0
  222. package/src/{core/tools → tools}/ask.ts +13 -21
  223. package/src/{core/tools → tools}/bash-interceptor.ts +1 -1
  224. package/src/{core/tools → tools}/bash.ts +61 -63
  225. package/src/{core/tools → tools}/calculator.ts +4 -4
  226. package/src/{core/tools → tools}/complete.ts +1 -1
  227. package/src/{core/tools → tools}/context.ts +2 -2
  228. package/src/{core/tools/web-fetch.ts → tools/fetch.ts} +97 -76
  229. package/src/{core/tools → tools}/find.ts +80 -104
  230. package/src/{core/tools → tools}/gemini-image.ts +420 -29
  231. package/src/{core/tools → tools}/grep.ts +155 -164
  232. package/src/{core/tools → tools}/index.ts +63 -56
  233. package/src/tools/list-limit.ts +40 -0
  234. package/src/{core/tools → tools}/ls.ts +44 -35
  235. package/src/{core/tools → tools}/notebook.ts +3 -3
  236. package/src/tools/output-meta.ts +443 -0
  237. package/src/tools/output-utils.ts +63 -0
  238. package/src/{core/tools → tools}/python.ts +105 -89
  239. package/src/tools/read.ts +882 -0
  240. package/src/{core/tools → tools}/render-utils.ts +1 -1
  241. package/src/{core/tools → tools}/renderers.ts +8 -10
  242. package/src/{core/tools → tools}/review.ts +2 -2
  243. package/src/{core/tools → tools}/ssh.ts +56 -59
  244. package/src/{core/tools → tools}/todo-write.ts +12 -23
  245. package/src/tools/tool-errors.ts +95 -0
  246. package/src/tools/tool-result.ts +92 -0
  247. package/src/{core/tools → tools}/truncate.ts +2 -2
  248. package/src/{core/tools → tools}/write.ts +15 -13
  249. package/src/utils/changelog.ts +1 -1
  250. package/src/{core → utils}/file-mentions.ts +4 -4
  251. package/src/utils/image-convert.ts +1 -1
  252. package/src/utils/image-resize.ts +1 -1
  253. package/src/utils/shell.ts +1 -1
  254. package/src/{core → utils}/title-generator.ts +4 -4
  255. package/src/utils/tools-manager.ts +1 -1
  256. package/src/{core/tools/web-scrapers → web/scrapers}/choosealicense.ts +1 -1
  257. package/src/{core/tools/web-scrapers → web/scrapers}/twitter.ts +3 -2
  258. package/src/{core/tools/web-scrapers → web/scrapers}/types.ts +4 -2
  259. package/src/{core/tools/web-scrapers → web/scrapers}/utils.ts +1 -1
  260. package/src/{core/tools/web-scrapers → web/scrapers}/youtube.ts +14 -13
  261. package/src/{core/tools/web-search → web/search}/auth.ts +4 -4
  262. package/src/{core/tools/web-search → web/search}/index.ts +22 -71
  263. package/src/{core/tools/web-search → web/search}/providers/anthropic.ts +7 -10
  264. package/src/{core/tools/web-search → web/search}/providers/exa.ts +2 -2
  265. package/src/{core/tools/web-search → web/search}/providers/perplexity.ts +4 -16
  266. package/src/{core/tools/web-search → web/search}/render.ts +3 -3
  267. package/tsconfig.json +25 -0
  268. package/scripts/migrate-sessions.sh +0 -93
  269. package/src/core/index.ts +0 -56
  270. package/src/core/python-prelude.ts +0 -3
  271. package/src/core/ssh-executor.ts +0 -5
  272. package/src/core/streaming-output.ts +0 -115
  273. package/src/core/tools/output.ts +0 -519
  274. package/src/core/tools/read.ts +0 -717
  275. package/src/core/tools/task/template.ts +0 -37
  276. package/src/prompts/tools/output.md +0 -47
  277. package/src/prompts/tools/web-fetch.md +0 -9
  278. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +0 -1
  279. /package/src/{core/tools/exa → exa}/types.ts +0 -0
  280. /package/src/{core → exec}/exec.ts +0 -0
  281. /package/src/{core/export-html → export/html}/template.css +0 -0
  282. /package/src/{core/export-html → export/html}/template.generated.ts +0 -0
  283. /package/src/{core/export-html → export/html}/template.html +0 -0
  284. /package/src/{core/export-html → export/html}/template.js +0 -0
  285. /package/src/{core/export-html → export/html}/template.macro.ts +0 -0
  286. /package/src/{core/export-html → export/html}/vendor/highlight.min.js +0 -0
  287. /package/src/{core/export-html → export/html}/vendor/marked.min.js +0 -0
  288. /package/src/{core → extensibility}/custom-commands/index.ts +0 -0
  289. /package/src/{core → extensibility}/custom-tools/index.ts +0 -0
  290. /package/src/{core → extensibility}/extensions/index.ts +0 -0
  291. /package/src/{core → extensibility}/hooks/tool-wrapper.ts +0 -0
  292. /package/src/{core → extensibility}/plugins/index.ts +0 -0
  293. /package/src/{core → extensibility}/plugins/loader.ts +0 -0
  294. /package/src/{core → extensibility}/plugins/manager.ts +0 -0
  295. /package/src/{core → extensibility}/plugins/parser.ts +0 -0
  296. /package/src/{core → extensibility}/plugins/types.ts +0 -0
  297. /package/src/{core/python-modules.ts → ipy/modules.ts} +0 -0
  298. /package/src/{core/python-prelude.py → ipy/prelude.py} +0 -0
  299. /package/src/{core/tools/lsp → lsp}/defaults.json +0 -0
  300. /package/src/{core/tools/lsp → lsp}/edits.ts +0 -0
  301. /package/src/{core/tools/lsp → lsp}/lspmux.ts +0 -0
  302. /package/src/{core/tools/lsp → lsp}/rust-analyzer.ts +0 -0
  303. /package/src/{core/mcp → mcp}/client.ts +0 -0
  304. /package/src/{core/mcp → mcp}/index.ts +0 -0
  305. /package/src/{core/mcp → mcp}/json-rpc.ts +0 -0
  306. /package/src/{core/mcp → mcp}/transports/index.ts +0 -0
  307. /package/src/modes/{interactive/components → components}/countdown-timer.ts +0 -0
  308. /package/src/modes/{interactive/components → components}/custom-editor.ts +0 -0
  309. /package/src/modes/{interactive/components → components}/extensions/index.ts +0 -0
  310. /package/src/modes/{interactive/components → components}/index.ts +0 -0
  311. /package/src/modes/{interactive/components → components}/status-line/index.ts +0 -0
  312. /package/src/modes/{interactive/components → components}/status-line/presets.ts +0 -0
  313. /package/src/modes/{interactive/components → components}/visual-truncate.ts +0 -0
  314. /package/src/modes/{interactive/theme → theme}/dark.json +0 -0
  315. /package/src/modes/{interactive/theme → theme}/defaults/alabaster.json +0 -0
  316. /package/src/modes/{interactive/theme → theme}/defaults/amethyst.json +0 -0
  317. /package/src/modes/{interactive/theme → theme}/defaults/anthracite.json +0 -0
  318. /package/src/modes/{interactive/theme → theme}/defaults/basalt.json +0 -0
  319. /package/src/modes/{interactive/theme → theme}/defaults/birch.json +0 -0
  320. /package/src/modes/{interactive/theme → theme}/defaults/dark-abyss.json +0 -0
  321. /package/src/modes/{interactive/theme → theme}/defaults/dark-arctic.json +0 -0
  322. /package/src/modes/{interactive/theme → theme}/defaults/dark-aurora.json +0 -0
  323. /package/src/modes/{interactive/theme → theme}/defaults/dark-catppuccin.json +0 -0
  324. /package/src/modes/{interactive/theme → theme}/defaults/dark-cavern.json +0 -0
  325. /package/src/modes/{interactive/theme → theme}/defaults/dark-copper.json +0 -0
  326. /package/src/modes/{interactive/theme → theme}/defaults/dark-cosmos.json +0 -0
  327. /package/src/modes/{interactive/theme → theme}/defaults/dark-cyberpunk.json +0 -0
  328. /package/src/modes/{interactive/theme → theme}/defaults/dark-dracula.json +0 -0
  329. /package/src/modes/{interactive/theme → theme}/defaults/dark-eclipse.json +0 -0
  330. /package/src/modes/{interactive/theme → theme}/defaults/dark-ember.json +0 -0
  331. /package/src/modes/{interactive/theme → theme}/defaults/dark-equinox.json +0 -0
  332. /package/src/modes/{interactive/theme → theme}/defaults/dark-forest.json +0 -0
  333. /package/src/modes/{interactive/theme → theme}/defaults/dark-github.json +0 -0
  334. /package/src/modes/{interactive/theme → theme}/defaults/dark-gruvbox.json +0 -0
  335. /package/src/modes/{interactive/theme → theme}/defaults/dark-lavender.json +0 -0
  336. /package/src/modes/{interactive/theme → theme}/defaults/dark-lunar.json +0 -0
  337. /package/src/modes/{interactive/theme → theme}/defaults/dark-midnight.json +0 -0
  338. /package/src/modes/{interactive/theme → theme}/defaults/dark-monochrome.json +0 -0
  339. /package/src/modes/{interactive/theme → theme}/defaults/dark-monokai.json +0 -0
  340. /package/src/modes/{interactive/theme → theme}/defaults/dark-nebula.json +0 -0
  341. /package/src/modes/{interactive/theme → theme}/defaults/dark-nord.json +0 -0
  342. /package/src/modes/{interactive/theme → theme}/defaults/dark-ocean.json +0 -0
  343. /package/src/modes/{interactive/theme → theme}/defaults/dark-one.json +0 -0
  344. /package/src/modes/{interactive/theme → theme}/defaults/dark-rainforest.json +0 -0
  345. /package/src/modes/{interactive/theme → theme}/defaults/dark-reef.json +0 -0
  346. /package/src/modes/{interactive/theme → theme}/defaults/dark-retro.json +0 -0
  347. /package/src/modes/{interactive/theme → theme}/defaults/dark-rose-pine.json +0 -0
  348. /package/src/modes/{interactive/theme → theme}/defaults/dark-sakura.json +0 -0
  349. /package/src/modes/{interactive/theme → theme}/defaults/dark-slate.json +0 -0
  350. /package/src/modes/{interactive/theme → theme}/defaults/dark-solarized.json +0 -0
  351. /package/src/modes/{interactive/theme → theme}/defaults/dark-solstice.json +0 -0
  352. /package/src/modes/{interactive/theme → theme}/defaults/dark-starfall.json +0 -0
  353. /package/src/modes/{interactive/theme → theme}/defaults/dark-sunset.json +0 -0
  354. /package/src/modes/{interactive/theme → theme}/defaults/dark-swamp.json +0 -0
  355. /package/src/modes/{interactive/theme → theme}/defaults/dark-synthwave.json +0 -0
  356. /package/src/modes/{interactive/theme → theme}/defaults/dark-taiga.json +0 -0
  357. /package/src/modes/{interactive/theme → theme}/defaults/dark-terminal.json +0 -0
  358. /package/src/modes/{interactive/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  359. /package/src/modes/{interactive/theme → theme}/defaults/dark-tundra.json +0 -0
  360. /package/src/modes/{interactive/theme → theme}/defaults/dark-twilight.json +0 -0
  361. /package/src/modes/{interactive/theme → theme}/defaults/dark-volcanic.json +0 -0
  362. /package/src/modes/{interactive/theme → theme}/defaults/graphite.json +0 -0
  363. /package/src/modes/{interactive/theme → theme}/defaults/index.ts +0 -0
  364. /package/src/modes/{interactive/theme → theme}/defaults/light-arctic.json +0 -0
  365. /package/src/modes/{interactive/theme → theme}/defaults/light-aurora-day.json +0 -0
  366. /package/src/modes/{interactive/theme → theme}/defaults/light-canyon.json +0 -0
  367. /package/src/modes/{interactive/theme → theme}/defaults/light-catppuccin.json +0 -0
  368. /package/src/modes/{interactive/theme → theme}/defaults/light-cirrus.json +0 -0
  369. /package/src/modes/{interactive/theme → theme}/defaults/light-coral.json +0 -0
  370. /package/src/modes/{interactive/theme → theme}/defaults/light-cyberpunk.json +0 -0
  371. /package/src/modes/{interactive/theme → theme}/defaults/light-dawn.json +0 -0
  372. /package/src/modes/{interactive/theme → theme}/defaults/light-dunes.json +0 -0
  373. /package/src/modes/{interactive/theme → theme}/defaults/light-eucalyptus.json +0 -0
  374. /package/src/modes/{interactive/theme → theme}/defaults/light-forest.json +0 -0
  375. /package/src/modes/{interactive/theme → theme}/defaults/light-frost.json +0 -0
  376. /package/src/modes/{interactive/theme → theme}/defaults/light-github.json +0 -0
  377. /package/src/modes/{interactive/theme → theme}/defaults/light-glacier.json +0 -0
  378. /package/src/modes/{interactive/theme → theme}/defaults/light-gruvbox.json +0 -0
  379. /package/src/modes/{interactive/theme → theme}/defaults/light-haze.json +0 -0
  380. /package/src/modes/{interactive/theme → theme}/defaults/light-honeycomb.json +0 -0
  381. /package/src/modes/{interactive/theme → theme}/defaults/light-lagoon.json +0 -0
  382. /package/src/modes/{interactive/theme → theme}/defaults/light-lavender.json +0 -0
  383. /package/src/modes/{interactive/theme → theme}/defaults/light-meadow.json +0 -0
  384. /package/src/modes/{interactive/theme → theme}/defaults/light-mint.json +0 -0
  385. /package/src/modes/{interactive/theme → theme}/defaults/light-monochrome.json +0 -0
  386. /package/src/modes/{interactive/theme → theme}/defaults/light-ocean.json +0 -0
  387. /package/src/modes/{interactive/theme → theme}/defaults/light-one.json +0 -0
  388. /package/src/modes/{interactive/theme → theme}/defaults/light-opal.json +0 -0
  389. /package/src/modes/{interactive/theme → theme}/defaults/light-orchard.json +0 -0
  390. /package/src/modes/{interactive/theme → theme}/defaults/light-paper.json +0 -0
  391. /package/src/modes/{interactive/theme → theme}/defaults/light-prism.json +0 -0
  392. /package/src/modes/{interactive/theme → theme}/defaults/light-retro.json +0 -0
  393. /package/src/modes/{interactive/theme → theme}/defaults/light-sand.json +0 -0
  394. /package/src/modes/{interactive/theme → theme}/defaults/light-savanna.json +0 -0
  395. /package/src/modes/{interactive/theme → theme}/defaults/light-solarized.json +0 -0
  396. /package/src/modes/{interactive/theme → theme}/defaults/light-soleil.json +0 -0
  397. /package/src/modes/{interactive/theme → theme}/defaults/light-sunset.json +0 -0
  398. /package/src/modes/{interactive/theme → theme}/defaults/light-synthwave.json +0 -0
  399. /package/src/modes/{interactive/theme → theme}/defaults/light-tokyo-night.json +0 -0
  400. /package/src/modes/{interactive/theme → theme}/defaults/light-wetland.json +0 -0
  401. /package/src/modes/{interactive/theme → theme}/defaults/light-zenith.json +0 -0
  402. /package/src/modes/{interactive/theme → theme}/defaults/limestone.json +0 -0
  403. /package/src/modes/{interactive/theme → theme}/defaults/mahogany.json +0 -0
  404. /package/src/modes/{interactive/theme → theme}/defaults/marble.json +0 -0
  405. /package/src/modes/{interactive/theme → theme}/defaults/obsidian.json +0 -0
  406. /package/src/modes/{interactive/theme → theme}/defaults/onyx.json +0 -0
  407. /package/src/modes/{interactive/theme → theme}/defaults/pearl.json +0 -0
  408. /package/src/modes/{interactive/theme → theme}/defaults/porcelain.json +0 -0
  409. /package/src/modes/{interactive/theme → theme}/defaults/quartz.json +0 -0
  410. /package/src/modes/{interactive/theme → theme}/defaults/sandstone.json +0 -0
  411. /package/src/modes/{interactive/theme → theme}/defaults/titanium.json +0 -0
  412. /package/src/modes/{interactive/theme → theme}/light.json +0 -0
  413. /package/src/modes/{interactive/theme → theme}/theme-schema.json +0 -0
  414. /package/src/{core/tools/patch → patch}/fuzzy.ts +0 -0
  415. /package/src/{core/tools/patch → patch}/normalize.ts +0 -0
  416. /package/src/{core/tools/patch → patch}/normative.ts +0 -0
  417. /package/src/{core/tools/patch → patch}/parser.ts +0 -0
  418. /package/src/{core/tools/patch → patch}/types.ts +0 -0
  419. /package/src/{core → session}/compaction/index.ts +0 -0
  420. /package/src/{core → session}/session-storage.ts +0 -0
  421. /package/src/{core/tools/task → task}/name-generator.ts +0 -0
  422. /package/src/{core/tools/task → task}/omp-command.ts +0 -0
  423. /package/src/{core/tools/task → task}/parallel.ts +0 -0
  424. /package/src/{core/tools → tools}/jtd-to-json-schema.ts +0 -0
  425. /package/src/{core/tools → tools}/path-utils.ts +0 -0
  426. /package/src/{core → utils}/event-bus.ts +0 -0
  427. /package/src/{core → utils}/frontmatter.ts +0 -0
  428. /package/src/{core → utils}/terminal-notify.ts +0 -0
  429. /package/src/{core → utils}/timings.ts +0 -0
  430. /package/src/{core → utils}/utils.ts +0 -0
  431. /package/src/{core/tools/web-scrapers → web/scrapers}/artifacthub.ts +0 -0
  432. /package/src/{core/tools/web-scrapers → web/scrapers}/arxiv.ts +0 -0
  433. /package/src/{core/tools/web-scrapers → web/scrapers}/aur.ts +0 -0
  434. /package/src/{core/tools/web-scrapers → web/scrapers}/biorxiv.ts +0 -0
  435. /package/src/{core/tools/web-scrapers → web/scrapers}/bluesky.ts +0 -0
  436. /package/src/{core/tools/web-scrapers → web/scrapers}/brew.ts +0 -0
  437. /package/src/{core/tools/web-scrapers → web/scrapers}/cheatsh.ts +0 -0
  438. /package/src/{core/tools/web-scrapers → web/scrapers}/chocolatey.ts +0 -0
  439. /package/src/{core/tools/web-scrapers → web/scrapers}/cisa-kev.ts +0 -0
  440. /package/src/{core/tools/web-scrapers → web/scrapers}/clojars.ts +0 -0
  441. /package/src/{core/tools/web-scrapers → web/scrapers}/coingecko.ts +0 -0
  442. /package/src/{core/tools/web-scrapers → web/scrapers}/crates-io.ts +0 -0
  443. /package/src/{core/tools/web-scrapers → web/scrapers}/crossref.ts +0 -0
  444. /package/src/{core/tools/web-scrapers → web/scrapers}/devto.ts +0 -0
  445. /package/src/{core/tools/web-scrapers → web/scrapers}/discogs.ts +0 -0
  446. /package/src/{core/tools/web-scrapers → web/scrapers}/discourse.ts +0 -0
  447. /package/src/{core/tools/web-scrapers → web/scrapers}/dockerhub.ts +0 -0
  448. /package/src/{core/tools/web-scrapers → web/scrapers}/fdroid.ts +0 -0
  449. /package/src/{core/tools/web-scrapers → web/scrapers}/firefox-addons.ts +0 -0
  450. /package/src/{core/tools/web-scrapers → web/scrapers}/flathub.ts +0 -0
  451. /package/src/{core/tools/web-scrapers → web/scrapers}/github-gist.ts +0 -0
  452. /package/src/{core/tools/web-scrapers → web/scrapers}/github.ts +0 -0
  453. /package/src/{core/tools/web-scrapers → web/scrapers}/gitlab.ts +0 -0
  454. /package/src/{core/tools/web-scrapers → web/scrapers}/go-pkg.ts +0 -0
  455. /package/src/{core/tools/web-scrapers → web/scrapers}/hackage.ts +0 -0
  456. /package/src/{core/tools/web-scrapers → web/scrapers}/hackernews.ts +0 -0
  457. /package/src/{core/tools/web-scrapers → web/scrapers}/hex.ts +0 -0
  458. /package/src/{core/tools/web-scrapers → web/scrapers}/huggingface.ts +0 -0
  459. /package/src/{core/tools/web-scrapers → web/scrapers}/iacr.ts +0 -0
  460. /package/src/{core/tools/web-scrapers → web/scrapers}/index.ts +0 -0
  461. /package/src/{core/tools/web-scrapers → web/scrapers}/jetbrains-marketplace.ts +0 -0
  462. /package/src/{core/tools/web-scrapers → web/scrapers}/lemmy.ts +0 -0
  463. /package/src/{core/tools/web-scrapers → web/scrapers}/lobsters.ts +0 -0
  464. /package/src/{core/tools/web-scrapers → web/scrapers}/mastodon.ts +0 -0
  465. /package/src/{core/tools/web-scrapers → web/scrapers}/maven.ts +0 -0
  466. /package/src/{core/tools/web-scrapers → web/scrapers}/mdn.ts +0 -0
  467. /package/src/{core/tools/web-scrapers → web/scrapers}/metacpan.ts +0 -0
  468. /package/src/{core/tools/web-scrapers → web/scrapers}/musicbrainz.ts +0 -0
  469. /package/src/{core/tools/web-scrapers → web/scrapers}/npm.ts +0 -0
  470. /package/src/{core/tools/web-scrapers → web/scrapers}/nuget.ts +0 -0
  471. /package/src/{core/tools/web-scrapers → web/scrapers}/nvd.ts +0 -0
  472. /package/src/{core/tools/web-scrapers → web/scrapers}/ollama.ts +0 -0
  473. /package/src/{core/tools/web-scrapers → web/scrapers}/open-vsx.ts +0 -0
  474. /package/src/{core/tools/web-scrapers → web/scrapers}/opencorporates.ts +0 -0
  475. /package/src/{core/tools/web-scrapers → web/scrapers}/openlibrary.ts +0 -0
  476. /package/src/{core/tools/web-scrapers → web/scrapers}/orcid.ts +0 -0
  477. /package/src/{core/tools/web-scrapers → web/scrapers}/osv.ts +0 -0
  478. /package/src/{core/tools/web-scrapers → web/scrapers}/packagist.ts +0 -0
  479. /package/src/{core/tools/web-scrapers → web/scrapers}/pub-dev.ts +0 -0
  480. /package/src/{core/tools/web-scrapers → web/scrapers}/pubmed.ts +0 -0
  481. /package/src/{core/tools/web-scrapers → web/scrapers}/pypi.ts +0 -0
  482. /package/src/{core/tools/web-scrapers → web/scrapers}/rawg.ts +0 -0
  483. /package/src/{core/tools/web-scrapers → web/scrapers}/readthedocs.ts +0 -0
  484. /package/src/{core/tools/web-scrapers → web/scrapers}/reddit.ts +0 -0
  485. /package/src/{core/tools/web-scrapers → web/scrapers}/repology.ts +0 -0
  486. /package/src/{core/tools/web-scrapers → web/scrapers}/rfc.ts +0 -0
  487. /package/src/{core/tools/web-scrapers → web/scrapers}/rubygems.ts +0 -0
  488. /package/src/{core/tools/web-scrapers → web/scrapers}/searchcode.ts +0 -0
  489. /package/src/{core/tools/web-scrapers → web/scrapers}/sec-edgar.ts +0 -0
  490. /package/src/{core/tools/web-scrapers → web/scrapers}/semantic-scholar.ts +0 -0
  491. /package/src/{core/tools/web-scrapers → web/scrapers}/snapcraft.ts +0 -0
  492. /package/src/{core/tools/web-scrapers → web/scrapers}/sourcegraph.ts +0 -0
  493. /package/src/{core/tools/web-scrapers → web/scrapers}/spdx.ts +0 -0
  494. /package/src/{core/tools/web-scrapers → web/scrapers}/spotify.ts +0 -0
  495. /package/src/{core/tools/web-scrapers → web/scrapers}/stackoverflow.ts +0 -0
  496. /package/src/{core/tools/web-scrapers → web/scrapers}/terraform.ts +0 -0
  497. /package/src/{core/tools/web-scrapers → web/scrapers}/tldr.ts +0 -0
  498. /package/src/{core/tools/web-scrapers → web/scrapers}/vimeo.ts +0 -0
  499. /package/src/{core/tools/web-scrapers → web/scrapers}/vscode-marketplace.ts +0 -0
  500. /package/src/{core/tools/web-scrapers → web/scrapers}/w3c.ts +0 -0
  501. /package/src/{core/tools/web-scrapers → web/scrapers}/wikidata.ts +0 -0
  502. /package/src/{core/tools/web-scrapers → web/scrapers}/wikipedia.ts +0 -0
  503. /package/src/{core/tools/web-search → web/search}/types.ts +0 -0
@@ -0,0 +1,882 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
+ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
5
+ import type { Component } from "@oh-my-pi/pi-tui";
6
+ import { Text } from "@oh-my-pi/pi-tui";
7
+ import { ptree } from "@oh-my-pi/pi-utils";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { CONFIG_DIR_NAME } from "$c/config";
10
+ import { renderPromptTemplate } from "$c/config/prompt-templates";
11
+ import type { RenderResultOptions } from "$c/extensibility/custom-tools/types";
12
+ import type { Theme } from "$c/modes/theme/theme";
13
+ import readDescription from "$c/prompts/tools/read.md" with { type: "text" };
14
+ import type { ToolSession } from "$c/sdk";
15
+ import type { OutputMeta } from "$c/tools/output-meta";
16
+ import { ToolAbortError, ToolError, throwIfAborted } from "$c/tools/tool-errors";
17
+ import { formatDimensionNote, resizeImage } from "$c/utils/image-resize";
18
+ import { detectSupportedImageMimeTypeFromFile } from "$c/utils/mime";
19
+ import { ensureTool } from "$c/utils/tools-manager";
20
+ import { runFd } from "./find";
21
+ import { applyListLimit } from "./list-limit";
22
+ import { LsTool } from "./ls";
23
+ import { resolveReadPath, resolveToCwd } from "./path-utils";
24
+ import { shortenPath, wrapBrackets } from "./render-utils";
25
+ import { toolResult } from "./tool-result";
26
+ import {
27
+ DEFAULT_MAX_BYTES,
28
+ DEFAULT_MAX_LINES,
29
+ formatSize,
30
+ type TruncationResult,
31
+ truncateHead,
32
+ truncateStringToBytesFromStart,
33
+ } from "./truncate";
34
+
35
+ // Document types convertible via markitdown
36
+ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
37
+
38
+ // Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
39
+ const REMOTE_MOUNT_PREFIX = path.join(homedir(), CONFIG_DIR_NAME, "remote") + path.sep;
40
+
41
+ function isRemoteMountPath(absolutePath: string): boolean {
42
+ return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
43
+ }
44
+
45
+ // Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
46
+ const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
47
+ const MAX_FUZZY_RESULTS = 5;
48
+ const MAX_FUZZY_CANDIDATES = 20000;
49
+ const MIN_BASE_SIMILARITY = 0.5;
50
+ const MIN_FULL_SIMILARITY = 0.6;
51
+
52
+ function normalizePathForMatch(value: string): string {
53
+ return value
54
+ .replace(/\\/g, "/")
55
+ .replace(/^\.\/+/, "")
56
+ .replace(/\/+$/, "")
57
+ .toLowerCase();
58
+ }
59
+
60
+ function isNotFoundError(error: unknown): boolean {
61
+ if (!error || typeof error !== "object") return false;
62
+ const code = (error as { code?: string }).code;
63
+ return code === "ENOENT" || code === "ENOTDIR";
64
+ }
65
+
66
+ function isPathWithin(basePath: string, targetPath: string): boolean {
67
+ const relativePath = path.relative(basePath, targetPath);
68
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
69
+ }
70
+
71
+ async function findExistingDirectory(startDir: string, signal?: AbortSignal): Promise<string | null> {
72
+ let current = startDir;
73
+ const root = path.parse(startDir).root;
74
+
75
+ while (true) {
76
+ throwIfAborted(signal);
77
+ try {
78
+ const stat = await Bun.file(current).stat();
79
+ if (stat.isDirectory()) {
80
+ return current;
81
+ }
82
+ } catch {
83
+ // Keep walking up.
84
+ }
85
+
86
+ if (current === root) {
87
+ break;
88
+ }
89
+ current = path.dirname(current);
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ function formatScopeLabel(searchRoot: string, cwd: string): string {
96
+ const relative = path.relative(cwd, searchRoot).replace(/\\/g, "/");
97
+ if (relative === "" || relative === ".") {
98
+ return ".";
99
+ }
100
+ if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
101
+ return relative;
102
+ }
103
+ return searchRoot;
104
+ }
105
+
106
+ function buildDisplayPath(searchRoot: string, cwd: string, relativePath: string): string {
107
+ const scopeLabel = formatScopeLabel(searchRoot, cwd);
108
+ const normalized = relativePath.replace(/\\/g, "/");
109
+ if (scopeLabel === ".") {
110
+ return normalized;
111
+ }
112
+ if (scopeLabel.startsWith("..") || path.isAbsolute(scopeLabel)) {
113
+ return path.join(searchRoot, normalized).replace(/\\/g, "/");
114
+ }
115
+ return `${scopeLabel}/${normalized}`;
116
+ }
117
+
118
+ function levenshteinDistance(a: string, b: string): number {
119
+ if (a === b) return 0;
120
+ const aLen = a.length;
121
+ const bLen = b.length;
122
+ if (aLen === 0) return bLen;
123
+ if (bLen === 0) return aLen;
124
+
125
+ let prev = new Array<number>(bLen + 1);
126
+ let curr = new Array<number>(bLen + 1);
127
+ for (let j = 0; j <= bLen; j++) {
128
+ prev[j] = j;
129
+ }
130
+
131
+ for (let i = 1; i <= aLen; i++) {
132
+ curr[0] = i;
133
+ const aCode = a.charCodeAt(i - 1);
134
+ for (let j = 1; j <= bLen; j++) {
135
+ const cost = aCode === b.charCodeAt(j - 1) ? 0 : 1;
136
+ const deletion = prev[j] + 1;
137
+ const insertion = curr[j - 1] + 1;
138
+ const substitution = prev[j - 1] + cost;
139
+ curr[j] = Math.min(deletion, insertion, substitution);
140
+ }
141
+ const tmp = prev;
142
+ prev = curr;
143
+ curr = tmp;
144
+ }
145
+
146
+ return prev[bLen];
147
+ }
148
+
149
+ function similarityScore(a: string, b: string): number {
150
+ if (a.length === 0 && b.length === 0) {
151
+ return 1;
152
+ }
153
+ const maxLen = Math.max(a.length, b.length);
154
+ if (maxLen === 0) {
155
+ return 1;
156
+ }
157
+ const distance = levenshteinDistance(a, b);
158
+ return 1 - distance / maxLen;
159
+ }
160
+
161
+ async function listCandidateFiles(
162
+ searchRoot: string,
163
+ signal?: AbortSignal,
164
+ ): Promise<{ files: string[]; truncated: boolean; error?: string }> {
165
+ let fdPath: string | undefined;
166
+ try {
167
+ fdPath = await ensureTool("fd", true);
168
+ } catch {
169
+ return { files: [], truncated: false, error: "fd not available" };
170
+ }
171
+
172
+ if (!fdPath) {
173
+ return { files: [], truncated: false, error: "fd not available" };
174
+ }
175
+
176
+ const args: string[] = ["--type", "f", "--color=never", "--hidden", "--max-results", String(MAX_FUZZY_CANDIDATES)];
177
+
178
+ const gitignoreFiles = new Set<string>();
179
+ const rootGitignore = path.join(searchRoot, ".gitignore");
180
+ if (await Bun.file(rootGitignore).exists()) {
181
+ gitignoreFiles.add(rootGitignore);
182
+ }
183
+
184
+ try {
185
+ const gitignoreArgs = [
186
+ "--type",
187
+ "f",
188
+ "--color=never",
189
+ "--hidden",
190
+ "--absolute-path",
191
+ "--glob",
192
+ ".gitignore",
193
+ "--exclude",
194
+ "node_modules",
195
+ "--exclude",
196
+ ".git",
197
+ searchRoot,
198
+ ];
199
+ const { stdout } = await runFd(fdPath, gitignoreArgs, signal);
200
+ const output = stdout.trim();
201
+ if (output) {
202
+ const nestedGitignores = output
203
+ .split("\n")
204
+ .map((line) => line.replace(/\r$/, "").trim())
205
+ .filter((line) => line.length > 0);
206
+ for (const file of nestedGitignores) {
207
+ const normalized = file.replace(/\\/g, "/");
208
+ if (normalized.includes("/node_modules/") || normalized.includes("/.git/")) {
209
+ continue;
210
+ }
211
+ gitignoreFiles.add(file);
212
+ }
213
+ }
214
+ } catch (error) {
215
+ if (error instanceof ToolAbortError) {
216
+ throw error;
217
+ }
218
+ // Ignore gitignore scan errors.
219
+ }
220
+
221
+ for (const gitignorePath of gitignoreFiles) {
222
+ args.push("--ignore-file", gitignorePath);
223
+ }
224
+
225
+ args.push(".", searchRoot);
226
+
227
+ const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
228
+ const output = stdout.trim();
229
+
230
+ if (!output) {
231
+ // fd exit codes: 0 = found, 1 = no matches, other = error
232
+ if (exitCode !== 0 && exitCode !== 1) {
233
+ return { files: [], truncated: false, error: stderr.trim() || `fd failed (exit ${exitCode})` };
234
+ }
235
+ return { files: [], truncated: false };
236
+ }
237
+
238
+ const files = output
239
+ .split("\n")
240
+ .map((line) => line.replace(/\r$/, "").trim())
241
+ .filter((line) => line.length > 0);
242
+
243
+ return { files, truncated: files.length >= MAX_FUZZY_CANDIDATES };
244
+ }
245
+
246
+ async function findReadPathSuggestions(
247
+ rawPath: string,
248
+ cwd: string,
249
+ signal?: AbortSignal,
250
+ ): Promise<{ suggestions: string[]; scopeLabel?: string; truncated?: boolean; error?: string } | null> {
251
+ const resolvedPath = resolveToCwd(rawPath, cwd);
252
+ const searchRoot = await findExistingDirectory(path.dirname(resolvedPath), signal);
253
+ if (!searchRoot) {
254
+ return null;
255
+ }
256
+
257
+ if (!isPathWithin(cwd, resolvedPath)) {
258
+ const root = path.parse(searchRoot).root;
259
+ if (searchRoot === root) {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ const { files, truncated, error } = await listCandidateFiles(searchRoot, signal);
265
+ const scopeLabel = formatScopeLabel(searchRoot, cwd);
266
+
267
+ if (error && files.length === 0) {
268
+ return { suggestions: [], scopeLabel, truncated, error };
269
+ }
270
+
271
+ if (files.length === 0) {
272
+ return null;
273
+ }
274
+
275
+ const queryPath = (() => {
276
+ if (path.isAbsolute(rawPath)) {
277
+ const relative = path.relative(cwd, resolvedPath).replace(/\\/g, "/");
278
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
279
+ return normalizePathForMatch(relative);
280
+ }
281
+ }
282
+ return normalizePathForMatch(rawPath);
283
+ })();
284
+ const baseQuery = path.posix.basename(queryPath);
285
+
286
+ const matches: Array<{ path: string; score: number; baseScore: number; fullScore: number }> = [];
287
+ const seen = new Set<string>();
288
+
289
+ for (const file of files) {
290
+ throwIfAborted(signal);
291
+ const cleaned = file.replace(/\r$/, "").trim();
292
+ if (!cleaned) continue;
293
+
294
+ const relativePath = path.isAbsolute(cleaned)
295
+ ? cleaned.startsWith(searchRoot)
296
+ ? cleaned.slice(searchRoot.length + 1)
297
+ : path.relative(searchRoot, cleaned)
298
+ : cleaned;
299
+
300
+ if (!relativePath || relativePath.startsWith("..")) {
301
+ continue;
302
+ }
303
+
304
+ const displayPath = buildDisplayPath(searchRoot, cwd, relativePath);
305
+ if (seen.has(displayPath)) {
306
+ continue;
307
+ }
308
+ seen.add(displayPath);
309
+
310
+ const normalizedDisplay = normalizePathForMatch(displayPath);
311
+ const baseCandidate = path.posix.basename(normalizedDisplay);
312
+
313
+ const fullScore = similarityScore(queryPath, normalizedDisplay);
314
+ const baseScore = baseQuery ? similarityScore(baseQuery, baseCandidate) : 0;
315
+
316
+ if (baseQuery) {
317
+ if (baseScore < MIN_BASE_SIMILARITY && fullScore < MIN_FULL_SIMILARITY) {
318
+ continue;
319
+ }
320
+ } else if (fullScore < MIN_FULL_SIMILARITY) {
321
+ continue;
322
+ }
323
+
324
+ const score = baseQuery ? baseScore * 0.75 + fullScore * 0.25 : fullScore;
325
+ matches.push({ path: displayPath, score, baseScore, fullScore });
326
+ }
327
+
328
+ if (matches.length === 0) {
329
+ return { suggestions: [], scopeLabel, truncated };
330
+ }
331
+
332
+ matches.sort((a, b) => {
333
+ if (b.score !== a.score) return b.score - a.score;
334
+ if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
335
+ return a.path.localeCompare(b.path);
336
+ });
337
+
338
+ const listLimit = applyListLimit(matches, { limit: MAX_FUZZY_RESULTS });
339
+ const suggestions = listLimit.items.map((match) => match.path);
340
+
341
+ return { suggestions, scopeLabel, truncated };
342
+ }
343
+
344
+ async function convertWithMarkitdown(
345
+ filePath: string,
346
+ signal?: AbortSignal,
347
+ ): Promise<{ content: string; ok: boolean; error?: string }> {
348
+ const cmd = await ensureTool("markitdown", true);
349
+ if (!cmd) {
350
+ return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
351
+ }
352
+
353
+ const child = ptree.cspawn([cmd, filePath], { signal });
354
+ let stdout: string;
355
+ try {
356
+ stdout = await child.nothrow().text();
357
+ } catch (err) {
358
+ if (err instanceof ptree.Exception && err.aborted) {
359
+ throw new ToolAbortError();
360
+ }
361
+ throw err;
362
+ }
363
+
364
+ if (child.exitCode === 0 && stdout.length > 0) {
365
+ return { content: stdout, ok: true };
366
+ }
367
+
368
+ return { content: "", ok: false, error: child.peekStderr().trim() || "Conversion failed" };
369
+ }
370
+
371
+ const readSchema = Type.Object({
372
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
373
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
374
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
375
+ lines: Type.Optional(Type.Boolean({ description: "Prepend line numbers to output (default: false)" })),
376
+ });
377
+
378
+ export interface ReadToolDetails {
379
+ truncation?: TruncationResult;
380
+ redirectedTo?: "ls";
381
+ meta?: OutputMeta;
382
+ }
383
+
384
+ type ReadParams = { path: string; offset?: number; limit?: number; lines?: boolean };
385
+
386
+ /**
387
+ * Read tool implementation.
388
+ *
389
+ * Reads files with support for images, documents (via markitdown), and text.
390
+ * Directories redirect to the ls tool.
391
+ */
392
+ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
393
+ public readonly name = "read";
394
+ public readonly label = "Read";
395
+ public readonly description: string;
396
+ public readonly parameters = readSchema;
397
+ public readonly nonAbortable = true;
398
+
399
+ private readonly session: ToolSession;
400
+ private readonly autoResizeImages: boolean;
401
+ private readonly defaultLineNumbers: boolean;
402
+ private readonly lsTool: LsTool;
403
+
404
+ constructor(session: ToolSession) {
405
+ this.session = session;
406
+ this.autoResizeImages = session.settings?.getImageAutoResize() ?? true;
407
+ this.defaultLineNumbers = session.settings?.getReadLineNumbers?.() ?? false;
408
+ this.lsTool = new LsTool(session);
409
+ this.description = renderPromptTemplate(readDescription, {
410
+ DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
411
+ });
412
+ }
413
+
414
+ public async execute(
415
+ toolCallId: string,
416
+ params: ReadParams,
417
+ signal?: AbortSignal,
418
+ _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
419
+ _context?: AgentToolContext,
420
+ ): Promise<AgentToolResult<ReadToolDetails>> {
421
+ const { path: readPath, offset, limit, lines } = params;
422
+
423
+ // Handle internal URLs (agent://, skill://)
424
+ const internalRouter = this.session.internalRouter;
425
+ if (internalRouter?.canHandle(readPath)) {
426
+ return this.handleInternalUrl(readPath, offset, limit, lines);
427
+ }
428
+
429
+ const absolutePath = resolveReadPath(readPath, this.session.cwd);
430
+
431
+ let isDirectory = false;
432
+ let fileSize = 0;
433
+ try {
434
+ const stat = await Bun.file(absolutePath).stat();
435
+ fileSize = stat.size;
436
+ isDirectory = stat.isDirectory();
437
+ } catch (error) {
438
+ if (isNotFoundError(error)) {
439
+ let message = `File not found: ${readPath}`;
440
+
441
+ // Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
442
+ if (!isRemoteMountPath(absolutePath)) {
443
+ const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal);
444
+
445
+ if (suggestions?.suggestions.length) {
446
+ const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
447
+ message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map((match) => `- ${match}`).join("\n")}`;
448
+ if (suggestions.truncated) {
449
+ message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
450
+ }
451
+ } else if (suggestions?.error) {
452
+ message += `\n\nFuzzy match failed: ${suggestions.error}`;
453
+ } else if (suggestions?.scopeLabel) {
454
+ message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
455
+ }
456
+ }
457
+
458
+ throw new ToolError(message);
459
+ }
460
+ throw error;
461
+ }
462
+
463
+ if (isDirectory) {
464
+ const lsResult = await this.lsTool.execute(toolCallId, { path: readPath, limit }, signal);
465
+ const details: ReadToolDetails = {
466
+ redirectedTo: "ls",
467
+ truncation: lsResult.details?.truncation,
468
+ meta: lsResult.details?.meta,
469
+ };
470
+ return toolResult(details).content(lsResult.content).done();
471
+ }
472
+
473
+ const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
474
+ const ext = path.extname(absolutePath).toLowerCase();
475
+
476
+ // Read the file based on type
477
+ let content: (TextContent | ImageContent)[];
478
+ let details: ReadToolDetails = {};
479
+ let sourcePath: string | undefined;
480
+ let truncationInfo:
481
+ | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
482
+ | undefined;
483
+
484
+ if (mimeType) {
485
+ if (fileSize > MAX_IMAGE_SIZE) {
486
+ const sizeStr = formatSize(fileSize);
487
+ const maxStr = formatSize(MAX_IMAGE_SIZE);
488
+ throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
489
+ } else {
490
+ // Read as image (binary)
491
+ const file = Bun.file(absolutePath);
492
+ const buffer = await file.arrayBuffer();
493
+
494
+ // Check actual buffer size after reading to prevent OOM during serialization
495
+ if (buffer.byteLength > MAX_IMAGE_SIZE) {
496
+ const sizeStr = formatSize(buffer.byteLength);
497
+ const maxStr = formatSize(MAX_IMAGE_SIZE);
498
+ throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
499
+ } else {
500
+ const base64 = Buffer.from(buffer).toString("base64");
501
+
502
+ if (this.autoResizeImages) {
503
+ // Resize image if needed - catch errors from WASM
504
+ try {
505
+ const resized = await resizeImage({ type: "image", data: base64, mimeType });
506
+ const dimensionNote = formatDimensionNote(resized);
507
+
508
+ let textNote = `Read image file [${resized.mimeType}]`;
509
+ if (dimensionNote) {
510
+ textNote += `\n${dimensionNote}`;
511
+ }
512
+
513
+ content = [
514
+ { type: "text", text: textNote },
515
+ { type: "image", data: resized.data, mimeType: resized.mimeType },
516
+ ];
517
+ details = {};
518
+ sourcePath = absolutePath;
519
+ } catch {
520
+ // Fall back to original image on resize failure
521
+ content = [
522
+ { type: "text", text: `Read image file [${mimeType}]` },
523
+ { type: "image", data: base64, mimeType },
524
+ ];
525
+ details = {};
526
+ sourcePath = absolutePath;
527
+ }
528
+ } else {
529
+ content = [
530
+ { type: "text", text: `Read image file [${mimeType}]` },
531
+ { type: "image", data: base64, mimeType },
532
+ ];
533
+ details = {};
534
+ sourcePath = absolutePath;
535
+ }
536
+ }
537
+ }
538
+ } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
539
+ // Convert document via markitdown
540
+ const result = await convertWithMarkitdown(absolutePath, signal);
541
+ if (result.ok) {
542
+ // Apply truncation to converted content
543
+ const truncation = truncateHead(result.content);
544
+ const outputText = truncation.content;
545
+
546
+ details = { truncation };
547
+ sourcePath = absolutePath;
548
+ truncationInfo = { result: truncation, options: { direction: "head", startLine: 1 } };
549
+
550
+ content = [{ type: "text", text: outputText }];
551
+ } else if (result.error) {
552
+ // markitdown not available or failed
553
+ const errorMsg =
554
+ result.error === "markitdown not found"
555
+ ? `markitdown not installed. Install with: pip install markitdown`
556
+ : result.error || "conversion failed";
557
+ content = [{ type: "text", text: `[Cannot read ${ext} file: ${errorMsg}]` }];
558
+ } else {
559
+ content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
560
+ }
561
+ } else {
562
+ // Read as text
563
+ const file = Bun.file(absolutePath);
564
+ const textContent = await file.text();
565
+ const allLines = textContent.split("\n");
566
+ const totalFileLines = allLines.length;
567
+
568
+ // Apply offset if specified (1-indexed to 0-indexed)
569
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
570
+ const startLineDisplay = startLine + 1; // For display (1-indexed)
571
+
572
+ // Check if offset is out of bounds - return graceful message instead of throwing
573
+ if (startLine >= allLines.length) {
574
+ const suggestion =
575
+ allLines.length === 0
576
+ ? "The file is empty."
577
+ : `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`;
578
+ return toolResult<ReadToolDetails>()
579
+ .text(`Offset ${offset} is beyond end of file (${allLines.length} lines total). ${suggestion}`)
580
+ .done();
581
+ }
582
+
583
+ // If limit is specified by user, use it; otherwise we'll let truncateHead decide
584
+ let selectedContent: string;
585
+ let userLimitedLines: number | undefined;
586
+ if (limit !== undefined) {
587
+ const endLine = Math.min(startLine + limit, allLines.length);
588
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
589
+ userLimitedLines = endLine - startLine;
590
+ } else {
591
+ selectedContent = allLines.slice(startLine).join("\n");
592
+ }
593
+
594
+ // Apply truncation (respects both line and byte limits)
595
+ const truncation = truncateHead(selectedContent);
596
+
597
+ // Add line numbers if requested (uses setting default if not specified)
598
+ const shouldAddLineNumbers = lines ?? this.defaultLineNumbers;
599
+ const prependLineNumbers = (text: string, startNum: number): string => {
600
+ const textLines = text.split("\n");
601
+ const lastLineNum = startNum + textLines.length - 1;
602
+ const padWidth = String(lastLineNum).length;
603
+ return textLines
604
+ .map((line, i) => {
605
+ const lineNum = String(startNum + i).padStart(padWidth, " ");
606
+ return `${lineNum}\t${line}`;
607
+ })
608
+ .join("\n");
609
+ };
610
+
611
+ let outputText: string;
612
+
613
+ if (truncation.firstLineExceedsLimit) {
614
+ const firstLine = allLines[startLine] ?? "";
615
+ const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
616
+ const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
617
+
618
+ outputText = shouldAddLineNumbers ? prependLineNumbers(snippet.text, startLineDisplay) : snippet.text;
619
+ if (snippet.text.length === 0) {
620
+ outputText = `[Line ${startLineDisplay} is ${formatSize(
621
+ firstLineBytes,
622
+ )}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
623
+ }
624
+ details = { truncation };
625
+ sourcePath = absolutePath;
626
+ truncationInfo = {
627
+ result: truncation,
628
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines },
629
+ };
630
+ } else if (truncation.truncated) {
631
+ outputText = shouldAddLineNumbers
632
+ ? prependLineNumbers(truncation.content, startLineDisplay)
633
+ : truncation.content;
634
+ details = { truncation };
635
+ sourcePath = absolutePath;
636
+ truncationInfo = {
637
+ result: truncation,
638
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines },
639
+ };
640
+ } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
641
+ const remaining = allLines.length - (startLine + userLimitedLines);
642
+ const nextOffset = startLine + userLimitedLines + 1;
643
+
644
+ outputText = shouldAddLineNumbers
645
+ ? prependLineNumbers(truncation.content, startLineDisplay)
646
+ : truncation.content;
647
+ outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
648
+ details = {};
649
+ sourcePath = absolutePath;
650
+ } else {
651
+ // No truncation, no user limit exceeded
652
+ outputText = shouldAddLineNumbers
653
+ ? prependLineNumbers(truncation.content, startLineDisplay)
654
+ : truncation.content;
655
+ details = {};
656
+ sourcePath = absolutePath;
657
+ }
658
+
659
+ content = [{ type: "text", text: outputText }];
660
+ }
661
+
662
+ const resultBuilder = toolResult(details).content(content);
663
+ if (sourcePath) {
664
+ resultBuilder.sourcePath(sourcePath);
665
+ }
666
+ if (truncationInfo) {
667
+ resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
668
+ }
669
+ return resultBuilder.done();
670
+ }
671
+
672
+ /**
673
+ * Handle internal URLs (agent://, skill://).
674
+ * Supports pagination via offset/limit but rejects them when query extraction is used.
675
+ */
676
+ private async handleInternalUrl(
677
+ url: string,
678
+ offset?: number,
679
+ limit?: number,
680
+ lines?: boolean,
681
+ ): Promise<AgentToolResult<ReadToolDetails>> {
682
+ const internalRouter = this.session.internalRouter!;
683
+
684
+ // Check if URL has query extraction (agent:// only)
685
+ let parsed: URL;
686
+ try {
687
+ parsed = new URL(url);
688
+ } catch {
689
+ throw new ToolError(`Invalid URL: ${url}`);
690
+ }
691
+ const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
692
+ const hasPathExtraction = parsed.pathname && parsed.pathname !== "/" && parsed.pathname !== "";
693
+ const queryParam = parsed.searchParams.get("q");
694
+ const hasQueryExtraction = queryParam !== null && queryParam !== "";
695
+ const hasExtraction = scheme === "agent" && (hasPathExtraction || hasQueryExtraction);
696
+
697
+ if (scheme !== "agent" && hasQueryExtraction) {
698
+ throw new ToolError("Only agent:// URLs support ?q= query extraction");
699
+ }
700
+
701
+ // Reject offset/limit with query extraction
702
+ if (hasExtraction && (offset !== undefined || limit !== undefined)) {
703
+ throw new ToolError("Cannot combine query extraction with offset/limit");
704
+ }
705
+
706
+ // Resolve the internal URL
707
+ const resource = await internalRouter.resolve(url);
708
+
709
+ // If extraction was used, return directly (no pagination)
710
+ if (hasExtraction) {
711
+ let text = resource.content;
712
+ if (resource.sourcePath) {
713
+ text += `\n\n[Resolved path: ${resource.sourcePath}]`;
714
+ }
715
+ return toolResult<ReadToolDetails>().text(text).sourceInternal(url).done();
716
+ }
717
+
718
+ // Apply pagination similar to file reading
719
+ const allLines = resource.content.split("\n");
720
+ const totalLines = allLines.length;
721
+
722
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
723
+ const startLineDisplay = startLine + 1;
724
+
725
+ if (startLine >= allLines.length) {
726
+ const suggestion =
727
+ allLines.length === 0
728
+ ? "The resource is empty."
729
+ : `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`;
730
+ return toolResult<ReadToolDetails>()
731
+ .text(`Offset ${offset} is beyond end of resource (${allLines.length} lines total). ${suggestion}`)
732
+ .done();
733
+ }
734
+
735
+ let selectedContent: string;
736
+ let userLimitedLines: number | undefined;
737
+ if (limit !== undefined) {
738
+ const endLine = Math.min(startLine + limit, allLines.length);
739
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
740
+ userLimitedLines = endLine - startLine;
741
+ } else {
742
+ selectedContent = allLines.slice(startLine).join("\n");
743
+ }
744
+
745
+ // Apply truncation
746
+ const truncation = truncateHead(selectedContent);
747
+
748
+ // Add line numbers if requested
749
+ const shouldAddLineNumbers = lines ?? this.defaultLineNumbers;
750
+ const prependLineNumbers = (text: string, startNum: number): string => {
751
+ const textLines = text.split("\n");
752
+ const lastLineNum = startNum + textLines.length - 1;
753
+ const padWidth = String(lastLineNum).length;
754
+ return textLines
755
+ .map((line, i) => {
756
+ const lineNum = String(startNum + i).padStart(padWidth, " ");
757
+ return `${lineNum}\t${line}`;
758
+ })
759
+ .join("\n");
760
+ };
761
+
762
+ let outputText: string;
763
+ let details: ReadToolDetails = {};
764
+ let truncationInfo:
765
+ | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
766
+ | undefined;
767
+
768
+ if (truncation.firstLineExceedsLimit) {
769
+ const firstLine = allLines[startLine] ?? "";
770
+ const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
771
+ const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
772
+
773
+ outputText = shouldAddLineNumbers ? prependLineNumbers(snippet.text, startLineDisplay) : snippet.text;
774
+ if (snippet.text.length === 0) {
775
+ outputText = `[Line ${startLineDisplay} is ${formatSize(
776
+ firstLineBytes,
777
+ )}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
778
+ }
779
+ details = { truncation };
780
+ truncationInfo = {
781
+ result: truncation,
782
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
783
+ };
784
+ } else if (truncation.truncated) {
785
+ outputText = shouldAddLineNumbers
786
+ ? prependLineNumbers(truncation.content, startLineDisplay)
787
+ : truncation.content;
788
+ details = { truncation };
789
+ truncationInfo = {
790
+ result: truncation,
791
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
792
+ };
793
+ } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
794
+ const remaining = allLines.length - (startLine + userLimitedLines);
795
+ const nextOffset = startLine + userLimitedLines + 1;
796
+
797
+ outputText = shouldAddLineNumbers
798
+ ? prependLineNumbers(truncation.content, startLineDisplay)
799
+ : truncation.content;
800
+ outputText += `\n\n[${remaining} more lines in resource. Use offset=${nextOffset} to continue]`;
801
+ details = {};
802
+ } else {
803
+ outputText = shouldAddLineNumbers
804
+ ? prependLineNumbers(truncation.content, startLineDisplay)
805
+ : truncation.content;
806
+ details = {};
807
+ }
808
+
809
+ // Append resolved path notice
810
+ if (resource.sourcePath) {
811
+ outputText += `\n\n[Resolved path: ${resource.sourcePath}]`;
812
+ }
813
+
814
+ const resultBuilder = toolResult(details).text(outputText).sourceInternal(url);
815
+ if (truncationInfo) {
816
+ resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
817
+ }
818
+ return resultBuilder.done();
819
+ }
820
+ }
821
+
822
+ // =============================================================================
823
+ // TUI Renderer
824
+ // =============================================================================
825
+
826
+ interface ReadRenderArgs {
827
+ path?: string;
828
+ file_path?: string;
829
+ offset?: number;
830
+ limit?: number;
831
+ }
832
+
833
+ export const readToolRenderer = {
834
+ renderCall(args: ReadRenderArgs, uiTheme: Theme): Component {
835
+ const rawPath = args.file_path || args.path || "";
836
+ const filePath = shortenPath(rawPath);
837
+ const offset = args.offset;
838
+ const limit = args.limit;
839
+
840
+ let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
841
+ if (offset !== undefined || limit !== undefined) {
842
+ const startLine = offset ?? 1;
843
+ const endLine = limit !== undefined ? startLine + limit - 1 : "";
844
+ pathDisplay += uiTheme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
845
+ }
846
+
847
+ const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Read"))} ${pathDisplay}`;
848
+ return new Text(text, 0, 0);
849
+ },
850
+
851
+ renderResult(
852
+ result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
853
+ _options: RenderResultOptions,
854
+ uiTheme: Theme,
855
+ _args?: ReadRenderArgs,
856
+ ): Component {
857
+ const details = result.details;
858
+ const lines: string[] = [];
859
+
860
+ lines.push(uiTheme.fg("dim", "Content hidden"));
861
+
862
+ const truncation = details?.meta?.truncation;
863
+ const fallback = details?.truncation;
864
+ if (truncation) {
865
+ let warning: string;
866
+ if (fallback?.firstLineExceedsLimit) {
867
+ warning = `First line exceeds ${formatSize(fallback.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
868
+ } else if (truncation.truncatedBy === "lines") {
869
+ warning = `Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${DEFAULT_MAX_LINES} line limit)`;
870
+ } else {
871
+ const maxBytes = fallback?.maxBytes ?? DEFAULT_MAX_BYTES;
872
+ warning = `Truncated: ${truncation.outputLines} lines (${formatSize(maxBytes)} limit)`;
873
+ }
874
+ if (truncation.artifactId) {
875
+ warning += `. Full output: artifact://${truncation.artifactId}`;
876
+ }
877
+ lines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
878
+ }
879
+
880
+ return new Text(lines.join("\n"), 0, 0);
881
+ },
882
+ };