lancedb-opencode-pro 0.1.1

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/dist/store.js ADDED
@@ -0,0 +1,283 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { tokenize } from "./utils.js";
4
+ const TABLE_NAME = "memories";
5
+ export class MemoryStore {
6
+ dbPath;
7
+ lancedb = null;
8
+ connection = null;
9
+ table = null;
10
+ indexState = {
11
+ vector: false,
12
+ fts: false,
13
+ ftsError: "",
14
+ };
15
+ scopeCache = new Map();
16
+ constructor(dbPath) {
17
+ this.dbPath = dbPath;
18
+ }
19
+ async init(vectorDim) {
20
+ await mkdir(this.dbPath, { recursive: true });
21
+ await mkdir(dirname(this.dbPath), { recursive: true });
22
+ this.lancedb = await import("@lancedb/lancedb");
23
+ this.connection = (await this.lancedb.connect(this.dbPath));
24
+ try {
25
+ this.table = await this.connection.openTable(TABLE_NAME);
26
+ }
27
+ catch {
28
+ const bootstrap = {
29
+ id: "__bootstrap__",
30
+ text: "",
31
+ vector: new Array(vectorDim).fill(0),
32
+ category: "other",
33
+ scope: "global",
34
+ importance: 0,
35
+ timestamp: 0,
36
+ schemaVersion: 1,
37
+ embeddingModel: "bootstrap",
38
+ vectorDim,
39
+ metadataJson: "{}",
40
+ };
41
+ this.table = await this.connection.createTable(TABLE_NAME, [bootstrap]);
42
+ await this.table.delete("id = '__bootstrap__'");
43
+ }
44
+ await this.ensureIndexes();
45
+ }
46
+ async put(record) {
47
+ const table = this.requireTable();
48
+ await table.add([record]);
49
+ this.invalidateScope(record.scope);
50
+ }
51
+ async search(params) {
52
+ const cached = await this.getCachedScopes(params.scopes);
53
+ if (cached.records.length === 0)
54
+ return [];
55
+ const queryTokens = tokenize(params.query);
56
+ const queryNorm = vecNorm(params.queryVector);
57
+ const scored = cached.records
58
+ .filter((record) => params.queryVector.length === 0 || record.vector.length === params.queryVector.length)
59
+ .map((record, index) => {
60
+ const recordNorm = cached.norms.get(record.id) ?? vecNorm(record.vector);
61
+ const vectorScore = fastCosine(params.queryVector, record.vector, queryNorm, recordNorm);
62
+ const bm25Score = bm25LikeScore(queryTokens, cached.tokenized[index], cached.idf);
63
+ const score = params.vectorWeight * vectorScore + params.bm25Weight * bm25Score;
64
+ return { record, score, vectorScore, bm25Score };
65
+ })
66
+ .filter((item) => item.score >= params.minScore)
67
+ .sort((a, b) => b.score - a.score)
68
+ .slice(0, params.limit);
69
+ return scored;
70
+ }
71
+ async deleteById(id, scopes) {
72
+ const rows = await this.readByScopes(scopes);
73
+ const match = rows.find((row) => row.id === id);
74
+ if (!match)
75
+ return false;
76
+ await this.requireTable().delete(`id = '${escapeSql(match.id)}'`);
77
+ this.invalidateScope(match.scope);
78
+ return true;
79
+ }
80
+ async clearScope(scope) {
81
+ const rows = await this.readByScopes([scope]);
82
+ if (rows.length === 0)
83
+ return 0;
84
+ await this.requireTable().delete(`scope = '${escapeSql(scope)}'`);
85
+ this.invalidateScope(scope);
86
+ return rows.length;
87
+ }
88
+ async list(scope, limit) {
89
+ const rows = await this.readByScopes([scope]);
90
+ return rows.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
91
+ }
92
+ async pruneScope(scope, maxEntries) {
93
+ const rows = await this.list(scope, 100000);
94
+ if (rows.length <= maxEntries)
95
+ return 0;
96
+ const toDelete = rows.slice(maxEntries);
97
+ for (const row of toDelete) {
98
+ await this.requireTable().delete(`id = '${escapeSql(row.id)}'`);
99
+ }
100
+ this.invalidateScope(scope);
101
+ return toDelete.length;
102
+ }
103
+ async countIncompatibleVectors(scopes, expectedDim) {
104
+ const rows = await this.readByScopes(scopes);
105
+ return rows.filter((row) => row.vectorDim !== expectedDim).length;
106
+ }
107
+ getIndexHealth() {
108
+ return {
109
+ vector: this.indexState.vector,
110
+ fts: this.indexState.fts,
111
+ ftsError: this.indexState.ftsError || undefined,
112
+ };
113
+ }
114
+ invalidateScope(scope) {
115
+ this.scopeCache.delete(scope);
116
+ }
117
+ async getCachedScopes(scopes) {
118
+ const allRecords = [];
119
+ const allTokenized = [];
120
+ const allNorms = new Map();
121
+ for (const scope of scopes) {
122
+ let entry = this.scopeCache.get(scope);
123
+ if (!entry) {
124
+ const records = await this.readByScopes([scope]);
125
+ const tokenized = records.map((record) => tokenize(record.text));
126
+ const idf = computeIdf(tokenized);
127
+ const norms = new Map();
128
+ for (const record of records) {
129
+ norms.set(record.id, vecNorm(record.vector));
130
+ }
131
+ entry = { records, tokenized, idf, norms };
132
+ this.scopeCache.set(scope, entry);
133
+ }
134
+ allRecords.push(...entry.records);
135
+ allTokenized.push(...entry.tokenized);
136
+ for (const [id, norm] of entry.norms) {
137
+ allNorms.set(id, norm);
138
+ }
139
+ }
140
+ const idf = scopes.length === 1 && this.scopeCache.has(scopes[0])
141
+ ? this.scopeCache.get(scopes[0]).idf
142
+ : computeIdf(allTokenized);
143
+ return { records: allRecords, tokenized: allTokenized, idf, norms: allNorms };
144
+ }
145
+ requireTable() {
146
+ if (!this.table) {
147
+ throw new Error("MemoryStore is not initialized");
148
+ }
149
+ return this.table;
150
+ }
151
+ async readByScopes(scopes) {
152
+ const table = this.requireTable();
153
+ if (scopes.length === 0)
154
+ return [];
155
+ const whereExpr = scopes.map((scope) => `scope = '${escapeSql(scope)}'`).join(" OR ");
156
+ const rows = await table
157
+ .query()
158
+ .where(`(${whereExpr})`)
159
+ .select([
160
+ "id",
161
+ "text",
162
+ "vector",
163
+ "category",
164
+ "scope",
165
+ "importance",
166
+ "timestamp",
167
+ "schemaVersion",
168
+ "embeddingModel",
169
+ "vectorDim",
170
+ "metadataJson",
171
+ ])
172
+ .limit(100000)
173
+ .toArray();
174
+ return rows
175
+ .map((row) => normalizeRow(row))
176
+ .filter((row) => row !== null);
177
+ }
178
+ async ensureIndexes() {
179
+ const table = this.requireTable();
180
+ try {
181
+ await table.createIndex("vector");
182
+ this.indexState.vector = true;
183
+ }
184
+ catch {
185
+ this.indexState.vector = false;
186
+ }
187
+ try {
188
+ if (this.lancedb && "Index" in this.lancedb) {
189
+ const anyLance = this.lancedb;
190
+ const cfg = anyLance.Index?.fts ? { config: anyLance.Index.fts() } : undefined;
191
+ await table.createIndex("text", cfg);
192
+ }
193
+ else {
194
+ await table.createIndex("text");
195
+ }
196
+ this.indexState.fts = true;
197
+ this.indexState.ftsError = "";
198
+ }
199
+ catch (error) {
200
+ this.indexState.fts = false;
201
+ this.indexState.ftsError = error instanceof Error ? error.message : String(error);
202
+ }
203
+ }
204
+ }
205
+ function normalizeRow(row) {
206
+ const vectorRaw = row.vector;
207
+ const vector = Array.isArray(vectorRaw) ? vectorRaw.map((item) => Number(item)) : Array.from((vectorRaw ?? []));
208
+ if (typeof row.id !== "string" || typeof row.text !== "string" || typeof row.scope !== "string") {
209
+ return null;
210
+ }
211
+ return {
212
+ id: row.id,
213
+ text: row.text,
214
+ vector,
215
+ category: row.category ?? "other",
216
+ scope: row.scope,
217
+ importance: Number(row.importance ?? 0.5),
218
+ timestamp: Number(row.timestamp ?? Date.now()),
219
+ schemaVersion: Number(row.schemaVersion ?? 1),
220
+ embeddingModel: String(row.embeddingModel ?? "unknown"),
221
+ vectorDim: Number(row.vectorDim ?? vector.length),
222
+ metadataJson: String(row.metadataJson ?? "{}"),
223
+ };
224
+ }
225
+ function escapeSql(value) {
226
+ return value.replace(/'/g, "''");
227
+ }
228
+ function computeIdf(docs) {
229
+ const df = new Map();
230
+ for (const doc of docs) {
231
+ const seen = new Set(doc);
232
+ for (const token of seen) {
233
+ df.set(token, (df.get(token) ?? 0) + 1);
234
+ }
235
+ }
236
+ const totalDocs = Math.max(1, docs.length);
237
+ const idf = new Map();
238
+ for (const [token, count] of df.entries()) {
239
+ idf.set(token, Math.log(1 + (totalDocs - count + 0.5) / (count + 0.5)));
240
+ }
241
+ return idf;
242
+ }
243
+ function vecNorm(v) {
244
+ let sum = 0;
245
+ for (let i = 0; i < v.length; i += 1) {
246
+ sum += v[i] * v[i];
247
+ }
248
+ return Math.sqrt(sum);
249
+ }
250
+ function fastCosine(a, b, normA, normB) {
251
+ if (a.length === 0 || b.length === 0 || a.length !== b.length)
252
+ return 0;
253
+ const denom = normA * normB;
254
+ if (denom === 0)
255
+ return 0;
256
+ let dot = 0;
257
+ for (let i = 0; i < a.length; i += 1) {
258
+ dot += a[i] * b[i];
259
+ }
260
+ return dot / denom;
261
+ }
262
+ function bm25LikeScore(query, doc, idf) {
263
+ if (query.length === 0 || doc.length === 0)
264
+ return 0;
265
+ const tf = new Map();
266
+ for (const token of doc) {
267
+ tf.set(token, (tf.get(token) ?? 0) + 1);
268
+ }
269
+ const avgDocLen = 120;
270
+ const k1 = 1.2;
271
+ const b = 0.75;
272
+ let score = 0;
273
+ for (const token of query) {
274
+ const freq = tf.get(token) ?? 0;
275
+ if (freq === 0)
276
+ continue;
277
+ const tokenIdf = idf.get(token) ?? 0.1;
278
+ const numerator = freq * (k1 + 1);
279
+ const denominator = freq + k1 * (1 - b + (b * doc.length) / avgDocLen);
280
+ score += tokenIdf * (numerator / denominator);
281
+ }
282
+ return 1 - Math.exp(-score);
283
+ }
@@ -0,0 +1,48 @@
1
+ export type EmbeddingProvider = "ollama";
2
+ export type RetrievalMode = "hybrid" | "vector";
3
+ export type MemoryCategory = "preference" | "fact" | "decision" | "entity" | "other";
4
+ export interface EmbeddingConfig {
5
+ provider: EmbeddingProvider;
6
+ model: string;
7
+ baseUrl?: string;
8
+ timeoutMs?: number;
9
+ }
10
+ export interface RetrievalConfig {
11
+ mode: RetrievalMode;
12
+ vectorWeight: number;
13
+ bm25Weight: number;
14
+ minScore: number;
15
+ }
16
+ export interface MemoryRuntimeConfig {
17
+ provider: string;
18
+ dbPath: string;
19
+ embedding: EmbeddingConfig;
20
+ retrieval: RetrievalConfig;
21
+ includeGlobalScope: boolean;
22
+ minCaptureChars: number;
23
+ maxEntriesPerScope: number;
24
+ }
25
+ export interface MemoryRecord {
26
+ id: string;
27
+ text: string;
28
+ vector: number[];
29
+ category: MemoryCategory;
30
+ scope: string;
31
+ importance: number;
32
+ timestamp: number;
33
+ schemaVersion: number;
34
+ embeddingModel: string;
35
+ vectorDim: number;
36
+ metadataJson: string;
37
+ }
38
+ export interface SearchResult {
39
+ record: MemoryRecord;
40
+ score: number;
41
+ vectorScore: number;
42
+ bm25Score: number;
43
+ }
44
+ export interface CaptureCandidate {
45
+ text: string;
46
+ category: MemoryCategory;
47
+ importance: number;
48
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export declare function expandHomePath(input: string): string;
2
+ export declare function toNumber(value: unknown, fallback: number): number;
3
+ export declare function toBoolean(value: unknown, fallback: boolean): boolean;
4
+ export declare function clamp(value: number, min: number, max: number): number;
5
+ export declare function stableHash(input: string): string;
6
+ export declare function tokenize(text: string): string[];
7
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
8
+ export declare function generateId(): string;
9
+ export declare function parseJsonObject<T>(value: string | undefined, fallback: T): T;
package/dist/utils.js ADDED
@@ -0,0 +1,73 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function expandHomePath(input) {
5
+ if (input === "~")
6
+ return homedir();
7
+ if (input.startsWith("~/"))
8
+ return join(homedir(), input.slice(2));
9
+ return input;
10
+ }
11
+ export function toNumber(value, fallback) {
12
+ if (typeof value === "number" && Number.isFinite(value))
13
+ return value;
14
+ if (typeof value === "string" && value.trim()) {
15
+ const parsed = Number(value);
16
+ if (Number.isFinite(parsed))
17
+ return parsed;
18
+ }
19
+ return fallback;
20
+ }
21
+ export function toBoolean(value, fallback) {
22
+ if (typeof value === "boolean")
23
+ return value;
24
+ if (typeof value === "string") {
25
+ const normalized = value.trim().toLowerCase();
26
+ if (["1", "true", "yes", "on"].includes(normalized))
27
+ return true;
28
+ if (["0", "false", "no", "off"].includes(normalized))
29
+ return false;
30
+ }
31
+ return fallback;
32
+ }
33
+ export function clamp(value, min, max) {
34
+ return Math.max(min, Math.min(max, value));
35
+ }
36
+ export function stableHash(input) {
37
+ return createHash("sha256").update(input, "utf8").digest("hex");
38
+ }
39
+ export function tokenize(text) {
40
+ return text
41
+ .toLowerCase()
42
+ .replace(/[^\p{L}\p{N}\s_-]+/gu, " ")
43
+ .split(/\s+/)
44
+ .filter((token) => token.length > 1);
45
+ }
46
+ export function cosineSimilarity(a, b) {
47
+ if (a.length === 0 || b.length === 0 || a.length !== b.length)
48
+ return 0;
49
+ let dot = 0;
50
+ let normA = 0;
51
+ let normB = 0;
52
+ for (let i = 0; i < a.length; i += 1) {
53
+ dot += a[i] * b[i];
54
+ normA += a[i] * a[i];
55
+ normB += b[i] * b[i];
56
+ }
57
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
58
+ return denom === 0 ? 0 : dot / denom;
59
+ }
60
+ export function generateId() {
61
+ return randomUUID();
62
+ }
63
+ export function parseJsonObject(value, fallback) {
64
+ if (!value)
65
+ return fallback;
66
+ try {
67
+ const parsed = JSON.parse(value);
68
+ return parsed;
69
+ }
70
+ catch {
71
+ return fallback;
72
+ }
73
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "lancedb-opencode-pro",
3
+ "version": "0.1.1",
4
+ "description": "LanceDB-backed long-term memory provider for OpenCode",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "dist/index.d.ts",
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "opencode",
22
+ "plugin",
23
+ "memory",
24
+ "lancedb"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://gitlab-238.ichiayi.com/jonathan/lancedb-opencode-pro.git"
29
+ },
30
+ "homepage": "https://gitlab-238.ichiayi.com/jonathan/lancedb-opencode-pro",
31
+ "bugs": {
32
+ "url": "https://gitlab-238.ichiayi.com/jonathan/lancedb-opencode-pro/-/issues"
33
+ },
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "build:test": "tsc -p tsconfig.test.json",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit",
46
+ "test": "npm run typecheck",
47
+ "test:foundation": "npm run build:test && node --test dist-test/test/foundation/foundation.test.js",
48
+ "test:regression": "npm run build:test && node --test dist-test/test/regression/plugin.test.js",
49
+ "test:retrieval": "npm run build:test && node --test dist-test/test/retrieval/retrieval.test.js",
50
+ "benchmark:latency": "npm run build:test && node dist-test/test/benchmark/latency.js",
51
+ "test:e2e": "node scripts/e2e-opencode-memory.mjs",
52
+ "verify": "npm run typecheck && npm run build && npm run test:foundation && npm run test:regression && npm run test:retrieval",
53
+ "verify:full": "npm run verify && npm run benchmark:latency && npm pack",
54
+ "release:check": "npm run verify:full && npm publish --dry-run",
55
+ "prepublishOnly": "npm run verify:full"
56
+ },
57
+ "dependencies": {
58
+ "@lancedb/lancedb": "^0.26.2",
59
+ "@opencode-ai/plugin": "1.2.25",
60
+ "@opencode-ai/sdk": "1.2.25"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^22.13.9",
64
+ "typescript": "^5.8.2"
65
+ }
66
+ }