@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.
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/plugin.json +36 -0
- package/.mcp.json +8 -0
- package/README.md +72 -0
- package/agents/trouve-search.md +41 -0
- package/claude-hooks.json +14 -0
- package/package.json +46 -0
- package/skills/trouve-search/SKILL.md +54 -0
- package/src/plugin.ts +353 -0
|
@@ -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
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
|