@trouve-ai/search-plugin 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "trouve-search",
3
+ "description": "Fast, accurate code search for agents: hybrid semantic + BM25 search over any local or remote git repository, with incremental branch-aware indexing. Adds trouve search tools (MCP), a trouve-search sub-agent, and a workflow skill.",
4
+ "version": "2.0.0",
5
+ "author": {
6
+ "name": "Jim Simon"
7
+ },
8
+ "homepage": "https://github.com/jimsimon/trouve",
9
+ "repository": "https://github.com/jimsimon/trouve",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "code-search",
13
+ "semantic-search",
14
+ "mcp",
15
+ "embeddings"
16
+ ],
17
+ "hooks": "./claude-hooks.json"
18
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "trouve-search",
3
+ "version": "2.0.0",
4
+ "description": "Fast, accurate code search for agents: hybrid semantic + BM25 search over any local or remote git repository, with incremental branch-aware indexing.",
5
+ "author": {
6
+ "name": "Jim Simon",
7
+ "url": "https://github.com/jimsimon"
8
+ },
9
+ "homepage": "https://github.com/jimsimon/trouve",
10
+ "repository": "https://github.com/jimsimon/trouve",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "code-search",
14
+ "semantic-search",
15
+ "mcp",
16
+ "embeddings"
17
+ ],
18
+ "skills": "./skills/",
19
+ "mcpServers": "./.mcp.json",
20
+ "interface": {
21
+ "displayName": "Trouve Code Search",
22
+ "shortDescription": "Instant semantic code search for any repository",
23
+ "longDescription": "Trouve indexes a repository once and answers natural-language or code queries in under a second, returning exact file paths and line numbers. Hybrid model2vec + BM25 retrieval with code-tuned reranking; the index is content-addressed, so edits, branch switches, and worktrees only re-embed what actually changed. Bundles the trouve-search MCP server (search and find_related tools) and a workflow skill. Installs the native binary via npm (@trouve-ai/search-core).",
24
+ "developerName": "Jim Simon",
25
+ "category": "Developer Tools",
26
+ "capabilities": [
27
+ "Read"
28
+ ],
29
+ "websiteURL": "https://github.com/jimsimon/trouve",
30
+ "defaultPrompt": [
31
+ "Find where authentication is implemented in this repo.",
32
+ "Search this codebase for the retry/backoff logic.",
33
+ "What code is related to src/auth.py line 42?"
34
+ ]
35
+ }
36
+ }
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "trouve-search": {
4
+ "command": "npx",
5
+ "args": ["-y", "@trouve-ai/search-core"]
6
+ }
7
+ }
8
+ }
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @trouve-ai/search-plugin
2
+
3
+ One plugin package for four agent harnesses:
4
+
5
+ - **OpenCode** and **Kilo Code** — native `trouve_search` and
6
+ `trouve_find_related` tools (npm install).
7
+ - **Claude Code** and **Codex** — MCP server, workflow skill, sub-agent
8
+ (Claude), and session-start index warming (git marketplace install).
9
+
10
+ Depends on **`@trouve-ai/search-core`** for the native binary.
11
+
12
+ ## OpenCode
13
+
14
+ ```json
15
+ { "plugin": ["@trouve-ai/search-plugin"] }
16
+ ```
17
+
18
+ ## Kilo Code
19
+
20
+ ```bash
21
+ kilo plugin @trouve-ai/search-plugin --global
22
+ ```
23
+
24
+ Options:
25
+
26
+ ```json
27
+ { "plugin": [["@trouve-ai/search-plugin", { "content": "all", "warm": true }]] }
28
+ ```
29
+
30
+ ## Claude Code
31
+
32
+ ```text
33
+ /plugin marketplace add jimsimon/trouve
34
+ /plugin install trouve-search@trouve
35
+ ```
36
+
37
+ Installs the trouve-search MCP server (tools surface as
38
+ `mcp__trouve-search__search` and `mcp__trouve-search__find_related`), the
39
+ `trouve-search` sub-agent, the workflow skill, and a `SessionStart` hook that
40
+ warms the project index in the background.
41
+
42
+ ## Codex
43
+
44
+ ```bash
45
+ codex plugin marketplace add 'https://github.com/jimsimon/trouve.git' --ref main
46
+ codex plugin add trouve-search@trouve
47
+ ```
48
+
49
+ ## MCP / npx
50
+
51
+ Run the MCP stdio server without a harness plugin:
52
+
53
+ ```bash
54
+ npx -y @trouve-ai/search-core
55
+ ```
56
+
57
+ See [INSTALL.md](../../INSTALL.md) for manual MCP setup and the native
58
+ OpenCode tool-file alternative.
59
+
60
+ ## Development
61
+
62
+ The `npm/` directory is an npm workspace, so install from there:
63
+
64
+ ```bash
65
+ cd npm
66
+ npm install # links @trouve-ai/search-core from ../search-core
67
+ npm run typecheck
68
+ ```
69
+
70
+ ## License
71
+
72
+ MIT, same as trouve-search.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: trouve-search
3
+ description: Code search agent for exploring any codebase. Use for finding code by intent, locating implementations, understanding how something works, or discovering related code. Prefer over Grep/Glob/Read for any semantic or exploratory question.
4
+ tools: Bash, Read
5
+ ---
6
+
7
+ Use `trouve-search search` to find code by describing what it does or naming a symbol/identifier, instead of grep:
8
+
9
+ ```bash
10
+ trouve-search search "authentication flow" ./my-project --max-snippet-lines 10 # first 10 lines only, concise
11
+ trouve-search search "save_pretrained" ./my-project # full chunk content
12
+ trouve-search search "save model to disk" ./my-project --top-k 10 # more results
13
+ ```
14
+
15
+ Results are cached automatically on first run and invalidated when files change.
16
+
17
+ Use `--content docs` to search documentation and prose, `--content config` for config files (yaml, toml, etc.), or `--content all` to search code, docs, and config:
18
+
19
+ ```bash
20
+ trouve-search search "deployment guide" ./my-project --content docs
21
+ trouve-search search "database host port" ./my-project --content config
22
+ trouve-search search "authentication" ./my-project --content all
23
+ ```
24
+
25
+ Use `trouve-search find-related` to discover code similar to a known location (pass `file_path` and `line` from a prior search result):
26
+
27
+ ```bash
28
+ trouve-search find-related src/auth.py 42 ./my-project
29
+ ```
30
+
31
+ `path` defaults to the current directory when omitted; git URLs are accepted.
32
+
33
+ If `trouve-search` is not on `$PATH`, install it with `npm i -g @trouve-ai/search-core`, `cargo install trouve-search` or download a release binary from GitHub.
34
+
35
+ ### Workflow
36
+
37
+ 1. Start with `trouve-search search` to find relevant chunks. The index is built and cached automatically.
38
+ 2. Use `--content docs` for documentation, `--content config` for config files, or `--content all` for everything.
39
+ 3. Navigate directly to the returned file and line. Do not re-search or grep for the same content.
40
+ 4. Optionally use `trouve-search find-related` with a promising result's `file_path` and `line` to discover related implementations.
41
+ 5. Use grep only when you need every occurrence of a literal string across the whole repo (e.g., all callers of a renamed function).
@@ -0,0 +1,14 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "command -v trouve-search >/dev/null 2>&1 || command -v npx >/dev/null 2>&1 || { echo 'trouve-search not found; install with `npm i -g @trouve-ai/search-core`, `cargo install trouve-search`, or download a release binary from https://github.com/jimsimon/trouve/releases' >&2; exit 1; }; nohup sh -c 'command -v trouve-search >/dev/null && trouve-search stats \"${CLAUDE_PROJECT_DIR:-.}\" || npx -y @trouve-ai/search-core stats \"${CLAUDE_PROJECT_DIR:-.}\"' >/dev/null 2>&1 &"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@trouve-ai/search-plugin",
3
+ "version": "2.0.0",
4
+ "description": "trouve code search plugin for OpenCode, Kilo Code, Claude Code, and Codex: native tools (OpenCode/Kilo), MCP server + skill + sub-agent (Claude/Codex).",
5
+ "type": "module",
6
+ "main": "./src/plugin.ts",
7
+ "exports": {
8
+ ".": "./src/plugin.ts",
9
+ "./server": "./src/plugin.ts"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "agents",
14
+ "skills",
15
+ "claude-hooks.json",
16
+ ".mcp.json",
17
+ ".claude-plugin",
18
+ ".codex-plugin"
19
+ ],
20
+ "keywords": [
21
+ "opencode",
22
+ "opencode-plugin",
23
+ "kilocode",
24
+ "kilo",
25
+ "code-search",
26
+ "semantic-search",
27
+ "trouve"
28
+ ],
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/jimsimon/trouve.git",
33
+ "directory": "npm/search-plugin"
34
+ },
35
+ "scripts": {
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "@opencode-ai/plugin": "^1.17.13",
40
+ "@trouve-ai/search-core": "2.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/bun": "^1.2.0",
44
+ "typescript": "^6.0.0"
45
+ }
46
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: trouve-search
3
+ description: Search a codebase by meaning with trouve. Use when looking for where something is implemented, how a feature works, or code related to a known location — instead of grep for exploratory or semantic questions.
4
+ ---
5
+
6
+ # Trouve Code Search
7
+
8
+ The trouve MCP server (bundled with this plugin) provides two tools:
9
+
10
+ - `search` — search a codebase with a natural-language or code query.
11
+ - `find_related` — find code similar to a specific file and line.
12
+
13
+ Use `search` to find where something is implemented — instead of grepping to
14
+ discover files. After trouve returns the file and line, navigate there
15
+ directly and read that file. Do not grep for the same content again.
16
+
17
+ ## Workflow
18
+
19
+ 1. Call `search` with a query describing what the code does or its name
20
+ (function/class names or behaviour descriptions, not error messages).
21
+ Pass the project root as `repo`; https:// git URLs also work. Results
22
+ include 10 lines of context each — signature plus first body lines,
23
+ enough to confirm the location.
24
+ 2. Navigate directly to the top result's file and line. Read only the
25
+ function or class at that location.
26
+ 3. Make the edit. Do not re-search or grep for the same content.
27
+ 4. Optionally call `find_related` with `file_path` and `line` from a search
28
+ result to discover similar code elsewhere (implementations of an
29
+ interface, callers, tests).
30
+ 5. Grep only when you need every occurrence of a literal string across the
31
+ whole repo (e.g., all callers of a renamed function).
32
+
33
+ The index is warmed in the background at session start where the harness
34
+ supports startup hooks (and built on first use otherwise); it is cached,
35
+ and updates are incremental and shared across branches and worktrees.
36
+
37
+ ## CLI fallback
38
+
39
+ Without MCP access, the `trouve-search` CLI provides the same search:
40
+
41
+ ```bash
42
+ trouve-search search "authentication flow" ./my-project --max-snippet-lines 10
43
+ trouve-search search "deployment guide" ./my-project --content docs
44
+ trouve-search search "database host port" ./my-project --content config
45
+ trouve-search find-related src/auth.py 42 ./my-project
46
+ ```
47
+
48
+ `--content` selects what to search: `code` (default), `docs`, `config`, or
49
+ `all`.
50
+
51
+ ## Requirements
52
+
53
+ Install with `npm i -g @trouve-ai/search-core`, `cargo install trouve-search` or
54
+ download a release binary from https://github.com/jimsimon/trouve/releases.
package/src/plugin.ts ADDED
@@ -0,0 +1,353 @@
1
+ // Plugin exposing trouve code search as native tools in OpenCode and
2
+ // Kilo Code (whose plugin runtime is identical to OpenCode's).
3
+ //
4
+ // Unlike the standalone tool file (src/agents/opencode-tool.ts, one CLI
5
+ // process per call), this plugin keeps a single `trouve-search` server process
6
+ // alive for the whole session and speaks its newline-delimited JSON-RPC
7
+ // protocol directly. That preserves the server's in-process index cache:
8
+ // repeat queries — including against remote git URLs — skip index reload
9
+ // entirely.
10
+ //
11
+ // Binary resolution is delegated to @trouve-ai/search-core.
12
+ import { resolveBinaryPath } from "@trouve-ai/search-core"
13
+ import { tool, type Plugin, type PluginModule } from "@opencode-ai/plugin"
14
+
15
+ import pkg from "../package.json"
16
+
17
+ const PROTOCOL_VERSION = "2024-11-05"
18
+ const CONTENT_TYPES = ["code", "docs", "config", "all"] as const
19
+ type ContentType = (typeof CONTENT_TYPES)[number]
20
+
21
+ interface Pending {
22
+ resolve(value: unknown): void
23
+ reject(error: Error): void
24
+ }
25
+
26
+ /// The initialize handshake involves no indexing and must be quick.
27
+ const INIT_TIMEOUT_MS = 30_000
28
+ /// A tool call may include a cold index build of a very large (or remote)
29
+ /// repo; anything beyond this is treated as a hung server.
30
+ const CALL_TIMEOUT_MS = 10 * 60 * 1000
31
+
32
+ /** Minimal client for trouve's newline-delimited JSON-RPC stdio server. */
33
+ class TrouveServer {
34
+ private proc: ReturnType<typeof Bun.spawn> | null = null
35
+ private pending = new Map<number, Pending>()
36
+ private nextId = 1
37
+ private starting: Promise<void> | null = null
38
+ private stderrTail = ""
39
+
40
+ constructor(private content: ContentType[]) {}
41
+
42
+ async callTool(name: string, args: Record<string, unknown>): Promise<string> {
43
+ await this.ensureStarted()
44
+ const result = (await this.request(
45
+ "tools/call",
46
+ { name, arguments: args },
47
+ CALL_TIMEOUT_MS,
48
+ )) as {
49
+ content?: Array<{ type: string; text?: string }>
50
+ }
51
+ const text = result.content
52
+ ?.map((part) => (part.type === "text" ? (part.text ?? "") : ""))
53
+ .join("")
54
+ return text?.trim() || "trouve-search returned no output."
55
+ }
56
+
57
+ private ensureStarted(): Promise<void> {
58
+ // `proc` is set before the initialize handshake completes, so an
59
+ // in-flight startup must win over the proc check: a concurrent caller
60
+ // would otherwise send tools/call before the server is initialized.
61
+ if (this.starting) return this.starting
62
+ if (this.proc) return Promise.resolve()
63
+ this.starting = this.start().finally(() => {
64
+ this.starting = null
65
+ })
66
+ return this.starting
67
+ }
68
+
69
+ private async start(): Promise<void> {
70
+ const binary = resolveBinaryPath()
71
+ const argv = [binary]
72
+ if (!(this.content.length === 1 && this.content[0] === "code")) {
73
+ argv.push("--content", ...this.content)
74
+ }
75
+ const proc = Bun.spawn(argv, {
76
+ stdin: "pipe",
77
+ stdout: "pipe",
78
+ stderr: "pipe",
79
+ })
80
+ this.proc = proc
81
+ proc.exited.then(() => this.onExit())
82
+ this.readLoop(proc.stdout).catch(() => this.onExit())
83
+ this.readStderr(proc.stderr).catch(() => {})
84
+ await this.request(
85
+ "initialize",
86
+ {
87
+ protocolVersion: PROTOCOL_VERSION,
88
+ capabilities: {},
89
+ clientInfo: { name: pkg.name, version: pkg.version },
90
+ },
91
+ INIT_TIMEOUT_MS,
92
+ )
93
+ this.write({ jsonrpc: "2.0", method: "notifications/initialized" })
94
+ }
95
+
96
+ /// Keep the tail of stderr so a crash can be diagnosed from the tool
97
+ /// output instead of vanishing silently.
98
+ private async readStderr(stderr: ReadableStream<Uint8Array>): Promise<void> {
99
+ const decoder = new TextDecoder()
100
+ for await (const chunk of stderr) {
101
+ this.stderrTail = (this.stderrTail + decoder.decode(chunk, { stream: true })).slice(-2000)
102
+ }
103
+ }
104
+
105
+ private onExit(): void {
106
+ this.proc = null
107
+ const pending = [...this.pending.values()]
108
+ this.pending.clear()
109
+ const detail = this.stderrTail.trim()
110
+ const message = detail
111
+ ? `trouve-search server exited unexpectedly: ${detail}`
112
+ : "trouve-search server exited unexpectedly"
113
+ for (const p of pending) p.reject(new Error(message))
114
+ }
115
+
116
+ private async readLoop(stdout: ReadableStream<Uint8Array>): Promise<void> {
117
+ const decoder = new TextDecoder()
118
+ let buffer = ""
119
+ for await (const chunk of stdout) {
120
+ buffer += decoder.decode(chunk, { stream: true })
121
+ let newline: number
122
+ while ((newline = buffer.indexOf("\n")) >= 0) {
123
+ const line = buffer.slice(0, newline).trim()
124
+ buffer = buffer.slice(newline + 1)
125
+ if (line) this.onMessage(line)
126
+ }
127
+ }
128
+ }
129
+
130
+ private onMessage(line: string): void {
131
+ let message: { id?: number; result?: unknown; error?: { message?: string } }
132
+ try {
133
+ message = JSON.parse(line)
134
+ } catch {
135
+ return
136
+ }
137
+ if (typeof message.id !== "number") return
138
+ const pending = this.pending.get(message.id)
139
+ if (!pending) return
140
+ this.pending.delete(message.id)
141
+ if (message.error) {
142
+ pending.reject(new Error(message.error.message ?? "trouve-search server error"))
143
+ } else {
144
+ pending.resolve(message.result)
145
+ }
146
+ }
147
+
148
+ private request(method: string, params: unknown, timeoutMs: number): Promise<unknown> {
149
+ const id = this.nextId++
150
+ const promise = new Promise((resolve, reject) => {
151
+ // A server that stalls without exiting would otherwise hang the
152
+ // agent turn forever: fail the request and kill the process so the
153
+ // next call starts fresh.
154
+ const timer = setTimeout(() => {
155
+ if (this.pending.delete(id)) {
156
+ this.proc?.kill()
157
+ reject(
158
+ new Error(
159
+ `trouve-search server did not respond to ${method} within ${timeoutMs / 1000}s ` +
160
+ "and was restarted. Index builds are incremental; retrying resumes.",
161
+ ),
162
+ )
163
+ }
164
+ }, timeoutMs)
165
+ this.pending.set(id, {
166
+ resolve: (value) => {
167
+ clearTimeout(timer)
168
+ resolve(value)
169
+ },
170
+ reject: (error) => {
171
+ clearTimeout(timer)
172
+ reject(error)
173
+ },
174
+ })
175
+ })
176
+ this.write({ jsonrpc: "2.0", id, method, params })
177
+ return promise
178
+ }
179
+
180
+ private write(message: unknown): void {
181
+ const stdin = this.proc?.stdin as { write(data: string): void; flush(): void } | undefined
182
+ if (!stdin) throw new Error("trouve-search server is not running")
183
+ stdin.write(JSON.stringify(message) + "\n")
184
+ stdin.flush()
185
+ }
186
+ }
187
+
188
+ function errorText(error: unknown): string {
189
+ const message = error instanceof Error ? error.message : String(error)
190
+ if (/ENOENT|executable|not.*found/i.test(message)) {
191
+ return (
192
+ "trouve-search failed: the `trouve-search` binary was not found. " +
193
+ "Install @trouve-ai/search-plugin (includes search-core), or `cargo install trouve-search`, " +
194
+ "or download a release binary from GitHub."
195
+ )
196
+ }
197
+ return `trouve-search failed: ${message}`
198
+ }
199
+
200
+ const REPO = tool.schema
201
+ .string()
202
+ .optional()
203
+ .describe(
204
+ "Local directory path or https:// git URL to search. Defaults to the project root. " +
205
+ "The index is built on first use and cached; updates are incremental.",
206
+ )
207
+
208
+ const TOP_K = tool.schema
209
+ .number()
210
+ .int()
211
+ .min(1)
212
+ .optional()
213
+ .describe("Number of results to return (default 5).")
214
+
215
+ const MAX_SNIPPET_LINES = tool.schema
216
+ .number()
217
+ .int()
218
+ .min(0)
219
+ .optional()
220
+ .describe(
221
+ "Lines of source per result. Default (10): signature + first body lines, enough to " +
222
+ "confirm the location. 0: file path and line range only. Larger values include " +
223
+ "more, up to the full chunk.",
224
+ )
225
+
226
+ /** Minimum interval between background index warms. */
227
+ const WARM_INTERVAL_MS = 60_000
228
+
229
+ /**
230
+ * Fire-and-forget index warm: `trouve-search stats` builds (or incrementally
231
+ * refreshes) the on-disk index and snapshot for `directory`, so the first
232
+ * real search of a session mmap-loads a warm snapshot instead of paying the
233
+ * build. Failures (e.g. no trouve binary) are silently ignored — the tools
234
+ * themselves report actionable errors when actually called.
235
+ */
236
+ function makeWarmer(directory: string | undefined, content: ContentType[]) {
237
+ let last = 0
238
+ return () => {
239
+ if (!directory) return
240
+ const now = Date.now()
241
+ if (now - last < WARM_INTERVAL_MS) return
242
+ last = now
243
+ try {
244
+ const binary = resolveBinaryPath()
245
+ const argv = [binary, "stats", directory]
246
+ if (!(content.length === 1 && content[0] === "code")) {
247
+ argv.push("--content", ...content)
248
+ }
249
+ Bun.spawn(argv, { stdin: "ignore", stdout: "ignore", stderr: "ignore" })
250
+ } catch {
251
+ // Missing binary: stay silent here; tool calls surface the real error.
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Plugin options (set in opencode/kilo config as `["@trouve-ai/search-plugin", {...}]`):
258
+ * - `content`: what the server indexes — "code" (default), "docs",
259
+ * "config", "all", or an array of those.
260
+ * - `warm`: build/refresh the project index in the background at session
261
+ * start and after each idle turn (default true).
262
+ */
263
+ export const TrouvePlugin: Plugin = async (input, options) => {
264
+ const opts = options as { content?: string | string[]; warm?: boolean } | undefined
265
+ const requested = opts?.content
266
+ const requestedList = Array.isArray(requested) ? requested : requested ? [requested] : ["code"]
267
+ const invalid = requestedList.filter(
268
+ (c) => !(CONTENT_TYPES as readonly string[]).includes(c),
269
+ )
270
+ if (invalid.length) {
271
+ console.warn(
272
+ `trouve-search: ignoring invalid content value(s) ${invalid.join(", ")}; ` +
273
+ `valid values are ${CONTENT_TYPES.join(", ")}.`,
274
+ )
275
+ }
276
+ const content = requestedList.filter((c): c is ContentType =>
277
+ (CONTENT_TYPES as readonly string[]).includes(c),
278
+ )
279
+ const resolved = content.length ? content : (["code"] as ContentType[])
280
+ const server = new TrouveServer(resolved)
281
+
282
+ const warm = opts?.warm === false ? () => {} : makeWarmer(input?.worktree, resolved)
283
+ // Warm at plugin load, so the index is ready before the first search;
284
+ // re-warm (throttled) whenever a session goes idle, absorbing any edits
285
+ // the agent made during the turn.
286
+ warm()
287
+
288
+ return {
289
+ event: async ({ event }) => {
290
+ if (event.type === "session.idle") warm()
291
+ },
292
+ tool: {
293
+ trouve_search: tool({
294
+ description:
295
+ "Search a codebase once with a focused query describing what the code does or its " +
296
+ "name. Write queries using function/class names or behaviour descriptions, not " +
297
+ "error messages. Returns file paths and line numbers — navigate directly there, " +
298
+ "do not grep for the same content again.",
299
+ args: {
300
+ query: tool.schema.string().describe("Natural language or code query."),
301
+ repo: REPO,
302
+ top_k: TOP_K,
303
+ max_snippet_lines: MAX_SNIPPET_LINES,
304
+ },
305
+ async execute(args, context) {
306
+ try {
307
+ return await server.callTool("search", {
308
+ query: args.query,
309
+ repo: args.repo ?? context.worktree,
310
+ top_k: args.top_k ?? 5,
311
+ max_snippet_lines: args.max_snippet_lines ?? 10,
312
+ })
313
+ } catch (error) {
314
+ return errorText(error)
315
+ }
316
+ },
317
+ }),
318
+ trouve_find_related: tool({
319
+ description:
320
+ "Find code similar to a known location. Useful for discovering all implementations " +
321
+ "of an interface, all callers of a function, or all tests for a class. Pass " +
322
+ "`file_path` and `line` from a prior trouve_search result.",
323
+ args: {
324
+ file_path: tool.schema
325
+ .string()
326
+ .describe("Path to the file as shown in a search result."),
327
+ line: tool.schema.number().int().min(1).describe("Line number (1-indexed)."),
328
+ repo: REPO,
329
+ top_k: TOP_K,
330
+ max_snippet_lines: MAX_SNIPPET_LINES,
331
+ },
332
+ async execute(args, context) {
333
+ try {
334
+ return await server.callTool("find_related", {
335
+ file_path: args.file_path,
336
+ line: args.line,
337
+ repo: args.repo ?? context.worktree,
338
+ top_k: args.top_k ?? 5,
339
+ max_snippet_lines: args.max_snippet_lines ?? 10,
340
+ })
341
+ } catch (error) {
342
+ return errorText(error)
343
+ }
344
+ },
345
+ }),
346
+ },
347
+ }
348
+ }
349
+
350
+ // Module descriptor: the shape both OpenCode's and Kilo Code's loaders
351
+ // prefer. The named `TrouvePlugin` export above remains for older loaders
352
+ // that invoke plugin function exports directly.
353
+ export default { id: "trouve-search", server: TrouvePlugin } satisfies PluginModule