@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.
- package/CHANGELOG.md +47 -2
- package/package.json +8 -8
- package/scripts/build-binary.ts +61 -0
- package/src/autoresearch/helpers.ts +10 -0
- package/src/autoresearch/index.ts +1 -11
- package/src/autoresearch/tools/init-experiment.ts +1 -10
- package/src/autoresearch/tools/log-experiment.ts +1 -11
- package/src/autoresearch/tools/run-experiment.ts +1 -10
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/plugin-cli.ts +23 -45
- package/src/commit/agentic/tools/propose-commit.ts +1 -14
- package/src/commit/agentic/tools/split-commit.ts +1 -15
- package/src/commit/utils.ts +15 -1
- package/src/config/model-registry.ts +3 -3
- package/src/config/prompt-templates.ts +4 -12
- package/src/config/settings-schema.ts +27 -2
- package/src/config/settings.ts +1 -1
- package/src/discovery/claude-plugins.ts +61 -6
- package/src/discovery/codex.ts +2 -15
- package/src/discovery/gemini.ts +2 -15
- package/src/discovery/helpers.ts +40 -1
- package/src/discovery/opencode.ts +2 -15
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +3 -14
- package/src/edit/index.ts +65 -2
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +63 -0
- package/src/edit/modes/hashline.ts +3 -3
- package/src/edit/modes/replace.ts +2 -13
- package/src/edit/read-file.ts +18 -0
- package/src/edit/renderer.ts +61 -33
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/runner.ts +11 -29
- package/src/extensibility/utils.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/lsp/render.ts +14 -2
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +29 -48
- package/src/memories/index.ts +7 -1
- package/src/modes/acp/acp-agent.ts +3 -16
- package/src/modes/components/model-selector.ts +15 -24
- package/src/modes/components/plugin-settings.ts +16 -5
- package/src/modes/components/read-tool-group.ts +92 -9
- package/src/modes/components/settings-defs.ts +18 -0
- package/src/modes/components/settings-selector.ts +2 -6
- package/src/modes/components/tool-execution.ts +61 -28
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +99 -150
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/print-mode.ts +4 -22
- package/src/modes/rpc/rpc-mode.ts +18 -38
- package/src/modes/shared.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +6 -2
- package/src/plan-mode/approved-plan.ts +5 -4
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/subagent-user-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +208 -243
- package/src/prompts/tools/apply-patch.md +67 -0
- package/src/prompts/tools/ast-edit.md +18 -23
- package/src/prompts/tools/ast-grep.md +24 -32
- package/src/prompts/tools/bash.md +11 -23
- package/src/prompts/tools/debug.md +8 -22
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/grep.md +3 -5
- package/src/prompts/tools/hashline.md +16 -10
- package/src/prompts/tools/python.md +10 -14
- package/src/prompts/tools/read.md +17 -24
- package/src/prompts/tools/task.md +57 -21
- package/src/prompts/tools/todo-write.md +45 -67
- package/src/session/agent-session.ts +4 -4
- package/src/session/session-manager.ts +15 -7
- package/src/session/streaming-output.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +3 -14
- package/src/task/executor.ts +13 -34
- package/src/task/index.ts +82 -18
- package/src/task/simple-mode.ts +27 -0
- package/src/task/template.ts +17 -3
- package/src/task/types.ts +77 -30
- package/src/tools/ask.ts +2 -4
- package/src/tools/ast-edit.ts +4 -15
- package/src/tools/ast-grep.ts +8 -27
- package/src/tools/bash-skill-urls.ts +9 -7
- package/src/tools/bash.ts +4 -12
- package/src/tools/browser.ts +1 -1
- package/src/tools/fetch.ts +1 -14
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +6 -3
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +1 -8
- package/src/tools/gh.ts +6 -13
- package/src/tools/grep.ts +9 -22
- package/src/tools/jtd-to-json-schema.ts +16 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/path-utils.ts +30 -2
- package/src/tools/plan-mode-guard.ts +6 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +38 -6
- package/src/tools/renderers.ts +1 -0
- package/src/tools/ssh.ts +3 -11
- package/src/tools/submit-result.ts +1 -13
- package/src/tools/todo-write.ts +137 -103
- package/src/tools/write.ts +2 -23
- package/src/tui/code-cell.ts +12 -7
- package/src/utils/edit-mode.ts +3 -2
- package/src/utils/git.ts +1 -1
- package/src/vim/engine.ts +41 -58
- package/src/web/scrapers/crates-io.ts +1 -14
- package/src/web/scrapers/types.ts +13 -0
- package/src/web/search/providers/base.ts +13 -0
- package/src/web/search/providers/brave.ts +2 -5
- package/src/web/search/providers/codex.ts +20 -24
- package/src/web/search/providers/gemini.ts +39 -1
- package/src/web/search/providers/jina.ts +2 -5
- package/src/web/search/providers/kagi.ts +3 -8
- package/src/web/search/providers/kimi.ts +3 -7
- package/src/web/search/providers/parallel.ts +3 -8
- package/src/web/search/providers/synthetic.ts +3 -7
- package/src/web/search/providers/tavily.ts +15 -11
- package/src/web/search/providers/utils.ts +36 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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> {
|