@herbertgao/pi-extensions 2026.9.2 → 2026.9.4
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 +4 -2
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +20 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
- package/node_modules/@herbertgao/sol-pi/README.md +159 -0
- package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
- package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
- package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
- package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
- package/node_modules/@herbertgao/sol-pi/package.json +76 -0
- package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
- package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
- package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
- package/node_modules/pi-antigravity/LICENSE +21 -0
- package/node_modules/pi-antigravity/README.md +194 -0
- package/node_modules/pi-antigravity/package.json +67 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
- package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
- package/node_modules/pi-antigravity/src/client/client.ts +561 -0
- package/node_modules/pi-antigravity/src/client/index.ts +1 -0
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/node_modules/pi-antigravity/src/image/image.ts +336 -0
- package/node_modules/pi-antigravity/src/image/index.ts +1 -0
- package/node_modules/pi-antigravity/src/index.ts +280 -0
- package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
- package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
- package/node_modules/pi-antigravity/src/models/index.ts +3 -0
- package/node_modules/pi-antigravity/src/models/models.ts +500 -0
- package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
- package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
- package/node_modules/pi-antigravity/src/types/index.ts +2 -0
- package/node_modules/pi-antigravity/src/types/types.ts +292 -0
- package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
- package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
- package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
- package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
- package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
- package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
- package/node_modules/pi-antigravity/tsconfig.json +21 -0
- package/node_modules/pi-lens/CHANGELOG.md +176 -0
- package/node_modules/pi-lens/README.md +13 -8
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
- package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
- package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
- package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
- package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
- package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
- package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
- package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
- package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
- package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
- package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
- package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
- package/node_modules/pi-lens/dist/index.js +21969 -20005
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
- package/node_modules/pi-lens/dist/mcp/server.js +135 -159
- package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
- package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
- package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
- package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
- package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
- package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
- package/node_modules/pi-lens/docs/agent-guide.md +3 -4
- package/node_modules/pi-lens/docs/agent-tools.md +46 -22
- package/node_modules/pi-lens/docs/configuration.md +9 -0
- package/node_modules/pi-lens/docs/environment-variables.md +0 -5
- package/node_modules/pi-lens/docs/features.md +5 -5
- package/node_modules/pi-lens/docs/globalconfig.md +11 -1
- package/node_modules/pi-lens/docs/mcp.md +13 -1
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/real-harness.md +46 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
- package/node_modules/pi-lens/docs/settings.md +37 -2
- package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
- package/node_modules/pi-lens/docs/usage.md +13 -3
- package/node_modules/pi-lens/package.json +10 -1
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
- package/node_modules/pi-web-access/CHANGELOG.md +29 -0
- package/node_modules/pi-web-access/README.md +33 -22
- package/node_modules/pi-web-access/abortable.ts +17 -0
- package/node_modules/pi-web-access/crawl4ai.ts +204 -0
- package/node_modules/pi-web-access/credential-source.ts +1 -0
- package/node_modules/pi-web-access/curator-page.ts +20 -2
- package/node_modules/pi-web-access/curator-run.ts +44 -0
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/duckduckgo.ts +1 -1
- package/node_modules/pi-web-access/extract.ts +57 -12
- package/node_modules/pi-web-access/fetch-params.ts +1 -1
- package/node_modules/pi-web-access/gemini-search.ts +9 -5
- package/node_modules/pi-web-access/index.ts +25 -18
- package/node_modules/pi-web-access/openai-search.ts +35 -13
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/page-query.ts +5 -2
- package/node_modules/pi-web-access/query-rewrite.ts +5 -2
- package/node_modules/pi-web-access/serpapi.ts +220 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
- package/node_modules/pi-web-access/storage.ts +10 -0
- package/node_modules/pi-web-access/summary-review.ts +34 -21
- package/node_modules/pi-web-access/utils.ts +6 -4
- package/package.json +11 -5
- package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
|
@@ -2,7 +2,7 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-
|
|
|
2
2
|
import { Box, Text, truncateToWidth, type KeyId } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import pLimit from "p-limit";
|
|
5
|
-
import {
|
|
5
|
+
import type { ImageContent, TextContent } from "@earendil-works/pi-ai/compat";
|
|
6
6
|
import type { ExtractedContent, ExtractOptions } from "./extract.ts";
|
|
7
7
|
import { normalizeFetchContentParams } from "./fetch-params.ts";
|
|
8
8
|
import { resolveAuthFetchProfile, type AuthFetchProfile } from "./auth-fetch.ts";
|
|
@@ -69,11 +69,13 @@ import { isMistralAvailable } from "./mistral-search.ts";
|
|
|
69
69
|
import { isKimiSearchAvailable } from "./kimi-search.ts";
|
|
70
70
|
import { isBrightDataAvailable } from "./brightdata.ts";
|
|
71
71
|
import { isSerpBaseAvailable } from "./serpbase.ts";
|
|
72
|
+
import { isSerpApiAvailable } from "./serpapi.ts";
|
|
72
73
|
import { isSerperAvailable } from "./serper.ts";
|
|
73
74
|
import { isValyuAvailable } from "./valyu.ts";
|
|
74
75
|
import { isXcrawlAvailable } from "./xcrawl.ts";
|
|
75
76
|
import { buildSearchErrorPlan, type SearchErrorDetails, type SearchErrorPlan } from "./render-search-error.ts";
|
|
76
77
|
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns, splitThinkingSuffix } from "./summary-model-scope.ts";
|
|
78
|
+
import { registerCuratorRunLifecycle, resolveWebSearchWorkflow, type WebSearchWorkflow } from "./curator-run.ts";
|
|
77
79
|
import {
|
|
78
80
|
buildResearchArtifact,
|
|
79
81
|
withClaimAssessment,
|
|
@@ -83,6 +85,16 @@ import {
|
|
|
83
85
|
type ResearchArtifact,
|
|
84
86
|
} from "./source-check.ts";
|
|
85
87
|
|
|
88
|
+
// Match pi-ai's StringEnum without loading its compat barrel during registration.
|
|
89
|
+
function StringEnum<T extends string[]>(values: T, options?: { description?: string; default?: T[number] }) {
|
|
90
|
+
return Type.Unsafe<T[number]>({
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: values,
|
|
93
|
+
...(options?.description && { description: options.description }),
|
|
94
|
+
...(options?.default && { default: options.default }),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
type ExtensionTheme = ExtensionContext["ui"]["theme"];
|
|
87
99
|
|
|
88
100
|
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
@@ -139,6 +151,7 @@ interface WebSearchConfig {
|
|
|
139
151
|
kagiApiKey?: unknown;
|
|
140
152
|
ollamaApiKey?: unknown;
|
|
141
153
|
serpbaseApiKey?: unknown;
|
|
154
|
+
serpapiApiKey?: unknown;
|
|
142
155
|
serperApiKey?: unknown;
|
|
143
156
|
tinyfishApiKey?: unknown;
|
|
144
157
|
valyuApiKey?: unknown;
|
|
@@ -170,7 +183,6 @@ interface WebSearchConfig {
|
|
|
170
183
|
};
|
|
171
184
|
}
|
|
172
185
|
|
|
173
|
-
type WebSearchWorkflow = "none" | "summary-review" | "auto-summary";
|
|
174
186
|
type CuratorWorkflow = "summary-review";
|
|
175
187
|
export type CuratorProvider = Exclude<SearchProvider, "auto">;
|
|
176
188
|
type SummaryWorkflow = "summary-review" | "auto-summary";
|
|
@@ -347,14 +359,6 @@ function normalizeCuratorTimeoutSeconds(value: unknown): number | undefined {
|
|
|
347
359
|
return Math.min(normalized, MAX_CURATOR_TIMEOUT_SECONDS);
|
|
348
360
|
}
|
|
349
361
|
|
|
350
|
-
function resolveWorkflow(input: unknown, hasUI: boolean): WebSearchWorkflow {
|
|
351
|
-
const normalized = typeof input === "string" ? input.trim().toLowerCase() : "";
|
|
352
|
-
if (normalized === "auto-summary") return "auto-summary";
|
|
353
|
-
if (!hasUI) return "none";
|
|
354
|
-
if (normalized === "none") return "none";
|
|
355
|
-
return "summary-review";
|
|
356
|
-
}
|
|
357
|
-
|
|
358
362
|
function normalizeQueryList(queryList: unknown[]): string[] {
|
|
359
363
|
const normalized: string[] = [];
|
|
360
364
|
for (const query of queryList) {
|
|
@@ -445,6 +449,7 @@ async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderA
|
|
|
445
449
|
mistral: isMistralAvailable(),
|
|
446
450
|
brightdata: isBrightDataAvailable(),
|
|
447
451
|
serpbase: isSerpBaseAvailable(),
|
|
452
|
+
serpapi: isSerpApiAvailable(),
|
|
448
453
|
serper: isSerperAvailable(),
|
|
449
454
|
valyu: isValyuAvailable(),
|
|
450
455
|
};
|
|
@@ -1058,6 +1063,7 @@ function handleSessionChange(ctx: ExtensionContext): void {
|
|
|
1058
1063
|
|
|
1059
1064
|
export default function (pi: ExtensionAPI) {
|
|
1060
1065
|
const initConfig = loadConfigForExtensionInit();
|
|
1066
|
+
const curatorRunState = registerCuratorRunLifecycle(pi);
|
|
1061
1067
|
installGlobalProxyFetch();
|
|
1062
1068
|
const toolNames = resolveToolNames(initConfig);
|
|
1063
1069
|
const webSearchEnabled = isToolEnabled(initConfig, "webSearch");
|
|
@@ -1559,6 +1565,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1559
1565
|
},
|
|
1560
1566
|
onSubmit(payload) {
|
|
1561
1567
|
if (pendingCurates.get(callId) !== pc) return;
|
|
1568
|
+
if (payload.autoApproveRemainingSearches) curatorRunState.approveRemainingSearches();
|
|
1562
1569
|
searchAbort.abort();
|
|
1563
1570
|
const filtered = payload.selectedQueryIndices.length > 0
|
|
1564
1571
|
? filterByQueryIndices(payload.selectedQueryIndices, pc.searchResults)
|
|
@@ -1783,7 +1790,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1783
1790
|
name: toolNames.webSearch,
|
|
1784
1791
|
label: "Web Search",
|
|
1785
1792
|
description:
|
|
1786
|
-
`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, Mistral, 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, Mistral, 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; Mistral search uses a Mistral API key. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, 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.`,
|
|
1793
|
+
`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, Mistral, Bright Data, SerpBase, SerpApi, 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, Mistral, Bright Data, SerpBase, SerpApi, 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; Mistral search uses a Mistral API key. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data, SerpBase, SerpApi, 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.`,
|
|
1787
1794
|
promptSnippet:
|
|
1788
1795
|
"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.",
|
|
1789
1796
|
parameters: Type.Object({
|
|
@@ -1795,14 +1802,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1795
1802
|
StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" }),
|
|
1796
1803
|
),
|
|
1797
1804
|
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
|
|
1798
|
-
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, Mistral, Bright Data, SerpBase, and Serper, omit this field to use the configured provider, or use auto when none is configured")),
|
|
1805
|
+
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, Mistral, Bright Data, SerpBase, SerpApi, and Serper, omit this field to use the configured provider, or use auto when none is configured")),
|
|
1799
1806
|
workflow: Type.Optional(
|
|
1800
1807
|
StringEnum(["none", "summary-review", "auto-summary"], {
|
|
1801
1808
|
description: "Search workflow mode: none = no curator, summary-review = open curator with auto summary draft (default), auto-summary = generate summary without opening curator",
|
|
1802
1809
|
}),
|
|
1803
1810
|
),
|
|
1804
1811
|
proxy: Type.Optional(Type.String({
|
|
1805
|
-
description: "http(s) proxy URL (e.g. http://host:port) used for every outbound request in this call (search APIs and content fetches). Node fetch ignores HTTP(S)_PROXY env vars, so set this (or `proxy` in web-search.json) when direct access is blocked; empty string forces direct access.",
|
|
1812
|
+
description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for every outbound request in this call (search APIs and content fetches). Node fetch ignores HTTP(S)_PROXY env vars, so set this (or `proxy` in web-search.json) when direct access is blocked; empty string forces direct access.",
|
|
1806
1813
|
})),
|
|
1807
1814
|
}),
|
|
1808
1815
|
|
|
@@ -1813,7 +1820,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1813
1820
|
: (params.query !== undefined ? expandQueryString(params.query) : []);
|
|
1814
1821
|
const queryList = normalizeQueryList(rawQueryList);
|
|
1815
1822
|
const configWorkflow = loadConfigForExtensionInit().workflow;
|
|
1816
|
-
const workflow =
|
|
1823
|
+
const workflow = curatorRunState.resolve(params.workflow, configWorkflow, ctx?.hasUI !== false);
|
|
1817
1824
|
const shouldCurate = workflow === "summary-review";
|
|
1818
1825
|
const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
|
|
1819
1826
|
|
|
@@ -2389,9 +2396,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2389
2396
|
fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
|
|
2390
2397
|
recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
|
|
2391
2398
|
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
|
|
2392
|
-
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, Mistral, Bright Data, SerpBase, and Serper")),
|
|
2399
|
+
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, Mistral, Bright Data, SerpBase, SerpApi, and Serper")),
|
|
2393
2400
|
proxy: Type.Optional(Type.String({
|
|
2394
|
-
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.",
|
|
2401
|
+
description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for every outbound request in this call (search APIs and result-page fetches). Empty string forces direct access.",
|
|
2395
2402
|
})),
|
|
2396
2403
|
}),
|
|
2397
2404
|
async execute(_callId, params, signal, _onUpdate, ctx) {
|
|
@@ -2512,7 +2519,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2512
2519
|
description: "Opt into an authFetch profile for local browser-cookie fetching. Use a profile name, or true only when exactly one profile exists.",
|
|
2513
2520
|
})),
|
|
2514
2521
|
proxy: Type.Optional(Type.String({
|
|
2515
|
-
description: "http(s) proxy URL (e.g. http://host:port) used for this fetch. Needed when the target is unreachable directly; localhost and NO_PROXY hosts always bypass the proxy. Empty string forces direct access.",
|
|
2522
|
+
description: "http(s) or socks proxy URL (e.g. http://host:port or socks5h://host:port) used for this fetch. Needed when the target is unreachable directly; localhost and NO_PROXY hosts always bypass the proxy. Empty string forces direct access.",
|
|
2516
2523
|
})),
|
|
2517
2524
|
}),
|
|
2518
2525
|
|
|
@@ -3424,7 +3431,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3424
3431
|
|
|
3425
3432
|
let newWorkflow: WebSearchWorkflow;
|
|
3426
3433
|
if (arg.length === 0) {
|
|
3427
|
-
const current =
|
|
3434
|
+
const current = resolveWebSearchWorkflow(loadConfigForExtensionInit().workflow, true);
|
|
3428
3435
|
newWorkflow = current === "none" ? "summary-review" : "none";
|
|
3429
3436
|
} else if (arg === "on") {
|
|
3430
3437
|
newWorkflow = "summary-review";
|
|
@@ -41,6 +41,8 @@ interface WebSearchConfig {
|
|
|
41
41
|
|
|
42
42
|
type ProviderHeaders = Record<string, string | null>;
|
|
43
43
|
|
|
44
|
+
class CustomOpenAIBaseUrlError extends Error {}
|
|
45
|
+
|
|
44
46
|
interface OpenAIAuth {
|
|
45
47
|
provider: string;
|
|
46
48
|
apiKey: string;
|
|
@@ -215,7 +217,7 @@ function toRequestHeaders(headers: ProviderHeaders): Record<string, string> {
|
|
|
215
217
|
return requestHeaders;
|
|
216
218
|
}
|
|
217
219
|
|
|
218
|
-
async function resolvePiAuth(ctx: ExtensionContext, responsesUrl: string, providers: readonly string[], modelOverride?: string): Promise<OpenAIAuth | undefined> {
|
|
220
|
+
async function resolvePiAuth(ctx: ExtensionContext, responsesUrl: string, providers: readonly string[], modelOverride?: string, hasExplicitResponsesUrl = false): Promise<OpenAIAuth | undefined> {
|
|
219
221
|
let models: ReturnType<typeof ctx.modelRegistry.getAll>;
|
|
220
222
|
try {
|
|
221
223
|
models = ctx.modelRegistry.getAll();
|
|
@@ -225,19 +227,34 @@ async function resolvePiAuth(ctx: ExtensionContext, responsesUrl: string, provid
|
|
|
225
227
|
for (const provider of providers) {
|
|
226
228
|
const preferred = pickSearchModel(models.filter((model) => model.provider === provider));
|
|
227
229
|
if (!preferred) continue;
|
|
230
|
+
let resolved: Awaited<ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>>;
|
|
228
231
|
try {
|
|
229
|
-
|
|
230
|
-
if (resolved.ok && resolved.apiKey) {
|
|
231
|
-
return {
|
|
232
|
-
provider,
|
|
233
|
-
apiKey: resolved.apiKey,
|
|
234
|
-
model: modelOverride ?? preferred.id,
|
|
235
|
-
headers: resolved.headers ?? {},
|
|
236
|
-
responsesUrl,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
232
|
+
resolved = await ctx.modelRegistry.getApiKeyAndHeaders(preferred);
|
|
239
233
|
} catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!resolved.ok || !resolved.apiKey) continue;
|
|
237
|
+
// Auth can override the model's base URL. Do not guess Responses/web_search
|
|
238
|
+
// support from a gateway URL, or pair its credential with the official API.
|
|
239
|
+
const baseUrl = resolved.baseUrl ?? preferred.baseUrl;
|
|
240
|
+
const useCodexEndpoint = provider === "openai-codex" || isCodexJwt(resolved.apiKey);
|
|
241
|
+
if (!hasExplicitResponsesUrl && !useCodexEndpoint && baseUrl !== undefined) {
|
|
242
|
+
let isOfficial = false;
|
|
243
|
+
try {
|
|
244
|
+
isOfficial = new URL(baseUrl).toString().replace(/\/+$/u, "") === "https://api.openai.com/v1";
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
if (!isOfficial) {
|
|
248
|
+
throw new CustomOpenAIBaseUrlError(`OpenAI web search cannot reuse Pi credentials with a custom baseUrl by default. Set openaiResponsesUrl in ${CONFIG_PATH} to the full Responses endpoint for this credential.`);
|
|
249
|
+
}
|
|
240
250
|
}
|
|
251
|
+
return {
|
|
252
|
+
provider,
|
|
253
|
+
apiKey: resolved.apiKey,
|
|
254
|
+
model: modelOverride ?? preferred.id,
|
|
255
|
+
headers: resolved.headers ?? {},
|
|
256
|
+
responsesUrl,
|
|
257
|
+
};
|
|
241
258
|
}
|
|
242
259
|
return undefined;
|
|
243
260
|
}
|
|
@@ -248,7 +265,7 @@ export async function resolveOpenAIAuth(ctx?: ExtensionContext, signal?: AbortSi
|
|
|
248
265
|
const modelOverride = resolveConfiguredSearchModel(config.openaiSearchModel);
|
|
249
266
|
const providers = resolveConfiguredSearchProviders(config.openaiSearchProviders);
|
|
250
267
|
if (ctx) {
|
|
251
|
-
const auth = await resolvePiAuth(ctx, responsesUrl, providers, modelOverride);
|
|
268
|
+
const auth = await resolvePiAuth(ctx, responsesUrl, providers, modelOverride, config.openaiResponsesUrl !== undefined);
|
|
252
269
|
if (auth) return auth;
|
|
253
270
|
}
|
|
254
271
|
|
|
@@ -273,7 +290,12 @@ export async function isOpenAISearchAvailable(ctx?: ExtensionContext): Promise<b
|
|
|
273
290
|
const config = loadConfig();
|
|
274
291
|
const responsesUrl = resolveConfiguredResponsesUrl(config.openaiResponsesUrl);
|
|
275
292
|
const providers = resolveConfiguredSearchProviders(config.openaiSearchProviders);
|
|
276
|
-
|
|
293
|
+
try {
|
|
294
|
+
if (ctx && await resolvePiAuth(ctx, responsesUrl, providers, undefined, config.openaiResponsesUrl !== undefined)) return true;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (err instanceof CustomOpenAIBaseUrlError) return false;
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
277
299
|
return hasCredentialSource({
|
|
278
300
|
provider: "OpenAI",
|
|
279
301
|
configuredValue: config.openaiApiKey,
|
|
@@ -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, Kimi, and Mistral.",
|
|
3
|
+
"version": "0.29.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, SerpApi, SearXNG, Exa, Perplexity, Gemini, Kimi, and Mistral.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node --test",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { complete,
|
|
1
|
+
import type { complete, Api, Message, Model } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns } from "./summary-model-scope.ts";
|
|
5
5
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
6
|
+
import { awaitWithAbort } from "./abortable.ts";
|
|
6
7
|
|
|
7
8
|
const OUTPUT_TOKENS = 2_000;
|
|
8
9
|
const INPUT_CONTEXT_FRACTION = 0.6;
|
|
@@ -111,7 +112,9 @@ export async function answerFromPage(
|
|
|
111
112
|
if (!auth.ok || !auth.apiKey) throw new Error(`No API key available for answer model ${model.provider}/${model.id}`);
|
|
112
113
|
const registry = ctx.modelRegistry as typeof ctx.modelRegistry & { complete?: typeof complete };
|
|
113
114
|
const usesRegistryComplete = typeof registry.complete === "function";
|
|
114
|
-
|
|
115
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
116
|
+
const completeFn = usesRegistryComplete ? registry.complete!.bind(registry) : (await awaitWithAbort(import("@earendil-works/pi-ai/compat"), signal)).complete;
|
|
117
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
115
118
|
|
|
116
119
|
const contextTokens = model.contextWindow > 0 ? model.contextWindow : FALLBACK_CONTEXT_TOKENS;
|
|
117
120
|
const maximumInputTokens = Math.max(1, Math.min(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { complete,
|
|
1
|
+
import type { complete, Api, Model, ProviderHeaders } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import type { SummaryGenerationContext } from "./summary-review.ts";
|
|
3
|
+
import { awaitWithAbort } from "./abortable.ts";
|
|
3
4
|
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns } from "./summary-model-scope.ts";
|
|
4
5
|
|
|
5
6
|
async function resolveFirstAvailableModel(
|
|
@@ -28,7 +29,9 @@ export async function rewriteSearchQuery(
|
|
|
28
29
|
]);
|
|
29
30
|
const registry = ctx.modelRegistry as typeof ctx.modelRegistry & { complete?: typeof complete };
|
|
30
31
|
const usesRegistryComplete = typeof registry.complete === "function";
|
|
31
|
-
|
|
32
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
33
|
+
const completeFn = usesRegistryComplete ? registry.complete!.bind(registry) : (await awaitWithAbort(import("@earendil-works/pi-ai/compat"), signal)).complete;
|
|
34
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
32
35
|
const response = await completeFn(
|
|
33
36
|
model,
|
|
34
37
|
{
|
|
@@ -0,0 +1,220 @@
|
|
|
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 SERPAPI_SEARCH_URL = "https://serpapi.com/search.json";
|
|
8
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
9
|
+
const SEARCH_TIMEOUT_MS = 60_000;
|
|
10
|
+
const RECENCY_TBS: Record<NonNullable<SearchOptions["recencyFilter"]>, string> = {
|
|
11
|
+
day: "qdr:d",
|
|
12
|
+
week: "qdr:w",
|
|
13
|
+
month: "qdr:m",
|
|
14
|
+
year: "qdr:y",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface WebSearchConfig {
|
|
18
|
+
serpapiApiKey?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SerpApiOrganicResult {
|
|
22
|
+
title?: unknown;
|
|
23
|
+
link?: unknown;
|
|
24
|
+
snippet?: unknown;
|
|
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
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
44
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
45
|
+
}
|
|
46
|
+
cachedConfig = parsed as WebSearchConfig;
|
|
47
|
+
return cachedConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getApiKey(signal?: AbortSignal): Promise<string | null> {
|
|
51
|
+
return resolveCredential({
|
|
52
|
+
provider: "SerpApi",
|
|
53
|
+
configuredValue: loadConfig().serpapiApiKey,
|
|
54
|
+
environmentValue: process.env.SERPAPI_KEY,
|
|
55
|
+
signal,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function requireApiKey(signal?: AbortSignal): Promise<string> {
|
|
60
|
+
const apiKey = await getApiKey(signal);
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"SerpApi API key not found. Either:\n" +
|
|
64
|
+
` 1. Create ${CONFIG_PATH} with { "serpapiApiKey": "your-key" }\n` +
|
|
65
|
+
" 2. Set SERPAPI_KEY environment variable\n" +
|
|
66
|
+
"Get a key at https://serpapi.com/manage-api-key",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return apiKey;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeCount(value: number | undefined): number {
|
|
73
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
74
|
+
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface DomainFilters {
|
|
78
|
+
include: string[];
|
|
79
|
+
exclude: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeDomain(value: string): string | null {
|
|
83
|
+
let input = value.trim().toLowerCase();
|
|
84
|
+
if (!input) return null;
|
|
85
|
+
if (input.startsWith("-")) input = input.slice(1).trim();
|
|
86
|
+
if (!input) return null;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
89
|
+
input = parsed.hostname;
|
|
90
|
+
} catch {
|
|
91
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
92
|
+
}
|
|
93
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
94
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
|
|
98
|
+
const filters: DomainFilters = { include: [], exclude: [] };
|
|
99
|
+
for (const raw of domainFilter ?? []) {
|
|
100
|
+
const domain = normalizeDomain(raw);
|
|
101
|
+
if (!domain) continue;
|
|
102
|
+
const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
|
|
103
|
+
if (!target.includes(domain)) target.push(domain);
|
|
104
|
+
}
|
|
105
|
+
return filters;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function passesDomainFilters(url: string, filters: DomainFilters): boolean {
|
|
109
|
+
if (filters.include.length === 0 && filters.exclude.length === 0) return true;
|
|
110
|
+
let hostname: string;
|
|
111
|
+
try {
|
|
112
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const matches = (domain: string) => hostname === domain || hostname.endsWith(`.${domain}`);
|
|
117
|
+
if (filters.exclude.some(matches)) return false;
|
|
118
|
+
return filters.include.length === 0 || filters.include.some(matches);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildQuery(query: string, filters: DomainFilters): string {
|
|
122
|
+
const parts = [query];
|
|
123
|
+
if (filters.include.length === 1) parts.push(`site:${filters.include[0]}`);
|
|
124
|
+
if (filters.include.length > 1) parts.push(`(${filters.include.map(domain => `site:${domain}`).join(" OR ")})`);
|
|
125
|
+
for (const domain of filters.exclude) parts.push(`-site:${domain}`);
|
|
126
|
+
return parts.join(" ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function errorMessage(err: unknown): string {
|
|
130
|
+
return err instanceof Error ? err.message : String(err);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function invalidResponse(message: string): Error {
|
|
134
|
+
return new Error(`SerpApi returned invalid response: ${message}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseResponse(value: unknown): SerpApiOrganicResult[] {
|
|
138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("expected an object envelope");
|
|
139
|
+
const envelope = value as Record<string, unknown>;
|
|
140
|
+
if (typeof envelope.error === "string" && envelope.error.trim()) throw invalidResponse(envelope.error.trim());
|
|
141
|
+
if (!Array.isArray(envelope.organic_results)) throw invalidResponse("expected organic_results array");
|
|
142
|
+
return envelope.organic_results as SerpApiOrganicResult[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildAnswer(results: SearchResponse["results"]): string {
|
|
146
|
+
return results.map((result) => result.snippet
|
|
147
|
+
? `${result.snippet}\nSource: ${result.title} (${result.url})`
|
|
148
|
+
: `Source: ${result.title} (${result.url})`).join("\n\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isSerpApiAvailable(): boolean {
|
|
152
|
+
return hasCredentialSource({ provider: "SerpApi", configuredValue: loadConfig().serpapiApiKey, environmentValue: process.env.SERPAPI_KEY });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function searchWithSerpApi(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
156
|
+
const apiKey = await requireApiKey(options.signal);
|
|
157
|
+
const numResults = normalizeCount(options.numResults);
|
|
158
|
+
const filters = parseDomainFilter(options.domainFilter);
|
|
159
|
+
const requestCount = options.domainFilter?.length ? Math.min(20, numResults + 5) : numResults;
|
|
160
|
+
const url = new URL(SERPAPI_SEARCH_URL);
|
|
161
|
+
url.searchParams.set("engine", "google");
|
|
162
|
+
url.searchParams.set("q", buildQuery(query, filters));
|
|
163
|
+
url.searchParams.set("api_key", apiKey);
|
|
164
|
+
url.searchParams.set("num", String(requestCount));
|
|
165
|
+
if (options.recencyFilter) url.searchParams.set("tbs", RECENCY_TBS[options.recencyFilter]);
|
|
166
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
167
|
+
const timeoutSignal = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
|
|
168
|
+
let response: Response;
|
|
169
|
+
let entries: SerpApiOrganicResult[];
|
|
170
|
+
try {
|
|
171
|
+
response = await fetch(url, {
|
|
172
|
+
headers: { Accept: "application/json" },
|
|
173
|
+
signal: options.signal ? AbortSignal.any([timeoutSignal, options.signal]) : timeoutSignal,
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const errorText = await response.text();
|
|
177
|
+
throw new Error(`SerpApi error ${response.status}: ${redactCredential(errorText, apiKey).slice(0, 300)}`);
|
|
178
|
+
}
|
|
179
|
+
let rawData: unknown;
|
|
180
|
+
try {
|
|
181
|
+
rawData = await response.json();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof Error && err.name === "TimeoutError") throw err;
|
|
184
|
+
throw new Error(`SerpApi returned invalid JSON: ${errorMessage(err)}`);
|
|
185
|
+
}
|
|
186
|
+
entries = parseResponse(rawData);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (options.signal?.aborted) {
|
|
189
|
+
activityMonitor.logComplete(activityId, 0);
|
|
190
|
+
throw new Error("Aborted");
|
|
191
|
+
}
|
|
192
|
+
const message = errorMessage(err);
|
|
193
|
+
const providerTimeout = timeoutSignal.aborted || (err instanceof Error && err.name === "TimeoutError");
|
|
194
|
+
const outgoing = providerTimeout
|
|
195
|
+
? new Error(`SerpApi request timed out after ${Math.round(SEARCH_TIMEOUT_MS / 1000)}s`)
|
|
196
|
+
: (() => {
|
|
197
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
198
|
+
if (redactedMessage === message && err instanceof Error) return err;
|
|
199
|
+
const redactedError = new Error(redactedMessage);
|
|
200
|
+
if (err instanceof Error) redactedError.name = err.name;
|
|
201
|
+
return redactedError;
|
|
202
|
+
})();
|
|
203
|
+
activityMonitor.logError(activityId, redactCredential(errorMessage(outgoing), apiKey));
|
|
204
|
+
throw outgoing;
|
|
205
|
+
}
|
|
206
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
207
|
+
const results: SearchResponse["results"] = [];
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (!entry || typeof entry !== "object") continue;
|
|
210
|
+
if (typeof entry.link !== "string" || !entry.link) continue;
|
|
211
|
+
if (!passesDomainFilters(entry.link, filters)) continue;
|
|
212
|
+
results.push({
|
|
213
|
+
title: typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : `Source ${results.length + 1}`,
|
|
214
|
+
url: entry.link,
|
|
215
|
+
snippet: typeof entry.snippet === "string" ? entry.snippet : "",
|
|
216
|
+
});
|
|
217
|
+
if (results.length >= numResults) break;
|
|
218
|
+
}
|
|
219
|
+
return { answer: buildAnswer(results), results };
|
|
220
|
+
}
|
|
@@ -232,6 +232,7 @@ export async function fetchRemoteUrl(
|
|
|
232
232
|
const fetchImpl = options.fetch ?? fetch;
|
|
233
233
|
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
234
234
|
let current = await validateRemoteUrl(url, options);
|
|
235
|
+
const configuredOrigin = current.origin;
|
|
235
236
|
let requestInit = init;
|
|
236
237
|
|
|
237
238
|
for (let redirects = 0; redirects <= maxRedirects; redirects++) {
|
|
@@ -243,7 +244,10 @@ export async function fetchRemoteUrl(
|
|
|
243
244
|
if (redirects === maxRedirects) throw new Error(`Too many redirects fetching ${current.toString()}`);
|
|
244
245
|
|
|
245
246
|
const from = current;
|
|
246
|
-
|
|
247
|
+
const next = new URL(location, current);
|
|
248
|
+
// allowLoopback exempts an explicitly configured endpoint, never a redirect target: a loopback
|
|
249
|
+
// base must not be able to pivot the request onto a different loopback origin.
|
|
250
|
+
current = await validateRemoteUrl(next, next.origin === configuredOrigin ? options : { ...options, allowLoopback: false });
|
|
247
251
|
if (response.status === 303 || ((response.status === 301 || response.status === 302) && requestInit.method?.toUpperCase() === "POST")) {
|
|
248
252
|
const { body: _body, ...nextInit } = requestInit;
|
|
249
253
|
requestInit = { ...nextInit, method: "GET" };
|
|
@@ -426,8 +426,17 @@ function readCachedFetchData(data: StoredSearchData, now = Date.now()): StoredSe
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
function pruneExpiredFetchedResults(now: number): void {
|
|
430
|
+
for (const [id, data] of storedResults) {
|
|
431
|
+
if (data.type === "fetch" && now - data.timestamp >= CACHE_TTL_MS) {
|
|
432
|
+
storedResults.set(id, unavailableFetchData(data, "Cached fetched content is missing or expired"));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
429
437
|
export function pruneExpiredFetchCache(now = Date.now(), requestedLimits?: Partial<FetchCacheLimits>): void {
|
|
430
438
|
const limits = cacheLimits(requestedLimits);
|
|
439
|
+
pruneExpiredFetchedResults(now);
|
|
431
440
|
try { pruneFetchCache(now, limits); } catch {}
|
|
432
441
|
}
|
|
433
442
|
|
|
@@ -436,6 +445,7 @@ export function storeResult(id: string, data: StoredSearchData): void {
|
|
|
436
445
|
}
|
|
437
446
|
|
|
438
447
|
export function storeFetchedContentResult(id: string, data: StoredSearchData & { type: "fetch"; urls: ExtractedContent[] }): StoredSearchData {
|
|
448
|
+
pruneExpiredFetchedResults(Date.now());
|
|
439
449
|
let ref: FetchCacheRef | null = null;
|
|
440
450
|
let cacheError: string | undefined;
|
|
441
451
|
try {
|