@oh-my-pi/pi-coding-agent 14.1.1 → 14.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 (123) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/package.json +8 -8
  3. package/scripts/build-binary.ts +61 -0
  4. package/src/autoresearch/helpers.ts +10 -0
  5. package/src/autoresearch/index.ts +1 -11
  6. package/src/autoresearch/tools/init-experiment.ts +1 -10
  7. package/src/autoresearch/tools/log-experiment.ts +1 -11
  8. package/src/autoresearch/tools/run-experiment.ts +1 -10
  9. package/src/bun-imports.d.ts +6 -0
  10. package/src/cli/plugin-cli.ts +23 -45
  11. package/src/commit/agentic/tools/propose-commit.ts +1 -14
  12. package/src/commit/agentic/tools/split-commit.ts +1 -15
  13. package/src/commit/utils.ts +15 -1
  14. package/src/config/model-registry.ts +3 -3
  15. package/src/config/prompt-templates.ts +4 -12
  16. package/src/config/settings-schema.ts +27 -2
  17. package/src/config/settings.ts +1 -1
  18. package/src/discovery/claude-plugins.ts +61 -6
  19. package/src/discovery/codex.ts +2 -15
  20. package/src/discovery/gemini.ts +2 -15
  21. package/src/discovery/helpers.ts +40 -1
  22. package/src/discovery/opencode.ts +2 -15
  23. package/src/edit/apply-patch/index.ts +87 -0
  24. package/src/edit/apply-patch/parser.ts +174 -0
  25. package/src/edit/diff.ts +3 -14
  26. package/src/edit/index.ts +65 -2
  27. package/src/edit/modes/apply-patch.lark +19 -0
  28. package/src/edit/modes/apply-patch.ts +63 -0
  29. package/src/edit/modes/hashline.ts +3 -3
  30. package/src/edit/modes/replace.ts +2 -13
  31. package/src/edit/read-file.ts +18 -0
  32. package/src/edit/renderer.ts +61 -33
  33. package/src/extensibility/extensions/compact-handler.ts +40 -0
  34. package/src/extensibility/extensions/runner.ts +11 -29
  35. package/src/extensibility/utils.ts +7 -1
  36. package/src/internal-urls/docs-index.generated.ts +9 -2
  37. package/src/lsp/render.ts +14 -2
  38. package/src/main.ts +1 -0
  39. package/src/mcp/manager.ts +29 -48
  40. package/src/memories/index.ts +7 -1
  41. package/src/modes/acp/acp-agent.ts +3 -16
  42. package/src/modes/components/model-selector.ts +15 -24
  43. package/src/modes/components/plugin-settings.ts +16 -5
  44. package/src/modes/components/read-tool-group.ts +92 -9
  45. package/src/modes/components/settings-defs.ts +18 -0
  46. package/src/modes/components/settings-selector.ts +2 -6
  47. package/src/modes/components/tool-execution.ts +61 -28
  48. package/src/modes/controllers/event-controller.ts +3 -1
  49. package/src/modes/controllers/extension-ui-controller.ts +99 -150
  50. package/src/modes/controllers/selector-controller.ts +3 -12
  51. package/src/modes/interactive-mode.ts +4 -2
  52. package/src/modes/print-mode.ts +4 -22
  53. package/src/modes/rpc/rpc-mode.ts +18 -38
  54. package/src/modes/shared.ts +10 -1
  55. package/src/modes/utils/ui-helpers.ts +6 -2
  56. package/src/plan-mode/approved-plan.ts +5 -4
  57. package/src/prompts/system/subagent-system-prompt.md +4 -4
  58. package/src/prompts/system/subagent-user-prompt.md +2 -2
  59. package/src/prompts/system/system-prompt.md +208 -243
  60. package/src/prompts/tools/apply-patch.md +67 -0
  61. package/src/prompts/tools/ast-edit.md +18 -23
  62. package/src/prompts/tools/ast-grep.md +24 -32
  63. package/src/prompts/tools/bash.md +11 -23
  64. package/src/prompts/tools/debug.md +8 -22
  65. package/src/prompts/tools/find.md +0 -4
  66. package/src/prompts/tools/grep.md +3 -5
  67. package/src/prompts/tools/hashline.md +16 -10
  68. package/src/prompts/tools/python.md +10 -14
  69. package/src/prompts/tools/read.md +17 -24
  70. package/src/prompts/tools/task.md +57 -21
  71. package/src/prompts/tools/todo-write.md +45 -67
  72. package/src/session/agent-session.ts +4 -4
  73. package/src/session/session-manager.ts +15 -7
  74. package/src/session/streaming-output.ts +24 -0
  75. package/src/slash-commands/builtin-registry.ts +3 -14
  76. package/src/task/executor.ts +13 -34
  77. package/src/task/index.ts +82 -18
  78. package/src/task/simple-mode.ts +27 -0
  79. package/src/task/template.ts +17 -3
  80. package/src/task/types.ts +77 -30
  81. package/src/tools/ask.ts +2 -4
  82. package/src/tools/ast-edit.ts +4 -15
  83. package/src/tools/ast-grep.ts +8 -27
  84. package/src/tools/bash-skill-urls.ts +9 -7
  85. package/src/tools/bash.ts +4 -12
  86. package/src/tools/browser.ts +1 -1
  87. package/src/tools/fetch.ts +1 -14
  88. package/src/tools/file-recorder.ts +35 -0
  89. package/src/tools/find.ts +6 -3
  90. package/src/tools/gh-format.ts +12 -0
  91. package/src/tools/gh-renderer.ts +1 -8
  92. package/src/tools/gh.ts +6 -13
  93. package/src/tools/grep.ts +9 -22
  94. package/src/tools/jtd-to-json-schema.ts +16 -0
  95. package/src/tools/match-line-format.ts +20 -0
  96. package/src/tools/path-utils.ts +30 -2
  97. package/src/tools/plan-mode-guard.ts +6 -5
  98. package/src/tools/python.ts +1 -1
  99. package/src/tools/read.ts +1 -1
  100. package/src/tools/render-utils.ts +38 -6
  101. package/src/tools/renderers.ts +1 -0
  102. package/src/tools/ssh.ts +3 -11
  103. package/src/tools/submit-result.ts +1 -13
  104. package/src/tools/todo-write.ts +137 -103
  105. package/src/tools/write.ts +2 -23
  106. package/src/tui/code-cell.ts +12 -7
  107. package/src/utils/edit-mode.ts +3 -2
  108. package/src/utils/git.ts +1 -1
  109. package/src/vim/engine.ts +41 -58
  110. package/src/web/scrapers/crates-io.ts +1 -14
  111. package/src/web/scrapers/types.ts +13 -0
  112. package/src/web/search/providers/base.ts +13 -0
  113. package/src/web/search/providers/brave.ts +2 -5
  114. package/src/web/search/providers/codex.ts +20 -24
  115. package/src/web/search/providers/gemini.ts +39 -1
  116. package/src/web/search/providers/jina.ts +2 -5
  117. package/src/web/search/providers/kagi.ts +3 -8
  118. package/src/web/search/providers/kimi.ts +3 -7
  119. package/src/web/search/providers/parallel.ts +3 -8
  120. package/src/web/search/providers/synthetic.ts +3 -7
  121. package/src/web/search/providers/tavily.ts +15 -11
  122. package/src/web/search/providers/utils.ts +36 -0
  123. package/src/web/search/providers/zai.ts +3 -7
@@ -10,6 +10,7 @@ import { SearchProviderError } from "../../../web/search/types";
10
10
  import { clampNumResults, dateToAgeSeconds } from "../utils";
11
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
+ import { isApiKeyAvailable } from "./utils";
13
14
 
14
15
  const BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
15
16
  const DEFAULT_NUM_RESULTS = 10;
@@ -132,11 +133,7 @@ export class BraveProvider extends SearchProvider {
132
133
  readonly label = "Brave";
133
134
 
134
135
  isAvailable() {
135
- try {
136
- return !!findApiKey();
137
- } catch {
138
- return false;
139
- }
136
+ return isApiKeyAvailable(findApiKey);
140
137
  }
141
138
 
142
139
  search(params: SearchParams): Promise<SearchResponse> {
@@ -6,6 +6,8 @@
6
6
  * Returns synthesized answers with web search sources.
7
7
  */
8
8
  import * as os from "node:os";
9
+ import { getBundledModels } from "@oh-my-pi/pi-ai";
10
+ import { decodeJwt } from "@oh-my-pi/pi-ai/utils/oauth/openai-codex";
9
11
  import { $env, getAgentDbPath, readSseJson } from "@oh-my-pi/pi-utils";
10
12
  import packageJson from "../../../../package.json" with { type: "json" };
11
13
  import { AgentStorage } from "../../../session/agent-storage";
@@ -16,14 +18,29 @@ import { SearchProvider } from "./base";
16
18
 
17
19
  const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
18
20
  const CODEX_RESPONSES_PATH = "/codex/responses";
19
- const DEFAULT_MODEL = "gpt-5-codex-mini";
21
+ const FALLBACK_MODEL = "gpt-5-codex-mini";
22
+ const DEFAULT_MODEL_PREFERENCES = [
23
+ "gpt-5-codex-mini",
24
+ "gpt-5.4",
25
+ "gpt-5.3-codex",
26
+ "gpt-5.2-codex",
27
+ "gpt-5.1-codex",
28
+ "gpt-5-codex",
29
+ ];
20
30
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
21
31
  const DEFAULT_INSTRUCTIONS =
22
32
  "You are a helpful assistant with web search capabilities. Search the web to answer the user's question accurately and cite your sources.";
23
33
 
24
34
  function getModel(): string {
25
35
  const configuredModel = $env.PI_CODEX_WEB_SEARCH_MODEL?.trim();
26
- return configuredModel ? configuredModel : DEFAULT_MODEL;
36
+ if (configuredModel) return configuredModel;
37
+
38
+ const bundledModels = getBundledModels("openai-codex");
39
+ const bundledIds = new Set(bundledModels.map(model => model.id));
40
+ const preferred = DEFAULT_MODEL_PREFERENCES.find(modelId => bundledIds.has(modelId));
41
+ if (preferred) return preferred;
42
+ const nonMini = bundledModels.find(model => !model.id.includes("mini") && !model.id.includes("spark"));
43
+ return nonMini?.id ?? bundledModels[0]?.id ?? FALLBACK_MODEL;
27
44
  }
28
45
 
29
46
  export interface CodexSearchParams {
@@ -44,11 +61,6 @@ interface CodexOAuthCredential {
44
61
  accountId?: string;
45
62
  }
46
63
 
47
- /** JWT payload structure for extracting account ID */
48
- type JwtPayload = {
49
- [key: string]: unknown;
50
- };
51
-
52
64
  /** Codex API response structure */
53
65
  interface CodexResponseItem {
54
66
  type: string;
@@ -94,23 +106,6 @@ function isImagePlaceholderAnswer(text: string): boolean {
94
106
  return text.trim().toLowerCase() === "(see attached image)";
95
107
  }
96
108
 
97
- /**
98
- * Decodes a JWT token and extracts the payload.
99
- * @param token - JWT token string
100
- * @returns Decoded payload, or null if parsing fails
101
- */
102
- function decodeJwt(token: string): JwtPayload | null {
103
- try {
104
- const parts = token.split(".");
105
- if (parts.length !== 3) return null;
106
- const payload = parts[1] ?? "";
107
- const decoded = Buffer.from(payload, "base64").toString("utf-8");
108
- return JSON.parse(decoded) as JwtPayload;
109
- } catch {
110
- return null;
111
- }
112
- }
113
-
114
109
  /**
115
110
  * Extracts account ID from a Codex access token.
116
111
  * @param accessToken - JWT access token
@@ -223,6 +218,7 @@ async function callCodexSearch(
223
218
  method: "POST",
224
219
  headers,
225
220
  body: JSON.stringify(body),
221
+ signal: options.signal,
226
222
  });
227
223
 
228
224
  if (!response.ok) {
@@ -10,6 +10,7 @@ import {
10
10
  extractRetryDelay,
11
11
  getAntigravityHeaders,
12
12
  getGeminiCliHeaders,
13
+ refreshAntigravityToken,
13
14
  refreshGoogleCloudToken,
14
15
  } from "@oh-my-pi/pi-ai";
15
16
  import { getAgentDbPath } from "@oh-my-pi/pi-utils";
@@ -72,6 +73,30 @@ interface GeminiAuth {
72
73
  isAntigravity: boolean;
73
74
  storage: AgentStorage;
74
75
  credentialId: number;
76
+ credential: GeminiOAuthCredential;
77
+ }
78
+
79
+ async function refreshGeminiAuth(auth: GeminiAuth): Promise<boolean> {
80
+ if (!auth.refreshToken) return false;
81
+ try {
82
+ const refreshed = auth.isAntigravity
83
+ ? await refreshAntigravityToken(auth.refreshToken, auth.projectId)
84
+ : await refreshGoogleCloudToken(auth.refreshToken, auth.projectId);
85
+ auth.accessToken = refreshed.access;
86
+ auth.refreshToken = refreshed.refresh ?? auth.refreshToken;
87
+ auth.storage.updateAuthCredential(auth.credentialId, {
88
+ ...auth.credential,
89
+ access: auth.accessToken,
90
+ refresh: auth.refreshToken,
91
+ expires: refreshed.expires,
92
+ });
93
+ auth.credential.access = auth.accessToken;
94
+ auth.credential.refresh = auth.refreshToken;
95
+ auth.credential.expires = refreshed.expires;
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
75
100
  }
76
101
 
77
102
  /**
@@ -108,7 +133,10 @@ export async function findGeminiAuth(): Promise<GeminiAuth | null> {
108
133
  // Try to refresh if we have a refresh token
109
134
  if (oauthCred.refresh) {
110
135
  try {
111
- const refreshed = await refreshGoogleCloudToken(oauthCred.refresh, projectId);
136
+ const refreshed =
137
+ provider === "google-antigravity"
138
+ ? await refreshAntigravityToken(oauthCred.refresh, projectId)
139
+ : await refreshGoogleCloudToken(oauthCred.refresh, projectId);
112
140
  // Update the credential in storage
113
141
  const updated = {
114
142
  ...oauthCred,
@@ -124,6 +152,7 @@ export async function findGeminiAuth(): Promise<GeminiAuth | null> {
124
152
  isAntigravity: provider === "google-antigravity",
125
153
  storage,
126
154
  credentialId: record.id,
155
+ credential: updated,
127
156
  };
128
157
  } catch {
129
158
  // Refresh failed, skip this credential
@@ -141,6 +170,7 @@ export async function findGeminiAuth(): Promise<GeminiAuth | null> {
141
170
  isAntigravity: provider === "google-antigravity",
142
171
  storage,
143
172
  credentialId: record.id,
173
+ credential: oauthCred,
144
174
  };
145
175
  }
146
176
  }
@@ -310,6 +340,14 @@ async function callGeminiSearch(
310
340
  }
311
341
 
312
342
  const errorText = await response.text();
343
+ const canRefreshAuth =
344
+ response.status === 401 ||
345
+ response.status === 403 ||
346
+ (response.status === 400 &&
347
+ /api key not valid|invalid credentials|invalid authentication/i.test(errorText));
348
+ if (canRefreshAuth && attempt === 0 && (await refreshGeminiAuth(auth))) {
349
+ continue;
350
+ }
313
351
  const isRetryableStatus =
314
352
  response.status === 429 ||
315
353
  response.status === 500 ||
@@ -10,6 +10,7 @@ import type { SearchResponse, SearchSource } from "../../../web/search/types";
10
10
  import { SearchProviderError } from "../../../web/search/types";
11
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
+ import { isApiKeyAvailable } from "./utils";
13
14
 
14
15
  const JINA_SEARCH_URL = "https://s.jina.ai";
15
16
 
@@ -83,11 +84,7 @@ export class JinaProvider extends SearchProvider {
83
84
  readonly label = "Jina";
84
85
 
85
86
  isAvailable() {
86
- try {
87
- return !!findApiKey();
88
- } catch {
89
- return false;
90
- }
87
+ return isApiKeyAvailable(findApiKey);
91
88
  }
92
89
 
93
90
  search(params: SearchParams): Promise<SearchResponse> {
@@ -6,9 +6,10 @@
6
6
  import type { SearchResponse } from "../../../web/search/types";
7
7
  import { SearchProviderError } from "../../../web/search/types";
8
8
  import { findKagiApiKey, KagiApiError, searchWithKagi } from "../../kagi";
9
- import { clampNumResults, dateToAgeSeconds } from "../utils";
9
+ import { clampNumResults } from "../utils";
10
10
  import type { SearchParams } from "./base";
11
11
  import { SearchProvider } from "./base";
12
+ import { toSearchSources } from "./utils";
12
13
 
13
14
  const DEFAULT_NUM_RESULTS = 10;
14
15
  const MAX_NUM_RESULTS = 40;
@@ -29,13 +30,7 @@ export async function searchKagi(params: {
29
30
 
30
31
  return {
31
32
  provider: "kagi",
32
- sources: result.sources.slice(0, numResults).map(source => ({
33
- title: source.title,
34
- url: source.url,
35
- snippet: source.snippet,
36
- publishedDate: source.publishedDate,
37
- ageSeconds: dateToAgeSeconds(source.publishedDate),
38
- })),
33
+ sources: toSearchSources(result.sources, numResults),
39
34
  relatedQuestions: result.relatedQuestions.length > 0 ? result.relatedQuestions : undefined,
40
35
  requestId: result.requestId,
41
36
  };
@@ -11,7 +11,7 @@ import { SearchProviderError } from "../../../web/search/types";
11
11
  import { clampNumResults, dateToAgeSeconds } from "../utils";
12
12
  import type { SearchParams } from "./base";
13
13
  import { SearchProvider } from "./base";
14
- import { findCredential } from "./utils";
14
+ import { findCredential, isApiKeyAvailable } from "./utils";
15
15
 
16
16
  const KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search";
17
17
 
@@ -139,12 +139,8 @@ export class KimiProvider extends SearchProvider {
139
139
  readonly id = "kimi";
140
140
  readonly label = "Kimi";
141
141
 
142
- async isAvailable(): Promise<boolean> {
143
- try {
144
- return !!(await findApiKey());
145
- } catch {
146
- return false;
147
- }
142
+ isAvailable(): Promise<boolean> {
143
+ return isApiKeyAvailable(findApiKey);
148
144
  }
149
145
 
150
146
  search(params: SearchParams): Promise<SearchResponse> {
@@ -1,9 +1,10 @@
1
1
  import type { SearchResponse } from "../../../web/search/types";
2
2
  import { SearchProviderError } from "../../../web/search/types";
3
3
  import { findParallelApiKey, ParallelApiError, searchWithParallel } from "../../parallel";
4
- import { clampNumResults, dateToAgeSeconds } from "../utils";
4
+ import { clampNumResults } from "../utils";
5
5
  import type { SearchParams } from "./base";
6
6
  import { SearchProvider } from "./base";
7
+ import { toSearchSources } from "./utils";
7
8
 
8
9
  const DEFAULT_NUM_RESULTS = 10;
9
10
  const MAX_NUM_RESULTS = 40;
@@ -24,13 +25,7 @@ export async function searchParallel(params: {
24
25
 
25
26
  return {
26
27
  provider: "parallel",
27
- sources: result.sources.slice(0, numResults).map(source => ({
28
- title: source.title,
29
- url: source.url,
30
- snippet: source.snippet,
31
- publishedDate: source.publishedDate,
32
- ageSeconds: dateToAgeSeconds(source.publishedDate),
33
- })),
28
+ sources: toSearchSources(result.sources, numResults),
34
29
  requestId: result.requestId,
35
30
  };
36
31
  } catch (err) {
@@ -10,7 +10,7 @@ import type { SearchResponse, SearchSource } from "../../../web/search/types";
10
10
  import { SearchProviderError } from "../../../web/search/types";
11
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
- import { findCredential } from "./utils";
13
+ import { findCredential, isApiKeyAvailable } from "./utils";
14
14
 
15
15
  const SYNTHETIC_SEARCH_URL = "https://api.synthetic.new/v2/search";
16
16
 
@@ -95,12 +95,8 @@ export class SyntheticProvider extends SearchProvider {
95
95
  readonly id = "synthetic";
96
96
  readonly label = "Synthetic";
97
97
 
98
- async isAvailable(): Promise<boolean> {
99
- try {
100
- return !!(await findApiKey());
101
- } catch {
102
- return false;
103
- }
98
+ isAvailable(): Promise<boolean> {
99
+ return isApiKeyAvailable(findApiKey);
104
100
  }
105
101
 
106
102
  search(params: SearchParams): Promise<SearchResponse> {
@@ -10,7 +10,7 @@ import { SearchProviderError } from "../../../web/search/types";
10
10
  import { clampNumResults, dateToAgeSeconds } from "../utils";
11
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
- import { findCredential } from "./utils";
13
+ import { findCredential, isApiKeyAvailable } from "./utils";
14
14
 
15
15
  const TAVILY_SEARCH_URL = "https://api.tavily.com/search";
16
16
  const DEFAULT_NUM_RESULTS = 5;
@@ -63,17 +63,25 @@ export async function findApiKey(): Promise<string | null> {
63
63
  return findCredential(getEnvApiKey("tavily"), "tavily");
64
64
  }
65
65
 
66
- function buildRequestBody(params: TavilySearchParams): Record<string, unknown> {
66
+ /** Exported for testing. Builds the Tavily request body from unified params. */
67
+ export function buildRequestBody(params: TavilySearchParams): Record<string, unknown> {
67
68
  const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
68
- return {
69
+ // Tavily's `topic` (general/news/finance) and `time_range` are orthogonal
70
+ // dimensions in the upstream API. Recency is a temporal filter only; it must
71
+ // not narrow the index to news-only, which would break technical queries
72
+ // (release notes, docs, GitHub) whenever a user sets --recency. Always use
73
+ // the default "general" topic and only send `time_range` when recency is set.
74
+ const body: Record<string, unknown> = {
69
75
  query: params.query,
70
76
  search_depth: "basic",
71
- topic: params.recency ? "news" : "general",
72
- time_range: params.recency,
73
77
  max_results: numResults,
74
78
  include_answer: "advanced",
75
79
  include_raw_content: false,
76
80
  };
81
+ if (params.recency) {
82
+ body.time_range = params.recency;
83
+ }
84
+ return body;
77
85
  }
78
86
 
79
87
  async function callTavilySearch(apiKey: string, params: TavilySearchParams): Promise<TavilySearchResponse> {
@@ -143,12 +151,8 @@ export class TavilyProvider extends SearchProvider {
143
151
  readonly id = "tavily";
144
152
  readonly label = "Tavily";
145
153
 
146
- async isAvailable(): Promise<boolean> {
147
- try {
148
- return !!(await findApiKey());
149
- } catch {
150
- return false;
151
- }
154
+ isAvailable(): Promise<boolean> {
155
+ return isApiKeyAvailable(findApiKey);
152
156
  }
153
157
 
154
158
  search(params: SearchParams): Promise<SearchResponse> {
@@ -1,5 +1,7 @@
1
1
  import { getAgentDbPath } from "@oh-my-pi/pi-utils";
2
2
  import { AgentStorage } from "../../../session/agent-storage";
3
+ import type { SearchSource } from "../../../web/search/types";
4
+ import { dateToAgeSeconds } from "../utils";
3
5
 
4
6
  /**
5
7
  * Search for an API credential by checking an env-derived key first,
@@ -34,3 +36,37 @@ export async function findCredential(
34
36
 
35
37
  return null;
36
38
  }
39
+
40
+ /**
41
+ * Probe whether a provider's API key lookup resolves to a truthy value.
42
+ * Swallows lookup errors and reports unavailability.
43
+ */
44
+ export async function isApiKeyAvailable(findApiKey: () => string | null | Promise<string | null>) {
45
+ try {
46
+ return !!(await findApiKey());
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Map a provider's raw source list to the unified SearchSource shape,
54
+ * clamped to the requested result count and annotated with ageSeconds.
55
+ */
56
+ export function toSearchSources(
57
+ sources: ReadonlyArray<{
58
+ title: string;
59
+ url: string;
60
+ snippet?: string;
61
+ publishedDate?: string;
62
+ }>,
63
+ numResults: number,
64
+ ): SearchSource[] {
65
+ return sources.slice(0, numResults).map(source => ({
66
+ title: source.title,
67
+ url: source.url,
68
+ snippet: source.snippet,
69
+ publishedDate: source.publishedDate,
70
+ ageSeconds: dateToAgeSeconds(source.publishedDate),
71
+ }));
72
+ }
@@ -11,7 +11,7 @@ import { SearchProviderError } from "../../../web/search/types";
11
11
  import { dateToAgeSeconds } from "../utils";
12
12
  import type { SearchParams } from "./base";
13
13
  import { SearchProvider } from "./base";
14
- import { findCredential } from "./utils";
14
+ import { findCredential, isApiKeyAvailable } from "./utils";
15
15
 
16
16
  const ZAI_MCP_URL = "https://api.z.ai/api/mcp/web_search_prime/mcp";
17
17
  const ZAI_TOOL_NAME = "webSearchPrime";
@@ -294,12 +294,8 @@ export class ZaiProvider extends SearchProvider {
294
294
  readonly id = "zai";
295
295
  readonly label = "Z.AI";
296
296
 
297
- async isAvailable(): Promise<boolean> {
298
- try {
299
- return !!(await findApiKey());
300
- } catch {
301
- return false;
302
- }
297
+ isAvailable(): Promise<boolean> {
298
+ return isApiKeyAvailable(findApiKey);
303
299
  }
304
300
 
305
301
  search(params: SearchParams): Promise<SearchResponse> {