@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.
- package/CHANGELOG.md +10031 -0
- package/README.md +36 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +104 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/extensions/README.md +142 -0
- package/examples/extensions/api-demo.ts +79 -0
- package/examples/extensions/chalk-logger.ts +25 -0
- package/examples/extensions/hello.ts +31 -0
- package/examples/extensions/pirate.ts +43 -0
- package/examples/extensions/plan-mode.ts +549 -0
- package/examples/extensions/reload-runtime.ts +38 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +17 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +48 -0
- package/examples/hooks/confirm-destructive.ts +58 -0
- package/examples/hooks/custom-compaction.ts +115 -0
- package/examples/hooks/dirty-repo-guard.ts +51 -0
- package/examples/hooks/file-trigger.ts +40 -0
- package/examples/hooks/git-checkpoint.ts +52 -0
- package/examples/hooks/handoff.ts +149 -0
- package/examples/hooks/permission-gate.ts +33 -0
- package/examples/hooks/protected-paths.ts +29 -0
- package/examples/hooks/qna.ts +118 -0
- package/examples/hooks/status-line.ts +39 -0
- package/examples/sdk/01-minimal.ts +21 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +46 -0
- package/examples/sdk/04-skills.ts +43 -0
- package/examples/sdk/06-extensions.ts +82 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +35 -0
- package/examples/sdk/08-prompt-templates.ts +41 -0
- package/examples/sdk/08-slash-commands.ts +46 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
- package/examples/sdk/11-sessions.ts +47 -0
- package/examples/sdk/12-redis-sessions.ts +54 -0
- package/examples/sdk/13-sql-sessions.ts +61 -0
- package/examples/sdk/README.md +172 -0
- package/package.json +554 -0
- package/scripts/build-binary.ts +100 -0
- package/scripts/bundle-dist.ts +90 -0
- package/scripts/format-prompts.ts +68 -0
- package/scripts/generate-docs-index.ts +40 -0
- package/scripts/generate-template.ts +33 -0
- package/scripts/omp +42 -0
- package/scripts/omp.ts +19 -0
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +625 -0
- package/src/auto-thinking/classifier.ts +185 -0
- package/src/autoresearch/command-resume.md +14 -0
- package/src/autoresearch/dashboard.ts +436 -0
- package/src/autoresearch/git.ts +319 -0
- package/src/autoresearch/helpers.ts +218 -0
- package/src/autoresearch/index.ts +536 -0
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +103 -0
- package/src/autoresearch/resume-message.md +10 -0
- package/src/autoresearch/state.ts +273 -0
- package/src/autoresearch/storage.ts +699 -0
- package/src/autoresearch/tools/init-experiment.ts +272 -0
- package/src/autoresearch/tools/log-experiment.ts +524 -0
- package/src/autoresearch/tools/run-experiment.ts +407 -0
- package/src/autoresearch/tools/update-notes.ts +109 -0
- package/src/autoresearch/types.ts +168 -0
- package/src/bun-imports.d.ts +28 -0
- package/src/capability/context-file.ts +44 -0
- package/src/capability/extension-module.ts +34 -0
- package/src/capability/extension.ts +47 -0
- package/src/capability/fs.ts +117 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +436 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +74 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule-buckets.ts +66 -0
- package/src/capability/rule.ts +261 -0
- package/src/capability/settings.ts +34 -0
- package/src/capability/skill.ts +63 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/ssh.ts +41 -0
- package/src/capability/system-prompt.ts +34 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +168 -0
- package/src/cli/agents-cli.ts +138 -0
- package/src/cli/args.ts +340 -0
- package/src/cli/auth-broker-cli.ts +895 -0
- package/src/cli/auth-gateway-cli.ts +611 -0
- package/src/cli/classify-install-target.ts +76 -0
- package/src/cli/claude-trace-cli.ts +795 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/completion-gen.ts +550 -0
- package/src/cli/config-cli.ts +418 -0
- package/src/cli/dry-balance-cli.ts +856 -0
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/file-processor.ts +133 -0
- package/src/cli/gallery-cli.ts +230 -0
- package/src/cli/gallery-fixtures/agentic.ts +407 -0
- package/src/cli/gallery-fixtures/codeintel.ts +187 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +220 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +250 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +57 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli/grep-cli.ts +160 -0
- package/src/cli/grievances-cli.ts +256 -0
- package/src/cli/initial-message.ts +58 -0
- package/src/cli/list-models.ts +194 -0
- package/src/cli/plugin-cli.ts +996 -0
- package/src/cli/read-cli.ts +57 -0
- package/src/cli/session-picker.ts +79 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli/shell-cli.ts +176 -0
- package/src/cli/ssh-cli.ts +179 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/cli/stats-cli.ts +238 -0
- package/src/cli/tiny-models-cli.ts +127 -0
- package/src/cli/update-cli.ts +611 -0
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli/web-search-cli.ts +132 -0
- package/src/cli/worktree-cli.ts +291 -0
- package/src/cli-commands.ts +79 -0
- package/src/cli.ts +200 -0
- package/src/commands/acp.ts +24 -0
- package/src/commands/agents.ts +57 -0
- package/src/commands/auth-broker.ts +99 -0
- package/src/commands/auth-gateway.ts +69 -0
- package/src/commands/commit.ts +46 -0
- package/src/commands/complete.ts +66 -0
- package/src/commands/completions.ts +60 -0
- package/src/commands/config.ts +51 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/grep.ts +48 -0
- package/src/commands/grievances.ts +51 -0
- package/src/commands/install.ts +107 -0
- package/src/commands/launch.ts +169 -0
- package/src/commands/plugin.ts +78 -0
- package/src/commands/read.ts +38 -0
- package/src/commands/setup.ts +67 -0
- package/src/commands/shell.ts +29 -0
- package/src/commands/ssh.ts +60 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/tiny-models.ts +36 -0
- package/src/commands/update.ts +21 -0
- package/src/commands/usage.ts +35 -0
- package/src/commands/web-search.ts +42 -0
- package/src/commands/worktree.ts +56 -0
- package/src/commit/agentic/agent.ts +317 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +355 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +25 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +38 -0
- package/src/commit/agentic/state.ts +60 -0
- package/src/commit/agentic/tools/analyze-file.ts +146 -0
- package/src/commit/agentic/tools/git-file-diff.ts +191 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +81 -0
- package/src/commit/agentic/tools/index.ts +54 -0
- package/src/commit/agentic/tools/propose-changelog.ts +144 -0
- package/src/commit/agentic/tools/propose-commit.ts +109 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/schemas.ts +23 -0
- package/src/commit/agentic/tools/split-commit.ts +245 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +183 -0
- package/src/commit/analysis/conventional.ts +64 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +105 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +40 -0
- package/src/commit/changelog/generate.ts +97 -0
- package/src/commit/changelog/index.ts +234 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +85 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +69 -0
- package/src/commit/map-reduce/map-phase.ts +193 -0
- package/src/commit/map-reduce/reduce-phase.ts +49 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +92 -0
- package/src/commit/pipeline.ts +243 -0
- package/src/commit/prompts/analysis-system.md +148 -0
- package/src/commit/prompts/analysis-user.md +38 -0
- package/src/commit/prompts/changelog-system.md +50 -0
- package/src/commit/prompts/changelog-user.md +18 -0
- package/src/commit/prompts/file-observer-system.md +24 -0
- package/src/commit/prompts/file-observer-user.md +8 -0
- package/src/commit/prompts/reduce-system.md +50 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +3 -0
- package/src/commit/prompts/summary-system.md +38 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/shared-llm.ts +77 -0
- package/src/commit/types.ts +118 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/commit/utils.ts +58 -0
- package/src/config/api-key-resolver.ts +60 -0
- package/src/config/append-only-context-mode.ts +31 -0
- package/src/config/config-file.ts +317 -0
- package/src/config/file-lock.ts +164 -0
- package/src/config/keybindings.ts +628 -0
- package/src/config/mcp-schema.json +230 -0
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +2090 -0
- package/src/config/model-resolver.ts +1502 -0
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +226 -0
- package/src/config/models-config.ts +129 -0
- package/src/config/prompt-templates.ts +185 -0
- package/src/config/resolve-config-value.ts +94 -0
- package/src/config/settings-schema.ts +3530 -0
- package/src/config/settings.ts +1178 -0
- package/src/config.ts +242 -0
- package/src/cursor.ts +340 -0
- package/src/dap/client.ts +760 -0
- package/src/dap/config.ts +189 -0
- package/src/dap/defaults.json +212 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1441 -0
- package/src/dap/types.ts +610 -0
- package/src/debug/index.ts +515 -0
- package/src/debug/log-formatting.ts +58 -0
- package/src/debug/log-viewer.ts +908 -0
- package/src/debug/profiler.ts +162 -0
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/raw-sse-buffer.ts +273 -0
- package/src/debug/raw-sse.ts +292 -0
- package/src/debug/report-bundle.ts +374 -0
- package/src/debug/system-info.ts +111 -0
- package/src/debug/terminal-info.ts +124 -0
- package/src/discovery/agents-md.ts +67 -0
- package/src/discovery/agents.ts +230 -0
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-defaults.ts +39 -0
- package/src/discovery/builtin-rules/index.ts +54 -0
- package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
- package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
- package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
- package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
- package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
- package/src/discovery/builtin-rules/rs-result-type.md +19 -0
- package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
- package/src/discovery/builtin-rules/ts-import-type.md +42 -0
- package/src/discovery/builtin-rules/ts-no-any.md +56 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
- package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
- package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/builtin-rules/ts-set-map.md +28 -0
- package/src/discovery/builtin.ts +906 -0
- package/src/discovery/claude-plugins.ts +386 -0
- package/src/discovery/claude.ts +584 -0
- package/src/discovery/cline.ts +83 -0
- package/src/discovery/codex.ts +522 -0
- package/src/discovery/cursor.ts +220 -0
- package/src/discovery/gemini.ts +383 -0
- package/src/discovery/github.ts +154 -0
- package/src/discovery/helpers.ts +1016 -0
- package/src/discovery/index.ts +81 -0
- package/src/discovery/mcp-json.ts +171 -0
- package/src/discovery/omp-extension-roots.ts +190 -0
- package/src/discovery/omp-plugins.ts +383 -0
- package/src/discovery/opencode.ts +398 -0
- package/src/discovery/plugin-dir-roots.ts +28 -0
- package/src/discovery/ssh.ts +153 -0
- package/src/discovery/substitute-plugin-root.ts +29 -0
- package/src/discovery/vscode.ts +105 -0
- package/src/discovery/windsurf.ts +147 -0
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +999 -0
- package/src/edit/file-snapshot-store.ts +91 -0
- package/src/edit/hashline/block-resolver.ts +33 -0
- package/src/edit/hashline/diff.ts +290 -0
- package/src/edit/hashline/execute.ts +242 -0
- package/src/edit/hashline/filesystem.ts +130 -0
- package/src/edit/hashline/index.ts +5 -0
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/edit/hashline/params.ts +18 -0
- package/src/edit/index.ts +571 -0
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +53 -0
- package/src/edit/modes/patch.ts +1891 -0
- package/src/edit/modes/replace.ts +1137 -0
- package/src/edit/normalize.ts +345 -0
- package/src/edit/notebook.ts +242 -0
- package/src/edit/read-file.ts +25 -0
- package/src/edit/renderer.ts +769 -0
- package/src/edit/streaming.ts +517 -0
- package/src/eval/__tests__/agent-bridge.test.ts +708 -0
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/budget-bridge.test.ts +69 -0
- package/src/eval/__tests__/completion-bridge.test.ts +412 -0
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/__tests__/idle-timeout.test.ts +80 -0
- package/src/eval/__tests__/js-context-manager.test.ts +241 -0
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/agent-bridge.ts +319 -0
- package/src/eval/backend.ts +71 -0
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/completion-bridge.ts +207 -0
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/idle-timeout.ts +91 -0
- package/src/eval/index.ts +4 -0
- package/src/eval/js/context-manager.ts +502 -0
- package/src/eval/js/executor.ts +173 -0
- package/src/eval/js/index.ts +51 -0
- package/src/eval/js/shared/helpers.ts +283 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/local-module-loader.ts +342 -0
- package/src/eval/js/shared/prelude.ts +2 -0
- package/src/eval/js/shared/prelude.txt +246 -0
- package/src/eval/js/shared/rewrite-imports.ts +532 -0
- package/src/eval/js/shared/runtime.ts +352 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +162 -0
- package/src/eval/js/worker-core.ts +132 -0
- package/src/eval/js/worker-entry.ts +30 -0
- package/src/eval/js/worker-protocol.ts +47 -0
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +742 -0
- package/src/eval/py/index.ts +68 -0
- package/src/eval/py/kernel.ts +748 -0
- package/src/eval/py/prelude.py +658 -0
- package/src/eval/py/prelude.ts +3 -0
- package/src/eval/py/runner.py +1133 -0
- package/src/eval/py/runtime.ts +276 -0
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/eval/py/tool-bridge.ts +182 -0
- package/src/eval/session-id.ts +8 -0
- package/src/eval/types.ts +48 -0
- package/src/exa/index.ts +2 -0
- package/src/exa/mcp-client.ts +370 -0
- package/src/exa/types.ts +69 -0
- package/src/exec/bash-executor.ts +419 -0
- package/src/exec/exec.ts +53 -0
- package/src/exec/non-interactive-env.ts +48 -0
- package/src/export/custom-share.ts +65 -0
- package/src/export/html/index.ts +164 -0
- package/src/export/html/template.css +1051 -0
- package/src/export/html/template.generated.ts +2 -0
- package/src/export/html/template.html +46 -0
- package/src/export/html/template.js +2271 -0
- package/src/export/html/template.macro.ts +25 -0
- package/src/export/html/vendor/highlight.min.js +1213 -0
- package/src/export/html/vendor/marked.min.js +6 -0
- package/src/export/ttsr.ts +583 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +489 -0
- package/src/extensibility/custom-commands/index.ts +2 -0
- package/src/extensibility/custom-commands/loader.ts +238 -0
- package/src/extensibility/custom-commands/types.ts +113 -0
- package/src/extensibility/custom-tools/index.ts +7 -0
- package/src/extensibility/custom-tools/loader.ts +269 -0
- package/src/extensibility/custom-tools/types.ts +270 -0
- package/src/extensibility/custom-tools/wrapper.ts +47 -0
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/get-commands-handler.ts +78 -0
- package/src/extensibility/extensions/index.ts +16 -0
- package/src/extensibility/extensions/loader.ts +572 -0
- package/src/extensibility/extensions/runner.ts +922 -0
- package/src/extensibility/extensions/types.ts +1322 -0
- package/src/extensibility/extensions/wrapper.ts +223 -0
- package/src/extensibility/hooks/index.ts +5 -0
- package/src/extensibility/hooks/loader.ts +257 -0
- package/src/extensibility/hooks/runner.ts +425 -0
- package/src/extensibility/hooks/tool-wrapper.ts +107 -0
- package/src/extensibility/hooks/types.ts +606 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
- package/src/extensibility/plugins/doctor.ts +65 -0
- package/src/extensibility/plugins/git-url.ts +367 -0
- package/src/extensibility/plugins/index.ts +9 -0
- package/src/extensibility/plugins/installer.ts +192 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +682 -0
- package/src/extensibility/plugins/loader.ts +313 -0
- package/src/extensibility/plugins/manager.ts +827 -0
- package/src/extensibility/plugins/marketplace/cache.ts +136 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +317 -0
- package/src/extensibility/plugins/marketplace/index.ts +6 -0
- package/src/extensibility/plugins/marketplace/manager.ts +770 -0
- package/src/extensibility/plugins/marketplace/registry.ts +196 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
- package/src/extensibility/plugins/marketplace/types.ts +191 -0
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/extensibility/plugins/parser.ts +105 -0
- package/src/extensibility/plugins/types.ts +194 -0
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +312 -0
- package/src/extensibility/slash-commands.ts +227 -0
- package/src/extensibility/tool-proxy.ts +25 -0
- package/src/extensibility/typebox.ts +418 -0
- package/src/extensibility/utils.ts +44 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +528 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +251 -0
- package/src/hindsight/backend.ts +354 -0
- package/src/hindsight/bank.ts +156 -0
- package/src/hindsight/client.ts +598 -0
- package/src/hindsight/config.ts +175 -0
- package/src/hindsight/content.ts +210 -0
- package/src/hindsight/index.ts +8 -0
- package/src/hindsight/mental-models.ts +429 -0
- package/src/hindsight/seeds.json +32 -0
- package/src/hindsight/state.ts +488 -0
- package/src/hindsight/transcript.ts +71 -0
- package/src/index.ts +59 -0
- package/src/internal-urls/agent-protocol.ts +146 -0
- package/src/internal-urls/artifact-protocol.ts +107 -0
- package/src/internal-urls/docs-index.generated.ts +106 -0
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +25 -0
- package/src/internal-urls/issue-pr-protocol.ts +584 -0
- package/src/internal-urls/json-query.ts +126 -0
- package/src/internal-urls/local-protocol.ts +287 -0
- package/src/internal-urls/mcp-protocol.ts +151 -0
- package/src/internal-urls/memory-protocol.ts +169 -0
- package/src/internal-urls/omp-protocol.ts +93 -0
- package/src/internal-urls/parse.ts +72 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +105 -0
- package/src/internal-urls/rule-protocol.ts +45 -0
- package/src/internal-urls/skill-protocol.ts +96 -0
- package/src/internal-urls/types.ts +152 -0
- package/src/internal-urls/vault-protocol.ts +936 -0
- package/src/irc/bus.ts +292 -0
- package/src/lib/xai-http.ts +124 -0
- package/src/lsp/client.ts +1193 -0
- package/src/lsp/clients/biome-client.ts +264 -0
- package/src/lsp/clients/index.ts +50 -0
- package/src/lsp/clients/lsp-linter-client.ts +93 -0
- package/src/lsp/clients/swiftlint-client.ts +120 -0
- package/src/lsp/config.ts +502 -0
- package/src/lsp/defaults.json +493 -0
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/edits.ts +267 -0
- package/src/lsp/index.ts +2477 -0
- package/src/lsp/lspmux.ts +233 -0
- package/src/lsp/render.ts +694 -0
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +455 -0
- package/src/lsp/utils.ts +718 -0
- package/src/main.ts +1325 -0
- package/src/mcp/client.ts +484 -0
- package/src/mcp/config-writer.ts +225 -0
- package/src/mcp/config.ts +365 -0
- package/src/mcp/index.ts +29 -0
- package/src/mcp/json-rpc.ts +122 -0
- package/src/mcp/loader.ts +124 -0
- package/src/mcp/manager.ts +1275 -0
- package/src/mcp/oauth-discovery.ts +442 -0
- package/src/mcp/oauth-flow.ts +442 -0
- package/src/mcp/render.ts +132 -0
- package/src/mcp/smithery-auth.ts +104 -0
- package/src/mcp/smithery-connect.ts +145 -0
- package/src/mcp/smithery-registry.ts +477 -0
- package/src/mcp/timeout.ts +59 -0
- package/src/mcp/tool-bridge.ts +426 -0
- package/src/mcp/tool-cache.ts +117 -0
- package/src/mcp/transports/http.ts +519 -0
- package/src/mcp/transports/index.ts +6 -0
- package/src/mcp/transports/stdio.ts +528 -0
- package/src/mcp/types.ts +423 -0
- package/src/memories/index.ts +1150 -0
- package/src/memories/storage.ts +577 -0
- package/src/memory-backend/index.ts +18 -0
- package/src/memory-backend/local-backend.ts +39 -0
- package/src/memory-backend/off-backend.ts +25 -0
- package/src/memory-backend/resolve.ts +25 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +166 -0
- package/src/mnemopi/backend.ts +547 -0
- package/src/mnemopi/config.ts +160 -0
- package/src/mnemopi/index.ts +3 -0
- package/src/mnemopi/state.ts +584 -0
- package/src/modes/acp/acp-agent.ts +2407 -0
- package/src/modes/acp/acp-client-bridge.ts +154 -0
- package/src/modes/acp/acp-event-mapper.ts +929 -0
- package/src/modes/acp/acp-mode.ts +23 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/agent-dashboard.ts +1206 -0
- package/src/modes/components/agent-hub.ts +1071 -0
- package/src/modes/components/assistant-message.ts +307 -0
- package/src/modes/components/bash-execution.ts +220 -0
- package/src/modes/components/bordered-loader.ts +41 -0
- package/src/modes/components/branch-summary-message.ts +45 -0
- package/src/modes/components/btw-panel.ts +104 -0
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/compaction-summary-message.ts +87 -0
- package/src/modes/components/copy-selector.ts +206 -0
- package/src/modes/components/countdown-timer.ts +75 -0
- package/src/modes/components/custom-editor.ts +398 -0
- package/src/modes/components/custom-message.ts +63 -0
- package/src/modes/components/diff.ts +277 -0
- package/src/modes/components/dynamic-border.ts +34 -0
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/eval-execution.ts +158 -0
- package/src/modes/components/execution-shared.ts +101 -0
- package/src/modes/components/extensions/extension-dashboard.ts +399 -0
- package/src/modes/components/extensions/extension-list.ts +502 -0
- package/src/modes/components/extensions/index.ts +9 -0
- package/src/modes/components/extensions/inspector-panel.ts +317 -0
- package/src/modes/components/extensions/state-manager.ts +627 -0
- package/src/modes/components/extensions/types.ts +186 -0
- package/src/modes/components/footer.ts +274 -0
- package/src/modes/components/history-search.ts +280 -0
- package/src/modes/components/hook-editor.ts +167 -0
- package/src/modes/components/hook-input.ts +87 -0
- package/src/modes/components/hook-message.ts +66 -0
- package/src/modes/components/hook-selector.ts +660 -0
- package/src/modes/components/index.ts +38 -0
- package/src/modes/components/keybinding-hints.ts +65 -0
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/login-dialog.ts +164 -0
- package/src/modes/components/mcp-add-wizard.ts +1340 -0
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1271 -0
- package/src/modes/components/oauth-selector.ts +368 -0
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +820 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/plugin-selector.ts +95 -0
- package/src/modes/components/plugin-settings.ts +722 -0
- package/src/modes/components/queue-mode-selector.ts +56 -0
- package/src/modes/components/read-tool-group.ts +670 -0
- package/src/modes/components/segment-track.ts +52 -0
- package/src/modes/components/session-selector.ts +625 -0
- package/src/modes/components/settings-defs.ts +189 -0
- package/src/modes/components/settings-selector.ts +651 -0
- package/src/modes/components/show-images-selector.ts +45 -0
- package/src/modes/components/skill-message.ts +89 -0
- package/src/modes/components/status-line/component.ts +869 -0
- package/src/modes/components/status-line/context-thresholds.ts +79 -0
- package/src/modes/components/status-line/git-utils.ts +42 -0
- package/src/modes/components/status-line/index.ts +5 -0
- package/src/modes/components/status-line/presets.ts +106 -0
- package/src/modes/components/status-line/segments.ts +584 -0
- package/src/modes/components/status-line/separators.ts +55 -0
- package/src/modes/components/status-line/token-rate.ts +66 -0
- package/src/modes/components/status-line/types.ts +108 -0
- package/src/modes/components/theme-selector.ts +63 -0
- package/src/modes/components/thinking-selector.ts +52 -0
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +19 -0
- package/src/modes/components/todo-reminder.ts +38 -0
- package/src/modes/components/tool-execution.ts +1024 -0
- package/src/modes/components/transcript-container.ts +608 -0
- package/src/modes/components/tree-selector.ts +978 -0
- package/src/modes/components/ttsr-notification.ts +122 -0
- package/src/modes/components/user-message-selector.ts +227 -0
- package/src/modes/components/user-message.ts +66 -0
- package/src/modes/components/visual-truncate.ts +63 -0
- package/src/modes/components/welcome.ts +493 -0
- package/src/modes/controllers/btw-controller.ts +105 -0
- package/src/modes/controllers/command-controller-shared.ts +109 -0
- package/src/modes/controllers/command-controller.ts +1566 -0
- package/src/modes/controllers/event-controller.ts +1054 -0
- package/src/modes/controllers/extension-ui-controller.ts +886 -0
- package/src/modes/controllers/input-controller.ts +1073 -0
- package/src/modes/controllers/mcp-command-controller.ts +2017 -0
- package/src/modes/controllers/omfg-controller.ts +283 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +1108 -0
- package/src/modes/controllers/ssh-command-controller.ts +384 -0
- package/src/modes/controllers/streaming-reveal.ts +279 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/controllers/todo-command-controller.ts +485 -0
- package/src/modes/data/emojis.json +1 -0
- package/src/modes/emoji-autocomplete.ts +285 -0
- package/src/modes/gradient-highlight.ts +87 -0
- package/src/modes/image-references.ts +117 -0
- package/src/modes/index.ts +17 -0
- package/src/modes/interactive-mode.ts +3370 -0
- package/src/modes/internal-url-autocomplete.ts +143 -0
- package/src/modes/loop-limit.ts +140 -0
- package/src/modes/magic-keywords.ts +20 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/oauth-manual-input.ts +69 -0
- package/src/modes/orchestrate.ts +42 -0
- package/src/modes/print-mode.ts +126 -0
- package/src/modes/prompt-action-autocomplete.ts +260 -0
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-client.ts +963 -0
- package/src/modes/rpc/rpc-mode.ts +947 -0
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +458 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +99 -0
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
- package/src/modes/setup-wizard/scenes/outro.ts +35 -0
- package/src/modes/setup-wizard/scenes/providers.ts +69 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +205 -0
- package/src/modes/setup-wizard/scenes/splash.ts +201 -0
- package/src/modes/setup-wizard/scenes/theme.ts +299 -0
- package/src/modes/setup-wizard/scenes/types.ts +48 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +129 -0
- package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
- package/src/modes/shared.ts +47 -0
- package/src/modes/theme/dark.json +95 -0
- package/src/modes/theme/defaults/alabaster.json +93 -0
- package/src/modes/theme/defaults/amethyst.json +96 -0
- package/src/modes/theme/defaults/anthracite.json +93 -0
- package/src/modes/theme/defaults/basalt.json +91 -0
- package/src/modes/theme/defaults/birch.json +95 -0
- package/src/modes/theme/defaults/dark-abyss.json +91 -0
- package/src/modes/theme/defaults/dark-arctic.json +104 -0
- package/src/modes/theme/defaults/dark-aurora.json +95 -0
- package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
- package/src/modes/theme/defaults/dark-cavern.json +91 -0
- package/src/modes/theme/defaults/dark-copper.json +95 -0
- package/src/modes/theme/defaults/dark-cosmos.json +90 -0
- package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
- package/src/modes/theme/defaults/dark-dracula.json +98 -0
- package/src/modes/theme/defaults/dark-eclipse.json +91 -0
- package/src/modes/theme/defaults/dark-ember.json +95 -0
- package/src/modes/theme/defaults/dark-equinox.json +90 -0
- package/src/modes/theme/defaults/dark-forest.json +96 -0
- package/src/modes/theme/defaults/dark-github.json +105 -0
- package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
- package/src/modes/theme/defaults/dark-lavender.json +95 -0
- package/src/modes/theme/defaults/dark-lunar.json +89 -0
- package/src/modes/theme/defaults/dark-midnight.json +95 -0
- package/src/modes/theme/defaults/dark-monochrome.json +94 -0
- package/src/modes/theme/defaults/dark-monokai.json +98 -0
- package/src/modes/theme/defaults/dark-nebula.json +90 -0
- package/src/modes/theme/defaults/dark-nord.json +97 -0
- package/src/modes/theme/defaults/dark-ocean.json +101 -0
- package/src/modes/theme/defaults/dark-one.json +100 -0
- package/src/modes/theme/defaults/dark-poimandres.json +142 -0
- package/src/modes/theme/defaults/dark-rainforest.json +91 -0
- package/src/modes/theme/defaults/dark-reef.json +91 -0
- package/src/modes/theme/defaults/dark-retro.json +92 -0
- package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
- package/src/modes/theme/defaults/dark-sakura.json +95 -0
- package/src/modes/theme/defaults/dark-slate.json +95 -0
- package/src/modes/theme/defaults/dark-solarized.json +97 -0
- package/src/modes/theme/defaults/dark-solstice.json +90 -0
- package/src/modes/theme/defaults/dark-starfall.json +91 -0
- package/src/modes/theme/defaults/dark-sunset.json +99 -0
- package/src/modes/theme/defaults/dark-swamp.json +90 -0
- package/src/modes/theme/defaults/dark-synthwave.json +103 -0
- package/src/modes/theme/defaults/dark-taiga.json +91 -0
- package/src/modes/theme/defaults/dark-terminal.json +95 -0
- package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
- package/src/modes/theme/defaults/dark-tundra.json +91 -0
- package/src/modes/theme/defaults/dark-twilight.json +91 -0
- package/src/modes/theme/defaults/dark-volcanic.json +91 -0
- package/src/modes/theme/defaults/graphite.json +92 -0
- package/src/modes/theme/defaults/index.ts +199 -0
- package/src/modes/theme/defaults/light-arctic.json +107 -0
- package/src/modes/theme/defaults/light-aurora-day.json +91 -0
- package/src/modes/theme/defaults/light-canyon.json +91 -0
- package/src/modes/theme/defaults/light-catppuccin.json +106 -0
- package/src/modes/theme/defaults/light-cirrus.json +90 -0
- package/src/modes/theme/defaults/light-coral.json +95 -0
- package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
- package/src/modes/theme/defaults/light-dawn.json +90 -0
- package/src/modes/theme/defaults/light-dunes.json +91 -0
- package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
- package/src/modes/theme/defaults/light-forest.json +100 -0
- package/src/modes/theme/defaults/light-frost.json +95 -0
- package/src/modes/theme/defaults/light-github.json +115 -0
- package/src/modes/theme/defaults/light-glacier.json +91 -0
- package/src/modes/theme/defaults/light-gruvbox.json +108 -0
- package/src/modes/theme/defaults/light-haze.json +90 -0
- package/src/modes/theme/defaults/light-honeycomb.json +95 -0
- package/src/modes/theme/defaults/light-lagoon.json +91 -0
- package/src/modes/theme/defaults/light-lavender.json +95 -0
- package/src/modes/theme/defaults/light-meadow.json +91 -0
- package/src/modes/theme/defaults/light-mint.json +95 -0
- package/src/modes/theme/defaults/light-monochrome.json +101 -0
- package/src/modes/theme/defaults/light-ocean.json +99 -0
- package/src/modes/theme/defaults/light-one.json +99 -0
- package/src/modes/theme/defaults/light-opal.json +91 -0
- package/src/modes/theme/defaults/light-orchard.json +91 -0
- package/src/modes/theme/defaults/light-paper.json +95 -0
- package/src/modes/theme/defaults/light-poimandres.json +142 -0
- package/src/modes/theme/defaults/light-prism.json +90 -0
- package/src/modes/theme/defaults/light-retro.json +98 -0
- package/src/modes/theme/defaults/light-sand.json +95 -0
- package/src/modes/theme/defaults/light-savanna.json +91 -0
- package/src/modes/theme/defaults/light-solarized.json +102 -0
- package/src/modes/theme/defaults/light-soleil.json +90 -0
- package/src/modes/theme/defaults/light-sunset.json +99 -0
- package/src/modes/theme/defaults/light-synthwave.json +98 -0
- package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
- package/src/modes/theme/defaults/light-wetland.json +91 -0
- package/src/modes/theme/defaults/light-zenith.json +89 -0
- package/src/modes/theme/defaults/limestone.json +94 -0
- package/src/modes/theme/defaults/mahogany.json +97 -0
- package/src/modes/theme/defaults/marble.json +93 -0
- package/src/modes/theme/defaults/obsidian.json +91 -0
- package/src/modes/theme/defaults/onyx.json +91 -0
- package/src/modes/theme/defaults/pearl.json +93 -0
- package/src/modes/theme/defaults/porcelain.json +91 -0
- package/src/modes/theme/defaults/quartz.json +96 -0
- package/src/modes/theme/defaults/sandstone.json +95 -0
- package/src/modes/theme/defaults/titanium.json +90 -0
- package/src/modes/theme/light.json +93 -0
- package/src/modes/theme/mermaid-cache.ts +29 -0
- package/src/modes/theme/shimmer.ts +235 -0
- package/src/modes/theme/theme-schema.json +459 -0
- package/src/modes/theme/theme.ts +2676 -0
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +359 -0
- package/src/modes/ultrathink.ts +41 -0
- package/src/modes/utils/context-usage.ts +339 -0
- package/src/modes/utils/copy-targets.ts +360 -0
- package/src/modes/utils/hotkeys-markdown.ts +61 -0
- package/src/modes/utils/keybinding-matchers.ts +51 -0
- package/src/modes/utils/tools-markdown.ts +27 -0
- package/src/modes/utils/ui-helpers.ts +801 -0
- package/src/modes/workflow.ts +42 -0
- package/src/plan-mode/approved-plan.ts +186 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/plan-mode/state.ts +6 -0
- package/src/priority.json +41 -0
- package/src/prompts/agents/designer.md +66 -0
- package/src/prompts/agents/explore.md +58 -0
- package/src/prompts/agents/frontmatter.md +11 -0
- package/src/prompts/agents/init.md +33 -0
- package/src/prompts/agents/librarian.md +119 -0
- package/src/prompts/agents/oracle.md +55 -0
- package/src/prompts/agents/plan.md +48 -0
- package/src/prompts/agents/reviewer.md +140 -0
- package/src/prompts/agents/task.md +16 -0
- package/src/prompts/ci-green-request.md +36 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +30 -0
- package/src/prompts/memories/read-path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +6 -0
- package/src/prompts/memories/stage_one_system.md +21 -0
- package/src/prompts/review-custom-request.md +22 -0
- package/src/prompts/review-headless-request.md +16 -0
- package/src/prompts/review-request.md +69 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +50 -0
- package/src/prompts/system/agent-creation-user.md +6 -0
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
- package/src/prompts/system/auto-thinking-difficulty.md +12 -0
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/commit-message-system.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +64 -0
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/system/irc-incoming.md +7 -0
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/system/omfg-user.md +50 -0
- package/src/prompts/system/orchestrate-notice.md +40 -0
- package/src/prompts/system/plan-mode-active.md +109 -0
- package/src/prompts/system/plan-mode-approved.md +25 -0
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +11 -0
- package/src/prompts/system/plan-mode-subagent.md +33 -0
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/system/project-prompt.md +52 -0
- package/src/prompts/system/subagent-system-prompt.md +64 -0
- package/src/prompts/system/subagent-user-prompt.md +3 -0
- package/src/prompts/system/subagent-yield-reminder.md +12 -0
- package/src/prompts/system/system-prompt.md +258 -0
- package/src/prompts/system/tiny-title-system.md +8 -0
- package/src/prompts/system/title-system.md +16 -0
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/prompts/system/ttsr-tool-reminder.md +5 -0
- package/src/prompts/system/ultrathink-notice.md +3 -0
- package/src/prompts/system/web-search.md +25 -0
- package/src/prompts/system/workflow-notice.md +70 -0
- package/src/prompts/tools/apply-patch.md +65 -0
- package/src/prompts/tools/ask.md +30 -0
- package/src/prompts/tools/ast-edit.md +39 -0
- package/src/prompts/tools/ast-grep.md +42 -0
- package/src/prompts/tools/async-result.md +8 -0
- package/src/prompts/tools/bash.md +46 -0
- package/src/prompts/tools/browser.md +73 -0
- package/src/prompts/tools/checkpoint.md +16 -0
- package/src/prompts/tools/debug.md +34 -0
- package/src/prompts/tools/eval.md +92 -0
- package/src/prompts/tools/find.md +36 -0
- package/src/prompts/tools/github.md +21 -0
- package/src/prompts/tools/goal.md +18 -0
- package/src/prompts/tools/image-gen.md +7 -0
- package/src/prompts/tools/inspect-image-system.md +20 -0
- package/src/prompts/tools/inspect-image.md +32 -0
- package/src/prompts/tools/irc.md +59 -0
- package/src/prompts/tools/job.md +19 -0
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/lsp.md +42 -0
- package/src/prompts/tools/memory-edit.md +8 -0
- package/src/prompts/tools/patch.md +70 -0
- package/src/prompts/tools/read.md +84 -0
- package/src/prompts/tools/recall.md +5 -0
- package/src/prompts/tools/reflect.md +5 -0
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/prompts/tools/replace.md +30 -0
- package/src/prompts/tools/resolve.md +9 -0
- package/src/prompts/tools/retain.md +6 -0
- package/src/prompts/tools/rewind.md +13 -0
- package/src/prompts/tools/search-tool-bm25.md +32 -0
- package/src/prompts/tools/search.md +24 -0
- package/src/prompts/tools/ssh.md +31 -0
- package/src/prompts/tools/task-summary.md +17 -0
- package/src/prompts/tools/task.md +88 -0
- package/src/prompts/tools/todo.md +62 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +14 -0
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +151 -0
- package/src/sdk.ts +2558 -0
- package/src/secrets/index.ts +123 -0
- package/src/secrets/obfuscator.ts +298 -0
- package/src/secrets/regex.ts +21 -0
- package/src/session/agent-session.ts +10121 -0
- package/src/session/agent-storage.ts +455 -0
- package/src/session/artifacts.ts +135 -0
- package/src/session/auth-broker-config.ts +131 -0
- package/src/session/auth-storage.ts +29 -0
- package/src/session/blob-store.ts +255 -0
- package/src/session/client-bridge.ts +85 -0
- package/src/session/history-storage.ts +348 -0
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/messages.ts +541 -0
- package/src/session/redis-session-storage.ts +170 -0
- package/src/session/session-dump-format.ts +209 -0
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +3676 -0
- package/src/session/session-storage.ts +529 -0
- package/src/session/shake-types.ts +43 -0
- package/src/session/sql-session-storage.ts +314 -0
- package/src/session/streaming-output.ts +1330 -0
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/session/yield-queue.ts +173 -0
- package/src/slash-commands/acp-builtins.ts +70 -0
- package/src/slash-commands/builtin-registry.ts +1798 -0
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +46 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +195 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +95 -0
- package/src/slash-commands/marketplace-install-parser.ts +99 -0
- package/src/slash-commands/types.ts +135 -0
- package/src/ssh/config-writer.ts +183 -0
- package/src/ssh/connection-manager.ts +509 -0
- package/src/ssh/ssh-executor.ts +189 -0
- package/src/ssh/sshfs-mount.ts +140 -0
- package/src/ssh/utils.ts +8 -0
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/stubs/natives/index.ts +814 -0
- package/src/stubs/natives/package.json +7 -0
- package/src/stubs/tui/index.ts +282 -0
- package/src/stubs/tui/package.json +7 -0
- package/src/system-prompt.ts +611 -0
- package/src/task/agents.ts +167 -0
- package/src/task/commands.ts +132 -0
- package/src/task/discovery.ts +122 -0
- package/src/task/executor.ts +2133 -0
- package/src/task/index.ts +1419 -0
- package/src/task/name-generator.ts +1577 -0
- package/src/task/omp-command.ts +26 -0
- package/src/task/output-manager.ts +88 -0
- package/src/task/parallel.ts +116 -0
- package/src/task/render.ts +1381 -0
- package/src/task/repair-args.ts +129 -0
- package/src/task/subprocess-tool-registry.ts +88 -0
- package/src/task/types.ts +336 -0
- package/src/task/worktree.ts +514 -0
- package/src/telemetry-export.ts +144 -0
- package/src/thinking.ts +167 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/device.ts +111 -0
- package/src/tiny/dtype.ts +101 -0
- package/src/tiny/models.ts +242 -0
- package/src/tiny/text.ts +165 -0
- package/src/tiny/title-client.ts +543 -0
- package/src/tiny/title-protocol.ts +56 -0
- package/src/tiny/worker.ts +568 -0
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tool-discovery/tool-index.ts +256 -0
- package/src/tools/approval.ts +189 -0
- package/src/tools/archive-reader.ts +721 -0
- package/src/tools/ask.ts +928 -0
- package/src/tools/ast-edit.ts +642 -0
- package/src/tools/ast-grep.ts +452 -0
- package/src/tools/auto-generated-guard.ts +322 -0
- package/src/tools/bash-command-fixup.ts +37 -0
- package/src/tools/bash-interactive.ts +408 -0
- package/src/tools/bash-interceptor.ts +67 -0
- package/src/tools/bash-pty-selection.ts +14 -0
- package/src/tools/bash-skill-urls.ts +248 -0
- package/src/tools/bash.ts +1386 -0
- package/src/tools/browser/attach.ts +175 -0
- package/src/tools/browser/launch.ts +660 -0
- package/src/tools/browser/readable.ts +112 -0
- package/src/tools/browser/registry.ts +197 -0
- package/src/tools/browser/render.ts +216 -0
- package/src/tools/browser/tab-protocol.ts +105 -0
- package/src/tools/browser/tab-supervisor.ts +628 -0
- package/src/tools/browser/tab-worker-entry.ts +21 -0
- package/src/tools/browser/tab-worker.ts +1226 -0
- package/src/tools/browser.ts +343 -0
- package/src/tools/checkpoint.ts +136 -0
- package/src/tools/conflict-detect.ts +718 -0
- package/src/tools/context.ts +39 -0
- package/src/tools/debug.ts +1067 -0
- package/src/tools/eval-backends.ts +27 -0
- package/src/tools/eval-render.ts +752 -0
- package/src/tools/eval.ts +577 -0
- package/src/tools/fetch.ts +1926 -0
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +609 -0
- package/src/tools/fs-cache-invalidation.ts +28 -0
- package/src/tools/gh-cache-invalidation.ts +255 -0
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +481 -0
- package/src/tools/gh.ts +3720 -0
- package/src/tools/github-cache.ts +637 -0
- package/src/tools/grouped-file-output.ts +210 -0
- package/src/tools/image-gen.ts +1517 -0
- package/src/tools/index.ts +599 -0
- package/src/tools/inspect-image-renderer.ts +132 -0
- package/src/tools/inspect-image.ts +174 -0
- package/src/tools/irc.ts +723 -0
- package/src/tools/job.ts +557 -0
- package/src/tools/json-tree.ts +243 -0
- package/src/tools/jtd-to-json-schema.ts +219 -0
- package/src/tools/jtd-to-typescript.ts +136 -0
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/list-limit.ts +40 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/memory-edit.ts +59 -0
- package/src/tools/memory-recall.ts +100 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +202 -0
- package/src/tools/memory-retain.ts +91 -0
- package/src/tools/output-meta.ts +754 -0
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/path-utils.ts +1054 -0
- package/src/tools/plan-mode-guard.ts +108 -0
- package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
- package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
- package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
- package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
- package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
- package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
- package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
- package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
- package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
- package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
- package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
- package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
- package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
- package/src/tools/read.ts +2929 -0
- package/src/tools/render-mermaid.ts +69 -0
- package/src/tools/render-utils.ts +838 -0
- package/src/tools/renderers.ts +77 -0
- package/src/tools/report-tool-issue.ts +534 -0
- package/src/tools/resolve.ts +276 -0
- package/src/tools/review.ts +253 -0
- package/src/tools/search-tool-bm25.ts +351 -0
- package/src/tools/search.ts +1580 -0
- package/src/tools/sqlite-reader.ts +828 -0
- package/src/tools/ssh.ts +349 -0
- package/src/tools/todo.ts +982 -0
- package/src/tools/tool-errors.ts +62 -0
- package/src/tools/tool-result.ts +94 -0
- package/src/tools/tool-timeouts.ts +30 -0
- package/src/tools/tts.ts +133 -0
- package/src/tools/write.ts +1217 -0
- package/src/tools/yield.ts +269 -0
- package/src/tui/code-cell.ts +216 -0
- package/src/tui/file-list.ts +55 -0
- package/src/tui/hyperlink.ts +175 -0
- package/src/tui/index.ts +12 -0
- package/src/tui/output-block.ts +240 -0
- package/src/tui/status-line.ts +54 -0
- package/src/tui/tree-list.ts +84 -0
- package/src/tui/types.ts +15 -0
- package/src/tui/utils.ts +103 -0
- package/src/utils/block-context.ts +312 -0
- package/src/utils/changelog.ts +132 -0
- package/src/utils/clipboard.ts +193 -0
- package/src/utils/command-args.ts +76 -0
- package/src/utils/commit-message-generator.ts +151 -0
- package/src/utils/edit-mode.ts +41 -0
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/event-bus.ts +33 -0
- package/src/utils/external-editor.ts +65 -0
- package/src/utils/file-display-mode.ts +45 -0
- package/src/utils/file-mentions.ts +281 -0
- package/src/utils/git.ts +1833 -0
- package/src/utils/image-loading.ts +132 -0
- package/src/utils/image-resize.ts +309 -0
- package/src/utils/jj.ts +248 -0
- package/src/utils/lang-from-path.ts +239 -0
- package/src/utils/markit.ts +89 -0
- package/src/utils/open.ts +55 -0
- package/src/utils/session-color.ts +68 -0
- package/src/utils/shell-snapshot.ts +187 -0
- package/src/utils/sixel.ts +69 -0
- package/src/utils/title-generator.ts +373 -0
- package/src/utils/tool-choice.ts +33 -0
- package/src/utils/tools-manager.ts +363 -0
- package/src/web/kagi.ts +305 -0
- package/src/web/parallel.ts +353 -0
- package/src/web/scrapers/artifacthub.ts +207 -0
- package/src/web/scrapers/arxiv.ts +83 -0
- package/src/web/scrapers/aur.ts +162 -0
- package/src/web/scrapers/biorxiv.ts +133 -0
- package/src/web/scrapers/bluesky.ts +262 -0
- package/src/web/scrapers/brew.ts +172 -0
- package/src/web/scrapers/cheatsh.ts +68 -0
- package/src/web/scrapers/chocolatey.ts +196 -0
- package/src/web/scrapers/choosealicense.ts +95 -0
- package/src/web/scrapers/cisa-kev.ts +87 -0
- package/src/web/scrapers/clojars.ts +154 -0
- package/src/web/scrapers/coingecko.ts +177 -0
- package/src/web/scrapers/crates-io.ts +97 -0
- package/src/web/scrapers/crossref.ts +136 -0
- package/src/web/scrapers/devto.ts +147 -0
- package/src/web/scrapers/discogs.ts +306 -0
- package/src/web/scrapers/discourse.ts +197 -0
- package/src/web/scrapers/dockerhub.ts +138 -0
- package/src/web/scrapers/docs-rs.ts +653 -0
- package/src/web/scrapers/fdroid.ts +134 -0
- package/src/web/scrapers/firefox-addons.ts +191 -0
- package/src/web/scrapers/flathub.ts +223 -0
- package/src/web/scrapers/github-gist.ts +58 -0
- package/src/web/scrapers/github.ts +704 -0
- package/src/web/scrapers/gitlab.ts +401 -0
- package/src/web/scrapers/go-pkg.ts +266 -0
- package/src/web/scrapers/hackage.ts +140 -0
- package/src/web/scrapers/hackernews.ts +189 -0
- package/src/web/scrapers/hex.ts +105 -0
- package/src/web/scrapers/huggingface.ts +321 -0
- package/src/web/scrapers/iacr.ts +89 -0
- package/src/web/scrapers/index.ts +252 -0
- package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
- package/src/web/scrapers/lemmy.ts +203 -0
- package/src/web/scrapers/lobsters.ts +175 -0
- package/src/web/scrapers/mastodon.ts +292 -0
- package/src/web/scrapers/maven.ts +138 -0
- package/src/web/scrapers/mdn.ts +173 -0
- package/src/web/scrapers/metacpan.ts +222 -0
- package/src/web/scrapers/musicbrainz.ts +250 -0
- package/src/web/scrapers/npm.ts +98 -0
- package/src/web/scrapers/nuget.ts +183 -0
- package/src/web/scrapers/nvd.ts +222 -0
- package/src/web/scrapers/ollama.ts +239 -0
- package/src/web/scrapers/open-vsx.ts +106 -0
- package/src/web/scrapers/opencorporates.ts +292 -0
- package/src/web/scrapers/openlibrary.ts +336 -0
- package/src/web/scrapers/orcid.ts +286 -0
- package/src/web/scrapers/osv.ts +176 -0
- package/src/web/scrapers/packagist.ts +160 -0
- package/src/web/scrapers/pub-dev.ts +143 -0
- package/src/web/scrapers/pubmed.ts +211 -0
- package/src/web/scrapers/pypi.ts +112 -0
- package/src/web/scrapers/rawg.ts +110 -0
- package/src/web/scrapers/readthedocs.ts +120 -0
- package/src/web/scrapers/reddit.ts +95 -0
- package/src/web/scrapers/repology.ts +251 -0
- package/src/web/scrapers/rfc.ts +201 -0
- package/src/web/scrapers/rubygems.ts +103 -0
- package/src/web/scrapers/searchcode.ts +189 -0
- package/src/web/scrapers/sec-edgar.ts +261 -0
- package/src/web/scrapers/semantic-scholar.ts +171 -0
- package/src/web/scrapers/snapcraft.ts +187 -0
- package/src/web/scrapers/sourcegraph.ts +336 -0
- package/src/web/scrapers/spdx.ts +108 -0
- package/src/web/scrapers/spotify.ts +198 -0
- package/src/web/scrapers/stackoverflow.ts +120 -0
- package/src/web/scrapers/terraform.ts +277 -0
- package/src/web/scrapers/tldr.ts +47 -0
- package/src/web/scrapers/twitter.ts +94 -0
- package/src/web/scrapers/types.ts +397 -0
- package/src/web/scrapers/utils.ts +109 -0
- package/src/web/scrapers/vimeo.ts +133 -0
- package/src/web/scrapers/vscode-marketplace.ts +187 -0
- package/src/web/scrapers/w3c.ts +156 -0
- package/src/web/scrapers/wikidata.ts +344 -0
- package/src/web/scrapers/wikipedia.ts +84 -0
- package/src/web/scrapers/youtube.ts +325 -0
- package/src/web/search/index.ts +292 -0
- package/src/web/search/provider.ts +157 -0
- package/src/web/search/providers/anthropic.ts +318 -0
- package/src/web/search/providers/base.ts +89 -0
- package/src/web/search/providers/brave.ts +152 -0
- package/src/web/search/providers/codex.ts +591 -0
- package/src/web/search/providers/exa.ts +400 -0
- package/src/web/search/providers/gemini.ts +460 -0
- package/src/web/search/providers/jina.ts +111 -0
- package/src/web/search/providers/kagi.ts +86 -0
- package/src/web/search/providers/kimi.ts +196 -0
- package/src/web/search/providers/parallel.ts +225 -0
- package/src/web/search/providers/perplexity.ts +730 -0
- package/src/web/search/providers/searxng.ts +313 -0
- package/src/web/search/providers/synthetic.ts +114 -0
- package/src/web/search/providers/tavily.ts +176 -0
- package/src/web/search/providers/utils.ts +128 -0
- package/src/web/search/providers/zai.ts +333 -0
- package/src/web/search/render.ts +262 -0
- package/src/web/search/types.ts +482 -0
- package/src/web/search/utils.ts +17 -0
- 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(/</g, "<")
|
|
541
|
+
.replace(/>/g, ">")
|
|
542
|
+
.replace(/&/g, "&")
|
|
543
|
+
.replace(/"/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
|
+
}
|