@herbertgao/pi-extensions 2026.8.1 → 2026.8.3
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/README.md +13 -3
- package/node_modules/@czottmann/pi-automode/README.md +16 -2
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +35 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +18 -9
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +6 -2
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +6 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +11 -7
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +100 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +32 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +78 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +16 -4
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +23 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +22 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +18 -2
- package/node_modules/@czottmann/pi-automode/package.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +23 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +99 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +30 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +16 -0
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +32 -0
- package/node_modules/@dietrichgebert/ponytail/LICENSE +21 -0
- package/node_modules/@dietrichgebert/ponytail/README.es.md +298 -0
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +315 -0
- package/node_modules/@dietrichgebert/ponytail/README.md +351 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +21 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +62 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +115 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +44 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +21 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +96 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +169 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +98 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +130 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +85 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.ps1 +24 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.sh +18 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +77 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +26 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +47 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +211 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/package.json +8 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +60 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +120 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +41 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +50 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +71 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +57 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +6 -10
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +5 -9
- package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +101 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +1 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/{utils/agent-summary.ts → feature/agent-summary/core.ts} +37 -35
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{agent-summary.ts → agent-summary/index.ts} +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{session-reference.ts → reference/index.ts} +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{agent-autocomplete.ts → reference/subagent.ts} +19 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{pi-startup-header.ts → shell/startup-header.ts} +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{working-message.ts → shell/working-message.ts} +5 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +32 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +452 -94
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +727 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +66 -776
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/{feature → renderer}/markdown-enhance.ts +134 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +130 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{mouse-interaction.ts → mouse/interaction.ts} +90 -568
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +144 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +143 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +195 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/ansi-utils.ts +56 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/diff-renderer.ts +4 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +12 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/write-execution.ts +1 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-grouping.ts → tool/grouping.ts} +27 -10
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-result.ts → tool/result.ts} +4 -13
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +91 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +0 -27
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -0
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
- package/node_modules/@herbertgao/pi-stash/README.md +9 -9
- package/node_modules/@herbertgao/pi-stash/package.json +1 -1
- package/node_modules/@herbertgao/pi-stash/src/index.ts +2 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +20 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +6 -2
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +1 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +3 -1
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +85 -11
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +61 -39
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +26 -35
- package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +15 -1
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +12 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +2 -0
- package/node_modules/@narumitw/pi-btw/LICENSE +21 -0
- package/node_modules/@narumitw/pi-btw/README.md +210 -0
- package/node_modules/@narumitw/pi-btw/package.json +53 -0
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +632 -0
- package/node_modules/@narumitw/pi-btw/src/btw.ts +996 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +339 -0
- package/node_modules/@narumitw/pi-btw/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +296 -0
- package/node_modules/@narumitw/pi-btw/src/settings.ts +237 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +238 -0
- package/node_modules/@narumitw/pi-btw/src/text.ts +10 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +620 -0
- package/node_modules/@pi-plugins/fast-mode/LICENSE +21 -0
- package/node_modules/@pi-plugins/fast-mode/README.md +92 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts +6 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +2026 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/package.json +73 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +80 -16
- package/node_modules/pi-mcp-adapter/agent-dir.ts +40 -0
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +377 -0
- package/node_modules/pi-mcp-adapter/commands.ts +21 -9
- package/node_modules/pi-mcp-adapter/config.ts +110 -49
- package/node_modules/pi-mcp-adapter/direct-tools.ts +40 -27
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +37 -36
- package/node_modules/pi-mcp-adapter/errors.ts +21 -17
- package/node_modules/pi-mcp-adapter/host-html-template.ts +13 -7
- package/node_modules/pi-mcp-adapter/index.ts +52 -17
- package/node_modules/pi-mcp-adapter/init.ts +9 -7
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +2 -2
- package/node_modules/pi-mcp-adapter/lifecycle.ts +8 -8
- package/node_modules/pi-mcp-adapter/logger.ts +1 -1
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +122 -25
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +120 -2
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +138 -61
- package/node_modules/pi-mcp-adapter/mcp-code.ts +13 -11
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +62 -17
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +15 -11
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +34 -15
- package/node_modules/pi-mcp-adapter/mcp-probe.ts +131 -34
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +2 -2
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +7 -4
- package/node_modules/pi-mcp-adapter/mcp-trace.ts +59 -64
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +34 -26
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +6 -4
- package/node_modules/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/node_modules/pi-mcp-adapter/oauth.ts +48 -0
- package/node_modules/pi-mcp-adapter/onboarding-state.ts +19 -11
- package/node_modules/pi-mcp-adapter/package.json +20 -9
- package/node_modules/pi-mcp-adapter/panel-keys.ts +20 -4
- package/node_modules/pi-mcp-adapter/prompts.ts +2 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +56 -21
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +22 -15
- package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +165 -113
- package/node_modules/pi-mcp-adapter/session-recovery.ts +11 -12
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +3 -3
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +67 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -5
- package/node_modules/pi-mcp-adapter/tool-registrar.ts +187 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +14 -2
- package/node_modules/pi-mcp-adapter/ts-shape.ts +9 -2
- package/node_modules/pi-mcp-adapter/types.ts +111 -22
- package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +40 -0
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +28 -19
- package/node_modules/pi-mcp-adapter/ui-server.ts +90 -58
- package/node_modules/pi-mcp-adapter/ui-session.ts +16 -14
- package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +4 -4
- package/node_modules/pi-mcp-adapter/utils.ts +23 -9
- package/node_modules/pi-web-access/CHANGELOG.md +45 -0
- package/node_modules/pi-web-access/README.md +104 -29
- package/node_modules/pi-web-access/bocha.ts +221 -0
- package/node_modules/pi-web-access/curator-page.ts +113 -12
- package/node_modules/pi-web-access/curator-server.ts +56 -20
- package/node_modules/pi-web-access/datalab-pdf-extract.ts +568 -0
- package/node_modules/pi-web-access/duckduckgo.ts +123 -0
- package/node_modules/pi-web-access/extract.ts +288 -143
- package/node_modules/pi-web-access/feature-config.ts +29 -0
- package/node_modules/pi-web-access/gemini-api.ts +4 -0
- package/node_modules/pi-web-access/gemini-search.ts +45 -12
- package/node_modules/pi-web-access/gemini-web.ts +68 -3
- package/node_modules/pi-web-access/index.ts +160 -71
- package/node_modules/pi-web-access/jina-search.ts +284 -0
- package/node_modules/pi-web-access/kagi.ts +12 -12
- package/node_modules/pi-web-access/openai-search.ts +12 -2
- package/node_modules/pi-web-access/package.json +4 -3
- package/node_modules/pi-web-access/pdf-extract.ts +327 -228
- package/node_modules/pi-web-access/storage.ts +440 -2
- package/node_modules/pi-web-access/summary-model-scope.ts +1 -1
- package/node_modules/pi-web-access/summary-review.ts +4 -2
- package/node_modules/pi-web-access/video-extract.ts +10 -8
- package/node_modules/pi-web-access/xai-search.ts +12 -2
- package/node_modules/pi-web-access/youtube-extract.ts +2 -1
- package/node_modules/remote-pi/LICENSE +21 -0
- package/node_modules/remote-pi/README.md +675 -0
- package/node_modules/remote-pi/dist/actions/handlers.d.ts +122 -0
- package/node_modules/remote-pi/dist/actions/handlers.js +160 -0
- package/node_modules/remote-pi/dist/actions/handlers.js.map +1 -0
- package/node_modules/remote-pi/dist/actions/registry.d.ts +39 -0
- package/node_modules/remote-pi/dist/actions/registry.js +58 -0
- package/node_modules/remote-pi/dist/actions/registry.js.map +1 -0
- package/node_modules/remote-pi/dist/bin/supervisord.d.ts +2 -0
- package/node_modules/remote-pi/dist/bin/supervisord.js +86 -0
- package/node_modules/remote-pi/dist/bin/supervisord.js.map +1 -0
- package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +58 -0
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js +71 -0
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +1 -0
- package/node_modules/remote-pi/dist/commands/list_commands.d.ts +60 -0
- package/node_modules/remote-pi/dist/commands/list_commands.js +73 -0
- package/node_modules/remote-pi/dist/commands/list_commands.js.map +1 -0
- package/node_modules/remote-pi/dist/config.d.ts +62 -0
- package/node_modules/remote-pi/dist/config.js +115 -0
- package/node_modules/remote-pi/dist/config.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/client.d.ts +20 -0
- package/node_modules/remote-pi/dist/daemon/client.js +131 -0
- package/node_modules/remote-pi/dist/daemon/client.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +176 -0
- package/node_modules/remote-pi/dist/daemon/control_protocol.js +63 -0
- package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +45 -0
- package/node_modules/remote-pi/dist/daemon/cron_log.js +72 -0
- package/node_modules/remote-pi/dist/daemon/cron_log.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +80 -0
- package/node_modules/remote-pi/dist/daemon/cron_registry.js +194 -0
- package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/id.d.ts +24 -0
- package/node_modules/remote-pi/dist/daemon/id.js +36 -0
- package/node_modules/remote-pi/dist/daemon/id.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/install.d.ts +170 -0
- package/node_modules/remote-pi/dist/daemon/install.js +547 -0
- package/node_modules/remote-pi/dist/daemon/install.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/registry.d.ts +63 -0
- package/node_modules/remote-pi/dist/daemon/registry.js +153 -0
- package/node_modules/remote-pi/dist/daemon/registry.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +194 -0
- package/node_modules/remote-pi/dist/daemon/rpc_child.js +389 -0
- package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +1 -0
- package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +93 -0
- package/node_modules/remote-pi/dist/daemon/supervisor.js +596 -0
- package/node_modules/remote-pi/dist/daemon/supervisor.js.map +1 -0
- package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +26 -0
- package/node_modules/remote-pi/dist/extension_ui_bridge.js +341 -0
- package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +1 -0
- package/node_modules/remote-pi/dist/index.d.ts +213 -0
- package/node_modules/remote-pi/dist/index.js +4996 -0
- package/node_modules/remote-pi/dist/index.js.map +1 -0
- package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +10 -0
- package/node_modules/remote-pi/dist/mcp/mesh_result.js +42 -0
- package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +1 -0
- package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +16 -0
- package/node_modules/remote-pi/dist/mcp/mesh_server.js +327 -0
- package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/canonical.d.ts +30 -0
- package/node_modules/remote-pi/dist/mesh/canonical.js +61 -0
- package/node_modules/remote-pi/dist/mesh/canonical.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/client.d.ts +18 -0
- package/node_modules/remote-pi/dist/mesh/client.js +125 -0
- package/node_modules/remote-pi/dist/mesh/client.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/encoding.d.ts +38 -0
- package/node_modules/remote-pi/dist/mesh/encoding.js +216 -0
- package/node_modules/remote-pi/dist/mesh/encoding.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +73 -0
- package/node_modules/remote-pi/dist/mesh/self_revoke.js +401 -0
- package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/siblings.d.ts +49 -0
- package/node_modules/remote-pi/dist/mesh/siblings.js +159 -0
- package/node_modules/remote-pi/dist/mesh/siblings.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/types.d.ts +34 -0
- package/node_modules/remote-pi/dist/mesh/types.js +11 -0
- package/node_modules/remote-pi/dist/mesh/types.js.map +1 -0
- package/node_modules/remote-pi/dist/mesh/verify.d.ts +17 -0
- package/node_modules/remote-pi/dist/mesh/verify.js +86 -0
- package/node_modules/remote-pi/dist/mesh/verify.js.map +1 -0
- package/node_modules/remote-pi/dist/pairing/crypto.d.ts +8 -0
- package/node_modules/remote-pi/dist/pairing/crypto.js +22 -0
- package/node_modules/remote-pi/dist/pairing/crypto.js.map +1 -0
- package/node_modules/remote-pi/dist/pairing/handshake.d.ts +28 -0
- package/node_modules/remote-pi/dist/pairing/handshake.js +113 -0
- package/node_modules/remote-pi/dist/pairing/handshake.js.map +1 -0
- package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +16 -0
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js +103 -0
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +1 -0
- package/node_modules/remote-pi/dist/pairing/qr.d.ts +59 -0
- package/node_modules/remote-pi/dist/pairing/qr.js +125 -0
- package/node_modules/remote-pi/dist/pairing/qr.js.map +1 -0
- package/node_modules/remote-pi/dist/pairing/storage.d.ts +120 -0
- package/node_modules/remote-pi/dist/pairing/storage.js +604 -0
- package/node_modules/remote-pi/dist/pairing/storage.js.map +1 -0
- package/node_modules/remote-pi/dist/protocol/codec.d.ts +7 -0
- package/node_modules/remote-pi/dist/protocol/codec.js +49 -0
- package/node_modules/remote-pi/dist/protocol/codec.js.map +1 -0
- package/node_modules/remote-pi/dist/protocol/types.d.ts +393 -0
- package/node_modules/remote-pi/dist/protocol/types.js +2 -0
- package/node_modules/remote-pi/dist/protocol/types.js.map +1 -0
- package/node_modules/remote-pi/dist/rooms.d.ts +29 -0
- package/node_modules/remote-pi/dist/rooms.js +57 -0
- package/node_modules/remote-pi/dist/rooms.js.map +1 -0
- package/node_modules/remote-pi/dist/session/address.d.ts +49 -0
- package/node_modules/remote-pi/dist/session/address.js +58 -0
- package/node_modules/remote-pi/dist/session/address.js.map +1 -0
- package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +55 -0
- package/node_modules/remote-pi/dist/session/agent_bridge.js +146 -0
- package/node_modules/remote-pi/dist/session/agent_bridge.js.map +1 -0
- package/node_modules/remote-pi/dist/session/bridge.d.ts +37 -0
- package/node_modules/remote-pi/dist/session/bridge.js +130 -0
- package/node_modules/remote-pi/dist/session/bridge.js.map +1 -0
- package/node_modules/remote-pi/dist/session/broker.d.ts +189 -0
- package/node_modules/remote-pi/dist/session/broker.js +507 -0
- package/node_modules/remote-pi/dist/session/broker.js.map +1 -0
- package/node_modules/remote-pi/dist/session/broker_remote.d.ts +74 -0
- package/node_modules/remote-pi/dist/session/broker_remote.js +583 -0
- package/node_modules/remote-pi/dist/session/broker_remote.js.map +1 -0
- package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +33 -0
- package/node_modules/remote-pi/dist/session/cwd_lock.js +126 -0
- package/node_modules/remote-pi/dist/session/cwd_lock.js.map +1 -0
- package/node_modules/remote-pi/dist/session/envelope.d.ts +33 -0
- package/node_modules/remote-pi/dist/session/envelope.js +117 -0
- package/node_modules/remote-pi/dist/session/envelope.js.map +1 -0
- package/node_modules/remote-pi/dist/session/global_config.d.ts +33 -0
- package/node_modules/remote-pi/dist/session/global_config.js +73 -0
- package/node_modules/remote-pi/dist/session/global_config.js.map +1 -0
- package/node_modules/remote-pi/dist/session/ipc.d.ts +27 -0
- package/node_modules/remote-pi/dist/session/ipc.js +22 -0
- package/node_modules/remote-pi/dist/session/ipc.js.map +1 -0
- package/node_modules/remote-pi/dist/session/leader_election.d.ts +32 -0
- package/node_modules/remote-pi/dist/session/leader_election.js +106 -0
- package/node_modules/remote-pi/dist/session/leader_election.js.map +1 -0
- package/node_modules/remote-pi/dist/session/local_config.d.ts +55 -0
- package/node_modules/remote-pi/dist/session/local_config.js +157 -0
- package/node_modules/remote-pi/dist/session/local_config.js.map +1 -0
- package/node_modules/remote-pi/dist/session/mesh_message.d.ts +20 -0
- package/node_modules/remote-pi/dist/session/mesh_message.js +28 -0
- package/node_modules/remote-pi/dist/session/mesh_message.js.map +1 -0
- package/node_modules/remote-pi/dist/session/mesh_node.d.ts +116 -0
- package/node_modules/remote-pi/dist/session/mesh_node.js +573 -0
- package/node_modules/remote-pi/dist/session/mesh_node.js.map +1 -0
- package/node_modules/remote-pi/dist/session/peer.d.ts +139 -0
- package/node_modules/remote-pi/dist/session/peer.js +451 -0
- package/node_modules/remote-pi/dist/session/peer.js.map +1 -0
- package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +13 -0
- package/node_modules/remote-pi/dist/session/peer_inventory.js +48 -0
- package/node_modules/remote-pi/dist/session/peer_inventory.js.map +1 -0
- package/node_modules/remote-pi/dist/session/peer_limits.d.ts +9 -0
- package/node_modules/remote-pi/dist/session/peer_limits.js +21 -0
- package/node_modules/remote-pi/dist/session/peer_limits.js.map +1 -0
- package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +39 -0
- package/node_modules/remote-pi/dist/session/setup_wizard.js +63 -0
- package/node_modules/remote-pi/dist/session/setup_wizard.js.map +1 -0
- package/node_modules/remote-pi/dist/session/tool_gate.d.ts +5 -0
- package/node_modules/remote-pi/dist/session/tool_gate.js +11 -0
- package/node_modules/remote-pi/dist/session/tool_gate.js.map +1 -0
- package/node_modules/remote-pi/dist/session/tools.d.ts +24 -0
- package/node_modules/remote-pi/dist/session/tools.js +252 -0
- package/node_modules/remote-pi/dist/session/tools.js.map +1 -0
- package/node_modules/remote-pi/dist/session/wizard.d.ts +13 -0
- package/node_modules/remote-pi/dist/session/wizard.js +20 -0
- package/node_modules/remote-pi/dist/session/wizard.js.map +1 -0
- package/node_modules/remote-pi/dist/settings.d.ts +15 -0
- package/node_modules/remote-pi/dist/settings.js +52 -0
- package/node_modules/remote-pi/dist/settings.js.map +1 -0
- package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +37 -0
- package/node_modules/remote-pi/dist/transport/peer_channel.js +98 -0
- package/node_modules/remote-pi/dist/transport/peer_channel.js.map +1 -0
- package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +29 -0
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js +62 -0
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +1 -0
- package/node_modules/remote-pi/dist/transport/relay_client.d.ts +91 -0
- package/node_modules/remote-pi/dist/transport/relay_client.js +206 -0
- package/node_modules/remote-pi/dist/transport/relay_client.js.map +1 -0
- package/node_modules/remote-pi/dist/ui/footer.d.ts +32 -0
- package/node_modules/remote-pi/dist/ui/footer.js +34 -0
- package/node_modules/remote-pi/dist/ui/footer.js.map +1 -0
- package/node_modules/remote-pi/docs/daemon.md +289 -0
- package/node_modules/remote-pi/package.json +88 -0
- package/node_modules/remote-pi/service-templates/launchd.plist.template +35 -0
- package/node_modules/remote-pi/service-templates/systemd.service.template +19 -0
- package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +10 -0
- package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +38 -0
- package/node_modules/remote-pi/skills/agent-network/SKILL.md +310 -0
- package/package.json +37 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-diff/write-display-utils.ts +0 -29
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{session-reference-core.ts → reference/session.ts} +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/{pi-aliases.ts → shell/aliases.ts} +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/ATTRIBUTION.md +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/diff-presentation.ts +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/index.ts +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/render-utils.ts +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/shiki-highlight.ts +0 -0
- /package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{tool-diff → tool/diff}/types.ts +0 -0
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
# Pi Web Access
|
|
6
6
|
|
|
7
|
-
**Web search, content extraction, and video understanding for Pi agent. OpenAI/Codex search, zero-config Exa search, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, SERPdive, Kagi, Ollama, AnySearch, xAI/Grok, Bright Data SERP, SerpBase, self-hosted SearXNG, optional browser-cookie Gemini Web, or bring your own API keys.**
|
|
7
|
+
**Web search, content extraction, and video understanding for Pi agent. OpenAI/Codex search, zero-config Exa search, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, xAI/Grok, Bright Data SERP, SerpBase, self-hosted SearXNG, keyless DuckDuckGo, optional browser-cookie Gemini Web, or bring your own API keys.**
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/pi-web-access)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[]()
|
|
12
12
|
|
|
13
|
-
https://github.com/user-attachments/assets/cac6a17a-1eeb-4dde-9818-cdf85d8ea98f
|
|
13
|
+
<https://github.com/user-attachments/assets/cac6a17a-1eeb-4dde-9818-cdf85d8ea98f>
|
|
14
14
|
|
|
15
15
|
## Why Pi Web Access
|
|
16
16
|
|
|
17
|
-
**Zero Config** — Works out of the box with Exa MCP (no API key needed). If you're signed into Pi with a Codex subscription, OpenAI web search can reuse that auth. Add API keys for OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, SERPdive, Kagi, Ollama, SerpBase, Exa, Perplexity, or Gemini API for more control; configure a self-hosted SearXNG endpoint for private search; or opt into browser-cookie access for Gemini Web.
|
|
17
|
+
**Zero Config** — Works out of the box with Exa MCP (no API key needed). If you're signed into Pi with a Codex subscription, OpenAI web search can reuse that auth. Add API keys for OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, SerpBase, Exa, Perplexity, or Gemini API for more control; configure a self-hosted SearXNG endpoint for private search; or opt into browser-cookie access for Gemini Web.
|
|
18
18
|
|
|
19
19
|
**Video Understanding** — Point it at a YouTube video or local screen recording and ask questions about what's on screen. Full transcripts, visual descriptions, and frame extraction at exact timestamps.
|
|
20
20
|
|
|
21
|
-
**Smart Fallbacks** — Every capability has a fallback chain. Search tries configured SearXNG first for local/private search, then OpenAI when suitable and available, Exa, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, SERPdive, Kagi, Ollama, Perplexity, Gemini API, and Gemini Web when browser cookies are enabled. With no SearXNG configured, the existing zero-config order is unchanged. YouTube tries Gemini Web when enabled, then API, then Perplexity. Blocked pages try configured self-hosted Firecrawl first
|
|
21
|
+
**Smart Fallbacks** — Every capability has a fallback chain. Search tries configured SearXNG first for local/private search, then OpenAI when suitable and available, Exa, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, and Gemini Web when browser cookies are enabled. With no SearXNG configured, the existing zero-config order is unchanged. YouTube tries Gemini Web when enabled, then API, then Perplexity. Blocked pages try configured self-hosted Firecrawl first. Third-party hosted page fetchers require explicit `fetchRouting.allowRemoteHostedProviders` opt-in for remote HTTP(S) targets.
|
|
22
22
|
|
|
23
23
|
**GitHub Cloning** — GitHub URLs are cloned locally instead of scraped. The agent gets real file contents and a local path to explore, not rendered HTML.
|
|
24
24
|
|
|
@@ -39,12 +39,14 @@ Works immediately with no API keys — Exa MCP provides zero-config search. If P
|
|
|
39
39
|
"search1apiApiKey": "...",
|
|
40
40
|
"searchinfinityApiKey": "...",
|
|
41
41
|
"queritApiKey": "...",
|
|
42
|
+
"jinaApiKey": "jina_...",
|
|
43
|
+
"bochaApiKey": "sk-...",
|
|
42
44
|
"perplexityApiKey": "pplx-...",
|
|
43
45
|
"geminiApiKey": "AIza..."
|
|
44
46
|
}
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
In `auto` mode (default), `web_search` tries a configured SearXNG endpoint first for local/private search, then OpenAI when suitable and available, Exa (direct API if keyed, MCP if not), Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, SERPdive, Perplexity, Gemini API, and Gemini Web when browser-cookie access is enabled. With no SearXNG configured, the existing zero-config order is unchanged. Exa handles search; curator summary drafts are generated separately by the configured Pi summary model. Slow summary drafts fall back to a deterministic result summary after a bounded deadline.
|
|
49
|
+
In `auto` mode (default), `web_search` tries a configured SearXNG endpoint first for local/private search, then OpenAI when suitable and available, Exa (direct API if keyed, MCP if not), Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Perplexity, Gemini API, and Gemini Web when browser-cookie access is enabled. With no SearXNG configured, the existing zero-config order is unchanged. Exa handles search; curator summary drafts are generated separately by the configured Pi summary model. Slow summary drafts fall back to a deterministic result summary after a bounded deadline.
|
|
48
50
|
|
|
49
51
|
If your OpenAI key belongs to a third-party Responses-compatible gateway, set `openaiResponsesUrl` to that gateway's full Responses endpoint. The default remains `https://api.openai.com/v1/responses`.
|
|
50
52
|
|
|
@@ -94,7 +96,7 @@ fetch_content({ url: "/path/to/recording.mp4", prompt: "What error appears on sc
|
|
|
94
96
|
|
|
95
97
|
### web_search
|
|
96
98
|
|
|
97
|
-
Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, SERPdive, Kagi, Ollama, AnySearch, xAI, Bright Data SERP, SerpBase, self-hosted SearXNG, Exa, Perplexity AI, or Gemini. Returns a synthesized answer with source citations.
|
|
99
|
+
Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, xAI, Bright Data SERP, SerpBase, self-hosted SearXNG, keyless DuckDuckGo, Exa, Perplexity AI, or Gemini. Returns a synthesized answer with source citations.
|
|
98
100
|
|
|
99
101
|
```typescript
|
|
100
102
|
web_search({ query: "rust async programming" })
|
|
@@ -110,12 +112,12 @@ web_search({ queries: ["query 1", "query 2"], workflow: "auto-summary" })
|
|
|
110
112
|
```
|
|
111
113
|
|
|
112
114
|
| Parameter | Description |
|
|
113
|
-
|
|
115
|
+
| ----------- | ------------- |
|
|
114
116
|
| `query` / `queries` | Single query or batch of queries |
|
|
115
117
|
| `numResults` | Results per query (default: 5, max: 20) |
|
|
116
118
|
| `recencyFilter` | `day`, `week`, `month`, or `year` |
|
|
117
119
|
| `domainFilter` | Limit to domains (prefix with `-` to exclude) |
|
|
118
|
-
| `provider` | Configured provider when omitted or set to `auto`; `all` searches every eligible provider except AnySearch, xAI, Bright Data, and SerpBase simultaneously; otherwise `openai`, `brave`, `parallel`, `tinyfish`, `search1api`, `searchinfinity`, `querit`, `tavily`, `serpdive`, `kagi`, `ollama`, `anysearch`, `xai`, `brightdata`, `serpbase`, `searxng`, `exa`, `perplexity`, or `gemini` (auto-selects when no provider or routing is configured; AnySearch, xAI, Bright Data, and SerpBase are explicit-only) |
|
|
120
|
+
| `provider` | Configured provider when omitted or set to `auto`; `all` searches every eligible provider except DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase simultaneously; otherwise `openai`, `brave`, `parallel`, `tinyfish`, `search1api`, `searchinfinity`, `querit`, `tavily`, `jina`, `serpdive`, `kagi`, `bocha`, `ollama`, `anysearch`, `xai`, `brightdata`, `serpbase`, `searxng`, `duckduckgo`, `exa`, `perplexity`, or `gemini` (auto-selects when no provider or routing is configured; DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase are explicit-only) |
|
|
119
121
|
| `includeContent` | Fetch full page content from sources in background |
|
|
120
122
|
| `workflow` | `none` (skip curator), `summary-review` (open curator and auto-generate a summary draft, default), or `auto-summary` (generate a summary without opening the curator) |
|
|
121
123
|
|
|
@@ -136,7 +138,7 @@ fetch_content({ url: "https://example.com/diagram.png" })
|
|
|
136
138
|
```
|
|
137
139
|
|
|
138
140
|
| Parameter | Description |
|
|
139
|
-
|
|
141
|
+
| ----------- | ------------- |
|
|
140
142
|
| `url` / `urls` | Single URL/path or multiple URLs |
|
|
141
143
|
| `prompt` | Question for video analysis, or the page-local question required by `mode: "answer"` |
|
|
142
144
|
| `mode` | `readable` (default), `raw` for exact textual HTTP bodies, or `answer` for a grounded answer from fetched content |
|
|
@@ -147,7 +149,7 @@ fetch_content({ url: "https://example.com/diagram.png" })
|
|
|
147
149
|
|
|
148
150
|
### get_search_content
|
|
149
151
|
|
|
150
|
-
Retrieve stored content from previous searches or fetches. Fetched URL content is stored in full,
|
|
152
|
+
Retrieve stored content from previous searches or fetches. Fetched URL content is stored in full in a private `web-search-cache` directory under the Pi config directory, not in the session JSONL. This includes `fetch_content` answer mode, which stores the original page content. The cache has a one-hour lifetime and fixed limits of 128 entries and 128 MiB; when either limit is reached, the oldest entries are removed first. On macOS and Linux the cache directory and files are kept at permissions `0700` and `0600`, respectively. Use `findText` to locate bounded matching passages without paging through a large page, or use `offset` and `limit` to retrieve slices intentionally.
|
|
151
153
|
|
|
152
154
|
```typescript
|
|
153
155
|
get_search_content({ responseId: "abc123", urlIndex: 0 })
|
|
@@ -157,11 +159,11 @@ get_search_content({ responseId: "abc123", urlIndex: 0, findText: "installation"
|
|
|
157
159
|
get_search_content({ responseId: "abc123", urlIndex: 0, findText: ["timeout", "retry"], findMode: "fuzzy" })
|
|
158
160
|
```
|
|
159
161
|
|
|
160
|
-
`findMode` supports `exact`, `case-insensitive` (default), and `fuzzy`. Finder output is capped at 20,000 characters with match counts and nearby context. `findText` cannot be combined with `offset` or `limit`.
|
|
162
|
+
`findMode` supports `exact`, `case-insensitive` (default), and `fuzzy`. Finder output is capped at 20,000 characters with match counts and nearby context. `findText` cannot be combined with `offset` or `limit`. The default `limit` and maximum permitted `limit` use `maxInlineContentChars`.
|
|
161
163
|
|
|
162
164
|
### source_check
|
|
163
165
|
|
|
164
|
-
Check a claim and return a machine-readable artifact with exact passage citations. Search results are deduplicated and capped at 20 sources; `fetchContent` fetches at most 5 pages, while stored and retrieved content remains subject to the
|
|
166
|
+
Check a claim and return a machine-readable artifact with exact passage citations. Search results are deduplicated and capped at 20 sources; `fetchContent` fetches at most 5 pages, while stored and retrieved content remains subject to the configured `maxInlineContentChars` `offset`/`limit` bounds.
|
|
165
167
|
|
|
166
168
|
```typescript
|
|
167
169
|
source_check({ claim: "The API supports streaming responses" })
|
|
@@ -211,26 +213,54 @@ Requires `ffmpeg` (and `yt-dlp` for YouTube). Timestamps accept `H:MM:SS`, `MM:S
|
|
|
211
213
|
|
|
212
214
|
### PDFs
|
|
213
215
|
|
|
214
|
-
PDF URLs are converted to Markdown
|
|
216
|
+
PDF URLs are converted to Markdown and saved under the temporary `pi-web-pdf` directory by default so the agent can `read` specific sections without loading the full document into context. Three engines are available, selected with `pdf.provider` (`"auto"` is the default):
|
|
217
|
+
|
|
218
|
+
| Provider | Engine | Trade-offs |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| `datalab` | Datalab hosted conversion (Marker) | Deterministic layout-aware output — tables, multi-column reading order, headings, math; `accurate` mode handles scanned pages; may return a `parse_quality_score`; requires a Datalab key, billed per page with a free monthly credit |
|
|
221
|
+
| `gemini` | Gemini API (vision LLM) | Best on scanned/complex pages; LLM transcription can occasionally drift or truncate; requires a Gemini key |
|
|
222
|
+
| `unpdf` | Local pdf.js text extraction | Free, offline, no key; flattened text only — no layout, no tables, no OCR |
|
|
223
|
+
|
|
224
|
+
`auto` order: Datalab (when a key is configured) → Gemini (when a key is configured) → local `unpdf`. Datalab runs first for layout-aware conversion. If its request fails — including after free-tier credit is exhausted — the chain continues to Gemini, then `unpdf`, automatically. Setting `pdf.provider` to `gemini`, `datalab`, or `unpdf` pins that engine and skips the other remote tiers (an explicit engine still falls back to `unpdf` when it errors, except for credential/config errors and caller cancellation). No Datalab key means the `datalab` tier is simply skipped — behavior is unchanged for existing users.
|
|
225
|
+
|
|
226
|
+
**Why Datalab.** The hosted converter uses a dedicated extraction engine (Marker) intended to retain document structure such as tables, multi-column reading order, headings, links, and math, where local `unpdf` extraction only yields flattened text. It is deterministic rather than LLM-based. Completed responses may include a `parse_quality_score` (0–5) for optional quality gating. Pricing is per processed page: **fast / balanced** $4 / 1,000 pages; **accurate** $10 / 1,000 pages. The free tier gives a **$10 monthly credit** (personal email; $20 with a work email) at **25 requests/minute** — roughly **2,500 pages/month free in `fast` mode** or 1,000 in `accurate` mode. Processing defaults to the **US region**. EU data residency uses **1.25× usage**; opt in with `DATALAB_PROCESSING_LOCATION=eu`.
|
|
227
|
+
|
|
228
|
+
Configure Datalab via the web-search config:
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{
|
|
232
|
+
"datalabApiKey": "$DATALAB_API_KEY",
|
|
233
|
+
"pdf": {
|
|
234
|
+
"maxSizeMB": 20,
|
|
235
|
+
"provider": "auto", // "auto" | "gemini" | "datalab" | "unpdf"
|
|
236
|
+
"datalabMode": "balanced", // "fast" | "balanced" | "accurate"
|
|
237
|
+
"datalabTimeoutMs": 120000
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Env vars: `DATALAB_API_KEY` (or `datalabApiKey` in config), `DATALAB_PROCESSING_LOCATION` (`us` default; `eu` enables EU data residency at 1.25× usage), `DATALAB_MODE` (`fast` / `balanced` / `accurate`), and `DATALAB_API_BASE` (custom gateway). `pdf.datalabMode` overrides `DATALAB_MODE`. The default `datalabTimeoutMs` is 120s and is capped at 300s.
|
|
243
|
+
|
|
244
|
+
> Privacy note: like the Gemini tier, the PDF bytes are sent to the Datalab cloud for conversion. Files are uploaded to the selected region's storage and deleted best-effort after conversion.
|
|
215
245
|
|
|
216
246
|
### Blocked pages
|
|
217
247
|
|
|
218
248
|
Raw and direct-image HTTP requests use the same SSRF validation, hostname domain policy, redirect checks, timeout, and 5MB streamed response bound as normal extraction. Raw mode returns textual bodies even for non-2xx responses and exposes the HTTP status in tool details; it does not run readability or hosted extraction fallbacks.
|
|
219
249
|
|
|
220
|
-
When Readability fails or returns only a cookie notice, the extension
|
|
250
|
+
When Readability fails or returns only a cookie notice, the extension can retry configured Firecrawl extraction, Jina Reader (handles JS rendering server-side, no API key needed), TinyFish, Search1API, Querit, Kagi Extract, Ollama Web Fetch, Parallel, Bright Data Web Unlocker, Gemini URL Context API, and Gemini Web extraction when browser cookies are enabled. Configure `fetchRouting.providers` to change the order or set of `fetch_content` providers. Supported values are `http`, `firecrawl`, `jina`, `tinyfish`, `search1api`, `querit`, `kagi`, `ollama`, `parallel`, `brightdata`, and `gemini`; when absent, the default order is unchanged. For remote HTTP(S) targets, third-party hosted providers are disabled unless `fetchRouting.allowRemoteHostedProviders` is `true`, because hosted services perform their own fetch and can see a different redirect chain than the local safety gate. Firecrawl stays available as a configured extraction service. Firecrawl requests are cache-only by default and require an explicit fresh-scrape opt-in before the Firecrawl server can fetch target URLs. Bright Data Web Unlocker runs last of the remote scraping providers, ahead of only the Gemini fallbacks, because it is billed per request against a paid account; it is skipped unless both a key and an `unblocker` zone are configured. It applies no minimum-length check, so any non-empty body it returns — including a short consent or paywall stub — is the final answer for that URL and the Gemini fallbacks are not tried. Handles SPAs, JS-heavy pages, and anti-bot protections transparently. Also parses Next.js RSC flight data when present. HTML extraction also surfaces registered discovery relations (`service-desc`, `service-doc`, `service-meta`, `api-catalog`, `describedby`) from the HTTP `Link` header and matching `link`/`a[rel]` markup. Readable or rendered content remains primary; on an empty shell, the normal extraction fallbacks run before declared links are returned on their own.
|
|
221
251
|
|
|
222
252
|
## How It Works
|
|
223
253
|
|
|
224
254
|
```
|
|
225
255
|
web_search(query)
|
|
226
|
-
→ SearXNG (if configured) → OpenAI (when suitable) → Exa → Brave → Parallel → TinyFish → Search1API → Searchinfinity → Querit → Tavily → SERPdive → Perplexity → Gemini
|
|
256
|
+
→ SearXNG (if configured) → OpenAI (when suitable) → Exa → Brave → Parallel → TinyFish → Search1API → Searchinfinity → Querit → Tavily → Jina → SERPdive → Perplexity → Gemini
|
|
227
257
|
|
|
228
258
|
fetch_content(url)
|
|
229
259
|
→ Video file? Gemini API (Files API) → Gemini Web (if browser cookies enabled)
|
|
230
260
|
→ GitHub URL? Clone repo, return file contents + local path
|
|
231
261
|
→ YouTube URL? Gemini Web (if browser cookies enabled) → Gemini API → Perplexity
|
|
232
|
-
→ HTTP fetch → PDF? Gemini API → local text extraction, save to temp pi-web-pdf
|
|
233
|
-
→ HTML? Readability (+ declared Link/rel discovery) → RSC parser → Firecrawl (if configured
|
|
262
|
+
→ HTTP fetch → PDF? Datalab → Gemini API → local text extraction, save to temp pi-web-pdf
|
|
263
|
+
→ HTML? Readability (+ declared Link/rel discovery) → RSC parser → Firecrawl (if configured) → third-party hosted fallbacks only when fetchRouting.allowRemoteHostedProviders is enabled
|
|
234
264
|
→ Text/JSON/Markdown? Return directly
|
|
235
265
|
```
|
|
236
266
|
|
|
@@ -294,6 +324,7 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
294
324
|
"tinyfishApiKey": "sk-tinyfish-...",
|
|
295
325
|
"search1apiApiKey": "...",
|
|
296
326
|
"tavilyApiKey": "tvly-...",
|
|
327
|
+
"jinaApiKey": "$JINA_API_KEY",
|
|
297
328
|
"serpdiveApiKey": "sd_live_...",
|
|
298
329
|
"serpdiveModel": "krill",
|
|
299
330
|
"kagiApiKey": "$KAGI_API_KEY",
|
|
@@ -318,15 +349,36 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
318
349
|
"provider": "openai",
|
|
319
350
|
"searchRouting": {
|
|
320
351
|
"providers": ["openai", "brave", "exa"],
|
|
321
|
-
"fallbackOn": ["transient", "quota", "network"]
|
|
352
|
+
"fallbackOn": ["transient", "quota", "network", "invalid-response"]
|
|
353
|
+
},
|
|
354
|
+
"fetchRouting": {
|
|
355
|
+
"providers": ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "brightdata", "gemini"],
|
|
356
|
+
"allowRemoteHostedProviders": false
|
|
322
357
|
},
|
|
323
358
|
"webSearch": {
|
|
324
359
|
"enabled": true
|
|
325
360
|
},
|
|
361
|
+
"tools": {
|
|
362
|
+
"webSearch": { "enabled": true },
|
|
363
|
+
"sourceCheck": { "enabled": true },
|
|
364
|
+
"fetchContent": { "enabled": true },
|
|
365
|
+
"getSearchContent": { "enabled": true }
|
|
366
|
+
},
|
|
367
|
+
"commands": {
|
|
368
|
+
"websearch": { "enabled": true },
|
|
369
|
+
"curator": { "enabled": true },
|
|
370
|
+
"search": { "enabled": true },
|
|
371
|
+
"google-account": { "enabled": true }
|
|
372
|
+
},
|
|
373
|
+
"image": {
|
|
374
|
+
"enabled": true
|
|
375
|
+
},
|
|
326
376
|
"chromeProfile": "Profile 2",
|
|
327
377
|
"allowBrowserCookies": false,
|
|
328
378
|
"searchModel": "gemini-3.6-flash",
|
|
329
379
|
"summaryModel": "anthropic/claude-haiku-4-5",
|
|
380
|
+
"summaryGenerationDeadlineMs": 30000,
|
|
381
|
+
"maxInlineContentChars": 30000,
|
|
330
382
|
"workflow": "summary-review",
|
|
331
383
|
"curatorTimeoutSeconds": 20,
|
|
332
384
|
"curatorRemote": {
|
|
@@ -350,7 +402,9 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
350
402
|
"maxSizeMB": 50
|
|
351
403
|
},
|
|
352
404
|
"pdf": {
|
|
353
|
-
"
|
|
405
|
+
"enabled": true,
|
|
406
|
+
"maxSizeMB": 20,
|
|
407
|
+
"provider": "auto"
|
|
354
408
|
},
|
|
355
409
|
"fetchContent": {
|
|
356
410
|
"domainPolicy": {
|
|
@@ -369,7 +423,7 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
369
423
|
}
|
|
370
424
|
```
|
|
371
425
|
|
|
372
|
-
All provider API-key fields (`openaiApiKey`, `braveApiKey`, `parallelApiKey`, `tinyfishApiKey`, `search1apiApiKey`, `searchinfinityApiKey`, `queritApiKey`, `tavilyApiKey`, `serpdiveApiKey`, `kagiApiKey`, `ollamaApiKey`, `serpbaseApiKey`, `anysearchApiKey`, `xaiApiKey`, `brightdataApiKey`, `firecrawlApiKey`, `exaApiKey`, `perplexityApiKey`, `geminiApiKey`, and `cloudflareApiKey`) accept explicit credential sources. Use `$NAME` or `${NAME}` to read one named environment variable, or prefix a trusted local shell command with `!` to resolve one value at provider request time. Escape `$$` as a literal leading `$` and `$!` as a literal leading `!`:
|
|
426
|
+
All provider API-key fields (`openaiApiKey`, `braveApiKey`, `parallelApiKey`, `tinyfishApiKey`, `search1apiApiKey`, `searchinfinityApiKey`, `queritApiKey`, `tavilyApiKey`, `jinaApiKey`, `serpdiveApiKey`, `kagiApiKey`, `bochaApiKey`, `ollamaApiKey`, `serpbaseApiKey`, `anysearchApiKey`, `xaiApiKey`, `brightdataApiKey`, `firecrawlApiKey`, `exaApiKey`, `perplexityApiKey`, `geminiApiKey`, `datalabApiKey`, and `cloudflareApiKey`) accept explicit credential sources. Use `$NAME` or `${NAME}` to read one named environment variable, or prefix a trusted local shell command with `!` to resolve one value at provider request time. Escape `$$` as a literal leading `$` and `$!` as a literal leading `!`:
|
|
373
427
|
|
|
374
428
|
```json
|
|
375
429
|
{
|
|
@@ -384,10 +438,12 @@ This syntax applies to provider credentials only; other configuration fields are
|
|
|
384
438
|
|
|
385
439
|
A command source is not run while the extension loads or registers tools. Each selected provider request runs it again with a five-second timeout, a 16 KiB output limit, a minimized environment, and a one-line non-empty stdout requirement. Command text and stderr are omitted from errors. These commands are trusted local configuration, not a same-user process isolation boundary; use absolute executable paths and protect the config file. `OP_SESSION_*` variables are forwarded to trusted resolver commands so shell-local 1Password sessions can be reused without storing them in config. An explicit source overrides legacy provider environment variables and fails that provider locally rather than falling back with a stale credential. Direct Google Gemini API requests send the resolved key only in the `x-goog-api-key` header, never in the URL.
|
|
386
440
|
|
|
387
|
-
`fetchContent.domainPolicy` is an optional hostname allow/deny policy for `fetch_content` target URLs. It is off when omitted. Each bare hostname matches itself and its subdomains; `deny` wins when a hostname matches both lists. The policy is checked before HTTP(S) target handling and before each redirect followed by this extension's own fetch path. Local file paths and non-HTTP sources are not subject to this policy. It is an additional restriction: the existing SSRF guard still blocks private and internal destinations. Remote extraction services can still perform their own DNS, redirects, and egress after this extension preflights the submitted target URL, so
|
|
441
|
+
`fetchContent.domainPolicy` is an optional hostname allow/deny policy for `fetch_content` target URLs. It is off when omitted. Each bare hostname matches itself and its subdomains; `deny` wins when a hostname matches both lists. The policy is checked before HTTP(S) target handling and before each redirect followed by this extension's own fetch path. Local file paths and non-HTTP sources are not subject to this policy. It is an additional restriction: the existing SSRF guard still blocks private and internal destinations. Remote extraction services can still perform their own DNS, redirects, and egress after this extension preflights the submitted target URL, so third-party hosted HTTP(S) fallbacks stay disabled unless `fetchRouting.allowRemoteHostedProviders` is enabled for separately isolated provider deployments.
|
|
388
442
|
|
|
389
443
|
Set `searxngBaseUrl` or `SEARXNG_BASE_URL` to use a self-hosted SearXNG JSON API. A configured endpoint is preferred first in `auto` mode for local/private search. Its base URL and redirects remain subject to the SSRF guard; add only the narrowest self-hosted range to `ssrf.allowRanges` when it resolves to a private or synthetic range. Optional `searxngHeaders` merges extra HTTP headers into each SearXNG request (string values only; invalid header names are ignored), which is useful for reverse-proxy or Zero Trust auth such as Cloudflare Access service tokens (`CF-Access-Client-Id` / `CF-Access-Client-Secret`). Configured headers override the default `Accept: application/json` when the same name is supplied. Thanks to Marcos A. Núñez (@marnunez) for PR #107 and Avinash Kanaujiya (@avinashkanaujiya) for issue #105.
|
|
390
444
|
|
|
445
|
+
**DuckDuckGo.** DuckDuckGo HTML search is keyless and explicit-only. Select it with `provider: "duckduckgo"` or place it in `searchRouting`; it is never chosen by `auto` and never participates in `provider: "all"`. Domain filters are enforced locally after DuckDuckGo redirect URLs are decoded. `recencyFilter` is not guaranteed because the HTML endpoint has no documented stable time parameter. A 200 page with no parseable results is reported as an invalid response, so routing can continue when `fallbackOn` includes `"invalid-response"`.
|
|
446
|
+
|
|
391
447
|
Set `firecrawlBaseUrl` or `FIRECRAWL_BASE_URL` to use Firecrawl as an extraction-only fallback for `fetch_content`. It calls `/v2/scrape` by default; set `firecrawlApiVersion` or `FIRECRAWL_API_VERSION` to `v1` for older self-hosted images. Firecrawl requests are cache-only by default (`lockdown: true`), so the Firecrawl server does not make fresh outbound target requests unless you explicitly set `firecrawlFreshScrape: true` or `FIRECRAWL_FRESH_SCRAPE=1`. Enable fresh scraping only for a Firecrawl deployment whose own egress, redirects, DNS rebinding behavior, and internal-network access are isolated or allowlisted; this extension can preflight the submitted URL but cannot control network requests made by the Firecrawl server. The configured Firecrawl API base URL and redirects are still validated by the same SSRF guard as other remote requests, and Firecrawl credentials are stripped from cross-origin API redirects.
|
|
392
448
|
|
|
393
449
|
**Bright Data.** Set `brightdataApiKey` or `BRIGHTDATA_API_KEY` to use Bright Data-backed features. The SERP search provider also requires `brightdataSerpZone` or `BRIGHTDATA_SERP_ZONE`, and the Web Unlocker extraction fallback also requires `brightdataUnlockerZone` or `BRIGHTDATA_UNLOCKER_ZONE`. These zone settings are separate and are never substituted for each other: SERP requires a Bright Data zone of type `serp`, while Web Unlocker requires a zone of type `unblocker`. Leaving either zone unset keeps that product unavailable, so enabling one Bright Data feature does not opt into the other.
|
|
@@ -402,15 +458,29 @@ Bright Data Web Unlocker is a paid `fetch_content` fallback after Parallel and b
|
|
|
402
458
|
|
|
403
459
|
**SerpBase.** Set `serpbaseApiKey` or `SERPBASE_API_KEY` and select `provider: "serpbase"` to query SerpBase's Google Search Results API. SerpBase is explicit-only: it is never chosen by `auto` and never participates in `provider: "all"`, because each request can consume paid Google SERP credits. Domain filters are sent as Google `site:` clauses and reapplied locally; recency maps to Google's `tbs` time filter.
|
|
404
460
|
|
|
405
|
-
|
|
406
|
-
Without an explicit `$` or `!` source, `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `ANYSEARCH_API_KEY`, `XAI_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`, `GEMINI_API_KEY`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, and `CLOUDFLARE_API_KEY` env vars retain their existing precedence over literal config file values. `openaiResponsesUrl` can point OpenAI `web_search` and `source_check` at a third-party gateway that supports the OpenAI Responses API and web search tool; it is an explicit endpoint override, not derived from Pi model provider settings, and defaults to `https://api.openai.com/v1/responses`. `openaiSearchModel` pins the model id used for OpenAI `web_search`, bypassing automatic selection (newest terra-tier model); the id is sent verbatim with whichever OpenAI auth resolves, so gateway-only model ids work too. `xaiSearchModel` similarly pins the xAI search model. Configured Exa API keys use Exa's own account limits directly; any legacy local `exa-usage.json` file is ignored. `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host for Gemini generate-content calls such as search, URL context, YouTube, and local video analysis. Set it to a bare host with no trailing slash and no version segment, for example `https://my-gateway.example.com/gemini`; `geminiBaseUrl` is the config-file equivalent. When the configured host contains `gateway.ai.cloudflare.com`, authentication uses `cf-aig-authorization: Bearer <token>` from `CLOUDFLARE_API_KEY` or `cloudflareApiKey`, and `GEMINI_API_KEY` is not required for generate-content calls. Local video file upload still uses Google's Files API directly, so gateway-only video extraction falls back to Gemini Web unless a `GEMINI_API_KEY` is also configured. `provider` or `searchProvider` sets the default search provider and is used when a tool call omits `provider` or sends `"auto"`: `"all"`, `"openai"`, `"brave"`, `"parallel"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"serpdive"`, `"kagi"`, `"ollama"`, `"anysearch"`, `"xai"`, `"brightdata"`, `"serpbase"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. AnySearch, xAI, Bright Data, and SerpBase are never selected by `auto`; choose them explicitly or place them in `searchRouting`. If either single-provider field is configured, it takes precedence over `searchRouting`. Otherwise, `searchRouting` can opt into an ordered `providers` list and an explicit `fallbackOn` list containing `"transient"`, `"quota"`, and/or `"network"`; only those typed failures continue to the next available candidate. `"all"` is not valid inside `searchRouting.providers`, because that list defines sequential fallback rather than multi-provider aggregation. Named providers remain strict, and exhausted routes return per-provider diagnostics. `provider` can also be a non-empty array of named providers such as `["brave", "exa"]`; those providers run concurrently using the same aggregation path as `"all"`, while `"auto"` and `"all"` are invalid inside arrays. Random, weighted, sticky, and cooldown routing are not enabled. This is also updated automatically when you change the provider in the curator UI. Set `webSearch.enabled` to `false` to unregister the configured search and source-check tools while leaving fetch/content tools available. `toolNames` can opt into alternate public tool names for environments where another extension or model reserves the defaults, without changing behavior: `webSearch`, `sourceCheck`, `fetchContent`, and `getSearchContent` default to `web_search`, `source_check`, `fetch_content`, and `get_search_content`. `workflow` sets the default search workflow: `"summary-review"` (default, opens curator with auto-generated summary draft), `"auto-summary"` (returns a model-generated summary without opening the curator), or `"none"` (raw results, no curator). Overridden per-call via the `workflow` parameter on the configured search tool, or toggled at runtime with `/curator`. `chromeProfile` pins Gemini Web cookie lookup to a specific Chromium profile. When omitted, detected Chromium profiles are scanned in stable order and the first profile containing the required Gemini cookies is used. `allowBrowserCookies` enables Chromium cookie extraction for Gemini Web; it defaults to `false` to avoid browser data access and surprise macOS Keychain prompts. You can also set `PI_ALLOW_BROWSER_COOKIES=1`. Cookie databases are copied to a temporary read-only working copy; the reader uses `node:sqlite` when available and otherwise tries the `sqlite3` CLI or Python's standard-library SQLite module. `searchModel` overrides the Gemini API model used by the configured search tool without changing URL, YouTube, or video extraction defaults. Gemini API grounded search uses `gemini-3.6-flash` by default; set `searchModel` to choose another model. Gemini Web browser-cookie fallback uses its separate `gemini-3.1-pro` default because Gemini Web relies on private header values; explicitly configured unsupported Web models fail instead of silently falling back to 2.5 Flash. `summaryModel` sets the default model used for generating summary drafts in the curator UI and `auto-summary` mode (e.g. `"anthropic/claude-haiku-4-5"`, `"openai-codex/gpt-5.3-codex-spark"`, or `"openrouter/nvidia/nemotron-3-super-120b-a12b:free"`). Preferred summary and query-rewrite models also resolve through routed provider registrations such as OpenRouter when the native provider is unavailable. When Pi `enabledModels` is configured, summaries are limited to that allowlist; if no enabled summary model is available, the tool returns a deterministic summary instead of calling an unrelated model. `curatorTimeoutSeconds` controls the initial curator idle timeout (default `20`, max `600`); users can still adjust the timer in the curator UI. `ssrf.allowRanges` lists CIDR ranges (e.g. `"198.18.0.0/15"`, `"fd00::/8"`) exempted from the SSRF guard that otherwise blocks private/reserved IP ranges. This unblocks `fetch_content`/`web_search` on hosts whose network proxy runs in TUN + fake-IP mode (Surge, Clash, Mihomo, Stash, ...), where public domains resolve into a synthetic reserved range. It is **off by default** — the guard stays fully enabled unless you list ranges here. Use the narrowest range that covers your proxy's fake-IP pool. All-address CIDRs such as `0.0.0.0/0` and `::/0` are rejected. `ssrf.trustEnvProxy` is a separate opt-in for sandboxed environments with valid HTTP(S) proxy env vars; it skips local DNS preflight only for proxied hostnames and still blocks localhost, literal private IPs, and `NO_PROXY` matches. It does not configure proxy transport.
|
|
461
|
+
Without an explicit `$` or `!` source, `OPENAI_API_KEY`, `BRAVE_API_KEY`, `PARALLEL_API_KEY`, `TINYFISH_API_KEY`, `SEARCH1API_KEY`, `SEARCHINFINITY_API_KEY`, `QUERIT_API_KEY`, `TAVILY_API_KEY`, `JINA_API_KEY`, `SERPDIVE_API_KEY`, `KAGI_API_KEY`, `BOCHA_API_KEY`, `OLLAMA_API_KEY`, `SERPBASE_API_KEY`, `ANYSEARCH_API_KEY`, `XAI_API_KEY`, `BRIGHTDATA_API_KEY`, `FIRECRAWL_API_KEY`, `EXA_API_KEY`, `GEMINI_API_KEY`, `DATALAB_API_KEY`, `DATALAB_PROCESSING_LOCATION`, `DATALAB_MODE`, `DATALAB_API_BASE`, `PERPLEXITY_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, and `CLOUDFLARE_API_KEY` env vars retain their existing precedence over literal config file values. `openaiResponsesUrl` can point OpenAI `web_search` and `source_check` at a third-party gateway that supports the OpenAI Responses API and web search tool; it is an explicit endpoint override, not derived from Pi model provider settings, and defaults to `https://api.openai.com/v1/responses`. `openaiSearchModel` pins the model id used for OpenAI `web_search`, bypassing automatic selection (newest terra-tier model); the id is sent verbatim with whichever OpenAI auth resolves, so gateway-only model ids work too. `xaiSearchModel` similarly pins the xAI search model. Configured Exa API keys use Exa's own account limits directly; any legacy local `exa-usage.json` file is ignored. `GOOGLE_GEMINI_BASE_URL` overrides the Gemini API host for Gemini generate-content calls such as search, URL context, YouTube, and local video analysis. Set it to a bare host with no trailing slash and no version segment, for example `https://my-gateway.example.com/gemini`; `geminiBaseUrl` is the config-file equivalent. When the configured host contains `gateway.ai.cloudflare.com`, authentication uses `cf-aig-authorization: Bearer <token>` from `CLOUDFLARE_API_KEY` or `cloudflareApiKey`, and `GEMINI_API_KEY` is not required for generate-content calls. Local video file upload still uses Google's Files API directly, so gateway-only video extraction falls back to Gemini Web unless a `GEMINI_API_KEY` is also configured. `provider` or `searchProvider` sets the default search provider and is used when a tool call omits `provider` or sends `"auto"`: `"all"`, `"openai"`, `"brave"`, `"parallel"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"xai"`, `"brightdata"`, `"serpbase"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. AnySearch, xAI, Bright Data, and SerpBase are never selected by `auto`; choose them explicitly or place them in `searchRouting`. If either single-provider field is configured, it takes precedence over `searchRouting`. Otherwise, `searchRouting` can opt into an ordered `providers` list and an explicit `fallbackOn` list containing `"transient"`, `"quota"`, `"network"`, and/or `"invalid-response"`; only those typed failures continue to the next available candidate. `"all"` is not valid inside `searchRouting.providers`, because that list defines sequential fallback rather than multi-provider aggregation. Named providers remain strict, and exhausted routes return per-provider diagnostics. `provider` can also be a non-empty array of named providers such as `["brave", "exa"]`; those providers run concurrently using the same aggregation path as `"all"`, while `"auto"` and `"all"` are invalid inside arrays. Random, weighted, sticky, and cooldown routing are not enabled. This is also updated automatically when you change the provider in the curator UI. Set `webSearch.enabled` to `false` to unregister the configured search and source-check tools while leaving fetch/content tools available. `toolNames` can opt into alternate public tool names for environments where another extension or model reserves the defaults, without changing behavior: `webSearch`, `sourceCheck`, `fetchContent`, and `getSearchContent` default to `web_search`, `source_check`, `fetch_content`, and `get_search_content`. `workflow` sets the default search workflow: `"summary-review"` (default, opens curator with auto-generated summary draft), `"auto-summary"` (returns a model-generated summary without opening the curator), or `"none"` (raw results, no curator). Overridden per-call via the `workflow` parameter on the configured search tool, or toggled at runtime with `/curator`. `chromeProfile` pins Gemini Web cookie lookup to a specific Chromium profile. When omitted, detected Chromium profiles are scanned in stable order and the first profile containing the required Gemini cookies is used. `allowBrowserCookies` enables Chromium cookie extraction for Gemini Web; it defaults to `false` to avoid browser data access and surprise macOS Keychain prompts. You can also set `PI_ALLOW_BROWSER_COOKIES=1`. Cookie databases are copied to a temporary read-only working copy; the reader uses `node:sqlite` when available and otherwise tries the `sqlite3` CLI or Python's standard-library SQLite module. `searchModel` overrides the Gemini API model used by the configured search tool without changing URL, YouTube, or video extraction defaults. Gemini API grounded search uses `gemini-3.6-flash` by default; set `searchModel` to choose another model. Gemini Web browser-cookie fallback uses its separate `gemini-3.1-pro` default because Gemini Web relies on private header values; explicitly configured unsupported Web models fail instead of silently falling back to 2.5 Flash. `summaryModel` sets the default model used for generating summary drafts in the curator UI and `auto-summary` mode (e.g. `"anthropic/claude-haiku-4-5"`, `"openai-codex/gpt-5.3-codex-spark"`, or `"openrouter/nvidia/nemotron-3-super-120b-a12b:free"`). Preferred summary and query-rewrite models also resolve through routed provider registrations such as OpenRouter when the native provider is unavailable. When Pi `enabledModels` is configured, summaries are limited to that allowlist; if no enabled summary model is available, the tool returns a deterministic summary instead of calling an unrelated model. `summaryGenerationDeadlineMs` sets the maximum time for one summary model attempt in the curator UI and `auto-summary` mode. It defaults to `30000`, must be a positive integer, and is capped at `600000`. `maxInlineContentChars` sets the direct `fetch_content` content slice and the default and maximum `get_search_content` slice. It defaults to `30000`, must be a positive integer, and is capped at `200000`; full fetched content remains stored for later retrieval. `curatorTimeoutSeconds` controls the initial curator idle timeout (default `20`, max `600`); users can still adjust the timer in the curator UI. `ssrf.allowRanges` lists CIDR ranges (e.g. `"198.18.0.0/15"`, `"fd00::/8"`) exempted from the SSRF guard that otherwise blocks private/reserved IP ranges. This unblocks `fetch_content`/`web_search` on hosts whose network proxy runs in TUN + fake-IP mode (Surge, Clash, Mihomo, Stash, ...), where public domains resolve into a synthetic reserved range. It is **off by default** — the guard stays fully enabled unless you list ranges here. Use the narrowest range that covers your proxy's fake-IP pool. All-address CIDRs such as `0.0.0.0/0` and `::/0` are rejected. `ssrf.trustEnvProxy` is a separate opt-in for sandboxed environments with valid HTTP(S) proxy env vars; it skips local DNS preflight only for proxied hostnames and still blocks localhost, literal private IPs, and `NO_PROXY` matches. It does not configure proxy transport.
|
|
407
462
|
|
|
408
463
|
### All providers
|
|
409
464
|
|
|
410
|
-
Set `provider: "all"` on `web_search` or `source_check`, or configure `"provider": "all"` as the default, to run the same query against every eligible search provider simultaneously. AnySearch, xAI, Bright Data, and SerpBase are always excluded because they are explicit-only; Bright Data and SerpBase are paid Google SERP providers, so `all` never spends on them. Exa remains eligible through its zero-config MCP path, OpenAI can use Pi auth, and other API-backed search providers participate when their API key, local endpoint, or gateway makes them available. Browser-cookie access alone does not opt Gemini into `all`; select Gemini explicitly or configure its API/gateway. Firecrawl is an extraction backend, not a search provider, so it is not part of `all`.
|
|
465
|
+
Set `provider: "all"` on `web_search` or `source_check`, or configure `"provider": "all"` as the default, to run the same query against every eligible search provider simultaneously. DuckDuckGo, AnySearch, xAI, Bright Data, and SerpBase are always excluded because they are explicit-only; Bright Data and SerpBase are paid Google SERP providers, so `all` never spends on them. Exa remains eligible through its zero-config MCP path, OpenAI can use Pi auth, and other API-backed search providers participate when their API key, local endpoint, or gateway makes them available. Browser-cookie access alone does not opt Gemini into `all`; select Gemini explicitly or configure its API/gateway. Firecrawl is an extraction backend, not a search provider, so it is not part of `all`.
|
|
411
466
|
|
|
412
467
|
Successful provider answers are preserved separately while source URLs and inline content are deduplicated, and one provider failure does not discard the other results. If every participating provider fails, the tool returns per-provider diagnostics. In the Curator, **All** can also be selected like the other provider buttons. Each participating provider gets its own result card, including a provider badge and independent selection checkbox; failed providers get their own disabled error card. The final summary is generated from the selected provider cards and is what Pi receives. Outside the Curator, the same provider answers remain available as labeled sections in one tool response.
|
|
413
468
|
|
|
469
|
+
### Jina Search
|
|
470
|
+
|
|
471
|
+
`jinaApiKey` enables [Jina Search](https://s.jina.ai); alternatively, set `JINA_API_KEY`. The key may be a literal, an environment-variable reference, or a trusted command credential source:
|
|
472
|
+
|
|
473
|
+
```json
|
|
474
|
+
{
|
|
475
|
+
"jinaApiKey": "$JINA_API_KEY",
|
|
476
|
+
"provider": "jina"
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Setting `provider` is optional. In `auto` mode, Jina is tried after Tavily and before SERPdive. It can also be selected per request with `provider: "jina"`, included in provider arrays or `provider: "all"`, or placed in `searchRouting.providers`.
|
|
481
|
+
|
|
482
|
+
Jina Search maps `numResults` to its bounded `count` parameter, sends included domains as `site` filters, and adds excluded domains and recency constraints to the search query. Without `includeContent`, it requests SERP metadata only. With `includeContent: true`, Jina visits matching pages and returns their Markdown inline, so requests can take longer and consume more Jina tokens. The fixed hosted endpoint is `https://s.jina.ai`; no custom endpoint is configured by this extension.
|
|
483
|
+
|
|
414
484
|
### TinyFish
|
|
415
485
|
|
|
416
486
|
`tinyfishApiKey` enables the TinyFish Search and Fetch APIs; alternatively, set `TINYFISH_API_KEY`. Get an API key from the [TinyFish API Keys](https://agent.tinyfish.ai/api-keys) page. Like the other provider keys, `tinyfishApiKey` can contain a literal key, an environment-variable reference, or a trusted command credential source:
|
|
@@ -492,6 +562,7 @@ The model is chosen for you: the registry path walks a best-first candidate list
|
|
|
492
562
|
Requests send only `{ model, input, tools }`, the shape verified against a live subscription account. `recencyFilter`, `domainFilter`, and `numResults` are folded into the prompt text rather than sent as tool parameters, so an unrecognized field can never turn a search into a 400. Sources are read from `url_citation` annotations on the answer and from each `web_search_call`'s own sources; there is no top-level `citations` array on this API.
|
|
493
563
|
|
|
494
564
|
xAI's older Live Search (`search_parameters` on `/v1/chat/completions`) is deprecated and now answers HTTP 410.
|
|
565
|
+
|
|
495
566
|
### Bright Data
|
|
496
567
|
|
|
497
568
|
Bright Data SERP is a **paid, third-party search proxy**: your query, its filters, and the result URLs
|
|
@@ -613,8 +684,9 @@ that the length cut turns a longer number into a three-digit one cannot manufact
|
|
|
613
684
|
It cannot impersonate a rate-limit phrase: those are quoted as `upstream rate-limit notice`, so a page
|
|
614
685
|
saying "you have exceeded your rate limit" cannot make a billed request look like a quota failure. The
|
|
615
686
|
number and the wording still reach you; only this extension's own text decides how the failure is
|
|
616
|
-
classified. A billed `200` therefore
|
|
617
|
-
|
|
687
|
+
classified. A billed `200` therefore surfaces as an unreadable-response error. It is not retried by
|
|
688
|
+
default; it only falls through a configured route when you explicitly include
|
|
689
|
+
`"invalid-response"` in `searchRouting.fallbackOn`.
|
|
618
690
|
|
|
619
691
|
Your token is removed from every quoted response body, error message and activity log line before it
|
|
620
692
|
is shown, and no prefix of it survives either: the parser's own message, which quotes the first
|
|
@@ -715,9 +787,9 @@ Both shortcuts are configurable via `~/.pi/web-search.json`:
|
|
|
715
787
|
|
|
716
788
|
Values use the same format as pi keybindings (e.g. `ctrl+s`, `ctrl+shift+s`, `alt+r`). Changes take effect on next pi restart.
|
|
717
789
|
|
|
718
|
-
Set `"enabled": false` under
|
|
790
|
+
Set `"enabled": false` under `tools`, `commands`, `image`, or `pdf` to disable that feature. Tool-specific settings override the legacy `webSearch.enabled` shorthand; without an override, it still disables `web_search` and `source_check`. `image.enabled: false` blocks direct image fetches and video frame extraction, and prevents video thumbnails. `pdf.enabled: false` blocks PDF extraction. For GitHub specifically, `githubClone.enabled: false` only skips clone/API specialization; it does not unregister `fetch_content` or block generic URL extraction. Pi restart is required for tool and command registration changes.
|
|
719
791
|
|
|
720
|
-
Rate limits: Perplexity is capped at 10 requests/minute (client-side). TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent with a 30s timeout for the direct HTTP fetch of each URL. Remote extraction fallbacks carry their own budgets and are not covered by that number: Jina Reader 30s, Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s. `pdf.maxSizeMB` defaults to 20 and is capped at 50.
|
|
792
|
+
Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent with a 30s timeout for the direct HTTP fetch of each URL. Remote extraction fallbacks carry their own budgets and are not covered by that number: Jina Reader 30s, Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, rate-limited to 25 requests/minute on the free tier). `pdf.maxSizeMB` defaults to 20 and is capped at 50.
|
|
721
793
|
|
|
722
794
|
## Limitations
|
|
723
795
|
|
|
@@ -733,7 +805,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). TinyFish,
|
|
|
733
805
|
<summary>Files</summary>
|
|
734
806
|
|
|
735
807
|
| File | Purpose |
|
|
736
|
-
|
|
808
|
+
| ------ | --------- |
|
|
737
809
|
| `index.ts` | Extension entry, tool definitions, commands, widget |
|
|
738
810
|
| `curator-page.ts` | HTML/CSS/JS generation for the curator UI with markdown rendering |
|
|
739
811
|
| `curator-server.ts` | Ephemeral HTTP server with SSE streaming and state machine |
|
|
@@ -747,6 +819,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). TinyFish,
|
|
|
747
819
|
| `searchinfinity.ts` | Byteplus Searchinfinity search provider |
|
|
748
820
|
| `querit.ts` | Querit Search and Contents API provider |
|
|
749
821
|
| `tavily.ts` | Tavily Search API provider |
|
|
822
|
+
| `jina-search.ts` | Jina Search API provider |
|
|
750
823
|
| `serpdive.ts` | SERPdive Search API provider |
|
|
751
824
|
| `kagi.ts` | Kagi Search API provider and Extract API fallback |
|
|
752
825
|
| `ollama.ts` | Ollama Cloud Web Search provider and Web Fetch fallback |
|
|
@@ -755,6 +828,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). TinyFish,
|
|
|
755
828
|
| `anysearch.ts` | Explicit-only AnySearch search provider |
|
|
756
829
|
| `xai-search.ts` | Explicit-only xAI (Grok) hosted web_search provider |
|
|
757
830
|
| `searxng.ts` | Self-hosted SearXNG JSON API search provider |
|
|
831
|
+
| `duckduckgo.ts` | Explicit-only keyless DuckDuckGo HTML search provider |
|
|
758
832
|
| `exa.ts` | Exa.ai search provider — direct API and MCP proxy |
|
|
759
833
|
| `extract.ts` | URL/file path routing, HTTP extraction, fallback orchestration |
|
|
760
834
|
| `content-find.ts` | Bounded exact, case-insensitive, and fuzzy passage lookup |
|
|
@@ -770,6 +844,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). TinyFish,
|
|
|
770
844
|
| `github-extract.ts` | GitHub URL parsing, clone cache, content generation |
|
|
771
845
|
| `github-api.ts` | GitHub API fallback for large repos and commit SHAs |
|
|
772
846
|
| `perplexity.ts` | Perplexity API client with rate limiting |
|
|
847
|
+
| `datalab-pdf-extract.ts` | Datalab hosted PDF-to-Markdown conversion client (upload → convert → poll) |
|
|
773
848
|
| `pdf-extract.ts` | PDF text extraction, saves to markdown |
|
|
774
849
|
| `rsc-extract.ts` | RSC flight data parser for Next.js pages |
|
|
775
850
|
| `utils.ts` | Shared formatting and error helpers |
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
4
|
+
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
5
|
+
import { getWebSearchConfigPath } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
const BOCHA_SEARCH_URL = "https://api.bochaai.com/v1/web-search";
|
|
8
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
9
|
+
const SEARCH_TIMEOUT_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
interface WebSearchConfig {
|
|
12
|
+
bochaApiKey?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cachedConfig: WebSearchConfig | null = null;
|
|
16
|
+
|
|
17
|
+
function loadConfig(): WebSearchConfig {
|
|
18
|
+
if (cachedConfig) return cachedConfig;
|
|
19
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
20
|
+
cachedConfig = {};
|
|
21
|
+
return cachedConfig;
|
|
22
|
+
}
|
|
23
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
24
|
+
let parsed: unknown;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
30
|
+
}
|
|
31
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
33
|
+
}
|
|
34
|
+
cachedConfig = parsed as WebSearchConfig;
|
|
35
|
+
return cachedConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getApiKey(signal?: AbortSignal): Promise<string | null> {
|
|
39
|
+
return resolveCredential({
|
|
40
|
+
provider: "Bocha",
|
|
41
|
+
configuredValue: loadConfig().bochaApiKey,
|
|
42
|
+
environmentValue: process.env.BOCHA_API_KEY,
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function requireApiKey(signal?: AbortSignal): Promise<string> {
|
|
48
|
+
const apiKey = await getApiKey(signal);
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Bocha API key not found. Either:\n" +
|
|
52
|
+
` 1. Create ${CONFIG_PATH} with { "bochaApiKey": "your-key" }\n` +
|
|
53
|
+
" 2. Set BOCHA_API_KEY environment variable\n" +
|
|
54
|
+
"Create a key at https://open.bochaai.com/",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return apiKey;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeCount(value: number | undefined): number {
|
|
61
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 8;
|
|
62
|
+
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mapFreshness(value: SearchOptions["recencyFilter"]): string {
|
|
66
|
+
switch (value) {
|
|
67
|
+
case "day": return "oneDay";
|
|
68
|
+
case "week": return "oneWeek";
|
|
69
|
+
case "month": return "oneMonth";
|
|
70
|
+
case "year": return "oneYear";
|
|
71
|
+
default: return "noLimit";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface DomainFilters {
|
|
76
|
+
include: string[];
|
|
77
|
+
exclude: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeDomain(value: string): string | null {
|
|
81
|
+
let input = value.trim().toLowerCase();
|
|
82
|
+
if (!input) return null;
|
|
83
|
+
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
84
|
+
if (!input) return null;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
87
|
+
input = parsed.hostname;
|
|
88
|
+
} catch {
|
|
89
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
90
|
+
}
|
|
91
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
92
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
|
|
96
|
+
const filters: DomainFilters = { include: [], exclude: [] };
|
|
97
|
+
for (const raw of domainFilter ?? []) {
|
|
98
|
+
const domain = normalizeDomain(raw);
|
|
99
|
+
if (!domain) continue;
|
|
100
|
+
const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
|
|
101
|
+
if (!target.includes(domain)) target.push(domain);
|
|
102
|
+
}
|
|
103
|
+
return filters;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function passesDomainFilters(url: string, filters: DomainFilters): boolean {
|
|
107
|
+
if (filters.include.length === 0 && filters.exclude.length === 0) return true;
|
|
108
|
+
let hostname: string;
|
|
109
|
+
try {
|
|
110
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const matches = (domain: string) => hostname === domain || hostname.endsWith(`.${domain}`);
|
|
115
|
+
if (filters.exclude.some(matches)) return false;
|
|
116
|
+
return filters.include.length === 0 || filters.include.some(matches);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function errorMessage(err: unknown): string {
|
|
120
|
+
return err instanceof Error ? err.message : String(err);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function invalidResponse(message: string): Error {
|
|
124
|
+
return new Error(`Bocha API returned invalid response: ${message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function firstString(...values: unknown[]): string | null {
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseSearchResponse(value: unknown): { results: SearchResponse["results"] } {
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("expected an object envelope");
|
|
136
|
+
const envelope = value as Record<string, unknown>;
|
|
137
|
+
if (envelope.code !== undefined && Number(envelope.code) !== 200) {
|
|
138
|
+
throw invalidResponse(`code ${String(envelope.code)}: ${firstString(envelope.msg) ?? "unknown error"}`);
|
|
139
|
+
}
|
|
140
|
+
const data = envelope.data;
|
|
141
|
+
const pages = (typeof data === "object" && data !== null && !Array.isArray(data))
|
|
142
|
+
? (data as Record<string, unknown>).webPages
|
|
143
|
+
: undefined;
|
|
144
|
+
const items = (typeof pages === "object" && pages !== null && !Array.isArray(pages))
|
|
145
|
+
? (pages as Record<string, unknown>).value
|
|
146
|
+
: undefined;
|
|
147
|
+
if (!Array.isArray(items)) throw invalidResponse("missing data.webPages.value array");
|
|
148
|
+
const results: SearchResponse["results"] = [];
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
151
|
+
const entry = item as Record<string, unknown>;
|
|
152
|
+
const url = firstString(entry.url, entry.link, entry.href);
|
|
153
|
+
if (!url) continue;
|
|
154
|
+
const title = firstString(entry.title, entry.name) ?? url;
|
|
155
|
+
const snippet = firstString(entry.summary, entry.snippet, entry.description, entry.content) ?? "";
|
|
156
|
+
results.push({ title, url, snippet });
|
|
157
|
+
}
|
|
158
|
+
return { results };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildAnswer(results: SearchResponse["results"]): string {
|
|
162
|
+
return results.map((result) => result.snippet
|
|
163
|
+
? `${result.snippet}\nSource: ${result.title} (${result.url})`
|
|
164
|
+
: `Source: ${result.title} (${result.url})`).join("\n\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isBochaAvailable(): boolean {
|
|
168
|
+
return hasCredentialSource({ provider: "Bocha", configuredValue: loadConfig().bochaApiKey, environmentValue: process.env.BOCHA_API_KEY });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function searchWithBocha(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
172
|
+
const apiKey = await requireApiKey(options.signal);
|
|
173
|
+
const numResults = normalizeCount(options.numResults);
|
|
174
|
+
const filters = parseDomainFilter(options.domainFilter);
|
|
175
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
176
|
+
let response: Response;
|
|
177
|
+
try {
|
|
178
|
+
response = await fetch(BOCHA_SEARCH_URL, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "application/json" },
|
|
181
|
+
body: JSON.stringify({ query, count: numResults, freshness: mapFreshness(options.recencyFilter), summary: true }),
|
|
182
|
+
signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
183
|
+
});
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const message = errorMessage(err);
|
|
186
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
187
|
+
if (redactedMessage.toLowerCase().includes("abort")) activityMonitor.logComplete(activityId, 0);
|
|
188
|
+
else activityMonitor.logError(activityId, redactedMessage);
|
|
189
|
+
if (redactedMessage === message) throw err;
|
|
190
|
+
const redactedError = new Error(redactedMessage);
|
|
191
|
+
if (err instanceof Error) redactedError.name = err.name;
|
|
192
|
+
throw redactedError;
|
|
193
|
+
}
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
196
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
197
|
+
throw new Error(`Bocha API error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
198
|
+
}
|
|
199
|
+
let rawData: unknown;
|
|
200
|
+
try {
|
|
201
|
+
rawData = await response.json();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
204
|
+
throw new Error(`Bocha API returned invalid JSON: ${errorMessage(err)}`);
|
|
205
|
+
}
|
|
206
|
+
let parsed: { results: SearchResponse["results"] };
|
|
207
|
+
try {
|
|
208
|
+
parsed = parseSearchResponse(rawData);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const message = errorMessage(err);
|
|
211
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
212
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
213
|
+
if (redactedMessage === message) throw err;
|
|
214
|
+
const redactedError = new Error(redactedMessage);
|
|
215
|
+
if (err instanceof Error) redactedError.name = err.name;
|
|
216
|
+
throw redactedError;
|
|
217
|
+
}
|
|
218
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
219
|
+
const results = parsed.results.filter((result) => passesDomainFilters(result.url, filters)).slice(0, numResults);
|
|
220
|
+
return { answer: buildAnswer(results), results };
|
|
221
|
+
}
|