From b7c5bf1b82d190840c4dcb450d300ceed784657e Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 12 Feb 2026 00:28:46 +0100 Subject: [PATCH 01/12] feat(tool): codebase search tool --- .opencode/opencode.jsonc | 18 +- bun.lock | 1 + package.json | 3 +- packages/opencode/src/cli/cmd/run.ts | 9 + .../src/cli/cmd/tui/routes/session/index.tsx | 16 +- .../cli/cmd/tui/routes/session/permission.tsx | 5 +- packages/opencode/src/tool/codebase-search.ts | 388 +++++++++ .../opencode/src/tool/codebase-search.txt | 98 +++ packages/opencode/src/tool/registry.ts | 2 + plans/codebase-search-to-opencode-plan.md | 762 ++++++++++++++++++ 10 files changed, 1295 insertions(+), 7 deletions(-) create mode 100644 packages/opencode/src/tool/codebase-search.ts create mode 100644 packages/opencode/src/tool/codebase-search.txt create mode 100644 plans/codebase-search-to-opencode-plan.md diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index d85201c698..e91e27819e 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -5,12 +5,22 @@ // }, "provider": { "kilo": { - "options": {}, - }, + "options": { + "codebase_search": { + "embedModel": "codestral-embed-2505", + "vectorDb": { + "type": "qdrant", + "url": "https://e27bba4f-3225-4d61-b1d5-dc3cfb617131.europe-west3-0.gcp.cloud.qdrant.io:6333" + }, + "similarityThreshold": 0.4, + "maxResults": 50 + } + } + } }, "mcp": {}, "tools": { "github-triage": false, - "github-pr-search": false, - }, + "github-pr-search": false + } } diff --git a/bun.lock b/bun.lock index b01fe5dadb..3309c1f1db 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", "@opencode-ai/script": "workspace:*", + "@types/bun": "1.3.5", "typescript": "catalog:", }, "devDependencies": { diff --git a/package.json b/package.json index a29c705cd2..776569a6fa 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,9 @@ "dependencies": { "@aws-sdk/client-s3": "3.933.0", "@kilocode/plugin": "workspace:*", - "@opencode-ai/script": "workspace:*", "@kilocode/sdk": "workspace:*", + "@opencode-ai/script": "workspace:*", + "@types/bun": "1.3.5", "typescript": "catalog:" }, "repository": { diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 72a601f78d..d394623c5c 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -20,6 +20,7 @@ import { WebFetchTool } from "../../tool/webfetch" import { EditTool } from "../../tool/edit" import { WriteTool } from "../../tool/write" import { CodeSearchTool } from "../../tool/codesearch" +import { CodebaseSearchTool } from "../../tool/codebase-search" import { WebSearchTool } from "../../tool/websearch" import { TaskTool } from "../../tool/task" import { SkillTool } from "../../tool/skill" @@ -159,6 +160,13 @@ function codesearch(info: ToolProps) { }) } +function codebasesearch(info: ToolProps) { + inline({ + icon: "◐", + title: `Codebase Search "${info.input.query}"`, + }) +} + function websearch(info: ToolProps) { inline({ icon: "◈", @@ -408,6 +416,7 @@ export const RunCommand = cmd({ if (part.tool === "webfetch") return webfetch(props(part)) if (part.tool === "edit") return edit(props(part)) if (part.tool === "codesearch") return codesearch(props(part)) + if (part.tool === "codebase-search") return codebasesearch(props(part)) if (part.tool === "websearch") return websearch(props(part)) if (part.tool === "task") return task(props(part)) if (part.tool === "todowrite") return todo(props(part)) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 16f3571783..58e8d01981 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -45,6 +45,7 @@ import type { WebFetchTool } from "@/tool/webfetch" import type { TaskTool } from "@/tool/task" import type { QuestionTool } from "@/tool/question" import type { SkillTool } from "@/tool/skill" +import type { CodebaseSearchTool } from "@/tool/codebase-search" import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import { useSDK } from "@tui/context/sdk" import { useCommandDialog } from "@tui/component/dialog-command" @@ -1458,9 +1459,12 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess - + + + + @@ -1843,6 +1847,16 @@ function WebSearch(props: ToolProps) { ) } +function CodebaseSearch(props: ToolProps) { + const input = props.input as any + const metadata = props.metadata as any + return ( + + Codebase Search "{input.query}" ({metadata.results} results) + + ) +} + function Task(props: ToolProps) { const { theme } = useTheme() const keybind = useKeybind() diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index aff48d2fce..5d418f74ac 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -236,9 +236,12 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { - + + + + {(() => { const meta = props.request.metadata ?? {} diff --git a/packages/opencode/src/tool/codebase-search.ts b/packages/opencode/src/tool/codebase-search.ts new file mode 100644 index 0000000000..2169144e1d --- /dev/null +++ b/packages/opencode/src/tool/codebase-search.ts @@ -0,0 +1,388 @@ +import z from "zod" +import { Tool } from "./tool" +import { createHash } from "crypto" +import { Instance } from "../project/instance" +import { Config } from "../config/config" +import { Auth } from "../auth" +import { Log } from "../util/log" +import DESCRIPTION from "./codebase-search.txt" + +const log = Log.create({ service: "tool.codebase-search" }) + +interface QdrantSearchResponse { + status: string + result: Array<{ + id: string + score: number + payload: { + filePath: string + startLine: number + endLine: number + codeChunk: string + [key: string]: any + } + }> +} + +// Helper function to generate collection name from workspace path (matches Kilo Code extension pattern) +function generateCollectionName(workspacePath: string): string { + const hash = createHash("sha256").update(workspacePath).digest("hex") + return `ws-${hash.substring(0, 16)}` +} + +// Helper function to map embed model to provider and model ID +function getEmbeddingProvider(model: string): { provider: string; modelId: string } { + const lowerModel = model.toLowerCase() + + if (lowerModel.includes("text-embedding")) { + return { provider: "openai", modelId: model } + } + if (lowerModel.includes("codestral") || lowerModel.includes("mistral")) { + return { provider: "mistral", modelId: model } + } + if (lowerModel.includes("nomic")) { + return { provider: "ollama", modelId: model } + } + if (lowerModel.includes("openai")) { + return { provider: "openai", modelId: model } + } + + return { provider: "openai", modelId: "text-embedding-3-small" } +} + +// Helper function to generate embedding using OpenAI +async function generateOpenAIEmbedding( + text: string, + apiKey: string, + model = "text-embedding-3-small", +): Promise { + const response = await fetch("https://api.openai.com/v1/embeddings", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + input: text, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`OpenAI embeddings API error (${response.status}): ${errorText}`) + } + + const data = await response.json() + return data.data[0].embedding +} + +// Helper function to generate embedding using Mistral +async function generateMistralEmbedding( + text: string, + apiKey: string, + model = "codestral-embed-2505", +): Promise { + const response = await fetch("https://api.mistral.ai/v1/embeddings", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + input: text, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Mistral embeddings API error (${response.status}): ${errorText}`) + } + + const data = await response.json() + return data.data[0].embedding +} + +// Helper function to generate embedding using Ollama (local) +async function generateOllamaEmbedding(text: string, model = "nomic-embed-text"): Promise { + const response = await fetch("http://localhost:11434/api/embeddings", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model, + prompt: text, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Ollama embeddings API error (${response.status}): ${errorText}`) + } + + const data = await response.json() + return data.embedding +} + +// Helper function to generate embedding +async function generateEmbedding(text: string, embedModel: string, authMap: Map): Promise { + const { provider, modelId } = getEmbeddingProvider(embedModel) + + if (provider === "openai") { + const openaiAuth = authMap.get("openai") + if (!openaiAuth) { + throw new Error("OpenAI API key not found. Please configure openai provider in auth settings.") + } + const apiKey = openaiAuth.type === "oauth" ? openaiAuth.access : openaiAuth.key + if (!apiKey) { + throw new Error("OpenAI API key not found in auth configuration.") + } + return generateOpenAIEmbedding(text, apiKey, modelId) + } + + if (provider === "mistral") { + const mistralAuth = authMap.get("mistral") + if (!mistralAuth) { + throw new Error("Mistral API key not found. Please configure mistral provider in auth settings.") + } + const apiKey = mistralAuth.type === "oauth" ? mistralAuth.access : mistralAuth.key + if (!apiKey) { + throw new Error("Mistral API key not found in auth configuration.") + } + return generateMistralEmbedding(text, apiKey, modelId) + } + + if (provider === "ollama") { + return generateOllamaEmbedding(text, modelId) + } + + throw new Error(`Unsupported embedding provider: ${provider}. Supported providers: openai, mistral, ollama`) +} + +// Helper function to search Qdrant +async function searchQdrant( + vector: number[], + qdrantUrl: string, + collection: string, + apiKey: string, + limit: number, + pathPrefix?: string, +): Promise> { + const url = new URL(qdrantUrl) + url.pathname = `/collections/${collection}/points/search` + + const headers: Record = { + "Content-Type": "application/json", + "api-key": apiKey, + } + + const body: any = { + vector, + limit, + with_payload: true, + score_threshold: 0, + } + + if (pathPrefix) { + body.filter = { + must: [ + { + key: "filePath", + match: { + prefix: pathPrefix, + }, + }, + ], + } + } + + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Qdrant search API error (${response.status}): ${errorText}`) + } + + const data = (await response.json()) as QdrantSearchResponse + + return data.result.map((item) => ({ + filePath: item.payload.filePath, + score: item.score, + startLine: item.payload.startLine, + endLine: item.payload.endLine, + codeChunk: item.payload.codeChunk, + })) +} + +// Helper function to format search results +function formatResults( + query: string, + results: Array<{ filePath: string; score: number; startLine: number; endLine: number; codeChunk: string }>, + similarityThreshold = 0.4, + maxResults = 50, +): string { + if (!results || results.length === 0) { + return `No relevant code snippets found for query: "${query}"` + } + + const filteredResults = results.filter((result) => result.score >= similarityThreshold) + + if (filteredResults.length === 0) { + return `No relevant code snippets found for query: "${query}" (results below similarity threshold of ${similarityThreshold})` + } + + const limitedResults = filteredResults.slice(0, maxResults) + + let output = `Query: ${query}\nResults:\n\n` + + for (const result of limitedResults) { + output += `File path: ${result.filePath}\n` + output += `Score: ${result.score.toFixed(3)}\n` + output += `Lines: ${result.startLine}-${result.endLine}\n` + if (result.codeChunk) { + output += `Code Chunk:\n${result.codeChunk.trim()}\n` + } + output += "\n" + } + + return output +} + +export const CodebaseSearchTool = Tool.define("codebase-search", { + description: DESCRIPTION, + parameters: z.object({ + query: z.string().describe("The search query in natural language (required)"), + path: z.string().describe("Optional directory path to filter results (relative to workspace)").default(""), + }), + async execute(params, ctx) { + const workspacePath = Instance.worktree || Instance.directory + if (!workspacePath) { + throw new Error("No workspace directory found") + } + + // Ask for permission + await ctx.ask({ + permission: "codebase-search", + patterns: [params.query, ...(params.path ? [params.path] : [])], + always: ["*"], + metadata: { + query: params.query, + path: params.path, + }, + }) + + const config = await Config.get() + + const codebaseSearch = config.provider?.kilo?.options?.codebase_search + + if (!codebaseSearch) { + throw new Error( + "Codebase search is not configured. Please configure in opencode.json:\n" + + JSON.stringify( + { + provider: { + kilo: { + options: { + codebase_search: { + embedModel: "codestral-embed-2505", + vectorDb: { + type: "qdrant", + url: "http://localhost:6333", + }, + similarityThreshold: 0.4, + maxResults: 50, + }, + }, + }, + }, + }, + null, + 2, + ), + ) + } + + const { embedModel, vectorDb, similarityThreshold = 0.4, maxResults = 50 } = codebaseSearch + + if (!embedModel) { + throw new Error( + "embedModel is not configured. Please set provider.kilo.options.codebase_search.embedModel in opencode.json", + ) + } + + if (!vectorDb) { + throw new Error( + "vectorDb is not configured. Please set provider.kilo.options.codebase_search.vectorDb in opencode.json", + ) + } + + if (vectorDb.type === "qdrant") { + if (!vectorDb.url) { + throw new Error( + "Qdrant URL is not configured. Please set provider.kilo.options.codebase_search.vectorDb.url in opencode.json", + ) + } + } else if (vectorDb.type === "lancedb") { + throw new Error("LanceDB is not yet supported for codebase search. Please use Qdrant.") + } else { + throw new Error(`Unsupported vector database type: ${vectorDb.type}. Supported types: qdrant`) + } + + // Auto-generate collection name if not specified + const collection = vectorDb.collection || generateCollectionName(workspacePath) + + // Get auth keys + const openaiAuth = await Auth.get("openai") + const mistralAuth = await Auth.get("mistral") + const qdrantAuth = await Auth.get("qdrant") + + if (!qdrantAuth) { + throw new Error("Qdrant API key not found. Please configure qdrant provider in auth settings.") + } + const qdrantApiKey = qdrantAuth.type === "oauth" ? qdrantAuth.access : qdrantAuth.key + if (!qdrantApiKey) { + throw new Error("Qdrant API key not found in auth configuration.") + } + + const authMap = new Map() + if (openaiAuth) authMap.set("openai", openaiAuth) + if (mistralAuth) authMap.set("mistral", mistralAuth) + + try { + const embedding = await generateEmbedding(params.query, embedModel, authMap) + + const results = await searchQdrant( + embedding, + vectorDb.url, + collection, + qdrantApiKey, + maxResults, + params.path || undefined, + ) + + const output = formatResults(params.query, results, similarityThreshold, maxResults) + + return { + title: `Codebase search: ${params.query}`, + output, + metadata: {}, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + if (errorMessage.includes("not found") || errorMessage.includes("not configured")) { + throw error + } + + throw new Error( + `Codebase search failed: ${errorMessage}\n\nPlease ensure:\n1. Your vector database (${vectorDb.type}) is running and accessible\n2. The collection "${collection}" exists and has indexed data\n3. Your embedding provider API key is configured correctly`, + ) + } + }, +}) diff --git a/packages/opencode/src/tool/codebase-search.txt b/packages/opencode/src/tool/codebase-search.txt new file mode 100644 index 0000000000..61ffea1813 --- /dev/null +++ b/packages/opencode/src/tool/codebase-search.txt @@ -0,0 +1,98 @@ +Use this tool to find files most relevant to a search query using semantic search. + +This tool searches the codebase based on meaning rather than exact text matches, making it far more effective than regex-based search for understanding implementations. By default, it searches the entire workspace. + +**CRITICAL: For ANY exploration of code you haven't examined yet in this conversation, you MUST use this tool FIRST before any other search or file exploration tools.** This applies throughout the entire conversation, not just at the beginning. Even if you've already explored some code, any new area of exploration requires codebase_search first. + +## Parameters + +- **query** (required): The search query in natural language. Reuse the user's exact wording/question format unless there's a clear reason not to. Queries MUST be in English (translate if needed). + +- **path** (optional): Limit search to a specific subdirectory (relative to the current workspace directory). Leave empty or omit to search the entire workspace. + +## Usage Notes + +- This tool uses local cloud indexing with your own embedding provider and vector database +- **Embedding providers supported**: OpenAI (text-embedding-3-small), Mistral (codestral-embed-2505), Ollama (nomic-embed-text) +- **Vector database supported**: Qdrant +- Requires configuration in `opencode.json` with embedding model and Qdrant connection details +- Requires API keys configured in auth settings for your chosen embedding provider +- Results include file path, relevance score, line numbers, and code snippets +- Results are filtered by similarity threshold (default: 0.4) and limited to maxResults (default: 50) + +## Configuration + +Configure in `opencode.json`: + +```json +{ + "provider": { + "kilo": { + "options": { + "codebase_search": { + "embedModel": "codestral-embed-2505", + "vectorDb": { + "type": "qdrant", + "url": "http://localhost:6333" + }, + "similarityThreshold": 0.4, + "maxResults": 50 + } + } + } + } +} +``` + +**Collection name** is automatically generated from workspace path using SHA-256 hash (e.g., `ws-a1b2c3d4e5f6g7h8`). This matches Kilo Code VSCode extension pattern. You can optionally override this by specifying `collection` in `vectorDb` config. + +Set API keys in auth settings (`~/.local/share/kilo/auth.json`): +- `openai`: For OpenAI embeddings (required if using OpenAI as embedModel) +- `mistral`: For Mistral embeddings (required if using Mistral as embedModel) +- `qdrant`: For Qdrant authentication (**required**) + +## Directory Scoping + +Use the optional path parameter to focus searches on specific parts of your codebase: + +- Search within API modules: `path: "src/api"` +- Search in test files: `path: "tests"` +- Search specific feature directories: `path: "src/components/auth"` + +## Usage Examples + +### Searching for authentication code in a specific directory +``` +query: "user login and authentication logic" +path: "src/auth" +``` + +### Searching for entire workspace +``` +query: "environment variables and application configuration" +path: (empty) +``` + +### Searching for database-related code in a specific directory +``` +query: "database connection and query execution" +path: "src/data" +``` + +### Looking for error handling patterns in API code +``` +query: "HTTP error responses and exception handling" +path: "src/api" +``` + +### Searching for testing utilities and mock setups +``` +query: "test setup and mock data creation" +path: "tests" +``` + +### Searching for React hooks +``` +query: "useState hook implementation" +path: "src/components" +``` diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index f5a04478e9..ebbe7c702d 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -21,6 +21,7 @@ import z from "zod" import { Plugin } from "../plugin" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" +import { CodebaseSearchTool } from "./codebase-search" import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { LspTool } from "./lsp" @@ -110,6 +111,7 @@ export namespace ToolRegistry { // TodoReadTool, WebSearchTool, CodeSearchTool, + CodebaseSearchTool, SkillTool, ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), diff --git a/plans/codebase-search-to-opencode-plan.md b/plans/codebase-search-to-opencode-plan.md new file mode 100644 index 0000000000..d129b42ff3 --- /dev/null +++ b/plans/codebase-search-to-opencode-plan.md @@ -0,0 +1,762 @@ +# Plan: Create codebase_search as OpenCode Custom Tool + +## Summary + +Create a `codebase_search` custom tool for OpenCode that provides semantic code search using AI embeddings. The tool will: + +- Use OpenCode's documented custom tool pattern (https://opencode.ai/docs/custom-tools) +- Follow exact pattern from existing tools (github-pr-search.ts, github-triage.ts) +- Support both managed (cloud) and local indexing +- Use OpenCode's centralized Config and Auth modules +- Be installed as a custom tool in `.opencode/tool/` directory + +## Analysis of Existing Tool Patterns + +### Tool Structure Pattern (from github-pr-search.ts and github-triage.ts) + +```typescript +/// +import { tool } from "@kilocode/plugin" +import DESCRIPTION from "./tool-name.txt" + +// Optional helper functions +async function helperFunction() { + // implementation +} + +export default tool({ + description: DESCRIPTION, + args: { + paramName: tool.schema.string().describe("Description").default("default value"), + optionalParam: tool.schema.number().describe("Description").default(0), + enumParam: tool.schema.enum(["option1", "option2"]).describe("Description").default("option1"), + arrayParam: tool.schema + .array(tool.schema.enum(["a", "b"])) + .describe("Description") + .default([]), + }, + async execute(args) { + // Implementation + return "formatted result string" + }, +}) +``` + +### Key Pattern Elements: + +1. **Import pattern**: `import { tool } from "@kilocode/plugin"` +2. **Description import**: `import DESCRIPTION from "./tool-name.txt"` +3. **Schema methods**: + - `tool.schema.string()` - for string parameters + - `tool.schema.number()` - for numeric parameters + - `tool.schema.enum([...])` - for enum parameters + - `tool.schema.array(...)` - for array parameters +4. **Schema methods**: `.describe()` and `.default()` +5. **Return value**: Formatted string (not JSON) + +### Description File Pattern (from github-pr-search.txt and github-triage.txt) + +- Clear usage instructions +- Examples of how to use the tool +- Detailed parameter descriptions +- Context about what the tool does + +## Analysis of codebase_search from Kilocode VSCode Extension + +### Key Features + +- **Semantic Understanding**: Finds code by meaning rather than exact keyword matches +- **Cross-Project Search**: Searches across your entire indexed codebase, not just open files +- **Contextual Results**: Returns code snippets with file paths and line numbers for easy navigation +- **Similarity Scoring**: Results ranked by relevance with similarity scores (0-1 scale) +- **Scope Filtering**: Optional path parameter to limit searches to specific directories +- **Intelligent Ranking**: Results sorted by semantic relevance to your query +- **UI Integration**: Results displayed with syntax highlighting and navigation links +- **Performance Optimized**: Fast vector-based search with configurable result limits + +### When is it used? + +- When Kilo Code needs to find code related to specific functionality across your project +- When looking for implementation patterns or similar code structures +- When searching for error handling, authentication, or other conceptual code patterns +- When exploring unfamiliar codebases to understand how features are implemented +- When finding related code that might be affected by changes or refactoring + +### Tool Behavior (from CodebaseSearchTool.ts) + +The codebase_search tool in kilocode has the following characteristics: + +1. **Two search modes**: + - **Managed indexing** (Kilo Gateway API) - tried first + - **Local indexing** (CodeIndexManager) - fallback + +2. **Parameters**: + - `query` (required): The search query + - `path` (optional): Directory to limit search scope + +3. **Return format**: + +``` +Query: {query} +Results: + +File path: {relativePath} +Score: {score} +Lines: {startLine}-{endLine} +Code Chunk: {codeChunk} +``` + +4. **Error handling**: + - Workspace path not found + - Query missing + - Indexing not ready (Indexing, Standby, Error states) + - Indexing not configured + - No results found + +5. **Status messages** (when indexing not ready): + - "Code indexing is still running" + - "Code indexing has not started" + - "Code indexing is in an error state" + - "Code indexing is not ready" + +### Requirements + +This tool is only available when the Codebase Indexing feature is properly configured: + +- **Feature Configured**: Codebase Indexing must be configured in settings +- **Embedding Provider**: OpenAI API key, Mistral, or Ollama configuration required +- **Vector Database**: Qdrant instance running and accessible +- **Index Status**: Codebase must be indexed (status: "Indexed" or "Indexing") + +**Supported Embedding Models**: +- `codestral-embed-2505` (Mistral) - default, code-optimized +- `text-embedding-3-small` (OpenAI) +- Other models via Ollama + +### Limitations + +- **Requires Configuration**: Depends on external services (embedding provider + Qdrant) +- **Index Dependency**: Only searches through indexed code blocks +- **Result Limits**: Maximum of 50 results per search to maintain performance +- **Similarity Threshold**: Only returns results above similarity threshold (default: 0.4, configurable) +- **File Size Limits**: Limited to files under 1MB that were successfully indexed +- **Language Support**: Effectiveness depends on Tree-sitter language support + +### How It Works + +When the codebase_search tool is invoked, it follows this process: + +1. **Availability Validation**: + - Verifies that the CodeIndexManager is available and initialized + - Confirms codebase indexing is enabled in settings + - Checks that indexing is properly configured (API keys, Qdrant URL) + - Validates the current index state allows searching + +2. **Query Processing**: + - Takes your natural language query and generates an embedding vector + - Uses the same embedding provider configured for indexing (OpenAI or Ollama) + - Converts the semantic meaning of your query into a mathematical representation + +3. **Vector Search Execution**: + - Searches the Qdrant vector database for similar code embeddings + - Uses cosine similarity to find the most relevant code blocks + - Applies the minimum similarity threshold (default: 0.4, configurable) to filter results + - Limits results to 50 matches for optimal performance + +4. **Path Filtering** (if specified): + - Filters results to only include files within the specified directory path + - Uses normalized path comparison for accurate filtering + - Maintains relevance ranking within the filtered scope + +5. **Result Processing and Formatting**: + - Converts absolute file paths to workspace-relative paths + - Structures results with file paths, line ranges, similarity scores, and code content + - Formats for both AI consumption and UI display with syntax highlighting + +6. **Dual Output Format**: + - AI Output: Structured text format with query, file paths, scores, and code chunks + - UI Output: JSON format with syntax highlighting and navigation capabilities + +### Search Query Best Practices + +**Effective Query Patterns**: + +**Good: Conceptual and specific** +``` +query: "user authentication and password validation" +``` + +**Good: Feature-focused** +``` +query: "database connection pool setup" +``` + +**Good: Problem-oriented** +``` +query: "error handling for API requests" +``` + +**Less effective: Too generic** +``` +query: "function" +``` + +### Query Types That Work Well + +- **Functional Descriptions**: "file upload processing", "email validation logic" +- **Technical Patterns**: "singleton pattern implementation", "factory method usage" +- **Domain Concepts**: "user profile management", "payment processing workflow" +- **Architecture Components**: "middleware configuration", "database migration scripts" + +### Result Interpretation + +**Similarity Scores**: +- **0.8-1.0**: Highly relevant matches, likely exactly what you're looking for +- **0.6-0.8**: Good matches with strong conceptual similarity +- **0.4-0.6**: Potentially relevant but may require review +- **Below 0.4**: Filtered out as too dissimilar + +**Result Structure**: +Each search result includes: +- **File Path**: Workspace-relative path to the file containing the match +- **Score**: Similarity score indicating relevance (0.4-1.0) +- **Line Range**: Start and end line numbers for the code block +- **Code Chunk**: The actual code content that matched your query + +### Examples When Used + +- When implementing a new feature, Kilo Code searches for "authentication middleware" to understand existing patterns before writing new code. +- When debugging an issue, Kilo Code searches for "error handling in API calls" to find related error patterns across the codebase. +- When refactoring code, Kilo Code searches for "database transaction patterns" to ensure consistency across all database operations. +- When onboarding to a new codebase, Kilo Code searches for "configuration loading" to understand how the application bootstraps. + +### Tool Description (from kilocode prompts) + +``` +Find files most relevant to search query using semantic search. Searches based on meaning rather than exact text matches. By default searches entire workspace. Reuse the user's exact wording unless there's a clear reason not to - their phrasing often helps semantic search. Queries MUST be in English (translate if needed). + +**CRITICAL: For ANY exploration of code you haven't examined yet in this conversation, you MUST use this tool FIRST before any other search or file exploration tools.** This applies throughout the entire conversation, not just at the beginning. This tool uses semantic search to find relevant code based on meaning rather than just keywords, making it far more effective than regex-based search_files for understanding implementations. Even if you've already explored some code, any new area of exploration requires codebase_search first. + +Parameters: +- query: (required) The search query. Reuse the user's exact wording/question format unless there's a clear reason not to. +- path: (optional) Limit search to specific subdirectory (relative to current workspace directory). Leave empty for entire workspace. +``` + +### Configuration System + +OpenCode provides two modules for custom tools: + +1. **Config Module** ([`packages/opencode/src/config/config.ts`](packages/opencode/src/config/config.ts:1273-1295)) + - Stores non-sensitive settings in `opencode.json` + - `codebaseSearch` object with: + - `projectId` - Project ID for codebase search + - `embedModel` - Embedding model (default: "codestral-embed-2505") + - `vectorDb` - Vector database configuration (Qdrant or LanceDB) + - `similarityThreshold` - Minimum similarity score for results (default: 0.4) + - `maxResults` - Maximum number of results to return (default: 50) + +2. **Auth Module** ([`packages/opencode/src/auth/index.ts`](packages/opencode/src/auth/index.ts:38)) + - Stores sensitive credentials in `~/.local/share/kilo/auth.json` + - API keys for providers (kilo, openai, qdrant, etc.) + - OAuth tokens + +### Kilo Gateway Integration + +From [`packages/kilo-gateway/`](packages/kilo-gateway/): + +- Provides `searchCode()` function for managed indexing +- Exposes authentication through OpenCode's Auth module +- Managed indexing API available via `@kilocode/kilo-gateway` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ OpenCode Custom Tool: codebase_search │ +├─────────────────────────────────────────────────────────┤ +│ Parameters: query (string), path? (string) │ +│ Location: .opencode/tool/codebase_search.ts │ +│ Description: .opencode/tool/codebase_search.txt │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────┐ + │ Load Configuration │ + │ (Config.get()) │ + │ Load Auth (Auth.get()) │ + └──────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────┐ + │ Try Managed Indexing │ + │ (via Kilo Gateway API) │ + └──────────────────────────────┘ + │ + │ Success? + │ No │ Yes + │ │ + ▼ │ + ┌──────────────────────┐ + │ Local Indexing │ + │ (embeddings + │ + │ vector store) │ + └──────────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ Format & Return │ + │ Results │ + └──────────────────────┘ +``` + +## Implementation Steps + +### Step 1: Create Description File + +**Location**: `.opencode/tool/codebase_search.txt` + +``` +Use this tool to find files most relevant to a search query using semantic search. + +This tool searches the codebase based on meaning rather than exact text matches, making it far more effective than regex-based search for understanding implementations. By default, it searches the entire workspace. + +**CRITICAL: For ANY exploration of code you haven't examined yet in this conversation, you MUST use this tool FIRST before any other search or file exploration tools.** This applies throughout the entire conversation, not just at the beginning. Even if you've already explored some code, any new area of exploration requires codebase_search first. + +## Parameters + +- **query** (required): The search query in natural language. Reuse the user's exact wording/question format unless there's a clear reason not to. Queries MUST be in English (translate if needed). + +- **path** (optional): Limit search to a specific subdirectory (relative to the current workspace directory). Leave empty or omit to search the entire workspace. + +## Usage Notes + +- This tool supports both cloud-based (managed) and local indexing +- Managed indexing is tried first, with automatic fallback to local indexing +- Managed indexing requires Kilo Gateway authentication +- Local indexing requires the codebase to be indexed first +- Results include file path, relevance score, line numbers, and code snippets +- If indexing is not ready, the tool will return status information + +### Directory Scoping + +Use the optional path parameter to focus searches on specific parts of your codebase: + +Search within API modules: +``` +query: "endpoint validation middleware" +path: "src/api" +``` + +Search in test files: +``` +query: "mock data setup patterns" +path: "tests" +``` + +Search specific feature directories: +``` +query: "component state management" +path: "src/components/auth" +``` + +## Usage Examples + +### Searching for authentication code in a specific directory +``` + +user login and authentication logic +src/auth + +``` + +### Searching for entire workspace +``` + +environment variables and application configuration + +``` + +### Searching for database-related code in a specific directory +``` + +database connection and query execution +src/data + +``` + +### Looking for error handling patterns in API code +``` + +HTTP error responses and exception handling +src/api + +``` + +### Searching for testing utilities and mock setups +``` + +test setup and mock data creation +tests + +``` + +### Searching for React hooks +``` + +useState hook implementation +src/components + +``` + +### Step 2: Create Tool File + +**Location**: `.opencode/tool/codebase_search.ts` + +```typescript +/// +import { tool } from "@kilocode/plugin" +import { searchCode } from "@kilocode/kilo-gateway" +import DESCRIPTION from "./codebase_search.txt" + +// Helper function to get current git branch +async function getCurrentGitBranch(workspacePath: string): Promise { + try { + const { spawn } = await import("child_process") + return new Promise((resolve, reject) => { + const proc = spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspacePath }) + let output = "" + proc.stdout?.on("data", (data) => { output += data.toString() }) + proc.on("close", (code) => { + if (code === 0) resolve(output.trim()) + else reject(new Error(`Git command failed with code ${code}`)) + }) + }) + } catch (error) { + return "main" // Default fallback + } +} + +// Helper function to format search results +function formatResults(query: string, results: any[], source: "managed" | "local"): string { + if (!results || results.length === 0) { + return `No relevant code snippets found for query: "${query}"` + } + + const output = `Query: ${query}\nResults:\n\n` + + for (const result of results) { + output += `File path: ${result.filePath}\n` + output += `Score: ${result.score.toFixed(3)}\n` + output += `Lines: ${result.startLine}-${result.endLine}\n` + if (result.codeChunk) { + output += `Code Chunk:\n${result.codeChunk.trim()}\n` + } + output += "\n" + } + + return output +} + +// Helper function to search local index (placeholder) +async function searchLocalIndex( + workspacePath: string, + codebaseSearch: any, + query: string, + directoryPath?: string, +): Promise { + // TODO: Implement local indexing + // This would use: + // - codebaseSearch.embedModel (e.g., "codestral-embed-2505") + // - codebaseSearch.vectorDb (Qdrant or LanceDB) + // - Auth.get("openai") or Auth.get("qdrant") for API keys + + throw new Error("Local indexing not yet implemented. Please use managed indexing.") +} + +export default tool({ + description: DESCRIPTION, + args: { + query: tool.schema.string().describe("The search query in natural language (required)"), + path: tool.schema.string().describe("Optional directory path to filter results (relative to workspace)").default(""), + }, + async execute(args) { + // 1. Load configuration from Config module + const config = await this.config.get() + const codebaseSearch = config.codebaseSearch ?? {} + + // 2. Load authentication from Auth module + const kiloAuth = await this.auth.get("kilo") + + // 3. Get workspace path + const workspacePath = this.directory + if (!workspacePath) { + throw new Error("No workspace directory found") + } + + // 4. Try managed (cloud) indexing first if kiloAuth is available + if (kiloAuth) { + try { + const kiloToken = kiloAuth.type === "oauth" ? kiloAuth.access : kiloAuth.key + const organizationId = kiloAuth.type === "oauth" ? (kiloAuth.accountId ?? null) : null + + if (kiloToken && codebaseSearch.projectId && organizationId) { + const results = await searchCode( + { + query: args.query, + organizationId, + projectId: codebaseSearch.projectId, + preferBranch: await getCurrentGitBranch(workspacePath), + fallbackBranch: "main", + excludeFiles: [], + path: args.path || undefined, + }, + kiloToken, + this.abort, + ) + + return formatResults(args.query, results, "managed") + } + } catch (error) { + // Fall through to local indexing + console.debug("Managed search failed, trying local:", error instanceof Error ? error.message : String(error)) + } + } + + // 5. Fall back to local indexing + if (codebaseSearch?.vectorDb) { + try { + const results = await searchLocalIndex( + workspacePath, + codebaseSearch, + args.query, + args.path || undefined, + ) + return formatResults(args.query, results, "local") + } catch (error) { + throw new Error(`Local indexing search failed: ${error instanceof Error ? error.message : String(error)}`) + } + } + + // 6. No indexing configured + throw new Error( + "Codebase search is not configured. Please configure in opencode.json:\n" + + JSON.stringify({ + codebaseSearch: { + projectId: "your-project-id", + embedModel: "codestral-embed-2505", + vectorDb: { + type: "qdrant", + url: "http://localhost:6333" + }, + similarityThreshold: 0.4, + maxResults: 50 + } + }, null, 2) + ) + }, +} +``` + +### Step 3: Configuration Examples + +**Example `opencode.json`**: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "codebaseSearch": { + "projectId": "your-project-id", + "embedModel": "codestral-embed-2505", + "vectorDb": { + "type": "qdrant", + "url": "http://localhost:6333" + }, + "similarityThreshold": 0.4, + "maxResults": 50 + } +} +``` + +**Example with LanceDB**: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "codebaseSearch": { + "projectId": "your-project-id", + "embedModel": "codestral-embed-2505", + "vectorDb": { + "type": "lancedb", + "path": ".opencode/index" + }, + "similarityThreshold": 0.4, + "maxResults": 50 + } +} +``` + +### Step 4: Authentication Setup + +**For managed indexing (Kilo Gateway)**: + +```bash +opencode auth set kilo +# Or via OAuth +opencode auth login +``` + +**For local indexing with OpenAI embeddings**: + +```bash +opencode auth set openai +``` + +**For local indexing with Mistral embeddings**: + +```bash +opencode auth set mistral +``` + +**For local indexing with Ollama**: + +```bash +# Configure Ollama in opencode.json with base URL (default: http://localhost:11434) +``` + +**For Qdrant vector database**: + +```bash +opencode auth set qdrant +``` + +### Step 5: Local Indexing Implementation (Future) + +To implement local indexing, you'll need to: + +1. **Create embeddings** using configured model: + - `codestral-embed-2505` (Mistral) + - `text-embedding-3-small` (OpenAI) + - Other models via Ollama + +2. **Store vectors** in configured database: + - Qdrant: Requires URL and API key from Auth module + - LanceDB: Requires path from config + +3. **Perform similarity search**: + - Query embedding + - Vector similarity search (using cosine similarity) + - Apply minimum similarity threshold (default: 0.4, configurable) + - Limit results to maxResults (default: 50) + - Return top N results with scores + +This can be implemented as a separate module or integrated directly into the tool. + +## File Structure + +``` +.opencode/ + tool/ + codebase_search.ts ← Custom tool file (this is what we create) + codebase_search.txt ← Description file (this is what we create) +``` + +**Configuration file**: `opencode.json` (in project root or global config) + +**Authentication**: `~/.local/share/kilo/auth.json` (managed by OpenCode) + +## Benefits of Custom Tool Approach + +1. **No core modifications** - Tool can be developed independently +2. **Easy distribution** - Can be shared as a standalone file +3. **Future-proof** - Won't conflict with upstream opencode merges +4. **Follows conventions** - Matches documented custom tool pattern +5. **Uses existing infrastructure** - Leverages Config and Auth modules +6. **Proper separation** - Credentials in Auth, settings in Config + +## Configuration vs Credentials + +| Type | Storage | Example | Access Method | +| ----------------------------------------- | ------------------------------- | -------------------------- | ------------- | +| **Credentials** (API keys, tokens) | `~/.local/share/kilo/auth.json` | `ctx.auth.get("provider")` | +| **Configuration** (settings, preferences) | `opencode.json` | `ctx.config.get()` | + +This separation follows security best practices: + +- **Credentials** → Auth module (sensitive, user-specific) +- **Configuration** → Config module (non-sensitive, project-specific) + +## Testing Strategy + +### Manual Testing Checklist + +- [ ] Tool loads from `.opencode/tool/` directory +- [ ] Tool description loads from `.txt` file +- [ ] Tool appears in available tools list +- [ ] Tool can be called from OpenCode CLI +- [ ] Tool can be called from OpenCode TUI +- [ ] Tool can be called from OpenCode Web +- [ ] Returns results for managed indexing +- [ ] Falls back to local indexing when configured +- [ ] Clear error messages for: + - [ ] No auth token + - [ ] No project ID + - [ ] Local indexing not configured + - [ ] Network errors +- [ ] Works with different query types: + - [ ] Single concept ("authentication") + - [ ] Multi-term ("user login password hashing") + - [ ] Domain-specific ("React useState hook") +- [ ] Respects directory filter +- [ ] Handles git branch detection correctly +- [ ] Returns properly formatted results matching kilocode format + +## Success Criteria + +- ✅ Tool file created at `.opencode/tool/codebase_search.ts` +- ✅ Description file created at `.opencode/tool/codebase_search.txt` +- ✅ Tool uses OpenCode's Config module for settings +- ✅ Tool uses OpenCode's Auth module for credentials +- ✅ Managed indexing works with Kilo Gateway +- ✅ Configuration stored in `opencode.json` +- ✅ Credentials stored in Auth module +- ✅ No modifications to core OpenCode code +- ✅ Follows custom tool documentation pattern +- ✅ Follows existing tool pattern (github-pr-search.ts, github-triage.ts) +- ✅ Works across all OpenCode interfaces (CLI, TUI, Web) +- ✅ Return format matches kilocode tool format + +## Open Questions + +1. **Local Indexing**: Should we implement local indexing now, or start with managed indexing only? +2. **Embedder Support**: Should we support multiple embedder types (OpenAI, Mistral, Ollama) from the start? +3. **Index Initialization**: Should the tool auto-initialize local index, or require a separate indexing command? + +## References + +### Kilo Documentation + +- codebase_search Tool: https://kilo.ai/docs/automate/tools/codebase_search + +### OpenCode Documentation + +- Custom Tools: https://opencode.ai/docs/custom-tools/ +- Tool Pattern: `packages/opencode/src/tool/` +- Plugin Types: `packages/plugin/src/` + +### Kilo Gateway + +- `packages/kilo-gateway/src/index.ts` +- `packages/kilo-gateway/src/services/code-indexing/managed/api-client.ts` + +### Existing Implementation + +- Kilocode tool: `../kilocode/src/core/tools/CodebaseSearchTool.ts` +- Kilocode description: `../kilocode/src/core/prompts/tools/native-tools/codebase_search.ts` +- Config: `packages/opencode/src/config/config.ts` (already has codebaseSearch schema) +- Auth: `packages/opencode/src/auth/index.ts` (already supports all providers) + +### Existing Tool Patterns + +- `github-pr-search.ts` - GitHub PR search tool +- `github-triage.ts` - GitHub issue triage tool +- `packages/plugin/src/example.ts` - Example custom tool From d5de8b078283b91558f28fddcb3d5e62ac738dfb Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 12 Feb 2026 11:44:24 +0100 Subject: [PATCH 02/12] refactor(tool): extract codebase-search logic to Kilo-specific module Extract Kilo-specific codebase search utilities into a dedicated module to improve code organization and minimize merge conflicts with upstream. Changes: - Add types.ts with Zod schemas for configuration validation - Add config.ts for configuration loading with defaults - Add collection.ts for SHA-256 hash-based collection naming - Add embeddings.ts with OpenAI, Mistral, Ollama provider support - Refactor codebase-search.ts to use extracted modules - Conditionally register tool only when configured - Add comprehensive unit tests --- .opencode/opencode.jsonc | 18 +- .../kilocode/codebase-search/collection.ts | 39 ++++ .../src/kilocode/codebase-search/config.ts | 68 ++++++ .../kilocode/codebase-search/embeddings.ts | 178 ++++++++++++++++ .../src/kilocode/codebase-search/index.ts | 19 ++ .../src/kilocode/codebase-search/types.ts | 66 ++++++ packages/opencode/src/kilocode/index.ts | 3 + packages/opencode/src/tool/codebase-search.ts | 195 ++---------------- packages/opencode/src/tool/registry.ts | 10 +- .../test/kilocode/codebase-search.test.ts | 138 +++++++++++++ 10 files changed, 545 insertions(+), 189 deletions(-) create mode 100644 packages/opencode/src/kilocode/codebase-search/collection.ts create mode 100644 packages/opencode/src/kilocode/codebase-search/config.ts create mode 100644 packages/opencode/src/kilocode/codebase-search/embeddings.ts create mode 100644 packages/opencode/src/kilocode/codebase-search/index.ts create mode 100644 packages/opencode/src/kilocode/codebase-search/types.ts create mode 100644 packages/opencode/test/kilocode/codebase-search.test.ts diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index e91e27819e..d85201c698 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -5,22 +5,12 @@ // }, "provider": { "kilo": { - "options": { - "codebase_search": { - "embedModel": "codestral-embed-2505", - "vectorDb": { - "type": "qdrant", - "url": "https://e27bba4f-3225-4d61-b1d5-dc3cfb617131.europe-west3-0.gcp.cloud.qdrant.io:6333" - }, - "similarityThreshold": 0.4, - "maxResults": 50 - } - } - } + "options": {}, + }, }, "mcp": {}, "tools": { "github-triage": false, - "github-pr-search": false - } + "github-pr-search": false, + }, } diff --git a/packages/opencode/src/kilocode/codebase-search/collection.ts b/packages/opencode/src/kilocode/codebase-search/collection.ts new file mode 100644 index 0000000000..052746ea50 --- /dev/null +++ b/packages/opencode/src/kilocode/codebase-search/collection.ts @@ -0,0 +1,39 @@ +// kilocode_change - new file +import { createHash } from "crypto" + +/** + * Collection naming utilities for codebase search + * Uses SHA-256 hash-based naming to match Kilo Code VSCode extension pattern + */ +export namespace CodebaseSearchCollection { + /** + * Generate collection name from workspace path + * Uses SHA-256 hash truncated to 16 hex chars + * Pattern: ws-{hash16} + * + * This matches the Kilo Code VSCode extension pattern for compatibility + * with collections created by the extension. + */ + export function generateFromWorkspace(workspacePath: string): string { + const hash = createHash("sha256").update(workspacePath).digest("hex") + return `ws-${hash.substring(0, 16)}` + } + + /** + * Get collection name for a workspace + * Returns explicit collection name if provided, otherwise generates one + */ + export function get( + workspacePath: string, + explicitCollection?: string, + ): string { + return explicitCollection || generateFromWorkspace(workspacePath) + } + + /** + * Check if a collection name follows the Kilo pattern + */ + export function isKiloPattern(collectionName: string): boolean { + return /^ws-[a-f0-9]{16}$/.test(collectionName) + } +} diff --git a/packages/opencode/src/kilocode/codebase-search/config.ts b/packages/opencode/src/kilocode/codebase-search/config.ts new file mode 100644 index 0000000000..3135a323d6 --- /dev/null +++ b/packages/opencode/src/kilocode/codebase-search/config.ts @@ -0,0 +1,68 @@ +// kilocode_change - new file +import { Config } from "@/config/config" +import { CodebaseSearchTypes, CODEBASE_SEARCH_DEFAULTS } from "./types" + +export namespace CodebaseSearchConfig { + /** + * Get codebase search configuration from Kilo provider options + * Returns null if not configured + */ + export async function get(): Promise { + const config = await Config.get() + const raw = config.provider?.kilo?.options?.codebase_search + + if (!raw) return null + + // Validate and return + return CodebaseSearchTypes.Config.parse(raw) + } + + /** + * Check if codebase search is configured + */ + export async function isConfigured(): Promise { + const config = await get() + return config !== null + } + + /** + * Get configuration with defaults applied + */ + export async function getWithDefaults(): Promise<{ + config: CodebaseSearchTypes.Config + similarityThreshold: number + maxResults: number + } | null> { + const config = await get() + if (!config) return null + + return { + config, + similarityThreshold: config.similarityThreshold ?? CODEBASE_SEARCH_DEFAULTS.similarityThreshold, + maxResults: config.maxResults ?? CODEBASE_SEARCH_DEFAULTS.maxResults, + } + } + + /** + * Generate example configuration for error messages + */ + export function getExampleConfig(): object { + return { + provider: { + kilo: { + options: { + codebase_search: { + embedModel: CODEBASE_SEARCH_DEFAULTS.defaultEmbedModel, + vectorDb: { + type: "qdrant", + url: CODEBASE_SEARCH_DEFAULTS.defaultQdrantUrl, + }, + similarityThreshold: CODEBASE_SEARCH_DEFAULTS.similarityThreshold, + maxResults: CODEBASE_SEARCH_DEFAULTS.maxResults, + }, + }, + }, + }, + } + } +} diff --git a/packages/opencode/src/kilocode/codebase-search/embeddings.ts b/packages/opencode/src/kilocode/codebase-search/embeddings.ts new file mode 100644 index 0000000000..4fb811c18d --- /dev/null +++ b/packages/opencode/src/kilocode/codebase-search/embeddings.ts @@ -0,0 +1,178 @@ +// kilocode_change - new file +import { Auth } from "@/auth" +import { CodebaseSearchTypes } from "./types" + +/** + * Embedding provider implementations for codebase search + * Supports OpenAI, Mistral, and Ollama (local) embedding providers + */ +export namespace CodebaseSearchEmbeddings { + /** + * Map embed model name to provider and model ID + */ + export function getProvider(model: string): CodebaseSearchTypes.EmbeddingProvider { + const lowerModel = model.toLowerCase() + + if (lowerModel.includes("text-embedding")) { + return { provider: "openai", modelId: model } + } + if (lowerModel.includes("codestral") || lowerModel.includes("mistral")) { + return { provider: "mistral", modelId: model } + } + if (lowerModel.includes("nomic")) { + return { provider: "ollama", modelId: model } + } + if (lowerModel.includes("openai")) { + return { provider: "openai", modelId: model } + } + + // Default to OpenAI + return { provider: "openai", modelId: "text-embedding-3-small" } + } + + /** + * Generate embedding using OpenAI + */ + export async function generateOpenAI( + text: string, + apiKey: string, + model = "text-embedding-3-small", + ): Promise { + const response = await fetch("https://api.openai.com/v1/embeddings", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + input: text, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`OpenAI embeddings API error (${response.status}): ${errorText}`) + } + + const data = await response.json() + return data.data[0].embedding + } + + /** + * Generate embedding using Mistral + */ + export async function generateMistral( + text: string, + apiKey: string, + model = "codestral-embed-2505", + ): Promise { + const response = await fetch("https://api.mistral.ai/v1/embeddings", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + input: text, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Mistral embeddings API error (${response.status}): ${errorText}`) + } + + const data = await response.json() + return data.data[0].embedding + } + + /** + * Generate embedding using Ollama (local) + */ + export async function generateOllama( + text: string, + model = "nomic-embed-text", + ): Promise { + const response = await fetch("http://localhost:11434/api/embeddings", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model, + prompt: text, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Ollama embeddings API error (${response.status}): ${errorText}`) + } + + const data = await response.json() + return data.embedding + } + + /** + * Auth map type for provider credentials + */ + type AuthMap = Map + + /** + * Generate embedding using the configured provider + */ + export async function generate( + text: string, + embedModel: string, + authMap: AuthMap, + ): Promise { + const { provider, modelId } = getProvider(embedModel) + + if (provider === "openai") { + const openaiAuth = authMap.get("openai") + if (!openaiAuth) { + throw new Error("OpenAI API key not found. Please configure openai provider in auth settings.") + } + const apiKey = openaiAuth.type === "oauth" ? openaiAuth.access : openaiAuth.key + if (!apiKey) { + throw new Error("OpenAI API key not found in auth configuration.") + } + return generateOpenAI(text, apiKey, modelId) + } + + if (provider === "mistral") { + const mistralAuth = authMap.get("mistral") + if (!mistralAuth) { + throw new Error("Mistral API key not found. Please configure mistral provider in auth settings.") + } + const apiKey = mistralAuth.type === "oauth" ? mistralAuth.access : mistralAuth.key + if (!apiKey) { + throw new Error("Mistral API key not found in auth configuration.") + } + return generateMistral(text, apiKey, modelId) + } + + if (provider === "ollama") { + return generateOllama(text, modelId) + } + + throw new Error(`Unsupported embedding provider: ${provider}. Supported providers: openai, mistral, ollama`) + } + + /** + * Build auth map from stored auth credentials + */ + export async function buildAuthMap(): Promise { + const authMap: AuthMap = new Map() + + const openaiAuth = await Auth.get("openai") + const mistralAuth = await Auth.get("mistral") + + if (openaiAuth) authMap.set("openai", openaiAuth) + if (mistralAuth) authMap.set("mistral", mistralAuth) + + return authMap + } +} diff --git a/packages/opencode/src/kilocode/codebase-search/index.ts b/packages/opencode/src/kilocode/codebase-search/index.ts new file mode 100644 index 0000000000..11abdc3566 --- /dev/null +++ b/packages/opencode/src/kilocode/codebase-search/index.ts @@ -0,0 +1,19 @@ +// kilocode_change - new file +/** + * Kilo Code codebase search module + * + * This module provides Kilo-specific configuration and utilities for + * the codebase-search tool. It extracts Kilo-specific logic from the + * shared tool implementation to minimize merge conflicts with upstream. + * + * Components: + * - types.ts: Configuration schemas and types + * - config.ts: Configuration loading and validation + * - collection.ts: Collection naming (matches VSCode extension pattern) + * - embeddings.ts: Embedding provider implementations + */ + +export { CodebaseSearchTypes, CODEBASE_SEARCH_DEFAULTS } from "./types" +export { CodebaseSearchConfig } from "./config" +export { CodebaseSearchCollection } from "./collection" +export { CodebaseSearchEmbeddings } from "./embeddings" diff --git a/packages/opencode/src/kilocode/codebase-search/types.ts b/packages/opencode/src/kilocode/codebase-search/types.ts new file mode 100644 index 0000000000..851feae2ac --- /dev/null +++ b/packages/opencode/src/kilocode/codebase-search/types.ts @@ -0,0 +1,66 @@ +// kilocode_change - new file +import z from "zod" + +/** + * Configuration types for codebase search + * These types define the Kilo-specific configuration schema for semantic code search + */ +export namespace CodebaseSearchTypes { + /** + * Supported vector database types + */ + export const VectorDbType = z.enum(["qdrant", "lancedb"]) + export type VectorDbType = z.infer + + /** + * Vector database configuration + */ + export const VectorDbConfig = z.object({ + type: VectorDbType, + url: z.string().optional(), + collection: z.string().optional(), + }) + export type VectorDbConfig = z.infer + + /** + * Full codebase search configuration + */ + export const Config = z.object({ + embedModel: z.string(), + vectorDb: VectorDbConfig, + similarityThreshold: z.number().min(0).max(1).optional(), + maxResults: z.number().int().positive().optional(), + }) + export type Config = z.infer + + /** + * Search result from vector database + */ + export const SearchResult = z.object({ + filePath: z.string(), + score: z.number(), + startLine: z.number(), + endLine: z.number(), + codeChunk: z.string(), + }) + export type SearchResult = z.infer + + /** + * Embedding provider info + */ + export const EmbeddingProvider = z.object({ + provider: z.string(), + modelId: z.string(), + }) + export type EmbeddingProvider = z.infer +} + +/** + * Default configuration values + */ +export const CODEBASE_SEARCH_DEFAULTS = { + similarityThreshold: 0.4, + maxResults: 50, + defaultEmbedModel: "codestral-embed-2505", + defaultQdrantUrl: "http://localhost:6333", +} as const diff --git a/packages/opencode/src/kilocode/index.ts b/packages/opencode/src/kilocode/index.ts index 261ff142c7..9f1c33cb85 100644 --- a/packages/opencode/src/kilocode/index.ts +++ b/packages/opencode/src/kilocode/index.ts @@ -5,3 +5,6 @@ export { McpMigrator } from "./mcp-migrator" export { IgnoreMigrator } from "./ignore-migrator" // kilocode_change export { KilocodeConfigInjector } from "./config-injector" export { KilocodePaths } from "./paths" +// kilocode_change start - export codebase-search module +export * as CodebaseSearch from "./codebase-search" +// kilocode_change end diff --git a/packages/opencode/src/tool/codebase-search.ts b/packages/opencode/src/tool/codebase-search.ts index 2169144e1d..e31e5ff24f 100644 --- a/packages/opencode/src/tool/codebase-search.ts +++ b/packages/opencode/src/tool/codebase-search.ts @@ -1,11 +1,12 @@ import z from "zod" import { Tool } from "./tool" -import { createHash } from "crypto" import { Instance } from "../project/instance" -import { Config } from "../config/config" import { Auth } from "../auth" import { Log } from "../util/log" import DESCRIPTION from "./codebase-search.txt" +// kilocode_change start - use Kilo-specific modules +import { CodebaseSearchConfig, CodebaseSearchCollection, CodebaseSearchEmbeddings } from "@/kilocode/codebase-search" +// kilocode_change end const log = Log.create({ service: "tool.codebase-search" }) @@ -24,143 +25,6 @@ interface QdrantSearchResponse { }> } -// Helper function to generate collection name from workspace path (matches Kilo Code extension pattern) -function generateCollectionName(workspacePath: string): string { - const hash = createHash("sha256").update(workspacePath).digest("hex") - return `ws-${hash.substring(0, 16)}` -} - -// Helper function to map embed model to provider and model ID -function getEmbeddingProvider(model: string): { provider: string; modelId: string } { - const lowerModel = model.toLowerCase() - - if (lowerModel.includes("text-embedding")) { - return { provider: "openai", modelId: model } - } - if (lowerModel.includes("codestral") || lowerModel.includes("mistral")) { - return { provider: "mistral", modelId: model } - } - if (lowerModel.includes("nomic")) { - return { provider: "ollama", modelId: model } - } - if (lowerModel.includes("openai")) { - return { provider: "openai", modelId: model } - } - - return { provider: "openai", modelId: "text-embedding-3-small" } -} - -// Helper function to generate embedding using OpenAI -async function generateOpenAIEmbedding( - text: string, - apiKey: string, - model = "text-embedding-3-small", -): Promise { - const response = await fetch("https://api.openai.com/v1/embeddings", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model, - input: text, - }), - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`OpenAI embeddings API error (${response.status}): ${errorText}`) - } - - const data = await response.json() - return data.data[0].embedding -} - -// Helper function to generate embedding using Mistral -async function generateMistralEmbedding( - text: string, - apiKey: string, - model = "codestral-embed-2505", -): Promise { - const response = await fetch("https://api.mistral.ai/v1/embeddings", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model, - input: text, - }), - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Mistral embeddings API error (${response.status}): ${errorText}`) - } - - const data = await response.json() - return data.data[0].embedding -} - -// Helper function to generate embedding using Ollama (local) -async function generateOllamaEmbedding(text: string, model = "nomic-embed-text"): Promise { - const response = await fetch("http://localhost:11434/api/embeddings", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model, - prompt: text, - }), - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Ollama embeddings API error (${response.status}): ${errorText}`) - } - - const data = await response.json() - return data.embedding -} - -// Helper function to generate embedding -async function generateEmbedding(text: string, embedModel: string, authMap: Map): Promise { - const { provider, modelId } = getEmbeddingProvider(embedModel) - - if (provider === "openai") { - const openaiAuth = authMap.get("openai") - if (!openaiAuth) { - throw new Error("OpenAI API key not found. Please configure openai provider in auth settings.") - } - const apiKey = openaiAuth.type === "oauth" ? openaiAuth.access : openaiAuth.key - if (!apiKey) { - throw new Error("OpenAI API key not found in auth configuration.") - } - return generateOpenAIEmbedding(text, apiKey, modelId) - } - - if (provider === "mistral") { - const mistralAuth = authMap.get("mistral") - if (!mistralAuth) { - throw new Error("Mistral API key not found. Please configure mistral provider in auth settings.") - } - const apiKey = mistralAuth.type === "oauth" ? mistralAuth.access : mistralAuth.key - if (!apiKey) { - throw new Error("Mistral API key not found in auth configuration.") - } - return generateMistralEmbedding(text, apiKey, modelId) - } - - if (provider === "ollama") { - return generateOllamaEmbedding(text, modelId) - } - - throw new Error(`Unsupported embedding provider: ${provider}. Supported providers: openai, mistral, ollama`) -} - // Helper function to search Qdrant async function searchQdrant( vector: number[], @@ -277,38 +141,20 @@ export const CodebaseSearchTool = Tool.define("codebase-search", { }, }) - const config = await Config.get() - - const codebaseSearch = config.provider?.kilo?.options?.codebase_search + // kilocode_change start - use Kilo-specific config module + const configResult = await CodebaseSearchConfig.getWithDefaults() - if (!codebaseSearch) { + if (!configResult) { throw new Error( "Codebase search is not configured. Please configure in opencode.json:\n" + - JSON.stringify( - { - provider: { - kilo: { - options: { - codebase_search: { - embedModel: "codestral-embed-2505", - vectorDb: { - type: "qdrant", - url: "http://localhost:6333", - }, - similarityThreshold: 0.4, - maxResults: 50, - }, - }, - }, - }, - }, - null, - 2, - ), + JSON.stringify(CodebaseSearchConfig.getExampleConfig(), null, 2), ) } - const { embedModel, vectorDb, similarityThreshold = 0.4, maxResults = 50 } = codebaseSearch + const { config, similarityThreshold, maxResults } = configResult + // kilocode_change end + + const { embedModel, vectorDb } = config if (!embedModel) { throw new Error( @@ -334,12 +180,11 @@ export const CodebaseSearchTool = Tool.define("codebase-search", { throw new Error(`Unsupported vector database type: ${vectorDb.type}. Supported types: qdrant`) } - // Auto-generate collection name if not specified - const collection = vectorDb.collection || generateCollectionName(workspacePath) + // kilocode_change start - use Kilo-specific collection naming + const collection = CodebaseSearchCollection.get(workspacePath, vectorDb.collection) + // kilocode_change end // Get auth keys - const openaiAuth = await Auth.get("openai") - const mistralAuth = await Auth.get("mistral") const qdrantAuth = await Auth.get("qdrant") if (!qdrantAuth) { @@ -350,16 +195,18 @@ export const CodebaseSearchTool = Tool.define("codebase-search", { throw new Error("Qdrant API key not found in auth configuration.") } - const authMap = new Map() - if (openaiAuth) authMap.set("openai", openaiAuth) - if (mistralAuth) authMap.set("mistral", mistralAuth) + // kilocode_change start - use Kilo-specific embeddings module + const authMap = await CodebaseSearchEmbeddings.buildAuthMap() + // kilocode_change end try { - const embedding = await generateEmbedding(params.query, embedModel, authMap) + // kilocode_change start - use Kilo-specific embeddings module + const embedding = await CodebaseSearchEmbeddings.generate(params.query, embedModel, authMap) + // kilocode_change end const results = await searchQdrant( embedding, - vectorDb.url, + vectorDb.url!, collection, qdrantApiKey, maxResults, diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index ebbe7c702d..f4b19bc0e4 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -28,6 +28,9 @@ import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" +// kilocode_change start - import for codebase-search config check +import { CodebaseSearchConfig } from "@/kilocode/codebase-search" +// kilocode_change end export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -95,6 +98,9 @@ export namespace ToolRegistry { async function all(): Promise { const custom = await state().then((x) => x.custom) const config = await Config.get() + // kilocode_change start - check if codebase-search is configured + const codebaseSearchConfigured = await CodebaseSearchConfig.isConfigured() + // kilocode_change end return [ InvalidTool, @@ -111,7 +117,9 @@ export namespace ToolRegistry { // TodoReadTool, WebSearchTool, CodeSearchTool, - CodebaseSearchTool, + // kilocode_change start - only include codebase-search if configured + ...(codebaseSearchConfigured ? [CodebaseSearchTool] : []), + // kilocode_change end SkillTool, ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), diff --git a/packages/opencode/test/kilocode/codebase-search.test.ts b/packages/opencode/test/kilocode/codebase-search.test.ts new file mode 100644 index 0000000000..98ee1afcf1 --- /dev/null +++ b/packages/opencode/test/kilocode/codebase-search.test.ts @@ -0,0 +1,138 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import { CodebaseSearchCollection } from "@/kilocode/codebase-search/collection" +import { CodebaseSearchTypes, CODEBASE_SEARCH_DEFAULTS } from "@/kilocode/codebase-search/types" +import { CodebaseSearchEmbeddings } from "@/kilocode/codebase-search/embeddings" + +describe("CodebaseSearchCollection", () => { + test("generates consistent collection names from workspace paths", () => { + const workspacePath = "/Users/test/projects/my-project" + const name1 = CodebaseSearchCollection.generateFromWorkspace(workspacePath) + const name2 = CodebaseSearchCollection.generateFromWorkspace(workspacePath) + + expect(name1).toBe(name2) + expect(name1).toMatch(/^ws-[a-f0-9]{16}$/) + }) + + test("generates different names for different paths", () => { + const name1 = CodebaseSearchCollection.generateFromWorkspace("/path/to/project-a") + const name2 = CodebaseSearchCollection.generateFromWorkspace("/path/to/project-b") + + expect(name1).not.toBe(name2) + }) + + test("returns explicit collection when provided", () => { + const explicitName = "my-custom-collection" + const result = CodebaseSearchCollection.get("/any/path", explicitName) + + expect(result).toBe(explicitName) + }) + + test("generates collection when explicit name not provided", () => { + const workspacePath = "/path/to/project" + const result = CodebaseSearchCollection.get(workspacePath) + const expected = CodebaseSearchCollection.generateFromWorkspace(workspacePath) + + expect(result).toBe(expected) + }) + + test("detects Kilo pattern correctly", () => { + const validName = CodebaseSearchCollection.generateFromWorkspace("/some/path") + expect(CodebaseSearchCollection.isKiloPattern(validName)).toBe(true) + + expect(CodebaseSearchCollection.isKiloPattern("my-collection")).toBe(false) + expect(CodebaseSearchCollection.isKiloPattern("ws-12345")).toBe(false) + expect(CodebaseSearchCollection.isKiloPattern("ws-ghijklmnopqrst")).toBe(false) + }) +}) + +describe("CodebaseSearchTypes", () => { + test("validates valid config", () => { + const validConfig = { + embedModel: "codestral-embed-2505", + vectorDb: { + type: "qdrant" as const, + url: "http://localhost:6333", + }, + similarityThreshold: 0.5, + maxResults: 25, + } + + const result = CodebaseSearchTypes.Config.safeParse(validConfig) + expect(result.success).toBe(true) + }) + + test("rejects invalid vector db type", () => { + const invalidConfig = { + embedModel: "text-embedding-3-small", + vectorDb: { + type: "invalid", + url: "http://localhost:6333", + }, + } + + const result = CodebaseSearchTypes.Config.safeParse(invalidConfig) + expect(result.success).toBe(false) + }) + + test("rejects similarity threshold out of range", () => { + const invalidConfig = { + embedModel: "text-embedding-3-small", + vectorDb: { + type: "qdrant" as const, + }, + similarityThreshold: 1.5, + } + + const result = CodebaseSearchTypes.Config.safeParse(invalidConfig) + expect(result.success).toBe(false) + }) + + test("applies defaults correctly", () => { + expect(CODEBASE_SEARCH_DEFAULTS.similarityThreshold).toBe(0.4) + expect(CODEBASE_SEARCH_DEFAULTS.maxResults).toBe(50) + expect(CODEBASE_SEARCH_DEFAULTS.defaultEmbedModel).toBe("codestral-embed-2505") + }) +}) + +describe("CodebaseSearchEmbeddings", () => { + test("detects OpenAI embedding provider", () => { + expect(CodebaseSearchEmbeddings.getProvider("text-embedding-3-small")).toEqual({ + provider: "openai", + modelId: "text-embedding-3-small", + }) + expect(CodebaseSearchEmbeddings.getProvider("text-embedding-3-large")).toEqual({ + provider: "openai", + modelId: "text-embedding-3-large", + }) + expect(CodebaseSearchEmbeddings.getProvider("openai-embedding")).toEqual({ + provider: "openai", + modelId: "openai-embedding", + }) + }) + + test("detects Mistral embedding provider", () => { + expect(CodebaseSearchEmbeddings.getProvider("codestral-embed-2505")).toEqual({ + provider: "mistral", + modelId: "codestral-embed-2505", + }) + expect(CodebaseSearchEmbeddings.getProvider("mistral-embed")).toEqual({ + provider: "mistral", + modelId: "mistral-embed", + }) + }) + + test("detects Ollama embedding provider", () => { + expect(CodebaseSearchEmbeddings.getProvider("nomic-embed-text")).toEqual({ + provider: "ollama", + modelId: "nomic-embed-text", + }) + }) + + test("defaults to OpenAI for unknown models", () => { + expect(CodebaseSearchEmbeddings.getProvider("unknown-model")).toEqual({ + provider: "openai", + modelId: "text-embedding-3-small", + }) + }) +}) From 127e74787f33854b839a1df9bf2b14652663f0c1 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 12 Feb 2026 15:29:43 +0100 Subject: [PATCH 03/12] refactor(tool): change codebase-search identifiers to snake_case Rename tool definition and permission identifiers from kebab-case (codebase-search) to snake_case (codebase_search) for naming consistency. --- packages/opencode/src/tool/codebase-search.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/codebase-search.ts b/packages/opencode/src/tool/codebase-search.ts index e31e5ff24f..11148de5b7 100644 --- a/packages/opencode/src/tool/codebase-search.ts +++ b/packages/opencode/src/tool/codebase-search.ts @@ -118,7 +118,7 @@ function formatResults( return output } -export const CodebaseSearchTool = Tool.define("codebase-search", { +export const CodebaseSearchTool = Tool.define("codebase_search", { description: DESCRIPTION, parameters: z.object({ query: z.string().describe("The search query in natural language (required)"), @@ -132,7 +132,7 @@ export const CodebaseSearchTool = Tool.define("codebase-search", { // Ask for permission await ctx.ask({ - permission: "codebase-search", + permission: "codebase_search", patterns: [params.query, ...(params.path ? [params.path] : [])], always: ["*"], metadata: { From 708f00854453da2a94183e47b4bec236b11ef21a Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Tue, 17 Feb 2026 23:15:00 +0100 Subject: [PATCH 04/12] feat(cli): add codebase search tool configuration Add a new codebase search tool with support for semantic search using Qdrant or LanceDB vector databases. Includes: - Documentation in AGENTS.md - Codebase search configuration dialog (dialog-tool-codebase-search.tsx) - Tool selection dialog (dialog-tool.tsx) - Integration with prompt menu in prompt/index.tsx The tool allows configuring: - Embedding model (e.g., codestral-embed-2505, text-embedding-3-small) - Vector database type (Qdrant or LanceDB) - Database connection settings (URL for Qdrant, path for LanceDB) - Similarity threshold for search results - Maximum number of results to return Configuration is saved to opencode.jsonc file in the project or global config directory. --- packages/opencode/AGENTS.md | 32 ++ .../component/dialog-tool-codebase-search.tsx | 273 ++++++++++++++++++ .../src/cli/cmd/tui/component/dialog-tool.tsx | 193 +++++++++++++ .../cli/cmd/tui/component/prompt/index.tsx | 16 + .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../cli/cmd/tui/routes/session/permission.tsx | 2 +- .../kilocode/codebase-search/collection.ts | 7 +- .../kilocode/codebase-search/embeddings.ts | 11 +- .../src/kilocode/codebase-search/index.ts | 4 +- packages/opencode/src/server/routes/config.ts | 25 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 20 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 20 ++ packages/sdk/openapi.json | 40 +++ plans/codebase-search-to-opencode-plan.md | 39 ++- 14 files changed, 659 insertions(+), 25 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-tool-codebase-search.tsx create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md index 216a931712..a27d9ce2a8 100644 --- a/packages/opencode/AGENTS.md +++ b/packages/opencode/AGENTS.md @@ -64,3 +64,35 @@ Hono-based HTTP server with OpenAPI spec generation. SSE for real-time events. W ## Providers and Models Uses the **Vercel AI SDK** as the abstraction layer. Providers are loaded from a bundled map or dynamically installed at runtime. Models come from models.dev (external API), cached locally. + +## Tools + +### codebase_search + +Requires configuration in `opencode.json`: + +```json +{ + "provider": { + "kilo": { + "options": { + "codebase_search": { + "embedModel": "codestral-embed-2505", // or "text-embedding-3-small" for OpenAI, "nomic-embed-text" for Ollama + "vectorDb": { + "type": "qdrant", + "url": "http://localhost:6333" + }, + "similarityThreshold": 0.4, + "maxResults": 50 + } + } + } + } +} +``` + +- **Embedding providers supported**: OpenAI (text-embedding-3-small), Mistral (codestral-embed-2505), Ollama (nomic-embed-text) +- **Vector database supported**: Qdrant +- **API keys**: Configure in auth settings (`~/.local/share/kilo/auth.json`) for openai, mistral, qdrant +- **Collection name**: Automatically generated from workspace path using SHA-256 hash +- **Requirements**: Qdrant running and accessible, collection exists with indexed data diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-tool-codebase-search.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-tool-codebase-search.tsx new file mode 100644 index 0000000000..fa0e4ea27e --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-tool-codebase-search.tsx @@ -0,0 +1,273 @@ +// kilocode_change - new file +import { TextAttributes, InputRenderable } from "@opentui/core" +import { useTheme } from "../context/theme" +import { useDialog } from "@tui/ui/dialog" +import { createStore } from "solid-js/store" +import { createEffect, onMount, createSignal, Show } from "solid-js" +import { useKeyboard } from "@opentui/solid" +import { CODEBASE_SEARCH_DEFAULTS } from "@/kilocode/codebase-search/types" + +export type CodebaseSearchConfig = { + embedModel: string + vectorDbType: "qdrant" | "lancedb" + qdrantUrl: string + lancedbPath: string + similarityThreshold: number + maxResults: number +} + +export type DialogToolCodebaseSearchProps = { + initialConfig?: Partial + onSave: (config: CodebaseSearchConfig) => void + onCancel?: () => void +} + +type FieldKey = "embedModel" | "vectorDbType" | "qdrantUrl" | "lancedbPath" | "similarityThreshold" | "maxResults" + +export function DialogToolCodebaseSearch(props: DialogToolCodebaseSearchProps) { + const dialog = useDialog() + const { theme } = useTheme() + const [store, setStore] = createStore({ + embedModel: props.initialConfig?.embedModel ?? CODEBASE_SEARCH_DEFAULTS.defaultEmbedModel, + vectorDbType: props.initialConfig?.vectorDbType ?? "qdrant", + qdrantUrl: props.initialConfig?.qdrantUrl ?? CODEBASE_SEARCH_DEFAULTS.defaultQdrantUrl, + lancedbPath: props.initialConfig?.lancedbPath ?? "", + similarityThreshold: props.initialConfig?.similarityThreshold ?? CODEBASE_SEARCH_DEFAULTS.similarityThreshold, + maxResults: props.initialConfig?.maxResults ?? CODEBASE_SEARCH_DEFAULTS.maxResults, + }) + + const [activeField, setActiveField] = createSignal(0) + + // Track raw string values for number fields during editing + // This prevents "0." from becoming "0" when user is still typing + const [rawNumberValues, setRawNumberValues] = createStore>({ + similarityThreshold: String(store.similarityThreshold), + maxResults: String(store.maxResults), + }) + + let inputs: (InputRenderable | undefined)[] = [] + let scrollboxRef: any + + dialog.setSize("large") + + const fields: { key: FieldKey; label: string; placeholder: string; type: "text" | "number" | "select" }[] = [ + { key: "embedModel", label: "Embed Model", placeholder: "e.g., codestral-embed-2505", type: "text" }, + { key: "vectorDbType", label: "Vector DB Type", placeholder: "qdrant or lancedb", type: "select" }, + { key: "qdrantUrl", label: "Qdrant URL", placeholder: "http://localhost:6333", type: "text" }, + { + key: "lancedbPath", + label: "LanceDB Vector Store Path", + placeholder: "Custom vector store path (optional)", + type: "text", + }, + { key: "similarityThreshold", label: "Similarity Threshold", placeholder: "0.0 - 1.0", type: "number" }, + { key: "maxResults", label: "Max Results", placeholder: "1 - 100", type: "number" }, + ] + + // Get visible fields based on current config + const visibleFields = () => { + const result: typeof fields = [] + for (const field of fields) { + // Only show Qdrant URL if vectorDbType is qdrant + if (field.key === "qdrantUrl" && store.vectorDbType !== "qdrant") continue + // Only show LanceDB path if vectorDbType is lancedb + if (field.key === "lancedbPath" && store.vectorDbType !== "lancedb") continue + result.push(field) + } + return result + } + + onMount(() => { + setTimeout(() => { + const visible = visibleFields() + const input = inputs[0] + if (input && !input.isDestroyed && visible.length > 0) { + input.focus() + } + }, 1) + }) + + createEffect(() => { + const idx = activeField() + const visible = visibleFields() + if (idx >= 0 && idx < visible.length) { + const field = visible[idx] + const actualIdx = fields.findIndex((f) => f.key === field.key) + const input = inputs[actualIdx] + if (input && !input.isDestroyed) { + input.focus() + } + } + }) + + // Commit all input values to store before saving + function commitAllValues() { + for (const field of fields) { + if (field.key === "vectorDbType") continue + + if (field.type === "number") { + // Use raw number value for parsing + const rawValue = rawNumberValues[field.key] + if (rawValue !== undefined) { + const val = parseFloat(rawValue) + if (!isNaN(val)) { + setStore(field.key, val) + } + } + } else { + const actualIdx = fields.findIndex((f) => f.key === field.key) + const input = inputs[actualIdx] + if (input && !input.isDestroyed) { + setStore(field.key, input.value) + } + } + } + } + + useKeyboard((evt) => { + const visible = visibleFields() + const currentField = visible[activeField()] + + // Enter on select field toggles the value + if (evt.name === "return" && currentField?.key === "vectorDbType") { + evt.preventDefault() + toggleVectorDb() + return + } + + // Enter on any other field saves the form + if (evt.name === "return") { + evt.preventDefault() + commitAllValues() + handleSave() + return + } + + if (evt.name === "tab") { + evt.preventDefault() + const direction = evt.shift ? -1 : 1 + let next = activeField() + direction + if (next < 0) next = visible.length - 1 + if (next >= visible.length) next = 0 + setActiveField(next) + } + + if (evt.name === "up") { + evt.preventDefault() + const next = activeField() - 1 + if (next >= 0) setActiveField(next) + } + + if (evt.name === "down") { + evt.preventDefault() + const next = activeField() + 1 + if (next < visible.length) setActiveField(next) + } + + // Space on select field toggles the value + if (evt.name === "space" && currentField?.key === "vectorDbType") { + evt.preventDefault() + toggleVectorDb() + return + } + + if (evt.name === "escape") { + props.onCancel?.() + dialog.clear() + } + }) + + function handleSave() { + props.onSave(store) + dialog.clear() + } + + function toggleVectorDb() { + setStore("vectorDbType", store.vectorDbType === "qdrant" ? "lancedb" : "qdrant") + } + + return ( + + + + Codebase Search Configuration + + dialog.clear()}> + esc + + + + Configure semantic code search settings + + + + + {visibleFields().map((field, idx) => { + const actualIdx = fields.findIndex((f) => f.key === field.key) + const isActive = activeField() === idx + return ( + + + {field.label}: + + { + inputs[actualIdx] = r + }} + focusedBackgroundColor={theme.backgroundPanel} + cursorColor={theme.primary} + focusedTextColor={theme.text} + textColor={theme.text} + onInput={(e) => { + if (field.type === "number") { + // Store raw string value to preserve "0." while typing + setRawNumberValues(field.key, e) + // Also parse and store numeric value if valid + const val = parseFloat(e) + if (!isNaN(val)) { + setStore(field.key, val) + } + } else { + setStore(field.key, e) + } + }} + value={ + field.type === "number" + ? (rawNumberValues[field.key] ?? String(store[field.key as keyof CodebaseSearchConfig])) + : (store[field.key as keyof CodebaseSearchConfig] as string) + } + placeholder={field.placeholder} + flexGrow={1} + maxWidth={70} + /> + } + > + toggleVectorDb()}> + + {store.vectorDbType.toUpperCase()} + + + (enter to toggle) + + + ) + })} + + + + + tab/↑↓ + navigate + | + space + toggle + | + enter + save + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx new file mode 100644 index 0000000000..00cd9aa58b --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx @@ -0,0 +1,193 @@ +// kilocode_change - new file +import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" +import { createMemo } from "solid-js" +import { useDialog } from "@tui/ui/dialog" +import { DialogToolCodebaseSearch, type CodebaseSearchConfig } from "./dialog-tool-codebase-search" +import { useToast } from "@tui/ui/toast" +import { useSync } from "@tui/context/sync" +import { useSDK } from "@tui/context/sdk" +import { modify, applyEdits, parse } from "jsonc-parser" +import { reconcile } from "solid-js/store" +import path from "path" +import fs from "fs/promises" + +export type DialogToolProps = { + onSelect?: (tool: string) => void +} + +// Static list of tools available in the prompt menu +const TOOLS = [ + { + name: "codebase_search", + description: "Find files most relevant to the search query using semantic search", + }, +] + +function getExistingConfig(sync: ReturnType): Partial | null { + const raw = (sync.data.config.provider as any)?.kilo?.options?.codebase_search + if (!raw) return null + + const vectorDb = raw.vectorDb || {} + return { + embedModel: raw.embedModel, + vectorDbType: vectorDb.type ?? "qdrant", + qdrantUrl: vectorDb.type === "qdrant" ? (vectorDb.url ?? "") : "", + lancedbPath: vectorDb.type === "lancedb" ? (vectorDb.path ?? "") : "", + similarityThreshold: raw.similarityThreshold, + maxResults: raw.maxResults, + } +} + +async function findConfigPath(sync: ReturnType): Promise { + const projectDir = sync.data.path.directory + const globalConfigDir = sync.data.path.config + + // Priority: .opencode/opencode.jsonc > project/opencode.jsonc > global/opencode.jsonc + const candidates = [ + projectDir && path.join(projectDir, ".opencode", "opencode.jsonc"), + projectDir && path.join(projectDir, ".opencode", "opencode.json"), + projectDir && path.join(projectDir, "opencode.jsonc"), + projectDir && path.join(projectDir, "opencode.json"), + globalConfigDir && path.join(globalConfigDir, "opencode.jsonc"), + globalConfigDir && path.join(globalConfigDir, "opencode.json"), + ].filter(Boolean) as string[] + + // Find first existing config + for (const candidate of candidates) { + try { + await fs.access(candidate) + return candidate + } catch {} + } + + // No existing config - default to project's .opencode directory + if (projectDir) { + return path.join(projectDir, ".opencode", "opencode.jsonc") + } + + // Fall back to global config + if (globalConfigDir) { + return path.join(globalConfigDir, "opencode.jsonc") + } + + // Throw error instead of returning invalid path + throw new Error("Could not determine config path: no project or global config directory available") +} + +async function readConfigFromFile(sync: ReturnType): Promise | null> { + try { + const configPath = await findConfigPath(sync) + const file = Bun.file(configPath) + if (!(await file.exists())) { + return null + } + + const text = await file.text() + const config = parse(text) as any + const raw = config?.provider?.kilo?.options?.codebase_search + if (!raw) return null + + const vectorDb = raw.vectorDb || {} + return { + embedModel: raw.embedModel, + vectorDbType: vectorDb.type ?? "qdrant", + qdrantUrl: vectorDb.type === "qdrant" ? (vectorDb.url ?? "") : "", + lancedbPath: vectorDb.type === "lancedb" ? (vectorDb.path ?? "") : "", + similarityThreshold: raw.similarityThreshold, + maxResults: raw.maxResults, + } + } catch { + return null + } +} + +export function DialogTool(props: DialogToolProps) { + const dialog = useDialog() + const toast = useToast() + const sync = useSync() + const sdk = useSDK() + dialog.setSize("large") + + async function saveCodebaseSearchConfig(config: CodebaseSearchConfig) { + try { + const configPath = await findConfigPath(sync) + + // Build the config object in the expected format + const vectorDbConfig: any = { + type: config.vectorDbType, + } + + if (config.vectorDbType === "qdrant") { + vectorDbConfig.url = config.qdrantUrl + } else { + vectorDbConfig.path = config.lancedbPath + } + + const codebaseSearchConfig = { + embedModel: config.embedModel, + vectorDb: vectorDbConfig, + similarityThreshold: config.similarityThreshold, + maxResults: config.maxResults, + } + + // Read existing config or create empty + let text = "{}" + const file = Bun.file(configPath) + if (await file.exists()) { + text = await file.text() + } + + // Use jsonc-parser to modify while preserving comments + const edits = modify(text, ["provider", "kilo", "options", "codebase_search"], codebaseSearchConfig, { + formattingOptions: { tabSize: 2, insertSpaces: true }, + }) + const result = applyEdits(text, edits) + + // Ensure directory exists + const dir = path.dirname(configPath) + await fs.mkdir(dir, { recursive: true }) + + // Write the config + await Bun.write(configPath, result) + + // Invalidate server config cache and reload + await sdk.client.config.reload() + const configResponse = await sdk.client.config.get() + if (configResponse.data) { + sync.set("config", reconcile(configResponse.data)) + } + + toast.show({ + variant: "success", + message: "Codebase search configuration saved to " + configPath, + duration: 3000, + }) + + props.onSelect?.("codebase_search") + } catch (error) { + toast.show({ + variant: "error", + message: `Failed to save config: ${error instanceof Error ? error.message : String(error)}`, + duration: 5000, + }) + } + } + + const options = createMemo[]>(() => { + const maxWidth = Math.max(0, ...TOOLS.map((t) => t.name.length)) + return TOOLS.map((tool) => ({ + title: tool.name.padEnd(maxWidth), + description: tool.description, + value: tool.name, + category: "Tools", + onSelect: async () => { + const config = await readConfigFromFile(sync) + dialog.replace(() => ( + + )) + }, + })) + }) + + return +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d59e683799..d4bd50736d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -32,6 +32,9 @@ import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" +// kilocode_change start +import { DialogTool } from "../dialog-tool" +// kilocode_change end export type PromptProps = { sessionID?: string @@ -351,6 +354,19 @@ export function Prompt(props: PromptProps) { )) }, }, + // kilocode_change start + { + title: "Tools", + value: "prompt.tools", + category: "Prompt", + slash: { + name: "tools", + }, + onSelect: () => { + dialog.replace(() => ) + }, + }, + // kilocode_change end ] }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index a1288607db..16cfcaae16 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1464,7 +1464,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess - + diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 5d418f74ac..e6fcd21ceb 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -236,7 +236,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { - + diff --git a/packages/opencode/src/kilocode/codebase-search/collection.ts b/packages/opencode/src/kilocode/codebase-search/collection.ts index 052746ea50..dd9447e4ae 100644 --- a/packages/opencode/src/kilocode/codebase-search/collection.ts +++ b/packages/opencode/src/kilocode/codebase-search/collection.ts @@ -10,7 +10,7 @@ export namespace CodebaseSearchCollection { * Generate collection name from workspace path * Uses SHA-256 hash truncated to 16 hex chars * Pattern: ws-{hash16} - * + * * This matches the Kilo Code VSCode extension pattern for compatibility * with collections created by the extension. */ @@ -23,10 +23,7 @@ export namespace CodebaseSearchCollection { * Get collection name for a workspace * Returns explicit collection name if provided, otherwise generates one */ - export function get( - workspacePath: string, - explicitCollection?: string, - ): string { + export function get(workspacePath: string, explicitCollection?: string): string { return explicitCollection || generateFromWorkspace(workspacePath) } diff --git a/packages/opencode/src/kilocode/codebase-search/embeddings.ts b/packages/opencode/src/kilocode/codebase-search/embeddings.ts index 4fb811c18d..b01b08c9e1 100644 --- a/packages/opencode/src/kilocode/codebase-search/embeddings.ts +++ b/packages/opencode/src/kilocode/codebase-search/embeddings.ts @@ -91,10 +91,7 @@ export namespace CodebaseSearchEmbeddings { /** * Generate embedding using Ollama (local) */ - export async function generateOllama( - text: string, - model = "nomic-embed-text", - ): Promise { + export async function generateOllama(text: string, model = "nomic-embed-text"): Promise { const response = await fetch("http://localhost:11434/api/embeddings", { method: "POST", headers: { @@ -123,11 +120,7 @@ export namespace CodebaseSearchEmbeddings { /** * Generate embedding using the configured provider */ - export async function generate( - text: string, - embedModel: string, - authMap: AuthMap, - ): Promise { + export async function generate(text: string, embedModel: string, authMap: AuthMap): Promise { const { provider, modelId } = getProvider(embedModel) if (provider === "openai") { diff --git a/packages/opencode/src/kilocode/codebase-search/index.ts b/packages/opencode/src/kilocode/codebase-search/index.ts index 11abdc3566..48a4e5f4a3 100644 --- a/packages/opencode/src/kilocode/codebase-search/index.ts +++ b/packages/opencode/src/kilocode/codebase-search/index.ts @@ -1,11 +1,11 @@ // kilocode_change - new file /** * Kilo Code codebase search module - * + * * This module provides Kilo-specific configuration and utilities for * the codebase-search tool. It extracts Kilo-specific logic from the * shared tool implementation to minimize merge conflicts with upstream. - * + * * Components: * - types.ts: Configuration schemas and types * - config.ts: Configuration loading and validation diff --git a/packages/opencode/src/server/routes/config.ts b/packages/opencode/src/server/routes/config.ts index e6ac26bb0d..11e3755b07 100644 --- a/packages/opencode/src/server/routes/config.ts +++ b/packages/opencode/src/server/routes/config.ts @@ -62,6 +62,31 @@ export const ConfigRoutes = lazy(() => return c.json(config) }, ) + // kilocode_change start - Add reload endpoint to invalidate config cache + .post( + "/reload", + describeRoute({ + summary: "Reload configuration", + description: "Invalidate config cache and reload from files.", + operationId: "config.reload", + responses: { + 200: { + description: "Config cache invalidated", + content: { + "application/json": { + schema: resolver(z.object({ success: z.boolean() })), + }, + }, + }, + }, + }), + async (c) => { + const { Instance } = await import("../../project/instance") + await Instance.dispose() + return c.json({ success: true }) + }, + ) + // kilocode_change end .get( "/providers", describeRoute({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 8f4101975c..1aeb95d9a3 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -17,6 +17,7 @@ import type { Config as Config3, ConfigGetResponses, ConfigProvidersResponses, + ConfigReloadResponses, ConfigUpdateErrors, ConfigUpdateResponses, EventSubscribeResponses, @@ -706,6 +707,25 @@ export class Config2 extends HeyApiClient { }) } + /** + * Reload configuration + * + * Invalidate config cache and reload from files. + */ + public reload( + parameters?: { + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + return (options?.client ?? this.client).post({ + url: "/config/reload", + ...options, + ...params, + }) + } + /** * List config providers * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d964a2990a..da95e60abc 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2680,6 +2680,26 @@ export type ConfigUpdateResponses = { export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] +export type ConfigReloadData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/config/reload" +} + +export type ConfigReloadResponses = { + /** + * Config cache invalidated + */ + 200: { + success: boolean + } +} + +export type ConfigReloadResponse = ConfigReloadResponses[keyof ConfigReloadResponses] + export type ConfigProvidersData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index e10a696aa2..f8fcc13675 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -854,6 +854,46 @@ ] } }, + "/config/reload": { + "post": { + "operationId": "config.reload", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Reload configuration", + "description": "Invalidate config cache and reload from files.", + "responses": { + "200": { + "description": "Config cache invalidated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.config.reload({\n ...\n})" + } + ] + } + }, "/config/providers": { "get": { "operationId": "config.providers", diff --git a/plans/codebase-search-to-opencode-plan.md b/plans/codebase-search-to-opencode-plan.md index d129b42ff3..5ffe2e7992 100644 --- a/plans/codebase-search-to-opencode-plan.md +++ b/plans/codebase-search-to-opencode-plan.md @@ -129,6 +129,7 @@ This tool is only available when the Codebase Indexing feature is properly confi - **Index Status**: Codebase must be indexed (status: "Indexed" or "Indexing") **Supported Embedding Models**: + - `codestral-embed-2505` (Mistral) - default, code-optimized - `text-embedding-3-small` (OpenAI) - Other models via Ollama @@ -182,21 +183,25 @@ When the codebase_search tool is invoked, it follows this process: **Effective Query Patterns**: **Good: Conceptual and specific** + ``` query: "user authentication and password validation" ``` **Good: Feature-focused** + ``` query: "database connection pool setup" ``` **Good: Problem-oriented** + ``` query: "error handling for API requests" ``` **Less effective: Too generic** + ``` query: "function" ``` @@ -211,6 +216,7 @@ query: "function" ### Result Interpretation **Similarity Scores**: + - **0.8-1.0**: Highly relevant matches, likely exactly what you're looking for - **0.6-0.8**: Good matches with strong conceptual similarity - **0.4-0.6**: Potentially relevant but may require review @@ -218,6 +224,7 @@ query: "function" **Result Structure**: Each search result includes: + - **File Path**: Workspace-relative path to the file containing the match - **Score**: Similarity score indicating relevance (0.4-1.0) - **Line Range**: Start and end line numbers for the code block @@ -249,11 +256,11 @@ OpenCode provides two modules for custom tools: 1. **Config Module** ([`packages/opencode/src/config/config.ts`](packages/opencode/src/config/config.ts:1273-1295)) - Stores non-sensitive settings in `opencode.json` - `codebaseSearch` object with: - - `projectId` - Project ID for codebase search - - `embedModel` - Embedding model (default: "codestral-embed-2505") - - `vectorDb` - Vector database configuration (Qdrant or LanceDB) - - `similarityThreshold` - Minimum similarity score for results (default: 0.4) - - `maxResults` - Maximum number of results to return (default: 50) + - `projectId` - Project ID for codebase search + - `embedModel` - Embedding model (default: "codestral-embed-2505") + - `vectorDb` - Vector database configuration (Qdrant or LanceDB) + - `similarityThreshold` - Minimum similarity score for results (default: 0.4) + - `maxResults` - Maximum number of results to return (default: 50) 2. **Auth Module** ([`packages/opencode/src/auth/index.ts`](packages/opencode/src/auth/index.ts:38)) - Stores sensitive credentials in `~/.local/share/kilo/auth.json` @@ -343,70 +350,88 @@ Use the optional path parameter to focus searches on specific parts of your code Search within API modules: ``` + query: "endpoint validation middleware" path: "src/api" + ``` Search in test files: ``` + query: "mock data setup patterns" path: "tests" + ``` Search specific feature directories: ``` + query: "component state management" path: "src/components/auth" + ``` ## Usage Examples ### Searching for authentication code in a specific directory ``` + user login and authentication logic src/auth + ``` ### Searching for entire workspace ``` + environment variables and application configuration + ``` ### Searching for database-related code in a specific directory ``` + database connection and query execution src/data + ``` ### Looking for error handling patterns in API code ``` + HTTP error responses and exception handling src/api + ``` ### Searching for testing utilities and mock setups ``` + test setup and mock data creation tests + ``` ### Searching for React hooks ``` + useState hook implementation src/components -``` + +```` ### Step 2: Create Tool File @@ -555,7 +580,7 @@ export default tool({ ) }, } -``` +```` ### Step 3: Configuration Examples From 03d2cd2ca0fe9e446b235cef2e9000478c1ca0b9 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Wed, 18 Feb 2026 10:03:50 +0100 Subject: [PATCH 05/12] test(codebase-search): add unit tests for formatResults helper Export formatResults function to enable direct testing. Add comprehensive test coverage for edge cases including empty results, threshold filtering, max results limiting, and output formatting. --- packages/opencode/src/tool/codebase-search.ts | 2 +- .../test/kilocode/codebase-search.test.ts | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/codebase-search.ts b/packages/opencode/src/tool/codebase-search.ts index 11148de5b7..0324e60db9 100644 --- a/packages/opencode/src/tool/codebase-search.ts +++ b/packages/opencode/src/tool/codebase-search.ts @@ -85,7 +85,7 @@ async function searchQdrant( } // Helper function to format search results -function formatResults( +export function formatResults( query: string, results: Array<{ filePath: string; score: number; startLine: number; endLine: number; codeChunk: string }>, similarityThreshold = 0.4, diff --git a/packages/opencode/test/kilocode/codebase-search.test.ts b/packages/opencode/test/kilocode/codebase-search.test.ts index 98ee1afcf1..677bd21157 100644 --- a/packages/opencode/test/kilocode/codebase-search.test.ts +++ b/packages/opencode/test/kilocode/codebase-search.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test" import { CodebaseSearchCollection } from "@/kilocode/codebase-search/collection" import { CodebaseSearchTypes, CODEBASE_SEARCH_DEFAULTS } from "@/kilocode/codebase-search/types" import { CodebaseSearchEmbeddings } from "@/kilocode/codebase-search/embeddings" +import { formatResults } from "@/tool/codebase-search" describe("CodebaseSearchCollection", () => { test("generates consistent collection names from workspace paths", () => { @@ -136,3 +137,68 @@ describe("CodebaseSearchEmbeddings", () => { }) }) }) + +describe("formatResults", () => { + test("returns 'no results' message for empty results", () => { + const output = formatResults("test query", []) + expect(output).toBe('No relevant code snippets found for query: "test query"') + }) + + test("returns 'no results' message for null/undefined results", () => { + expect(formatResults("test", null as any)).toContain("No relevant code snippets found") + expect(formatResults("test", undefined as any)).toContain("No relevant code snippets found") + }) + + test("filters results below similarity threshold", () => { + const results = [ + { filePath: "/src/a.ts", score: 0.8, startLine: 1, endLine: 10, codeChunk: "code a" }, + { filePath: "/src/b.ts", score: 0.2, startLine: 5, endLine: 15, codeChunk: "code b" }, + ] + const output = formatResults("test", results, 0.5) + expect(output).toContain("/src/a.ts") + expect(output).not.toContain("/src/b.ts") + }) + + test("returns message when all results below threshold", () => { + const results = [ + { filePath: "/src/a.ts", score: 0.1, startLine: 1, endLine: 10, codeChunk: "code a" }, + ] + const output = formatResults("test", results, 0.5) + expect(output).toContain("No relevant code snippets found") + expect(output).toContain("below similarity threshold of 0.5") + }) + + test("limits results to maxResults", () => { + const results = [ + { filePath: "/src/a.ts", score: 0.9, startLine: 1, endLine: 10, codeChunk: "code a" }, + { filePath: "/src/b.ts", score: 0.8, startLine: 5, endLine: 15, codeChunk: "code b" }, + { filePath: "/src/c.ts", score: 0.7, startLine: 20, endLine: 30, codeChunk: "code c" }, + ] + const output = formatResults("test", results, 0.5, 2) + expect(output).toContain("/src/a.ts") + expect(output).toContain("/src/b.ts") + expect(output).not.toContain("/src/c.ts") + }) + + test("formats output with all fields", () => { + const results = [ + { filePath: "/src/test.ts", score: 0.856, startLine: 10, endLine: 25, codeChunk: " function hello() {} " }, + ] + const output = formatResults("my query", results, 0.5) + expect(output).toContain("Query: my query") + expect(output).toContain("File path: /src/test.ts") + expect(output).toContain("Score: 0.856") + expect(output).toContain("Lines: 10-25") + expect(output).toContain("Code Chunk:") + expect(output).toContain("function hello() {}") // trimmed + }) + + test("handles results without codeChunk", () => { + const results = [ + { filePath: "/src/test.ts", score: 0.8, startLine: 1, endLine: 5, codeChunk: "" }, + ] + const output = formatResults("test", results, 0.5) + expect(output).toContain("File path: /src/test.ts") + expect(output).not.toContain("Code Chunk:") + }) +}) From cc7e1be2ba1fb070bb5ded26e934576afe903f97 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 15:07:23 +0100 Subject: [PATCH 06/12] perf(codebase-search): filter search results in-memory after database retrieval --- packages/opencode/src/tool/codebase-search.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/tool/codebase-search.ts b/packages/opencode/src/tool/codebase-search.ts index 0324e60db9..617b54d096 100644 --- a/packages/opencode/src/tool/codebase-search.ts +++ b/packages/opencode/src/tool/codebase-search.ts @@ -49,19 +49,6 @@ async function searchQdrant( score_threshold: 0, } - if (pathPrefix) { - body.filter = { - must: [ - { - key: "filePath", - match: { - prefix: pathPrefix, - }, - }, - ], - } - } - const response = await fetch(url.toString(), { method: "POST", headers, @@ -75,13 +62,23 @@ async function searchQdrant( const data = (await response.json()) as QdrantSearchResponse - return data.result.map((item) => ({ + let results = data.result.map((item) => ({ filePath: item.payload.filePath, score: item.score, startLine: item.payload.startLine, endLine: item.payload.endLine, codeChunk: item.payload.codeChunk, })) + + if (pathPrefix) { + const normalizedPrefix = pathPrefix.startsWith("/") ? pathPrefix.slice(1) : pathPrefix + results = results.filter((r) => { + const normalizedPath = r.filePath.startsWith("/") ? r.filePath.slice(1) : r.filePath + return normalizedPath.startsWith(normalizedPrefix) + }) + } + + return results } // Helper function to format search results From 4d564b9583fc836aad0b206198e4070e67bfbd91 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 19:52:25 +0100 Subject: [PATCH 07/12] fix: exit process when stdin closes in non-interactive mode --- packages/opencode/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index cc79306bc2..c179c356cd 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -30,6 +30,8 @@ import { SessionCommand } from "./cli/cmd/session" import { Telemetry } from "@kilocode/kilo-telemetry" import { migrateLegacyKiloAuth, ENV_FEATURE } from "@kilocode/kilo-gateway" +process.on("SIGHUP", () => process.exit(0)) + // kilocode_change - set feature for tracking. 'serve' is spawned by other services // (extension, cloud) which set their own KILOCODE_FEATURE env var. Direct CLI use // (any command other than 'serve') is tagged as 'cli'. If 'serve' is spawned without From af9000d3b481d617970537db4a0f25194d94019a Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 20:36:38 +0100 Subject: [PATCH 08/12] fix(config): improve config cache clearing and fix codebase-search tool name - Rename codebase-search tool to codebase_search for consistency - Add Config.clearCache() to clear config cache without disposing entire instance - Add State.clearEntry() and State.clearAll() for granular state management - Improve embeddings error handling with null check for embedding data - Change Zod parse to safeParse in codebase-search config for better error handling --- packages/opencode/src/cli/cmd/run.ts | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../src/cli/cmd/tui/routes/session/permission.tsx | 2 +- packages/opencode/src/config/config.ts | 14 +++++++++++++- packages/opencode/src/index.ts | 4 ++-- .../src/kilocode/codebase-search/config.ts | 3 ++- .../src/kilocode/codebase-search/embeddings.ts | 7 ++++++- packages/opencode/src/project/instance.ts | 3 ++- packages/opencode/src/project/state.ts | 10 ++++++++++ packages/opencode/src/server/routes/config.ts | 6 +++--- 10 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index c9c13109ba..e152d72342 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -416,7 +416,7 @@ export const RunCommand = cmd({ if (part.tool === "webfetch") return webfetch(props(part)) if (part.tool === "edit") return edit(props(part)) if (part.tool === "codesearch") return codesearch(props(part)) - if (part.tool === "codebase-search") return codebasesearch(props(part)) + if (part.tool === "codebase_search") return codebasesearch(props(part)) if (part.tool === "websearch") return websearch(props(part)) if (part.tool === "task") return task(props(part)) if (part.tool === "todowrite") return todo(props(part)) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 9b5c616810..f18ff662e9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1477,7 +1477,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess - + diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index e6fcd21ceb..7bc5a6f761 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -239,7 +239,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { - + diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 063fa5b780..a7eb93dcfe 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -43,6 +43,18 @@ export namespace Config { const log = Log.create({ service: "config" }) + // kilocode_change - Clear just the config cache without disposing of the entire instance + export async function clearCache() { + const { State } = await import("../project/state") + const initFn = (state as any)._init + if (!initFn) { + log.warn("unable to clear config cache: init function not found on state") + return + } + State.clearEntry(Instance.directory, initFn) + log.debug("config cache cleared", { directory: Instance.directory }) + } + // Managed settings directory for enterprise deployments (highest priority, admin-controlled) // These settings override all user and project settings function getManagedConfigDir(): string { @@ -1460,7 +1472,7 @@ export namespace Config { const filepath = path.join(Instance.directory, "config.json") const existing = await loadFile(filepath) await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2)) - await Instance.dispose() + await clearCache() } function globalConfigFile() { diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index c179c356cd..28ae7d5a6b 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -30,8 +30,6 @@ import { SessionCommand } from "./cli/cmd/session" import { Telemetry } from "@kilocode/kilo-telemetry" import { migrateLegacyKiloAuth, ENV_FEATURE } from "@kilocode/kilo-gateway" -process.on("SIGHUP", () => process.exit(0)) - // kilocode_change - set feature for tracking. 'serve' is spawned by other services // (extension, cloud) which set their own KILOCODE_FEATURE env var. Direct CLI use // (any command other than 'serve') is tagged as 'cli'. If 'serve' is spawned without @@ -43,6 +41,8 @@ if (!process.env[ENV_FEATURE]) { import { Global } from "./global" import { Config } from "./config/config" import { Auth } from "./auth" + +process.on("SIGHUP", () => process.exit(0)) // kilocode_change end process.on("unhandledRejection", (e) => { diff --git a/packages/opencode/src/kilocode/codebase-search/config.ts b/packages/opencode/src/kilocode/codebase-search/config.ts index 3135a323d6..5c2eb7e3fc 100644 --- a/packages/opencode/src/kilocode/codebase-search/config.ts +++ b/packages/opencode/src/kilocode/codebase-search/config.ts @@ -14,7 +14,8 @@ export namespace CodebaseSearchConfig { if (!raw) return null // Validate and return - return CodebaseSearchTypes.Config.parse(raw) + const result = CodebaseSearchTypes.Config.safeParse(raw) + return result.success ? result.data : null } /** diff --git a/packages/opencode/src/kilocode/codebase-search/embeddings.ts b/packages/opencode/src/kilocode/codebase-search/embeddings.ts index b01b08c9e1..cd18fc6094 100644 --- a/packages/opencode/src/kilocode/codebase-search/embeddings.ts +++ b/packages/opencode/src/kilocode/codebase-search/embeddings.ts @@ -56,7 +56,12 @@ export namespace CodebaseSearchEmbeddings { } const data = await response.json() - return data.data[0].embedding + const [firstResult] = data?.data || [] + const { embedding } = firstResult || {} + if (!embedding) { + throw new Error("OpenAI returned no embedding data") + } + return embedding } /** diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 98031f18d3..f35547e3c7 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -64,7 +64,8 @@ export const Instance = { return Filesystem.contains(Instance.worktree, filepath) }, state(init: () => S, dispose?: (state: Awaited) => Promise): () => S { - return State.create(() => Instance.directory, init, dispose) + const fn = State.create(() => Instance.directory, init, dispose) + return Object.assign(fn, { _init: init }) }, async dispose() { Log.Default.info("disposing instance", { directory: Instance.directory }) diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index a9dce565b5..0111cf11cb 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -67,4 +67,14 @@ export namespace State { disposalFinished = true log.info("state disposal completed", { key }) } + + export function clearEntry(key: string, init: () => any) { + const entries = recordsByKey.get(key) + if (!entries) return + entries.delete(init) + } + + export function clearAll(key: string) { + recordsByKey.delete(key) + } } diff --git a/packages/opencode/src/server/routes/config.ts b/packages/opencode/src/server/routes/config.ts index 11e3755b07..3b99936cd1 100644 --- a/packages/opencode/src/server/routes/config.ts +++ b/packages/opencode/src/server/routes/config.ts @@ -67,7 +67,7 @@ export const ConfigRoutes = lazy(() => "/reload", describeRoute({ summary: "Reload configuration", - description: "Invalidate config cache and reload from files.", + description: "Invalidate config cache and reload from files on next access.", operationId: "config.reload", responses: { 200: { @@ -81,8 +81,8 @@ export const ConfigRoutes = lazy(() => }, }), async (c) => { - const { Instance } = await import("../../project/instance") - await Instance.dispose() + const { Config } = await import("../../config/config") + await Config.clearCache() return c.json({ success: true }) }, ) From 0c88f6327dd705786e7e3457505c563a92ce3ccc Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 21:04:32 +0100 Subject: [PATCH 09/12] refactor(embeddings): add defensive checks for embedding responses Add consistent error handling across all embedding providers (OpenAI, Mistral, Ollama) to properly validate response data and throw meaningful errors when embedding data is missing. Also removes unused SIGHUP handler from index.ts. --- packages/opencode/src/index.ts | 2 -- .../kilocode/codebase-search/embeddings.ts | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 28ae7d5a6b..cc79306bc2 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -41,8 +41,6 @@ if (!process.env[ENV_FEATURE]) { import { Global } from "./global" import { Config } from "./config/config" import { Auth } from "./auth" - -process.on("SIGHUP", () => process.exit(0)) // kilocode_change end process.on("unhandledRejection", (e) => { diff --git a/packages/opencode/src/kilocode/codebase-search/embeddings.ts b/packages/opencode/src/kilocode/codebase-search/embeddings.ts index cd18fc6094..f319e0893b 100644 --- a/packages/opencode/src/kilocode/codebase-search/embeddings.ts +++ b/packages/opencode/src/kilocode/codebase-search/embeddings.ts @@ -58,9 +58,11 @@ export namespace CodebaseSearchEmbeddings { const data = await response.json() const [firstResult] = data?.data || [] const { embedding } = firstResult || {} + if (!embedding) { throw new Error("OpenAI returned no embedding data") } + return embedding } @@ -90,7 +92,14 @@ export namespace CodebaseSearchEmbeddings { } const data = await response.json() - return data.data[0].embedding + const [firstResult] = data?.data || [] + const { embedding } = firstResult || {} + + if (!embedding) { + throw new Error("Mistral returned no embedding data") + } + + return embedding } /** @@ -114,7 +123,13 @@ export namespace CodebaseSearchEmbeddings { } const data = await response.json() - return data.embedding + const { embedding } = data + + if (!embedding) { + throw new Error("Ollama returned no embedding data") + } + + return embedding } /** From 16e78af9b7cb93b05b9f3a0cd949c061d405fed9 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 21:20:28 +0100 Subject: [PATCH 10/12] feat(codebase-search): filter and limit search results before formatting --- packages/opencode/src/tool/codebase-search.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/codebase-search.ts b/packages/opencode/src/tool/codebase-search.ts index 617b54d096..624b2b0356 100644 --- a/packages/opencode/src/tool/codebase-search.ts +++ b/packages/opencode/src/tool/codebase-search.ts @@ -210,12 +210,14 @@ export const CodebaseSearchTool = Tool.define("codebase_search", { params.path || undefined, ) + const filteredResults = results.filter((result) => result.score >= similarityThreshold) + const limitedResults = filteredResults.slice(0, maxResults) const output = formatResults(params.query, results, similarityThreshold, maxResults) return { title: `Codebase search: ${params.query}`, output, - metadata: {}, + metadata: { results: limitedResults.length }, } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) From fd623f2336785b2bb50a12184841f760fe16a717 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 23:26:19 +0100 Subject: [PATCH 11/12] refactor(codebase-search): simplify formatResults by removing filtering logic Move similarity threshold filtering and result limiting to the caller. Also refactor config file existence check to use Bun.file().exists() and improve error logging. --- .../src/cli/cmd/tui/component/dialog-tool.tsx | 26 +++++-------------- packages/opencode/src/tool/codebase-search.ts | 14 ++-------- .../test/kilocode/codebase-search.test.ts | 22 +++++++--------- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx index 00cd9aa58b..3214483e3d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-tool.tsx @@ -23,21 +23,6 @@ const TOOLS = [ }, ] -function getExistingConfig(sync: ReturnType): Partial | null { - const raw = (sync.data.config.provider as any)?.kilo?.options?.codebase_search - if (!raw) return null - - const vectorDb = raw.vectorDb || {} - return { - embedModel: raw.embedModel, - vectorDbType: vectorDb.type ?? "qdrant", - qdrantUrl: vectorDb.type === "qdrant" ? (vectorDb.url ?? "") : "", - lancedbPath: vectorDb.type === "lancedb" ? (vectorDb.path ?? "") : "", - similarityThreshold: raw.similarityThreshold, - maxResults: raw.maxResults, - } -} - async function findConfigPath(sync: ReturnType): Promise { const projectDir = sync.data.path.directory const globalConfigDir = sync.data.path.config @@ -54,10 +39,9 @@ async function findConfigPath(sync: ReturnType): Promise // Find first existing config for (const candidate of candidates) { - try { - await fs.access(candidate) + if (await Bun.file(candidate).exists()) { return candidate - } catch {} + } } // No existing config - default to project's .opencode directory @@ -75,8 +59,9 @@ async function findConfigPath(sync: ReturnType): Promise } async function readConfigFromFile(sync: ReturnType): Promise | null> { + let configPath: string | undefined try { - const configPath = await findConfigPath(sync) + configPath = await findConfigPath(sync) const file = Bun.file(configPath) if (!(await file.exists())) { return null @@ -96,7 +81,8 @@ async function readConfigFromFile(sync: ReturnType): Promise, - similarityThreshold = 0.4, - maxResults = 50, ): string { if (!results || results.length === 0) { return `No relevant code snippets found for query: "${query}"` } - const filteredResults = results.filter((result) => result.score >= similarityThreshold) - - if (filteredResults.length === 0) { - return `No relevant code snippets found for query: "${query}" (results below similarity threshold of ${similarityThreshold})` - } - - const limitedResults = filteredResults.slice(0, maxResults) - let output = `Query: ${query}\nResults:\n\n` - for (const result of limitedResults) { + for (const result of results) { output += `File path: ${result.filePath}\n` output += `Score: ${result.score.toFixed(3)}\n` output += `Lines: ${result.startLine}-${result.endLine}\n` @@ -212,7 +202,7 @@ export const CodebaseSearchTool = Tool.define("codebase_search", { const filteredResults = results.filter((result) => result.score >= similarityThreshold) const limitedResults = filteredResults.slice(0, maxResults) - const output = formatResults(params.query, results, similarityThreshold, maxResults) + const output = formatResults(params.query, limitedResults) return { title: `Codebase search: ${params.query}`, diff --git a/packages/opencode/test/kilocode/codebase-search.test.ts b/packages/opencode/test/kilocode/codebase-search.test.ts index 677bd21157..e61df85faa 100644 --- a/packages/opencode/test/kilocode/codebase-search.test.ts +++ b/packages/opencode/test/kilocode/codebase-search.test.ts @@ -154,18 +154,17 @@ describe("formatResults", () => { { filePath: "/src/a.ts", score: 0.8, startLine: 1, endLine: 10, codeChunk: "code a" }, { filePath: "/src/b.ts", score: 0.2, startLine: 5, endLine: 15, codeChunk: "code b" }, ] - const output = formatResults("test", results, 0.5) + const filtered = results.filter((r) => r.score >= 0.5) + const output = formatResults("test", filtered) expect(output).toContain("/src/a.ts") expect(output).not.toContain("/src/b.ts") }) test("returns message when all results below threshold", () => { - const results = [ - { filePath: "/src/a.ts", score: 0.1, startLine: 1, endLine: 10, codeChunk: "code a" }, - ] - const output = formatResults("test", results, 0.5) + const results = [{ filePath: "/src/a.ts", score: 0.1, startLine: 1, endLine: 10, codeChunk: "code a" }] + const filtered = results.filter((r) => r.score >= 0.5) + const output = formatResults("test", filtered) expect(output).toContain("No relevant code snippets found") - expect(output).toContain("below similarity threshold of 0.5") }) test("limits results to maxResults", () => { @@ -174,7 +173,8 @@ describe("formatResults", () => { { filePath: "/src/b.ts", score: 0.8, startLine: 5, endLine: 15, codeChunk: "code b" }, { filePath: "/src/c.ts", score: 0.7, startLine: 20, endLine: 30, codeChunk: "code c" }, ] - const output = formatResults("test", results, 0.5, 2) + const filtered = results.filter((r) => r.score >= 0.5).slice(0, 2) + const output = formatResults("test", filtered) expect(output).toContain("/src/a.ts") expect(output).toContain("/src/b.ts") expect(output).not.toContain("/src/c.ts") @@ -184,7 +184,7 @@ describe("formatResults", () => { const results = [ { filePath: "/src/test.ts", score: 0.856, startLine: 10, endLine: 25, codeChunk: " function hello() {} " }, ] - const output = formatResults("my query", results, 0.5) + const output = formatResults("my query", results) expect(output).toContain("Query: my query") expect(output).toContain("File path: /src/test.ts") expect(output).toContain("Score: 0.856") @@ -194,10 +194,8 @@ describe("formatResults", () => { }) test("handles results without codeChunk", () => { - const results = [ - { filePath: "/src/test.ts", score: 0.8, startLine: 1, endLine: 5, codeChunk: "" }, - ] - const output = formatResults("test", results, 0.5) + const results = [{ filePath: "/src/test.ts", score: 0.8, startLine: 1, endLine: 5, codeChunk: "" }] + const output = formatResults("test", results) expect(output).toContain("File path: /src/test.ts") expect(output).not.toContain("Code Chunk:") }) From 06c5bd74461512355a8ca16ba3e6185aa7fcc904 Mon Sep 17 00:00:00 2001 From: Mickey Lazarevic Date: Thu, 19 Feb 2026 23:50:51 +0100 Subject: [PATCH 12/12] refactor: remove clearAll function and unused Config import Removes the unused `clearAll` function from State namespace and cleans up the unused Config import in the config route handler. Also updates AGENTS.md to use jsonc code block for better syntax highlighting. --- packages/opencode/AGENTS.md | 2 +- packages/opencode/src/project/state.ts | 4 ---- packages/opencode/src/server/routes/config.ts | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md index a27d9ce2a8..ba5d40d2b2 100644 --- a/packages/opencode/AGENTS.md +++ b/packages/opencode/AGENTS.md @@ -71,7 +71,7 @@ Uses the **Vercel AI SDK** as the abstraction layer. Providers are loaded from a Requires configuration in `opencode.json`: -```json +```jsonc { "provider": { "kilo": { diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 0111cf11cb..2e74219a3d 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -73,8 +73,4 @@ export namespace State { if (!entries) return entries.delete(init) } - - export function clearAll(key: string) { - recordsByKey.delete(key) - } } diff --git a/packages/opencode/src/server/routes/config.ts b/packages/opencode/src/server/routes/config.ts index 3b99936cd1..575be4dbf2 100644 --- a/packages/opencode/src/server/routes/config.ts +++ b/packages/opencode/src/server/routes/config.ts @@ -81,7 +81,6 @@ export const ConfigRoutes = lazy(() => }, }), async (c) => { - const { Config } = await import("../../config/config") await Config.clearCache() return c.json({ success: true }) },