@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
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 +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -4,7 +4,7 @@
|
|
|
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, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, Valyu, xAI/Grok, Bright Data SERP, SerpBase, Serper, self-hosted SearXNG, keyless DuckDuckGo, optional browser-cookie Gemini Web, Kimi Code Plan search, 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, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, XCrawl, Valyu, xAI/Grok, Bright Data SERP, SerpBase, Serper, self-hosted SearXNG, keyless DuckDuckGo, optional browser-cookie Gemini Web, Kimi Code Plan search, 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)
|
|
@@ -110,7 +110,7 @@ fetch_content({ url: "/path/to/recording.mp4", prompt: "What error appears on sc
|
|
|
110
110
|
|
|
111
111
|
### web_search
|
|
112
112
|
|
|
113
|
-
Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, Valyu, xAI, Bright Data SERP, SerpBase, Serper, self-hosted SearXNG, keyless DuckDuckGo, Exa, Perplexity AI, Gemini, or Kimi. Returns a synthesized answer with source citations.
|
|
113
|
+
Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, XCrawl, Valyu, xAI, Bright Data SERP, SerpBase, Serper, self-hosted SearXNG, keyless DuckDuckGo, Exa, Perplexity AI, Gemini, or Kimi. Returns a synthesized answer with source citations.
|
|
114
114
|
|
|
115
115
|
```typescript
|
|
116
116
|
web_search({ query: "rust async programming" })
|
|
@@ -132,7 +132,7 @@ web_search({ queries: ["query 1", "query 2"], workflow: "auto-summary" })
|
|
|
132
132
|
| `numResults` | Results per query (default: 5, max: 20) |
|
|
133
133
|
| `recencyFilter` | `day`, `week`, `month`, or `year` |
|
|
134
134
|
| `domainFilter` | Limit to domains (prefix with `-` to exclude) |
|
|
135
|
-
| `provider` | Configured provider when omitted or set to `auto`; `all` searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper simultaneously; otherwise `openai`, `brave`, `parallel`, `parallel-mcp`, `tinyfish`, `search1api`, `searchinfinity`, `querit`, `tavily`, `firecrawl`, `jina`, `serpdive`, `kagi`, `bocha`, `ollama`, `anysearch`, `valyu`, `xai`, `brightdata`, `serpbase`, `serper`, `searxng`, `duckduckgo`, `exa`, `perplexity`, `gemini`, or `kimi` (auto-selects when no provider or routing is configured; Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are explicit-only) |
|
|
135
|
+
| `provider` | Configured provider when omitted or set to `auto`; `all` searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper simultaneously; otherwise `openai`, `brave`, `parallel`, `parallel-mcp`, `tinyfish`, `search1api`, `searchinfinity`, `querit`, `tavily`, `firecrawl`, `jina`, `serpdive`, `kagi`, `bocha`, `ollama`, `anysearch`, `xcrawl`, `valyu`, `xai`, `brightdata`, `serpbase`, `serper`, `searxng`, `duckduckgo`, `exa`, `perplexity`, `gemini`, or `kimi` (auto-selects when no provider or routing is configured; Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper are explicit-only) |
|
|
136
136
|
| `includeContent` | Fetch full page content from sources in background |
|
|
137
137
|
| `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) |
|
|
138
138
|
|
|
@@ -159,11 +159,15 @@ fetch_content({ url: "https://example.com/diagram.png" })
|
|
|
159
159
|
| `url` / `urls` | Single URL/path or multiple URLs |
|
|
160
160
|
| `prompt` | Question for video analysis, or the page-local question required by `mode: "answer"` |
|
|
161
161
|
| `mode` | `readable` (default), `raw` for exact textual HTTP bodies, or `answer` for a grounded answer from fetched content |
|
|
162
|
-
| `answerModel` | Optional `provider/model-id` override for answer mode; defaults to the current enabled Pi model |
|
|
162
|
+
| `answerModel` | Optional `provider/model-id` override for answer mode; defaults to the configured `fetch.answerProvider` + `fetch.answerModel` pair, or the current enabled Pi model when no pair is configured |
|
|
163
163
|
| `timestamp` | Extract frame(s) — single (`"23:41"`), range (`"23:41-25:00"`), or seconds (`"85"`) |
|
|
164
164
|
| `frames` | Number of frames to extract (max 12) |
|
|
165
165
|
| `forceClone` | Clone GitHub repos that exceed the 350MB size threshold |
|
|
166
166
|
|
|
167
|
+
For a standing answer-mode model, set both `fetch.answerProvider` and `fetch.answerModel` in `web-search.json`; a per-call `answerModel` takes precedence. Configured answer defaults are opt-in and can send fetched page text to a different provider/model, which may change privacy and cost behavior.
|
|
168
|
+
|
|
169
|
+
Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #328.
|
|
170
|
+
|
|
167
171
|
### get_search_content
|
|
168
172
|
|
|
169
173
|
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.
|
|
@@ -403,6 +407,11 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
403
407
|
"providers": ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "brightdata", "gemini"],
|
|
404
408
|
"allowRemoteHostedProviders": false
|
|
405
409
|
},
|
|
410
|
+
"fetch": {
|
|
411
|
+
"timeout": 30,
|
|
412
|
+
"answerProvider": "openai",
|
|
413
|
+
"answerModel": "gpt-5.6"
|
|
414
|
+
},
|
|
406
415
|
"webSearch": {
|
|
407
416
|
"enabled": true
|
|
408
417
|
},
|
|
@@ -479,7 +488,7 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
479
488
|
|
|
480
489
|
`summaryModel` accepts an optional thinking-level suffix, such as `anthropic/claude-haiku-4-5:low`. Supported suffixes are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`.
|
|
481
490
|
|
|
482
|
-
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 `!`:
|
|
491
|
+
All provider API-key fields (`openaiApiKey`, `braveApiKey`, `parallelApiKey`, `tinyfishApiKey`, `search1apiApiKey`, `searchinfinityApiKey`, `queritApiKey`, `tavilyApiKey`, `jinaApiKey`, `serpdiveApiKey`, `kagiApiKey`, `bochaApiKey`, `ollamaApiKey`, `serpbaseApiKey`, `anysearchApiKey`, `xcrawlApiKey`, `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 `!`:
|
|
483
492
|
|
|
484
493
|
```json
|
|
485
494
|
{
|
|
@@ -502,6 +511,10 @@ Set `braveBaseUrl`, `exaBaseUrl`, or `tavilyBaseUrl` to route those providers th
|
|
|
502
511
|
|
|
503
512
|
`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.
|
|
504
513
|
|
|
514
|
+
`fetch.timeout` is an optional positive finite number of seconds for direct HTTP fetches and the Jina Reader fallback. When omitted, both use a 30-second budget. Fractional values are supported and rounded up to at least 1 millisecond; values that cannot be converted to a finite safe integer delay from 1 through Node's 2,147,483,647 ms timer maximum are rejected. An invalid declared value fails closed with an error naming `web-search.json`. An internal/per-call `timeoutMs` override takes precedence over this setting. Other remote extraction fallbacks keep their own documented budgets.
|
|
515
|
+
|
|
516
|
+
`fetch.answerProvider` and `fetch.answerModel` are an optional pair that selects the model used by `fetch_content` answer mode when no per-call `answerModel` is supplied. Both values must be non-empty strings and must identify an enabled text-capable model available in Pi's model registry; invalid or partial configuration fails closed. This is opt-in: answering with the configured provider/model can send fetched page text outside the current session and may incur that provider's costs. A per-call `answerModel` override is resolved first and remains usable even when these configured defaults are malformed.
|
|
517
|
+
|
|
505
518
|
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.
|
|
506
519
|
|
|
507
520
|
**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"`.
|
|
@@ -526,9 +539,7 @@ Bright Data Web Unlocker is a paid `fetch_content` fallback after Parallel and b
|
|
|
526
539
|
|
|
527
540
|
**Parallel MCP.** Select `provider: "parallel-mcp"` to use Parallel Search MCP without an API key, or add it to `searchRouting`. It is explicit-only and is never chosen by `auto` or `provider: "all"`; the existing `parallel` provider remains the key-required REST API. A configured `parallelApiKey` or `PARALLEL_API_KEY` is sent as an optional Bearer token for higher MCP limits. To use MCP `web_fetch`, add `parallel-mcp` to `fetchRouting.providers` and set `fetchRouting.allowRemoteHostedProviders` to `true`; it is not part of the default fetch route.
|
|
528
541
|
|
|
529
|
-
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. `openaiSearchProviders` sets which Pi model providers OpenAI `web_search` resolves login credentials from, in priority order; it defaults to `["openai-codex", "openai"]`, entries that are not registered or not signed in are skipped, and an empty array skips Pi credentials entirely so the `openaiApiKey` / `OPENAI_API_KEY` fallback applies. Useful for choosing between multiple Codex accounts (for example a second account registered by an extension) or forcing API-key billing while signed into Codex. 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. Alternatively, set `geminiAuth` to `"adc"` to authenticate Gemini generate-content calls with Google Application Default Credentials (ADC) instead of an API key; calls go to the Vertex AI endpoint (`aiplatform.googleapis.com`) with an OAuth bearer token minted from the ADC file (`GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`, i.e. `gcloud auth application-default login`). `geminiProject`/`geminiLocation` set the Vertex project and location and fall back to the `GOOGLE_CLOUD_PROJECT`/`GOOGLE_CLOUD_LOCATION` (or `GCLOUD_PROJECT`) env vars; project and location are required. ADC supports `authorized_user` (OAuth refresh token) and `service_account` (JWT assertion) credential files, and tokens are cached and refreshed from expiry. ADC mode covers search, URL context, and PDF/inline-data extraction; YouTube and local video analysis still go through the Gemini Files API, so they fall back to Gemini Web unless a `GEMINI_API_KEY` is also configured. The access token is treated as a credential and is redacted from errors. 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"`, `"parallel-mcp"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"firecrawl"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"valyu"`, `"xai"`, `"brightdata"`, `"serpbase"`, `"serper"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. Parallel MCP, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper 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`. `browserCookies.profile` 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. macOS discovery supports Helium, Chrome, Brave, and Arc; Linux discovery supports Chromium and Chrome. `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.
|
|
530
|
-
`VALYU_API_KEY` and `SERPER_API_KEY` also retain this precedence. `provider` and `searchProvider` also accept `"parallel-mcp"`, `"kimi"`, `"valyu"`, and `"serper"`; all four remain explicit-only.
|
|
531
|
-
|
|
542
|
+
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`, `SERPER_API_KEY`, `ANYSEARCH_API_KEY`, `XCRAWL_API_KEY`, `VALYU_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. `openaiSearchProviders` sets which Pi model providers OpenAI `web_search` resolves login credentials from, in priority order; it defaults to `["openai-codex", "openai"]`, entries that are not registered or not signed in are skipped, and an empty array skips Pi credentials entirely so the `openaiApiKey` / `OPENAI_API_KEY` fallback applies. Useful for choosing between multiple Codex accounts (for example a second account registered by an extension) or forcing API-key billing while signed into Codex. 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. Alternatively, set `geminiAuth` to `"adc"` to authenticate Gemini generate-content calls with Google Application Default Credentials (ADC) instead of an API key; calls go to the Vertex AI endpoint (`aiplatform.googleapis.com`) with an OAuth bearer token minted from the ADC file (`GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`, i.e. `gcloud auth application-default login`). `geminiProject`/`geminiLocation` set the Vertex project and location and fall back to the `GOOGLE_CLOUD_PROJECT`/`GOOGLE_CLOUD_LOCATION` (or `GCLOUD_PROJECT`) env vars; project and location are required. ADC supports `authorized_user` (OAuth refresh token) and `service_account` (JWT assertion) credential files, and tokens are cached and refreshed from expiry. ADC mode covers search, URL context, and PDF/inline-data extraction; YouTube and local video analysis still go through the Gemini Files API, so they fall back to Gemini Web unless a `GEMINI_API_KEY` is also configured. The access token is treated as a credential and is redacted from errors. 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"`, `"parallel-mcp"`, `"tinyfish"`, `"search1api"`, `"searchinfinity"`, `"querit"`, `"tavily"`, `"firecrawl"`, `"jina"`, `"serpdive"`, `"kagi"`, `"bocha"`, `"ollama"`, `"anysearch"`, `"xcrawl"`, `"valyu"`, `"xai"`, `"brightdata"`, `"serpbase"`, `"serper"`, `"searxng"`, `"exa"`, `"perplexity"`, or `"gemini"`. Parallel MCP, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper 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`. `browserCookies.profile` 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. macOS discovery supports Helium, Chrome, Brave, and Arc; Linux discovery supports Chromium and Chrome. `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.
|
|
532
543
|
### Kimi Code Plan
|
|
533
544
|
|
|
534
545
|
Run `/login kimi-coding` in Pi and complete sign-in for an active Kimi Code Plan. Then select `provider: "kimi"`, include `"kimi"` in an explicit provider array, or add it to `searchRouting.providers`. The extension resolves a model with provider `kimi-coding` from Pi's model registry and reuses Pi's refreshed OAuth credential; no Moonshot Open Platform key is configured here.
|
|
@@ -539,7 +550,7 @@ Kimi is explicit-only: it is never chosen by `auto` and never participates in `p
|
|
|
539
550
|
|
|
540
551
|
### All providers
|
|
541
552
|
|
|
542
|
-
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. Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are always excluded because they are explicit-only; Bright Data, SerpBase, and Serper are paid Google SERP providers, while Kimi draws from the user's shared Code Plan quota, so `all` never spends either resource without an explicit request. 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.
|
|
553
|
+
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. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper are always excluded because they are explicit-only; Bright Data, SerpBase, and Serper are paid Google SERP providers, while Kimi draws from the user's shared Code Plan quota, so `all` never spends either resource without an explicit request. 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.
|
|
543
554
|
|
|
544
555
|
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. Configured Firecrawl participates in `all` like other eligible providers. 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.
|
|
545
556
|
|
|
@@ -626,6 +637,10 @@ Search requests follow the official [`querit-python`](https://github.com/querit-
|
|
|
626
637
|
|
|
627
638
|
AnySearch is an explicit-only provider: it is never included in zero-config `auto` fallback or in `provider: "all"`, but it can be selected with `provider: "anysearch"`, configured as the named provider, or placed in `searchRouting`. It supports anonymous requests and optional `anysearchApiKey` / `ANYSEARCH_API_KEY` credentials. Requests intentionally send only `{ query, max_results }`; `recencyFilter`, `domainFilter`, and `includeContent` do not add API request parameters. When `includeContent` is true, returned `content` fields are exposed as inline content.
|
|
628
639
|
|
|
640
|
+
### XCrawl
|
|
641
|
+
|
|
642
|
+
XCrawl is an explicit-only provider: it is never included in zero-config `auto` fallback or in `provider: "all"`, but it can be selected with `provider: "xcrawl"`, configured as the named provider, or placed in `searchRouting`. It requires `xcrawlApiKey` / `XCRAWL_API_KEY` credentials ([dashboard](https://dash.xcrawl.com/)). Requests send `{ engine: "google_search", q }` to the SERP endpoint; `recencyFilter`, `domainFilter`, and `includeContent` do not add API request parameters, and the shared include/exclude `domainFilter` is applied client-side. A provider-side timeout surfaces as a retriable failure rather than caller cancellation. Results with a null title fall back to the result URL; result items expose their SERP snippet as usual.
|
|
643
|
+
|
|
629
644
|
### xAI (Grok)
|
|
630
645
|
|
|
631
646
|
xAI is an explicit-only provider: it is never included in zero-config `auto` fallback or in `provider: "all"`, but it can be selected with `provider: "xai"`, configured as the named provider, or placed in `searchRouting`.
|
|
@@ -866,7 +881,7 @@ Values use the same format as pi keybindings (e.g. `ctrl+s`, `ctrl+shift+s`, `al
|
|
|
866
881
|
|
|
867
882
|
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, and `githubPrIssue.enabled: false` only skips PR/issue specialization; neither setting unregisters `fetch_content` or blocks generic URL extraction. Pi restart is required for tool and command registration changes.
|
|
868
883
|
|
|
869
|
-
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
|
|
884
|
+
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; direct HTTP fetches and Jina Reader use a 30s timeout by default, configurable together with `fetch.timeout` in seconds. Remote extraction fallbacks carry their own budgets and are not covered by that setting: 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. `pdf.maxPages` defaults to 100 and limits every PDF provider to the first N pages.
|
|
870
885
|
|
|
871
886
|
## Limitations
|
|
872
887
|
|
|
@@ -905,6 +920,7 @@ Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Sear
|
|
|
905
920
|
| `serpbase.ts` | Explicit-only SerpBase Google SERP provider |
|
|
906
921
|
| `serper.ts` | Explicit-only Serper Google SERP provider |
|
|
907
922
|
| `anysearch.ts` | Explicit-only AnySearch search provider |
|
|
923
|
+
| `xcrawl.ts` | Explicit-only XCrawl search provider |
|
|
908
924
|
| `valyu.ts` | Explicit-only Valyu research search provider |
|
|
909
925
|
| `xai-search.ts` | Explicit-only xAI (Grok) hosted web_search provider |
|
|
910
926
|
| `kimi-search.ts` | Explicit-only Kimi Code Plan search provider |
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ProviderAvailability } from "./gemini-search.ts";
|
|
2
|
+
|
|
1
3
|
function safeInlineJSON(data: unknown): string {
|
|
2
4
|
return JSON.stringify(data)
|
|
3
5
|
.replace(/</g, "\\u003c")
|
|
@@ -8,7 +10,7 @@ function safeInlineJSON(data: unknown): string {
|
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
function buildProviderButtons(
|
|
11
|
-
available:
|
|
13
|
+
available: ProviderAvailability,
|
|
12
14
|
selected: string,
|
|
13
15
|
hasInitialQueries: boolean,
|
|
14
16
|
): string {
|
|
@@ -36,6 +38,7 @@ function buildProviderButtons(
|
|
|
36
38
|
{ value: "gemini", label: "Gemini", available: available.gemini },
|
|
37
39
|
{ value: "kimi", label: "Kimi", available: available.kimi },
|
|
38
40
|
{ value: "anysearch", label: "AnySearch", available: available.anysearch },
|
|
41
|
+
{ value: "xcrawl", label: "XCrawl", available: available.xcrawl },
|
|
39
42
|
{ value: "xai", label: "xAI", available: available.xai },
|
|
40
43
|
{ value: "brightdata", label: "Bright Data", available: available.brightdata },
|
|
41
44
|
{ value: "serpbase", label: "SerpBase", available: available.serpbase },
|
|
@@ -59,7 +62,7 @@ export function generateCuratorPage(
|
|
|
59
62
|
queries: string[],
|
|
60
63
|
sessionToken: string,
|
|
61
64
|
timeout: number,
|
|
62
|
-
availableProviders:
|
|
65
|
+
availableProviders: ProviderAvailability,
|
|
63
66
|
defaultProvider: string,
|
|
64
67
|
searchProvider: string,
|
|
65
68
|
summaryModels: Array<{ value: string; label: string }>,
|
|
@@ -677,6 +680,11 @@ main {
|
|
|
677
680
|
background: rgba(249, 199, 79, 0.14);
|
|
678
681
|
border-color: rgba(249, 199, 79, 0.3);
|
|
679
682
|
}
|
|
683
|
+
.provider-tag.provider-xcrawl {
|
|
684
|
+
color: #7dd3ae;
|
|
685
|
+
background: rgba(125, 211, 174, 0.14);
|
|
686
|
+
border-color: rgba(125, 211, 174, 0.3);
|
|
687
|
+
}
|
|
680
688
|
.provider-tag.provider-xai {
|
|
681
689
|
color: #c4b5fd;
|
|
682
690
|
background: rgba(196, 181, 253, 0.14);
|
|
@@ -1460,7 +1468,7 @@ const SCRIPT = `(function() {
|
|
|
1460
1468
|
var token = DATA.sessionToken;
|
|
1461
1469
|
var timeoutSec = DATA.timeout;
|
|
1462
1470
|
var queries = Array.isArray(DATA.queries) ? DATA.queries : [];
|
|
1463
|
-
var providers = ["all", "openai", "exa", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "bocha", "ollama", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "anysearch", "xai", "brightdata", "serpbase", "serper", "valyu"];
|
|
1471
|
+
var providers = ["all", "openai", "exa", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "bocha", "ollama", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "anysearch", "xcrawl", "xai", "brightdata", "serpbase", "serper", "valyu"];
|
|
1464
1472
|
var availProviders = DATA.availableProviders && typeof DATA.availableProviders === "object" ? DATA.availableProviders : {};
|
|
1465
1473
|
var workflow = "summary-review";
|
|
1466
1474
|
var initialDefaultProvider = typeof DATA.defaultProvider === "string" ? DATA.defaultProvider : "exa";
|
|
@@ -1685,6 +1693,7 @@ const SCRIPT = `(function() {
|
|
|
1685
1693
|
if (provider === "gemini") return "Gemini";
|
|
1686
1694
|
if (provider === "kimi") return "Kimi";
|
|
1687
1695
|
if (provider === "anysearch") return "AnySearch";
|
|
1696
|
+
if (provider === "xcrawl") return "XCrawl";
|
|
1688
1697
|
if (provider === "xai") return "xAI";
|
|
1689
1698
|
if (provider === "brightdata") return "Bright Data";
|
|
1690
1699
|
if (provider === "serpbase") return "SerpBase";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http, { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import { generateCuratorPage } from "./curator-page.ts";
|
|
3
|
+
import type { ProviderAvailability } from "./gemini-search.ts";
|
|
3
4
|
import type { SummaryMeta } from "./summary-review.ts";
|
|
4
5
|
import { resolveCuratorNetworkConfig } from "./utils.ts";
|
|
5
6
|
|
|
@@ -18,7 +19,7 @@ export interface CuratorServerOptions {
|
|
|
18
19
|
queries: string[];
|
|
19
20
|
sessionToken: string;
|
|
20
21
|
timeout: number;
|
|
21
|
-
availableProviders:
|
|
22
|
+
availableProviders: ProviderAvailability;
|
|
22
23
|
defaultProvider: string;
|
|
23
24
|
searchProvider: string;
|
|
24
25
|
summaryModels: Array<{ value: string; label: string }>;
|
|
@@ -294,6 +295,7 @@ export function startCuratorServer(
|
|
|
294
295
|
if (provider === "gemini") return availableProviders.gemini;
|
|
295
296
|
if (provider === "kimi") return availableProviders.kimi;
|
|
296
297
|
if (provider === "anysearch") return availableProviders.anysearch;
|
|
298
|
+
if (provider === "xcrawl") return availableProviders.xcrawl;
|
|
297
299
|
if (provider === "xai") return availableProviders.xai;
|
|
298
300
|
if (provider === "brightdata") return availableProviders.brightdata;
|
|
299
301
|
if (provider === "serpbase") return availableProviders.serpbase;
|
|
@@ -31,22 +31,89 @@ import { getBrowserCookiesForHosts, getLastBrowserCookieDiagnostic } from "./chr
|
|
|
31
31
|
import { sanitizeInlineDataUris } from "./data-uri-sanitize.ts";
|
|
32
32
|
|
|
33
33
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
34
|
+
const MAX_CONFIGURED_TIMEOUT_MS = 2_147_483_647;
|
|
34
35
|
const CONCURRENT_LIMIT = 3;
|
|
36
|
+
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
37
|
+
|
|
38
|
+
function loadFetchTimeoutMs(): number {
|
|
39
|
+
if (!existsSync(WEB_SEARCH_CONFIG_PATH)) return DEFAULT_TIMEOUT_MS;
|
|
40
|
+
|
|
41
|
+
let raw: unknown;
|
|
42
|
+
try {
|
|
43
|
+
raw = JSON.parse(readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8"));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
49
|
+
throw new Error(`Invalid config in ${WEB_SEARCH_CONFIG_PATH}: expected a JSON object`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fetchConfig = (raw as Record<string, unknown>).fetch;
|
|
53
|
+
if (fetchConfig === undefined) return DEFAULT_TIMEOUT_MS;
|
|
54
|
+
if (!fetchConfig || typeof fetchConfig !== "object" || Array.isArray(fetchConfig)) {
|
|
55
|
+
throw new Error(`fetch in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const value = (fetchConfig as Record<string, unknown>).timeout;
|
|
59
|
+
if (value === undefined) return DEFAULT_TIMEOUT_MS;
|
|
60
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
61
|
+
throw new Error(`Invalid fetch.timeout in ${WEB_SEARCH_CONFIG_PATH}: expected a positive finite number of seconds, got ${JSON.stringify(value)}`);
|
|
62
|
+
}
|
|
63
|
+
const timeoutMs = Math.ceil(value * 1000);
|
|
64
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_CONFIGURED_TIMEOUT_MS) {
|
|
65
|
+
throw new Error(`Invalid fetch.timeout in ${WEB_SEARCH_CONFIG_PATH}: converted timeout must be a finite safe integer from 1 through ${MAX_CONFIGURED_TIMEOUT_MS} milliseconds`);
|
|
66
|
+
}
|
|
67
|
+
return Math.max(1, timeoutMs);
|
|
68
|
+
}
|
|
35
69
|
|
|
36
70
|
const NON_RECOVERABLE_ERRORS = ["Unsupported content type", "Response too large", "PDF extraction is disabled", "Image fetching is disabled"];
|
|
37
71
|
const MIN_USEFUL_CONTENT = 500;
|
|
38
72
|
const SUPPORTED_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
39
|
-
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
40
73
|
const FETCH_PROVIDERS = ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "parallel-mcp", "brightdata", "gemini"] as const;
|
|
41
74
|
type FetchProvider = typeof FETCH_PROVIDERS[number];
|
|
42
75
|
type FetchRouting = { providers: FetchProvider[]; allowRemoteHostedProviders: boolean };
|
|
43
76
|
const DEFAULT_FETCH_PROVIDER_ORDER: FetchProvider[] = ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "brightdata", "gemini"];
|
|
44
77
|
const REMOTE_HOSTED_FETCH_PROVIDERS = new Set<FetchProvider>(["jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "parallel-mcp", "brightdata", "gemini"]);
|
|
45
78
|
|
|
79
|
+
function isDefuddleConsoleError(args: Parameters<typeof console.error>): boolean {
|
|
80
|
+
const prefix = args[0];
|
|
81
|
+
return prefix === "Defuddle" || (typeof prefix === "string" && /^Defuddle(?:\s|:)/.test(prefix));
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
async function extractWithDefuddle(text: string, url: string): Promise<{ title: string; content: string } | null> {
|
|
47
85
|
const { Defuddle } = await import("defuddle/node");
|
|
48
86
|
const { document } = parseHTML(text);
|
|
49
|
-
|
|
87
|
+
Object.defineProperty(document, "location", {
|
|
88
|
+
value: new URL(url),
|
|
89
|
+
configurable: true,
|
|
90
|
+
});
|
|
91
|
+
let processingError: unknown;
|
|
92
|
+
const originalConsoleError = console.error;
|
|
93
|
+
console.error = (...args) => {
|
|
94
|
+
if (isDefuddleConsoleError(args)) {
|
|
95
|
+
if (args[0] === "Defuddle" && args[1] === "Error processing document:") {
|
|
96
|
+
processingError = args[2];
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
originalConsoleError(...args);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let resultPromise: ReturnType<typeof Defuddle>;
|
|
104
|
+
try {
|
|
105
|
+
// With useAsync:false, Defuddle parses synchronously before returning its promise.
|
|
106
|
+
// Keep the console interception limited to that call so unrelated Pi output is
|
|
107
|
+
// never routed through this fallback's handler.
|
|
108
|
+
resultPromise = Defuddle(document as unknown as Document, url, { markdown: true, useAsync: false });
|
|
109
|
+
} finally {
|
|
110
|
+
console.error = originalConsoleError;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = await resultPromise;
|
|
114
|
+
if (processingError !== undefined) {
|
|
115
|
+
throw new Error(`Defuddle failed to process document: ${errorMessage(processingError)}`);
|
|
116
|
+
}
|
|
50
117
|
return typeof result.content === "string" ? { title: result.title, content: result.content } : null;
|
|
51
118
|
}
|
|
52
119
|
|
|
@@ -268,11 +335,16 @@ export interface ExtractOptions {
|
|
|
268
335
|
lookup?: Lookup;
|
|
269
336
|
}
|
|
270
337
|
|
|
338
|
+
/** Resolve the direct HTTP/Jina fetch budget, with a per-call override taking precedence. */
|
|
339
|
+
export function resolveFetchTimeoutMs(options?: Pick<ExtractOptions, "timeoutMs">): number {
|
|
340
|
+
return options?.timeoutMs ?? loadFetchTimeoutMs();
|
|
341
|
+
}
|
|
342
|
+
|
|
271
343
|
const JINA_READER_BASE = "https://r.jina.ai/";
|
|
272
|
-
const JINA_TIMEOUT_MS = 30000;
|
|
273
344
|
|
|
274
345
|
async function extractWithJinaReader(
|
|
275
346
|
url: string,
|
|
347
|
+
timeoutMs: number,
|
|
276
348
|
signal?: AbortSignal,
|
|
277
349
|
lookup?: Lookup,
|
|
278
350
|
): Promise<ExtractedContent | null> {
|
|
@@ -295,7 +367,7 @@ async function extractWithJinaReader(
|
|
|
295
367
|
"X-No-Cache": "true",
|
|
296
368
|
},
|
|
297
369
|
signal: AbortSignal.any([
|
|
298
|
-
AbortSignal.timeout(
|
|
370
|
+
AbortSignal.timeout(timeoutMs),
|
|
299
371
|
...(signal ? [signal] : []),
|
|
300
372
|
]),
|
|
301
373
|
});
|
|
@@ -450,18 +522,14 @@ export async function extractContent(
|
|
|
450
522
|
}
|
|
451
523
|
}
|
|
452
524
|
|
|
453
|
-
if (options?.authFetchProfile) {
|
|
525
|
+
if (options?.authFetchProfile || options?.mode === "raw") {
|
|
454
526
|
try {
|
|
455
|
-
return await extractViaHttp(url, signal, options);
|
|
527
|
+
return await extractViaHttp(url, resolveFetchTimeoutMs(options), signal, options);
|
|
456
528
|
} catch (err) {
|
|
457
529
|
return { url, title: "", content: "", error: errorMessage(err) };
|
|
458
530
|
}
|
|
459
531
|
}
|
|
460
532
|
|
|
461
|
-
if (options?.mode === "raw") {
|
|
462
|
-
return extractViaHttp(url, signal, options);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
533
|
if (options?.frames || options?.timestamp) {
|
|
466
534
|
const disabled = imageGateError();
|
|
467
535
|
if (disabled) return { url, title: "", content: "", error: disabled };
|
|
@@ -673,6 +741,13 @@ export async function extractContent(
|
|
|
673
741
|
|
|
674
742
|
if (signal?.aborted) return abortedResult(url);
|
|
675
743
|
|
|
744
|
+
let fetchTimeoutMs: number;
|
|
745
|
+
try {
|
|
746
|
+
fetchTimeoutMs = resolveFetchTimeoutMs(options);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
return { url, title: "", content: "", error: errorMessage(err) };
|
|
749
|
+
}
|
|
750
|
+
|
|
676
751
|
let fetchRouting: FetchRouting;
|
|
677
752
|
try {
|
|
678
753
|
fetchRouting = loadFetchRouting();
|
|
@@ -701,7 +776,7 @@ export async function extractContent(
|
|
|
701
776
|
? { ...httpResult, error: message }
|
|
702
777
|
: { url, title: "", content: "", error: message };
|
|
703
778
|
const runHttpProvider = async (): Promise<ExtractedContent | null> => {
|
|
704
|
-
const { declaredLinks: discoveredLinks = [], ...result } = await extractViaHttp(url, signal, options);
|
|
779
|
+
const { declaredLinks: discoveredLinks = [], ...result } = await extractViaHttp(url, fetchTimeoutMs, signal, options);
|
|
705
780
|
httpResult = result;
|
|
706
781
|
declaredLinks = discoveredLinks;
|
|
707
782
|
if (signal?.aborted) return abortedResult(url);
|
|
@@ -756,7 +831,7 @@ export async function extractContent(
|
|
|
756
831
|
}
|
|
757
832
|
|
|
758
833
|
if (provider === "jina") {
|
|
759
|
-
const jinaResult = await extractWithJinaReader(url, signal, options?.lookup);
|
|
834
|
+
const jinaResult = await extractWithJinaReader(url, fetchTimeoutMs, signal, options?.lookup);
|
|
760
835
|
if (jinaResult) return withDeclaredLinks(jinaResult);
|
|
761
836
|
continue;
|
|
762
837
|
}
|
|
@@ -1036,10 +1111,10 @@ function responseSizeLimitError(maxBytes: number): Error {
|
|
|
1036
1111
|
|
|
1037
1112
|
async function extractViaHttp(
|
|
1038
1113
|
url: string,
|
|
1114
|
+
timeoutMs: number,
|
|
1039
1115
|
signal?: AbortSignal,
|
|
1040
1116
|
options?: ExtractOptions,
|
|
1041
1117
|
): Promise<HttpExtractedContent> {
|
|
1042
|
-
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1043
1118
|
const activityId = activityMonitor.logStart({ type: "fetch", url });
|
|
1044
1119
|
|
|
1045
1120
|
const controller = new AbortController();
|
|
@@ -30,6 +30,7 @@ import { isOllamaAvailable, searchWithOllama } from "./ollama.ts";
|
|
|
30
30
|
import { isSearXNGAvailable, searchWithSearXNG } from "./searxng.ts";
|
|
31
31
|
import { isDuckDuckGoAvailable, searchWithDuckDuckGo } from "./duckduckgo.ts";
|
|
32
32
|
import { isAnySearchAvailable, searchWithAnySearch } from "./anysearch.ts";
|
|
33
|
+
import { isXcrawlAvailable, searchWithXCrawl } from "./xcrawl.ts";
|
|
33
34
|
import { isXaiSearchAvailable, searchWithXai } from "./xai-search.ts";
|
|
34
35
|
import { isBrightDataAvailable, searchWithBrightData } from "./brightdata.ts";
|
|
35
36
|
import { isSerpBaseAvailable, searchWithSerpBase } from "./serpbase.ts";
|
|
@@ -38,12 +39,13 @@ import { isValyuAvailable, searchWithValyu } from "./valyu.ts";
|
|
|
38
39
|
import { isKimiSearchAvailable, searchWithKimi } from "./kimi-search.ts";
|
|
39
40
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
40
41
|
|
|
41
|
-
export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "brightdata", "serpbase", "serper", "valyu", "bocha"] as const;
|
|
42
|
+
export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "brightdata", "serpbase", "serper", "valyu", "bocha", "xcrawl"] as const;
|
|
42
43
|
export const SEARCH_PROVIDERS = ["auto", "all", ...RESOLVED_SEARCH_PROVIDERS] as const;
|
|
43
44
|
|
|
44
45
|
export type ResolvedSearchProvider = typeof RESOLVED_SEARCH_PROVIDERS[number];
|
|
45
46
|
export type SearchProvider = typeof SEARCH_PROVIDERS[number];
|
|
46
47
|
export type SearchProviderSelection = SearchProvider | ResolvedSearchProvider[];
|
|
48
|
+
export type ProviderAvailability = { all: boolean } & Record<ResolvedSearchProvider, boolean>;
|
|
47
49
|
export type SearchProviderErrorKind =
|
|
48
50
|
| "transient"
|
|
49
51
|
| "quota"
|
|
@@ -102,9 +104,9 @@ export interface AttributedSearchResponse extends SearchResponse {
|
|
|
102
104
|
|
|
103
105
|
const CONFIG_PATH = getWebSearchConfigPath();
|
|
104
106
|
const DEFAULT_SEARCH_MODEL = "gemini-3.6-flash";
|
|
105
|
-
// Explicit-only providers (Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Bright Data, SerpBase, Serper, Valyu) are deliberately absent:
|
|
107
|
+
// Explicit-only providers (Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, xAI, Bright Data, SerpBase, Serper, Valyu) are deliberately absent:
|
|
106
108
|
// `all` must never fan out to an opt-in or paid provider without the user asking for it.
|
|
107
|
-
const ALL_SEARCH_PROVIDERS: ResolvedSearchProvider[] = ["searxng", "openai", "exa", "brave", "parallel", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "ollama", "perplexity", "gemini", "bocha"];
|
|
109
|
+
export const ALL_SEARCH_PROVIDERS: ResolvedSearchProvider[] = ["searxng", "openai", "exa", "brave", "parallel", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "ollama", "perplexity", "gemini", "bocha"];
|
|
108
110
|
const VALID_ROUTING_KINDS = ["transient", "quota", "network", "invalid-response", "unsupported"] as const;
|
|
109
111
|
|
|
110
112
|
type SearchConfig = {
|
|
@@ -362,6 +364,7 @@ async function searchWithResolvedProvider(
|
|
|
362
364
|
if (provider === "serpbase") return { ...(await searchWithSerpBase(query, options)), provider };
|
|
363
365
|
if (provider === "serper") return { ...(await searchWithSerper(query, options)), provider };
|
|
364
366
|
if (provider === "valyu") return { ...(await searchWithValyu(query, options)), provider };
|
|
367
|
+
if (provider === "xcrawl") return { ...(await searchWithXCrawl(query, options)), provider };
|
|
365
368
|
if (provider === "perplexity") return { ...(await searchWithPerplexity(query, options)), provider };
|
|
366
369
|
if (provider === "searxng") return { ...(await searchWithSearXNG(query, options)), provider };
|
|
367
370
|
if (provider === "duckduckgo") return { ...(await searchWithDuckDuckGo(query, options)), provider };
|
|
@@ -408,6 +411,7 @@ async function isResolvedProviderAvailable(provider: ResolvedSearchProvider, opt
|
|
|
408
411
|
if (provider === "serpbase") return isSerpBaseAvailable();
|
|
409
412
|
if (provider === "serper") return isSerperAvailable();
|
|
410
413
|
if (provider === "valyu") return isValyuAvailable();
|
|
414
|
+
if (provider === "xcrawl") return isXcrawlAvailable();
|
|
411
415
|
if (provider === "perplexity") return isPerplexityAvailable();
|
|
412
416
|
if (provider === "searxng") return isSearXNGAvailable();
|
|
413
417
|
if (provider === "duckduckgo") return isDuckDuckGoAvailable();
|
|
@@ -437,6 +441,7 @@ function providerLabel(provider: ResolvedSearchProvider): string {
|
|
|
437
441
|
if (provider === "duckduckgo") return "DuckDuckGo";
|
|
438
442
|
if (provider === "kagi") return "Kagi";
|
|
439
443
|
if (provider === "bocha") return "Bocha";
|
|
444
|
+
if (provider === "xcrawl") return "XCrawl";
|
|
440
445
|
if (provider === "kimi") return "Kimi";
|
|
441
446
|
if (provider === "ollama") return "Ollama";
|
|
442
447
|
if (provider === "xai") return "xAI";
|
|
@@ -470,7 +475,7 @@ async function searchWithProviders(
|
|
|
470
475
|
: await isResolvedProviderAvailable(provider, options),
|
|
471
476
|
})))).filter((entry) => entry.available).map((entry) => entry.provider);
|
|
472
477
|
if (providers.length === 0) {
|
|
473
|
-
throw new Error("No configured search provider available for provider \"all\". Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Bright Data, SerpBase, Serper, and
|
|
478
|
+
throw new Error("No configured search provider available for provider \"all\". Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Bright Data, SerpBase, Serper, Valyu, and XCrawl are excluded.");
|
|
474
479
|
}
|
|
475
480
|
|
|
476
481
|
const settled = await Promise.allSettled(
|
|
@@ -763,7 +768,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
|
|
|
763
768
|
" 3. Set OPENAI_API_KEY, BRAVE_API_KEY, PARALLEL_API_KEY, TINYFISH_API_KEY, SEARCH1API_KEY, SEARCHINFINITY_API_KEY, QUERIT_API_KEY, TAVILY_API_KEY, FIRECRAWL_BASE_URL, JINA_API_KEY, SERPDIVE_API_KEY, KAGI_API_KEY, BOCHA_API_KEY, OLLAMA_API_KEY, SEARXNG_BASE_URL, EXA_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY, or CLOUDFLARE_API_KEY env vars\n" +
|
|
764
769
|
" 4. Set GOOGLE_GEMINI_BASE_URL with CLOUDFLARE_API_KEY for Cloudflare AI Gateway routing\n" +
|
|
765
770
|
" 5. Sign into gemini.google.com in a supported Chromium-based browser\n" +
|
|
766
|
-
" 6. Explicitly select provider: \"anysearch\" for anonymous AnySearch, \"xai\" for Grok, \"brightdata\" with brightdataSerpZone for paid Bright Data SERP, \"serpbase\" or \"serper\" for Google SERP, or \"valyu\" for research search"
|
|
771
|
+
" 6. Explicitly select provider: \"anysearch\" for anonymous AnySearch, \"xcrawl\" for XCrawl, \"xai\" for Grok, \"brightdata\" with brightdataSerpZone for paid Bright Data SERP, \"serpbase\" or \"serper\" for Google SERP, or \"valyu\" for research search"
|
|
767
772
|
);
|
|
768
773
|
}
|
|
769
774
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { execFile, spawn, type ChildProcess } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { basename, dirname, extname, join, resolve as resolvePath, sep as pathSep } from "node:path";
|
|
@@ -59,6 +59,7 @@ interface GitHubCloneConfig {
|
|
|
59
59
|
const cloneCache = new Map<string, CachedClone>();
|
|
60
60
|
|
|
61
61
|
let cachedConfig: GitHubCloneConfig | null = null;
|
|
62
|
+
let cloneRuntime: { parentPath: string; rootPath: string } | null = null;
|
|
62
63
|
|
|
63
64
|
function normalizeEnabled(value: unknown, fallback: boolean): boolean {
|
|
64
65
|
return typeof value === "boolean" ? value : fallback;
|
|
@@ -189,10 +190,48 @@ function cacheKey(owner: string, repo: string, ref?: string): string {
|
|
|
189
190
|
return ref ? `${owner}/${repo}@${ref}` : `${owner}/${repo}`;
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
function removeCloneRuntime(parentPath: string, runtimePath: string): void {
|
|
194
|
+
const normalizedParentPath = resolvePath(parentPath);
|
|
195
|
+
const normalizedRuntimePath = resolvePath(runtimePath);
|
|
196
|
+
if (dirname(normalizedRuntimePath) !== normalizedParentPath || !basename(normalizedRuntimePath).startsWith("runtime-")) return;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const entry = lstatSync(normalizedRuntimePath);
|
|
200
|
+
if (entry.isSymbolicLink()) unlinkSync(normalizedRuntimePath);
|
|
201
|
+
else rmSync(normalizedRuntimePath, { recursive: true, force: true });
|
|
202
|
+
} catch {
|
|
203
|
+
// The runtime directory may already have been removed externally.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getCloneRuntimeRoot(config: GitHubCloneConfig): string | null {
|
|
208
|
+
if (cloneRuntime) return cloneRuntime.rootPath;
|
|
209
|
+
|
|
210
|
+
let parentPath: string | null = null;
|
|
211
|
+
let runtimePath: string | null = null;
|
|
212
|
+
try {
|
|
213
|
+
const configuredPath = resolvePath(config.clonePath);
|
|
214
|
+
mkdirSync(configuredPath, { recursive: true });
|
|
215
|
+
parentPath = realpathSync(configuredPath);
|
|
216
|
+
runtimePath = mkdtempSync(join(parentPath, "runtime-"));
|
|
217
|
+
chmodSync(runtimePath, 0o700);
|
|
218
|
+
const rootPath = realpathSync(runtimePath);
|
|
219
|
+
if (dirname(rootPath) !== parentPath) {
|
|
220
|
+
removeCloneRuntime(parentPath, runtimePath);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
cloneRuntime = { parentPath, rootPath };
|
|
224
|
+
return rootPath;
|
|
225
|
+
} catch {
|
|
226
|
+
if (parentPath && runtimePath) removeCloneRuntime(parentPath, runtimePath);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
192
231
|
function cloneDestination(config: GitHubCloneConfig, owner: string, repo: string, ref?: string): CloneDestination | null {
|
|
193
232
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
233
|
+
const rootPath = getCloneRuntimeRoot(config);
|
|
234
|
+
if (!rootPath) return null;
|
|
196
235
|
const digest = createHash("sha256").update(JSON.stringify([owner, repo, ref ?? null])).digest("hex");
|
|
197
236
|
const localPath = resolvePath(rootPath, digest);
|
|
198
237
|
if (dirname(localPath) !== rootPath) return null;
|
|
@@ -742,5 +781,10 @@ export function clearCloneCache(): void {
|
|
|
742
781
|
removeCloneDestination(entry.destination);
|
|
743
782
|
}
|
|
744
783
|
cloneCache.clear();
|
|
784
|
+
|
|
785
|
+
if (cloneRuntime) {
|
|
786
|
+
removeCloneRuntime(cloneRuntime.parentPath, cloneRuntime.rootPath);
|
|
787
|
+
}
|
|
788
|
+
cloneRuntime = null;
|
|
745
789
|
cachedConfig = null;
|
|
746
790
|
}
|