@takara-ai/miru-code 1.5.0 → 1.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "1.5.0",
3
+ "version": "1.5.3",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  /** Native grep baseline for Miru benchmark comparisons. */
2
2
 
3
3
  import { relative } from "node:path";
4
+ import { envOptionalInt } from "../env.ts";
4
5
  import { countTokens } from "../token-count.ts";
5
6
 
6
7
  const STOPWORDS = new Set(
@@ -12,6 +13,9 @@ const STOPWORDS = new Set(
12
13
  export const GREP_LINES_PER_FILE = 3;
13
14
  export const GREP_CONTEXT = 2;
14
15
 
16
+ /** Default cap for each rg/grep/findstr spawn in benchmark mode (seconds). */
17
+ const DEFAULT_BENCHMARK_SEARCH_TIMEOUT_SEC = 10;
18
+
15
19
  /** Shared `-g` excludes for ripgrep baselines (search + locate). */
16
20
  export const RG_EXCLUDE_ARGS = [
17
21
  "-g",
@@ -24,6 +28,44 @@ export const RG_EXCLUDE_ARGS = [
24
28
  "!tokenizer/**",
25
29
  ] as const;
26
30
 
31
+ export class BenchmarkSearchTimeoutError extends Error {
32
+ constructor(timeoutSec: number) {
33
+ super(`Benchmark search timed out after ${timeoutSec}s`);
34
+ this.name = "BenchmarkSearchTimeoutError";
35
+ }
36
+ }
37
+
38
+ /** `Bun.spawn` for benchmark baselines, killed after MIRU_BENCHMARK_SEARCH_TIMEOUT. */
39
+ export async function spawnBenchmarkSearch(args: string[], cwd?: string): Promise<string> {
40
+ const timeoutSec =
41
+ envOptionalInt(["MIRU_BENCHMARK_SEARCH_TIMEOUT"], 1) ?? DEFAULT_BENCHMARK_SEARCH_TIMEOUT_SEC;
42
+ const proc = Bun.spawn(args, {
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ cwd,
46
+ timeout: timeoutSec * 1000,
47
+ });
48
+ const text = await new Response(proc.stdout).text();
49
+ await proc.exited;
50
+ // Prefer signalCode over killed — Bun may set killed=true after a normal exit.
51
+ if (proc.signalCode != null) {
52
+ throw new BenchmarkSearchTimeoutError(timeoutSec);
53
+ }
54
+ return text;
55
+ }
56
+
57
+ /** Run a Grep baseline; return null on timeout so Miru results can still be returned. */
58
+ export async function withGrepTimeoutFallback<T>(run: () => Promise<T>): Promise<T | null> {
59
+ try {
60
+ return await run();
61
+ } catch (err) {
62
+ if (err instanceof BenchmarkSearchTimeoutError) {
63
+ return null;
64
+ }
65
+ throw err;
66
+ }
67
+ }
68
+
27
69
  export interface GrepFileHit {
28
70
  file: string;
29
71
  matchCount: number;
@@ -172,34 +214,31 @@ export async function grepSearch(
172
214
  }
173
215
 
174
216
  async function rgRankedMatches(repoRoot: string, pattern: string, topK: number) {
175
- const countProc = Bun.spawn(
176
- ["rg", "-i", "--count-matches", pattern, repoRoot, ...RG_EXCLUDE_ARGS],
177
- { stdout: "pipe", stderr: "pipe" },
178
- );
179
- const countText = await new Response(countProc.stdout).text();
180
- await countProc.exited;
217
+ const countText = await spawnBenchmarkSearch([
218
+ "rg",
219
+ "-i",
220
+ "--count-matches",
221
+ pattern,
222
+ repoRoot,
223
+ ...RG_EXCLUDE_ARGS,
224
+ ]);
181
225
  return parseCountMatches(repoRoot, countText, topK);
182
226
  }
183
227
 
184
228
  async function grepRankedMatches(repoRoot: string, pattern: string, topK: number) {
185
- const countProc = Bun.spawn(
186
- [
187
- "grep",
188
- "-R",
189
- "-I",
190
- "-i",
191
- "-E",
192
- "-o",
193
- "--exclude-dir=node_modules",
194
- "--exclude-dir=.git",
195
- "--exclude-dir=tokenizer",
196
- pattern,
197
- repoRoot,
198
- ],
199
- { stdout: "pipe", stderr: "pipe" },
200
- );
201
- const countText = await new Response(countProc.stdout).text();
202
- await countProc.exited;
229
+ const countText = await spawnBenchmarkSearch([
230
+ "grep",
231
+ "-R",
232
+ "-I",
233
+ "-i",
234
+ "-E",
235
+ "-o",
236
+ "--exclude-dir=node_modules",
237
+ "--exclude-dir=.git",
238
+ "--exclude-dir=tokenizer",
239
+ pattern,
240
+ repoRoot,
241
+ ]);
203
242
  const counts = new Map<string, number>();
204
243
  for (const line of countText.split("\n")) {
205
244
  if (!line) {
@@ -240,13 +279,10 @@ function splitSearchToolLine(line: string): { path: string; rest: string } | nul
240
279
  async function findstrRankedMatches(repoRoot: string, keywords: string[], topK: number) {
241
280
  const counts = new Map<string, number>();
242
281
  for (const keyword of keywords) {
243
- const proc = Bun.spawn(["findstr", "/S", "/N", "/I", "/P", `/C:${keyword}`, "*"], {
244
- stdout: "pipe",
245
- stderr: "pipe",
246
- cwd: repoRoot,
247
- });
248
- const text = await new Response(proc.stdout).text();
249
- await proc.exited;
282
+ const text = await spawnBenchmarkSearch(
283
+ ["findstr", "/S", "/N", "/I", "/P", `/C:${keyword}`, "*"],
284
+ repoRoot,
285
+ );
250
286
  for (const line of text.split("\n")) {
251
287
  const parsed = parsePathLinePrefix(line);
252
288
  if (!parsed?.path) {
@@ -302,51 +338,42 @@ function parseCountMatches(repoRoot: string, countText: string, topK: number) {
302
338
  }
303
339
 
304
340
  async function rgFilePreview(absPath: string, pattern: string): Promise<string> {
305
- const proc = Bun.spawn(
306
- [
307
- "rg",
308
- "-i",
309
- "-n",
310
- "-C",
311
- String(GREP_CONTEXT),
312
- "-m",
313
- String(GREP_LINES_PER_FILE),
314
- pattern,
315
- absPath,
316
- ],
317
- { stdout: "pipe", stderr: "pipe" },
318
- );
319
- const output = await new Response(proc.stdout).text();
320
- await proc.exited;
321
- return output;
341
+ return spawnBenchmarkSearch([
342
+ "rg",
343
+ "-i",
344
+ "-n",
345
+ "-C",
346
+ String(GREP_CONTEXT),
347
+ "-m",
348
+ String(GREP_LINES_PER_FILE),
349
+ pattern,
350
+ absPath,
351
+ ]);
322
352
  }
323
353
 
324
354
  async function grepFilePreview(absPath: string, pattern: string): Promise<string> {
325
- const proc = Bun.spawn(
326
- [
327
- "grep",
328
- "-I",
329
- "-i",
330
- "-n",
331
- "-E",
332
- "-C",
333
- String(GREP_CONTEXT),
334
- "-m",
335
- String(GREP_LINES_PER_FILE),
336
- pattern,
337
- absPath,
338
- ],
339
- { stdout: "pipe", stderr: "pipe" },
340
- );
341
- const output = await new Response(proc.stdout).text();
342
- await proc.exited;
343
- return output;
355
+ return spawnBenchmarkSearch([
356
+ "grep",
357
+ "-I",
358
+ "-i",
359
+ "-n",
360
+ "-E",
361
+ "-C",
362
+ String(GREP_CONTEXT),
363
+ "-m",
364
+ String(GREP_LINES_PER_FILE),
365
+ pattern,
366
+ absPath,
367
+ ]);
344
368
  }
345
369
 
346
370
  async function findstrFilePreview(absPath: string, keywords: string[]): Promise<string> {
347
- const args = ["findstr", "/N", "/I", "/P", ...keywords.map((k) => `/C:${k}`), absPath];
348
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
349
- const output = await new Response(proc.stdout).text();
350
- await proc.exited;
351
- return output;
371
+ return spawnBenchmarkSearch([
372
+ "findstr",
373
+ "/N",
374
+ "/I",
375
+ "/P",
376
+ ...keywords.map((k) => `/C:${k}`),
377
+ absPath,
378
+ ]);
352
379
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { relative } from "node:path";
4
4
  import { countTokens } from "../token-count.ts";
5
- import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool } from "./grep.ts";
5
+ import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool, spawnBenchmarkSearch } from "./grep.ts";
6
6
 
7
7
  export interface RgLiteralOutput {
8
8
  text: string;
@@ -76,16 +76,10 @@ export async function rgLiteralOutput(
76
76
  throw new Error("No search tool found in PATH (tried rg, grep, and findstr on Windows)");
77
77
  }
78
78
  const start = performance.now();
79
- const proc = Bun.spawn(
79
+ const text = await spawnBenchmarkSearch(
80
80
  buildLiteralArgs(tool, repoRoot, literals, context, maxCount, !!options.ignoreCase),
81
- {
82
- stdout: "pipe",
83
- stderr: "pipe",
84
- ...(tool === "findstr" ? { cwd: repoRoot } : {}),
85
- },
81
+ tool === "findstr" ? repoRoot : undefined,
86
82
  );
87
- const text = await new Response(proc.stdout).text();
88
- await proc.exited;
89
83
  const latency_ms = performance.now() - start;
90
84
  const stats = parseRgLiteralStats(text, repoRoot);
91
85
  return { text, tokens: countTokens(text), latency_ms, ...stats };
@@ -61,6 +61,19 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
61
61
  }
62
62
  }
63
63
 
64
+ function clearSageMakerEnv(sagemaker?: StoredSageMakerCredentials): void {
65
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
66
+ delete process.env.MIRU_SAGEMAKER_ENDPOINT_NAME;
67
+ delete process.env.MIRU_SAGEMAKER_REGION;
68
+ if (sagemaker?.profile && process.env.AWS_PROFILE === sagemaker.profile) {
69
+ delete process.env.AWS_PROFILE;
70
+ }
71
+ }
72
+
73
+ function clearTakaraEnv(): void {
74
+ delete process.env.TAKARA_API_KEY;
75
+ }
76
+
64
77
  function hydrateSageMakerEnv(sagemaker: StoredSageMakerCredentials): void {
65
78
  process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = sagemaker.endpoint_arn;
66
79
  if (sagemaker.profile && !process.env.AWS_PROFILE) {
@@ -68,81 +81,81 @@ function hydrateSageMakerEnv(sagemaker: StoredSageMakerCredentials): void {
68
81
  }
69
82
  }
70
83
 
71
- /** Hydrate TAKARA_API_KEY / SageMaker env vars from the credentials file when env is unset. */
72
- export async function loadStoredCredentials(): Promise<boolean> {
73
- normalizeTakaraApiKeyEnv();
74
- const needsTakara = !hasTakaraApiKeyInEnv();
75
- const needsSageMaker = !process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
76
- if (!needsTakara && !needsSageMaker) {
77
- return false;
78
- }
79
-
84
+ /** Drop the other backend from env so setup validation cannot see a stale mode. */
85
+ export async function beginModeSwitch(to: "takara" | "sagemaker"): Promise<void> {
80
86
  const stored = await readStoredCredentials();
81
- if (!stored) {
82
- return false;
87
+ if (to === "takara") {
88
+ clearSageMakerEnv(stored?.sagemaker);
89
+ } else {
90
+ clearTakaraEnv();
83
91
  }
84
-
85
- let changed = false;
86
- if (needsTakara && stored.takara_api_key) {
87
- process.env.TAKARA_API_KEY = stored.takara_api_key;
88
- changed = true;
89
- }
90
- if (needsSageMaker && stored.sagemaker) {
91
- hydrateSageMakerEnv(stored.sagemaker);
92
- changed = true;
93
- }
94
- return changed;
95
92
  }
96
93
 
97
- export async function saveStoredCredentials(apiKey: string): Promise<string> {
94
+ async function writeExclusive(payload: StoredCredentials): Promise<string> {
98
95
  const dir = resolveCredentialsDir();
99
96
  const path = resolveCredentialsPath();
100
97
  await mkdir(dir, { recursive: true, mode: 0o700 });
101
- const existing = await readStoredCredentials();
102
- // Takara and SageMaker modes are mutually exclusive — saving a key drops any stored endpoint.
103
- const previousSageMaker = existing?.sagemaker;
104
- const payload: StoredCredentials = {
105
- version: CREDENTIALS_VERSION,
106
- takara_api_key: apiKey,
107
- };
108
98
  await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
109
99
  try {
110
100
  await chmod(path, 0o600);
111
101
  } catch {
112
102
  // Windows may not support Unix mode bits on all filesystems.
113
103
  }
114
- if (previousSageMaker) {
115
- if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN === previousSageMaker.endpoint_arn) {
116
- delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
104
+ return path;
105
+ }
106
+
107
+ /** Apply credentials.json as the sole mode — never leave Takara and SageMaker both active. */
108
+ export async function loadStoredCredentials(): Promise<boolean> {
109
+ normalizeTakaraApiKeyEnv();
110
+ const stored = await readStoredCredentials();
111
+ if (!stored) {
112
+ return false;
113
+ }
114
+
115
+ if (stored.takara_api_key) {
116
+ let changed = false;
117
+ if (!hasTakaraApiKeyInEnv()) {
118
+ process.env.TAKARA_API_KEY = stored.takara_api_key;
119
+ changed = true;
120
+ }
121
+ if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN || process.env.MIRU_SAGEMAKER_ENDPOINT_NAME) {
122
+ clearSageMakerEnv(stored.sagemaker);
123
+ changed = true;
124
+ }
125
+ return changed;
126
+ }
127
+
128
+ if (stored.sagemaker) {
129
+ let changed = false;
130
+ if (!process.env.MIRU_SAGEMAKER_ENDPOINT_ARN?.trim()) {
131
+ hydrateSageMakerEnv(stored.sagemaker);
132
+ changed = true;
117
133
  }
118
- if (previousSageMaker.profile && process.env.AWS_PROFILE === previousSageMaker.profile) {
119
- delete process.env.AWS_PROFILE;
134
+ if (hasTakaraApiKeyInEnv()) {
135
+ clearTakaraEnv();
136
+ changed = true;
120
137
  }
138
+ return changed;
121
139
  }
140
+ return false;
141
+ }
142
+
143
+ export async function saveStoredCredentials(apiKey: string): Promise<string> {
144
+ const previous = await readStoredCredentials();
145
+ const path = await writeExclusive({ version: CREDENTIALS_VERSION, takara_api_key: apiKey });
146
+ process.env.TAKARA_API_KEY = apiKey;
147
+ clearSageMakerEnv(previous?.sagemaker);
122
148
  return path;
123
149
  }
124
150
 
125
151
  export async function saveStoredSageMakerCredentials(
126
152
  sagemaker: StoredSageMakerCredentials,
127
153
  ): Promise<string> {
128
- const dir = resolveCredentialsDir();
129
- const path = resolveCredentialsPath();
130
- await mkdir(dir, { recursive: true, mode: 0o700 });
131
- const existing = await readStoredCredentials();
132
- // Takara and SageMaker modes are mutually exclusive — saving an endpoint drops any stored key.
133
- const previousTakaraKey = existing?.takara_api_key;
134
- const payload: StoredCredentials = {
135
- version: CREDENTIALS_VERSION,
136
- sagemaker,
137
- };
138
- await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
139
- try {
140
- await chmod(path, 0o600);
141
- } catch {
142
- // Windows may not support Unix mode bits on all filesystems.
143
- }
144
- if (previousTakaraKey && process.env.TAKARA_API_KEY === previousTakaraKey) {
145
- delete process.env.TAKARA_API_KEY;
154
+ const path = await writeExclusive({ version: CREDENTIALS_VERSION, sagemaker });
155
+ clearTakaraEnv();
156
+ process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = sagemaker.endpoint_arn;
157
+ if (sagemaker.profile) {
158
+ process.env.AWS_PROFILE = sagemaker.profile;
146
159
  }
147
160
  return path;
148
161
  }
@@ -157,16 +170,10 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
157
170
  await Bun.file(path).delete();
158
171
 
159
172
  if (stored?.takara_api_key && process.env.TAKARA_API_KEY === stored.takara_api_key) {
160
- delete process.env.TAKARA_API_KEY;
173
+ clearTakaraEnv();
161
174
  }
162
- const sagemaker = stored?.sagemaker;
163
- if (sagemaker) {
164
- if (process.env.MIRU_SAGEMAKER_ENDPOINT_ARN === sagemaker.endpoint_arn) {
165
- delete process.env.MIRU_SAGEMAKER_ENDPOINT_ARN;
166
- }
167
- if (sagemaker.profile && process.env.AWS_PROFILE === sagemaker.profile) {
168
- delete process.env.AWS_PROFILE;
169
- }
175
+ if (stored?.sagemaker) {
176
+ clearSageMakerEnv(stored.sagemaker);
170
177
  }
171
178
 
172
179
  return { cleared: true, path };
@@ -3,7 +3,7 @@ import {
3
3
  embeddingDimensions,
4
4
  resolveEmbeddingBaseUrl,
5
5
  resolveEmbeddingDimensions,
6
- resolveEmbeddingModel,
6
+ resolveTakaraEmbeddingModel,
7
7
  } from "./openai.ts";
8
8
 
9
9
  export interface ValidateApiKeyResult {
@@ -19,7 +19,7 @@ export async function validateEmbeddingApiKey(options: {
19
19
  dimensions?: number;
20
20
  }): Promise<ValidateApiKeyResult> {
21
21
  const baseUrl = (options.baseUrl ?? resolveEmbeddingBaseUrl()).replace(/\/$/, "");
22
- const model = options.model ?? resolveEmbeddingModel();
22
+ const model = options.model ?? resolveTakaraEmbeddingModel();
23
23
  const dimensions = options.dimensions ?? resolveEmbeddingDimensions(model);
24
24
  const endpoint = `${baseUrl}/embeddings`;
25
25
 
package/src/git.ts CHANGED
@@ -20,22 +20,17 @@ export async function cloneGitRepository(url: string, ref?: string | null): Prom
20
20
  stdout: "pipe",
21
21
  stderr: "pipe",
22
22
  stdin: "ignore",
23
+ timeout: timeoutSec * 1000,
23
24
  });
24
25
  } catch {
25
26
  await rm(dir, { recursive: true, force: true });
26
27
  throw new Error("git is not installed or not on PATH");
27
28
  }
28
29
 
29
- let timedOut = false;
30
- const timer = setTimeout(() => {
31
- timedOut = true;
32
- proc.kill();
33
- }, timeoutSec * 1000);
34
-
35
30
  const code = await proc.exited;
36
- clearTimeout(timer);
37
31
 
38
- if (timedOut) {
32
+ // Prefer signalCode over killed — Bun may set killed=true after a normal exit.
33
+ if (proc.signalCode != null) {
39
34
  await rm(dir, { recursive: true, force: true });
40
35
  throw new Error(`git clone timed out for ${url} (limit: ${timeoutSec}s)`);
41
36
  }
package/src/help.ts CHANGED
@@ -202,6 +202,10 @@ export function printCommandHelp(command: string): void {
202
202
  writeStdout("to a Grep baseline (rg search + read of the top matched file),");
203
203
  writeStdout("not bare chunk content and not the agent's full tool chain.");
204
204
  writeStdout("Local paths only — git URL repos skip comparison with benchmark_skipped.");
205
+ writeStdout(
206
+ "Cap each rg/grep spawn with MIRU_BENCHMARK_SEARCH_TIMEOUT (seconds, default 10).",
207
+ );
208
+ writeStdout("On timeout Miru still returns results with benchmark_skipped=grep_timeout.");
205
209
  writeStdout("");
206
210
  return;
207
211
  case "init":
package/src/mcp/server.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import packageJson from "../../package.json";
3
3
  import { benchmarkSearchComparison, toAgentBenchmarkSummary } from "../benchmark/compare.ts";
4
+ import { withGrepTimeoutFallback } from "../benchmark/grep.ts";
4
5
  import {
5
6
  appendBenchmarkQuery,
6
7
  readBenchmarkRollup,
@@ -46,16 +47,32 @@ const REPO_DESCRIPTION =
46
47
  "Pass the project root for local workspaces. " +
47
48
  "The index is built on the first tool call and cached for the session.";
48
49
 
49
- const BENCHMARK_LOCAL_ONLY_NOTE =
50
- "Benchmark comparisons require a local repo path; git URL repos are skipped.";
50
+ const BENCHMARK_SKIP_NOTES = {
51
+ local_repo_only: "Benchmark comparisons require a local repo path; git URL repos are skipped.",
52
+ grep_timeout:
53
+ "Benchmark Grep baseline timed out; Miru results are still returned. " +
54
+ "Raise MIRU_BENCHMARK_SEARCH_TIMEOUT (seconds) if needed.",
55
+ } as const;
51
56
 
52
- function withBenchmarkSkippedNote(body: string): string {
57
+ type BenchmarkSkipReason = keyof typeof BENCHMARK_SKIP_NOTES;
58
+
59
+ function withBenchmarkSkippedNote(body: string, reason: BenchmarkSkipReason): string {
53
60
  return `${body}\n\n${JSON.stringify({
54
- benchmark_skipped: "local_repo_only",
55
- note: BENCHMARK_LOCAL_ONLY_NOTE,
61
+ benchmark_skipped: reason,
62
+ note: BENCHMARK_SKIP_NOTES[reason],
56
63
  })}`;
57
64
  }
58
65
 
66
+ async function persistBenchmarkQuery(
67
+ ...args: Parameters<typeof appendBenchmarkQuery>
68
+ ): Promise<void> {
69
+ try {
70
+ await appendBenchmarkQuery(...args);
71
+ } catch {
72
+ // History is best-effort; never fail the tool on persist errors.
73
+ }
74
+ }
75
+
59
76
  export function createMcpServer(
60
77
  cache: IndexCache,
61
78
  options?: { benchmark?: boolean },
@@ -101,29 +118,33 @@ export function createMcpServer(
101
118
  const repoRoot = localRepoRoot(repo);
102
119
  const k = clampMcpTopK(topK);
103
120
 
121
+ let skip: BenchmarkSkipReason | undefined;
104
122
  if (benchmark && repoRoot) {
105
- const comparison = await benchmarkSearchComparison({
106
- query,
107
- repoPath: repoRoot,
108
- index,
109
- topK: k,
110
- dedupeByFile: dedupeByFile !== false,
111
- });
112
- const results = comparison.results;
113
- if (results.length === 0) {
114
- return toolText("No results found.");
115
- }
116
- try {
117
- await appendBenchmarkQuery(recordFromBenchmark(repoRoot, comparison.benchmark));
118
- } catch {
119
- // History is best-effort; never fail the search on persist errors.
120
- }
121
- const body = formatResultsText(
122
- formatResults(query, results, { repoRoot, snippet: true }),
123
- );
124
- return toolText(
125
- appendAgentBenchmark(body, toAgentBenchmarkSummary(comparison.benchmark)),
123
+ const comparison = await withGrepTimeoutFallback(() =>
124
+ benchmarkSearchComparison({
125
+ query,
126
+ repoPath: repoRoot,
127
+ index,
128
+ topK: k,
129
+ dedupeByFile: dedupeByFile !== false,
130
+ }),
126
131
  );
132
+ if (comparison) {
133
+ const results = comparison.results;
134
+ if (results.length === 0) {
135
+ return toolText("No results found.");
136
+ }
137
+ await persistBenchmarkQuery(recordFromBenchmark(repoRoot, comparison.benchmark));
138
+ const body = formatResultsText(
139
+ formatResults(query, results, { repoRoot, snippet: true }),
140
+ );
141
+ return toolText(
142
+ appendAgentBenchmark(body, toAgentBenchmarkSummary(comparison.benchmark)),
143
+ );
144
+ }
145
+ skip = "grep_timeout";
146
+ } else if (benchmark && !repoRoot) {
147
+ skip = "local_repo_only";
127
148
  }
128
149
 
129
150
  let results = await index.search({ query, topK: k });
@@ -135,10 +156,7 @@ export function createMcpServer(
135
156
  }
136
157
  const payload = formatResults(query, results, { repoRoot, snippet: true });
137
158
  const body = formatResultsText(payload);
138
- if (benchmark && !repoRoot) {
139
- return toolText(withBenchmarkSkippedNote(body));
140
- }
141
- return toolText(body);
159
+ return toolText(skip ? withBenchmarkSkippedNote(body, skip) : body);
142
160
  } catch (err) {
143
161
  return toolText(err instanceof Error ? err.message : String(err));
144
162
  }
@@ -200,32 +218,31 @@ export function createMcpServer(
200
218
  try {
201
219
  const index = await getIndexForRepo(repo, cache);
202
220
 
203
- if (benchmark) {
204
- const repoRoot = localRepoRoot(repo);
205
- if (repoRoot && typeof literal === "string") {
206
- const comparison = await benchmarkLocateComparison({
221
+ let skip: BenchmarkSkipReason | undefined;
222
+ const repoRoot = localRepoRoot(repo);
223
+ if (benchmark && repoRoot && typeof literal === "string") {
224
+ const comparison = await withGrepTimeoutFallback(() =>
225
+ benchmarkLocateComparison({
207
226
  literal,
208
227
  repoPath: repoRoot,
209
228
  index,
210
229
  locate: locateOpts,
211
- });
212
- try {
213
- await appendBenchmarkQuery(recordFromAgentSummary(repoRoot, comparison.benchmark));
214
- } catch {
215
- // History is best-effort; never fail locate on persist errors.
216
- }
230
+ }),
231
+ );
232
+ if (comparison) {
233
+ await persistBenchmarkQuery(recordFromAgentSummary(repoRoot, comparison.benchmark));
217
234
  const body = formatLiteralLocateText(comparison.payload);
218
235
  return toolText(appendAgentBenchmark(body, comparison.benchmark));
219
236
  }
237
+ skip = "grep_timeout";
238
+ } else if (benchmark && !repoRoot) {
239
+ skip = "local_repo_only";
220
240
  }
221
241
 
222
242
  const result = index.locateLiteral(literal, locateOpts);
223
243
  const payload = formatLiteralLocate(result);
224
244
  const body = formatLiteralLocateText(payload);
225
- if (benchmark && !localRepoRoot(repo)) {
226
- return toolText(withBenchmarkSkippedNote(body));
227
- }
228
- return toolText(body);
245
+ return toolText(skip ? withBenchmarkSkippedNote(body, skip) : body);
229
246
  } catch (err) {
230
247
  return toolText(err instanceof Error ? err.message : String(err));
231
248
  }
package/src/setup.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { divider, fail, hint, info, printBrandBanner, success, writeStdout } from "./cli-ui.ts";
2
2
  import {
3
+ beginModeSwitch,
3
4
  clearStoredCredentials,
4
5
  loadStoredCredentials,
5
6
  readStoredCredentials,
@@ -163,6 +164,7 @@ export async function runSageMakerSetup(options: RunSetupOptions = {}): Promise<
163
164
  profile = await promptAwsProfile();
164
165
  }
165
166
 
167
+ await beginModeSwitch("sagemaker");
166
168
  process.env.MIRU_SAGEMAKER_ENDPOINT_ARN = arnInput;
167
169
  process.env.AWS_PROFILE = profile;
168
170
 
@@ -206,7 +208,15 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
206
208
  if (!options.force && hasTakaraApiKeyInEnv()) {
207
209
  const path = resolveCredentialsPath();
208
210
  const stored = await readStoredCredentials();
209
- if (stored) {
211
+ if (stored?.sagemaker) {
212
+ const saved = await saveStoredCredentials(resolveEmbeddingApiKey());
213
+ writeStdout("");
214
+ success(`Saved credentials to ${saved}`);
215
+ hint("Removed the stored SageMaker endpoint — Miru now embeds only via Takara.");
216
+ writeStdout("");
217
+ return { path: saved, newlySaved: true };
218
+ }
219
+ if (stored?.takara_api_key) {
210
220
  info(`API key already configured (env + ${path}). Use --force to replace stored key.`);
211
221
  return { path, newlySaved: false };
212
222
  }
@@ -236,6 +246,7 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
236
246
 
237
247
  const apiKey = options.apiKey ?? (await promptApiKey());
238
248
 
249
+ await beginModeSwitch("takara");
239
250
  if (!options.skipValidation) {
240
251
  const spinner = new Spinner("Validating API key");
241
252
  spinner.start();
@@ -249,7 +260,6 @@ export async function runSetup(options: RunSetupOptions = {}): Promise<RunSetupR
249
260
 
250
261
  const hadSageMaker = Boolean((await readStoredCredentials())?.sagemaker);
251
262
  const path = await saveStoredCredentials(apiKey);
252
- process.env.TAKARA_API_KEY = apiKey;
253
263
  writeStdout("");
254
264
  success(`Saved credentials to ${path}`);
255
265
  if (hadSageMaker) {