@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
|
@@ -1,48 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
* Retrieve Layer — Public API
|
|
3
|
-
*
|
|
4
|
-
* All read-path query logic: hybrid semantic search and any future
|
|
5
|
-
* query patterns (scoped, recency-weighted, etc.).
|
|
6
|
-
*
|
|
7
|
-
* Agent Constraint: Read-only access to DB. Never writes.
|
|
8
|
-
*/
|
|
1
|
+
import type { BaseCtx, SearchResult, SearchOptions, VaultEntry } from "./types.js";
|
|
9
2
|
|
|
10
3
|
const NEAR_DUP_THRESHOLD = 0.92;
|
|
11
|
-
|
|
12
4
|
const RRF_K = 60;
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
* Exponential recency decay score based on updated_at timestamp.
|
|
16
|
-
* Returns e^(-decayRate * ageDays) for valid dates, or 0.5 as a neutral
|
|
17
|
-
* score when updatedAt is null/undefined.
|
|
18
|
-
*
|
|
19
|
-
* @param {string|null|undefined} updatedAt - ISO timestamp
|
|
20
|
-
* @param {number} decayRate - Decay rate per day (default 0.05)
|
|
21
|
-
* @returns {number} Score in [0, 1]
|
|
22
|
-
*/
|
|
23
|
-
export function recencyDecayScore(updatedAt, decayRate = 0.05) {
|
|
6
|
+
export function recencyDecayScore(updatedAt: string | null | undefined, decayRate = 0.05): number {
|
|
24
7
|
if (updatedAt == null) return 0.5;
|
|
25
8
|
const ageDays = (Date.now() - new Date(updatedAt).getTime()) / 86400000;
|
|
26
9
|
return Math.exp(-decayRate * ageDays);
|
|
27
10
|
}
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
* Dot product of two Float32Array vectors (cosine similarity for unit vectors).
|
|
31
|
-
*/
|
|
32
|
-
export function dotProduct(a, b) {
|
|
12
|
+
export function dotProduct(a: Float32Array, b: Float32Array): number {
|
|
33
13
|
let sum = 0;
|
|
34
14
|
for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
|
|
35
15
|
return sum;
|
|
36
16
|
}
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
* Build a tiered FTS5 query that prioritises phrase match, then proximity,
|
|
40
|
-
* then AND. Multi-word queries become:
|
|
41
|
-
* "word1 word2" OR NEAR("word1" "word2", 10) OR "word1" AND "word2"
|
|
42
|
-
* Single-word queries remain a simple quoted term.
|
|
43
|
-
* Returns null if no valid words remain after stripping FTS5 metacharacters.
|
|
44
|
-
*/
|
|
45
|
-
export function buildFtsQuery(query) {
|
|
18
|
+
export function buildFtsQuery(query: string): string | null {
|
|
46
19
|
const words = query
|
|
47
20
|
.split(/[\s-]+/)
|
|
48
21
|
.map((w) => w.replace(/[*"():^~{}]/g, ""))
|
|
@@ -55,40 +28,27 @@ export function buildFtsQuery(query) {
|
|
|
55
28
|
return `${phrase} OR ${near} OR ${and}`;
|
|
56
29
|
}
|
|
57
30
|
|
|
58
|
-
|
|
59
|
-
* Category-aware recency decay:
|
|
60
|
-
* knowledge + entity: no decay (enduring)
|
|
61
|
-
* event: steeper decay (~0.5 at 30 days)
|
|
62
|
-
*/
|
|
63
|
-
export function recencyBoost(createdAt, category, decayDays = 30) {
|
|
31
|
+
export function recencyBoost(createdAt: string, category: string, decayDays = 30): number {
|
|
64
32
|
if (category !== "event") return 1.0;
|
|
65
33
|
const ageDays = (Date.now() - new Date(createdAt).getTime()) / 86400000;
|
|
66
34
|
return 1 / (1 + ageDays / decayDays);
|
|
67
35
|
}
|
|
68
36
|
|
|
69
|
-
/**
|
|
70
|
-
* Build additional WHERE clauses for category/time filtering.
|
|
71
|
-
* Returns { clauses: string[], params: any[] }
|
|
72
|
-
*/
|
|
73
37
|
export function buildFilterClauses({
|
|
74
38
|
categoryFilter,
|
|
75
39
|
excludeEvents = false,
|
|
76
40
|
since,
|
|
77
41
|
until,
|
|
78
|
-
userIdFilter,
|
|
79
|
-
teamIdFilter,
|
|
80
42
|
includeSuperseeded = false,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
params.push(teamIdFilter);
|
|
91
|
-
}
|
|
43
|
+
}: {
|
|
44
|
+
categoryFilter?: string | null;
|
|
45
|
+
excludeEvents?: boolean;
|
|
46
|
+
since?: string | null;
|
|
47
|
+
until?: string | null;
|
|
48
|
+
includeSuperseeded?: boolean;
|
|
49
|
+
}): { clauses: string[]; params: unknown[] } {
|
|
50
|
+
const clauses: string[] = [];
|
|
51
|
+
const params: unknown[] = [];
|
|
92
52
|
if (categoryFilter) {
|
|
93
53
|
clauses.push("e.category = ?");
|
|
94
54
|
params.push(categoryFilter);
|
|
@@ -111,16 +71,11 @@ export function buildFilterClauses({
|
|
|
111
71
|
return { clauses, params };
|
|
112
72
|
}
|
|
113
73
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
* @param {number} k - Smoothing constant (default RRF_K = 60).
|
|
120
|
-
* @returns {Map<string, number>} Map of id -> RRF score.
|
|
121
|
-
*/
|
|
122
|
-
export function reciprocalRankFusion(rankedLists, k = RRF_K) {
|
|
123
|
-
const scores = new Map();
|
|
74
|
+
export function reciprocalRankFusion(
|
|
75
|
+
rankedLists: string[][],
|
|
76
|
+
k: number = RRF_K,
|
|
77
|
+
): Map<string, number> {
|
|
78
|
+
const scores = new Map<string, number>();
|
|
124
79
|
for (const list of rankedLists) {
|
|
125
80
|
for (let rank = 0; rank < list.length; rank++) {
|
|
126
81
|
const id = list[rank];
|
|
@@ -130,26 +85,12 @@ export function reciprocalRankFusion(rankedLists, k = RRF_K) {
|
|
|
130
85
|
return scores;
|
|
131
86
|
}
|
|
132
87
|
|
|
133
|
-
/**
|
|
134
|
-
* Hybrid search combining FTS5 text matching and vector similarity,
|
|
135
|
-
* with RRF merging, recency decay, and near-duplicate suppression.
|
|
136
|
-
*
|
|
137
|
-
* Pipeline:
|
|
138
|
-
* 1. FTS5 ranked list
|
|
139
|
-
* 2. Vector (semantic) ranked list
|
|
140
|
-
* 3. RRF: merge the two ranked lists into a single score
|
|
141
|
-
* 4. Recency decay: penalise old events (knowledge/entity entries unaffected)
|
|
142
|
-
* 5. Near-duplicate suppression (cosine similarity > 0.92 threshold)
|
|
143
|
-
*
|
|
144
|
-
* @param {import('../server/types.js').BaseCtx} ctx
|
|
145
|
-
* @param {string} query
|
|
146
|
-
* @param {{ kindFilter?: string|null, categoryFilter?: string|null, since?: string|null, until?: string|null, limit?: number, offset?: number }} opts
|
|
147
|
-
* @returns {Promise<Array<{id, kind, category, title, body, meta, tags, source, file_path, created_at, score}>>}
|
|
148
|
-
*/
|
|
149
88
|
export async function hybridSearch(
|
|
150
|
-
ctx,
|
|
151
|
-
query,
|
|
152
|
-
{
|
|
89
|
+
ctx: BaseCtx,
|
|
90
|
+
query: string,
|
|
91
|
+
opts: SearchOptions = {},
|
|
92
|
+
): Promise<SearchResult[]> {
|
|
93
|
+
const {
|
|
153
94
|
kindFilter = null,
|
|
154
95
|
categoryFilter = null,
|
|
155
96
|
excludeEvents = false,
|
|
@@ -158,33 +99,28 @@ export async function hybridSearch(
|
|
|
158
99
|
limit = 20,
|
|
159
100
|
offset = 0,
|
|
160
101
|
decayDays = 30,
|
|
161
|
-
userIdFilter,
|
|
162
|
-
teamIdFilter = null,
|
|
163
102
|
includeSuperseeded = false,
|
|
164
|
-
} =
|
|
165
|
-
|
|
166
|
-
const rowMap = new Map();
|
|
167
|
-
const idToRowid = new Map();
|
|
168
|
-
let queryVec = null;
|
|
103
|
+
} = opts;
|
|
104
|
+
|
|
105
|
+
const rowMap = new Map<string, VaultEntry>();
|
|
106
|
+
const idToRowid = new Map<string, number>();
|
|
107
|
+
let queryVec: Float32Array | null = null;
|
|
169
108
|
|
|
170
109
|
const extraFilters = buildFilterClauses({
|
|
171
110
|
categoryFilter,
|
|
172
111
|
excludeEvents,
|
|
173
112
|
since,
|
|
174
113
|
until,
|
|
175
|
-
userIdFilter,
|
|
176
|
-
teamIdFilter,
|
|
177
114
|
includeSuperseeded,
|
|
178
115
|
});
|
|
179
116
|
|
|
180
|
-
const ftsRankedIds = [];
|
|
117
|
+
const ftsRankedIds: string[] = [];
|
|
181
118
|
|
|
182
|
-
// Stage 1a: FTS5 — collect ranked list of IDs
|
|
183
119
|
const ftsQuery = buildFtsQuery(query);
|
|
184
120
|
if (ftsQuery) {
|
|
185
121
|
try {
|
|
186
122
|
const whereParts = ["vault_fts MATCH ?"];
|
|
187
|
-
const ftsParams = [ftsQuery];
|
|
123
|
+
const ftsParams: unknown[] = [ftsQuery];
|
|
188
124
|
|
|
189
125
|
if (kindFilter) {
|
|
190
126
|
whereParts.push("e.kind = ?");
|
|
@@ -194,43 +130,34 @@ export async function hybridSearch(
|
|
|
194
130
|
ftsParams.push(...extraFilters.params);
|
|
195
131
|
|
|
196
132
|
const ftsSQL = `SELECT e.*, rank FROM vault_fts f JOIN vault e ON f.rowid = e.rowid WHERE ${whereParts.join(" AND ")} ORDER BY rank LIMIT 15`;
|
|
197
|
-
|
|
133
|
+
// @ts-expect-error -- node:sqlite types are overly strict for dynamic SQL params
|
|
134
|
+
const rows = ctx.db.prepare(ftsSQL).all(...ftsParams) as unknown as (VaultEntry & { rank: number })[];
|
|
198
135
|
|
|
199
136
|
for (const { rank: _rank, ...row } of rows) {
|
|
200
137
|
ftsRankedIds.push(row.id);
|
|
201
138
|
if (!rowMap.has(row.id)) rowMap.set(row.id, row);
|
|
202
139
|
}
|
|
203
140
|
} catch (err) {
|
|
204
|
-
if (!err.message?.includes("fts5: syntax error")) {
|
|
205
|
-
console.error(`[retrieve] FTS search error: ${err.message}`);
|
|
141
|
+
if (!(err as Error).message?.includes("fts5: syntax error")) {
|
|
142
|
+
console.error(`[retrieve] FTS search error: ${(err as Error).message}`);
|
|
206
143
|
}
|
|
207
144
|
}
|
|
208
145
|
}
|
|
209
146
|
|
|
210
|
-
const vecRankedIds = [];
|
|
211
|
-
const vecSimMap = new Map();
|
|
147
|
+
const vecRankedIds: string[] = [];
|
|
148
|
+
const vecSimMap = new Map<string, number>();
|
|
212
149
|
|
|
213
|
-
// Stage 1b: Vector similarity — collect ranked list of IDs and raw similarity scores
|
|
214
150
|
try {
|
|
215
|
-
const vecCount = ctx.db
|
|
216
|
-
.prepare("SELECT COUNT(*) as c FROM vault_vec")
|
|
217
|
-
.get().c;
|
|
151
|
+
const vecCount = (ctx.db.prepare("SELECT COUNT(*) as c FROM vault_vec").get() as { c: number }).c;
|
|
218
152
|
if (vecCount > 0) {
|
|
219
153
|
queryVec = await ctx.embed(query);
|
|
220
154
|
if (queryVec) {
|
|
221
|
-
const
|
|
222
|
-
const vecLimit = hasPostFilter
|
|
223
|
-
? kindFilter
|
|
224
|
-
? 60
|
|
225
|
-
: 30
|
|
226
|
-
: kindFilter
|
|
227
|
-
? 30
|
|
228
|
-
: 15;
|
|
155
|
+
const vecLimit = kindFilter ? 30 : 15;
|
|
229
156
|
const vecRows = ctx.db
|
|
230
157
|
.prepare(
|
|
231
158
|
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
|
|
232
159
|
)
|
|
233
|
-
.all(queryVec, vecLimit);
|
|
160
|
+
.all(queryVec, vecLimit) as { rowid: number; distance: number }[];
|
|
234
161
|
|
|
235
162
|
if (vecRows.length) {
|
|
236
163
|
const rowids = vecRows.map((vr) => vr.rowid);
|
|
@@ -239,17 +166,14 @@ export async function hybridSearch(
|
|
|
239
166
|
.prepare(
|
|
240
167
|
`SELECT rowid, * FROM vault WHERE rowid IN (${placeholders})`,
|
|
241
168
|
)
|
|
242
|
-
.all(...rowids);
|
|
169
|
+
.all(...rowids) as unknown as (VaultEntry & { rowid: number })[];
|
|
243
170
|
|
|
244
|
-
const byRowid = new Map();
|
|
171
|
+
const byRowid = new Map<number, VaultEntry & { rowid: number }>();
|
|
245
172
|
for (const row of hydrated) byRowid.set(row.rowid, row);
|
|
246
173
|
|
|
247
174
|
for (const vr of vecRows) {
|
|
248
175
|
const row = byRowid.get(vr.rowid);
|
|
249
176
|
if (!row) continue;
|
|
250
|
-
if (userIdFilter !== undefined && row.user_id !== userIdFilter)
|
|
251
|
-
continue;
|
|
252
|
-
if (teamIdFilter && row.team_id !== teamIdFilter) continue;
|
|
253
177
|
if (kindFilter && row.kind !== kindFilter) continue;
|
|
254
178
|
if (categoryFilter && row.category !== categoryFilter) continue;
|
|
255
179
|
if (excludeEvents && row.category === "event") continue;
|
|
@@ -261,8 +185,6 @@ export async function hybridSearch(
|
|
|
261
185
|
const { rowid: _rowid, ...cleanRow } = row;
|
|
262
186
|
idToRowid.set(cleanRow.id, Number(row.rowid));
|
|
263
187
|
|
|
264
|
-
// sqlite-vec returns L2 distance [0, 2] for normalized vectors.
|
|
265
|
-
// Convert to similarity [0, 1]: 1 - distance/2
|
|
266
188
|
const vecSim = Math.max(0, 1 - vr.distance / 2);
|
|
267
189
|
vecSimMap.set(cleanRow.id, vecSim);
|
|
268
190
|
vecRankedIds.push(cleanRow.id);
|
|
@@ -273,33 +195,29 @@ export async function hybridSearch(
|
|
|
273
195
|
}
|
|
274
196
|
}
|
|
275
197
|
} catch (err) {
|
|
276
|
-
if (!err.message?.includes("no such table")) {
|
|
277
|
-
console.error(`[retrieve] Vector search error: ${err.message}`);
|
|
198
|
+
if (!(err as Error).message?.includes("no such table")) {
|
|
199
|
+
console.error(`[retrieve] Vector search error: ${(err as Error).message}`);
|
|
278
200
|
}
|
|
279
201
|
}
|
|
280
202
|
|
|
281
203
|
if (rowMap.size === 0) return [];
|
|
282
204
|
|
|
283
|
-
// Stage 2: RRF — merge FTS and vector ranked lists into a single score
|
|
284
205
|
const rrfScores = reciprocalRankFusion([ftsRankedIds, vecRankedIds]);
|
|
285
206
|
|
|
286
|
-
// Stage 3: Apply category-aware recency boost to RRF scores
|
|
287
207
|
for (const [id, entry] of rowMap) {
|
|
288
208
|
const boost = recencyBoost(entry.created_at, entry.category, decayDays);
|
|
289
209
|
rrfScores.set(id, (rrfScores.get(id) ?? 0) * boost);
|
|
290
210
|
}
|
|
291
211
|
|
|
292
|
-
|
|
293
|
-
const candidates = [...rowMap.values()].map((entry) => ({
|
|
212
|
+
const candidates: SearchResult[] = [...rowMap.values()].map((entry) => ({
|
|
294
213
|
...entry,
|
|
295
214
|
score: rrfScores.get(entry.id) ?? 0,
|
|
296
215
|
}));
|
|
297
216
|
candidates.sort((a, b) => b.score - a.score);
|
|
298
217
|
|
|
299
|
-
|
|
300
|
-
const embeddingMap = new Map();
|
|
218
|
+
const embeddingMap = new Map<string, Float32Array>();
|
|
301
219
|
if (queryVec && idToRowid.size > 0) {
|
|
302
|
-
const rowidToId = new Map();
|
|
220
|
+
const rowidToId = new Map<number, string>();
|
|
303
221
|
for (const [id, rowid] of idToRowid) rowidToId.set(rowid, id);
|
|
304
222
|
|
|
305
223
|
const rowidsToFetch = [...idToRowid.values()];
|
|
@@ -309,7 +227,7 @@ export async function hybridSearch(
|
|
|
309
227
|
.prepare(
|
|
310
228
|
`SELECT rowid, embedding FROM vault_vec WHERE rowid IN (${placeholders})`,
|
|
311
229
|
)
|
|
312
|
-
.all(...rowidsToFetch);
|
|
230
|
+
.all(...rowidsToFetch) as { rowid: number; embedding: Buffer }[];
|
|
313
231
|
for (const row of vecData) {
|
|
314
232
|
const id = rowidToId.get(Number(row.rowid));
|
|
315
233
|
const buf = row.embedding;
|
|
@@ -320,15 +238,14 @@ export async function hybridSearch(
|
|
|
320
238
|
);
|
|
321
239
|
}
|
|
322
240
|
}
|
|
323
|
-
} catch
|
|
324
|
-
// Embeddings unavailable
|
|
241
|
+
} catch {
|
|
242
|
+
// Embeddings unavailable
|
|
325
243
|
}
|
|
326
244
|
}
|
|
327
245
|
|
|
328
|
-
// Stage 5: Near-duplicate suppression (cosine similarity > 0.92 threshold)
|
|
329
246
|
if (queryVec && embeddingMap.size > 0) {
|
|
330
|
-
const selected = [];
|
|
331
|
-
const selectedVecs = [];
|
|
247
|
+
const selected: SearchResult[] = [];
|
|
248
|
+
const selectedVecs: Float32Array[] = [];
|
|
332
249
|
for (const candidate of candidates) {
|
|
333
250
|
if (selected.length >= offset + limit) break;
|
|
334
251
|
const vec = embeddingMap.get(candidate.id);
|
|
@@ -344,30 +261,25 @@ export async function hybridSearch(
|
|
|
344
261
|
if (vec) selectedVecs.push(vec);
|
|
345
262
|
}
|
|
346
263
|
const dedupedPage = selected.slice(offset, offset + limit);
|
|
347
|
-
trackAccess(ctx
|
|
264
|
+
trackAccess(ctx, dedupedPage);
|
|
348
265
|
return dedupedPage;
|
|
349
266
|
}
|
|
350
267
|
|
|
351
268
|
const finalPage = candidates.slice(offset, offset + limit);
|
|
352
|
-
trackAccess(ctx
|
|
269
|
+
trackAccess(ctx, finalPage);
|
|
353
270
|
return finalPage;
|
|
354
271
|
}
|
|
355
272
|
|
|
356
|
-
|
|
357
|
-
* Increment hit_count and set last_accessed_at for a batch of retrieved entries.
|
|
358
|
-
* Single batched UPDATE for efficiency.
|
|
359
|
-
*
|
|
360
|
-
* @param {import('node:sqlite').DatabaseSync} db
|
|
361
|
-
* @param {Array<{id: string}>} entries
|
|
362
|
-
*/
|
|
363
|
-
function trackAccess(db, entries) {
|
|
273
|
+
function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
|
|
364
274
|
if (!entries.length) return;
|
|
365
275
|
try {
|
|
366
276
|
const placeholders = entries.map(() => "?").join(",");
|
|
367
|
-
db
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
277
|
+
ctx.db
|
|
278
|
+
.prepare(
|
|
279
|
+
`UPDATE vault SET hit_count = hit_count + 1, last_accessed_at = datetime('now') WHERE id IN (${placeholders})`,
|
|
280
|
+
)
|
|
281
|
+
.run(...entries.map((e) => e.id));
|
|
282
|
+
} catch {
|
|
283
|
+
// Non-fatal
|
|
372
284
|
}
|
|
373
285
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { DatabaseSync, StatementSync } from "node:sqlite";
|
|
2
|
+
|
|
3
|
+
export interface VaultConfig {
|
|
4
|
+
vaultDir: string;
|
|
5
|
+
dataDir: string;
|
|
6
|
+
dbPath: string;
|
|
7
|
+
devDir: string;
|
|
8
|
+
eventDecayDays: number;
|
|
9
|
+
thresholds: GrowthThresholds;
|
|
10
|
+
telemetry: boolean;
|
|
11
|
+
resolvedFrom: string;
|
|
12
|
+
configPath?: string;
|
|
13
|
+
vaultDirExists?: boolean;
|
|
14
|
+
recall: RecallConfig;
|
|
15
|
+
consolidation: ConsolidationConfig;
|
|
16
|
+
lifecycle: Record<string, { archiveAfterDays?: number }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RecallConfig {
|
|
20
|
+
maxResults: number;
|
|
21
|
+
maxOutputBytes: number;
|
|
22
|
+
minRelevanceScore: number;
|
|
23
|
+
excludeKinds: string[];
|
|
24
|
+
excludeCategories: string[];
|
|
25
|
+
bodyTruncateChars: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConsolidationConfig {
|
|
29
|
+
tagThreshold: number;
|
|
30
|
+
maxAgeDays: number;
|
|
31
|
+
autoConsolidate: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GrowthThresholds {
|
|
35
|
+
totalEntries: { warn: number; critical: number };
|
|
36
|
+
eventEntries: { warn: number; critical: number };
|
|
37
|
+
vaultSizeBytes: { warn: number; critical: number };
|
|
38
|
+
eventsWithoutTtl: { warn: number };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PreparedStatements {
|
|
42
|
+
insertEntry: StatementSync;
|
|
43
|
+
updateEntry: StatementSync;
|
|
44
|
+
deleteEntry: StatementSync;
|
|
45
|
+
getRowid: StatementSync;
|
|
46
|
+
getRowidByPath: StatementSync;
|
|
47
|
+
getEntryById: StatementSync;
|
|
48
|
+
getByIdentityKey: StatementSync;
|
|
49
|
+
upsertByIdentityKey: StatementSync;
|
|
50
|
+
updateSourceFiles: StatementSync;
|
|
51
|
+
updateRelatedTo: StatementSync;
|
|
52
|
+
insertVecStmt: StatementSync;
|
|
53
|
+
deleteVecStmt: StatementSync;
|
|
54
|
+
updateSupersededBy: StatementSync;
|
|
55
|
+
clearSupersededByRef: StatementSync;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface VaultEntry {
|
|
59
|
+
id: string;
|
|
60
|
+
kind: string;
|
|
61
|
+
category: string;
|
|
62
|
+
title: string | null;
|
|
63
|
+
body: string;
|
|
64
|
+
meta: string | null;
|
|
65
|
+
tags: string | null;
|
|
66
|
+
source: string | null;
|
|
67
|
+
file_path: string | null;
|
|
68
|
+
identity_key: string | null;
|
|
69
|
+
expires_at: string | null;
|
|
70
|
+
superseded_by: string | null;
|
|
71
|
+
created_at: string;
|
|
72
|
+
updated_at: string | null;
|
|
73
|
+
hit_count: number;
|
|
74
|
+
last_accessed_at: string | null;
|
|
75
|
+
source_files: string | null;
|
|
76
|
+
tier: string;
|
|
77
|
+
related_to: string | null;
|
|
78
|
+
rowid?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SearchResult extends VaultEntry {
|
|
82
|
+
score: number;
|
|
83
|
+
stale?: boolean;
|
|
84
|
+
stale_reason?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CaptureInput {
|
|
88
|
+
kind: string;
|
|
89
|
+
title?: string | null;
|
|
90
|
+
body: string;
|
|
91
|
+
meta?: Record<string, unknown> | null;
|
|
92
|
+
tags?: string[] | null;
|
|
93
|
+
source?: string | null;
|
|
94
|
+
folder?: string | null;
|
|
95
|
+
identity_key?: string | null;
|
|
96
|
+
expires_at?: string | null;
|
|
97
|
+
supersedes?: string[] | null;
|
|
98
|
+
related_to?: string[] | null;
|
|
99
|
+
source_files?: Array<{ path: string; hash: string }> | null;
|
|
100
|
+
tier?: string | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface CaptureResult {
|
|
104
|
+
id: string;
|
|
105
|
+
filePath: string;
|
|
106
|
+
kind: string;
|
|
107
|
+
category: string;
|
|
108
|
+
title: string | null;
|
|
109
|
+
body: string;
|
|
110
|
+
meta: Record<string, unknown> | undefined;
|
|
111
|
+
tags: string[] | null;
|
|
112
|
+
source: string | null;
|
|
113
|
+
createdAt: string;
|
|
114
|
+
updatedAt: string;
|
|
115
|
+
identity_key: string | null;
|
|
116
|
+
expires_at: string | null;
|
|
117
|
+
supersedes: string[] | null;
|
|
118
|
+
related_to: string[] | null;
|
|
119
|
+
source_files: Array<{ path: string; hash: string }> | null;
|
|
120
|
+
tier: string | null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface IndexEntryInput {
|
|
124
|
+
id: string;
|
|
125
|
+
kind: string;
|
|
126
|
+
category: string;
|
|
127
|
+
title: string | null;
|
|
128
|
+
body: string;
|
|
129
|
+
meta: Record<string, unknown> | undefined;
|
|
130
|
+
tags: string[] | null;
|
|
131
|
+
source: string | null;
|
|
132
|
+
filePath: string;
|
|
133
|
+
createdAt: string;
|
|
134
|
+
identity_key: string | null;
|
|
135
|
+
expires_at: string | null;
|
|
136
|
+
source_files: Array<{ path: string; hash: string }> | null;
|
|
137
|
+
tier: string | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ReindexStats {
|
|
141
|
+
added: number;
|
|
142
|
+
updated: number;
|
|
143
|
+
removed: number;
|
|
144
|
+
unchanged: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface BaseCtx {
|
|
148
|
+
db: DatabaseSync;
|
|
149
|
+
config: VaultConfig;
|
|
150
|
+
stmts: PreparedStatements;
|
|
151
|
+
embed: (text: string) => Promise<Float32Array | null>;
|
|
152
|
+
insertVec: (rowid: number, embedding: Float32Array) => void;
|
|
153
|
+
deleteVec: (rowid: number) => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface SearchOptions {
|
|
157
|
+
kindFilter?: string | null;
|
|
158
|
+
categoryFilter?: string | null;
|
|
159
|
+
excludeEvents?: boolean;
|
|
160
|
+
since?: string | null;
|
|
161
|
+
until?: string | null;
|
|
162
|
+
limit?: number;
|
|
163
|
+
offset?: number;
|
|
164
|
+
decayDays?: number;
|
|
165
|
+
includeSuperseeded?: boolean;
|
|
166
|
+
}
|
package/src/capture/file-ops.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file-ops.js — Capture-specific file operations
|
|
3
|
-
*
|
|
4
|
-
* Writes markdown entry files with frontmatter to the vault directory.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { resolve, relative } from "node:path";
|
|
9
|
-
import { formatFrontmatter } from "../core/frontmatter.js";
|
|
10
|
-
import { slugify, kindToPath } from "../core/files.js";
|
|
11
|
-
import { formatBody } from "./formatters.js";
|
|
12
|
-
|
|
13
|
-
export function safeFolderPath(vaultDir, kind, folder) {
|
|
14
|
-
const base = resolve(vaultDir, kindToPath(kind));
|
|
15
|
-
if (!folder) return base;
|
|
16
|
-
const resolved = resolve(base, folder);
|
|
17
|
-
const rel = relative(base, resolved);
|
|
18
|
-
if (rel.startsWith("..") || resolve(base, rel) !== resolved) {
|
|
19
|
-
throw new Error(`Folder path escapes vault: "${folder}"`);
|
|
20
|
-
}
|
|
21
|
-
return resolved;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function writeEntryFile(
|
|
25
|
-
vaultDir,
|
|
26
|
-
kind,
|
|
27
|
-
{
|
|
28
|
-
id,
|
|
29
|
-
title,
|
|
30
|
-
body,
|
|
31
|
-
meta,
|
|
32
|
-
tags,
|
|
33
|
-
source,
|
|
34
|
-
createdAt,
|
|
35
|
-
updatedAt,
|
|
36
|
-
folder,
|
|
37
|
-
category,
|
|
38
|
-
identity_key,
|
|
39
|
-
expires_at,
|
|
40
|
-
supersedes,
|
|
41
|
-
related_to,
|
|
42
|
-
},
|
|
43
|
-
) {
|
|
44
|
-
// P5: folder is now a top-level param; also accept from meta for backward compat
|
|
45
|
-
const resolvedFolder = folder || meta?.folder || "";
|
|
46
|
-
const dir = safeFolderPath(vaultDir, kind, resolvedFolder);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
mkdirSync(dir, { recursive: true });
|
|
50
|
-
} catch (e) {
|
|
51
|
-
throw new Error(`Failed to create directory "${dir}": ${e.message}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const created = createdAt || new Date().toISOString();
|
|
55
|
-
const fmFields = { id };
|
|
56
|
-
|
|
57
|
-
// Add kind-specific meta fields to frontmatter (flattened, not nested)
|
|
58
|
-
if (meta) {
|
|
59
|
-
for (const [k, v] of Object.entries(meta)) {
|
|
60
|
-
if (k === "folder") continue;
|
|
61
|
-
if (v !== null && v !== undefined) fmFields[k] = v;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (identity_key) fmFields.identity_key = identity_key;
|
|
66
|
-
if (expires_at) fmFields.expires_at = expires_at;
|
|
67
|
-
if (supersedes?.length) fmFields.supersedes = supersedes;
|
|
68
|
-
if (related_to?.length) fmFields.related_to = related_to;
|
|
69
|
-
fmFields.tags = tags || [];
|
|
70
|
-
fmFields.source = source || "claude-code";
|
|
71
|
-
fmFields.created = created;
|
|
72
|
-
if (updatedAt && updatedAt !== created) fmFields.updated = updatedAt;
|
|
73
|
-
|
|
74
|
-
const mdBody = formatBody(kind, { title, body, meta });
|
|
75
|
-
|
|
76
|
-
// Entity kinds: deterministic filename from identity_key (no ULID suffix)
|
|
77
|
-
let filename;
|
|
78
|
-
if (category === "entity" && identity_key) {
|
|
79
|
-
const identitySlug = slugify(identity_key);
|
|
80
|
-
filename = identitySlug
|
|
81
|
-
? `${identitySlug}.md`
|
|
82
|
-
: `${id.slice(-8).toLowerCase()}.md`;
|
|
83
|
-
} else {
|
|
84
|
-
const slug = slugify((title || body).slice(0, 40));
|
|
85
|
-
const shortId = id.slice(-8).toLowerCase();
|
|
86
|
-
filename = slug ? `${slug}-${shortId}.md` : `${shortId}.md`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const filePath = resolve(dir, filename);
|
|
90
|
-
const md = formatFrontmatter(fmFields) + mdBody;
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
writeFileSync(filePath, md);
|
|
94
|
-
} catch (e) {
|
|
95
|
-
throw new Error(`Failed to write entry file "${filePath}": ${e.message}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return filePath;
|
|
99
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { captureAndIndex } from "./index.js";
|
|
2
|
-
|
|
3
|
-
export async function importEntries(ctx, entries, opts = {}) {
|
|
4
|
-
const { onProgress, source } = opts;
|
|
5
|
-
let imported = 0;
|
|
6
|
-
let failed = 0;
|
|
7
|
-
const errors = [];
|
|
8
|
-
|
|
9
|
-
for (let i = 0; i < entries.length; i++) {
|
|
10
|
-
const entry = entries[i];
|
|
11
|
-
|
|
12
|
-
if (onProgress) {
|
|
13
|
-
onProgress(i + 1, entries.length);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
if (!entry.body?.trim()) {
|
|
18
|
-
failed++;
|
|
19
|
-
errors.push({ index: i, title: entry.title, error: "Empty body" });
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
await captureAndIndex(ctx, {
|
|
24
|
-
kind: entry.kind || "insight",
|
|
25
|
-
title: entry.title || null,
|
|
26
|
-
body: entry.body,
|
|
27
|
-
meta: entry.meta,
|
|
28
|
-
tags: entry.tags,
|
|
29
|
-
source: entry.source || source || "import",
|
|
30
|
-
identity_key: entry.identity_key,
|
|
31
|
-
expires_at: entry.expires_at,
|
|
32
|
-
userId: ctx.userId || null,
|
|
33
|
-
});
|
|
34
|
-
imported++;
|
|
35
|
-
} catch (err) {
|
|
36
|
-
failed++;
|
|
37
|
-
errors.push({
|
|
38
|
-
index: i,
|
|
39
|
-
title: entry.title || null,
|
|
40
|
-
error: err.message,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { imported, failed, errors };
|
|
46
|
-
}
|