@oh-my-pi/pi-coding-agent 13.7.0 → 13.7.3
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 +32 -0
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +3 -12
- package/src/debug/log-formatting.ts +2 -4
- package/src/debug/log-viewer.ts +3 -3
- package/src/discovery/helpers.ts +16 -1
- package/src/exa/factory.ts +1 -1
- package/src/exa/mcp-client.ts +1 -1
- package/src/exa/websets.ts +1 -1
- package/src/extensibility/skills.ts +6 -2
- package/src/ipy/gateway-coordinator.ts +1 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/tool-execution.ts +5 -5
- package/src/modes/controllers/command-controller.ts +4 -4
- package/src/modes/controllers/input-controller.ts +8 -9
- package/src/modes/controllers/selector-controller.ts +98 -94
- package/src/modes/interactive-mode.ts +4 -4
- package/src/modes/types.ts +3 -3
- package/src/patch/hashline.ts +15 -12
- package/src/patch/index.ts +5 -4
- package/src/prompts/tools/hashline.md +148 -66
- package/src/slash-commands/builtin-registry.ts +17 -1
- package/src/task/executor.ts +1 -1
- package/src/task/render.ts +3 -1
- package/src/tools/fetch.ts +17 -6
- package/src/tools/submit-result.ts +4 -51
- package/src/tui/output-block.ts +7 -8
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/kagi.ts +161 -0
- package/src/web/scrapers/youtube.ts +22 -4
- package/src/web/search/index.ts +16 -39
- package/src/web/search/providers/kagi.ts +26 -104
package/src/web/kagi.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { findCredential } from "./search/providers/utils";
|
|
3
|
+
|
|
4
|
+
const KAGI_SUMMARIZE_URL = "https://kagi.com/api/v0/summarize";
|
|
5
|
+
const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
|
|
6
|
+
|
|
7
|
+
interface KagiSummarizeResponse {
|
|
8
|
+
data?: {
|
|
9
|
+
output?: string;
|
|
10
|
+
};
|
|
11
|
+
error?: Array<{
|
|
12
|
+
msg?: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface KagiSearchResultObject {
|
|
17
|
+
t: 0;
|
|
18
|
+
url: string;
|
|
19
|
+
title: string;
|
|
20
|
+
snippet?: string;
|
|
21
|
+
published?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface KagiRelatedSearchesObject {
|
|
25
|
+
t: 1;
|
|
26
|
+
list: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type KagiSearchObject = KagiSearchResultObject | KagiRelatedSearchesObject;
|
|
30
|
+
|
|
31
|
+
interface KagiSearchResponse {
|
|
32
|
+
meta: {
|
|
33
|
+
id: string;
|
|
34
|
+
};
|
|
35
|
+
data: KagiSearchObject[];
|
|
36
|
+
error?: Array<{
|
|
37
|
+
code: number;
|
|
38
|
+
msg: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class KagiApiError extends Error {
|
|
43
|
+
readonly statusCode?: number;
|
|
44
|
+
|
|
45
|
+
constructor(message: string, statusCode?: number) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "KagiApiError";
|
|
48
|
+
this.statusCode = statusCode;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface KagiSummarizeOptions {
|
|
53
|
+
engine?: string;
|
|
54
|
+
summaryType?: string;
|
|
55
|
+
targetLanguage?: string;
|
|
56
|
+
cache?: boolean;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface KagiSearchOptions {
|
|
61
|
+
limit?: number;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface KagiSearchSource {
|
|
66
|
+
title: string;
|
|
67
|
+
url: string;
|
|
68
|
+
snippet?: string;
|
|
69
|
+
publishedDate?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface KagiSearchResult {
|
|
73
|
+
requestId: string;
|
|
74
|
+
sources: KagiSearchSource[];
|
|
75
|
+
relatedQuestions: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function findKagiApiKey(): Promise<string | null> {
|
|
79
|
+
return findCredential(getEnvApiKey("kagi"), "kagi");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getAuthHeaders(apiKey: string): Record<string, string> {
|
|
83
|
+
return {
|
|
84
|
+
Authorization: `Bot ${apiKey}`,
|
|
85
|
+
Accept: "application/json",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function summarizeUrlWithKagi(url: string, options: KagiSummarizeOptions = {}): Promise<string | null> {
|
|
90
|
+
const apiKey = await findKagiApiKey();
|
|
91
|
+
if (!apiKey) return null;
|
|
92
|
+
|
|
93
|
+
const requestUrl = new URL(KAGI_SUMMARIZE_URL);
|
|
94
|
+
requestUrl.searchParams.set("url", url);
|
|
95
|
+
requestUrl.searchParams.set("summary_type", options.summaryType ?? "summary");
|
|
96
|
+
if (options.engine) requestUrl.searchParams.set("engine", options.engine);
|
|
97
|
+
if (options.targetLanguage) requestUrl.searchParams.set("target_language", options.targetLanguage);
|
|
98
|
+
if (options.cache !== undefined) requestUrl.searchParams.set("cache", String(options.cache));
|
|
99
|
+
|
|
100
|
+
const response = await fetch(requestUrl, {
|
|
101
|
+
headers: getAuthHeaders(apiKey),
|
|
102
|
+
signal: options.signal,
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) return null;
|
|
105
|
+
|
|
106
|
+
const payload = (await response.json()) as KagiSummarizeResponse;
|
|
107
|
+
if (payload.error && payload.error.length > 0) return null;
|
|
108
|
+
|
|
109
|
+
const output = payload.data?.output?.trim();
|
|
110
|
+
return output && output.length > 0 ? output : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function searchWithKagi(query: string, options: KagiSearchOptions = {}): Promise<KagiSearchResult> {
|
|
114
|
+
const apiKey = await findKagiApiKey();
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const requestUrl = new URL(KAGI_SEARCH_URL);
|
|
120
|
+
requestUrl.searchParams.set("q", query);
|
|
121
|
+
if (options.limit !== undefined) {
|
|
122
|
+
requestUrl.searchParams.set("limit", String(options.limit));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await fetch(requestUrl, {
|
|
126
|
+
headers: getAuthHeaders(apiKey),
|
|
127
|
+
signal: options.signal,
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const errorText = await response.text();
|
|
131
|
+
throw new KagiApiError(`Kagi API error (${response.status}): ${errorText}`, response.status);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const payload = (await response.json()) as KagiSearchResponse;
|
|
135
|
+
if (payload.error && payload.error.length > 0) {
|
|
136
|
+
const firstError = payload.error[0];
|
|
137
|
+
throw new KagiApiError(`Kagi API error: ${firstError.msg}`, firstError.code);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sources: KagiSearchSource[] = [];
|
|
141
|
+
const relatedQuestions: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const item of payload.data) {
|
|
144
|
+
if (item.t === 0) {
|
|
145
|
+
sources.push({
|
|
146
|
+
title: item.title,
|
|
147
|
+
url: item.url,
|
|
148
|
+
snippet: item.snippet,
|
|
149
|
+
publishedDate: item.published ?? undefined,
|
|
150
|
+
});
|
|
151
|
+
} else if (item.t === 1) {
|
|
152
|
+
relatedQuestions.push(...item.list);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
requestId: payload.meta.id,
|
|
158
|
+
sources,
|
|
159
|
+
relatedQuestions,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { ptree, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
6
6
|
import { ensureTool } from "../../utils/tools-manager";
|
|
7
|
+
import { summarizeUrlWithKagi } from "../kagi";
|
|
7
8
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
8
9
|
import { buildResult, formatMediaDuration, formatNumber } from "./types";
|
|
9
10
|
|
|
@@ -104,8 +105,28 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
104
105
|
const yt = parseYouTubeUrl(url);
|
|
105
106
|
if (!yt) return null;
|
|
106
107
|
|
|
107
|
-
// Ensure yt-dlp is available (auto-download if missing)
|
|
108
108
|
const signal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
109
|
+
const fetchedAt = new Date().toISOString();
|
|
110
|
+
const notes: string[] = [];
|
|
111
|
+
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
112
|
+
|
|
113
|
+
// Prefer Kagi Universal Summarizer when credentials are available
|
|
114
|
+
try {
|
|
115
|
+
const kagiSummary = await summarizeUrlWithKagi(videoUrl, { signal });
|
|
116
|
+
if (kagiSummary && kagiSummary.length > 100) {
|
|
117
|
+
return buildResult(kagiSummary, {
|
|
118
|
+
url,
|
|
119
|
+
finalUrl: videoUrl,
|
|
120
|
+
method: "kagi",
|
|
121
|
+
fetchedAt,
|
|
122
|
+
notes: ["Used Kagi Universal Summarizer for YouTube"],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
throwIfAborted(signal);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ensure yt-dlp is available (auto-download if missing)
|
|
109
130
|
const ytdlp = await ensureTool("yt-dlp", { signal, silent: true });
|
|
110
131
|
if (!ytdlp) {
|
|
111
132
|
return {
|
|
@@ -120,9 +141,6 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
120
141
|
};
|
|
121
142
|
}
|
|
122
143
|
|
|
123
|
-
const fetchedAt = new Date().toISOString();
|
|
124
|
-
const notes: string[] = [];
|
|
125
|
-
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
126
144
|
const execOptions = {
|
|
127
145
|
mode: "group" as const,
|
|
128
146
|
signal,
|
package/src/web/search/index.ts
CHANGED
|
@@ -126,23 +126,21 @@ function formatCount(label: string, count: number): string {
|
|
|
126
126
|
function formatForLLM(response: SearchResponse): string {
|
|
127
127
|
const parts: string[] = [];
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
if (response.answer) {
|
|
130
|
+
parts.push(response.answer);
|
|
131
|
+
if (response.sources.length > 0) {
|
|
132
|
+
parts.push("\n## Sources");
|
|
133
|
+
parts.push(formatCount("source", response.sources.length));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const [i, src] of response.sources.entries()) {
|
|
138
|
+
const age = formatAge(src.ageSeconds) || src.publishedDate;
|
|
139
|
+
const agePart = age ? ` (${age})` : "";
|
|
140
|
+
parts.push(`[${i + 1}] ${src.title}${agePart}\n ${src.url}`);
|
|
141
|
+
if (src.snippet) {
|
|
142
|
+
parts.push(` ${truncateText(src.snippet, 240)}`);
|
|
142
143
|
}
|
|
143
|
-
} else {
|
|
144
|
-
parts.push("\n## Sources");
|
|
145
|
-
parts.push("0 sources");
|
|
146
144
|
}
|
|
147
145
|
|
|
148
146
|
if (response.citations && response.citations.length > 0) {
|
|
@@ -163,29 +161,8 @@ function formatForLLM(response: SearchResponse): string {
|
|
|
163
161
|
for (const q of response.relatedQuestions) {
|
|
164
162
|
parts.push(`- ${q}`);
|
|
165
163
|
}
|
|
166
|
-
} else {
|
|
167
|
-
parts.push("\n## Related");
|
|
168
|
-
parts.push("0 questions");
|
|
169
164
|
}
|
|
170
165
|
|
|
171
|
-
parts.push("\n## Meta");
|
|
172
|
-
parts.push(`Provider: ${response.provider}`);
|
|
173
|
-
if (response.model) {
|
|
174
|
-
parts.push(`Model: ${response.model}`);
|
|
175
|
-
}
|
|
176
|
-
if (response.usage) {
|
|
177
|
-
const usageParts: string[] = [];
|
|
178
|
-
if (response.usage.inputTokens !== undefined) usageParts.push(`in ${response.usage.inputTokens}`);
|
|
179
|
-
if (response.usage.outputTokens !== undefined) usageParts.push(`out ${response.usage.outputTokens}`);
|
|
180
|
-
if (response.usage.totalTokens !== undefined) usageParts.push(`total ${response.usage.totalTokens}`);
|
|
181
|
-
if (response.usage.searchRequests !== undefined) usageParts.push(`search ${response.usage.searchRequests}`);
|
|
182
|
-
if (usageParts.length > 0) {
|
|
183
|
-
parts.push(`Usage: ${usageParts.join(" | ")}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (response.requestId) {
|
|
187
|
-
parts.push(`Request: ${truncateText(response.requestId, 64)}`);
|
|
188
|
-
}
|
|
189
166
|
if (response.searchQueries && response.searchQueries.length > 0) {
|
|
190
167
|
parts.push(`Search queries: ${response.searchQueries.length}`);
|
|
191
168
|
for (const query of response.searchQueries.slice(0, 3)) {
|
|
@@ -368,7 +345,7 @@ async function executeExaTool(
|
|
|
368
345
|
toolName: string,
|
|
369
346
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: ExaRenderDetails }> {
|
|
370
347
|
try {
|
|
371
|
-
const apiKey =
|
|
348
|
+
const apiKey = findExaKey();
|
|
372
349
|
const response = await callExaTool(mcpToolName, params, apiKey);
|
|
373
350
|
|
|
374
351
|
if (isSearchResponse(response)) {
|
|
@@ -581,7 +558,7 @@ export async function getSearchTools(options: SearchToolsOptions = {}): Promise<
|
|
|
581
558
|
tools.push(webSearchDeepTool, webSearchCodeContextTool);
|
|
582
559
|
|
|
583
560
|
// Advanced/add-on tools remain key-gated to avoid exposing known unauthenticated failures
|
|
584
|
-
const exaKey =
|
|
561
|
+
const exaKey = findExaKey();
|
|
585
562
|
if (exaKey) {
|
|
586
563
|
tools.push(webSearchCrawlTool);
|
|
587
564
|
|
|
@@ -1,91 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Kagi Web Search Provider
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* SearchResponse shape used by the web search tool.
|
|
4
|
+
* Thin wrapper that adapts shared Kagi API utilities to SearchResponse shape.
|
|
6
5
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
6
|
+
import type { SearchResponse } from "../../../web/search/types";
|
|
9
7
|
import { SearchProviderError } from "../../../web/search/types";
|
|
8
|
+
import { findKagiApiKey, KagiApiError, searchWithKagi } from "../../kagi";
|
|
10
9
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
11
10
|
import type { SearchParams } from "./base";
|
|
12
11
|
import { SearchProvider } from "./base";
|
|
13
12
|
|
|
14
|
-
const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
|
|
15
13
|
const DEFAULT_NUM_RESULTS = 10;
|
|
16
14
|
const MAX_NUM_RESULTS = 40;
|
|
17
15
|
|
|
18
|
-
interface KagiSearchResult {
|
|
19
|
-
t: 0;
|
|
20
|
-
url: string;
|
|
21
|
-
title: string;
|
|
22
|
-
snippet?: string;
|
|
23
|
-
published?: string;
|
|
24
|
-
thumbnail?: {
|
|
25
|
-
url: string;
|
|
26
|
-
width?: number | null;
|
|
27
|
-
height?: number | null;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface KagiRelatedSearches {
|
|
32
|
-
t: 1;
|
|
33
|
-
list: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type KagiSearchObject = KagiSearchResult | KagiRelatedSearches;
|
|
37
|
-
|
|
38
|
-
interface KagiMeta {
|
|
39
|
-
id: string;
|
|
40
|
-
node: string;
|
|
41
|
-
ms: number;
|
|
42
|
-
api_balance?: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface KagiSearchResponse {
|
|
46
|
-
meta: KagiMeta;
|
|
47
|
-
data: KagiSearchObject[];
|
|
48
|
-
error?: Array<{ code: number; msg: string; ref?: unknown }>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Find KAGI_API_KEY from environment or .env files. */
|
|
52
|
-
export function findApiKey(): string | null {
|
|
53
|
-
return getEnvApiKey("kagi") ?? null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function callKagiSearch(
|
|
57
|
-
apiKey: string,
|
|
58
|
-
query: string,
|
|
59
|
-
limit: number,
|
|
60
|
-
signal?: AbortSignal,
|
|
61
|
-
): Promise<KagiSearchResponse> {
|
|
62
|
-
const url = new URL(KAGI_SEARCH_URL);
|
|
63
|
-
url.searchParams.set("q", query);
|
|
64
|
-
url.searchParams.set("limit", String(limit));
|
|
65
|
-
|
|
66
|
-
const response = await fetch(url, {
|
|
67
|
-
headers: {
|
|
68
|
-
Authorization: `Bot ${apiKey}`,
|
|
69
|
-
Accept: "application/json",
|
|
70
|
-
},
|
|
71
|
-
signal,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
const errorText = await response.text();
|
|
76
|
-
throw new SearchProviderError("kagi", `Kagi API error (${response.status}): ${errorText}`, response.status);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const data = (await response.json()) as KagiSearchResponse;
|
|
80
|
-
|
|
81
|
-
if (data.error && data.error.length > 0) {
|
|
82
|
-
const firstError = data.error[0];
|
|
83
|
-
throw new SearchProviderError("kagi", `Kagi API error: ${firstError.msg}`, firstError.code);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return data;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
16
|
/** Execute Kagi web search. */
|
|
90
17
|
export async function searchKagi(params: {
|
|
91
18
|
query: string;
|
|
@@ -93,36 +20,31 @@ export async function searchKagi(params: {
|
|
|
93
20
|
signal?: AbortSignal;
|
|
94
21
|
}): Promise<SearchResponse> {
|
|
95
22
|
const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
96
|
-
const apiKey = findApiKey();
|
|
97
|
-
if (!apiKey) {
|
|
98
|
-
throw new Error("KAGI_API_KEY not found. Set it in environment or .env file.");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const data = await callKagiSearch(apiKey, params.query, numResults, params.signal);
|
|
102
23
|
|
|
103
|
-
|
|
104
|
-
|
|
24
|
+
try {
|
|
25
|
+
const result = await searchWithKagi(params.query, {
|
|
26
|
+
limit: numResults,
|
|
27
|
+
signal: params.signal,
|
|
28
|
+
});
|
|
105
29
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sources.
|
|
109
|
-
title:
|
|
110
|
-
url:
|
|
111
|
-
snippet:
|
|
112
|
-
publishedDate:
|
|
113
|
-
ageSeconds: dateToAgeSeconds(
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
|
|
30
|
+
return {
|
|
31
|
+
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
|
+
})),
|
|
39
|
+
relatedQuestions: result.relatedQuestions.length > 0 ? result.relatedQuestions : undefined,
|
|
40
|
+
requestId: result.requestId,
|
|
41
|
+
};
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof KagiApiError) {
|
|
44
|
+
throw new SearchProviderError("kagi", err.message, err.statusCode);
|
|
117
45
|
}
|
|
46
|
+
throw err;
|
|
118
47
|
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
provider: "kagi",
|
|
122
|
-
sources: sources.slice(0, numResults),
|
|
123
|
-
relatedQuestions: relatedQuestions.length > 0 ? relatedQuestions : undefined,
|
|
124
|
-
requestId: data.meta.id,
|
|
125
|
-
};
|
|
126
48
|
}
|
|
127
49
|
|
|
128
50
|
/** Search provider for Kagi web search. */
|
|
@@ -130,9 +52,9 @@ export class KagiProvider extends SearchProvider {
|
|
|
130
52
|
readonly id = "kagi";
|
|
131
53
|
readonly label = "Kagi";
|
|
132
54
|
|
|
133
|
-
isAvailable() {
|
|
55
|
+
async isAvailable() {
|
|
134
56
|
try {
|
|
135
|
-
return !!
|
|
57
|
+
return !!(await findKagiApiKey());
|
|
136
58
|
} catch {
|
|
137
59
|
return false;
|
|
138
60
|
}
|