@herbertgao/pi-extensions 2026.9.10 → 2026.9.11

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.
Files changed (122) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  8. package/node_modules/@herbertgao/pi-subagents/package.json +2 -2
  9. package/node_modules/@herbertgao/resume-from/package.json +1 -1
  10. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  11. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  12. package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
  13. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
  14. package/node_modules/@herbertgao/sol-pi/package.json +2 -2
  15. package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
  16. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  17. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  18. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
  19. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
  20. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
  21. package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
  22. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
  23. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  24. package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
  25. package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
  26. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
  27. package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
  28. package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
  29. package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
  30. package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
  31. package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
  32. package/node_modules/pi-multi-account/LICENSE +21 -0
  33. package/node_modules/pi-multi-account/README.md +197 -0
  34. package/node_modules/pi-multi-account/SECURITY.md +27 -0
  35. package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
  36. package/node_modules/pi-multi-account/child-usability.ts +233 -0
  37. package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
  38. package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
  39. package/node_modules/pi-multi-account/context-guard.ts +420 -0
  40. package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
  41. package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
  42. package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
  43. package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
  44. package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
  45. package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
  46. package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
  47. package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
  48. package/node_modules/pi-multi-account/cursor/index.ts +572 -0
  49. package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
  50. package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
  51. package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
  52. package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
  53. package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
  54. package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
  55. package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
  56. package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
  57. package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
  58. package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
  59. package/node_modules/pi-multi-account/index.ts +11825 -0
  60. package/node_modules/pi-multi-account/model-catalog.ts +354 -0
  61. package/node_modules/pi-multi-account/package.json +101 -0
  62. package/node_modules/pi-multi-account/pi-contract.ts +281 -0
  63. package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
  64. package/node_modules/pi-multi-account/provider-priority.ts +189 -0
  65. package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
  66. package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
  67. package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
  68. package/node_modules/pi-multi-account/usage.ts +1099 -0
  69. package/node_modules/pi-typesafe/README.md +6 -2
  70. package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
  71. package/node_modules/pi-typesafe/dist/client.js +45 -10
  72. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  73. package/node_modules/pi-typesafe/dist/index.js +1 -1
  74. package/node_modules/pi-typesafe/package.json +2 -2
  75. package/node_modules/pi-web-access/CHANGELOG.md +36 -0
  76. package/node_modules/pi-web-access/README.md +75 -18
  77. package/node_modules/pi-web-access/anysearch.ts +4 -15
  78. package/node_modules/pi-web-access/bocha.ts +3 -22
  79. package/node_modules/pi-web-access/brave.ts +3 -21
  80. package/node_modules/pi-web-access/brightdata.ts +5 -32
  81. package/node_modules/pi-web-access/content-find.ts +168 -53
  82. package/node_modules/pi-web-access/curator-page.ts +4 -1
  83. package/node_modules/pi-web-access/curator-run.ts +2 -1
  84. package/node_modules/pi-web-access/curator-server.ts +1 -0
  85. package/node_modules/pi-web-access/dist/index.js +24620 -0
  86. package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
  87. package/node_modules/pi-web-access/duckduckgo.ts +3 -21
  88. package/node_modules/pi-web-access/extract.ts +3 -1
  89. package/node_modules/pi-web-access/firecrawl.ts +5 -29
  90. package/node_modules/pi-web-access/gemini-search.ts +81 -32
  91. package/node_modules/pi-web-access/index.ts +149 -148
  92. package/node_modules/pi-web-access/jina-search.ts +4 -15
  93. package/node_modules/pi-web-access/kagi.ts +4 -13
  94. package/node_modules/pi-web-access/kimi-search.ts +5 -30
  95. package/node_modules/pi-web-access/mistral-search.ts +1 -15
  96. package/node_modules/pi-web-access/ollama.ts +2 -7
  97. package/node_modules/pi-web-access/openai-search.ts +174 -36
  98. package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
  99. package/node_modules/pi-web-access/package.json +10 -4
  100. package/node_modules/pi-web-access/page-query.ts +10 -2
  101. package/node_modules/pi-web-access/parallel.ts +1 -15
  102. package/node_modules/pi-web-access/pdf-extract.ts +3 -0
  103. package/node_modules/pi-web-access/querit.ts +5 -29
  104. package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
  105. package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
  106. package/node_modules/pi-web-access/search1api.ts +5 -29
  107. package/node_modules/pi-web-access/searchinfinity.ts +5 -29
  108. package/node_modules/pi-web-access/searxng.ts +3 -21
  109. package/node_modules/pi-web-access/serpapi.ts +5 -28
  110. package/node_modules/pi-web-access/serpbase.ts +3 -22
  111. package/node_modules/pi-web-access/serpdive.ts +3 -21
  112. package/node_modules/pi-web-access/serper.ts +5 -28
  113. package/node_modules/pi-web-access/serply.ts +197 -0
  114. package/node_modules/pi-web-access/source-check.ts +11 -47
  115. package/node_modules/pi-web-access/summary-review.ts +7 -3
  116. package/node_modules/pi-web-access/tavily.ts +3 -21
  117. package/node_modules/pi-web-access/tinyfish.ts +5 -29
  118. package/node_modules/pi-web-access/utils.ts +9 -1
  119. package/node_modules/pi-web-access/valyu.ts +5 -28
  120. package/node_modules/pi-web-access/xai-search.ts +1 -15
  121. package/node_modules/pi-web-access/xcrawl.ts +5 -32
  122. package/package.json +17 -11
@@ -0,0 +1,14 @@
1
+ export function normalizeDomain(value: string): string | null {
2
+ let input = value.trim().toLowerCase();
3
+ if (!input) return null;
4
+ if (input.startsWith("-")) input = input.slice(1).trim();
5
+ if (!input) return null;
6
+ try {
7
+ const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
8
+ input = parsed.hostname;
9
+ } catch {
10
+ input = input.split("/")[0]?.split(":")[0] ?? "";
11
+ }
12
+ input = input.replace(/^\.+|\.+$/g, "");
13
+ return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
14
+ }
@@ -1,5 +1,7 @@
1
1
  import { activityMonitor } from "./activity.ts";
2
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
2
3
  import type { SearchOptions, SearchResult, SearchResponse } from "./perplexity.ts";
4
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
3
5
 
4
6
  const SEARCH_URL = "https://html.duckduckgo.com/html/";
5
7
  const SEARCH_TIMEOUT_MS = 30_000;
@@ -9,26 +11,6 @@ interface NormalizedDomainFilters {
9
11
  blocked: string[];
10
12
  }
11
13
 
12
- function normalizeCount(value: number | undefined): number {
13
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
14
- return Math.max(1, Math.min(Math.floor(value), 20));
15
- }
16
-
17
- function normalizeDomain(value: string): string | null {
18
- let input = value.trim().toLowerCase();
19
- if (!input) return null;
20
- if (input.startsWith("-")) input = input.slice(1).trim();
21
- if (!input) return null;
22
- try {
23
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
24
- input = parsed.hostname;
25
- } catch {
26
- input = input.split("/")[0]?.split(":")[0] ?? "";
27
- }
28
- input = input.replace(/^\.+|\.+$/g, "");
29
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
30
- }
31
-
32
14
  function normalizeDomainFilters(domainFilter: string[] | undefined): NormalizedDomainFilters {
33
15
  const filters: NormalizedDomainFilters = { allowed: [], blocked: [] };
34
16
  for (const raw of domainFilter ?? []) {
@@ -103,7 +85,7 @@ export async function searchWithDuckDuckGo(query: string, options: SearchOptions
103
85
  if (!matchesDomainFilters(resultUrl, filters)) continue;
104
86
  const snippet = container.querySelector(".result__snippet")?.textContent?.trim() ?? "";
105
87
  results.push({ title, url: resultUrl, snippet });
106
- if (results.length >= normalizeCount(options.numResults)) break;
88
+ if (results.length >= normalizeSearchResultCount(options.numResults)) break;
107
89
  }
108
90
  if (parseableResults === 0) {
109
91
  throw new Error("DuckDuckGo returned no parseable results (invalid response)");
@@ -1275,7 +1275,9 @@ async function extractViaHttp(
1275
1275
  return {
1276
1276
  url,
1277
1277
  title: result.title,
1278
- content: `PDF extracted and saved to: ${result.outputPath}\n\nPages: ${result.pages}\nCharacters: ${result.chars}`,
1278
+ content: options?.mode === "answer"
1279
+ ? result.content
1280
+ : `PDF extracted and saved to: ${result.outputPath}\n\nPages: ${result.pages}\nCharacters: ${result.chars}`,
1279
1281
  error: null,
1280
1282
  };
1281
1283
  } catch (err) {
@@ -1,9 +1,12 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import net from "node:net";
3
3
  import { activityMonitor } from "./activity.ts";
4
+ import { normalizeDomain } from "./domain-filter-normalization.ts";
4
5
  import { redactCredential, resolveCredential } from "./credential-source.ts";
5
6
  import type { ExtractedContent, ExtractOptions } from "./extract.ts";
6
7
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
8
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
9
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
7
10
  import { loadSsrfConfig, validateRemoteUrl, type Lookup } from "./ssrf-protection.ts";
8
11
  import { getWebSearchConfigPath } from "./utils.ts";
9
12
 
@@ -242,26 +245,6 @@ function scrapeBody(url: string): Record<string, unknown> {
242
245
  };
243
246
  }
244
247
 
245
- function normalizeCount(value: number | undefined): number {
246
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
247
- return Math.max(1, Math.min(Math.floor(value), 20));
248
- }
249
-
250
- function normalizeDomain(value: string): string | null {
251
- let input = value.trim().toLowerCase();
252
- if (!input) return null;
253
- if (input.startsWith("-")) input = input.slice(1).trim();
254
- if (!input) return null;
255
- try {
256
- const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
257
- input = parsed.hostname;
258
- } catch {
259
- input = input.split("/")[0]?.split(":")[0] ?? "";
260
- }
261
- input = input.replace(/^\.+|\.+$/g, "");
262
- return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
263
- }
264
-
265
248
  function parseDomainFilter(domainFilter: string[] | undefined): DomainFilters {
266
249
  const filters: DomainFilters = { include: [], exclude: [] };
267
250
  for (const raw of domainFilter ?? []) {
@@ -321,13 +304,6 @@ function firstString(...values: unknown[]): string | null {
321
304
  return null;
322
305
  }
323
306
 
324
- function buildAnswer(results: SearchResponse["results"]): string {
325
- return results.map((result) => {
326
- if (result.snippet) return `${result.snippet}\nSource: ${result.title} (${result.url})`;
327
- return `Source: ${result.title} (${result.url})`;
328
- }).join("\n\n");
329
- }
330
-
331
307
  function mapSearchResults(data: unknown, numResults: number, filters: DomainFilters): {
332
308
  results: SearchResponse["results"];
333
309
  inlineContent: ExtractedContent[];
@@ -413,7 +389,7 @@ export function isFirecrawlAvailable(): boolean {
413
389
 
414
390
  export async function searchWithFirecrawl(query: string, options: FirecrawlSearchOptions = {}): Promise<SearchResponse> {
415
391
  requireBaseUrl();
416
- const numResults = normalizeCount(options.numResults);
392
+ const numResults = normalizeSearchResultCount(options.numResults);
417
393
  const filters = parseDomainFilter(options.domainFilter);
418
394
  const envelope = await firecrawlFetch(
419
395
  "search",
@@ -424,7 +400,7 @@ export async function searchWithFirecrawl(query: string, options: FirecrawlSearc
424
400
  { type: "api", query },
425
401
  );
426
402
  const mapped = mapSearchResults(envelope.data, numResults, filters);
427
- const response: SearchResponse = { answer: buildAnswer(mapped.results), results: mapped.results };
403
+ const response: SearchResponse = { answer: formatSearchResultsAsAnswer(mapped.results), results: mapped.results };
428
404
  if (options.includeContent && mapped.inlineContent.length > 0) response.inlineContent = mapped.inlineContent;
429
405
  return response;
430
406
  }
@@ -11,6 +11,7 @@ import { isBraveAvailable, searchWithBrave } from "./brave.ts";
11
11
  import {
12
12
  isCurrentModelHostedSearchEligible,
13
13
  isOpenAISearchAvailable,
14
+ OpenAIAlphaSearchUnsupportedError,
14
15
  searchWithCurrentModelOpenAI,
15
16
  searchWithOpenAI,
16
17
  } from "./openai-search.ts";
@@ -36,12 +37,13 @@ import { isBrightDataAvailable, searchWithBrightData } from "./brightdata.ts";
36
37
  import { isSerpBaseAvailable, searchWithSerpBase } from "./serpbase.ts";
37
38
  import { isSerpApiAvailable, searchWithSerpApi } from "./serpapi.ts";
38
39
  import { isSerperAvailable, searchWithSerper } from "./serper.ts";
40
+ import { isSerplyAvailable, searchWithSerply } from "./serply.ts";
39
41
  import { isValyuAvailable, searchWithValyu } from "./valyu.ts";
40
42
  import { isKimiSearchAvailable, searchWithKimi } from "./kimi-search.ts";
41
43
  import { isMistralAvailable, searchWithMistral } from "./mistral-search.ts";
42
44
  import { getWebSearchConfigPath } from "./utils.ts";
43
45
 
44
- export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "valyu", "bocha", "xcrawl"] as const;
46
+ export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "bocha", "xcrawl"] as const;
45
47
  export const SEARCH_PROVIDERS = ["auto", "all", ...RESOLVED_SEARCH_PROVIDERS] as const;
46
48
 
47
49
  export type ResolvedSearchProvider = typeof RESOLVED_SEARCH_PROVIDERS[number];
@@ -106,7 +108,7 @@ export interface AttributedSearchResponse extends SearchResponse {
106
108
 
107
109
  const CONFIG_PATH = getWebSearchConfigPath();
108
110
  const DEFAULT_SEARCH_MODEL = "gemini-3.6-flash";
109
- // Explicit-only providers (Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Valyu) are deliberately absent:
111
+ // Explicit-only providers (Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, Valyu) are deliberately absent:
110
112
  // `all` must never fan out to an opt-in or paid provider without the user asking for it.
111
113
  export const ALL_SEARCH_PROVIDERS: ResolvedSearchProvider[] = ["searxng", "openai", "exa", "brave", "parallel", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "serpdive", "kagi", "ollama", "perplexity", "gemini", "bocha"];
112
114
  const VALID_ROUTING_KINDS = ["transient", "quota", "network", "invalid-response", "unsupported"] as const;
@@ -116,6 +118,7 @@ type SearchConfig = {
116
118
  searchProviderConfigured: boolean;
117
119
  searchRouting?: SearchRoutingConfig;
118
120
  searchModel?: string;
121
+ allowedProviders?: ResolvedSearchProvider[];
119
122
  };
120
123
 
121
124
  let cachedSearchConfig: SearchConfig | null = null;
@@ -141,12 +144,31 @@ function getSearchConfig(): SearchConfig {
141
144
  }
142
145
 
143
146
  const searchModel = normalizeSearchModel(raw.searchModel);
147
+ const webSearch = raw.webSearch;
148
+ if (webSearch !== undefined && (!webSearch || typeof webSearch !== "object" || Array.isArray(webSearch))) {
149
+ throw new Error(`webSearch in ${CONFIG_PATH} must be an object`);
150
+ }
151
+ const allowedProviders = webSearch && Object.hasOwn(webSearch, "allowedProviders")
152
+ ? normalizeResolvedProviderList((webSearch as Record<string, unknown>).allowedProviders, `webSearch.allowedProviders in ${CONFIG_PATH}`)
153
+ : undefined;
144
154
  const searchProviderConfigured = Object.hasOwn(raw, "searchProvider") || Object.hasOwn(raw, "provider");
155
+ const searchProvider = normalizeSearchProviderSelection(raw.searchProvider ?? raw.provider, `provider in ${CONFIG_PATH}`);
156
+ const searchRouting = Object.hasOwn(raw, "searchRouting") ? normalizeSearchRouting(raw.searchRouting) : undefined;
157
+ if (allowedProviders) {
158
+ if (Object.hasOwn(raw, "searchProvider")) {
159
+ assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.searchProvider), `searchProvider in ${CONFIG_PATH}`, allowedProviders);
160
+ }
161
+ if (Object.hasOwn(raw, "provider")) {
162
+ assertSearchProviderSelectionAllowed(normalizeSearchProviderSelection(raw.provider), `provider in ${CONFIG_PATH}`, allowedProviders);
163
+ }
164
+ if (searchRouting) assertSearchProviderSelectionAllowed(searchRouting.providers, `searchRouting.providers in ${CONFIG_PATH}`, allowedProviders);
165
+ }
145
166
  cachedSearchConfig = {
146
- searchProvider: normalizeSearchProviderSelection(raw.searchProvider ?? raw.provider, `provider in ${CONFIG_PATH}`),
167
+ searchProvider,
147
168
  searchProviderConfigured,
148
- ...(Object.hasOwn(raw, "searchRouting") ? { searchRouting: normalizeSearchRouting(raw.searchRouting) } : {}),
169
+ ...(searchRouting ? { searchRouting } : {}),
149
170
  ...(searchModel ? { searchModel } : {}),
171
+ ...(allowedProviders ? { allowedProviders } : {}),
150
172
  };
151
173
  return cachedSearchConfig;
152
174
  }
@@ -209,6 +231,25 @@ function normalizeResolvedProviderList(value: unknown, label: string): ResolvedS
209
231
  return providers;
210
232
  }
211
233
 
234
+ export function assertSearchProviderSelectionAllowed(
235
+ selection: SearchProviderSelection,
236
+ label = "provider",
237
+ allowedProviders = getSearchConfig().allowedProviders,
238
+ ): void {
239
+ if (!allowedProviders) return;
240
+ const requested = Array.isArray(selection)
241
+ ? selection
242
+ : selection === "auto" || selection === "all" ? [] : [selection];
243
+ const disabled = requested.filter(provider => !allowedProviders.includes(provider));
244
+ if (disabled.length > 0) {
245
+ throw new Error(`${label} ${disabled.length === 1 ? `references disabled provider "${disabled[0]}"` : `references disabled providers: ${disabled.join(", ")}`}; allowed by webSearch.allowedProviders: ${allowedProviders.join(", ")}`);
246
+ }
247
+ }
248
+
249
+ export function getAllowedSearchProviders(): readonly ResolvedSearchProvider[] {
250
+ return getSearchConfig().allowedProviders ?? RESOLVED_SEARCH_PROVIDERS;
251
+ }
252
+
212
253
  export function normalizeSearchProviderSelection(value: unknown, label = "provider"): SearchProviderSelection {
213
254
  if (Array.isArray(value)) return normalizeResolvedProviderList(value, label);
214
255
  const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
@@ -304,6 +345,8 @@ function classifyProviderError(provider: ResolvedSearchProvider, err: unknown):
304
345
  kind = "credential";
305
346
  } else if (isAbortError(err)) {
306
347
  kind = "aborted";
348
+ } else if (err instanceof OpenAIAlphaSearchUnsupportedError) {
349
+ kind = "unsupported";
307
350
  } else if (provider === "xai" && status === 403 && /spending[- ]limit|(?:no|out of) credits?|insufficient quota|quota (?:exceeded|exhausted)|credits? (?:exhausted|depleted|used up)/.test(lower)) {
308
351
  kind = "quota";
309
352
  } else if (status === 401 || status === 403) {
@@ -339,7 +382,7 @@ async function searchWithResolvedProvider(
339
382
  query: string,
340
383
  options: FullSearchOptions,
341
384
  useCurrentModel = false,
342
- ): Promise<AttributedSearchResponse> {
385
+ ): Promise<ProviderSearchResponse> {
343
386
  if (provider === "openai") {
344
387
  const result = useCurrentModel
345
388
  ? await searchWithCurrentModelOpenAI(query, options, options.extensionContext)
@@ -367,6 +410,7 @@ async function searchWithResolvedProvider(
367
410
  if (provider === "serpbase") return { ...(await searchWithSerpBase(query, options)), provider };
368
411
  if (provider === "serpapi") return { ...(await searchWithSerpApi(query, options)), provider };
369
412
  if (provider === "serper") return { ...(await searchWithSerper(query, options)), provider };
413
+ if (provider === "serply") return { ...(await searchWithSerply(query, options)), provider };
370
414
  if (provider === "valyu") return { ...(await searchWithValyu(query, options)), provider };
371
415
  if (provider === "xcrawl") return { ...(await searchWithXCrawl(query, options)), provider };
372
416
  if (provider === "perplexity") return { ...(await searchWithPerplexity(query, options)), provider };
@@ -416,6 +460,7 @@ async function isResolvedProviderAvailable(provider: ResolvedSearchProvider, opt
416
460
  if (provider === "serpbase") return isSerpBaseAvailable();
417
461
  if (provider === "serpapi") return isSerpApiAvailable();
418
462
  if (provider === "serper") return isSerperAvailable();
463
+ if (provider === "serply") return isSerplyAvailable();
419
464
  if (provider === "valyu") return isValyuAvailable();
420
465
  if (provider === "xcrawl") return isXcrawlAvailable();
421
466
  if (provider === "perplexity") return isPerplexityAvailable();
@@ -434,7 +479,7 @@ async function isGeminiWebOptionallyAvailable(): Promise<boolean> {
434
479
  }
435
480
  }
436
481
 
437
- function providerLabel(provider: ResolvedSearchProvider): string {
482
+ export function providerLabel(provider: ResolvedSearchProvider): string {
438
483
  if (provider === "openai") return "OpenAI";
439
484
  if (provider === "parallel-mcp") return "Parallel MCP";
440
485
  if (provider === "tinyfish") return "TinyFish";
@@ -456,6 +501,7 @@ function providerLabel(provider: ResolvedSearchProvider): string {
456
501
  if (provider === "serpbase") return "SerpBase";
457
502
  if (provider === "serpapi") return "SerpApi";
458
503
  if (provider === "serper") return "Serper";
504
+ if (provider === "serply") return "Serply";
459
505
  if (provider === "valyu") return "Valyu";
460
506
  return provider.charAt(0).toUpperCase() + provider.slice(1);
461
507
  }
@@ -464,7 +510,7 @@ async function searchWithAllProvider(
464
510
  provider: ResolvedSearchProvider,
465
511
  query: string,
466
512
  options: FullSearchOptions,
467
- ): Promise<AttributedSearchResponse> {
513
+ ): Promise<ProviderSearchResponse> {
468
514
  if (provider !== "gemini") return searchWithResolvedProvider(provider, query, options);
469
515
  const result = await searchWithGeminiApi(query, options);
470
516
  if (result) return { ...result, provider };
@@ -476,14 +522,15 @@ async function searchWithProviders(
476
522
  options: FullSearchOptions,
477
523
  selectedProviders?: ResolvedSearchProvider[],
478
524
  ): Promise<AttributedSearchResponse> {
479
- const providers = selectedProviders ?? (await Promise.all(ALL_SEARCH_PROVIDERS.map(async (provider) => ({
525
+ const allowed = getAllowedSearchProviders();
526
+ const providers = selectedProviders ?? (await Promise.all(ALL_SEARCH_PROVIDERS.filter(provider => allowed.includes(provider)).map(async (provider) => ({
480
527
  provider,
481
528
  available: provider === "gemini"
482
529
  ? isGeminiApiAvailable()
483
530
  : await isResolvedProviderAvailable(provider, options),
484
531
  })))).filter((entry) => entry.available).map((entry) => entry.provider);
485
532
  if (providers.length === 0) {
486
- throw new Error("No configured search provider available for provider \"all\". Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Valyu, and XCrawl are excluded.");
533
+ throw new Error("No configured search provider available for provider \"all\". Parallel MCP, DuckDuckGo, Kimi, AnySearch, xAI, Mistral, Bright Data, SerpBase, SerpApi, Serper, Serply, Valyu, and XCrawl are excluded.");
487
534
  }
488
535
 
489
536
  const settled = await Promise.allSettled(
@@ -493,7 +540,7 @@ async function searchWithProviders(
493
540
  );
494
541
  if (options.signal?.aborted) throw new Error("Aborted");
495
542
 
496
- const successes: AttributedSearchResponse[] = [];
543
+ const successes: ProviderSearchResponse[] = [];
497
544
  const failures: Array<{ provider: ResolvedSearchProvider; error: string }> = [];
498
545
  for (let index = 0; index < settled.length; index++) {
499
546
  const outcome = settled[index];
@@ -526,7 +573,7 @@ async function searchWithProviders(
526
573
  }
527
574
 
528
575
  const answerSections = successes.map((response) =>
529
- `## ${providerLabel(response.provider as ResolvedSearchProvider)}\n\n${response.answer || "(No answer text returned.)"}`
576
+ `## ${providerLabel(response.provider)}\n\n${response.answer || "(No answer text returned.)"}`
530
577
  );
531
578
  if (failures.length > 0) {
532
579
  answerSections.push(
@@ -538,7 +585,7 @@ async function searchWithProviders(
538
585
  provider: "all",
539
586
  answer: answerSections.join("\n\n"),
540
587
  results,
541
- providerResponses: successes as ProviderSearchResponse[],
588
+ providerResponses: successes,
542
589
  ...(failures.length > 0 ? { providerErrors: failures } : {}),
543
590
  ...(inlineContent.length > 0 ? { inlineContent } : {}),
544
591
  };
@@ -574,6 +621,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
574
621
  const provider = options.provider === undefined || options.provider === "auto"
575
622
  ? config.searchProvider
576
623
  : options.provider;
624
+ assertSearchProviderSelectionAllowed(provider, "Requested provider");
577
625
  if (Array.isArray(provider)) {
578
626
  return searchWithProviders(query, options, normalizeResolvedProviderList(provider, "provider"));
579
627
  }
@@ -584,8 +632,9 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
584
632
  }
585
633
 
586
634
  const fallbackErrors: string[] = [];
635
+ const allowed = new Set(config.allowedProviders ?? RESOLVED_SEARCH_PROVIDERS);
587
636
 
588
- if (isSearXNGAvailable()) {
637
+ if (allowed.has("searxng") && isSearXNGAvailable()) {
589
638
  try {
590
639
  const result = await searchWithSearXNG(query, options);
591
640
  return { ...result, provider: "searxng" };
@@ -596,13 +645,13 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
596
645
  }
597
646
 
598
647
  let triedOpenAI = false;
599
- if (!options.extensionContext || isOpenAICodexSelected(options.extensionContext)) {
648
+ if (allowed.has("openai") && (!options.extensionContext || isOpenAICodexSelected(options.extensionContext))) {
600
649
  triedOpenAI = true;
601
650
  const result = await tryOpenAIInAuto(query, options, fallbackErrors);
602
651
  if (result) return result;
603
652
  }
604
653
 
605
- if (isExaAvailable()) {
654
+ if (allowed.has("exa") && isExaAvailable()) {
606
655
  try {
607
656
  const result = await searchWithExa(query, options);
608
657
  if (result) return { ...result, provider: "exa" };
@@ -612,12 +661,12 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
612
661
  }
613
662
  }
614
663
 
615
- if (!triedOpenAI) {
664
+ if (allowed.has("openai") && !triedOpenAI) {
616
665
  const result = await tryOpenAIInAuto(query, options, fallbackErrors);
617
666
  if (result) return result;
618
667
  }
619
668
 
620
- if (isBraveAvailable()) {
669
+ if (allowed.has("brave") && isBraveAvailable()) {
621
670
  try {
622
671
  const result = await searchWithBrave(query, options);
623
672
  return { ...result, provider: "brave" };
@@ -627,7 +676,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
627
676
  }
628
677
  }
629
678
 
630
- if (isParallelAvailable()) {
679
+ if (allowed.has("parallel") && isParallelAvailable()) {
631
680
  try {
632
681
  const result = await searchWithParallel(query, options);
633
682
  return { ...result, provider: "parallel" };
@@ -637,7 +686,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
637
686
  }
638
687
  }
639
688
 
640
- if (isTinyFishAvailable()) {
689
+ if (allowed.has("tinyfish") && isTinyFishAvailable()) {
641
690
  try {
642
691
  const result = await searchWithTinyFish(query, options);
643
692
  return { ...result, provider: "tinyfish" };
@@ -647,7 +696,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
647
696
  }
648
697
  }
649
698
 
650
- if (isSearch1APIAvailable()) {
699
+ if (allowed.has("search1api") && isSearch1APIAvailable()) {
651
700
  try {
652
701
  const result = await searchWithSearch1API(query, options);
653
702
  return { ...result, provider: "search1api" };
@@ -657,7 +706,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
657
706
  }
658
707
  }
659
708
 
660
- if (isSearchinfinityAvailable()) {
709
+ if (allowed.has("searchinfinity") && isSearchinfinityAvailable()) {
661
710
  try {
662
711
  const result = await searchWithSearchinfinity(query, options);
663
712
  return { ...result, provider: "searchinfinity" };
@@ -667,7 +716,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
667
716
  }
668
717
  }
669
718
 
670
- if (isQueritAvailable()) {
719
+ if (allowed.has("querit") && isQueritAvailable()) {
671
720
  try {
672
721
  const result = await searchWithQuerit(query, options);
673
722
  return { ...result, provider: "querit" };
@@ -677,7 +726,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
677
726
  }
678
727
  }
679
728
 
680
- if (isTavilyAvailable()) {
729
+ if (allowed.has("tavily") && isTavilyAvailable()) {
681
730
  try {
682
731
  const result = await searchWithTavily(query, options);
683
732
  return { ...result, provider: "tavily" };
@@ -687,7 +736,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
687
736
  }
688
737
  }
689
738
 
690
- if (isFirecrawlAvailable()) {
739
+ if (allowed.has("firecrawl") && isFirecrawlAvailable()) {
691
740
  try {
692
741
  const result = await searchWithFirecrawl(query, options);
693
742
  return { ...result, provider: "firecrawl" };
@@ -697,7 +746,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
697
746
  }
698
747
  }
699
748
 
700
- if (isJinaSearchAvailable()) {
749
+ if (allowed.has("jina") && isJinaSearchAvailable()) {
701
750
  try {
702
751
  const result = await searchWithJina(query, options);
703
752
  return { ...result, provider: "jina" };
@@ -707,7 +756,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
707
756
  }
708
757
  }
709
758
 
710
- if (isSerpdiveAvailable()) {
759
+ if (allowed.has("serpdive") && isSerpdiveAvailable()) {
711
760
  try {
712
761
  const result = await searchWithSerpdive(query, options);
713
762
  return { ...result, provider: "serpdive" };
@@ -717,7 +766,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
717
766
  }
718
767
  }
719
768
 
720
- if (isKagiAvailable()) {
769
+ if (allowed.has("kagi") && isKagiAvailable()) {
721
770
  try {
722
771
  const result = await searchWithKagi(query, options);
723
772
  return { ...result, provider: "kagi" };
@@ -727,7 +776,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
727
776
  }
728
777
  }
729
778
 
730
- if (isBochaAvailable()) {
779
+ if (allowed.has("bocha") && isBochaAvailable()) {
731
780
  try {
732
781
  const result = await searchWithBocha(query, options);
733
782
  return { ...result, provider: "bocha" };
@@ -737,7 +786,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
737
786
  }
738
787
  }
739
788
 
740
- if (isOllamaAvailable()) {
789
+ if (allowed.has("ollama") && isOllamaAvailable()) {
741
790
  try {
742
791
  const result = await searchWithOllama(query, options);
743
792
  return { ...result, provider: "ollama" };
@@ -747,7 +796,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
747
796
  }
748
797
  }
749
798
 
750
- if (isPerplexityAvailable()) {
799
+ if (allowed.has("perplexity") && isPerplexityAvailable()) {
751
800
  try {
752
801
  const result = await searchWithPerplexity(query, options);
753
802
  return { ...result, provider: "perplexity" };
@@ -757,7 +806,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
757
806
  }
758
807
  }
759
808
 
760
- try {
809
+ if (allowed.has("gemini")) try {
761
810
  const geminiResult = await searchWithGemini(query, options, false);
762
811
  if (geminiResult) return { ...geminiResult, provider: "gemini" };
763
812
  } catch (err) {
@@ -776,7 +825,7 @@ export async function search(query: string, options: FullSearchOptions = {}): Pr
776
825
  " 3. Set OPENAI_API_KEY, BRAVE_API_KEY, PARALLEL_API_KEY, TINYFISH_API_KEY, SEARCH1API_KEY, SEARCHINFINITY_API_KEY, QUERIT_API_KEY, TAVILY_API_KEY, FIRECRAWL_BASE_URL, JINA_API_KEY, SERPDIVE_API_KEY, KAGI_API_KEY, BOCHA_API_KEY, OLLAMA_API_KEY, SEARXNG_BASE_URL, EXA_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY, or CLOUDFLARE_API_KEY env vars\n" +
777
826
  " 4. Set GOOGLE_GEMINI_BASE_URL with CLOUDFLARE_API_KEY for Cloudflare AI Gateway routing\n" +
778
827
  " 5. Sign into gemini.google.com in a supported Chromium-based browser\n" +
779
- " 6. Explicitly select provider: \"anysearch\" for anonymous AnySearch, \"xcrawl\" for XCrawl, \"xai\" for Grok, \"mistral\" for Mistral Conversations web search, \"brightdata\" with brightdataSerpZone for paid Bright Data SERP, \"serpbase\", \"serpapi\", or \"serper\" for Google SERP, or \"valyu\" for research search"
828
+ " 6. Explicitly select provider: \"anysearch\" for anonymous AnySearch, \"xcrawl\" for XCrawl, \"xai\" for Grok, \"mistral\" for Mistral Conversations web search, \"brightdata\" with brightdataSerpZone for paid Bright Data SERP, \"serpbase\", \"serpapi\", \"serper\", or \"serply\" for Google SERP, or \"valyu\" for research search"
780
829
  );
781
830
  }
782
831