@context-vault/core 2.17.1 → 3.0.3
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/capture.d.ts +21 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +269 -0
- package/dist/capture.js.map +1 -0
- package/dist/categories.d.ts +6 -0
- package/dist/categories.d.ts.map +1 -0
- package/dist/categories.js +50 -0
- package/dist/categories.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +190 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +23 -0
- package/dist/constants.js.map +1 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +191 -0
- package/dist/db.js.map +1 -0
- package/dist/embed.d.ts +5 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +78 -0
- package/dist/embed.js.map +1 -0
- package/dist/files.d.ts +13 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +66 -0
- package/dist/files.js.map +1 -0
- package/dist/formatters.d.ts +8 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +18 -0
- package/dist/formatters.js.map +1 -0
- package/dist/frontmatter.d.ts +12 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +101 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +297 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-url.d.ts +20 -0
- package/dist/ingest-url.d.ts.map +1 -0
- package/dist/ingest-url.js +113 -0
- package/dist/ingest-url.js.map +1 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +25 -0
- package/dist/main.js.map +1 -0
- package/dist/search.d.ts +18 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +238 -0
- package/dist/search.js.map +1 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -16
- package/src/capture.ts +308 -0
- package/src/categories.ts +54 -0
- package/src/{core/config.js → config.ts} +34 -33
- package/src/{constants.js → constants.ts} +6 -3
- package/src/db.ts +229 -0
- package/src/{index/embed.js → embed.ts} +10 -35
- package/src/{core/files.js → files.ts} +15 -20
- package/src/{capture/formatters.js → formatters.ts} +13 -11
- package/src/{core/frontmatter.js → frontmatter.ts} +26 -33
- package/src/index.ts +353 -0
- package/src/ingest-url.ts +99 -0
- package/src/main.ts +111 -0
- package/src/{retrieve/index.js → search.ts} +62 -150
- package/src/types.ts +166 -0
- package/src/capture/file-ops.js +0 -99
- package/src/capture/import-pipeline.js +0 -46
- package/src/capture/importers.js +0 -387
- package/src/capture/index.js +0 -250
- package/src/capture/ingest-url.js +0 -252
- package/src/consolidation/index.js +0 -112
- package/src/core/categories.js +0 -73
- package/src/core/error-log.js +0 -54
- package/src/core/linking.js +0 -161
- package/src/core/migrate-dirs.js +0 -196
- package/src/core/status.js +0 -350
- package/src/core/telemetry.js +0 -90
- package/src/core/temporal.js +0 -146
- package/src/index/db.js +0 -586
- package/src/index/index.js +0 -583
- package/src/index.js +0 -71
- package/src/server/helpers.js +0 -44
- package/src/server/tools/clear-context.js +0 -47
- package/src/server/tools/context-status.js +0 -182
- package/src/server/tools/create-snapshot.js +0 -200
- package/src/server/tools/delete-context.js +0 -60
- package/src/server/tools/get-context.js +0 -765
- package/src/server/tools/ingest-project.js +0 -244
- package/src/server/tools/ingest-url.js +0 -88
- package/src/server/tools/list-buckets.js +0 -116
- package/src/server/tools/list-context.js +0 -163
- package/src/server/tools/save-context.js +0 -632
- package/src/server/tools/session-start.js +0 -285
- package/src/server/tools.js +0 -172
- package/src/sync/sync.js +0 -235
package/src/index/db.js
DELETED
|
@@ -1,586 +0,0 @@
|
|
|
1
|
-
import { unlinkSync, copyFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { DatabaseSync } from "node:sqlite";
|
|
3
|
-
|
|
4
|
-
export class NativeModuleError extends Error {
|
|
5
|
-
constructor(originalError) {
|
|
6
|
-
const diagnostic = formatNativeModuleError(originalError);
|
|
7
|
-
super(diagnostic);
|
|
8
|
-
this.name = "NativeModuleError";
|
|
9
|
-
this.originalError = originalError;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatNativeModuleError(err) {
|
|
14
|
-
const msg = err.message || "";
|
|
15
|
-
return [
|
|
16
|
-
`sqlite-vec extension failed to load: ${msg}`,
|
|
17
|
-
"",
|
|
18
|
-
` Running Node.js: ${process.version} (${process.execPath})`,
|
|
19
|
-
"",
|
|
20
|
-
" Fix: Reinstall context-vault:",
|
|
21
|
-
" npx -y context-vault@latest setup",
|
|
22
|
-
].join("\n");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let _sqliteVec = null;
|
|
26
|
-
|
|
27
|
-
async function loadSqliteVec() {
|
|
28
|
-
if (_sqliteVec) return _sqliteVec;
|
|
29
|
-
const vecMod = await import("sqlite-vec");
|
|
30
|
-
_sqliteVec = vecMod;
|
|
31
|
-
return _sqliteVec;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function runTransaction(db, fn) {
|
|
35
|
-
db.exec("BEGIN");
|
|
36
|
-
try {
|
|
37
|
-
fn();
|
|
38
|
-
db.exec("COMMIT");
|
|
39
|
-
} catch (e) {
|
|
40
|
-
db.exec("ROLLBACK");
|
|
41
|
-
throw e;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Local-mode schema: no multi-tenancy or encryption columns.
|
|
46
|
-
// Identity uniqueness is scoped to (kind, identity_key) — no user_id.
|
|
47
|
-
export const LOCAL_SCHEMA_DDL = `
|
|
48
|
-
CREATE TABLE IF NOT EXISTS vault (
|
|
49
|
-
id TEXT PRIMARY KEY,
|
|
50
|
-
kind TEXT NOT NULL,
|
|
51
|
-
category TEXT NOT NULL DEFAULT 'knowledge',
|
|
52
|
-
title TEXT,
|
|
53
|
-
body TEXT NOT NULL,
|
|
54
|
-
meta TEXT,
|
|
55
|
-
tags TEXT,
|
|
56
|
-
source TEXT,
|
|
57
|
-
file_path TEXT UNIQUE,
|
|
58
|
-
identity_key TEXT,
|
|
59
|
-
expires_at TEXT,
|
|
60
|
-
superseded_by TEXT,
|
|
61
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
62
|
-
updated_at TEXT,
|
|
63
|
-
hit_count INTEGER DEFAULT 0,
|
|
64
|
-
last_accessed_at TEXT,
|
|
65
|
-
source_files TEXT,
|
|
66
|
-
tier TEXT DEFAULT 'working' CHECK(tier IN ('ephemeral', 'working', 'durable')),
|
|
67
|
-
related_to TEXT
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
CREATE INDEX IF NOT EXISTS idx_vault_kind ON vault(kind);
|
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_vault_category ON vault(category);
|
|
72
|
-
CREATE INDEX IF NOT EXISTS idx_vault_category_created ON vault(category, created_at DESC);
|
|
73
|
-
CREATE INDEX IF NOT EXISTS idx_vault_updated ON vault(updated_at DESC);
|
|
74
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(kind, identity_key) WHERE identity_key IS NOT NULL AND category = 'entity';
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL;
|
|
76
|
-
CREATE INDEX IF NOT EXISTS idx_vault_tier ON vault(tier);
|
|
77
|
-
|
|
78
|
-
-- Single FTS5 table
|
|
79
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS vault_fts USING fts5(
|
|
80
|
-
title, body, tags, kind,
|
|
81
|
-
content='vault', content_rowid='rowid'
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
-- FTS sync triggers
|
|
85
|
-
CREATE TRIGGER IF NOT EXISTS vault_ai AFTER INSERT ON vault BEGIN
|
|
86
|
-
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
87
|
-
VALUES (new.rowid, new.title, new.body, new.tags, new.kind);
|
|
88
|
-
END;
|
|
89
|
-
CREATE TRIGGER IF NOT EXISTS vault_ad AFTER DELETE ON vault BEGIN
|
|
90
|
-
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
91
|
-
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
92
|
-
END;
|
|
93
|
-
CREATE TRIGGER IF NOT EXISTS vault_au AFTER UPDATE ON vault BEGIN
|
|
94
|
-
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
95
|
-
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
96
|
-
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
97
|
-
VALUES (new.rowid, new.title, new.body, new.tags, new.kind);
|
|
98
|
-
END;
|
|
99
|
-
|
|
100
|
-
-- Single vec table (384-dim float32 for all-MiniLM-L6-v2)
|
|
101
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS vault_vec USING vec0(embedding float[384]);
|
|
102
|
-
`;
|
|
103
|
-
|
|
104
|
-
// Hosted-mode schema: adds multi-tenancy (user_id, team_id) and at-rest
|
|
105
|
-
// encryption columns (body_encrypted, title_encrypted, meta_encrypted, iv).
|
|
106
|
-
// Identity uniqueness is scoped to (user_id, kind, identity_key).
|
|
107
|
-
export const HOSTED_SCHEMA_DDL = `
|
|
108
|
-
CREATE TABLE IF NOT EXISTS vault (
|
|
109
|
-
id TEXT PRIMARY KEY,
|
|
110
|
-
kind TEXT NOT NULL,
|
|
111
|
-
category TEXT NOT NULL DEFAULT 'knowledge',
|
|
112
|
-
title TEXT,
|
|
113
|
-
body TEXT NOT NULL,
|
|
114
|
-
meta TEXT,
|
|
115
|
-
tags TEXT,
|
|
116
|
-
source TEXT,
|
|
117
|
-
file_path TEXT UNIQUE,
|
|
118
|
-
identity_key TEXT,
|
|
119
|
-
expires_at TEXT,
|
|
120
|
-
superseded_by TEXT,
|
|
121
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
122
|
-
updated_at TEXT,
|
|
123
|
-
user_id TEXT,
|
|
124
|
-
team_id TEXT,
|
|
125
|
-
body_encrypted BLOB,
|
|
126
|
-
title_encrypted BLOB,
|
|
127
|
-
meta_encrypted BLOB,
|
|
128
|
-
iv BLOB,
|
|
129
|
-
hit_count INTEGER DEFAULT 0,
|
|
130
|
-
last_accessed_at TEXT,
|
|
131
|
-
source_files TEXT,
|
|
132
|
-
tier TEXT DEFAULT 'working' CHECK(tier IN ('ephemeral', 'working', 'durable')),
|
|
133
|
-
related_to TEXT
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
CREATE INDEX IF NOT EXISTS idx_vault_kind ON vault(kind);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_vault_category ON vault(category);
|
|
138
|
-
CREATE INDEX IF NOT EXISTS idx_vault_category_created ON vault(category, created_at DESC);
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_vault_updated ON vault(updated_at DESC);
|
|
140
|
-
CREATE INDEX IF NOT EXISTS idx_vault_user ON vault(user_id);
|
|
141
|
-
CREATE INDEX IF NOT EXISTS idx_vault_team ON vault(team_id);
|
|
142
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(user_id, kind, identity_key) WHERE identity_key IS NOT NULL;
|
|
143
|
-
CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL;
|
|
144
|
-
CREATE INDEX IF NOT EXISTS idx_vault_tier ON vault(tier);
|
|
145
|
-
|
|
146
|
-
-- Single FTS5 table
|
|
147
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS vault_fts USING fts5(
|
|
148
|
-
title, body, tags, kind,
|
|
149
|
-
content='vault', content_rowid='rowid'
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
-- FTS sync triggers
|
|
153
|
-
CREATE TRIGGER IF NOT EXISTS vault_ai AFTER INSERT ON vault BEGIN
|
|
154
|
-
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
155
|
-
VALUES (new.rowid, new.title, new.body, new.tags, new.kind);
|
|
156
|
-
END;
|
|
157
|
-
CREATE TRIGGER IF NOT EXISTS vault_ad AFTER DELETE ON vault BEGIN
|
|
158
|
-
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
159
|
-
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
160
|
-
END;
|
|
161
|
-
CREATE TRIGGER IF NOT EXISTS vault_au AFTER UPDATE ON vault BEGIN
|
|
162
|
-
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
163
|
-
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
164
|
-
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
165
|
-
VALUES (new.rowid, new.title, new.body, new.tags, new.kind);
|
|
166
|
-
END;
|
|
167
|
-
|
|
168
|
-
-- Single vec table (384-dim float32 for all-MiniLM-L6-v2)
|
|
169
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS vault_vec USING vec0(embedding float[384]);
|
|
170
|
-
`;
|
|
171
|
-
|
|
172
|
-
// Backward-compatible alias — kept for external consumers that reference SCHEMA_DDL.
|
|
173
|
-
export const SCHEMA_DDL = HOSTED_SCHEMA_DDL;
|
|
174
|
-
|
|
175
|
-
// Current target schema version. Bump this on every migration.
|
|
176
|
-
const CURRENT_VERSION = 14;
|
|
177
|
-
|
|
178
|
-
export async function initDatabase(dbPath, { mode = "local" } = {}) {
|
|
179
|
-
const sqliteVec = await loadSqliteVec();
|
|
180
|
-
|
|
181
|
-
function createDb(path) {
|
|
182
|
-
const db = new DatabaseSync(path, { allowExtension: true });
|
|
183
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
184
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
185
|
-
try {
|
|
186
|
-
sqliteVec.load(db);
|
|
187
|
-
} catch (e) {
|
|
188
|
-
throw new NativeModuleError(e);
|
|
189
|
-
}
|
|
190
|
-
return db;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const schemaDdl = mode === "hosted" ? HOSTED_SCHEMA_DDL : LOCAL_SCHEMA_DDL;
|
|
194
|
-
|
|
195
|
-
const db = createDb(dbPath);
|
|
196
|
-
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
197
|
-
|
|
198
|
-
// Enforce fresh-DB-only — old schemas get a full rebuild (with backup)
|
|
199
|
-
if (version > 0 && version < 5) {
|
|
200
|
-
console.error(
|
|
201
|
-
`[context-vault] Schema v${version} is outdated. Rebuilding database...`,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
// Backup old DB before destroying it
|
|
205
|
-
const backupPath = `${dbPath}.v${version}.backup`;
|
|
206
|
-
try {
|
|
207
|
-
db.close();
|
|
208
|
-
if (existsSync(dbPath)) {
|
|
209
|
-
copyFileSync(dbPath, backupPath);
|
|
210
|
-
console.error(
|
|
211
|
-
`[context-vault] Backed up old database to: ${backupPath}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
} catch (backupErr) {
|
|
215
|
-
console.error(
|
|
216
|
-
`[context-vault] Warning: could not backup old database: ${backupErr.message}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
unlinkSync(dbPath);
|
|
221
|
-
try {
|
|
222
|
-
unlinkSync(dbPath + "-wal");
|
|
223
|
-
} catch {}
|
|
224
|
-
try {
|
|
225
|
-
unlinkSync(dbPath + "-shm");
|
|
226
|
-
} catch {}
|
|
227
|
-
|
|
228
|
-
const freshDb = createDb(dbPath);
|
|
229
|
-
freshDb.exec(schemaDdl);
|
|
230
|
-
freshDb.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
231
|
-
return freshDb;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (version < 5) {
|
|
235
|
-
db.exec(schemaDdl);
|
|
236
|
-
db.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
237
|
-
} else if (version === 5) {
|
|
238
|
-
// v5 -> v6 migration: add multi-tenancy + encryption columns
|
|
239
|
-
// Wrapped in transaction with duplicate-column guards for idempotent retry
|
|
240
|
-
runTransaction(db, () => {
|
|
241
|
-
const addColumnSafe = (sql) => {
|
|
242
|
-
try {
|
|
243
|
-
db.exec(sql);
|
|
244
|
-
} catch (e) {
|
|
245
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN user_id TEXT`);
|
|
249
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN body_encrypted BLOB`);
|
|
250
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN title_encrypted BLOB`);
|
|
251
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN meta_encrypted BLOB`);
|
|
252
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN iv BLOB`);
|
|
253
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN team_id TEXT`);
|
|
254
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN updated_at TEXT`);
|
|
255
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN superseded_by TEXT`);
|
|
256
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_vault_user ON vault(user_id)`);
|
|
257
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_vault_team ON vault(team_id)`);
|
|
258
|
-
db.exec(`DROP INDEX IF EXISTS idx_vault_identity`);
|
|
259
|
-
db.exec(
|
|
260
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(user_id, kind, identity_key) WHERE identity_key IS NOT NULL`,
|
|
261
|
-
);
|
|
262
|
-
db.exec(
|
|
263
|
-
`UPDATE vault SET updated_at = created_at WHERE updated_at IS NULL`,
|
|
264
|
-
);
|
|
265
|
-
db.exec(
|
|
266
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_updated ON vault(updated_at DESC)`,
|
|
267
|
-
);
|
|
268
|
-
db.exec(
|
|
269
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL`,
|
|
270
|
-
);
|
|
271
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN hit_count INTEGER DEFAULT 0`);
|
|
272
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN last_accessed_at TEXT`);
|
|
273
|
-
db.exec("PRAGMA user_version = 10");
|
|
274
|
-
});
|
|
275
|
-
} else if (version === 6) {
|
|
276
|
-
// v6 -> v7+v8+v9 migration: add team_id, updated_at, superseded_by columns
|
|
277
|
-
runTransaction(db, () => {
|
|
278
|
-
try {
|
|
279
|
-
db.exec(`ALTER TABLE vault ADD COLUMN team_id TEXT`);
|
|
280
|
-
} catch (e) {
|
|
281
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
282
|
-
}
|
|
283
|
-
try {
|
|
284
|
-
db.exec(`ALTER TABLE vault ADD COLUMN updated_at TEXT`);
|
|
285
|
-
} catch (e) {
|
|
286
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
287
|
-
}
|
|
288
|
-
try {
|
|
289
|
-
db.exec(`ALTER TABLE vault ADD COLUMN superseded_by TEXT`);
|
|
290
|
-
} catch (e) {
|
|
291
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
292
|
-
}
|
|
293
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_vault_team ON vault(team_id)`);
|
|
294
|
-
db.exec(
|
|
295
|
-
`UPDATE vault SET updated_at = created_at WHERE updated_at IS NULL`,
|
|
296
|
-
);
|
|
297
|
-
db.exec(
|
|
298
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_updated ON vault(updated_at DESC)`,
|
|
299
|
-
);
|
|
300
|
-
db.exec(
|
|
301
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL`,
|
|
302
|
-
);
|
|
303
|
-
try {
|
|
304
|
-
db.exec(`ALTER TABLE vault ADD COLUMN hit_count INTEGER DEFAULT 0`);
|
|
305
|
-
} catch (e) {
|
|
306
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
db.exec(`ALTER TABLE vault ADD COLUMN last_accessed_at TEXT`);
|
|
310
|
-
} catch (e) {
|
|
311
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
312
|
-
}
|
|
313
|
-
db.exec("PRAGMA user_version = 10");
|
|
314
|
-
});
|
|
315
|
-
} else if (version === 7) {
|
|
316
|
-
// v7 -> v8+v9 migration: add updated_at, superseded_by columns
|
|
317
|
-
runTransaction(db, () => {
|
|
318
|
-
try {
|
|
319
|
-
db.exec(`ALTER TABLE vault ADD COLUMN updated_at TEXT`);
|
|
320
|
-
} catch (e) {
|
|
321
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
db.exec(`ALTER TABLE vault ADD COLUMN superseded_by TEXT`);
|
|
325
|
-
} catch (e) {
|
|
326
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
327
|
-
}
|
|
328
|
-
db.exec(
|
|
329
|
-
`UPDATE vault SET updated_at = created_at WHERE updated_at IS NULL`,
|
|
330
|
-
);
|
|
331
|
-
db.exec(
|
|
332
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_updated ON vault(updated_at DESC)`,
|
|
333
|
-
);
|
|
334
|
-
db.exec(
|
|
335
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL`,
|
|
336
|
-
);
|
|
337
|
-
try {
|
|
338
|
-
db.exec(`ALTER TABLE vault ADD COLUMN hit_count INTEGER DEFAULT 0`);
|
|
339
|
-
} catch (e) {
|
|
340
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
341
|
-
}
|
|
342
|
-
try {
|
|
343
|
-
db.exec(`ALTER TABLE vault ADD COLUMN last_accessed_at TEXT`);
|
|
344
|
-
} catch (e) {
|
|
345
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
346
|
-
}
|
|
347
|
-
db.exec("PRAGMA user_version = 10");
|
|
348
|
-
});
|
|
349
|
-
} else if (version === 8) {
|
|
350
|
-
// v8 -> v9 migration: add superseded_by column
|
|
351
|
-
runTransaction(db, () => {
|
|
352
|
-
try {
|
|
353
|
-
db.exec(`ALTER TABLE vault ADD COLUMN superseded_by TEXT`);
|
|
354
|
-
} catch (e) {
|
|
355
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
356
|
-
}
|
|
357
|
-
db.exec(
|
|
358
|
-
`CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL`,
|
|
359
|
-
);
|
|
360
|
-
db.exec("PRAGMA user_version = 9");
|
|
361
|
-
});
|
|
362
|
-
// fall through to v9 migration
|
|
363
|
-
runTransaction(db, () => {
|
|
364
|
-
const addColumnSafe = (sql) => {
|
|
365
|
-
try {
|
|
366
|
-
db.exec(sql);
|
|
367
|
-
} catch (e) {
|
|
368
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN hit_count INTEGER DEFAULT 0`);
|
|
372
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN last_accessed_at TEXT`);
|
|
373
|
-
db.exec("PRAGMA user_version = 10");
|
|
374
|
-
});
|
|
375
|
-
} else if (version === 9) {
|
|
376
|
-
// v9 -> v10 migration: add hit_count + last_accessed_at columns
|
|
377
|
-
runTransaction(db, () => {
|
|
378
|
-
const addColumnSafe = (sql) => {
|
|
379
|
-
try {
|
|
380
|
-
db.exec(sql);
|
|
381
|
-
} catch (e) {
|
|
382
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN hit_count INTEGER DEFAULT 0`);
|
|
386
|
-
addColumnSafe(`ALTER TABLE vault ADD COLUMN last_accessed_at TEXT`);
|
|
387
|
-
db.exec("PRAGMA user_version = 10");
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (version >= 5 && version <= 10) {
|
|
392
|
-
// v10 -> v11 migration: add source_files column for stale-linking
|
|
393
|
-
runTransaction(db, () => {
|
|
394
|
-
try {
|
|
395
|
-
db.exec(`ALTER TABLE vault ADD COLUMN source_files TEXT`);
|
|
396
|
-
} catch (e) {
|
|
397
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
398
|
-
}
|
|
399
|
-
db.exec("PRAGMA user_version = 11");
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (version >= 5 && version <= 11) {
|
|
404
|
-
// v11 -> v12 migration: add tier column for memory tiers
|
|
405
|
-
runTransaction(db, () => {
|
|
406
|
-
try {
|
|
407
|
-
db.exec(
|
|
408
|
-
`ALTER TABLE vault ADD COLUMN tier TEXT DEFAULT 'working' CHECK(tier IN ('ephemeral', 'working', 'durable'))`,
|
|
409
|
-
);
|
|
410
|
-
} catch (e) {
|
|
411
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
412
|
-
}
|
|
413
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_vault_tier ON vault(tier)`);
|
|
414
|
-
db.exec("PRAGMA user_version = 12");
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (version >= 5 && version <= 12) {
|
|
419
|
-
// v12 -> v13 migration: add related_to column for graph linking
|
|
420
|
-
runTransaction(db, () => {
|
|
421
|
-
try {
|
|
422
|
-
db.exec(`ALTER TABLE vault ADD COLUMN related_to TEXT`);
|
|
423
|
-
} catch (e) {
|
|
424
|
-
if (!e.message.includes("duplicate column")) throw e;
|
|
425
|
-
}
|
|
426
|
-
db.exec("PRAGMA user_version = 13");
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (version >= 5 && version <= 13) {
|
|
431
|
-
// v13 -> v14 migration: separate local and hosted schemas.
|
|
432
|
-
// Local mode: drop the 6 hosted-only columns (user_id, team_id,
|
|
433
|
-
// body_encrypted, title_encrypted, meta_encrypted, iv) and rebuild
|
|
434
|
-
// the identity index without user_id.
|
|
435
|
-
// Hosted mode: no structural change — just bump version.
|
|
436
|
-
runTransaction(db, () => {
|
|
437
|
-
if (mode === "local") {
|
|
438
|
-
// Must drop indexes that reference the columns before dropping columns.
|
|
439
|
-
db.exec(`DROP INDEX IF EXISTS idx_vault_user`);
|
|
440
|
-
db.exec(`DROP INDEX IF EXISTS idx_vault_team`);
|
|
441
|
-
db.exec(`DROP INDEX IF EXISTS idx_vault_identity`);
|
|
442
|
-
const dropColumnSafe = (col) => {
|
|
443
|
-
try {
|
|
444
|
-
db.exec(`ALTER TABLE vault DROP COLUMN ${col}`);
|
|
445
|
-
} catch (e) {
|
|
446
|
-
// Column may not exist on older schemas that never had it added.
|
|
447
|
-
if (!e.message.includes("no such column")) throw e;
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
dropColumnSafe("user_id");
|
|
451
|
-
dropColumnSafe("team_id");
|
|
452
|
-
dropColumnSafe("body_encrypted");
|
|
453
|
-
dropColumnSafe("title_encrypted");
|
|
454
|
-
dropColumnSafe("meta_encrypted");
|
|
455
|
-
dropColumnSafe("iv");
|
|
456
|
-
// Recreate identity uniqueness index scoped to (kind, identity_key),
|
|
457
|
-
// restricted to entity-category entries only (knowledge/event entries
|
|
458
|
-
// with identity_key are informational and may duplicate).
|
|
459
|
-
db.exec(
|
|
460
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(kind, identity_key) WHERE identity_key IS NOT NULL AND category = 'entity'`,
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
db.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return db;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export function prepareStatements(db, mode = "local") {
|
|
471
|
-
try {
|
|
472
|
-
if (mode === "local") {
|
|
473
|
-
// Local mode: no user_id, team_id, or encryption columns.
|
|
474
|
-
// insertEntry has 15 params (no user_id).
|
|
475
|
-
// getByIdentityKey and upsertByIdentityKey have no user_id WHERE clause.
|
|
476
|
-
return {
|
|
477
|
-
_mode: "local",
|
|
478
|
-
insertEntry: db.prepare(
|
|
479
|
-
`INSERT INTO vault (id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at, source_files, tier) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
480
|
-
),
|
|
481
|
-
updateEntry: db.prepare(
|
|
482
|
-
`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, identity_key = ?, expires_at = ?, updated_at = datetime('now') WHERE file_path = ?`,
|
|
483
|
-
),
|
|
484
|
-
deleteEntry: db.prepare(`DELETE FROM vault WHERE id = ?`),
|
|
485
|
-
getRowid: db.prepare(`SELECT rowid FROM vault WHERE id = ?`),
|
|
486
|
-
getRowidByPath: db.prepare(
|
|
487
|
-
`SELECT rowid FROM vault WHERE file_path = ?`,
|
|
488
|
-
),
|
|
489
|
-
getEntryById: db.prepare(`SELECT * FROM vault WHERE id = ?`),
|
|
490
|
-
getByIdentityKey: db.prepare(
|
|
491
|
-
`SELECT * FROM vault WHERE kind = ? AND identity_key = ?`,
|
|
492
|
-
),
|
|
493
|
-
upsertByIdentityKey: db.prepare(
|
|
494
|
-
`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, file_path = ?, expires_at = ?, source_files = ?, updated_at = datetime('now') WHERE kind = ? AND identity_key = ?`,
|
|
495
|
-
),
|
|
496
|
-
updateSourceFiles: db.prepare(
|
|
497
|
-
`UPDATE vault SET source_files = ? WHERE id = ?`,
|
|
498
|
-
),
|
|
499
|
-
updateRelatedTo: db.prepare(
|
|
500
|
-
`UPDATE vault SET related_to = ? WHERE id = ?`,
|
|
501
|
-
),
|
|
502
|
-
insertVecStmt: db.prepare(
|
|
503
|
-
`INSERT INTO vault_vec (rowid, embedding) VALUES (?, ?)`,
|
|
504
|
-
),
|
|
505
|
-
deleteVecStmt: db.prepare(`DELETE FROM vault_vec WHERE rowid = ?`),
|
|
506
|
-
updateSupersededBy: db.prepare(
|
|
507
|
-
`UPDATE vault SET superseded_by = ? WHERE id = ?`,
|
|
508
|
-
),
|
|
509
|
-
clearSupersededByRef: db.prepare(
|
|
510
|
-
`UPDATE vault SET superseded_by = NULL WHERE superseded_by = ?`,
|
|
511
|
-
),
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Hosted mode: full schema with user_id scoping and encryption support.
|
|
516
|
-
// insertEntry has 16 params (includes user_id).
|
|
517
|
-
// getByIdentityKey and upsertByIdentityKey scope by user_id IS ?.
|
|
518
|
-
return {
|
|
519
|
-
_mode: "hosted",
|
|
520
|
-
insertEntry: db.prepare(
|
|
521
|
-
`INSERT INTO vault (id, user_id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at, source_files, tier) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
522
|
-
),
|
|
523
|
-
insertEntryEncrypted: db.prepare(
|
|
524
|
-
`INSERT INTO vault (id, user_id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at, body_encrypted, title_encrypted, meta_encrypted, iv, source_files, tier) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
525
|
-
),
|
|
526
|
-
updateEntry: db.prepare(
|
|
527
|
-
`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, identity_key = ?, expires_at = ?, updated_at = datetime('now') WHERE file_path = ?`,
|
|
528
|
-
),
|
|
529
|
-
deleteEntry: db.prepare(`DELETE FROM vault WHERE id = ?`),
|
|
530
|
-
getRowid: db.prepare(`SELECT rowid FROM vault WHERE id = ?`),
|
|
531
|
-
getRowidByPath: db.prepare(`SELECT rowid FROM vault WHERE file_path = ?`),
|
|
532
|
-
getEntryById: db.prepare(`SELECT * FROM vault WHERE id = ?`),
|
|
533
|
-
getByIdentityKey: db.prepare(
|
|
534
|
-
`SELECT * FROM vault WHERE kind = ? AND identity_key = ? AND user_id IS ?`,
|
|
535
|
-
),
|
|
536
|
-
upsertByIdentityKey: db.prepare(
|
|
537
|
-
`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, file_path = ?, expires_at = ?, source_files = ?, updated_at = datetime('now') WHERE kind = ? AND identity_key = ? AND user_id IS ?`,
|
|
538
|
-
),
|
|
539
|
-
updateSourceFiles: db.prepare(
|
|
540
|
-
`UPDATE vault SET source_files = ? WHERE id = ?`,
|
|
541
|
-
),
|
|
542
|
-
updateRelatedTo: db.prepare(
|
|
543
|
-
`UPDATE vault SET related_to = ? WHERE id = ?`,
|
|
544
|
-
),
|
|
545
|
-
insertVecStmt: db.prepare(
|
|
546
|
-
`INSERT INTO vault_vec (rowid, embedding) VALUES (?, ?)`,
|
|
547
|
-
),
|
|
548
|
-
deleteVecStmt: db.prepare(`DELETE FROM vault_vec WHERE rowid = ?`),
|
|
549
|
-
updateSupersededBy: db.prepare(
|
|
550
|
-
`UPDATE vault SET superseded_by = ? WHERE id = ?`,
|
|
551
|
-
),
|
|
552
|
-
clearSupersededByRef: db.prepare(
|
|
553
|
-
`UPDATE vault SET superseded_by = NULL WHERE superseded_by = ?`,
|
|
554
|
-
),
|
|
555
|
-
};
|
|
556
|
-
} catch (e) {
|
|
557
|
-
throw new Error(
|
|
558
|
-
`Failed to prepare database statements. The database may be corrupted.\n` +
|
|
559
|
-
`Try deleting and rebuilding: context-vault reindex\n` +
|
|
560
|
-
`Original error: ${e.message}`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
export function insertVec(stmts, rowid, embedding) {
|
|
566
|
-
// sqlite-vec requires BigInt for primary key — node:sqlite may bind Number as REAL
|
|
567
|
-
// for vec0 virtual tables which only accept INTEGER rowids
|
|
568
|
-
const safeRowid = BigInt(rowid);
|
|
569
|
-
if (safeRowid < 1n) throw new Error(`Invalid rowid: ${rowid}`);
|
|
570
|
-
stmts.insertVecStmt.run(safeRowid, embedding);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
export function deleteVec(stmts, rowid) {
|
|
574
|
-
const safeRowid = BigInt(rowid);
|
|
575
|
-
if (safeRowid < 1n) throw new Error(`Invalid rowid: ${rowid}`);
|
|
576
|
-
stmts.deleteVecStmt.run(safeRowid);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export function testConnection(db) {
|
|
580
|
-
try {
|
|
581
|
-
db.prepare("SELECT 1").get();
|
|
582
|
-
return true;
|
|
583
|
-
} catch {
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
|
-
}
|