@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +24 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +5 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +321 -184
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
|
@@ -62,6 +62,7 @@ interface SearXNGResult {
|
|
|
62
62
|
title?: string;
|
|
63
63
|
url?: string;
|
|
64
64
|
content?: string;
|
|
65
|
+
snippet?: string;
|
|
65
66
|
engine?: string;
|
|
66
67
|
publishedDate?: string;
|
|
67
68
|
/** SearXNG sometimes uses publishedDate, sometimes just date */
|
|
@@ -76,6 +77,7 @@ interface SearXNGResponse {
|
|
|
76
77
|
suggestions?: string[];
|
|
77
78
|
corrections?: string[];
|
|
78
79
|
unresponsive_engines?: Array<[string, string]>;
|
|
80
|
+
answers?: unknown[];
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
interface SearXNGAuth {
|
|
@@ -272,6 +274,61 @@ function stripExternalBangs(query: string): string {
|
|
|
272
274
|
.join(" ");
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
/** Extract displayable text from both legacy string answers and modern
|
|
278
|
+
* structured answer plugins (legacy, translations, weather). */
|
|
279
|
+
function extractAnswerText(answer: unknown): string | undefined {
|
|
280
|
+
if (typeof answer === "string") return answer.trim() || undefined;
|
|
281
|
+
if (!answer || typeof answer !== "object") return undefined;
|
|
282
|
+
|
|
283
|
+
const record = answer as Record<string, unknown>;
|
|
284
|
+
if (typeof record.answer === "string") return record.answer.trim() || undefined;
|
|
285
|
+
|
|
286
|
+
if (Array.isArray(record.translations)) {
|
|
287
|
+
const translations: string[] = [];
|
|
288
|
+
for (const item of record.translations) {
|
|
289
|
+
if (!item || typeof item !== "object") continue;
|
|
290
|
+
const text = (item as Record<string, unknown>).text;
|
|
291
|
+
if (typeof text === "string" && text.trim()) translations.push(text.trim());
|
|
292
|
+
if (translations.length === 3) break;
|
|
293
|
+
}
|
|
294
|
+
if (translations.length) return translations.join("\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (record.current && typeof record.current === "object") {
|
|
298
|
+
const current = record.current as Record<string, unknown>;
|
|
299
|
+
if (typeof current.summary === "string" && current.summary.trim()) return current.summary.trim();
|
|
300
|
+
const location =
|
|
301
|
+
current.location && typeof current.location === "object"
|
|
302
|
+
? (current.location as Record<string, unknown>).name
|
|
303
|
+
: undefined;
|
|
304
|
+
const temperature =
|
|
305
|
+
current.temperature && typeof current.temperature === "object"
|
|
306
|
+
? (current.temperature as Record<string, unknown>)
|
|
307
|
+
: undefined;
|
|
308
|
+
const temperatureText =
|
|
309
|
+
temperature && (typeof temperature.val === "string" || typeof temperature.val === "number")
|
|
310
|
+
? `${temperature.val}${typeof temperature.unit === "string" ? temperature.unit : ""}`
|
|
311
|
+
: undefined;
|
|
312
|
+
const condition = typeof current.condition === "string" ? current.condition : undefined;
|
|
313
|
+
const parts = [location, temperatureText, condition].filter(
|
|
314
|
+
(part): part is string => typeof part === "string" && part.trim().length > 0,
|
|
315
|
+
);
|
|
316
|
+
if (parts.length) return parts.join(": ");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function formatAnswers(answers: unknown[] | undefined): string | undefined {
|
|
323
|
+
const texts: string[] = [];
|
|
324
|
+
for (const answer of answers ?? []) {
|
|
325
|
+
const text = extractAnswerText(answer);
|
|
326
|
+
if (text) texts.push(text);
|
|
327
|
+
if (texts.length === 3) break;
|
|
328
|
+
}
|
|
329
|
+
return texts.length ? texts.join("\n\n") : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
275
332
|
/** Build the search URL and headers for a SearXNG request */
|
|
276
333
|
function buildRequest(
|
|
277
334
|
endpoint: string,
|
|
@@ -282,6 +339,7 @@ function buildRequest(
|
|
|
282
339
|
categories?: string;
|
|
283
340
|
engines?: string;
|
|
284
341
|
language?: string;
|
|
342
|
+
safesearch?: 0 | 1 | 2;
|
|
285
343
|
signal?: AbortSignal;
|
|
286
344
|
},
|
|
287
345
|
auth: SearXNGAuth | null,
|
|
@@ -308,6 +366,10 @@ function buildRequest(
|
|
|
308
366
|
url.searchParams.set("engines", params.engines);
|
|
309
367
|
}
|
|
310
368
|
|
|
369
|
+
if (params.safesearch !== undefined) {
|
|
370
|
+
url.searchParams.set("safesearch", String(params.safesearch));
|
|
371
|
+
}
|
|
372
|
+
|
|
311
373
|
if (params.language) {
|
|
312
374
|
url.searchParams.set("language", params.language);
|
|
313
375
|
}
|
|
@@ -326,6 +388,7 @@ async function callSearXNGSearch(
|
|
|
326
388
|
categories?: string;
|
|
327
389
|
engines?: string;
|
|
328
390
|
language?: string;
|
|
391
|
+
safesearch?: 0 | 1 | 2;
|
|
329
392
|
signal?: AbortSignal;
|
|
330
393
|
timeoutMs?: number;
|
|
331
394
|
fetch?: FetchImpl;
|
|
@@ -372,12 +435,23 @@ export async function searchSearXNG(params: {
|
|
|
372
435
|
|
|
373
436
|
let categories: string | undefined;
|
|
374
437
|
let language: string | undefined;
|
|
438
|
+
let configuredSafesearch: number | undefined;
|
|
375
439
|
try {
|
|
376
440
|
categories = settings.get("searxng.categories") ?? undefined;
|
|
377
441
|
language = settings.get("searxng.language") ?? undefined;
|
|
442
|
+
configuredSafesearch = settings.get("searxng.safesearch");
|
|
378
443
|
} catch {
|
|
379
444
|
// Settings not initialized yet
|
|
380
445
|
}
|
|
446
|
+
if (
|
|
447
|
+
configuredSafesearch !== undefined &&
|
|
448
|
+
configuredSafesearch !== 0 &&
|
|
449
|
+
configuredSafesearch !== 1 &&
|
|
450
|
+
configuredSafesearch !== 2
|
|
451
|
+
) {
|
|
452
|
+
throw new Error("searxng.safesearch must be 0 (off), 1 (moderate), or 2 (strict).");
|
|
453
|
+
}
|
|
454
|
+
const safesearch = configuredSafesearch;
|
|
381
455
|
const configuredEngines = findEngines();
|
|
382
456
|
|
|
383
457
|
// SearXNG forwards `q` to downstream engines, so build it with the shared
|
|
@@ -402,6 +476,7 @@ export async function searchSearXNG(params: {
|
|
|
402
476
|
categories,
|
|
403
477
|
engines,
|
|
404
478
|
language,
|
|
479
|
+
safesearch,
|
|
405
480
|
fetch: params.fetch,
|
|
406
481
|
},
|
|
407
482
|
auth,
|
|
@@ -415,7 +490,7 @@ export async function searchSearXNG(params: {
|
|
|
415
490
|
sources.push({
|
|
416
491
|
title: result.title ?? result.url,
|
|
417
492
|
url: result.url,
|
|
418
|
-
snippet: result.content?.trim() || undefined,
|
|
493
|
+
snippet: (result.content ?? result.snippet)?.trim() || undefined,
|
|
419
494
|
publishedDate: publishedDate ?? undefined,
|
|
420
495
|
ageSeconds: dateToAgeSeconds(publishedDate),
|
|
421
496
|
});
|
|
@@ -435,6 +510,7 @@ export async function searchSearXNG(params: {
|
|
|
435
510
|
|
|
436
511
|
return {
|
|
437
512
|
provider: "searxng",
|
|
513
|
+
answer: formatAnswers(response.answers),
|
|
438
514
|
sources: limitedSources,
|
|
439
515
|
relatedQuestions: response.suggestions?.length ? response.suggestions : undefined,
|
|
440
516
|
};
|
|
@@ -34,17 +34,10 @@ export interface TavilySearchParams {
|
|
|
34
34
|
fetch?: FetchImpl;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
interface TavilySearchResult {
|
|
38
|
-
title?: string | null;
|
|
39
|
-
url?: string | null;
|
|
40
|
-
content?: string | null;
|
|
41
|
-
published_date?: string | null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
37
|
interface TavilySearchResponse {
|
|
45
|
-
answer?:
|
|
46
|
-
results?:
|
|
47
|
-
request_id?:
|
|
38
|
+
answer?: unknown;
|
|
39
|
+
results?: unknown;
|
|
40
|
+
request_id?: unknown;
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -141,28 +134,36 @@ async function callTavilySearch(apiKey: string, params: TavilySearchParams): Pro
|
|
|
141
134
|
throw new SearchProviderError("tavily", `Tavily API error (${response.status}): ${message}`, response.status);
|
|
142
135
|
}
|
|
143
136
|
|
|
144
|
-
|
|
137
|
+
const payload: unknown = await response.json();
|
|
138
|
+
return asRecord(payload) ?? {};
|
|
145
139
|
}
|
|
146
140
|
|
|
147
141
|
function toSearchResponse(response: TavilySearchResponse, numResults: number): SearchResponse {
|
|
148
142
|
const sources: SearchSource[] = [];
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
snippet
|
|
156
|
-
publishedDate
|
|
157
|
-
|
|
158
|
-
|
|
144
|
+
if (Array.isArray(response.results)) {
|
|
145
|
+
for (const value of response.results) {
|
|
146
|
+
const result = asRecord(value);
|
|
147
|
+
if (!result || typeof result.url !== "string" || !result.url) continue;
|
|
148
|
+
const title = typeof result.title === "string" && result.title ? result.title : result.url;
|
|
149
|
+
const snippet = typeof result.content === "string" ? result.content : undefined;
|
|
150
|
+
const publishedDate = typeof result.published_date === "string" ? result.published_date : undefined;
|
|
151
|
+
sources.push({
|
|
152
|
+
title,
|
|
153
|
+
url: result.url,
|
|
154
|
+
snippet,
|
|
155
|
+
publishedDate,
|
|
156
|
+
ageSeconds: dateToAgeSeconds(publishedDate),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
const answer = typeof response.answer === "string" ? response.answer.trim() || undefined : undefined;
|
|
161
162
|
return {
|
|
162
163
|
provider: "tavily",
|
|
163
|
-
answer
|
|
164
|
+
answer,
|
|
164
165
|
sources: sources.slice(0, numResults),
|
|
165
|
-
requestId: response.request_id
|
|
166
|
+
requestId: typeof response.request_id === "string" ? response.request_id : undefined,
|
|
166
167
|
authMode: "api_key",
|
|
167
168
|
};
|
|
168
169
|
}
|
|
@@ -19,7 +19,7 @@ const MAX_NUM_RESULTS = 20;
|
|
|
19
19
|
const MAX_PAGE = 10;
|
|
20
20
|
|
|
21
21
|
/** TinyFish is SERP-backed: common Google-style operators pass through. */
|
|
22
|
-
const TINYFISH_QUERY_SYNTAX: QuerySyntax = { phrases: true, negation: true,
|
|
22
|
+
const TINYFISH_QUERY_SYNTAX: QuerySyntax = { phrases: true, negation: true, filetype: true };
|
|
23
23
|
|
|
24
24
|
const RECENCY_MINUTES: Record<NonNullable<SearchParams["recency"]>, number> = {
|
|
25
25
|
day: 1440,
|
|
@@ -33,6 +33,8 @@ export interface TinyFishSearchParams {
|
|
|
33
33
|
num_results?: number;
|
|
34
34
|
recency?: SearchParams["recency"];
|
|
35
35
|
page?: number;
|
|
36
|
+
include_domains?: string[];
|
|
37
|
+
exclude_domains?: string[];
|
|
36
38
|
signal?: AbortSignal;
|
|
37
39
|
timeoutMs?: number;
|
|
38
40
|
fetch?: FetchImpl;
|
|
@@ -66,6 +68,12 @@ async function callTinyFishSearch(apiKey: string, params: TinyFishSearchParams):
|
|
|
66
68
|
if (params.recency) {
|
|
67
69
|
url.searchParams.set("recency_minutes", String(RECENCY_MINUTES[params.recency]));
|
|
68
70
|
}
|
|
71
|
+
if (params.include_domains?.length) {
|
|
72
|
+
url.searchParams.set("include_domains", params.include_domains.join(","));
|
|
73
|
+
}
|
|
74
|
+
if (params.exclude_domains?.length) {
|
|
75
|
+
url.searchParams.set("exclude_domains", params.exclude_domains.join(","));
|
|
76
|
+
}
|
|
69
77
|
if (params.num_results !== undefined) {
|
|
70
78
|
url.searchParams.set("num_results", String(params.num_results));
|
|
71
79
|
}
|
|
@@ -96,18 +104,35 @@ async function callTinyFishSearch(apiKey: string, params: TinyFishSearchParams):
|
|
|
96
104
|
return (await response.json()) as TinyFishSearchResponse;
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
function appendTinyFishSources(
|
|
107
|
+
function appendTinyFishSources(
|
|
108
|
+
sources: SearchSource[],
|
|
109
|
+
results: readonly TinyFishSearchResult[],
|
|
110
|
+
seenUrls: Set<string>,
|
|
111
|
+
): void {
|
|
100
112
|
for (const result of results) {
|
|
101
|
-
|
|
113
|
+
const url = result.url?.trim();
|
|
114
|
+
if (!url || seenUrls.has(url)) continue;
|
|
115
|
+
seenUrls.add(url);
|
|
116
|
+
const siteName = result.site_name?.trim();
|
|
102
117
|
sources.push({
|
|
103
|
-
title: result.title
|
|
104
|
-
url
|
|
105
|
-
snippet: result.snippet
|
|
106
|
-
author:
|
|
118
|
+
title: result.title?.trim() || siteName || url,
|
|
119
|
+
url,
|
|
120
|
+
snippet: result.snippet?.replace(/\s+/g, " ").trim() || undefined,
|
|
121
|
+
author: siteName || undefined,
|
|
107
122
|
});
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
|
|
126
|
+
/** Bare hosts from `site:` values; path constraints remain centrally post-filtered. */
|
|
127
|
+
function siteHosts(sites: readonly string[]): string[] {
|
|
128
|
+
const hosts = new Set<string>();
|
|
129
|
+
for (const site of sites) {
|
|
130
|
+
const host = site.split("/", 1)[0];
|
|
131
|
+
if (host) hosts.add(host);
|
|
132
|
+
}
|
|
133
|
+
return [...hosts];
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
/** Execute TinyFish web search. */
|
|
112
137
|
export async function searchTinyFish(params: SearchParams): Promise<SearchResponse> {
|
|
113
138
|
const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
@@ -121,6 +146,12 @@ export async function searchTinyFish(params: SearchParams): Promise<SearchRespon
|
|
|
121
146
|
timeoutMs: params.timeoutMs,
|
|
122
147
|
fetch: params.fetch,
|
|
123
148
|
};
|
|
149
|
+
if (parsed.hasDirectives) {
|
|
150
|
+
const includeDomains = siteHosts(parsed.sites);
|
|
151
|
+
const excludeDomains = siteHosts(parsed.excludedSites);
|
|
152
|
+
if (includeDomains.length > 0) tinyFishParams.include_domains = includeDomains;
|
|
153
|
+
if (excludeDomains.length > 0) tinyFishParams.exclude_domains = excludeDomains;
|
|
154
|
+
}
|
|
124
155
|
const keyOrResolver: ApiKey = params.authStorage.resolver("tinyfish", {
|
|
125
156
|
sessionId: params.sessionId,
|
|
126
157
|
});
|
|
@@ -128,11 +159,14 @@ export async function searchTinyFish(params: SearchParams): Promise<SearchRespon
|
|
|
128
159
|
keyOrResolver,
|
|
129
160
|
async key => {
|
|
130
161
|
const collected: SearchSource[] = [];
|
|
162
|
+
const seenUrls = new Set<string>();
|
|
131
163
|
for (let page = 0; page <= MAX_PAGE && collected.length < numResults; page += 1) {
|
|
132
164
|
const searchPage = await callTinyFishSearch(key, { ...tinyFishParams, page });
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
if (!Array.isArray(searchPage.results)) {
|
|
166
|
+
throw new Error("TinyFish Search API returned an unexpected response shape");
|
|
167
|
+
}
|
|
168
|
+
appendTinyFishSources(collected, searchPage.results, seenUrls);
|
|
169
|
+
if (searchPage.results.length < pageSize) break;
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
return collected.slice(0, numResults);
|
|
@@ -25,6 +25,8 @@ interface XAIUrlCitationAnnotation {
|
|
|
25
25
|
title?: string | null;
|
|
26
26
|
text?: string | null;
|
|
27
27
|
cited_text?: string | null;
|
|
28
|
+
start_index?: number | null;
|
|
29
|
+
end_index?: number | null;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface XAIResponseContentPart {
|
|
@@ -34,9 +36,20 @@ interface XAIResponseContentPart {
|
|
|
34
36
|
annotations?: XAIUrlCitationAnnotation[] | null;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
interface XAIWebSearchSource {
|
|
40
|
+
url?: string | null;
|
|
41
|
+
source_website_url?: string | null;
|
|
42
|
+
title?: string | null;
|
|
43
|
+
caption?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
interface XAIResponseOutputItem {
|
|
47
|
+
type?: string;
|
|
38
48
|
content?: XAIResponseContentPart[] | null;
|
|
39
49
|
annotations?: XAIUrlCitationAnnotation[] | null;
|
|
50
|
+
action?: { sources?: XAIWebSearchSource[] | null } | null;
|
|
51
|
+
sources?: XAIWebSearchSource[] | null;
|
|
52
|
+
results?: XAIWebSearchSource[] | null;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
interface XAIResponsesUsage {
|
|
@@ -161,7 +174,12 @@ async function callXAIResponses(
|
|
|
161
174
|
throwXAIResponsesError(response.status, await response.text());
|
|
162
175
|
}
|
|
163
176
|
|
|
164
|
-
|
|
177
|
+
try {
|
|
178
|
+
return (await response.json()) as XAIResponsesResponse;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
throw new SearchProviderError("xai", `xAI Responses API returned invalid JSON: ${message}`, response.status);
|
|
182
|
+
}
|
|
165
183
|
}
|
|
166
184
|
|
|
167
185
|
function addCitationSource(
|
|
@@ -189,38 +207,77 @@ function addCitationSource(
|
|
|
189
207
|
citedText: sourceSnippet,
|
|
190
208
|
});
|
|
191
209
|
}
|
|
210
|
+
function extractSnippetAround(
|
|
211
|
+
text: string | null | undefined,
|
|
212
|
+
start: number | null | undefined,
|
|
213
|
+
end: number | null | undefined,
|
|
214
|
+
): string | undefined {
|
|
215
|
+
if (!text || typeof start !== "number" || typeof end !== "number") return undefined;
|
|
216
|
+
const before = Math.max(0, start - 100);
|
|
217
|
+
const after = Math.min(text.length, end + 100);
|
|
218
|
+
const snippet = text
|
|
219
|
+
.slice(before, after)
|
|
220
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
221
|
+
.trim();
|
|
222
|
+
if (!snippet) return undefined;
|
|
223
|
+
return snippet.length > 300 ? `${snippet.slice(0, 297)}...` : snippet;
|
|
224
|
+
}
|
|
192
225
|
|
|
193
226
|
function collectAnnotationSources(
|
|
194
227
|
annotations: readonly XAIUrlCitationAnnotation[] | null | undefined,
|
|
195
228
|
sources: SearchSource[],
|
|
196
229
|
citations: SearchCitation[],
|
|
197
230
|
seenUrls: Set<string>,
|
|
231
|
+
contentText?: string | null,
|
|
198
232
|
): void {
|
|
199
|
-
if (!annotations) return;
|
|
233
|
+
if (!Array.isArray(annotations)) return;
|
|
200
234
|
for (const annotation of annotations) {
|
|
201
|
-
if (annotation
|
|
235
|
+
if (!annotation || typeof annotation !== "object") continue;
|
|
236
|
+
if (annotation.type !== "url_citation" || typeof annotation.url !== "string") continue;
|
|
202
237
|
addCitationSource(
|
|
203
238
|
sources,
|
|
204
239
|
citations,
|
|
205
240
|
seenUrls,
|
|
206
241
|
annotation.url,
|
|
207
242
|
annotation.title,
|
|
208
|
-
annotation.cited_text ??
|
|
243
|
+
annotation.cited_text ??
|
|
244
|
+
annotation.text ??
|
|
245
|
+
extractSnippetAround(contentText, annotation.start_index, annotation.end_index),
|
|
209
246
|
);
|
|
210
247
|
}
|
|
211
248
|
}
|
|
212
249
|
|
|
250
|
+
function collectWebSearchSources(
|
|
251
|
+
item: XAIResponseOutputItem,
|
|
252
|
+
sources: SearchSource[],
|
|
253
|
+
citations: SearchCitation[],
|
|
254
|
+
seenUrls: Set<string>,
|
|
255
|
+
): void {
|
|
256
|
+
if (item.type !== "web_search_call") return;
|
|
257
|
+
for (const group of [item.action?.sources, item.sources, item.results]) {
|
|
258
|
+
if (!Array.isArray(group)) continue;
|
|
259
|
+
for (const source of group) {
|
|
260
|
+
if (!source || typeof source !== "object") continue;
|
|
261
|
+
const url = source.url ?? source.source_website_url;
|
|
262
|
+
if (typeof url !== "string") continue;
|
|
263
|
+
addCitationSource(sources, citations, seenUrls, url, source.title ?? source.caption);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
213
268
|
function parseAnswer(response: XAIResponsesResponse): string | undefined {
|
|
214
269
|
const topLevelText = response.output_text?.trim();
|
|
215
270
|
if (topLevelText) return topLevelText;
|
|
216
271
|
|
|
217
272
|
const answerParts: string[] = [];
|
|
218
|
-
|
|
219
|
-
|
|
273
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
274
|
+
for (const item of output) {
|
|
275
|
+
if (!item || typeof item !== "object") continue;
|
|
276
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
277
|
+
for (const part of content) {
|
|
278
|
+
if (!part || typeof part !== "object") continue;
|
|
220
279
|
const text = part.output_text ?? part.text;
|
|
221
|
-
if ((
|
|
222
|
-
answerParts.push(text.trim());
|
|
223
|
-
}
|
|
280
|
+
if (text?.trim()) answerParts.push(text.trim());
|
|
224
281
|
}
|
|
225
282
|
}
|
|
226
283
|
|
|
@@ -259,13 +316,23 @@ function parseResponse(response: XAIResponsesResponse, resultCap: number): Searc
|
|
|
259
316
|
const seenUrls = new Set<string>();
|
|
260
317
|
|
|
261
318
|
collectAnnotationSources(response.annotations, sources, citations, seenUrls);
|
|
262
|
-
|
|
319
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
320
|
+
for (const item of output) {
|
|
321
|
+
if (!item || typeof item !== "object") continue;
|
|
263
322
|
collectAnnotationSources(item.annotations, sources, citations, seenUrls);
|
|
264
|
-
|
|
265
|
-
|
|
323
|
+
const content = Array.isArray(item.content) ? item.content : [];
|
|
324
|
+
for (const part of content) {
|
|
325
|
+
if (!part || typeof part !== "object") continue;
|
|
326
|
+
collectAnnotationSources(part.annotations, sources, citations, seenUrls, part.output_text ?? part.text);
|
|
266
327
|
}
|
|
267
328
|
}
|
|
268
|
-
for (const
|
|
329
|
+
for (const item of output) {
|
|
330
|
+
if (!item || typeof item !== "object") continue;
|
|
331
|
+
collectWebSearchSources(item, sources, citations, seenUrls);
|
|
332
|
+
}
|
|
333
|
+
const topLevelCitations = Array.isArray(response.citations) ? response.citations : [];
|
|
334
|
+
for (const url of topLevelCitations) {
|
|
335
|
+
if (typeof url !== "string") continue;
|
|
269
336
|
addCitationSource(sources, citations, seenUrls, url);
|
|
270
337
|
}
|
|
271
338
|
const limited = applyResultCap(sources, citations, resultCap);
|
|
@@ -354,7 +421,11 @@ export async function searchXAI(params: SearchParams): Promise<SearchResponse> {
|
|
|
354
421
|
signal: params.signal,
|
|
355
422
|
missingKeyMessage: 'xAI credentials not found. Set XAI_API_KEY or configure an API key for provider "xai".',
|
|
356
423
|
});
|
|
357
|
-
|
|
424
|
+
const parsed = parseResponse(response, resultCap);
|
|
425
|
+
if (!parsed.answer && parsed.sources.length === 0) {
|
|
426
|
+
throw new SearchProviderError("xai", "xAI web_search returned no answer or sources", 502);
|
|
427
|
+
}
|
|
428
|
+
return parsed;
|
|
358
429
|
}
|
|
359
430
|
|
|
360
431
|
/** Search provider for xAI web search. */
|