@takara-ai/miru-code 1.12.0 → 1.12.1
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 +3 -3
- package/package.json +1 -1
- package/src/installer/search-policy.ts +15 -13
- package/src/mcp/format-text.ts +13 -4
- package/src/snippet.ts +42 -1
- package/src/utils.ts +3 -0
package/README.md
CHANGED
|
@@ -200,9 +200,9 @@ When wired via `miru install`, the MCP server exposes `search`, `locate`, `expan
|
|
|
200
200
|
### Workflow
|
|
201
201
|
|
|
202
202
|
1. **`search`** with `query` — returns compact snippets (~±15 lines) and relevance scores.
|
|
203
|
-
2. If a hit has **`truncated: true`**,
|
|
204
|
-
3. To trace similar patterns elsewhere, call **`find_related`** with the same `file_path` and `anchor_line`.
|
|
205
|
-
4. Use your editor's **Read** on `absolute_path` only
|
|
203
|
+
2. If a hit has **`truncated: true`**, its header names the symbol and its exact lines (`path:start-end Symbol`) plus `anchor_line`. Read just those lines with a ranged **Read** (`offset`/`limit`) — not another search or a full-file read.
|
|
204
|
+
3. If that range lacks context, call **`expand`** with `file_path` and `anchor_line` for neighbouring chunks. To trace similar patterns elsewhere, call **`find_related`** with the same `file_path` and `anchor_line`.
|
|
205
|
+
4. Use your editor's **Read** on `absolute_path` only for a hit's line range, to see a truncated body or edit it.
|
|
206
206
|
|
|
207
207
|
Keep `repo` on follow-up calls when set.
|
|
208
208
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Shared code-search policy text for instructions, Cursor rules, and sub-agents. */
|
|
2
2
|
|
|
3
3
|
export const SNIPPET_GUIDANCE =
|
|
4
|
-
"Snippets are ~±15 lines.
|
|
5
|
-
"
|
|
4
|
+
"Snippets are ~±15 lines. A `truncated: true` hit names its symbol and exact lines (`path:start-end Symbol`) plus `anchor_line`: " +
|
|
5
|
+
"to see the full body, read exactly those lines with a ranged read (`offset`/`limit`), never the whole file. " +
|
|
6
|
+
"`expand` (`file_path` + `anchor_line`) is the fallback for neighbouring chunks — only if the snippet doesn't already answer.";
|
|
6
7
|
|
|
7
8
|
export const WHEN_TO_REACH_FOR_MIRU =
|
|
8
9
|
"Reach for Miru FIRST, before opening files, whenever you don't know the exact path: starting in unfamiliar code, " +
|
|
@@ -23,10 +24,11 @@ export function buildSearchPolicyTable(native: NativeToolNames): string {
|
|
|
23
24
|
| Regex over code | Miru MCP \`locate\` with \`/regex/\` (\`mode=files\` for paths only) | ${native.grep} |
|
|
24
25
|
| List files by name pattern | Miru MCP \`locate\` with \`include\` globs and no literal | Glob |
|
|
25
26
|
| How/where/what handles X? (conceptual) | Miru MCP \`search\` (once) | ${native.grep}, ${native.explorationDenied} |
|
|
26
|
-
|
|
|
27
|
+
| Full body of a \`truncated: true\` hit | ${native.read} the hit's \`start-end\` lines (\`offset\`/\`limit\`) | Re-search, ${native.read} whole file |
|
|
28
|
+
| Same file, neighbouring chunks | Miru MCP \`expand\` (fallback) | Re-search |
|
|
27
29
|
| Similar code elsewhere | Miru MCP \`find_related\` | ${native.grep} chains |
|
|
28
30
|
| Search docs or config | Miru \`search\` | ${native.grep} README paths |
|
|
29
|
-
| Edit a known file:line | ${native.read} (after Miru found it) | ${native.read}-before-search |`;
|
|
31
|
+
| Edit a known file:line | ${native.read} the hit's \`start-end\` lines (after Miru found it) | ${native.read}-before-search |`;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function buildSearchPolicyBody(native: NativeToolNames): string {
|
|
@@ -43,8 +45,8 @@ ${SNIPPET_GUIDANCE}
|
|
|
43
45
|
Use Miru MCP tools:
|
|
44
46
|
- \`locate\` — exact substring (env var, symbol, error code) or \`/regex/\`; no literal + \`include\` globs lists files; prefer \`mode=count\`, \`files\` or \`locations\`
|
|
45
47
|
- \`search\` — one call per conceptual question; use \`include\` / \`exclude\` Gitignore-style globs to scope files (same syntax as \`locate\`)
|
|
46
|
-
- \`expand\` —
|
|
47
|
-
- \`find_related\` — similar code in other files (hits may also be snippets;
|
|
48
|
+
- \`expand\` — fallback: neighbouring chunks in the same file around \`anchor_line\` (\`file_path\` + \`anchor_line\`)
|
|
49
|
+
- \`find_related\` — similar code in other files (hits may also be snippets; read the hit's lines if truncated)
|
|
48
50
|
|
|
49
51
|
Call shapes:
|
|
50
52
|
- \`locate(literal="<exact substring>")\` — prefer \`mode="locations"\`/\`"count"\`
|
|
@@ -53,8 +55,8 @@ Call shapes:
|
|
|
53
55
|
Stop rules:
|
|
54
56
|
- Literal in the request → \`locate\`, not \`search\`
|
|
55
57
|
- One \`search\` per question — no paraphrase retries
|
|
56
|
-
- \`expand\` only if
|
|
57
|
-
- ${native.read} only
|
|
58
|
+
- On \`truncated: true\`, ${native.read} only the hit's \`start-end\` lines (\`offset\`/\`limit\`); \`expand\` only if that doesn't answer
|
|
59
|
+
- ${native.read} only a path Miru already found, to see a truncated body or edit it — never to explore, never the whole file
|
|
58
60
|
|
|
59
61
|
Native tools are allowed ONLY when:
|
|
60
62
|
- reading a file you already located via Miru, to edit it
|
|
@@ -69,9 +71,9 @@ export function buildMcpWorkflow(native: NativeToolNames): string {
|
|
|
69
71
|
|
|
70
72
|
1. Call \`search\`; add \`repo\` only for another repo.
|
|
71
73
|
2. For exact literals, call \`locate\` (prefer \`mode=count\` or \`locations\`).
|
|
72
|
-
3. On \`truncated: true\`,
|
|
73
|
-
4. Use \`
|
|
74
|
-
5. ${native.read} via \`absolute_path\` only
|
|
74
|
+
3. On \`truncated: true\`, ${native.read} the hit's \`start-end\` lines (\`offset\`/\`limit\`) — only if the snippet doesn't already answer.
|
|
75
|
+
4. Use \`expand\` for neighbouring chunks if that range lacks context; \`find_related\` to trace similar code in other files.
|
|
76
|
+
5. ${native.read} via \`absolute_path\` only for the hit's range, to see a truncated body or edit it.`;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const CLI_FALLBACK = `### CLI fallback (no MCP in this session)
|
|
@@ -159,8 +161,8 @@ export const MCP_LOCATE_TOOL_DESCRIPTION =
|
|
|
159
161
|
"`context_lines` (mode=lines) saves a follow-up `expand`. In benchmark mode, includes token savings vs Grep.";
|
|
160
162
|
|
|
161
163
|
export const MCP_EXPAND_TOOL_DESCRIPTION =
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
+
"Neighbouring chunks in the SAME file as a search hit (`file_path` + `anchor_line` from the hit). " +
|
|
165
|
+
"For a `truncated: true` hit's own full body, read its `start-end` lines instead; use this only if that doesn't answer — not for similar code elsewhere (use find_related).";
|
|
164
166
|
|
|
165
167
|
export const MCP_FIND_RELATED_TOOL_DESCRIPTION =
|
|
166
168
|
"Find up to 5 chunks similar to a file:line in OTHER parts of the codebase. Results may be snippets; use `expand` when `truncated: true`. " +
|
package/src/mcp/format-text.ts
CHANGED
|
@@ -12,10 +12,19 @@ export function formatResultsText(payload: {
|
|
|
12
12
|
|
|
13
13
|
const blocks = payload.results.map((result) => {
|
|
14
14
|
const chunk = result.chunk as Record<string, unknown>;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
15
|
+
// A truncated hit names the matched symbol's exact range (not the source file's) so the full
|
|
16
|
+
// body is one ranged read away; `anchor_line` is the matched line for `expand`/`find_related`.
|
|
17
|
+
const startLine = chunk.truncated
|
|
18
|
+
? (chunk.match_start_line ?? chunk.full_start_line)
|
|
19
|
+
: chunk.start_line;
|
|
20
|
+
const endLine = chunk.truncated
|
|
21
|
+
? (chunk.match_end_line ?? chunk.full_end_line)
|
|
22
|
+
: chunk.end_line;
|
|
23
|
+
const symbol = chunk.truncated && chunk.symbol ? ` ${chunk.symbol}` : "";
|
|
24
|
+
const truncatedNote = chunk.truncated
|
|
25
|
+
? ` [truncated: anchor_line ${chunk.anchor_line}; read lines ${startLine}-${endLine} for the full symbol, or call expand]`
|
|
26
|
+
: "";
|
|
27
|
+
const header = `${chunk.file_path}:${startLine}-${endLine}${symbol}${truncatedNote}`;
|
|
19
28
|
return `${header}\n${chunk.content}`;
|
|
20
29
|
});
|
|
21
30
|
|
package/src/snippet.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface SnippetMeta {
|
|
|
13
13
|
anchor_line: number;
|
|
14
14
|
full_start_line: number;
|
|
15
15
|
full_end_line: number;
|
|
16
|
+
/** Exact lines of the matched (ranked) chunk — the symbol-sized range to read, not the source file. */
|
|
17
|
+
match_start_line: number;
|
|
18
|
+
match_end_line: number;
|
|
19
|
+
/** Name of the symbol the matched chunk declares, when one is recognizable. */
|
|
20
|
+
symbol: string | null;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export interface SnippetResult {
|
|
@@ -73,6 +78,24 @@ function scoreLine(line: string, terms: Set<string>): number {
|
|
|
73
78
|
|
|
74
79
|
const DECLARATION_RE =
|
|
75
80
|
/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|enum|type|struct|trait|impl|record|namespace|contract|library|object)\b|^\s*(?:export\s+)?(?:const|let|var)\s+[$\w]+\s*=\s*[{[(]|^\s*(?:async\s+)?def\s+\w+|^\s*def\s+\w+|^\s*fn\s+\w+|^\s*func\s+\w+|^\s*defmodule\b|^\s*def\w*\b|^\s*(?:(?:public|private|protected|internal|static|final|abstract|virtual|override|sealed|partial|unsafe|extern)\s+)+(?:[A-Za-z_$][\w:<>[\]?.,\s]*\s+)+[A-Za-z_$]\w*\s*\(|^\s*(?:void|bool|boolean|char|short|int|long|float|double|string|[A-Z][\w:<>[\]?.,]*)\s+[A-Za-z_$]\w*\s*\(|^\s*(?:function\s+)?[A-Za-z_]\w*\s*\(\)\s*\{/;
|
|
81
|
+
const DECLARED_NAME_RE =
|
|
82
|
+
/\b(?:function\*?|class|interface|enum|type|struct|trait|impl|record|namespace|contract|library|object|def|fn|func|defmodule)\s+([A-Za-z_$][\w$]*)|\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/;
|
|
83
|
+
|
|
84
|
+
/** Name declared by the first declaration line in `content`, or null when none is recognizable. */
|
|
85
|
+
export function declaredSymbol(content: string): string | null {
|
|
86
|
+
for (const line of content.split("\n").slice(0, MAX_DECLARATION_LOOKBACK)) {
|
|
87
|
+
if (!DECLARATION_RE.test(line)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const match = DECLARED_NAME_RE.exec(line);
|
|
91
|
+
const name = match?.[1] ?? match?.[2];
|
|
92
|
+
if (name) {
|
|
93
|
+
return name;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
const BRACE_BODY_RE =
|
|
77
100
|
/\b(?:function|class|interface|enum|struct|trait|impl|record|namespace|contract|library|object|fn|func)\b|=\s*[{[(]|[A-Za-z_$]\w*\s*\([^)]*\)\s*\{/;
|
|
78
101
|
const RUBY_OR_ELIXIR_BODY_RE = /^\s*def\w*\b|^\s*defmodule\b/;
|
|
@@ -419,6 +442,9 @@ export function trimChunkToSnippet(
|
|
|
419
442
|
anchor_line: chunk.start_line + anchorOffset,
|
|
420
443
|
full_start_line: chunk.start_line,
|
|
421
444
|
full_end_line: chunk.end_line,
|
|
445
|
+
match_start_line: chunk.start_line,
|
|
446
|
+
match_end_line: chunk.end_line,
|
|
447
|
+
symbol: declaredSymbol(chunk.content),
|
|
422
448
|
},
|
|
423
449
|
};
|
|
424
450
|
}
|
|
@@ -436,6 +462,9 @@ export function trimChunkToSnippet(
|
|
|
436
462
|
anchor_line: chunk.start_line + anchorOffset,
|
|
437
463
|
full_start_line: chunk.start_line,
|
|
438
464
|
full_end_line: chunk.end_line,
|
|
465
|
+
match_start_line: chunk.start_line,
|
|
466
|
+
match_end_line: chunk.end_line,
|
|
467
|
+
symbol: declaredSymbol(chunk.content),
|
|
439
468
|
},
|
|
440
469
|
};
|
|
441
470
|
}
|
|
@@ -460,8 +489,20 @@ export function applySnippetsToResults(
|
|
|
460
489
|
});
|
|
461
490
|
return contextualResults.map(({ result, anchorLine }, rank) => {
|
|
462
491
|
const { chunk, meta } = trimChunkToSnippet(result.chunk, query, radius, anchorLine);
|
|
492
|
+
// `result.chunk` may be the whole source file; the ranked chunk is the symbol-sized match.
|
|
493
|
+
const ranked = results[rank]?.chunk;
|
|
463
494
|
const snippet = compactSupportingSnippet(
|
|
464
|
-
{
|
|
495
|
+
{
|
|
496
|
+
chunk,
|
|
497
|
+
meta: ranked
|
|
498
|
+
? {
|
|
499
|
+
...meta,
|
|
500
|
+
match_start_line: ranked.start_line,
|
|
501
|
+
match_end_line: ranked.end_line,
|
|
502
|
+
symbol: declaredSymbol(ranked.content) ?? meta.symbol,
|
|
503
|
+
}
|
|
504
|
+
: meta,
|
|
505
|
+
},
|
|
465
506
|
rank === 0 ? primaryMaxLines : supportingMaxLines,
|
|
466
507
|
);
|
|
467
508
|
return { result: { chunk: snippet.chunk, score: result.score }, meta: snippet.meta };
|
package/src/utils.ts
CHANGED
|
@@ -210,6 +210,9 @@ export function formatResults(
|
|
|
210
210
|
anchor_line: entry.meta.anchor_line,
|
|
211
211
|
full_start_line: entry.meta.full_start_line,
|
|
212
212
|
full_end_line: entry.meta.full_end_line,
|
|
213
|
+
match_start_line: entry.meta.match_start_line,
|
|
214
|
+
match_end_line: entry.meta.match_end_line,
|
|
215
|
+
...(entry.meta.symbol ? { symbol: entry.meta.symbol } : {}),
|
|
213
216
|
},
|
|
214
217
|
score,
|
|
215
218
|
};
|