@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
@@ -10,10 +10,10 @@ import { findContent, type FindMode } from "./content-find.ts";
10
10
  import { answerFromPage } from "./page-query.ts";
11
11
  import { rewriteSearchQuery } from "./query-rewrite.ts";
12
12
  import { clearCloneCache } from "./github-extract.ts";
13
- import { ALL_SEARCH_PROVIDERS, getConfiguredSearchRouting, normalizeSearchProviderSelection, RESOLVED_SEARCH_PROVIDERS, SEARCH_PROVIDERS, search, type AttributedSearchResponse, type ProviderAvailability, type SearchProvider, type SearchProviderSelection, type ResolvedSearchProvider } from "./gemini-search.ts";
13
+ import { ALL_SEARCH_PROVIDERS, assertSearchProviderSelectionAllowed, getAllowedSearchProviders, getConfiguredSearchRouting, normalizeSearchProviderSelection, providerLabel, RESOLVED_SEARCH_PROVIDERS, search, type AttributedSearchResponse, type ProviderAvailability, type SearchProvider, type SearchProviderSelection, type ResolvedSearchProvider } from "./gemini-search.ts";
14
14
  export type { ProviderAvailability } from "./gemini-search.ts";
15
15
  import type { SearchResult } from "./perplexity.ts";
16
- import { formatSeconds, getWebSearchConfigDir, getWebSearchConfigPath, installGlobalProxyFetch, resolveCuratorNetworkConfig, runWithProxy } from "./utils.ts";
16
+ import { formatSeconds, getWebSearchConfigDir, getWebSearchConfigPath, resolveCuratorNetworkConfig, runWithProxy } from "./utils.ts";
17
17
  import {
18
18
  clearResults,
19
19
  deleteResult,
@@ -71,6 +71,7 @@ import { isBrightDataAvailable } from "./brightdata.ts";
71
71
  import { isSerpBaseAvailable } from "./serpbase.ts";
72
72
  import { isSerpApiAvailable } from "./serpapi.ts";
73
73
  import { isSerperAvailable } from "./serper.ts";
74
+ import { isSerplyAvailable } from "./serply.ts";
74
75
  import { isValyuAvailable } from "./valyu.ts";
75
76
  import { isXcrawlAvailable } from "./xcrawl.ts";
76
77
  import { buildSearchErrorPlan, type SearchErrorDetails, type SearchErrorPlan } from "./render-search-error.ts";
@@ -153,6 +154,7 @@ interface WebSearchConfig {
153
154
  serpbaseApiKey?: unknown;
154
155
  serpapiApiKey?: unknown;
155
156
  serperApiKey?: unknown;
157
+ serplyApiKey?: unknown;
156
158
  tinyfishApiKey?: unknown;
157
159
  valyuApiKey?: unknown;
158
160
  xaiApiKey?: unknown;
@@ -165,8 +167,13 @@ interface WebSearchConfig {
165
167
  summaryModel?: string;
166
168
  summaryGenerationDeadlineMs?: unknown;
167
169
  maxInlineContentChars?: unknown;
170
+ fetch?: {
171
+ defaultMode?: unknown;
172
+ allowedModes?: unknown;
173
+ };
168
174
  webSearch?: {
169
175
  enabled?: boolean;
176
+ allowedProviders?: unknown;
170
177
  };
171
178
  tools?: Partial<Record<keyof ToolNames, { enabled?: boolean }>>;
172
179
  commands?: Partial<Record<"websearch" | "curator" | "search" | "google-account", { enabled?: boolean }>>;
@@ -184,12 +191,12 @@ interface WebSearchConfig {
184
191
  }
185
192
 
186
193
  type CuratorWorkflow = "summary-review";
187
- export type CuratorProvider = Exclude<SearchProvider, "auto">;
194
+ export type CuratorProvider = SearchProvider;
188
195
  type SummaryWorkflow = "summary-review" | "auto-summary";
189
196
 
190
197
  interface CuratorBootstrap {
191
198
  availableProviders: ProviderAvailability;
192
- defaultProvider: CuratorProvider;
199
+ defaultProvider: SearchProvider;
193
200
  timeoutSeconds: number;
194
201
  }
195
202
 
@@ -244,6 +251,30 @@ const DEFAULT_REMOTE_CURATOR_TIMEOUT_SECONDS = 60;
244
251
  const MAX_CURATOR_TIMEOUT_SECONDS = 600;
245
252
  const MAX_SUMMARY_GENERATION_DEADLINE_MS = 600_000;
246
253
  const SEARCH_QUERY_CONCURRENCY = 3;
254
+ const FETCH_MODES = ["readable", "raw", "answer"] as const;
255
+ type FetchMode = typeof FETCH_MODES[number];
256
+ const FETCH_MODE_DESCRIPTIONS: Record<FetchMode, string> = {
257
+ readable: "extract readable content as markdown",
258
+ raw: "return the exact textual body using direct HTTP only",
259
+ answer: "answer a prompt using only fetched content",
260
+ };
261
+
262
+ function resolveFetchModeConfig(config: WebSearchConfig): { defaultMode: FetchMode; allowedModes: FetchMode[] } {
263
+ const configuredModes = config.fetch?.allowedModes ?? FETCH_MODES;
264
+ if (!Array.isArray(configuredModes) || configuredModes.length === 0 || configuredModes.some(mode => !FETCH_MODES.includes(mode as FetchMode))) {
265
+ throw new Error(`fetch.allowedModes in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array containing only "readable", "raw", or "answer"`);
266
+ }
267
+ const allowedModes = configuredModes as FetchMode[];
268
+ const duplicateMode = allowedModes.find((mode, index) => allowedModes.indexOf(mode) !== index);
269
+ if (duplicateMode) {
270
+ throw new Error(`fetch.allowedModes in ${WEB_SEARCH_CONFIG_PATH} must not contain duplicates: "${duplicateMode}"`);
271
+ }
272
+ const defaultMode = config.fetch?.defaultMode ?? "readable";
273
+ if (!allowedModes.includes(defaultMode as FetchMode)) {
274
+ throw new Error(`fetch.defaultMode in ${WEB_SEARCH_CONFIG_PATH} must be one of fetch.allowedModes`);
275
+ }
276
+ return { defaultMode: defaultMode as FetchMode, allowedModes };
277
+ }
247
278
 
248
279
  // Limit each batch independently so separate Pi tool calls can still run in parallel.
249
280
  function runSearchQueries<T>(queries: string[], run: (query: string, index: number) => Promise<T>): Promise<T[]> {
@@ -261,10 +292,10 @@ function curatorResultIndexCapacity(queryCount: number): number {
261
292
  return queryCount * RESOLVED_SEARCH_PROVIDERS.length;
262
293
  }
263
294
 
264
- function searchProviderSchema(description: string) {
295
+ function searchProviderSchema(description: string, allowedProviders: readonly ResolvedSearchProvider[]) {
265
296
  return Type.Union([
266
- StringEnum([...SEARCH_PROVIDERS]),
267
- Type.Array(StringEnum([...RESOLVED_SEARCH_PROVIDERS]), { minItems: 1 }),
297
+ StringEnum(["auto", "all", ...allowedProviders]),
298
+ Type.Array(StringEnum([...allowedProviders]), { minItems: 1 }),
268
299
  ], { description });
269
300
  }
270
301
 
@@ -329,12 +360,17 @@ function normalizeProviderInput(value: unknown, label = "provider"): SearchProvi
329
360
 
330
361
  function resolveRequestedProvider(requested: unknown): SearchProviderSelection {
331
362
  const normalizedRequested = normalizeProviderInput(requested);
332
- if (normalizedRequested && normalizedRequested !== "auto") return normalizedRequested;
363
+ if (normalizedRequested && normalizedRequested !== "auto") {
364
+ assertSearchProviderSelectionAllowed(normalizedRequested, "Requested provider");
365
+ return normalizedRequested;
366
+ }
333
367
  const config = loadConfig();
334
- return normalizeProviderInput(config.searchProvider ?? config.provider, `provider in ${WEB_SEARCH_CONFIG_PATH}`) ?? "auto";
368
+ const provider = normalizeProviderInput(config.searchProvider ?? config.provider, `provider in ${WEB_SEARCH_CONFIG_PATH}`) ?? "auto";
369
+ assertSearchProviderSelectionAllowed(provider, `configured provider in ${WEB_SEARCH_CONFIG_PATH}`);
370
+ return provider;
335
371
  }
336
372
 
337
- function toCuratorProvider(provider: SearchProviderSelection): CuratorProvider | undefined {
373
+ function toCuratorProvider(provider: SearchProviderSelection): SearchProvider | undefined {
338
374
  if (Array.isArray(provider)) return "all";
339
375
  return provider === "auto" ? undefined : provider;
340
376
  }
@@ -342,8 +378,7 @@ function toCuratorProvider(provider: SearchProviderSelection): CuratorProvider |
342
378
  function resolveCuratorSearchProvider(requested: unknown, current: SearchProviderSelection): SearchProviderSelection {
343
379
  const normalized = normalizeProviderInput(requested);
344
380
  if (!normalized || normalized === "auto") return current;
345
- if (normalized === "all" && Array.isArray(current)) return current;
346
- return normalized;
381
+ return normalized === "all" && Array.isArray(current) ? current : normalized;
347
382
  }
348
383
 
349
384
  function normalizeRecencyFilter(value: unknown): RecencyFilter | undefined {
@@ -419,43 +454,44 @@ function shouldAutoOpenCuratorBrowser(config: WebSearchConfig): boolean {
419
454
  }
420
455
 
421
456
  async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderAvailability> {
422
- const geminiWebAvail = await getOptionalGeminiWebAvailability();
423
- const geminiApiAvail = isGeminiApiAvailable();
457
+ const allowedProviders = new Set(getAllowedSearchProviders());
458
+ const geminiWebAvail = allowedProviders.has("gemini") ? await getOptionalGeminiWebAvailability() : null;
459
+ const geminiApiAvail = allowedProviders.has("gemini") && isGeminiApiAvailable();
424
460
  const providers = {
425
- openai: await isOpenAISearchAvailable(ctx),
426
- brave: isBraveAvailable(),
427
- parallel: isParallelAvailable(),
428
- "parallel-mcp": isParallelMcpAvailable(),
429
- tinyfish: isTinyFishAvailable(),
430
- search1api: isSearch1APIAvailable(),
431
- searchinfinity: isSearchinfinityAvailable(),
432
- querit: isQueritAvailable(),
433
- tavily: isTavilyAvailable(),
434
- firecrawl: isFirecrawlAvailable(),
435
- jina: isJinaSearchAvailable(),
436
- serpdive: isSerpdiveAvailable(),
437
- kagi: isKagiAvailable(),
438
- bocha: isBochaAvailable(),
439
- ollama: isOllamaAvailable(),
440
- searxng: isSearXNGAvailable(),
441
- duckduckgo: isDuckDuckGoAvailable(),
442
- perplexity: isPerplexityAvailable(),
443
- exa: isExaAvailable(),
461
+ openai: allowedProviders.has("openai") && await isOpenAISearchAvailable(ctx),
462
+ brave: allowedProviders.has("brave") && isBraveAvailable(),
463
+ parallel: allowedProviders.has("parallel") && isParallelAvailable(),
464
+ "parallel-mcp": allowedProviders.has("parallel-mcp") && isParallelMcpAvailable(),
465
+ tinyfish: allowedProviders.has("tinyfish") && isTinyFishAvailable(),
466
+ search1api: allowedProviders.has("search1api") && isSearch1APIAvailable(),
467
+ searchinfinity: allowedProviders.has("searchinfinity") && isSearchinfinityAvailable(),
468
+ querit: allowedProviders.has("querit") && isQueritAvailable(),
469
+ tavily: allowedProviders.has("tavily") && isTavilyAvailable(),
470
+ firecrawl: allowedProviders.has("firecrawl") && isFirecrawlAvailable(),
471
+ jina: allowedProviders.has("jina") && isJinaSearchAvailable(),
472
+ serpdive: allowedProviders.has("serpdive") && isSerpdiveAvailable(),
473
+ kagi: allowedProviders.has("kagi") && isKagiAvailable(),
474
+ bocha: allowedProviders.has("bocha") && isBochaAvailable(),
475
+ ollama: allowedProviders.has("ollama") && isOllamaAvailable(),
476
+ searxng: allowedProviders.has("searxng") && isSearXNGAvailable(),
477
+ duckduckgo: allowedProviders.has("duckduckgo") && isDuckDuckGoAvailable(),
478
+ perplexity: allowedProviders.has("perplexity") && isPerplexityAvailable(),
479
+ exa: allowedProviders.has("exa") && isExaAvailable(),
444
480
  gemini: geminiApiAvail || !!geminiWebAvail,
445
- kimi: await isKimiSearchAvailable(ctx),
446
- anysearch: isAnySearchAvailable(),
447
- xcrawl: isXcrawlAvailable(),
448
- xai: await isXaiSearchAvailable(ctx),
449
- mistral: isMistralAvailable(),
450
- brightdata: isBrightDataAvailable(),
451
- serpbase: isSerpBaseAvailable(),
452
- serpapi: isSerpApiAvailable(),
453
- serper: isSerperAvailable(),
454
- valyu: isValyuAvailable(),
481
+ kimi: allowedProviders.has("kimi") && await isKimiSearchAvailable(ctx),
482
+ anysearch: allowedProviders.has("anysearch") && isAnySearchAvailable(),
483
+ xcrawl: allowedProviders.has("xcrawl") && isXcrawlAvailable(),
484
+ xai: allowedProviders.has("xai") && await isXaiSearchAvailable(ctx),
485
+ mistral: allowedProviders.has("mistral") && isMistralAvailable(),
486
+ brightdata: allowedProviders.has("brightdata") && isBrightDataAvailable(),
487
+ serpbase: allowedProviders.has("serpbase") && isSerpBaseAvailable(),
488
+ serpapi: allowedProviders.has("serpapi") && isSerpApiAvailable(),
489
+ serper: allowedProviders.has("serper") && isSerperAvailable(),
490
+ serply: allowedProviders.has("serply") && isSerplyAvailable(),
491
+ valyu: allowedProviders.has("valyu") && isValyuAvailable(),
455
492
  };
456
- const allSearchProviders = new Set<ResolvedSearchProvider>(ALL_SEARCH_PROVIDERS);
457
493
  return {
458
- all: Object.entries(providers).some(([provider, available]) => provider !== "gemini" && allSearchProviders.has(provider as ResolvedSearchProvider) && available) || geminiApiAvail,
494
+ all: ALL_SEARCH_PROVIDERS.some(provider => provider === "gemini" ? geminiApiAvail : providers[provider]),
459
495
  ...providers,
460
496
  };
461
497
  }
@@ -500,31 +536,20 @@ export function resolveCuratorDefaultProvider(
500
536
  available: ProviderAvailability,
501
537
  ctx?: Pick<ExtensionContext, "model">,
502
538
  options?: Pick<PendingCurate, "numResults" | "recencyFilter">,
503
- ): CuratorProvider {
539
+ ): SearchProvider {
504
540
  return resolveProvider(provider, available, options, shouldUseOpenAICodexDefault(ctx), ctx);
505
541
  }
506
542
 
507
- function firstAvailableProvider(available: ProviderAvailability, preferOpenAI: boolean, fallback: ResolvedSearchProvider): ResolvedSearchProvider {
543
+ function firstAvailableProvider(available: ProviderAvailability, preferOpenAI: boolean, fallback: ResolvedSearchProvider): ResolvedSearchProvider | "auto" {
508
544
  if (available.searxng) return "searxng";
509
545
  if (preferOpenAI && available.openai) return "openai";
510
546
  if (available.exa) return "exa";
511
- if (available.openai) return "openai";
512
- if (available.brave) return "brave";
513
- if (available.parallel) return "parallel";
514
- if (available.tinyfish) return "tinyfish";
515
- if (available.search1api) return "search1api";
516
- if (available.searchinfinity) return "searchinfinity";
517
- if (available.querit) return "querit";
518
- if (available.tavily) return "tavily";
519
- if (available.firecrawl) return "firecrawl";
520
- if (available.jina) return "jina";
521
- if (available.serpdive) return "serpdive";
522
- if (available.kagi) return "kagi";
523
- if (available.bocha) return "bocha";
524
- if (available.ollama) return "ollama";
525
- if (available.perplexity) return "perplexity";
526
- if (available.gemini) return "gemini";
527
- return fallback;
547
+ for (const provider of ALL_SEARCH_PROVIDERS) {
548
+ if (provider === "ollama" && available.bocha) return "bocha";
549
+ if (available[provider]) return provider;
550
+ }
551
+ const allowed = getAllowedSearchProviders();
552
+ return allowed.includes(fallback) ? fallback : "auto";
528
553
  }
529
554
 
530
555
  function resolveProvider(
@@ -533,7 +558,7 @@ function resolveProvider(
533
558
  options?: Pick<PendingCurate, "numResults" | "recencyFilter">,
534
559
  preferOpenAICodexDefault = false,
535
560
  ctx?: Pick<ExtensionContext, "model">,
536
- ): CuratorProvider {
561
+ ): SearchProvider {
537
562
  if (Array.isArray(provider)) return "all";
538
563
  const preferOpenAI = shouldPreferOpenAI(options, preferOpenAICodexDefault);
539
564
 
@@ -548,62 +573,11 @@ function resolveProvider(
548
573
  }
549
574
  return firstAvailableProvider(available, preferOpenAI, "exa");
550
575
  }
551
- if (provider === "all" && !available.all) {
552
- return firstAvailableProvider(available, preferOpenAI, "exa");
553
- }
554
- if (provider === "openai" && !available.openai) {
555
- return firstAvailableProvider(available, false, "openai");
556
- }
557
- if (provider === "brave" && !available.brave) {
558
- return firstAvailableProvider(available, preferOpenAI, "brave");
559
- }
560
- if (provider === "parallel" && !available.parallel) {
561
- return firstAvailableProvider(available, preferOpenAI, "parallel");
562
- }
563
- if (provider === "tinyfish" && !available.tinyfish) {
564
- return firstAvailableProvider(available, preferOpenAI, "tinyfish");
565
- }
566
- if (provider === "search1api" && !available.search1api) {
567
- return firstAvailableProvider(available, preferOpenAI, "search1api");
568
- }
569
- if (provider === "searchinfinity" && !available.searchinfinity) {
570
- return firstAvailableProvider(available, preferOpenAI, "searchinfinity");
571
- }
572
- if (provider === "querit" && !available.querit) {
573
- return firstAvailableProvider(available, preferOpenAI, "querit");
574
- }
575
- if (provider === "tavily" && !available.tavily) {
576
- return firstAvailableProvider(available, preferOpenAI, "tavily");
577
- }
578
- if (provider === "firecrawl" && !available.firecrawl) {
579
- return firstAvailableProvider(available, preferOpenAI, "firecrawl");
580
- }
581
- if (provider === "jina" && !available.jina) {
582
- return firstAvailableProvider(available, preferOpenAI, "jina");
583
- }
584
- if (provider === "serpdive" && !available.serpdive) {
585
- return firstAvailableProvider(available, preferOpenAI, "serpdive");
586
- }
587
- if (provider === "kagi" && !available.kagi) {
588
- return firstAvailableProvider(available, preferOpenAI, "kagi");
589
- }
590
- if (provider === "bocha" && !available.bocha) {
591
- return firstAvailableProvider(available, preferOpenAI, "bocha");
592
- }
593
- if (provider === "ollama" && !available.ollama) {
594
- return firstAvailableProvider(available, preferOpenAI, "ollama");
595
- }
596
- if (provider === "searxng" && !available.searxng) {
597
- return firstAvailableProvider(available, preferOpenAI, "searxng");
598
- }
599
- if (provider === "exa" && !available.exa) {
600
- return firstAvailableProvider(available, preferOpenAI, "exa");
601
- }
602
- if (provider === "perplexity" && !available.perplexity) {
603
- return firstAvailableProvider(available, preferOpenAI, "perplexity");
576
+ if (provider === "all") {
577
+ return available.all ? "all" : firstAvailableProvider(available, preferOpenAI, "exa");
604
578
  }
605
- if (provider === "gemini" && !available.gemini) {
606
- return firstAvailableProvider(available, preferOpenAI, "gemini");
579
+ if (ALL_SEARCH_PROVIDERS.includes(provider) && !available[provider]) {
580
+ return firstAvailableProvider(available, provider === "openai" ? false : preferOpenAI, provider);
607
581
  }
608
582
  return provider;
609
583
  }
@@ -629,7 +603,7 @@ interface PendingCurate {
629
603
  recencyFilter?: "day" | "week" | "month" | "year";
630
604
  domainFilter?: string[];
631
605
  availableProviders: ProviderAvailability;
632
- defaultProvider: CuratorProvider;
606
+ defaultProvider: SearchProvider;
633
607
  searchProvider: SearchProviderSelection;
634
608
  summaryModels: Array<{ value: string; label: string }>;
635
609
  defaultSummaryModel: string | null;
@@ -1063,8 +1037,16 @@ function handleSessionChange(ctx: ExtensionContext): void {
1063
1037
 
1064
1038
  export default function (pi: ExtensionAPI) {
1065
1039
  const initConfig = loadConfigForExtensionInit();
1040
+ const fetchModeConfig = resolveFetchModeConfig(initConfig);
1041
+ const allowedSearchProviders = initConfig.webSearch?.allowedProviders === undefined ? RESOLVED_SEARCH_PROVIDERS : getAllowedSearchProviders();
1042
+ const allEligibleProviders = allowedSearchProviders.filter(provider => ALL_SEARCH_PROVIDERS.includes(provider));
1043
+ const allExcludedProviders = allowedSearchProviders.filter(provider => !ALL_SEARCH_PROVIDERS.includes(provider));
1044
+ const allPolicyDescription = allEligibleProviders.length === 0
1045
+ ? `all has no eligible allowed providers; explicit-only allowed providers (${allExcludedProviders.map(providerLabel).join(", ")}) remain excluded`
1046
+ : allExcludedProviders.length > 0
1047
+ ? `all searches eligible allowed providers (${allEligibleProviders.map(providerLabel).join(", ")}); explicit-only allowed providers (${allExcludedProviders.map(providerLabel).join(", ")}) remain excluded`
1048
+ : `all searches every eligible allowed provider (${allEligibleProviders.map(providerLabel).join(", ")})`;
1066
1049
  const curatorRunState = registerCuratorRunLifecycle(pi);
1067
- installGlobalProxyFetch();
1068
1050
  const toolNames = resolveToolNames(initConfig);
1069
1051
  const webSearchEnabled = isToolEnabled(initConfig, "webSearch");
1070
1052
  const sourceCheckEnabled = isToolEnabled(initConfig, "sourceCheck");
@@ -1087,6 +1069,9 @@ export default function (pi: ExtensionAPI) {
1087
1069
  const fetchContentStorageNote = getSearchContentEnabled
1088
1070
  ? `Full original content is stored for retrieval with ${toolNames.getSearchContent}.`
1089
1071
  : "Full original content is stored internally, but the retrieval tool is not registered.";
1072
+ const fetchModeDescription = fetchModeConfig.allowedModes
1073
+ .map(mode => `${mode}${mode === fetchModeConfig.defaultMode ? " (default)" : ""}: ${FETCH_MODE_DESCRIPTIONS[mode]}`)
1074
+ .join("; ");
1090
1075
  const curateKey = initConfig.shortcuts?.curate || DEFAULT_SHORTCUTS.curate;
1091
1076
  const activityKey = initConfig.shortcuts?.activity || DEFAULT_SHORTCUTS.activity;
1092
1077
 
@@ -1790,22 +1775,22 @@ export default function (pi: ExtensionAPI) {
1790
1775
  name: toolNames.webSearch,
1791
1776
  label: "Web Search",
1792
1777
  description:
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.`,
1778
+ `Search the web with ${allowedSearchProviders.map(providerLabel).join(", ")}. Provider arrays run simultaneously; ${allPolicyDescription}. By default, returns source-linked search results or provider answers. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query. When includeContent is true, full page content is fetched in the background. Searches return without the interactive browser curator by default; set workflow to "summary-review" to open the curator with an auto-generated summary draft or "auto-summary" to generate a summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it.`,
1794
1779
  promptSnippet:
1795
1780
  "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.",
1796
1781
  parameters: Type.Object({
1797
1782
  query: Type.Optional(Type.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
1798
- queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched concurrently (up to three at a time), each returning its own synthesized answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
1783
+ queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched concurrently (up to three at a time), each returning source-linked search results or a provider answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
1799
1784
  numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)" })),
1800
1785
  includeContent: Type.Optional(Type.Boolean({ description: "Fetch full page content (async)" })),
1801
1786
  recencyFilter: Type.Optional(
1802
1787
  StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" }),
1803
1788
  ),
1804
1789
  domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
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")),
1790
+ provider: Type.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}; omit this field to use the configured provider, or use auto when none is configured`, allowedSearchProviders)),
1806
1791
  workflow: Type.Optional(
1807
1792
  StringEnum(["none", "summary-review", "auto-summary"], {
1808
- description: "Search workflow mode: none = no curator, summary-review = open curator with auto summary draft (default), auto-summary = generate summary without opening curator",
1793
+ description: "Search workflow mode: none = no curator (default), summary-review = open curator with auto summary draft, auto-summary = generate summary without opening curator",
1809
1794
  }),
1810
1795
  ),
1811
1796
  proxy: Type.Optional(Type.String({
@@ -1869,6 +1854,7 @@ export default function (pi: ExtensionAPI) {
1869
1854
  const summaryContext: SummaryGenerationContext = {
1870
1855
  model: ctx.model,
1871
1856
  modelRegistry: ctx.modelRegistry,
1857
+ sessionManager: ctx.sessionManager,
1872
1858
  cwd: ctx.cwd,
1873
1859
  isProjectTrusted: () => ctx.isProjectTrusted(),
1874
1860
  };
@@ -2097,6 +2083,7 @@ export default function (pi: ExtensionAPI) {
2097
2083
  const summaryContext: SummaryGenerationContext = {
2098
2084
  model: ctx.model,
2099
2085
  modelRegistry: ctx.modelRegistry,
2086
+ sessionManager: ctx.sessionManager,
2100
2087
  cwd: ctx.cwd,
2101
2088
  isProjectTrusted: () => ctx.isProjectTrusted(),
2102
2089
  };
@@ -2387,16 +2374,16 @@ export default function (pi: ExtensionAPI) {
2387
2374
  if (sourceCheckEnabled) pi.registerTool({
2388
2375
  name: toolNames.sourceCheck,
2389
2376
  label: "Source Check",
2390
- description: "Check a claim against web sources and return a bounded machine-readable research artifact with exact passage citations.",
2391
- promptSnippet: "Verify a claim with structured source evidence and passage-level citations.",
2377
+ description: "Gather web sources for a claim and return a bounded machine-readable research artifact with exact passage citations for manual review.",
2378
+ promptSnippet: "Gather structured source evidence and passage-level citations for manual semantic review of a claim.",
2392
2379
  parameters: Type.Object({
2393
- claim: Type.String({ description: "The assertion to check against web sources." }),
2380
+ claim: Type.String({ description: "The assertion to gather web sources for." }),
2394
2381
  queries: Type.Optional(Type.Array(Type.String(), { description: "Search queries (default: the claim)." })),
2395
2382
  numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)." })),
2396
2383
  fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
2397
2384
  recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
2398
2385
  domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
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")),
2386
+ provider: Type.Optional(searchProviderSchema(`Search provider or non-empty list of allowed providers to search simultaneously; ${allPolicyDescription}`, allowedSearchProviders)),
2400
2387
  proxy: Type.Optional(Type.String({
2401
2388
  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.",
2402
2389
  })),
@@ -2486,9 +2473,9 @@ export default function (pi: ExtensionAPI) {
2486
2473
  if (fetchContentEnabled) pi.registerTool({
2487
2474
  name: toolNames.fetchContent,
2488
2475
  label: "Fetch Content",
2489
- description: `Fetch URL(s) and extract readable content as markdown. Use mode "raw" for exact textual HTTP response bodies or mode "answer" with prompt to answer using only fetched content. Direct image URLs return resized image content. Supports YouTube transcripts, GitHub repositories, PDFs, and local videos. ${fetchContentStorageNote}`,
2476
+ description: `Fetch URL(s). Available modes: ${fetchModeDescription}. Direct image URLs return resized image content when supported by the selected mode. Supports YouTube transcripts, GitHub repositories, PDFs, and local videos when supported by the selected mode. ${fetchContentStorageNote}`,
2490
2477
  promptSnippet:
2491
- "Use to fetch readable or raw URL content, direct images, GitHub repos, and videos. Mode answer answers a prompt using only the fetched source.",
2478
+ "Use to fetch URL content, direct images, GitHub repos, and videos.",
2492
2479
  parameters: Type.Object({
2493
2480
  url: Type.Optional(Type.String({ description: "Single URL to fetch" })),
2494
2481
  urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs (parallel)" })),
@@ -2496,14 +2483,18 @@ export default function (pi: ExtensionAPI) {
2496
2483
  description: "Force cloning large GitHub repositories that exceed the size threshold",
2497
2484
  })),
2498
2485
  prompt: Type.Optional(Type.String({
2499
- description: "Question or instruction for video analysis, or the page-local question required by mode answer.",
2500
- })),
2501
- mode: Type.Optional(StringEnum(["readable", "raw", "answer"], {
2502
- description: "Fetch mode: readable (default extraction), raw (exact textual HTTP body), or answer (answer prompt using only fetched content).",
2486
+ description: fetchModeConfig.allowedModes.includes("answer")
2487
+ ? "Question or instruction for video analysis, or the page-local question required by answer mode."
2488
+ : "Question or instruction for video analysis.",
2503
2489
  })),
2504
- answerModel: Type.Optional(Type.String({
2505
- description: "Optional provider/model-id override for mode answer. Defaults to fetch.answerProvider + fetch.answerModel when configured, otherwise the current Pi model.",
2490
+ mode: Type.Optional(StringEnum(fetchModeConfig.allowedModes, {
2491
+ description: `Fetch mode. ${fetchModeDescription}.`,
2506
2492
  })),
2493
+ ...(fetchModeConfig.allowedModes.includes("answer") ? {
2494
+ answerModel: Type.Optional(Type.String({
2495
+ description: "Optional provider/model-id override for answer mode. Defaults to fetch.answerProvider + fetch.answerModel when configured, otherwise the current Pi model.",
2496
+ })),
2497
+ } : {}),
2507
2498
  timestamp: Type.Optional(Type.String({
2508
2499
  description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame.",
2509
2500
  })),
@@ -2532,8 +2523,12 @@ export default function (pi: ExtensionAPI) {
2532
2523
  return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
2533
2524
  }
2534
2525
  const { urlList, options } = normalized;
2526
+ const mode = options.mode ?? fetchModeConfig.defaultMode;
2527
+ if (!fetchModeConfig.allowedModes.includes(mode)) {
2528
+ const error = `Fetch mode "${mode}" is disabled by fetch.allowedModes.`;
2529
+ return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
2530
+ }
2535
2531
  return runWithProxy(options.proxy, async () => {
2536
- const mode = options.mode ?? "readable";
2537
2532
  if (mode === "answer" && !options.prompt) {
2538
2533
  return { content: [{ type: "text", text: "Error: mode answer requires prompt." }], details: { error: "mode answer requires prompt" } };
2539
2534
  }
@@ -2570,13 +2565,12 @@ export default function (pi: ExtensionAPI) {
2570
2565
  details: { phase: "fetch", progress: 0 },
2571
2566
  });
2572
2567
 
2573
- const { answerModel: _answerModel, auth: _auth, ...extractionOptions } = options;
2574
- const fetchOptions = mode === "answer"
2575
- ? (() => {
2576
- const { prompt: _prompt, ...rest } = extractionOptions;
2577
- return { ...rest, ...(authFetchProfile ? { authFetchProfile } : {}) };
2578
- })()
2579
- : { ...extractionOptions, ...(authFetchProfile ? { authFetchProfile } : {}) };
2568
+ const { answerModel: _answerModel, auth: _auth, ...extractionOptions } = { ...options, mode };
2569
+ const { prompt: _prompt, ...answerExtractionOptions } = extractionOptions;
2570
+ const fetchOptions = {
2571
+ ...(mode === "answer" ? answerExtractionOptions : extractionOptions),
2572
+ ...(authFetchProfile ? { authFetchProfile } : {}),
2573
+ };
2580
2574
  const fetchResults = await fetchAllContent(urlList, signal, withRegisteredFetchOptions(fetchOptions, registeredToolNames, options.proxy));
2581
2575
  const presentedResults = mode === "answer"
2582
2576
  ? await Promise.all(fetchResults.map(async result => {
@@ -2693,7 +2687,13 @@ export default function (pi: ExtensionAPI) {
2693
2687
  },
2694
2688
 
2695
2689
  renderCall(args, theme) {
2696
- const { urlList, options } = normalizeFetchContentParams(args);
2690
+ let normalized: ReturnType<typeof normalizeFetchContentParams>;
2691
+ try {
2692
+ normalized = normalizeFetchContentParams(args);
2693
+ } catch {
2694
+ return new Text(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("error", "(invalid parameters)"), 0, 0);
2695
+ }
2696
+ const { urlList, options } = normalized;
2697
2697
  const { prompt, timestamp, frames, model, mode, answerModel, auth } = options;
2698
2698
  if (urlList.length === 0) {
2699
2699
  return new Text(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("error", "(no URL)"), 0, 0);
@@ -3184,7 +3184,7 @@ export default function (pi: ExtensionAPI) {
3184
3184
  const availableProviders = bootstrap.availableProviders;
3185
3185
  const initialProvider = bootstrap.defaultProvider;
3186
3186
  const curatorTimeoutSeconds = bootstrap.timeoutSeconds;
3187
- let currentProvider: CuratorProvider = initialProvider;
3187
+ let currentProvider: SearchProvider = initialProvider;
3188
3188
  const commandConfig = loadConfig();
3189
3189
  const rawSearchProvider = normalizeProviderInput(
3190
3190
  commandConfig.searchProvider ?? commandConfig.provider ?? "auto",
@@ -3196,6 +3196,7 @@ export default function (pi: ExtensionAPI) {
3196
3196
  const summaryContext: SummaryGenerationContext = {
3197
3197
  model: ctx.model,
3198
3198
  modelRegistry: ctx.modelRegistry,
3199
+ sessionManager: ctx.sessionManager,
3199
3200
  cwd: ctx.cwd,
3200
3201
  isProjectTrusted: () => ctx.isProjectTrusted(),
3201
3202
  };
@@ -3,6 +3,8 @@ import { activityMonitor } from "./activity.ts";
3
3
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
4
4
  import type { ExtractedContent } from "./extract.ts";
5
5
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
6
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
7
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
6
8
  import { getWebSearchConfigPath } from "./utils.ts";
7
9
 
8
10
  const JINA_SEARCH_BASE_URL = "https://s.jina.ai/";
@@ -74,11 +76,6 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
74
76
  return apiKey;
75
77
  }
76
78
 
77
- function normalizeCount(value: number | undefined): number {
78
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
79
- return Math.max(1, Math.min(Math.floor(value), 20));
80
- }
81
-
82
79
  function normalizeDomain(value: string): string | null {
83
80
  let input = value.trim().toLowerCase();
84
81
  if (input.startsWith("-")) input = input.slice(1).trim();
@@ -181,14 +178,6 @@ function mapItems(items: JinaSearchItem[], numResults: number, filters: ReturnTy
181
178
  return { results, content };
182
179
  }
183
180
 
184
- function buildAnswer(results: SearchResponse["results"]): string {
185
- return results
186
- .map((result) => result.snippet
187
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
188
- : `Source: ${result.title} (${result.url})`)
189
- .join("\n\n");
190
- }
191
-
192
181
  function rethrowRequestError(
193
182
  err: unknown,
194
183
  apiKey: string,
@@ -220,7 +209,7 @@ export function isJinaSearchAvailable(): boolean {
220
209
 
221
210
  export async function searchWithJina(query: string, options: JinaSearchOptions = {}): Promise<SearchResponse> {
222
211
  const apiKey = await requireApiKey(options.signal);
223
- const numResults = normalizeCount(options.numResults);
212
+ const numResults = normalizeSearchResultCount(options.numResults);
224
213
  const { url, filters } = buildSearchRequest(query, options, numResults);
225
214
  const activityId = activityMonitor.logStart({ type: "api", query });
226
215
  const request = requestSignal(options.signal);
@@ -276,7 +265,7 @@ export async function searchWithJina(query: string, options: JinaSearchOptions =
276
265
  }
277
266
  activityMonitor.logComplete(activityId, response.status);
278
267
  const result: SearchResponse = {
279
- answer: buildAnswer(mapped.results),
268
+ answer: formatSearchResultsAsAnswer(mapped.results),
280
269
  results: mapped.results,
281
270
  };
282
271
  if (options.includeContent && mapped.content.length > 0) result.inlineContent = mapped.content;
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { activityMonitor } from "./activity.ts";
3
3
  import type { ExtractedContent, ExtractOptions } from "./extract.ts";
4
4
  import type { SearchOptions, SearchResponse } from "./perplexity.ts";
5
+ import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
6
+ import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
5
7
  import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
6
8
  import { fetchRemoteUrl, loadFetchContentDomainPolicy, loadSsrfConfig, validateRemoteUrl, type SsrfConfig } from "./ssrf-protection.ts";
7
9
  import { getWebSearchConfigPath } from "./utils.ts";
@@ -68,11 +70,6 @@ async function requireApiKey(signal?: AbortSignal): Promise<string> {
68
70
  return apiKey;
69
71
  }
70
72
 
71
- function normalizeCount(value: number | undefined): number {
72
- if (typeof value !== "number" || !Number.isFinite(value)) return 5;
73
- return Math.max(1, Math.min(Math.floor(value), 20));
74
- }
75
-
76
73
  function errorMessage(err: unknown): string {
77
74
  return err instanceof Error ? err.message : String(err);
78
75
  }
@@ -153,19 +150,13 @@ function parseExtractResponse(value: unknown, requestedUrl: string): ExtractedCo
153
150
  return null;
154
151
  }
155
152
 
156
- function buildAnswer(results: SearchResponse["results"]): string {
157
- return results.map((result) => result.snippet
158
- ? `${result.snippet}\nSource: ${result.title} (${result.url})`
159
- : `Source: ${result.title} (${result.url})`).join("\n\n");
160
- }
161
-
162
153
  export function isKagiAvailable(): boolean {
163
154
  return hasCredentialSource({ provider: "Kagi", configuredValue: loadConfig().kagiApiKey, environmentValue: process.env.KAGI_API_KEY });
164
155
  }
165
156
 
166
157
  export async function searchWithKagi(query: string, options: KagiSearchOptions = {}): Promise<SearchResponse> {
167
158
  const apiKey = await requireApiKey(options.signal);
168
- const numResults = normalizeCount(options.numResults);
159
+ const numResults = normalizeSearchResultCount(options.numResults);
169
160
  const activityId = activityMonitor.logStart({ type: "api", query });
170
161
  let response: Response;
171
162
  try {
@@ -200,7 +191,7 @@ export async function searchWithKagi(query: string, options: KagiSearchOptions =
200
191
  const parsed = parseSearchResponse(rawData);
201
192
  activityMonitor.logComplete(activityId, response.status);
202
193
  const results = parsed.results.slice(0, numResults);
203
- const mapped: SearchResponse = { answer: buildAnswer(results), results };
194
+ const mapped: SearchResponse = { answer: formatSearchResultsAsAnswer(results), results };
204
195
  if (options.includeContent) {
205
196
  const urls = new Set(results.map(result => result.url));
206
197
  const inlineContent = parsed.inlineContent.filter(content => urls.has(content.url));