summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.opencode/opencode.json15
-rw-r--r--packages/opencode/src/flag/flag.ts2
-rw-r--r--packages/opencode/src/tool/codesearch.ts138
-rw-r--r--packages/opencode/src/tool/codesearch.txt12
-rw-r--r--packages/opencode/src/tool/registry.ts4
-rw-r--r--packages/opencode/src/tool/websearch.ts150
-rw-r--r--packages/opencode/src/tool/websearch.txt14
7 files changed, 314 insertions, 21 deletions
diff --git a/.opencode/opencode.json b/.opencode/opencode.json
index ae0362547..7da874d36 100644
--- a/.opencode/opencode.json
+++ b/.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"]
}
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 86ca07652..77a88843d 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/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()
diff --git a/packages/opencode/src/tool/codesearch.ts b/packages/opencode/src/tool/codesearch.ts
new file mode 100644
index 000000000..0227c06f5
--- /dev/null
+++ b/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
+ }
+ },
+})
diff --git a/packages/opencode/src/tool/codesearch.txt b/packages/opencode/src/tool/codesearch.txt
new file mode 100644
index 000000000..4187f08d1
--- /dev/null
+++ b/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'
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 6234a4e61..f7888761a 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/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,
]
}
diff --git a/packages/opencode/src/tool/websearch.ts b/packages/opencode/src/tool/websearch.ts
new file mode 100644
index 000000000..4064d12f3
--- /dev/null
+++ b/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
+ }
+ },
+})
diff --git a/packages/opencode/src/tool/websearch.txt b/packages/opencode/src/tool/websearch.txt
index 09d2eaa26..22427e246 100644
--- a/packages/opencode/src/tool/websearch.txt
+++ b/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