@takara-ai/miru-code 1.0.4 → 1.1.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.
@@ -1,5 +1,6 @@
1
+ import { quantizedDotFlat } from "./int8-dot.ts";
1
2
  import type { SemanticIndex } from "./semantic-index.ts";
2
- import { selectTopKByDistance } from "./topk.ts";
3
+ import { TopKDistanceCollector } from "./topk.ts";
3
4
 
4
5
  export interface QuantizedVector {
5
6
  codes: Int8Array;
@@ -28,54 +29,49 @@ export function quantizeVector(vector: Float32Array): QuantizedVector {
28
29
  return { codes, scale };
29
30
  }
30
31
 
31
- function quantizedDot(query: QuantizedVector, docCodes: Int8Array, docScale: number): number {
32
- let sum = 0;
33
- const q = query.codes;
34
- for (let i = 0; i < q.length; i++) {
35
- sum += (q[i] ?? 0) * (docCodes[i] ?? 0);
36
- }
37
- return sum * query.scale * docScale;
38
- }
39
-
40
- /** int8 codes + per-vector scale; ~4x smaller than float32 at same dimensionality. */
32
+ /** Production semantic index: symmetric int8 codes with per-vector scale (~4x less RAM than float32). */
41
33
  export class QuantizedVectorIndex implements SemanticIndex {
42
- private readonly codes: Int8Array[];
34
+ private readonly codes: Int8Array;
43
35
  private readonly scales: Float32Array;
36
+ private readonly count: number;
44
37
  private readonly dim: number;
45
38
 
46
39
  constructor(vectors: Float32Array[]) {
47
40
  if (vectors.length === 0) {
41
+ this.count = 0;
48
42
  this.dim = 0;
49
- this.codes = [];
43
+ this.codes = new Int8Array(0);
50
44
  this.scales = new Float32Array(0);
51
45
  return;
52
46
  }
47
+ this.count = vectors.length;
53
48
  this.dim = vectors[0]?.length ?? 0;
54
- this.codes = new Array(vectors.length);
55
- this.scales = new Float32Array(vectors.length);
56
- for (let i = 0; i < vectors.length; i++) {
49
+ this.codes = new Int8Array(this.count * this.dim);
50
+ this.scales = new Float32Array(this.count);
51
+ for (let i = 0; i < this.count; i++) {
57
52
  const vector = vectors[i];
58
53
  if (!vector) {
59
54
  continue;
60
55
  }
61
56
  const { codes, scale } = quantizeVector(vector);
62
- this.codes[i] = codes;
57
+ this.codes.set(codes, i * this.dim);
63
58
  this.scales[i] = scale;
64
59
  }
65
60
  }
66
61
 
67
62
  static fromPersisted(
68
- codes: Int8Array[],
63
+ codes: Int8Array,
69
64
  scales: Float32Array,
65
+ count: number,
70
66
  dim: number,
71
67
  ): QuantizedVectorIndex {
72
68
  const index = Object.create(QuantizedVectorIndex.prototype) as QuantizedVectorIndex;
73
- Object.assign(index, { dim, codes, scales });
69
+ Object.assign(index, { codes, scales, count, dim });
74
70
  return index;
75
71
  }
76
72
 
77
73
  get size(): number {
78
- return this.codes.length;
74
+ return this.count;
79
75
  }
80
76
 
81
77
  get dimensions(): number {
@@ -83,18 +79,21 @@ export class QuantizedVectorIndex implements SemanticIndex {
83
79
  }
84
80
 
85
81
  memoryBytes(): number {
86
- return this.codes.length * this.dim + this.scales.byteLength;
82
+ return this.count * this.dim + this.scales.byteLength;
87
83
  }
88
84
 
89
85
  vectorAt(docIndex: number): Float32Array {
90
- const codes = this.codes[docIndex];
86
+ if (docIndex < 0 || docIndex >= this.count) {
87
+ throw new Error(`Missing quantized vector at index ${docIndex}`);
88
+ }
89
+ const offset = docIndex * this.dim;
91
90
  const scale = this.scales[docIndex];
92
- if (!codes || scale === undefined) {
91
+ if (scale === undefined) {
93
92
  throw new Error(`Missing quantized vector at index ${docIndex}`);
94
93
  }
95
- const out = new Float32Array(codes.length);
96
- for (let i = 0; i < codes.length; i++) {
97
- out[i] = (codes[i] ?? 0) * scale;
94
+ const out = new Float32Array(this.dim);
95
+ for (let i = 0; i < this.dim; i++) {
96
+ out[i] = (this.codes[offset + i] ?? 0) * scale;
98
97
  }
99
98
  let norm = 0;
100
99
  for (let i = 0; i < out.length; i++) {
@@ -113,38 +112,46 @@ export class QuantizedVectorIndex implements SemanticIndex {
113
112
  query(
114
113
  queryVector: Float32Array,
115
114
  k: number,
116
- selector?: number[],
115
+ selector?: readonly number[],
117
116
  ): { indices: number[]; distances: number[] } {
118
117
  if (k < 1) {
119
118
  throw new Error(`k should be >= 1, is now ${k}`);
120
119
  }
121
- if (this.size === 0) {
120
+ if (this.count === 0) {
122
121
  return { indices: [], distances: [] };
123
122
  }
124
123
 
125
124
  const q = quantizeVector(queryVector);
126
- const indices = selector ?? Array.from({ length: this.size }, (_, i) => i);
127
- const effectiveK = Math.min(k, indices.length);
125
+ const effectiveK = Math.min(k, selector?.length ?? this.count);
128
126
  if (effectiveK === 0) {
129
127
  return { indices: [], distances: [] };
130
128
  }
131
129
 
132
- const top = selectTopKByDistance(
133
- indices.flatMap((idx) => {
134
- const codes = this.codes[idx];
135
- if (!codes) {
136
- return [];
130
+ const collector = new TopKDistanceCollector(effectiveK);
131
+ if (selector) {
132
+ for (const idx of selector) {
133
+ if (idx < 0 || idx >= this.count) {
134
+ continue;
137
135
  }
138
136
  const scale = this.scales[idx];
139
137
  if (scale === undefined) {
140
- return [];
138
+ continue;
141
139
  }
142
- const similarity = quantizedDot(q, codes, scale);
143
- return [{ index: idx, distance: 1 - similarity }];
144
- }),
145
- effectiveK,
146
- );
140
+ const similarity = quantizedDotFlat(q, this.codes, idx * this.dim, this.dim, scale);
141
+ collector.offer(idx, 1 - similarity);
142
+ }
143
+ } else {
144
+ for (let i = 0; i < this.count; i++) {
145
+ const scale = this.scales[i];
146
+ if (scale === undefined) {
147
+ continue;
148
+ }
149
+ const similarity = quantizedDotFlat(q, this.codes, i * this.dim, this.dim, scale);
150
+ collector.offer(i, 1 - similarity);
151
+ }
152
+ }
147
153
 
154
+ const top = collector.finish();
148
155
  return {
149
156
  indices: top.map((t) => t.index),
150
157
  distances: top.map((t) => t.distance),
@@ -152,23 +159,11 @@ export class QuantizedVectorIndex implements SemanticIndex {
152
159
  }
153
160
 
154
161
  async save(dir: string): Promise<void> {
155
- const { mkdir } = await import("node:fs/promises");
156
- await mkdir(dir, { recursive: true });
157
-
158
- const count = this.size;
159
- const dim = this.dim;
160
- const flatCodes = new Int8Array(count * dim);
161
- for (let i = 0; i < count; i++) {
162
- const codes = this.codes[i];
163
- if (codes) {
164
- flatCodes.set(codes, i * dim);
165
- }
166
- }
167
- await Bun.write(`${dir}/codes.bin`, flatCodes);
162
+ await Bun.write(`${dir}/codes.bin`, this.codes);
168
163
  await Bun.write(`${dir}/scales.bin`, this.scales);
169
164
  await Bun.write(
170
165
  `${dir}/meta.json`,
171
- JSON.stringify({ count, dimensions: dim, storage: "int8" }),
166
+ JSON.stringify({ count: this.count, dimensions: this.dim, storage: "int8" }),
172
167
  );
173
168
  }
174
169
 
@@ -178,15 +173,8 @@ export class QuantizedVectorIndex implements SemanticIndex {
178
173
  dimensions: number;
179
174
  storage?: string;
180
175
  };
181
- const rawCodes = new Int8Array(await Bun.file(`${dir}/codes.bin`).arrayBuffer());
176
+ const codes = new Int8Array(await Bun.file(`${dir}/codes.bin`).arrayBuffer());
182
177
  const scales = new Float32Array(await Bun.file(`${dir}/scales.bin`).arrayBuffer());
183
- const index = QuantizedVectorIndex.fromPersisted(
184
- Array.from({ length: meta.count }, (_, i) =>
185
- rawCodes.subarray(i * meta.dimensions, (i + 1) * meta.dimensions),
186
- ),
187
- scales,
188
- meta.dimensions,
189
- );
190
- return index;
178
+ return QuantizedVectorIndex.fromPersisted(codes, scales, meta.count, meta.dimensions);
191
179
  }
192
180
  }
@@ -6,6 +6,6 @@ export interface SemanticIndex {
6
6
  query(
7
7
  queryVector: Float32Array,
8
8
  k: number,
9
- selector?: number[],
9
+ selector?: readonly number[],
10
10
  ): { indices: number[]; distances: number[] };
11
11
  }
@@ -1,7 +1,7 @@
1
1
  import type { Chunk } from "../types.ts";
2
2
 
3
3
  export function selectorToMask(
4
- selector: number[] | null | undefined,
4
+ selector: readonly number[] | null | undefined,
5
5
  size: number,
6
6
  ): boolean[] | undefined {
7
7
  if (!selector || selector.length === 0) {
package/src/index/topk.ts CHANGED
@@ -3,7 +3,7 @@ export interface TopKDistanceEntry {
3
3
  distance: number;
4
4
  }
5
5
 
6
- /** Keep the k smallest distances (best cosine matches). */
6
+ /** Keep the k smallest distances (best cosine matches). Legacy reference implementation. */
7
7
  export function selectTopKByDistance(
8
8
  entries: Iterable<TopKDistanceEntry>,
9
9
  k: number,
@@ -25,6 +25,83 @@ export function selectTopKByDistance(
25
25
  return top;
26
26
  }
27
27
 
28
+ /**
29
+ * Incremental top-k by smallest distance using a size-k max-heap.
30
+ * Avoids materializing all candidate scores before selection.
31
+ */
32
+ export class TopKDistanceCollector {
33
+ private readonly heap: TopKDistanceEntry[] = [];
34
+
35
+ constructor(private readonly k: number) {}
36
+
37
+ offer(index: number, distance: number): void {
38
+ if (this.k <= 0) {
39
+ return;
40
+ }
41
+ if (this.heap.length < this.k) {
42
+ this.heap.push({ index, distance });
43
+ this.bubbleUp(this.heap.length - 1);
44
+ return;
45
+ }
46
+ const worst = this.heap[0];
47
+ if (worst && distance < worst.distance) {
48
+ this.heap[0] = { index, distance };
49
+ this.siftDown(0);
50
+ }
51
+ }
52
+
53
+ finish(): TopKDistanceEntry[] {
54
+ return [...this.heap].sort((a, b) => a.distance - b.distance);
55
+ }
56
+
57
+ private parent(i: number): number {
58
+ return (i - 1) >> 1;
59
+ }
60
+
61
+ private left(i: number): number {
62
+ return (i << 1) + 1;
63
+ }
64
+
65
+ private bubbleUp(i: number): void {
66
+ while (i > 0) {
67
+ const p = this.parent(i);
68
+ const here = this.heap[i];
69
+ const parent = this.heap[p];
70
+ if (!here || !parent || here.distance <= parent.distance) {
71
+ break;
72
+ }
73
+ this.heap[i] = parent;
74
+ this.heap[p] = here;
75
+ i = p;
76
+ }
77
+ }
78
+
79
+ private siftDown(i: number): void {
80
+ while (true) {
81
+ const left = this.left(i);
82
+ if (left >= this.heap.length) {
83
+ break;
84
+ }
85
+ const right = left + 1;
86
+ let largest = left;
87
+ if (
88
+ right < this.heap.length &&
89
+ (this.heap[right]?.distance ?? -Infinity) > (this.heap[left]?.distance ?? -Infinity)
90
+ ) {
91
+ largest = right;
92
+ }
93
+ const here = this.heap[i];
94
+ const child = this.heap[largest];
95
+ if (!here || !child || child.distance <= here.distance) {
96
+ break;
97
+ }
98
+ this.heap[i] = child;
99
+ this.heap[largest] = here;
100
+ i = largest;
101
+ }
102
+ }
103
+ }
104
+
28
105
  /** Indices of the k highest scores in a dense array. */
29
106
  export function selectTopKScoreIndices(scores: number[], k: number): number[] {
30
107
  if (k <= 0 || scores.length === 0) {
@@ -4,7 +4,12 @@ import type { SemanticIndex } from "./semantic-index.ts";
4
4
 
5
5
  export type SemanticStorage = "int8" | "float32";
6
6
 
7
- /** Default: int8 quantized vectors (~4x less RAM). Set MIRU_FLOAT_VECTORS=1 for float32. */
7
+ /**
8
+ * Production default: int8 quantized vectors (~4x less RAM than float32).
9
+ *
10
+ * Set `MIRU_FLOAT_VECTORS=1` to build/load float32 instead. That path exists for
11
+ * debugging, precision baselines, and quantization A/B tests — not normal use.
12
+ */
8
13
  export function resolveSemanticStorage(): SemanticStorage {
9
14
  return process.env.MIRU_FLOAT_VECTORS === "1" ? "float32" : "int8";
10
15
  }
package/src/index.ts CHANGED
@@ -1,3 +1,25 @@
1
+ /**
2
+ * Public library entry point for programmatic Miru usage.
3
+ *
4
+ * Import from here rather than deep paths so internal module layout can change
5
+ * without breaking consumers. Typical flow:
6
+ *
7
+ * 1. `MiruIndex.fromPath` / `fromGit` / `fromSource` — build or load an index
8
+ * 2. `index.search` — hybrid keyword + semantic retrieval
9
+ * 3. `findIndexCachePath` / `resolveCacheFolder` — inspect or manage on-disk cache
10
+ */
11
+
12
+ /** Remove cached index bundles (all or for one source key). */
1
13
  export { clearCache, findIndexCachePath, resolveCacheFolder } from "./cache.ts";
14
+
15
+ /** Main index type: chunking, BM25, embeddings, search, and persistence. */
2
16
  export { MiruIndex } from "./miru-index.ts";
3
- export type { Chunk, ContentType, SearchResult } from "./types.ts";
17
+
18
+ export type {
19
+ /** A searchable text span from a source file (path, line range, content). */
20
+ Chunk,
21
+ /** What to index: source code, markdown docs, or both. */
22
+ ContentType,
23
+ /** Ranked hit returned by `search` or `findRelated`. */
24
+ SearchResult,
25
+ } from "./types.ts";
@@ -1,5 +1,4 @@
1
- import { mkdir, unlink, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
1
+ import { unlink } from "node:fs/promises";
3
2
  import { type InstallAction, MIRU_END, MIRU_START } from "./agents.ts";
4
3
 
5
4
  const CODEX_MCP_HEADER = "[mcp_servers.miru]";
@@ -114,8 +113,7 @@ export async function mergeJsonMember(
114
113
 
115
114
  const next = `${JSON.stringify(parsed, null, 2)}\n`;
116
115
 
117
- await mkdir(dirname(path), { recursive: true });
118
- await writeFile(path, next, "utf-8");
116
+ await Bun.write(path, next);
119
117
  return existed ? "updated" : "created";
120
118
  }
121
119
 
@@ -156,12 +154,11 @@ export async function removeJsonMember(
156
154
  return "removed";
157
155
  }
158
156
 
159
- await writeFile(path, next, "utf-8");
157
+ await Bun.write(path, next);
160
158
  return "removed";
161
159
  }
162
160
 
163
161
  export async function replaceOrAppendMarked(path: string, content: string): Promise<InstallAction> {
164
- await mkdir(dirname(path), { recursive: true });
165
162
  const existed = await Bun.file(path).exists();
166
163
  const existing = existed ? await Bun.file(path).text() : "";
167
164
 
@@ -175,13 +172,13 @@ export async function replaceOrAppendMarked(path: string, content: string): Prom
175
172
  if (updated === existing) {
176
173
  return "unchanged";
177
174
  }
178
- await writeFile(path, updated, "utf-8");
175
+ await Bun.write(path, updated);
179
176
  return "updated";
180
177
  }
181
178
 
182
179
  const separator =
183
180
  existing && !existing.endsWith("\n\n") ? (existing.endsWith("\n") ? "\n" : "\n\n") : "";
184
- await writeFile(path, `${existing}${separator}${content}`, "utf-8");
181
+ await Bun.write(path, `${existing}${separator}${content}`);
185
182
  return existed ? "updated" : "created";
186
183
  }
187
184
 
@@ -208,7 +205,7 @@ export async function removeMarked(path: string): Promise<InstallAction> {
208
205
  return "removed";
209
206
  }
210
207
 
211
- await writeFile(path, withNewline, "utf-8");
208
+ await Bun.write(path, withNewline);
212
209
  return "removed";
213
210
  }
214
211
 
@@ -237,7 +234,6 @@ function stripTomlSection(text: string, header: string): string {
237
234
  }
238
235
 
239
236
  export async function mergeTomlBlock(path: string): Promise<InstallAction> {
240
- await mkdir(dirname(path), { recursive: true });
241
237
  const existed = await Bun.file(path).exists();
242
238
  const existing = existed ? await Bun.file(path).text() : "";
243
239
 
@@ -247,7 +243,7 @@ export async function mergeTomlBlock(path: string): Promise<InstallAction> {
247
243
 
248
244
  const base = stripTomlSection(existing, CODEX_MCP_HEADER).replace(/\n+$/, "");
249
245
  const next = base.length > 0 ? `${base}\n\n${CODEX_MCP_BLOCK}` : CODEX_MCP_BLOCK;
250
- await writeFile(path, next.endsWith("\n") ? next : `${next}\n`, "utf-8");
246
+ await Bun.write(path, next.endsWith("\n") ? next : `${next}\n`);
251
247
  return existed ? "updated" : "created";
252
248
  }
253
249
 
@@ -267,6 +263,6 @@ export async function removeTomlBlock(path: string): Promise<InstallAction> {
267
263
  return "removed";
268
264
  }
269
265
 
270
- await writeFile(path, `${remaining}\n`, "utf-8");
266
+ await Bun.write(path, `${remaining}\n`);
271
267
  return "removed";
272
268
  }
@@ -1,5 +1,5 @@
1
- import { mkdir, unlink, writeFile } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
1
+ import { unlink } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
3
  import type { HooksFormat, InstallAction } from "../agents.ts";
4
4
  import { stripJsonComments } from "../config.ts";
5
5
 
@@ -121,8 +121,7 @@ async function writeJson(
121
121
  data: Record<string, unknown>,
122
122
  existed: boolean,
123
123
  ): Promise<InstallAction> {
124
- await mkdir(dirname(path), { recursive: true });
125
- await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
124
+ await Bun.write(path, `${JSON.stringify(data, null, 2)}\n`);
126
125
  return existed ? "updated" : "created";
127
126
  }
128
127
 
@@ -220,7 +219,7 @@ async function removeHooksSection(
220
219
  return "removed";
221
220
  }
222
221
 
223
- await writeFile(path, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8");
222
+ await Bun.write(path, `${JSON.stringify(parsed, null, 2)}\n`);
224
223
  return "removed";
225
224
  }
226
225
 
@@ -327,8 +326,7 @@ export async function mergeVscodeHooks(path: string): Promise<InstallAction> {
327
326
  return "unchanged";
328
327
  }
329
328
  }
330
- await mkdir(dirname(path), { recursive: true });
331
- await writeFile(path, `${JSON.stringify(content, null, 2)}\n`, "utf-8");
329
+ await Bun.write(path, `${JSON.stringify(content, null, 2)}\n`);
332
330
  return existed ? "updated" : "created";
333
331
  }
334
332
 
@@ -403,8 +401,7 @@ export async function mergeOpenCodePlugin(pluginPath: string): Promise<InstallAc
403
401
  if (existed && (await Bun.file(pluginPath).text()) === content) {
404
402
  return "unchanged";
405
403
  }
406
- await mkdir(dirname(pluginPath), { recursive: true });
407
- await writeFile(pluginPath, content, "utf-8");
404
+ await Bun.write(pluginPath, content);
408
405
  return existed ? "updated" : "created";
409
406
  }
410
407
 
@@ -1,5 +1,4 @@
1
- import { mkdir, unlink, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
1
+ import { unlink } from "node:fs/promises";
3
2
  import { loadAgentTemplate } from "../agents.ts";
4
3
  import { brandTitle, dim, divider, green, hint, success, writeStdout } from "../cli-ui.ts";
5
4
  import { ensureCredentials } from "../setup.ts";
@@ -119,8 +118,7 @@ async function applyCursorRules(
119
118
  }
120
119
 
121
120
  const existed = await Bun.file(path).exists();
122
- await mkdir(dirname(path), { recursive: true });
123
- await writeFile(path, `${CURSOR_RULES_MDC.trim()}\n`, "utf-8");
121
+ await Bun.write(path, `${CURSOR_RULES_MDC.trim()}\n`);
124
122
  return { path, action: existed ? "updated" : "created" };
125
123
  }
126
124
 
@@ -142,8 +140,7 @@ async function applySubagent(agent: AgentTarget, mode: InstallMode): Promise<Wri
142
140
  try {
143
141
  const content = await loadAgentTemplate(templateId);
144
142
  const existed = await Bun.file(dest).exists();
145
- await mkdir(dirname(dest), { recursive: true });
146
- await writeFile(dest, content, "utf-8");
143
+ await Bun.write(dest, content);
147
144
  return { path: dest, action: existed ? "updated" : "created" };
148
145
  } catch {
149
146
  return { path: dest, action: "error" };
@@ -2,8 +2,7 @@
2
2
 
3
3
  export const SNIPPET_GUIDANCE =
4
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` " +
6
- "(or `start_line`) — do not re-search or Read the whole file.";
5
+ "When a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — do not re-search or Read the whole file.";
7
6
 
8
7
  export interface NativeToolNames {
9
8
  explorationDenied: string;
@@ -14,11 +13,12 @@ export interface NativeToolNames {
14
13
  export function buildSearchPolicyTable(native: NativeToolNames): string {
15
14
  return `| Task | Use | Not |
16
15
  |------|-----|-----|
16
+ | Quick lookup — where is X handled/defined? | Miru MCP \`search\` (once) | ${native.grep}, ${native.explorationDenied} |
17
17
  | How/where/what handles X? | Miru MCP \`search\` (once) | ${native.explorationDenied}, repeat searches |
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 exists? | ${native.grep} | Miru |
21
+ | Exact literal string in a file? | ${native.grep} | Miru |
22
22
  | Edit a known file:line | ${native.read} (after Miru found it) | ${native.read}-before-search |`;
23
23
  }
24
24
 
@@ -29,7 +29,7 @@ ${SNIPPET_GUIDANCE}
29
29
 
30
30
  Use Miru MCP tools:
31
31
  - \`search\` — one call per question; pass project root as \`repo\`
32
- - \`expand\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\` or \`start_line\`)
32
+ - \`expand\` — more context in the same file when \`truncated: true\` (\`file_path\` + \`anchor_line\`)
33
33
  - \`find_related\` — similar code in other files (hits may also be snippets; use \`expand\` if truncated)
34
34
 
35
35
  Stop rules:
@@ -49,7 +49,7 @@ export function buildMcpWorkflow(native: NativeToolNames): string {
49
49
  return `### MCP workflow
50
50
 
51
51
  1. Call \`search\` with \`repo\` set to the project root (local path or https:// git URL).
52
- 2. If a hit has \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\` (or \`start_line\`).
52
+ 2. If a hit has \`truncated: true\`, call \`expand\` with \`file_path\` and \`anchor_line\`.
53
53
  3. Use \`find_related\` to trace similar code in other files — not for more context in the same file.
54
54
  4. ${native.read} via \`absolute_path\` only when editing or when \`expand\` still lacks context.`;
55
55
  }
@@ -113,20 +113,23 @@ When Miru MCP is connected, call \`search\` once per question. On \`truncated: t
113
113
  export const SUBAGENT_BODY = buildSubagentBody(DEFAULT_NATIVE_TOOLS);
114
114
 
115
115
  export const MCP_SERVER_INSTRUCTIONS =
116
- "DO NOT use Grep, Glob, SemanticSearch, or Read to explore code when this server is available. " +
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. " +
117
+ "DO NOT use Grep, grep_search, codebase_search, Glob, SemanticSearch, or Read to explore code when this server is available. " +
117
118
  `${SNIPPET_GUIDANCE} ` +
118
119
  "Workflow: (1) `search` once per question — do NOT re-search with paraphrases. " +
119
- "(2) On `truncated: true`, call `expand` with `file_path` and `anchor_line` (or `start_line`) — do NOT read the whole file. " +
120
+ "(2) On `truncated: true`, call `expand` with `file_path` and `anchor_line` — do NOT read the whole file. " +
120
121
  "(3) `find_related` traces similar code in other files, not more context in the same file. " +
121
122
  "Always pass the project root as `repo`. Local repos return `absolute_path` on each hit — use Read only to edit. " +
122
- "Native search is only for exact literal confirmation or files outside the indexed repo.";
123
+ "Native grep is only for confirming a known exact literal (env var name, quoted string) or files outside the indexed repo.";
123
124
 
124
125
  export const MCP_SEARCH_TOOL_DESCRIPTION =
125
- "DEFAULT code exploration tool. Returns compact snippets (~±15 lines). One call per question is usually enough. " +
126
- "When a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` (or `start_line`) — not re-search or Read.";
126
+ "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. " +
127
+ "Returns compact snippets (~±15 lines). One call per question is usually enough. " +
128
+ "Built-in grep is only when you already have an exact literal string to confirm. " +
129
+ "When a hit has `truncated: true`, call `expand` with `file_path` and `anchor_line` — not re-search or Read.";
127
130
 
128
131
  export const MCP_EXPAND_TOOL_DESCRIPTION =
129
- "More context in the SAME file as a search hit. Pass `file_path` + `anchor_line` or `start_line` from a truncated hit; " +
132
+ "More context in the SAME file as a search hit. Pass `file_path` + `anchor_line` from the hit; " +
130
133
  "returns adjacent indexed chunks. Use when `truncated: true` — NOT for similar code in other files (use find_related). " +
131
134
  "Prefer this over re-searching or reading the whole file.";
132
135
 
@@ -118,7 +118,7 @@ export class IndexCache {
118
118
  const task = (async () => {
119
119
  const index = await MiruIndex.fromSource(source, this.content, undefined, ref);
120
120
  if (!isGitUrl(source)) {
121
- await index.saveToDefaultCache(resolve(source));
121
+ await index.saveToCache(resolve(source));
122
122
  }
123
123
  return index;
124
124
  })();
@@ -254,7 +254,7 @@ export class IndexCache {
254
254
  try {
255
255
  await index.applyFileChanges(paths);
256
256
  if (!isGitUrl(source)) {
257
- await index.persistToCache(resolve(source));
257
+ await index.saveToCache(resolve(source), { force: true });
258
258
  }
259
259
  } catch {
260
260
  for (const p of paths) {