MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / search

Method search

src/tools/web/firecrawl.ts:30–81  ·  view source on GitHub ↗
(query: string, opts: SearchOptions)

Source from the content-addressed store, hash-verified

28 readonly requiresAuth = true;
29
30 async search(query: string, opts: SearchOptions): Promise<WebSearchResult[]> {
31 const apiKey = process.env.FIRECRAWL_API_KEY;
32 if (!apiKey) {
33 throw new WebSearchError(
34 'Firecrawl backend selected but FIRECRAWL_API_KEY is not set. ' +
35 'Get a free key at https://www.firecrawl.dev — then paste it in chat (the agent saves it via save_api_key and retries), ' +
36 'or add FIRECRAWL_API_KEY=fc-... to ~/.qodex/.env.',
37 this.name,
38 );
39 }
40
41 const scrapeContent = process.env.FIRECRAWL_SCRAPE_CONTENT === '1';
42 const timeoutMs = opts.timeoutMs ?? (scrapeContent ? 40_000 : 20_000); // scraping is slower
43 const internalAbort = new AbortController();
44 const timer = setTimeout(() => internalAbort.abort(), timeoutMs);
45 const onOuterAbort = (): void => internalAbort.abort();
46 opts.signal?.addEventListener('abort', onOuterAbort);
47
48 try {
49 const body: Record<string, unknown> = { query, limit: Math.min(opts.limit, 20) };
50 if (scrapeContent) body.scrapeOptions = { formats: ['markdown'] };
51
52 const res = await proxyFetch(FIRECRAWL_URL, {
53 method: 'POST',
54 headers: {
55 'Content-Type': 'application/json',
56 'Authorization': `Bearer ${apiKey}`,
57 },
58 body: JSON.stringify(body),
59 signal: internalAbort.signal,
60 });
61 if (!res.ok) {
62 const text = await res.text().catch(() => '');
63 throw new WebSearchError(`Firecrawl HTTP ${res.status}: ${text.slice(0, 300)}`, this.name);
64 }
65 const payload = await res.json() as { success?: boolean; data?: Array<{ title?: string; url?: string; description?: string; markdown?: string }>; error?: string };
66 if (payload.success === false) {
67 throw new WebSearchError(`Firecrawl error: ${payload.error ?? 'unknown'}`, this.name);
68 }
69 // Trim scraped markdown to the passages most relevant to the search query (semantic), and
70 // record how often that beat the positional fallback — surfaced in the dashboard.
71 const { recordExtract } = await import('./extract-metrics.js');
72 return mapFirecrawlResults(payload, opts.limit, { query, onExtract: (mode) => { void recordExtract(mode); } });
73 } catch (e: any) {
74 if (e instanceof WebSearchError) throw e;
75 if (e?.name === 'AbortError') throw new WebSearchError('Request aborted (timeout or cancellation)', this.name, e);
76 throw new WebSearchError(`Network error: ${e?.message ?? e}`, this.name, e);
77 } finally {
78 clearTimeout(timer);
79 opts.signal?.removeEventListener('abort', onOuterAbort);
80 }
81 }
82}

Callers

nothing calls this directly

Calls 4

proxyFetchFunction · 0.85
mapFirecrawlResultsFunction · 0.85
recordExtractFunction · 0.85
abortMethod · 0.80

Tested by

no test coverage detected