import { isSafeHttpUrl, normalizeSearchResult } from './search-capability.mjs'; export async function searchSearxng(query, { limit = 10, endpoint = process.env.TKMIND_SEARCH_SEARXNG_URL, fetchImpl = fetch } = {}) { if (!endpoint) throw new Error('SearXNG endpoint is not configured'); try { if (!['http:', 'https:'].includes(new URL(endpoint).protocol)) throw new Error('unsupported protocol'); } catch { throw new Error('SearXNG endpoint is not configured safely'); } const url = new URL(endpoint); url.searchParams.set('q', query); url.searchParams.set('format', 'json'); const response = await fetchImpl(url, { signal: AbortSignal.timeout(Number(process.env.TKMIND_SEARCH_TIMEOUT_MS ?? 8000)), headers: { accept: 'application/json' } }); if (!response.ok) throw new Error(`SearXNG returned ${response.status}`); const body = await response.json(); return (Array.isArray(body.results) ? body.results : []).slice(0, limit).map((item, index) => normalizeSearchResult({ ...item, source: 'searxng' }, index)); } export async function readSafeUrl(target, { fetchImpl = fetch } = {}) { if (!isSafeHttpUrl(target)) throw new Error('unsafe URL'); const response = await fetchImpl(target, { signal: AbortSignal.timeout(8000), headers: { accept: 'text/html,text/plain' } }); if (!response.ok) throw new Error(`reader returned ${response.status}`); return { url: target, content: (await response.text()).slice(0, Number(process.env.TKMIND_SEARCH_READER_MAX_CHARS ?? 12000)) }; } export async function searchGithubCode(query, { limit = 10, token = process.env.GITHUB_TOKEN, endpoint = process.env.TKMIND_SEARCH_GITHUB_URL || 'https://api.github.com/search/code', fetchImpl = fetch } = {}) { const url = new URL(endpoint); if (url.protocol !== 'https:' || url.hostname !== 'api.github.com') throw new Error('GitHub endpoint is not configured safely'); url.searchParams.set('q', query); url.searchParams.set('per_page', String(Math.min(20, limit))); const headers = { accept: 'application/vnd.github+json', 'user-agent': 'tkmind-search' }; if (token) headers.authorization = `Bearer ${token}`; const response = await fetchImpl(url, { headers, signal: AbortSignal.timeout(8000) }); if (!response.ok) throw new Error(`GitHub returned ${response.status}`); const body = await response.json(); return (Array.isArray(body.items) ? body.items : []).slice(0, limit).map((item, index) => normalizeSearchResult({ title: `${item.repository?.full_name ?? ''}:${item.path ?? item.name ?? ''}`, url: item.html_url, snippet: item.repository?.description ?? '', source: 'github' }, index)); }