style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,32 +1,26 @@
import React from 'react'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js'
import { Box, Text } from '@anthropic/ink'
import type { ProgressMessage } from 'src/types/message.js'
import { truncate } from 'src/utils/format.js'
import type {
Output,
SearchResult,
WebSearchProgress,
} from './WebSearchTool.js'
import React from 'react';
import { MessageResponse } from 'src/components/MessageResponse.js';
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js';
import { Box, Text } from '@anthropic/ink';
import type { ProgressMessage } from 'src/types/message.js';
import { truncate } from 'src/utils/format.js';
import type { Output, SearchResult, WebSearchProgress } from './WebSearchTool.js';
function getSearchSummary(
results: (SearchResult | string | null | undefined)[],
): {
searchCount: number
totalResultCount: number
function getSearchSummary(results: (SearchResult | string | null | undefined)[]): {
searchCount: number;
totalResultCount: number;
} {
let searchCount = 0
let totalResultCount = 0
let searchCount = 0;
let totalResultCount = 0;
for (const result of results) {
if (result != null && typeof result !== 'string') {
searchCount++
totalResultCount += result.content?.length ?? 0
searchCount++;
totalResultCount += result.content?.length ?? 0;
}
}
return { searchCount, totalResultCount }
return { searchCount, totalResultCount };
}
export function renderToolUseMessage(
@@ -35,48 +29,46 @@ export function renderToolUseMessage(
allowed_domains,
blocked_domains,
}: Partial<{
query: string
allowed_domains?: string[]
blocked_domains?: string[]
query: string;
allowed_domains?: string[];
blocked_domains?: string[];
}>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!query) {
return null
return null;
}
let message = ''
let message = '';
if (query) {
message += `"${query}"`
message += `"${query}"`;
}
if (verbose) {
if (allowed_domains && allowed_domains.length > 0) {
message += `, only allowing domains: ${allowed_domains.join(', ')}`
message += `, only allowing domains: ${allowed_domains.join(', ')}`;
}
if (blocked_domains && blocked_domains.length > 0) {
message += `, blocking domains: ${blocked_domains.join(', ')}`
message += `, blocking domains: ${blocked_domains.join(', ')}`;
}
}
return message
return message;
}
export function renderToolUseProgressMessage(
progressMessages: ProgressMessage<WebSearchProgress>[],
): React.ReactNode {
export function renderToolUseProgressMessage(progressMessages: ProgressMessage<WebSearchProgress>[]): React.ReactNode {
if (progressMessages.length === 0) {
return null
return null;
}
const lastProgress = progressMessages[progressMessages.length - 1]
const lastProgress = progressMessages[progressMessages.length - 1];
if (!lastProgress?.data) {
return null
return null;
}
const data = lastProgress.data
const data = lastProgress.data;
switch (data.type) {
case 'query_update':
@@ -84,7 +76,7 @@ export function renderToolUseProgressMessage(
<MessageResponse>
<Text dimColor>Searching: {data.query}</Text>
</MessageResponse>
)
);
case 'search_results_received':
return (
<MessageResponse>
@@ -92,18 +84,18 @@ export function renderToolUseProgressMessage(
Found {data.resultCount} results for &quot;{data.query}&quot;
</Text>
</MessageResponse>
)
);
default:
return null
return null;
}
}
export function renderToolResultMessage(output: Output): React.ReactNode {
const { searchCount } = getSearchSummary(output.results ?? [])
const { searchCount } = getSearchSummary(output.results ?? []);
const timeDisplay =
output.durationSeconds >= 1
? `${Math.round(output.durationSeconds)}s`
: `${Math.round(output.durationSeconds * 1000)}ms`
: `${Math.round(output.durationSeconds * 1000)}ms`;
return (
<Box justifyContent="space-between" width="100%">
@@ -114,14 +106,12 @@ export function renderToolResultMessage(output: Output): React.ReactNode {
</Text>
</MessageResponse>
</Box>
)
);
}
export function getToolUseSummary(
input: Partial<{ query: string }> | undefined,
): string | null {
export function getToolUseSummary(input: Partial<{ query: string }> | undefined): string | null {
if (!input?.query) {
return null
return null;
}
return truncate(input.query, TOOL_SUMMARY_MAX_LENGTH)
return truncate(input.query, TOOL_SUMMARY_MAX_LENGTH);
}

View File

@@ -42,7 +42,9 @@ const inputSchema = lazySchema(() =>
context_max_characters: z
.number()
.optional()
.describe('Maximum characters for context string optimized for LLMs (default: 10000)'),
.describe(
'Maximum characters for context string optimized for LLMs (default: 10000)',
),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
@@ -51,7 +53,10 @@ const searchResultSchema = lazySchema(() => {
const searchHitSchema = z.object({
title: z.string().describe('The title of the search result'),
url: z.string().describe('The URL of the search result'),
snippet: z.string().optional().describe('A short description of the search result'),
snippet: z
.string()
.optional()
.describe('A short description of the search result'),
})
return z.object({
@@ -192,7 +197,11 @@ export const WebSearchTool = buildTool({
if (adapterResults.length > 0) {
results.push({
tool_use_id: 'adapter-search-1',
content: adapterResults.map(r => ({ title: r.title, url: r.url, snippet: r.snippet })),
content: adapterResults.map(r => ({
title: r.title,
url: r.url,
snippet: r.snippet,
})),
})
} else {
results.push('No search results found.')

View File

@@ -24,7 +24,7 @@ async function main() {
const startTime = Date.now()
const results = await adapter.search(query, {
onProgress: (p) => {
onProgress: p => {
if (p.type === 'query_update') {
console.log(` → Query sent: ${p.query}`)
}
@@ -51,7 +51,9 @@ async function main() {
console.log(` ${r.url}`)
if (r.snippet) {
const snippet = r.snippet.replace(/\n/g, ' ')
console.log(` ${snippet.slice(0, 150)}${snippet.length > 150 ? '…' : ''}`)
console.log(
` ${snippet.slice(0, 150)}${snippet.length > 150 ? '…' : ''}`,
)
}
console.log()
}
@@ -76,7 +78,7 @@ async function main() {
}
}
main().catch((e) => {
main().catch(e => {
console.error('❌ Fatal error:', e)
process.exit(1)
})

View File

@@ -2,9 +2,13 @@ import { describe, expect, mock, test } from 'bun:test'
const _abortMock = () => ({
AbortError: class AbortError extends Error {
constructor(message?: string) { super(message); this.name = 'AbortError' }
constructor(message?: string) {
super(message)
this.name = 'AbortError'
}
},
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
isAbortError: (e: unknown) =>
e instanceof Error && (e as Error).name === 'AbortError',
})
mock.module('src/utils/errors.js', _abortMock)
mock.module('src/utils/errors', _abortMock)
@@ -45,7 +49,9 @@ describe('decodeHtmlEntities', () => {
})
test('handles mixed entities in one string', () => {
expect(decodeHtmlEntities('&lt;a&nbsp;href=&quot;x&quot;&gt;')).toBe('<a\u00A0href="x">')
expect(decodeHtmlEntities('&lt;a&nbsp;href=&quot;x&quot;&gt;')).toBe(
'<a\u00A0href="x">',
)
})
})

View File

@@ -44,7 +44,11 @@ describe('extractBraveResults', () => {
})
expect(results).toEqual([
{ title: 'Generic', url: 'https://example.com/generic', snippet: undefined },
{
title: 'Generic',
url: 'https://example.com/generic',
snippet: undefined,
},
{ title: 'POI', url: 'https://maps.example.com/poi', snippet: undefined },
{ title: 'Map', url: 'https://maps.example.com/map', snippet: undefined },
])

View File

@@ -5,9 +5,13 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
// import in createAdapter() never needs to resolve the alias at runtime.
const _abortMock = () => ({
AbortError: class AbortError extends Error {
constructor(message?: string) { super(message); this.name = 'AbortError' }
constructor(message?: string) {
super(message)
this.name = 'AbortError'
}
},
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
isAbortError: (e: unknown) =>
e instanceof Error && (e as Error).name === 'AbortError',
})
mock.module('src/utils/errors.js', _abortMock)
mock.module('src/utils/errors', _abortMock)

View File

@@ -2,9 +2,13 @@ import { afterEach, describe, expect, mock, test } from 'bun:test'
const _abortMock = () => ({
AbortError: class AbortError extends Error {
constructor(message?: string) { super(message); this.name = 'AbortError' }
constructor(message?: string) {
super(message)
this.name = 'AbortError'
}
},
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
isAbortError: (e: unknown) =>
e instanceof Error && (e as Error).name === 'AbortError',
})
mock.module('src/utils/errors.js', _abortMock)
mock.module('src/utils/errors', _abortMock)
@@ -16,7 +20,8 @@ describe('ExaSearchAdapter.search', () => {
}
// Exa MCP returns SSE lines like: data: {"result":{"content":[{"type":"text","text":"..."}]}}
const buildSseResponse = (text: string) => `data: ${JSON.stringify({ result: { content: [{ type: 'text', text }] } })}\n`
const buildSseResponse = (text: string) =>
`data: ${JSON.stringify({ result: { content: [{ type: 'text', text }] } })}\n`
const STRUCTURED_TEXT = [
'Title: Example Result 1',
@@ -37,7 +42,9 @@ describe('ExaSearchAdapter.search', () => {
test('parses structured Title/URL/Content blocks from SSE response', async () => {
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
),
isCancel: () => false,
},
}))
@@ -59,10 +66,13 @@ describe('ExaSearchAdapter.search', () => {
})
test('parses markdown link fallback when no structured blocks', async () => {
const markdownText = '- [React Docs](https://react.dev/docs)\n- [React Hooks](https://react.dev/hooks)'
const markdownText =
'- [React Docs](https://react.dev/docs)\n- [React Hooks](https://react.dev/hooks)'
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(markdownText) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(markdownText) }),
),
isCancel: () => false,
},
}))
@@ -83,7 +93,9 @@ describe('ExaSearchAdapter.search', () => {
const plainUrlText = 'https://example.com/page1\nhttps://example.com/page2'
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(plainUrlText) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(plainUrlText) }),
),
isCancel: () => false,
},
}))
@@ -130,7 +142,9 @@ describe('ExaSearchAdapter.search', () => {
test('calls onProgress with query_update and search_results_received', async () => {
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
),
isCancel: () => false,
},
}))
@@ -161,13 +175,17 @@ describe('ExaSearchAdapter.search', () => {
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(mixedText) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(mixedText) }),
),
isCancel: () => false,
},
}))
const adapter = await createAdapter()
const results = await adapter.search('test', { allowedDomains: ['allowed.com'] })
const results = await adapter.search('test', {
allowedDomains: ['allowed.com'],
})
expect(results).toHaveLength(1)
expect(results[0].url).toBe('https://allowed.com/a')
@@ -184,13 +202,17 @@ describe('ExaSearchAdapter.search', () => {
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(mixedText) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(mixedText) }),
),
isCancel: () => false,
},
}))
const adapter = await createAdapter()
const results = await adapter.search('test', { blockedDomains: ['spam.com'] })
const results = await adapter.search('test', {
blockedDomains: ['spam.com'],
})
expect(results).toHaveLength(1)
expect(results[0].url).toBe('https://good.com/a')
@@ -213,7 +235,9 @@ describe('ExaSearchAdapter.search', () => {
}))
const adapter = await createAdapter()
const results = await adapter.search('test', { allowedDomains: ['example.com'] })
const results = await adapter.search('test', {
allowedDomains: ['example.com'],
})
expect(results).toHaveLength(1)
expect(results[0].url).toBe('https://docs.example.com/page')
@@ -222,7 +246,9 @@ describe('ExaSearchAdapter.search', () => {
test('throws AbortError when signal is already aborted', async () => {
mock.module('axios', () => ({
default: {
post: mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) })),
post: mock(() =>
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
),
isCancel: () => false,
},
}))
@@ -251,7 +277,9 @@ describe('ExaSearchAdapter.search', () => {
})
test('sends correct MCP request payload to Exa endpoint', async () => {
const axiosPost = mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }))
const axiosPost = mock(() =>
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
)
mock.module('axios', () => ({
default: {
post: axiosPost,
@@ -277,7 +305,9 @@ describe('ExaSearchAdapter.search', () => {
})
test('passes custom search options to MCP request', async () => {
const axiosPost = mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }))
const axiosPost = mock(() =>
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
)
mock.module('axios', () => ({
default: {
post: axiosPost,

View File

@@ -9,7 +9,11 @@ import type {
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { queryModelWithStreaming } from 'src/services/api/claude.js'
import { createTrace, endTrace, isLangfuseEnabled } from 'src/services/langfuse/index.js'
import {
createTrace,
endTrace,
isLangfuseEnabled,
} from 'src/services/langfuse/index.js'
import { getSessionId } from 'src/bootstrap/state.js'
import { getAPIProvider } from 'src/utils/model/providers.js'
import { createUserMessage } from 'src/utils/messages.js'
@@ -18,7 +22,10 @@ import { jsonParse } from 'src/utils/slowOperations.js'
import { asSystemPrompt } from 'src/utils/systemPromptType.js'
import type { SearchResult, SearchOptions, WebSearchAdapter } from './types.js'
function makeToolSchema(input: { allowedDomains?: string[]; blockedDomains?: string[] }): BetaWebSearchTool20250305 {
function makeToolSchema(input: {
allowedDomains?: string[]
blockedDomains?: string[]
}): BetaWebSearchTool20250305 {
return {
type: 'web_search_20250305',
name: 'web_search',
@@ -29,10 +36,7 @@ function makeToolSchema(input: { allowedDomains?: string[]; blockedDomains?: str
}
export class ApiSearchAdapter implements WebSearchAdapter {
async search(
query: string,
options: SearchOptions,
): Promise<SearchResult[]> {
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
const { signal, onProgress, allowedDomains, blockedDomains } = options
const userMessage = createUserMessage({
@@ -40,7 +44,10 @@ export class ApiSearchAdapter implements WebSearchAdapter {
})
const toolSchema = makeToolSchema({ allowedDomains, blockedDomains })
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE('tengu_plum_vx3', false)
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_plum_vx3',
false,
)
const model = useHaiku ? getSmallFastModel() : getMainLoopModel()
const langfuseTrace = isLangfuseEnabled()
? createTrace({
@@ -71,7 +78,9 @@ export class ApiSearchAdapter implements WebSearchAdapter {
isBypassPermissionsModeAvailable: false,
}),
model,
toolChoice: useHaiku ? { type: 'tool' as const, name: 'web_search' } : undefined,
toolChoice: useHaiku
? { type: 'tool' as const, name: 'web_search' }
: undefined,
isNonInteractiveSession: false,
hasAppendSystemPrompt: false,
extraToolSchemas: [toolSchema],
@@ -101,8 +110,18 @@ export class ApiSearchAdapter implements WebSearchAdapter {
const streamEvt = event as {
event?: {
type: string
content_block?: { type: string; id?: string; tool_use_id?: string; content?: unknown; [key: string]: unknown }
delta?: { type: string; partial_json?: string; [key: string]: unknown }
content_block?: {
type: string
id?: string
tool_use_id?: string
content?: unknown
[key: string]: unknown
}
delta?: {
type: string
partial_json?: string
[key: string]: unknown
}
[key: string]: unknown
}
}
@@ -116,7 +135,10 @@ export class ApiSearchAdapter implements WebSearchAdapter {
}
}
if (currentToolUseId && streamEvt.event?.type === 'content_block_delta') {
if (
currentToolUseId &&
streamEvt.event?.type === 'content_block_delta'
) {
const delta = streamEvt.event.delta
if (delta?.type === 'input_json_delta' && delta.partial_json) {
currentToolUseJson += delta.partial_json
@@ -168,14 +190,20 @@ export class ApiSearchAdapter implements WebSearchAdapter {
}
}
function extractSearchResults(
blocks: BetaContentBlock[],
): SearchResult[] {
function extractSearchResults(blocks: BetaContentBlock[]): SearchResult[] {
const results: SearchResult[] = []
for (const block of blocks) {
if (block.type === 'web_search_tool_result' && Array.isArray(block.content)) {
for (const r of block.content as Array<{ title: string; url: string; page_age?: string; type?: string }>) {
if (
block.type === 'web_search_tool_result' &&
Array.isArray(block.content)
) {
for (const r of block.content as Array<{
title: string
url: string
page_age?: string
type?: string
}>) {
results.push({
title: r.title,
url: r.url,

View File

@@ -23,7 +23,8 @@ const BROWSER_HEADERS = {
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
'Sec-Ch-Ua': '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'Sec-Ch-Ua':
'"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
'Sec-Fetch-Dest': 'document',
@@ -34,10 +35,7 @@ const BROWSER_HEADERS = {
} as const
export class BingSearchAdapter implements WebSearchAdapter {
async search(
query: string,
options: SearchOptions,
): Promise<SearchResult[]> {
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
const { signal, onProgress, allowedDomains, blockedDomains } = options
if (signal?.aborted) {
@@ -50,7 +48,9 @@ export class BingSearchAdapter implements WebSearchAdapter {
const abortController = new AbortController()
if (signal) {
signal.addEventListener('abort', () => abortController.abort(), { once: true })
signal.addEventListener('abort', () => abortController.abort(), {
once: true,
})
}
let html: string
@@ -76,14 +76,22 @@ export class BingSearchAdapter implements WebSearchAdapter {
const rawResults = extractBingResults(html)
// Client-side domain filtering
const results = rawResults.filter((r) => {
const results = rawResults.filter(r => {
if (!r.url) return false
try {
const hostname = new URL(r.url).hostname
if (allowedDomains?.length && !allowedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
if (
allowedDomains?.length &&
!allowedDomains.some(
d => hostname === d || hostname.endsWith('.' + d),
)
) {
return false
}
if (blockedDomains?.length && blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
if (
blockedDomains?.length &&
blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))
) {
return false
}
} catch {
@@ -116,7 +124,8 @@ export function extractBingResults(html: string): SearchResult[] {
const block = blockMatch[1]
// Extract the primary link from <h2><a href="...">...</a></h2>
const h2LinkRegex = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i
const h2LinkRegex =
/<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i
const linkMatch = h2LinkRegex.exec(block)
if (!linkMatch) continue
@@ -128,9 +137,7 @@ export function extractBingResults(html: string): SearchResult[] {
const url = resolveBingUrl(rawUrl)
if (!url) continue
const title = decodeHtmlEntities(
titleHtml.replace(/<[^>]+>/g, '').trim(),
)
const title = decodeHtmlEntities(titleHtml.replace(/<[^>]+>/g, '').trim())
// Extract snippet: try b_lineclamp → b_caption <p> → b_caption fallback
const snippet = extractSnippet(block)
@@ -150,14 +157,16 @@ function extractSnippet(block: string): string | undefined {
}
// 2. Try <p> inside b_caption
const captionPRegex = /<div[^>]*class="b_caption[^"]*"[^>]*>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/i
const captionPRegex =
/<div[^>]*class="b_caption[^"]*"[^>]*>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/i
match = captionPRegex.exec(block)
if (match) {
return decodeHtmlEntities(match[1].replace(/<[^>]+>/g, '').trim())
}
// 3. Fallback: any text inside b_caption <div>
const fallbackRegex = /<div[^>]*class="b_caption[^"]*"[^>]*>([\s\S]*?)<\/div>/i
const fallbackRegex =
/<div[^>]*class="b_caption[^"]*"[^>]*>([\s\S]*?)<\/div>/i
const fallbackMatch = fallbackRegex.exec(block)
if (fallbackMatch) {
const text = fallbackMatch[1].replace(/<[^>]+>/g, '').trim()

View File

@@ -9,7 +9,10 @@ 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
const BRAVE_API_KEY_ENV_VARS = [
'BRAVE_SEARCH_API_KEY',
'BRAVE_API_KEY',
] as const
interface BraveGroundingResult {
title?: string
@@ -26,10 +29,7 @@ interface BraveSearchResponse {
}
export class BraveSearchAdapter implements WebSearchAdapter {
async search(
query: string,
options: SearchOptions,
): Promise<SearchResult[]> {
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
const { signal, onProgress, allowedDomains, blockedDomains } = options
if (signal?.aborted) {

View File

@@ -16,10 +16,7 @@ const EXA_MCP_URL = 'https://mcp.exa.ai/mcp'
const FETCH_TIMEOUT_MS = 25_000
export class ExaSearchAdapter implements WebSearchAdapter {
async search(
query: string,
options: SearchOptions,
): Promise<SearchResult[]> {
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
const { signal, onProgress, allowedDomains, blockedDomains } = options
if (signal?.aborted) {
@@ -30,7 +27,9 @@ export class ExaSearchAdapter implements WebSearchAdapter {
const abortController = new AbortController()
if (signal) {
signal.addEventListener('abort', () => abortController.abort(), { once: true })
signal.addEventListener('abort', () => abortController.abort(), {
once: true,
})
}
// Use options to derive search params — matches kilocode websearch.ts defaults
@@ -90,14 +89,22 @@ export class ExaSearchAdapter implements WebSearchAdapter {
const results = this.parseResults(searchText)
// Client-side domain filtering
const filteredResults = results.filter((r) => {
const filteredResults = results.filter(r => {
if (!r.url) return false
try {
const hostname = new URL(r.url).hostname
if (allowedDomains?.length && !allowedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
if (
allowedDomains?.length &&
!allowedDomains.some(
d => hostname === d || hostname.endsWith('.' + d),
)
) {
return false
}
if (blockedDomains?.length && blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
if (
blockedDomains?.length &&
blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))
) {
return false
}
} catch {
@@ -160,7 +167,9 @@ export class ExaSearchAdapter implements WebSearchAdapter {
for (const block of blocks) {
const titleMatch = block.match(/^Title:\s*(.+)$/m)
const urlMatch = block.match(/^URL:\s*(https?:\/\/[^\s]+)$/m)
const contentMatch = block.match(/^Content:\s*([\s\S]+?)(?=\n(?:Title:|URL:|---)|$)/m)
const contentMatch = block.match(
/^Content:\s*([\s\S]+?)(?=\n(?:Title:|URL:|---)|$)/m,
)
if (urlMatch) {
results.push({
@@ -173,7 +182,7 @@ export class ExaSearchAdapter implements WebSearchAdapter {
// Fallback: markdown links
if (results.length === 0) {
const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g
const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g
let match: RegExpExecArray | null
while ((match = markdownLinkRegex.exec(text)) !== null) {
results.push({

View File

@@ -41,7 +41,10 @@ export function createAdapter(): WebSearchAdapter {
// 3. First-party Anthropic API → api (server-side web search + connector_text)
// 4. Fallback → bing
const adapterKey =
envAdapter === 'api' || envAdapter === 'bing' || envAdapter === 'brave' || envAdapter === 'exa'
envAdapter === 'api' ||
envAdapter === 'bing' ||
envAdapter === 'brave' ||
envAdapter === 'exa'
? envAdapter
: isThirdPartyProvider()
? 'bing'

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getLocalMonthYear = any;
export type getLocalMonthYear = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type getAPIProvider = any;
export type getAPIProvider = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type PermissionResult = any;
export type PermissionResult = any