@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
|
@@ -9,7 +9,8 @@ import { findContent, type FindMode } from "./content-find.ts";
|
|
|
9
9
|
import { answerFromPage } from "./page-query.ts";
|
|
10
10
|
import { rewriteSearchQuery } from "./query-rewrite.ts";
|
|
11
11
|
import { clearCloneCache } from "./github-extract.ts";
|
|
12
|
-
import { getConfiguredSearchRouting, normalizeSearchProviderSelection, RESOLVED_SEARCH_PROVIDERS, SEARCH_PROVIDERS, search, type AttributedSearchResponse, type SearchProvider, type SearchProviderSelection, type ResolvedSearchProvider } from "./gemini-search.ts";
|
|
12
|
+
import { ALL_SEARCH_PROVIDERS, getConfiguredSearchRouting, normalizeSearchProviderSelection, RESOLVED_SEARCH_PROVIDERS, SEARCH_PROVIDERS, search, type AttributedSearchResponse, type ProviderAvailability, type SearchProvider, type SearchProviderSelection, type ResolvedSearchProvider } from "./gemini-search.ts";
|
|
13
|
+
export type { ProviderAvailability } from "./gemini-search.ts";
|
|
13
14
|
import type { SearchResult } from "./perplexity.ts";
|
|
14
15
|
import { formatSeconds, getWebSearchConfigDir, getWebSearchConfigPath, installGlobalProxyFetch, resolveCuratorNetworkConfig, runWithProxy } from "./utils.ts";
|
|
15
16
|
import {
|
|
@@ -68,6 +69,7 @@ import { isBrightDataAvailable } from "./brightdata.ts";
|
|
|
68
69
|
import { isSerpBaseAvailable } from "./serpbase.ts";
|
|
69
70
|
import { isSerperAvailable } from "./serper.ts";
|
|
70
71
|
import { isValyuAvailable } from "./valyu.ts";
|
|
72
|
+
import { isXcrawlAvailable } from "./xcrawl.ts";
|
|
71
73
|
import { buildSearchErrorPlan, type SearchErrorDetails, type SearchErrorPlan } from "./render-search-error.ts";
|
|
72
74
|
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns, splitThinkingSuffix } from "./summary-model-scope.ts";
|
|
73
75
|
import {
|
|
@@ -129,6 +131,7 @@ function renderSearchErrorPlan(plan: SearchErrorPlan, expanded: boolean, theme:
|
|
|
129
131
|
|
|
130
132
|
interface WebSearchConfig {
|
|
131
133
|
anysearchApiKey?: unknown;
|
|
134
|
+
xcrawlApiKey?: unknown;
|
|
132
135
|
brightdataApiKey?: unknown;
|
|
133
136
|
brightdataSerpZone?: unknown;
|
|
134
137
|
kagiApiKey?: unknown;
|
|
@@ -165,37 +168,6 @@ interface WebSearchConfig {
|
|
|
165
168
|
};
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
export interface ProviderAvailability {
|
|
169
|
-
all: boolean;
|
|
170
|
-
openai: boolean;
|
|
171
|
-
brave: boolean;
|
|
172
|
-
parallel: boolean;
|
|
173
|
-
"parallel-mcp": boolean;
|
|
174
|
-
tinyfish: boolean;
|
|
175
|
-
search1api: boolean;
|
|
176
|
-
searchinfinity: boolean;
|
|
177
|
-
querit: boolean;
|
|
178
|
-
tavily: boolean;
|
|
179
|
-
firecrawl: boolean;
|
|
180
|
-
jina: boolean;
|
|
181
|
-
serpdive: boolean;
|
|
182
|
-
searxng: boolean;
|
|
183
|
-
duckduckgo: boolean;
|
|
184
|
-
perplexity: boolean;
|
|
185
|
-
exa: boolean;
|
|
186
|
-
gemini: boolean;
|
|
187
|
-
kimi: boolean;
|
|
188
|
-
kagi: boolean;
|
|
189
|
-
bocha: boolean;
|
|
190
|
-
ollama: boolean;
|
|
191
|
-
anysearch: boolean;
|
|
192
|
-
xai: boolean;
|
|
193
|
-
brightdata: boolean;
|
|
194
|
-
serpbase: boolean;
|
|
195
|
-
serper: boolean;
|
|
196
|
-
valyu: boolean;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
171
|
type WebSearchWorkflow = "none" | "summary-review" | "auto-summary";
|
|
200
172
|
type CuratorWorkflow = "summary-review";
|
|
201
173
|
export type CuratorProvider = Exclude<SearchProvider, "auto">;
|
|
@@ -374,6 +346,33 @@ function normalizeQueryList(queryList: unknown[]): string[] {
|
|
|
374
346
|
return normalized;
|
|
375
347
|
}
|
|
376
348
|
|
|
349
|
+
// Some local models serialize a multi-query list into the single-string `query`
|
|
350
|
+
// field as a JSON array (query: "[\"a\", \"b\"]") instead of using the
|
|
351
|
+
// `queries` parameter. Forwarding that raw string verbatim makes every backend
|
|
352
|
+
// search for the literal array text and return zero results, with no signal to
|
|
353
|
+
// the model that its argument shape was wrong. Expand a string that parses as a
|
|
354
|
+
// JSON array of strings so each element is searched independently.
|
|
355
|
+
function expandQueryString(query: unknown): string[] {
|
|
356
|
+
if (typeof query !== "string") return [];
|
|
357
|
+
const trimmed = query.trim();
|
|
358
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
359
|
+
try {
|
|
360
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
361
|
+
// Only expand an unambiguously string-only array. Mixed or non-string
|
|
362
|
+
// arrays are kept as the literal query so we never silently drop
|
|
363
|
+
// members or collapse them into an empty search.
|
|
364
|
+
if (Array.isArray(parsed) && parsed.every((entry): entry is string => typeof entry === "string")) {
|
|
365
|
+
return parsed
|
|
366
|
+
.map((entry) => entry.trim())
|
|
367
|
+
.filter((entry) => entry.length > 0);
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
// Not JSON — treat as a literal query string.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return [query];
|
|
374
|
+
}
|
|
375
|
+
|
|
377
376
|
function getCuratorTimeoutSeconds(): number {
|
|
378
377
|
const source = loadConfig();
|
|
379
378
|
const explicit = normalizeCuratorTimeoutSeconds(source.curatorTimeoutSeconds);
|
|
@@ -422,15 +421,16 @@ async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderA
|
|
|
422
421
|
gemini: geminiApiAvail || !!geminiWebAvail,
|
|
423
422
|
kimi: await isKimiSearchAvailable(ctx),
|
|
424
423
|
anysearch: isAnySearchAvailable(),
|
|
424
|
+
xcrawl: isXcrawlAvailable(),
|
|
425
425
|
xai: await isXaiSearchAvailable(ctx),
|
|
426
426
|
brightdata: isBrightDataAvailable(),
|
|
427
427
|
serpbase: isSerpBaseAvailable(),
|
|
428
428
|
serper: isSerperAvailable(),
|
|
429
429
|
valyu: isValyuAvailable(),
|
|
430
430
|
};
|
|
431
|
+
const allSearchProviders = new Set<ResolvedSearchProvider>(ALL_SEARCH_PROVIDERS);
|
|
431
432
|
return {
|
|
432
|
-
|
|
433
|
-
all: Object.entries(providers).some(([provider, available]) => provider !== "parallel-mcp" && provider !== "duckduckgo" && provider !== "kimi" && provider !== "anysearch" && provider !== "valyu" && provider !== "xai" && provider !== "brightdata" && provider !== "serpbase" && provider !== "serper" && provider !== "gemini" && available) || geminiApiAvail,
|
|
433
|
+
all: Object.entries(providers).some(([provider, available]) => provider !== "gemini" && allSearchProviders.has(provider as ResolvedSearchProvider) && available) || geminiApiAvail,
|
|
434
434
|
...providers,
|
|
435
435
|
};
|
|
436
436
|
}
|
|
@@ -1750,7 +1750,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1750
1750
|
name: toolNames.webSearch,
|
|
1751
1751
|
label: "Web Search",
|
|
1752
1752
|
description:
|
|
1753
|
-
`Search the web using OpenAI, Brave, Parallel, Parallel MCP, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, or Serper. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; Kimi search uses a Kimi Code Plan authenticated through /login kimi-coding; xAI search uses a SuperGrok/X Premium subscription or xAI API key. Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, SearXNG is preferred first for local/private search. When the active Pi model is openai-codex, Codex-backed OpenAI search is preferred next. Otherwise Exa is preferred before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web.`,
|
|
1753
|
+
`Search the web using OpenAI, Brave, Parallel, Parallel MCP, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, or Serper. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; Kimi search uses a Kimi Code Plan authenticated through /login kimi-coding; xAI search uses a SuperGrok/X Premium subscription or xAI API key. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, SearXNG is preferred first for local/private search. When the active Pi model is openai-codex, Codex-backed OpenAI search is preferred next. Otherwise Exa is preferred before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web.`,
|
|
1754
1754
|
promptSnippet:
|
|
1755
1755
|
"Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage. Omit provider unless explicitly overriding the configured default.",
|
|
1756
1756
|
parameters: Type.Object({
|
|
@@ -1762,7 +1762,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1762
1762
|
StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" }),
|
|
1763
1763
|
),
|
|
1764
1764
|
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
|
|
1765
|
-
provider: Type.Optional(searchProviderSchema("Search provider or non-empty list of providers to search simultaneously; use all to search every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper, omit this field to use the configured provider, or use auto when none is configured")),
|
|
1765
|
+
provider: Type.Optional(searchProviderSchema("Search provider or non-empty list of providers to search simultaneously; use all to search every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper, omit this field to use the configured provider, or use auto when none is configured")),
|
|
1766
1766
|
workflow: Type.Optional(
|
|
1767
1767
|
StringEnum(["none", "summary-review", "auto-summary"], {
|
|
1768
1768
|
description: "Search workflow mode: none = no curator, summary-review = open curator with auto summary draft (default), auto-summary = generate summary without opening curator",
|
|
@@ -1777,7 +1777,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1777
1777
|
return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
|
|
1778
1778
|
const rawQueryList: unknown[] = Array.isArray(params.queries)
|
|
1779
1779
|
? params.queries
|
|
1780
|
-
: (params.query !== undefined ?
|
|
1780
|
+
: (params.query !== undefined ? expandQueryString(params.query) : []);
|
|
1781
1781
|
const queryList = normalizeQueryList(rawQueryList);
|
|
1782
1782
|
const configWorkflow = loadConfigForExtensionInit().workflow;
|
|
1783
1783
|
const workflow = resolveWorkflow(params.workflow ?? configWorkflow, ctx?.hasUI !== false);
|
|
@@ -2073,7 +2073,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2073
2073
|
const input = args as { query?: unknown; queries?: unknown };
|
|
2074
2074
|
const rawQueryList: unknown[] = Array.isArray(input.queries)
|
|
2075
2075
|
? input.queries
|
|
2076
|
-
: (input.query !== undefined ?
|
|
2076
|
+
: (input.query !== undefined ? expandQueryString(input.query) : []);
|
|
2077
2077
|
const queryList = normalizeQueryList(rawQueryList);
|
|
2078
2078
|
if (queryList.length === 0) {
|
|
2079
2079
|
return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("error", "(no query)"), 0, 0);
|
|
@@ -2337,7 +2337,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2337
2337
|
fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
|
|
2338
2338
|
recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
|
|
2339
2339
|
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
|
|
2340
|
-
provider: Type.Optional(searchProviderSchema("Search provider or non-empty list of providers to search simultaneously; all searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, Valyu, xAI, Bright Data, SerpBase, and Serper")),
|
|
2340
|
+
provider: Type.Optional(searchProviderSchema("Search provider or non-empty list of providers to search simultaneously; all searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper")),
|
|
2341
2341
|
proxy: Type.Optional(Type.String({
|
|
2342
2342
|
description: "http(s) proxy URL (e.g. http://host:port) used for every outbound request in this call (search APIs and result-page fetches). Empty string forces direct access.",
|
|
2343
2343
|
})),
|
|
@@ -2443,7 +2443,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2443
2443
|
description: "Fetch mode: readable (default extraction), raw (exact textual HTTP body), or answer (answer prompt using only fetched content).",
|
|
2444
2444
|
})),
|
|
2445
2445
|
answerModel: Type.Optional(Type.String({
|
|
2446
|
-
description: "Optional provider/model-id override for mode answer. Defaults to the current Pi model.",
|
|
2446
|
+
description: "Optional provider/model-id override for mode answer. Defaults to fetch.answerProvider + fetch.answerModel when configured, otherwise the current Pi model.",
|
|
2447
2447
|
})),
|
|
2448
2448
|
timestamp: Type.Optional(Type.String({
|
|
2449
2449
|
description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-access",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Web search, URL fetching, GitHub repo cloning, PDF extraction, YouTube video understanding, and local video analysis for Pi coding agent. Supports OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Ollama, AnySearch, Bright Data SERP, SerpBase, SearXNG, Exa, Perplexity, Gemini, and Kimi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { complete, type Api, type Message, type Model } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns } from "./summary-model-scope.ts";
|
|
5
|
+
import { getWebSearchConfigPath } from "./utils.ts";
|
|
4
6
|
|
|
5
7
|
const OUTPUT_TOKENS = 2_000;
|
|
6
8
|
const INPUT_CONTEXT_FRACTION = 0.6;
|
|
@@ -16,7 +18,48 @@ export interface PageAnswer {
|
|
|
16
18
|
truncated: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
interface AnswerModelSelector {
|
|
22
|
+
provider: string;
|
|
23
|
+
id: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadConfiguredAnswerModel(): AnswerModelSelector | undefined {
|
|
27
|
+
const configPath = getWebSearchConfigPath();
|
|
28
|
+
if (!existsSync(configPath)) return undefined;
|
|
29
|
+
|
|
30
|
+
let raw: unknown;
|
|
31
|
+
try {
|
|
32
|
+
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
throw new Error(`Failed to parse ${configPath}: ${message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const root = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
39
|
+
? raw as Record<string, unknown>
|
|
40
|
+
: undefined;
|
|
41
|
+
const fetchConfig = root?.fetch;
|
|
42
|
+
if (!fetchConfig || typeof fetchConfig !== "object" || Array.isArray(fetchConfig)) return undefined;
|
|
43
|
+
const values = fetchConfig as Record<string, unknown>;
|
|
44
|
+
const hasProvider = Object.hasOwn(values, "answerProvider");
|
|
45
|
+
const hasModel = Object.hasOwn(values, "answerModel");
|
|
46
|
+
if (!hasProvider && !hasModel) return undefined;
|
|
47
|
+
|
|
48
|
+
const provider = values.answerProvider;
|
|
49
|
+
const model = values.answerModel;
|
|
50
|
+
if (hasProvider && (typeof provider !== "string" || provider.trim().length === 0)) {
|
|
51
|
+
throw new Error(`fetch.answerProvider in ${configPath} must be a non-empty string`);
|
|
52
|
+
}
|
|
53
|
+
if (hasModel && (typeof model !== "string" || model.trim().length === 0)) {
|
|
54
|
+
throw new Error(`fetch.answerModel in ${configPath} must be a non-empty string`);
|
|
55
|
+
}
|
|
56
|
+
if (!hasProvider || !hasModel) {
|
|
57
|
+
throw new Error(`fetch.answerProvider and fetch.answerModel must be configured together in ${configPath}`);
|
|
58
|
+
}
|
|
59
|
+
return { provider: (provider as string).trim(), id: (model as string).trim() };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseModelSelector(value: string): AnswerModelSelector {
|
|
20
63
|
const separator = value.indexOf("/");
|
|
21
64
|
if (separator <= 0 || separator === value.length - 1) {
|
|
22
65
|
throw new Error(`Invalid answerModel: ${value}. Use provider/model-id.`);
|
|
@@ -24,14 +67,20 @@ function parseModelSelector(value: string): { provider: string; id: string } {
|
|
|
24
67
|
return { provider: value.slice(0, separator), id: value.slice(separator + 1) };
|
|
25
68
|
}
|
|
26
69
|
|
|
27
|
-
function resolveModel(ctx: ExtensionContext, override?: string): Model<Api> {
|
|
28
|
-
const
|
|
70
|
+
function resolveModel(ctx: ExtensionContext, override?: string, configured?: AnswerModelSelector): Model<Api> {
|
|
71
|
+
const selector = override ? parseModelSelector(override) : configured;
|
|
72
|
+
const model = selector
|
|
29
73
|
? (() => {
|
|
30
|
-
|
|
31
|
-
return ctx.modelRegistry.find(selector.provider, selector.id);
|
|
74
|
+
return findModelWithProviderRouting(ctx.modelRegistry, selector.provider, selector.id);
|
|
32
75
|
})()
|
|
33
76
|
: ctx.model;
|
|
34
|
-
if (!model)
|
|
77
|
+
if (!model) {
|
|
78
|
+
if (override) throw new Error(`Answer model not found: ${override}`);
|
|
79
|
+
if (configured) {
|
|
80
|
+
throw new Error(`Answer model not found: ${configured.provider}/${configured.id} (from fetch.answerProvider/fetch.answerModel in ${getWebSearchConfigPath()})`);
|
|
81
|
+
}
|
|
82
|
+
throw new Error("No current model available for page answering");
|
|
83
|
+
}
|
|
35
84
|
if (!model.input.includes("text")) throw new Error(`Answer model does not support text input: ${model.provider}/${model.id}`);
|
|
36
85
|
if (!modelMatchesEnabledPatterns(model, loadEnabledModelPatterns(ctx))) {
|
|
37
86
|
throw new Error(`Answer model is not enabled: ${model.provider}/${model.id}`);
|
|
@@ -53,7 +102,11 @@ export async function answerFromPage(
|
|
|
53
102
|
ctx: ExtensionContext,
|
|
54
103
|
signal?: AbortSignal,
|
|
55
104
|
): Promise<PageAnswer> {
|
|
56
|
-
|
|
105
|
+
// Resolve an explicit per-call model first. In particular, do not read a
|
|
106
|
+
// malformed or partial config when the caller has supplied an override.
|
|
107
|
+
const model = input.model
|
|
108
|
+
? resolveModel(ctx, input.model)
|
|
109
|
+
: resolveModel(ctx, undefined, loadConfiguredAnswerModel());
|
|
57
110
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
58
111
|
if (!auth.ok || !auth.apiKey) throw new Error(`No API key available for answer model ${model.provider}/${model.id}`);
|
|
59
112
|
const registry = ctx.modelRegistry as typeof ctx.modelRegistry & { complete?: typeof complete };
|
|
@@ -0,0 +1,264 @@
|
|
|
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 XCRAWL_API_URL = "https://run.xcrawl.com/v1/serp";
|
|
8
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
9
|
+
// The SERP API is usually fast (a few seconds, cached responses are quicker),
|
|
10
|
+
// but leave generous headroom before treating a slow job as a provider failure.
|
|
11
|
+
const SEARCH_TIMEOUT_MS = 60_000;
|
|
12
|
+
|
|
13
|
+
interface WebSearchConfig {
|
|
14
|
+
xcrawlApiKey?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface XCrawlSerpResult {
|
|
18
|
+
title?: unknown;
|
|
19
|
+
link?: unknown;
|
|
20
|
+
snippet?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface XCrawlSerpResponse {
|
|
24
|
+
search_metadata?: unknown;
|
|
25
|
+
organic_results?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cachedConfig: WebSearchConfig | null = null;
|
|
29
|
+
|
|
30
|
+
function loadConfig(): WebSearchConfig {
|
|
31
|
+
if (cachedConfig) return cachedConfig;
|
|
32
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
33
|
+
cachedConfig = {};
|
|
34
|
+
return cachedConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
38
|
+
let parsed: unknown;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
47
|
+
}
|
|
48
|
+
cachedConfig = parsed as WebSearchConfig;
|
|
49
|
+
return cachedConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function getApiKey(signal?: AbortSignal): Promise<string | null> {
|
|
53
|
+
return resolveCredential({
|
|
54
|
+
provider: "XCrawl",
|
|
55
|
+
configuredValue: loadConfig().xcrawlApiKey,
|
|
56
|
+
environmentValue: process.env.XCRAWL_API_KEY,
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isXcrawlAvailable(): boolean {
|
|
62
|
+
return hasCredentialSource({ provider: "XCrawl", configuredValue: loadConfig().xcrawlApiKey, environmentValue: process.env.XCRAWL_API_KEY });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function errorMessage(err: unknown): string {
|
|
66
|
+
return err instanceof Error ? err.message : String(err);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function invalidResponse(message: string): Error {
|
|
70
|
+
return new Error(`XCrawl API returned invalid response: ${message}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeCount(value: number | undefined): number {
|
|
74
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
75
|
+
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hostnameOf(url: string): string {
|
|
79
|
+
try {
|
|
80
|
+
return new URL(url).hostname.toLowerCase();
|
|
81
|
+
} catch {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// XCrawl's Google SERP results can carry links relative to the API origin
|
|
87
|
+
// (e.g. "/goto?url=..." redirect stubs) instead of the documented absolute
|
|
88
|
+
// URLs. Resolve those against the API origin so callers always receive a
|
|
89
|
+
// well-formed absolute URL; absolute http(s) links pass through unchanged.
|
|
90
|
+
function absolutizeLink(link: string): string {
|
|
91
|
+
if (/^https?:\/\//i.test(link)) return link;
|
|
92
|
+
try {
|
|
93
|
+
return new URL(link, XCRAWL_API_URL).href;
|
|
94
|
+
} catch {
|
|
95
|
+
return link;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Normalize a shared domainFilter entry the same way Valyu does before
|
|
100
|
+
// matching: trim, lowercase, strip URL/paths/ports, validate the shape.
|
|
101
|
+
function normalizeDomain(value: string): string | null {
|
|
102
|
+
let input = value.trim().toLowerCase();
|
|
103
|
+
if (!input) return null;
|
|
104
|
+
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
105
|
+
if (!input) return null;
|
|
106
|
+
try {
|
|
107
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
108
|
+
input = parsed.hostname;
|
|
109
|
+
} catch {
|
|
110
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
111
|
+
}
|
|
112
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
113
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// XCrawl's SERP API has no server-side domain filter, so apply the shared
|
|
117
|
+
// include/exclude domainFilter locally instead of returning off-domain results.
|
|
118
|
+
function applyDomainFilter(results: SearchResponse["results"], domainFilter: NonNullable<SearchOptions["domainFilter"]>): SearchResponse["results"] {
|
|
119
|
+
const includes: string[] = [];
|
|
120
|
+
const excludes: string[] = [];
|
|
121
|
+
for (const raw of domainFilter) {
|
|
122
|
+
const normalized = normalizeDomain(raw);
|
|
123
|
+
if (!normalized) continue;
|
|
124
|
+
if (raw.trim().startsWith("-")) excludes.push(normalized);
|
|
125
|
+
else includes.push(normalized);
|
|
126
|
+
}
|
|
127
|
+
if (!includes.length && !excludes.length) return results;
|
|
128
|
+
return results.filter((result) => {
|
|
129
|
+
const host = hostnameOf(result.url);
|
|
130
|
+
if (!host) return false;
|
|
131
|
+
const matches = (domain: string) => host === domain || host.endsWith(`.${domain}`);
|
|
132
|
+
if (excludes.some(matches)) return false;
|
|
133
|
+
if (includes.length && !includes.some(matches)) return false;
|
|
134
|
+
return true;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseResponse(value: unknown): SearchResponse["results"] {
|
|
139
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
140
|
+
throw invalidResponse("expected an object envelope");
|
|
141
|
+
}
|
|
142
|
+
const envelope = value as XCrawlSerpResponse;
|
|
143
|
+
const metadata = envelope.search_metadata;
|
|
144
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
145
|
+
throw invalidResponse("expected search_metadata object");
|
|
146
|
+
}
|
|
147
|
+
const status = (metadata as Record<string, unknown>).status;
|
|
148
|
+
if (status !== undefined && status !== "completed") {
|
|
149
|
+
throw invalidResponse(`expected search_metadata.status \"completed\", got ${JSON.stringify(status)}`);
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(envelope.organic_results)) throw invalidResponse("expected organic_results array");
|
|
152
|
+
|
|
153
|
+
const results: SearchResponse["results"] = [];
|
|
154
|
+
for (const [index, value] of (envelope.organic_results as unknown[]).entries()) {
|
|
155
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
156
|
+
throw invalidResponse(`expected organic_results[${index}] object`);
|
|
157
|
+
}
|
|
158
|
+
const result = value as XCrawlSerpResult;
|
|
159
|
+
const { title, link, snippet } = result;
|
|
160
|
+
if (typeof link !== "string" || !link.trim()) throw invalidResponse(`expected organic_results[${index}].link to be a non-empty string`);
|
|
161
|
+
if (title !== null && title !== undefined && typeof title !== "string") {
|
|
162
|
+
throw invalidResponse(`expected organic_results[${index}].title to be a string or null`);
|
|
163
|
+
}
|
|
164
|
+
if (snippet !== undefined && snippet !== null && typeof snippet !== "string") {
|
|
165
|
+
throw invalidResponse(`expected organic_results[${index}].snippet to be a string or null`);
|
|
166
|
+
}
|
|
167
|
+
const resolved = absolutizeLink(link.trim());
|
|
168
|
+
results.push({
|
|
169
|
+
title: typeof title === "string" && title.trim().length > 0 ? title : resolved,
|
|
170
|
+
url: resolved,
|
|
171
|
+
snippet: typeof snippet === "string" ? snippet : "",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildAnswer(results: SearchResponse["results"]): string {
|
|
179
|
+
return results
|
|
180
|
+
.map((result) => result.snippet
|
|
181
|
+
? `${result.snippet}\nSource: ${result.title} (${result.url})`
|
|
182
|
+
: `Source: ${result.title} (${result.url})`)
|
|
183
|
+
.join("\n\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function searchWithXCrawl(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
187
|
+
const apiKey = await getApiKey(options.signal);
|
|
188
|
+
const numResults = normalizeCount(options.numResults);
|
|
189
|
+
if (!apiKey) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"XCrawl search requires an API key. Set xcrawlApiKey in " + CONFIG_PATH +
|
|
192
|
+
" or export XCRAWL_API_KEY. Get one at https://dash.xcrawl.com/",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const body = { engine: "google_search", q: query };
|
|
196
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
197
|
+
// Distinguishing caller cancellation from a provider-side timeout lets the
|
|
198
|
+
// timeout surface as a retriable failure instead of looking like an abort.
|
|
199
|
+
const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
|
|
200
|
+
let response: Response;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
response = await fetch(XCRAWL_API_URL, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
Authorization: `Bearer ${apiKey}`,
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify(body),
|
|
210
|
+
signal: options.signal
|
|
211
|
+
? AbortSignal.any([timeoutSignal, options.signal])
|
|
212
|
+
: timeoutSignal,
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const message = errorMessage(err);
|
|
216
|
+
if (options.signal?.aborted) {
|
|
217
|
+
activityMonitor.logComplete(activityId, 0);
|
|
218
|
+
throw new Error("Aborted");
|
|
219
|
+
}
|
|
220
|
+
// AbortSignal.timeout rejects with a TimeoutError; treat either that
|
|
221
|
+
// shape or our own fired timer as a retriable provider-side timeout so
|
|
222
|
+
// routing fallback still applies.
|
|
223
|
+
const providerTimeout = timeoutSignal.aborted || (err instanceof Error && err.name === "TimeoutError");
|
|
224
|
+
let outgoing: Error;
|
|
225
|
+
if (providerTimeout) {
|
|
226
|
+
outgoing = new Error(`XCrawl search request timed out after ${Math.round(SEARCH_TIMEOUT_MS / 1000)}s`);
|
|
227
|
+
} else {
|
|
228
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
229
|
+
outgoing = redactedMessage === message && err instanceof Error ? err : new Error(redactedMessage);
|
|
230
|
+
if (err instanceof Error && outgoing.name === "Error") outgoing.name = err.name;
|
|
231
|
+
}
|
|
232
|
+
activityMonitor.logError(activityId, redactCredential(errorMessage(outgoing), apiKey));
|
|
233
|
+
throw outgoing;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
238
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
239
|
+
let detail = errorText.slice(0, 300);
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(errorText) as { message?: unknown; error?: unknown };
|
|
242
|
+
if (typeof parsed.message === "string") detail = parsed.message.slice(0, 300);
|
|
243
|
+
else if (typeof parsed.error === "string") detail = parsed.error.slice(0, 300);
|
|
244
|
+
} catch {
|
|
245
|
+
// keep raw text slice
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`XCrawl API error (${response.status}): ${detail}`);
|
|
248
|
+
}
|
|
249
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
250
|
+
|
|
251
|
+
let payload: unknown;
|
|
252
|
+
try {
|
|
253
|
+
payload = await response.json();
|
|
254
|
+
} catch (err) {
|
|
255
|
+
throw invalidResponse(`response body is not valid JSON: ${errorMessage(err)}`);
|
|
256
|
+
}
|
|
257
|
+
const results = parseResponse(payload);
|
|
258
|
+
const filtered = (options.domainFilter?.length ? applyDomainFilter(results, options.domainFilter) : results).slice(0, numResults);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
answer: buildAnswer(filtered),
|
|
262
|
+
results: filtered,
|
|
263
|
+
};
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-extensions",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.15",
|
|
4
4
|
"description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"extensions",
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@ast-grep/cli": "^0.45.0",
|
|
39
39
|
"@ast-grep/napi": "^0.45.0",
|
|
40
|
-
"@czottmann/pi-automode": "1.
|
|
40
|
+
"@czottmann/pi-automode": "1.15.0",
|
|
41
41
|
"@dietrichgebert/ponytail": "4.9.0",
|
|
42
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
43
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
43
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
44
44
|
"@effect/platform-node": "4.0.0-beta.103",
|
|
45
45
|
"@effect/platform-node-shared": "4.0.0-beta.103",
|
|
46
|
-
"@herbertgao/pi-
|
|
47
|
-
"@herbertgao/pi-
|
|
48
|
-
"@herbertgao/pi-subagents": "0.17.
|
|
46
|
+
"@herbertgao/pi-bark": "0.1.0",
|
|
47
|
+
"@herbertgao/pi-cc-extensions": "0.8.60",
|
|
48
|
+
"@herbertgao/pi-subagents": "0.17.1",
|
|
49
49
|
"@herbertgao/resume-from": "0.2.0",
|
|
50
|
-
"@juicesharp/rpiv-ask-user-question": "2.
|
|
51
|
-
"@juicesharp/rpiv-config": "^2.
|
|
50
|
+
"@juicesharp/rpiv-ask-user-question": "2.8.0",
|
|
51
|
+
"@juicesharp/rpiv-config": "^2.8.0",
|
|
52
52
|
"@luxusai/pi-hindsight": "0.12.0",
|
|
53
53
|
"@modelcontextprotocol/client": "2.0.0",
|
|
54
54
|
"@modelcontextprotocol/core": "2.0.0",
|
|
@@ -56,20 +56,19 @@
|
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
57
57
|
"@mozilla/readability": "^0.6.0",
|
|
58
58
|
"@napi-rs/keyring": "^1.3.0",
|
|
59
|
-
"@narumitw/pi-btw": "0.
|
|
59
|
+
"@narumitw/pi-btw": "0.56.0",
|
|
60
60
|
"@narumitw/pi-tui-kit": "^0.59.0",
|
|
61
61
|
"@noble/ed25519": "^3.1.0",
|
|
62
62
|
"@pi-plugins/fast-mode": "0.1.10",
|
|
63
63
|
"@shikijs/cli": "^4.0.2",
|
|
64
64
|
"@sinclair/typebox": "^0.34.49",
|
|
65
65
|
"@tifan/pi-copy-response": "0.2.6",
|
|
66
|
-
"@tifan/pi-handoff": "2.0
|
|
66
|
+
"@tifan/pi-handoff": "2.2.0",
|
|
67
67
|
"@tifan/pi-inline-skills": "1.0.5",
|
|
68
68
|
"@tifan/pi-mermaid-open": "0.2.0",
|
|
69
|
-
"@tifan/pi-preferred-thinking": "1.0.
|
|
70
|
-
"@tifan/pi-recap": "0.4.
|
|
71
|
-
"@tifan/pi-rename": "0.
|
|
72
|
-
"@tifan/pi-titlebar-spinner": "0.1.3",
|
|
69
|
+
"@tifan/pi-preferred-thinking": "1.0.1",
|
|
70
|
+
"@tifan/pi-recap": "0.4.6",
|
|
71
|
+
"@tifan/pi-rename": "0.6.0",
|
|
73
72
|
"@vectorize-io/hindsight-client": "^0.9.0",
|
|
74
73
|
"ajv": "^8.17.1",
|
|
75
74
|
"ajv-formats": "^3.0.1",
|
|
@@ -89,9 +88,9 @@
|
|
|
89
88
|
"open": "^10.2.0",
|
|
90
89
|
"p-limit": "^6.1.0",
|
|
91
90
|
"pi-footer": "0.5.1",
|
|
92
|
-
"pi-lens": "4.1.
|
|
93
|
-
"pi-mcp-adapter": "2.
|
|
94
|
-
"pi-web-access": "0.
|
|
91
|
+
"pi-lens": "4.1.3",
|
|
92
|
+
"pi-mcp-adapter": "2.31.0",
|
|
93
|
+
"pi-web-access": "0.27.0",
|
|
95
94
|
"pidusage": "^4.0.1",
|
|
96
95
|
"promise.try": "^2.0.1",
|
|
97
96
|
"qrcode-terminal": "^0.12.0",
|
|
@@ -114,8 +113,8 @@
|
|
|
114
113
|
"bundledDependencies": [
|
|
115
114
|
"@czottmann/pi-automode",
|
|
116
115
|
"@dietrichgebert/ponytail",
|
|
116
|
+
"@herbertgao/pi-bark",
|
|
117
117
|
"@herbertgao/pi-cc-extensions",
|
|
118
|
-
"@herbertgao/pi-stash",
|
|
119
118
|
"@herbertgao/pi-subagents",
|
|
120
119
|
"@herbertgao/resume-from",
|
|
121
120
|
"@juicesharp/rpiv-ask-user-question",
|
|
@@ -129,7 +128,6 @@
|
|
|
129
128
|
"@tifan/pi-preferred-thinking",
|
|
130
129
|
"@tifan/pi-recap",
|
|
131
130
|
"@tifan/pi-rename",
|
|
132
|
-
"@tifan/pi-titlebar-spinner",
|
|
133
131
|
"pi-footer",
|
|
134
132
|
"pi-lens",
|
|
135
133
|
"pi-mcp-adapter",
|
|
@@ -152,10 +150,9 @@
|
|
|
152
150
|
"./node_modules/@tifan/pi-preferred-thinking/src/index.ts",
|
|
153
151
|
"./node_modules/@tifan/pi-recap/src/index.ts",
|
|
154
152
|
"./node_modules/@tifan/pi-rename/src/index.ts",
|
|
155
|
-
"./node_modules/@herbertgao/pi-stash/src/index.ts",
|
|
156
153
|
"./node_modules/@herbertgao/pi-subagents/src/index.ts",
|
|
157
|
-
"./node_modules/@tifan/pi-titlebar-spinner/src/index.ts",
|
|
158
154
|
"./node_modules/@juicesharp/rpiv-ask-user-question/index.ts",
|
|
155
|
+
"./node_modules/@herbertgao/pi-bark/src/index.ts",
|
|
159
156
|
"./node_modules/@luxusai/pi-hindsight/extensions/index.ts",
|
|
160
157
|
"./node_modules/@narumitw/pi-btw/dist/index.ts",
|
|
161
158
|
"./node_modules/@pi-plugins/fast-mode/dist/index.mjs",
|