@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,1091 @@
1
+ import * as path from "node:path";
2
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
3
+ import { htmlToMarkdown } from "@nghyane/arcane-natives";
4
+ import type { Component } from "@nghyane/arcane-tui";
5
+ import { Text } from "@nghyane/arcane-tui";
6
+ import { ptree } from "@nghyane/arcane-utils";
7
+ import { type Static, Type } from "@sinclair/typebox";
8
+ import { parse as parseHtml } from "node-html-parser";
9
+ import { renderPromptTemplate } from "../config/prompt-templates";
10
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
+ import { type Theme, theme } from "../modes/theme/theme";
12
+ import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
13
+ import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
14
+ import { renderStatusLine } from "../tui";
15
+ import { CachedOutputBlock } from "../tui/output-block";
16
+ import { ensureTool } from "../utils/tools-manager";
17
+ import { specialHandlers } from "../web/scrapers";
18
+ import type { RenderResult } from "../web/scrapers/types";
19
+ import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
20
+ import { convertWithMarkitdown, fetchBinary } from "../web/scrapers/utils";
21
+ import type { ToolSession } from ".";
22
+ import { applyListLimit } from "./list-limit";
23
+ import type { OutputMeta } from "./output-meta";
24
+ import { allocateOutputArtifact } from "./output-utils";
25
+ import { formatExpandHint } from "./render-utils";
26
+ import { ToolAbortError } from "./tool-errors";
27
+ import { toolResult } from "./tool-result";
28
+
29
+ // =============================================================================
30
+ // Types and Constants
31
+ // =============================================================================
32
+
33
+ const FETCH_DEFAULT_MAX_LINES = 300;
34
+ // Convertible document types (markitdown supported)
35
+ const CONVERTIBLE_MIMES = new Set([
36
+ "application/pdf",
37
+ "application/msword",
38
+ "application/vnd.ms-powerpoint",
39
+ "application/vnd.ms-excel",
40
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
41
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
42
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
43
+ "application/rtf",
44
+ "application/epub+zip",
45
+ "application/zip",
46
+ "image/png",
47
+ "image/jpeg",
48
+ "image/gif",
49
+ "image/webp",
50
+ "audio/mpeg",
51
+ "audio/wav",
52
+ "audio/ogg",
53
+ ]);
54
+
55
+ const CONVERTIBLE_EXTENSIONS = new Set([
56
+ ".pdf",
57
+ ".doc",
58
+ ".docx",
59
+ ".ppt",
60
+ ".pptx",
61
+ ".xls",
62
+ ".xlsx",
63
+ ".rtf",
64
+ ".epub",
65
+ ".png",
66
+ ".jpg",
67
+ ".jpeg",
68
+ ".gif",
69
+ ".webp",
70
+ ".mp3",
71
+ ".wav",
72
+ ".ogg",
73
+ ]);
74
+
75
+ // =============================================================================
76
+ // Utilities
77
+ // =============================================================================
78
+
79
+ /**
80
+ * Check if a command exists (cross-platform)
81
+ */
82
+ function hasCommand(cmd: string): boolean {
83
+ return Boolean(Bun.which(cmd));
84
+ }
85
+
86
+ /**
87
+ * Extract origin from URL
88
+ */
89
+ function getOrigin(url: string): string {
90
+ try {
91
+ const parsed = new URL(url);
92
+ return `${parsed.protocol}//${parsed.host}`;
93
+ } catch {
94
+ return "";
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Normalize URL (add scheme if missing)
100
+ */
101
+ function normalizeUrl(url: string): string {
102
+ if (!url.match(/^https?:\/\//i)) {
103
+ return `https://${url}`;
104
+ }
105
+ return url;
106
+ }
107
+
108
+ /**
109
+ * Normalize MIME type (lowercase, strip charset/params)
110
+ */
111
+ function normalizeMime(contentType: string): string {
112
+ return contentType.split(";")[0].trim().toLowerCase();
113
+ }
114
+
115
+ /**
116
+ * Get extension from URL or Content-Disposition
117
+ */
118
+ function getExtensionHint(url: string, contentDisposition?: string): string {
119
+ // Try Content-Disposition filename first
120
+ if (contentDisposition) {
121
+ const match = contentDisposition.match(/filename[*]?=["']?([^"';\n]+)/i);
122
+ if (match) {
123
+ const ext = path.extname(match[1]).toLowerCase();
124
+ if (ext) return ext;
125
+ }
126
+ }
127
+
128
+ // Fall back to URL path
129
+ try {
130
+ const pathname = new URL(url).pathname;
131
+ const ext = path.extname(pathname).toLowerCase();
132
+ if (ext) return ext;
133
+ } catch {}
134
+
135
+ return "";
136
+ }
137
+
138
+ /**
139
+ * Check if content type is convertible via markitdown
140
+ */
141
+ function isConvertible(mime: string, extensionHint: string): boolean {
142
+ if (CONVERTIBLE_MIMES.has(mime)) return true;
143
+ if (mime === "application/octet-stream" && CONVERTIBLE_EXTENSIONS.has(extensionHint)) return true;
144
+ if (CONVERTIBLE_EXTENSIONS.has(extensionHint)) return true;
145
+ return false;
146
+ }
147
+
148
+ /**
149
+ * Check if content looks like HTML
150
+ */
151
+ function looksLikeHtml(content: string): boolean {
152
+ const trimmed = content.trim().toLowerCase();
153
+ return (
154
+ trimmed.startsWith("<!doctype") ||
155
+ trimmed.startsWith("<html") ||
156
+ trimmed.startsWith("<head") ||
157
+ trimmed.startsWith("<body")
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Try fetching URL with .md appended (llms.txt convention)
163
+ */
164
+ async function tryMdSuffix(url: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
165
+ const candidates: string[] = [];
166
+
167
+ try {
168
+ const parsed = new URL(url);
169
+ const pathname = parsed.pathname;
170
+
171
+ if (pathname.endsWith("/")) {
172
+ // /foo/bar/ -> /foo/bar/index.html.md
173
+ candidates.push(`${parsed.origin}${pathname}index.html.md`);
174
+ } else if (pathname.includes(".")) {
175
+ // /foo/bar.html -> /foo/bar.html.md
176
+ candidates.push(`${parsed.origin}${pathname}.md`);
177
+ } else {
178
+ // /foo/bar -> /foo/bar.md
179
+ candidates.push(`${parsed.origin}${pathname}.md`);
180
+ }
181
+ } catch {
182
+ return null;
183
+ }
184
+
185
+ if (signal?.aborted) {
186
+ return null;
187
+ }
188
+
189
+ for (const candidate of candidates) {
190
+ if (signal?.aborted) {
191
+ return null;
192
+ }
193
+ const result = await loadPage(candidate, { timeout, signal });
194
+ if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
195
+ return result.content;
196
+ }
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Try to fetch LLM-friendly endpoints
204
+ */
205
+ async function tryLlmEndpoints(origin: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
206
+ const endpoints = [`${origin}/.well-known/llms.txt`, `${origin}/llms.txt`, `${origin}/llms.md`];
207
+
208
+ if (signal?.aborted) {
209
+ return null;
210
+ }
211
+
212
+ for (const endpoint of endpoints) {
213
+ if (signal?.aborted) {
214
+ return null;
215
+ }
216
+ const result = await loadPage(endpoint, { timeout: Math.min(timeout, 5), signal });
217
+ if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
218
+ return result.content;
219
+ }
220
+ }
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Try content negotiation for markdown/plain
226
+ */
227
+ async function tryContentNegotiation(
228
+ url: string,
229
+ timeout: number,
230
+ signal?: AbortSignal,
231
+ ): Promise<{ content: string; type: string } | null> {
232
+ if (signal?.aborted) {
233
+ return null;
234
+ }
235
+
236
+ const result = await loadPage(url, {
237
+ timeout,
238
+ headers: { Accept: "text/markdown, text/plain;q=0.9, text/html;q=0.8" },
239
+ signal,
240
+ });
241
+
242
+ if (!result.ok) return null;
243
+
244
+ const mime = normalizeMime(result.contentType);
245
+ if ((mime.includes("markdown") || mime === "text/plain") && !looksLikeHtml(result.content)) {
246
+ return { content: result.content, type: result.contentType };
247
+ }
248
+
249
+ return null;
250
+ }
251
+
252
+ /**
253
+ * Parse alternate links from HTML head
254
+ */
255
+ function parseAlternateLinks(html: string, pageUrl: string): string[] {
256
+ const links: string[] = [];
257
+
258
+ try {
259
+ const doc = parseHtml(html.slice(0, 262144));
260
+ const alternateLinks = doc.querySelectorAll('link[rel="alternate"]');
261
+
262
+ for (const link of alternateLinks) {
263
+ const href = link.getAttribute("href");
264
+ const type = link.getAttribute("type")?.toLowerCase() ?? "";
265
+
266
+ if (!href) continue;
267
+
268
+ // Skip site-wide feeds
269
+ if (
270
+ href.includes("RecentChanges") ||
271
+ href.includes("Special:") ||
272
+ href.includes("/feed/") ||
273
+ href.includes("action=feed")
274
+ ) {
275
+ continue;
276
+ }
277
+
278
+ if (type.includes("markdown")) {
279
+ links.push(href);
280
+ } else if (
281
+ (type.includes("rss") || type.includes("atom") || type.includes("feed")) &&
282
+ (href.includes(new URL(pageUrl).pathname) || href.includes("comments"))
283
+ ) {
284
+ links.push(href);
285
+ }
286
+ }
287
+ } catch {}
288
+
289
+ return links;
290
+ }
291
+
292
+ /**
293
+ * Extract document links from HTML (for PDF/DOCX wrapper pages)
294
+ */
295
+ function extractDocumentLinks(html: string, baseUrl: string): string[] {
296
+ const links: string[] = [];
297
+
298
+ try {
299
+ const doc = parseHtml(html);
300
+ const anchors = doc.querySelectorAll("a[href]");
301
+
302
+ for (const anchor of anchors) {
303
+ const href = anchor.getAttribute("href");
304
+ if (!href) continue;
305
+
306
+ const ext = path.extname(href).toLowerCase();
307
+ if (CONVERTIBLE_EXTENSIONS.has(ext)) {
308
+ const resolved = href.startsWith("http") ? href : new URL(href, baseUrl).href;
309
+ links.push(resolved);
310
+ }
311
+ }
312
+ } catch {}
313
+
314
+ return links;
315
+ }
316
+
317
+ /**
318
+ * Strip CDATA wrapper and clean text
319
+ */
320
+ function cleanFeedText(text: string): string {
321
+ return text
322
+ .replace(/<!\[CDATA\[/g, "")
323
+ .replace(/\]\]>/g, "")
324
+ .replace(/&lt;/g, "<")
325
+ .replace(/&gt;/g, ">")
326
+ .replace(/&amp;/g, "&")
327
+ .replace(/&quot;/g, '"')
328
+ .replace(/<[^>]+>/g, "") // Strip HTML tags
329
+ .trim();
330
+ }
331
+
332
+ /**
333
+ * Parse RSS/Atom feed to markdown
334
+ */
335
+ function parseFeedToMarkdown(content: string, maxItems = 10): string {
336
+ try {
337
+ const doc = parseHtml(content, { parseNoneClosedTags: true });
338
+
339
+ // Try RSS
340
+ const channel = doc.querySelector("channel");
341
+ if (channel) {
342
+ const title = cleanFeedText(channel.querySelector("title")?.text || "RSS Feed");
343
+ const items = channel.querySelectorAll("item").slice(0, maxItems);
344
+
345
+ let md = `# ${title}\n\n`;
346
+ for (const item of items) {
347
+ const itemTitle = cleanFeedText(item.querySelector("title")?.text || "Untitled");
348
+ const link = cleanFeedText(item.querySelector("link")?.text || "");
349
+ const pubDate = cleanFeedText(item.querySelector("pubDate")?.text || "");
350
+ const desc = cleanFeedText(item.querySelector("description")?.text || "");
351
+
352
+ md += `## ${itemTitle}\n`;
353
+ if (pubDate) md += `*${pubDate}*\n\n`;
354
+ if (desc) md += `${desc.slice(0, 500)}${desc.length > 500 ? "..." : ""}\n\n`;
355
+ if (link) md += `[Read more](${link})\n\n`;
356
+ md += "---\n\n";
357
+ }
358
+ return md;
359
+ }
360
+
361
+ // Try Atom
362
+ const feed = doc.querySelector("feed");
363
+ if (feed) {
364
+ const title = cleanFeedText(feed.querySelector("title")?.text || "Atom Feed");
365
+ const entries = feed.querySelectorAll("entry").slice(0, maxItems);
366
+
367
+ let md = `# ${title}\n\n`;
368
+ for (const entry of entries) {
369
+ const entryTitle = cleanFeedText(entry.querySelector("title")?.text || "Untitled");
370
+ const link = entry.querySelector("link")?.getAttribute("href") || "";
371
+ const updated = cleanFeedText(entry.querySelector("updated")?.text || "");
372
+ const summary = cleanFeedText(
373
+ entry.querySelector("summary")?.text || entry.querySelector("content")?.text || "",
374
+ );
375
+
376
+ md += `## ${entryTitle}\n`;
377
+ if (updated) md += `*${updated}*\n\n`;
378
+ if (summary) md += `${summary.slice(0, 500)}${summary.length > 500 ? "..." : ""}\n\n`;
379
+ if (link) md += `[Read more](${link})\n\n`;
380
+ md += "---\n\n";
381
+ }
382
+ return md;
383
+ }
384
+ } catch {}
385
+
386
+ return content; // Fall back to raw content
387
+ }
388
+
389
+ /**
390
+ * Render HTML to markdown using native, jina, trafilatura, lynx (in order of preference)
391
+ */
392
+ async function renderHtmlToText(
393
+ url: string,
394
+ html: string,
395
+ timeout: number,
396
+ userSignal?: AbortSignal,
397
+ ): Promise<{ content: string; ok: boolean; method: string }> {
398
+ const signal = ptree.combineSignals(userSignal, timeout * 1000);
399
+ const execOptions = {
400
+ mode: "group" as const,
401
+ allowNonZero: true,
402
+ allowAbort: true,
403
+ stderr: "full" as const,
404
+ signal,
405
+ };
406
+
407
+ // Try jina first (reader API)
408
+ try {
409
+ const jinaUrl = `https://r.jina.ai/${url}`;
410
+ const response = await fetch(jinaUrl, {
411
+ headers: { Accept: "text/markdown" },
412
+ signal,
413
+ });
414
+ if (response.ok) {
415
+ const content = await response.text();
416
+ if (content.trim().length > 100 && !isLowQualityOutput(content)) {
417
+ return { content, ok: true, method: "jina" };
418
+ }
419
+ }
420
+ } catch {
421
+ // Jina failed, continue to next method
422
+ signal?.throwIfAborted();
423
+ }
424
+
425
+ // Try trafilatura (auto-install via uv/pip)
426
+ const trafilatura = await ensureTool("trafilatura", { signal, silent: true });
427
+ if (trafilatura) {
428
+ const result = await ptree.exec([trafilatura, "-u", url, "--output-format", "markdown"], execOptions);
429
+ if (result.ok && result.stdout.trim().length > 100) {
430
+ return { content: result.stdout, ok: true, method: "trafilatura" };
431
+ }
432
+ }
433
+
434
+ // Try lynx (can't auto-install, system package)
435
+ const lynx = hasCommand("lynx");
436
+ if (lynx) {
437
+ const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
438
+ if (result.ok) {
439
+ return { content: result.stdout, ok: true, method: "lynx" };
440
+ }
441
+ }
442
+
443
+ // Fall back to native converter (fastest, no network/subprocess)
444
+ try {
445
+ const content = await htmlToMarkdown(html, { cleanContent: true });
446
+ if (content.trim().length > 100 && !isLowQualityOutput(content)) {
447
+ return { content, ok: true, method: "native" };
448
+ }
449
+ } catch {
450
+ // Native converter failed, continue to next method
451
+ signal?.throwIfAborted();
452
+ }
453
+ return { content: "", ok: false, method: "none" };
454
+ }
455
+
456
+ /**
457
+ * Check if lynx output looks JS-gated or mostly navigation
458
+ */
459
+ function isLowQualityOutput(content: string): boolean {
460
+ const lower = content.toLowerCase();
461
+
462
+ // JS-gated indicators
463
+ const jsGated = [
464
+ "enable javascript",
465
+ "javascript required",
466
+ "turn on javascript",
467
+ "please enable javascript",
468
+ "browser not supported",
469
+ ];
470
+ if (content.length < 1024 && jsGated.some(t => lower.includes(t))) {
471
+ return true;
472
+ }
473
+
474
+ // Mostly navigation (high link/menu density)
475
+ const lines = content.split("\n").filter(l => l.trim());
476
+ const shortLines = lines.filter(l => l.trim().length < 40);
477
+ if (lines.length > 10 && shortLines.length / lines.length > 0.7) {
478
+ return true;
479
+ }
480
+
481
+ return false;
482
+ }
483
+
484
+ /**
485
+ * Format JSON
486
+ */
487
+ function formatJson(content: string): string {
488
+ try {
489
+ return JSON.stringify(JSON.parse(content), null, 2);
490
+ } catch {
491
+ return content;
492
+ }
493
+ }
494
+
495
+ // =============================================================================
496
+ // Unified Special Handler Dispatch
497
+ // =============================================================================
498
+
499
+ /**
500
+ * Try all special handlers
501
+ */
502
+ async function handleSpecialUrls(url: string, timeout: number, signal?: AbortSignal): Promise<RenderResult | null> {
503
+ for (const handler of specialHandlers) {
504
+ if (signal?.aborted) {
505
+ throw new ToolAbortError();
506
+ }
507
+ const result = await handler(url, timeout, signal);
508
+ if (result) return result;
509
+ }
510
+ return null;
511
+ }
512
+
513
+ // =============================================================================
514
+ // Main Render Function
515
+ // =============================================================================
516
+
517
+ /**
518
+ * Main render function implementing the full pipeline
519
+ */
520
+ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: AbortSignal): Promise<RenderResult> {
521
+ const notes: string[] = [];
522
+ const fetchedAt = new Date().toISOString();
523
+ if (signal?.aborted) {
524
+ throw new ToolAbortError();
525
+ }
526
+
527
+ // Handle internal protocol URLs (e.g., pi-internal://) - return empty
528
+ if (url.startsWith("pi-internal://")) {
529
+ return {
530
+ url,
531
+ finalUrl: url,
532
+ contentType: "text/plain",
533
+ method: "internal",
534
+ content: "",
535
+ fetchedAt,
536
+ truncated: false,
537
+ notes: ["Internal protocol URL - no external content"],
538
+ };
539
+ }
540
+
541
+ // Step 0: Normalize URL (ensure scheme for special handlers)
542
+ url = normalizeUrl(url);
543
+ const origin = getOrigin(url);
544
+
545
+ // Step 1: Try special handlers for known sites (unless raw mode)
546
+ if (!raw) {
547
+ const specialResult = await handleSpecialUrls(url, timeout, signal);
548
+ if (specialResult) return specialResult;
549
+ }
550
+
551
+ // Step 2: Fetch page
552
+ const response = await loadPage(url, { timeout, signal });
553
+ if (signal?.aborted) {
554
+ throw new ToolAbortError();
555
+ }
556
+ if (!response.ok) {
557
+ return {
558
+ url,
559
+ finalUrl: response.finalUrl || url,
560
+ contentType: response.contentType || "unknown",
561
+ method: "failed",
562
+ content: "",
563
+ fetchedAt,
564
+ truncated: false,
565
+ notes: [response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL"],
566
+ };
567
+ }
568
+
569
+ const { finalUrl, content: rawContent } = response;
570
+ const mime = normalizeMime(response.contentType);
571
+ const extHint = getExtensionHint(finalUrl);
572
+
573
+ // Step 3: Handle convertible binary files (PDF, DOCX, etc.)
574
+ if (isConvertible(mime, extHint)) {
575
+ const binary = await fetchBinary(finalUrl, timeout, signal);
576
+ if (binary.ok) {
577
+ const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
578
+ const converted = await convertWithMarkitdown(binary.buffer, ext, timeout, signal);
579
+ if (converted.ok) {
580
+ if (converted.content.trim().length > 50) {
581
+ notes.push("Converted with markitdown");
582
+ const output = finalizeOutput(converted.content);
583
+ return {
584
+ url,
585
+ finalUrl,
586
+ contentType: mime,
587
+ method: "markitdown",
588
+ content: output.content,
589
+ fetchedAt,
590
+ truncated: output.truncated,
591
+ notes,
592
+ };
593
+ }
594
+ notes.push("markitdown conversion produced no usable output");
595
+ } else if (converted.error) {
596
+ notes.push(`markitdown conversion failed: ${converted.error}`);
597
+ } else {
598
+ notes.push("markitdown conversion failed");
599
+ }
600
+ } else if (binary.error) {
601
+ notes.push(`Binary fetch failed: ${binary.error}`);
602
+ } else {
603
+ notes.push("Binary fetch failed");
604
+ }
605
+ }
606
+
607
+ // Step 4: Handle non-HTML text content
608
+ const isHtml = mime.includes("html") || mime.includes("xhtml");
609
+ const isJson = mime.includes("json");
610
+ const isXml = mime.includes("xml") && !isHtml;
611
+ const isText = mime.includes("text/plain") || mime.includes("text/markdown");
612
+ const isFeed = mime.includes("rss") || mime.includes("atom") || mime.includes("feed");
613
+
614
+ if (isJson) {
615
+ const output = finalizeOutput(formatJson(rawContent));
616
+ return {
617
+ url,
618
+ finalUrl,
619
+ contentType: mime,
620
+ method: "json",
621
+ content: output.content,
622
+ fetchedAt,
623
+ truncated: output.truncated,
624
+ notes,
625
+ };
626
+ }
627
+
628
+ if (isFeed || (isXml && (rawContent.includes("<rss") || rawContent.includes("<feed")))) {
629
+ const parsed = parseFeedToMarkdown(rawContent);
630
+ const output = finalizeOutput(parsed);
631
+ return {
632
+ url,
633
+ finalUrl,
634
+ contentType: mime,
635
+ method: "feed",
636
+ content: output.content,
637
+ fetchedAt,
638
+ truncated: output.truncated,
639
+ notes,
640
+ };
641
+ }
642
+
643
+ if (isText && !looksLikeHtml(rawContent)) {
644
+ const output = finalizeOutput(rawContent);
645
+ return {
646
+ url,
647
+ finalUrl,
648
+ contentType: mime,
649
+ method: "text",
650
+ content: output.content,
651
+ fetchedAt,
652
+ truncated: output.truncated,
653
+ notes,
654
+ };
655
+ }
656
+
657
+ // Step 5: For HTML, try digestible formats first (unless raw mode)
658
+ if (isHtml && !raw) {
659
+ // 5A: Check for page-specific markdown alternate
660
+ const alternates = parseAlternateLinks(rawContent, finalUrl);
661
+ const markdownAlt = alternates.find(alt => alt.endsWith(".md") || alt.includes("markdown"));
662
+ if (markdownAlt) {
663
+ const resolved = markdownAlt.startsWith("http") ? markdownAlt : new URL(markdownAlt, finalUrl).href;
664
+ const altResult = await loadPage(resolved, { timeout, signal });
665
+ if (altResult.ok && altResult.content.trim().length > 100 && !looksLikeHtml(altResult.content)) {
666
+ notes.push(`Used markdown alternate: ${resolved}`);
667
+ const output = finalizeOutput(altResult.content);
668
+ return {
669
+ url,
670
+ finalUrl,
671
+ contentType: "text/markdown",
672
+ method: "alternate-markdown",
673
+ content: output.content,
674
+ fetchedAt,
675
+ truncated: output.truncated,
676
+ notes,
677
+ };
678
+ }
679
+ }
680
+
681
+ // 5B: Try URL.md suffix (llms.txt convention)
682
+ const mdSuffix = await tryMdSuffix(finalUrl, timeout, signal);
683
+ if (mdSuffix) {
684
+ notes.push("Found .md suffix version");
685
+ const output = finalizeOutput(mdSuffix);
686
+ return {
687
+ url,
688
+ finalUrl,
689
+ contentType: "text/markdown",
690
+ method: "md-suffix",
691
+ content: output.content,
692
+ fetchedAt,
693
+ truncated: output.truncated,
694
+ notes,
695
+ };
696
+ }
697
+
698
+ // 5C: LLM-friendly endpoints
699
+ const llmContent = await tryLlmEndpoints(origin, timeout, signal);
700
+ if (llmContent) {
701
+ notes.push("Found llms.txt");
702
+ const output = finalizeOutput(llmContent);
703
+ return {
704
+ url,
705
+ finalUrl,
706
+ contentType: "text/plain",
707
+ method: "llms.txt",
708
+ content: output.content,
709
+ fetchedAt,
710
+ truncated: output.truncated,
711
+ notes,
712
+ };
713
+ }
714
+
715
+ // 5D: Content negotiation
716
+ const negotiated = await tryContentNegotiation(url, timeout, signal);
717
+ if (negotiated) {
718
+ notes.push(`Content negotiation returned ${negotiated.type}`);
719
+ const output = finalizeOutput(negotiated.content);
720
+ return {
721
+ url,
722
+ finalUrl,
723
+ contentType: normalizeMime(negotiated.type),
724
+ method: "content-negotiation",
725
+ content: output.content,
726
+ fetchedAt,
727
+ truncated: output.truncated,
728
+ notes,
729
+ };
730
+ }
731
+
732
+ // 5E: Check for feed alternates
733
+ const feedAlternates = alternates.filter(alt => !alt.endsWith(".md") && !alt.includes("markdown"));
734
+ for (const altUrl of feedAlternates.slice(0, 2)) {
735
+ const resolved = altUrl.startsWith("http") ? altUrl : new URL(altUrl, finalUrl).href;
736
+ const altResult = await loadPage(resolved, { timeout, signal });
737
+ if (altResult.ok && altResult.content.trim().length > 200) {
738
+ notes.push(`Used feed alternate: ${resolved}`);
739
+ const parsed = parseFeedToMarkdown(altResult.content);
740
+ const output = finalizeOutput(parsed);
741
+ return {
742
+ url,
743
+ finalUrl,
744
+ contentType: "application/feed",
745
+ method: "alternate-feed",
746
+ content: output.content,
747
+ fetchedAt,
748
+ truncated: output.truncated,
749
+ notes,
750
+ };
751
+ }
752
+ }
753
+
754
+ if (signal?.aborted) {
755
+ throw new ToolAbortError();
756
+ }
757
+
758
+ // Step 6: Render HTML with lynx or html2text
759
+ const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
760
+ if (!htmlResult.ok) {
761
+ notes.push("html rendering failed (lynx/html2text unavailable)");
762
+ const output = finalizeOutput(rawContent);
763
+ return {
764
+ url,
765
+ finalUrl,
766
+ contentType: mime,
767
+ method: "raw-html",
768
+ content: output.content,
769
+ fetchedAt,
770
+ truncated: output.truncated,
771
+ notes,
772
+ };
773
+ }
774
+
775
+ // Step 7: If lynx output is low quality, try extracting document links
776
+ if (isLowQualityOutput(htmlResult.content)) {
777
+ const docLinks = extractDocumentLinks(rawContent, finalUrl);
778
+ if (docLinks.length > 0) {
779
+ const docUrl = docLinks[0];
780
+ const binary = await fetchBinary(docUrl, timeout, signal);
781
+ if (binary.ok) {
782
+ const ext = getExtensionHint(docUrl, binary.contentDisposition);
783
+ const converted = await convertWithMarkitdown(binary.buffer, ext, timeout, signal);
784
+ if (converted.ok && converted.content.trim().length > htmlResult.content.length) {
785
+ notes.push(`Extracted and converted document: ${docUrl}`);
786
+ const output = finalizeOutput(converted.content);
787
+ return {
788
+ url,
789
+ finalUrl,
790
+ contentType: "application/document",
791
+ method: "extracted-document",
792
+ content: output.content,
793
+ fetchedAt,
794
+ truncated: output.truncated,
795
+ notes,
796
+ };
797
+ }
798
+ if (!converted.ok && converted.error) {
799
+ notes.push(`markitdown conversion failed: ${converted.error}`);
800
+ }
801
+ } else if (binary.error) {
802
+ notes.push(`Binary fetch failed: ${binary.error}`);
803
+ }
804
+ }
805
+ notes.push("Page appears to require JavaScript or is mostly navigation");
806
+ }
807
+
808
+ const output = finalizeOutput(htmlResult.content);
809
+ return {
810
+ url,
811
+ finalUrl,
812
+ contentType: mime,
813
+ method: htmlResult.method,
814
+ content: output.content,
815
+ fetchedAt,
816
+ truncated: output.truncated,
817
+ notes,
818
+ };
819
+ }
820
+
821
+ // Fallback: return raw content
822
+ const output = finalizeOutput(rawContent);
823
+ return {
824
+ url,
825
+ finalUrl,
826
+ contentType: mime,
827
+ method: "raw",
828
+ content: output.content,
829
+ fetchedAt,
830
+ truncated: output.truncated,
831
+ notes,
832
+ };
833
+ }
834
+
835
+ // =============================================================================
836
+ // Tool Definition
837
+ // =============================================================================
838
+
839
+ const fetchSchema = Type.Object({
840
+ url: Type.String({ description: "URL to fetch" }),
841
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20)" })),
842
+ raw: Type.Optional(Type.Boolean({ description: "Return raw HTML without transforms" })),
843
+ });
844
+
845
+ export interface FetchToolDetails {
846
+ url: string;
847
+ finalUrl: string;
848
+ contentType: string;
849
+ method: string;
850
+ truncated: boolean;
851
+ notes: string[];
852
+ meta?: OutputMeta;
853
+ }
854
+
855
+ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails> {
856
+ readonly name = "fetch";
857
+ readonly label = "Fetch";
858
+ readonly description: string;
859
+ readonly parameters = fetchSchema;
860
+
861
+ constructor(private readonly session: ToolSession) {
862
+ this.description = renderPromptTemplate(fetchDescription);
863
+ }
864
+
865
+ async execute(
866
+ _toolCallId: string,
867
+ params: Static<typeof fetchSchema>,
868
+ signal?: AbortSignal,
869
+ _onUpdate?: AgentToolUpdateCallback<FetchToolDetails>,
870
+ _context?: AgentToolContext,
871
+ ): Promise<AgentToolResult<FetchToolDetails>> {
872
+ const { url, timeout: rawTimeout = 20, raw = false } = params;
873
+
874
+ // Clamp to valid range (seconds)
875
+ const effectiveTimeout = Math.min(Math.max(rawTimeout, 1), 45);
876
+
877
+ if (signal?.aborted) {
878
+ throw new ToolAbortError();
879
+ }
880
+
881
+ const result = await renderUrl(url, effectiveTimeout, raw, signal);
882
+ const truncation = truncateHead(result.content, {
883
+ maxBytes: DEFAULT_MAX_BYTES,
884
+ maxLines: FETCH_DEFAULT_MAX_LINES,
885
+ });
886
+ const needsArtifact = truncation.truncated;
887
+ let artifactId: string | undefined;
888
+
889
+ const buildOutput = (content: string): string => {
890
+ let output = "";
891
+ output += `URL: ${result.finalUrl}\n`;
892
+ output += `Content-Type: ${result.contentType}\n`;
893
+ output += `Method: ${result.method}\n`;
894
+ if (result.notes.length > 0) {
895
+ output += `Notes: ${result.notes.join("; ")}\n`;
896
+ }
897
+ output += `\n---\n\n`;
898
+ output += content;
899
+ return output;
900
+ };
901
+
902
+ if (needsArtifact) {
903
+ const { artifactPath, artifactId: allocatedId } = await allocateOutputArtifact(this.session, "fetch");
904
+ if (artifactPath) {
905
+ await Bun.write(artifactPath, buildOutput(result.content));
906
+ artifactId = allocatedId;
907
+ }
908
+ }
909
+
910
+ const output = buildOutput(needsArtifact ? truncation.content : result.content);
911
+
912
+ const details: FetchToolDetails = {
913
+ url: result.url,
914
+ finalUrl: result.finalUrl,
915
+ contentType: result.contentType,
916
+ method: result.method,
917
+ truncated: result.truncated || needsArtifact,
918
+ notes: result.notes,
919
+ };
920
+
921
+ const resultBuilder = toolResult(details).text(output).sourceUrl(result.finalUrl);
922
+ if (needsArtifact) {
923
+ resultBuilder.truncation(truncation, { direction: "head", artifactId });
924
+ } else if (result.truncated) {
925
+ const outputLines = result.content.split("\n").length;
926
+ const outputBytes = Buffer.byteLength(result.content, "utf-8");
927
+ const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
928
+ const totalLines = outputLines + 1;
929
+ resultBuilder.truncationFromText(result.content, {
930
+ direction: "tail",
931
+ totalLines,
932
+ totalBytes,
933
+ maxBytes: MAX_OUTPUT_CHARS,
934
+ });
935
+ }
936
+
937
+ return resultBuilder.done();
938
+ }
939
+ }
940
+
941
+ // =============================================================================
942
+ // TUI Rendering
943
+ // =============================================================================
944
+
945
+ /** Truncate text to max length with ellipsis */
946
+ function truncate(text: string, maxLen: number, ellipsis: string): string {
947
+ if (text.length <= maxLen) return text;
948
+ const sliceLen = Math.max(0, maxLen - ellipsis.length);
949
+ return `${text.slice(0, sliceLen)}${ellipsis}`;
950
+ }
951
+
952
+ /** Extract domain from URL */
953
+ function getDomain(url: string): string {
954
+ try {
955
+ const u = new URL(url);
956
+ return u.hostname.replace(/^www\./, "");
957
+ } catch {
958
+ return url;
959
+ }
960
+ }
961
+
962
+ /** Count non-empty lines */
963
+ function countNonEmptyLines(text: string): number {
964
+ return text.split("\n").filter(l => l.trim()).length;
965
+ }
966
+
967
+ /** Render fetch call (URL preview) */
968
+ export function renderFetchCall(
969
+ args: { url?: string; timeout?: number; raw?: boolean },
970
+ _options: RenderResultOptions,
971
+ uiTheme: Theme = theme,
972
+ ): Component {
973
+ const url = args.url ?? "";
974
+ const domain = getDomain(url);
975
+ const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "\u2026");
976
+ const description = `${domain}${path ? ` ${path}` : ""}`.trim();
977
+ const meta: string[] = [];
978
+ if (args.raw) meta.push("raw");
979
+ if (args.timeout !== undefined) meta.push(`timeout:${args.timeout}s`);
980
+ const text = renderStatusLine({ icon: "pending", title: "Fetch", description, meta }, uiTheme);
981
+ return new Text(text, 0, 0);
982
+ }
983
+
984
+ /** Render fetch result with tree-based layout */
985
+ export function renderFetchResult(
986
+ result: { content: Array<{ type: string; text?: string }>; details?: FetchToolDetails },
987
+ options: RenderResultOptions,
988
+ uiTheme: Theme = theme,
989
+ ): Component {
990
+ const details = result.details;
991
+
992
+ if (!details) {
993
+ return new Text(uiTheme.fg("error", "No response data"), 0, 0);
994
+ }
995
+
996
+ const domain = getDomain(details.finalUrl);
997
+ const path = truncate(details.finalUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
998
+ const hasRedirect = details.url !== details.finalUrl;
999
+ const hasNotes = details.notes.length > 0;
1000
+ const truncation = details.meta?.truncation;
1001
+ const truncated = Boolean(details.truncated || truncation);
1002
+
1003
+ const header = renderStatusLine(
1004
+ {
1005
+ icon: truncated ? "warning" : "success",
1006
+ title: "Fetch",
1007
+ description: `${domain}${path ? ` ${path}` : ""}`,
1008
+ },
1009
+ uiTheme,
1010
+ );
1011
+
1012
+ const contentText = result.content[0]?.text ?? "";
1013
+ const contentBody = contentText.includes("---\n\n")
1014
+ ? contentText.split("---\n\n").slice(1).join("---\n\n")
1015
+ : contentText;
1016
+ const lineCount = countNonEmptyLines(contentBody);
1017
+ const charCount = contentBody.trim().length;
1018
+ const contentLines = contentBody.split("\n").filter(l => l.trim());
1019
+
1020
+ const metadataLines: string[] = [
1021
+ `${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
1022
+ `${uiTheme.fg("muted", "Method:")} ${details.method}`,
1023
+ ];
1024
+ if (hasRedirect) {
1025
+ metadataLines.push(`${uiTheme.fg("muted", "Final URL:")} ${uiTheme.fg("mdLinkUrl", details.finalUrl)}`);
1026
+ }
1027
+ const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
1028
+ metadataLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
1029
+ metadataLines.push(`${uiTheme.fg("muted", "Chars:")} ${charCount}`);
1030
+ if (truncated) {
1031
+ metadataLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
1032
+ if (truncation?.artifactId) {
1033
+ metadataLines.push(uiTheme.fg("warning", `Full output: artifact://${truncation.artifactId}`));
1034
+ }
1035
+ }
1036
+ if (hasNotes) {
1037
+ metadataLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
1038
+ }
1039
+
1040
+ const outputBlock = new CachedOutputBlock();
1041
+ let lastExpanded: boolean | undefined;
1042
+ let contentPreviewLines: string[] | undefined;
1043
+
1044
+ return {
1045
+ render: (width: number) => {
1046
+ const { expanded } = options;
1047
+
1048
+ if (contentPreviewLines === undefined || lastExpanded !== expanded) {
1049
+ const previewLimit = expanded ? 12 : 3;
1050
+ const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
1051
+ const previewLines = previewList.items.map(line => truncate(line.trimEnd(), 120, "…"));
1052
+ const remaining = Math.max(0, contentLines.length - previewLines.length);
1053
+ contentPreviewLines =
1054
+ previewLines.length > 0
1055
+ ? previewLines.map(line => uiTheme.fg("dim", line))
1056
+ : [uiTheme.fg("dim", "(no content)")];
1057
+ if (remaining > 0) {
1058
+ const hint = formatExpandHint(uiTheme, expanded, true);
1059
+ contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
1060
+ }
1061
+ lastExpanded = expanded;
1062
+ outputBlock.invalidate();
1063
+ }
1064
+
1065
+ return outputBlock.render(
1066
+ {
1067
+ header,
1068
+ state: truncated ? "warning" : "success",
1069
+ sections: [
1070
+ { label: uiTheme.fg("toolTitle", "Metadata"), lines: metadataLines },
1071
+ { label: uiTheme.fg("toolTitle", "Content Preview"), lines: contentPreviewLines },
1072
+ ],
1073
+ width,
1074
+ applyBg: false,
1075
+ },
1076
+ uiTheme,
1077
+ );
1078
+ },
1079
+ invalidate: () => {
1080
+ outputBlock.invalidate();
1081
+ contentPreviewLines = undefined;
1082
+ lastExpanded = undefined;
1083
+ },
1084
+ };
1085
+ }
1086
+
1087
+ export const fetchToolRenderer = {
1088
+ renderCall: renderFetchCall,
1089
+ renderResult: renderFetchResult,
1090
+ mergeCallAndResult: true,
1091
+ };