@takara-ai/miru-code 1.8.2 → 1.10.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.
Files changed (60) hide show
  1. package/README.md +65 -53
  2. package/docs/coverage-badge.json +6 -0
  3. package/grammars/manifest.json +10 -2
  4. package/grammars/tree-sitter-astro.wasm +0 -0
  5. package/grammars/tree-sitter-php.wasm +0 -0
  6. package/grammars/tree-sitter-php_only.wasm +0 -0
  7. package/grammars/tree-sitter-sql.wasm +0 -0
  8. package/grammars/tree-sitter-svelte.wasm +0 -0
  9. package/grammars/tree-sitter-vue.wasm +0 -0
  10. package/package.json +10 -2
  11. package/src/agents.ts +4 -4
  12. package/src/auth/device.ts +26 -21
  13. package/src/auth/providers.ts +26 -11
  14. package/src/benchmark/compare.ts +3 -10
  15. package/src/benchmark/grep.ts +64 -37
  16. package/src/benchmark/locate-compare.ts +8 -1
  17. package/src/benchmark/rg-literal.ts +9 -4
  18. package/src/cache.ts +3 -3
  19. package/src/chunking/ast.ts +21 -3
  20. package/src/chunking/grammars.ts +8 -3
  21. package/src/chunking/structural.ts +13 -24
  22. package/src/cli-mode.ts +18 -0
  23. package/src/cli.ts +54 -98
  24. package/src/credentials.ts +14 -14
  25. package/src/embeddings/openai.ts +6 -3
  26. package/src/embeddings/sagemaker.ts +18 -3
  27. package/src/embeddings/validate.ts +10 -7
  28. package/src/git.ts +6 -2
  29. package/src/help.ts +7 -7
  30. package/src/index/bm25.ts +66 -6
  31. package/src/index/chunk-selector.ts +39 -11
  32. package/src/index/create.ts +5 -9
  33. package/src/index/files.ts +5 -1
  34. package/src/index/incremental.ts +48 -8
  35. package/src/index/sparse.ts +14 -2
  36. package/src/installer/agents.ts +85 -65
  37. package/src/installer/benchmark-mode.ts +40 -11
  38. package/src/installer/config.ts +105 -42
  39. package/src/installer/installer.ts +59 -34
  40. package/src/installer/legacy-hooks.ts +131 -0
  41. package/src/installer/prompt.ts +20 -22
  42. package/src/installer/search-policy.ts +10 -10
  43. package/src/mcp/index-cache.ts +16 -3
  44. package/src/mcp/server.ts +65 -46
  45. package/src/mcp/stdio.ts +17 -2
  46. package/src/miru-index.ts +90 -22
  47. package/src/prompt.ts +51 -14
  48. package/src/ranking/boosting.ts +5 -6
  49. package/src/ranking/location.ts +6 -10
  50. package/src/ranking/penalties.ts +14 -22
  51. package/src/search.ts +8 -12
  52. package/src/setup.ts +25 -13
  53. package/src/snippet.ts +8 -26
  54. package/src/spinner.ts +26 -16
  55. package/src/tokenizer/bert-wordpiece.ts +0 -6
  56. package/src/utils.ts +1 -15
  57. package/src/version.ts +4 -4
  58. package/src/installer/hooks/install.ts +0 -549
  59. package/src/installer/hooks/opencode-plugin.ts +0 -54
  60. package/src/installer/hooks/search-guard.ts +0 -272
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Miru (見る)
2
2
 
3
- [![CI](https://github.com/takara-ai/miru-code/actions/workflows/ci.yml/badge.svg)](https://github.com/takara-ai/miru-code/actions/workflows/ci.yml) [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) [![bun](https://img.shields.io/badge/runtime-bun%201.1%2B-black)](https://bun.sh)
3
+ [![CI](https://github.com/takara-ai/miru-code/actions/workflows/ci.yml/badge.svg)](https://github.com/takara-ai/miru-code/actions/workflows/ci.yml) [![coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftakara-ai%2Fmiru-code%2Fmain%2Fdocs%2Fcoverage-badge.json)](https://github.com/takara-ai/miru-code/actions/workflows/ci.yml) [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) [![bun](https://img.shields.io/badge/runtime-bun%201.1%2B-black)](https://bun.sh)
4
4
 
5
5
 
6
6
  **Hybrid code search for AI coding agents.** Find code by meaning, not grep.
@@ -38,6 +38,22 @@ Restart Claude Code (or reload plugins) when prompted. Update: `/plugin marketpl
38
38
 
39
39
  **Any other IDE, or if marketplace install isn't available:** use the CLI install below.
40
40
 
41
+ ### What you get from a plugin install vs the CLI
42
+
43
+ Plugin installs don't carry full CLI parity — what each one gives you depends on the IDE's own plugin capabilities, not just Miru's packaging:
44
+
45
+ | | Claude Code | Codex | Cursor |
46
+ |---|---|---|---|
47
+ | MCP tools (`search`, `locate`, `expand`, `find_related`) | ✅ | ✅ | ❌ *(plugin ships skills + rules only — no MCP entry yet)* |
48
+ | `miru` / Caveman / STE skills | ✅ | ✅ | ✅ |
49
+ | Dedicated sub-agent (`miru:miru-code`) | ✅ | — | — |
50
+ | Benchmark mode toggle (`/plugin configure`) | ✅ | — | — |
51
+ | Credentials | own plugin-scoped dir | own plugin-scoped dir | n/a |
52
+
53
+ `—` means the IDE's plugin schema has no equivalent mechanism to port these to (not a packaging gap we can close): Codex's and Cursor's plugin manifests have no `agents` or `userConfig` fields, so the dedicated sub-agent and the benchmark toggle are Claude-Code-only.
54
+
55
+ **Credentials are plugin-scoped, not shared with your CLI install.** Claude Code and Codex plugins each store auth in their own IDE-managed data directory (survives plugin updates, removed on uninstall). This means `miru setup` done via the CLI does **not** carry over to a plugin install, or vice versa — each authenticates independently on first use (interactive device-code login bootstraps automatically). If you use both the CLI and a plugin, you'll sign in twice.
56
+
41
57
  ## Install (CLI)
42
58
 
43
59
  ```bash
@@ -75,7 +91,7 @@ Interactive TUI — **↑↓** move, **space** toggle, **a** all, **enter** conf
75
91
  | Instructions | Search policy in `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` |
76
92
  | Sub-agent | Dedicated `miru-code` agent file |
77
93
  | Cursor rules | Always-on `.cursor/rules/miru-code.mdc` (Cursor only) |
78
- | Search hooks *(experimental)* | Block built-in Grep/Glob and redirect to Miru MCP |
94
+ | Old Miru hooks | Removes stale search hooks from older releases |
79
95
  | Caveman *(experimental)* | On-demand chat compression skill (`/caveman`) |
80
96
  | STE writing *(experimental)* | On-demand clear technical English for docs (`/ste`) |
81
97
 
@@ -89,35 +105,33 @@ miru uninstall # remove miru config
89
105
  **Supported:** Cursor · Claude Code · Gemini CLI · Kiro · OpenCode · GitHub Copilot · Codex · VS Code · Visual Studio (Windows) · Windsurf / Devin Desktop
90
106
 
91
107
 
92
- | IDE | MCP | Instructions / rules | Hooks *(experimental)* | Caveman *(experimental)* | STE *(experimental)* |
93
- | -------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------- | --------------------------------------------------- | -------------------------------------------- |
94
- | Cursor | `~/.cursor/mcp.json` | `~/.cursor/rules/miru-code.mdc` | `~/.cursor/hooks.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
95
- | Claude Code | `~/.claude.json` | `~/.claude/CLAUDE.md` | `~/.claude/settings.json` | `~/.claude/skills/caveman/SKILL.md` | `~/.claude/skills/ste/SKILL.md` |
96
- | Gemini CLI | `~/.gemini/settings.json` | `~/.gemini/GEMINI.md` | `~/.gemini/settings.json` (`BeforeTool`) | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
97
- | Kiro | `~/.kiro/settings/mcp.json` | `~/.kiro/steering/miru.md` | `~/.kiro/settings/hooks.json` | `~/.kiro/skills/caveman/SKILL.md` | `~/.kiro/skills/ste/SKILL.md` |
98
- | OpenCode | `$XDG_CONFIG_HOME/opencode/opencode.json(c)` *(else `~/.config/opencode/…`)* | `…/AGENTS.md` | `…/plugins/miru-search-guard.ts` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
99
- | GitHub Copilot | `~/.copilot/mcp-config.json` | — | `~/.copilot/hooks/miru-search.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
100
- | Codex | `~/.codex/config.toml` | `~/.codex/AGENTS.md` | `~/.codex/hooks.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
101
- | VS Code | `…/Code/User/mcp.json` | — | `~/.copilot/hooks/miru-search.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
102
- | Visual Studio | `%USERPROFILE%\.mcp.json` | — | `~/.copilot/hooks/miru-search.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
103
- | Windsurf | — | — | `~/.codeium/windsurf/hooks.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
108
+ | IDE | MCP | Instructions / rules | Caveman *(experimental)* | STE *(experimental)* |
109
+ | -------------- | ------------------------------------- | ------------------------------- | ----------------------------------- | --------------------------------------- |
110
+ | Cursor | `~/.cursor/mcp.json` | `~/.cursor/rules/miru-code.mdc` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
111
+ | Claude Code | `~/.claude.json` | `~/.claude/CLAUDE.md` | `~/.claude/skills/caveman/SKILL.md` | `~/.claude/skills/ste/SKILL.md` |
112
+ | Gemini CLI | `~/.gemini/settings.json` | `~/.gemini/GEMINI.md` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
113
+ | Kiro | `~/.kiro/settings/mcp.json` | `~/.kiro/steering/miru.md` | `~/.kiro/skills/caveman/SKILL.md` | `~/.kiro/skills/ste/SKILL.md` |
114
+ | OpenCode | `$XDG_CONFIG_HOME/opencode/opencode.json(c)` *(else `~/.config/opencode/…`)* | `…/AGENTS.md` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
115
+ | GitHub Copilot | `~/.copilot/mcp-config.json` | — | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
116
+ | Codex | `~/.codex/config.toml` | `~/.codex/AGENTS.md` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
117
+ | VS Code | `…/Code/User/mcp.json` | — | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
118
+ | Visual Studio | `%USERPROFILE%\.mcp.json` | — | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
119
+ | Windsurf | — | — | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
104
120
 
105
121
 
106
122
  ### Plugin packaging
107
123
 
108
- This repo now includes plugin packaging for:
109
-
110
- - Codex: `.codex-plugin/plugin.json` and `.agents/plugins/marketplace.json`
111
- - Claude Code: `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`
124
+ - Codex: `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json`, shares the root `mcp.json`
125
+ - Claude Code: `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, its own `.claude-plugin/mcp.json` (needed for the `userConfig` benchmark toggle — see [What you get from a plugin install vs the CLI](#what-you-get-from-a-plugin-install-vs-the-cli))
112
126
  - Cursor: `plugin.json` and `.cursor/rules/miru-code-search.mdc`
113
127
 
114
128
  Current limitation:
115
129
 
116
- - these plugin manifests still launch the published Miru runtime through `bunx @takara-ai/miru-code`
130
+ - these plugin manifests launch the published Miru runtime through `bunx @takara-ai/miru-code@latest mcp`
117
131
  - that means local source edits do not affect plugin behavior until a package version is published
118
132
  - and a fully self-contained “no Bun required” plugin install is still future work
119
133
 
120
- ### Search hooks
134
+ ### On-demand skills
121
135
 
122
136
  Sub-agent files are also written where supported (see `miru install` plan). Windsurf hooks only *(experimental)* — no MCP entry yet. Caveman is an on-demand Agent Skill (default off): invoke with `/caveman` or “talk like caveman”; stop with “normal mode”. Invocation UI varies by IDE (`/caveman`, `$caveman`, `@caveman`, etc.). Most IDEs share `~/.agents/skills/caveman/SKILL.md` (including Copilot / VS Code / Visual Studio); Claude Code and Kiro keep vendor-native skill dirs. Ownership is tracked on the shared path so uninstalling one IDE keeps the skill while another still owns it; selecting all owners removes it once. STE is an on-demand Agent Skill (default off): invoke with `/ste` or “de-slop this”; keep articles and complete sentences. Most IDEs share `~/.agents/skills/ste/` the same way (ownership via `miru-owners.json`); Claude Code and Kiro keep vendor-native STE dirs.
123
137
 
@@ -143,17 +157,6 @@ Off by default at install time. Enable Caveman in the installer for any supporte
143
157
 
144
158
  </details>
145
159
 
146
- <details>
147
- <summary>Search hooks <em>(experimental)</em></summary>
148
-
149
- Search hooks are **experimental** — behavior and IDE support may change between releases.
150
-
151
- Hooks run `miru hook-guard` before built-in search tools execute. They **block** conceptual Grep/Glob/SemanticSearch and shell `rg`/`grep`/`find`, and tell the agent to use Miru MCP `search` (or `locate` for exact literals) instead. Exact literal lookups (e.g. `REDIS_HOST`, a symbol name) still pass through.
152
-
153
- Hooks are **off by default** at install time. Enable them in the installer if you want built-in Grep/Glob redirected to Miru MCP.
154
-
155
- </details>
156
-
157
160
  **Team sub-agent in a repo** (optional):
158
161
 
159
162
  ```bash
@@ -174,14 +177,14 @@ miru find-related src/auth.ts 42 ./src
174
177
  Terminal output is human-readable; use `--json` for scripts. One-off without installing:
175
178
 
176
179
  ```bash
177
- bunx @takara-ai/miru-code search "auth middleware" ./src
180
+ bunx @takara-ai/miru-code@latest search "auth middleware" ./src
178
181
  ```
179
182
 
180
183
  ---
181
184
 
182
185
  ## MCP tools
183
186
 
184
- When wired via `miru install`, the MCP server exposes `search`, `locate`, `expand`, and `find_related`. `read_benchmark` appears only in [benchmark mode](#benchmark-mode). Pass the **project root** as `repo` for local workspaces (or an `https://` git URL). The index is built on the first call and cached for the session.
187
+ When wired via `miru install`, the MCP server exposes `search`, `locate`, `expand`, and `find_related`. `read_benchmark` appears only in [benchmark mode](#benchmark-mode). `repo` is optional: it defaults to the server's startup directory. Set it for another repo, or if your MCP client starts elsewhere. The index is built on the first call and cached for the session.
185
188
 
186
189
 
187
190
  | Tool | When to use |
@@ -195,11 +198,13 @@ When wired via `miru install`, the MCP server exposes `search`, `locate`, `expan
195
198
 
196
199
  ### Workflow
197
200
 
198
- 1. **`search`** with `query` + `repo` — returns compact snippets (~±15 lines) and relevance scores.
199
- 2. If a hit has **`truncated: true`**, call **`expand`** with `file_path`, `anchor_line`, and `repo` — not another search or a full-file read.
200
- 3. To trace similar patterns elsewhere, call **`find_related`** with the same `file_path`, `anchor_line`, and `repo`.
201
+ 1. **`search`** with `query` — returns compact snippets (~±15 lines) and relevance scores.
202
+ 2. If a hit has **`truncated: true`**, call **`expand`** with `file_path` and `anchor_line` — not another search or a full-file read.
203
+ 3. To trace similar patterns elsewhere, call **`find_related`** with the same `file_path` and `anchor_line`.
201
204
  4. Use your editor's **Read** on `absolute_path` only when editing or when `expand` still lacks context.
202
205
 
206
+ Keep `repo` on follow-up calls when set.
207
+
203
208
  Prefer these tools over Grep, Glob, or SemanticSearch when Miru MCP is connected — hooks and instructions enforce that when enabled.
204
209
 
205
210
  Local repo hits include `absolute_path` for one-click navigation. Parameter reference is below under [MCP parameters](#mcp-parameters).
@@ -220,10 +225,11 @@ MCP watches local files and updates the index incrementally. Package upgrades in
220
225
 
221
226
  Miru chunks source in tiers: **AST** (tree-sitter, default) → **structural** heuristics → **line** splits.
222
227
 
223
- **AST chunking** — 22 languages (syntax-aware boundaries via vendored `web-tree-sitter` grammars):
228
+ **AST chunking** — 26 languages (syntax-aware boundaries via vendored `web-tree-sitter` grammars):
224
229
 
225
230
  | Language | Typical extensions |
226
231
  |----------|-------------------|
232
+ | astro | `.astro` |
227
233
  | bash | `.sh`, `.bash`, `.zsh` |
228
234
  | c | `.c` |
229
235
  | cpp | `.cpp`, `.h`, `.hpp`, etc. |
@@ -231,10 +237,10 @@ Miru chunks source in tiers: **AST** (tree-sitter, default) → **structural** h
231
237
  | css | `.css` |
232
238
  | dart | `.dart` |
233
239
  | elixir | `.ex`, `.exs` |
234
- | embeddedtemplate | ERB-style templates |
240
+ | embeddedtemplate | `.erb`, `.ejs` |
235
241
  | go | `.go` |
236
242
  | haskell | `.hs` |
237
- | html | `.html` |
243
+ | html | `.html`, `.htm` |
238
244
  | java | `.java` |
239
245
  | javascript | `.js`, `.jsx`, `.mjs`, `.cjs` |
240
246
  | json | `.json` |
@@ -245,17 +251,20 @@ Miru chunks source in tiers: **AST** (tree-sitter, default) → **structural** h
245
251
  | rust | `.rs` |
246
252
  | scala | `.scala` |
247
253
  | solidity | `.sol` |
254
+ | sql | `.sql` |
255
+ | svelte | `.svelte` |
248
256
  | typescript | `.ts`, `.tsx`, `.mts`, `.cts` |
257
+ | vue | `.vue` |
249
258
 
250
259
  **Structural fallback** (brace/indent heuristics when AST is unavailable): python, go, typescript, javascript, cpp, c.
251
260
 
252
- **Line fallback:** everything else that gets indexed (kotlin, swift, vue, sql, etc.) — still searchable, coarser chunks.
261
+ **Line fallback:** everything else that gets indexed (kotlin, swift, etc.) — still searchable, coarser chunks.
253
262
 
254
263
  Set `MIRU_AST_CHUNKING=0` to disable AST and use structural → lines only.
255
264
 
256
265
  ## CLI reference
257
266
 
258
- Run `miru -h` for the command list, or `miru <command> -h` for details.
267
+ Run `miru` in a terminal or `miru -h` for the command list, or `miru <command> -h` for details.
259
268
 
260
269
  | Command | Purpose |
261
270
  | ----------------------------------------- | --------------------------------------------------------------------------------- |
@@ -269,8 +278,7 @@ Run `miru -h` for the command list, or `miru <command> -h` for details.
269
278
  | `miru benchmark on/off/status/clear` | Toggle MCP benchmark mode / clear report |
270
279
  | `miru init --agent <id>` | Project-local sub-agent |
271
280
  | `miru clear [path]` | Drop index cache (use after big CLI-only refactors) |
272
- | `miru hook-guard` | PreToolUse hook entrypoint *(experimental)*; used by installers, reads JSON stdin |
273
- | `miru` | Start MCP server (`--benchmark` for comparisons) |
281
+ | `miru mcp` | Start MCP server (`--benchmark` for comparisons) |
274
282
 
275
283
 
276
284
  CLI uses hyphens (`find-related`); MCP tool names use underscores (`find_related`).
@@ -285,7 +293,7 @@ bun add @takara-ai/miru-code
285
293
  import { MiruIndex } from "@takara-ai/miru-code";
286
294
 
287
295
  const index = await MiruIndex.fromPath("./src");
288
- const results = await index.search({ query: "BM25 tokenize", topK: 10 });
296
+ const results = await index.search({ query: "BM25 tokenize" });
289
297
  ```
290
298
 
291
299
  ## Environment
@@ -352,8 +360,9 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
352
360
  | Param | Required | Notes |
353
361
  | ---------------- | -------- | --------------------------------------- |
354
362
  | `query` | yes | Natural language or code query |
355
- | `repo` | yes | Project root or git URL |
356
- | `top_k` | no | Results to return (default 3, max 10) |
363
+ | `repo` | no | Startup directory by default; set for another repo |
364
+ | `include` | no | Gitignore-style glob patterns; only matching files are searched (same as `locate.include`) |
365
+ | `exclude` | no | Gitignore-style glob patterns; matching files are skipped (same as `locate.exclude`) |
357
366
  | `dedupe_by_file` | no | Keep best hit per file (default `true`) |
358
367
 
359
368
 
@@ -363,7 +372,9 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
363
372
  | Param | Required | Notes |
364
373
  | ------------- | -------- | ----------------------------------------------------------------------------------- |
365
374
  | `literal` | yes | Exact substring to find |
366
- | `repo` | yes | Project root or git URL |
375
+ | `repo` | no | Startup directory by default; set for another repo |
376
+ | `include` | no | Gitignore-style glob patterns; only matching files are searched |
377
+ | `exclude` | no | Gitignore-style glob patterns; matching files are skipped |
367
378
  | `mode` | no | `count` · `locations` · `lines` (default). Prefer `count`/`locations` when possible |
368
379
  | `limit` | no | Cap returned hits. Omit to return all matches |
369
380
  | `ignore_case` | no | Case-insensitive match (default `false`) |
@@ -376,7 +387,7 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
376
387
  | ------------------ | -------- | --------------------------------------------------------------------- |
377
388
  | `file_path` | yes | From hit `file_path` or `absolute_path` (local repos) |
378
389
  | `anchor_line` | yes | From the search hit (`anchor_line` when truncated, else `start_line`) |
379
- | `repo` | yes | Same repo as the search |
390
+ | `repo` | no | Same repo as the search, if set |
380
391
  | `before` / `after` | no | Extra chunks before/after anchor (default 1 each) |
381
392
 
382
393
 
@@ -387,8 +398,7 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
387
398
  | ------------- | -------- | -------------------------------------------- |
388
399
  | `file_path` | yes | From a search hit |
389
400
  | `anchor_line` | yes | From the search hit |
390
- | `repo` | yes | Same repo as the search |
391
- | `top_k` | no | Related chunks to return (default 3, max 10) |
401
+ | `repo` | no | Same repo as the search, if set |
392
402
 
393
403
 
394
404
  `**read_benchmark**` *(benchmark mode only)*
@@ -405,16 +415,18 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
405
415
  {
406
416
  "miru": {
407
417
  "command": "miru",
408
- "args": []
418
+ "args": ["mcp"]
409
419
  }
410
420
  }
411
421
  ```
412
422
 
413
- For benchmark mode, set `"args": ["--benchmark"]` (or append that flag). Prefer `miru benchmark on` after a normal install — it updates every agent config.
423
+ For benchmark mode, set `"args": ["mcp", "--benchmark"]` (or append the flag). Prefer `miru benchmark on` after a normal install — it updates every agent config.
414
424
 
415
425
  Run `miru setup` once so the server can load credentials from `credentials.json`. If the MCP server starts in an interactive terminal without stored credentials, it will start device login automatically.
416
426
 
417
- Use `bunx` + `@takara-ai/miru-code` if `miru` is not global. Wrapper key varies by IDE (`mcpServers`, `servers`, or `mcp`).
427
+ Use `bunx` + `@takara-ai/miru-code@latest` if `miru` is not global. The installer uses this command so each new MCP server launch can pick up a published version without a global update. A running server keeps its current version until restarted. Wrapper key varies by IDE (`mcpServers`, `servers`, or `mcp`).
428
+
429
+ Older headless MCP configs that launch `miru` without a subcommand, with or without MCP flags, continue to work. Run `miru install` again to update managed entries to the explicit `mcp` subcommand, then restart your coding agent. For manual MCP configs, add `mcp` after the executable or package name.
418
430
 
419
431
  ## Developing
420
432
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "label": "coverage",
4
+ "message": "100.0%",
5
+ "color": "brightgreen"
6
+ }
@@ -2,6 +2,7 @@
2
2
  "version": 1,
3
3
  "runtime": "web-tree-sitter@^0.26.9",
4
4
  "languages": {
5
+ "astro": "tree-sitter-astro.wasm",
5
6
  "bash": "tree-sitter-bash.wasm",
6
7
  "c": "tree-sitter-c.wasm",
7
8
  "cpp": "tree-sitter-cpp.wasm",
@@ -23,9 +24,13 @@
23
24
  "rust": "tree-sitter-rust.wasm",
24
25
  "scala": "tree-sitter-scala.wasm",
25
26
  "solidity": "tree-sitter-solidity.wasm",
26
- "typescript": "tree-sitter-typescript.wasm"
27
+ "sql": "tree-sitter-sql.wasm",
28
+ "svelte": "tree-sitter-svelte.wasm",
29
+ "typescript": "tree-sitter-typescript.wasm",
30
+ "vue": "tree-sitter-vue.wasm"
27
31
  },
28
32
  "files": [
33
+ "tree-sitter-astro.wasm",
29
34
  "tree-sitter-bash.wasm",
30
35
  "tree-sitter-c.wasm",
31
36
  "tree-sitter-c_sharp.wasm",
@@ -50,7 +55,10 @@
50
55
  "tree-sitter-rust.wasm",
51
56
  "tree-sitter-scala.wasm",
52
57
  "tree-sitter-solidity.wasm",
58
+ "tree-sitter-sql.wasm",
59
+ "tree-sitter-svelte.wasm",
53
60
  "tree-sitter-tsx.wasm",
54
- "tree-sitter-typescript.wasm"
61
+ "tree-sitter-typescript.wasm",
62
+ "tree-sitter-vue.wasm"
55
63
  ]
56
64
  }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "1.8.2",
3
+ "version": "1.10.0",
4
+ "mcpName": "ai.takara/miru",
4
5
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
6
  "type": "module",
6
7
  "license": "MIT",
@@ -50,13 +51,15 @@
50
51
  "miru": "bun run src/cli.ts",
51
52
  "mcp": "bun run src/cli.ts",
52
53
  "test": "bun test",
54
+ "coverage": "bun test --coverage --coverage-reporter=lcov",
55
+ "coverage:badge": "bun run coverage && bun run scripts/update-coverage-badge.ts",
53
56
  "typecheck": "tsc --noEmit",
54
57
  "lint": "biome check .",
55
58
  "lint:fix": "biome check --write .",
56
59
  "prepare": "bun run scripts/install-git-hooks.ts",
57
60
  "build:kiro-plugin": "bun run scripts/build-kiro-plugin.ts",
58
61
  "vendor-grammars": "bun run scripts/vendor-grammars.ts",
59
- "prepublishOnly": "bun run vendor-grammars && bun run typecheck && bun test",
62
+ "prepublishOnly": "bun run typecheck && bun test",
60
63
  "benchmark:ab": "bun run scripts/search-ab-local.ts",
61
64
  "benchmark:ab-multi": "bun run scripts/search-ab-multi.ts",
62
65
  "benchmark:codesearchnet-compression": "bun run scripts/benchmark-codesearchnet-compression.ts",
@@ -83,13 +86,17 @@
83
86
  },
84
87
  "devDependencies": {
85
88
  "@biomejs/biome": "^2.5.1",
89
+ "@derekstride/tree-sitter-sql": "^0.3.11",
86
90
  "@huggingface/tokenizers": "^0.1.3",
91
+ "@tree-sitter-grammars/tree-sitter-svelte": "^1.0.2",
87
92
  "@types/bun": "^1.3.14",
88
93
  "ajv": "^8.17.1",
89
94
  "ajv-formats": "^3.0.1",
95
+ "tree-sitter-astro": "github:virchau13/tree-sitter-astro#213f6e6",
90
96
  "tree-sitter-bash": "^0.25.1",
91
97
  "tree-sitter-c": "^0.24.1",
92
98
  "tree-sitter-c-sharp": "^0.23.5",
99
+ "tree-sitter-cli": "^0.27.0",
93
100
  "tree-sitter-cpp": "^0.23.4",
94
101
  "tree-sitter-css": "^0.25.0",
95
102
  "tree-sitter-dart": "^1.0.0",
@@ -109,6 +116,7 @@
109
116
  "tree-sitter-scala": "^0.24.0",
110
117
  "tree-sitter-solidity": "^1.2.13",
111
118
  "tree-sitter-typescript": "^0.23.2",
119
+ "tree-sitter-vue": "github:tree-sitter-grammars/tree-sitter-vue#ce8011a",
112
120
  "typescript": "^6.0.3"
113
121
  }
114
122
  }
package/src/agents.ts CHANGED
@@ -38,9 +38,9 @@ const AGENT_NATIVE_TOOLS: Record<AgentId, NativeToolNames> = {
38
38
  },
39
39
  };
40
40
 
41
- export function agentDestination(agent: AgentId): string {
41
+ export function agentDestination(agent: AgentId, cwd = "."): string {
42
42
  const baseDir = agent === "copilot" ? ".github" : `.${agent}`;
43
- return join(baseDir, "agents", "miru-code.md");
43
+ return join(cwd, baseDir, "agents", "miru-code.md");
44
44
  }
45
45
 
46
46
  export async function loadAgentTemplate(agent: AgentId): Promise<string> {
@@ -51,9 +51,9 @@ export async function loadAgentTemplate(agent: AgentId): Promise<string> {
51
51
 
52
52
  export async function writeAgentFile(
53
53
  agent: AgentId,
54
- options: { force?: boolean } = {},
54
+ options: { force?: boolean; cwd?: string } = {},
55
55
  ): Promise<string> {
56
- const dest = agentDestination(agent);
56
+ const dest = agentDestination(agent, options.cwd);
57
57
  const existing = await Bun.file(dest).exists();
58
58
  if (existing && !options.force) {
59
59
  throw new Error(`${dest} already exists. Run with --force to overwrite.`);
@@ -205,30 +205,31 @@ export async function checkDeviceAuthorizationOnce(
205
205
 
206
206
  export async function pollDeviceAuthorization(
207
207
  start: DeviceAuthorizationStart,
208
- options?: { config?: DeviceAuthConfig; fetchImpl?: typeof fetch },
208
+ options?: {
209
+ config?: DeviceAuthConfig;
210
+ fetchImpl?: typeof fetch;
211
+ sleepImpl?: (ms: number) => Promise<void>;
212
+ },
209
213
  ): Promise<DeviceAuthorizationTokens> {
210
214
  const deadline = Date.now() + start.expiresIn * 1000;
211
215
  let intervalMs = start.interval * 1000;
212
216
 
213
217
  while (Date.now() < deadline) {
214
- await sleep(intervalMs);
218
+ await (options?.sleepImpl ?? sleep)(intervalMs);
215
219
 
216
220
  const check = await checkDeviceAuthorizationOnce(start, options);
217
- if (check.status === "success") {
218
- return check.tokens;
219
- }
220
- if (check.status === "pending") {
221
- continue;
222
- }
223
- if (check.status === "slow_down") {
224
- intervalMs += 5_000;
225
- continue;
226
- }
227
- if (check.status === "denied") {
228
- throw new Error("Device login was denied.");
229
- }
230
- if (check.status === "expired") {
231
- throw new Error("Device login expired before it was completed.");
221
+ switch (check.status) {
222
+ case "success":
223
+ return check.tokens;
224
+ case "pending":
225
+ continue;
226
+ case "slow_down":
227
+ intervalMs += 5_000;
228
+ continue;
229
+ case "denied":
230
+ throw new Error("Device login was denied.");
231
+ case "expired":
232
+ throw new Error("Device login expired before it was completed.");
232
233
  }
233
234
  }
234
235
 
@@ -284,15 +285,19 @@ export function deviceCredentialsNeedRefresh(credentials: StoredDeviceCodeCreden
284
285
  return expiresAt <= Date.now() + EXPIRY_SKEW_MS;
285
286
  }
286
287
 
287
- export function openBrowserForDeviceLogin(url: string): boolean {
288
+ export function openBrowserForDeviceLogin(
289
+ url: string,
290
+ platform: NodeJS.Platform = process.platform,
291
+ spawn: typeof Bun.spawn = Bun.spawn,
292
+ ): boolean {
288
293
  const command =
289
- process.platform === "darwin"
294
+ platform === "darwin"
290
295
  ? ["open", url]
291
- : process.platform === "win32"
296
+ : platform === "win32"
292
297
  ? ["cmd", "/c", "start", "", url]
293
298
  : ["xdg-open", url];
294
299
  try {
295
- const proc = Bun.spawn(command, {
300
+ const proc = spawn(command, {
296
301
  stdin: "ignore",
297
302
  stdout: "ignore",
298
303
  stderr: "ignore",
@@ -20,10 +20,19 @@ export interface AuthenticateOptions {
20
20
  interactive?: boolean;
21
21
  }
22
22
 
23
- async function promptApiKey(): Promise<string> {
23
+ export interface AuthProviderDependencies {
24
+ promptHidden?: typeof promptHidden;
25
+ validateApiKey?: typeof validateEmbeddingApiKey;
26
+ promptConfirm?: typeof promptConfirm;
27
+ startDeviceAuthorization?: typeof startDeviceAuthorization;
28
+ pollDeviceAuthorization?: typeof pollDeviceAuthorization;
29
+ openBrowser?: (url: string) => boolean;
30
+ }
31
+
32
+ async function promptApiKey(promptHiddenImpl: typeof promptHidden = promptHidden): Promise<string> {
24
33
  let key = "";
25
34
  while (!key) {
26
- key = await promptHidden("Takara API key (input hidden): ", process.stderr);
35
+ key = await promptHiddenImpl("Takara API key (input hidden): ", process.stderr);
27
36
  if (!key) {
28
37
  warn("API key cannot be empty.");
29
38
  }
@@ -43,12 +52,13 @@ async function resolveAuthMode(options: AuthenticateOptions): Promise<AuthMode>
43
52
 
44
53
  async function authenticateWithApiKey(
45
54
  options: AuthenticateOptions,
55
+ dependencies: AuthProviderDependencies,
46
56
  ): Promise<AuthenticatedCredentials> {
47
- const apiKey = options.apiKey ?? (await promptApiKey());
57
+ const apiKey = options.apiKey ?? (await promptApiKey(dependencies.promptHidden));
48
58
  if (!options.skipValidation) {
49
59
  const spinner = new Spinner("Validating API key");
50
60
  spinner.start();
51
- const result = await validateEmbeddingApiKey({ apiKey });
61
+ const result = await (dependencies.validateApiKey ?? validateEmbeddingApiKey)({ apiKey });
52
62
  if (!result.valid) {
53
63
  spinner.stop();
54
64
  throw new Error(result.message);
@@ -60,19 +70,20 @@ async function authenticateWithApiKey(
60
70
 
61
71
  async function authenticateWithDeviceCode(
62
72
  options: AuthenticateOptions,
73
+ dependencies: AuthProviderDependencies,
63
74
  ): Promise<AuthenticatedCredentials> {
64
75
  writeStderr("");
65
76
  const spinner = new Spinner("Authenticating");
66
77
  spinner.start();
67
78
  try {
68
- const start = await startDeviceAuthorization();
79
+ const start = await (dependencies.startDeviceAuthorization ?? startDeviceAuthorization)();
69
80
  const verificationUrl = start.verificationUriComplete ?? start.verificationUri;
70
81
 
71
82
  const shouldOpenBrowser =
72
83
  options.interactive &&
73
84
  (process.env.MIRU_OPEN_BROWSER === undefined || process.env.MIRU_OPEN_BROWSER === "1");
74
85
  if (shouldOpenBrowser) {
75
- openBrowserForDeviceLogin(verificationUrl);
86
+ (dependencies.openBrowser ?? openBrowserForDeviceLogin)(verificationUrl);
76
87
  }
77
88
 
78
89
  let visit = dim(` Visit ${verificationUrl}`);
@@ -81,7 +92,7 @@ async function authenticateWithDeviceCode(
81
92
  }
82
93
  spinner.follow(visit);
83
94
 
84
- const tokens = await pollDeviceAuthorization(start);
95
+ const tokens = await (dependencies.pollDeviceAuthorization ?? pollDeviceAuthorization)(start);
85
96
  spinner.succeed("");
86
97
  return {
87
98
  kind: "device_code",
@@ -95,9 +106,12 @@ async function authenticateWithDeviceCode(
95
106
  spinner.stop();
96
107
  if (options.allowManualFallback && options.interactive) {
97
108
  warn(err instanceof Error ? err.message : String(err));
98
- const fallback = await promptConfirm("Enter an API key instead?", true);
109
+ const fallback = await (dependencies.promptConfirm ?? promptConfirm)(
110
+ "Enter an API key instead?",
111
+ true,
112
+ );
99
113
  if (fallback) {
100
- return authenticateWithApiKey(options);
114
+ return authenticateWithApiKey(options, dependencies);
101
115
  }
102
116
  }
103
117
  throw err;
@@ -106,10 +120,11 @@ async function authenticateWithDeviceCode(
106
120
 
107
121
  export async function authenticateWithProvider(
108
122
  options: AuthenticateOptions,
123
+ dependencies: AuthProviderDependencies = {},
109
124
  ): Promise<AuthenticatedCredentials> {
110
125
  const mode = await resolveAuthMode(options);
111
126
  if (mode === "device_code") {
112
- return authenticateWithDeviceCode(options);
127
+ return authenticateWithDeviceCode(options, dependencies);
113
128
  }
114
- return authenticateWithApiKey(options);
129
+ return authenticateWithApiKey(options, dependencies);
115
130
  }
@@ -49,9 +49,6 @@ function firstGrepMatchLine(hit: GrepFileHit | undefined): number | null {
49
49
  }
50
50
 
51
51
  function expandLineSpan(chunks: { start_line: number; end_line: number }[]): number {
52
- if (chunks.length === 0) {
53
- return 0;
54
- }
55
52
  const start = Math.min(...chunks.map((c) => c.start_line));
56
53
  const end = Math.max(...chunks.map((c) => c.end_line));
57
54
  return end - start + 1;
@@ -74,9 +71,6 @@ async function readLineWindowTokens(
74
71
  try {
75
72
  const text = await readFile(absPath, "utf-8");
76
73
  const lines = text.split("\n");
77
- if (lines.length === 0) {
78
- return 0;
79
- }
80
74
  const half = Math.max(1, Math.floor(lineSpan / 2));
81
75
  const start = Math.max(1, centerLine - half);
82
76
  const end = Math.min(lines.length, centerLine + half);
@@ -127,9 +121,6 @@ function topKOverlapPct(miruFiles: string[], grepFiles: string[]): number {
127
121
  return 100;
128
122
  }
129
123
  const union = new Set([...miruFiles, ...grepFiles]);
130
- if (union.size === 0) {
131
- return 0;
132
- }
133
124
  const overlap = miruFiles.filter((file) => grepFiles.includes(file)).length;
134
125
  return Math.round((overlap / union.size) * 100);
135
126
  }
@@ -169,9 +160,11 @@ export async function benchmarkSearchComparison(options: {
169
160
  /** Keep only the best hit per file (default true). Matches MCP `dedupe_by_file`. */
170
161
  dedupeByFile?: boolean;
171
162
  relevant?: string[];
163
+ dependencies?: { grepSearch?: typeof grepSearch };
172
164
  }): Promise<{ benchmark: SearchBenchmarkBlock; results: SearchResult[] }> {
173
165
  const { query, repoPath, index, topK, relevant } = options;
174
166
  const dedupeByFile = options.dedupeByFile !== false;
167
+ const searchGrep = options.dependencies?.grepSearch ?? grepSearch;
175
168
  const parallelStart = performance.now();
176
169
 
177
170
  const miruPromise = (async () => {
@@ -194,7 +187,7 @@ export async function benchmarkSearchComparison(options: {
194
187
 
195
188
  const grepSearchPromise = (async () => {
196
189
  const started = performance.now();
197
- const grep = await grepSearch(repoPath, query, topK);
190
+ const grep = await searchGrep(repoPath, query, topK);
198
191
  return { grep, latencyMs: performance.now() - started };
199
192
  })();
200
193