@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.0

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 (143) hide show
  1. package/CHANGELOG.md +199 -49
  2. package/README.md +1 -1
  3. package/docs/config-usage.md +3 -4
  4. package/docs/sdk.md +6 -5
  5. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  6. package/examples/sdk/README.md +1 -1
  7. package/package.json +19 -11
  8. package/src/cli/args.ts +11 -94
  9. package/src/cli/config-cli.ts +1 -1
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/oclif-help.ts +26 -0
  12. package/src/cli/web-search-cli.ts +148 -0
  13. package/src/cli.ts +8 -2
  14. package/src/commands/commit.ts +36 -0
  15. package/src/commands/config.ts +51 -0
  16. package/src/commands/grep.ts +41 -0
  17. package/src/commands/index/index.ts +136 -0
  18. package/src/commands/jupyter.ts +32 -0
  19. package/src/commands/plugin.ts +70 -0
  20. package/src/commands/setup.ts +39 -0
  21. package/src/commands/shell.ts +29 -0
  22. package/src/commands/stats.ts +29 -0
  23. package/src/commands/update.ts +21 -0
  24. package/src/commands/web-search.ts +50 -0
  25. package/src/commit/agentic/index.ts +3 -2
  26. package/src/commit/agentic/tools/analyze-file.ts +1 -3
  27. package/src/commit/git/errors.ts +4 -6
  28. package/src/commit/pipeline.ts +3 -2
  29. package/src/config/keybindings.ts +1 -3
  30. package/src/config/model-registry.ts +89 -162
  31. package/src/config/settings-schema.ts +10 -0
  32. package/src/config.ts +202 -132
  33. package/src/exa/mcp-client.ts +8 -41
  34. package/src/export/html/index.ts +1 -1
  35. package/src/extensibility/extensions/loader.ts +7 -10
  36. package/src/extensibility/extensions/runner.ts +5 -15
  37. package/src/extensibility/extensions/types.ts +1 -1
  38. package/src/extensibility/hooks/runner.ts +6 -9
  39. package/src/index.ts +0 -1
  40. package/src/ipy/kernel.ts +10 -22
  41. package/src/lsp/clients/biome-client.ts +4 -7
  42. package/src/lsp/clients/lsp-linter-client.ts +4 -6
  43. package/src/lsp/index.ts +5 -4
  44. package/src/lsp/utils.ts +18 -0
  45. package/src/main.ts +86 -181
  46. package/src/mcp/json-rpc.ts +2 -2
  47. package/src/mcp/transports/http.ts +12 -49
  48. package/src/modes/components/armin.ts +1 -3
  49. package/src/modes/components/assistant-message.ts +4 -4
  50. package/src/modes/components/bash-execution.ts +5 -3
  51. package/src/modes/components/branch-summary-message.ts +1 -3
  52. package/src/modes/components/compaction-summary-message.ts +1 -3
  53. package/src/modes/components/custom-message.ts +4 -5
  54. package/src/modes/components/extensions/extension-dashboard.ts +10 -16
  55. package/src/modes/components/extensions/extension-list.ts +5 -5
  56. package/src/modes/components/footer.ts +1 -4
  57. package/src/modes/components/hook-editor.ts +7 -32
  58. package/src/modes/components/hook-message.ts +4 -5
  59. package/src/modes/components/model-selector.ts +2 -2
  60. package/src/modes/components/plugin-settings.ts +16 -20
  61. package/src/modes/components/python-execution.ts +5 -5
  62. package/src/modes/components/session-selector.ts +6 -7
  63. package/src/modes/components/settings-defs.ts +49 -40
  64. package/src/modes/components/settings-selector.ts +8 -17
  65. package/src/modes/components/skill-message.ts +1 -3
  66. package/src/modes/components/status-line-segment-editor.ts +1 -3
  67. package/src/modes/components/status-line.ts +1 -3
  68. package/src/modes/components/todo-reminder.ts +5 -7
  69. package/src/modes/components/tree-selector.ts +10 -12
  70. package/src/modes/components/ttsr-notification.ts +1 -3
  71. package/src/modes/components/user-message-selector.ts +2 -4
  72. package/src/modes/components/welcome.ts +6 -18
  73. package/src/modes/controllers/event-controller.ts +1 -0
  74. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  75. package/src/modes/controllers/input-controller.ts +7 -34
  76. package/src/modes/controllers/selector-controller.ts +3 -3
  77. package/src/modes/interactive-mode.ts +27 -1
  78. package/src/modes/rpc/rpc-client.ts +2 -5
  79. package/src/modes/rpc/rpc-mode.ts +2 -2
  80. package/src/modes/theme/theme.ts +2 -6
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +6 -1
  83. package/src/patch/index.ts +1 -4
  84. package/src/prompts/agents/explore.md +1 -0
  85. package/src/prompts/agents/frontmatter.md +2 -1
  86. package/src/prompts/agents/init.md +1 -0
  87. package/src/prompts/agents/plan.md +1 -0
  88. package/src/prompts/agents/reviewer.md +1 -0
  89. package/src/prompts/system/subagent-submit-reminder.md +2 -0
  90. package/src/prompts/system/subagent-system-prompt.md +2 -0
  91. package/src/prompts/system/subagent-user-prompt.md +8 -0
  92. package/src/prompts/system/system-prompt.md +5 -3
  93. package/src/prompts/system/web-search.md +6 -4
  94. package/src/prompts/tools/task.md +216 -163
  95. package/src/sdk.ts +11 -110
  96. package/src/session/agent-session.ts +117 -83
  97. package/src/session/auth-storage.ts +10 -51
  98. package/src/session/messages.ts +17 -3
  99. package/src/session/session-manager.ts +30 -30
  100. package/src/session/streaming-output.ts +1 -1
  101. package/src/ssh/ssh-executor.ts +6 -3
  102. package/src/task/agents.ts +2 -0
  103. package/src/task/discovery.ts +1 -1
  104. package/src/task/executor.ts +5 -10
  105. package/src/task/index.ts +43 -23
  106. package/src/task/render.ts +67 -64
  107. package/src/task/template.ts +17 -34
  108. package/src/task/types.ts +49 -22
  109. package/src/tools/ask.ts +1 -3
  110. package/src/tools/bash.ts +1 -4
  111. package/src/tools/browser.ts +5 -7
  112. package/src/tools/exit-plan-mode.ts +1 -4
  113. package/src/tools/fetch.ts +1 -3
  114. package/src/tools/find.ts +4 -3
  115. package/src/tools/gemini-image.ts +24 -55
  116. package/src/tools/grep.ts +4 -4
  117. package/src/tools/index.ts +12 -14
  118. package/src/tools/notebook.ts +1 -5
  119. package/src/tools/python.ts +4 -3
  120. package/src/tools/read.ts +2 -4
  121. package/src/tools/render-utils.ts +23 -0
  122. package/src/tools/ssh.ts +8 -12
  123. package/src/tools/todo-write.ts +1 -4
  124. package/src/tools/tool-errors.ts +1 -4
  125. package/src/tools/write.ts +1 -3
  126. package/src/utils/external-editor.ts +59 -0
  127. package/src/utils/file-mentions.ts +39 -1
  128. package/src/utils/image-convert.ts +1 -1
  129. package/src/utils/image-resize.ts +4 -4
  130. package/src/web/search/auth.ts +3 -33
  131. package/src/web/search/index.ts +73 -139
  132. package/src/web/search/provider.ts +58 -0
  133. package/src/web/search/providers/anthropic.ts +53 -14
  134. package/src/web/search/providers/base.ts +22 -0
  135. package/src/web/search/providers/codex.ts +38 -16
  136. package/src/web/search/providers/exa.ts +30 -6
  137. package/src/web/search/providers/gemini.ts +56 -20
  138. package/src/web/search/providers/jina.ts +28 -5
  139. package/src/web/search/providers/perplexity.ts +103 -36
  140. package/src/web/search/render.ts +84 -74
  141. package/src/web/search/types.ts +285 -59
  142. package/src/migrations.ts +0 -175
  143. package/src/session/storage-migration.ts +0 -173
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Unified Web Search Tool
3
3
  *
4
- * Single tool supporting Anthropic, Perplexity, Exa, and Jina providers with
5
- * provider-specific parameters exposed conditionally.
4
+ * Single tool supporting Anthropic, Perplexity, Exa, Jina, Gemini, and Codex
5
+ * providers with provider-specific parameters exposed conditionally.
6
6
  *
7
7
  * When EXA_API_KEY is available, additional specialized tools are exposed:
8
8
  * - web_search_deep: Natural language web search with synthesized results
@@ -24,20 +24,16 @@ import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { ty
24
24
  import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" };
25
25
  import type { ToolSession } from "../../tools";
26
26
  import { formatAge } from "../../tools/render-utils";
27
- import { findAnthropicAuth } from "./auth";
28
- import { searchAnthropic } from "./providers/anthropic";
29
- import { searchExa } from "./providers/exa";
30
- import { findApiKey as findJinaKey, searchJina } from "./providers/jina";
31
- import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity";
32
- import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./render";
33
- import type { WebSearchProvider, WebSearchResponse } from "./types";
34
- import { WebSearchProviderError } from "./types";
27
+ import { getSearchProvider, resolveProviderChain, type SearchProvider } from "./provider";
28
+ import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
29
+ import type { SearchResponse } from "./types";
30
+ import { SearchProviderError } from "./types";
35
31
 
36
32
  /** Web search parameters schema */
37
33
  export const webSearchSchema = Type.Object({
38
34
  query: Type.String({ description: "Search query" }),
39
35
  provider: Type.Optional(
40
- StringEnum(["auto", "exa", "jina", "anthropic", "perplexity"], {
36
+ StringEnum(["auto", "exa", "jina", "anthropic", "perplexity", "gemini", "codex"], {
41
37
  description: "Search provider (default: auto)",
42
38
  }),
43
39
  ),
@@ -49,86 +45,35 @@ export const webSearchSchema = Type.Object({
49
45
  limit: Type.Optional(Type.Number({ description: "Max results to return" })),
50
46
  });
51
47
 
52
- export type WebSearchParams = {
48
+ export type SearchParams = {
53
49
  query: string;
54
- provider?: "auto" | "exa" | "jina" | "anthropic" | "perplexity";
50
+ provider?: "auto" | "exa" | "jina" | "anthropic" | "perplexity" | "gemini" | "codex";
55
51
  recency?: "day" | "week" | "month" | "year";
56
52
  limit?: number;
53
+ /** Maximum output tokens. Defaults to 4096. */
54
+ max_tokens?: number;
55
+ /** Sampling temperature (0–1). Lower = more focused/factual. Defaults to 0.2. */
56
+ temperature?: number;
57
+ /** Number of search results to retrieve. Defaults to 10. */
58
+ num_search_results?: number;
57
59
  };
58
60
 
59
- /** Preferred provider set via settings (default: auto) */
60
- let preferredProvider: WebSearchProvider | "auto" = "auto";
61
-
62
- /** Set the preferred web search provider from settings */
63
- export function setPreferredWebSearchProvider(provider: WebSearchProvider | "auto"): void {
64
- preferredProvider = provider;
65
- }
66
-
67
- /** Determine which providers are configured (priority order) */
68
- async function getAvailableProviders(): Promise<WebSearchProvider[]> {
69
- const providers: WebSearchProvider[] = [];
70
-
71
- const exaKey = await findExaKey();
72
- if (exaKey) providers.push("exa");
73
-
74
- const jinaKey = await findJinaKey();
75
- if (jinaKey) providers.push("jina");
76
-
77
- const perplexityKey = await findPerplexityKey();
78
- if (perplexityKey) providers.push("perplexity");
79
-
80
- const anthropicAuth = await findAnthropicAuth();
81
- if (anthropicAuth) providers.push("anthropic");
82
-
83
- return providers;
84
- }
85
-
86
- function formatProviderLabel(provider: WebSearchProvider): string {
87
- switch (provider) {
88
- case "exa":
89
- return "Exa";
90
- case "jina":
91
- return "Jina";
92
- case "perplexity":
93
- return "Perplexity";
94
- case "anthropic":
95
- return "Anthropic";
96
- default:
97
- return provider;
98
- }
99
- }
100
-
101
- function formatProviderList(providers: WebSearchProvider[]): string {
102
- return providers.map(provider => formatProviderLabel(provider)).join(", ");
61
+ function formatProviderList(providers: SearchProvider[]): string {
62
+ return providers.map(provider => provider.label).join(", ");
103
63
  }
104
64
 
105
- function formatProviderError(error: unknown, provider: WebSearchProvider): string {
106
- if (error instanceof WebSearchProviderError) {
65
+ function formatProviderError(error: unknown, provider: SearchProvider): string {
66
+ if (error instanceof SearchProviderError) {
107
67
  if (error.provider === "anthropic" && error.status === 404) {
108
68
  return "Anthropic web search returned 404 (model or endpoint not found).";
109
69
  }
110
70
  if (error.status === 401 || error.status === 403) {
111
- return `${formatProviderLabel(error.provider)} authorization failed (${error.status}). Check API key or base URL.`;
71
+ return `${getSearchProvider(error.provider).label} authorization failed (${error.status}). Check API key or base URL.`;
112
72
  }
113
73
  return error.message;
114
74
  }
115
75
  if (error instanceof Error) return error.message;
116
- return `Unknown error from ${formatProviderLabel(provider)}`;
117
- }
118
-
119
- async function resolveProviderChain(
120
- requestedProvider?: WebSearchProvider | "auto",
121
- ): Promise<{ providers: WebSearchProvider[]; allowFallback: boolean }> {
122
- if (requestedProvider && requestedProvider !== "auto") {
123
- return { providers: [requestedProvider], allowFallback: false };
124
- }
125
-
126
- if (preferredProvider !== "auto") {
127
- return { providers: [preferredProvider], allowFallback: false };
128
- }
129
-
130
- const providers = await getAvailableProviders();
131
- return { providers, allowFallback: true };
76
+ return `Unknown error from ${provider.label}`;
132
77
  }
133
78
 
134
79
  /** Truncate text for tool output */
@@ -142,7 +87,7 @@ function formatCount(label: string, count: number): string {
142
87
  }
143
88
 
144
89
  /** Format response for LLM consumption */
145
- function formatForLLM(response: WebSearchResponse): string {
90
+ function formatForLLM(response: SearchResponse): string {
146
91
  const parts: string[] = [];
147
92
 
148
93
  parts.push("## Answer");
@@ -216,18 +161,17 @@ function formatForLLM(response: WebSearchResponse): string {
216
161
  }
217
162
 
218
163
  /** Execute web search */
219
- async function executeWebSearch(
164
+ async function executeSearch(
220
165
  _toolCallId: string,
221
- params: WebSearchParams,
222
- ): Promise<{ content: Array<{ type: "text"; text: string }>; details: WebSearchRenderDetails }> {
223
- const { providers, allowFallback } = await resolveProviderChain(params.provider);
166
+ params: SearchParams,
167
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
168
+ const providers = await resolveProviderChain(params.provider);
224
169
 
225
170
  if (providers.length === 0) {
226
171
  const message = "No web search provider configured.";
227
- const fallbackProvider = preferredProvider === "auto" ? "anthropic" : preferredProvider;
228
172
  return {
229
173
  content: [{ type: "text" as const, text: `Error: ${message}` }],
230
- details: { response: { provider: fallbackProvider, sources: [] }, error: message },
174
+ details: { response: { provider: "none", sources: [] }, error: message },
231
175
  };
232
176
  }
233
177
 
@@ -237,31 +181,15 @@ async function executeWebSearch(
237
181
  for (const provider of providers) {
238
182
  lastProvider = provider;
239
183
  try {
240
- let response: WebSearchResponse;
241
- if (provider === "exa") {
242
- response = await searchExa({
243
- query: params.query,
244
- num_results: params.limit,
245
- });
246
- } else if (provider === "jina") {
247
- response = await searchJina({
248
- query: params.query,
249
- num_results: params.limit,
250
- });
251
- } else if (provider === "anthropic") {
252
- response = await searchAnthropic({
253
- query: params.query,
254
- system_prompt: webSearchSystemPrompt,
255
- num_results: params.limit,
256
- });
257
- } else {
258
- response = await searchPerplexity({
259
- query: params.query,
260
- system_prompt: webSearchSystemPrompt,
261
- search_recency_filter: params.recency,
262
- num_results: params.limit,
263
- });
264
- }
184
+ const response = await provider.search({
185
+ query: params.query.replace(/202\d/g, String(new Date().getFullYear())), // LUL
186
+ limit: params.limit,
187
+ recency: params.recency,
188
+ systemPrompt: webSearchSystemPrompt,
189
+ maxOutputTokens: params.max_tokens,
190
+ numSearchResults: params.num_search_results,
191
+ temperature: params.temperature,
192
+ });
265
193
 
266
194
  const text = formatForLLM(response);
267
195
 
@@ -271,29 +199,37 @@ async function executeWebSearch(
271
199
  };
272
200
  } catch (error) {
273
201
  lastError = error;
274
- if (!allowFallback) break;
275
202
  }
276
203
  }
277
204
 
278
205
  const baseMessage = formatProviderError(lastError, lastProvider);
279
206
  const message =
280
- allowFallback && providers.length > 1
207
+ providers.length > 1
281
208
  ? `All web search providers failed (${formatProviderList(providers)}). Last error: ${baseMessage}`
282
209
  : baseMessage;
283
210
 
284
211
  return {
285
212
  content: [{ type: "text" as const, text: `Error: ${message}` }],
286
- details: { response: { provider: lastProvider, sources: [] }, error: message },
213
+ details: { response: { provider: lastProvider.id, sources: [] }, error: message },
287
214
  };
288
215
  }
289
216
 
217
+ /**
218
+ * Execute a web search query for CLI/testing workflows.
219
+ */
220
+ export async function runSearchQuery(
221
+ params: SearchParams,
222
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
223
+ return executeSearch("cli-web-search", params);
224
+ }
225
+
290
226
  /**
291
227
  * Web search tool implementation.
292
228
  *
293
- * Supports Anthropic, Perplexity, Exa, and Jina providers with automatic fallback.
229
+ * Supports Anthropic, Perplexity, Exa, Jina, Gemini, and Codex providers with automatic fallback.
294
230
  * Session is accepted for interface consistency but not used.
295
231
  */
296
- export class WebSearchTool implements AgentTool<typeof webSearchSchema, WebSearchRenderDetails> {
232
+ export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
297
233
  public readonly name = "web_search";
298
234
  public readonly label = "Web Search";
299
235
  public readonly description: string;
@@ -305,38 +241,32 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, WebSearc
305
241
 
306
242
  public async execute(
307
243
  _toolCallId: string,
308
- params: WebSearchParams,
244
+ params: SearchParams,
309
245
  _signal?: AbortSignal,
310
- _onUpdate?: AgentToolUpdateCallback<WebSearchRenderDetails>,
246
+ _onUpdate?: AgentToolUpdateCallback<SearchRenderDetails>,
311
247
  _context?: AgentToolContext,
312
- ): Promise<AgentToolResult<WebSearchRenderDetails>> {
313
- return executeWebSearch(_toolCallId, params);
248
+ ): Promise<AgentToolResult<SearchRenderDetails>> {
249
+ return executeSearch(_toolCallId, params);
314
250
  }
315
251
  }
316
252
 
317
253
  /** Web search tool as CustomTool (for TUI rendering support) */
318
- export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRenderDetails> = {
254
+ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails> = {
319
255
  name: "web_search",
320
256
  label: "Web Search",
321
257
  description: renderPromptTemplate(webSearchDescription),
322
258
  parameters: webSearchSchema,
323
259
 
324
- async execute(
325
- toolCallId: string,
326
- params: WebSearchParams,
327
- _onUpdate,
328
- _ctx: CustomToolContext,
329
- _signal?: AbortSignal,
330
- ) {
331
- return executeWebSearch(toolCallId, params);
260
+ async execute(toolCallId: string, params: SearchParams, _onUpdate, _ctx: CustomToolContext, _signal?: AbortSignal) {
261
+ return executeSearch(toolCallId, params);
332
262
  },
333
263
 
334
- renderCall(args: WebSearchParams, theme: Theme) {
335
- return renderWebSearchCall(args, theme);
264
+ renderCall(args: SearchParams, theme: Theme) {
265
+ return renderSearchCall(args, theme);
336
266
  },
337
267
 
338
268
  renderResult(result, options: RenderResultOptions, theme: Theme) {
339
- return renderWebSearchResult(result, options, theme);
269
+ return renderSearchResult(result, options, theme);
340
270
  },
341
271
  };
342
272
 
@@ -582,19 +512,19 @@ Parameters:
582
512
  };
583
513
 
584
514
  /** All Exa-specific web search tools */
585
- export const exaWebSearchTools: CustomTool<any, ExaRenderDetails>[] = [
515
+ export const exaSearchTools: CustomTool<any, ExaRenderDetails>[] = [
586
516
  webSearchDeepTool,
587
517
  webSearchCodeContextTool,
588
518
  webSearchCrawlTool,
589
519
  ];
590
520
 
591
521
  /** LinkedIn-specific tool (requires LinkedIn addon on Exa account) */
592
- export const linkedinWebSearchTools: CustomTool<any, ExaRenderDetails>[] = [webSearchLinkedinTool];
522
+ export const linkedinSearchTools: CustomTool<any, ExaRenderDetails>[] = [webSearchLinkedinTool];
593
523
 
594
524
  /** Company-specific tool (requires Company addon on Exa account) */
595
- export const companyWebSearchTools: CustomTool<any, ExaRenderDetails>[] = [webSearchCompanyTool];
525
+ export const companySearchTools: CustomTool<any, ExaRenderDetails>[] = [webSearchCompanyTool];
596
526
 
597
- export interface WebSearchToolsOptions {
527
+ export interface SearchToolsOptions {
598
528
  /** Enable LinkedIn search tool (requires Exa LinkedIn addon) */
599
529
  enableLinkedin?: boolean;
600
530
  /** Enable company research tool (requires Exa Company addon) */
@@ -610,19 +540,19 @@ export interface WebSearchToolsOptions {
610
540
  * - With EXA_API_KEY + options.enableLinkedin: web_search_linkedin
611
541
  * - With EXA_API_KEY + options.enableCompany: web_search_company
612
542
  */
613
- export async function getWebSearchTools(options: WebSearchToolsOptions = {}): Promise<CustomTool<any, any>[]> {
543
+ export async function getSearchTools(options: SearchToolsOptions = {}): Promise<CustomTool<any, any>[]> {
614
544
  const tools: CustomTool<any, any>[] = [webSearchCustomTool];
615
545
 
616
546
  // Check for Exa API key
617
547
  const exaKey = await findExaKey();
618
548
  if (exaKey) {
619
- tools.push(...exaWebSearchTools);
549
+ tools.push(...exaSearchTools);
620
550
 
621
551
  if (options.enableLinkedin) {
622
- tools.push(...linkedinWebSearchTools);
552
+ tools.push(...linkedinSearchTools);
623
553
  }
624
554
  if (options.enableCompany) {
625
- tools.push(...companyWebSearchTools);
555
+ tools.push(...companySearchTools);
626
556
  }
627
557
  }
628
558
 
@@ -632,9 +562,13 @@ export async function getWebSearchTools(options: WebSearchToolsOptions = {}): Pr
632
562
  /**
633
563
  * Check if Exa-specific web search tools are available.
634
564
  */
635
- export async function hasExaWebSearch(): Promise<boolean> {
565
+ export async function hasExaSearch(): Promise<boolean> {
636
566
  const exaKey = await findExaKey();
637
567
  return exaKey !== null;
638
568
  }
639
569
 
640
- export type { WebSearchProvider, WebSearchResponse } from "./types";
570
+ export {
571
+ getSearchProvider,
572
+ setPreferredSearchProvider,
573
+ } from "./provider";
574
+ export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
@@ -0,0 +1,58 @@
1
+ import { AnthropicProvider } from "./providers/anthropic";
2
+ import type { SearchProvider } from "./providers/base";
3
+ import { CodexProvider } from "./providers/codex";
4
+ import { ExaProvider } from "./providers/exa";
5
+ import { GeminiProvider } from "./providers/gemini";
6
+ import { JinaProvider } from "./providers/jina";
7
+ import { PerplexityProvider } from "./providers/perplexity";
8
+ import type { SearchProviderId } from "./types";
9
+
10
+ export type { SearchParams } from "./providers/base";
11
+ export { SearchProvider } from "./providers/base";
12
+
13
+ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
14
+ exa: new ExaProvider(),
15
+ jina: new JinaProvider(),
16
+ perplexity: new PerplexityProvider(),
17
+ anthropic: new AnthropicProvider(),
18
+ gemini: new GeminiProvider(),
19
+ codex: new CodexProvider(),
20
+ } as const;
21
+
22
+ const SEARCH_PROVIDER_ORDER: SearchProviderId[] = ["exa", "jina", "perplexity", "anthropic", "gemini", "codex"];
23
+
24
+ export function getSearchProvider(provider: SearchProviderId): SearchProvider {
25
+ return SEARCH_PROVIDERS[provider];
26
+ }
27
+
28
+ /** Preferred provider set via settings (default: auto) */
29
+ let preferredProvId: SearchProviderId | "auto" = "auto";
30
+
31
+ /** Set the preferred web search provider from settings */
32
+ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void {
33
+ preferredProvId = provider;
34
+ }
35
+
36
+ /** Determine which providers are configured (priority order) */
37
+ export async function resolveProviderChain(
38
+ preferredProvider: SearchProviderId | "auto" = preferredProvId,
39
+ ): Promise<SearchProvider[]> {
40
+ const providers: SearchProvider[] = [];
41
+
42
+ if (preferredProvider !== "auto") {
43
+ if (await getSearchProvider(preferredProvider).isAvailable()) {
44
+ providers.push(getSearchProvider(preferredProvider));
45
+ }
46
+ }
47
+
48
+ for (const id of SEARCH_PROVIDER_ORDER) {
49
+ if (id === preferredProvider) continue;
50
+
51
+ const provider = getSearchProvider(id);
52
+ if (await provider.isAvailable()) {
53
+ providers.push(provider);
54
+ }
55
+ }
56
+
57
+ return providers;
58
+ }
@@ -11,11 +11,13 @@ import type {
11
11
  AnthropicApiResponse,
12
12
  AnthropicAuthConfig,
13
13
  AnthropicCitation,
14
- WebSearchCitation,
15
- WebSearchResponse,
16
- WebSearchSource,
14
+ SearchCitation,
15
+ SearchResponse,
16
+ SearchSource,
17
17
  } from "../../../web/search/types";
18
- import { WebSearchProviderError } from "../../../web/search/types";
18
+ import { SearchProviderError } from "../../../web/search/types";
19
+ import type { SearchParams } from "./base";
20
+ import { SearchProvider } from "./base";
19
21
 
20
22
  const DEFAULT_MODEL = "claude-haiku-4-5";
21
23
  const DEFAULT_MAX_TOKENS = 4096;
@@ -36,6 +38,10 @@ export interface AnthropicSearchParams {
36
38
  query: string;
37
39
  system_prompt?: string;
38
40
  num_results?: number;
41
+ /** Maximum output tokens. Defaults to 4096. */
42
+ max_tokens?: number;
43
+ /** Sampling temperature (0–1). Lower = more focused/factual. */
44
+ temperature?: number;
39
45
  }
40
46
 
41
47
  /**
@@ -74,13 +80,15 @@ function buildSystemBlocks(
74
80
  * @param query - Search query from the user
75
81
  * @param systemPrompt - Optional system prompt for guiding response style
76
82
  * @returns Raw API response from Anthropic
77
- * @throws {WebSearchProviderError} If the API request fails
83
+ * @throws {SearchProviderError} If the API request fails
78
84
  */
79
- async function callWebSearch(
85
+ async function callSearch(
80
86
  auth: AnthropicAuthConfig,
81
87
  model: string,
82
88
  query: string,
83
89
  systemPrompt?: string,
90
+ maxTokens?: number,
91
+ temperature?: number,
84
92
  ): Promise<AnthropicApiResponse> {
85
93
  const url = buildAnthropicUrl(auth);
86
94
  const headers = buildAnthropicHeaders(auth);
@@ -89,7 +97,7 @@ async function callWebSearch(
89
97
 
90
98
  const body: Record<string, unknown> = {
91
99
  model,
92
- max_tokens: DEFAULT_MAX_TOKENS,
100
+ max_tokens: maxTokens ?? DEFAULT_MAX_TOKENS,
93
101
  messages: [{ role: "user", content: query }],
94
102
  tools: [
95
103
  {
@@ -99,6 +107,10 @@ async function callWebSearch(
99
107
  ],
100
108
  };
101
109
 
110
+ if (temperature !== undefined) {
111
+ body.temperature = temperature;
112
+ }
113
+
102
114
  if (systemBlocks && systemBlocks.length > 0) {
103
115
  body.system = systemBlocks;
104
116
  }
@@ -111,7 +123,7 @@ async function callWebSearch(
111
123
 
112
124
  if (!response.ok) {
113
125
  const errorText = await response.text();
114
- throw new WebSearchProviderError(
126
+ throw new SearchProviderError(
115
127
  "anthropic",
116
128
  `Anthropic API error (${response.status}): ${errorText}`,
117
129
  response.status,
@@ -158,15 +170,15 @@ function parsePageAge(pageAge: string | null | undefined): number | undefined {
158
170
  }
159
171
 
160
172
  /**
161
- * Parses the Anthropic API response into a unified WebSearchResponse.
173
+ * Parses the Anthropic API response into a unified SearchResponse.
162
174
  * @param response - Raw API response containing content blocks
163
175
  * @returns Normalized response with answer, sources, citations, and usage
164
176
  */
165
- function parseResponse(response: AnthropicApiResponse): WebSearchResponse {
177
+ function parseResponse(response: AnthropicApiResponse): SearchResponse {
166
178
  const answerParts: string[] = [];
167
179
  const searchQueries: string[] = [];
168
- const sources: WebSearchSource[] = [];
169
- const citations: WebSearchCitation[] = [];
180
+ const sources: SearchSource[] = [];
181
+ const citations: SearchCitation[] = [];
170
182
 
171
183
  for (const block of response.content) {
172
184
  if (
@@ -228,7 +240,7 @@ function parseResponse(response: AnthropicApiResponse): WebSearchResponse {
228
240
  * @returns Search response with synthesized answer, sources, and citations
229
241
  * @throws {Error} If no Anthropic credentials are configured
230
242
  */
231
- export async function searchAnthropic(params: AnthropicSearchParams): Promise<WebSearchResponse> {
243
+ export async function searchAnthropic(params: AnthropicSearchParams): Promise<SearchResponse> {
232
244
  const auth = await findAnthropicAuth();
233
245
  if (!auth) {
234
246
  throw new Error(
@@ -237,7 +249,14 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<We
237
249
  }
238
250
 
239
251
  const model = getModel();
240
- const response = await callWebSearch(auth, model, params.query, params.system_prompt);
252
+ const response = await callSearch(
253
+ auth,
254
+ model,
255
+ params.query,
256
+ params.system_prompt,
257
+ params.max_tokens,
258
+ params.temperature,
259
+ );
241
260
 
242
261
  const result = parseResponse(response);
243
262
 
@@ -248,3 +267,23 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<We
248
267
 
249
268
  return result;
250
269
  }
270
+
271
+ /** Search provider for Anthropic Claude web search. */
272
+ export class AnthropicProvider extends SearchProvider {
273
+ readonly id = "anthropic";
274
+ readonly label = "Anthropic";
275
+
276
+ isAvailable() {
277
+ return findAnthropicAuth().then(Boolean);
278
+ }
279
+
280
+ search(params: SearchParams): Promise<SearchResponse> {
281
+ return searchAnthropic({
282
+ query: params.query,
283
+ system_prompt: params.systemPrompt,
284
+ num_results: params.numSearchResults ?? params.limit,
285
+ max_tokens: params.maxOutputTokens,
286
+ temperature: params.temperature,
287
+ });
288
+ }
289
+ }
@@ -0,0 +1,22 @@
1
+ import type { SearchProviderId, SearchResponse } from "../types";
2
+
3
+ /** Shared web search parameters passed to providers. */
4
+ export interface SearchParams {
5
+ query: string;
6
+ limit?: number;
7
+ recency?: "day" | "week" | "month" | "year";
8
+ systemPrompt: string;
9
+ signal?: AbortSignal;
10
+ maxOutputTokens?: number;
11
+ numSearchResults?: number;
12
+ temperature?: number;
13
+ }
14
+
15
+ /** Base class for web search providers. */
16
+ export abstract class SearchProvider {
17
+ abstract readonly id: SearchProviderId;
18
+ abstract readonly label: string;
19
+
20
+ abstract isAvailable(): Promise<boolean> | boolean;
21
+ abstract search(params: SearchParams): Promise<SearchResponse>;
22
+ }