@takara-ai/miru-code 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -48
- package/docs/coverage-badge.json +6 -0
- package/grammars/manifest.json +10 -2
- package/grammars/tree-sitter-astro.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-php_only.wasm +0 -0
- package/grammars/tree-sitter-sql.wasm +0 -0
- package/grammars/tree-sitter-svelte.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/package.json +10 -2
- package/src/agents.ts +4 -4
- package/src/auth/device.ts +26 -21
- package/src/auth/providers.ts +26 -11
- package/src/benchmark/compare.ts +3 -10
- package/src/benchmark/grep.ts +64 -37
- package/src/benchmark/locate-compare.ts +8 -1
- package/src/benchmark/rg-literal.ts +9 -4
- package/src/cache.ts +3 -3
- package/src/chunking/ast.ts +21 -3
- package/src/chunking/grammars.ts +8 -3
- package/src/chunking/structural.ts +13 -24
- package/src/cli-mode.ts +18 -0
- package/src/cli.ts +10 -45
- package/src/credentials.ts +8 -13
- package/src/embeddings/openai.ts +6 -3
- package/src/embeddings/sagemaker.ts +18 -3
- package/src/embeddings/validate.ts +10 -7
- package/src/git.ts +6 -2
- package/src/help.ts +5 -4
- package/src/index/bm25.ts +66 -6
- package/src/index/create.ts +5 -9
- package/src/index/files.ts +5 -1
- package/src/index/incremental.ts +48 -8
- package/src/index/sparse.ts +14 -2
- package/src/installer/agents.ts +85 -65
- package/src/installer/benchmark-mode.ts +40 -11
- package/src/installer/config.ts +105 -42
- package/src/installer/installer.ts +59 -34
- package/src/installer/legacy-hooks.ts +131 -0
- package/src/installer/prompt.ts +20 -22
- package/src/installer/search-policy.ts +8 -8
- package/src/mcp/auth-tool.ts +93 -33
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +42 -22
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +54 -18
- package/src/prompt.ts +51 -14
- package/src/ranking/boosting.ts +5 -6
- package/src/ranking/location.ts +6 -10
- package/src/ranking/penalties.ts +14 -22
- package/src/search.ts +4 -12
- package/src/setup.ts +25 -13
- package/src/snippet.ts +8 -26
- package/src/spinner.ts +26 -16
- package/src/tokenizer/bert-wordpiece.ts +0 -6
- package/src/utils.ts +0 -4
- package/src/version.ts +4 -4
- package/src/installer/hooks/install.ts +0 -549
- package/src/installer/hooks/opencode-plugin.ts +0 -54
- package/src/installer/hooks/search-guard.ts +0 -272
package/src/installer/agents.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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: ["
|
|
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: ["
|
|
90
|
+
args: [MIRU_BUNX_PACKAGE, "mcp"],
|
|
101
91
|
};
|
|
102
92
|
|
|
103
93
|
const OPENCODE_SERVER_CONFIG: Record<string, unknown> = {
|
|
104
|
-
command: ["bunx", "
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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(
|
|
391
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
441
|
+
return (dependencies.existsSync ?? existsSync)(agent.configDir);
|
|
422
442
|
}
|
|
423
443
|
return false;
|
|
424
444
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
? `["
|
|
11
|
-
:
|
|
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
|
-
|
|
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(
|
|
217
|
+
export async function getBenchmarkModeStatus(
|
|
218
|
+
targets: readonly AgentTarget[] = AGENT_TARGETS,
|
|
219
|
+
): Promise<BenchmarkModeTargetResult[]> {
|
|
194
220
|
const results: BenchmarkModeTargetResult[] = [];
|
|
195
|
-
for (const agent of
|
|
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(
|
|
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
|
|
253
|
+
for (const agent of targets) {
|
|
225
254
|
const mcp = agent.mcp;
|
|
226
255
|
if (!mcp) {
|
|
227
256
|
continue;
|
package/src/installer/config.ts
CHANGED
|
@@ -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 = ["
|
|
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 = ["
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
return section
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
166
|
-
const
|
|
167
|
-
next
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
next[
|
|
193
|
-
|
|
194
|
-
|
|
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() : "";
|