@context-vault/core 2.17.0 → 3.0.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/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 -17
- 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/files.ts +80 -0
- package/src/{capture/formatters.js → formatters.ts} +13 -11
- package/src/{core/frontmatter.js → frontmatter.ts} +27 -33
- package/src/index.ts +351 -0
- package/src/ingest-url.ts +99 -0
- package/src/main.ts +111 -0
- package/src/search.ts +285 -0
- package/src/types.ts +166 -0
- package/src/capture/file-ops.js +0 -97
- package/src/capture/import-pipeline.js +0 -46
- package/src/capture/importers.js +0 -387
- package/src/capture/index.js +0 -236
- package/src/capture/ingest-url.js +0 -252
- package/src/consolidation/index.js +0 -112
- package/src/core/categories.js +0 -72
- package/src/core/error-log.js +0 -54
- package/src/core/files.js +0 -108
- package/src/core/status.js +0 -350
- package/src/core/telemetry.js +0 -90
- package/src/index/db.js +0 -416
- package/src/index/index.js +0 -522
- package/src/index.js +0 -66
- package/src/retrieve/index.js +0 -500
- 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 -231
- package/src/server/tools/delete-context.js +0 -60
- package/src/server/tools/get-context.js +0 -678
- 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 -609
- package/src/server/tools/session-start.js +0 -285
- package/src/server/tools/submit-feedback.js +0 -55
- package/src/server/tools.js +0 -174
- package/src/sync/sync.js +0 -235
package/src/index/index.js
DELETED
|
@@ -1,522 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Index Layer — Public API
|
|
3
|
-
*
|
|
4
|
-
* Owns the database as a derived index. Handles both bulk sync (reindex)
|
|
5
|
-
* and single-entry indexing (indexEntry) for write-through capture.
|
|
6
|
-
*
|
|
7
|
-
* Agent Constraint: Can import ../core. Owns db.js and embed.js.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, readdirSync, existsSync, unlinkSync } from "node:fs";
|
|
11
|
-
import { join, basename } from "node:path";
|
|
12
|
-
import { dirToKind, walkDir, ulid } from "../core/files.js";
|
|
13
|
-
import {
|
|
14
|
-
categoryFor,
|
|
15
|
-
defaultTierFor,
|
|
16
|
-
CATEGORY_DIRS,
|
|
17
|
-
} from "../core/categories.js";
|
|
18
|
-
import {
|
|
19
|
-
parseFrontmatter,
|
|
20
|
-
parseEntryFromMarkdown,
|
|
21
|
-
} from "../core/frontmatter.js";
|
|
22
|
-
import { embedBatch } from "./embed.js";
|
|
23
|
-
|
|
24
|
-
const EXCLUDED_DIRS = new Set(["projects", "_archive"]);
|
|
25
|
-
const EXCLUDED_FILES = new Set(["context.md", "memory.md", "README.md"]);
|
|
26
|
-
|
|
27
|
-
const EMBED_BATCH_SIZE = 32;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Index a single entry with idempotent upsert behavior.
|
|
31
|
-
* Called immediately after Capture Layer writes the file.
|
|
32
|
-
*
|
|
33
|
-
* For entities with identity_key: uses upsertByIdentityKey if existing row found.
|
|
34
|
-
*
|
|
35
|
-
* @param {import('../server/types.js').BaseCtx & Partial<import('../server/types.js').HostedCtxExtensions>} ctx
|
|
36
|
-
* @param {{ id, kind, category, title, body, meta, tags, source, filePath, createdAt, identity_key, expires_at, userId }} entry
|
|
37
|
-
*/
|
|
38
|
-
export async function indexEntry(
|
|
39
|
-
ctx,
|
|
40
|
-
{
|
|
41
|
-
id,
|
|
42
|
-
kind,
|
|
43
|
-
category,
|
|
44
|
-
title,
|
|
45
|
-
body,
|
|
46
|
-
meta,
|
|
47
|
-
tags,
|
|
48
|
-
source,
|
|
49
|
-
filePath,
|
|
50
|
-
createdAt,
|
|
51
|
-
identity_key,
|
|
52
|
-
expires_at,
|
|
53
|
-
source_files,
|
|
54
|
-
tier,
|
|
55
|
-
userId,
|
|
56
|
-
},
|
|
57
|
-
) {
|
|
58
|
-
// Don't index entries that have already expired
|
|
59
|
-
if (expires_at && new Date(expires_at) <= new Date()) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const tagsJson = tags ? JSON.stringify(tags) : null;
|
|
64
|
-
const metaJson = meta ? JSON.stringify(meta) : null;
|
|
65
|
-
const sourceFilesJson = source_files ? JSON.stringify(source_files) : null;
|
|
66
|
-
const cat = category || categoryFor(kind);
|
|
67
|
-
const effectiveTier = tier || defaultTierFor(kind);
|
|
68
|
-
const userIdVal = userId || null;
|
|
69
|
-
|
|
70
|
-
let wasUpdate = false;
|
|
71
|
-
|
|
72
|
-
// Entity upsert: check by (kind, identity_key, user_id) first
|
|
73
|
-
if (cat === "entity" && identity_key) {
|
|
74
|
-
const existing = ctx.stmts.getByIdentityKey.get(
|
|
75
|
-
kind,
|
|
76
|
-
identity_key,
|
|
77
|
-
userIdVal,
|
|
78
|
-
);
|
|
79
|
-
if (existing) {
|
|
80
|
-
ctx.stmts.upsertByIdentityKey.run(
|
|
81
|
-
title || null,
|
|
82
|
-
body,
|
|
83
|
-
metaJson,
|
|
84
|
-
tagsJson,
|
|
85
|
-
source || "claude-code",
|
|
86
|
-
cat,
|
|
87
|
-
filePath,
|
|
88
|
-
expires_at || null,
|
|
89
|
-
sourceFilesJson,
|
|
90
|
-
kind,
|
|
91
|
-
identity_key,
|
|
92
|
-
userIdVal,
|
|
93
|
-
);
|
|
94
|
-
wasUpdate = true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!wasUpdate) {
|
|
99
|
-
// Prepare encryption if ctx.encrypt is available
|
|
100
|
-
let encrypted = null;
|
|
101
|
-
if (ctx.encrypt) {
|
|
102
|
-
encrypted = await ctx.encrypt({ title, body, meta });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
if (encrypted) {
|
|
107
|
-
// Encrypted insert: store preview in body column for FTS, full content in encrypted columns
|
|
108
|
-
const bodyPreview = body.slice(0, 200);
|
|
109
|
-
ctx.stmts.insertEntryEncrypted.run(
|
|
110
|
-
id,
|
|
111
|
-
userIdVal,
|
|
112
|
-
kind,
|
|
113
|
-
cat,
|
|
114
|
-
title || null,
|
|
115
|
-
bodyPreview,
|
|
116
|
-
metaJson,
|
|
117
|
-
tagsJson,
|
|
118
|
-
source || "claude-code",
|
|
119
|
-
filePath,
|
|
120
|
-
identity_key || null,
|
|
121
|
-
expires_at || null,
|
|
122
|
-
createdAt,
|
|
123
|
-
createdAt,
|
|
124
|
-
encrypted.body_encrypted,
|
|
125
|
-
encrypted.title_encrypted,
|
|
126
|
-
encrypted.meta_encrypted,
|
|
127
|
-
encrypted.iv,
|
|
128
|
-
sourceFilesJson,
|
|
129
|
-
effectiveTier,
|
|
130
|
-
);
|
|
131
|
-
} else {
|
|
132
|
-
ctx.stmts.insertEntry.run(
|
|
133
|
-
id,
|
|
134
|
-
userIdVal,
|
|
135
|
-
kind,
|
|
136
|
-
cat,
|
|
137
|
-
title || null,
|
|
138
|
-
body,
|
|
139
|
-
metaJson,
|
|
140
|
-
tagsJson,
|
|
141
|
-
source || "claude-code",
|
|
142
|
-
filePath,
|
|
143
|
-
identity_key || null,
|
|
144
|
-
expires_at || null,
|
|
145
|
-
createdAt,
|
|
146
|
-
createdAt,
|
|
147
|
-
sourceFilesJson,
|
|
148
|
-
effectiveTier,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
} catch (e) {
|
|
152
|
-
if (e.message.includes("UNIQUE constraint")) {
|
|
153
|
-
ctx.stmts.updateEntry.run(
|
|
154
|
-
title || null,
|
|
155
|
-
body,
|
|
156
|
-
metaJson,
|
|
157
|
-
tagsJson,
|
|
158
|
-
source || "claude-code",
|
|
159
|
-
cat,
|
|
160
|
-
identity_key || null,
|
|
161
|
-
expires_at || null,
|
|
162
|
-
filePath,
|
|
163
|
-
);
|
|
164
|
-
if (sourceFilesJson !== null && ctx.stmts.updateSourceFiles) {
|
|
165
|
-
const entryRow = ctx.stmts.getRowidByPath.get(filePath);
|
|
166
|
-
if (entryRow) {
|
|
167
|
-
const idRow = ctx.db
|
|
168
|
-
.prepare("SELECT id FROM vault WHERE file_path = ?")
|
|
169
|
-
.get(filePath);
|
|
170
|
-
if (idRow)
|
|
171
|
-
ctx.stmts.updateSourceFiles.run(sourceFilesJson, idRow.id);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
wasUpdate = true;
|
|
175
|
-
} else {
|
|
176
|
-
throw e;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// After update, get rowid by file_path (since id might differ); otherwise by id
|
|
182
|
-
const rowidResult = wasUpdate
|
|
183
|
-
? ctx.stmts.getRowidByPath.get(filePath)
|
|
184
|
-
: ctx.stmts.getRowid.get(id);
|
|
185
|
-
|
|
186
|
-
if (!rowidResult || rowidResult.rowid == null) {
|
|
187
|
-
throw new Error(
|
|
188
|
-
`Could not find rowid for entry: ${wasUpdate ? `file_path=${filePath}` : `id=${id}`}`,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const rowid = Number(rowidResult.rowid);
|
|
193
|
-
if (!Number.isFinite(rowid) || rowid < 1) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Invalid rowid retrieved: ${rowidResult.rowid} (type: ${typeof rowidResult.rowid})`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Skip embedding generation for event entries — they are excluded from
|
|
200
|
-
// default semantic search and don't need vector representations
|
|
201
|
-
if (cat !== "event") {
|
|
202
|
-
const embeddingText = [title, body].filter(Boolean).join(" ");
|
|
203
|
-
const embedding = await ctx.embed(embeddingText);
|
|
204
|
-
|
|
205
|
-
if (embedding) {
|
|
206
|
-
try {
|
|
207
|
-
ctx.deleteVec(rowid);
|
|
208
|
-
} catch {
|
|
209
|
-
/* no-op if not found */
|
|
210
|
-
}
|
|
211
|
-
ctx.insertVec(rowid, embedding);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Prune expired entries: delete files, vec rows, and DB rows for all entries
|
|
218
|
-
* where expires_at <= now(). Safe to call on startup or CLI — non-destructive
|
|
219
|
-
* to active data.
|
|
220
|
-
*
|
|
221
|
-
* @param {import('../server/types.js').BaseCtx} ctx
|
|
222
|
-
* @returns {Promise<number>} count of pruned entries
|
|
223
|
-
*/
|
|
224
|
-
export async function pruneExpired(ctx) {
|
|
225
|
-
const expired = ctx.db
|
|
226
|
-
.prepare(
|
|
227
|
-
"SELECT id, file_path FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')",
|
|
228
|
-
)
|
|
229
|
-
.all();
|
|
230
|
-
|
|
231
|
-
for (const row of expired) {
|
|
232
|
-
if (row.file_path) {
|
|
233
|
-
try {
|
|
234
|
-
unlinkSync(row.file_path);
|
|
235
|
-
} catch {}
|
|
236
|
-
}
|
|
237
|
-
const vRowid = ctx.stmts.getRowid.get(row.id)?.rowid;
|
|
238
|
-
if (vRowid) {
|
|
239
|
-
try {
|
|
240
|
-
ctx.deleteVec(Number(vRowid));
|
|
241
|
-
} catch {}
|
|
242
|
-
}
|
|
243
|
-
ctx.stmts.deleteEntry.run(row.id);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return expired.length;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Bulk reindex: sync vault directory into the database.
|
|
251
|
-
* P2: Wrapped in a transaction for atomicity.
|
|
252
|
-
* P3: Detects title/tag/meta changes, not just body.
|
|
253
|
-
* P4: Batches embedding calls for performance.
|
|
254
|
-
*
|
|
255
|
-
* @param {import('../server/types.js').BaseCtx} ctx
|
|
256
|
-
* @param {{ fullSync?: boolean }} opts — fullSync=true adds/updates/deletes; false=add-only
|
|
257
|
-
* @returns {Promise<{added: number, updated: number, removed: number, unchanged: number}>}
|
|
258
|
-
*/
|
|
259
|
-
export async function reindex(ctx, opts = {}) {
|
|
260
|
-
const { fullSync = true } = opts;
|
|
261
|
-
const stats = { added: 0, updated: 0, removed: 0, unchanged: 0 };
|
|
262
|
-
|
|
263
|
-
if (!existsSync(ctx.config.vaultDir)) return stats;
|
|
264
|
-
|
|
265
|
-
// Use INSERT OR IGNORE for reindex — handles files with duplicate frontmatter IDs
|
|
266
|
-
// user_id is NULL for reindex (always local mode)
|
|
267
|
-
const upsertEntry = ctx.db.prepare(
|
|
268
|
-
`INSERT OR IGNORE INTO vault (id, user_id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
// Auto-discover kind directories, supporting both:
|
|
272
|
-
// - Nested: knowledge/insights/, events/sessions/ (category dirs at top level)
|
|
273
|
-
// - Flat: insights/, decisions/ (legacy — kind dirs at top level)
|
|
274
|
-
const kindEntries = []; // { kind, dir }
|
|
275
|
-
const topDirs = readdirSync(ctx.config.vaultDir, {
|
|
276
|
-
withFileTypes: true,
|
|
277
|
-
}).filter(
|
|
278
|
-
(d) =>
|
|
279
|
-
d.isDirectory() && !EXCLUDED_DIRS.has(d.name) && !d.name.startsWith("_"),
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
for (const d of topDirs) {
|
|
283
|
-
if (CATEGORY_DIRS.has(d.name)) {
|
|
284
|
-
// Category directory — look one level deeper for kind directories
|
|
285
|
-
const catDir = join(ctx.config.vaultDir, d.name);
|
|
286
|
-
const subDirs = readdirSync(catDir, { withFileTypes: true }).filter(
|
|
287
|
-
(sd) => sd.isDirectory() && !sd.name.startsWith("_"),
|
|
288
|
-
);
|
|
289
|
-
for (const sd of subDirs) {
|
|
290
|
-
kindEntries.push({
|
|
291
|
-
kind: dirToKind(sd.name),
|
|
292
|
-
dir: join(catDir, sd.name),
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
// Legacy flat structure — top-level dir is a kind dir
|
|
297
|
-
kindEntries.push({
|
|
298
|
-
kind: dirToKind(d.name),
|
|
299
|
-
dir: join(ctx.config.vaultDir, d.name),
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Phase 1: Sync DB ops in a transaction — FTS is searchable immediately after COMMIT.
|
|
305
|
-
// Phase 2: Async embedding runs post-transaction so it can't hold the write lock
|
|
306
|
-
// or roll back DB state on failure.
|
|
307
|
-
const pendingEmbeds = []; // { rowid, text, isUpdate }
|
|
308
|
-
|
|
309
|
-
ctx.db.exec("BEGIN");
|
|
310
|
-
try {
|
|
311
|
-
for (const { kind, dir } of kindEntries) {
|
|
312
|
-
const category = categoryFor(kind);
|
|
313
|
-
const mdFiles = walkDir(dir).filter(
|
|
314
|
-
(f) => !EXCLUDED_FILES.has(basename(f.filePath)),
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
// P3: Fetch all mutable fields for change detection
|
|
318
|
-
const dbRows = ctx.db
|
|
319
|
-
.prepare(
|
|
320
|
-
"SELECT id, file_path, body, title, tags, meta FROM vault WHERE kind = ?",
|
|
321
|
-
)
|
|
322
|
-
.all(kind);
|
|
323
|
-
const dbByPath = new Map(dbRows.map((r) => [r.file_path, r]));
|
|
324
|
-
const diskPaths = new Set(mdFiles.map((e) => e.filePath));
|
|
325
|
-
|
|
326
|
-
for (const { filePath, relDir } of mdFiles) {
|
|
327
|
-
const existing = dbByPath.get(filePath);
|
|
328
|
-
|
|
329
|
-
// In add-only mode, skip files already in DB
|
|
330
|
-
if (!fullSync && existing) {
|
|
331
|
-
stats.unchanged++;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
336
|
-
if (!raw.startsWith("---\n")) {
|
|
337
|
-
console.error(`[reindex] skipping (no frontmatter): ${filePath}`);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const { meta: fmMeta, body: rawBody } = parseFrontmatter(raw);
|
|
341
|
-
const parsed = parseEntryFromMarkdown(kind, rawBody, fmMeta);
|
|
342
|
-
|
|
343
|
-
// Extract identity_key and expires_at from frontmatter
|
|
344
|
-
const identity_key = fmMeta.identity_key || null;
|
|
345
|
-
const expires_at = fmMeta.expires_at || null;
|
|
346
|
-
|
|
347
|
-
// Derive folder from disk location (source of truth)
|
|
348
|
-
const meta = { ...(parsed.meta || {}) };
|
|
349
|
-
if (relDir) meta.folder = relDir;
|
|
350
|
-
else delete meta.folder;
|
|
351
|
-
const metaJson = Object.keys(meta).length ? JSON.stringify(meta) : null;
|
|
352
|
-
|
|
353
|
-
if (!existing) {
|
|
354
|
-
// New file — add to DB (OR IGNORE if ID already exists at another path)
|
|
355
|
-
const id = fmMeta.id || ulid();
|
|
356
|
-
const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
|
|
357
|
-
const created = fmMeta.created || new Date().toISOString();
|
|
358
|
-
|
|
359
|
-
const result = upsertEntry.run(
|
|
360
|
-
id,
|
|
361
|
-
kind,
|
|
362
|
-
category,
|
|
363
|
-
parsed.title || null,
|
|
364
|
-
parsed.body,
|
|
365
|
-
metaJson,
|
|
366
|
-
tagsJson,
|
|
367
|
-
fmMeta.source || "file",
|
|
368
|
-
filePath,
|
|
369
|
-
identity_key,
|
|
370
|
-
expires_at,
|
|
371
|
-
created,
|
|
372
|
-
fmMeta.updated || created,
|
|
373
|
-
);
|
|
374
|
-
if (result.changes > 0) {
|
|
375
|
-
if (category !== "event") {
|
|
376
|
-
const rowidResult = ctx.stmts.getRowid.get(id);
|
|
377
|
-
if (rowidResult?.rowid) {
|
|
378
|
-
const embeddingText = [parsed.title, parsed.body]
|
|
379
|
-
.filter(Boolean)
|
|
380
|
-
.join(" ");
|
|
381
|
-
pendingEmbeds.push({
|
|
382
|
-
rowid: rowidResult.rowid,
|
|
383
|
-
text: embeddingText,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
stats.added++;
|
|
388
|
-
} else {
|
|
389
|
-
stats.unchanged++;
|
|
390
|
-
}
|
|
391
|
-
} else if (fullSync) {
|
|
392
|
-
// P3: Compare all mutable fields, not just body
|
|
393
|
-
const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
|
|
394
|
-
const titleChanged =
|
|
395
|
-
(parsed.title || null) !== (existing.title || null);
|
|
396
|
-
const bodyChanged = existing.body !== parsed.body;
|
|
397
|
-
const tagsChanged = tagsJson !== (existing.tags || null);
|
|
398
|
-
const metaChanged = metaJson !== (existing.meta || null);
|
|
399
|
-
|
|
400
|
-
if (bodyChanged || titleChanged || tagsChanged || metaChanged) {
|
|
401
|
-
ctx.stmts.updateEntry.run(
|
|
402
|
-
parsed.title || null,
|
|
403
|
-
parsed.body,
|
|
404
|
-
metaJson,
|
|
405
|
-
tagsJson,
|
|
406
|
-
fmMeta.source || "file",
|
|
407
|
-
category,
|
|
408
|
-
identity_key,
|
|
409
|
-
expires_at,
|
|
410
|
-
filePath,
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
// Queue re-embed if title or body changed (vector ops deferred to Phase 2)
|
|
414
|
-
if ((bodyChanged || titleChanged) && category !== "event") {
|
|
415
|
-
const rowid = ctx.stmts.getRowid.get(existing.id)?.rowid;
|
|
416
|
-
if (rowid) {
|
|
417
|
-
const embeddingText = [parsed.title, parsed.body]
|
|
418
|
-
.filter(Boolean)
|
|
419
|
-
.join(" ");
|
|
420
|
-
pendingEmbeds.push({
|
|
421
|
-
rowid,
|
|
422
|
-
text: embeddingText,
|
|
423
|
-
isUpdate: true,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
stats.updated++;
|
|
428
|
-
} else {
|
|
429
|
-
stats.unchanged++;
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
stats.unchanged++;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Find deleted files (in DB but not on disk) — only in fullSync mode
|
|
437
|
-
if (fullSync) {
|
|
438
|
-
for (const [dbPath, row] of dbByPath) {
|
|
439
|
-
if (!diskPaths.has(dbPath)) {
|
|
440
|
-
const vRowid = ctx.stmts.getRowid.get(row.id)?.rowid;
|
|
441
|
-
if (vRowid) {
|
|
442
|
-
try {
|
|
443
|
-
ctx.deleteVec(vRowid);
|
|
444
|
-
} catch {}
|
|
445
|
-
}
|
|
446
|
-
ctx.stmts.deleteEntry.run(row.id);
|
|
447
|
-
stats.removed++;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Clean up entries for kinds whose directories no longer exist on disk
|
|
454
|
-
if (fullSync) {
|
|
455
|
-
const indexedKinds = new Set(kindEntries.map((ke) => ke.kind));
|
|
456
|
-
const allDbKinds = ctx.db
|
|
457
|
-
.prepare("SELECT DISTINCT kind FROM vault")
|
|
458
|
-
.all();
|
|
459
|
-
for (const { kind } of allDbKinds) {
|
|
460
|
-
if (!indexedKinds.has(kind)) {
|
|
461
|
-
const orphaned = ctx.db
|
|
462
|
-
.prepare("SELECT id, rowid FROM vault WHERE kind = ?")
|
|
463
|
-
.all(kind);
|
|
464
|
-
for (const row of orphaned) {
|
|
465
|
-
try {
|
|
466
|
-
ctx.deleteVec(row.rowid);
|
|
467
|
-
} catch {}
|
|
468
|
-
ctx.stmts.deleteEntry.run(row.id);
|
|
469
|
-
stats.removed++;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Prune expired entries
|
|
476
|
-
const expired = ctx.db
|
|
477
|
-
.prepare(
|
|
478
|
-
"SELECT id, file_path FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')",
|
|
479
|
-
)
|
|
480
|
-
.all();
|
|
481
|
-
|
|
482
|
-
for (const row of expired) {
|
|
483
|
-
if (row.file_path) {
|
|
484
|
-
try {
|
|
485
|
-
unlinkSync(row.file_path);
|
|
486
|
-
} catch {}
|
|
487
|
-
}
|
|
488
|
-
const vRowid = ctx.stmts.getRowid.get(row.id)?.rowid;
|
|
489
|
-
if (vRowid) {
|
|
490
|
-
try {
|
|
491
|
-
ctx.deleteVec(vRowid);
|
|
492
|
-
} catch {}
|
|
493
|
-
}
|
|
494
|
-
ctx.stmts.deleteEntry.run(row.id);
|
|
495
|
-
stats.removed++;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
ctx.db.exec("COMMIT");
|
|
499
|
-
} catch (e) {
|
|
500
|
-
ctx.db.exec("ROLLBACK");
|
|
501
|
-
throw e;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Phase 2: Async embedding — runs after COMMIT so FTS is already searchable.
|
|
505
|
-
// Failures here are non-fatal; semantic search catches up on next reindex.
|
|
506
|
-
// Vec delete happens atomically with insert (only on success) to avoid
|
|
507
|
-
// leaving entries permanently without vectors if embedBatch() fails mid-batch.
|
|
508
|
-
for (let i = 0; i < pendingEmbeds.length; i += EMBED_BATCH_SIZE) {
|
|
509
|
-
const batch = pendingEmbeds.slice(i, i + EMBED_BATCH_SIZE);
|
|
510
|
-
const embeddings = await embedBatch(batch.map((e) => e.text));
|
|
511
|
-
for (let j = 0; j < batch.length; j++) {
|
|
512
|
-
if (embeddings[j]) {
|
|
513
|
-
try {
|
|
514
|
-
ctx.deleteVec(batch[j].rowid);
|
|
515
|
-
} catch {}
|
|
516
|
-
ctx.insertVec(batch[j].rowid, embeddings[j]);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return stats;
|
|
522
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @context-vault/core — Shared core for context-vault
|
|
3
|
-
*
|
|
4
|
-
* Re-exports all public APIs from capture, index, retrieve, server, and core layers.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Core utilities
|
|
8
|
-
export {
|
|
9
|
-
categoryFor,
|
|
10
|
-
categoryDirFor,
|
|
11
|
-
defaultTierFor,
|
|
12
|
-
CATEGORY_DIRS,
|
|
13
|
-
} from "./core/categories.js";
|
|
14
|
-
export { parseArgs, resolveConfig } from "./core/config.js";
|
|
15
|
-
export {
|
|
16
|
-
ulid,
|
|
17
|
-
slugify,
|
|
18
|
-
kindToDir,
|
|
19
|
-
dirToKind,
|
|
20
|
-
normalizeKind,
|
|
21
|
-
kindToPath,
|
|
22
|
-
safeJoin,
|
|
23
|
-
walkDir,
|
|
24
|
-
} from "./core/files.js";
|
|
25
|
-
export {
|
|
26
|
-
formatFrontmatter,
|
|
27
|
-
parseFrontmatter,
|
|
28
|
-
extractCustomMeta,
|
|
29
|
-
parseEntryFromMarkdown,
|
|
30
|
-
} from "./core/frontmatter.js";
|
|
31
|
-
export { gatherVaultStatus } from "./core/status.js";
|
|
32
|
-
|
|
33
|
-
// Capture layer
|
|
34
|
-
export {
|
|
35
|
-
writeEntry,
|
|
36
|
-
updateEntryFile,
|
|
37
|
-
captureAndIndex,
|
|
38
|
-
} from "./capture/index.js";
|
|
39
|
-
export { writeEntryFile } from "./capture/file-ops.js";
|
|
40
|
-
export { formatBody } from "./capture/formatters.js";
|
|
41
|
-
|
|
42
|
-
// Index layer
|
|
43
|
-
export {
|
|
44
|
-
SCHEMA_DDL,
|
|
45
|
-
initDatabase,
|
|
46
|
-
prepareStatements,
|
|
47
|
-
insertVec,
|
|
48
|
-
deleteVec,
|
|
49
|
-
} from "./index/db.js";
|
|
50
|
-
export { embed, embedBatch, resetEmbedPipeline } from "./index/embed.js";
|
|
51
|
-
export { indexEntry, reindex, pruneExpired } from "./index/index.js";
|
|
52
|
-
|
|
53
|
-
// Retrieve layer
|
|
54
|
-
export { hybridSearch } from "./retrieve/index.js";
|
|
55
|
-
|
|
56
|
-
// Consolidation utilities
|
|
57
|
-
export { findHotTags, findColdEntries } from "./consolidation/index.js";
|
|
58
|
-
|
|
59
|
-
// Server tools & helpers
|
|
60
|
-
export { registerTools } from "./server/tools.js";
|
|
61
|
-
export {
|
|
62
|
-
ok,
|
|
63
|
-
err,
|
|
64
|
-
ensureVaultExists,
|
|
65
|
-
ensureValidKind,
|
|
66
|
-
} from "./server/helpers.js";
|