@takara-ai/miru-code 1.12.1 → 1.12.2
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/installer/config.ts +6 -73
- package/src/installer/search-policy.ts +1 -1
- package/src/literal.ts +62 -24
- package/src/mcp/server.ts +1 -1
package/package.json
CHANGED
package/src/installer/config.ts
CHANGED
|
@@ -14,53 +14,13 @@ args = ["${MIRU_BUNX_PACKAGE}", "mcp", "--benchmark"]
|
|
|
14
14
|
startup_timeout_sec = 60
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
|
+
/** A string literal (kept) or a comment (dropped); a string left open runs to the end of the input. */
|
|
18
|
+
const STRING_OR_COMMENT =
|
|
19
|
+
/("(?:\\[\s\S]|[^"\\])*\\?(?:"|$)|'(?:\\[\s\S]|[^'\\])*\\?(?:'|$))|\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/g;
|
|
20
|
+
|
|
17
21
|
/** Strip line and block comments so JSONC configs can be parsed. */
|
|
18
22
|
export function stripJsonComments(text: string): string {
|
|
19
|
-
|
|
20
|
-
let i = 0;
|
|
21
|
-
let inString = false;
|
|
22
|
-
let stringQuote = '"';
|
|
23
|
-
while (i < text.length) {
|
|
24
|
-
const ch = text[i];
|
|
25
|
-
const next = text[i + 1];
|
|
26
|
-
if (inString) {
|
|
27
|
-
out += ch;
|
|
28
|
-
if (ch === "\\" && next !== undefined) {
|
|
29
|
-
out += next;
|
|
30
|
-
i += 2;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (ch === stringQuote) {
|
|
34
|
-
inString = false;
|
|
35
|
-
}
|
|
36
|
-
i++;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (ch === '"' || ch === "'") {
|
|
40
|
-
inString = true;
|
|
41
|
-
stringQuote = ch;
|
|
42
|
-
out += ch;
|
|
43
|
-
i++;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (ch === "/" && next === "/") {
|
|
47
|
-
while (i < text.length && text[i] !== "\n") {
|
|
48
|
-
i++;
|
|
49
|
-
}
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (ch === "/" && next === "*") {
|
|
53
|
-
i += 2;
|
|
54
|
-
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
55
|
-
i++;
|
|
56
|
-
}
|
|
57
|
-
i += 2;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
out += ch;
|
|
61
|
-
i++;
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
23
|
+
return text.replace(STRING_OR_COMMENT, (_match, str?: string) => str ?? "");
|
|
64
24
|
}
|
|
65
25
|
|
|
66
26
|
function parseJsonObject(text: string): Record<string, unknown> | "error" {
|
|
@@ -285,34 +245,7 @@ function removeJsonMemberText(text: string, sectionKey: string, memberKey: strin
|
|
|
285
245
|
|
|
286
246
|
/** True only when // or /* appear outside of strings (URLs like https:// must not match). */
|
|
287
247
|
function hasJsoncSyntax(text: string): boolean {
|
|
288
|
-
|
|
289
|
-
let quote = "";
|
|
290
|
-
for (let i = 0; i < text.length; i++) {
|
|
291
|
-
const ch = text[i] ?? "";
|
|
292
|
-
const next = text[i + 1] ?? "";
|
|
293
|
-
if (inString) {
|
|
294
|
-
if (ch === "\\") {
|
|
295
|
-
i++;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (ch === quote) {
|
|
299
|
-
inString = false;
|
|
300
|
-
}
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (ch === '"' || ch === "'") {
|
|
304
|
-
inString = true;
|
|
305
|
-
quote = ch;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (ch === "/" && next === "/") {
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
if (ch === "/" && next === "*") {
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return false;
|
|
248
|
+
return stripJsonComments(text) !== text;
|
|
316
249
|
}
|
|
317
250
|
|
|
318
251
|
/** Locate an object section by dotted path, matching each key among direct children. */
|
|
@@ -156,7 +156,7 @@ export const MCP_SEARCH_TOOL_DESCRIPTION =
|
|
|
156
156
|
export const MCP_LOCATE_TOOL_DESCRIPTION =
|
|
157
157
|
"Exact substring locator over the Miru index, also `/regex/` and file lists (grep for identifiers, env vars, error strings, quoted text, without the noise). " +
|
|
158
158
|
'Use for known literals, not meaning-based questions (`search`), even when phrased "where is X" if X is a literal. ' +
|
|
159
|
-
"A literal in slashes (`/foo\\w+Bar/`,
|
|
159
|
+
"A literal wrapped in slashes is always a regex (`/foo\\w+Bar/`, `/bench/i`); omit the slashes for plain text. No literal + `include` globs lists files by name pattern; mode=files returns just matching paths. " +
|
|
160
160
|
"Returns ALL matches by default — do NOT fall back to Grep/rg when n is large; prefer mode=count/files/locations unless you need line text. " +
|
|
161
161
|
"`context_lines` (mode=lines) saves a follow-up `expand`. In benchmark mode, includes token savings vs Grep.";
|
|
162
162
|
|
package/src/literal.ts
CHANGED
|
@@ -56,19 +56,22 @@ export interface LiteralLocateOptions {
|
|
|
56
56
|
context_lines?: number;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
/**
|
|
60
|
+
* `/body/` and `/body/i` are always regexes. Anything else is plain text, so drop the outer
|
|
61
|
+
* slashes to search a path such as `api/users/` as a substring.
|
|
62
|
+
*/
|
|
63
|
+
const SLASH_REGEX = /^\/(?<body>.+)\/(?<flag>i?)$/s;
|
|
62
64
|
/** Regex is tested per line; cap the tested prefix so a minified line cannot stall the scan. */
|
|
63
65
|
const MAX_REGEX_LINE_CHARS = 4096;
|
|
64
66
|
|
|
67
|
+
/** The regex a slash-wrapped literal denotes, or null for a plain-text literal. */
|
|
65
68
|
function parseRegexLiteral(literal: string, ignoreCase: boolean): RegExp | null {
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
69
|
+
const groups = SLASH_REGEX.exec(literal)?.groups;
|
|
70
|
+
if (!groups) {
|
|
68
71
|
return null;
|
|
69
72
|
}
|
|
70
73
|
try {
|
|
71
|
-
return new RegExp(
|
|
74
|
+
return new RegExp(groups.body ?? "", ignoreCase || groups.flag === "i" ? "i" : "");
|
|
72
75
|
} catch (err) {
|
|
73
76
|
throw new Error(
|
|
74
77
|
`Invalid regex ${literal}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -100,6 +103,9 @@ function lineMatchesAny(
|
|
|
100
103
|
needles: readonly string[],
|
|
101
104
|
ignoreCase: boolean,
|
|
102
105
|
): boolean {
|
|
106
|
+
if (needles.length === 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
103
109
|
const hay = ignoreCase ? haystack.toLowerCase() : haystack;
|
|
104
110
|
for (const needle of needles) {
|
|
105
111
|
const n = ignoreCase ? needle.toLowerCase() : needle;
|
|
@@ -158,6 +164,51 @@ function expandLiterals(literals: readonly string[], matchVariants: boolean): st
|
|
|
158
164
|
return [...seen];
|
|
159
165
|
}
|
|
160
166
|
|
|
167
|
+
interface LiteralMatcher {
|
|
168
|
+
/** Plain substrings actually searched, after identifier-variant expansion. */
|
|
169
|
+
needles: readonly string[];
|
|
170
|
+
/** The slash-wrapped inputs, verbatim. */
|
|
171
|
+
regexLiterals: readonly string[];
|
|
172
|
+
isEmpty: boolean;
|
|
173
|
+
matchesLine(line: string): boolean;
|
|
174
|
+
/** Cheap whole-chunk test: false means no line in the chunk can match. */
|
|
175
|
+
chunkMayMatch(content: string): boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Splits literals into plain substrings and regexes, and builds the line and chunk tests. */
|
|
179
|
+
function compileMatcher(
|
|
180
|
+
literals: readonly string[],
|
|
181
|
+
ignoreCase: boolean,
|
|
182
|
+
matchVariants: boolean,
|
|
183
|
+
): LiteralMatcher {
|
|
184
|
+
const regexes: RegExp[] = [];
|
|
185
|
+
const regexLiterals: string[] = [];
|
|
186
|
+
const plain: string[] = [];
|
|
187
|
+
for (const literal of literals) {
|
|
188
|
+
const re = parseRegexLiteral(literal, ignoreCase);
|
|
189
|
+
if (re) {
|
|
190
|
+
regexes.push(re);
|
|
191
|
+
regexLiterals.push(literal);
|
|
192
|
+
} else {
|
|
193
|
+
plain.push(literal);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const needles = expandLiterals(plain, matchVariants);
|
|
197
|
+
// One JIT-compiled pass per chunk rules out most chunks before any line is split or tested.
|
|
198
|
+
// Multiline anchors keep `^`/`$` line-relative, so this can only over-match, never miss.
|
|
199
|
+
const chunkRegexes = regexes.map((re) => new RegExp(re.source, `${re.flags}m`));
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
needles,
|
|
203
|
+
regexLiterals,
|
|
204
|
+
isEmpty: needles.length === 0 && regexes.length === 0,
|
|
205
|
+
matchesLine: (line) =>
|
|
206
|
+
lineMatchesAny(line, needles, ignoreCase) || regexMatchesLine(line, regexes),
|
|
207
|
+
chunkMayMatch: (content) =>
|
|
208
|
+
lineMatchesAny(content, needles, ignoreCase) || chunkRegexes.some((re) => re.test(content)),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
161
212
|
export function filterChunksByGlob(
|
|
162
213
|
chunks: readonly Chunk[],
|
|
163
214
|
include: string[] | undefined,
|
|
@@ -242,21 +293,8 @@ export function locateLiteral(
|
|
|
242
293
|
|
|
243
294
|
const inputLiterals = (Array.isArray(literal) ? literal : [literal]).filter((l) => l.length > 0);
|
|
244
295
|
const label = inputLiterals.join(" | ");
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
for (const lit of inputLiterals) {
|
|
248
|
-
const re = parseRegexLiteral(lit, ignoreCase);
|
|
249
|
-
if (re) {
|
|
250
|
-
regexes.push(re);
|
|
251
|
-
} else {
|
|
252
|
-
plainLiterals.push(lit);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const needles = expandLiterals(plainLiterals, options.match_variants === true);
|
|
256
|
-
const matchesLine = (text: string): boolean =>
|
|
257
|
-
lineMatchesAny(text, needles, ignoreCase) || regexMatchesLine(text, regexes);
|
|
258
|
-
|
|
259
|
-
if (needles.length === 0 && regexes.length === 0) {
|
|
296
|
+
const matcher = compileMatcher(inputLiterals, ignoreCase, options.match_variants === true);
|
|
297
|
+
if (matcher.isEmpty) {
|
|
260
298
|
return { literal: label, mode, n: 0, files: 0, truncated: false, hits: [] };
|
|
261
299
|
}
|
|
262
300
|
|
|
@@ -270,13 +308,13 @@ export function locateLiteral(
|
|
|
270
308
|
let n = 0;
|
|
271
309
|
|
|
272
310
|
for (const chunk of scopedChunks) {
|
|
273
|
-
if (
|
|
311
|
+
if (!matcher.chunkMayMatch(chunk.content)) {
|
|
274
312
|
continue;
|
|
275
313
|
}
|
|
276
314
|
const lines = chunk.content.split("\n");
|
|
277
315
|
for (let i = 0; i < lines.length; i++) {
|
|
278
316
|
const lineText = lines[i] ?? "";
|
|
279
|
-
if (!matchesLine(lineText)) {
|
|
317
|
+
if (!matcher.matchesLine(lineText)) {
|
|
280
318
|
continue;
|
|
281
319
|
}
|
|
282
320
|
const line = chunk.start_line + i;
|
|
@@ -318,7 +356,7 @@ export function locateLiteral(
|
|
|
318
356
|
if (mode === "files") {
|
|
319
357
|
result.file_list = [...fileSet].sort();
|
|
320
358
|
}
|
|
321
|
-
const searched = [...needles, ...
|
|
359
|
+
const searched = [...matcher.needles, ...matcher.regexLiterals];
|
|
322
360
|
if (searched.length > 1) {
|
|
323
361
|
result.literals = searched;
|
|
324
362
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -250,7 +250,7 @@ export function createMcpServer(
|
|
|
250
250
|
.union([z.string().min(1), z.array(z.string().min(1)).min(1)])
|
|
251
251
|
.optional()
|
|
252
252
|
.describe(
|
|
253
|
-
"Exact substring, or a regex in slashes like `/foo\\w+Bar/`
|
|
253
|
+
"Exact substring, or a regex wrapped in slashes like `/foo\\w+Bar/` or `/bench/i` (ignores case). An array ORs several. Omit and pass `include` to list files.",
|
|
254
254
|
),
|
|
255
255
|
query: z
|
|
256
256
|
.union([z.string().min(1), z.array(z.string().min(1)).min(1)])
|