@nghyane/arcane 0.1.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 (738) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +12 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +109 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/custom-tools/todo/index.ts +206 -0
  7. package/examples/extensions/README.md +143 -0
  8. package/examples/extensions/api-demo.ts +89 -0
  9. package/examples/extensions/chalk-logger.ts +25 -0
  10. package/examples/extensions/hello.ts +32 -0
  11. package/examples/extensions/pirate.ts +43 -0
  12. package/examples/extensions/plan-mode.ts +550 -0
  13. package/examples/extensions/reload-runtime.ts +37 -0
  14. package/examples/extensions/todo.ts +296 -0
  15. package/examples/extensions/tools.ts +144 -0
  16. package/examples/extensions/with-deps/index.ts +35 -0
  17. package/examples/extensions/with-deps/package-lock.json +31 -0
  18. package/examples/extensions/with-deps/package.json +16 -0
  19. package/examples/hooks/README.md +56 -0
  20. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  21. package/examples/hooks/confirm-destructive.ts +58 -0
  22. package/examples/hooks/custom-compaction.ts +116 -0
  23. package/examples/hooks/dirty-repo-guard.ts +51 -0
  24. package/examples/hooks/file-trigger.ts +40 -0
  25. package/examples/hooks/git-checkpoint.ts +52 -0
  26. package/examples/hooks/handoff.ts +150 -0
  27. package/examples/hooks/permission-gate.ts +33 -0
  28. package/examples/hooks/protected-paths.ts +29 -0
  29. package/examples/hooks/qna.ts +119 -0
  30. package/examples/hooks/status-line.ts +39 -0
  31. package/examples/sdk/01-minimal.ts +21 -0
  32. package/examples/sdk/02-custom-model.ts +49 -0
  33. package/examples/sdk/03-custom-prompt.ts +43 -0
  34. package/examples/sdk/04-skills.ts +43 -0
  35. package/examples/sdk/06-extensions.ts +80 -0
  36. package/examples/sdk/06-hooks.ts +61 -0
  37. package/examples/sdk/07-context-files.ts +35 -0
  38. package/examples/sdk/08-prompt-templates.ts +36 -0
  39. package/examples/sdk/08-slash-commands.ts +41 -0
  40. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  41. package/examples/sdk/11-sessions.ts +47 -0
  42. package/examples/sdk/README.md +150 -0
  43. package/package.json +464 -0
  44. package/scripts/format-prompts.ts +184 -0
  45. package/scripts/generate-docs-index.ts +40 -0
  46. package/scripts/generate-template.ts +32 -0
  47. package/src/bun-imports.d.ts +22 -0
  48. package/src/capability/context-file.ts +39 -0
  49. package/src/capability/extension-module.ts +33 -0
  50. package/src/capability/extension.ts +47 -0
  51. package/src/capability/fs.ts +89 -0
  52. package/src/capability/hook.ts +39 -0
  53. package/src/capability/index.ts +432 -0
  54. package/src/capability/instruction.ts +36 -0
  55. package/src/capability/mcp.ts +60 -0
  56. package/src/capability/prompt.ts +34 -0
  57. package/src/capability/rule.ts +223 -0
  58. package/src/capability/settings.ts +34 -0
  59. package/src/capability/skill.ts +48 -0
  60. package/src/capability/slash-command.ts +39 -0
  61. package/src/capability/ssh.ts +41 -0
  62. package/src/capability/system-prompt.ts +34 -0
  63. package/src/capability/tool.ts +37 -0
  64. package/src/capability/types.ts +156 -0
  65. package/src/cli/args.ts +259 -0
  66. package/src/cli/config-cli.ts +357 -0
  67. package/src/cli/file-processor.ts +124 -0
  68. package/src/cli/grep-cli.ts +152 -0
  69. package/src/cli/jupyter-cli.ts +106 -0
  70. package/src/cli/list-models.ts +103 -0
  71. package/src/cli/plugin-cli.ts +661 -0
  72. package/src/cli/session-picker.ts +42 -0
  73. package/src/cli/setup-cli.ts +376 -0
  74. package/src/cli/shell-cli.ts +174 -0
  75. package/src/cli/ssh-cli.ts +179 -0
  76. package/src/cli/stats-cli.ts +197 -0
  77. package/src/cli/update-cli.ts +286 -0
  78. package/src/cli/web-search-cli.ts +143 -0
  79. package/src/cli.ts +65 -0
  80. package/src/commands/commit.ts +36 -0
  81. package/src/commands/config.ts +51 -0
  82. package/src/commands/grep.ts +41 -0
  83. package/src/commands/jupyter.ts +32 -0
  84. package/src/commands/launch.ts +139 -0
  85. package/src/commands/plugin.ts +70 -0
  86. package/src/commands/setup.ts +42 -0
  87. package/src/commands/shell.ts +29 -0
  88. package/src/commands/ssh.ts +60 -0
  89. package/src/commands/stats.ts +29 -0
  90. package/src/commands/update.ts +21 -0
  91. package/src/commands/web-search.ts +42 -0
  92. package/src/commit/agentic/agent.ts +311 -0
  93. package/src/commit/agentic/fallback.ts +96 -0
  94. package/src/commit/agentic/index.ts +359 -0
  95. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  96. package/src/commit/agentic/prompts/session-user.md +25 -0
  97. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  98. package/src/commit/agentic/prompts/system.md +38 -0
  99. package/src/commit/agentic/state.ts +69 -0
  100. package/src/commit/agentic/tools/analyze-file.ts +118 -0
  101. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  102. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  103. package/src/commit/agentic/tools/git-overview.ts +84 -0
  104. package/src/commit/agentic/tools/index.ts +56 -0
  105. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  106. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  107. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  108. package/src/commit/agentic/tools/split-commit.ts +280 -0
  109. package/src/commit/agentic/topo-sort.ts +44 -0
  110. package/src/commit/agentic/trivial.ts +51 -0
  111. package/src/commit/agentic/validation.ts +200 -0
  112. package/src/commit/analysis/conventional.ts +165 -0
  113. package/src/commit/analysis/index.ts +4 -0
  114. package/src/commit/analysis/scope.ts +242 -0
  115. package/src/commit/analysis/summary.ts +112 -0
  116. package/src/commit/analysis/validation.ts +66 -0
  117. package/src/commit/changelog/detect.ts +37 -0
  118. package/src/commit/changelog/generate.ts +110 -0
  119. package/src/commit/changelog/index.ts +234 -0
  120. package/src/commit/changelog/parse.ts +44 -0
  121. package/src/commit/cli.ts +93 -0
  122. package/src/commit/git/diff.ts +148 -0
  123. package/src/commit/git/errors.ts +9 -0
  124. package/src/commit/git/index.ts +211 -0
  125. package/src/commit/git/operations.ts +54 -0
  126. package/src/commit/index.ts +5 -0
  127. package/src/commit/map-reduce/index.ts +64 -0
  128. package/src/commit/map-reduce/map-phase.ts +178 -0
  129. package/src/commit/map-reduce/reduce-phase.ts +145 -0
  130. package/src/commit/map-reduce/utils.ts +9 -0
  131. package/src/commit/message.ts +11 -0
  132. package/src/commit/model-selection.ts +69 -0
  133. package/src/commit/pipeline.ts +243 -0
  134. package/src/commit/prompts/analysis-system.md +148 -0
  135. package/src/commit/prompts/analysis-user.md +38 -0
  136. package/src/commit/prompts/changelog-system.md +50 -0
  137. package/src/commit/prompts/changelog-user.md +18 -0
  138. package/src/commit/prompts/file-observer-system.md +24 -0
  139. package/src/commit/prompts/file-observer-user.md +8 -0
  140. package/src/commit/prompts/reduce-system.md +50 -0
  141. package/src/commit/prompts/reduce-user.md +17 -0
  142. package/src/commit/prompts/summary-retry.md +3 -0
  143. package/src/commit/prompts/summary-system.md +38 -0
  144. package/src/commit/prompts/summary-user.md +13 -0
  145. package/src/commit/prompts/types-description.md +2 -0
  146. package/src/commit/types.ts +109 -0
  147. package/src/commit/utils/exclusions.ts +42 -0
  148. package/src/config/file-lock.ts +121 -0
  149. package/src/config/keybindings.ts +280 -0
  150. package/src/config/model-registry.ts +1140 -0
  151. package/src/config/model-resolver.ts +812 -0
  152. package/src/config/prompt-templates.ts +526 -0
  153. package/src/config/resolve-config-value.ts +92 -0
  154. package/src/config/settings-schema.ts +1236 -0
  155. package/src/config/settings.ts +706 -0
  156. package/src/config.ts +414 -0
  157. package/src/cursor.ts +239 -0
  158. package/src/debug/index.ts +431 -0
  159. package/src/debug/log-formatting.ts +60 -0
  160. package/src/debug/log-viewer.ts +903 -0
  161. package/src/debug/profiler.ts +158 -0
  162. package/src/debug/report-bundle.ts +366 -0
  163. package/src/debug/system-info.ts +112 -0
  164. package/src/discovery/agents-md.ts +68 -0
  165. package/src/discovery/agents.ts +199 -0
  166. package/src/discovery/builtin.ts +815 -0
  167. package/src/discovery/claude-plugins.ts +205 -0
  168. package/src/discovery/claude.ts +506 -0
  169. package/src/discovery/cline.ts +83 -0
  170. package/src/discovery/codex.ts +532 -0
  171. package/src/discovery/cursor.ts +218 -0
  172. package/src/discovery/gemini.ts +395 -0
  173. package/src/discovery/github.ts +117 -0
  174. package/src/discovery/helpers.ts +698 -0
  175. package/src/discovery/index.ts +89 -0
  176. package/src/discovery/mcp-json.ts +156 -0
  177. package/src/discovery/opencode.ts +394 -0
  178. package/src/discovery/ssh.ts +160 -0
  179. package/src/discovery/vscode.ts +103 -0
  180. package/src/discovery/windsurf.ts +145 -0
  181. package/src/exa/company.ts +57 -0
  182. package/src/exa/index.ts +62 -0
  183. package/src/exa/linkedin.ts +57 -0
  184. package/src/exa/mcp-client.ts +289 -0
  185. package/src/exa/render.ts +244 -0
  186. package/src/exa/researcher.ts +89 -0
  187. package/src/exa/search.ts +330 -0
  188. package/src/exa/types.ts +166 -0
  189. package/src/exa/websets.ts +247 -0
  190. package/src/exec/bash-executor.ts +184 -0
  191. package/src/exec/exec.ts +53 -0
  192. package/src/export/custom-share.ts +65 -0
  193. package/src/export/html/index.ts +162 -0
  194. package/src/export/html/template.css +889 -0
  195. package/src/export/html/template.generated.ts +2 -0
  196. package/src/export/html/template.html +45 -0
  197. package/src/export/html/template.js +1329 -0
  198. package/src/export/html/template.macro.ts +24 -0
  199. package/src/export/html/vendor/highlight.min.js +1213 -0
  200. package/src/export/html/vendor/marked.min.js +6 -0
  201. package/src/export/ttsr.ts +434 -0
  202. package/src/extensibility/custom-commands/bundled/review/index.ts +433 -0
  203. package/src/extensibility/custom-commands/index.ts +15 -0
  204. package/src/extensibility/custom-commands/loader.ts +231 -0
  205. package/src/extensibility/custom-commands/types.ts +111 -0
  206. package/src/extensibility/custom-tools/index.ts +22 -0
  207. package/src/extensibility/custom-tools/loader.ts +235 -0
  208. package/src/extensibility/custom-tools/types.ts +226 -0
  209. package/src/extensibility/custom-tools/wrapper.ts +45 -0
  210. package/src/extensibility/extensions/index.ts +136 -0
  211. package/src/extensibility/extensions/loader.ts +520 -0
  212. package/src/extensibility/extensions/runner.ts +774 -0
  213. package/src/extensibility/extensions/types.ts +1293 -0
  214. package/src/extensibility/extensions/wrapper.ts +188 -0
  215. package/src/extensibility/hooks/index.ts +16 -0
  216. package/src/extensibility/hooks/loader.ts +273 -0
  217. package/src/extensibility/hooks/runner.ts +441 -0
  218. package/src/extensibility/hooks/tool-wrapper.ts +106 -0
  219. package/src/extensibility/hooks/types.ts +817 -0
  220. package/src/extensibility/plugins/doctor.ts +65 -0
  221. package/src/extensibility/plugins/git-url.ts +281 -0
  222. package/src/extensibility/plugins/index.ts +33 -0
  223. package/src/extensibility/plugins/installer.ts +192 -0
  224. package/src/extensibility/plugins/loader.ts +338 -0
  225. package/src/extensibility/plugins/manager.ts +716 -0
  226. package/src/extensibility/plugins/parser.ts +105 -0
  227. package/src/extensibility/plugins/types.ts +190 -0
  228. package/src/extensibility/skills.ts +385 -0
  229. package/src/extensibility/slash-commands.ts +287 -0
  230. package/src/extensibility/tool-proxy.ts +25 -0
  231. package/src/index.ts +275 -0
  232. package/src/internal-urls/agent-protocol.ts +136 -0
  233. package/src/internal-urls/artifact-protocol.ts +97 -0
  234. package/src/internal-urls/docs-index.generated.ts +54 -0
  235. package/src/internal-urls/docs-protocol.ts +84 -0
  236. package/src/internal-urls/index.ts +31 -0
  237. package/src/internal-urls/json-query.ts +126 -0
  238. package/src/internal-urls/memory-protocol.ts +133 -0
  239. package/src/internal-urls/router.ts +70 -0
  240. package/src/internal-urls/rule-protocol.ts +55 -0
  241. package/src/internal-urls/skill-protocol.ts +111 -0
  242. package/src/internal-urls/types.ts +52 -0
  243. package/src/ipy/executor.ts +556 -0
  244. package/src/ipy/gateway-coordinator.ts +426 -0
  245. package/src/ipy/kernel.ts +892 -0
  246. package/src/ipy/modules.ts +109 -0
  247. package/src/ipy/prelude.py +831 -0
  248. package/src/ipy/prelude.ts +3 -0
  249. package/src/ipy/runtime.ts +222 -0
  250. package/src/lsp/client.ts +867 -0
  251. package/src/lsp/clients/biome-client.ts +202 -0
  252. package/src/lsp/clients/index.ts +50 -0
  253. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  254. package/src/lsp/clients/swiftlint-client.ts +120 -0
  255. package/src/lsp/config.ts +397 -0
  256. package/src/lsp/defaults.json +464 -0
  257. package/src/lsp/edits.ts +109 -0
  258. package/src/lsp/index.ts +1268 -0
  259. package/src/lsp/lspmux.ts +250 -0
  260. package/src/lsp/render.ts +689 -0
  261. package/src/lsp/types.ts +414 -0
  262. package/src/lsp/utils.ts +549 -0
  263. package/src/main.ts +773 -0
  264. package/src/mcp/client.ts +239 -0
  265. package/src/mcp/config-writer.ts +215 -0
  266. package/src/mcp/config.ts +363 -0
  267. package/src/mcp/index.ts +55 -0
  268. package/src/mcp/json-rpc.ts +84 -0
  269. package/src/mcp/loader.ts +124 -0
  270. package/src/mcp/manager.ts +490 -0
  271. package/src/mcp/oauth-discovery.ts +274 -0
  272. package/src/mcp/oauth-flow.ts +229 -0
  273. package/src/mcp/render.ts +123 -0
  274. package/src/mcp/tool-bridge.ts +372 -0
  275. package/src/mcp/tool-cache.ts +121 -0
  276. package/src/mcp/transports/http.ts +332 -0
  277. package/src/mcp/transports/index.ts +6 -0
  278. package/src/mcp/transports/stdio.ts +281 -0
  279. package/src/mcp/types.ts +248 -0
  280. package/src/memories/index.ts +1099 -0
  281. package/src/memories/storage.ts +563 -0
  282. package/src/modes/components/agent-dashboard.ts +1130 -0
  283. package/src/modes/components/assistant-message.ts +144 -0
  284. package/src/modes/components/bash-execution.ts +218 -0
  285. package/src/modes/components/bordered-loader.ts +41 -0
  286. package/src/modes/components/branch-summary-message.ts +45 -0
  287. package/src/modes/components/codemode-group.ts +369 -0
  288. package/src/modes/components/compaction-summary-message.ts +51 -0
  289. package/src/modes/components/countdown-timer.ts +46 -0
  290. package/src/modes/components/custom-editor.ts +181 -0
  291. package/src/modes/components/custom-message.ts +91 -0
  292. package/src/modes/components/diff.ts +186 -0
  293. package/src/modes/components/dynamic-border.ts +25 -0
  294. package/src/modes/components/extensions/extension-dashboard.ts +325 -0
  295. package/src/modes/components/extensions/extension-list.ts +484 -0
  296. package/src/modes/components/extensions/index.ts +9 -0
  297. package/src/modes/components/extensions/inspector-panel.ts +321 -0
  298. package/src/modes/components/extensions/state-manager.ts +586 -0
  299. package/src/modes/components/extensions/types.ts +191 -0
  300. package/src/modes/components/footer.ts +315 -0
  301. package/src/modes/components/history-search.ts +157 -0
  302. package/src/modes/components/hook-editor.ts +101 -0
  303. package/src/modes/components/hook-input.ts +72 -0
  304. package/src/modes/components/hook-message.ts +100 -0
  305. package/src/modes/components/hook-selector.ts +155 -0
  306. package/src/modes/components/index.ts +41 -0
  307. package/src/modes/components/keybinding-hints.ts +65 -0
  308. package/src/modes/components/login-dialog.ts +164 -0
  309. package/src/modes/components/mcp-add-wizard.ts +1295 -0
  310. package/src/modes/components/model-selector.ts +625 -0
  311. package/src/modes/components/oauth-selector.ts +210 -0
  312. package/src/modes/components/plugin-settings.ts +477 -0
  313. package/src/modes/components/python-execution.ts +196 -0
  314. package/src/modes/components/queue-mode-selector.ts +56 -0
  315. package/src/modes/components/read-tool-group.ts +119 -0
  316. package/src/modes/components/session-selector.ts +242 -0
  317. package/src/modes/components/settings-defs.ts +340 -0
  318. package/src/modes/components/settings-selector.ts +529 -0
  319. package/src/modes/components/show-images-selector.ts +45 -0
  320. package/src/modes/components/skill-message.ts +90 -0
  321. package/src/modes/components/status-line/index.ts +4 -0
  322. package/src/modes/components/status-line/presets.ts +94 -0
  323. package/src/modes/components/status-line/segments.ts +352 -0
  324. package/src/modes/components/status-line/separators.ts +55 -0
  325. package/src/modes/components/status-line/types.ts +75 -0
  326. package/src/modes/components/status-line-segment-editor.ts +354 -0
  327. package/src/modes/components/status-line.ts +421 -0
  328. package/src/modes/components/theme-selector.ts +63 -0
  329. package/src/modes/components/thinking-selector.ts +64 -0
  330. package/src/modes/components/todo-display.ts +115 -0
  331. package/src/modes/components/todo-reminder.ts +40 -0
  332. package/src/modes/components/tool-execution.ts +703 -0
  333. package/src/modes/components/tree-selector.ts +904 -0
  334. package/src/modes/components/ttsr-notification.ts +80 -0
  335. package/src/modes/components/user-message-selector.ts +146 -0
  336. package/src/modes/components/user-message.ts +22 -0
  337. package/src/modes/components/visual-truncate.ts +63 -0
  338. package/src/modes/components/welcome.ts +247 -0
  339. package/src/modes/controllers/command-controller.ts +1120 -0
  340. package/src/modes/controllers/event-controller.ts +479 -0
  341. package/src/modes/controllers/extension-ui-controller.ts +778 -0
  342. package/src/modes/controllers/input-controller.ts +671 -0
  343. package/src/modes/controllers/mcp-command-controller.ts +1315 -0
  344. package/src/modes/controllers/selector-controller.ts +712 -0
  345. package/src/modes/controllers/ssh-command-controller.ts +452 -0
  346. package/src/modes/index.ts +15 -0
  347. package/src/modes/interactive-mode.ts +1027 -0
  348. package/src/modes/print-mode.ts +191 -0
  349. package/src/modes/rpc/rpc-client.ts +583 -0
  350. package/src/modes/rpc/rpc-mode.ts +700 -0
  351. package/src/modes/rpc/rpc-types.ts +236 -0
  352. package/src/modes/theme/dark.json +95 -0
  353. package/src/modes/theme/defaults/alabaster.json +93 -0
  354. package/src/modes/theme/defaults/amethyst.json +96 -0
  355. package/src/modes/theme/defaults/anthracite.json +93 -0
  356. package/src/modes/theme/defaults/basalt.json +91 -0
  357. package/src/modes/theme/defaults/birch.json +95 -0
  358. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  359. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  360. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  361. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  362. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  363. package/src/modes/theme/defaults/dark-copper.json +95 -0
  364. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  365. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  366. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  367. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  368. package/src/modes/theme/defaults/dark-ember.json +95 -0
  369. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  370. package/src/modes/theme/defaults/dark-forest.json +96 -0
  371. package/src/modes/theme/defaults/dark-github.json +105 -0
  372. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  373. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  374. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  375. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  376. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  377. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  378. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  379. package/src/modes/theme/defaults/dark-nord.json +97 -0
  380. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  381. package/src/modes/theme/defaults/dark-one.json +100 -0
  382. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  383. package/src/modes/theme/defaults/dark-reef.json +91 -0
  384. package/src/modes/theme/defaults/dark-retro.json +92 -0
  385. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  386. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  387. package/src/modes/theme/defaults/dark-slate.json +95 -0
  388. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  389. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  390. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  391. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  392. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  393. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  394. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  395. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  396. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  397. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  398. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  399. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  400. package/src/modes/theme/defaults/graphite.json +92 -0
  401. package/src/modes/theme/defaults/index.ts +195 -0
  402. package/src/modes/theme/defaults/light-arctic.json +107 -0
  403. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  404. package/src/modes/theme/defaults/light-canyon.json +91 -0
  405. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  406. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  407. package/src/modes/theme/defaults/light-coral.json +95 -0
  408. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  409. package/src/modes/theme/defaults/light-dawn.json +90 -0
  410. package/src/modes/theme/defaults/light-dunes.json +91 -0
  411. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  412. package/src/modes/theme/defaults/light-forest.json +100 -0
  413. package/src/modes/theme/defaults/light-frost.json +95 -0
  414. package/src/modes/theme/defaults/light-github.json +115 -0
  415. package/src/modes/theme/defaults/light-glacier.json +91 -0
  416. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  417. package/src/modes/theme/defaults/light-haze.json +90 -0
  418. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  419. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  420. package/src/modes/theme/defaults/light-lavender.json +95 -0
  421. package/src/modes/theme/defaults/light-meadow.json +91 -0
  422. package/src/modes/theme/defaults/light-mint.json +95 -0
  423. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  424. package/src/modes/theme/defaults/light-ocean.json +99 -0
  425. package/src/modes/theme/defaults/light-one.json +99 -0
  426. package/src/modes/theme/defaults/light-opal.json +91 -0
  427. package/src/modes/theme/defaults/light-orchard.json +91 -0
  428. package/src/modes/theme/defaults/light-paper.json +95 -0
  429. package/src/modes/theme/defaults/light-prism.json +90 -0
  430. package/src/modes/theme/defaults/light-retro.json +98 -0
  431. package/src/modes/theme/defaults/light-sand.json +95 -0
  432. package/src/modes/theme/defaults/light-savanna.json +91 -0
  433. package/src/modes/theme/defaults/light-solarized.json +102 -0
  434. package/src/modes/theme/defaults/light-soleil.json +90 -0
  435. package/src/modes/theme/defaults/light-sunset.json +99 -0
  436. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  437. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  438. package/src/modes/theme/defaults/light-wetland.json +91 -0
  439. package/src/modes/theme/defaults/light-zenith.json +89 -0
  440. package/src/modes/theme/defaults/limestone.json +94 -0
  441. package/src/modes/theme/defaults/mahogany.json +97 -0
  442. package/src/modes/theme/defaults/marble.json +93 -0
  443. package/src/modes/theme/defaults/obsidian.json +91 -0
  444. package/src/modes/theme/defaults/onyx.json +91 -0
  445. package/src/modes/theme/defaults/pearl.json +93 -0
  446. package/src/modes/theme/defaults/porcelain.json +91 -0
  447. package/src/modes/theme/defaults/quartz.json +96 -0
  448. package/src/modes/theme/defaults/sandstone.json +95 -0
  449. package/src/modes/theme/defaults/titanium.json +90 -0
  450. package/src/modes/theme/light.json +93 -0
  451. package/src/modes/theme/mermaid-cache.ts +111 -0
  452. package/src/modes/theme/theme-schema.json +429 -0
  453. package/src/modes/theme/theme.ts +2333 -0
  454. package/src/modes/types.ts +216 -0
  455. package/src/modes/utils/ui-helpers.ts +529 -0
  456. package/src/patch/applicator.ts +1482 -0
  457. package/src/patch/diff.ts +425 -0
  458. package/src/patch/fuzzy.ts +784 -0
  459. package/src/patch/hashline.ts +972 -0
  460. package/src/patch/index.ts +964 -0
  461. package/src/patch/normalize.ts +397 -0
  462. package/src/patch/normative.ts +72 -0
  463. package/src/patch/parser.ts +532 -0
  464. package/src/patch/shared.ts +400 -0
  465. package/src/patch/types.ts +292 -0
  466. package/src/priority.json +35 -0
  467. package/src/prompts/agents/explore.md +48 -0
  468. package/src/prompts/agents/frontmatter.md +9 -0
  469. package/src/prompts/agents/init.md +36 -0
  470. package/src/prompts/agents/librarian.md +53 -0
  471. package/src/prompts/agents/oracle.md +51 -0
  472. package/src/prompts/agents/reviewer.md +70 -0
  473. package/src/prompts/agents/task.md +14 -0
  474. package/src/prompts/compaction/branch-summary-context.md +5 -0
  475. package/src/prompts/compaction/branch-summary-preamble.md +2 -0
  476. package/src/prompts/compaction/branch-summary.md +30 -0
  477. package/src/prompts/compaction/compaction-short-summary.md +9 -0
  478. package/src/prompts/compaction/compaction-summary-context.md +5 -0
  479. package/src/prompts/compaction/compaction-summary.md +38 -0
  480. package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
  481. package/src/prompts/compaction/compaction-update-summary.md +45 -0
  482. package/src/prompts/memories/consolidation.md +30 -0
  483. package/src/prompts/memories/read_path.md +11 -0
  484. package/src/prompts/memories/stage_one_input.md +6 -0
  485. package/src/prompts/memories/stage_one_system.md +21 -0
  486. package/src/prompts/review-request.md +64 -0
  487. package/src/prompts/system/agent-creation-architect.md +65 -0
  488. package/src/prompts/system/agent-creation-user.md +6 -0
  489. package/src/prompts/system/custom-system-prompt.md +68 -0
  490. package/src/prompts/system/file-operations.md +10 -0
  491. package/src/prompts/system/subagent-submit-reminder.md +11 -0
  492. package/src/prompts/system/subagent-system-prompt.md +31 -0
  493. package/src/prompts/system/subagent-user-prompt.md +8 -0
  494. package/src/prompts/system/summarization-system.md +3 -0
  495. package/src/prompts/system/system-prompt.md +300 -0
  496. package/src/prompts/system/title-system.md +2 -0
  497. package/src/prompts/system/ttsr-interrupt.md +7 -0
  498. package/src/prompts/system/web-search.md +28 -0
  499. package/src/prompts/tools/ask.md +44 -0
  500. package/src/prompts/tools/bash.md +24 -0
  501. package/src/prompts/tools/browser.md +33 -0
  502. package/src/prompts/tools/calculator.md +12 -0
  503. package/src/prompts/tools/explore.md +29 -0
  504. package/src/prompts/tools/fetch.md +16 -0
  505. package/src/prompts/tools/find.md +18 -0
  506. package/src/prompts/tools/gemini-image.md +23 -0
  507. package/src/prompts/tools/grep.md +28 -0
  508. package/src/prompts/tools/hashline.md +232 -0
  509. package/src/prompts/tools/librarian.md +24 -0
  510. package/src/prompts/tools/lsp.md +28 -0
  511. package/src/prompts/tools/oracle.md +26 -0
  512. package/src/prompts/tools/patch.md +74 -0
  513. package/src/prompts/tools/python.md +66 -0
  514. package/src/prompts/tools/read.md +36 -0
  515. package/src/prompts/tools/replace.md +38 -0
  516. package/src/prompts/tools/reviewer.md +41 -0
  517. package/src/prompts/tools/ssh.md +51 -0
  518. package/src/prompts/tools/task-summary.md +28 -0
  519. package/src/prompts/tools/task.md +275 -0
  520. package/src/prompts/tools/todo-write.md +65 -0
  521. package/src/prompts/tools/undo-edit.md +7 -0
  522. package/src/prompts/tools/web-search.md +19 -0
  523. package/src/prompts/tools/write.md +18 -0
  524. package/src/sdk.ts +1287 -0
  525. package/src/secrets/index.ts +116 -0
  526. package/src/secrets/obfuscator.ts +269 -0
  527. package/src/secrets/regex.ts +21 -0
  528. package/src/session/agent-session.ts +4669 -0
  529. package/src/session/agent-storage.ts +621 -0
  530. package/src/session/artifacts.ts +132 -0
  531. package/src/session/auth-storage.ts +1433 -0
  532. package/src/session/blob-store.ts +103 -0
  533. package/src/session/compaction/branch-summarization.ts +315 -0
  534. package/src/session/compaction/compaction.ts +864 -0
  535. package/src/session/compaction/index.ts +7 -0
  536. package/src/session/compaction/pruning.ts +91 -0
  537. package/src/session/compaction/utils.ts +171 -0
  538. package/src/session/history-storage.ts +170 -0
  539. package/src/session/messages.ts +317 -0
  540. package/src/session/session-manager.ts +2276 -0
  541. package/src/session/session-storage.ts +342 -0
  542. package/src/session/streaming-output.ts +565 -0
  543. package/src/slash-commands/builtin-registry.ts +439 -0
  544. package/src/ssh/config-writer.ts +183 -0
  545. package/src/ssh/connection-manager.ts +444 -0
  546. package/src/ssh/ssh-executor.ts +127 -0
  547. package/src/ssh/sshfs-mount.ts +135 -0
  548. package/src/stt/downloader.ts +71 -0
  549. package/src/stt/index.ts +3 -0
  550. package/src/stt/recorder.ts +351 -0
  551. package/src/stt/setup.ts +52 -0
  552. package/src/stt/stt-controller.ts +160 -0
  553. package/src/stt/transcribe.py +70 -0
  554. package/src/stt/transcriber.ts +91 -0
  555. package/src/system-prompt.ts +685 -0
  556. package/src/task/agents.ts +155 -0
  557. package/src/task/batch.ts +102 -0
  558. package/src/task/commands.ts +134 -0
  559. package/src/task/discovery.ts +126 -0
  560. package/src/task/executor.ts +908 -0
  561. package/src/task/index.ts +223 -0
  562. package/src/task/output-manager.ts +107 -0
  563. package/src/task/parallel.ts +84 -0
  564. package/src/task/render.ts +326 -0
  565. package/src/task/subprocess-tool-registry.ts +88 -0
  566. package/src/task/template.ts +32 -0
  567. package/src/task/types.ts +144 -0
  568. package/src/tools/ask.ts +523 -0
  569. package/src/tools/bash-interactive.ts +419 -0
  570. package/src/tools/bash-interceptor.ts +105 -0
  571. package/src/tools/bash-normalize.ts +107 -0
  572. package/src/tools/bash-skill-urls.ts +177 -0
  573. package/src/tools/bash.ts +347 -0
  574. package/src/tools/browser.ts +1374 -0
  575. package/src/tools/calculator.ts +537 -0
  576. package/src/tools/context.ts +39 -0
  577. package/src/tools/explore.ts +23 -0
  578. package/src/tools/fetch.ts +1091 -0
  579. package/src/tools/find.ts +540 -0
  580. package/src/tools/fs-cache-invalidation.ts +28 -0
  581. package/src/tools/gemini-image.ts +907 -0
  582. package/src/tools/grep.ts +489 -0
  583. package/src/tools/index.ts +337 -0
  584. package/src/tools/json-tree.ts +231 -0
  585. package/src/tools/jtd-to-json-schema.ts +247 -0
  586. package/src/tools/jtd-to-typescript.ts +198 -0
  587. package/src/tools/librarian.ts +33 -0
  588. package/src/tools/list-limit.ts +40 -0
  589. package/src/tools/notebook.ts +287 -0
  590. package/src/tools/oracle.ts +40 -0
  591. package/src/tools/output-meta.ts +459 -0
  592. package/src/tools/output-utils.ts +63 -0
  593. package/src/tools/path-utils.ts +116 -0
  594. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  595. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  596. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  597. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  598. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  599. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  600. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  601. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  602. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  603. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  604. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  605. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  606. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  607. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  608. package/src/tools/python.ts +1118 -0
  609. package/src/tools/read.ts +1193 -0
  610. package/src/tools/render-utils.ts +680 -0
  611. package/src/tools/renderers.ts +60 -0
  612. package/src/tools/reviewer-tool.ts +41 -0
  613. package/src/tools/ssh.ts +326 -0
  614. package/src/tools/subagent-tool.ts +169 -0
  615. package/src/tools/submit-result.ts +152 -0
  616. package/src/tools/todo-write.ts +255 -0
  617. package/src/tools/tool-errors.ts +92 -0
  618. package/src/tools/tool-result.ts +86 -0
  619. package/src/tools/undo-edit.ts +145 -0
  620. package/src/tools/undo-history.ts +22 -0
  621. package/src/tools/write.ts +274 -0
  622. package/src/tui/code-cell.ts +108 -0
  623. package/src/tui/file-list.ts +47 -0
  624. package/src/tui/index.ts +11 -0
  625. package/src/tui/output-block.ts +144 -0
  626. package/src/tui/status-line.ts +39 -0
  627. package/src/tui/tree-list.ts +53 -0
  628. package/src/tui/types.ts +16 -0
  629. package/src/tui/utils.ts +116 -0
  630. package/src/utils/changelog.ts +98 -0
  631. package/src/utils/event-bus.ts +33 -0
  632. package/src/utils/external-editor.ts +59 -0
  633. package/src/utils/file-display-mode.ts +36 -0
  634. package/src/utils/file-mentions.ts +384 -0
  635. package/src/utils/frontmatter.ts +101 -0
  636. package/src/utils/fuzzy.ts +108 -0
  637. package/src/utils/ignore-files.ts +119 -0
  638. package/src/utils/image-convert.ts +27 -0
  639. package/src/utils/image-resize.ts +236 -0
  640. package/src/utils/mime.ts +30 -0
  641. package/src/utils/open.ts +20 -0
  642. package/src/utils/shell-snapshot.ts +199 -0
  643. package/src/utils/timings.ts +26 -0
  644. package/src/utils/title-generator.ts +167 -0
  645. package/src/utils/tools-manager.ts +362 -0
  646. package/src/web/scrapers/artifacthub.ts +215 -0
  647. package/src/web/scrapers/arxiv.ts +88 -0
  648. package/src/web/scrapers/aur.ts +175 -0
  649. package/src/web/scrapers/biorxiv.ts +141 -0
  650. package/src/web/scrapers/bluesky.ts +284 -0
  651. package/src/web/scrapers/brew.ts +177 -0
  652. package/src/web/scrapers/cheatsh.ts +78 -0
  653. package/src/web/scrapers/chocolatey.ts +158 -0
  654. package/src/web/scrapers/choosealicense.ts +110 -0
  655. package/src/web/scrapers/cisa-kev.ts +100 -0
  656. package/src/web/scrapers/clojars.ts +180 -0
  657. package/src/web/scrapers/coingecko.ts +184 -0
  658. package/src/web/scrapers/crates-io.ts +128 -0
  659. package/src/web/scrapers/crossref.ts +149 -0
  660. package/src/web/scrapers/devto.ts +177 -0
  661. package/src/web/scrapers/discogs.ts +307 -0
  662. package/src/web/scrapers/discourse.ts +221 -0
  663. package/src/web/scrapers/dockerhub.ts +160 -0
  664. package/src/web/scrapers/fdroid.ts +158 -0
  665. package/src/web/scrapers/firefox-addons.ts +214 -0
  666. package/src/web/scrapers/flathub.ts +239 -0
  667. package/src/web/scrapers/github-gist.ts +68 -0
  668. package/src/web/scrapers/github.ts +490 -0
  669. package/src/web/scrapers/gitlab.ts +456 -0
  670. package/src/web/scrapers/go-pkg.ts +275 -0
  671. package/src/web/scrapers/hackage.ts +94 -0
  672. package/src/web/scrapers/hackernews.ts +208 -0
  673. package/src/web/scrapers/hex.ts +121 -0
  674. package/src/web/scrapers/huggingface.ts +385 -0
  675. package/src/web/scrapers/iacr.ts +86 -0
  676. package/src/web/scrapers/index.ts +249 -0
  677. package/src/web/scrapers/jetbrains-marketplace.ts +169 -0
  678. package/src/web/scrapers/lemmy.ts +220 -0
  679. package/src/web/scrapers/lobsters.ts +186 -0
  680. package/src/web/scrapers/mastodon.ts +310 -0
  681. package/src/web/scrapers/maven.ts +152 -0
  682. package/src/web/scrapers/mdn.ts +172 -0
  683. package/src/web/scrapers/metacpan.ts +253 -0
  684. package/src/web/scrapers/musicbrainz.ts +272 -0
  685. package/src/web/scrapers/npm.ts +114 -0
  686. package/src/web/scrapers/nuget.ts +205 -0
  687. package/src/web/scrapers/nvd.ts +243 -0
  688. package/src/web/scrapers/ollama.ts +265 -0
  689. package/src/web/scrapers/open-vsx.ts +119 -0
  690. package/src/web/scrapers/opencorporates.ts +275 -0
  691. package/src/web/scrapers/openlibrary.ts +319 -0
  692. package/src/web/scrapers/orcid.ts +298 -0
  693. package/src/web/scrapers/osv.ts +192 -0
  694. package/src/web/scrapers/packagist.ts +174 -0
  695. package/src/web/scrapers/pub-dev.ts +185 -0
  696. package/src/web/scrapers/pubmed.ts +177 -0
  697. package/src/web/scrapers/pypi.ts +129 -0
  698. package/src/web/scrapers/rawg.ts +124 -0
  699. package/src/web/scrapers/readthedocs.ts +125 -0
  700. package/src/web/scrapers/reddit.ts +104 -0
  701. package/src/web/scrapers/repology.ts +262 -0
  702. package/src/web/scrapers/rfc.ts +209 -0
  703. package/src/web/scrapers/rubygems.ts +117 -0
  704. package/src/web/scrapers/searchcode.ts +217 -0
  705. package/src/web/scrapers/sec-edgar.ts +274 -0
  706. package/src/web/scrapers/semantic-scholar.ts +190 -0
  707. package/src/web/scrapers/snapcraft.ts +200 -0
  708. package/src/web/scrapers/sourcegraph.ts +373 -0
  709. package/src/web/scrapers/spdx.ts +121 -0
  710. package/src/web/scrapers/spotify.ts +217 -0
  711. package/src/web/scrapers/stackoverflow.ts +124 -0
  712. package/src/web/scrapers/terraform.ts +304 -0
  713. package/src/web/scrapers/tldr.ts +51 -0
  714. package/src/web/scrapers/twitter.ts +97 -0
  715. package/src/web/scrapers/types.ts +200 -0
  716. package/src/web/scrapers/utils.ts +142 -0
  717. package/src/web/scrapers/vimeo.ts +152 -0
  718. package/src/web/scrapers/vscode-marketplace.ts +195 -0
  719. package/src/web/scrapers/w3c.ts +163 -0
  720. package/src/web/scrapers/wikidata.ts +357 -0
  721. package/src/web/scrapers/wikipedia.ts +95 -0
  722. package/src/web/scrapers/youtube.ts +312 -0
  723. package/src/web/search/auth.ts +178 -0
  724. package/src/web/search/index.ts +598 -0
  725. package/src/web/search/provider.ts +77 -0
  726. package/src/web/search/providers/anthropic.ts +284 -0
  727. package/src/web/search/providers/base.ts +22 -0
  728. package/src/web/search/providers/brave.ts +165 -0
  729. package/src/web/search/providers/codex.ts +377 -0
  730. package/src/web/search/providers/exa.ts +158 -0
  731. package/src/web/search/providers/gemini.ts +437 -0
  732. package/src/web/search/providers/jina.ts +99 -0
  733. package/src/web/search/providers/kimi.ts +196 -0
  734. package/src/web/search/providers/perplexity.ts +546 -0
  735. package/src/web/search/providers/synthetic.ts +136 -0
  736. package/src/web/search/providers/zai.ts +352 -0
  737. package/src/web/search/render.ts +299 -0
  738. package/src/web/search/types.ts +437 -0
@@ -0,0 +1,1193 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
4
+ import type { ImageContent, TextContent } from "@nghyane/arcane-ai";
5
+ import { FileType, glob } from "@nghyane/arcane-natives";
6
+ import type { Component } from "@nghyane/arcane-tui";
7
+ import { Text } from "@nghyane/arcane-tui";
8
+ import { ptree, untilAborted } from "@nghyane/arcane-utils";
9
+ import { getRemoteDir } from "@nghyane/arcane-utils/dirs";
10
+ import { type Static, Type } from "@sinclair/typebox";
11
+ import { renderPromptTemplate } from "../config/prompt-templates";
12
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
+ import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
14
+ import { computeLineHash } from "../patch/hashline";
15
+ import readDescription from "../prompts/tools/read.md" with { type: "text" };
16
+ import type { ToolSession } from "../sdk";
17
+ import {
18
+ DEFAULT_MAX_BYTES,
19
+ DEFAULT_MAX_LINES,
20
+ formatBytes,
21
+ type TruncationResult,
22
+ truncateHead,
23
+ truncateStringToBytesFromStart,
24
+ } from "../session/streaming-output";
25
+ import { renderCodeCell, renderStatusLine } from "../tui";
26
+ import { CachedOutputBlock } from "../tui/output-block";
27
+ import { resolveFileDisplayMode } from "../utils/file-display-mode";
28
+ import { formatDimensionNote, resizeImage } from "../utils/image-resize";
29
+ import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
30
+ import { ensureTool } from "../utils/tools-manager";
31
+ import { applyListLimit } from "./list-limit";
32
+ import type { OutputMeta } from "./output-meta";
33
+ import { resolveReadPath, resolveToCwd } from "./path-utils";
34
+ import { formatAge, shortenPath, wrapBrackets } from "./render-utils";
35
+ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
36
+ import { toolResult } from "./tool-result";
37
+
38
+ // Document types convertible via markitdown
39
+ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
40
+
41
+ // Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
42
+ const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
43
+
44
+ function isRemoteMountPath(absolutePath: string): boolean {
45
+ return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
46
+ }
47
+
48
+ const READ_CHUNK_SIZE = 8 * 1024;
49
+
50
+ async function streamLinesFromFile(
51
+ filePath: string,
52
+ startLine: number,
53
+ maxLinesToCollect: number,
54
+ maxBytes: number,
55
+ selectedLineLimit: number | null,
56
+ signal?: AbortSignal,
57
+ ): Promise<{
58
+ lines: string[];
59
+ totalFileLines: number;
60
+ collectedBytes: number;
61
+ stoppedByByteLimit: boolean;
62
+ firstLinePreview?: { text: string; bytes: number };
63
+ firstLineByteLength?: number;
64
+ selectedBytesTotal: number;
65
+ }> {
66
+ const bufferChunk = Buffer.allocUnsafe(READ_CHUNK_SIZE);
67
+ const collectedLines: string[] = [];
68
+ let lineIndex = 0;
69
+ let collectedBytes = 0;
70
+ let stoppedByByteLimit = false;
71
+ let doneCollecting = false;
72
+ let fileHandle: fs.FileHandle | null = null;
73
+ let currentLineLength = 0;
74
+ let currentLineChunks: Buffer[] = [];
75
+ let sawAnyByte = false;
76
+ let endedWithNewline = false;
77
+ let firstLinePreviewBytes = 0;
78
+ const firstLinePreviewChunks: Buffer[] = [];
79
+ let firstLineByteLength: number | undefined;
80
+ let selectedBytesTotal = 0;
81
+ let selectedLinesSeen = 0;
82
+ let captureLine = false;
83
+ let discardLineChunks = false;
84
+ let lineCaptureLimit = 0;
85
+
86
+ const setupLineState = () => {
87
+ captureLine = !doneCollecting && lineIndex >= startLine;
88
+ discardLineChunks = !captureLine;
89
+ if (captureLine) {
90
+ const separatorBytes = collectedLines.length > 0 ? 1 : 0;
91
+ lineCaptureLimit = maxBytes - collectedBytes - separatorBytes;
92
+ if (lineCaptureLimit <= 0) {
93
+ discardLineChunks = true;
94
+ }
95
+ } else {
96
+ lineCaptureLimit = 0;
97
+ }
98
+ };
99
+
100
+ const decodeLine = (): string => {
101
+ if (currentLineLength === 0) return "";
102
+ if (currentLineChunks.length === 1 && currentLineChunks[0]?.length === currentLineLength) {
103
+ return currentLineChunks[0].toString("utf-8");
104
+ }
105
+ return Buffer.concat(currentLineChunks, currentLineLength).toString("utf-8");
106
+ };
107
+
108
+ const maybeCapturePreview = (segment: Uint8Array) => {
109
+ if (doneCollecting || lineIndex < startLine || collectedLines.length !== 0) return;
110
+ if (firstLinePreviewBytes >= maxBytes || segment.length === 0) return;
111
+ const remaining = maxBytes - firstLinePreviewBytes;
112
+ const slice = segment.length > remaining ? segment.subarray(0, remaining) : segment;
113
+ if (slice.length === 0) return;
114
+ firstLinePreviewChunks.push(Buffer.from(slice));
115
+ firstLinePreviewBytes += slice.length;
116
+ };
117
+
118
+ const appendSegment = (segment: Uint8Array) => {
119
+ currentLineLength += segment.length;
120
+ maybeCapturePreview(segment);
121
+ if (!captureLine || discardLineChunks || segment.length === 0) return;
122
+ if (currentLineLength <= lineCaptureLimit) {
123
+ currentLineChunks.push(Buffer.from(segment));
124
+ } else {
125
+ discardLineChunks = true;
126
+ }
127
+ };
128
+
129
+ const finalizeLine = () => {
130
+ if (lineIndex >= startLine && (selectedLineLimit === null || selectedLinesSeen < selectedLineLimit)) {
131
+ selectedBytesTotal += currentLineLength + (selectedLinesSeen > 0 ? 1 : 0);
132
+ selectedLinesSeen++;
133
+ }
134
+
135
+ if (!doneCollecting && lineIndex >= startLine) {
136
+ const separatorBytes = collectedLines.length > 0 ? 1 : 0;
137
+ if (collectedLines.length >= maxLinesToCollect) {
138
+ doneCollecting = true;
139
+ } else if (collectedLines.length === 0 && currentLineLength > maxBytes) {
140
+ stoppedByByteLimit = true;
141
+ doneCollecting = true;
142
+ if (firstLineByteLength === undefined) {
143
+ firstLineByteLength = currentLineLength;
144
+ }
145
+ } else if (collectedLines.length > 0 && collectedBytes + separatorBytes + currentLineLength > maxBytes) {
146
+ stoppedByByteLimit = true;
147
+ doneCollecting = true;
148
+ } else {
149
+ const lineText = decodeLine();
150
+ collectedLines.push(lineText);
151
+ collectedBytes += separatorBytes + currentLineLength;
152
+ if (firstLineByteLength === undefined) {
153
+ firstLineByteLength = currentLineLength;
154
+ }
155
+ if (collectedBytes > maxBytes) {
156
+ stoppedByByteLimit = true;
157
+ doneCollecting = true;
158
+ } else if (collectedLines.length >= maxLinesToCollect) {
159
+ doneCollecting = true;
160
+ }
161
+ }
162
+ } else if (lineIndex >= startLine && firstLineByteLength === undefined) {
163
+ firstLineByteLength = currentLineLength;
164
+ }
165
+
166
+ lineIndex++;
167
+ currentLineLength = 0;
168
+ currentLineChunks = [];
169
+ setupLineState();
170
+ };
171
+
172
+ setupLineState();
173
+
174
+ try {
175
+ fileHandle = await fs.open(filePath, "r");
176
+
177
+ while (true) {
178
+ throwIfAborted(signal);
179
+ const { bytesRead } = await fileHandle.read(bufferChunk, 0, bufferChunk.length, null);
180
+ if (bytesRead === 0) break;
181
+
182
+ sawAnyByte = true;
183
+ const chunk = bufferChunk.subarray(0, bytesRead);
184
+ endedWithNewline = chunk[bytesRead - 1] === 0x0a;
185
+
186
+ let start = 0;
187
+ for (let i = 0; i < chunk.length; i++) {
188
+ if (chunk[i] === 0x0a) {
189
+ const segment = chunk.subarray(start, i);
190
+ if (segment.length > 0) {
191
+ appendSegment(segment);
192
+ }
193
+ finalizeLine();
194
+ start = i + 1;
195
+ }
196
+ }
197
+
198
+ if (start < chunk.length) {
199
+ appendSegment(chunk.subarray(start));
200
+ }
201
+ }
202
+ } finally {
203
+ if (fileHandle) {
204
+ await fileHandle.close();
205
+ }
206
+ }
207
+
208
+ if (endedWithNewline || currentLineLength > 0 || !sawAnyByte) {
209
+ finalizeLine();
210
+ }
211
+
212
+ let firstLinePreview: { text: string; bytes: number } | undefined;
213
+ if (firstLinePreviewBytes > 0) {
214
+ const buf = Buffer.concat(firstLinePreviewChunks, firstLinePreviewBytes);
215
+ let end = Math.min(buf.length, maxBytes);
216
+ while (end > 0 && (buf[end] & 0xc0) === 0x80) {
217
+ end--;
218
+ }
219
+ if (end > 0) {
220
+ const text = buf.slice(0, end).toString("utf-8");
221
+ firstLinePreview = { text, bytes: Buffer.byteLength(text, "utf-8") };
222
+ } else {
223
+ firstLinePreview = { text: "", bytes: 0 };
224
+ }
225
+ }
226
+
227
+ return {
228
+ lines: collectedLines,
229
+ totalFileLines: lineIndex,
230
+ collectedBytes,
231
+ stoppedByByteLimit,
232
+ firstLinePreview,
233
+ firstLineByteLength,
234
+ selectedBytesTotal,
235
+ };
236
+ }
237
+
238
+ // Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
239
+ const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
240
+ const MAX_FUZZY_RESULTS = 5;
241
+ const MAX_FUZZY_CANDIDATES = 20000;
242
+ const MIN_BASE_SIMILARITY = 0.5;
243
+ const MIN_FULL_SIMILARITY = 0.6;
244
+ const GLOB_TIMEOUT_MS = 5000;
245
+
246
+ function normalizePathForMatch(value: string): string {
247
+ return value
248
+ .replace(/\\/g, "/")
249
+ .replace(/^\.\/+/, "")
250
+ .replace(/\/+$/, "")
251
+ .toLowerCase();
252
+ }
253
+
254
+ function isNotFoundError(error: unknown): boolean {
255
+ if (!error || typeof error !== "object") return false;
256
+ const code = (error as { code?: string }).code;
257
+ return code === "ENOENT" || code === "ENOTDIR";
258
+ }
259
+
260
+ function isPathWithin(basePath: string, targetPath: string): boolean {
261
+ const relativePath = path.relative(basePath, targetPath);
262
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
263
+ }
264
+
265
+ async function findExistingDirectory(startDir: string, signal?: AbortSignal): Promise<string | null> {
266
+ let current = startDir;
267
+ const root = path.parse(startDir).root;
268
+
269
+ while (true) {
270
+ throwIfAborted(signal);
271
+ try {
272
+ const stat = await Bun.file(current).stat();
273
+ if (stat.isDirectory()) {
274
+ return current;
275
+ }
276
+ } catch {
277
+ // Keep walking up.
278
+ }
279
+
280
+ if (current === root) {
281
+ break;
282
+ }
283
+ current = path.dirname(current);
284
+ }
285
+
286
+ return null;
287
+ }
288
+
289
+ function formatScopeLabel(searchRoot: string, cwd: string): string {
290
+ const relative = path.relative(cwd, searchRoot).replace(/\\/g, "/");
291
+ if (relative === "" || relative === ".") {
292
+ return ".";
293
+ }
294
+ if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
295
+ return relative;
296
+ }
297
+ return searchRoot;
298
+ }
299
+
300
+ function buildDisplayPath(searchRoot: string, cwd: string, relativePath: string): string {
301
+ const scopeLabel = formatScopeLabel(searchRoot, cwd);
302
+ const normalized = relativePath.replace(/\\/g, "/");
303
+ if (scopeLabel === ".") {
304
+ return normalized;
305
+ }
306
+ if (scopeLabel.startsWith("..") || path.isAbsolute(scopeLabel)) {
307
+ return path.join(searchRoot, normalized).replace(/\\/g, "/");
308
+ }
309
+ return `${scopeLabel}/${normalized}`;
310
+ }
311
+
312
+ function levenshteinDistance(a: string, b: string): number {
313
+ if (a === b) return 0;
314
+ const aLen = a.length;
315
+ const bLen = b.length;
316
+ if (aLen === 0) return bLen;
317
+ if (bLen === 0) return aLen;
318
+
319
+ let prev = new Array<number>(bLen + 1);
320
+ let curr = new Array<number>(bLen + 1);
321
+ for (let j = 0; j <= bLen; j++) {
322
+ prev[j] = j;
323
+ }
324
+
325
+ for (let i = 1; i <= aLen; i++) {
326
+ curr[0] = i;
327
+ const aCode = a.charCodeAt(i - 1);
328
+ for (let j = 1; j <= bLen; j++) {
329
+ const cost = aCode === b.charCodeAt(j - 1) ? 0 : 1;
330
+ const deletion = prev[j] + 1;
331
+ const insertion = curr[j - 1] + 1;
332
+ const substitution = prev[j - 1] + cost;
333
+ curr[j] = Math.min(deletion, insertion, substitution);
334
+ }
335
+ const tmp = prev;
336
+ prev = curr;
337
+ curr = tmp;
338
+ }
339
+
340
+ return prev[bLen];
341
+ }
342
+
343
+ function similarityScore(a: string, b: string): number {
344
+ if (a.length === 0 && b.length === 0) {
345
+ return 1;
346
+ }
347
+ const maxLen = Math.max(a.length, b.length);
348
+ if (maxLen === 0) {
349
+ return 1;
350
+ }
351
+ const distance = levenshteinDistance(a, b);
352
+ return 1 - distance / maxLen;
353
+ }
354
+
355
+ async function listCandidateFiles(
356
+ searchRoot: string,
357
+ signal?: AbortSignal,
358
+ _notify?: (message: string) => void,
359
+ ): Promise<{ files: string[]; truncated: boolean; error?: string }> {
360
+ let files: string[];
361
+ const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
362
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
363
+ try {
364
+ const result = await untilAborted(combinedSignal, () =>
365
+ glob({
366
+ pattern: "**/*",
367
+ path: searchRoot,
368
+ fileType: FileType.File,
369
+ hidden: true,
370
+ }),
371
+ );
372
+ files = result.matches.map(match => match.path);
373
+ } catch (error) {
374
+ if (error instanceof Error && error.name === "AbortError") {
375
+ if (timeoutSignal.aborted && !signal?.aborted) {
376
+ const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
377
+ return { files: [], truncated: false, error: `find timed out after ${timeoutSeconds}s` };
378
+ }
379
+ throw new ToolAbortError();
380
+ }
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ return { files: [], truncated: false, error: message };
383
+ }
384
+
385
+ const normalizedFiles = files.filter(line => line.length > 0);
386
+ const truncated = normalizedFiles.length > MAX_FUZZY_CANDIDATES;
387
+ const limited = truncated ? normalizedFiles.slice(0, MAX_FUZZY_CANDIDATES) : normalizedFiles;
388
+
389
+ return { files: limited, truncated };
390
+ }
391
+
392
+ async function findReadPathSuggestions(
393
+ rawPath: string,
394
+ cwd: string,
395
+ signal?: AbortSignal,
396
+ notify?: (message: string) => void,
397
+ ): Promise<{ suggestions: string[]; scopeLabel?: string; truncated?: boolean; error?: string } | null> {
398
+ const resolvedPath = resolveToCwd(rawPath, cwd);
399
+ const searchRoot = await findExistingDirectory(path.dirname(resolvedPath), signal);
400
+ if (!searchRoot) {
401
+ return null;
402
+ }
403
+
404
+ if (!isPathWithin(cwd, resolvedPath)) {
405
+ const root = path.parse(searchRoot).root;
406
+ if (searchRoot === root) {
407
+ return null;
408
+ }
409
+ }
410
+
411
+ const { files, truncated, error } = await listCandidateFiles(searchRoot, signal, notify);
412
+ const scopeLabel = formatScopeLabel(searchRoot, cwd);
413
+
414
+ if (error && files.length === 0) {
415
+ return { suggestions: [], scopeLabel, truncated, error };
416
+ }
417
+
418
+ if (files.length === 0) {
419
+ return null;
420
+ }
421
+
422
+ const queryPath = (() => {
423
+ if (path.isAbsolute(rawPath)) {
424
+ const relative = path.relative(cwd, resolvedPath).replace(/\\/g, "/");
425
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
426
+ return normalizePathForMatch(relative);
427
+ }
428
+ }
429
+ return normalizePathForMatch(rawPath);
430
+ })();
431
+ const baseQuery = path.posix.basename(queryPath);
432
+
433
+ const matches: Array<{ path: string; score: number; baseScore: number; fullScore: number }> = [];
434
+ const seen = new Set<string>();
435
+
436
+ for (const file of files) {
437
+ throwIfAborted(signal);
438
+ const cleaned = file.replace(/\r$/, "").trim();
439
+ if (!cleaned) continue;
440
+
441
+ const relativePath = cleaned;
442
+
443
+ if (!relativePath || relativePath.startsWith("..")) {
444
+ continue;
445
+ }
446
+
447
+ const displayPath = buildDisplayPath(searchRoot, cwd, relativePath);
448
+ if (seen.has(displayPath)) {
449
+ continue;
450
+ }
451
+ seen.add(displayPath);
452
+
453
+ const normalizedDisplay = normalizePathForMatch(displayPath);
454
+ const baseCandidate = path.posix.basename(normalizedDisplay);
455
+
456
+ const fullScore = similarityScore(queryPath, normalizedDisplay);
457
+ const baseScore = baseQuery ? similarityScore(baseQuery, baseCandidate) : 0;
458
+
459
+ if (baseQuery) {
460
+ if (baseScore < MIN_BASE_SIMILARITY && fullScore < MIN_FULL_SIMILARITY) {
461
+ continue;
462
+ }
463
+ } else if (fullScore < MIN_FULL_SIMILARITY) {
464
+ continue;
465
+ }
466
+
467
+ const score = baseQuery ? baseScore * 0.75 + fullScore * 0.25 : fullScore;
468
+ matches.push({ path: displayPath, score, baseScore, fullScore });
469
+ }
470
+
471
+ if (matches.length === 0) {
472
+ return { suggestions: [], scopeLabel, truncated };
473
+ }
474
+
475
+ matches.sort((a, b) => {
476
+ if (b.score !== a.score) return b.score - a.score;
477
+ if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
478
+ return a.path.localeCompare(b.path);
479
+ });
480
+
481
+ const listLimit = applyListLimit(matches, { limit: MAX_FUZZY_RESULTS });
482
+ const suggestions = listLimit.items.map(match => match.path);
483
+
484
+ return { suggestions, scopeLabel, truncated };
485
+ }
486
+
487
+ async function convertWithMarkitdown(
488
+ filePath: string,
489
+ signal?: AbortSignal,
490
+ ): Promise<{ content: string; ok: boolean; error?: string }> {
491
+ const cmd = await ensureTool("markitdown", { signal, silent: true });
492
+ if (!cmd) {
493
+ return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
494
+ }
495
+
496
+ const result = await ptree.exec([cmd, filePath], {
497
+ signal,
498
+ allowNonZero: true,
499
+ allowAbort: true,
500
+ stderr: "buffer",
501
+ });
502
+
503
+ if (result.exitError?.aborted) {
504
+ throw new ToolAbortError();
505
+ }
506
+
507
+ if (result.exitCode === 0 && result.stdout.length > 0) {
508
+ return { content: result.stdout, ok: true };
509
+ }
510
+
511
+ return { content: "", ok: false, error: result.stderr.trim() || "Conversion failed" };
512
+ }
513
+
514
+ const readSchema = Type.Object({
515
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
516
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
517
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
518
+ });
519
+
520
+ export type ReadToolInput = Static<typeof readSchema>;
521
+
522
+ export interface ReadToolDetails {
523
+ truncation?: TruncationResult;
524
+ isDirectory?: boolean;
525
+ resolvedPath?: string;
526
+ meta?: OutputMeta;
527
+ }
528
+
529
+ type ReadParams = ReadToolInput;
530
+
531
+ /**
532
+ * Read tool implementation.
533
+ *
534
+ * Reads files with support for images, documents (via markitdown), and text.
535
+ * Directories return a formatted listing with modification times.
536
+ */
537
+ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
538
+ readonly name = "read";
539
+ readonly label = "Read";
540
+ readonly description: string;
541
+ readonly parameters = readSchema;
542
+ readonly nonAbortable = true;
543
+
544
+ readonly #autoResizeImages: boolean;
545
+
546
+ constructor(private readonly session: ToolSession) {
547
+ const displayMode = resolveFileDisplayMode(session);
548
+ this.#autoResizeImages = session.settings.get("images.autoResize");
549
+ this.description = renderPromptTemplate(readDescription, {
550
+ DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
551
+ IS_HASHLINE_MODE: displayMode.hashLines,
552
+ IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
553
+ });
554
+ }
555
+
556
+ async execute(
557
+ _toolCallId: string,
558
+ params: ReadParams,
559
+ signal?: AbortSignal,
560
+ _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
561
+ toolContext?: AgentToolContext,
562
+ ): Promise<AgentToolResult<ReadToolDetails>> {
563
+ const { path: readPath, offset, limit } = params;
564
+
565
+ const displayMode = resolveFileDisplayMode(this.session);
566
+
567
+ // Handle internal URLs (agent://, artifact://, plan://, memory://, skill://, rule://)
568
+ const internalRouter = this.session.internalRouter;
569
+ if (internalRouter?.canHandle(readPath)) {
570
+ return this.#handleInternalUrl(readPath, offset, limit);
571
+ }
572
+
573
+ const absolutePath = resolveReadPath(readPath, this.session.cwd);
574
+
575
+ let isDirectory = false;
576
+ let fileSize = 0;
577
+ try {
578
+ const stat = await Bun.file(absolutePath).stat();
579
+ fileSize = stat.size;
580
+ isDirectory = stat.isDirectory();
581
+ } catch (error) {
582
+ if (isNotFoundError(error)) {
583
+ let message = `File not found: ${readPath}`;
584
+
585
+ // Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
586
+ if (!isRemoteMountPath(absolutePath)) {
587
+ const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal, message =>
588
+ toolContext?.ui?.notify(message, "info"),
589
+ );
590
+
591
+ if (suggestions?.suggestions.length) {
592
+ const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
593
+ message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map(match => `- ${match}`).join("\n")}`;
594
+ if (suggestions.truncated) {
595
+ message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
596
+ }
597
+ } else if (suggestions?.error) {
598
+ message += `\n\nFuzzy match failed: ${suggestions.error}`;
599
+ } else if (suggestions?.scopeLabel) {
600
+ message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
601
+ }
602
+ }
603
+
604
+ throw new ToolError(message);
605
+ }
606
+ throw error;
607
+ }
608
+
609
+ if (isDirectory) {
610
+ return this.#readDirectory(absolutePath, limit, signal);
611
+ }
612
+
613
+ const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
614
+ const ext = path.extname(absolutePath).toLowerCase();
615
+
616
+ // Read the file based on type
617
+ let content: (TextContent | ImageContent)[];
618
+ let details: ReadToolDetails = {};
619
+ let sourcePath: string | undefined;
620
+ let truncationInfo:
621
+ | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
622
+ | undefined;
623
+
624
+ if (mimeType) {
625
+ if (fileSize > MAX_IMAGE_SIZE) {
626
+ const sizeStr = formatBytes(fileSize);
627
+ const maxStr = formatBytes(MAX_IMAGE_SIZE);
628
+ throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
629
+ } else {
630
+ // Read as image (binary)
631
+ const file = Bun.file(absolutePath);
632
+ const buffer = await file.arrayBuffer();
633
+
634
+ // Check actual buffer size after reading to prevent OOM during serialization
635
+ if (buffer.byteLength > MAX_IMAGE_SIZE) {
636
+ const sizeStr = formatBytes(buffer.byteLength);
637
+ const maxStr = formatBytes(MAX_IMAGE_SIZE);
638
+ throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
639
+ } else {
640
+ const base64 = new Uint8Array(buffer).toBase64();
641
+
642
+ if (this.#autoResizeImages) {
643
+ // Resize image if needed - catch errors from Photon
644
+ try {
645
+ const resized = await resizeImage({ type: "image", data: base64, mimeType });
646
+ const dimensionNote = formatDimensionNote(resized);
647
+
648
+ let textNote = `Read image file [${resized.mimeType}]`;
649
+ if (dimensionNote) {
650
+ textNote += `\n${dimensionNote}`;
651
+ }
652
+
653
+ content = [
654
+ { type: "text", text: textNote },
655
+ { type: "image", data: resized.data, mimeType: resized.mimeType },
656
+ ];
657
+ details = {};
658
+ sourcePath = absolutePath;
659
+ } catch {
660
+ // Fall back to original image on resize failure
661
+ content = [
662
+ { type: "text", text: `Read image file [${mimeType}]` },
663
+ { type: "image", data: base64, mimeType },
664
+ ];
665
+ details = {};
666
+ sourcePath = absolutePath;
667
+ }
668
+ } else {
669
+ content = [
670
+ { type: "text", text: `Read image file [${mimeType}]` },
671
+ { type: "image", data: base64, mimeType },
672
+ ];
673
+ details = {};
674
+ sourcePath = absolutePath;
675
+ }
676
+ }
677
+ }
678
+ } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
679
+ // Convert document via markitdown
680
+ const result = await convertWithMarkitdown(absolutePath, signal);
681
+ if (result.ok) {
682
+ // Apply truncation to converted content
683
+ const truncation = truncateHead(result.content);
684
+ const outputText = truncation.content;
685
+
686
+ details = { truncation };
687
+ sourcePath = absolutePath;
688
+ truncationInfo = { result: truncation, options: { direction: "head", startLine: 1 } };
689
+
690
+ content = [{ type: "text", text: outputText }];
691
+ } else if (result.error) {
692
+ // markitdown not available or failed
693
+ const errorMsg =
694
+ result.error === "markitdown not found"
695
+ ? `markitdown not installed. Install with: pip install markitdown`
696
+ : result.error || "conversion failed";
697
+ content = [{ type: "text", text: `[Cannot read ${ext} file: ${errorMsg}]` }];
698
+ } else {
699
+ content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
700
+ }
701
+ } else {
702
+ // Read as text using streaming to avoid loading huge files into memory
703
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
704
+ const startLineDisplay = startLine + 1; // For display (1-indexed)
705
+
706
+ const maxLinesToCollect = limit !== undefined ? Math.min(limit, DEFAULT_MAX_LINES) : DEFAULT_MAX_LINES;
707
+ const selectedLineLimit = limit ?? null;
708
+ const streamResult = await streamLinesFromFile(
709
+ absolutePath,
710
+ startLine,
711
+ maxLinesToCollect,
712
+ DEFAULT_MAX_BYTES,
713
+ selectedLineLimit,
714
+ signal,
715
+ );
716
+
717
+ const {
718
+ lines: collectedLines,
719
+ totalFileLines,
720
+ collectedBytes,
721
+ stoppedByByteLimit,
722
+ firstLinePreview,
723
+ firstLineByteLength,
724
+ } = streamResult;
725
+
726
+ // Check if offset is out of bounds - return graceful message instead of throwing
727
+ if (startLine >= totalFileLines) {
728
+ const suggestion =
729
+ totalFileLines === 0
730
+ ? "The file is empty."
731
+ : `Use offset=1 to read from the start, or offset=${totalFileLines} to read the last line.`;
732
+ return toolResult<ReadToolDetails>()
733
+ .text(`Offset ${offset} is beyond end of file (${totalFileLines} lines total). ${suggestion}`)
734
+ .done();
735
+ }
736
+
737
+ const selectedContent = collectedLines.join("\n");
738
+ const userLimitedLines = limit !== undefined ? collectedLines.length : undefined;
739
+
740
+ const totalSelectedLines = totalFileLines - startLine;
741
+ const totalSelectedBytes = collectedBytes;
742
+ const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
743
+ const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > DEFAULT_MAX_BYTES;
744
+
745
+ const truncation: TruncationResult = {
746
+ content: selectedContent,
747
+ truncated: wasTruncated,
748
+ truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : null,
749
+ totalLines: totalSelectedLines,
750
+ totalBytes: totalSelectedBytes,
751
+ outputLines: collectedLines.length,
752
+ outputBytes: collectedBytes,
753
+ lastLinePartial: false,
754
+ firstLineExceedsLimit,
755
+ maxLines: DEFAULT_MAX_LINES,
756
+ maxBytes: DEFAULT_MAX_BYTES,
757
+ };
758
+
759
+ const shouldAddHashLines = displayMode.hashLines;
760
+ const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
761
+ const prependLineNumbers = (text: string, startNum: number): string => {
762
+ const textLines = text.split("\n");
763
+ const lastLineNum = startNum + textLines.length - 1;
764
+ const padWidth = String(lastLineNum).length;
765
+ return textLines
766
+ .map((line, i) => {
767
+ const lineNum = String(startNum + i).padStart(padWidth, " ");
768
+ return `${lineNum}|${line}`;
769
+ })
770
+ .join("\n");
771
+ };
772
+ const prependHashLines = (text: string, startNum: number): string => {
773
+ const textLines = text.split("\n");
774
+ return textLines
775
+ .map((line, i) => `${startNum + i}#${computeLineHash(startNum + i, line)}:${line}`)
776
+ .join("\n");
777
+ };
778
+ const formatText = (text: string, startNum: number): string => {
779
+ if (shouldAddHashLines) return prependHashLines(text, startNum);
780
+ if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
781
+ return text;
782
+ };
783
+
784
+ let outputText: string;
785
+
786
+ if (truncation.firstLineExceedsLimit) {
787
+ const firstLineBytes = firstLineByteLength ?? 0;
788
+ const snippet = firstLinePreview ?? { text: "", bytes: 0 };
789
+
790
+ if (shouldAddHashLines) {
791
+ outputText = `[Line ${startLineDisplay} is ${formatBytes(
792
+ firstLineBytes,
793
+ )}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
794
+ } else {
795
+ outputText = formatText(snippet.text, startLineDisplay);
796
+ }
797
+ if (snippet.text.length === 0) {
798
+ outputText = `[Line ${startLineDisplay} is ${formatBytes(
799
+ firstLineBytes,
800
+ )}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
801
+ }
802
+ details = { truncation };
803
+ sourcePath = absolutePath;
804
+ truncationInfo = {
805
+ result: truncation,
806
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines },
807
+ };
808
+ } else if (truncation.truncated) {
809
+ outputText = formatText(truncation.content, startLineDisplay);
810
+ details = { truncation };
811
+ sourcePath = absolutePath;
812
+ truncationInfo = {
813
+ result: truncation,
814
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines },
815
+ };
816
+ } else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) {
817
+ const remaining = totalFileLines - (startLine + userLimitedLines);
818
+ const nextOffset = startLine + userLimitedLines + 1;
819
+
820
+ outputText = formatText(truncation.content, startLineDisplay);
821
+ outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
822
+ details = {};
823
+ sourcePath = absolutePath;
824
+ } else {
825
+ // No truncation, no user limit exceeded
826
+ outputText = formatText(truncation.content, startLineDisplay);
827
+ details = {};
828
+ sourcePath = absolutePath;
829
+ }
830
+
831
+ content = [{ type: "text", text: outputText }];
832
+ }
833
+
834
+ const resultBuilder = toolResult(details).content(content);
835
+ if (sourcePath) {
836
+ resultBuilder.sourcePath(sourcePath);
837
+ }
838
+ if (truncationInfo) {
839
+ resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
840
+ }
841
+ return resultBuilder.done();
842
+ }
843
+
844
+ /**
845
+ * Handle internal URLs (agent://, artifact://, plan://, memory://, skill://, rule://).
846
+ * Supports pagination via offset/limit but rejects them when query extraction is used.
847
+ */
848
+ async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise<AgentToolResult<ReadToolDetails>> {
849
+ const internalRouter = this.session.internalRouter!;
850
+
851
+ const displayMode = resolveFileDisplayMode(this.session);
852
+
853
+ // Check if URL has query extraction (agent:// only)
854
+ let parsed: URL;
855
+ try {
856
+ parsed = new URL(url);
857
+ } catch {
858
+ throw new ToolError(`Invalid URL: ${url}`);
859
+ }
860
+ const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
861
+ const hasPathExtraction = parsed.pathname && parsed.pathname !== "/" && parsed.pathname !== "";
862
+ const queryParam = parsed.searchParams.get("q");
863
+ const hasQueryExtraction = queryParam !== null && queryParam !== "";
864
+ const hasExtraction = scheme === "agent" && (hasPathExtraction || hasQueryExtraction);
865
+
866
+ if (scheme !== "agent" && hasQueryExtraction) {
867
+ throw new ToolError("Only agent:// URLs support ?q= query extraction");
868
+ }
869
+
870
+ // Reject offset/limit with query extraction
871
+ if (hasExtraction && (offset !== undefined || limit !== undefined)) {
872
+ throw new ToolError("Cannot combine query extraction with offset/limit");
873
+ }
874
+
875
+ // Resolve the internal URL
876
+ const resource = await internalRouter.resolve(url);
877
+
878
+ // If extraction was used, return directly (no pagination)
879
+ if (hasExtraction) {
880
+ const details: ReadToolDetails = {};
881
+ if (resource.sourcePath) {
882
+ details.resolvedPath = resource.sourcePath;
883
+ }
884
+ return toolResult(details).text(resource.content).sourceInternal(url).done();
885
+ }
886
+
887
+ // Apply pagination similar to file reading
888
+ const allLines = resource.content.split("\n");
889
+ const totalLines = allLines.length;
890
+
891
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
892
+ const startLineDisplay = startLine + 1;
893
+
894
+ if (startLine >= allLines.length) {
895
+ const suggestion =
896
+ allLines.length === 0
897
+ ? "The resource is empty."
898
+ : `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`;
899
+ return toolResult<ReadToolDetails>()
900
+ .text(`Offset ${offset} is beyond end of resource (${allLines.length} lines total). ${suggestion}`)
901
+ .done();
902
+ }
903
+
904
+ let selectedContent: string;
905
+ let userLimitedLines: number | undefined;
906
+ if (limit !== undefined) {
907
+ const endLine = Math.min(startLine + limit, allLines.length);
908
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
909
+ userLimitedLines = endLine - startLine;
910
+ } else {
911
+ selectedContent = allLines.slice(startLine).join("\n");
912
+ }
913
+
914
+ // Apply truncation
915
+ const truncation = truncateHead(selectedContent);
916
+
917
+ const shouldAddHashLines = displayMode.hashLines;
918
+ const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
919
+ const prependLineNumbers = (text: string, startNum: number): string => {
920
+ const textLines = text.split("\n");
921
+ const lastLineNum = startNum + textLines.length - 1;
922
+ const padWidth = String(lastLineNum).length;
923
+ return textLines
924
+ .map((line, i) => {
925
+ const lineNum = String(startNum + i).padStart(padWidth, " ");
926
+ return `${lineNum}|${line}`;
927
+ })
928
+ .join("\n");
929
+ };
930
+ const prependHashLines = (text: string, startNum: number): string => {
931
+ const textLines = text.split("\n");
932
+ return textLines.map((line, i) => `${startNum + i}#${computeLineHash(startNum + i, line)}:${line}`).join("\n");
933
+ };
934
+ const formatText = (text: string, startNum: number): string => {
935
+ if (shouldAddHashLines) return prependHashLines(text, startNum);
936
+ if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
937
+ return text;
938
+ };
939
+
940
+ let outputText: string;
941
+ let details: ReadToolDetails = {};
942
+ let truncationInfo:
943
+ | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
944
+ | undefined;
945
+
946
+ if (truncation.firstLineExceedsLimit) {
947
+ const firstLine = allLines[startLine] ?? "";
948
+ const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
949
+ const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
950
+
951
+ if (shouldAddHashLines) {
952
+ outputText = `[Line ${startLineDisplay} is ${formatBytes(
953
+ firstLineBytes,
954
+ )}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
955
+ } else {
956
+ outputText = formatText(snippet.text, startLineDisplay);
957
+ }
958
+ if (snippet.text.length === 0) {
959
+ outputText = `[Line ${startLineDisplay} is ${formatBytes(
960
+ firstLineBytes,
961
+ )}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
962
+ }
963
+ details = { truncation };
964
+ truncationInfo = {
965
+ result: truncation,
966
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
967
+ };
968
+ } else if (truncation.truncated) {
969
+ outputText = formatText(truncation.content, startLineDisplay);
970
+ details = { truncation };
971
+ truncationInfo = {
972
+ result: truncation,
973
+ options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
974
+ };
975
+ } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
976
+ const remaining = allLines.length - (startLine + userLimitedLines);
977
+ const nextOffset = startLine + userLimitedLines + 1;
978
+
979
+ outputText = formatText(truncation.content, startLineDisplay);
980
+ outputText += `\n\n[${remaining} more lines in resource. Use offset=${nextOffset} to continue]`;
981
+ details = {};
982
+ } else {
983
+ outputText = formatText(truncation.content, startLineDisplay);
984
+ details = {};
985
+ }
986
+
987
+ if (resource.sourcePath) {
988
+ details.resolvedPath = resource.sourcePath;
989
+ }
990
+
991
+ const resultBuilder = toolResult(details).text(outputText).sourceInternal(url);
992
+ if (truncationInfo) {
993
+ resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
994
+ }
995
+ return resultBuilder.done();
996
+ }
997
+
998
+ /** Read directory contents as a formatted listing */
999
+ async #readDirectory(
1000
+ absolutePath: string,
1001
+ limit: number | undefined,
1002
+ signal?: AbortSignal,
1003
+ ): Promise<AgentToolResult<ReadToolDetails>> {
1004
+ const DEFAULT_LIMIT = 500;
1005
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
1006
+
1007
+ let entries: string[];
1008
+ try {
1009
+ entries = await fs.readdir(absolutePath);
1010
+ } catch (error) {
1011
+ const message = error instanceof Error ? error.message : String(error);
1012
+ throw new ToolError(`Cannot read directory: ${message}`);
1013
+ }
1014
+
1015
+ // Sort alphabetically (case-insensitive)
1016
+ entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1017
+
1018
+ const listLimit = applyListLimit(entries, { limit: effectiveLimit });
1019
+ const limitedEntries = listLimit.items;
1020
+ const limitMeta = listLimit.meta;
1021
+
1022
+ // Format entries with directory indicators and ages
1023
+ const results: string[] = [];
1024
+
1025
+ for (const entry of limitedEntries) {
1026
+ throwIfAborted(signal);
1027
+ const fullPath = path.join(absolutePath, entry);
1028
+ let suffix = "";
1029
+ let age = "";
1030
+
1031
+ try {
1032
+ const entryStat = await fs.stat(fullPath);
1033
+ suffix = entryStat.isDirectory() ? "/" : "";
1034
+ const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
1035
+ age = formatAge(ageSeconds);
1036
+ } catch {
1037
+ // Skip entries we can't stat
1038
+ continue;
1039
+ }
1040
+
1041
+ const line = age ? `${entry}${suffix} (${age})` : entry + suffix;
1042
+ results.push(line);
1043
+ }
1044
+
1045
+ if (results.length === 0) {
1046
+ return { content: [{ type: "text", text: "(empty directory)" }], details: {} };
1047
+ }
1048
+
1049
+ const output = results.join("\n");
1050
+ const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
1051
+
1052
+ const details: ReadToolDetails = {
1053
+ isDirectory: true,
1054
+ };
1055
+
1056
+ const resultBuilder = toolResult(details)
1057
+ .text(truncation.content)
1058
+ .limits({ resultLimit: limitMeta.resultLimit?.reached });
1059
+ if (truncation.truncated) {
1060
+ resultBuilder.truncation(truncation, { direction: "head" });
1061
+ details.truncation = truncation;
1062
+ }
1063
+
1064
+ return resultBuilder.done();
1065
+ }
1066
+ }
1067
+
1068
+ // =============================================================================
1069
+ // TUI Renderer
1070
+ // =============================================================================
1071
+
1072
+ interface ReadRenderArgs {
1073
+ path?: string;
1074
+ file_path?: string;
1075
+ offset?: number;
1076
+ limit?: number;
1077
+ }
1078
+
1079
+ export const readToolRenderer = {
1080
+ renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
1081
+ const rawPath = args.file_path || args.path || "";
1082
+ const filePath = shortenPath(rawPath);
1083
+ const offset = args.offset;
1084
+ const limit = args.limit;
1085
+
1086
+ let pathDisplay = filePath || "…";
1087
+ if (offset !== undefined || limit !== undefined) {
1088
+ const startLine = offset ?? 1;
1089
+ const endLine = limit !== undefined ? startLine + limit - 1 : "";
1090
+ pathDisplay += `:${startLine}${endLine ? `-${endLine}` : ""}`;
1091
+ }
1092
+
1093
+ const text = renderStatusLine({ icon: "pending", title: "Read", description: pathDisplay }, uiTheme);
1094
+ return new Text(text, 0, 0);
1095
+ },
1096
+
1097
+ renderResult(
1098
+ result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
1099
+ _options: RenderResultOptions,
1100
+ uiTheme: Theme,
1101
+ args?: ReadRenderArgs,
1102
+ ): Component {
1103
+ const details = result.details;
1104
+ const contentText = result.content?.find(c => c.type === "text")?.text ?? "";
1105
+ const imageContent = result.content?.find(c => c.type === "image");
1106
+ const rawPath = args?.file_path || args?.path || "";
1107
+ const filePath = shortenPath(rawPath);
1108
+ const lang = getLanguageFromPath(rawPath);
1109
+
1110
+ const warningLines: string[] = [];
1111
+ const truncation = details?.meta?.truncation;
1112
+ const fallback = details?.truncation;
1113
+ if (details?.resolvedPath) {
1114
+ warningLines.push(uiTheme.fg("dim", wrapBrackets(`Resolved path: ${details.resolvedPath}`, uiTheme)));
1115
+ }
1116
+ if (truncation) {
1117
+ let warning: string;
1118
+ if (fallback?.firstLineExceedsLimit) {
1119
+ warning = `First line exceeds ${formatBytes(fallback.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
1120
+ } else if (truncation.truncatedBy === "lines") {
1121
+ warning = `Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${DEFAULT_MAX_LINES} line limit)`;
1122
+ } else {
1123
+ const maxBytes = fallback?.maxBytes ?? DEFAULT_MAX_BYTES;
1124
+ warning = `Truncated: ${truncation.outputLines} lines (${formatBytes(maxBytes)} limit)`;
1125
+ }
1126
+ if (truncation.artifactId) {
1127
+ warning += `. Full output: artifact://${truncation.artifactId}`;
1128
+ }
1129
+ warningLines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
1130
+ }
1131
+
1132
+ if (imageContent) {
1133
+ const header = renderStatusLine(
1134
+ { icon: "success", title: "Read", description: filePath || rawPath || "image" },
1135
+ uiTheme,
1136
+ );
1137
+ const detailLines = contentText ? contentText.split("\n").map(line => uiTheme.fg("toolOutput", line)) : [];
1138
+ const lines = [...detailLines, ...warningLines];
1139
+ const outputBlock = new CachedOutputBlock();
1140
+ return {
1141
+ render: (width: number) =>
1142
+ outputBlock.render(
1143
+ {
1144
+ header,
1145
+ state: "success",
1146
+ sections: [
1147
+ {
1148
+ label: uiTheme.fg("toolTitle", "Details"),
1149
+ lines: lines.length > 0 ? lines : [uiTheme.fg("dim", "(image)")],
1150
+ },
1151
+ ],
1152
+ width,
1153
+ },
1154
+ uiTheme,
1155
+ ),
1156
+ invalidate: () => outputBlock.invalidate(),
1157
+ };
1158
+ }
1159
+
1160
+ let title = filePath ? `Read ${filePath}` : "Read";
1161
+ if (args?.offset !== undefined || args?.limit !== undefined) {
1162
+ const startLine = args.offset ?? 1;
1163
+ const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
1164
+ title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
1165
+ }
1166
+ let cachedWidth: number | undefined;
1167
+ let cachedLines: string[] | undefined;
1168
+ return {
1169
+ render: (width: number) => {
1170
+ if (cachedLines && cachedWidth === width) return cachedLines;
1171
+ cachedLines = renderCodeCell(
1172
+ {
1173
+ code: contentText,
1174
+ language: lang,
1175
+ title,
1176
+ status: "complete",
1177
+ output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
1178
+ expanded: true,
1179
+ width,
1180
+ },
1181
+ uiTheme,
1182
+ );
1183
+ cachedWidth = width;
1184
+ return cachedLines;
1185
+ },
1186
+ invalidate: () => {
1187
+ cachedWidth = undefined;
1188
+ cachedLines = undefined;
1189
+ },
1190
+ };
1191
+ },
1192
+ mergeCallAndResult: true,
1193
+ };