mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 11:14:23 +01:00
add web and codesearch tools
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
packages/opencode/src/tool/codesearch.ts
Normal file
138
packages/opencode/src/tool/codesearch.ts
Normal file
@@ -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
packages/opencode/src/tool/codesearch.txt
Normal file
12
packages/opencode/src/tool/codesearch.txt
Normal file
@@ -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'
|
||||
@@ -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
packages/opencode/src/tool/websearch.ts
Normal file
150
packages/opencode/src/tool/websearch.ts
Normal file
@@ -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
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user