@takara-ai/miru-code 1.1.3 → 1.2.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 +178 -107
- package/package.json +9 -5
- package/src/benchmark/compare.ts +278 -0
- package/src/benchmark/grep.ts +352 -0
- package/src/benchmark/history.ts +206 -0
- package/src/benchmark/locate-compare.ts +65 -0
- package/src/benchmark/rg-literal.ts +143 -0
- package/src/benchmark/summary.ts +29 -0
- package/src/benchmark/types.ts +65 -0
- package/src/cli.ts +183 -4
- package/src/credentials.ts +8 -0
- package/src/help.ts +45 -2
- package/src/index/create.ts +2 -2
- package/src/index/incremental.ts +4 -3
- package/src/index.ts +3 -1
- package/src/installer/benchmark-mode.ts +248 -0
- package/src/installer/config.ts +14 -2
- package/src/installer/hooks/opencode-plugin.ts +6 -20
- package/src/installer/hooks/search-guard.ts +4 -5
- package/src/installer/installer.ts +58 -5
- package/src/installer/search-policy.ts +28 -9
- package/src/literal.ts +139 -0
- package/src/mcp/index-cache.ts +2 -2
- package/src/mcp/serve.ts +4 -3
- package/src/mcp/server.ts +166 -9
- package/src/miru-index.ts +15 -6
- package/src/snippet.ts +6 -3
- package/src/token-count.ts +62 -0
- package/src/tokenizer/bert-wordpiece.ts +219 -0
- package/src/tokenizer/load.ts +14 -0
- package/src/types.ts +8 -0
- package/src/utils.ts +8 -1
- package/tokenizer/tokenizer.json +1 -0
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { type AgentId, writeAgentFile } from "./agents.ts";
|
|
3
|
+
import { clearBenchmarkHistory, resolveBenchmarkHistoryPath } from "./benchmark/history.ts";
|
|
3
4
|
import { clearCache } from "./cache.ts";
|
|
4
5
|
import {
|
|
5
6
|
fail,
|
|
@@ -7,8 +8,10 @@ import {
|
|
|
7
8
|
formatSearchErrorPretty,
|
|
8
9
|
formatSearchResultsPretty,
|
|
9
10
|
hint,
|
|
11
|
+
info,
|
|
10
12
|
prefersJsonOutput,
|
|
11
13
|
success,
|
|
14
|
+
writeStdout,
|
|
12
15
|
} from "./cli-ui.ts";
|
|
13
16
|
import { loadStoredCredentials } from "./credentials.ts";
|
|
14
17
|
import { normalizeTakaraApiKeyEnv } from "./env.ts";
|
|
@@ -20,9 +23,11 @@ import {
|
|
|
20
23
|
printFullHelp,
|
|
21
24
|
printMainHelp,
|
|
22
25
|
} from "./help.ts";
|
|
26
|
+
import { getBenchmarkModeStatus, setBenchmarkMode } from "./installer/benchmark-mode.ts";
|
|
23
27
|
import { runSearchGuardFromStdin } from "./installer/hooks/search-guard.ts";
|
|
24
28
|
import { runInstaller } from "./installer/installer.ts";
|
|
25
29
|
import { promptConfirm } from "./installer/prompt.ts";
|
|
30
|
+
import { formatLiteralLocate, type LiteralMode } from "./literal.ts";
|
|
26
31
|
import { serveMcp } from "./mcp/serve.ts";
|
|
27
32
|
import { MiruIndex } from "./miru-index.ts";
|
|
28
33
|
import {
|
|
@@ -34,6 +39,8 @@ import {
|
|
|
34
39
|
import { withSpinner } from "./spinner.ts";
|
|
35
40
|
import type { ContentType, SearchResult } from "./types.ts";
|
|
36
41
|
import {
|
|
42
|
+
DEFAULT_EXPAND_AFTER,
|
|
43
|
+
DEFAULT_EXPAND_BEFORE,
|
|
37
44
|
expandChunksAtLine,
|
|
38
45
|
formatExpandResults,
|
|
39
46
|
formatResults,
|
|
@@ -52,6 +59,7 @@ await loadStoredCredentials();
|
|
|
52
59
|
|
|
53
60
|
const CLI_COMMANDS = new Set([
|
|
54
61
|
"search",
|
|
62
|
+
"locate",
|
|
55
63
|
"expand",
|
|
56
64
|
"find-related",
|
|
57
65
|
"init",
|
|
@@ -59,6 +67,7 @@ const CLI_COMMANDS = new Set([
|
|
|
59
67
|
"uninstall",
|
|
60
68
|
"setup",
|
|
61
69
|
"clear",
|
|
70
|
+
"benchmark",
|
|
62
71
|
"hook-guard",
|
|
63
72
|
"help",
|
|
64
73
|
"-h",
|
|
@@ -104,7 +113,7 @@ function parseContentArgv(argv: string[]): { content: ContentType[]; rest: strin
|
|
|
104
113
|
rest.push(arg);
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
|
-
return { content: resolveContent(content
|
|
116
|
+
return { content: resolveContent(content), rest };
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
function parseTopK(argv: string[]): { topK: number; rest: string[] } {
|
|
@@ -250,6 +259,53 @@ class RelatedChunkNotFoundError extends Error {
|
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
async function runLocate(
|
|
263
|
+
path: string,
|
|
264
|
+
literal: string,
|
|
265
|
+
content: ContentType[],
|
|
266
|
+
jsonFlag: boolean,
|
|
267
|
+
mode: LiteralMode,
|
|
268
|
+
limit: number | undefined,
|
|
269
|
+
ignoreCase: boolean,
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
await ensureCredentials({ interactive: true });
|
|
272
|
+
|
|
273
|
+
const payload = await withSpinner("Locating literal", async () => {
|
|
274
|
+
const built = await MiruIndex.fromSource(path, content);
|
|
275
|
+
await built.saveToCache(path);
|
|
276
|
+
return formatLiteralLocate(
|
|
277
|
+
built.locateLiteral(literal, {
|
|
278
|
+
mode,
|
|
279
|
+
ignore_case: ignoreCase,
|
|
280
|
+
...(limit != null ? { limit } : {}),
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (prefersJsonOutput(jsonFlag)) {
|
|
286
|
+
console.log(JSON.stringify(payload));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const n = Number(payload.n ?? 0);
|
|
291
|
+
const files = Number(payload.files ?? 0);
|
|
292
|
+
writeStdout(`literal=${literal} n=${n} files=${files} mode=${mode}`);
|
|
293
|
+
const hits = payload.hits as Array<{ f: string; l: number; t?: string }> | undefined;
|
|
294
|
+
if (hits) {
|
|
295
|
+
for (const hit of hits) {
|
|
296
|
+
if (hit.t !== undefined) {
|
|
297
|
+
writeStdout(` ${hit.f}:${hit.l}: ${hit.t}`);
|
|
298
|
+
} else {
|
|
299
|
+
writeStdout(` ${hit.f}:${hit.l}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (payload.truncated) {
|
|
303
|
+
writeStdout(` … truncated (showing ${hits.length} of ${n})`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
writeStdout("");
|
|
307
|
+
}
|
|
308
|
+
|
|
253
309
|
async function runInit(agent: AgentId, force: boolean): Promise<void> {
|
|
254
310
|
try {
|
|
255
311
|
const dest = await writeAgentFile(agent, { force });
|
|
@@ -267,6 +323,75 @@ async function runClear(path: string): Promise<void> {
|
|
|
267
323
|
success(`Cleared cached index for ${path}`);
|
|
268
324
|
}
|
|
269
325
|
|
|
326
|
+
async function runBenchmarkCommand(rest: string[]): Promise<void> {
|
|
327
|
+
const action = rest[0];
|
|
328
|
+
if (!action || action === "-h" || action === "--help") {
|
|
329
|
+
printCommandHelp("benchmark");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (action === "status") {
|
|
334
|
+
const rows = await getBenchmarkModeStatus();
|
|
335
|
+
const installed = rows.filter((row) => row.action !== "missing");
|
|
336
|
+
if (installed.length === 0) {
|
|
337
|
+
info("No Miru MCP installs found. Run `miru install` first.");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const row of installed) {
|
|
341
|
+
writeStdout(` ${row.enabled ? "on " : "off"} ${row.agent.padEnd(16)} ${row.path}`);
|
|
342
|
+
}
|
|
343
|
+
const onCount = installed.filter((row) => row.enabled).length;
|
|
344
|
+
writeStdout("");
|
|
345
|
+
if (onCount > 0) {
|
|
346
|
+
hint(
|
|
347
|
+
`Benchmark mode on for ${onCount}/${installed.length}. Run \`miru benchmark off\` to leave.`,
|
|
348
|
+
);
|
|
349
|
+
} else {
|
|
350
|
+
hint("Benchmark mode is off for all installed agents.");
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (action === "on" || action === "off") {
|
|
356
|
+
const enabled = action === "on";
|
|
357
|
+
const rows = await setBenchmarkMode(enabled);
|
|
358
|
+
const touched = rows.filter((row) => row.action === "updated" || row.action === "unchanged");
|
|
359
|
+
if (touched.length === 0) {
|
|
360
|
+
info("No Miru MCP installs found. Run `miru install` first.");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
for (const row of touched) {
|
|
364
|
+
const label = row.action === "updated" ? (enabled ? "enabled" : "disabled") : "unchanged";
|
|
365
|
+
writeStdout(` ${label.padEnd(9)} ${row.agent.padEnd(16)} ${row.path}`);
|
|
366
|
+
}
|
|
367
|
+
writeStdout("");
|
|
368
|
+
success(
|
|
369
|
+
enabled
|
|
370
|
+
? "Benchmark mode on. Restart agents to apply."
|
|
371
|
+
: "Benchmark mode off. Restart agents to apply.",
|
|
372
|
+
);
|
|
373
|
+
if (enabled) {
|
|
374
|
+
hint("Leave anytime with `miru benchmark off`.");
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (action === "clear") {
|
|
380
|
+
const path = resolveBenchmarkHistoryPath();
|
|
381
|
+
const result = await clearBenchmarkHistory(path);
|
|
382
|
+
if (result.cleared) {
|
|
383
|
+
success(`Cleared benchmark report ${result.path}`);
|
|
384
|
+
} else {
|
|
385
|
+
info(`No benchmark report at ${result.path}`);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fail(`Unknown benchmark action "${action}". Use on, off, status, or clear.`);
|
|
391
|
+
printCommandHelp("benchmark");
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
|
|
270
395
|
async function runCli(argv: string[]): Promise<void> {
|
|
271
396
|
const [command, ...rest] = argv;
|
|
272
397
|
|
|
@@ -304,6 +429,11 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
304
429
|
return;
|
|
305
430
|
}
|
|
306
431
|
|
|
432
|
+
if (command === "benchmark") {
|
|
433
|
+
await runBenchmarkCommand(rest);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
307
437
|
if (command === "init") {
|
|
308
438
|
let agent: AgentId | undefined;
|
|
309
439
|
let force = false;
|
|
@@ -392,6 +522,49 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
392
522
|
return;
|
|
393
523
|
}
|
|
394
524
|
|
|
525
|
+
if (command === "locate") {
|
|
526
|
+
const literal = sizedRest[0];
|
|
527
|
+
if (!literal) {
|
|
528
|
+
printCommandHelp("locate");
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
let mode: LiteralMode = "lines";
|
|
532
|
+
let limit: number | undefined;
|
|
533
|
+
let ignoreCase = false;
|
|
534
|
+
const pathArgs: string[] = [];
|
|
535
|
+
for (let i = 1; i < sizedRest.length; i++) {
|
|
536
|
+
const arg = sizedRest[i];
|
|
537
|
+
if (arg === "--mode" && sizedRest[i + 1]) {
|
|
538
|
+
const value = sizedRest[++i];
|
|
539
|
+
if (value !== "count" && value !== "locations" && value !== "lines") {
|
|
540
|
+
fail(`Unknown locate mode "${value}". Use count, locations, or lines.`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
mode = value;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (arg === "--limit" && sizedRest[i + 1]) {
|
|
547
|
+
const raw = Number(sizedRest[++i]);
|
|
548
|
+
if (!Number.isFinite(raw) || raw < 1) {
|
|
549
|
+
fail("locate --limit must be a positive integer.");
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
limit = Math.floor(raw);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (arg === "--ignore-case") {
|
|
556
|
+
ignoreCase = true;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (arg !== undefined) {
|
|
560
|
+
pathArgs.push(arg);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const path = resolveSearchPath(pathArgs[0] ?? process.cwd());
|
|
564
|
+
await runLocate(path, literal, content, jsonFlag, mode, limit, ignoreCase);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
395
568
|
if (command === "expand") {
|
|
396
569
|
const filePath = sizedRest[0];
|
|
397
570
|
const lineRaw = sizedRest[1];
|
|
@@ -401,8 +574,8 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
401
574
|
}
|
|
402
575
|
const line = Number(lineRaw);
|
|
403
576
|
const path = resolveSearchPath(sizedRest[2] ?? process.cwd());
|
|
404
|
-
let before =
|
|
405
|
-
let after =
|
|
577
|
+
let before = DEFAULT_EXPAND_BEFORE;
|
|
578
|
+
let after = DEFAULT_EXPAND_AFTER;
|
|
406
579
|
for (let i = 3; i < sizedRest.length; i++) {
|
|
407
580
|
const arg = sizedRest[i];
|
|
408
581
|
if (arg === "--before" && sizedRest[i + 1]) {
|
|
@@ -435,10 +608,15 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
435
608
|
|
|
436
609
|
async function runMcp(argv: string[]): Promise<void> {
|
|
437
610
|
let ref: string | null = null;
|
|
611
|
+
let benchmark = false;
|
|
438
612
|
const contentTokens: string[] = [];
|
|
439
613
|
|
|
440
614
|
for (let i = 0; i < argv.length; i++) {
|
|
441
615
|
const arg = argv[i];
|
|
616
|
+
if (arg === "--benchmark") {
|
|
617
|
+
benchmark = true;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
442
620
|
if (arg === "--ref" && argv[i + 1]) {
|
|
443
621
|
ref = argv[++i] ?? null;
|
|
444
622
|
continue;
|
|
@@ -459,7 +637,8 @@ async function runMcp(argv: string[]): Promise<void> {
|
|
|
459
637
|
|
|
460
638
|
await serveMcp({
|
|
461
639
|
ref,
|
|
462
|
-
content: resolveContent(contentTokens
|
|
640
|
+
content: resolveContent(contentTokens),
|
|
641
|
+
benchmark,
|
|
463
642
|
});
|
|
464
643
|
}
|
|
465
644
|
|
package/src/credentials.ts
CHANGED
|
@@ -27,6 +27,14 @@ export function resolveCredentialsDir(): string {
|
|
|
27
27
|
return join(xdg, "miru");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Global Miru state directory (credentials, benchmark history, etc.).
|
|
32
|
+
* Same location as `resolveCredentialsDir()`; prefer this name for non-secret files.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveMiruStateDir(): string {
|
|
35
|
+
return resolveCredentialsDir();
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
export function resolveCredentialsPath(): string {
|
|
31
39
|
return join(resolveCredentialsDir(), CREDENTIALS_FILENAME);
|
|
32
40
|
}
|
package/src/help.ts
CHANGED
|
@@ -34,11 +34,13 @@ export function printMainHelp(): void {
|
|
|
34
34
|
|
|
35
35
|
section("Commands");
|
|
36
36
|
commandRow("search", "Hybrid search over a codebase");
|
|
37
|
+
commandRow("locate", "Exact substring location in the index");
|
|
37
38
|
commandRow("expand", "Adjacent chunks in the same file as a hit");
|
|
38
39
|
commandRow("find-related", "Find chunks related to a file:line");
|
|
39
40
|
commandRow("setup", "Save your Takara API key locally");
|
|
40
41
|
commandRow("install", "Configure miru across coding agents");
|
|
41
42
|
commandRow("uninstall", "Remove miru agent configuration");
|
|
43
|
+
commandRow("benchmark", "Turn MCP benchmark mode on or off");
|
|
42
44
|
commandRow("init", "Write a project-local sub-agent file");
|
|
43
45
|
commandRow("clear", "Remove cached index for a path");
|
|
44
46
|
commandRow("help", "Show help for a command");
|
|
@@ -62,6 +64,8 @@ export function printEnvHelp(): void {
|
|
|
62
64
|
writeStdout(` Default: ${DEFAULT_EMBEDDING_MODEL}`);
|
|
63
65
|
writeStdout(" MIRU_CONCURRENCY");
|
|
64
66
|
writeStdout(` Parallel workers (default: CPUs − ${DEFAULT_RESERVE_CORES})`);
|
|
67
|
+
writeStdout(" MIRU_TOKENIZER_JSON");
|
|
68
|
+
writeStdout(" Path to tokenizer.json (default: <package>/tokenizer/tokenizer.json)");
|
|
65
69
|
writeStdout("");
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -78,12 +82,26 @@ export function printCommandHelp(command: string): void {
|
|
|
78
82
|
writeStdout(" miru search <query> [path] [options]");
|
|
79
83
|
section("Options");
|
|
80
84
|
writeStdout(" -k, --top-k N Number of results (default: 5)");
|
|
81
|
-
writeStdout(" --content TYPE code | docs | config | all");
|
|
85
|
+
writeStdout(" --content TYPE code | docs | config | all (default: code config)");
|
|
82
86
|
writeStdout(" --json JSON output (default when piped)");
|
|
83
87
|
section("Example");
|
|
84
88
|
writeStdout(' miru search "where is auth" ./src -k 10 --content code docs');
|
|
85
89
|
writeStdout("");
|
|
86
90
|
return;
|
|
91
|
+
case "locate":
|
|
92
|
+
commandHeader("locate", "Exact substring location over the Miru index.");
|
|
93
|
+
section("Usage");
|
|
94
|
+
writeStdout(" miru locate <literal> [path] [options]");
|
|
95
|
+
section("Options");
|
|
96
|
+
writeStdout(" --mode MODE count | locations | lines (default: lines)");
|
|
97
|
+
writeStdout(" --limit N Optional hit cap (default: all matches)");
|
|
98
|
+
writeStdout(" --ignore-case Case-insensitive match");
|
|
99
|
+
writeStdout(" --content TYPE code | docs | config | all (default: code config)");
|
|
100
|
+
writeStdout(" --json JSON output (default when piped)");
|
|
101
|
+
section("Example");
|
|
102
|
+
writeStdout(" miru locate MIRU_BENCHMARK_HISTORY_PATH . --mode locations");
|
|
103
|
+
writeStdout("");
|
|
104
|
+
return;
|
|
87
105
|
case "expand":
|
|
88
106
|
commandHeader("expand", "More context in the same file as a search hit.");
|
|
89
107
|
section("Usage");
|
|
@@ -121,6 +139,28 @@ export function printCommandHelp(command: string): void {
|
|
|
121
139
|
case "uninstall":
|
|
122
140
|
commandHeader("uninstall", "Remove miru configuration from agents.");
|
|
123
141
|
writeStdout("Removes MCP entries, marked instruction blocks, and global sub-agents.");
|
|
142
|
+
writeStdout("Also deletes the global benchmark report from Miru's state directory.");
|
|
143
|
+
writeStdout("");
|
|
144
|
+
return;
|
|
145
|
+
case "benchmark":
|
|
146
|
+
commandHeader("benchmark", "Toggle MCP benchmark mode on installed agents.");
|
|
147
|
+
section("Usage");
|
|
148
|
+
writeStdout(" miru benchmark on");
|
|
149
|
+
writeStdout(" miru benchmark off");
|
|
150
|
+
writeStdout(" miru benchmark status");
|
|
151
|
+
writeStdout(" miru benchmark clear");
|
|
152
|
+
writeStdout("");
|
|
153
|
+
writeStdout("Adds or removes `--benchmark` from Miru MCP args in agent configs.");
|
|
154
|
+
writeStdout("If `--benchmark` is present, benchmark mode is on — no env overrides.");
|
|
155
|
+
writeStdout("Restart agents after changing mode. Prefer `off` when finished measuring.");
|
|
156
|
+
writeStdout("");
|
|
157
|
+
writeStdout("History is a global report (not per-repo) under Miru's state directory.");
|
|
158
|
+
writeStdout("Override path with MIRU_BENCHMARK_HISTORY_PATH. Append-only JSONL of savings.");
|
|
159
|
+
writeStdout("Repo paths in the report are plaintext — use `clear` on shared machines.");
|
|
160
|
+
writeStdout("");
|
|
161
|
+
writeStdout("Savings compare Miru workflow tokens to a Grep baseline");
|
|
162
|
+
writeStdout("(rg search + read of the top matched file), not the agent's full tool chain.");
|
|
163
|
+
writeStdout("Local paths only — git URL repos skip comparison with benchmark_skipped.");
|
|
124
164
|
writeStdout("");
|
|
125
165
|
return;
|
|
126
166
|
case "init":
|
|
@@ -145,9 +185,12 @@ export function printCommandHelp(command: string): void {
|
|
|
145
185
|
case "mcp":
|
|
146
186
|
commandHeader("mcp", "Stdio MCP server (default with no subcommand).");
|
|
147
187
|
section("Usage");
|
|
148
|
-
writeStdout(" miru [--ref BRANCH] [--content TYPE ...]");
|
|
188
|
+
writeStdout(" miru [--ref BRANCH] [--content TYPE ...] [--benchmark]");
|
|
189
|
+
writeStdout(" Default content: code config");
|
|
149
190
|
writeStdout("");
|
|
150
191
|
writeStdout("Indexes on the first search/expand/find_related tool call (repo argument).");
|
|
192
|
+
writeStdout("Use --benchmark (or `miru benchmark on`) for token-savings comparisons.");
|
|
193
|
+
writeStdout("Leave with `miru benchmark off` and restart the agent.");
|
|
151
194
|
writeStdout("");
|
|
152
195
|
return;
|
|
153
196
|
default:
|
package/src/index/create.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { EmbeddingBackend } from "../embeddings/openai.ts";
|
|
|
6
6
|
import { envOptionalInt } from "../env.ts";
|
|
7
7
|
import { searchImprovementsEnabled } from "../ranking/features.ts";
|
|
8
8
|
import { tokenize } from "../tokens.ts";
|
|
9
|
-
import type
|
|
9
|
+
import { type Chunk, type ContentType, defaultContentTypes } from "../types.ts";
|
|
10
10
|
import { BM25Index } from "./bm25.ts";
|
|
11
11
|
import { loadRootEntryChunks } from "./entry-chunks.ts";
|
|
12
12
|
import { walkFiles } from "./file-walker.ts";
|
|
@@ -33,7 +33,7 @@ function resolveMaxIndexFiles(): number | undefined {
|
|
|
33
33
|
export async function createIndexFromPath(
|
|
34
34
|
path: string,
|
|
35
35
|
embeddings: EmbeddingBackend,
|
|
36
|
-
content: ContentType[] =
|
|
36
|
+
content: ContentType[] = defaultContentTypes(),
|
|
37
37
|
displayRoot?: string,
|
|
38
38
|
): Promise<{ bm25: BM25Index; semantic: SemanticIndex; chunks: Chunk[] }> {
|
|
39
39
|
const profile = process.env.MIRU_PROFILE === "1";
|
package/src/index/incremental.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, relative, resolve } from "node:path";
|
|
1
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
2
|
import { chunkSource } from "../chunking/chunking.ts";
|
|
3
3
|
import type { EmbeddingBackend } from "../embeddings/openai.ts";
|
|
4
4
|
import { tokenize } from "../tokens.ts";
|
|
@@ -123,8 +123,9 @@ function rebuildBm25(chunks: Chunk[]): BM25Index {
|
|
|
123
123
|
export function relativePathFromRoot(root: string, absoluteOrRelative: string): string {
|
|
124
124
|
const resolvedRoot = resolve(root);
|
|
125
125
|
const candidate = resolve(resolvedRoot, absoluteOrRelative);
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const rel = normalizeRelativePath(relative(resolvedRoot, candidate));
|
|
127
|
+
if (rel === "" || (!rel.startsWith("../") && rel !== ".." && !isAbsolute(rel))) {
|
|
128
|
+
return rel;
|
|
128
129
|
}
|
|
129
130
|
return normalizeRelativePath(absoluteOrRelative);
|
|
130
131
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,8 +18,10 @@ export { MiruIndex } from "./miru-index.ts";
|
|
|
18
18
|
export type {
|
|
19
19
|
/** A searchable text span from a source file (path, line range, content). */
|
|
20
20
|
Chunk,
|
|
21
|
-
/** What to index: source code,
|
|
21
|
+
/** What to index: source code, docs, and/or config. */
|
|
22
22
|
ContentType,
|
|
23
23
|
/** Ranked hit returned by `search` or `findRelated`. */
|
|
24
24
|
SearchResult,
|
|
25
25
|
} from "./types.ts";
|
|
26
|
+
|
|
27
|
+
export { DEFAULT_CONTENT_TYPES, defaultContentTypes } from "./types.ts";
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { AGENT_TARGETS, type AgentTarget, type InstallAction } from "./agents.ts";
|
|
2
|
+
import { removeTomlBlock, stripJsonComments } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
export const MCP_BENCHMARK_FLAG = "--benchmark";
|
|
5
|
+
|
|
6
|
+
const CODEX_MCP_HEADER = "[mcp_servers.miru]";
|
|
7
|
+
|
|
8
|
+
function codexMcpBlock(enabled: boolean): string {
|
|
9
|
+
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`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function withBenchmarkFlag(list: string[], enabled: boolean): string[] {
|
|
16
|
+
const without = list.filter((item) => item !== MCP_BENCHMARK_FLAG);
|
|
17
|
+
return enabled ? [...without, MCP_BENCHMARK_FLAG] : without;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listHasBenchmarkFlag(list: unknown): boolean {
|
|
21
|
+
return Array.isArray(list) && list.some((item) => item === MCP_BENCHMARK_FLAG);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Toggle `--benchmark` on a JSON MCP server entry (`args` and/or array `command`). */
|
|
25
|
+
export function applyBenchmarkFlagToMcpEntry(
|
|
26
|
+
entry: Record<string, unknown>,
|
|
27
|
+
enabled: boolean,
|
|
28
|
+
): { entry: Record<string, unknown>; changed: boolean; enabled: boolean } {
|
|
29
|
+
const next: Record<string, unknown> = { ...entry };
|
|
30
|
+
let changed = false;
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(next.args)) {
|
|
33
|
+
const args = next.args.filter((item): item is string => typeof item === "string");
|
|
34
|
+
const updated = withBenchmarkFlag(args, enabled);
|
|
35
|
+
if (JSON.stringify(updated) !== JSON.stringify(args)) {
|
|
36
|
+
next.args = updated;
|
|
37
|
+
changed = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(next.command)) {
|
|
42
|
+
const command = next.command.filter((item): item is string => typeof item === "string");
|
|
43
|
+
const updated = withBenchmarkFlag(command, enabled);
|
|
44
|
+
if (JSON.stringify(updated) !== JSON.stringify(command)) {
|
|
45
|
+
next.command = updated;
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
entry: next,
|
|
52
|
+
changed,
|
|
53
|
+
enabled: mcpEntryHasBenchmark(next),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mcpEntryHasBenchmark(entry: Record<string, unknown>): boolean {
|
|
58
|
+
return listHasBenchmarkFlag(entry.args) || listHasBenchmarkFlag(entry.command);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Keep `--benchmark` on the canonical install entry when the existing MCP
|
|
63
|
+
* entry already had it (so `miru install` does not silently turn mode off).
|
|
64
|
+
*/
|
|
65
|
+
export function withPreservedBenchmarkFlag(
|
|
66
|
+
canonical: Record<string, unknown>,
|
|
67
|
+
existing: Record<string, unknown> | null | undefined,
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
if (!existing || !mcpEntryHasBenchmark(existing)) {
|
|
70
|
+
return canonical;
|
|
71
|
+
}
|
|
72
|
+
return applyBenchmarkFlagToMcpEntry(canonical, true).entry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface BenchmarkModeTargetResult {
|
|
76
|
+
agent: string;
|
|
77
|
+
path: string;
|
|
78
|
+
action: InstallAction | "enabled" | "disabled" | "missing";
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readJsonObject(path: string): Promise<Record<string, unknown> | null> {
|
|
83
|
+
if (!(await Bun.file(path).exists())) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const text = stripJsonComments(await Bun.file(path).text());
|
|
88
|
+
const parsed = JSON.parse(text) as unknown;
|
|
89
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return parsed as Record<string, unknown>;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeJsonObject(path: string, value: Record<string, unknown>): Promise<void> {
|
|
99
|
+
await Bun.write(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function statusForToml(path: string): Promise<boolean | null> {
|
|
103
|
+
if (!(await Bun.file(path).exists())) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const text = await Bun.file(path).text();
|
|
107
|
+
if (!text.includes(CODEX_MCP_HEADER)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return text.includes(`"${MCP_BENCHMARK_FLAG}"`) || text.includes(`'${MCP_BENCHMARK_FLAG}'`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function setTomlBenchmark(
|
|
114
|
+
path: string,
|
|
115
|
+
enabled: boolean,
|
|
116
|
+
): Promise<InstallAction | "missing"> {
|
|
117
|
+
if (!(await Bun.file(path).exists())) {
|
|
118
|
+
return "missing";
|
|
119
|
+
}
|
|
120
|
+
const text = await Bun.file(path).text();
|
|
121
|
+
if (!text.includes(CODEX_MCP_HEADER)) {
|
|
122
|
+
return "missing";
|
|
123
|
+
}
|
|
124
|
+
const current = await statusForToml(path);
|
|
125
|
+
if (current === enabled) {
|
|
126
|
+
return "unchanged";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Replace the miru block with the desired args, preserving the rest of the file.
|
|
130
|
+
await removeTomlBlock(path);
|
|
131
|
+
const existed = await Bun.file(path).exists();
|
|
132
|
+
const existing = existed ? await Bun.file(path).text() : "";
|
|
133
|
+
const base = existing.replace(/\n+$/, "");
|
|
134
|
+
const block = codexMcpBlock(enabled);
|
|
135
|
+
const next = base.length > 0 ? `${base}\n\n${block}` : block;
|
|
136
|
+
await Bun.write(path, next.endsWith("\n") ? next : `${next}\n`);
|
|
137
|
+
return "updated";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function statusForJson(agent: AgentTarget): Promise<boolean | null> {
|
|
141
|
+
const mcp = agent.mcp;
|
|
142
|
+
if (mcp?.format !== "json") {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const parsed = await readJsonObject(mcp.path);
|
|
146
|
+
if (!parsed) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const section = parsed[mcp.key];
|
|
150
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const entry = (section as Record<string, unknown>)[mcp.memberKey];
|
|
154
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return mcpEntryHasBenchmark(entry as Record<string, unknown>);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function setJsonBenchmark(
|
|
161
|
+
agent: AgentTarget,
|
|
162
|
+
enabled: boolean,
|
|
163
|
+
): Promise<InstallAction | "missing"> {
|
|
164
|
+
const mcp = agent.mcp;
|
|
165
|
+
if (mcp?.format !== "json") {
|
|
166
|
+
return "missing";
|
|
167
|
+
}
|
|
168
|
+
const parsed = await readJsonObject(mcp.path);
|
|
169
|
+
if (!parsed) {
|
|
170
|
+
return "missing";
|
|
171
|
+
}
|
|
172
|
+
const sectionRaw = parsed[mcp.key];
|
|
173
|
+
if (!sectionRaw || typeof sectionRaw !== "object" || Array.isArray(sectionRaw)) {
|
|
174
|
+
return "missing";
|
|
175
|
+
}
|
|
176
|
+
const section = sectionRaw as Record<string, unknown>;
|
|
177
|
+
const entryRaw = section[mcp.memberKey];
|
|
178
|
+
if (!entryRaw || typeof entryRaw !== "object" || Array.isArray(entryRaw)) {
|
|
179
|
+
return "missing";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const applied = applyBenchmarkFlagToMcpEntry(entryRaw as Record<string, unknown>, enabled);
|
|
183
|
+
if (!applied.changed) {
|
|
184
|
+
return "unchanged";
|
|
185
|
+
}
|
|
186
|
+
section[mcp.memberKey] = applied.entry;
|
|
187
|
+
parsed[mcp.key] = section;
|
|
188
|
+
await writeJsonObject(mcp.path, parsed);
|
|
189
|
+
return "updated";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Inspect installed Miru MCP configs for `--benchmark`. */
|
|
193
|
+
export async function getBenchmarkModeStatus(): Promise<BenchmarkModeTargetResult[]> {
|
|
194
|
+
const results: BenchmarkModeTargetResult[] = [];
|
|
195
|
+
for (const agent of AGENT_TARGETS) {
|
|
196
|
+
const mcp = agent.mcp;
|
|
197
|
+
if (!mcp) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (mcp.format === "toml") {
|
|
201
|
+
const enabled = await statusForToml(mcp.path);
|
|
202
|
+
results.push({
|
|
203
|
+
agent: agent.displayName,
|
|
204
|
+
path: mcp.path,
|
|
205
|
+
action: enabled == null ? "missing" : enabled ? "enabled" : "disabled",
|
|
206
|
+
enabled: enabled === true,
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const enabled = await statusForJson(agent);
|
|
211
|
+
results.push({
|
|
212
|
+
agent: agent.displayName,
|
|
213
|
+
path: mcp.path,
|
|
214
|
+
action: enabled == null ? "missing" : enabled ? "enabled" : "disabled",
|
|
215
|
+
enabled: enabled === true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Enable or disable `--benchmark` on every installed Miru MCP entry. */
|
|
222
|
+
export async function setBenchmarkMode(enabled: boolean): Promise<BenchmarkModeTargetResult[]> {
|
|
223
|
+
const results: BenchmarkModeTargetResult[] = [];
|
|
224
|
+
for (const agent of AGENT_TARGETS) {
|
|
225
|
+
const mcp = agent.mcp;
|
|
226
|
+
if (!mcp) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (mcp.format === "toml") {
|
|
230
|
+
const action = await setTomlBenchmark(mcp.path, enabled);
|
|
231
|
+
results.push({
|
|
232
|
+
agent: agent.displayName,
|
|
233
|
+
path: mcp.path,
|
|
234
|
+
action,
|
|
235
|
+
enabled: action === "missing" ? false : enabled,
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const action = await setJsonBenchmark(agent, enabled);
|
|
240
|
+
results.push({
|
|
241
|
+
agent: agent.displayName,
|
|
242
|
+
path: mcp.path,
|
|
243
|
+
action,
|
|
244
|
+
enabled: action === "missing" ? false : enabled,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return results;
|
|
248
|
+
}
|