// SearXNG's API client — searxng's own code, living in the module (novox/hq ADR 0044). SearXNG is a // privacy-respecting metasearch engine: it forwards a query to many upstream engines and returns the // merged results. Its JSON API (`/search?q=...&format=json`) is what makes a `searxng_search` tool // useful; the client speaks only that. No credential — the instance is reached inside the mesh. export interface SearxResult { title: string; url: string; content?: string; engine?: string; category?: string; score?: number; } export interface SearxSearch { query: string; numberOfResults: number; results: SearxResult[]; suggestions: string[]; answers: string[]; } export interface SearxOptions { categories?: string; language?: string; pageno?: number; } export class SearxngClient { readonly baseUrl: string; constructor(url: string) { this.baseUrl = url.replace(/\/$/, ""); } /** Build from the module's environment. No key: SearXNG's search API is open on the mesh, so a URL * is all it takes — defaulting to the container's own listen port. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): SearxngClient { const url = env.MESH_SEARXNG_URL ?? `http://127.0.0.1:${env.SEARXNG_PORT ?? "8080"}`; return new SearxngClient(url); } async search(query: string, opts: SearxOptions = {}): Promise { const params = new URLSearchParams({ q: query, format: "json" }); if (opts.categories) params.set("categories", opts.categories); if (opts.language) params.set("language", opts.language); if (opts.pageno) params.set("pageno", String(opts.pageno)); const res = await fetch(`${this.baseUrl}/search?${params.toString()}`, { headers: { Accept: "application/json" }, }); if (!res.ok) throw new Error(`SearXNG search: ${res.status} ${await res.text()}`); const data = (await res.json()) as { results?: SearxResult[]; suggestions?: string[]; answers?: string[]; number_of_results?: number; }; const results = data.results ?? []; return { query, // SearXNG's own count is often 0 even with results; fall back to what we actually got. numberOfResults: data.number_of_results || results.length, results: results.map((r) => ({ title: r.title, url: r.url, content: r.content, engine: r.engine, category: r.category, score: r.score, })), suggestions: data.suggestions ?? [], answers: data.answers ?? [], }; } }