@wahack/pi-coding-agent 15.11.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 (1152) hide show
  1. package/CHANGELOG.md +10031 -0
  2. package/README.md +36 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +104 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/extensions/README.md +142 -0
  7. package/examples/extensions/api-demo.ts +79 -0
  8. package/examples/extensions/chalk-logger.ts +25 -0
  9. package/examples/extensions/hello.ts +31 -0
  10. package/examples/extensions/pirate.ts +43 -0
  11. package/examples/extensions/plan-mode.ts +549 -0
  12. package/examples/extensions/reload-runtime.ts +38 -0
  13. package/examples/extensions/thinking-note.ts +13 -0
  14. package/examples/extensions/tools.ts +145 -0
  15. package/examples/extensions/with-deps/index.ts +36 -0
  16. package/examples/extensions/with-deps/package-lock.json +31 -0
  17. package/examples/extensions/with-deps/package.json +17 -0
  18. package/examples/hooks/README.md +56 -0
  19. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  20. package/examples/hooks/confirm-destructive.ts +58 -0
  21. package/examples/hooks/custom-compaction.ts +115 -0
  22. package/examples/hooks/dirty-repo-guard.ts +51 -0
  23. package/examples/hooks/file-trigger.ts +40 -0
  24. package/examples/hooks/git-checkpoint.ts +52 -0
  25. package/examples/hooks/handoff.ts +149 -0
  26. package/examples/hooks/permission-gate.ts +33 -0
  27. package/examples/hooks/protected-paths.ts +29 -0
  28. package/examples/hooks/qna.ts +118 -0
  29. package/examples/hooks/status-line.ts +39 -0
  30. package/examples/sdk/01-minimal.ts +21 -0
  31. package/examples/sdk/02-custom-model.ts +49 -0
  32. package/examples/sdk/03-custom-prompt.ts +46 -0
  33. package/examples/sdk/04-skills.ts +43 -0
  34. package/examples/sdk/06-extensions.ts +82 -0
  35. package/examples/sdk/06-hooks.ts +61 -0
  36. package/examples/sdk/07-context-files.ts +35 -0
  37. package/examples/sdk/08-prompt-templates.ts +41 -0
  38. package/examples/sdk/08-slash-commands.ts +46 -0
  39. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  40. package/examples/sdk/11-sessions.ts +47 -0
  41. package/examples/sdk/12-redis-sessions.ts +54 -0
  42. package/examples/sdk/13-sql-sessions.ts +61 -0
  43. package/examples/sdk/README.md +172 -0
  44. package/package.json +554 -0
  45. package/scripts/build-binary.ts +100 -0
  46. package/scripts/bundle-dist.ts +90 -0
  47. package/scripts/format-prompts.ts +68 -0
  48. package/scripts/generate-docs-index.ts +40 -0
  49. package/scripts/generate-template.ts +33 -0
  50. package/scripts/omp +42 -0
  51. package/scripts/omp.ts +19 -0
  52. package/src/async/index.ts +1 -0
  53. package/src/async/job-manager.ts +625 -0
  54. package/src/auto-thinking/classifier.ts +185 -0
  55. package/src/autoresearch/command-resume.md +14 -0
  56. package/src/autoresearch/dashboard.ts +436 -0
  57. package/src/autoresearch/git.ts +319 -0
  58. package/src/autoresearch/helpers.ts +218 -0
  59. package/src/autoresearch/index.ts +536 -0
  60. package/src/autoresearch/prompt-setup.md +43 -0
  61. package/src/autoresearch/prompt.md +103 -0
  62. package/src/autoresearch/resume-message.md +10 -0
  63. package/src/autoresearch/state.ts +273 -0
  64. package/src/autoresearch/storage.ts +699 -0
  65. package/src/autoresearch/tools/init-experiment.ts +272 -0
  66. package/src/autoresearch/tools/log-experiment.ts +524 -0
  67. package/src/autoresearch/tools/run-experiment.ts +407 -0
  68. package/src/autoresearch/tools/update-notes.ts +109 -0
  69. package/src/autoresearch/types.ts +168 -0
  70. package/src/bun-imports.d.ts +28 -0
  71. package/src/capability/context-file.ts +44 -0
  72. package/src/capability/extension-module.ts +34 -0
  73. package/src/capability/extension.ts +47 -0
  74. package/src/capability/fs.ts +117 -0
  75. package/src/capability/hook.ts +40 -0
  76. package/src/capability/index.ts +436 -0
  77. package/src/capability/instruction.ts +37 -0
  78. package/src/capability/mcp.ts +74 -0
  79. package/src/capability/prompt.ts +35 -0
  80. package/src/capability/rule-buckets.ts +66 -0
  81. package/src/capability/rule.ts +261 -0
  82. package/src/capability/settings.ts +34 -0
  83. package/src/capability/skill.ts +63 -0
  84. package/src/capability/slash-command.ts +40 -0
  85. package/src/capability/ssh.ts +41 -0
  86. package/src/capability/system-prompt.ts +34 -0
  87. package/src/capability/tool.ts +38 -0
  88. package/src/capability/types.ts +168 -0
  89. package/src/cli/agents-cli.ts +138 -0
  90. package/src/cli/args.ts +340 -0
  91. package/src/cli/auth-broker-cli.ts +895 -0
  92. package/src/cli/auth-gateway-cli.ts +611 -0
  93. package/src/cli/classify-install-target.ts +76 -0
  94. package/src/cli/claude-trace-cli.ts +795 -0
  95. package/src/cli/commands/init-xdg.ts +27 -0
  96. package/src/cli/completion-gen.ts +550 -0
  97. package/src/cli/config-cli.ts +418 -0
  98. package/src/cli/dry-balance-cli.ts +856 -0
  99. package/src/cli/extension-flags.ts +48 -0
  100. package/src/cli/file-processor.ts +133 -0
  101. package/src/cli/gallery-cli.ts +230 -0
  102. package/src/cli/gallery-fixtures/agentic.ts +407 -0
  103. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  104. package/src/cli/gallery-fixtures/edit.ts +194 -0
  105. package/src/cli/gallery-fixtures/fs.ts +220 -0
  106. package/src/cli/gallery-fixtures/index.ts +40 -0
  107. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  108. package/src/cli/gallery-fixtures/memory.ts +81 -0
  109. package/src/cli/gallery-fixtures/misc.ts +250 -0
  110. package/src/cli/gallery-fixtures/search.ts +213 -0
  111. package/src/cli/gallery-fixtures/shell.ts +167 -0
  112. package/src/cli/gallery-fixtures/types.ts +57 -0
  113. package/src/cli/gallery-fixtures/web.ts +158 -0
  114. package/src/cli/gallery-screenshot.ts +279 -0
  115. package/src/cli/grep-cli.ts +160 -0
  116. package/src/cli/grievances-cli.ts +256 -0
  117. package/src/cli/initial-message.ts +58 -0
  118. package/src/cli/list-models.ts +194 -0
  119. package/src/cli/plugin-cli.ts +996 -0
  120. package/src/cli/read-cli.ts +57 -0
  121. package/src/cli/session-picker.ts +79 -0
  122. package/src/cli/setup-cli.ts +231 -0
  123. package/src/cli/shell-cli.ts +176 -0
  124. package/src/cli/ssh-cli.ts +179 -0
  125. package/src/cli/startup-cwd.ts +68 -0
  126. package/src/cli/stats-cli.ts +238 -0
  127. package/src/cli/tiny-models-cli.ts +127 -0
  128. package/src/cli/update-cli.ts +611 -0
  129. package/src/cli/usage-cli.ts +603 -0
  130. package/src/cli/web-search-cli.ts +132 -0
  131. package/src/cli/worktree-cli.ts +291 -0
  132. package/src/cli-commands.ts +79 -0
  133. package/src/cli.ts +200 -0
  134. package/src/commands/acp.ts +24 -0
  135. package/src/commands/agents.ts +57 -0
  136. package/src/commands/auth-broker.ts +99 -0
  137. package/src/commands/auth-gateway.ts +69 -0
  138. package/src/commands/commit.ts +46 -0
  139. package/src/commands/complete.ts +66 -0
  140. package/src/commands/completions.ts +60 -0
  141. package/src/commands/config.ts +51 -0
  142. package/src/commands/dry-balance.ts +43 -0
  143. package/src/commands/gallery.ts +52 -0
  144. package/src/commands/grep.ts +48 -0
  145. package/src/commands/grievances.ts +51 -0
  146. package/src/commands/install.ts +107 -0
  147. package/src/commands/launch.ts +169 -0
  148. package/src/commands/plugin.ts +78 -0
  149. package/src/commands/read.ts +38 -0
  150. package/src/commands/setup.ts +67 -0
  151. package/src/commands/shell.ts +29 -0
  152. package/src/commands/ssh.ts +60 -0
  153. package/src/commands/stats.ts +29 -0
  154. package/src/commands/tiny-models.ts +36 -0
  155. package/src/commands/update.ts +21 -0
  156. package/src/commands/usage.ts +35 -0
  157. package/src/commands/web-search.ts +42 -0
  158. package/src/commands/worktree.ts +56 -0
  159. package/src/commit/agentic/agent.ts +317 -0
  160. package/src/commit/agentic/fallback.ts +96 -0
  161. package/src/commit/agentic/index.ts +355 -0
  162. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  163. package/src/commit/agentic/prompts/session-user.md +25 -0
  164. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  165. package/src/commit/agentic/prompts/system.md +38 -0
  166. package/src/commit/agentic/state.ts +60 -0
  167. package/src/commit/agentic/tools/analyze-file.ts +146 -0
  168. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  169. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  170. package/src/commit/agentic/tools/git-overview.ts +81 -0
  171. package/src/commit/agentic/tools/index.ts +54 -0
  172. package/src/commit/agentic/tools/propose-changelog.ts +144 -0
  173. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  174. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  175. package/src/commit/agentic/tools/schemas.ts +23 -0
  176. package/src/commit/agentic/tools/split-commit.ts +245 -0
  177. package/src/commit/agentic/topo-sort.ts +44 -0
  178. package/src/commit/agentic/trivial.ts +51 -0
  179. package/src/commit/agentic/validation.ts +183 -0
  180. package/src/commit/analysis/conventional.ts +64 -0
  181. package/src/commit/analysis/index.ts +4 -0
  182. package/src/commit/analysis/scope.ts +242 -0
  183. package/src/commit/analysis/summary.ts +105 -0
  184. package/src/commit/analysis/validation.ts +66 -0
  185. package/src/commit/changelog/detect.ts +40 -0
  186. package/src/commit/changelog/generate.ts +97 -0
  187. package/src/commit/changelog/index.ts +234 -0
  188. package/src/commit/changelog/parse.ts +44 -0
  189. package/src/commit/cli.ts +85 -0
  190. package/src/commit/git/diff.ts +148 -0
  191. package/src/commit/index.ts +5 -0
  192. package/src/commit/map-reduce/index.ts +69 -0
  193. package/src/commit/map-reduce/map-phase.ts +193 -0
  194. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  195. package/src/commit/map-reduce/utils.ts +9 -0
  196. package/src/commit/message.ts +11 -0
  197. package/src/commit/model-selection.ts +92 -0
  198. package/src/commit/pipeline.ts +243 -0
  199. package/src/commit/prompts/analysis-system.md +148 -0
  200. package/src/commit/prompts/analysis-user.md +38 -0
  201. package/src/commit/prompts/changelog-system.md +50 -0
  202. package/src/commit/prompts/changelog-user.md +18 -0
  203. package/src/commit/prompts/file-observer-system.md +24 -0
  204. package/src/commit/prompts/file-observer-user.md +8 -0
  205. package/src/commit/prompts/reduce-system.md +50 -0
  206. package/src/commit/prompts/reduce-user.md +17 -0
  207. package/src/commit/prompts/summary-retry.md +3 -0
  208. package/src/commit/prompts/summary-system.md +38 -0
  209. package/src/commit/prompts/summary-user.md +13 -0
  210. package/src/commit/prompts/types-description.md +2 -0
  211. package/src/commit/shared-llm.ts +77 -0
  212. package/src/commit/types.ts +118 -0
  213. package/src/commit/utils/exclusions.ts +42 -0
  214. package/src/commit/utils.ts +58 -0
  215. package/src/config/api-key-resolver.ts +60 -0
  216. package/src/config/append-only-context-mode.ts +31 -0
  217. package/src/config/config-file.ts +317 -0
  218. package/src/config/file-lock.ts +164 -0
  219. package/src/config/keybindings.ts +628 -0
  220. package/src/config/mcp-schema.json +230 -0
  221. package/src/config/model-discovery.ts +554 -0
  222. package/src/config/model-registry.ts +2090 -0
  223. package/src/config/model-resolver.ts +1502 -0
  224. package/src/config/model-roles.ts +74 -0
  225. package/src/config/models-config-schema.ts +226 -0
  226. package/src/config/models-config.ts +129 -0
  227. package/src/config/prompt-templates.ts +185 -0
  228. package/src/config/resolve-config-value.ts +94 -0
  229. package/src/config/settings-schema.ts +3530 -0
  230. package/src/config/settings.ts +1178 -0
  231. package/src/config.ts +242 -0
  232. package/src/cursor.ts +340 -0
  233. package/src/dap/client.ts +760 -0
  234. package/src/dap/config.ts +189 -0
  235. package/src/dap/defaults.json +212 -0
  236. package/src/dap/index.ts +4 -0
  237. package/src/dap/session.ts +1441 -0
  238. package/src/dap/types.ts +610 -0
  239. package/src/debug/index.ts +515 -0
  240. package/src/debug/log-formatting.ts +58 -0
  241. package/src/debug/log-viewer.ts +908 -0
  242. package/src/debug/profiler.ts +162 -0
  243. package/src/debug/protocol-probe.ts +267 -0
  244. package/src/debug/raw-sse-buffer.ts +273 -0
  245. package/src/debug/raw-sse.ts +292 -0
  246. package/src/debug/report-bundle.ts +374 -0
  247. package/src/debug/system-info.ts +111 -0
  248. package/src/debug/terminal-info.ts +124 -0
  249. package/src/discovery/agents-md.ts +67 -0
  250. package/src/discovery/agents.ts +230 -0
  251. package/src/discovery/at-imports.ts +273 -0
  252. package/src/discovery/builtin-defaults.ts +39 -0
  253. package/src/discovery/builtin-rules/index.ts +54 -0
  254. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  255. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  256. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  257. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  258. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  259. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  260. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  261. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  262. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  263. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
  264. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  265. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  266. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  267. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  268. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  269. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  270. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  271. package/src/discovery/builtin.ts +906 -0
  272. package/src/discovery/claude-plugins.ts +386 -0
  273. package/src/discovery/claude.ts +584 -0
  274. package/src/discovery/cline.ts +83 -0
  275. package/src/discovery/codex.ts +522 -0
  276. package/src/discovery/cursor.ts +220 -0
  277. package/src/discovery/gemini.ts +383 -0
  278. package/src/discovery/github.ts +154 -0
  279. package/src/discovery/helpers.ts +1016 -0
  280. package/src/discovery/index.ts +81 -0
  281. package/src/discovery/mcp-json.ts +171 -0
  282. package/src/discovery/omp-extension-roots.ts +190 -0
  283. package/src/discovery/omp-plugins.ts +383 -0
  284. package/src/discovery/opencode.ts +398 -0
  285. package/src/discovery/plugin-dir-roots.ts +28 -0
  286. package/src/discovery/ssh.ts +153 -0
  287. package/src/discovery/substitute-plugin-root.ts +29 -0
  288. package/src/discovery/vscode.ts +105 -0
  289. package/src/discovery/windsurf.ts +147 -0
  290. package/src/edit/apply-patch/index.ts +87 -0
  291. package/src/edit/apply-patch/parser.ts +174 -0
  292. package/src/edit/diff.ts +999 -0
  293. package/src/edit/file-snapshot-store.ts +91 -0
  294. package/src/edit/hashline/block-resolver.ts +33 -0
  295. package/src/edit/hashline/diff.ts +290 -0
  296. package/src/edit/hashline/execute.ts +242 -0
  297. package/src/edit/hashline/filesystem.ts +130 -0
  298. package/src/edit/hashline/index.ts +5 -0
  299. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  300. package/src/edit/hashline/params.ts +18 -0
  301. package/src/edit/index.ts +571 -0
  302. package/src/edit/modes/apply-patch.lark +19 -0
  303. package/src/edit/modes/apply-patch.ts +53 -0
  304. package/src/edit/modes/patch.ts +1891 -0
  305. package/src/edit/modes/replace.ts +1137 -0
  306. package/src/edit/normalize.ts +345 -0
  307. package/src/edit/notebook.ts +242 -0
  308. package/src/edit/read-file.ts +25 -0
  309. package/src/edit/renderer.ts +769 -0
  310. package/src/edit/streaming.ts +517 -0
  311. package/src/eval/__tests__/agent-bridge.test.ts +708 -0
  312. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  313. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  314. package/src/eval/__tests__/completion-bridge.test.ts +412 -0
  315. package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
  316. package/src/eval/__tests__/idle-timeout.test.ts +80 -0
  317. package/src/eval/__tests__/js-context-manager.test.ts +241 -0
  318. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  319. package/src/eval/agent-bridge.ts +319 -0
  320. package/src/eval/backend.ts +71 -0
  321. package/src/eval/bridge-timeout.ts +44 -0
  322. package/src/eval/budget-bridge.ts +48 -0
  323. package/src/eval/completion-bridge.ts +207 -0
  324. package/src/eval/concurrency-bridge.ts +34 -0
  325. package/src/eval/idle-timeout.ts +91 -0
  326. package/src/eval/index.ts +4 -0
  327. package/src/eval/js/context-manager.ts +502 -0
  328. package/src/eval/js/executor.ts +173 -0
  329. package/src/eval/js/index.ts +51 -0
  330. package/src/eval/js/shared/helpers.ts +283 -0
  331. package/src/eval/js/shared/indirect-eval.ts +30 -0
  332. package/src/eval/js/shared/local-module-loader.ts +342 -0
  333. package/src/eval/js/shared/prelude.ts +2 -0
  334. package/src/eval/js/shared/prelude.txt +246 -0
  335. package/src/eval/js/shared/rewrite-imports.ts +532 -0
  336. package/src/eval/js/shared/runtime.ts +352 -0
  337. package/src/eval/js/shared/types.ts +18 -0
  338. package/src/eval/js/tool-bridge.ts +162 -0
  339. package/src/eval/js/worker-core.ts +132 -0
  340. package/src/eval/js/worker-entry.ts +30 -0
  341. package/src/eval/js/worker-protocol.ts +47 -0
  342. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  343. package/src/eval/py/display.ts +71 -0
  344. package/src/eval/py/executor.ts +742 -0
  345. package/src/eval/py/index.ts +68 -0
  346. package/src/eval/py/kernel.ts +748 -0
  347. package/src/eval/py/prelude.py +658 -0
  348. package/src/eval/py/prelude.ts +3 -0
  349. package/src/eval/py/runner.py +1133 -0
  350. package/src/eval/py/runtime.ts +276 -0
  351. package/src/eval/py/spawn-options.ts +126 -0
  352. package/src/eval/py/tool-bridge.ts +182 -0
  353. package/src/eval/session-id.ts +8 -0
  354. package/src/eval/types.ts +48 -0
  355. package/src/exa/index.ts +2 -0
  356. package/src/exa/mcp-client.ts +370 -0
  357. package/src/exa/types.ts +69 -0
  358. package/src/exec/bash-executor.ts +419 -0
  359. package/src/exec/exec.ts +53 -0
  360. package/src/exec/non-interactive-env.ts +48 -0
  361. package/src/export/custom-share.ts +65 -0
  362. package/src/export/html/index.ts +164 -0
  363. package/src/export/html/template.css +1051 -0
  364. package/src/export/html/template.generated.ts +2 -0
  365. package/src/export/html/template.html +46 -0
  366. package/src/export/html/template.js +2271 -0
  367. package/src/export/html/template.macro.ts +25 -0
  368. package/src/export/html/vendor/highlight.min.js +1213 -0
  369. package/src/export/html/vendor/marked.min.js +6 -0
  370. package/src/export/ttsr.ts +583 -0
  371. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  372. package/src/extensibility/custom-commands/bundled/review/index.ts +489 -0
  373. package/src/extensibility/custom-commands/index.ts +2 -0
  374. package/src/extensibility/custom-commands/loader.ts +238 -0
  375. package/src/extensibility/custom-commands/types.ts +113 -0
  376. package/src/extensibility/custom-tools/index.ts +7 -0
  377. package/src/extensibility/custom-tools/loader.ts +269 -0
  378. package/src/extensibility/custom-tools/types.ts +270 -0
  379. package/src/extensibility/custom-tools/wrapper.ts +47 -0
  380. package/src/extensibility/extensions/compact-handler.ts +40 -0
  381. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  382. package/src/extensibility/extensions/index.ts +16 -0
  383. package/src/extensibility/extensions/loader.ts +572 -0
  384. package/src/extensibility/extensions/runner.ts +922 -0
  385. package/src/extensibility/extensions/types.ts +1322 -0
  386. package/src/extensibility/extensions/wrapper.ts +223 -0
  387. package/src/extensibility/hooks/index.ts +5 -0
  388. package/src/extensibility/hooks/loader.ts +257 -0
  389. package/src/extensibility/hooks/runner.ts +425 -0
  390. package/src/extensibility/hooks/tool-wrapper.ts +107 -0
  391. package/src/extensibility/hooks/types.ts +606 -0
  392. package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
  393. package/src/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
  394. package/src/extensibility/plugins/doctor.ts +65 -0
  395. package/src/extensibility/plugins/git-url.ts +367 -0
  396. package/src/extensibility/plugins/index.ts +9 -0
  397. package/src/extensibility/plugins/installer.ts +192 -0
  398. package/src/extensibility/plugins/legacy-pi-compat.ts +682 -0
  399. package/src/extensibility/plugins/loader.ts +313 -0
  400. package/src/extensibility/plugins/manager.ts +827 -0
  401. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  402. package/src/extensibility/plugins/marketplace/fetcher.ts +317 -0
  403. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  404. package/src/extensibility/plugins/marketplace/manager.ts +770 -0
  405. package/src/extensibility/plugins/marketplace/registry.ts +196 -0
  406. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  407. package/src/extensibility/plugins/marketplace/types.ts +191 -0
  408. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  409. package/src/extensibility/plugins/parser.ts +105 -0
  410. package/src/extensibility/plugins/types.ts +194 -0
  411. package/src/extensibility/shared-events.ts +343 -0
  412. package/src/extensibility/skills.ts +312 -0
  413. package/src/extensibility/slash-commands.ts +227 -0
  414. package/src/extensibility/tool-proxy.ts +25 -0
  415. package/src/extensibility/typebox.ts +418 -0
  416. package/src/extensibility/utils.ts +44 -0
  417. package/src/goals/index.ts +3 -0
  418. package/src/goals/runtime.ts +528 -0
  419. package/src/goals/state.ts +37 -0
  420. package/src/goals/tools/goal-tool.ts +251 -0
  421. package/src/hindsight/backend.ts +354 -0
  422. package/src/hindsight/bank.ts +156 -0
  423. package/src/hindsight/client.ts +598 -0
  424. package/src/hindsight/config.ts +175 -0
  425. package/src/hindsight/content.ts +210 -0
  426. package/src/hindsight/index.ts +8 -0
  427. package/src/hindsight/mental-models.ts +429 -0
  428. package/src/hindsight/seeds.json +32 -0
  429. package/src/hindsight/state.ts +488 -0
  430. package/src/hindsight/transcript.ts +71 -0
  431. package/src/index.ts +59 -0
  432. package/src/internal-urls/agent-protocol.ts +146 -0
  433. package/src/internal-urls/artifact-protocol.ts +107 -0
  434. package/src/internal-urls/docs-index.generated.ts +106 -0
  435. package/src/internal-urls/history-protocol.ts +113 -0
  436. package/src/internal-urls/index.ts +25 -0
  437. package/src/internal-urls/issue-pr-protocol.ts +584 -0
  438. package/src/internal-urls/json-query.ts +126 -0
  439. package/src/internal-urls/local-protocol.ts +287 -0
  440. package/src/internal-urls/mcp-protocol.ts +151 -0
  441. package/src/internal-urls/memory-protocol.ts +169 -0
  442. package/src/internal-urls/omp-protocol.ts +93 -0
  443. package/src/internal-urls/parse.ts +72 -0
  444. package/src/internal-urls/registry-helpers.ts +25 -0
  445. package/src/internal-urls/router.ts +105 -0
  446. package/src/internal-urls/rule-protocol.ts +45 -0
  447. package/src/internal-urls/skill-protocol.ts +96 -0
  448. package/src/internal-urls/types.ts +152 -0
  449. package/src/internal-urls/vault-protocol.ts +936 -0
  450. package/src/irc/bus.ts +292 -0
  451. package/src/lib/xai-http.ts +124 -0
  452. package/src/lsp/client.ts +1193 -0
  453. package/src/lsp/clients/biome-client.ts +264 -0
  454. package/src/lsp/clients/index.ts +50 -0
  455. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  456. package/src/lsp/clients/swiftlint-client.ts +120 -0
  457. package/src/lsp/config.ts +502 -0
  458. package/src/lsp/defaults.json +493 -0
  459. package/src/lsp/diagnostics-ledger.ts +51 -0
  460. package/src/lsp/edits.ts +267 -0
  461. package/src/lsp/index.ts +2477 -0
  462. package/src/lsp/lspmux.ts +233 -0
  463. package/src/lsp/render.ts +694 -0
  464. package/src/lsp/startup-events.ts +13 -0
  465. package/src/lsp/types.ts +455 -0
  466. package/src/lsp/utils.ts +718 -0
  467. package/src/main.ts +1325 -0
  468. package/src/mcp/client.ts +484 -0
  469. package/src/mcp/config-writer.ts +225 -0
  470. package/src/mcp/config.ts +365 -0
  471. package/src/mcp/index.ts +29 -0
  472. package/src/mcp/json-rpc.ts +122 -0
  473. package/src/mcp/loader.ts +124 -0
  474. package/src/mcp/manager.ts +1275 -0
  475. package/src/mcp/oauth-discovery.ts +442 -0
  476. package/src/mcp/oauth-flow.ts +442 -0
  477. package/src/mcp/render.ts +132 -0
  478. package/src/mcp/smithery-auth.ts +104 -0
  479. package/src/mcp/smithery-connect.ts +145 -0
  480. package/src/mcp/smithery-registry.ts +477 -0
  481. package/src/mcp/timeout.ts +59 -0
  482. package/src/mcp/tool-bridge.ts +426 -0
  483. package/src/mcp/tool-cache.ts +117 -0
  484. package/src/mcp/transports/http.ts +519 -0
  485. package/src/mcp/transports/index.ts +6 -0
  486. package/src/mcp/transports/stdio.ts +528 -0
  487. package/src/mcp/types.ts +423 -0
  488. package/src/memories/index.ts +1150 -0
  489. package/src/memories/storage.ts +577 -0
  490. package/src/memory-backend/index.ts +18 -0
  491. package/src/memory-backend/local-backend.ts +39 -0
  492. package/src/memory-backend/off-backend.ts +25 -0
  493. package/src/memory-backend/resolve.ts +25 -0
  494. package/src/memory-backend/runtime.ts +66 -0
  495. package/src/memory-backend/types.ts +166 -0
  496. package/src/mnemopi/backend.ts +547 -0
  497. package/src/mnemopi/config.ts +160 -0
  498. package/src/mnemopi/index.ts +3 -0
  499. package/src/mnemopi/state.ts +584 -0
  500. package/src/modes/acp/acp-agent.ts +2407 -0
  501. package/src/modes/acp/acp-client-bridge.ts +154 -0
  502. package/src/modes/acp/acp-event-mapper.ts +929 -0
  503. package/src/modes/acp/acp-mode.ts +23 -0
  504. package/src/modes/acp/index.ts +2 -0
  505. package/src/modes/acp/terminal-auth.ts +37 -0
  506. package/src/modes/components/agent-dashboard.ts +1206 -0
  507. package/src/modes/components/agent-hub.ts +1071 -0
  508. package/src/modes/components/assistant-message.ts +307 -0
  509. package/src/modes/components/bash-execution.ts +220 -0
  510. package/src/modes/components/bordered-loader.ts +41 -0
  511. package/src/modes/components/branch-summary-message.ts +45 -0
  512. package/src/modes/components/btw-panel.ts +104 -0
  513. package/src/modes/components/chat-block.ts +111 -0
  514. package/src/modes/components/compaction-summary-message.ts +87 -0
  515. package/src/modes/components/copy-selector.ts +206 -0
  516. package/src/modes/components/countdown-timer.ts +75 -0
  517. package/src/modes/components/custom-editor.ts +398 -0
  518. package/src/modes/components/custom-message.ts +63 -0
  519. package/src/modes/components/diff.ts +277 -0
  520. package/src/modes/components/dynamic-border.ts +34 -0
  521. package/src/modes/components/error-banner.ts +33 -0
  522. package/src/modes/components/eval-execution.ts +158 -0
  523. package/src/modes/components/execution-shared.ts +101 -0
  524. package/src/modes/components/extensions/extension-dashboard.ts +399 -0
  525. package/src/modes/components/extensions/extension-list.ts +502 -0
  526. package/src/modes/components/extensions/index.ts +9 -0
  527. package/src/modes/components/extensions/inspector-panel.ts +317 -0
  528. package/src/modes/components/extensions/state-manager.ts +627 -0
  529. package/src/modes/components/extensions/types.ts +186 -0
  530. package/src/modes/components/footer.ts +274 -0
  531. package/src/modes/components/history-search.ts +280 -0
  532. package/src/modes/components/hook-editor.ts +167 -0
  533. package/src/modes/components/hook-input.ts +87 -0
  534. package/src/modes/components/hook-message.ts +66 -0
  535. package/src/modes/components/hook-selector.ts +660 -0
  536. package/src/modes/components/index.ts +38 -0
  537. package/src/modes/components/keybinding-hints.ts +65 -0
  538. package/src/modes/components/late-diagnostics-message.ts +60 -0
  539. package/src/modes/components/login-dialog.ts +164 -0
  540. package/src/modes/components/mcp-add-wizard.ts +1340 -0
  541. package/src/modes/components/message-frame.ts +88 -0
  542. package/src/modes/components/model-selector.ts +1271 -0
  543. package/src/modes/components/oauth-selector.ts +368 -0
  544. package/src/modes/components/omfg-panel.ts +141 -0
  545. package/src/modes/components/overlay-box.ts +108 -0
  546. package/src/modes/components/plan-review-overlay.ts +820 -0
  547. package/src/modes/components/plan-toc.ts +138 -0
  548. package/src/modes/components/plugin-selector.ts +95 -0
  549. package/src/modes/components/plugin-settings.ts +722 -0
  550. package/src/modes/components/queue-mode-selector.ts +56 -0
  551. package/src/modes/components/read-tool-group.ts +670 -0
  552. package/src/modes/components/segment-track.ts +52 -0
  553. package/src/modes/components/session-selector.ts +625 -0
  554. package/src/modes/components/settings-defs.ts +189 -0
  555. package/src/modes/components/settings-selector.ts +651 -0
  556. package/src/modes/components/show-images-selector.ts +45 -0
  557. package/src/modes/components/skill-message.ts +89 -0
  558. package/src/modes/components/status-line/component.ts +869 -0
  559. package/src/modes/components/status-line/context-thresholds.ts +79 -0
  560. package/src/modes/components/status-line/git-utils.ts +42 -0
  561. package/src/modes/components/status-line/index.ts +5 -0
  562. package/src/modes/components/status-line/presets.ts +106 -0
  563. package/src/modes/components/status-line/segments.ts +584 -0
  564. package/src/modes/components/status-line/separators.ts +55 -0
  565. package/src/modes/components/status-line/token-rate.ts +66 -0
  566. package/src/modes/components/status-line/types.ts +108 -0
  567. package/src/modes/components/theme-selector.ts +63 -0
  568. package/src/modes/components/thinking-selector.ts +52 -0
  569. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  570. package/src/modes/components/tips.txt +19 -0
  571. package/src/modes/components/todo-reminder.ts +38 -0
  572. package/src/modes/components/tool-execution.ts +1024 -0
  573. package/src/modes/components/transcript-container.ts +608 -0
  574. package/src/modes/components/tree-selector.ts +978 -0
  575. package/src/modes/components/ttsr-notification.ts +122 -0
  576. package/src/modes/components/user-message-selector.ts +227 -0
  577. package/src/modes/components/user-message.ts +66 -0
  578. package/src/modes/components/visual-truncate.ts +63 -0
  579. package/src/modes/components/welcome.ts +493 -0
  580. package/src/modes/controllers/btw-controller.ts +105 -0
  581. package/src/modes/controllers/command-controller-shared.ts +109 -0
  582. package/src/modes/controllers/command-controller.ts +1566 -0
  583. package/src/modes/controllers/event-controller.ts +1054 -0
  584. package/src/modes/controllers/extension-ui-controller.ts +886 -0
  585. package/src/modes/controllers/input-controller.ts +1073 -0
  586. package/src/modes/controllers/mcp-command-controller.ts +2017 -0
  587. package/src/modes/controllers/omfg-controller.ts +283 -0
  588. package/src/modes/controllers/omfg-rule.ts +647 -0
  589. package/src/modes/controllers/selector-controller.ts +1108 -0
  590. package/src/modes/controllers/ssh-command-controller.ts +384 -0
  591. package/src/modes/controllers/streaming-reveal.ts +279 -0
  592. package/src/modes/controllers/tan-command-controller.ts +173 -0
  593. package/src/modes/controllers/todo-command-controller.ts +485 -0
  594. package/src/modes/data/emojis.json +1 -0
  595. package/src/modes/emoji-autocomplete.ts +285 -0
  596. package/src/modes/gradient-highlight.ts +87 -0
  597. package/src/modes/image-references.ts +117 -0
  598. package/src/modes/index.ts +17 -0
  599. package/src/modes/interactive-mode.ts +3370 -0
  600. package/src/modes/internal-url-autocomplete.ts +143 -0
  601. package/src/modes/loop-limit.ts +140 -0
  602. package/src/modes/magic-keywords.ts +20 -0
  603. package/src/modes/markdown-prose.ts +247 -0
  604. package/src/modes/oauth-manual-input.ts +69 -0
  605. package/src/modes/orchestrate.ts +42 -0
  606. package/src/modes/print-mode.ts +126 -0
  607. package/src/modes/prompt-action-autocomplete.ts +260 -0
  608. package/src/modes/rpc/host-tools.ts +186 -0
  609. package/src/modes/rpc/host-uris.ts +235 -0
  610. package/src/modes/rpc/rpc-client.ts +963 -0
  611. package/src/modes/rpc/rpc-mode.ts +947 -0
  612. package/src/modes/rpc/rpc-subagents.ts +265 -0
  613. package/src/modes/rpc/rpc-types.ts +458 -0
  614. package/src/modes/runtime-init.ts +116 -0
  615. package/src/modes/session-observer-registry.ts +146 -0
  616. package/src/modes/setup-version.ts +11 -0
  617. package/src/modes/setup-wizard/index.ts +99 -0
  618. package/src/modes/setup-wizard/lazy.ts +16 -0
  619. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  620. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  621. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  622. package/src/modes/setup-wizard/scenes/sign-in.ts +205 -0
  623. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  624. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  625. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  626. package/src/modes/setup-wizard/scenes/web-search.ts +129 -0
  627. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  628. package/src/modes/shared.ts +47 -0
  629. package/src/modes/theme/dark.json +95 -0
  630. package/src/modes/theme/defaults/alabaster.json +93 -0
  631. package/src/modes/theme/defaults/amethyst.json +96 -0
  632. package/src/modes/theme/defaults/anthracite.json +93 -0
  633. package/src/modes/theme/defaults/basalt.json +91 -0
  634. package/src/modes/theme/defaults/birch.json +95 -0
  635. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  636. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  637. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  638. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  639. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  640. package/src/modes/theme/defaults/dark-copper.json +95 -0
  641. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  642. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  643. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  644. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  645. package/src/modes/theme/defaults/dark-ember.json +95 -0
  646. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  647. package/src/modes/theme/defaults/dark-forest.json +96 -0
  648. package/src/modes/theme/defaults/dark-github.json +105 -0
  649. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  650. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  651. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  652. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  653. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  654. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  655. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  656. package/src/modes/theme/defaults/dark-nord.json +97 -0
  657. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  658. package/src/modes/theme/defaults/dark-one.json +100 -0
  659. package/src/modes/theme/defaults/dark-poimandres.json +142 -0
  660. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  661. package/src/modes/theme/defaults/dark-reef.json +91 -0
  662. package/src/modes/theme/defaults/dark-retro.json +92 -0
  663. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  664. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  665. package/src/modes/theme/defaults/dark-slate.json +95 -0
  666. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  667. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  668. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  669. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  670. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  671. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  672. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  673. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  674. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  675. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  676. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  677. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  678. package/src/modes/theme/defaults/graphite.json +92 -0
  679. package/src/modes/theme/defaults/index.ts +199 -0
  680. package/src/modes/theme/defaults/light-arctic.json +107 -0
  681. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  682. package/src/modes/theme/defaults/light-canyon.json +91 -0
  683. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  684. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  685. package/src/modes/theme/defaults/light-coral.json +95 -0
  686. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  687. package/src/modes/theme/defaults/light-dawn.json +90 -0
  688. package/src/modes/theme/defaults/light-dunes.json +91 -0
  689. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  690. package/src/modes/theme/defaults/light-forest.json +100 -0
  691. package/src/modes/theme/defaults/light-frost.json +95 -0
  692. package/src/modes/theme/defaults/light-github.json +115 -0
  693. package/src/modes/theme/defaults/light-glacier.json +91 -0
  694. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  695. package/src/modes/theme/defaults/light-haze.json +90 -0
  696. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  697. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  698. package/src/modes/theme/defaults/light-lavender.json +95 -0
  699. package/src/modes/theme/defaults/light-meadow.json +91 -0
  700. package/src/modes/theme/defaults/light-mint.json +95 -0
  701. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  702. package/src/modes/theme/defaults/light-ocean.json +99 -0
  703. package/src/modes/theme/defaults/light-one.json +99 -0
  704. package/src/modes/theme/defaults/light-opal.json +91 -0
  705. package/src/modes/theme/defaults/light-orchard.json +91 -0
  706. package/src/modes/theme/defaults/light-paper.json +95 -0
  707. package/src/modes/theme/defaults/light-poimandres.json +142 -0
  708. package/src/modes/theme/defaults/light-prism.json +90 -0
  709. package/src/modes/theme/defaults/light-retro.json +98 -0
  710. package/src/modes/theme/defaults/light-sand.json +95 -0
  711. package/src/modes/theme/defaults/light-savanna.json +91 -0
  712. package/src/modes/theme/defaults/light-solarized.json +102 -0
  713. package/src/modes/theme/defaults/light-soleil.json +90 -0
  714. package/src/modes/theme/defaults/light-sunset.json +99 -0
  715. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  716. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  717. package/src/modes/theme/defaults/light-wetland.json +91 -0
  718. package/src/modes/theme/defaults/light-zenith.json +89 -0
  719. package/src/modes/theme/defaults/limestone.json +94 -0
  720. package/src/modes/theme/defaults/mahogany.json +97 -0
  721. package/src/modes/theme/defaults/marble.json +93 -0
  722. package/src/modes/theme/defaults/obsidian.json +91 -0
  723. package/src/modes/theme/defaults/onyx.json +91 -0
  724. package/src/modes/theme/defaults/pearl.json +93 -0
  725. package/src/modes/theme/defaults/porcelain.json +91 -0
  726. package/src/modes/theme/defaults/quartz.json +96 -0
  727. package/src/modes/theme/defaults/sandstone.json +95 -0
  728. package/src/modes/theme/defaults/titanium.json +90 -0
  729. package/src/modes/theme/light.json +93 -0
  730. package/src/modes/theme/mermaid-cache.ts +29 -0
  731. package/src/modes/theme/shimmer.ts +235 -0
  732. package/src/modes/theme/theme-schema.json +459 -0
  733. package/src/modes/theme/theme.ts +2676 -0
  734. package/src/modes/turn-budget.ts +31 -0
  735. package/src/modes/types.ts +359 -0
  736. package/src/modes/ultrathink.ts +41 -0
  737. package/src/modes/utils/context-usage.ts +339 -0
  738. package/src/modes/utils/copy-targets.ts +360 -0
  739. package/src/modes/utils/hotkeys-markdown.ts +61 -0
  740. package/src/modes/utils/keybinding-matchers.ts +51 -0
  741. package/src/modes/utils/tools-markdown.ts +27 -0
  742. package/src/modes/utils/ui-helpers.ts +801 -0
  743. package/src/modes/workflow.ts +42 -0
  744. package/src/plan-mode/approved-plan.ts +186 -0
  745. package/src/plan-mode/plan-handoff.ts +37 -0
  746. package/src/plan-mode/plan-protection.ts +31 -0
  747. package/src/plan-mode/state.ts +6 -0
  748. package/src/priority.json +41 -0
  749. package/src/prompts/agents/designer.md +66 -0
  750. package/src/prompts/agents/explore.md +58 -0
  751. package/src/prompts/agents/frontmatter.md +11 -0
  752. package/src/prompts/agents/init.md +33 -0
  753. package/src/prompts/agents/librarian.md +119 -0
  754. package/src/prompts/agents/oracle.md +55 -0
  755. package/src/prompts/agents/plan.md +48 -0
  756. package/src/prompts/agents/reviewer.md +140 -0
  757. package/src/prompts/agents/task.md +16 -0
  758. package/src/prompts/ci-green-request.md +36 -0
  759. package/src/prompts/dry-balance-bench.md +8 -0
  760. package/src/prompts/goals/goal-budget-limit.md +16 -0
  761. package/src/prompts/goals/goal-continuation.md +28 -0
  762. package/src/prompts/goals/goal-mode-active.md +23 -0
  763. package/src/prompts/memories/consolidation.md +30 -0
  764. package/src/prompts/memories/read-path.md +11 -0
  765. package/src/prompts/memories/stage_one_input.md +6 -0
  766. package/src/prompts/memories/stage_one_system.md +21 -0
  767. package/src/prompts/review-custom-request.md +22 -0
  768. package/src/prompts/review-headless-request.md +16 -0
  769. package/src/prompts/review-request.md +69 -0
  770. package/src/prompts/steering/user-interjection.md +10 -0
  771. package/src/prompts/system/agent-creation-architect.md +50 -0
  772. package/src/prompts/system/agent-creation-user.md +6 -0
  773. package/src/prompts/system/auto-continue.md +1 -0
  774. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  775. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  776. package/src/prompts/system/background-tan-dispatch.md +8 -0
  777. package/src/prompts/system/btw-user.md +8 -0
  778. package/src/prompts/system/commit-message-system.md +14 -0
  779. package/src/prompts/system/custom-system-prompt.md +64 -0
  780. package/src/prompts/system/eager-todo.md +13 -0
  781. package/src/prompts/system/empty-stop-retry.md +6 -0
  782. package/src/prompts/system/irc-incoming.md +7 -0
  783. package/src/prompts/system/manual-continue.md +7 -0
  784. package/src/prompts/system/memory-consolidation-system.md +8 -0
  785. package/src/prompts/system/memory-extraction-system.md +26 -0
  786. package/src/prompts/system/omfg-user.md +50 -0
  787. package/src/prompts/system/orchestrate-notice.md +40 -0
  788. package/src/prompts/system/plan-mode-active.md +109 -0
  789. package/src/prompts/system/plan-mode-approved.md +25 -0
  790. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  791. package/src/prompts/system/plan-mode-reference.md +11 -0
  792. package/src/prompts/system/plan-mode-subagent.md +33 -0
  793. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  794. package/src/prompts/system/project-prompt.md +52 -0
  795. package/src/prompts/system/subagent-system-prompt.md +64 -0
  796. package/src/prompts/system/subagent-user-prompt.md +3 -0
  797. package/src/prompts/system/subagent-yield-reminder.md +12 -0
  798. package/src/prompts/system/system-prompt.md +258 -0
  799. package/src/prompts/system/tiny-title-system.md +8 -0
  800. package/src/prompts/system/title-system.md +16 -0
  801. package/src/prompts/system/ttsr-interrupt.md +7 -0
  802. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  803. package/src/prompts/system/ultrathink-notice.md +3 -0
  804. package/src/prompts/system/web-search.md +25 -0
  805. package/src/prompts/system/workflow-notice.md +70 -0
  806. package/src/prompts/tools/apply-patch.md +65 -0
  807. package/src/prompts/tools/ask.md +30 -0
  808. package/src/prompts/tools/ast-edit.md +39 -0
  809. package/src/prompts/tools/ast-grep.md +42 -0
  810. package/src/prompts/tools/async-result.md +8 -0
  811. package/src/prompts/tools/bash.md +46 -0
  812. package/src/prompts/tools/browser.md +73 -0
  813. package/src/prompts/tools/checkpoint.md +16 -0
  814. package/src/prompts/tools/debug.md +34 -0
  815. package/src/prompts/tools/eval.md +92 -0
  816. package/src/prompts/tools/find.md +36 -0
  817. package/src/prompts/tools/github.md +21 -0
  818. package/src/prompts/tools/goal.md +18 -0
  819. package/src/prompts/tools/image-gen.md +7 -0
  820. package/src/prompts/tools/inspect-image-system.md +20 -0
  821. package/src/prompts/tools/inspect-image.md +32 -0
  822. package/src/prompts/tools/irc.md +59 -0
  823. package/src/prompts/tools/job.md +19 -0
  824. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  825. package/src/prompts/tools/lsp.md +42 -0
  826. package/src/prompts/tools/memory-edit.md +8 -0
  827. package/src/prompts/tools/patch.md +70 -0
  828. package/src/prompts/tools/read.md +84 -0
  829. package/src/prompts/tools/recall.md +5 -0
  830. package/src/prompts/tools/reflect.md +5 -0
  831. package/src/prompts/tools/render-mermaid.md +9 -0
  832. package/src/prompts/tools/replace.md +30 -0
  833. package/src/prompts/tools/resolve.md +9 -0
  834. package/src/prompts/tools/retain.md +6 -0
  835. package/src/prompts/tools/rewind.md +13 -0
  836. package/src/prompts/tools/search-tool-bm25.md +32 -0
  837. package/src/prompts/tools/search.md +24 -0
  838. package/src/prompts/tools/ssh.md +31 -0
  839. package/src/prompts/tools/task-summary.md +17 -0
  840. package/src/prompts/tools/task.md +88 -0
  841. package/src/prompts/tools/todo.md +62 -0
  842. package/src/prompts/tools/web-search.md +10 -0
  843. package/src/prompts/tools/write.md +14 -0
  844. package/src/registry/agent-lifecycle.ts +218 -0
  845. package/src/registry/agent-registry.ts +151 -0
  846. package/src/sdk.ts +2558 -0
  847. package/src/secrets/index.ts +123 -0
  848. package/src/secrets/obfuscator.ts +298 -0
  849. package/src/secrets/regex.ts +21 -0
  850. package/src/session/agent-session.ts +10121 -0
  851. package/src/session/agent-storage.ts +455 -0
  852. package/src/session/artifacts.ts +135 -0
  853. package/src/session/auth-broker-config.ts +131 -0
  854. package/src/session/auth-storage.ts +29 -0
  855. package/src/session/blob-store.ts +255 -0
  856. package/src/session/client-bridge.ts +85 -0
  857. package/src/session/history-storage.ts +348 -0
  858. package/src/session/indexed-session-storage.ts +430 -0
  859. package/src/session/messages.ts +541 -0
  860. package/src/session/redis-session-storage.ts +170 -0
  861. package/src/session/session-dump-format.ts +209 -0
  862. package/src/session/session-history-format.ts +246 -0
  863. package/src/session/session-manager.ts +3676 -0
  864. package/src/session/session-storage.ts +529 -0
  865. package/src/session/shake-types.ts +43 -0
  866. package/src/session/sql-session-storage.ts +314 -0
  867. package/src/session/streaming-output.ts +1330 -0
  868. package/src/session/tool-choice-queue.ts +213 -0
  869. package/src/session/yield-queue.ts +173 -0
  870. package/src/slash-commands/acp-builtins.ts +70 -0
  871. package/src/slash-commands/builtin-registry.ts +1798 -0
  872. package/src/slash-commands/helpers/context-report.ts +39 -0
  873. package/src/slash-commands/helpers/format.ts +46 -0
  874. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  875. package/src/slash-commands/helpers/mcp.ts +532 -0
  876. package/src/slash-commands/helpers/parse.ts +85 -0
  877. package/src/slash-commands/helpers/ssh.ts +195 -0
  878. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  879. package/src/slash-commands/helpers/todo.ts +279 -0
  880. package/src/slash-commands/helpers/usage-report.ts +95 -0
  881. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  882. package/src/slash-commands/types.ts +135 -0
  883. package/src/ssh/config-writer.ts +183 -0
  884. package/src/ssh/connection-manager.ts +509 -0
  885. package/src/ssh/ssh-executor.ts +189 -0
  886. package/src/ssh/sshfs-mount.ts +140 -0
  887. package/src/ssh/utils.ts +8 -0
  888. package/src/stt/downloader.ts +71 -0
  889. package/src/stt/index.ts +3 -0
  890. package/src/stt/recorder.ts +351 -0
  891. package/src/stt/setup.ts +52 -0
  892. package/src/stt/stt-controller.ts +160 -0
  893. package/src/stt/transcribe.py +70 -0
  894. package/src/stt/transcriber.ts +91 -0
  895. package/src/stubs/natives/index.ts +814 -0
  896. package/src/stubs/natives/package.json +7 -0
  897. package/src/stubs/tui/index.ts +282 -0
  898. package/src/stubs/tui/package.json +7 -0
  899. package/src/system-prompt.ts +611 -0
  900. package/src/task/agents.ts +167 -0
  901. package/src/task/commands.ts +132 -0
  902. package/src/task/discovery.ts +122 -0
  903. package/src/task/executor.ts +2133 -0
  904. package/src/task/index.ts +1419 -0
  905. package/src/task/name-generator.ts +1577 -0
  906. package/src/task/omp-command.ts +26 -0
  907. package/src/task/output-manager.ts +88 -0
  908. package/src/task/parallel.ts +116 -0
  909. package/src/task/render.ts +1381 -0
  910. package/src/task/repair-args.ts +129 -0
  911. package/src/task/subprocess-tool-registry.ts +88 -0
  912. package/src/task/types.ts +336 -0
  913. package/src/task/worktree.ts +514 -0
  914. package/src/telemetry-export.ts +144 -0
  915. package/src/thinking.ts +167 -0
  916. package/src/tiny/compiled-runtime.ts +179 -0
  917. package/src/tiny/device.ts +111 -0
  918. package/src/tiny/dtype.ts +101 -0
  919. package/src/tiny/models.ts +242 -0
  920. package/src/tiny/text.ts +165 -0
  921. package/src/tiny/title-client.ts +543 -0
  922. package/src/tiny/title-protocol.ts +56 -0
  923. package/src/tiny/worker.ts +568 -0
  924. package/src/tool-discovery/mode.ts +24 -0
  925. package/src/tool-discovery/tool-index.ts +256 -0
  926. package/src/tools/approval.ts +189 -0
  927. package/src/tools/archive-reader.ts +721 -0
  928. package/src/tools/ask.ts +928 -0
  929. package/src/tools/ast-edit.ts +642 -0
  930. package/src/tools/ast-grep.ts +452 -0
  931. package/src/tools/auto-generated-guard.ts +322 -0
  932. package/src/tools/bash-command-fixup.ts +37 -0
  933. package/src/tools/bash-interactive.ts +408 -0
  934. package/src/tools/bash-interceptor.ts +67 -0
  935. package/src/tools/bash-pty-selection.ts +14 -0
  936. package/src/tools/bash-skill-urls.ts +248 -0
  937. package/src/tools/bash.ts +1386 -0
  938. package/src/tools/browser/attach.ts +175 -0
  939. package/src/tools/browser/launch.ts +660 -0
  940. package/src/tools/browser/readable.ts +112 -0
  941. package/src/tools/browser/registry.ts +197 -0
  942. package/src/tools/browser/render.ts +216 -0
  943. package/src/tools/browser/tab-protocol.ts +105 -0
  944. package/src/tools/browser/tab-supervisor.ts +628 -0
  945. package/src/tools/browser/tab-worker-entry.ts +21 -0
  946. package/src/tools/browser/tab-worker.ts +1226 -0
  947. package/src/tools/browser.ts +343 -0
  948. package/src/tools/checkpoint.ts +136 -0
  949. package/src/tools/conflict-detect.ts +718 -0
  950. package/src/tools/context.ts +39 -0
  951. package/src/tools/debug.ts +1067 -0
  952. package/src/tools/eval-backends.ts +27 -0
  953. package/src/tools/eval-render.ts +752 -0
  954. package/src/tools/eval.ts +577 -0
  955. package/src/tools/fetch.ts +1926 -0
  956. package/src/tools/file-recorder.ts +35 -0
  957. package/src/tools/find.ts +609 -0
  958. package/src/tools/fs-cache-invalidation.ts +28 -0
  959. package/src/tools/gh-cache-invalidation.ts +255 -0
  960. package/src/tools/gh-format.ts +12 -0
  961. package/src/tools/gh-renderer.ts +481 -0
  962. package/src/tools/gh.ts +3720 -0
  963. package/src/tools/github-cache.ts +637 -0
  964. package/src/tools/grouped-file-output.ts +210 -0
  965. package/src/tools/image-gen.ts +1517 -0
  966. package/src/tools/index.ts +599 -0
  967. package/src/tools/inspect-image-renderer.ts +132 -0
  968. package/src/tools/inspect-image.ts +174 -0
  969. package/src/tools/irc.ts +723 -0
  970. package/src/tools/job.ts +557 -0
  971. package/src/tools/json-tree.ts +243 -0
  972. package/src/tools/jtd-to-json-schema.ts +219 -0
  973. package/src/tools/jtd-to-typescript.ts +136 -0
  974. package/src/tools/jtd-utils.ts +102 -0
  975. package/src/tools/list-limit.ts +40 -0
  976. package/src/tools/match-line-format.ts +20 -0
  977. package/src/tools/memory-edit.ts +59 -0
  978. package/src/tools/memory-recall.ts +100 -0
  979. package/src/tools/memory-reflect.ts +88 -0
  980. package/src/tools/memory-render.ts +202 -0
  981. package/src/tools/memory-retain.ts +91 -0
  982. package/src/tools/output-meta.ts +754 -0
  983. package/src/tools/output-schema-validator.ts +132 -0
  984. package/src/tools/path-utils.ts +1054 -0
  985. package/src/tools/plan-mode-guard.ts +108 -0
  986. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  987. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  988. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  989. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  990. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  991. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  992. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  993. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  994. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  995. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  996. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  997. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  998. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  999. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  1000. package/src/tools/read.ts +2929 -0
  1001. package/src/tools/render-mermaid.ts +69 -0
  1002. package/src/tools/render-utils.ts +838 -0
  1003. package/src/tools/renderers.ts +77 -0
  1004. package/src/tools/report-tool-issue.ts +534 -0
  1005. package/src/tools/resolve.ts +276 -0
  1006. package/src/tools/review.ts +253 -0
  1007. package/src/tools/search-tool-bm25.ts +351 -0
  1008. package/src/tools/search.ts +1580 -0
  1009. package/src/tools/sqlite-reader.ts +828 -0
  1010. package/src/tools/ssh.ts +349 -0
  1011. package/src/tools/todo.ts +982 -0
  1012. package/src/tools/tool-errors.ts +62 -0
  1013. package/src/tools/tool-result.ts +94 -0
  1014. package/src/tools/tool-timeouts.ts +30 -0
  1015. package/src/tools/tts.ts +133 -0
  1016. package/src/tools/write.ts +1217 -0
  1017. package/src/tools/yield.ts +269 -0
  1018. package/src/tui/code-cell.ts +216 -0
  1019. package/src/tui/file-list.ts +55 -0
  1020. package/src/tui/hyperlink.ts +175 -0
  1021. package/src/tui/index.ts +12 -0
  1022. package/src/tui/output-block.ts +240 -0
  1023. package/src/tui/status-line.ts +54 -0
  1024. package/src/tui/tree-list.ts +84 -0
  1025. package/src/tui/types.ts +15 -0
  1026. package/src/tui/utils.ts +103 -0
  1027. package/src/utils/block-context.ts +312 -0
  1028. package/src/utils/changelog.ts +132 -0
  1029. package/src/utils/clipboard.ts +193 -0
  1030. package/src/utils/command-args.ts +76 -0
  1031. package/src/utils/commit-message-generator.ts +151 -0
  1032. package/src/utils/edit-mode.ts +41 -0
  1033. package/src/utils/enhanced-paste.ts +230 -0
  1034. package/src/utils/event-bus.ts +33 -0
  1035. package/src/utils/external-editor.ts +65 -0
  1036. package/src/utils/file-display-mode.ts +45 -0
  1037. package/src/utils/file-mentions.ts +281 -0
  1038. package/src/utils/git.ts +1833 -0
  1039. package/src/utils/image-loading.ts +132 -0
  1040. package/src/utils/image-resize.ts +309 -0
  1041. package/src/utils/jj.ts +248 -0
  1042. package/src/utils/lang-from-path.ts +239 -0
  1043. package/src/utils/markit.ts +89 -0
  1044. package/src/utils/open.ts +55 -0
  1045. package/src/utils/session-color.ts +68 -0
  1046. package/src/utils/shell-snapshot.ts +187 -0
  1047. package/src/utils/sixel.ts +69 -0
  1048. package/src/utils/title-generator.ts +373 -0
  1049. package/src/utils/tool-choice.ts +33 -0
  1050. package/src/utils/tools-manager.ts +363 -0
  1051. package/src/web/kagi.ts +305 -0
  1052. package/src/web/parallel.ts +353 -0
  1053. package/src/web/scrapers/artifacthub.ts +207 -0
  1054. package/src/web/scrapers/arxiv.ts +83 -0
  1055. package/src/web/scrapers/aur.ts +162 -0
  1056. package/src/web/scrapers/biorxiv.ts +133 -0
  1057. package/src/web/scrapers/bluesky.ts +262 -0
  1058. package/src/web/scrapers/brew.ts +172 -0
  1059. package/src/web/scrapers/cheatsh.ts +68 -0
  1060. package/src/web/scrapers/chocolatey.ts +196 -0
  1061. package/src/web/scrapers/choosealicense.ts +95 -0
  1062. package/src/web/scrapers/cisa-kev.ts +87 -0
  1063. package/src/web/scrapers/clojars.ts +154 -0
  1064. package/src/web/scrapers/coingecko.ts +177 -0
  1065. package/src/web/scrapers/crates-io.ts +97 -0
  1066. package/src/web/scrapers/crossref.ts +136 -0
  1067. package/src/web/scrapers/devto.ts +147 -0
  1068. package/src/web/scrapers/discogs.ts +306 -0
  1069. package/src/web/scrapers/discourse.ts +197 -0
  1070. package/src/web/scrapers/dockerhub.ts +138 -0
  1071. package/src/web/scrapers/docs-rs.ts +653 -0
  1072. package/src/web/scrapers/fdroid.ts +134 -0
  1073. package/src/web/scrapers/firefox-addons.ts +191 -0
  1074. package/src/web/scrapers/flathub.ts +223 -0
  1075. package/src/web/scrapers/github-gist.ts +58 -0
  1076. package/src/web/scrapers/github.ts +704 -0
  1077. package/src/web/scrapers/gitlab.ts +401 -0
  1078. package/src/web/scrapers/go-pkg.ts +266 -0
  1079. package/src/web/scrapers/hackage.ts +140 -0
  1080. package/src/web/scrapers/hackernews.ts +189 -0
  1081. package/src/web/scrapers/hex.ts +105 -0
  1082. package/src/web/scrapers/huggingface.ts +321 -0
  1083. package/src/web/scrapers/iacr.ts +89 -0
  1084. package/src/web/scrapers/index.ts +252 -0
  1085. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1086. package/src/web/scrapers/lemmy.ts +203 -0
  1087. package/src/web/scrapers/lobsters.ts +175 -0
  1088. package/src/web/scrapers/mastodon.ts +292 -0
  1089. package/src/web/scrapers/maven.ts +138 -0
  1090. package/src/web/scrapers/mdn.ts +173 -0
  1091. package/src/web/scrapers/metacpan.ts +222 -0
  1092. package/src/web/scrapers/musicbrainz.ts +250 -0
  1093. package/src/web/scrapers/npm.ts +98 -0
  1094. package/src/web/scrapers/nuget.ts +183 -0
  1095. package/src/web/scrapers/nvd.ts +222 -0
  1096. package/src/web/scrapers/ollama.ts +239 -0
  1097. package/src/web/scrapers/open-vsx.ts +106 -0
  1098. package/src/web/scrapers/opencorporates.ts +292 -0
  1099. package/src/web/scrapers/openlibrary.ts +336 -0
  1100. package/src/web/scrapers/orcid.ts +286 -0
  1101. package/src/web/scrapers/osv.ts +176 -0
  1102. package/src/web/scrapers/packagist.ts +160 -0
  1103. package/src/web/scrapers/pub-dev.ts +143 -0
  1104. package/src/web/scrapers/pubmed.ts +211 -0
  1105. package/src/web/scrapers/pypi.ts +112 -0
  1106. package/src/web/scrapers/rawg.ts +110 -0
  1107. package/src/web/scrapers/readthedocs.ts +120 -0
  1108. package/src/web/scrapers/reddit.ts +95 -0
  1109. package/src/web/scrapers/repology.ts +251 -0
  1110. package/src/web/scrapers/rfc.ts +201 -0
  1111. package/src/web/scrapers/rubygems.ts +103 -0
  1112. package/src/web/scrapers/searchcode.ts +189 -0
  1113. package/src/web/scrapers/sec-edgar.ts +261 -0
  1114. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1115. package/src/web/scrapers/snapcraft.ts +187 -0
  1116. package/src/web/scrapers/sourcegraph.ts +336 -0
  1117. package/src/web/scrapers/spdx.ts +108 -0
  1118. package/src/web/scrapers/spotify.ts +198 -0
  1119. package/src/web/scrapers/stackoverflow.ts +120 -0
  1120. package/src/web/scrapers/terraform.ts +277 -0
  1121. package/src/web/scrapers/tldr.ts +47 -0
  1122. package/src/web/scrapers/twitter.ts +94 -0
  1123. package/src/web/scrapers/types.ts +397 -0
  1124. package/src/web/scrapers/utils.ts +109 -0
  1125. package/src/web/scrapers/vimeo.ts +133 -0
  1126. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1127. package/src/web/scrapers/w3c.ts +156 -0
  1128. package/src/web/scrapers/wikidata.ts +344 -0
  1129. package/src/web/scrapers/wikipedia.ts +84 -0
  1130. package/src/web/scrapers/youtube.ts +325 -0
  1131. package/src/web/search/index.ts +292 -0
  1132. package/src/web/search/provider.ts +157 -0
  1133. package/src/web/search/providers/anthropic.ts +318 -0
  1134. package/src/web/search/providers/base.ts +89 -0
  1135. package/src/web/search/providers/brave.ts +152 -0
  1136. package/src/web/search/providers/codex.ts +591 -0
  1137. package/src/web/search/providers/exa.ts +400 -0
  1138. package/src/web/search/providers/gemini.ts +460 -0
  1139. package/src/web/search/providers/jina.ts +111 -0
  1140. package/src/web/search/providers/kagi.ts +86 -0
  1141. package/src/web/search/providers/kimi.ts +196 -0
  1142. package/src/web/search/providers/parallel.ts +225 -0
  1143. package/src/web/search/providers/perplexity.ts +730 -0
  1144. package/src/web/search/providers/searxng.ts +313 -0
  1145. package/src/web/search/providers/synthetic.ts +114 -0
  1146. package/src/web/search/providers/tavily.ts +176 -0
  1147. package/src/web/search/providers/utils.ts +128 -0
  1148. package/src/web/search/providers/zai.ts +333 -0
  1149. package/src/web/search/render.ts +262 -0
  1150. package/src/web/search/types.ts +482 -0
  1151. package/src/web/search/utils.ts +17 -0
  1152. package/src/workspace-tree.ts +286 -0
@@ -0,0 +1,1926 @@
1
+ import { Database } from "bun:sqlite";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
6
+ import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
+ import { htmlToMarkdown } from './stubs/natives/index.ts';
8
+ import { type Component, Text } from './stubs/tui/index.ts';
9
+ import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
10
+ import { LRUCache } from "lru-cache/raw";
11
+ import type { Settings } from "../config/settings";
12
+ import { readEditableNotebookText } from "../edit/notebook";
13
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
14
+ import { type Theme, theme } from "../modes/theme/theme";
15
+ import type { ToolSession } from "../sdk";
16
+ import type { AgentStorage } from "../session/agent-storage";
17
+ import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
18
+ import { renderStatusLine, urlHyperlink } from "../tui";
19
+ import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
20
+ import { formatDimensionNote, resizeImage } from "../utils/image-resize";
21
+ import { ensureTool } from "../utils/tools-manager";
22
+ import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
23
+ import { specialHandlers } from "../web/scrapers";
24
+ import type { RenderResult } from "../web/scrapers/types";
25
+ import { finalizeOutput, loadPage, looksLikeHtml, MAX_BYTES, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
26
+ import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
27
+ import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "./archive-reader";
28
+ import { applyListLimit } from "./list-limit";
29
+ import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
30
+ import { type LineRange, parseLineRanges } from "./path-utils";
31
+ import { formatBytes, formatExpandHint, getDomain, replaceTabs } from "./render-utils";
32
+ import { listTables, looksLikeSqlite, renderTableList } from "./sqlite-reader";
33
+ import { ToolAbortError, ToolError } from "./tool-errors";
34
+ import { toolResult } from "./tool-result";
35
+ import { clampTimeout } from "./tool-timeouts";
36
+
37
+ // =============================================================================
38
+ // Types and Constants
39
+ // =============================================================================
40
+
41
+ const FETCH_DEFAULT_MAX_LINES = 300;
42
+ // Convertible document types handled by markit.
43
+ const CONVERTIBLE_MIMES = new Set([
44
+ "application/pdf",
45
+ "application/msword",
46
+ "application/vnd.ms-powerpoint",
47
+ "application/vnd.ms-excel",
48
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
49
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
50
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
51
+ "application/rtf",
52
+ "application/epub+zip",
53
+ "image/png",
54
+ "image/jpeg",
55
+ "image/gif",
56
+ "image/webp",
57
+ "audio/mpeg",
58
+ "audio/wav",
59
+ "audio/ogg",
60
+ ]);
61
+
62
+ const CONVERTIBLE_EXTENSIONS = new Set([
63
+ ".pdf",
64
+ ".doc",
65
+ ".docx",
66
+ ".ppt",
67
+ ".pptx",
68
+ ".xls",
69
+ ".xlsx",
70
+ ".rtf",
71
+ ".epub",
72
+ ".png",
73
+ ".jpg",
74
+ ".jpeg",
75
+ ".gif",
76
+ ".webp",
77
+ ".mp3",
78
+ ".wav",
79
+ ".ogg",
80
+ ]);
81
+
82
+ const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]);
83
+ const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]);
84
+
85
+ const SQLITE_MIMES = new Set([
86
+ "application/vnd.sqlite3",
87
+ "application/x-sqlite3",
88
+ "application/sqlite3",
89
+ "application/sqlite",
90
+ ]);
91
+ const SQLITE_EXTENSIONS = new Set([".sqlite", ".sqlite3", ".db", ".db3"]);
92
+
93
+ const ARCHIVE_MIMES = new Set([
94
+ "application/zip",
95
+ "application/x-zip-compressed",
96
+ "application/x-tar",
97
+ "application/tar",
98
+ "application/gzip",
99
+ "application/x-gzip",
100
+ ]);
101
+ const ARCHIVE_EXTENSIONS = new Set([".zip", ".tar", ".tar.gz", ".tgz", ".gz"]);
102
+
103
+ const IMAGE_MIME_BY_EXTENSION = new Map<string, string>([
104
+ [".png", "image/png"],
105
+ [".jpg", "image/jpeg"],
106
+ [".jpeg", "image/jpeg"],
107
+ [".gif", "image/gif"],
108
+ [".webp", "image/webp"],
109
+ ]);
110
+ const SUPPORTED_INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
111
+ const MAX_INLINE_IMAGE_SOURCE_BYTES = 20 * 1024 * 1024;
112
+ const MAX_INLINE_IMAGE_OUTPUT_BYTES = 300 * 1024;
113
+
114
+ // =============================================================================
115
+ // Utilities
116
+ // =============================================================================
117
+
118
+ /**
119
+ * Check if a command exists (cross-platform)
120
+ */
121
+ function hasCommand(cmd: string): boolean {
122
+ return Boolean($which(cmd));
123
+ }
124
+
125
+ /**
126
+ * Build llms.txt candidates scoped to the requested URL
127
+ */
128
+ function buildLlmEndpointCandidates(url: string): string[] {
129
+ try {
130
+ const parsed = new URL(url);
131
+ if (parsed.pathname === "/") {
132
+ return [`${parsed.origin}/.well-known/llms.txt`, `${parsed.origin}/llms.txt`, `${parsed.origin}/llms.md`];
133
+ }
134
+
135
+ const trimmedPath = parsed.pathname.replace(/\/+$/, "");
136
+ const segments = trimmedPath.split("/").filter(Boolean);
137
+ const scopeDepth = parsed.pathname.endsWith("/") ? segments.length : Math.max(segments.length - 1, 1);
138
+ const endpoints: string[] = [];
139
+
140
+ for (let depth = scopeDepth; depth >= 1; depth--) {
141
+ const scope = `/${segments.slice(0, depth).join("/")}/`;
142
+ endpoints.push(`${parsed.origin}${scope}llms.txt`, `${parsed.origin}${scope}llms.md`);
143
+ }
144
+
145
+ return endpoints;
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Repair a URL whose scheme `//` collapsed to a single `/`. Node's `path.normalize`/
153
+ * `path.resolve` collapse `//` → `/`, so any URL routed through path normalization arrives
154
+ * as `https:/host/x` instead of `https://host/x`. No local filesystem path begins with
155
+ * `http:/` or `https:/`, so repairing the scheme back to `//` is unambiguous.
156
+ */
157
+ function repairCollapsedScheme(value: string): string {
158
+ const m = value.match(/^(https?):\/(?!\/)/i);
159
+ return m ? `${m[1]}://${value.slice(m[0].length)}` : value;
160
+ }
161
+
162
+ /**
163
+ * Normalize URL (repair a collapsed scheme, then add a scheme if one is missing).
164
+ */
165
+ function normalizeUrl(url: string): string {
166
+ url = repairCollapsedScheme(url);
167
+ if (!url.match(/^https?:\/\//i)) {
168
+ return `https://${url}`;
169
+ }
170
+ return url;
171
+ }
172
+
173
+ export function isReadableUrlPath(value: string): boolean {
174
+ return /^https?:\/\/?/i.test(value) || /^www\./i.test(value);
175
+ }
176
+
177
+ // URL line selectors mirror the file form: `:50`, `:50-100`, `:50+150`, `:5-10,20-30`, `:raw`,
178
+ // or `:raw:N-M` / `:N-M:raw` to combine raw mode with a range. If a URL would otherwise look
179
+ // like `host:port`, add a trailing slash before the selector (e.g. `https://example.com/:80`
180
+ // to read line 80 of the document at `https://example.com/`).
181
+
182
+ export interface ParsedReadUrlTarget {
183
+ path: string;
184
+ raw: boolean;
185
+ offset?: number;
186
+ limit?: number;
187
+ /** Populated only when the selector carries 2+ ranges. Single-range stays on offset/limit. */
188
+ ranges?: readonly LineRange[];
189
+ }
190
+
191
+ /** Recognize a single selector token (`raw` or one/many line ranges). */
192
+ function isUrlSelectorToken(token: string): boolean {
193
+ if (token.toLowerCase() === "raw") return true;
194
+ try {
195
+ return parseLineRanges(token) !== null;
196
+ } catch {
197
+ // `parseLineRanges` throws `ToolError` for malformed ranges (e.g. `5+0`). Only treat the
198
+ // token as a selector when it parses cleanly so URL ports like `:80` keep flowing
199
+ // through to the URL path.
200
+ return false;
201
+ }
202
+ }
203
+
204
+ export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null {
205
+ const repaired = repairCollapsedScheme(readPath);
206
+ const embedded = tryExtractEmbeddedUrlSelector(repaired);
207
+ const urlPath = embedded?.path ?? repaired;
208
+ if (!isReadableUrlPath(urlPath)) {
209
+ return null;
210
+ }
211
+
212
+ let raw = false;
213
+ let ranges: readonly LineRange[] | undefined;
214
+ for (const sel of embedded?.sels ?? []) {
215
+ if (sel.toLowerCase() === "raw") {
216
+ raw = true;
217
+ continue;
218
+ }
219
+ if (ranges !== undefined) {
220
+ // Two range groups on the same URL (`…:5-10:20-30`) — combine with commas instead.
221
+ throw new ToolError(
222
+ `URL selector has multiple range groups; combine them with commas (e.g. \`:5-10,20-30\`).`,
223
+ );
224
+ }
225
+ const parsed = parseLineRanges(sel);
226
+ if (parsed === null) {
227
+ // Shouldn't happen — isUrlSelectorToken vetted it. Belt-and-suspenders.
228
+ throw new ToolError(`Invalid URL line selector: ${sel}`);
229
+ }
230
+ ranges = parsed;
231
+ }
232
+
233
+ if (!ranges || ranges.length === 0) return { path: urlPath, raw };
234
+ if (ranges.length === 1) {
235
+ const r = ranges[0];
236
+ return {
237
+ path: urlPath,
238
+ raw,
239
+ offset: r.startLine,
240
+ limit: r.endLine !== undefined ? r.endLine - r.startLine + 1 : undefined,
241
+ };
242
+ }
243
+ return { path: urlPath, raw, ranges };
244
+ }
245
+
246
+ /**
247
+ * Peel one or more selector tokens off the right of a URL string. Walks back through
248
+ * trailing `:tok` segments while each token (a) looks like a selector and (b) leaves
249
+ * behind a string that still parses as a URL. Returns selectors left-to-right so callers
250
+ * can apply them in source order.
251
+ */
252
+ function tryExtractEmbeddedUrlSelector(readPath: string): { path: string; sels: string[] } | null {
253
+ let basePath = readPath;
254
+ const sels: string[] = [];
255
+ while (true) {
256
+ const lastColonIndex = basePath.lastIndexOf(":");
257
+ if (lastColonIndex <= 0) break;
258
+
259
+ const candidate = basePath.slice(lastColonIndex + 1);
260
+ const remainder = basePath.slice(0, lastColonIndex);
261
+ if (!isReadableUrlPath(remainder)) break;
262
+ if (!isUrlSelectorToken(candidate)) break;
263
+
264
+ try {
265
+ new URL(
266
+ remainder.startsWith("http://") || remainder.startsWith("https://") ? remainder : `https://${remainder}`,
267
+ );
268
+ } catch {
269
+ break;
270
+ }
271
+
272
+ sels.unshift(candidate);
273
+ basePath = remainder;
274
+ }
275
+ if (sels.length === 0) return null;
276
+ return { path: basePath, sels };
277
+ }
278
+
279
+ /**
280
+ * Normalize MIME type (lowercase, strip charset/params)
281
+ */
282
+ function normalizeMime(contentType: string): string {
283
+ return contentType.split(";")[0].trim().toLowerCase();
284
+ }
285
+
286
+ function getFilenameExtensionHint(filename: string): string {
287
+ const lower = filename.toLowerCase();
288
+ if (lower.endsWith(".tar.gz")) return ".tar.gz";
289
+ return path.extname(filename).toLowerCase();
290
+ }
291
+
292
+ /**
293
+ * Get extension from URL or Content-Disposition
294
+ */
295
+ function getExtensionHint(url: string, contentDisposition?: string): string {
296
+ // Try Content-Disposition filename first
297
+ if (contentDisposition) {
298
+ const match = contentDisposition.match(/filename[*]?=["']?([^"';\n]+)/i);
299
+ if (match) {
300
+ const ext = getFilenameExtensionHint(match[1]);
301
+ if (ext) return ext;
302
+ }
303
+ }
304
+
305
+ // Fall back to URL path
306
+ try {
307
+ const pathname = new URL(url).pathname;
308
+ const ext = getFilenameExtensionHint(pathname);
309
+ if (ext) return ext;
310
+ } catch {}
311
+
312
+ return "";
313
+ }
314
+
315
+ /**
316
+ * Check if content type is convertible via markit.
317
+ */
318
+ function isConvertible(mime: string, extensionHint: string): boolean {
319
+ if (CONVERTIBLE_MIMES.has(mime)) return true;
320
+ if (mime === "application/octet-stream" && CONVERTIBLE_EXTENSIONS.has(extensionHint)) return true;
321
+ if (CONVERTIBLE_EXTENSIONS.has(extensionHint)) return true;
322
+ return false;
323
+ }
324
+
325
+ function resolveImageMimeType(mime: string, extensionHint: string): string | null {
326
+ if (mime.startsWith("image/")) return mime;
327
+ const shouldUseExtensionHint =
328
+ mime.length === 0 || mime === "application/octet-stream" || mime === "binary/octet-stream" || mime === "unknown";
329
+ if (!shouldUseExtensionHint) return null;
330
+ return IMAGE_MIME_BY_EXTENSION.get(extensionHint) ?? null;
331
+ }
332
+
333
+ function isInlineImageMimeTypeSupported(mimeType: string): boolean {
334
+ return SUPPORTED_INLINE_IMAGE_MIME_TYPES.has(mimeType);
335
+ }
336
+
337
+ /**
338
+ * Try fetching URL with .md appended (llms.txt convention)
339
+ */
340
+ async function tryMdSuffix(url: string, timeout: number, signal?: AbortSignal): Promise<string | null> {
341
+ const candidates: string[] = [];
342
+
343
+ try {
344
+ const parsed = new URL(url);
345
+ const pathname = parsed.pathname;
346
+
347
+ if (pathname.endsWith("/")) {
348
+ // /foo/bar/ -> /foo/bar/index.html.md
349
+ candidates.push(`${parsed.origin}${pathname}index.html.md`);
350
+ } else if (pathname.includes(".")) {
351
+ // /foo/bar.html -> /foo/bar.html.md
352
+ candidates.push(`${parsed.origin}${pathname}.md`);
353
+ } else {
354
+ // /foo/bar -> /foo/bar.md
355
+ candidates.push(`${parsed.origin}${pathname}.md`);
356
+ }
357
+ } catch {
358
+ return null;
359
+ }
360
+
361
+ if (signal?.aborted) {
362
+ return null;
363
+ }
364
+
365
+ for (const candidate of candidates) {
366
+ if (signal?.aborted) {
367
+ return null;
368
+ }
369
+ const result = await loadPage(candidate, { timeout, signal });
370
+ if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
371
+ return result.content;
372
+ }
373
+ }
374
+
375
+ return null;
376
+ }
377
+
378
+ /**
379
+ * Try to fetch LLM-friendly endpoints
380
+ */
381
+ async function tryLlmEndpoints(
382
+ url: string,
383
+ timeout: number,
384
+ signal?: AbortSignal,
385
+ ): Promise<{ content: string; endpoint: string } | null> {
386
+ const endpoints = buildLlmEndpointCandidates(url);
387
+
388
+ if (signal?.aborted || endpoints.length === 0) {
389
+ return null;
390
+ }
391
+
392
+ for (const endpoint of endpoints) {
393
+ if (signal?.aborted) {
394
+ return null;
395
+ }
396
+ const result = await loadPage(endpoint, { timeout: Math.min(timeout, 5), signal });
397
+ if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
398
+ return { content: result.content, endpoint };
399
+ }
400
+ }
401
+ return null;
402
+ }
403
+
404
+ /**
405
+ * Try content negotiation for markdown/plain
406
+ */
407
+ async function tryContentNegotiation(
408
+ url: string,
409
+ timeout: number,
410
+ signal?: AbortSignal,
411
+ ): Promise<{ content: string; type: string } | null> {
412
+ if (signal?.aborted) {
413
+ return null;
414
+ }
415
+
416
+ const result = await loadPage(url, {
417
+ timeout,
418
+ headers: { Accept: "text/markdown, text/plain;q=0.9, text/html;q=0.8" },
419
+ signal,
420
+ });
421
+
422
+ if (!result.ok) return null;
423
+
424
+ const mime = normalizeMime(result.contentType);
425
+ if ((mime.includes("markdown") || mime === "text/plain") && !looksLikeHtml(result.content)) {
426
+ return { content: result.content, type: result.contentType };
427
+ }
428
+
429
+ return null;
430
+ }
431
+
432
+ /**
433
+ * Read a single HTML attribute from a tag string
434
+ */
435
+ function getHtmlAttribute(tag: string, attribute: string): string | null {
436
+ const pattern = new RegExp(`\\b${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>]+))`, "i");
437
+ const match = tag.match(pattern);
438
+ if (!match) return null;
439
+ return (match[1] ?? match[2] ?? match[3] ?? "").trim();
440
+ }
441
+
442
+ /**
443
+ * Extract bounded <head> markup to avoid expensive whole-page parsing
444
+ */
445
+ function extractHeadHtml(html: string): string {
446
+ const lower = html.toLowerCase();
447
+ const headStart = lower.indexOf("<head");
448
+ if (headStart === -1) {
449
+ return html.slice(0, 32 * 1024);
450
+ }
451
+
452
+ const headTagEnd = html.indexOf(">", headStart);
453
+ if (headTagEnd === -1) {
454
+ return html.slice(headStart, headStart + 32 * 1024);
455
+ }
456
+
457
+ const headEnd = lower.indexOf("</head>", headTagEnd + 1);
458
+ const fallbackEnd = Math.min(html.length, headTagEnd + 1 + 32 * 1024);
459
+ return html.slice(headStart, headEnd === -1 ? fallbackEnd : headEnd + 7);
460
+ }
461
+
462
+ /**
463
+ * Parse alternate links from HTML head
464
+ */
465
+ function parseAlternateLinks(html: string, pageUrl: string): string[] {
466
+ const links: string[] = [];
467
+
468
+ try {
469
+ const pagePath = new URL(pageUrl).pathname;
470
+ const headHtml = extractHeadHtml(html);
471
+ const linkTags = headHtml.match(/<link\b[^>]*>/gi) ?? [];
472
+
473
+ for (const tag of linkTags) {
474
+ const rel = getHtmlAttribute(tag, "rel")?.toLowerCase() ?? "";
475
+ const relTokens = rel.split(/\s+/).filter(Boolean);
476
+ if (!relTokens.includes("alternate")) continue;
477
+
478
+ const href = getHtmlAttribute(tag, "href");
479
+ const type = getHtmlAttribute(tag, "type")?.toLowerCase() ?? "";
480
+ if (!href) continue;
481
+
482
+ // Skip site-wide feeds
483
+ if (
484
+ href.includes("RecentChanges") ||
485
+ href.includes("Special:") ||
486
+ href.includes("/feed/") ||
487
+ href.includes("action=feed")
488
+ ) {
489
+ continue;
490
+ }
491
+
492
+ if (type.includes("markdown")) {
493
+ links.push(href);
494
+ } else if (
495
+ (type.includes("rss") || type.includes("atom") || type.includes("feed")) &&
496
+ (href.includes(pagePath) || href.includes("comments"))
497
+ ) {
498
+ links.push(href);
499
+ }
500
+ }
501
+ } catch {}
502
+
503
+ return links;
504
+ }
505
+
506
+ /**
507
+ * Extract document links from HTML (for PDF/DOCX wrapper pages)
508
+ */
509
+ function extractDocumentLinks(html: string, baseUrl: string): string[] {
510
+ const links: string[] = [];
511
+ const seen = new Set<string>();
512
+
513
+ try {
514
+ const anchorTags = html.slice(0, 512 * 1024).match(/<a\b[^>]*>/gi) ?? [];
515
+ for (const tag of anchorTags) {
516
+ const href = getHtmlAttribute(tag, "href");
517
+ if (!href) continue;
518
+
519
+ const ext = path.extname(href).toLowerCase();
520
+ if (!CONVERTIBLE_EXTENSIONS.has(ext)) continue;
521
+
522
+ const resolved = href.startsWith("http") ? href : new URL(href, baseUrl).href;
523
+ if (seen.has(resolved)) continue;
524
+ seen.add(resolved);
525
+ links.push(resolved);
526
+ if (links.length >= 20) break;
527
+ }
528
+ } catch {}
529
+
530
+ return links;
531
+ }
532
+
533
+ /**
534
+ * Strip CDATA wrapper and clean text
535
+ */
536
+ function cleanFeedText(text: string): string {
537
+ return text
538
+ .replace(/<!\[CDATA\[/g, "")
539
+ .replace(/\]\]>/g, "")
540
+ .replace(/&lt;/g, "<")
541
+ .replace(/&gt;/g, ">")
542
+ .replace(/&amp;/g, "&")
543
+ .replace(/&quot;/g, '"')
544
+ .replace(/<[^>]+>/g, "") // Strip HTML tags
545
+ .trim();
546
+ }
547
+
548
+ /**
549
+ * Parse RSS/Atom feed to markdown
550
+ */
551
+ async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<string> {
552
+ const { parseHTML } = await import("linkedom");
553
+ try {
554
+ const doc = parseHTML(content).document;
555
+
556
+ // Try RSS
557
+ const channel = doc.querySelector("channel");
558
+ if (channel) {
559
+ const title = cleanFeedText(channel.querySelector("title")?.text || "RSS Feed");
560
+ const items = channel.querySelectorAll("item").slice(0, maxItems);
561
+
562
+ let md = `# ${title}\n\n`;
563
+ for (const item of items) {
564
+ const itemTitle = cleanFeedText(item.querySelector("title")?.text || "Untitled");
565
+ const link = cleanFeedText(item.querySelector("link")?.text || "");
566
+ const pubDate = cleanFeedText(item.querySelector("pubDate")?.text || "");
567
+ const desc = cleanFeedText(item.querySelector("description")?.text || "");
568
+
569
+ md += `## ${itemTitle}\n`;
570
+ if (pubDate) md += `*${pubDate}*\n\n`;
571
+ if (desc) md += `${desc.slice(0, 500)}${desc.length > 500 ? "..." : ""}\n\n`;
572
+ if (link) md += `[Read more](${link})\n\n`;
573
+ md += "---\n\n";
574
+ }
575
+ return md;
576
+ }
577
+
578
+ // Try Atom
579
+ const feed = doc.querySelector("feed");
580
+ if (feed) {
581
+ const title = cleanFeedText(feed.querySelector("title")?.text || "Atom Feed");
582
+ const entries = feed.querySelectorAll("entry").slice(0, maxItems);
583
+
584
+ let md = `# ${title}\n\n`;
585
+ for (const entry of entries) {
586
+ const entryTitle = cleanFeedText(entry.querySelector("title")?.text || "Untitled");
587
+ const link = entry.querySelector("link")?.getAttribute("href") || "";
588
+ const updated = cleanFeedText(entry.querySelector("updated")?.text || "");
589
+ const summary = cleanFeedText(
590
+ entry.querySelector("summary")?.text || entry.querySelector("content")?.text || "",
591
+ );
592
+
593
+ md += `## ${entryTitle}\n`;
594
+ if (updated) md += `*${updated}*\n\n`;
595
+ if (summary) md += `${summary.slice(0, 500)}${summary.length > 500 ? "..." : ""}\n\n`;
596
+ if (link) md += `[Read more](${link})\n\n`;
597
+ md += "---\n\n";
598
+ }
599
+ return md;
600
+ }
601
+ } catch {}
602
+
603
+ return content; // Fall back to raw content
604
+ }
605
+
606
+ /**
607
+ * Cap on any single remote reader-mode request (Parallel, Jina) so a stalled
608
+ * remote endpoint cannot consume the whole reader-mode budget and starve the
609
+ * local fallback renderers (trafilatura, lynx, native). See #1449.
610
+ */
611
+ const REMOTE_READER_MAX_MS = 10_000;
612
+
613
+ /** Reader backends for {@link renderHtmlToText}, in default priority order. */
614
+ export type FetchProvider = "native" | "trafilatura" | "lynx" | "parallel" | "jina";
615
+
616
+ const FETCH_PROVIDER_ORDER: readonly FetchProvider[] = ["native", "trafilatura", "lynx", "parallel", "jina"];
617
+
618
+ /**
619
+ * Render HTML to markdown by trying reader backends in priority order: native
620
+ * (in-process), trafilatura, lynx, Parallel, then Jina. The `providers.fetch`
621
+ * setting picks the order — `auto` uses the default above; any specific backend
622
+ * is tried first, then the remaining backends as fallbacks. Every backend's
623
+ * output must clear the same quality gate (>100 non-whitespace chars and not
624
+ * {@link isLowQualityOutput}) before it is accepted, otherwise the next backend
625
+ * is tried.
626
+ *
627
+ * The overall `timeout` budget bounds the whole call; remote backends (Parallel,
628
+ * Jina) are additionally capped at `REMOTE_READER_MAX_MS` so a hung endpoint
629
+ * cannot starve later renderers — especially the purely-local native converter,
630
+ * which always works on already-loaded HTML. Only a real `userSignal`
631
+ * cancellation aborts the chain (#1449).
632
+ */
633
+ export async function renderHtmlToText(
634
+ url: string,
635
+ html: string,
636
+ timeout: number,
637
+ settings: Settings,
638
+ userSignal: AbortSignal | undefined,
639
+ storage: AgentStorage | null,
640
+ fetchOverride?: FetchImpl,
641
+ ): Promise<{ content: string; ok: boolean; method: string }> {
642
+ const overallSignal = ptree.combineSignals(userSignal, timeout * 1000);
643
+ const execOptions = {
644
+ mode: "group" as const,
645
+ allowNonZero: true,
646
+ allowAbort: true,
647
+ stderr: "full" as const,
648
+ signal: overallSignal,
649
+ };
650
+ const remoteBudgetMs = Math.min(timeout * 1000, REMOTE_READER_MAX_MS);
651
+ // Per-attempt budget for remote endpoints so one stall cannot consume the
652
+ // whole reader-mode budget and starve the local fallbacks.
653
+ const remoteSignal = () => ptree.combineSignals(userSignal, remoteBudgetMs);
654
+ const fetchImpl = fetchOverride ?? fetch;
655
+
656
+ const runners: Record<FetchProvider, () => Promise<string | null>> = {
657
+ // Purely local, no network/subprocess: still works on already-loaded HTML
658
+ // even after remote/subprocess attempts are aborted by the budget.
659
+ native: () => htmlToMarkdown(html, { cleanContent: true }),
660
+ trafilatura: async () => {
661
+ const trafilatura = await ensureTool("trafilatura", { signal: overallSignal, silent: true });
662
+ if (!trafilatura) return null;
663
+ const result = await ptree.exec([trafilatura, "-u", url, "--output-format", "markdown"], execOptions);
664
+ return result.ok ? result.stdout : null;
665
+ },
666
+ lynx: async () => {
667
+ if (!hasCommand("lynx")) return null;
668
+ const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
669
+ return result.ok ? result.stdout : null;
670
+ },
671
+ parallel: async () => {
672
+ if (!findParallelApiKey(storage)) return null;
673
+ const parallelResult = await extractWithParallel(
674
+ [url],
675
+ {
676
+ objective: "Extract the main content",
677
+ excerpts: true,
678
+ fullContent: false,
679
+ signal: remoteSignal(),
680
+ fetch: fetchImpl,
681
+ },
682
+ storage,
683
+ );
684
+ const firstDocument = parallelResult.results[0];
685
+ return firstDocument ? getParallelExtractContent(firstDocument) : null;
686
+ },
687
+ jina: async () => {
688
+ const response = await fetchImpl(`https://r.jina.ai/${url}`, {
689
+ headers: { Accept: "text/markdown" },
690
+ signal: remoteSignal(),
691
+ });
692
+ return response.ok ? await response.text() : null;
693
+ },
694
+ };
695
+
696
+ const preference = settings.get("providers.fetch");
697
+ const order: readonly FetchProvider[] =
698
+ preference === "auto"
699
+ ? FETCH_PROVIDER_ORDER
700
+ : [preference, ...FETCH_PROVIDER_ORDER.filter(method => method !== preference)];
701
+
702
+ // Highest-priority output that is substantial but fails the low-quality gate.
703
+ // Surfaced (ok: true) only when no backend clears the gate, so the caller's
704
+ // targeted fallbacks (llms.txt / document extraction) still run and we beat
705
+ // returning the unrendered raw HTML.
706
+ let lowQuality: { content: string; method: FetchProvider } | null = null;
707
+
708
+ for (const method of order) {
709
+ // Honour real user cancellation between attempts; remote per-attempt and
710
+ // overall-budget timeouts still fall through to later (local) renderers.
711
+ userSignal?.throwIfAborted();
712
+ try {
713
+ const content = await runners[method]();
714
+ if (!content || content.trim().length <= 100) continue;
715
+ if (!isLowQualityOutput(content)) {
716
+ return { content, ok: true, method };
717
+ }
718
+ lowQuality ??= { content, method };
719
+ } catch {
720
+ userSignal?.throwIfAborted();
721
+ }
722
+ }
723
+
724
+ if (lowQuality) {
725
+ return { content: lowQuality.content, ok: true, method: lowQuality.method };
726
+ }
727
+ return { content: "", ok: false, method: "none" };
728
+ }
729
+
730
+ /**
731
+ * Check if lynx output looks JS-gated or mostly navigation
732
+ */
733
+ function isLowQualityOutput(content: string): boolean {
734
+ const lower = content.toLowerCase();
735
+
736
+ // JS-gated indicators
737
+ const jsGated = [
738
+ "enable javascript",
739
+ "javascript required",
740
+ "turn on javascript",
741
+ "please enable javascript",
742
+ "browser not supported",
743
+ ];
744
+ if (content.length < 1024 && jsGated.some(t => lower.includes(t))) {
745
+ return true;
746
+ }
747
+
748
+ // Mostly navigation (high link/menu density)
749
+ const lines = content.split("\n").filter(l => l.trim());
750
+ const shortLines = lines.filter(l => l.trim().length < 40);
751
+ if (lines.length > 10 && shortLines.length / lines.length > 0.7) {
752
+ return true;
753
+ }
754
+
755
+ return false;
756
+ }
757
+
758
+ /**
759
+ * Format JSON
760
+ */
761
+ function formatJson(content: string): string {
762
+ try {
763
+ return JSON.stringify(JSON.parse(content), null, 2);
764
+ } catch {
765
+ return content;
766
+ }
767
+ }
768
+
769
+ interface FetchImagePayload {
770
+ data: string;
771
+ mimeType: string;
772
+ }
773
+
774
+ type FetchRenderResult = RenderResult & {
775
+ image?: FetchImagePayload;
776
+ };
777
+
778
+ const BINARY_SAMPLE_CHARS = 4096;
779
+ const URL_ARCHIVE_LIST_LIMIT = 500;
780
+ const URL_SQLITE_LIST_LIMIT = 500;
781
+
782
+ function sampleLooksBinary(text: string): boolean {
783
+ const limit = Math.min(text.length, BINARY_SAMPLE_CHARS);
784
+ if (limit === 0) return false;
785
+
786
+ let replacementCount = 0;
787
+ for (let index = 0; index < limit; index++) {
788
+ const code = text.charCodeAt(index);
789
+ if (code === 0) return true;
790
+ if (code === 0xfffd) replacementCount++;
791
+ }
792
+
793
+ return replacementCount >= 3 && replacementCount / limit > 0.01;
794
+ }
795
+
796
+ function isNotebookHint(mime: string, extensionHint: string): boolean {
797
+ return NOTEBOOK_MIMES.has(mime) || NOTEBOOK_EXTENSIONS.has(extensionHint);
798
+ }
799
+
800
+ function isSqliteHint(mime: string, extensionHint: string): boolean {
801
+ return SQLITE_MIMES.has(mime) || SQLITE_EXTENSIONS.has(extensionHint);
802
+ }
803
+
804
+ function isArchiveHint(mime: string, extensionHint: string): boolean {
805
+ return ARCHIVE_MIMES.has(mime) || ARCHIVE_EXTENSIONS.has(extensionHint);
806
+ }
807
+
808
+ /**
809
+ * Content types whose payload renderUrl always re-fetches via fetchBinary.
810
+ * Skipping the initial body read for them avoids downloading and
811
+ * string-decoding huge binaries (PDFs, archives, images) twice.
812
+ */
813
+ function shouldSkipBodyDownload(contentType: string): boolean {
814
+ return (
815
+ CONVERTIBLE_MIMES.has(contentType) ||
816
+ NOTEBOOK_MIMES.has(contentType) ||
817
+ SQLITE_MIMES.has(contentType) ||
818
+ ARCHIVE_MIMES.has(contentType) ||
819
+ SUPPORTED_INLINE_IMAGE_MIME_TYPES.has(contentType)
820
+ );
821
+ }
822
+
823
+ function getArchiveFormatHint(mime: string, extensionHint: string): ArchiveFormat | undefined {
824
+ if (extensionHint === ".zip" || mime === "application/zip" || mime === "application/x-zip-compressed") {
825
+ return "zip";
826
+ }
827
+ if (extensionHint === ".tar" || mime === "application/x-tar" || mime === "application/tar") {
828
+ return "tar";
829
+ }
830
+ if (
831
+ extensionHint === ".tar.gz" ||
832
+ extensionHint === ".tgz" ||
833
+ extensionHint === ".gz" ||
834
+ mime === "application/gzip" ||
835
+ mime === "application/x-gzip"
836
+ ) {
837
+ return "tar.gz";
838
+ }
839
+ return undefined;
840
+ }
841
+
842
+ function formatErrorMessage(error: unknown): string {
843
+ return error instanceof Error ? error.message : String(error);
844
+ }
845
+
846
+ function binaryContentType(mime: string): string {
847
+ return mime || "application/octet-stream";
848
+ }
849
+
850
+ function buildBinaryNotice(finalUrl: string, mime: string, byteLength?: number): string {
851
+ const size = byteLength === undefined ? "unknown size" : formatBytes(byteLength);
852
+ return `[Binary content: ${binaryContentType(mime)}, ${size}] ${finalUrl}`;
853
+ }
854
+
855
+ function buildBinaryPayloadResult(
856
+ url: string,
857
+ finalUrl: string,
858
+ mime: string,
859
+ method: string,
860
+ content: string,
861
+ fetchedAt: string,
862
+ notes: string[],
863
+ ): FetchRenderResult {
864
+ const output = finalizeOutput(content);
865
+ return {
866
+ url,
867
+ finalUrl,
868
+ contentType: binaryContentType(mime),
869
+ method,
870
+ content: output.content,
871
+ fetchedAt,
872
+ truncated: output.truncated,
873
+ notes,
874
+ };
875
+ }
876
+
877
+ async function withTempBinaryFile<T>(
878
+ prefix: string,
879
+ extension: string,
880
+ bytes: Uint8Array,
881
+ readTempFile: (tempPath: string) => Promise<T>,
882
+ ): Promise<T> {
883
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
884
+ const tempPath = path.join(tempDir, `payload${extension}`);
885
+ try {
886
+ await Bun.write(tempPath, bytes);
887
+ return await readTempFile(tempPath);
888
+ } finally {
889
+ await fs.rm(tempDir, { recursive: true, force: true });
890
+ }
891
+ }
892
+
893
+ async function renderNotebookPayload(bytes: Uint8Array, displayUrl: string): Promise<string> {
894
+ return withTempBinaryFile("omp-url-notebook-", ".ipynb", bytes, tempPath =>
895
+ readEditableNotebookText(tempPath, displayUrl),
896
+ );
897
+ }
898
+
899
+ async function renderSqlitePayload(bytes: Uint8Array): Promise<string> {
900
+ return withTempBinaryFile("omp-url-sqlite-", ".sqlite", bytes, async tempPath => {
901
+ let db: Database | null = null;
902
+ try {
903
+ db = new Database(tempPath, { readonly: true, strict: true });
904
+ db.run("PRAGMA busy_timeout = 3000");
905
+ const listLimit = applyListLimit(listTables(db), { limit: URL_SQLITE_LIST_LIMIT });
906
+ return renderTableList(listLimit.items);
907
+ } finally {
908
+ db?.close();
909
+ }
910
+ });
911
+ }
912
+
913
+ async function tryRenderBinaryPayload(
914
+ url: string,
915
+ finalUrl: string,
916
+ mime: string,
917
+ extHint: string,
918
+ rawContent: string,
919
+ bodySkipped: boolean,
920
+ timeout: number,
921
+ signal: AbortSignal | undefined,
922
+ fetchedAt: string,
923
+ notes: readonly string[],
924
+ ): Promise<FetchRenderResult | null> {
925
+ const hasNotebookHint = isNotebookHint(mime, extHint);
926
+ const hasSqliteHint = isSqliteHint(mime, extHint);
927
+ const hasArchiveHint = isArchiveHint(mime, extHint);
928
+ const rawLooksBinary = bodySkipped || sampleLooksBinary(rawContent);
929
+ if (!hasNotebookHint && !hasSqliteHint && !hasArchiveHint && !rawLooksBinary) {
930
+ return null;
931
+ }
932
+
933
+ const resultNotes = [...notes];
934
+ const binary = await fetchBinary(finalUrl, timeout, signal);
935
+ if (!binary.ok) {
936
+ resultNotes.push(binary.error ? `Binary fetch failed: ${binary.error}` : "Binary fetch failed");
937
+ return buildBinaryPayloadResult(
938
+ url,
939
+ finalUrl,
940
+ mime,
941
+ "binary",
942
+ buildBinaryNotice(finalUrl, mime),
943
+ fetchedAt,
944
+ resultNotes,
945
+ );
946
+ }
947
+
948
+ const binaryExtHint = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
949
+ if (isNotebookHint(mime, binaryExtHint)) {
950
+ try {
951
+ return buildBinaryPayloadResult(
952
+ url,
953
+ finalUrl,
954
+ mime,
955
+ "notebook",
956
+ await renderNotebookPayload(binary.buffer, finalUrl),
957
+ fetchedAt,
958
+ resultNotes,
959
+ );
960
+ } catch (error) {
961
+ resultNotes.push(`Notebook rendering failed: ${formatErrorMessage(error)}`);
962
+ return buildBinaryPayloadResult(
963
+ url,
964
+ finalUrl,
965
+ mime,
966
+ "binary",
967
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
968
+ fetchedAt,
969
+ resultNotes,
970
+ );
971
+ }
972
+ }
973
+
974
+ if (isSqliteHint(mime, binaryExtHint) || looksLikeSqlite(binary.buffer)) {
975
+ try {
976
+ return buildBinaryPayloadResult(
977
+ url,
978
+ finalUrl,
979
+ mime,
980
+ "sqlite",
981
+ await renderSqlitePayload(binary.buffer),
982
+ fetchedAt,
983
+ resultNotes,
984
+ );
985
+ } catch (error) {
986
+ resultNotes.push(`SQLite rendering failed: ${formatErrorMessage(error)}`);
987
+ return buildBinaryPayloadResult(
988
+ url,
989
+ finalUrl,
990
+ mime,
991
+ "binary",
992
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
993
+ fetchedAt,
994
+ resultNotes,
995
+ );
996
+ }
997
+ }
998
+
999
+ const hintedArchiveFormat = getArchiveFormatHint(mime, binaryExtHint);
1000
+ const shouldArchiveSniff = hintedArchiveFormat !== undefined || !isConvertible(mime, binaryExtHint);
1001
+ const archiveFormat = hintedArchiveFormat ?? (shouldArchiveSniff ? sniffArchiveFormat(binary.buffer) : undefined);
1002
+ if (archiveFormat) {
1003
+ try {
1004
+ return buildBinaryPayloadResult(
1005
+ url,
1006
+ finalUrl,
1007
+ mime,
1008
+ "archive",
1009
+ await listArchiveRoot(binary.buffer, archiveFormat, { limit: URL_ARCHIVE_LIST_LIMIT }),
1010
+ fetchedAt,
1011
+ resultNotes,
1012
+ );
1013
+ } catch (error) {
1014
+ resultNotes.push(`Archive rendering failed: ${formatErrorMessage(error)}`);
1015
+ return buildBinaryPayloadResult(
1016
+ url,
1017
+ finalUrl,
1018
+ mime,
1019
+ "binary",
1020
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
1021
+ fetchedAt,
1022
+ resultNotes,
1023
+ );
1024
+ }
1025
+ }
1026
+
1027
+ if (rawLooksBinary) {
1028
+ return buildBinaryPayloadResult(
1029
+ url,
1030
+ finalUrl,
1031
+ mime,
1032
+ "binary",
1033
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
1034
+ fetchedAt,
1035
+ resultNotes,
1036
+ );
1037
+ }
1038
+
1039
+ return null;
1040
+ }
1041
+
1042
+ // =============================================================================
1043
+ // Unified Special Handler Dispatch
1044
+ // =============================================================================
1045
+
1046
+ /**
1047
+ * Try all special handlers
1048
+ */
1049
+ async function handleSpecialUrls(
1050
+ url: string,
1051
+ timeout: number,
1052
+ signal: AbortSignal | undefined,
1053
+ storage: AgentStorage | null,
1054
+ ): Promise<FetchRenderResult | null> {
1055
+ for (const handler of specialHandlers) {
1056
+ if (signal?.aborted) {
1057
+ throw new ToolAbortError();
1058
+ }
1059
+ const result = await handler(url, timeout, signal, storage);
1060
+ if (result) return result;
1061
+ }
1062
+ return null;
1063
+ }
1064
+
1065
+ // =============================================================================
1066
+ // Main Render Function
1067
+ // =============================================================================
1068
+
1069
+ /**
1070
+ * Main render function implementing the full pipeline
1071
+ */
1072
+ async function renderUrl(
1073
+ url: string,
1074
+ timeout: number,
1075
+ raw: boolean,
1076
+ settings: Settings,
1077
+ signal: AbortSignal | undefined,
1078
+ storage: AgentStorage | null,
1079
+ fetchOverride?: FetchImpl,
1080
+ ): Promise<FetchRenderResult> {
1081
+ const notes: string[] = [];
1082
+ const fetchedAt = new Date().toISOString();
1083
+ if (signal?.aborted) {
1084
+ throw new ToolAbortError();
1085
+ }
1086
+
1087
+ // Handle internal protocol URLs (e.g., pi-internal://) - return empty
1088
+ if (url.startsWith("pi-internal://")) {
1089
+ return {
1090
+ url,
1091
+ finalUrl: url,
1092
+ contentType: "text/plain",
1093
+ method: "internal",
1094
+ content: "",
1095
+ fetchedAt,
1096
+ truncated: false,
1097
+ notes: ["Internal protocol URL - no external content"],
1098
+ };
1099
+ }
1100
+
1101
+ // Step 0: Normalize URL (ensure scheme for special handlers)
1102
+ url = normalizeUrl(url);
1103
+
1104
+ // Step 1: Try special handlers for known sites (unless raw mode)
1105
+ if (!raw) {
1106
+ const specialResult = await handleSpecialUrls(url, timeout, signal, storage);
1107
+ if (specialResult) return specialResult;
1108
+ }
1109
+
1110
+ // Step 2: Fetch page
1111
+ const response = await loadPage(url, { timeout, signal, skipBodyForContentType: shouldSkipBodyDownload });
1112
+ if (signal?.aborted) {
1113
+ throw new ToolAbortError();
1114
+ }
1115
+ if (!response.ok) {
1116
+ return {
1117
+ url,
1118
+ finalUrl: response.finalUrl || url,
1119
+ contentType: response.contentType || "unknown",
1120
+ method: "failed",
1121
+ content: "",
1122
+ fetchedAt,
1123
+ truncated: false,
1124
+ notes: [
1125
+ response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL",
1126
+ ...(response.error ? [`Cause: ${response.error}`] : []),
1127
+ ],
1128
+ };
1129
+ }
1130
+
1131
+ const { finalUrl, content: rawContent } = response;
1132
+ if (response.truncated) {
1133
+ notes.push(`Response body exceeded ${formatBytes(MAX_BYTES)} and was cut mid-stream; content is incomplete`);
1134
+ }
1135
+ const mime = normalizeMime(response.contentType);
1136
+ const extHint = getExtensionHint(finalUrl);
1137
+
1138
+ const imageMimeType = resolveImageMimeType(mime, extHint);
1139
+ let skipConvertibleBinaryRetry = false;
1140
+ if (imageMimeType) {
1141
+ if (!isInlineImageMimeTypeSupported(imageMimeType)) {
1142
+ notes.push(
1143
+ `Image MIME type ${imageMimeType} is unsupported for inline model serialization; returning text metadata only`,
1144
+ );
1145
+ const shouldTryConvertibleFallback = isConvertible(mime, extHint);
1146
+ if (shouldTryConvertibleFallback) {
1147
+ notes.push("Attempting binary conversion fallback for unsupported image MIME type");
1148
+ } else {
1149
+ notes.push("Falling back to textual rendering from initial response");
1150
+ }
1151
+ skipConvertibleBinaryRetry = !shouldTryConvertibleFallback;
1152
+ } else {
1153
+ const binary = await fetchBinary(finalUrl, timeout, signal);
1154
+ if (binary.ok) {
1155
+ notes.push("Fetched image binary");
1156
+ const conversionExtension = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
1157
+ let convertedText: string | null = null;
1158
+ const converted = await convertWithMarkit(binary.buffer, conversionExtension, timeout, signal);
1159
+ if (converted.ok) {
1160
+ if (converted.content.trim().length > 50) {
1161
+ notes.push("Converted with markit");
1162
+ convertedText = converted.content;
1163
+ } else {
1164
+ notes.push("markit conversion produced no usable output");
1165
+ }
1166
+ } else if (converted.error) {
1167
+ notes.push(`markit conversion failed: ${converted.error}`);
1168
+ } else {
1169
+ notes.push("markit conversion failed");
1170
+ }
1171
+
1172
+ if (binary.buffer.byteLength > MAX_INLINE_IMAGE_SOURCE_BYTES) {
1173
+ notes.push(
1174
+ `Image exceeds inline source limit (${binary.buffer.byteLength} bytes > ${MAX_INLINE_IMAGE_SOURCE_BYTES} bytes)`,
1175
+ );
1176
+ const output = finalizeOutput(
1177
+ convertedText ?? `Fetched image content (${imageMimeType}), but it is too large to inline render.`,
1178
+ );
1179
+ return {
1180
+ url,
1181
+ finalUrl,
1182
+ contentType: imageMimeType,
1183
+ method: convertedText ? "markit" : "image-too-large",
1184
+ content: output.content,
1185
+ fetchedAt,
1186
+ truncated: output.truncated,
1187
+ notes,
1188
+ };
1189
+ }
1190
+
1191
+ const resized = await resizeImage(
1192
+ { type: "image", data: Buffer.from(binary.buffer).toBase64(), mimeType: imageMimeType },
1193
+ { maxBytes: MAX_INLINE_IMAGE_OUTPUT_BYTES },
1194
+ );
1195
+ const isDecodedImage =
1196
+ resized.originalWidth > 0 && resized.originalHeight > 0 && resized.width > 0 && resized.height > 0;
1197
+ if (!isDecodedImage) {
1198
+ notes.push(`Fetched payload could not be decoded as ${imageMimeType}; returning text metadata only`);
1199
+ const output = finalizeOutput(
1200
+ convertedText ??
1201
+ rawContent ??
1202
+ `Fetched payload was labeled ${imageMimeType}, but bytes were not a valid image.`,
1203
+ );
1204
+ return {
1205
+ url,
1206
+ finalUrl,
1207
+ contentType: imageMimeType,
1208
+ method: convertedText ? "markit" : "image-invalid",
1209
+ content: output.content,
1210
+ fetchedAt,
1211
+ truncated: output.truncated,
1212
+ notes,
1213
+ };
1214
+ }
1215
+ if (resized.buffer.length > MAX_INLINE_IMAGE_OUTPUT_BYTES) {
1216
+ notes.push(
1217
+ `Image exceeds inline output limit after resize (${resized.buffer.length} bytes > ${MAX_INLINE_IMAGE_OUTPUT_BYTES} bytes)`,
1218
+ );
1219
+ const output = finalizeOutput(
1220
+ convertedText ?? `Fetched image content (${imageMimeType}), but it is too large to inline render.`,
1221
+ );
1222
+ return {
1223
+ url,
1224
+ finalUrl,
1225
+ contentType: imageMimeType,
1226
+ method: convertedText ? "markit" : "image-too-large",
1227
+ content: output.content,
1228
+ fetchedAt,
1229
+ truncated: output.truncated,
1230
+ notes,
1231
+ };
1232
+ }
1233
+
1234
+ const dimensionNote = formatDimensionNote(resized);
1235
+ let imageSummary = convertedText ?? `Fetched image content (${resized.mimeType}).`;
1236
+ if (dimensionNote) {
1237
+ imageSummary += `\n${dimensionNote}`;
1238
+ }
1239
+ const output = finalizeOutput(imageSummary);
1240
+ return {
1241
+ url,
1242
+ finalUrl,
1243
+ contentType: resized.mimeType,
1244
+ method: "image",
1245
+ content: output.content,
1246
+ fetchedAt,
1247
+ truncated: output.truncated,
1248
+ notes,
1249
+ image: {
1250
+ data: resized.data,
1251
+ mimeType: resized.mimeType,
1252
+ },
1253
+ };
1254
+ }
1255
+ notes.push(binary.error ? `Binary fetch failed: ${binary.error}` : "Binary fetch failed");
1256
+ notes.push("Falling back to textual rendering from initial response");
1257
+ skipConvertibleBinaryRetry = true;
1258
+ }
1259
+ }
1260
+
1261
+ // Step 3: Handle convertible binary files (PDF, DOCX, etc.)
1262
+ if (!skipConvertibleBinaryRetry && isConvertible(mime, extHint)) {
1263
+ const binary = await fetchBinary(finalUrl, timeout, signal);
1264
+ if (binary.ok) {
1265
+ const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
1266
+ const converted = await convertWithMarkit(binary.buffer, ext, timeout, signal);
1267
+ if (converted.ok) {
1268
+ if (converted.content.trim().length > 50) {
1269
+ notes.push("Converted with markit");
1270
+ const output = finalizeOutput(converted.content);
1271
+ return {
1272
+ url,
1273
+ finalUrl,
1274
+ contentType: mime,
1275
+ method: "markit",
1276
+ content: output.content,
1277
+ fetchedAt,
1278
+ truncated: output.truncated,
1279
+ notes,
1280
+ };
1281
+ }
1282
+ notes.push("markit conversion produced no usable output");
1283
+ } else if (converted.error) {
1284
+ notes.push(`markit conversion failed: ${converted.error}`);
1285
+ } else {
1286
+ notes.push("markit conversion failed");
1287
+ }
1288
+ } else if (binary.error) {
1289
+ notes.push(`Binary fetch failed: ${binary.error}`);
1290
+ } else {
1291
+ notes.push("Binary fetch failed");
1292
+ }
1293
+ }
1294
+
1295
+ const binaryPayloadResult = await tryRenderBinaryPayload(
1296
+ url,
1297
+ finalUrl,
1298
+ mime,
1299
+ extHint,
1300
+ rawContent,
1301
+ response.bodySkipped === true,
1302
+ timeout,
1303
+ signal,
1304
+ fetchedAt,
1305
+ notes,
1306
+ );
1307
+ if (binaryPayloadResult) return binaryPayloadResult;
1308
+
1309
+ // Step 4: Handle non-HTML text content
1310
+ const isHtml = mime.includes("html") || mime.includes("xhtml");
1311
+ const isJson = mime.includes("json");
1312
+ const isXml = mime.includes("xml") && !isHtml;
1313
+ const isText = mime.includes("text/plain") || mime.includes("text/markdown");
1314
+ const isFeed = mime.includes("rss") || mime.includes("atom") || mime.includes("feed");
1315
+
1316
+ // Raw mode skips every text-shaping branch below (JSON pretty-print, feed-to-markdown,
1317
+ // HTML extraction) and returns the response body verbatim. Binary-oriented branches
1318
+ // above already ran because raw isn't useful for binary payloads.
1319
+ if (raw) {
1320
+ const output = finalizeOutput(rawContent);
1321
+ return {
1322
+ url,
1323
+ finalUrl,
1324
+ contentType: mime,
1325
+ method: "raw",
1326
+ content: output.content,
1327
+ fetchedAt,
1328
+ truncated: output.truncated,
1329
+ notes,
1330
+ };
1331
+ }
1332
+ if (isJson) {
1333
+ const output = finalizeOutput(formatJson(rawContent));
1334
+ return {
1335
+ url,
1336
+ finalUrl,
1337
+ contentType: mime,
1338
+ method: "json",
1339
+ content: output.content,
1340
+ fetchedAt,
1341
+ truncated: output.truncated,
1342
+ notes,
1343
+ };
1344
+ }
1345
+
1346
+ if (isFeed || (isXml && (rawContent.includes("<rss") || rawContent.includes("<feed")))) {
1347
+ const parsed = await parseFeedToMarkdown(rawContent);
1348
+ const output = finalizeOutput(parsed);
1349
+ return {
1350
+ url,
1351
+ finalUrl,
1352
+ contentType: mime,
1353
+ method: "feed",
1354
+ content: output.content,
1355
+ fetchedAt,
1356
+ truncated: output.truncated,
1357
+ notes,
1358
+ };
1359
+ }
1360
+
1361
+ if (isText && !looksLikeHtml(rawContent)) {
1362
+ const output = finalizeOutput(rawContent);
1363
+ return {
1364
+ url,
1365
+ finalUrl,
1366
+ contentType: mime,
1367
+ method: "text",
1368
+ content: output.content,
1369
+ fetchedAt,
1370
+ truncated: output.truncated,
1371
+ notes,
1372
+ };
1373
+ }
1374
+
1375
+ // Step 5: For HTML, try digestible formats first (unless raw mode)
1376
+ if (isHtml && !raw) {
1377
+ // 5A: Check for page-specific markdown alternate
1378
+ const alternates = parseAlternateLinks(rawContent, finalUrl);
1379
+ const markdownAlt = alternates.find(alt => alt.endsWith(".md") || alt.includes("markdown"));
1380
+ if (markdownAlt) {
1381
+ const resolved = markdownAlt.startsWith("http") ? markdownAlt : new URL(markdownAlt, finalUrl).href;
1382
+ const altResult = await loadPage(resolved, { timeout, signal });
1383
+ if (altResult.ok && altResult.content.trim().length > 100 && !looksLikeHtml(altResult.content)) {
1384
+ notes.push(`Used markdown alternate: ${resolved}`);
1385
+ const output = finalizeOutput(altResult.content);
1386
+ return {
1387
+ url,
1388
+ finalUrl,
1389
+ contentType: "text/markdown",
1390
+ method: "alternate-markdown",
1391
+ content: output.content,
1392
+ fetchedAt,
1393
+ truncated: output.truncated,
1394
+ notes,
1395
+ };
1396
+ }
1397
+ }
1398
+
1399
+ // 5B: Try URL.md suffix (llms.txt convention)
1400
+ const mdSuffix = await tryMdSuffix(finalUrl, timeout, signal);
1401
+ if (mdSuffix) {
1402
+ notes.push("Found .md suffix version");
1403
+ const output = finalizeOutput(mdSuffix);
1404
+ return {
1405
+ url,
1406
+ finalUrl,
1407
+ contentType: "text/markdown",
1408
+ method: "md-suffix",
1409
+ content: output.content,
1410
+ fetchedAt,
1411
+ truncated: output.truncated,
1412
+ notes,
1413
+ };
1414
+ }
1415
+
1416
+ // 5C: Content negotiation
1417
+ const negotiated = await tryContentNegotiation(url, timeout, signal);
1418
+ if (negotiated) {
1419
+ notes.push(`Content negotiation returned ${negotiated.type}`);
1420
+ const output = finalizeOutput(negotiated.content);
1421
+ return {
1422
+ url,
1423
+ finalUrl,
1424
+ contentType: normalizeMime(negotiated.type),
1425
+ method: "content-negotiation",
1426
+ content: output.content,
1427
+ fetchedAt,
1428
+ truncated: output.truncated,
1429
+ notes,
1430
+ };
1431
+ }
1432
+
1433
+ // 5D: Check for feed alternates
1434
+ const feedAlternates = alternates.filter(alt => !alt.endsWith(".md") && !alt.includes("markdown"));
1435
+ for (const altUrl of feedAlternates.slice(0, 2)) {
1436
+ const resolved = altUrl.startsWith("http") ? altUrl : new URL(altUrl, finalUrl).href;
1437
+ const altResult = await loadPage(resolved, { timeout, signal });
1438
+ if (altResult.ok && altResult.content.trim().length > 200) {
1439
+ notes.push(`Used feed alternate: ${resolved}`);
1440
+ const parsed = await parseFeedToMarkdown(altResult.content);
1441
+ const output = finalizeOutput(parsed);
1442
+ return {
1443
+ url,
1444
+ finalUrl,
1445
+ contentType: "application/feed",
1446
+ method: "alternate-feed",
1447
+ content: output.content,
1448
+ fetchedAt,
1449
+ truncated: output.truncated,
1450
+ notes,
1451
+ };
1452
+ }
1453
+ }
1454
+
1455
+ if (signal?.aborted) {
1456
+ throw new ToolAbortError();
1457
+ }
1458
+
1459
+ // 5E: Render HTML via the reader-backend chain (native/trafilatura/lynx/parallel/jina)
1460
+ const htmlResult = await renderHtmlToText(
1461
+ finalUrl,
1462
+ rawContent,
1463
+ timeout,
1464
+ settings,
1465
+ signal,
1466
+ storage,
1467
+ fetchOverride,
1468
+ );
1469
+ if (!htmlResult.ok) {
1470
+ notes.push("html rendering failed (no reader backend produced usable output)");
1471
+
1472
+ const llmResult = await tryLlmEndpoints(finalUrl, timeout, signal);
1473
+ if (llmResult) {
1474
+ notes.push(`Used llms.txt fallback: ${llmResult.endpoint}`);
1475
+ const output = finalizeOutput(llmResult.content);
1476
+ return {
1477
+ url,
1478
+ finalUrl,
1479
+ contentType: "text/plain",
1480
+ method: "llms.txt",
1481
+ content: output.content,
1482
+ fetchedAt,
1483
+ truncated: output.truncated,
1484
+ notes,
1485
+ };
1486
+ }
1487
+
1488
+ const output = finalizeOutput(rawContent);
1489
+ return {
1490
+ url,
1491
+ finalUrl,
1492
+ contentType: mime,
1493
+ method: "raw-html",
1494
+ content: output.content,
1495
+ fetchedAt,
1496
+ truncated: output.truncated,
1497
+ notes,
1498
+ };
1499
+ }
1500
+
1501
+ // Step 6: If rendered output is low quality, try more targeted fallbacks
1502
+ if (isLowQualityOutput(htmlResult.content)) {
1503
+ const docLinks = extractDocumentLinks(rawContent, finalUrl);
1504
+ if (docLinks.length > 0) {
1505
+ const docUrl = docLinks[0];
1506
+ const binary = await fetchBinary(docUrl, timeout, signal);
1507
+ if (binary.ok) {
1508
+ const ext = getExtensionHint(docUrl, binary.contentDisposition);
1509
+ const converted = await convertWithMarkit(binary.buffer, ext, timeout, signal);
1510
+ if (converted.ok && converted.content.trim().length > htmlResult.content.length) {
1511
+ notes.push(`Extracted and converted document: ${docUrl}`);
1512
+ const output = finalizeOutput(converted.content);
1513
+ return {
1514
+ url,
1515
+ finalUrl,
1516
+ contentType: "application/document",
1517
+ method: "extracted-document",
1518
+ content: output.content,
1519
+ fetchedAt,
1520
+ truncated: output.truncated,
1521
+ notes,
1522
+ };
1523
+ }
1524
+ if (!converted.ok && converted.error) {
1525
+ notes.push(`markit conversion failed: ${converted.error}`);
1526
+ }
1527
+ } else if (binary.error) {
1528
+ notes.push(`Binary fetch failed: ${binary.error}`);
1529
+ }
1530
+ }
1531
+
1532
+ const llmResult = await tryLlmEndpoints(finalUrl, timeout, signal);
1533
+ if (llmResult) {
1534
+ notes.push(`Used llms.txt fallback: ${llmResult.endpoint}`);
1535
+ const output = finalizeOutput(llmResult.content);
1536
+ return {
1537
+ url,
1538
+ finalUrl,
1539
+ contentType: "text/plain",
1540
+ method: "llms.txt",
1541
+ content: output.content,
1542
+ fetchedAt,
1543
+ truncated: output.truncated,
1544
+ notes,
1545
+ };
1546
+ }
1547
+
1548
+ notes.push("Page appears to require JavaScript or is mostly navigation");
1549
+ }
1550
+
1551
+ const output = finalizeOutput(htmlResult.content);
1552
+ return {
1553
+ url,
1554
+ finalUrl,
1555
+ contentType: mime,
1556
+ method: htmlResult.method,
1557
+ content: output.content,
1558
+ fetchedAt,
1559
+ truncated: output.truncated,
1560
+ notes,
1561
+ };
1562
+ }
1563
+
1564
+ // Fallback: return raw content
1565
+ const output = finalizeOutput(rawContent);
1566
+ return {
1567
+ url,
1568
+ finalUrl,
1569
+ contentType: mime,
1570
+ method: "raw",
1571
+ content: output.content,
1572
+ fetchedAt,
1573
+ truncated: output.truncated,
1574
+ notes,
1575
+ };
1576
+ }
1577
+
1578
+ // =============================================================================
1579
+ // Tool Definition
1580
+ // =============================================================================
1581
+
1582
+ export interface ReadUrlToolDetails {
1583
+ kind: "url";
1584
+ url: string;
1585
+ finalUrl: string;
1586
+ contentType: string;
1587
+ method: string;
1588
+ truncated: boolean;
1589
+ notes: string[];
1590
+ meta?: OutputMeta;
1591
+ }
1592
+
1593
+ interface ReadUrlCacheEntry {
1594
+ artifactId?: string;
1595
+ details: ReadUrlToolDetails;
1596
+ image?: FetchImagePayload;
1597
+ output: string;
1598
+ }
1599
+
1600
+ const READ_URL_CACHE_MAX_ENTRIES = 100;
1601
+ const readUrlCache = new LRUCache<string, ReadUrlCacheEntry>({ max: READ_URL_CACHE_MAX_ENTRIES });
1602
+
1603
+ function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
1604
+ const scope = session.getSessionFile() ?? session.cwd;
1605
+ return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
1606
+ }
1607
+
1608
+ async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
1609
+ const artifactsDir = session.getArtifactsDir?.();
1610
+ if (!artifactsDir) return null;
1611
+
1612
+ try {
1613
+ const files = await fs.readdir(artifactsDir);
1614
+ const match = files.find(file => file.startsWith(`${artifactId}.`));
1615
+ if (!match) return null;
1616
+ return await Bun.file(path.join(artifactsDir, match)).text();
1617
+ } catch {
1618
+ return null;
1619
+ }
1620
+ }
1621
+
1622
+ async function materializeReadUrlCacheEntry(
1623
+ session: ToolSession,
1624
+ entry: ReadUrlCacheEntry,
1625
+ ): Promise<ReadUrlCacheEntry | null> {
1626
+ if (entry.artifactId) {
1627
+ const artifactOutput = await readArtifactOutput(session, entry.artifactId);
1628
+ if (artifactOutput !== null) {
1629
+ return { ...entry, output: artifactOutput };
1630
+ }
1631
+ }
1632
+
1633
+ return entry.output.length > 0 ? entry : null;
1634
+ }
1635
+
1636
+ async function persistReadUrlArtifact(session: ToolSession, output: string): Promise<string | undefined> {
1637
+ const { path: artifactPath, id } = (await session.allocateOutputArtifact?.("read")) ?? {};
1638
+ if (!artifactPath) return undefined;
1639
+ await Bun.write(artifactPath, output);
1640
+ return id;
1641
+ }
1642
+
1643
+ async function ensureReadUrlCacheArtifact(session: ToolSession, entry: ReadUrlCacheEntry): Promise<ReadUrlCacheEntry> {
1644
+ if (entry.artifactId) return entry;
1645
+ const artifactId = await persistReadUrlArtifact(session, entry.output);
1646
+ return artifactId ? { ...entry, artifactId } : entry;
1647
+ }
1648
+
1649
+ function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
1650
+ readUrlCache.set(getReadUrlCacheKey(session, requestedUrl, raw), entry);
1651
+ readUrlCache.set(getReadUrlCacheKey(session, entry.details.finalUrl, raw), entry);
1652
+ }
1653
+
1654
+ async function buildReadUrlCacheEntry(
1655
+ session: ToolSession,
1656
+ params: { path: string; raw?: boolean },
1657
+ signal?: AbortSignal,
1658
+ options?: { ensureArtifact?: boolean },
1659
+ ): Promise<ReadUrlCacheEntry> {
1660
+ const { path: url, raw = false } = params;
1661
+
1662
+ const effectiveTimeout = clampTimeout("fetch", 30);
1663
+
1664
+ if (signal?.aborted) {
1665
+ throw new ToolAbortError();
1666
+ }
1667
+
1668
+ const storage = session.settings.getStorage();
1669
+ const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage, session.fetch);
1670
+ const output = buildUrlReadOutput(result, result.content);
1671
+ const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
1672
+
1673
+ return {
1674
+ artifactId,
1675
+ details: {
1676
+ kind: "url",
1677
+ url: result.url,
1678
+ finalUrl: result.finalUrl,
1679
+ contentType: result.contentType,
1680
+ method: result.method,
1681
+ truncated: Boolean(result.truncated),
1682
+ notes: result.notes,
1683
+ },
1684
+ image: result.image,
1685
+ output,
1686
+ };
1687
+ }
1688
+
1689
+ export async function loadReadUrlCacheEntry(
1690
+ session: ToolSession,
1691
+ params: { path: string; raw?: boolean },
1692
+ signal?: AbortSignal,
1693
+ options?: { ensureArtifact?: boolean; preferCached?: boolean },
1694
+ ): Promise<ReadUrlCacheEntry> {
1695
+ const raw = params.raw ?? false;
1696
+ const cached = readUrlCache.get(getReadUrlCacheKey(session, params.path, raw));
1697
+ if (options?.preferCached && cached) {
1698
+ const prepared = options.ensureArtifact ? await ensureReadUrlCacheArtifact(session, cached) : cached;
1699
+ const materialized = await materializeReadUrlCacheEntry(session, prepared);
1700
+ if (materialized) {
1701
+ cacheReadUrlEntry(session, params.path, raw, materialized);
1702
+ return materialized;
1703
+ }
1704
+ }
1705
+
1706
+ const fresh = await buildReadUrlCacheEntry(session, params, signal, {
1707
+ ensureArtifact: options?.ensureArtifact,
1708
+ });
1709
+ cacheReadUrlEntry(session, params.path, raw, fresh);
1710
+ return fresh;
1711
+ }
1712
+
1713
+ function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
1714
+ let output = "";
1715
+ output += `URL: ${result.finalUrl}\n`;
1716
+ output += `Content-Type: ${result.contentType}\n`;
1717
+ output += `Method: ${result.method}\n`;
1718
+ if (result.notes.length > 0) {
1719
+ output += `Notes: ${result.notes.join("; ")}\n`;
1720
+ }
1721
+ output += `\n---\n\n`;
1722
+ output += content;
1723
+ return output;
1724
+ }
1725
+
1726
+ export async function executeReadUrl(
1727
+ session: ToolSession,
1728
+ params: { path: string; raw?: boolean },
1729
+ signal?: AbortSignal,
1730
+ ): Promise<AgentToolResult<ReadUrlToolDetails>> {
1731
+ let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
1732
+ const truncation = truncateHead(cacheEntry.output, {
1733
+ maxBytes: DEFAULT_MAX_BYTES,
1734
+ maxLines: FETCH_DEFAULT_MAX_LINES,
1735
+ });
1736
+ const needsArtifact = truncation.truncated;
1737
+ if (needsArtifact && !cacheEntry.artifactId) {
1738
+ cacheEntry = await ensureReadUrlCacheArtifact(session, cacheEntry);
1739
+ cacheReadUrlEntry(session, params.path, params.raw ?? false, cacheEntry);
1740
+ }
1741
+ const output = needsArtifact ? truncation.content : cacheEntry.output;
1742
+ const details: ReadUrlToolDetails = {
1743
+ ...cacheEntry.details,
1744
+ truncated: Boolean(cacheEntry.details.truncated || needsArtifact),
1745
+ };
1746
+
1747
+ const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: output }];
1748
+ if (cacheEntry.image) {
1749
+ contentBlocks.push({ type: "image", data: cacheEntry.image.data, mimeType: cacheEntry.image.mimeType });
1750
+ }
1751
+
1752
+ const resultBuilder = toolResult(details).content(contentBlocks).sourceUrl(details.finalUrl);
1753
+ if (needsArtifact) {
1754
+ resultBuilder.truncation(truncation, { direction: "head", artifactId: cacheEntry.artifactId });
1755
+ } else if (cacheEntry.details.truncated) {
1756
+ const outputLines = cacheEntry.output.split("\n").length;
1757
+ const outputBytes = Buffer.byteLength(cacheEntry.output, "utf-8");
1758
+ const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
1759
+ const totalLines = outputLines + 1;
1760
+ resultBuilder.truncationFromText(cacheEntry.output, {
1761
+ direction: "tail",
1762
+ totalLines,
1763
+ totalBytes,
1764
+ maxBytes: MAX_OUTPUT_CHARS,
1765
+ });
1766
+ }
1767
+
1768
+ return resultBuilder.done();
1769
+ }
1770
+
1771
+ // =============================================================================
1772
+ // TUI Rendering
1773
+ // =============================================================================
1774
+
1775
+ /** Count non-empty lines */
1776
+ function countNonEmptyLines(text: string): number {
1777
+ return text.split("\n").filter(l => l.trim()).length;
1778
+ }
1779
+
1780
+ function readUrlLinkTarget(input: string): string {
1781
+ try {
1782
+ return parseReadUrlTarget(input)?.path ?? input;
1783
+ } catch {
1784
+ return input;
1785
+ }
1786
+ }
1787
+
1788
+ function formatReadUrlDescription(input: string): string {
1789
+ const target = readUrlLinkTarget(input);
1790
+ const displayUrl = target.match(/^www\./i) ? `https://${target}` : target;
1791
+ const domain = getDomain(displayUrl);
1792
+ const urlPath = truncate(displayUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
1793
+ const label = `${domain}${urlPath ? ` ${urlPath}` : ""}`.trim();
1794
+ return urlHyperlink(target, label);
1795
+ }
1796
+
1797
+ function formatReadUrlMetadataValue(url: string, uiTheme: Theme): string {
1798
+ return urlHyperlink(url, uiTheme.fg("mdLinkUrl", url));
1799
+ }
1800
+
1801
+ /** Render URL read call (URL preview) */
1802
+ export function renderReadUrlCall(
1803
+ args: { path?: string; url?: string; raw?: boolean },
1804
+ _options: RenderResultOptions,
1805
+ uiTheme: Theme = theme,
1806
+ ): Component {
1807
+ const url = args.path ?? args.url ?? "";
1808
+ const description = formatReadUrlDescription(url);
1809
+ const meta: string[] = [];
1810
+ if (args.raw) meta.push("raw");
1811
+ const text = renderStatusLine({ icon: "pending", title: "Read", description, meta }, uiTheme);
1812
+ return new Text(text, 0, 0);
1813
+ }
1814
+
1815
+ /** Render URL read result with tree-based layout */
1816
+ export function renderReadUrlResult(
1817
+ result: { content: Array<{ type: string; text?: string }>; details?: ReadUrlToolDetails; isError?: boolean },
1818
+ options: RenderResultOptions,
1819
+ uiTheme: Theme = theme,
1820
+ ): Component {
1821
+ const details = result.details;
1822
+
1823
+ if (result.isError || !details) {
1824
+ const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
1825
+ const errorText = (rawErrorText || "No response data").replace(/^Error:\s*/, "");
1826
+ const urlText = details?.finalUrl ?? details?.url ?? "";
1827
+ const description = urlText ? formatReadUrlDescription(urlText) : undefined;
1828
+ const header = renderStatusLine({ icon: "error", title: "Read", description }, uiTheme);
1829
+ const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
1830
+ const outputBlock = new CachedOutputBlock();
1831
+ return markFramedBlockComponent({
1832
+ render: (width: number) =>
1833
+ outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
1834
+ invalidate: () => outputBlock.invalidate(),
1835
+ });
1836
+ }
1837
+
1838
+ const description = formatReadUrlDescription(details.finalUrl);
1839
+ const hasRedirect = details.url !== details.finalUrl;
1840
+ const hasNotes = details.notes.length > 0;
1841
+ const truncation = details.meta?.truncation;
1842
+ const truncated = Boolean(details.truncated || truncation);
1843
+
1844
+ const header = renderStatusLine(
1845
+ {
1846
+ icon: truncated ? "warning" : "success",
1847
+ title: "Read",
1848
+ description,
1849
+ },
1850
+ uiTheme,
1851
+ );
1852
+
1853
+ const contentText = result.content[0]?.text ?? "";
1854
+ const contentBody = contentText.includes("---\n\n")
1855
+ ? contentText.split("---\n\n").slice(1).join("---\n\n")
1856
+ : contentText;
1857
+ const lineCount = countNonEmptyLines(contentBody);
1858
+ const charCount = contentBody.trim().length;
1859
+ const contentLines = contentBody.split("\n").filter(l => l.trim());
1860
+
1861
+ const metadataLines: string[] = [
1862
+ `${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
1863
+ `${uiTheme.fg("muted", "Method:")} ${details.method}`,
1864
+ ];
1865
+ if (hasRedirect) {
1866
+ metadataLines.push(
1867
+ `${uiTheme.fg("muted", "Final URL:")} ${formatReadUrlMetadataValue(details.finalUrl, uiTheme)}`,
1868
+ );
1869
+ }
1870
+ const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
1871
+ metadataLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
1872
+ metadataLines.push(`${uiTheme.fg("muted", "Chars:")} ${charCount}`);
1873
+ if (truncated) {
1874
+ metadataLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
1875
+ if (truncation?.artifactId) metadataLines.push(formatStyledArtifactReference(truncation.artifactId, uiTheme));
1876
+ }
1877
+ if (hasNotes) {
1878
+ metadataLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
1879
+ }
1880
+
1881
+ const outputBlock = new CachedOutputBlock();
1882
+ let lastExpanded: boolean | undefined;
1883
+ let contentPreviewLines: string[] | undefined;
1884
+
1885
+ return markFramedBlockComponent({
1886
+ render: (width: number) => {
1887
+ const { expanded } = options;
1888
+
1889
+ if (contentPreviewLines === undefined || lastExpanded !== expanded) {
1890
+ const previewLimit = expanded ? 12 : 3;
1891
+ const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
1892
+ const previewLines = previewList.items.map(line => line.trimEnd());
1893
+ const remaining = Math.max(0, contentLines.length - previewList.items.length);
1894
+ contentPreviewLines =
1895
+ previewLines.length > 0
1896
+ ? previewLines.map(line => uiTheme.fg("dim", line))
1897
+ : [uiTheme.fg("dim", "(no content)")];
1898
+ if (remaining > 0) {
1899
+ const hint = formatExpandHint(uiTheme, expanded, true);
1900
+ contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
1901
+ }
1902
+ lastExpanded = expanded;
1903
+ outputBlock.invalidate();
1904
+ }
1905
+
1906
+ return outputBlock.render(
1907
+ {
1908
+ header,
1909
+ state: truncated ? "warning" : "success",
1910
+ sections: [
1911
+ { label: uiTheme.fg("toolTitle", "Metadata"), lines: metadataLines },
1912
+ { label: uiTheme.fg("toolTitle", "Content Preview"), lines: contentPreviewLines },
1913
+ ],
1914
+ width,
1915
+ applyBg: false,
1916
+ },
1917
+ uiTheme,
1918
+ );
1919
+ },
1920
+ invalidate: () => {
1921
+ outputBlock.invalidate();
1922
+ contentPreviewLines = undefined;
1923
+ lastExpanded = undefined;
1924
+ },
1925
+ });
1926
+ }