@takara-ai/miru-code 1.1.2 → 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/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,
@@ -44,12 +51,15 @@ import {
44
51
  } from "./utils.ts";
45
52
  import { maybeNotifyUpdate, miruVersion } from "./version.ts";
46
53
 
54
+ process.title = "miru";
55
+
47
56
  await loadEnvFiles();
48
57
  normalizeTakaraApiKeyEnv();
49
58
  await loadStoredCredentials();
50
59
 
51
60
  const CLI_COMMANDS = new Set([
52
61
  "search",
62
+ "locate",
53
63
  "expand",
54
64
  "find-related",
55
65
  "init",
@@ -57,6 +67,7 @@ const CLI_COMMANDS = new Set([
57
67
  "uninstall",
58
68
  "setup",
59
69
  "clear",
70
+ "benchmark",
60
71
  "hook-guard",
61
72
  "help",
62
73
  "-h",
@@ -102,7 +113,7 @@ function parseContentArgv(argv: string[]): { content: ContentType[]; rest: strin
102
113
  rest.push(arg);
103
114
  }
104
115
  }
105
- return { content: resolveContent(content.length > 0 ? content : ["code"]), rest };
116
+ return { content: resolveContent(content), rest };
106
117
  }
107
118
 
108
119
  function parseTopK(argv: string[]): { topK: number; rest: string[] } {
@@ -248,6 +259,53 @@ class RelatedChunkNotFoundError extends Error {
248
259
  }
249
260
  }
250
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
+
251
309
  async function runInit(agent: AgentId, force: boolean): Promise<void> {
252
310
  try {
253
311
  const dest = await writeAgentFile(agent, { force });
@@ -265,6 +323,75 @@ async function runClear(path: string): Promise<void> {
265
323
  success(`Cleared cached index for ${path}`);
266
324
  }
267
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
+
268
395
  async function runCli(argv: string[]): Promise<void> {
269
396
  const [command, ...rest] = argv;
270
397
 
@@ -302,6 +429,11 @@ async function runCli(argv: string[]): Promise<void> {
302
429
  return;
303
430
  }
304
431
 
432
+ if (command === "benchmark") {
433
+ await runBenchmarkCommand(rest);
434
+ return;
435
+ }
436
+
305
437
  if (command === "init") {
306
438
  let agent: AgentId | undefined;
307
439
  let force = false;
@@ -390,6 +522,49 @@ async function runCli(argv: string[]): Promise<void> {
390
522
  return;
391
523
  }
392
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
+
393
568
  if (command === "expand") {
394
569
  const filePath = sizedRest[0];
395
570
  const lineRaw = sizedRest[1];
@@ -399,8 +574,8 @@ async function runCli(argv: string[]): Promise<void> {
399
574
  }
400
575
  const line = Number(lineRaw);
401
576
  const path = resolveSearchPath(sizedRest[2] ?? process.cwd());
402
- let before = 1;
403
- let after = 1;
577
+ let before = DEFAULT_EXPAND_BEFORE;
578
+ let after = DEFAULT_EXPAND_AFTER;
404
579
  for (let i = 3; i < sizedRest.length; i++) {
405
580
  const arg = sizedRest[i];
406
581
  if (arg === "--before" && sizedRest[i + 1]) {
@@ -433,10 +608,15 @@ async function runCli(argv: string[]): Promise<void> {
433
608
 
434
609
  async function runMcp(argv: string[]): Promise<void> {
435
610
  let ref: string | null = null;
611
+ let benchmark = false;
436
612
  const contentTokens: string[] = [];
437
613
 
438
614
  for (let i = 0; i < argv.length; i++) {
439
615
  const arg = argv[i];
616
+ if (arg === "--benchmark") {
617
+ benchmark = true;
618
+ continue;
619
+ }
440
620
  if (arg === "--ref" && argv[i + 1]) {
441
621
  ref = argv[++i] ?? null;
442
622
  continue;
@@ -457,7 +637,8 @@ async function runMcp(argv: string[]): Promise<void> {
457
637
 
458
638
  await serveMcp({
459
639
  ref,
460
- content: resolveContent(contentTokens.length > 0 ? contentTokens : ["code"]),
640
+ content: resolveContent(contentTokens),
641
+ benchmark,
461
642
  });
462
643
  }
463
644
 
@@ -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:
@@ -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 { Chunk, ContentType } from "../types.ts";
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[] = ["code"],
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";
@@ -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
- if (candidate.startsWith(`${resolvedRoot}/`) || candidate === resolvedRoot) {
127
- return normalizeRelativePath(relative(resolvedRoot, candidate));
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, markdown docs, or both. */
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";