@takara-ai/miru-code 1.0.5 → 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.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 +4 -4
- 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 +6 -7
- package/src/mcp/index-cache.ts +2 -2
- package/src/mcp/server.ts +13 -17
- package/src/miru-index.ts +116 -19
- package/src/search.ts +4 -4
- package/src/setup.ts +8 -2
- package/src/terminal.ts +171 -0
- package/src/version.ts +21 -10
package/src/credentials.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir
|
|
1
|
+
import { chmod, mkdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { hasTakaraApiKeyInEnv, normalizeTakaraApiKeyEnv } from "./env.ts";
|
|
4
4
|
|
|
@@ -37,7 +37,7 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
|
-
const parsed = JSON.parse(await
|
|
40
|
+
const parsed = JSON.parse(await Bun.file(path).text()) as StoredCredentials;
|
|
41
41
|
if (parsed.version !== CREDENTIALS_VERSION || !parsed.takara_api_key) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
@@ -69,10 +69,7 @@ export async function saveStoredCredentials(apiKey: string): Promise<string> {
|
|
|
69
69
|
version: CREDENTIALS_VERSION,
|
|
70
70
|
takara_api_key: apiKey,
|
|
71
71
|
};
|
|
72
|
-
await
|
|
73
|
-
encoding: "utf-8",
|
|
74
|
-
mode: 0o600,
|
|
75
|
-
});
|
|
72
|
+
await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
76
73
|
try {
|
|
77
74
|
await chmod(path, 0o600);
|
|
78
75
|
} catch {
|
|
@@ -88,7 +85,7 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
|
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
const stored = await readStoredCredentials();
|
|
91
|
-
await
|
|
88
|
+
await Bun.file(path).delete();
|
|
92
89
|
|
|
93
90
|
if (stored && process.env.TAKARA_API_KEY === stored.takara_api_key) {
|
|
94
91
|
delete process.env.TAKARA_API_KEY;
|
package/src/embeddings/openai.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mapPool, resolveWorkerConcurrency } from "../concurrency.ts";
|
|
2
2
|
import { envFirstString, envOptionalInt, resolveEmbeddingApiKey } from "../env.ts";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
export const DEFAULT_EMBEDDING_MODEL = "ds1-miru-int8";
|
|
5
|
+
export const DEFAULT_EMBEDDING_BASE_URL = "https://infer.takara.ai/v1";
|
|
6
6
|
const DEFAULT_BATCH_SIZE = 32;
|
|
7
7
|
const DEFAULT_MAX_EMBED_CHARS = 1300;
|
|
8
8
|
const WINDOW_OVERLAP_CHARS = 120;
|
|
@@ -32,7 +32,10 @@ export interface EmbeddingBackend {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function resolveEmbeddingModel(): string {
|
|
35
|
-
return envFirstString(
|
|
35
|
+
return envFirstString(
|
|
36
|
+
["MIRU_OPENAI_EMBEDDING_MODEL", "OPENAI_EMBEDDING_MODEL"],
|
|
37
|
+
DEFAULT_EMBEDDING_MODEL,
|
|
38
|
+
);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export function resolveMaxEmbedChars(): number {
|
|
@@ -118,10 +121,10 @@ export function resolveEmbeddingBatchSize(): number {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export function resolveEmbeddingBaseUrl(): string {
|
|
121
|
-
return envFirstString(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
124
|
+
return envFirstString(
|
|
125
|
+
["MIRU_OPENAI_BASE_URL", "OPENAI_BASE_URL"],
|
|
126
|
+
DEFAULT_EMBEDDING_BASE_URL,
|
|
127
|
+
).replace(/\/$/, "");
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
interface Int8Embedding {
|
package/src/env-files.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
|
|
4
3
|
function applyEnvLine(line: string): void {
|
|
@@ -24,21 +23,25 @@ function applyEnvLine(line: string): void {
|
|
|
24
23
|
process.env[key] = value;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
function loadEnvFile(path: string): void {
|
|
28
|
-
|
|
26
|
+
async function loadEnvFile(path: string): Promise<void> {
|
|
27
|
+
const file = Bun.file(path);
|
|
28
|
+
if (!(await file.exists())) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
for (const line of
|
|
31
|
+
for (const line of (await file.text()).split("\n")) {
|
|
32
32
|
applyEnvLine(line);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** Load .env.local / .env without overriding vars already set (e.g. MCP config env). */
|
|
37
|
-
export function loadEnvFiles(options?: {
|
|
37
|
+
export async function loadEnvFiles(options?: {
|
|
38
|
+
packageRoot?: string;
|
|
39
|
+
cwd?: string;
|
|
40
|
+
}): Promise<void> {
|
|
38
41
|
const cwd = options?.cwd ?? process.cwd();
|
|
39
42
|
const packageRoot = options?.packageRoot ?? join(import.meta.dir, "..");
|
|
40
43
|
for (const dir of [packageRoot, cwd]) {
|
|
41
|
-
loadEnvFile(join(dir, ".env.local"));
|
|
42
|
-
loadEnvFile(join(dir, ".env"));
|
|
44
|
+
await loadEnvFile(join(dir, ".env.local"));
|
|
45
|
+
await loadEnvFile(join(dir, ".env"));
|
|
43
46
|
}
|
|
44
47
|
}
|
package/src/help.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId } from "./agents.ts";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
commandHeader,
|
|
4
4
|
commandRow,
|
|
5
5
|
divider,
|
|
6
6
|
fail,
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
section,
|
|
10
10
|
writeStdout,
|
|
11
11
|
} from "./cli-ui.ts";
|
|
12
|
+
import { DEFAULT_RESERVE_CORES } from "./concurrency.ts";
|
|
13
|
+
import { DEFAULT_EMBEDDING_BASE_URL, DEFAULT_EMBEDDING_MODEL } from "./embeddings/openai.ts";
|
|
14
|
+
import { TAKARA_API_KEY_ENV } from "./env.ts";
|
|
12
15
|
|
|
13
16
|
export const AGENT_IDS: readonly AgentId[] = [
|
|
14
17
|
"claude",
|
|
@@ -22,7 +25,7 @@ export const AGENT_IDS: readonly AgentId[] = [
|
|
|
22
25
|
const AGENT_LIST = AGENT_IDS.join(", ");
|
|
23
26
|
|
|
24
27
|
export function printMainHelp(): void {
|
|
25
|
-
header(
|
|
28
|
+
header();
|
|
26
29
|
|
|
27
30
|
section("Usage");
|
|
28
31
|
writeStdout(" miru Start MCP server (stdio)");
|
|
@@ -51,14 +54,14 @@ export function printMainHelp(): void {
|
|
|
51
54
|
|
|
52
55
|
export function printEnvHelp(): void {
|
|
53
56
|
section("Environment");
|
|
54
|
-
writeStdout(
|
|
57
|
+
writeStdout(` ${TAKARA_API_KEY_ENV}`);
|
|
55
58
|
writeStdout(" Takara bearer token for embeddings");
|
|
56
59
|
writeStdout(" MIRU_OPENAI_BASE_URL");
|
|
57
|
-
writeStdout(
|
|
60
|
+
writeStdout(` Default: ${DEFAULT_EMBEDDING_BASE_URL}`);
|
|
58
61
|
writeStdout(" MIRU_OPENAI_EMBEDDING_MODEL");
|
|
59
|
-
writeStdout(
|
|
62
|
+
writeStdout(` Default: ${DEFAULT_EMBEDDING_MODEL}`);
|
|
60
63
|
writeStdout(" MIRU_CONCURRENCY");
|
|
61
|
-
writeStdout(
|
|
64
|
+
writeStdout(` Parallel workers (default: CPUs − ${DEFAULT_RESERVE_CORES})`);
|
|
62
65
|
writeStdout("");
|
|
63
66
|
}
|
|
64
67
|
|
|
@@ -67,17 +70,10 @@ export function printFullHelp(): void {
|
|
|
67
70
|
printEnvHelp();
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
function printCommandHeader(name: string, summary: string): void {
|
|
71
|
-
writeStdout("");
|
|
72
|
-
writeStdout(`${brandTitle()} ${name}`);
|
|
73
|
-
writeStdout(summary);
|
|
74
|
-
divider();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
export function printCommandHelp(command: string): void {
|
|
78
74
|
switch (command) {
|
|
79
75
|
case "search":
|
|
80
|
-
|
|
76
|
+
commandHeader("search", "Hybrid semantic + keyword search.");
|
|
81
77
|
section("Usage");
|
|
82
78
|
writeStdout(" miru search <query> [path] [options]");
|
|
83
79
|
section("Options");
|
|
@@ -89,7 +85,7 @@ export function printCommandHelp(command: string): void {
|
|
|
89
85
|
writeStdout("");
|
|
90
86
|
return;
|
|
91
87
|
case "expand":
|
|
92
|
-
|
|
88
|
+
commandHeader("expand", "More context in the same file as a search hit.");
|
|
93
89
|
section("Usage");
|
|
94
90
|
writeStdout(" miru expand <file> <line> [path] [--before N] [--after N]");
|
|
95
91
|
section("Example");
|
|
@@ -97,7 +93,7 @@ export function printCommandHelp(command: string): void {
|
|
|
97
93
|
writeStdout("");
|
|
98
94
|
return;
|
|
99
95
|
case "find-related":
|
|
100
|
-
|
|
96
|
+
commandHeader("find-related", "Semantic neighbors of a file location.");
|
|
101
97
|
section("Usage");
|
|
102
98
|
writeStdout(" miru find-related <file> <line> [path] [options]");
|
|
103
99
|
section("Example");
|
|
@@ -105,7 +101,7 @@ export function printCommandHelp(command: string): void {
|
|
|
105
101
|
writeStdout("");
|
|
106
102
|
return;
|
|
107
103
|
case "setup":
|
|
108
|
-
|
|
104
|
+
commandHeader("setup", "Store and validate your Takara API key.");
|
|
109
105
|
section("Usage");
|
|
110
106
|
writeStdout(" miru setup [--key TOKEN] [--force] [--clear]");
|
|
111
107
|
section("Options");
|
|
@@ -115,7 +111,7 @@ export function printCommandHelp(command: string): void {
|
|
|
115
111
|
writeStdout("");
|
|
116
112
|
return;
|
|
117
113
|
case "install":
|
|
118
|
-
|
|
114
|
+
commandHeader("install", "Interactive global agent setup.");
|
|
119
115
|
writeStdout("Configures MCP server, instructions, and sub-agent files under");
|
|
120
116
|
writeStdout("your user config (~/.claude, ~/.cursor, etc.).");
|
|
121
117
|
writeStdout("");
|
|
@@ -123,12 +119,12 @@ export function printCommandHelp(command: string): void {
|
|
|
123
119
|
writeStdout("");
|
|
124
120
|
return;
|
|
125
121
|
case "uninstall":
|
|
126
|
-
|
|
122
|
+
commandHeader("uninstall", "Remove miru configuration from agents.");
|
|
127
123
|
writeStdout("Removes MCP entries, marked instruction blocks, and global sub-agents.");
|
|
128
124
|
writeStdout("");
|
|
129
125
|
return;
|
|
130
126
|
case "init":
|
|
131
|
-
|
|
127
|
+
commandHeader("init", "Project-local sub-agent file.");
|
|
132
128
|
writeStdout("Prefer miru install for global setup. Use init to commit into a repo.");
|
|
133
129
|
section("Usage");
|
|
134
130
|
writeStdout(" miru init --agent AGENT [--force]");
|
|
@@ -139,7 +135,7 @@ export function printCommandHelp(command: string): void {
|
|
|
139
135
|
writeStdout("");
|
|
140
136
|
return;
|
|
141
137
|
case "clear":
|
|
142
|
-
|
|
138
|
+
commandHeader("clear", "Drop the on-disk index cache.");
|
|
143
139
|
section("Usage");
|
|
144
140
|
writeStdout(" miru clear [path]");
|
|
145
141
|
section("Example");
|
|
@@ -147,7 +143,7 @@ export function printCommandHelp(command: string): void {
|
|
|
147
143
|
writeStdout("");
|
|
148
144
|
return;
|
|
149
145
|
case "mcp":
|
|
150
|
-
|
|
146
|
+
commandHeader("mcp", "Stdio MCP server (default with no subcommand).");
|
|
151
147
|
section("Usage");
|
|
152
148
|
writeStdout(" miru [--ref BRANCH] [--content TYPE ...]");
|
|
153
149
|
writeStdout("");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ChunkIndexMappings = {
|
|
2
|
+
fileMapping: ReadonlyMap<string, readonly number[]>;
|
|
3
|
+
languageMapping: ReadonlyMap<string, readonly number[]>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Union of chunk indices matching language and/or path filters.
|
|
8
|
+
*
|
|
9
|
+
* Filters combine with OR semantics: a chunk is included if it matches any
|
|
10
|
+
* requested language or path. Returns `undefined` when no filters are given
|
|
11
|
+
* (search all chunks) or when no chunks match.
|
|
12
|
+
*
|
|
13
|
+
* Single-filter queries return the prebuilt map array directly (read-only for
|
|
14
|
+
* callers). Multi-filter queries allocate a deduped index list.
|
|
15
|
+
*/
|
|
16
|
+
export function buildChunkSelector(
|
|
17
|
+
mappings: ChunkIndexMappings,
|
|
18
|
+
filterLanguages?: readonly string[],
|
|
19
|
+
filterPaths?: readonly string[],
|
|
20
|
+
): readonly number[] | undefined {
|
|
21
|
+
const langCount = filterLanguages?.length ?? 0;
|
|
22
|
+
const pathCount = filterPaths?.length ?? 0;
|
|
23
|
+
|
|
24
|
+
if (langCount === 0 && pathCount === 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (langCount === 1 && pathCount === 0) {
|
|
29
|
+
const lang = filterLanguages?.[0];
|
|
30
|
+
if (!lang) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const indices = mappings.languageMapping.get(lang);
|
|
34
|
+
return indices && indices.length > 0 ? indices : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (pathCount === 1 && langCount === 0) {
|
|
38
|
+
const fp = filterPaths?.[0];
|
|
39
|
+
if (!fp) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const indices = mappings.fileMapping.get(fp);
|
|
43
|
+
return indices && indices.length > 0 ? indices : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const selector: number[] = [];
|
|
47
|
+
for (const lang of filterLanguages ?? []) {
|
|
48
|
+
selector.push(...(mappings.languageMapping.get(lang) ?? []));
|
|
49
|
+
}
|
|
50
|
+
for (const fp of filterPaths ?? []) {
|
|
51
|
+
selector.push(...(mappings.fileMapping.get(fp) ?? []));
|
|
52
|
+
}
|
|
53
|
+
if (selector.length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return [...new Set(selector)];
|
|
57
|
+
}
|
package/src/index/dense.ts
CHANGED
|
@@ -1,70 +1,114 @@
|
|
|
1
|
-
/**
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
1
|
+
/** Uncompressed float32 semantic index. Debug/precision baseline only; production uses int8. */
|
|
3
2
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
4
|
-
import {
|
|
3
|
+
import { TopKDistanceCollector } from "./topk.ts";
|
|
4
|
+
|
|
5
|
+
function cosineDistanceFlat(
|
|
6
|
+
data: Float32Array,
|
|
7
|
+
dim: number,
|
|
8
|
+
docIndex: number,
|
|
9
|
+
query: Float32Array,
|
|
10
|
+
): number {
|
|
11
|
+
const offset = docIndex * dim;
|
|
12
|
+
let dot = 0;
|
|
13
|
+
for (let i = 0; i < dim; i++) {
|
|
14
|
+
dot += (query[i] ?? 0) * (data[offset + i] ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return 1 - dot;
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
export class VectorIndex implements SemanticIndex {
|
|
7
|
-
private
|
|
20
|
+
private readonly data: Float32Array;
|
|
21
|
+
private readonly count: number;
|
|
22
|
+
private readonly dim: number;
|
|
23
|
+
private vectorsCache: Float32Array[] | null = null;
|
|
8
24
|
|
|
9
25
|
constructor(vectors: Float32Array[]) {
|
|
10
|
-
|
|
26
|
+
if (vectors.length === 0) {
|
|
27
|
+
this.count = 0;
|
|
28
|
+
this.dim = 0;
|
|
29
|
+
this.data = new Float32Array(0);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.count = vectors.length;
|
|
33
|
+
this.dim = vectors[0]?.length ?? 0;
|
|
34
|
+
this.data = new Float32Array(this.count * this.dim);
|
|
35
|
+
for (let i = 0; i < this.count; i++) {
|
|
36
|
+
const vec = vectors[i];
|
|
37
|
+
if (vec) {
|
|
38
|
+
this.data.set(vec, i * this.dim);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static fromFlatBuffer(data: Float32Array, count: number, dim: number): VectorIndex {
|
|
44
|
+
const index = Object.create(VectorIndex.prototype) as VectorIndex;
|
|
45
|
+
Object.assign(index, { data, count, dim });
|
|
46
|
+
return index;
|
|
11
47
|
}
|
|
12
48
|
|
|
13
49
|
get size(): number {
|
|
14
|
-
return this.
|
|
50
|
+
return this.count;
|
|
15
51
|
}
|
|
16
52
|
|
|
17
53
|
get dimensions(): number {
|
|
18
|
-
return this.
|
|
54
|
+
return this.dim;
|
|
19
55
|
}
|
|
20
56
|
|
|
21
57
|
getVectors(): readonly Float32Array[] {
|
|
22
|
-
|
|
58
|
+
if (this.vectorsCache) {
|
|
59
|
+
return this.vectorsCache;
|
|
60
|
+
}
|
|
61
|
+
const vectors: Float32Array[] = [];
|
|
62
|
+
for (let i = 0; i < this.count; i++) {
|
|
63
|
+
vectors.push(this.data.subarray(i * this.dim, (i + 1) * this.dim));
|
|
64
|
+
}
|
|
65
|
+
this.vectorsCache = vectors;
|
|
66
|
+
return vectors;
|
|
23
67
|
}
|
|
24
68
|
|
|
25
69
|
vectorAt(docIndex: number): Float32Array {
|
|
26
|
-
|
|
27
|
-
if (!vec) {
|
|
70
|
+
if (docIndex < 0 || docIndex >= this.count) {
|
|
28
71
|
throw new Error(`Missing vector at index ${docIndex}`);
|
|
29
72
|
}
|
|
30
|
-
return
|
|
73
|
+
return this.data.subarray(docIndex * this.dim, (docIndex + 1) * this.dim);
|
|
31
74
|
}
|
|
32
75
|
|
|
33
76
|
memoryBytes(): number {
|
|
34
|
-
return this.
|
|
77
|
+
return this.count * this.dim * 4;
|
|
35
78
|
}
|
|
36
79
|
|
|
37
80
|
query(
|
|
38
81
|
queryVector: Float32Array,
|
|
39
82
|
k: number,
|
|
40
|
-
selector?: number[],
|
|
83
|
+
selector?: readonly number[],
|
|
41
84
|
): { indices: number[]; distances: number[] } {
|
|
42
85
|
if (k < 1) {
|
|
43
86
|
throw new Error(`k should be >= 1, is now ${k}`);
|
|
44
87
|
}
|
|
45
|
-
|
|
46
|
-
const numVectors = this.vectors.length;
|
|
47
|
-
if (numVectors === 0) {
|
|
88
|
+
if (this.count === 0) {
|
|
48
89
|
return { indices: [], distances: [] };
|
|
49
90
|
}
|
|
50
91
|
|
|
51
|
-
const
|
|
52
|
-
const effectiveK = Math.min(k, indices.length);
|
|
92
|
+
const effectiveK = Math.min(k, selector?.length ?? this.count);
|
|
53
93
|
if (effectiveK === 0) {
|
|
54
94
|
return { indices: [], distances: [] };
|
|
55
95
|
}
|
|
56
96
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
97
|
+
const collector = new TopKDistanceCollector(effectiveK);
|
|
98
|
+
if (selector) {
|
|
99
|
+
for (const idx of selector) {
|
|
100
|
+
if (idx < 0 || idx >= this.count) {
|
|
101
|
+
continue;
|
|
62
102
|
}
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
collector.offer(idx, cosineDistanceFlat(this.data, this.dim, idx, queryVector));
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
for (let i = 0; i < this.count; i++) {
|
|
107
|
+
collector.offer(i, cosineDistanceFlat(this.data, this.dim, i, queryVector));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
67
110
|
|
|
111
|
+
const top = collector.finish();
|
|
68
112
|
return {
|
|
69
113
|
indices: top.map((t) => t.index),
|
|
70
114
|
distances: top.map((t) => t.distance),
|
|
@@ -72,21 +116,10 @@ export class VectorIndex implements SemanticIndex {
|
|
|
72
116
|
}
|
|
73
117
|
|
|
74
118
|
async save(dir: string): Promise<void> {
|
|
75
|
-
await
|
|
76
|
-
|
|
77
|
-
const dim = this.dimensions;
|
|
78
|
-
const count = this.size;
|
|
79
|
-
const buffer = new Float32Array(count * dim);
|
|
80
|
-
for (let i = 0; i < count; i++) {
|
|
81
|
-
const vec = this.vectors[i];
|
|
82
|
-
if (vec) {
|
|
83
|
-
buffer.set(vec, i * dim);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
await Bun.write(`${dir}/vectors.bin`, buffer);
|
|
119
|
+
await Bun.write(`${dir}/vectors.bin`, this.data);
|
|
87
120
|
await Bun.write(
|
|
88
121
|
`${dir}/meta.json`,
|
|
89
|
-
JSON.stringify({ count, dimensions: dim, storage: "float32" }),
|
|
122
|
+
JSON.stringify({ count: this.count, dimensions: this.dim, storage: "float32" }),
|
|
90
123
|
);
|
|
91
124
|
}
|
|
92
125
|
|
|
@@ -95,19 +128,7 @@ export class VectorIndex implements SemanticIndex {
|
|
|
95
128
|
count: number;
|
|
96
129
|
dimensions: number;
|
|
97
130
|
};
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
for (let i = 0; i < meta.count; i++) {
|
|
101
|
-
vectors.push(raw.subarray(i * meta.dimensions, (i + 1) * meta.dimensions));
|
|
102
|
-
}
|
|
103
|
-
return new VectorIndex(vectors);
|
|
131
|
+
const data = new Float32Array(await Bun.file(`${dir}/vectors.bin`).arrayBuffer());
|
|
132
|
+
return VectorIndex.fromFlatBuffer(data, meta.count, meta.dimensions);
|
|
104
133
|
}
|
|
105
134
|
}
|
|
106
|
-
|
|
107
|
-
function cosineDistance(a: Float32Array, b: Float32Array): number {
|
|
108
|
-
let dot = 0;
|
|
109
|
-
for (let i = 0; i < a.length; i++) {
|
|
110
|
-
dot += (a[i] ?? 0) * (b[i] ?? 0);
|
|
111
|
-
}
|
|
112
|
-
return 1 - dot;
|
|
113
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
2
|
import type { Chunk } from "../types.ts";
|
|
3
3
|
|
|
4
|
-
function indexedPath(absPath: string,
|
|
4
|
+
function indexedPath(absPath: string, _repoRoot: string, displayRoot?: string): string {
|
|
5
5
|
if (displayRoot) {
|
|
6
6
|
return relative(displayRoot, absPath).replace(/\\/g, "/");
|
|
7
7
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
type QuantizedVector = {
|
|
2
|
+
codes: Int8Array;
|
|
3
|
+
scale: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/** Reference scalar int8 dot product (A/B regression baseline). */
|
|
7
|
+
export function quantizedDotFlatScalar(
|
|
8
|
+
query: QuantizedVector,
|
|
9
|
+
codes: Int8Array,
|
|
10
|
+
offset: number,
|
|
11
|
+
dim: number,
|
|
12
|
+
docScale: number,
|
|
13
|
+
): number {
|
|
14
|
+
let sum = 0;
|
|
15
|
+
const q = query.codes;
|
|
16
|
+
for (let i = 0; i < dim; i++) {
|
|
17
|
+
sum += q[i] * codes[offset + i];
|
|
18
|
+
}
|
|
19
|
+
return sum * query.scale * docScale;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Production int8 dot product with 8-wide unrolling for JSC auto-vectorization. */
|
|
23
|
+
export function quantizedDotFlat(
|
|
24
|
+
query: QuantizedVector,
|
|
25
|
+
codes: Int8Array,
|
|
26
|
+
offset: number,
|
|
27
|
+
dim: number,
|
|
28
|
+
docScale: number,
|
|
29
|
+
): number {
|
|
30
|
+
const q = query.codes;
|
|
31
|
+
let s0 = 0;
|
|
32
|
+
let s1 = 0;
|
|
33
|
+
let s2 = 0;
|
|
34
|
+
let s3 = 0;
|
|
35
|
+
let s4 = 0;
|
|
36
|
+
let s5 = 0;
|
|
37
|
+
let s6 = 0;
|
|
38
|
+
let s7 = 0;
|
|
39
|
+
let i = 0;
|
|
40
|
+
for (; i + 7 < dim; i += 8) {
|
|
41
|
+
s0 += q[i] * codes[offset + i];
|
|
42
|
+
s1 += q[i + 1] * codes[offset + i + 1];
|
|
43
|
+
s2 += q[i + 2] * codes[offset + i + 2];
|
|
44
|
+
s3 += q[i + 3] * codes[offset + i + 3];
|
|
45
|
+
s4 += q[i + 4] * codes[offset + i + 4];
|
|
46
|
+
s5 += q[i + 5] * codes[offset + i + 5];
|
|
47
|
+
s6 += q[i + 6] * codes[offset + i + 6];
|
|
48
|
+
s7 += q[i + 7] * codes[offset + i + 7];
|
|
49
|
+
}
|
|
50
|
+
let sum = s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7;
|
|
51
|
+
for (; i < dim; i++) {
|
|
52
|
+
sum += q[i] * codes[offset + i];
|
|
53
|
+
}
|
|
54
|
+
return sum * query.scale * docScale;
|
|
55
|
+
}
|
package/src/index/persistence.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import { BM25Index } from "./bm25.ts";
|
|
4
3
|
import { VectorIndex } from "./dense.ts";
|
|
@@ -78,7 +77,6 @@ export async function saveIndexBundle(options: {
|
|
|
78
77
|
metadata: Record<string, unknown>;
|
|
79
78
|
}): Promise<void> {
|
|
80
79
|
const { paths, bm25, semantic, chunks, metadata } = options;
|
|
81
|
-
await mkdir(paths.root, { recursive: true });
|
|
82
80
|
await saveBm25(bm25, paths.bm25Index);
|
|
83
81
|
await saveSemantic(semantic, paths.semanticIndex);
|
|
84
82
|
await Bun.write(
|