@takara-ai/miru-code 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -48
- package/docs/coverage-badge.json +6 -0
- package/grammars/manifest.json +10 -2
- package/grammars/tree-sitter-astro.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-php_only.wasm +0 -0
- package/grammars/tree-sitter-sql.wasm +0 -0
- package/grammars/tree-sitter-svelte.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/package.json +10 -2
- package/src/agents.ts +4 -4
- package/src/auth/device.ts +26 -21
- package/src/auth/providers.ts +26 -11
- package/src/benchmark/compare.ts +3 -10
- package/src/benchmark/grep.ts +64 -37
- package/src/benchmark/locate-compare.ts +8 -1
- package/src/benchmark/rg-literal.ts +9 -4
- package/src/cache.ts +3 -3
- package/src/chunking/ast.ts +21 -3
- package/src/chunking/grammars.ts +8 -3
- package/src/chunking/structural.ts +13 -24
- package/src/cli-mode.ts +18 -0
- package/src/cli.ts +10 -45
- package/src/credentials.ts +8 -13
- package/src/embeddings/openai.ts +6 -3
- package/src/embeddings/sagemaker.ts +18 -3
- package/src/embeddings/validate.ts +10 -7
- package/src/git.ts +6 -2
- package/src/help.ts +5 -4
- package/src/index/bm25.ts +66 -6
- package/src/index/create.ts +5 -9
- package/src/index/files.ts +5 -1
- package/src/index/incremental.ts +48 -8
- package/src/index/sparse.ts +14 -2
- package/src/installer/agents.ts +85 -65
- package/src/installer/benchmark-mode.ts +40 -11
- package/src/installer/config.ts +105 -42
- package/src/installer/installer.ts +59 -34
- package/src/installer/legacy-hooks.ts +131 -0
- package/src/installer/prompt.ts +20 -22
- package/src/installer/search-policy.ts +8 -8
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +42 -22
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +54 -18
- package/src/prompt.ts +51 -14
- package/src/ranking/boosting.ts +5 -6
- package/src/ranking/location.ts +6 -10
- package/src/ranking/penalties.ts +14 -22
- package/src/search.ts +4 -12
- package/src/setup.ts +25 -13
- package/src/snippet.ts +8 -26
- package/src/spinner.ts +26 -16
- package/src/tokenizer/bert-wordpiece.ts +0 -6
- package/src/utils.ts +0 -4
- package/src/version.ts +4 -4
- package/src/installer/hooks/install.ts +0 -549
- package/src/installer/hooks/opencode-plugin.ts +0 -54
- package/src/installer/hooks/search-guard.ts +0 -272
package/src/benchmark/grep.ts
CHANGED
|
@@ -147,7 +147,7 @@ function normalizeRepoFile(repoRoot: string, filePath: string): string {
|
|
|
147
147
|
return rel;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
function parsePathLinePrefix(line: string): { path: string; line: number } | null {
|
|
150
|
+
export function parsePathLinePrefix(line: string): { path: string; line: number } | null {
|
|
151
151
|
for (let i = 0; i < line.length; i++) {
|
|
152
152
|
const delim = line[i];
|
|
153
153
|
if (delim !== ":" && delim !== "-") {
|
|
@@ -161,14 +161,12 @@ function parsePathLinePrefix(line: string): { path: string; line: number } | nul
|
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
163
|
const trailing = line[j];
|
|
164
|
-
if (trailing
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
continue;
|
|
164
|
+
if (trailing === ":" || trailing === "-") {
|
|
165
|
+
const lineNo = Number(line.slice(i + 1, j));
|
|
166
|
+
if (Number.isFinite(lineNo)) {
|
|
167
|
+
return { path: line.slice(0, i), line: lineNo };
|
|
168
|
+
}
|
|
170
169
|
}
|
|
171
|
-
return { path: line.slice(0, i), line: lineNo };
|
|
172
170
|
}
|
|
173
171
|
return null;
|
|
174
172
|
}
|
|
@@ -177,6 +175,10 @@ export async function grepSearch(
|
|
|
177
175
|
repoRoot: string,
|
|
178
176
|
query: string,
|
|
179
177
|
topK: number,
|
|
178
|
+
options: {
|
|
179
|
+
tool?: BenchmarkSearchTool | null;
|
|
180
|
+
spawn?: typeof spawnBenchmarkSearch;
|
|
181
|
+
} = {},
|
|
180
182
|
): Promise<GrepSearchResult> {
|
|
181
183
|
const keywords = queryKeywords(query);
|
|
182
184
|
const pattern = buildGrepPattern(keywords);
|
|
@@ -184,17 +186,18 @@ export async function grepSearch(
|
|
|
184
186
|
return { files: [], hits: [], tokens: 0, pattern: null, keywords };
|
|
185
187
|
}
|
|
186
188
|
|
|
187
|
-
const tool = selectBenchmarkSearchTool();
|
|
189
|
+
const tool = Object.hasOwn(options, "tool") ? options.tool : selectBenchmarkSearchTool();
|
|
188
190
|
if (!tool) {
|
|
189
191
|
throw new Error("No search tool found in PATH (tried rg, grep, and findstr on Windows)");
|
|
190
192
|
}
|
|
193
|
+
const spawn = options.spawn ?? spawnBenchmarkSearch;
|
|
191
194
|
|
|
192
195
|
const ranked =
|
|
193
196
|
tool === "rg"
|
|
194
|
-
? await rgRankedMatches(repoRoot, pattern, topK)
|
|
197
|
+
? await rgRankedMatches(repoRoot, pattern, topK, spawn)
|
|
195
198
|
: tool === "grep"
|
|
196
|
-
? await grepRankedMatches(repoRoot, pattern, topK)
|
|
197
|
-
: await findstrRankedMatches(repoRoot, keywords, topK);
|
|
199
|
+
? await grepRankedMatches(repoRoot, pattern, topK, spawn)
|
|
200
|
+
: await findstrRankedMatches(repoRoot, keywords, topK, spawn);
|
|
198
201
|
|
|
199
202
|
const hits: GrepFileHit[] = [];
|
|
200
203
|
let tokens = 0;
|
|
@@ -202,10 +205,10 @@ export async function grepSearch(
|
|
|
202
205
|
for (const row of ranked) {
|
|
203
206
|
const output =
|
|
204
207
|
tool === "rg"
|
|
205
|
-
? await rgFilePreview(row.absPath, pattern)
|
|
208
|
+
? await rgFilePreview(row.absPath, pattern, spawn)
|
|
206
209
|
: tool === "grep"
|
|
207
|
-
? await grepFilePreview(row.absPath, pattern)
|
|
208
|
-
: await findstrFilePreview(row.absPath, keywords);
|
|
210
|
+
? await grepFilePreview(row.absPath, pattern, spawn)
|
|
211
|
+
: await findstrFilePreview(row.absPath, keywords, spawn);
|
|
209
212
|
hits.push({ file: row.file, matchCount: row.matchCount, output });
|
|
210
213
|
tokens += countTokens(output);
|
|
211
214
|
}
|
|
@@ -219,8 +222,13 @@ export async function grepSearch(
|
|
|
219
222
|
};
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
async function rgRankedMatches(
|
|
223
|
-
|
|
225
|
+
async function rgRankedMatches(
|
|
226
|
+
repoRoot: string,
|
|
227
|
+
pattern: string,
|
|
228
|
+
topK: number,
|
|
229
|
+
spawn: typeof spawnBenchmarkSearch,
|
|
230
|
+
) {
|
|
231
|
+
const countText = await spawn([
|
|
224
232
|
"rg",
|
|
225
233
|
"-i",
|
|
226
234
|
"--count-matches",
|
|
@@ -231,8 +239,13 @@ async function rgRankedMatches(repoRoot: string, pattern: string, topK: number)
|
|
|
231
239
|
return parseCountMatches(repoRoot, countText, topK);
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
async function grepRankedMatches(
|
|
235
|
-
|
|
242
|
+
async function grepRankedMatches(
|
|
243
|
+
repoRoot: string,
|
|
244
|
+
pattern: string,
|
|
245
|
+
topK: number,
|
|
246
|
+
spawn: typeof spawnBenchmarkSearch,
|
|
247
|
+
) {
|
|
248
|
+
const countText = await spawn([
|
|
236
249
|
"grep",
|
|
237
250
|
"-R",
|
|
238
251
|
"-I",
|
|
@@ -285,13 +298,15 @@ function splitSearchToolLine(line: string): { path: string; rest: string } | nul
|
|
|
285
298
|
return { path: line.slice(0, colon), rest: line.slice(colon + 1) };
|
|
286
299
|
}
|
|
287
300
|
|
|
288
|
-
async function findstrRankedMatches(
|
|
301
|
+
async function findstrRankedMatches(
|
|
302
|
+
repoRoot: string,
|
|
303
|
+
keywords: string[],
|
|
304
|
+
topK: number,
|
|
305
|
+
spawn: typeof spawnBenchmarkSearch,
|
|
306
|
+
) {
|
|
289
307
|
const counts = new Map<string, number>();
|
|
290
308
|
for (const keyword of keywords) {
|
|
291
|
-
const text = await
|
|
292
|
-
["findstr", "/S", "/N", "/I", "/P", `/C:${keyword}`, "*"],
|
|
293
|
-
repoRoot,
|
|
294
|
-
);
|
|
309
|
+
const text = await spawn(["findstr", "/S", "/N", "/I", "/P", `/C:${keyword}`, "*"], repoRoot);
|
|
295
310
|
for (const line of text.split("\n")) {
|
|
296
311
|
const parsed = parsePathLinePrefix(line);
|
|
297
312
|
if (!parsed?.path) {
|
|
@@ -346,8 +361,19 @@ function parseCountMatches(repoRoot: string, countText: string, topK: number) {
|
|
|
346
361
|
.slice(0, topK);
|
|
347
362
|
}
|
|
348
363
|
|
|
349
|
-
|
|
350
|
-
|
|
364
|
+
export const grepTestUtils = {
|
|
365
|
+
buildGrepPattern,
|
|
366
|
+
normalizeRepoFile,
|
|
367
|
+
splitSearchToolLine,
|
|
368
|
+
parseCountMatches,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
async function rgFilePreview(
|
|
372
|
+
absPath: string,
|
|
373
|
+
pattern: string,
|
|
374
|
+
spawn: typeof spawnBenchmarkSearch,
|
|
375
|
+
): Promise<string> {
|
|
376
|
+
return spawn([
|
|
351
377
|
"rg",
|
|
352
378
|
"-i",
|
|
353
379
|
"-n",
|
|
@@ -360,8 +386,12 @@ async function rgFilePreview(absPath: string, pattern: string): Promise<string>
|
|
|
360
386
|
]);
|
|
361
387
|
}
|
|
362
388
|
|
|
363
|
-
async function grepFilePreview(
|
|
364
|
-
|
|
389
|
+
async function grepFilePreview(
|
|
390
|
+
absPath: string,
|
|
391
|
+
pattern: string,
|
|
392
|
+
spawn: typeof spawnBenchmarkSearch,
|
|
393
|
+
): Promise<string> {
|
|
394
|
+
return spawn([
|
|
365
395
|
"grep",
|
|
366
396
|
"-I",
|
|
367
397
|
"-i",
|
|
@@ -376,13 +406,10 @@ async function grepFilePreview(absPath: string, pattern: string): Promise<string
|
|
|
376
406
|
]);
|
|
377
407
|
}
|
|
378
408
|
|
|
379
|
-
async function findstrFilePreview(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
...keywords.map((k) => `/C:${k}`),
|
|
386
|
-
absPath,
|
|
387
|
-
]);
|
|
409
|
+
async function findstrFilePreview(
|
|
410
|
+
absPath: string,
|
|
411
|
+
keywords: string[],
|
|
412
|
+
spawn: typeof spawnBenchmarkSearch,
|
|
413
|
+
): Promise<string> {
|
|
414
|
+
return spawn(["findstr", "/N", "/I", "/P", ...keywords.map((k) => `/C:${k}`), absPath]);
|
|
388
415
|
}
|
|
@@ -42,13 +42,20 @@ export async function benchmarkLocateComparison(options: {
|
|
|
42
42
|
repoPath: string;
|
|
43
43
|
index: MiruIndex;
|
|
44
44
|
locate?: LiteralLocateOptions;
|
|
45
|
+
dependencies?: {
|
|
46
|
+
selectComparableLiteralSearchTool?: typeof selectComparableLiteralSearchTool;
|
|
47
|
+
};
|
|
45
48
|
}): Promise<LocateBenchmarkComparison> {
|
|
46
49
|
if (options.locate?.limit != null) {
|
|
47
50
|
throw new Error(
|
|
48
51
|
"Cannot benchmark a limited locate response against native grep: rg/grep limits are per file, while locate.limit is global. Omit limit to keep token and recall comparisons valid.",
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
|
-
if (
|
|
54
|
+
if (
|
|
55
|
+
!(
|
|
56
|
+
options.dependencies?.selectComparableLiteralSearchTool ?? selectComparableLiteralSearchTool
|
|
57
|
+
)()
|
|
58
|
+
) {
|
|
52
59
|
throw new Error(
|
|
53
60
|
"A comparable literal benchmark requires rg or compatible grep; findstr is not equivalent for scoped locate output.",
|
|
54
61
|
);
|
|
@@ -135,11 +135,16 @@ export async function rgLiteralOutput(
|
|
|
135
135
|
repoRoot: string,
|
|
136
136
|
literal: string | readonly string[],
|
|
137
137
|
options: RgLiteralOptions = {},
|
|
138
|
+
dependencies: {
|
|
139
|
+
tool?: "rg" | "grep" | null;
|
|
140
|
+
spawn?: (args: string[]) => Promise<string>;
|
|
141
|
+
} = {},
|
|
138
142
|
): Promise<RgLiteralOutput> {
|
|
139
143
|
const literals = Array.isArray(literal) ? literal : [literal as string];
|
|
140
144
|
const context = options.context ?? 0;
|
|
141
145
|
const maxCount = options.maxCount ?? 20;
|
|
142
|
-
const tool =
|
|
146
|
+
const tool =
|
|
147
|
+
dependencies.tool === undefined ? selectComparableLiteralSearchTool() : dependencies.tool;
|
|
143
148
|
if (!tool) {
|
|
144
149
|
throw new Error(
|
|
145
150
|
"A comparable literal benchmark requires rg or compatible grep; findstr cannot preserve locate scope, context, and count semantics.",
|
|
@@ -150,7 +155,7 @@ export async function rgLiteralOutput(
|
|
|
150
155
|
const output: string[] = [];
|
|
151
156
|
for (const paths of pathBatches) {
|
|
152
157
|
output.push(
|
|
153
|
-
await spawnBenchmarkSearch(
|
|
158
|
+
await (dependencies.spawn ?? spawnBenchmarkSearch)(
|
|
154
159
|
buildLiteralArgs(
|
|
155
160
|
tool,
|
|
156
161
|
repoRoot,
|
|
@@ -178,8 +183,8 @@ export async function rgLiteralOutput(
|
|
|
178
183
|
* OR-matches every literal, mirroring what an agent without `locate` would have to run
|
|
179
184
|
* to get the same recall as `literals`/`match_variants` (one pattern isn't equivalent).
|
|
180
185
|
*/
|
|
181
|
-
function buildLiteralArgs(
|
|
182
|
-
tool:
|
|
186
|
+
export function buildLiteralArgs(
|
|
187
|
+
tool: string,
|
|
183
188
|
repoRoot: string,
|
|
184
189
|
literals: readonly string[],
|
|
185
190
|
context: number,
|
package/src/cache.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { computeSourceCacheKey } from "./utils.ts";
|
|
|
23
23
|
import { indexCacheEpoch } from "./version.ts";
|
|
24
24
|
|
|
25
25
|
/** Platform cache root; override with `MIRU_CACHE_HOME`. */
|
|
26
|
-
export function resolveCacheFolder(): string {
|
|
26
|
+
export function resolveCacheFolder(platform: NodeJS.Platform = process.platform): string {
|
|
27
27
|
const override = process.env.MIRU_CACHE_HOME?.trim();
|
|
28
28
|
if (override) {
|
|
29
29
|
return override;
|
|
@@ -31,10 +31,10 @@ export function resolveCacheFolder(): string {
|
|
|
31
31
|
const name = "miru";
|
|
32
32
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
33
33
|
let base: string;
|
|
34
|
-
if (
|
|
34
|
+
if (platform === "win32") {
|
|
35
35
|
base = process.env.LOCALAPPDATA ?? process.env.APPDATA ?? join(home, "AppData", "Local");
|
|
36
36
|
base = join(base, name, "Cache");
|
|
37
|
-
} else if (
|
|
37
|
+
} else if (platform === "darwin") {
|
|
38
38
|
base = join(home, "Library", "Caches", name);
|
|
39
39
|
} else {
|
|
40
40
|
base = process.env.XDG_CACHE_HOME
|
package/src/chunking/ast.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Node } from "web-tree-sitter";
|
|
1
|
+
import type { Language, Node } from "web-tree-sitter";
|
|
2
2
|
import { Parser } from "web-tree-sitter";
|
|
3
3
|
import { getLanguageForFile } from "./grammars.ts";
|
|
4
4
|
import { type ChunkBoundary, mergeAdjacentChunks } from "./lines.ts";
|
|
@@ -87,17 +87,35 @@ export async function chunkAst(
|
|
|
87
87
|
filePath: string,
|
|
88
88
|
language: string | null,
|
|
89
89
|
desiredLength: number,
|
|
90
|
+
dependencies: {
|
|
91
|
+
getLanguage?: typeof getLanguageForFile;
|
|
92
|
+
createParser?: () => Pick<Parser, "setLanguage" | "parse"> & Partial<Pick<Parser, "delete">>;
|
|
93
|
+
} = {},
|
|
90
94
|
): Promise<ChunkBoundary[] | null> {
|
|
91
95
|
if (!source.trim() || !astChunkingEnabled()) {
|
|
92
96
|
return null;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
const languageObj = await getLanguageForFile(filePath, language);
|
|
99
|
+
const languageObj = await (dependencies.getLanguage ?? getLanguageForFile)(filePath, language);
|
|
96
100
|
if (!languageObj) {
|
|
97
101
|
return null;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
// A Parser owns wasm memory the JS GC can't free, so it must be deleted explicitly.
|
|
105
|
+
const parser = dependencies.createParser?.() ?? new Parser();
|
|
106
|
+
try {
|
|
107
|
+
return parseBoundaries(parser, languageObj, source, desiredLength);
|
|
108
|
+
} finally {
|
|
109
|
+
parser.delete?.();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseBoundaries(
|
|
114
|
+
parser: Pick<Parser, "setLanguage" | "parse">,
|
|
115
|
+
languageObj: Language,
|
|
116
|
+
source: string,
|
|
117
|
+
desiredLength: number,
|
|
118
|
+
): ChunkBoundary[] | null {
|
|
101
119
|
parser.setLanguage(languageObj);
|
|
102
120
|
|
|
103
121
|
let tree: ReturnType<Parser["parse"]>;
|
package/src/chunking/grammars.ts
CHANGED
|
@@ -36,13 +36,17 @@ export function wasmPathForLanguage(language: string | null): string | null {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** Pick grammar wasm for a file (e.g. tsx vs typescript). */
|
|
39
|
-
export function wasmPathForFile(
|
|
39
|
+
export function wasmPathForFile(
|
|
40
|
+
filePath: string,
|
|
41
|
+
language: string | null,
|
|
42
|
+
pathExists: (path: string) => boolean = existsSync,
|
|
43
|
+
): string | null {
|
|
40
44
|
if (!language) {
|
|
41
45
|
return null;
|
|
42
46
|
}
|
|
43
47
|
if (language === "typescript" && filePath.toLowerCase().endsWith(".tsx")) {
|
|
44
48
|
const tsxPath = join(grammarsDir(), "tree-sitter-tsx.wasm");
|
|
45
|
-
if (
|
|
49
|
+
if (pathExists(tsxPath)) {
|
|
46
50
|
return tsxPath;
|
|
47
51
|
}
|
|
48
52
|
}
|
|
@@ -86,13 +90,14 @@ async function loadLanguage(wasmPath: string): Promise<Language> {
|
|
|
86
90
|
export async function getLanguageForFile(
|
|
87
91
|
filePath: string,
|
|
88
92
|
language: string | null,
|
|
93
|
+
load: typeof loadLanguage = loadLanguage,
|
|
89
94
|
): Promise<Language | null> {
|
|
90
95
|
const wasmPath = wasmPathForFile(filePath, language);
|
|
91
96
|
if (!wasmPath || !existsSync(wasmPath)) {
|
|
92
97
|
return null;
|
|
93
98
|
}
|
|
94
99
|
try {
|
|
95
|
-
return await
|
|
100
|
+
return await load(wasmPath);
|
|
96
101
|
} catch {
|
|
97
102
|
return null;
|
|
98
103
|
}
|
|
@@ -14,8 +14,6 @@ const SUPPORTED_STRUCTURAL_LANGUAGES = new Set([
|
|
|
14
14
|
"c",
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
|
-
const BRACE_FAMILY_LANGUAGES = new Set(["cpp", "c"]);
|
|
18
|
-
|
|
19
17
|
export function chunkStructural(
|
|
20
18
|
source: string,
|
|
21
19
|
language: string | null,
|
|
@@ -55,18 +53,6 @@ function stripLine(text: string): string {
|
|
|
55
53
|
return text.replace(/[\r\n]+$/, "");
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
function _isBlank(text: string): boolean {
|
|
59
|
-
return stripLine(text).trim().length === 0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function _isCommentLine(text: string, language: string): boolean {
|
|
63
|
-
const trimmed = stripLine(text).trim();
|
|
64
|
-
if (language === "python") {
|
|
65
|
-
return trimmed.startsWith("#");
|
|
66
|
-
}
|
|
67
|
-
return trimmed.startsWith("//");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
56
|
function pythonUnits(lines: LineGroup[]): ChunkBoundary[] {
|
|
71
57
|
const units: ChunkBoundary[] = [];
|
|
72
58
|
let firstDeclStart: number | null = null;
|
|
@@ -182,16 +168,21 @@ function declPattern(language: string): RegExp {
|
|
|
182
168
|
return /^\s*(export\s+)?(async\s+function\b|function\b|class\b|interface\b|type\b|(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)(?:\s*:\s*[^=]+)?\s*=>)/;
|
|
183
169
|
}
|
|
184
170
|
|
|
185
|
-
function isBraceFamilyTypeDecl(text: string, language:
|
|
171
|
+
function isBraceFamilyTypeDecl(text: string, language: "cpp" | "c"): boolean {
|
|
186
172
|
if (language === "cpp") {
|
|
187
173
|
return /^\s*(?:(?:template\s*<[^>]*>\s*)?(?:class|struct|namespace|enum(?:\s+class)?)\b)/.test(
|
|
188
174
|
text,
|
|
189
175
|
);
|
|
190
176
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
177
|
+
return /^\s*(?:struct|enum|union)\b/.test(text);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function shouldSkipNestedBraceDecl(text: string, language: string): boolean {
|
|
181
|
+
return (
|
|
182
|
+
(language === "cpp" || language === "c") &&
|
|
183
|
+
lineIndent(text) > 0 &&
|
|
184
|
+
!isBraceFamilyTypeDecl(text, language)
|
|
185
|
+
);
|
|
195
186
|
}
|
|
196
187
|
|
|
197
188
|
function braceUnits(lines: LineGroup[], language: string): ChunkBoundary[] {
|
|
@@ -205,11 +196,7 @@ function braceUnits(lines: LineGroup[], language: string): ChunkBoundary[] {
|
|
|
205
196
|
if (!re.test(text)) {
|
|
206
197
|
continue;
|
|
207
198
|
}
|
|
208
|
-
if (
|
|
209
|
-
BRACE_FAMILY_LANGUAGES.has(language) &&
|
|
210
|
-
lineIndent(text) > 0 &&
|
|
211
|
-
!isBraceFamilyTypeDecl(text, language)
|
|
212
|
-
) {
|
|
199
|
+
if (shouldSkipNestedBraceDecl(text, language)) {
|
|
213
200
|
continue;
|
|
214
201
|
}
|
|
215
202
|
if (firstDeclStart === null) {
|
|
@@ -263,6 +250,8 @@ function braceUnits(lines: LineGroup[], language: string): ChunkBoundary[] {
|
|
|
263
250
|
return dedupeAndSort(units);
|
|
264
251
|
}
|
|
265
252
|
|
|
253
|
+
export const structuralTestUtils = { shouldSkipNestedBraceDecl };
|
|
254
|
+
|
|
266
255
|
function dedupeAndSort(units: ChunkBoundary[]): ChunkBoundary[] {
|
|
267
256
|
const out: ChunkBoundary[] = [];
|
|
268
257
|
const seen = new Set<string>();
|
package/src/cli-mode.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Keep older stdio MCP configs working when they launch Miru without a subcommand. */
|
|
2
|
+
export function shouldRunMcp(
|
|
3
|
+
argv: readonly string[],
|
|
4
|
+
stdinIsTTY: boolean,
|
|
5
|
+
stdoutIsTTY: boolean,
|
|
6
|
+
): boolean {
|
|
7
|
+
if (argv[0] === "mcp") return argv[1] !== "-h" && argv[1] !== "--help";
|
|
8
|
+
if (stdinIsTTY || stdoutIsTTY) return false;
|
|
9
|
+
const first = argv[0];
|
|
10
|
+
return (
|
|
11
|
+
first === undefined ||
|
|
12
|
+
first === "--benchmark" ||
|
|
13
|
+
first === "--benchmark=true" ||
|
|
14
|
+
first === "--benchmark=false" ||
|
|
15
|
+
first === "--ref" ||
|
|
16
|
+
first === "--content"
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type AgentId, writeAgentFile } from "./agents.ts";
|
|
3
3
|
import { clearBenchmarkHistory, resolveBenchmarkHistoryPath } from "./benchmark/history.ts";
|
|
4
4
|
import { clearCache } from "./cache.ts";
|
|
5
|
+
import { shouldRunMcp } from "./cli-mode.ts";
|
|
5
6
|
import {
|
|
6
7
|
fail,
|
|
7
8
|
formatRelatedHeader,
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
printSageMakerHelp,
|
|
27
28
|
} from "./help.ts";
|
|
28
29
|
import { getBenchmarkModeStatus, setBenchmarkMode } from "./installer/benchmark-mode.ts";
|
|
29
|
-
import { runSearchGuardFromStdin } from "./installer/hooks/search-guard.ts";
|
|
30
30
|
import { runInstaller } from "./installer/installer.ts";
|
|
31
31
|
import { promptConfirm } from "./installer/prompt.ts";
|
|
32
32
|
import {
|
|
@@ -72,27 +72,6 @@ try {
|
|
|
72
72
|
// Leave recovery to ensureCredentials / setup.
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const CLI_COMMANDS = new Set([
|
|
76
|
-
"search",
|
|
77
|
-
"locate",
|
|
78
|
-
"expand",
|
|
79
|
-
"find-related",
|
|
80
|
-
"init",
|
|
81
|
-
"install",
|
|
82
|
-
"uninstall",
|
|
83
|
-
"setup",
|
|
84
|
-
"clear",
|
|
85
|
-
"benchmark",
|
|
86
|
-
"env",
|
|
87
|
-
"environment",
|
|
88
|
-
"hook-guard",
|
|
89
|
-
"help",
|
|
90
|
-
"-h",
|
|
91
|
-
"--help",
|
|
92
|
-
"-v",
|
|
93
|
-
"--version",
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
75
|
const AGENTS = new Set<AgentId>(AGENT_IDS);
|
|
97
76
|
|
|
98
77
|
function parseFlagArgv(argv: string[], flag: string): { present: boolean; rest: string[] } {
|
|
@@ -392,21 +371,11 @@ async function runBenchmarkCommand(rest: string[]): Promise<void> {
|
|
|
392
371
|
async function runCli(argv: string[]): Promise<void> {
|
|
393
372
|
const [command, ...rest] = argv;
|
|
394
373
|
|
|
395
|
-
if (command === undefined) {
|
|
374
|
+
if (command === undefined || command === "-h" || command === "--help") {
|
|
396
375
|
printMainHelp();
|
|
397
376
|
return;
|
|
398
377
|
}
|
|
399
378
|
|
|
400
|
-
if (command === "-h" || command === "--help") {
|
|
401
|
-
printMainHelp();
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (command === "-v" || command === "--version") {
|
|
406
|
-
console.log(miruVersion());
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
379
|
if (command === "help") {
|
|
411
380
|
const topic = rest[0];
|
|
412
381
|
if (!topic || topic === "-h" || topic === "--help") {
|
|
@@ -436,10 +405,6 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
436
405
|
return;
|
|
437
406
|
}
|
|
438
407
|
|
|
439
|
-
if (command === "hook-guard") {
|
|
440
|
-
process.exit(await runSearchGuardFromStdin());
|
|
441
|
-
}
|
|
442
|
-
|
|
443
408
|
if (command === "install" || command === "uninstall") {
|
|
444
409
|
await runInstaller(command);
|
|
445
410
|
return;
|
|
@@ -725,24 +690,24 @@ async function main(): Promise<void> {
|
|
|
725
690
|
const argv = process.argv.slice(2);
|
|
726
691
|
const first = argv[0];
|
|
727
692
|
|
|
693
|
+
// Old hook configs may survive until the next install/uninstall; let their
|
|
694
|
+
// search commands proceed normally while the installer removes the hooks.
|
|
695
|
+
if (first === "hook-guard") return;
|
|
696
|
+
|
|
728
697
|
if (first === "-v" || first === "--version") {
|
|
729
698
|
console.log(miruVersion());
|
|
730
699
|
return;
|
|
731
700
|
}
|
|
732
701
|
|
|
733
|
-
if (first === "hook-guard") {
|
|
734
|
-
process.exit(await runSearchGuardFromStdin());
|
|
735
|
-
}
|
|
736
|
-
|
|
737
702
|
const updateNotice = maybeNotifyUpdate();
|
|
738
703
|
|
|
739
|
-
if (
|
|
740
|
-
await
|
|
704
|
+
if (shouldRunMcp(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
|
|
705
|
+
await updateNotice;
|
|
706
|
+
await runMcpWithCredentials(first === "mcp" ? argv.slice(1) : argv);
|
|
741
707
|
return;
|
|
742
708
|
}
|
|
743
709
|
|
|
744
|
-
await updateNotice;
|
|
745
|
-
await runMcpWithCredentials(argv);
|
|
710
|
+
await Promise.all([runCli(argv), updateNotice]);
|
|
746
711
|
}
|
|
747
712
|
|
|
748
713
|
main().catch((err) => {
|
package/src/credentials.ts
CHANGED
|
@@ -25,17 +25,17 @@ function isUnsubstitutedPlaceholder(value: string): boolean {
|
|
|
25
25
|
return value.startsWith("${") && value.endsWith("}");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function resolveCredentialsDir(): string {
|
|
28
|
+
export function resolveCredentialsDir(platform: NodeJS.Platform = process.platform): string {
|
|
29
29
|
const override = process.env.MIRU_CREDENTIALS_DIR;
|
|
30
30
|
if (override && !isUnsubstitutedPlaceholder(override)) {
|
|
31
31
|
return override;
|
|
32
32
|
}
|
|
33
33
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
34
|
-
if (
|
|
34
|
+
if (platform === "win32") {
|
|
35
35
|
const base = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
36
36
|
return join(base, "miru");
|
|
37
37
|
}
|
|
38
|
-
if (
|
|
38
|
+
if (platform === "darwin") {
|
|
39
39
|
return join(home, "Library", "Application Support", "miru");
|
|
40
40
|
}
|
|
41
41
|
const xdg = process.env.XDG_CONFIG_HOME ?? join(home, ".config");
|
|
@@ -46,12 +46,12 @@ export function resolveCredentialsDir(): string {
|
|
|
46
46
|
* Global Miru state directory (credentials, benchmark history, etc.).
|
|
47
47
|
* Same location as `resolveCredentialsDir()`; prefer this name for non-secret files.
|
|
48
48
|
*/
|
|
49
|
-
export function resolveMiruStateDir(): string {
|
|
50
|
-
return resolveCredentialsDir();
|
|
49
|
+
export function resolveMiruStateDir(platform: NodeJS.Platform = process.platform): string {
|
|
50
|
+
return resolveCredentialsDir(platform);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function resolveCredentialsPath(): string {
|
|
54
|
-
return join(resolveCredentialsDir(), CREDENTIALS_FILENAME);
|
|
53
|
+
export function resolveCredentialsPath(platform: NodeJS.Platform = process.platform): string {
|
|
54
|
+
return join(resolveCredentialsDir(platform), CREDENTIALS_FILENAME);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function readStoredCredentials(): Promise<StoredCredentials | null> {
|
|
@@ -267,12 +267,7 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
// Read for env cleanup, but never let parse errors block deletion.
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
stored = await readStoredCredentials();
|
|
273
|
-
} catch {
|
|
274
|
-
// Ignore parse/read errors — removal still proceeds.
|
|
275
|
-
}
|
|
270
|
+
const stored = await readStoredCredentials();
|
|
276
271
|
await rm(path, { force: true });
|
|
277
272
|
|
|
278
273
|
if (stored) {
|
package/src/embeddings/openai.ts
CHANGED
|
@@ -353,7 +353,9 @@ export class EmbeddingApiError extends Error {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
|
|
356
|
+
type EmbeddingFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
357
|
+
|
|
358
|
+
function createClient(fetchImpl: EmbeddingFetch = fetch): EmbeddingClient {
|
|
357
359
|
const baseUrl = resolveEmbeddingBaseUrl();
|
|
358
360
|
const endpoint = `${baseUrl}/embeddings`;
|
|
359
361
|
|
|
@@ -369,7 +371,7 @@ function createClient(): EmbeddingClient {
|
|
|
369
371
|
if (dimensions != null) {
|
|
370
372
|
body.dimensions = dimensions;
|
|
371
373
|
}
|
|
372
|
-
const response = await
|
|
374
|
+
const response = await fetchImpl(endpoint, {
|
|
373
375
|
method: "POST",
|
|
374
376
|
headers: {
|
|
375
377
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -407,9 +409,10 @@ export class OpenAIEmbeddingBackend implements EmbeddingBackend {
|
|
|
407
409
|
maxEmbedChars?: number;
|
|
408
410
|
dimensions?: number;
|
|
409
411
|
client?: EmbeddingClient;
|
|
412
|
+
fetchImpl?: EmbeddingFetch;
|
|
410
413
|
}) {
|
|
411
414
|
this.model = options?.model ?? resolveEmbeddingModel();
|
|
412
|
-
this.client = options?.client ?? createClient();
|
|
415
|
+
this.client = options?.client ?? createClient(options?.fetchImpl);
|
|
413
416
|
this.batchSize = options?.batchSize ?? resolveEmbeddingBatchSize();
|
|
414
417
|
this.maxEmbedChars = options?.maxEmbedChars ?? resolveMaxEmbedChars();
|
|
415
418
|
this.requestedDimensions = options?.dimensions ?? resolveEmbeddingDimensions(this.model);
|