/** * Kagi Web Search Provider * * Thin wrapper that adapts shared Kagi API utilities to SearchResponse shape. */ import type { SearchResponse } from "../../../web/search/types"; import { SearchProviderError } from "../../../web/search/types"; import { findKagiApiKey, KagiApiError, searchWithKagi } from "../../kagi"; import { clampNumResults, dateToAgeSeconds } from "../utils"; import type { SearchParams } from "./base"; import { SearchProvider } from "./base"; const DEFAULT_NUM_RESULTS = 10; const MAX_NUM_RESULTS = 40; /** Execute Kagi web search. */ export async function searchKagi(params: { query: string; num_results?: number; signal?: AbortSignal; }): Promise { const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS); try { const result = await searchWithKagi(params.query, { limit: numResults, signal: params.signal, }); return { provider: "kagi", sources: result.sources.slice(0, numResults).map(source => ({ title: source.title, url: source.url, snippet: source.snippet, publishedDate: source.publishedDate, ageSeconds: dateToAgeSeconds(source.publishedDate), })), relatedQuestions: result.relatedQuestions.length > 0 ? result.relatedQuestions : undefined, requestId: result.requestId, }; } catch (err) { if (err instanceof KagiApiError) { throw new SearchProviderError("kagi", err.message, err.statusCode); } throw err; } } /** Search provider for Kagi web search. */ export class KagiProvider extends SearchProvider { readonly id = "kagi"; readonly label = "Kagi"; async isAvailable() { try { return !!(await findKagiApiKey()); } catch { return false; } } search(params: SearchParams): Promise { return searchKagi({ query: params.query, num_results: params.numSearchResults ?? params.limit, signal: params.signal, }); } }