@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/search.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { BaseCtx, SearchResult, SearchOptions, VaultEntry } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const NEAR_DUP_THRESHOLD = 0.92;
|
|
4
|
+
const RRF_K = 60;
|
|
5
|
+
|
|
6
|
+
export function recencyDecayScore(updatedAt: string | null | undefined, decayRate = 0.05): number {
|
|
7
|
+
if (updatedAt == null) return 0.5;
|
|
8
|
+
const ageDays = (Date.now() - new Date(updatedAt).getTime()) / 86400000;
|
|
9
|
+
return Math.exp(-decayRate * ageDays);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dotProduct(a: Float32Array, b: Float32Array): number {
|
|
13
|
+
let sum = 0;
|
|
14
|
+
for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
|
|
15
|
+
return sum;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildFtsQuery(query: string): string | null {
|
|
19
|
+
const words = query
|
|
20
|
+
.split(/[\s-]+/)
|
|
21
|
+
.map((w) => w.replace(/[*"():^~{}]/g, ""))
|
|
22
|
+
.filter((w) => w.length > 0);
|
|
23
|
+
if (!words.length) return null;
|
|
24
|
+
if (words.length === 1) return `"${words[0]}"`;
|
|
25
|
+
const phrase = `"${words.join(" ")}"`;
|
|
26
|
+
const near = `NEAR(${words.map((w) => `"${w}"`).join(" ")}, 10)`;
|
|
27
|
+
const and = words.map((w) => `"${w}"`).join(" AND ");
|
|
28
|
+
return `${phrase} OR ${near} OR ${and}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function recencyBoost(createdAt: string, category: string, decayDays = 30): number {
|
|
32
|
+
if (category !== "event") return 1.0;
|
|
33
|
+
const ageDays = (Date.now() - new Date(createdAt).getTime()) / 86400000;
|
|
34
|
+
return 1 / (1 + ageDays / decayDays);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildFilterClauses({
|
|
38
|
+
categoryFilter,
|
|
39
|
+
excludeEvents = false,
|
|
40
|
+
since,
|
|
41
|
+
until,
|
|
42
|
+
includeSuperseeded = false,
|
|
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[] = [];
|
|
52
|
+
if (categoryFilter) {
|
|
53
|
+
clauses.push("e.category = ?");
|
|
54
|
+
params.push(categoryFilter);
|
|
55
|
+
}
|
|
56
|
+
if (excludeEvents && !categoryFilter) {
|
|
57
|
+
clauses.push("e.category != 'event'");
|
|
58
|
+
}
|
|
59
|
+
if (since) {
|
|
60
|
+
clauses.push("e.created_at >= ?");
|
|
61
|
+
params.push(since);
|
|
62
|
+
}
|
|
63
|
+
if (until) {
|
|
64
|
+
clauses.push("e.created_at <= ?");
|
|
65
|
+
params.push(until);
|
|
66
|
+
}
|
|
67
|
+
clauses.push("(e.expires_at IS NULL OR e.expires_at > datetime('now'))");
|
|
68
|
+
if (!includeSuperseeded) {
|
|
69
|
+
clauses.push("e.superseded_by IS NULL");
|
|
70
|
+
}
|
|
71
|
+
return { clauses, params };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function reciprocalRankFusion(
|
|
75
|
+
rankedLists: string[][],
|
|
76
|
+
k: number = RRF_K,
|
|
77
|
+
): Map<string, number> {
|
|
78
|
+
const scores = new Map<string, number>();
|
|
79
|
+
for (const list of rankedLists) {
|
|
80
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
81
|
+
const id = list[rank];
|
|
82
|
+
scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank + 1));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return scores;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function hybridSearch(
|
|
89
|
+
ctx: BaseCtx,
|
|
90
|
+
query: string,
|
|
91
|
+
opts: SearchOptions = {},
|
|
92
|
+
): Promise<SearchResult[]> {
|
|
93
|
+
const {
|
|
94
|
+
kindFilter = null,
|
|
95
|
+
categoryFilter = null,
|
|
96
|
+
excludeEvents = false,
|
|
97
|
+
since = null,
|
|
98
|
+
until = null,
|
|
99
|
+
limit = 20,
|
|
100
|
+
offset = 0,
|
|
101
|
+
decayDays = 30,
|
|
102
|
+
includeSuperseeded = false,
|
|
103
|
+
} = opts;
|
|
104
|
+
|
|
105
|
+
const rowMap = new Map<string, VaultEntry>();
|
|
106
|
+
const idToRowid = new Map<string, number>();
|
|
107
|
+
let queryVec: Float32Array | null = null;
|
|
108
|
+
|
|
109
|
+
const extraFilters = buildFilterClauses({
|
|
110
|
+
categoryFilter,
|
|
111
|
+
excludeEvents,
|
|
112
|
+
since,
|
|
113
|
+
until,
|
|
114
|
+
includeSuperseeded,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const ftsRankedIds: string[] = [];
|
|
118
|
+
|
|
119
|
+
const ftsQuery = buildFtsQuery(query);
|
|
120
|
+
if (ftsQuery) {
|
|
121
|
+
try {
|
|
122
|
+
const whereParts = ["vault_fts MATCH ?"];
|
|
123
|
+
const ftsParams: unknown[] = [ftsQuery];
|
|
124
|
+
|
|
125
|
+
if (kindFilter) {
|
|
126
|
+
whereParts.push("e.kind = ?");
|
|
127
|
+
ftsParams.push(kindFilter);
|
|
128
|
+
}
|
|
129
|
+
whereParts.push(...extraFilters.clauses);
|
|
130
|
+
ftsParams.push(...extraFilters.params);
|
|
131
|
+
|
|
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`;
|
|
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 })[];
|
|
135
|
+
|
|
136
|
+
for (const { rank: _rank, ...row } of rows) {
|
|
137
|
+
ftsRankedIds.push(row.id);
|
|
138
|
+
if (!rowMap.has(row.id)) rowMap.set(row.id, row);
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (!(err as Error).message?.includes("fts5: syntax error")) {
|
|
142
|
+
console.error(`[retrieve] FTS search error: ${(err as Error).message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const vecRankedIds: string[] = [];
|
|
148
|
+
const vecSimMap = new Map<string, number>();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const vecCount = (ctx.db.prepare("SELECT COUNT(*) as c FROM vault_vec").get() as { c: number }).c;
|
|
152
|
+
if (vecCount > 0) {
|
|
153
|
+
queryVec = await ctx.embed(query);
|
|
154
|
+
if (queryVec) {
|
|
155
|
+
const vecLimit = kindFilter ? 30 : 15;
|
|
156
|
+
const vecRows = ctx.db
|
|
157
|
+
.prepare(
|
|
158
|
+
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
|
|
159
|
+
)
|
|
160
|
+
.all(queryVec, vecLimit) as { rowid: number; distance: number }[];
|
|
161
|
+
|
|
162
|
+
if (vecRows.length) {
|
|
163
|
+
const rowids = vecRows.map((vr) => vr.rowid);
|
|
164
|
+
const placeholders = rowids.map(() => "?").join(",");
|
|
165
|
+
const hydrated = ctx.db
|
|
166
|
+
.prepare(
|
|
167
|
+
`SELECT rowid, * FROM vault WHERE rowid IN (${placeholders})`,
|
|
168
|
+
)
|
|
169
|
+
.all(...rowids) as unknown as (VaultEntry & { rowid: number })[];
|
|
170
|
+
|
|
171
|
+
const byRowid = new Map<number, VaultEntry & { rowid: number }>();
|
|
172
|
+
for (const row of hydrated) byRowid.set(row.rowid, row);
|
|
173
|
+
|
|
174
|
+
for (const vr of vecRows) {
|
|
175
|
+
const row = byRowid.get(vr.rowid);
|
|
176
|
+
if (!row) continue;
|
|
177
|
+
if (kindFilter && row.kind !== kindFilter) continue;
|
|
178
|
+
if (categoryFilter && row.category !== categoryFilter) continue;
|
|
179
|
+
if (excludeEvents && row.category === "event") continue;
|
|
180
|
+
if (since && row.created_at < since) continue;
|
|
181
|
+
if (until && row.created_at > until) continue;
|
|
182
|
+
if (row.expires_at && new Date(row.expires_at) <= new Date())
|
|
183
|
+
continue;
|
|
184
|
+
|
|
185
|
+
const { rowid: _rowid, ...cleanRow } = row;
|
|
186
|
+
idToRowid.set(cleanRow.id, Number(row.rowid));
|
|
187
|
+
|
|
188
|
+
const vecSim = Math.max(0, 1 - vr.distance / 2);
|
|
189
|
+
vecSimMap.set(cleanRow.id, vecSim);
|
|
190
|
+
vecRankedIds.push(cleanRow.id);
|
|
191
|
+
|
|
192
|
+
if (!rowMap.has(cleanRow.id)) rowMap.set(cleanRow.id, cleanRow);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (!(err as Error).message?.includes("no such table")) {
|
|
199
|
+
console.error(`[retrieve] Vector search error: ${(err as Error).message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (rowMap.size === 0) return [];
|
|
204
|
+
|
|
205
|
+
const rrfScores = reciprocalRankFusion([ftsRankedIds, vecRankedIds]);
|
|
206
|
+
|
|
207
|
+
for (const [id, entry] of rowMap) {
|
|
208
|
+
const boost = recencyBoost(entry.created_at, entry.category, decayDays);
|
|
209
|
+
rrfScores.set(id, (rrfScores.get(id) ?? 0) * boost);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const candidates: SearchResult[] = [...rowMap.values()].map((entry) => ({
|
|
213
|
+
...entry,
|
|
214
|
+
score: rrfScores.get(entry.id) ?? 0,
|
|
215
|
+
}));
|
|
216
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
217
|
+
|
|
218
|
+
const embeddingMap = new Map<string, Float32Array>();
|
|
219
|
+
if (queryVec && idToRowid.size > 0) {
|
|
220
|
+
const rowidToId = new Map<number, string>();
|
|
221
|
+
for (const [id, rowid] of idToRowid) rowidToId.set(rowid, id);
|
|
222
|
+
|
|
223
|
+
const rowidsToFetch = [...idToRowid.values()];
|
|
224
|
+
try {
|
|
225
|
+
const placeholders = rowidsToFetch.map(() => "?").join(",");
|
|
226
|
+
const vecData = ctx.db
|
|
227
|
+
.prepare(
|
|
228
|
+
`SELECT rowid, embedding FROM vault_vec WHERE rowid IN (${placeholders})`,
|
|
229
|
+
)
|
|
230
|
+
.all(...rowidsToFetch) as { rowid: number; embedding: Buffer }[];
|
|
231
|
+
for (const row of vecData) {
|
|
232
|
+
const id = rowidToId.get(Number(row.rowid));
|
|
233
|
+
const buf = row.embedding;
|
|
234
|
+
if (id && buf) {
|
|
235
|
+
embeddingMap.set(
|
|
236
|
+
id,
|
|
237
|
+
new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Embeddings unavailable
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (queryVec && embeddingMap.size > 0) {
|
|
247
|
+
const selected: SearchResult[] = [];
|
|
248
|
+
const selectedVecs: Float32Array[] = [];
|
|
249
|
+
for (const candidate of candidates) {
|
|
250
|
+
if (selected.length >= offset + limit) break;
|
|
251
|
+
const vec = embeddingMap.get(candidate.id);
|
|
252
|
+
if (vec && selectedVecs.length > 0) {
|
|
253
|
+
let maxSim = 0;
|
|
254
|
+
for (const sv of selectedVecs) {
|
|
255
|
+
const sim = dotProduct(sv, vec);
|
|
256
|
+
if (sim > maxSim) maxSim = sim;
|
|
257
|
+
}
|
|
258
|
+
if (maxSim > NEAR_DUP_THRESHOLD) continue;
|
|
259
|
+
}
|
|
260
|
+
selected.push(candidate);
|
|
261
|
+
if (vec) selectedVecs.push(vec);
|
|
262
|
+
}
|
|
263
|
+
const dedupedPage = selected.slice(offset, offset + limit);
|
|
264
|
+
trackAccess(ctx, dedupedPage);
|
|
265
|
+
return dedupedPage;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const finalPage = candidates.slice(offset, offset + limit);
|
|
269
|
+
trackAccess(ctx, finalPage);
|
|
270
|
+
return finalPage;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
|
|
274
|
+
if (!entries.length) return;
|
|
275
|
+
try {
|
|
276
|
+
const placeholders = entries.map(() => "?").join(",");
|
|
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
|
|
284
|
+
}
|
|
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,97 +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
|
-
},
|
|
42
|
-
) {
|
|
43
|
-
// P5: folder is now a top-level param; also accept from meta for backward compat
|
|
44
|
-
const resolvedFolder = folder || meta?.folder || "";
|
|
45
|
-
const dir = safeFolderPath(vaultDir, kind, resolvedFolder);
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
mkdirSync(dir, { recursive: true });
|
|
49
|
-
} catch (e) {
|
|
50
|
-
throw new Error(`Failed to create directory "${dir}": ${e.message}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const created = createdAt || new Date().toISOString();
|
|
54
|
-
const fmFields = { id };
|
|
55
|
-
|
|
56
|
-
// Add kind-specific meta fields to frontmatter (flattened, not nested)
|
|
57
|
-
if (meta) {
|
|
58
|
-
for (const [k, v] of Object.entries(meta)) {
|
|
59
|
-
if (k === "folder") continue;
|
|
60
|
-
if (v !== null && v !== undefined) fmFields[k] = v;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (identity_key) fmFields.identity_key = identity_key;
|
|
65
|
-
if (expires_at) fmFields.expires_at = expires_at;
|
|
66
|
-
if (supersedes?.length) fmFields.supersedes = supersedes;
|
|
67
|
-
fmFields.tags = tags || [];
|
|
68
|
-
fmFields.source = source || "claude-code";
|
|
69
|
-
fmFields.created = created;
|
|
70
|
-
if (updatedAt && updatedAt !== created) fmFields.updated = updatedAt;
|
|
71
|
-
|
|
72
|
-
const mdBody = formatBody(kind, { title, body, meta });
|
|
73
|
-
|
|
74
|
-
// Entity kinds: deterministic filename from identity_key (no ULID suffix)
|
|
75
|
-
let filename;
|
|
76
|
-
if (category === "entity" && identity_key) {
|
|
77
|
-
const identitySlug = slugify(identity_key);
|
|
78
|
-
filename = identitySlug
|
|
79
|
-
? `${identitySlug}.md`
|
|
80
|
-
: `${id.slice(-8).toLowerCase()}.md`;
|
|
81
|
-
} else {
|
|
82
|
-
const slug = slugify((title || body).slice(0, 40));
|
|
83
|
-
const shortId = id.slice(-8).toLowerCase();
|
|
84
|
-
filename = slug ? `${slug}-${shortId}.md` : `${shortId}.md`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const filePath = resolve(dir, filename);
|
|
88
|
-
const md = formatFrontmatter(fmFields) + mdBody;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
writeFileSync(filePath, md);
|
|
92
|
-
} catch (e) {
|
|
93
|
-
throw new Error(`Failed to write entry file "${filePath}": ${e.message}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return filePath;
|
|
97
|
-
}
|
|
@@ -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
|
-
}
|