@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 +1 -1
- package/src/benchmark/grep.ts +99 -72
- package/src/benchmark/rg-literal.ts +3 -9
- package/src/credentials.ts +69 -62
- package/src/embeddings/validate.ts +2 -2
- package/src/git.ts +3 -8
- package/src/help.ts +4 -0
- package/src/mcp/server.ts +61 -44
- package/src/setup.ts +12 -2
package/package.json
CHANGED
package/src/benchmark/grep.ts
CHANGED
|
@@ -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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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 };
|
package/src/credentials.ts
CHANGED
|
@@ -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
|
-
/**
|
|
72
|
-
export async function
|
|
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 (
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
119
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
173
|
+
clearTakaraEnv();
|
|
161
174
|
}
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
55
|
-
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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) {
|