UNPKG

@deep-assistant/agent

Version:

A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.

134 lines (119 loc) 3.77 kB
import z from "zod" import { Tool } from "./tool" import DESCRIPTION from "./websearch.txt" import { Config } from "../config/config" const API_CONFIG = { BASE_URL: "https://mcp.exa.ai", ENDPOINTS: { SEARCH: "/mcp", }, DEFAULT_NUM_RESULTS: 8, } as const interface McpSearchRequest { jsonrpc: string id: number method: string params: { name: string arguments: { query: string numResults?: number livecrawl?: "fallback" | "preferred" type?: "auto" | "fast" | "deep" contextMaxCharacters?: number } } } interface McpSearchResponse { jsonrpc: string result: { content: Array<{ type: string text: string }> } } export const WebSearchTool = Tool.define("websearch", { description: DESCRIPTION, parameters: z.object({ query: z.string().describe("Websearch query"), numResults: z.number().optional().describe("Number of search results to return (default: 8)"), livecrawl: z .enum(["fallback", "preferred"]) .optional() .describe( "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')", ), type: z .enum(["auto", "fast", "deep"]) .optional() .describe("Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search"), contextMaxCharacters: z .number() .optional() .describe("Maximum characters for context string optimized for LLMs (default: 10000)"), }), async execute(params, ctx) { // No restrictions - unrestricted web search const searchRequest: McpSearchRequest = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "web_search_exa", arguments: { query: params.query, type: params.type || "auto", numResults: params.numResults || API_CONFIG.DEFAULT_NUM_RESULTS, livecrawl: params.livecrawl || "fallback", contextMaxCharacters: params.contextMaxCharacters, }, }, } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 25000) try { const headers: Record<string, string> = { accept: "application/json, text/event-stream", "content-type": "application/json", } const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, { method: "POST", headers, body: JSON.stringify(searchRequest), signal: AbortSignal.any([controller.signal, ctx.abort]), }) clearTimeout(timeoutId) if (!response.ok) { const errorText = await response.text() throw new Error(`Search error (${response.status}): ${errorText}`) } const responseText = await response.text() // Parse SSE response const lines = responseText.split("\n") for (const line of lines) { if (line.startsWith("data: ")) { const data: McpSearchResponse = JSON.parse(line.substring(6)) if (data.result && data.result.content && data.result.content.length > 0) { return { output: data.result.content[0].text, title: `Web search: ${params.query}`, metadata: {}, } } } } return { output: "No search results found. Please try a different query.", title: `Web search: ${params.query}`, metadata: {}, } } catch (error) { clearTimeout(timeoutId) if (error instanceof Error && error.name === "AbortError") { throw new Error("Search request timed out") } throw error } }, })