mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
170 lines
4.0 KiB
TypeScript
170 lines
4.0 KiB
TypeScript
/**
|
|
* Brave-based search adapter — fetches Brave's LLM context API and maps the
|
|
* grounding payload into SearchResult objects.
|
|
*/
|
|
|
|
import axios from 'axios'
|
|
import { AbortError } from '../../../utils/errors.js'
|
|
import type { SearchResult, SearchOptions, WebSearchAdapter } from './types.js'
|
|
|
|
const FETCH_TIMEOUT_MS = 30_000
|
|
const BRAVE_LLM_CONTEXT_URL = 'https://api.search.brave.com/res/v1/llm/context'
|
|
const BRAVE_API_KEY_ENV_VARS = ['BRAVE_SEARCH_API_KEY', 'BRAVE_API_KEY'] as const
|
|
|
|
interface BraveGroundingResult {
|
|
title?: string
|
|
url?: string
|
|
snippets?: string[]
|
|
}
|
|
|
|
interface BraveSearchResponse {
|
|
grounding?: {
|
|
generic?: BraveGroundingResult[]
|
|
map?: BraveGroundingResult[]
|
|
poi?: BraveGroundingResult | null
|
|
}
|
|
}
|
|
|
|
export class BraveSearchAdapter implements WebSearchAdapter {
|
|
async search(
|
|
query: string,
|
|
options: SearchOptions,
|
|
): Promise<SearchResult[]> {
|
|
const { signal, onProgress, allowedDomains, blockedDomains } = options
|
|
|
|
if (signal?.aborted) {
|
|
throw new AbortError()
|
|
}
|
|
|
|
onProgress?.({ type: 'query_update', query })
|
|
|
|
const abortController = new AbortController()
|
|
if (signal) {
|
|
signal.addEventListener('abort', () => abortController.abort(), {
|
|
once: true,
|
|
})
|
|
}
|
|
|
|
let payload: BraveSearchResponse
|
|
try {
|
|
const response = await axios.get<BraveSearchResponse>(
|
|
BRAVE_LLM_CONTEXT_URL,
|
|
{
|
|
signal: abortController.signal,
|
|
timeout: FETCH_TIMEOUT_MS,
|
|
responseType: 'json',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'X-Subscription-Token': getBraveApiKey(),
|
|
},
|
|
params: { q: query },
|
|
},
|
|
)
|
|
payload = response.data
|
|
} catch (e) {
|
|
if (axios.isCancel(e) || abortController.signal.aborted) {
|
|
throw new AbortError()
|
|
}
|
|
throw e
|
|
}
|
|
|
|
if (abortController.signal.aborted) {
|
|
throw new AbortError()
|
|
}
|
|
|
|
const rawResults = extractBraveResults(payload)
|
|
const results = rawResults.filter(r => {
|
|
try {
|
|
const hostname = new URL(r.url).hostname
|
|
if (
|
|
allowedDomains?.length &&
|
|
!allowedDomains.some(
|
|
d => hostname === d || hostname.endsWith('.' + d),
|
|
)
|
|
) {
|
|
return false
|
|
}
|
|
if (
|
|
blockedDomains?.length &&
|
|
blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))
|
|
) {
|
|
return false
|
|
}
|
|
} catch {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
onProgress?.({
|
|
type: 'search_results_received',
|
|
resultCount: results.length,
|
|
query,
|
|
})
|
|
|
|
return results
|
|
}
|
|
}
|
|
|
|
export function extractBraveResults(
|
|
payload: BraveSearchResponse,
|
|
): SearchResult[] {
|
|
const grounding = payload.grounding
|
|
if (!grounding) {
|
|
return []
|
|
}
|
|
|
|
const entries = [
|
|
...(Array.isArray(grounding.generic) ? grounding.generic : []),
|
|
...(grounding.poi ? [grounding.poi] : []),
|
|
...(Array.isArray(grounding.map) ? grounding.map : []),
|
|
]
|
|
|
|
const seenUrls = new Set<string>()
|
|
const results: SearchResult[] = []
|
|
|
|
for (const entry of entries) {
|
|
if (!entry?.url || !entry.title || seenUrls.has(entry.url)) {
|
|
continue
|
|
}
|
|
|
|
seenUrls.add(entry.url)
|
|
results.push({
|
|
title: entry.title,
|
|
url: entry.url,
|
|
snippet: normalizeSnippet(entry.snippets),
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
function normalizeSnippet(snippets: string[] | undefined): string | undefined {
|
|
if (!Array.isArray(snippets)) {
|
|
return undefined
|
|
}
|
|
|
|
const normalized = snippets
|
|
.map(snippet => snippet.trim())
|
|
.filter(snippet => snippet.length > 0)
|
|
|
|
if (normalized.length === 0) {
|
|
return undefined
|
|
}
|
|
|
|
return normalized.join(' ')
|
|
}
|
|
|
|
function getBraveApiKey(): string {
|
|
for (const envVar of BRAVE_API_KEY_ENV_VARS) {
|
|
const value = process.env[envVar]?.trim()
|
|
if (value) {
|
|
return value
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
'BraveSearchAdapter requires BRAVE_SEARCH_API_KEY or BRAVE_API_KEY',
|
|
)
|
|
}
|