@herbertgao/pi-extensions 2026.9.0 → 2026.9.1
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/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
- package/node_modules/@czottmann/pi-automode/README.md +20 -0
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/pi-lens/CHANGELOG.md +988 -0
- package/node_modules/pi-lens/README.md +3 -0
- package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
- package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
- package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
- package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
- package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
- package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
- package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
- package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
- package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
- package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
- package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
- package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
- package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
- package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
- package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
- package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
- package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
- package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
- package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
- package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
- package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
- package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
- package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
- package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
- package/node_modules/pi-lens/dist/index.js +64335 -54300
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +127 -5
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/docs/agent-guide.md +64 -1
- package/node_modules/pi-lens/docs/configuration.md +222 -0
- package/node_modules/pi-lens/docs/dependencies.md +3 -3
- package/node_modules/pi-lens/docs/features.md +44 -5
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
- package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
- package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
- package/node_modules/pi-lens/package.json +15 -23
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +62 -15
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +242 -1
- package/node_modules/pi-web-access/index.ts +118 -64
- package/node_modules/pi-web-access/mistral-search.ts +281 -0
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/perplexity.ts +14 -1
- package/node_modules/pi-web-access/utils.ts +23 -6
- package/node_modules/pi-web-access/xai-search.ts +96 -33
- package/package.json +5 -5
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.ts";
|
|
3
|
+
import type { SearchOptions, SearchResponse, SearchResult } from "./perplexity.ts";
|
|
4
|
+
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
5
|
+
import { getWebSearchConfigPath } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
const MISTRAL_CONVERSATIONS_URL = "https://api.mistral.ai/v1/conversations";
|
|
8
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
9
|
+
const SEARCH_TIMEOUT_MS = 60_000;
|
|
10
|
+
const DEFAULT_SEARCH_MODEL = "mistral-small-latest";
|
|
11
|
+
const DEFAULT_SEARCH_TOOL = "web_search";
|
|
12
|
+
const MAX_RESULTS = 20;
|
|
13
|
+
|
|
14
|
+
type MistralSearchTool = "web_search" | "web_search_premium";
|
|
15
|
+
|
|
16
|
+
interface WebSearchConfig {
|
|
17
|
+
mistralApiKey?: unknown;
|
|
18
|
+
mistralSearchModel?: unknown;
|
|
19
|
+
mistralSearchTool?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DomainFilters {
|
|
23
|
+
include: string[];
|
|
24
|
+
exclude: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let cachedConfig: WebSearchConfig | null = null;
|
|
28
|
+
|
|
29
|
+
function loadConfig(): WebSearchConfig {
|
|
30
|
+
if (cachedConfig) return cachedConfig;
|
|
31
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
32
|
+
cachedConfig = {};
|
|
33
|
+
return cachedConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
37
|
+
let parsed: unknown;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(raw);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
45
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
46
|
+
}
|
|
47
|
+
cachedConfig = parsed as WebSearchConfig;
|
|
48
|
+
return cachedConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveSearchModel(value: unknown): string {
|
|
52
|
+
if (value === undefined) return DEFAULT_SEARCH_MODEL;
|
|
53
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
54
|
+
throw new Error(`mistralSearchModel in ${CONFIG_PATH} must be a non-empty string`);
|
|
55
|
+
}
|
|
56
|
+
return value.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveSearchTool(value: unknown): MistralSearchTool {
|
|
60
|
+
if (value === undefined) return DEFAULT_SEARCH_TOOL;
|
|
61
|
+
if (value !== "web_search" && value !== "web_search_premium") {
|
|
62
|
+
throw new Error(`mistralSearchTool in ${CONFIG_PATH} must be either web_search or web_search_premium`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeCount(value: number | undefined): number {
|
|
68
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
69
|
+
return Math.max(1, Math.min(Math.floor(value), MAX_RESULTS));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeDomain(value: string): string | null {
|
|
73
|
+
let input = value.trim().toLowerCase();
|
|
74
|
+
if (!input) return null;
|
|
75
|
+
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
76
|
+
if (!input) return null;
|
|
77
|
+
try {
|
|
78
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
79
|
+
input = parsed.hostname;
|
|
80
|
+
} catch {
|
|
81
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
82
|
+
}
|
|
83
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
84
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseDomainFilters(domainFilter: string[] | undefined): DomainFilters {
|
|
88
|
+
const filters: DomainFilters = { include: [], exclude: [] };
|
|
89
|
+
for (const raw of domainFilter ?? []) {
|
|
90
|
+
const domain = normalizeDomain(raw);
|
|
91
|
+
if (!domain) continue;
|
|
92
|
+
const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
|
|
93
|
+
if (!target.includes(domain)) target.push(domain);
|
|
94
|
+
}
|
|
95
|
+
return filters;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function passesDomainFilters(url: string, filters: DomainFilters): boolean {
|
|
99
|
+
if (filters.include.length === 0 && filters.exclude.length === 0) return true;
|
|
100
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
101
|
+
const matches = (domain: string) => hostname === domain || hostname.endsWith(`.${domain}`);
|
|
102
|
+
if (filters.exclude.some(matches)) return false;
|
|
103
|
+
return filters.include.length === 0 || filters.include.some(matches);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeResultUrl(value: unknown): string | null {
|
|
107
|
+
if (typeof value !== "string") return null;
|
|
108
|
+
const url = value.trim();
|
|
109
|
+
if (!url) return null;
|
|
110
|
+
try {
|
|
111
|
+
const parsed = new URL(url);
|
|
112
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return url;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildSearchPrompt(query: string, options: SearchOptions, numResults: number): string {
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
if (options.recencyFilter) {
|
|
122
|
+
const labels: Record<string, string> = {
|
|
123
|
+
day: "past 24 hours",
|
|
124
|
+
week: "past week",
|
|
125
|
+
month: "past month",
|
|
126
|
+
year: "past year",
|
|
127
|
+
};
|
|
128
|
+
lines.push(`Prefer sources from the ${labels[options.recencyFilter] ?? options.recencyFilter}.`);
|
|
129
|
+
}
|
|
130
|
+
if (typeof options.numResults === "number" && Number.isFinite(options.numResults) && options.numResults > 0) {
|
|
131
|
+
lines.push(`Prefer up to ${numResults} distinct sources.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const filters = parseDomainFilters(options.domainFilter);
|
|
135
|
+
if (filters.include.length > 0) lines.push(`Only use sources from: ${filters.include.slice(0, 100).join(", ")}.`);
|
|
136
|
+
if (filters.exclude.length > 0) lines.push(`Do not use sources from: ${filters.exclude.slice(0, 100).join(", ")}.`);
|
|
137
|
+
return lines.length > 0 ? `${lines.join(" ")}\n\n${query}` : query;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function errorMessage(err: unknown): string {
|
|
141
|
+
return err instanceof Error ? err.message : String(err);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function invalidResponse(message: string): Error {
|
|
145
|
+
return new Error(`Mistral API returned invalid response: ${message}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseConversationResponse(value: unknown, options: SearchOptions, numResults: number): SearchResponse {
|
|
149
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
150
|
+
throw invalidResponse("expected an object envelope");
|
|
151
|
+
}
|
|
152
|
+
const outputs = (value as Record<string, unknown>).outputs;
|
|
153
|
+
if (!Array.isArray(outputs)) throw invalidResponse("outputs must be an array");
|
|
154
|
+
|
|
155
|
+
const answerParts: string[] = [];
|
|
156
|
+
const results: SearchResult[] = [];
|
|
157
|
+
const seenUrls = new Set<string>();
|
|
158
|
+
const domainFilters = parseDomainFilters(options.domainFilter);
|
|
159
|
+
|
|
160
|
+
for (const output of outputs) {
|
|
161
|
+
if (!output || typeof output !== "object" || Array.isArray(output)) continue;
|
|
162
|
+
const entry = output as Record<string, unknown>;
|
|
163
|
+
if (entry.type !== "message.output") continue;
|
|
164
|
+
const content = entry.content;
|
|
165
|
+
if (typeof content === "string") {
|
|
166
|
+
if (content.trim()) answerParts.push(content.trim());
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!Array.isArray(content)) continue;
|
|
170
|
+
|
|
171
|
+
for (const chunk of content) {
|
|
172
|
+
if (!chunk || typeof chunk !== "object" || Array.isArray(chunk)) continue;
|
|
173
|
+
const part = chunk as Record<string, unknown>;
|
|
174
|
+
if (part.type === "text") {
|
|
175
|
+
if (typeof part.text === "string" && part.text.trim()) answerParts.push(part.text.trim());
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (part.type !== "tool_reference" || results.length >= numResults) continue;
|
|
179
|
+
|
|
180
|
+
const url = normalizeResultUrl(part.url);
|
|
181
|
+
if (!url || !passesDomainFilters(url, domainFilters) || seenUrls.has(url)) continue;
|
|
182
|
+
seenUrls.add(url);
|
|
183
|
+
const title = typeof part.title === "string" && part.title.trim() ? part.title.trim() : url;
|
|
184
|
+
const snippet = typeof part.description === "string" ? part.description : "";
|
|
185
|
+
results.push({ title, url, snippet });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const answer = answerParts.join("\n").trim();
|
|
190
|
+
if (!answer && results.length === 0) throw invalidResponse("no answer or sources");
|
|
191
|
+
return { answer, results };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function getApiKey(signal?: AbortSignal): Promise<string> {
|
|
195
|
+
const apiKey = await resolveCredential({
|
|
196
|
+
provider: "Mistral",
|
|
197
|
+
configuredValue: loadConfig().mistralApiKey,
|
|
198
|
+
environmentValue: process.env.MISTRAL_API_KEY,
|
|
199
|
+
signal,
|
|
200
|
+
});
|
|
201
|
+
if (!apiKey) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Mistral API key not found. Either:\n" +
|
|
204
|
+
` 1. Create ${CONFIG_PATH} with { "mistralApiKey": "your-key" }\n` +
|
|
205
|
+
" 2. Set MISTRAL_API_KEY environment variable\n" +
|
|
206
|
+
"Get a key at https://console.mistral.ai/api-keys",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return apiKey;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isMistralAvailable(): boolean {
|
|
213
|
+
try {
|
|
214
|
+
const config = loadConfig();
|
|
215
|
+
resolveSearchModel(config.mistralSearchModel);
|
|
216
|
+
resolveSearchTool(config.mistralSearchTool);
|
|
217
|
+
return hasCredentialSource({
|
|
218
|
+
provider: "Mistral",
|
|
219
|
+
configuredValue: config.mistralApiKey,
|
|
220
|
+
environmentValue: process.env.MISTRAL_API_KEY,
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function searchWithMistral(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
228
|
+
const config = loadConfig();
|
|
229
|
+
const model = resolveSearchModel(config.mistralSearchModel);
|
|
230
|
+
const tool = resolveSearchTool(config.mistralSearchTool);
|
|
231
|
+
const numResults = normalizeCount(options.numResults);
|
|
232
|
+
const apiKey = await getApiKey(options.signal);
|
|
233
|
+
const requestSignal = options.signal
|
|
234
|
+
? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal])
|
|
235
|
+
: AbortSignal.timeout(SEARCH_TIMEOUT_MS);
|
|
236
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(MISTRAL_CONVERSATIONS_URL, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: {
|
|
242
|
+
Authorization: `Bearer ${apiKey}`,
|
|
243
|
+
"Content-Type": "application/json",
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
inputs: [{ role: "user", content: buildSearchPrompt(query, options, numResults) }],
|
|
247
|
+
stream: false,
|
|
248
|
+
model,
|
|
249
|
+
tools: [{ type: tool }],
|
|
250
|
+
}),
|
|
251
|
+
signal: requestSignal,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
256
|
+
throw new Error(`Mistral API error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let parsed: unknown;
|
|
260
|
+
try {
|
|
261
|
+
parsed = await response.json();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw new Error(`Mistral API returned invalid JSON: ${errorMessage(err)}`);
|
|
264
|
+
}
|
|
265
|
+
const result = parseConversationResponse(parsed, options, numResults);
|
|
266
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
267
|
+
return result;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
const message = errorMessage(err);
|
|
270
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
271
|
+
if (redactedMessage.toLowerCase().includes("abort")) {
|
|
272
|
+
activityMonitor.logComplete(activityId, 0);
|
|
273
|
+
} else {
|
|
274
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
275
|
+
}
|
|
276
|
+
if (redactedMessage === message) throw err;
|
|
277
|
+
const redactedError = new Error(redactedMessage);
|
|
278
|
+
if (err instanceof Error) redactedError.name = err.name;
|
|
279
|
+
throw redactedError;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-access",
|
|
3
|
-
"version": "0.
|
|
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
|
|
3
|
+
"version": "0.28.0",
|
|
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, Kimi, and Mistral.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node --test",
|
|
@@ -105,6 +105,18 @@ export function isPerplexityAvailable(): boolean {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Hard ceiling on kept citations, matching the `numResults` clamp. */
|
|
109
|
+
const MAX_CITATIONS = 20;
|
|
110
|
+
|
|
111
|
+
// Preserve citation numbering by keeping the prefix through the highest cited index, capped at 20.
|
|
112
|
+
function citationsToKeep(answer: string, available: number, numResults: number): number {
|
|
113
|
+
let highestCited = 0;
|
|
114
|
+
for (const match of answer.matchAll(/\[(\d{1,3})\]/g)) {
|
|
115
|
+
highestCited = Math.max(highestCited, Number(match[1]));
|
|
116
|
+
}
|
|
117
|
+
return Math.min(available, MAX_CITATIONS, Math.max(numResults, highestCited));
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
export async function searchWithPerplexity(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
109
121
|
checkRateLimit();
|
|
110
122
|
|
|
@@ -184,7 +196,8 @@ export async function searchWithPerplexity(query: string, options: SearchOptions
|
|
|
184
196
|
const citations = Array.isArray(data.citations) ? data.citations : [];
|
|
185
197
|
|
|
186
198
|
const results: SearchResult[] = [];
|
|
187
|
-
|
|
199
|
+
const citationCount = citationsToKeep(answer, citations.length, numResults);
|
|
200
|
+
for (let i = 0; i < citationCount; i++) {
|
|
188
201
|
const citation = citations[i];
|
|
189
202
|
if (typeof citation === "string") {
|
|
190
203
|
results.push({ title: `Source ${i + 1}`, url: citation, snippet: "" });
|
|
@@ -5,10 +5,24 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
|
+
let cachedWebSearchConfigDir: string | undefined;
|
|
9
|
+
|
|
8
10
|
export function getWebSearchConfigDir(): string {
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
if (cachedWebSearchConfigDir) return cachedWebSearchConfigDir;
|
|
12
|
+
|
|
13
|
+
const explicitDir = process.env.PI_CODING_AGENT_DIR;
|
|
14
|
+
if (explicitDir) return cachedWebSearchConfigDir = explicitDir;
|
|
15
|
+
|
|
16
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
17
|
+
if (xdgConfigHome) {
|
|
18
|
+
const xdgDir = join(xdgConfigHome, "pi");
|
|
19
|
+
if (existsSync(join(xdgDir, "web-search.json"))) return cachedWebSearchConfigDir = xdgDir;
|
|
20
|
+
|
|
21
|
+
const legacyDir = join(homedir(), ".pi");
|
|
22
|
+
if (existsSync(join(legacyDir, "web-search.json"))) return cachedWebSearchConfigDir = legacyDir;
|
|
23
|
+
return cachedWebSearchConfigDir = xdgDir;
|
|
24
|
+
}
|
|
25
|
+
return cachedWebSearchConfigDir = join(homedir(), ".pi");
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
export function getWebSearchConfigPath(): string {
|
|
@@ -237,14 +251,17 @@ function loadConfiguredProxy(): string | null {
|
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
export function runWithProxy<T>(proxy: string | undefined, fn: () => T): T {
|
|
240
|
-
if (proxy === undefined)
|
|
254
|
+
if (proxy === undefined) {
|
|
255
|
+
const configured = loadConfiguredProxy();
|
|
256
|
+
if (configured === null) return fn();
|
|
257
|
+
return proxyStorage.run(configured, fn);
|
|
258
|
+
}
|
|
241
259
|
const normalized = normalizeProxyUrl(proxy, "proxy");
|
|
242
260
|
return proxyStorage.run(normalized, fn);
|
|
243
261
|
}
|
|
244
262
|
|
|
245
263
|
export function getActiveProxy(): string | null {
|
|
246
|
-
|
|
247
|
-
return scoped !== undefined ? scoped : loadConfiguredProxy();
|
|
264
|
+
return proxyStorage.getStore() ?? null;
|
|
248
265
|
}
|
|
249
266
|
|
|
250
267
|
export function hasScopedProxyDecision(): boolean {
|
|
@@ -5,8 +5,8 @@ import type { SearchOptions, SearchResponse, SearchResult } from "./perplexity.t
|
|
|
5
5
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
6
6
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
7
7
|
|
|
8
|
-
// xAI's Agent Tools API:
|
|
9
|
-
// Responses endpoint. The search runs inside xAI's own inference, so — unlike
|
|
8
|
+
// xAI's Agent Tools API: hosted `web_search` and opt-in `x_search` tools on an
|
|
9
|
+
// OpenAI-compatible Responses endpoint. The search runs inside xAI's own inference, so — unlike
|
|
10
10
|
// every keyed backend here — it is paid for by whatever credential answers for
|
|
11
11
|
// the model, including a SuperGrok / X Premium subscription resolved through
|
|
12
12
|
// pi's model registry.
|
|
@@ -19,8 +19,8 @@ import { getWebSearchConfigPath } from "./utils.ts";
|
|
|
19
19
|
// is the shape verified against a live subscription account. `stream`,
|
|
20
20
|
// `include`, `tool_choice` and `parallel_tool_calls` are all sent by the OpenAI
|
|
21
21
|
// backend but were NOT verified here, and a 400 from an unsupported field would
|
|
22
|
-
// cost the user their search.
|
|
23
|
-
//
|
|
22
|
+
// cost the user their search. Responses API inline citations are enabled by
|
|
23
|
+
// default; sources may also come back in a search call's `action.sources`.
|
|
24
24
|
|
|
25
25
|
const XAI_RESPONSES_URL = "https://api.x.ai/v1/responses";
|
|
26
26
|
const CONFIG_PATH = getWebSearchConfigPath();
|
|
@@ -29,11 +29,18 @@ const SEARCH_TIMEOUT_MS = 60_000;
|
|
|
29
29
|
// Ordered best-first. pi's builtin xai catalog is small and xAI retires models
|
|
30
30
|
// briskly, so this is a preference list, not an assumption: the first one the
|
|
31
31
|
// registry actually knows wins, and an unknown id is skipped rather than sent.
|
|
32
|
-
|
|
32
|
+
// xAI's current X Search docs use grok-4.6. Keep the existing web-only default
|
|
33
|
+
// unchanged, while preferring the documented model when X Search is opted in.
|
|
34
|
+
const WEB_SEARCH_AUTH_MODEL_CANDIDATES = ["grok-4.5", "grok-4.3", "grok-build-0.1"] as const;
|
|
35
|
+
const X_SEARCH_AUTH_MODEL_CANDIDATES = ["grok-4.6", ...WEB_SEARCH_AUTH_MODEL_CANDIDATES] as const;
|
|
36
|
+
|
|
37
|
+
type XaiSearchTool = "web_search" | "x_search";
|
|
38
|
+
const DEFAULT_XAI_SEARCH_TOOLS = ["web_search"] as const satisfies readonly XaiSearchTool[];
|
|
33
39
|
|
|
34
40
|
interface WebSearchConfig {
|
|
35
41
|
xaiApiKey?: unknown;
|
|
36
42
|
xaiSearchModel?: unknown;
|
|
43
|
+
xaiSearchTools?: unknown;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
type ProviderHeaders = Record<string, string | null>;
|
|
@@ -71,6 +78,29 @@ function resolveConfiguredSearchModel(value: unknown): string | undefined {
|
|
|
71
78
|
return value.trim();
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
function resolveConfiguredSearchTools(value: unknown): XaiSearchTool[] {
|
|
82
|
+
if (value === undefined) return [...DEFAULT_XAI_SEARCH_TOOLS];
|
|
83
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
84
|
+
throw new Error(`xaiSearchTools in ${CONFIG_PATH} must be a non-empty array containing only web_search or x_search`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tools: XaiSearchTool[] = [];
|
|
88
|
+
for (const tool of value) {
|
|
89
|
+
if (tool !== "web_search" && tool !== "x_search") {
|
|
90
|
+
throw new Error(`xaiSearchTools in ${CONFIG_PATH} may only contain web_search or x_search`);
|
|
91
|
+
}
|
|
92
|
+
if (tools.includes(tool)) {
|
|
93
|
+
throw new Error(`xaiSearchTools in ${CONFIG_PATH} must not contain duplicates: ${tool}`);
|
|
94
|
+
}
|
|
95
|
+
tools.push(tool);
|
|
96
|
+
}
|
|
97
|
+
return tools;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function modelCandidatesForTools(tools: readonly XaiSearchTool[]): readonly string[] {
|
|
101
|
+
return tools.includes("x_search") ? X_SEARCH_AUTH_MODEL_CANDIDATES : WEB_SEARCH_AUTH_MODEL_CANDIDATES;
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
function toRequestHeaders(headers: ProviderHeaders): Record<string, string> {
|
|
75
105
|
const requestHeaders: Record<string, string> = {};
|
|
76
106
|
for (const [name, value] of Object.entries(headers)) {
|
|
@@ -84,8 +114,12 @@ function toRequestHeaders(headers: ProviderHeaders): Record<string, string> {
|
|
|
84
114
|
* its own searches and no api key has to be configured at all. Mirrors how the
|
|
85
115
|
* OpenAI backend picks up a Codex sign-in.
|
|
86
116
|
*/
|
|
87
|
-
async function resolvePiAuth(
|
|
88
|
-
|
|
117
|
+
async function resolvePiAuth(
|
|
118
|
+
ctx: ExtensionContext,
|
|
119
|
+
modelOverride: string | undefined,
|
|
120
|
+
tools: readonly XaiSearchTool[],
|
|
121
|
+
): Promise<XaiAuth | undefined> {
|
|
122
|
+
for (const modelId of modelCandidatesForTools(tools)) {
|
|
89
123
|
try {
|
|
90
124
|
const model = ctx.modelRegistry.find("xai", modelId);
|
|
91
125
|
if (!model) continue;
|
|
@@ -102,8 +136,9 @@ async function resolvePiAuth(ctx: ExtensionContext, modelOverride?: string): Pro
|
|
|
102
136
|
export async function resolveXaiAuth(ctx?: ExtensionContext, signal?: AbortSignal): Promise<XaiAuth | undefined> {
|
|
103
137
|
const config = loadConfig();
|
|
104
138
|
const modelOverride = resolveConfiguredSearchModel(config.xaiSearchModel);
|
|
139
|
+
const tools = resolveConfiguredSearchTools(config.xaiSearchTools);
|
|
105
140
|
if (ctx) {
|
|
106
|
-
const auth = await resolvePiAuth(ctx, modelOverride);
|
|
141
|
+
const auth = await resolvePiAuth(ctx, modelOverride, tools);
|
|
107
142
|
if (auth) return auth;
|
|
108
143
|
}
|
|
109
144
|
|
|
@@ -119,12 +154,19 @@ export async function resolveXaiAuth(ctx?: ExtensionContext, signal?: AbortSigna
|
|
|
119
154
|
environmentValue: process.env.XAI_API_KEY,
|
|
120
155
|
signal,
|
|
121
156
|
});
|
|
122
|
-
return apiKey ? { apiKey, model: modelOverride ??
|
|
157
|
+
return apiKey ? { apiKey, model: modelOverride ?? modelCandidatesForTools(tools)[0], headers: {} } : undefined;
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
export async function isXaiSearchAvailable(ctx?: ExtensionContext): Promise<boolean> {
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
let config: WebSearchConfig;
|
|
162
|
+
let tools: XaiSearchTool[];
|
|
163
|
+
try {
|
|
164
|
+
config = loadConfig();
|
|
165
|
+
tools = resolveConfiguredSearchTools(config.xaiSearchTools);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (ctx && await resolvePiAuth(ctx, undefined, tools)) return true;
|
|
128
170
|
return hasCredentialSource({
|
|
129
171
|
provider: "xAI",
|
|
130
172
|
configuredValue: config.xaiApiKey,
|
|
@@ -153,9 +195,17 @@ function normalizeDomain(value: string): string | null {
|
|
|
153
195
|
* an unknown field risks a 400 that costs the whole search; steering through
|
|
154
196
|
* the instruction text degrades to "the model ignored it" instead.
|
|
155
197
|
*/
|
|
156
|
-
function buildInput(query: string, options: SearchOptions): string {
|
|
198
|
+
function buildInput(query: string, options: SearchOptions, tools: readonly XaiSearchTool[]): string {
|
|
199
|
+
let searchInstruction: string;
|
|
200
|
+
if (tools.includes("x_search") && tools.includes("web_search")) {
|
|
201
|
+
searchInstruction = "Search the web and X and answer using only what the search results say.";
|
|
202
|
+
} else if (tools.includes("x_search")) {
|
|
203
|
+
searchInstruction = "Search X and answer using only what the X results say.";
|
|
204
|
+
} else {
|
|
205
|
+
searchInstruction = "Search the web and answer using only what the web results say.";
|
|
206
|
+
}
|
|
157
207
|
const lines = [
|
|
158
|
-
|
|
208
|
+
searchInstruction,
|
|
159
209
|
"Cite your sources inline.",
|
|
160
210
|
];
|
|
161
211
|
|
|
@@ -198,6 +248,21 @@ function addResult(results: SearchResult[], seen: Set<string>, url: unknown, tit
|
|
|
198
248
|
});
|
|
199
249
|
}
|
|
200
250
|
|
|
251
|
+
function addSource(results: SearchResult[], seen: Set<string>, source: unknown): void {
|
|
252
|
+
if (typeof source === "string") {
|
|
253
|
+
addResult(results, seen, source, undefined);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!source || typeof source !== "object") return;
|
|
257
|
+
const record = source as Record<string, unknown>;
|
|
258
|
+
addResult(results, seen, record.url ?? record.source_website_url, record.title ?? record.caption);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function addSources(results: SearchResult[], seen: Set<string>, sources: unknown): void {
|
|
262
|
+
if (!Array.isArray(sources)) return;
|
|
263
|
+
for (const source of sources) addSource(results, seen, source);
|
|
264
|
+
}
|
|
265
|
+
|
|
201
266
|
function extractSnippetAround(text: string, start: unknown, end: unknown): string {
|
|
202
267
|
if (typeof start !== "number" || typeof end !== "number" || !text) return "";
|
|
203
268
|
const before = Math.max(0, start - 100);
|
|
@@ -222,13 +287,13 @@ function extractAnswer(output: unknown[]): string {
|
|
|
222
287
|
}
|
|
223
288
|
|
|
224
289
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
290
|
+
* Keep annotation sources first because they carry titles and offsets and must
|
|
291
|
+
* win URL deduplication. Raw source lists from web/X search calls are next,
|
|
292
|
+
* followed by xAI's response-level `citations` URL list. The latter is usually
|
|
293
|
+
* strings, but object entries are tolerated when a compatible gateway adds
|
|
294
|
+
* metadata.
|
|
230
295
|
*/
|
|
231
|
-
function extractSearchResults(output: unknown[], numResults: number | undefined): SearchResult[] {
|
|
296
|
+
function extractSearchResults(output: unknown[], citations: unknown, numResults: number | undefined): SearchResult[] {
|
|
232
297
|
const results: SearchResult[] = [];
|
|
233
298
|
const seenUrls = new Set<string>();
|
|
234
299
|
|
|
@@ -256,21 +321,18 @@ function extractSearchResults(output: unknown[], numResults: number | undefined)
|
|
|
256
321
|
}
|
|
257
322
|
|
|
258
323
|
for (const item of output) {
|
|
259
|
-
if (!item || typeof item !== "object"
|
|
324
|
+
if (!item || typeof item !== "object") continue;
|
|
325
|
+
const type = (item as { type?: unknown }).type;
|
|
326
|
+
if (type !== "web_search_call" && type !== "x_search_call") continue;
|
|
260
327
|
const value = item as { action?: unknown; sources?: unknown; results?: unknown };
|
|
261
328
|
const actionSources = value.action && typeof value.action === "object"
|
|
262
329
|
? (value.action as { sources?: unknown }).sources
|
|
263
330
|
: undefined;
|
|
264
|
-
for (const group of [actionSources, value.sources, value.results])
|
|
265
|
-
if (!Array.isArray(group)) continue;
|
|
266
|
-
for (const source of group) {
|
|
267
|
-
if (!source || typeof source !== "object") continue;
|
|
268
|
-
const record = source as Record<string, unknown>;
|
|
269
|
-
addResult(results, seenUrls, record.url ?? record.source_website_url, record.title ?? record.caption);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
331
|
+
for (const group of [actionSources, value.sources, value.results]) addSources(results, seenUrls, group);
|
|
272
332
|
}
|
|
273
333
|
|
|
334
|
+
addSources(results, seenUrls, citations);
|
|
335
|
+
|
|
274
336
|
if (typeof numResults === "number" && Number.isFinite(numResults) && numResults > 0) {
|
|
275
337
|
return results.slice(0, Math.min(Math.floor(numResults), 20));
|
|
276
338
|
}
|
|
@@ -282,10 +344,11 @@ export async function searchWithXai(
|
|
|
282
344
|
options: SearchOptions = {},
|
|
283
345
|
ctx?: ExtensionContext,
|
|
284
346
|
): Promise<SearchResponse> {
|
|
347
|
+
const tools = resolveConfiguredSearchTools(loadConfig().xaiSearchTools);
|
|
285
348
|
const auth = await resolveXaiAuth(ctx, options.signal);
|
|
286
349
|
if (!auth) {
|
|
287
350
|
throw new Error(
|
|
288
|
-
"xAI
|
|
351
|
+
"xAI search unavailable. Either:\n" +
|
|
289
352
|
" 1. Use /login to sign in with a SuperGrok or X Premium subscription\n" +
|
|
290
353
|
` 2. Create ${CONFIG_PATH} with { "xaiApiKey": "your-key" }\n` +
|
|
291
354
|
" 3. Set XAI_API_KEY environment variable",
|
|
@@ -303,8 +366,8 @@ export async function searchWithXai(
|
|
|
303
366
|
},
|
|
304
367
|
body: JSON.stringify({
|
|
305
368
|
model: auth.model,
|
|
306
|
-
input: buildInput(query, options),
|
|
307
|
-
tools:
|
|
369
|
+
input: buildInput(query, options, tools),
|
|
370
|
+
tools: tools.map((type) => ({ type })),
|
|
308
371
|
}),
|
|
309
372
|
signal: options.signal
|
|
310
373
|
? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal])
|
|
@@ -326,10 +389,10 @@ export async function searchWithXai(
|
|
|
326
389
|
}
|
|
327
390
|
const output = Array.isArray(parsed.output) ? parsed.output : [];
|
|
328
391
|
const answer = extractAnswer(output);
|
|
329
|
-
const results = extractSearchResults(output, options.numResults);
|
|
392
|
+
const results = extractSearchResults(output, parsed.citations, options.numResults);
|
|
330
393
|
|
|
331
394
|
if (!answer && results.length === 0) {
|
|
332
|
-
throw new Error(
|
|
395
|
+
throw new Error(`xAI ${tools.join("+")} returned no answer or sources`);
|
|
333
396
|
}
|
|
334
397
|
|
|
335
398
|
activityMonitor.logComplete(activityId, response.status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-extensions",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.1",
|
|
4
4
|
"description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"extensions",
|
|
@@ -37,7 +37,7 @@
|
|
|
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.16.0",
|
|
41
41
|
"@dietrichgebert/ponytail": "4.9.0",
|
|
42
42
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
43
43
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@narumitw/pi-caffeinate": "0.49.7",
|
|
61
61
|
"@narumitw/pi-tui-kit": "^0.59.0",
|
|
62
62
|
"@noble/ed25519": "^3.1.0",
|
|
63
|
-
"@pi-plugins/fast-mode": "0.1.
|
|
63
|
+
"@pi-plugins/fast-mode": "0.1.11",
|
|
64
64
|
"@shikijs/cli": "^4.0.2",
|
|
65
65
|
"@sinclair/typebox": "^0.34.49",
|
|
66
66
|
"@tifan/pi-copy-response": "0.2.6",
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
"open": "^10.2.0",
|
|
91
91
|
"p-limit": "^6.1.0",
|
|
92
92
|
"pi-footer": "0.5.1",
|
|
93
|
-
"pi-lens": "4.1.
|
|
93
|
+
"pi-lens": "4.1.4",
|
|
94
94
|
"pi-mcp-adapter": "2.32.1",
|
|
95
|
-
"pi-web-access": "0.
|
|
95
|
+
"pi-web-access": "0.28.0",
|
|
96
96
|
"pidusage": "^4.0.1",
|
|
97
97
|
"promise.try": "^2.0.1",
|
|
98
98
|
"qrcode-terminal": "^0.12.0",
|