Przeglądaj źródła

add web and codesearch tools

Dax Raad 5 miesięcy temu
rodzic
commit
3f5acc3dff

+ 1 - 14
.opencode/opencode.json

@@ -1,17 +1,4 @@
 {
   "$schema": "https://opencode.ai/config.json",
-  "plugin": ["opencode-openai-codex-auth"],
-  "mcp": {
-    "weather": {
-      "type": "local",
-      "command": ["bun", "x", "@h1deya/mcp-server-weather"]
-    },
-    "context7": {
-      "type": "remote",
-      "url": "https://mcp.context7.com/mcp",
-      "headers": {
-        "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
-      }
-    }
-  }
+  "plugin": ["opencode-openai-codex-auth"]
 }

+ 2 - 0
packages/opencode/src/flag/flag.ts

@@ -13,9 +13,11 @@ export namespace Flag {
   export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
 
   // Experimental
+  export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
   export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
   export const OPENCODE_EXPERIMENTAL_TURN_SUMMARY = truthy("OPENCODE_EXPERIMENTAL_TURN_SUMMARY")
   export const OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP = truthy("OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP")
+  export const OPENCODE_EXPERIMENTAL_EXA = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
 
   function truthy(key: string) {
     const value = process.env[key]?.toLowerCase()

+ 138 - 0
packages/opencode/src/tool/codesearch.ts

@@ -0,0 +1,138 @@
+import z from "zod"
+import { Tool } from "./tool"
+import DESCRIPTION from "./codesearch.txt"
+import { Config } from "../config/config"
+import { Permission } from "../permission"
+
+const API_CONFIG = {
+  BASE_URL: "https://mcp.exa.ai",
+  ENDPOINTS: {
+    CONTEXT: "/mcp",
+  },
+} as const
+
+interface McpCodeRequest {
+  jsonrpc: string
+  id: number
+  method: string
+  params: {
+    name: string
+    arguments: {
+      query: string
+      tokensNum: number
+    }
+  }
+}
+
+interface McpCodeResponse {
+  jsonrpc: string
+  result: {
+    content: Array<{
+      type: string
+      text: string
+    }>
+  }
+}
+
+export const CodeSearchTool = Tool.define("codesearch", {
+  description: DESCRIPTION,
+  parameters: z.object({
+    query: z
+      .string()
+      .describe(
+        "Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'",
+      ),
+    tokensNum: z
+      .number()
+      .min(1000)
+      .max(50000)
+      .default(5000)
+      .describe(
+        "Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.",
+      ),
+  }),
+  async execute(params, ctx) {
+    const cfg = await Config.get()
+    if (cfg.permission?.webfetch === "ask")
+      await Permission.ask({
+        type: "codesearch",
+        sessionID: ctx.sessionID,
+        messageID: ctx.messageID,
+        callID: ctx.callID,
+        title: "Search code for: " + params.query,
+        metadata: {
+          query: params.query,
+          tokensNum: params.tokensNum,
+        },
+      })
+
+    const codeRequest: McpCodeRequest = {
+      jsonrpc: "2.0",
+      id: 1,
+      method: "tools/call",
+      params: {
+        name: "get_code_context_exa",
+        arguments: {
+          query: params.query,
+          tokensNum: params.tokensNum || 5000,
+        },
+      },
+    }
+
+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), 30000)
+
+    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.CONTEXT}`, {
+        method: "POST",
+        headers,
+        body: JSON.stringify(codeRequest),
+        signal: AbortSignal.any([controller.signal, ctx.abort]),
+      })
+
+      clearTimeout(timeoutId)
+
+      if (!response.ok) {
+        const errorText = await response.text()
+        throw new Error(`Code 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: McpCodeResponse = JSON.parse(line.substring(6))
+          if (data.result && data.result.content && data.result.content.length > 0) {
+            return {
+              output: data.result.content[0].text,
+              title: `Code search: ${params.query}`,
+              metadata: {},
+            }
+          }
+        }
+      }
+
+      return {
+        output:
+          "No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.",
+        title: `Code search: ${params.query}`,
+        metadata: {},
+      }
+    } catch (error) {
+      clearTimeout(timeoutId)
+
+      if (error instanceof Error && error.name === "AbortError") {
+        throw new Error("Code search request timed out")
+      }
+
+      throw error
+    }
+  },
+})

+ 12 - 0
packages/opencode/src/tool/codesearch.txt

@@ -0,0 +1,12 @@
+- Search and get relevant context for any programming task using Exa Code API
+- Provides the highest quality and freshest context for libraries, SDKs, and APIs
+- Use this tool for ANY question or task related to programming
+- Returns comprehensive code examples, documentation, and API references
+- Optimized for finding specific programming patterns and solutions
+
+Usage notes:
+  - Adjustable token count (1000-50000) for focused or comprehensive results
+  - Default 5000 tokens provides balanced context for most queries
+  - Use lower values for specific questions, higher values for comprehensive documentation
+  - Supports queries about frameworks, libraries, APIs, and programming concepts
+  - Examples: 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware'

+ 4 - 0
packages/opencode/src/tool/registry.ts

@@ -17,6 +17,9 @@ import path from "path"
 import { type ToolDefinition } from "@opencode-ai/plugin"
 import z from "zod"
 import { Plugin } from "../plugin"
+import { WebSearchTool } from "./websearch"
+import { CodeSearchTool } from "./codesearch"
+import { Flag } from "@/flag/flag"
 
 export namespace ToolRegistry {
   export const state = Instance.state(async () => {
@@ -91,6 +94,7 @@ export namespace ToolRegistry {
       TodoWriteTool,
       TodoReadTool,
       TaskTool,
+      ...(Flag.OPENCODE_EXPERIMENTAL_EXA ? [WebSearchTool, CodeSearchTool] : []),
       ...custom,
     ]
   }

+ 150 - 0
packages/opencode/src/tool/websearch.ts

@@ -0,0 +1,150 @@
+import z from "zod"
+import { Tool } from "./tool"
+import DESCRIPTION from "./websearch.txt"
+import { Config } from "../config/config"
+import { Permission } from "../permission"
+
+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) {
+    const cfg = await Config.get()
+    if (cfg.permission?.webfetch === "ask")
+      await Permission.ask({
+        type: "websearch",
+        sessionID: ctx.sessionID,
+        messageID: ctx.messageID,
+        callID: ctx.callID,
+        title: "Search web for: " + params.query,
+        metadata: {
+          query: params.query,
+          numResults: params.numResults,
+          livecrawl: params.livecrawl,
+          type: params.type,
+          contextMaxCharacters: params.contextMaxCharacters,
+        },
+      })
+
+    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
+    }
+  },
+})

+ 7 - 7
packages/opencode/src/tool/websearch.txt

@@ -1,11 +1,11 @@
-
-- Allows opencode to search the web and use the results to inform responses
+- Search the web using Exa AI - performs real-time web searches and can scrape content from specific URLs
 - Provides up-to-date information for current events and recent data
-- Returns search result information formatted as search result blocks
-- Use this tool for accessing information beyond Claude's knowledge cutoff
+- Supports configurable result counts and returns the content from the most relevant websites
+- Use this tool for accessing information beyond knowledge cutoff
 - Searches are performed automatically within a single API call
 
 Usage notes:
-  - Domain filtering is supported to include or block specific websites
-  - Web search is only available in the US
-
+  - Supports live crawling modes: 'fallback' (backup if cached unavailable) or 'preferred' (prioritize live crawling)
+  - Search types: 'auto' (balanced), 'fast' (quick results), 'deep' (comprehensive search)
+  - Configurable context length for optimal LLM integration
+  - Domain filtering and advanced search options available