@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.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 +146 -198
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +5 -5
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +62 -46
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
package/src/web/kagi.ts
CHANGED
|
@@ -1,42 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kagi API Client
|
|
3
|
+
*
|
|
4
|
+
* Implements the Kagi V1 Search API (POST /api/v1/search), the public-preview
|
|
5
|
+
* successor to the sunset V0 endpoint. Authentication is resolved exclusively
|
|
6
|
+
* through the shared {@link AuthStorage} broker (Bearer token), and responses
|
|
7
|
+
* are categorized result buckets rather than the legacy flat object array.
|
|
8
|
+
*/
|
|
1
9
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
10
|
import { withHardTimeout } from "./search/providers/utils";
|
|
3
11
|
|
|
4
|
-
const KAGI_SEARCH_URL = "https://kagi.com/api/
|
|
12
|
+
const KAGI_SEARCH_URL = "https://kagi.com/api/v1/search";
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Request / Response Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** V1 search request body. */
|
|
19
|
+
export interface KagiSearchRequest {
|
|
20
|
+
query: string;
|
|
21
|
+
/** Workflow mode: "search" | "research". */
|
|
22
|
+
workflow?: string;
|
|
23
|
+
/** Number of results (1-100). */
|
|
24
|
+
limit?: number;
|
|
25
|
+
/** Lens identifier (e.g. "news", "reddit"). */
|
|
26
|
+
lens?: string;
|
|
27
|
+
/** Time-based filters as ISO date strings (YYYY-MM-DD). */
|
|
28
|
+
filters?: {
|
|
29
|
+
after?: string;
|
|
30
|
+
before?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Individual V1 result item. */
|
|
35
|
+
export interface KagiSearchResultItem {
|
|
8
36
|
url: string;
|
|
9
37
|
title: string;
|
|
10
38
|
snippet?: string;
|
|
11
|
-
|
|
39
|
+
/** ISO timestamp or relative string ("2h ago"). */
|
|
40
|
+
time?: string;
|
|
41
|
+
/** Thumbnail image. */
|
|
42
|
+
image?: { url: string; height?: number; width?: number };
|
|
43
|
+
/** Extra metadata key-value pairs. */
|
|
44
|
+
props?: Record<string, unknown>;
|
|
12
45
|
}
|
|
13
46
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
47
|
+
/** V1 categorizes results into named buckets; only consumed buckets are typed. */
|
|
48
|
+
export interface KagiSearchData {
|
|
49
|
+
search?: KagiSearchResultItem[];
|
|
50
|
+
video?: KagiSearchResultItem[];
|
|
51
|
+
news?: KagiSearchResultItem[];
|
|
52
|
+
infobox?: KagiSearchResultItem[];
|
|
53
|
+
adjacent_question?: KagiSearchResultItem[];
|
|
54
|
+
related_search?: KagiSearchResultItem[];
|
|
55
|
+
direct_answer?: KagiSearchResultItem[];
|
|
17
56
|
}
|
|
18
57
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
interface KagiErrorEntry {
|
|
58
|
+
/** V1 error entry. */
|
|
59
|
+
export interface KagiErrorEntry {
|
|
22
60
|
code?: number;
|
|
61
|
+
url?: string;
|
|
62
|
+
message?: string;
|
|
23
63
|
msg?: string;
|
|
64
|
+
location?: string;
|
|
24
65
|
}
|
|
25
66
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
67
|
+
/** V1 success response. */
|
|
68
|
+
export interface KagiSearchResponse {
|
|
69
|
+
meta?: {
|
|
70
|
+
trace?: string;
|
|
71
|
+
id?: string;
|
|
72
|
+
ms?: number;
|
|
29
73
|
};
|
|
30
|
-
data
|
|
74
|
+
data?: KagiSearchData;
|
|
31
75
|
error?: KagiErrorEntry[];
|
|
32
76
|
}
|
|
33
77
|
|
|
34
|
-
|
|
78
|
+
/** V1 error response. */
|
|
79
|
+
export interface KagiErrorResponse {
|
|
80
|
+
meta?: Record<string, unknown>;
|
|
35
81
|
error?: string | KagiErrorEntry[];
|
|
36
82
|
message?: string;
|
|
37
83
|
detail?: string;
|
|
38
84
|
}
|
|
39
85
|
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Error Handling
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
40
90
|
export class KagiApiError extends Error {
|
|
41
91
|
readonly statusCode?: number;
|
|
42
92
|
|
|
@@ -64,9 +114,11 @@ function extractKagiErrorMessage(payload: unknown): string | null {
|
|
|
64
114
|
if (Array.isArray(record.error)) {
|
|
65
115
|
for (const entry of record.error) {
|
|
66
116
|
if (!entry || typeof entry !== "object") continue;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
117
|
+
const e = entry as Record<string, unknown>;
|
|
118
|
+
for (const value of [e.message, e.msg]) {
|
|
119
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
120
|
+
return value.trim();
|
|
121
|
+
}
|
|
70
122
|
}
|
|
71
123
|
}
|
|
72
124
|
}
|
|
@@ -82,21 +134,26 @@ function createKagiApiError(statusCode: number, detail?: string): KagiApiError {
|
|
|
82
134
|
}
|
|
83
135
|
|
|
84
136
|
function parseKagiErrorResponse(statusCode: number, responseText: string): KagiApiError {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
137
|
+
const trimmed = responseText.trim();
|
|
138
|
+
if (trimmed.length === 0) {
|
|
87
139
|
return createKagiApiError(statusCode);
|
|
88
140
|
}
|
|
89
141
|
|
|
90
142
|
try {
|
|
91
|
-
const payload = JSON.parse(
|
|
92
|
-
return createKagiApiError(statusCode, extractKagiErrorMessage(payload) ??
|
|
143
|
+
const payload = JSON.parse(trimmed) as KagiErrorResponse;
|
|
144
|
+
return createKagiApiError(statusCode, extractKagiErrorMessage(payload) ?? trimmed);
|
|
93
145
|
} catch {
|
|
94
|
-
return createKagiApiError(statusCode,
|
|
146
|
+
return createKagiApiError(statusCode, trimmed);
|
|
95
147
|
}
|
|
96
148
|
}
|
|
97
149
|
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Public API
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
98
154
|
export interface KagiSearchOptions {
|
|
99
155
|
limit?: number;
|
|
156
|
+
recency?: "day" | "week" | "month" | "year";
|
|
100
157
|
sessionId?: string;
|
|
101
158
|
signal?: AbortSignal;
|
|
102
159
|
}
|
|
@@ -112,6 +169,7 @@ export interface KagiSearchResult {
|
|
|
112
169
|
requestId: string;
|
|
113
170
|
sources: KagiSearchSource[];
|
|
114
171
|
relatedQuestions: string[];
|
|
172
|
+
answer?: string;
|
|
115
173
|
}
|
|
116
174
|
|
|
117
175
|
export async function findKagiApiKey(
|
|
@@ -122,11 +180,65 @@ export async function findKagiApiKey(
|
|
|
122
180
|
return (await authStorage.getApiKey("kagi", sessionId, { signal })) ?? null;
|
|
123
181
|
}
|
|
124
182
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Compute a YYYY-MM-DD date string `recency` units before now, in UTC.
|
|
185
|
+
* UTC keeps the recency window deterministic regardless of host timezone and
|
|
186
|
+
* matches Kagi's date-formatted `filters.after`. Date setters handle month
|
|
187
|
+
* drift (Mar 31 −1mo → Feb 28/29) and leap years correctly.
|
|
188
|
+
*/
|
|
189
|
+
function recencyToDate(recency: "day" | "week" | "month" | "year"): string {
|
|
190
|
+
const d = new Date();
|
|
191
|
+
switch (recency) {
|
|
192
|
+
case "day":
|
|
193
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
194
|
+
break;
|
|
195
|
+
case "week":
|
|
196
|
+
d.setUTCDate(d.getUTCDate() - 7);
|
|
197
|
+
break;
|
|
198
|
+
case "month":
|
|
199
|
+
d.setUTCMonth(d.getUTCMonth() - 1);
|
|
200
|
+
break;
|
|
201
|
+
case "year":
|
|
202
|
+
d.setUTCFullYear(d.getUTCFullYear() - 1);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const yyyy = d.getUTCFullYear();
|
|
206
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
207
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
208
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildRequestBody(query: string, options: KagiSearchOptions): KagiSearchRequest {
|
|
212
|
+
const req: KagiSearchRequest = {
|
|
213
|
+
query,
|
|
214
|
+
workflow: "search",
|
|
215
|
+
limit: options.limit,
|
|
129
216
|
};
|
|
217
|
+
|
|
218
|
+
if (options.recency) {
|
|
219
|
+
req.filters = { after: recencyToDate(options.recency) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return req;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Push every item in a result bucket as a source, with an optional title tag. */
|
|
226
|
+
function collectSources(sources: KagiSearchSource[], items: KagiSearchResultItem[] | undefined, tag?: string): void {
|
|
227
|
+
if (!items) return;
|
|
228
|
+
for (const item of items) {
|
|
229
|
+
sources.push({
|
|
230
|
+
title: tag ? `${tag} ${item.title}` : item.title,
|
|
231
|
+
url: item.url,
|
|
232
|
+
snippet: item.snippet,
|
|
233
|
+
publishedDate: item.time,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Pull a related/adjacent question from an item's props or fall back to title. */
|
|
239
|
+
function questionOf(item: KagiSearchResultItem): string | undefined {
|
|
240
|
+
const q = item.props?.question ?? item.props?.query ?? item.title;
|
|
241
|
+
return typeof q === "string" && q.length > 0 ? q : undefined;
|
|
130
242
|
}
|
|
131
243
|
|
|
132
244
|
export async function searchWithKagi(
|
|
@@ -139,45 +251,52 @@ export async function searchWithKagi(
|
|
|
139
251
|
throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
|
|
140
252
|
}
|
|
141
253
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
254
|
+
const response = await fetch(KAGI_SEARCH_URL, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: {
|
|
257
|
+
Authorization: `Bearer ${apiKey}`,
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
Accept: "application/json",
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify(buildRequestBody(query, options)),
|
|
150
262
|
signal: withHardTimeout(options.signal),
|
|
151
263
|
});
|
|
264
|
+
|
|
152
265
|
if (!response.ok) {
|
|
153
266
|
throw parseKagiErrorResponse(response.status, await response.text());
|
|
154
267
|
}
|
|
155
268
|
|
|
156
269
|
const payload = (await response.json()) as KagiSearchResponse;
|
|
157
270
|
if (payload.error && payload.error.length > 0) {
|
|
158
|
-
const
|
|
159
|
-
throw createKagiApiError(
|
|
271
|
+
const first = payload.error[0];
|
|
272
|
+
throw createKagiApiError(first.code ?? response.status, extractKagiErrorMessage(payload) ?? first.message);
|
|
160
273
|
}
|
|
161
274
|
|
|
275
|
+
const data = payload.data;
|
|
162
276
|
const sources: KagiSearchSource[] = [];
|
|
163
277
|
const relatedQuestions: string[] = [];
|
|
164
278
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
279
|
+
collectSources(sources, data?.search);
|
|
280
|
+
collectSources(sources, data?.video, "[Video]");
|
|
281
|
+
collectSources(sources, data?.news, "[News]");
|
|
282
|
+
collectSources(sources, data?.infobox, "[Info]");
|
|
283
|
+
|
|
284
|
+
for (const item of data?.adjacent_question ?? []) {
|
|
285
|
+
const q = questionOf(item);
|
|
286
|
+
if (q) relatedQuestions.push(q);
|
|
287
|
+
}
|
|
288
|
+
for (const item of data?.related_search ?? []) {
|
|
289
|
+
const q = questionOf(item);
|
|
290
|
+
if (q) relatedQuestions.push(q);
|
|
176
291
|
}
|
|
177
292
|
|
|
293
|
+
const directAnswer = data?.direct_answer?.[0];
|
|
294
|
+
const answer = directAnswer ? (directAnswer.snippet ?? directAnswer.title) : undefined;
|
|
295
|
+
|
|
178
296
|
return {
|
|
179
|
-
requestId: payload.meta.id,
|
|
297
|
+
requestId: payload.meta?.trace ?? payload.meta?.id ?? "",
|
|
180
298
|
sources,
|
|
181
299
|
relatedQuestions,
|
|
300
|
+
answer,
|
|
182
301
|
};
|
|
183
302
|
}
|
|
@@ -255,7 +255,7 @@ export async function searchAnthropic(
|
|
|
255
255
|
const apiKey = await params.authStorage.getApiKey("anthropic", params.sessionId, {
|
|
256
256
|
signal: params.signal,
|
|
257
257
|
});
|
|
258
|
-
if (apiKey) auth = buildAnthropicAuthConfig(apiKey);
|
|
258
|
+
if (apiKey) auth = buildAnthropicAuthConfig(apiKey, searchBaseUrl);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
if (!auth) {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { settings } from "../../../config/settings";
|
|
11
|
-
import { callExaTool, findApiKey, isSearchResponse } from "../../../exa/mcp-client";
|
|
12
11
|
|
|
13
12
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
14
13
|
import { SearchProviderError } from "../../../web/search/types";
|
|
@@ -32,6 +31,12 @@ export interface ExaSearchParams {
|
|
|
32
31
|
start_published_date?: string;
|
|
33
32
|
end_published_date?: string;
|
|
34
33
|
signal?: AbortSignal;
|
|
34
|
+
/**
|
|
35
|
+
* Credential source. Resolved before falling back to `EXA_API_KEY` so
|
|
36
|
+
* Exa works when the key is stored via the broker/auth pipeline.
|
|
37
|
+
*/
|
|
38
|
+
authStorage?: AuthStorage;
|
|
39
|
+
sessionId?: string;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
interface ExaSearchResult {
|
|
@@ -52,72 +57,6 @@ interface ExaSearchResponse {
|
|
|
52
57
|
searchTime?: number;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
56
|
-
if (typeof value !== "object" || value === null) return null;
|
|
57
|
-
return value as Record<string, unknown>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseOptionalField(section: string, label: string): string | null | undefined {
|
|
61
|
-
const regex = new RegExp(`(?:^|\\n)${label}:\\s*([^\\n]*)`);
|
|
62
|
-
const match = section.match(regex);
|
|
63
|
-
if (!match) return undefined;
|
|
64
|
-
const value = match[1].trim();
|
|
65
|
-
return value.length > 0 ? value : null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseTextField(section: string): string | null | undefined {
|
|
69
|
-
const match = section.match(/(?:^|\n)Text:\s*([\s\S]*)$/);
|
|
70
|
-
if (!match) return undefined;
|
|
71
|
-
const value = match[1].trim();
|
|
72
|
-
return value.length > 0 ? value : null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parseExaMcpTextPayload(payload: unknown): ExaSearchResponse | null {
|
|
76
|
-
const root = asRecord(payload);
|
|
77
|
-
if (!root) return null;
|
|
78
|
-
|
|
79
|
-
const content = root.content;
|
|
80
|
-
if (!Array.isArray(content)) return null;
|
|
81
|
-
|
|
82
|
-
const textBlocks = content
|
|
83
|
-
.map(item => {
|
|
84
|
-
const part = asRecord(item);
|
|
85
|
-
const text = typeof part?.text === "string" ? part.text : "";
|
|
86
|
-
return text.replace(/\r\n?/g, "\n").trim();
|
|
87
|
-
})
|
|
88
|
-
.filter(text => text.length > 0);
|
|
89
|
-
|
|
90
|
-
if (textBlocks.length === 0) return null;
|
|
91
|
-
|
|
92
|
-
const sections = textBlocks
|
|
93
|
-
.join("\n\n")
|
|
94
|
-
.split(/\n{2,}(?=Title:\s*[^\n]*(?:\n(?:URL|Author|Published Date|Text):))/)
|
|
95
|
-
.map(section => section.trim())
|
|
96
|
-
.filter(section => section.startsWith("Title:"));
|
|
97
|
-
|
|
98
|
-
const results: ExaSearchResult[] = [];
|
|
99
|
-
for (const section of sections) {
|
|
100
|
-
const title = parseOptionalField(section, "Title");
|
|
101
|
-
const url = parseOptionalField(section, "URL");
|
|
102
|
-
const author = parseOptionalField(section, "Author");
|
|
103
|
-
const publishedDate = parseOptionalField(section, "Published Date");
|
|
104
|
-
const text = parseTextField(section);
|
|
105
|
-
|
|
106
|
-
if (!title && !url && !text) continue;
|
|
107
|
-
|
|
108
|
-
results.push({
|
|
109
|
-
title: title ?? undefined,
|
|
110
|
-
url: url ?? undefined,
|
|
111
|
-
author: author ?? undefined,
|
|
112
|
-
publishedDate: publishedDate ?? undefined,
|
|
113
|
-
text: text ?? undefined,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (results.length === 0) return null;
|
|
118
|
-
return { results };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
60
|
export function normalizeSearchType(type: ExaSearchParamType | undefined): ExaSearchType {
|
|
122
61
|
if (!type) return "auto";
|
|
123
62
|
if (type === "keyword") return "fast";
|
|
@@ -195,24 +134,17 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
|
|
|
195
134
|
return response.json() as Promise<ExaSearchResponse>;
|
|
196
135
|
}
|
|
197
136
|
|
|
198
|
-
async function callExaMcpSearch(params: ExaSearchParams): Promise<ExaSearchResponse> {
|
|
199
|
-
const response = await callExaTool("web_search_exa", { ...params }, findApiKey());
|
|
200
|
-
if (isSearchResponse(response)) {
|
|
201
|
-
return response as ExaSearchResponse;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const parsed = parseExaMcpTextPayload(response);
|
|
205
|
-
if (parsed) {
|
|
206
|
-
return parsed;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
throw new Error("Exa MCP search returned unexpected response shape.");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
137
|
/** Execute Exa web search */
|
|
213
138
|
export async function searchExa(params: ExaSearchParams): Promise<SearchResponse> {
|
|
214
|
-
const
|
|
215
|
-
|
|
139
|
+
const storedKey = params.authStorage
|
|
140
|
+
? await params.authStorage.getApiKey("exa", params.sessionId, { signal: params.signal })
|
|
141
|
+
: undefined;
|
|
142
|
+
const apiKey = storedKey ?? getEnvApiKey("exa");
|
|
143
|
+
if (!apiKey) {
|
|
144
|
+
throw new Error("Exa credentials not found. Set EXA_API_KEY or login with 'omp /login exa'.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const response = await callExaSearch(apiKey, params);
|
|
216
148
|
|
|
217
149
|
// Convert to unified SearchResponse
|
|
218
150
|
const sources: SearchSource[] = [];
|
|
@@ -250,15 +182,15 @@ export class ExaProvider extends SearchProvider {
|
|
|
250
182
|
readonly id = "exa";
|
|
251
183
|
readonly label = "Exa";
|
|
252
184
|
|
|
253
|
-
isAvailable(
|
|
185
|
+
isAvailable(authStorage: AuthStorage): boolean {
|
|
254
186
|
try {
|
|
255
187
|
if (settings.get("exa.enabled") === false || settings.get("exa.enableSearch") === false) {
|
|
256
188
|
return false;
|
|
257
189
|
}
|
|
258
190
|
} catch {
|
|
259
|
-
// Settings
|
|
191
|
+
// Settings may be unavailable before CLI initialization; credential availability is still authoritative.
|
|
260
192
|
}
|
|
261
|
-
return
|
|
193
|
+
return authStorage.hasAuth("exa");
|
|
262
194
|
}
|
|
263
195
|
|
|
264
196
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
@@ -266,6 +198,8 @@ export class ExaProvider extends SearchProvider {
|
|
|
266
198
|
query: params.query,
|
|
267
199
|
num_results: params.numSearchResults ?? params.limit,
|
|
268
200
|
signal: params.signal,
|
|
201
|
+
authStorage: params.authStorage,
|
|
202
|
+
sessionId: params.sessionId,
|
|
269
203
|
});
|
|
270
204
|
}
|
|
271
205
|
}
|
|
@@ -19,6 +19,7 @@ const MAX_NUM_RESULTS = 40;
|
|
|
19
19
|
export async function searchKagi(params: {
|
|
20
20
|
query: string;
|
|
21
21
|
num_results?: number;
|
|
22
|
+
recency?: SearchParams["recency"];
|
|
22
23
|
signal?: AbortSignal;
|
|
23
24
|
authStorage: AuthStorage;
|
|
24
25
|
sessionId?: string;
|
|
@@ -30,6 +31,7 @@ export async function searchKagi(params: {
|
|
|
30
31
|
params.query,
|
|
31
32
|
{
|
|
32
33
|
limit: numResults,
|
|
34
|
+
recency: params.recency,
|
|
33
35
|
sessionId: params.sessionId,
|
|
34
36
|
signal: params.signal,
|
|
35
37
|
},
|
|
@@ -41,6 +43,7 @@ export async function searchKagi(params: {
|
|
|
41
43
|
sources: toSearchSources(result.sources, numResults),
|
|
42
44
|
relatedQuestions: result.relatedQuestions.length > 0 ? result.relatedQuestions : undefined,
|
|
43
45
|
requestId: result.requestId,
|
|
46
|
+
answer: result.answer,
|
|
44
47
|
};
|
|
45
48
|
} catch (err) {
|
|
46
49
|
if (err instanceof KagiApiError) {
|
|
@@ -67,6 +70,7 @@ export class KagiProvider extends SearchProvider {
|
|
|
67
70
|
return searchKagi({
|
|
68
71
|
query: params.query,
|
|
69
72
|
num_results: params.numSearchResults ?? params.limit,
|
|
73
|
+
recency: params.recency,
|
|
70
74
|
signal: params.signal,
|
|
71
75
|
authStorage: params.authStorage,
|
|
72
76
|
sessionId: params.sessionId,
|