/** * Web search API wrapper using SearXNG public instances. * No API key required — free for server-side use. */ export interface SearchResult { title: string; url: string; snippet: string; } const SEARXNG_INSTANCES = [ "https://searx.be", "https://search.sapti.me", "https://searx.tiekoetter.com", "https://search.bus-hit.me", ]; async function searchSearXNG(query: string): Promise { for (const instance of SEARXNG_INSTANCES) { try { const url = `${instance}/search?q=${encodeURIComponent(query)}&format=json&categories=general&language=en`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 8000); const res = await fetch(url, { signal: controller.signal, headers: { "User-Agent": "PromptArch/1.4 (https://rommark.dev)", Accept: "application/json", }, }); clearTimeout(timeout); if (!res.ok) continue; const data = await res.json(); const results: SearchResult[] = (data.results || []) .slice(0, 8) .map((r: Record) => ({ title: r.title || "", url: r.url || "", snippet: r.content || "", })) .filter((r: SearchResult) => r.title && r.url); if (results.length > 0) return results; } catch { // Try next instance } } return []; } export async function searchWeb(query: string): Promise { // Clean the query — take first meaningful line, max 200 chars const cleanQuery = query .split("\n")[0] .replace(/\[.*?\]/g, "") .trim() .substring(0, 200); if (!cleanQuery || cleanQuery.length < 3) return []; return searchSearXNG(cleanQuery); }