@takara-ai/miru-code 1.8.1 → 1.8.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,8 +1,8 @@
1
1
  /** Shared code-search policy text for instructions, Cursor rules, and sub-agents. */
2
2
 
3
3
  export const SNIPPET_GUIDANCE =
4
- "Search returns compact snippets (~±15 lines around the best match). " +
5
- "When a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — do not re-search or Read the whole file.";
4
+ "Search returns compact snippets (~±15 lines). On `truncated: true`, call `expand` with " +
5
+ "`file_path` and `anchor_line` — only if the snippet doesn't already answer the question.";
6
6
 
7
7
  export interface NativeToolNames {
8
8
  explorationDenied: string;
@@ -13,30 +13,36 @@ export interface NativeToolNames {
13
13
  export function buildSearchPolicyTable(native: NativeToolNames): string {
14
14
  return `| Task | Use | Not |
15
15
  |------|-----|-----|
16
- | Quick lookup — where is X handled/defined? | Miru MCP \`search\` (once) | ${native.grep}, ${native.explorationDenied} |
17
- | How/where/what handles X? | Miru MCP \`search\` (once) | ${native.explorationDenied}, repeat searches |
16
+ | Exact literal (identifier, quoted string, env var, error code) | Miru MCP \`locate\` | Miru \`search\`, ${native.grep} |
17
+ | How/where/what handles X? (conceptual) | Miru MCP \`search\` (once) | ${native.grep}, ${native.explorationDenied} |
18
18
  | Same file, more context | Miru MCP \`expand\` on \`truncated: true\` | Re-search, ${native.read} whole file |
19
19
  | Similar code elsewhere | Miru MCP \`find_related\` | ${native.grep} chains |
20
20
  | Search docs or config | Miru \`search\` | ${native.grep} README paths |
21
- | Exact literal string in a file? | Miru MCP \`locate\` | ${native.grep}, Miru \`search\` |
22
21
  | Edit a known file:line | ${native.read} (after Miru found it) | ${native.read}-before-search |`;
23
22
  }
24
23
 
25
24
  export function buildSearchPolicyBody(native: NativeToolNames): string {
26
25
  return `DO NOT use ${native.explorationDenied} to explore how code works when Miru MCP is available.
27
26
 
27
+ **Check first:** exact token (identifier, quoted string, env var, error code) → \`locate\`. Otherwise → \`search\`.
28
+
28
29
  ${SNIPPET_GUIDANCE}
29
30
 
30
31
  Use Miru MCP tools:
31
- - \`search\` — one call per question; pass project root as \`repo\`
32
32
  - \`locate\` — exact substring (env var, symbol, error code); prefer \`mode=count\` or \`locations\`
33
+ - \`search\` — one call per conceptual question; pass project root as \`repo\`
33
34
  - \`expand\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\`)
34
35
  - \`find_related\` — similar code in other files (hits may also be snippets; use \`expand\` if truncated)
35
36
 
37
+ Call shapes:
38
+ - \`locate(literal="<exact substring>", repo="<project root>")\` — prefer \`mode="locations"\`/\`"count"\`
39
+ - \`search(query="<natural language>", repo="<project root>")\`
40
+
36
41
  Stop rules:
37
- - Answer from the first \`search\` — do not re-search with paraphrases
38
- - On \`truncated: true\`, call \`expand\` — not another \`search\` or a full-file ${native.read}
39
- - ${native.read} is for editing a path Miru already gave you, not for exploration
42
+ - Literal in the request → \`locate\`, not \`search\`
43
+ - One \`search\` per question — no paraphrase retries
44
+ - \`expand\` only if the snippet doesn't already answer
45
+ - ${native.read} only to edit a path Miru already found, not to explore
40
46
 
41
47
  Native tools are allowed ONLY when:
42
48
  - reading a file you already located via Miru, to edit it
@@ -50,7 +56,7 @@ export function buildMcpWorkflow(native: NativeToolNames): string {
50
56
 
51
57
  1. Call \`search\` with \`repo\` set to the project root (local path or https:// git URL).
52
58
  2. For exact literals, call \`locate\` (prefer \`mode=count\` or \`locations\`).
53
- 3. If a hit has \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\`.
59
+ 3. On \`truncated: true\`, call \`expand\` — only if the snippet doesn't already answer.
54
60
  4. Use \`find_related\` to trace similar code in other files — not for more context in the same file.
55
61
  5. ${native.read} via \`absolute_path\` only when editing or when \`expand\` still lacks context.`;
56
62
  }
@@ -64,6 +70,7 @@ miru expand src/auth.ts 42 .
64
70
  miru find-related src/auth.ts 42 .
65
71
  \`\`\`
66
72
 
73
+ After large refactors or CLI-only use, run \`miru clear <path>\` to rebuild the disk cache.
67
74
  If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;
68
75
 
69
76
  export function buildSubagentBody(native: NativeToolNames): string {
@@ -90,16 +97,7 @@ ${SEARCH_POLICY_BODY}
90
97
 
91
98
  Run \`miru setup\` once — the MCP server loads credentials from \`credentials.json\`, and interactive first use can auto-start device login when nothing is stored.
92
99
 
93
- CLI fallback when MCP is unavailable:
94
-
95
- \`\`\`bash
96
- miru search "authentication flow" .
97
- miru expand src/auth.ts 42 .
98
- miru find-related src/auth.ts 42 .
99
- \`\`\`
100
-
101
- After large refactors or CLI-only use, run \`miru clear <path>\` to rebuild the disk cache.
102
- If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\`.`;
100
+ ${CLI_FALLBACK}`;
103
101
 
104
102
  export const CURSOR_RULES_MDC = `---
105
103
  description: Miru MCP is the default for all code exploration
@@ -110,20 +108,19 @@ alwaysApply: true
110
108
 
111
109
  ${SEARCH_POLICY_BODY}
112
110
 
113
- When Miru MCP is connected, call \`search\` once per question. On \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\`. Use \`locate\` for exact literals — not Grep. Never use Cursor SemanticSearch for codebase questions.`;
111
+ When Miru MCP is connected: literal in the request → \`locate\`, not Grep or \`search\`. Otherwise \`search\` once. \`expand\` only if the snippet doesn't already answer. Never use Cursor SemanticSearch for codebase questions.`;
114
112
 
115
113
  export const SUBAGENT_BODY = buildSubagentBody(DEFAULT_NATIVE_TOOLS);
116
114
 
117
115
  export const MCP_SERVER_INSTRUCTIONS =
118
116
  "Miru `search` is your default for all code search queries in indexed repos — the best, fastest, and cheapest way to find code; better than any other tool. " +
119
117
  "DO NOT use Grep, grep_search, codebase_search, Glob, SemanticSearch, or Read to explore code when this server is available. " +
118
+ "Exact token (identifier, quoted string, env var, error code) → `locate`; everything else → `search` once, no paraphrase retries. " +
120
119
  `${SNIPPET_GUIDANCE} ` +
121
- "Workflow: (1) `search` once per question — do NOT re-search with paraphrases. " +
122
- "(2) On `truncated: true`, call `expand` with `file_path` and `anchor_line` — do NOT read the whole file. " +
123
- "(3) `find_related` traces similar code in other files, not more context in the same file. " +
124
- "(4) `locate` finds exact substrings (env vars, symbols, error codes) — prefer over Grep; use mode=count or locations when possible. " +
125
- "Always pass the project root as `repo`. Local repos return `absolute_path` on each hit — use Read only to edit. " +
120
+ "`find_related` traces similar code elsewhere, not more context in the same file. " +
121
+ "Always pass the project root as `repo`; local repos return `absolute_path` — use Read only to edit. " +
126
122
  "Native Grep/Glob only outside the indexed repo or for non-code tasks. " +
123
+ "Default indexed scope is code, config, and docs; an empty result can mean out-of-scope, not nonexistent. " +
127
124
  "On credential errors, call this server's `auth` tool — not the host `mcp_auth` tool.";
128
125
 
129
126
  export const MCP_BENCHMARK_SERVER_INSTRUCTIONS =
@@ -137,25 +134,25 @@ export const MCP_BENCHMARK_SERVER_INSTRUCTIONS =
137
134
 
138
135
  export const MCP_SEARCH_TOOL_DESCRIPTION =
139
136
  "Your default search for all code search queries in this indexed repo — the best, fastest, and cheapest way to find code; better than any other tool. " +
140
- "Returns compact snippets (~±15 lines). One call per question is usually enough. " +
141
- "For exact literals (env vars, symbols, error codes), use `locate` instead. " +
142
- "When a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — not re-search or Read.";
137
+ "Returns compact snippets (~±15 lines). One call per question. " +
138
+ "Exact literal (env var, symbol, error code, quoted text)? Use `locate` instead. " +
139
+ "On `truncated: true`, call `expand` — only if the snippet doesn't already answer the question.";
143
140
 
144
141
  export const MCP_LOCATE_TOOL_DESCRIPTION =
145
- "Exact substring locator over the Miru index. Use for known literals (env vars, symbols, error codes, quoted text) — not meaning-based questions (`search`). " +
142
+ "Exact substring locator over the Miru index. Use for known literals (env vars, symbols, error codes, quoted text) — not meaning-based questions (`search`), " +
143
+ 'even ones phrased as "where is X" or "what handles X" if X is a literal. ' +
146
144
  "Returns ALL matches by default as compact {n,files,hits} — do NOT fall back to Grep/rg when n is large. " +
147
145
  "Prefer mode=locations (or count for totals only); use lines when you need matching line text. " +
148
146
  "Optional `limit` only if you intentionally want a sample. " +
149
- "Pass an array to `literal` to match several spellings in one call; " +
150
- 'or set `match_variants: true` to also match other ways the same word is written in code (e.g. "rateLimit" also finds "rate_limit"). ' +
151
- "Use `include`/`exclude` (gitignore-style globs) to scope to part of a monorepo. " +
152
- "Use `context_lines` in `lines` mode to get surrounding lines inline instead of a follow-up `expand`/Read. " +
147
+ "Pass an array to `literal` for several spellings in one call. " +
148
+ '`match_variants: true` also matches other casings (e.g. "rateLimit" also finds "rate_limit"). ' +
149
+ "`include`/`exclude` (gitignore-style globs) scope to part of a monorepo; " +
150
+ "`context_lines` in `lines` mode gets inline context instead of a follow-up `expand`. " +
153
151
  "In benchmark mode, includes compact token savings vs agent Grep.";
154
152
 
155
153
  export const MCP_EXPAND_TOOL_DESCRIPTION =
156
- "More context in the SAME file as a search hit. Pass `file_path` + `anchor_line` from the hit; " +
157
- "returns adjacent indexed chunks. Use when `truncated: true` — NOT for similar code in other files (use find_related). " +
158
- "Prefer this over re-searching or reading the whole file.";
154
+ "More context in the SAME file as a search hit. Pass `file_path` + `anchor_line` from the hit. " +
155
+ "Use only if `truncated: true` and the snippet doesn't already answer the question — not for similar code elsewhere (use find_related).";
159
156
 
160
157
  export const MCP_FIND_RELATED_TOOL_DESCRIPTION =
161
158
  "Find code similar to a file:line in OTHER parts of the codebase. Results may be snippets; use `expand` when `truncated: true`. " +
@@ -165,5 +162,4 @@ export const MCP_READ_BENCHMARK_TOOL_DESCRIPTION =
165
162
  "Cumulative Miru vs Grep token savings from saved `search` and `locate` calls. Returns compact totals {n,saved,save_pct,miru,grep}. " +
166
163
  "Do not call unless the user asks about savings.";
167
164
 
168
- export const SEARCH_GUARD_EXPAND_HINT =
169
- "If a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — do not re-search or read the whole file.";
165
+ export const SEARCH_GUARD_EXPAND_HINT = SNIPPET_GUIDANCE;
@@ -1,5 +1,6 @@
1
1
  import { watch } from "node:fs";
2
2
  import { relative, resolve } from "node:path";
3
+ import { isCredentialsError } from "../auth/errors.ts";
3
4
  import { walkFiles } from "../index/file-walker.ts";
4
5
  import { getExtensions } from "../index/files.ts";
5
6
  import { normalizeRelativePath, relativePathFromRoot } from "../index/incremental.ts";
@@ -13,7 +14,6 @@ import {
13
14
  } from "../utils.ts";
14
15
 
15
16
  const CACHE_MAX_SIZE = 10;
16
- const BUN_WATCH_RECONCILE_INTERVAL_MS = 500;
17
17
 
18
18
  /** Directory names we skip for MCP fs.watch update triggers (aligned with file-walker). */
19
19
  const WATCH_IGNORED_DIR_NAMES = new Set([
@@ -45,6 +45,8 @@ type CacheEntry = {
45
45
  pendingPaths: Set<string>;
46
46
  flushQueued: boolean;
47
47
  updateChain: Promise<void>;
48
+ /** Dead-credentials failure from a background re-embed; `get()` throws it until a retry clears it. */
49
+ lastError: Error | null;
48
50
  };
49
51
 
50
52
  export function mcpWatchEnabled(): boolean {
@@ -98,6 +100,7 @@ export class IndexCache {
98
100
  pendingPaths: new Set(),
99
101
  flushQueued: false,
100
102
  updateChain: Promise.resolve(),
103
+ lastError: null,
101
104
  };
102
105
  this.entries.set(cacheKey, entry);
103
106
  }
@@ -171,6 +174,9 @@ export class IndexCache {
171
174
  throw new Error(`Failed to load index for ${source}`);
172
175
  }
173
176
  await entry.updateChain;
177
+ if (entry.lastError) {
178
+ throw entry.lastError;
179
+ }
174
180
  return index;
175
181
  }
176
182
 
@@ -183,39 +189,11 @@ export class IndexCache {
183
189
  }
184
190
  }
185
191
 
186
- private queueIndexedPaths(source: string, index: MiruIndex): void {
187
- const cacheKey = computeSourceCacheKey(source);
188
- const entry = this.ensureEntry(cacheKey, source);
189
- for (const chunk of index.chunks) {
190
- entry.pendingPaths.add(normalizeRelativePath(chunk.file_path));
191
- }
192
- this.scheduleFlush(cacheKey, source, entry);
193
- }
194
-
195
- /** macOS recursive fs.watch often omits filename; refresh all indexed paths incrementally. */
196
- private noteAmbiguousDirectoryChange(source: string): void {
197
- const cacheKey = computeSourceCacheKey(source);
198
- const entry = this.entries.get(cacheKey);
199
- if (!entry) {
200
- return;
201
- }
202
-
203
- if (entry.index) {
204
- this.queueIndexedPaths(source, entry.index);
205
- return;
206
- }
207
-
208
- if (entry.task) {
209
- void entry.task.then((index) => {
210
- entry.index = index;
211
- this.queueIndexedPaths(source, index);
212
- });
213
- }
214
- }
215
-
216
192
  private noteFileChange(source: string, filename: string | null | undefined): void {
217
193
  if (!filename) {
218
- this.noteAmbiguousDirectoryChange(source);
194
+ // macOS recursive fs.watch could omit the filename on old Bun releases (pre-1.3.14
195
+ // fs.watch rewrite); confirmed fixed on current Bun. No reconcile fallback for
196
+ // this case anymore -- an event with no filename is simply dropped.
219
197
  return;
220
198
  }
221
199
  if (shouldIgnoreWatchPath(filename)) {
@@ -253,34 +231,38 @@ export class IndexCache {
253
231
  return;
254
232
  }
255
233
 
234
+ const requeue = (err?: unknown): void => {
235
+ if (err !== undefined && isCredentialsError(err)) {
236
+ entry.lastError = err instanceof Error ? err : new Error(String(err));
237
+ }
238
+ for (const p of paths) {
239
+ entry.pendingPaths.add(p);
240
+ }
241
+ };
242
+
256
243
  // Prefer an index already in hand (or published on the entry). Never await
257
244
  // entry.task while that task is itself waiting on this flush — that deadlocks.
258
245
  let index = knownIndex ?? entry.index;
259
246
  if (!index && entry.task) {
260
247
  try {
261
248
  index = await entry.task;
262
- } catch {
263
- for (const p of paths) {
264
- entry.pendingPaths.add(p);
265
- }
249
+ } catch (err) {
250
+ requeue(err);
266
251
  return;
267
252
  }
268
253
  }
269
254
  if (!index) {
270
- for (const p of paths) {
271
- entry.pendingPaths.add(p);
272
- }
255
+ requeue();
273
256
  return;
274
257
  }
275
258
 
276
259
  try {
277
260
  await index.applyFileChanges(paths);
278
- } catch {
279
- for (const p of paths) {
280
- entry.pendingPaths.add(p);
281
- }
261
+ } catch (err) {
262
+ requeue(err);
282
263
  return;
283
264
  }
265
+ entry.lastError = null;
284
266
  if (!isGitUrl(source)) {
285
267
  try {
286
268
  await index.saveToCache(resolve(source), { force: true });
@@ -364,40 +346,13 @@ export class IndexCache {
364
346
  this.noteFileChange(path, filename);
365
347
  });
366
348
  } catch {
367
- // Recursive fs.watch is unavailable on some platforms. The reconciliation
368
- // fallback below keeps local indexes current there.
349
+ // Recursive fs.watch is unavailable on some platforms (no fallback here
350
+ // anymore -- such a local index will only refresh on next cold load).
369
351
  }
370
352
 
371
- // Bun 1.3 on macOS can create a recursive watcher without delivering file
372
- // events. Reconcile periodically in Bun so a silent watcher cannot leave an
373
- // MCP index stale. Native watchers remain the fast path everywhere else.
374
- let reconcileInFlight = false;
375
- const reconcile = (): void => {
376
- if (reconcileInFlight) {
377
- return;
378
- }
379
- const cacheKey = computeSourceCacheKey(path);
380
- const entry = this.entries.get(cacheKey);
381
- if (!entry?.index) {
382
- return;
383
- }
384
- reconcileInFlight = true;
385
- void this.checkAndQueueStaleFiles(path, entry.index, cacheKey).finally(() => {
386
- reconcileInFlight = false;
387
- });
388
- };
389
- const interval =
390
- process.versions.bun || !nativeWatcher
391
- ? setInterval(reconcile, BUN_WATCH_RECONCILE_INTERVAL_MS)
392
- : null;
393
- interval?.unref();
394
-
395
353
  this.watchers.set(resolved, {
396
354
  close: () => {
397
355
  nativeWatcher?.close();
398
- if (interval) {
399
- clearInterval(interval);
400
- }
401
356
  },
402
357
  });
403
358
  }
package/src/mcp/server.ts CHANGED
@@ -226,10 +226,17 @@ export function createMcpServer(
226
226
  inputSchema: {
227
227
  literal: z
228
228
  .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
229
+ .optional()
229
230
  .describe(
230
231
  "Exact substring to find (env var, symbol, error code, quoted text). " +
231
232
  "Pass an array to OR-match several substrings (e.g. spelling/casing variants) in one call.",
232
233
  ),
234
+ query: z
235
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
236
+ .optional()
237
+ .describe(
238
+ "Alias for `literal` — accepted so a call shaped like `search(query=...)` still resolves as an exact-substring lookup.",
239
+ ),
233
240
  repo: z.string().describe(REPO_DESCRIPTION),
234
241
  mode: z
235
242
  .enum(["count", "locations", "lines"])
@@ -270,13 +277,17 @@ export function createMcpServer(
270
277
  .describe("Lines of context before/after each match, like `grep -C` (mode=lines only)."),
271
278
  },
272
279
  },
273
- async ({ literal, repo, ...locateOpts }) => {
280
+ async ({ literal, query, repo, ...locateOpts }) => {
281
+ const lit = literal ?? query;
282
+ if (lit == null) {
283
+ return toolErrorText(new Error("locate requires `literal` (exact substring to find)."));
284
+ }
274
285
  try {
275
286
  const index = await getIndexForRepo(repo, cache);
276
287
 
277
288
  let skip: BenchmarkSkipReason | undefined;
278
289
  const repoRoot = localRepoRoot(repo);
279
- if (benchmark && repoRoot && typeof literal === "string") {
290
+ if (benchmark && repoRoot && typeof lit === "string") {
280
291
  if (locateOpts.limit != null) {
281
292
  skip = "limited_locate";
282
293
  } else if (!selectComparableLiteralSearchTool()) {
@@ -285,7 +296,7 @@ export function createMcpServer(
285
296
  if (!skip) {
286
297
  const comparison = await withGrepTimeoutFallback(() =>
287
298
  benchmarkLocateComparison({
288
- literal,
299
+ literal: lit,
289
300
  repoPath: repoRoot,
290
301
  index,
291
302
  locate: locateOpts,
@@ -302,7 +313,7 @@ export function createMcpServer(
302
313
  skip = "local_repo_only";
303
314
  }
304
315
 
305
- const result = index.locateLiteral(literal, locateOpts);
316
+ const result = index.locateLiteral(lit, locateOpts);
306
317
  const payload = formatLiteralLocate(result);
307
318
  const body = formatLiteralLocateText(payload);
308
319
  return toolText(skip ? withBenchmarkSkippedNote(body, skip) : body);
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type ContentType = "code" | "docs" | "config";
2
2
 
3
- /** Default index corpus: source code plus config (json/yaml/toml/…). */
4
- export const DEFAULT_CONTENT_TYPES: readonly ContentType[] = ["code", "config"];
3
+ /** Default index corpus: source code, config (json/yaml/toml/…), and docs (md/rst/txt). */
4
+ export const DEFAULT_CONTENT_TYPES: readonly ContentType[] = ["code", "config", "docs"];
5
5
 
6
6
  /** Mutable copy of {@link DEFAULT_CONTENT_TYPES} for API defaults. */
7
7
  export function defaultContentTypes(): ContentType[] {
package/src/version.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { join } from "node:path";
2
3
  import packageJson from "../package.json";
3
4
  import { resolveCacheFolder } from "./cache.ts";
@@ -79,7 +80,12 @@ function updateCheckPath(): string {
79
80
 
80
81
  async function readUpdateCheckCache(): Promise<UpdateCheckCache | null> {
81
82
  try {
82
- const raw = await Bun.file(updateCheckPath()).text();
83
+ // node:fs/promises, not Bun.file().text(): a rejecting Bun.file() read
84
+ // (the common case here — no cache on a fresh install) doesn't keep
85
+ // Bun's Windows event loop alive, so the process can exit silently
86
+ // before this ever settles. Confirmed regression in Bun 1.4.0-1.4.2:
87
+ // github.com/oven-sh/bun/issues/39787 (fix open, not yet released).
88
+ const raw = await readFile(updateCheckPath(), "utf8");
83
89
  const parsed = JSON.parse(raw) as UpdateCheckCache;
84
90
  if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string") {
85
91
  return null;