@takara-ai/miru-code 0.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/LICENSE +21 -0
- package/README.md +176 -0
- package/bin/miru.cjs +41 -0
- package/package.json +72 -0
- package/src/agents/claude.md +41 -0
- package/src/agents/copilot.md +41 -0
- package/src/agents/cursor.md +40 -0
- package/src/agents/gemini.md +43 -0
- package/src/agents/kiro.md +43 -0
- package/src/agents/opencode.md +44 -0
- package/src/agents.ts +30 -0
- package/src/cache.ts +141 -0
- package/src/chunking/chunking.ts +36 -0
- package/src/chunking/lines.ts +79 -0
- package/src/chunking/structural.ts +244 -0
- package/src/cli.ts +245 -0
- package/src/concurrency.ts +58 -0
- package/src/embeddings/openai.ts +442 -0
- package/src/env-files.ts +44 -0
- package/src/env.ts +50 -0
- package/src/git.ts +52 -0
- package/src/index/bm25-types.ts +18 -0
- package/src/index/bm25-worker.ts +48 -0
- package/src/index/bm25.ts +238 -0
- package/src/index/create.ts +195 -0
- package/src/index/dense.ts +105 -0
- package/src/index/file-walker.ts +113 -0
- package/src/index/files.ts +126 -0
- package/src/index/persistence.ts +100 -0
- package/src/index/quantize.ts +168 -0
- package/src/index/semantic-index.ts +11 -0
- package/src/index/sparse.ts +25 -0
- package/src/index/topk.ts +52 -0
- package/src/index/vector-storage.ts +27 -0
- package/src/index.ts +3 -0
- package/src/mcp/index-cache.ts +95 -0
- package/src/mcp/serve.ts +24 -0
- package/src/mcp/server.ts +100 -0
- package/src/miru-index.ts +269 -0
- package/src/ranking/boosting.ts +329 -0
- package/src/ranking/penalties.ts +110 -0
- package/src/ranking/weighting.ts +11 -0
- package/src/search.ts +180 -0
- package/src/tokens.ts +27 -0
- package/src/types.ts +47 -0
- package/src/utils.ts +64 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { basename, extname } from "node:path";
|
|
2
|
+
import type { ContentType } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
const MAX_FILE_BYTES = 1_000_000;
|
|
5
|
+
const EMPTY_FILE_BYTES = 128;
|
|
6
|
+
|
|
7
|
+
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
8
|
+
".py": "python",
|
|
9
|
+
".pyi": "python",
|
|
10
|
+
".js": "javascript",
|
|
11
|
+
".mjs": "javascript",
|
|
12
|
+
".cjs": "javascript",
|
|
13
|
+
".jsx": "javascript",
|
|
14
|
+
".ts": "typescript",
|
|
15
|
+
".tsx": "typescript",
|
|
16
|
+
".mts": "typescript",
|
|
17
|
+
".cts": "typescript",
|
|
18
|
+
".go": "go",
|
|
19
|
+
".rs": "rust",
|
|
20
|
+
".java": "java",
|
|
21
|
+
".kt": "kotlin",
|
|
22
|
+
".kts": "kotlin",
|
|
23
|
+
".c": "c",
|
|
24
|
+
".h": "c",
|
|
25
|
+
".cpp": "cpp",
|
|
26
|
+
".cc": "cpp",
|
|
27
|
+
".cxx": "cpp",
|
|
28
|
+
".hpp": "cpp",
|
|
29
|
+
".cs": "csharp",
|
|
30
|
+
".rb": "ruby",
|
|
31
|
+
".php": "php",
|
|
32
|
+
".swift": "swift",
|
|
33
|
+
".scala": "scala",
|
|
34
|
+
".clj": "clojure",
|
|
35
|
+
".cljs": "clojure",
|
|
36
|
+
".ex": "elixir",
|
|
37
|
+
".exs": "elixir",
|
|
38
|
+
".erl": "erlang",
|
|
39
|
+
".hs": "haskell",
|
|
40
|
+
".lua": "lua",
|
|
41
|
+
".sh": "bash",
|
|
42
|
+
".bash": "bash",
|
|
43
|
+
".zsh": "bash",
|
|
44
|
+
".fish": "fish",
|
|
45
|
+
".sql": "sql",
|
|
46
|
+
".r": "r",
|
|
47
|
+
".R": "r",
|
|
48
|
+
".dart": "dart",
|
|
49
|
+
".zig": "zig",
|
|
50
|
+
".vue": "vue",
|
|
51
|
+
".svelte": "svelte",
|
|
52
|
+
".md": "markdown",
|
|
53
|
+
".mdx": "markdown",
|
|
54
|
+
".rst": "rst",
|
|
55
|
+
".txt": "text",
|
|
56
|
+
".json": "json",
|
|
57
|
+
".yaml": "yaml",
|
|
58
|
+
".yml": "yaml",
|
|
59
|
+
".toml": "toml",
|
|
60
|
+
".ini": "ini",
|
|
61
|
+
".cfg": "ini",
|
|
62
|
+
".xml": "xml",
|
|
63
|
+
".html": "html",
|
|
64
|
+
".css": "css",
|
|
65
|
+
".scss": "scss",
|
|
66
|
+
".less": "less",
|
|
67
|
+
".dockerfile": "dockerfile",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const CODE_EXTENSIONS = new Set(
|
|
71
|
+
Object.entries(EXTENSION_TO_LANGUAGE)
|
|
72
|
+
.filter(
|
|
73
|
+
([, lang]) =>
|
|
74
|
+
!["markdown", "rst", "text", "json", "yaml", "toml", "ini", "xml", "html"].includes(lang),
|
|
75
|
+
)
|
|
76
|
+
.map(([ext]) => ext),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const DOC_EXTENSIONS = new Set([".md", ".mdx", ".rst", ".txt"]);
|
|
80
|
+
const CONFIG_EXTENSIONS = new Set([".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".xml"]);
|
|
81
|
+
|
|
82
|
+
const CONTENT_EXTENSIONS: Record<ContentType, Set<string>> = {
|
|
83
|
+
code: CODE_EXTENSIONS,
|
|
84
|
+
docs: DOC_EXTENSIONS,
|
|
85
|
+
config: CONFIG_EXTENSIONS,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function getExtensions(types: ContentType[]): string[] {
|
|
89
|
+
const exts = new Set<string>();
|
|
90
|
+
for (const t of types) {
|
|
91
|
+
for (const e of CONTENT_EXTENSIONS[t] ?? []) {
|
|
92
|
+
exts.add(e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...exts].sort();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function detectLanguage(filePath: string): string | null {
|
|
99
|
+
const ext = extname(filePath).toLowerCase();
|
|
100
|
+
if (basename(filePath).toLowerCase() === "dockerfile") {
|
|
101
|
+
return "dockerfile";
|
|
102
|
+
}
|
|
103
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type FileStatus = "too_large" | "empty" | "valid";
|
|
107
|
+
|
|
108
|
+
export async function readFileText(filePath: string): Promise<string> {
|
|
109
|
+
const file = Bun.file(filePath);
|
|
110
|
+
return file.text();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function getFileStatus(filePath: string): Promise<FileStatus> {
|
|
114
|
+
const file = Bun.file(filePath);
|
|
115
|
+
const stat = await file.stat();
|
|
116
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
117
|
+
return "too_large";
|
|
118
|
+
}
|
|
119
|
+
if (stat.size < EMPTY_FILE_BYTES) {
|
|
120
|
+
const text = await file.text();
|
|
121
|
+
if (!text.trim()) {
|
|
122
|
+
return "empty";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return "valid";
|
|
126
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { BM25Index } from "./bm25.ts";
|
|
4
|
+
import { VectorIndex } from "./dense.ts";
|
|
5
|
+
import { QuantizedVectorIndex } from "./quantize.ts";
|
|
6
|
+
import type { SemanticIndex } from "./semantic-index.ts";
|
|
7
|
+
import { resolveSemanticStorage, semanticStorageOf } from "./vector-storage.ts";
|
|
8
|
+
|
|
9
|
+
export interface PersistencePaths {
|
|
10
|
+
root: string;
|
|
11
|
+
bm25Index: string;
|
|
12
|
+
semanticIndex: string;
|
|
13
|
+
chunks: string;
|
|
14
|
+
metadata: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function persistencePaths(root: string): PersistencePaths {
|
|
18
|
+
return {
|
|
19
|
+
root,
|
|
20
|
+
bm25Index: join(root, "bm25_index.json"),
|
|
21
|
+
semanticIndex: join(root, "semantic_index"),
|
|
22
|
+
chunks: join(root, "chunks.json"),
|
|
23
|
+
metadata: join(root, "metadata.json"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function pathsExist(paths: PersistencePaths): Promise<string[]> {
|
|
28
|
+
const missing: string[] = [];
|
|
29
|
+
const checks: [string, string][] = [
|
|
30
|
+
["bm25", paths.bm25Index],
|
|
31
|
+
["semantic", `${paths.semanticIndex}/meta.json`],
|
|
32
|
+
["chunks", paths.chunks],
|
|
33
|
+
["metadata", paths.metadata],
|
|
34
|
+
];
|
|
35
|
+
for (const [name, p] of checks) {
|
|
36
|
+
if (!(await Bun.file(p).exists())) {
|
|
37
|
+
missing.push(name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return missing;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function saveBm25(index: BM25Index, path: string): Promise<void> {
|
|
44
|
+
await Bun.write(path, JSON.stringify(index.toJSON()));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function loadBm25(path: string): Promise<BM25Index> {
|
|
48
|
+
return BM25Index.fromJSON(JSON.parse(await Bun.file(path).text()));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function saveSemantic(index: SemanticIndex, path: string): Promise<void> {
|
|
52
|
+
if (index instanceof QuantizedVectorIndex) {
|
|
53
|
+
await index.save(path);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (index instanceof VectorIndex) {
|
|
57
|
+
await index.save(path);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
throw new Error("Unsupported semantic index type");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function loadSemantic(path: string): Promise<SemanticIndex> {
|
|
64
|
+
const meta = JSON.parse(await Bun.file(`${path}/meta.json`).text()) as {
|
|
65
|
+
storage?: string;
|
|
66
|
+
};
|
|
67
|
+
if (meta.storage === "int8") {
|
|
68
|
+
return QuantizedVectorIndex.load(path);
|
|
69
|
+
}
|
|
70
|
+
return VectorIndex.load(path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function saveIndexBundle(options: {
|
|
74
|
+
paths: PersistencePaths;
|
|
75
|
+
bm25: BM25Index;
|
|
76
|
+
semantic: SemanticIndex;
|
|
77
|
+
chunks: unknown[];
|
|
78
|
+
metadata: Record<string, unknown>;
|
|
79
|
+
}): Promise<void> {
|
|
80
|
+
const { paths, bm25, semantic, chunks, metadata } = options;
|
|
81
|
+
await mkdir(paths.root, { recursive: true });
|
|
82
|
+
await saveBm25(bm25, paths.bm25Index);
|
|
83
|
+
await saveSemantic(semantic, paths.semanticIndex);
|
|
84
|
+
await Bun.write(
|
|
85
|
+
paths.metadata,
|
|
86
|
+
JSON.stringify({
|
|
87
|
+
...metadata,
|
|
88
|
+
vector_storage: semanticStorageOf(semantic),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
await Bun.write(paths.chunks, JSON.stringify(chunks));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function semanticIndexMatchesStorage(path: string): Promise<boolean> {
|
|
95
|
+
const meta = JSON.parse(await Bun.file(`${path}/meta.json`).text()) as {
|
|
96
|
+
storage?: string;
|
|
97
|
+
};
|
|
98
|
+
const stored = meta.storage === "int8" ? "int8" : "float32";
|
|
99
|
+
return stored === resolveSemanticStorage();
|
|
100
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { SemanticIndex } from "./semantic-index.ts";
|
|
2
|
+
import { selectTopKByDistance } from "./topk.ts";
|
|
3
|
+
|
|
4
|
+
export interface QuantizedVector {
|
|
5
|
+
codes: Int8Array;
|
|
6
|
+
scale: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Symmetric int8 quantization per vector (embeddings are L2-normalized upstream). */
|
|
10
|
+
export function quantizeVector(vector: Float32Array): QuantizedVector {
|
|
11
|
+
let maxAbs = 0;
|
|
12
|
+
for (let i = 0; i < vector.length; i++) {
|
|
13
|
+
const value = vector[i] ?? 0;
|
|
14
|
+
const abs = Math.abs(value);
|
|
15
|
+
if (abs > maxAbs) {
|
|
16
|
+
maxAbs = abs;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const scale = maxAbs > 0 ? maxAbs / 127 : 1;
|
|
20
|
+
const codes = new Int8Array(vector.length);
|
|
21
|
+
if (maxAbs > 0) {
|
|
22
|
+
const inv = 127 / maxAbs;
|
|
23
|
+
for (let i = 0; i < vector.length; i++) {
|
|
24
|
+
const value = vector[i] ?? 0;
|
|
25
|
+
codes[i] = Math.max(-127, Math.min(127, Math.round(value * inv)));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { codes, scale };
|
|
29
|
+
}
|
|
30
|
+
|
|
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. */
|
|
41
|
+
export class QuantizedVectorIndex implements SemanticIndex {
|
|
42
|
+
private readonly codes: Int8Array[];
|
|
43
|
+
private readonly scales: Float32Array;
|
|
44
|
+
private readonly dim: number;
|
|
45
|
+
|
|
46
|
+
constructor(vectors: Float32Array[]) {
|
|
47
|
+
if (vectors.length === 0) {
|
|
48
|
+
this.dim = 0;
|
|
49
|
+
this.codes = [];
|
|
50
|
+
this.scales = new Float32Array(0);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
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++) {
|
|
57
|
+
const vector = vectors[i];
|
|
58
|
+
if (!vector) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const { codes, scale } = quantizeVector(vector);
|
|
62
|
+
this.codes[i] = codes;
|
|
63
|
+
this.scales[i] = scale;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static fromPersisted(
|
|
68
|
+
codes: Int8Array[],
|
|
69
|
+
scales: Float32Array,
|
|
70
|
+
dim: number,
|
|
71
|
+
): QuantizedVectorIndex {
|
|
72
|
+
const index = Object.create(QuantizedVectorIndex.prototype) as QuantizedVectorIndex;
|
|
73
|
+
Object.assign(index, { dim, codes, scales });
|
|
74
|
+
return index;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get size(): number {
|
|
78
|
+
return this.codes.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get dimensions(): number {
|
|
82
|
+
return this.dim;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
memoryBytes(): number {
|
|
86
|
+
return this.codes.length * this.dim + this.scales.byteLength;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
query(
|
|
90
|
+
queryVector: Float32Array,
|
|
91
|
+
k: number,
|
|
92
|
+
selector?: number[],
|
|
93
|
+
): { indices: number[]; distances: number[] } {
|
|
94
|
+
if (k < 1) {
|
|
95
|
+
throw new Error(`k should be >= 1, is now ${k}`);
|
|
96
|
+
}
|
|
97
|
+
if (this.size === 0) {
|
|
98
|
+
return { indices: [], distances: [] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const q = quantizeVector(queryVector);
|
|
102
|
+
const indices = selector ?? Array.from({ length: this.size }, (_, i) => i);
|
|
103
|
+
const effectiveK = Math.min(k, indices.length);
|
|
104
|
+
if (effectiveK === 0) {
|
|
105
|
+
return { indices: [], distances: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const top = selectTopKByDistance(
|
|
109
|
+
indices.flatMap((idx) => {
|
|
110
|
+
const codes = this.codes[idx];
|
|
111
|
+
if (!codes) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const scale = this.scales[idx];
|
|
115
|
+
if (scale === undefined) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const similarity = quantizedDot(q, codes, scale);
|
|
119
|
+
return [{ index: idx, distance: 1 - similarity }];
|
|
120
|
+
}),
|
|
121
|
+
effectiveK,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
indices: top.map((t) => t.index),
|
|
126
|
+
distances: top.map((t) => t.distance),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async save(dir: string): Promise<void> {
|
|
131
|
+
const { mkdir } = await import("node:fs/promises");
|
|
132
|
+
await mkdir(dir, { recursive: true });
|
|
133
|
+
|
|
134
|
+
const count = this.size;
|
|
135
|
+
const dim = this.dim;
|
|
136
|
+
const flatCodes = new Int8Array(count * dim);
|
|
137
|
+
for (let i = 0; i < count; i++) {
|
|
138
|
+
const codes = this.codes[i];
|
|
139
|
+
if (codes) {
|
|
140
|
+
flatCodes.set(codes, i * dim);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
await Bun.write(`${dir}/codes.bin`, flatCodes);
|
|
144
|
+
await Bun.write(`${dir}/scales.bin`, this.scales);
|
|
145
|
+
await Bun.write(
|
|
146
|
+
`${dir}/meta.json`,
|
|
147
|
+
JSON.stringify({ count, dimensions: dim, storage: "int8" }),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static async load(dir: string): Promise<QuantizedVectorIndex> {
|
|
152
|
+
const meta = JSON.parse(await Bun.file(`${dir}/meta.json`).text()) as {
|
|
153
|
+
count: number;
|
|
154
|
+
dimensions: number;
|
|
155
|
+
storage?: string;
|
|
156
|
+
};
|
|
157
|
+
const rawCodes = new Int8Array(await Bun.file(`${dir}/codes.bin`).arrayBuffer());
|
|
158
|
+
const scales = new Float32Array(await Bun.file(`${dir}/scales.bin`).arrayBuffer());
|
|
159
|
+
const index = QuantizedVectorIndex.fromPersisted(
|
|
160
|
+
Array.from({ length: meta.count }, (_, i) =>
|
|
161
|
+
rawCodes.subarray(i * meta.dimensions, (i + 1) * meta.dimensions),
|
|
162
|
+
),
|
|
163
|
+
scales,
|
|
164
|
+
meta.dimensions,
|
|
165
|
+
);
|
|
166
|
+
return index;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Shared query interface for float and quantized vector indexes. */
|
|
2
|
+
export interface SemanticIndex {
|
|
3
|
+
readonly size: number;
|
|
4
|
+
readonly dimensions: number;
|
|
5
|
+
memoryBytes(): number;
|
|
6
|
+
query(
|
|
7
|
+
queryVector: Float32Array,
|
|
8
|
+
k: number,
|
|
9
|
+
selector?: number[],
|
|
10
|
+
): { indices: number[]; distances: number[] };
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Chunk } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function selectorToMask(
|
|
4
|
+
selector: number[] | null | undefined,
|
|
5
|
+
size: number,
|
|
6
|
+
): boolean[] | undefined {
|
|
7
|
+
if (!selector || selector.length === 0) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const mask = new Array<boolean>(size).fill(false);
|
|
11
|
+
for (const idx of selector) {
|
|
12
|
+
if (idx >= 0 && idx < size) {
|
|
13
|
+
mask[idx] = true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return mask;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function enrichForBm25(chunk: Chunk): string {
|
|
20
|
+
const parts = chunk.file_path.replace(/\\/g, "/").split("/");
|
|
21
|
+
const stem = parts[parts.length - 1]?.replace(/\.[^.]+$/, "") ?? "";
|
|
22
|
+
const dirParts = parts.slice(0, -1).filter((p) => p && p !== "." && p !== "..");
|
|
23
|
+
const dirText = dirParts.slice(-3).join(" ");
|
|
24
|
+
return `${chunk.content} ${stem} ${stem} ${dirText}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface TopKDistanceEntry {
|
|
2
|
+
index: number;
|
|
3
|
+
distance: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Keep the k smallest distances (best cosine matches). */
|
|
7
|
+
export function selectTopKByDistance(
|
|
8
|
+
entries: Iterable<TopKDistanceEntry>,
|
|
9
|
+
k: number,
|
|
10
|
+
): TopKDistanceEntry[] {
|
|
11
|
+
const top: TopKDistanceEntry[] = [];
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (top.length < k) {
|
|
14
|
+
top.push(entry);
|
|
15
|
+
top.sort((a, b) => b.distance - a.distance);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const worst = top[0];
|
|
19
|
+
if (worst && entry.distance < worst.distance) {
|
|
20
|
+
top[0] = entry;
|
|
21
|
+
top.sort((a, b) => b.distance - a.distance);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
top.sort((a, b) => a.distance - b.distance);
|
|
25
|
+
return top;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Indices of the k highest scores in a dense array. */
|
|
29
|
+
export function selectTopKScoreIndices(scores: number[], k: number): number[] {
|
|
30
|
+
if (k <= 0 || scores.length === 0) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const top: { v: number; i: number }[] = [];
|
|
34
|
+
for (let i = 0; i < scores.length; i++) {
|
|
35
|
+
const v = scores[i];
|
|
36
|
+
if (v === undefined) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (top.length < k) {
|
|
40
|
+
top.push({ v, i });
|
|
41
|
+
top.sort((a, b) => a.v - b.v);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const worst = top[0];
|
|
45
|
+
if (worst && v > worst.v) {
|
|
46
|
+
top[0] = { v, i };
|
|
47
|
+
top.sort((a, b) => a.v - b.v);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
top.sort((a, b) => b.v - a.v);
|
|
51
|
+
return top.map((x) => x.i);
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { VectorIndex } from "./dense.ts";
|
|
2
|
+
import { QuantizedVectorIndex } from "./quantize.ts";
|
|
3
|
+
import type { SemanticIndex } from "./semantic-index.ts";
|
|
4
|
+
|
|
5
|
+
export type SemanticStorage = "int8" | "float32";
|
|
6
|
+
|
|
7
|
+
/** Default: int8 quantized vectors (~4x less RAM). Set MIRU_FLOAT_VECTORS=1 for float32. */
|
|
8
|
+
export function resolveSemanticStorage(): SemanticStorage {
|
|
9
|
+
return process.env.MIRU_FLOAT_VECTORS === "1" || process.env.SEMBLE_FLOAT_VECTORS === "1"
|
|
10
|
+
? "float32"
|
|
11
|
+
: "int8";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function semanticStorageFromMetadata(metadata: Record<string, unknown>): SemanticStorage {
|
|
15
|
+
return metadata.vector_storage === "float32" ? "float32" : "int8";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildSemanticIndex(vectors: Float32Array[]): SemanticIndex {
|
|
19
|
+
if (resolveSemanticStorage() === "int8") {
|
|
20
|
+
return new QuantizedVectorIndex(vectors);
|
|
21
|
+
}
|
|
22
|
+
return new VectorIndex(vectors);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function semanticStorageOf(index: SemanticIndex): SemanticStorage {
|
|
26
|
+
return index instanceof QuantizedVectorIndex ? "int8" : "float32";
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { MiruIndex } from "../miru-index.ts";
|
|
4
|
+
import type { ContentType } from "../types.ts";
|
|
5
|
+
import { computeSourceCacheKey, isGitUrl } from "../utils.ts";
|
|
6
|
+
|
|
7
|
+
const CACHE_MAX_SIZE = 10;
|
|
8
|
+
|
|
9
|
+
type IndexTask = Promise<MiruIndex>;
|
|
10
|
+
|
|
11
|
+
export class IndexCache {
|
|
12
|
+
private readonly content: ContentType[];
|
|
13
|
+
private readonly tasks = new Map<string, IndexTask>();
|
|
14
|
+
private watcher: ReturnType<typeof watch> | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(content: ContentType[] = ["code"]) {
|
|
17
|
+
this.content = content;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
evict(source: string, ref?: string | null): void {
|
|
21
|
+
this.tasks.delete(computeSourceCacheKey(source, ref));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async get(source: string, ref?: string | null): Promise<MiruIndex> {
|
|
25
|
+
const cacheKey = computeSourceCacheKey(source, ref);
|
|
26
|
+
let task = this.tasks.get(cacheKey);
|
|
27
|
+
if (!task) {
|
|
28
|
+
if (this.tasks.size >= CACHE_MAX_SIZE) {
|
|
29
|
+
const oldest = this.tasks.keys().next().value;
|
|
30
|
+
if (oldest) {
|
|
31
|
+
this.tasks.delete(oldest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
task = MiruIndex.fromSource(source, this.content, undefined, ref);
|
|
35
|
+
this.tasks.set(cacheKey, task);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return await task;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (this.tasks.get(cacheKey) === task) {
|
|
42
|
+
this.tasks.delete(cacheKey);
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
startWatcher(path: string): void {
|
|
49
|
+
const resolved = resolve(path);
|
|
50
|
+
if (this.watcher) {
|
|
51
|
+
this.watcher.close();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.watcher = watch(resolved, { recursive: true }, () => {
|
|
55
|
+
this.evict(resolved);
|
|
56
|
+
void this.get(resolved).catch(() => undefined);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close(): void {
|
|
61
|
+
this.watcher?.close();
|
|
62
|
+
this.watcher = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getIndexForRepo(
|
|
67
|
+
repo: string | null | undefined,
|
|
68
|
+
defaultSource: string | null,
|
|
69
|
+
cache: IndexCache,
|
|
70
|
+
ref?: string | null,
|
|
71
|
+
): Promise<MiruIndex> {
|
|
72
|
+
if (repo && isGitUrl(repo) && !repo.startsWith("https://") && !repo.startsWith("http://")) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Only https://, http://, or local directory paths are accepted as \`repo\`. Got: ${repo}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const source = repo ?? defaultSource;
|
|
79
|
+
if (!source) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"No repo specified and no default index. Pass an https:// or http:// git URL or local directory path as `repo`.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
return await cache.get(source, ref);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
89
|
+
throw new Error(`Failed to index ${source}: ${message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function toolText(content: string): { content: Array<{ type: "text"; text: string }> } {
|
|
94
|
+
return { content: [{ type: "text", text: content }] };
|
|
95
|
+
}
|
package/src/mcp/serve.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import type { ContentType } from "../types.ts";
|
|
4
|
+
import { isGitUrl } from "../utils.ts";
|
|
5
|
+
import { IndexCache } from "./index-cache.ts";
|
|
6
|
+
import { createMcpServer } from "./server.ts";
|
|
7
|
+
|
|
8
|
+
export async function serveMcp(options: {
|
|
9
|
+
path?: string | null;
|
|
10
|
+
ref?: string | null;
|
|
11
|
+
content?: ContentType[];
|
|
12
|
+
}): Promise<void> {
|
|
13
|
+
const cache = new IndexCache(options.content ?? ["code"]);
|
|
14
|
+
const defaultSource = options.path ?? resolve(process.cwd());
|
|
15
|
+
|
|
16
|
+
void cache.get(defaultSource, options.ref).catch(() => undefined);
|
|
17
|
+
if (!isGitUrl(defaultSource)) {
|
|
18
|
+
cache.startWatcher(defaultSource);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const server = createMcpServer(cache, defaultSource);
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
}
|