@takara-ai/miru-code 1.9.0 → 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 (59) hide show
  1. package/README.md +43 -48
  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 +10 -45
  24. package/src/credentials.ts +8 -13
  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 +5 -4
  30. package/src/index/bm25.ts +66 -6
  31. package/src/index/create.ts +5 -9
  32. package/src/index/files.ts +5 -1
  33. package/src/index/incremental.ts +48 -8
  34. package/src/index/sparse.ts +14 -2
  35. package/src/installer/agents.ts +85 -65
  36. package/src/installer/benchmark-mode.ts +40 -11
  37. package/src/installer/config.ts +105 -42
  38. package/src/installer/installer.ts +59 -34
  39. package/src/installer/legacy-hooks.ts +131 -0
  40. package/src/installer/prompt.ts +20 -22
  41. package/src/installer/search-policy.ts +8 -8
  42. package/src/mcp/index-cache.ts +16 -3
  43. package/src/mcp/server.ts +42 -22
  44. package/src/mcp/stdio.ts +17 -2
  45. package/src/miru-index.ts +54 -18
  46. package/src/prompt.ts +51 -14
  47. package/src/ranking/boosting.ts +5 -6
  48. package/src/ranking/location.ts +6 -10
  49. package/src/ranking/penalties.ts +14 -22
  50. package/src/search.ts +4 -12
  51. package/src/setup.ts +25 -13
  52. package/src/snippet.ts +8 -26
  53. package/src/spinner.ts +26 -16
  54. package/src/tokenizer/bert-wordpiece.ts +0 -6
  55. package/src/utils.ts +0 -4
  56. package/src/version.ts +4 -4
  57. package/src/installer/hooks/install.ts +0 -549
  58. package/src/installer/hooks/opencode-plugin.ts +0 -54
  59. 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.
@@ -48,10 +48,9 @@ Plugin installs don't carry full CLI parity — what each one gives you depends
48
48
  | `miru` / Caveman / STE skills | ✅ | ✅ | ✅ |
49
49
  | Dedicated sub-agent (`miru:miru-code`) | ✅ | — | — |
50
50
  | Benchmark mode toggle (`/plugin configure`) | ✅ | — | — |
51
- | Search hooks | — | — | — |
52
51
  | Credentials | own plugin-scoped dir | own plugin-scoped dir | n/a |
53
52
 
54
- `—` 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. Search hooks aren't shipped to any plugin — they're slated for removal from Miru entirely (tracked internally), so not worth packaging.
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.
55
54
 
56
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.
57
56
 
@@ -92,7 +91,7 @@ Interactive TUI — **↑↓** move, **space** toggle, **a** all, **enter** conf
92
91
  | Instructions | Search policy in `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` |
93
92
  | Sub-agent | Dedicated `miru-code` agent file |
94
93
  | Cursor rules | Always-on `.cursor/rules/miru-code.mdc` (Cursor only) |
95
- | Search hooks *(experimental)* | Block built-in Grep/Glob and redirect to Miru MCP |
94
+ | Old Miru hooks | Removes stale search hooks from older releases |
96
95
  | Caveman *(experimental)* | On-demand chat compression skill (`/caveman`) |
97
96
  | STE writing *(experimental)* | On-demand clear technical English for docs (`/ste`) |
98
97
 
@@ -106,18 +105,18 @@ miru uninstall # remove miru config
106
105
  **Supported:** Cursor · Claude Code · Gemini CLI · Kiro · OpenCode · GitHub Copilot · Codex · VS Code · Visual Studio (Windows) · Windsurf / Devin Desktop
107
106
 
108
107
 
109
- | IDE | MCP | Instructions / rules | Hooks *(experimental)* | Caveman *(experimental)* | STE *(experimental)* |
110
- | -------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------- | --------------------------------------------------- | -------------------------------------------- |
111
- | Cursor | `~/.cursor/mcp.json` | `~/.cursor/rules/miru-code.mdc` | `~/.cursor/hooks.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
112
- | Claude Code | `~/.claude.json` | `~/.claude/CLAUDE.md` | `~/.claude/settings.json` | `~/.claude/skills/caveman/SKILL.md` | `~/.claude/skills/ste/SKILL.md` |
113
- | Gemini CLI | `~/.gemini/settings.json` | `~/.gemini/GEMINI.md` | `~/.gemini/settings.json` (`BeforeTool`) | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
114
- | Kiro | `~/.kiro/settings/mcp.json` | `~/.kiro/steering/miru.md` | `~/.kiro/settings/hooks.json` | `~/.kiro/skills/caveman/SKILL.md` | `~/.kiro/skills/ste/SKILL.md` |
115
- | 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` |
116
- | GitHub Copilot | `~/.copilot/mcp-config.json` | — | `~/.copilot/hooks/miru-search.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
117
- | Codex | `~/.codex/config.toml` | `~/.codex/AGENTS.md` | `~/.codex/hooks.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
118
- | VS Code | `…/Code/User/mcp.json` | — | `~/.copilot/hooks/miru-search.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
119
- | Visual Studio | `%USERPROFILE%\.mcp.json` | — | `~/.copilot/hooks/miru-search.json` | `~/.agents/skills/caveman/SKILL.md` | `~/.agents/skills/ste/SKILL.md` |
120
- | 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` |
121
120
 
122
121
 
123
122
  ### Plugin packaging
@@ -128,11 +127,11 @@ miru uninstall # remove miru config
128
127
 
129
128
  Current limitation:
130
129
 
131
- - 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`
132
131
  - that means local source edits do not affect plugin behavior until a package version is published
133
132
  - and a fully self-contained “no Bun required” plugin install is still future work
134
133
 
135
- ### Search hooks
134
+ ### On-demand skills
136
135
 
137
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.
138
137
 
@@ -158,17 +157,6 @@ Off by default at install time. Enable Caveman in the installer for any supporte
158
157
 
159
158
  </details>
160
159
 
161
- <details>
162
- <summary>Search hooks <em>(experimental)</em></summary>
163
-
164
- Search hooks are **experimental** — behavior and IDE support may change between releases.
165
-
166
- 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.
167
-
168
- Hooks are **off by default** at install time. Enable them in the installer if you want built-in Grep/Glob redirected to Miru MCP.
169
-
170
- </details>
171
-
172
160
  **Team sub-agent in a repo** (optional):
173
161
 
174
162
  ```bash
@@ -189,14 +177,14 @@ miru find-related src/auth.ts 42 ./src
189
177
  Terminal output is human-readable; use `--json` for scripts. One-off without installing:
190
178
 
191
179
  ```bash
192
- bunx @takara-ai/miru-code search "auth middleware" ./src
180
+ bunx @takara-ai/miru-code@latest search "auth middleware" ./src
193
181
  ```
194
182
 
195
183
  ---
196
184
 
197
185
  ## MCP tools
198
186
 
199
- 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.
200
188
 
201
189
 
202
190
  | Tool | When to use |
@@ -210,11 +198,13 @@ When wired via `miru install`, the MCP server exposes `search`, `locate`, `expan
210
198
 
211
199
  ### Workflow
212
200
 
213
- 1. **`search`** with `query` + `repo` — returns compact snippets (~±15 lines) and relevance scores.
214
- 2. If a hit has **`truncated: true`**, call **`expand`** with `file_path`, `anchor_line`, and `repo` — not another search or a full-file read.
215
- 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`.
216
204
  4. Use your editor's **Read** on `absolute_path` only when editing or when `expand` still lacks context.
217
205
 
206
+ Keep `repo` on follow-up calls when set.
207
+
218
208
  Prefer these tools over Grep, Glob, or SemanticSearch when Miru MCP is connected — hooks and instructions enforce that when enabled.
219
209
 
220
210
  Local repo hits include `absolute_path` for one-click navigation. Parameter reference is below under [MCP parameters](#mcp-parameters).
@@ -235,10 +225,11 @@ MCP watches local files and updates the index incrementally. Package upgrades in
235
225
 
236
226
  Miru chunks source in tiers: **AST** (tree-sitter, default) → **structural** heuristics → **line** splits.
237
227
 
238
- **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):
239
229
 
240
230
  | Language | Typical extensions |
241
231
  |----------|-------------------|
232
+ | astro | `.astro` |
242
233
  | bash | `.sh`, `.bash`, `.zsh` |
243
234
  | c | `.c` |
244
235
  | cpp | `.cpp`, `.h`, `.hpp`, etc. |
@@ -246,10 +237,10 @@ Miru chunks source in tiers: **AST** (tree-sitter, default) → **structural** h
246
237
  | css | `.css` |
247
238
  | dart | `.dart` |
248
239
  | elixir | `.ex`, `.exs` |
249
- | embeddedtemplate | ERB-style templates |
240
+ | embeddedtemplate | `.erb`, `.ejs` |
250
241
  | go | `.go` |
251
242
  | haskell | `.hs` |
252
- | html | `.html` |
243
+ | html | `.html`, `.htm` |
253
244
  | java | `.java` |
254
245
  | javascript | `.js`, `.jsx`, `.mjs`, `.cjs` |
255
246
  | json | `.json` |
@@ -260,17 +251,20 @@ Miru chunks source in tiers: **AST** (tree-sitter, default) → **structural** h
260
251
  | rust | `.rs` |
261
252
  | scala | `.scala` |
262
253
  | solidity | `.sol` |
254
+ | sql | `.sql` |
255
+ | svelte | `.svelte` |
263
256
  | typescript | `.ts`, `.tsx`, `.mts`, `.cts` |
257
+ | vue | `.vue` |
264
258
 
265
259
  **Structural fallback** (brace/indent heuristics when AST is unavailable): python, go, typescript, javascript, cpp, c.
266
260
 
267
- **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.
268
262
 
269
263
  Set `MIRU_AST_CHUNKING=0` to disable AST and use structural → lines only.
270
264
 
271
265
  ## CLI reference
272
266
 
273
- 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.
274
268
 
275
269
  | Command | Purpose |
276
270
  | ----------------------------------------- | --------------------------------------------------------------------------------- |
@@ -284,8 +278,7 @@ Run `miru -h` for the command list, or `miru <command> -h` for details.
284
278
  | `miru benchmark on/off/status/clear` | Toggle MCP benchmark mode / clear report |
285
279
  | `miru init --agent <id>` | Project-local sub-agent |
286
280
  | `miru clear [path]` | Drop index cache (use after big CLI-only refactors) |
287
- | `miru hook-guard` | PreToolUse hook entrypoint *(experimental)*; used by installers, reads JSON stdin |
288
- | `miru` | Start MCP server (`--benchmark` for comparisons) |
281
+ | `miru mcp` | Start MCP server (`--benchmark` for comparisons) |
289
282
 
290
283
 
291
284
  CLI uses hyphens (`find-related`); MCP tool names use underscores (`find_related`).
@@ -367,7 +360,7 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
367
360
  | Param | Required | Notes |
368
361
  | ---------------- | -------- | --------------------------------------- |
369
362
  | `query` | yes | Natural language or code query |
370
- | `repo` | yes | Project root or git URL |
363
+ | `repo` | no | Startup directory by default; set for another repo |
371
364
  | `include` | no | Gitignore-style glob patterns; only matching files are searched (same as `locate.include`) |
372
365
  | `exclude` | no | Gitignore-style glob patterns; matching files are skipped (same as `locate.exclude`) |
373
366
  | `dedupe_by_file` | no | Keep best hit per file (default `true`) |
@@ -379,7 +372,7 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
379
372
  | Param | Required | Notes |
380
373
  | ------------- | -------- | ----------------------------------------------------------------------------------- |
381
374
  | `literal` | yes | Exact substring to find |
382
- | `repo` | yes | Project root or git URL |
375
+ | `repo` | no | Startup directory by default; set for another repo |
383
376
  | `include` | no | Gitignore-style glob patterns; only matching files are searched |
384
377
  | `exclude` | no | Gitignore-style glob patterns; matching files are skipped |
385
378
  | `mode` | no | `count` · `locations` · `lines` (default). Prefer `count`/`locations` when possible |
@@ -394,7 +387,7 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
394
387
  | ------------------ | -------- | --------------------------------------------------------------------- |
395
388
  | `file_path` | yes | From hit `file_path` or `absolute_path` (local repos) |
396
389
  | `anchor_line` | yes | From the search hit (`anchor_line` when truncated, else `start_line`) |
397
- | `repo` | yes | Same repo as the search |
390
+ | `repo` | no | Same repo as the search, if set |
398
391
  | `before` / `after` | no | Extra chunks before/after anchor (default 1 each) |
399
392
 
400
393
 
@@ -405,7 +398,7 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
405
398
  | ------------- | -------- | -------------------------------------------- |
406
399
  | `file_path` | yes | From a search hit |
407
400
  | `anchor_line` | yes | From the search hit |
408
- | `repo` | yes | Same repo as the search |
401
+ | `repo` | no | Same repo as the search, if set |
409
402
 
410
403
 
411
404
  `**read_benchmark**` *(benchmark mode only)*
@@ -422,16 +415,18 @@ Override with `MIRU_BENCHMARK_HISTORY_PATH`. Append-only JSONL of compact token
422
415
  {
423
416
  "miru": {
424
417
  "command": "miru",
425
- "args": []
418
+ "args": ["mcp"]
426
419
  }
427
420
  }
428
421
  ```
429
422
 
430
- 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.
431
424
 
432
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.
433
426
 
434
- 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.
435
430
 
436
431
  ## Developing
437
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.9.0",
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