@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
@@ -5,11 +5,7 @@ import { join } from "node:path";
5
5
  import type { AgentId } from "../agents.ts";
6
6
  import { INSTRUCTIONS_MARKDOWN } from "./search-policy.ts";
7
7
 
8
- function copilotHooksPath(home: string): string {
9
- return join(home, ".copilot", "hooks", "miru-search.json");
10
- }
11
-
12
- /** Shared Copilot / VS Code / Visual Studio config root (MCP, hooks, agents). */
8
+ /** Shared Copilot / VS Code / Visual Studio config root (MCP, agents). */
13
9
  export function copilotHomeDir(home: string): string {
14
10
  return join(home, ".copilot");
15
11
  }
@@ -55,22 +51,15 @@ export function nativeSteSkillDir(home: string, vendor: NativeSkillVendor): stri
55
51
  return skillDir(home, `.${vendor}`, "ste");
56
52
  }
57
53
 
58
- function kiroHooksPath(home: string): string {
59
- return join(home, ".kiro", "settings", "hooks.json");
60
- }
61
-
62
- function windsurfHooksPath(home: string): string {
63
- return join(home, ".codeium", "windsurf", "hooks.json");
64
- }
65
-
66
54
  /** OpenCode config root (`$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`). */
67
55
  export function opencodeConfigDir(home: string): string {
68
56
  const xdg = process.env.XDG_CONFIG_HOME;
69
57
  return xdg ? join(xdg, "opencode") : join(home, ".config", "opencode");
70
58
  }
71
59
 
72
- function opencodePluginPath(home: string): string {
73
- return join(opencodeConfigDir(home), "plugins", "miru-search-guard.ts");
60
+ /** OpenCode's first-class global skill location for Miru code-search routing. */
61
+ export function opencodeMiruSkillPath(home: string): string {
62
+ return join(opencodeConfigDir(home), "skills", "miru-code-search", "SKILL.md");
74
63
  }
75
64
 
76
65
  export type InstallAction =
@@ -86,24 +75,24 @@ export type InstallMode = "install" | "uninstall";
86
75
 
87
76
  export const MIRU_START = "<!-- miru:start -->";
88
77
  export const MIRU_END = "<!-- miru:end -->";
78
+ export const MIRU_BUNX_PACKAGE = "@takara-ai/miru-code@latest";
89
79
 
90
80
  const HOME = homedir();
91
81
 
92
82
  const STDIO_SERVER_CONFIG: Record<string, unknown> = {
93
83
  command: "bunx",
94
- args: ["@takara-ai/miru-code"],
84
+ args: [MIRU_BUNX_PACKAGE, "mcp"],
95
85
  type: "stdio",
96
86
  };
97
87
 
98
88
  const BARE_STDIO_SERVER_CONFIG: Record<string, unknown> = {
99
89
  command: "bunx",
100
- args: ["@takara-ai/miru-code"],
90
+ args: [MIRU_BUNX_PACKAGE, "mcp"],
101
91
  };
102
92
 
103
93
  const OPENCODE_SERVER_CONFIG: Record<string, unknown> = {
104
- command: ["bunx", "@takara-ai/miru-code"],
94
+ command: ["bunx", MIRU_BUNX_PACKAGE, "mcp"],
105
95
  type: "local",
106
- enabled: true,
107
96
  };
108
97
 
109
98
  export const INSTRUCTIONS = `${MIRU_START}
@@ -121,7 +110,7 @@ export interface McpConfig {
121
110
  format: McpConfigFormat;
122
111
  }
123
112
 
124
- export type HooksFormat =
113
+ export type LegacyHooksFormat =
125
114
  | "claude"
126
115
  | "cursor"
127
116
  | "gemini"
@@ -138,8 +127,9 @@ export interface AgentTarget {
138
127
  mcp: McpConfig | null;
139
128
  instructionsPath: string | null;
140
129
  cursorRulesPath: string | null;
141
- hooksPath: string | null;
142
- hooksFormat: HooksFormat | null;
130
+ /** Search-hook paths are retained for uninstalling hooks created by older releases. */
131
+ legacyHooksPath: string | null;
132
+ legacyHooksFormat: LegacyHooksFormat | null;
143
133
  subagentPath: string | null;
144
134
  subagentId: AgentId | null;
145
135
  /**
@@ -151,6 +141,8 @@ export interface AgentTarget {
151
141
  * On-demand STE skill directory (`…/skills/ste/`), or null if unsupported.
152
142
  */
153
143
  steSkillDir: string | null;
144
+ /** OpenCode's global Miru code-search skill, advertised on each model step. */
145
+ miruSkillPath?: string | null;
154
146
  }
155
147
 
156
148
  export function opencodeMcpPath(): string {
@@ -166,11 +158,11 @@ export function opencodeMcpPath(): string {
166
158
  return jsonc;
167
159
  }
168
160
 
169
- export function vscodeMcpPath(): string {
170
- if (process.platform === "darwin") {
161
+ export function vscodeMcpPath(platform: NodeJS.Platform = process.platform): string {
162
+ if (platform === "darwin") {
171
163
  return join(HOME, "Library", "Application Support", "Code", "User", "mcp.json");
172
164
  }
173
- if (process.platform === "win32") {
165
+ if (platform === "win32") {
174
166
  const appData = process.env.APPDATA ?? HOME;
175
167
  return join(appData, "Code", "User", "mcp.json");
176
168
  }
@@ -184,23 +176,27 @@ export function visualStudioMcpPath(): string {
184
176
  return join(profile, ".mcp.json");
185
177
  }
186
178
 
187
- export function visualStudioInstallDir(): string | null {
188
- if (process.platform !== "win32") {
179
+ export function visualStudioInstallDir(
180
+ platform: NodeJS.Platform = process.platform,
181
+ ): string | null {
182
+ if (platform !== "win32") {
189
183
  return null;
190
184
  }
191
185
  const programFiles = process.env.ProgramFiles ?? join("C:", "Program Files");
192
186
  return join(programFiles, "Microsoft Visual Studio");
193
187
  }
194
188
 
195
- async function detectVisualStudio(): Promise<boolean> {
196
- if (process.platform !== "win32") {
189
+ async function detectVisualStudio(dependencies: AgentDetectionDependencies = {}): Promise<boolean> {
190
+ const platform = dependencies.platform ?? process.platform;
191
+ if (platform !== "win32") {
197
192
  return false;
198
193
  }
199
194
 
200
- const installDir = visualStudioInstallDir();
201
- if (installDir && existsSync(installDir)) {
195
+ const installDir = visualStudioInstallDir(platform);
196
+ const pathExists = dependencies.existsSync ?? existsSync;
197
+ if (installDir && pathExists(installDir)) {
202
198
  try {
203
- const entries = await readdir(installDir);
199
+ const entries = await (dependencies.readdir ?? ((path: string) => readdir(path)))(installDir);
204
200
  if (entries.length > 0) {
205
201
  return true;
206
202
  }
@@ -210,8 +206,9 @@ async function detectVisualStudio(): Promise<boolean> {
210
206
  }
211
207
 
212
208
  try {
213
- const proc = Bun.spawn(["where", "devenv"], { stdout: "pipe", stderr: "ignore" });
214
- return (await proc.exited) === 0;
209
+ return await (dependencies.commandOnPath
210
+ ? dependencies.commandOnPath("devenv")
211
+ : commandOnPath("devenv", platform, dependencies.spawn));
215
212
  } catch {
216
213
  return false;
217
214
  }
@@ -233,8 +230,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
233
230
  mcp: jsonMcp(join(HOME, ".claude.json"), "mcpServers", STDIO_SERVER_CONFIG),
234
231
  instructionsPath: join(HOME, ".claude", "CLAUDE.md"),
235
232
  cursorRulesPath: null,
236
- hooksPath: join(HOME, ".claude", "settings.json"),
237
- hooksFormat: "claude",
233
+ legacyHooksPath: join(HOME, ".claude", "settings.json"),
234
+ legacyHooksFormat: "claude",
238
235
  subagentPath: join(HOME, ".claude", "agents", "miru-code.md"),
239
236
  subagentId: "claude",
240
237
  cavemanSkillPath: nativeCavemanSkillPath(HOME, "claude"),
@@ -248,12 +245,13 @@ export const AGENT_TARGETS: AgentTarget[] = [
248
245
  mcp: jsonMcp(join(HOME, ".cursor", "mcp.json"), "mcpServers", STDIO_SERVER_CONFIG),
249
246
  instructionsPath: null,
250
247
  cursorRulesPath: join(HOME, ".cursor", "rules", "miru-code.mdc"),
251
- hooksPath: join(HOME, ".cursor", "hooks.json"),
252
- hooksFormat: "cursor",
248
+ legacyHooksPath: join(HOME, ".cursor", "hooks.json"),
249
+ legacyHooksFormat: "cursor",
253
250
  subagentPath: join(HOME, ".cursor", "agents", "miru-code.md"),
254
251
  subagentId: "cursor",
255
252
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
256
253
  steSkillDir: SHARED_STE_SKILL_DIR,
254
+ miruSkillPath: opencodeMiruSkillPath(HOME),
257
255
  },
258
256
  {
259
257
  id: "gemini",
@@ -263,8 +261,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
263
261
  mcp: jsonMcp(join(HOME, ".gemini", "settings.json"), "mcpServers", STDIO_SERVER_CONFIG),
264
262
  instructionsPath: join(HOME, ".gemini", "GEMINI.md"),
265
263
  cursorRulesPath: null,
266
- hooksPath: join(HOME, ".gemini", "settings.json"),
267
- hooksFormat: "gemini",
264
+ legacyHooksPath: join(HOME, ".gemini", "settings.json"),
265
+ legacyHooksFormat: "gemini",
268
266
  subagentPath: join(HOME, ".gemini", "agents", "miru-code.md"),
269
267
  subagentId: "gemini",
270
268
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -278,8 +276,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
278
276
  mcp: jsonMcp(join(HOME, ".kiro", "settings", "mcp.json"), "mcpServers", STDIO_SERVER_CONFIG),
279
277
  instructionsPath: join(HOME, ".kiro", "steering", "miru.md"),
280
278
  cursorRulesPath: null,
281
- hooksPath: kiroHooksPath(HOME),
282
- hooksFormat: "kiro",
279
+ legacyHooksPath: join(HOME, ".kiro", "settings", "hooks.json"),
280
+ legacyHooksFormat: "kiro",
283
281
  subagentPath: join(HOME, ".kiro", "agents", "miru-code.md"),
284
282
  subagentId: "kiro",
285
283
  cavemanSkillPath: nativeCavemanSkillPath(HOME, "kiro"),
@@ -290,11 +288,11 @@ export const AGENT_TARGETS: AgentTarget[] = [
290
288
  displayName: "OpenCode",
291
289
  binary: "opencode",
292
290
  configDir: opencodeConfigDir(HOME),
293
- mcp: jsonMcp(opencodeMcpPath(), "mcp", OPENCODE_SERVER_CONFIG),
291
+ mcp: jsonMcp(opencodeMcpPath(), "mcp.servers", OPENCODE_SERVER_CONFIG),
294
292
  instructionsPath: join(opencodeConfigDir(HOME), "AGENTS.md"),
295
293
  cursorRulesPath: null,
296
- hooksPath: opencodePluginPath(HOME),
297
- hooksFormat: "opencode",
294
+ legacyHooksPath: join(opencodeConfigDir(HOME), "plugins", "miru-search-guard.ts"),
295
+ legacyHooksFormat: "opencode",
298
296
  subagentPath: join(opencodeConfigDir(HOME), "agents", "miru-code.md"),
299
297
  subagentId: "opencode",
300
298
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -312,8 +310,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
312
310
  ),
313
311
  instructionsPath: null,
314
312
  cursorRulesPath: null,
315
- hooksPath: copilotHooksPath(HOME),
316
- hooksFormat: "vscode",
313
+ legacyHooksPath: join(copilotHomeDir(HOME), "hooks", "miru-search.json"),
314
+ legacyHooksFormat: "vscode",
317
315
  subagentPath: join(copilotHomeDir(HOME), "agents", "miru-code.agent.md"),
318
316
  subagentId: "copilot",
319
317
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -333,8 +331,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
333
331
  },
334
332
  instructionsPath: join(HOME, ".codex", "AGENTS.md"),
335
333
  cursorRulesPath: null,
336
- hooksPath: join(HOME, ".codex", "hooks.json"),
337
- hooksFormat: "claude",
334
+ legacyHooksPath: join(HOME, ".codex", "hooks.json"),
335
+ legacyHooksFormat: "claude",
338
336
  subagentPath: null,
339
337
  subagentId: null,
340
338
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -348,8 +346,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
348
346
  mcp: jsonMcp(vscodeMcpPath(), "servers", STDIO_SERVER_CONFIG),
349
347
  instructionsPath: null,
350
348
  cursorRulesPath: null,
351
- hooksPath: copilotHooksPath(HOME),
352
- hooksFormat: "vscode",
349
+ legacyHooksPath: join(copilotHomeDir(HOME), "hooks", "miru-search.json"),
350
+ legacyHooksFormat: "vscode",
353
351
  subagentPath: null,
354
352
  subagentId: null,
355
353
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -363,8 +361,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
363
361
  mcp: null,
364
362
  instructionsPath: null,
365
363
  cursorRulesPath: null,
366
- hooksPath: windsurfHooksPath(HOME),
367
- hooksFormat: "windsurf",
364
+ legacyHooksPath: join(HOME, ".codeium", "windsurf", "hooks.json"),
365
+ legacyHooksFormat: "windsurf",
368
366
  subagentPath: null,
369
367
  subagentId: null,
370
368
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -378,8 +376,8 @@ export const AGENT_TARGETS: AgentTarget[] = [
378
376
  mcp: jsonMcp(visualStudioMcpPath(), "servers", STDIO_SERVER_CONFIG),
379
377
  instructionsPath: null,
380
378
  cursorRulesPath: null,
381
- hooksPath: copilotHooksPath(HOME),
382
- hooksFormat: "vscode",
379
+ legacyHooksPath: join(copilotHomeDir(HOME), "hooks", "miru-search.json"),
380
+ legacyHooksFormat: "vscode",
383
381
  subagentPath: null,
384
382
  subagentId: null,
385
383
  cavemanSkillPath: SHARED_CAVEMAN_SKILL,
@@ -387,38 +385,60 @@ export const AGENT_TARGETS: AgentTarget[] = [
387
385
  },
388
386
  ];
389
387
 
390
- async function commandOnPath(command: string): Promise<boolean> {
391
- const lookup = process.platform === "win32" ? ["where", command] : ["which", command];
388
+ async function commandOnPath(
389
+ command: string,
390
+ platform: NodeJS.Platform = process.platform,
391
+ spawn: typeof Bun.spawn = Bun.spawn,
392
+ ): Promise<boolean> {
393
+ const lookup = platform === "win32" ? ["where", command] : ["which", command];
392
394
  try {
393
- const proc = Bun.spawn(lookup, { stdout: "pipe", stderr: "ignore" });
395
+ const proc = spawn(lookup, { stdout: "pipe", stderr: "ignore" });
394
396
  return (await proc.exited) === 0;
395
397
  } catch {
396
398
  return false;
397
399
  }
398
400
  }
399
401
 
400
- export async function isAgentDetected(agent: AgentTarget): Promise<boolean> {
402
+ export interface AgentDetectionDependencies {
403
+ platform?: NodeJS.Platform;
404
+ existsSync?: typeof existsSync;
405
+ readdir?: (path: string) => Promise<string[]>;
406
+ commandOnPath?: (command: string) => Promise<boolean>;
407
+ spawn?: typeof Bun.spawn;
408
+ home?: string;
409
+ }
410
+
411
+ export async function isAgentDetected(
412
+ agent: AgentTarget,
413
+ dependencies: AgentDetectionDependencies = {},
414
+ ): Promise<boolean> {
401
415
  if (agent.id === "visualstudio") {
402
- return detectVisualStudio();
416
+ return detectVisualStudio(dependencies);
403
417
  }
404
418
  if (agent.id === "windsurf") {
405
- if (agent.configDir && existsSync(agent.configDir)) {
419
+ if (agent.configDir && (dependencies.existsSync ?? existsSync)(agent.configDir)) {
406
420
  return true;
407
421
  }
408
- if (agent.binary && (await commandOnPath(agent.binary))) {
422
+ const commandExists = dependencies.commandOnPath
423
+ ? dependencies.commandOnPath
424
+ : (command: string) => commandOnPath(command, dependencies.platform, dependencies.spawn);
425
+ if (agent.binary && (await commandExists(agent.binary))) {
409
426
  return true;
410
427
  }
411
428
  return false;
412
429
  }
413
430
  // Copilot-specific artifacts only — ~/.copilot alone is shared with VS Code / VS.
414
431
  if (agent.id === "copilot") {
415
- return isCopilotInstalled(HOME);
432
+ return isCopilotInstalled(dependencies.home ?? HOME);
416
433
  }
417
- if (agent.binary && (await commandOnPath(agent.binary))) {
434
+ const commandExists = dependencies.commandOnPath
435
+ ? dependencies.commandOnPath
436
+ : (command: string) => commandOnPath(command, dependencies.platform, dependencies.spawn);
437
+ if (agent.binary && (await commandExists(agent.binary))) {
418
438
  return true;
419
439
  }
420
440
  if (agent.configDir) {
421
- return Bun.file(agent.configDir).exists();
441
+ return (dependencies.existsSync ?? existsSync)(agent.configDir);
422
442
  }
423
443
  return false;
424
444
  }
@@ -1,4 +1,9 @@
1
- import { AGENT_TARGETS, type AgentTarget, type InstallAction } from "./agents.ts";
1
+ import {
2
+ AGENT_TARGETS,
3
+ type AgentTarget,
4
+ type InstallAction,
5
+ MIRU_BUNX_PACKAGE,
6
+ } from "./agents.ts";
2
7
  import { removeTomlBlock, stripJsonComments } from "./config.ts";
3
8
 
4
9
  export const MCP_BENCHMARK_FLAG = "--benchmark";
@@ -7,9 +12,25 @@ const CODEX_MCP_HEADER = "[mcp_servers.miru]";
7
12
 
8
13
  function codexMcpBlock(enabled: boolean): string {
9
14
  const args = enabled
10
- ? `["@takara-ai/miru-code", "${MCP_BENCHMARK_FLAG}"]`
11
- : '["@takara-ai/miru-code"]';
12
- return `${CODEX_MCP_HEADER}\ncommand = "bunx"\nargs = ${args}\n`;
15
+ ? `["${MIRU_BUNX_PACKAGE}", "mcp", "${MCP_BENCHMARK_FLAG}"]`
16
+ : `["${MIRU_BUNX_PACKAGE}", "mcp"]`;
17
+ return `${CODEX_MCP_HEADER}\ncommand = "bunx"\nargs = ${args}\nstartup_timeout_sec = 60\n`;
18
+ }
19
+
20
+ function withMcpSubcommand(list: string[], command: unknown): string[] {
21
+ if (list.includes("mcp")) return list;
22
+ if (command === "miru") return ["mcp", ...list];
23
+
24
+ const packageIndex = list.findIndex((item) => /^@takara-ai\/miru-code(?:@.+)?$/.test(item));
25
+ if (packageIndex >= 0) {
26
+ return [...list.slice(0, packageIndex + 1), "mcp", ...list.slice(packageIndex + 1)];
27
+ }
28
+
29
+ const miruIndex = list.indexOf("miru");
30
+ if (miruIndex >= 0) {
31
+ return [...list.slice(0, miruIndex + 1), "mcp", ...list.slice(miruIndex + 1)];
32
+ }
33
+ return list;
13
34
  }
14
35
 
15
36
  export function withBenchmarkFlag(list: string[], enabled: boolean): string[] {
@@ -31,7 +52,7 @@ export function applyBenchmarkFlagToMcpEntry(
31
52
 
32
53
  if (Array.isArray(next.args)) {
33
54
  const args = next.args.filter((item): item is string => typeof item === "string");
34
- const updated = withBenchmarkFlag(args, enabled);
55
+ const updated = withBenchmarkFlag(withMcpSubcommand(args, next.command), enabled);
35
56
  if (JSON.stringify(updated) !== JSON.stringify(args)) {
36
57
  next.args = updated;
37
58
  changed = true;
@@ -40,7 +61,7 @@ export function applyBenchmarkFlagToMcpEntry(
40
61
 
41
62
  if (Array.isArray(next.command)) {
42
63
  const command = next.command.filter((item): item is string => typeof item === "string");
43
- const updated = withBenchmarkFlag(command, enabled);
64
+ const updated = withBenchmarkFlag(withMcpSubcommand(command, next.command), enabled);
44
65
  if (JSON.stringify(updated) !== JSON.stringify(command)) {
45
66
  next.command = updated;
46
67
  changed = true;
@@ -122,7 +143,10 @@ async function setTomlBenchmark(
122
143
  return "missing";
123
144
  }
124
145
  const current = await statusForToml(path);
125
- if (current === enabled) {
146
+ const section =
147
+ text.slice(text.indexOf(CODEX_MCP_HEADER) + CODEX_MCP_HEADER.length).split(/^\[/m)[0] ?? "";
148
+ const hasMcpArg = /^args\s*=\s*\[[^\n]*["']mcp["']/m.test(section);
149
+ if (current === enabled && hasMcpArg) {
126
150
  return "unchanged";
127
151
  }
128
152
 
@@ -190,9 +214,11 @@ async function setJsonBenchmark(
190
214
  }
191
215
 
192
216
  /** Inspect installed Miru MCP configs for `--benchmark`. */
193
- export async function getBenchmarkModeStatus(): Promise<BenchmarkModeTargetResult[]> {
217
+ export async function getBenchmarkModeStatus(
218
+ targets: readonly AgentTarget[] = AGENT_TARGETS,
219
+ ): Promise<BenchmarkModeTargetResult[]> {
194
220
  const results: BenchmarkModeTargetResult[] = [];
195
- for (const agent of AGENT_TARGETS) {
221
+ for (const agent of targets) {
196
222
  const mcp = agent.mcp;
197
223
  if (!mcp) {
198
224
  continue;
@@ -219,9 +245,12 @@ export async function getBenchmarkModeStatus(): Promise<BenchmarkModeTargetResul
219
245
  }
220
246
 
221
247
  /** Enable or disable `--benchmark` on every installed Miru MCP entry. */
222
- export async function setBenchmarkMode(enabled: boolean): Promise<BenchmarkModeTargetResult[]> {
248
+ export async function setBenchmarkMode(
249
+ enabled: boolean,
250
+ targets: readonly AgentTarget[] = AGENT_TARGETS,
251
+ ): Promise<BenchmarkModeTargetResult[]> {
223
252
  const results: BenchmarkModeTargetResult[] = [];
224
- for (const agent of AGENT_TARGETS) {
253
+ for (const agent of targets) {
225
254
  const mcp = agent.mcp;
226
255
  if (!mcp) {
227
256
  continue;
@@ -1,15 +1,17 @@
1
1
  import { unlink } from "node:fs/promises";
2
- import { type InstallAction, MIRU_END, MIRU_START } from "./agents.ts";
2
+ import { type InstallAction, MIRU_BUNX_PACKAGE, MIRU_END, MIRU_START } from "./agents.ts";
3
3
 
4
4
  const CODEX_MCP_HEADER = "[mcp_servers.miru]";
5
5
  const CODEX_MCP_BLOCK = `[mcp_servers.miru]
6
6
  command = "bunx"
7
- args = ["@takara-ai/miru-code"]
7
+ args = ["${MIRU_BUNX_PACKAGE}", "mcp"]
8
+ startup_timeout_sec = 60
8
9
  `;
9
10
  /** Same Codex block with `--benchmark` preserved across reinstall. */
10
11
  const CODEX_MCP_BLOCK_BENCHMARK = `[mcp_servers.miru]
11
12
  command = "bunx"
12
- args = ["@takara-ai/miru-code", "--benchmark"]
13
+ args = ["${MIRU_BUNX_PACKAGE}", "mcp", "--benchmark"]
14
+ startup_timeout_sec = 60
13
15
  `;
14
16
 
15
17
  /** Strip line and block comments so JSONC configs can be parsed. */
@@ -81,14 +83,16 @@ function sectionObject(
81
83
  root: Record<string, unknown>,
82
84
  sectionKey: string,
83
85
  ): Record<string, unknown> | "error" {
84
- const section = root[sectionKey];
85
- if (section === undefined) {
86
- return {};
87
- }
88
- if (!section || typeof section !== "object" || Array.isArray(section)) {
89
- return "error";
90
- }
91
- return section as Record<string, unknown>;
86
+ let section: unknown = root;
87
+ for (const key of sectionKey.split(".")) {
88
+ if (!section || typeof section !== "object" || Array.isArray(section)) return "error";
89
+ const child = (section as Record<string, unknown>)[key];
90
+ if (child === undefined) return {};
91
+ section = child;
92
+ }
93
+ return section && typeof section === "object" && !Array.isArray(section)
94
+ ? (section as Record<string, unknown>)
95
+ : "error";
92
96
  }
93
97
 
94
98
  export async function mergeJsonMember(
@@ -143,11 +147,12 @@ export async function removeJsonMember(
143
147
  }
144
148
  const nextText = removeJsonMemberText(text, sectionKey, memberKey);
145
149
  const nextObject = withRemovedMember(parsed, sectionKey, memberKey);
146
- const next = nextText ?? JSON.stringify(nextObject, null, 2);
147
- if (ensureTrailingNewline(next) === ensureTrailingNewline(text)) {
148
- return "not-found";
150
+ let next = nextText ?? JSON.stringify(nextObject, null, 2);
151
+ let nextParsed = parseJsonObject(next);
152
+ if (nextParsed === "error") {
153
+ next = JSON.stringify(nextObject, null, 2);
154
+ nextParsed = parseJsonObject(next);
149
155
  }
150
- const nextParsed = parseJsonObject(next);
151
156
  if (!nextParsed || typeof nextParsed !== "object" || Object.keys(nextParsed).length === 0) {
152
157
  await unlink(path);
153
158
  return "removed";
@@ -162,14 +167,24 @@ function withMergedMember(
162
167
  memberKey: string,
163
168
  value: Record<string, unknown>,
164
169
  ): Record<string, unknown> {
165
- const next = { ...root };
166
- const section =
167
- next[sectionKey] && typeof next[sectionKey] === "object" && !Array.isArray(next[sectionKey])
168
- ? { ...(next[sectionKey] as Record<string, unknown>) }
169
- : {};
170
- section[memberKey] = value;
171
- next[sectionKey] = section;
172
- return next;
170
+ const keys = sectionKey.split(".");
171
+ const update = (object: Record<string, unknown>, depth: number): Record<string, unknown> => {
172
+ const next = { ...object };
173
+ if (depth === keys.length) {
174
+ next[memberKey] = value;
175
+ return next;
176
+ }
177
+ const key = keys[depth];
178
+ if (!key) return next;
179
+ const child = next[key];
180
+ const section =
181
+ child && typeof child === "object" && !Array.isArray(child)
182
+ ? (child as Record<string, unknown>)
183
+ : {};
184
+ next[key] = update(section, depth + 1);
185
+ return next;
186
+ };
187
+ return update(root, 0);
173
188
  }
174
189
 
175
190
  function withRemovedMember(
@@ -177,21 +192,29 @@ function withRemovedMember(
177
192
  sectionKey: string,
178
193
  memberKey: string,
179
194
  ): Record<string, unknown> {
180
- const next = { ...root };
181
- const section =
182
- next[sectionKey] && typeof next[sectionKey] === "object" && !Array.isArray(next[sectionKey])
183
- ? { ...(next[sectionKey] as Record<string, unknown>) }
184
- : null;
185
- if (!section || !(memberKey in section)) {
186
- return next;
187
- }
188
- delete section[memberKey];
189
- if (Object.keys(section).length === 0) {
190
- delete next[sectionKey];
191
- } else {
192
- next[sectionKey] = section;
193
- }
194
- return next;
195
+ const keys = sectionKey.split(".");
196
+ const remove = (
197
+ object: Record<string, unknown>,
198
+ depth: number,
199
+ ): { value: Record<string, unknown>; changed: boolean } => {
200
+ const next = { ...object };
201
+ if (depth === keys.length) {
202
+ if (!(memberKey in next)) return { value: next, changed: false };
203
+ delete next[memberKey];
204
+ return { value: next, changed: true };
205
+ }
206
+ const key = keys[depth];
207
+ const child = key ? next[key] : undefined;
208
+ if (!key || !child || typeof child !== "object" || Array.isArray(child)) {
209
+ return { value: next, changed: false };
210
+ }
211
+ const result = remove(child as Record<string, unknown>, depth + 1);
212
+ if (!result.changed) return { value: next, changed: false };
213
+ if (Object.keys(result.value).length === 0) delete next[key];
214
+ else next[key] = result.value;
215
+ return { value: next, changed: true };
216
+ };
217
+ return remove(root, 0).value;
195
218
  }
196
219
 
197
220
  function upsertJsonMemberText(
@@ -292,13 +315,38 @@ function hasJsoncSyntax(text: string): boolean {
292
315
  return false;
293
316
  }
294
317
 
295
- /**
296
- * Locate a section object at the document root only.
297
- * Nested keys with the same name (e.g. Claude Code projects.*.mcpServers) are ignored.
298
- */
318
+ /** Locate an object section by dotted path, matching each key among direct children. */
299
319
  function findSectionRange(
300
320
  text: string,
301
321
  sectionKey: string,
322
+ ): { openBrace: number; closeBrace: number; indent: string } | null {
323
+ const keys = sectionKey.split(".");
324
+ let container = text;
325
+ let offset = 0;
326
+ let result: { openBrace: number; closeBrace: number; indent: string } | null = null;
327
+ for (let index = 0; index < keys.length; index++) {
328
+ const key = keys[index];
329
+ if (!key) return null;
330
+ result = findDirectSectionRange(container, key);
331
+ if (!result) return null;
332
+ if (index < keys.length - 1) {
333
+ offset += result.openBrace;
334
+ container = container.slice(result.openBrace, result.closeBrace + 1);
335
+ }
336
+ }
337
+ return result
338
+ ? {
339
+ ...result,
340
+ openBrace: offset + result.openBrace,
341
+ closeBrace: offset + result.closeBrace,
342
+ }
343
+ : null;
344
+ }
345
+
346
+ /** Locate a single child object in a root object, ignoring similarly named nested keys. */
347
+ function findDirectSectionRange(
348
+ text: string,
349
+ sectionKey: string,
302
350
  ): { openBrace: number; closeBrace: number; indent: string } | null {
303
351
  const keyPrefix = new RegExp(`^"${escapeRegExp(sectionKey)}"\\s*:\\s*\\{`);
304
352
  let depth = 0;
@@ -501,6 +549,21 @@ function ensureTrailingNewline(text: string): string {
501
549
  return text.endsWith("\n") ? text : `${text}\n`;
502
550
  }
503
551
 
552
+ /** Internal parser helpers exposed for focused edge-case tests. */
553
+ export const jsoncTestUtils = {
554
+ hasJsoncSyntax,
555
+ findSectionRange,
556
+ findMatchingBrace,
557
+ parseJsonValueEnd,
558
+ findMatchingBracket,
559
+ findStringEnd,
560
+ skipWhitespace,
561
+ findLeadingComma,
562
+ withRemovedMember,
563
+ upsertJsonMemberText,
564
+ removeJsonMemberText,
565
+ };
566
+
504
567
  export async function replaceOrAppendMarked(path: string, content: string): Promise<InstallAction> {
505
568
  const existed = await Bun.file(path).exists();
506
569
  const existing = existed ? await Bun.file(path).text() : "";