@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.
- package/README.md +68 -13
- package/package.json +1 -1
- package/src/agents/copilot.md +1 -1
- package/src/agents.ts +2 -4
- package/src/brand-banner.ts +151 -0
- package/src/cache.ts +30 -2
- package/src/cli-ui.ts +40 -61
- package/src/cli.ts +23 -6
- package/src/concurrency.ts +36 -29
- package/src/credentials.ts +4 -7
- package/src/embeddings/openai.ts +10 -7
- package/src/env-files.ts +10 -7
- package/src/help.ts +18 -22
- package/src/index/chunk-selector.ts +57 -0
- package/src/index/dense.ts +75 -54
- package/src/index/entry-chunks.ts +1 -1
- package/src/index/int8-dot.ts +55 -0
- package/src/index/persistence.ts +0 -2
- package/src/index/quantize.ts +52 -64
- package/src/index/semantic-index.ts +1 -1
- package/src/index/sparse.ts +1 -1
- package/src/index/topk.ts +78 -1
- package/src/index/vector-storage.ts +6 -1
- package/src/index.ts +23 -1
- package/src/installer/config.ts +8 -12
- package/src/installer/hooks/install.ts +6 -9
- package/src/installer/installer.ts +3 -6
- package/src/installer/search-policy.ts +14 -11
- package/src/mcp/index-cache.ts +2 -2
- package/src/mcp/server.ts +18 -18
- package/src/miru-index.ts +116 -19
- package/src/search.ts +4 -4
- package/src/setup.ts +18 -7
- package/src/terminal.ts +171 -0
- package/src/version.ts +21 -10
package/src/index/quantize.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { quantizedDotFlat } from "./int8-dot.ts";
|
|
1
2
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
55
|
-
this.scales = new Float32Array(
|
|
56
|
-
for (let i = 0; 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
|
|
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, {
|
|
69
|
+
Object.assign(index, { codes, scales, count, dim });
|
|
74
70
|
return index;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
get size(): number {
|
|
78
|
-
return this.
|
|
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.
|
|
82
|
+
return this.count * this.dim + this.scales.byteLength;
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
vectorAt(docIndex: number): Float32Array {
|
|
90
|
-
|
|
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 (
|
|
91
|
+
if (scale === undefined) {
|
|
93
92
|
throw new Error(`Missing quantized vector at index ${docIndex}`);
|
|
94
93
|
}
|
|
95
|
-
const out = new Float32Array(
|
|
96
|
-
for (let i = 0; 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.
|
|
120
|
+
if (this.count === 0) {
|
|
122
121
|
return { indices: [], distances: [] };
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
const q = quantizeVector(queryVector);
|
|
126
|
-
const
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
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
|
-
|
|
138
|
+
continue;
|
|
141
139
|
}
|
|
142
|
-
const similarity =
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/index/sparse.ts
CHANGED
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
|
-
/**
|
|
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
|
-
|
|
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";
|
package/src/installer/config.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
266
|
+
await Bun.write(path, `${remaining}\n`);
|
|
271
267
|
return "removed";
|
|
272
268
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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\`
|
|
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
|
|
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
|
-
"
|
|
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`
|
|
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
|
|
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
|
-
"
|
|
126
|
-
"
|
|
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`
|
|
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
|
|
package/src/mcp/index-cache.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
257
|
+
await index.saveToCache(resolve(source), { force: true });
|
|
258
258
|
}
|
|
259
259
|
} catch {
|
|
260
260
|
for (const p of paths) {
|