@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 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`**, call **`expand`** with `file_path` and `anchor_line` — not another search or a full-file read.
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 when editing or when `expand` still lacks context.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "mcpName": "ai.takara/miru",
5
5
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
6
6
  "type": "module",
@@ -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. On `truncated: true`, call `expand` with `file_path` and " +
5
- "`anchor_line` — only if the snippet doesn't already answer.";
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
- | Same file, more context | Miru MCP \`expand\` on \`truncated: true\` | Re-search, ${native.read} whole file |
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\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\`)
47
- - \`find_related\` — similar code in other files (hits may also be snippets; use \`expand\` if truncated)
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 the snippet doesn't already answer
57
- - ${native.read} only to edit a path Miru already found, not to explore
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\`, call \`expand\` — only if the snippet doesn't already answer.
73
- 4. Use \`find_related\` to trace similar code in other files — not for more context in the same file.
74
- 5. ${native.read} via \`absolute_path\` only when editing or when \`expand\` still lacks context.`;
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
- "More context in the SAME file as a search hit. Pass `file_path` + `anchor_line` from the hit. " +
163
- "Use only if `truncated: true` and the snippet doesn't already answer the question — not for similar code elsewhere (use find_related).";
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`. " +
@@ -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
- const startLine = chunk.truncated ? chunk.full_start_line : chunk.start_line;
16
- const endLine = chunk.truncated ? chunk.full_end_line : chunk.end_line;
17
- const truncatedNote = chunk.truncated ? " [truncated: call expand for full chunk]" : "";
18
- const header = `${chunk.file_path}:${startLine}-${endLine}${truncatedNote}`;
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
- { chunk, meta },
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
  };