@takara-ai/miru-code 1.8.2 → 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 +65 -53
- 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 +54 -98
- package/src/credentials.ts +14 -14
- 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 +7 -7
- package/src/index/bm25.ts +66 -6
- package/src/index/chunk-selector.ts +39 -11
- 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 +10 -10
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +65 -46
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +90 -22
- 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 +8 -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 +1 -15
- 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
|
+
}
|