@context-vault/core 2.10.3 → 2.12.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/package.json +1 -1
- package/src/capture/file-ops.js +4 -0
- package/src/capture/index.js +26 -0
- package/src/constants.js +13 -0
- package/src/core/categories.js +11 -0
- package/src/core/config.js +32 -0
- package/src/core/frontmatter.js +2 -0
- package/src/core/status.js +165 -0
- package/src/core/telemetry.js +90 -0
- package/src/index/db.js +86 -9
- package/src/index/index.js +37 -1
- package/src/index.js +1 -1
- package/src/retrieve/index.js +81 -4
- package/src/server/tools/context-status.js +37 -3
- package/src/server/tools/get-context.js +23 -2
- package/src/server/tools/ingest-url.js +0 -11
- package/src/server/tools/list-context.js +7 -3
- package/src/server/tools/save-context.js +144 -13
- package/src/server/tools.js +13 -0
|
@@ -4,6 +4,7 @@ import { indexEntry } from "../../index/index.js";
|
|
|
4
4
|
import { categoryFor } from "../../core/categories.js";
|
|
5
5
|
import { normalizeKind } from "../../core/files.js";
|
|
6
6
|
import { ok, err, ensureVaultExists, ensureValidKind } from "../helpers.js";
|
|
7
|
+
import { maybeShowFeedbackPrompt } from "../../core/telemetry.js";
|
|
7
8
|
import {
|
|
8
9
|
MAX_BODY_LENGTH,
|
|
9
10
|
MAX_TITLE_LENGTH,
|
|
@@ -15,6 +16,63 @@ import {
|
|
|
15
16
|
MAX_IDENTITY_KEY_LENGTH,
|
|
16
17
|
} from "../../constants.js";
|
|
17
18
|
|
|
19
|
+
const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
|
|
20
|
+
|
|
21
|
+
async function findSimilar(ctx, embedding, threshold, userId) {
|
|
22
|
+
try {
|
|
23
|
+
const vecCount = ctx.db
|
|
24
|
+
.prepare("SELECT COUNT(*) as c FROM vault_vec")
|
|
25
|
+
.get().c;
|
|
26
|
+
if (vecCount === 0) return [];
|
|
27
|
+
|
|
28
|
+
const vecRows = ctx.db
|
|
29
|
+
.prepare(
|
|
30
|
+
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
|
|
31
|
+
)
|
|
32
|
+
.all(embedding, 10);
|
|
33
|
+
|
|
34
|
+
if (!vecRows.length) return [];
|
|
35
|
+
|
|
36
|
+
const rowids = vecRows.map((vr) => vr.rowid);
|
|
37
|
+
const placeholders = rowids.map(() => "?").join(",");
|
|
38
|
+
const hydrated = ctx.db
|
|
39
|
+
.prepare(
|
|
40
|
+
`SELECT rowid, id, title, category, user_id FROM vault WHERE rowid IN (${placeholders})`,
|
|
41
|
+
)
|
|
42
|
+
.all(...rowids);
|
|
43
|
+
|
|
44
|
+
const byRowid = new Map();
|
|
45
|
+
for (const row of hydrated) byRowid.set(row.rowid, row);
|
|
46
|
+
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const vr of vecRows) {
|
|
49
|
+
const similarity = Math.max(0, 1 - vr.distance / 2);
|
|
50
|
+
if (similarity < threshold) continue;
|
|
51
|
+
const row = byRowid.get(vr.rowid);
|
|
52
|
+
if (!row) continue;
|
|
53
|
+
if (userId !== undefined && row.user_id !== userId) continue;
|
|
54
|
+
if (row.category === "entity") continue;
|
|
55
|
+
results.push({ id: row.id, title: row.title, score: similarity });
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatSimilarWarning(similar) {
|
|
64
|
+
const lines = ["", "⚠ Similar entries already exist:"];
|
|
65
|
+
for (const e of similar) {
|
|
66
|
+
const score = e.score.toFixed(2);
|
|
67
|
+
const title = e.title ? `"${e.title}"` : "(no title)";
|
|
68
|
+
lines.push(` - ${title} (${score}) — id: ${e.id}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push(
|
|
71
|
+
" Consider using `id: <existing>` in save_context to update instead.",
|
|
72
|
+
);
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
18
76
|
/**
|
|
19
77
|
* Validate input fields for save_context. Returns an error response or null.
|
|
20
78
|
*/
|
|
@@ -150,6 +208,26 @@ export const inputSchema = {
|
|
|
150
208
|
"Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.",
|
|
151
209
|
),
|
|
152
210
|
expires_at: z.string().optional().describe("ISO date for TTL expiry"),
|
|
211
|
+
supersedes: z
|
|
212
|
+
.array(z.string())
|
|
213
|
+
.optional()
|
|
214
|
+
.describe(
|
|
215
|
+
"Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.",
|
|
216
|
+
),
|
|
217
|
+
dry_run: z
|
|
218
|
+
.boolean()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe(
|
|
221
|
+
"If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.",
|
|
222
|
+
),
|
|
223
|
+
similarity_threshold: z
|
|
224
|
+
.number()
|
|
225
|
+
.min(0)
|
|
226
|
+
.max(1)
|
|
227
|
+
.optional()
|
|
228
|
+
.describe(
|
|
229
|
+
"Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.",
|
|
230
|
+
),
|
|
153
231
|
};
|
|
154
232
|
|
|
155
233
|
/**
|
|
@@ -169,6 +247,9 @@ export async function handler(
|
|
|
169
247
|
source,
|
|
170
248
|
identity_key,
|
|
171
249
|
expires_at,
|
|
250
|
+
supersedes,
|
|
251
|
+
dry_run,
|
|
252
|
+
similarity_threshold,
|
|
172
253
|
},
|
|
173
254
|
ctx,
|
|
174
255
|
{ ensureIndexed },
|
|
@@ -231,6 +312,7 @@ export async function handler(
|
|
|
231
312
|
meta,
|
|
232
313
|
source,
|
|
233
314
|
expires_at,
|
|
315
|
+
supersedes,
|
|
234
316
|
});
|
|
235
317
|
await indexEntry(ctx, entry);
|
|
236
318
|
const relPath = entry.filePath
|
|
@@ -261,25 +343,45 @@ export async function handler(
|
|
|
261
343
|
);
|
|
262
344
|
}
|
|
263
345
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
346
|
+
await ensureIndexed();
|
|
347
|
+
|
|
348
|
+
// ── Similarity check (knowledge + event only) ────────────────────────────
|
|
349
|
+
const category = categoryFor(normalizedKind);
|
|
350
|
+
let similarEntries = [];
|
|
351
|
+
|
|
352
|
+
if (category === "knowledge" || category === "event") {
|
|
353
|
+
const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
354
|
+
const embeddingText = [title, body].filter(Boolean).join(" ");
|
|
355
|
+
const queryEmbedding = await ctx.embed(embeddingText);
|
|
356
|
+
if (queryEmbedding) {
|
|
357
|
+
similarEntries = await findSimilar(
|
|
358
|
+
ctx,
|
|
359
|
+
queryEmbedding,
|
|
360
|
+
threshold,
|
|
361
|
+
userId,
|
|
271
362
|
);
|
|
272
363
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (dry_run) {
|
|
367
|
+
const parts = ["(dry run — nothing saved)"];
|
|
368
|
+
if (similarEntries.length) {
|
|
369
|
+
parts.push("", "⚠ Similar entries already exist:");
|
|
370
|
+
for (const e of similarEntries) {
|
|
371
|
+
const score = e.score.toFixed(2);
|
|
372
|
+
const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
|
|
373
|
+
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
374
|
+
}
|
|
375
|
+
parts.push(
|
|
376
|
+
"",
|
|
377
|
+
"Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
|
|
277
378
|
);
|
|
379
|
+
} else {
|
|
380
|
+
parts.push("", "No similar entries found. Safe to save.");
|
|
278
381
|
}
|
|
382
|
+
return ok(parts.join("\n"));
|
|
279
383
|
}
|
|
280
384
|
|
|
281
|
-
await ensureIndexed();
|
|
282
|
-
|
|
283
385
|
const mergedMeta = { ...(meta || {}) };
|
|
284
386
|
if (folder) mergedMeta.folder = folder;
|
|
285
387
|
const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
|
|
@@ -294,8 +396,14 @@ export async function handler(
|
|
|
294
396
|
folder,
|
|
295
397
|
identity_key,
|
|
296
398
|
expires_at,
|
|
399
|
+
supersedes,
|
|
297
400
|
userId,
|
|
298
401
|
});
|
|
402
|
+
|
|
403
|
+
if (ctx.config?.dataDir) {
|
|
404
|
+
maybeShowFeedbackPrompt(ctx.config.dataDir);
|
|
405
|
+
}
|
|
406
|
+
|
|
299
407
|
const relPath = entry.filePath
|
|
300
408
|
? entry.filePath.replace(config.vaultDir + "/", "")
|
|
301
409
|
: entry.filePath;
|
|
@@ -303,5 +411,28 @@ export async function handler(
|
|
|
303
411
|
if (title) parts.push(` title: ${title}`);
|
|
304
412
|
if (tags?.length) parts.push(` tags: ${tags.join(", ")}`);
|
|
305
413
|
parts.push("", "_Use this id to update or delete later._");
|
|
414
|
+
if (similarEntries.length) {
|
|
415
|
+
parts.push(formatSimilarWarning(similarEntries));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const criticalLimit = config.thresholds?.totalEntries?.critical;
|
|
419
|
+
if (criticalLimit != null) {
|
|
420
|
+
try {
|
|
421
|
+
const countRow = ctx.db
|
|
422
|
+
.prepare(
|
|
423
|
+
userId !== undefined
|
|
424
|
+
? "SELECT COUNT(*) as c FROM vault WHERE user_id = ?"
|
|
425
|
+
: "SELECT COUNT(*) as c FROM vault",
|
|
426
|
+
)
|
|
427
|
+
.get(...(userId !== undefined ? [userId] : []));
|
|
428
|
+
if (countRow.c >= criticalLimit) {
|
|
429
|
+
parts.push(
|
|
430
|
+
``,
|
|
431
|
+
`ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} catch {}
|
|
435
|
+
}
|
|
436
|
+
|
|
306
437
|
return ok(parts.join("\n"));
|
|
307
438
|
}
|
package/src/server/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reindex } from "../index/index.js";
|
|
2
2
|
import { captureAndIndex } from "../capture/index.js";
|
|
3
3
|
import { err } from "./helpers.js";
|
|
4
|
+
import { sendTelemetryEvent } from "../core/telemetry.js";
|
|
4
5
|
import pkg from "../../package.json" with { type: "json" };
|
|
5
6
|
|
|
6
7
|
import * as getContext from "./tools/get-context.js";
|
|
@@ -57,6 +58,12 @@ export function registerTools(server, ctx) {
|
|
|
57
58
|
timestamp: Date.now(),
|
|
58
59
|
};
|
|
59
60
|
}
|
|
61
|
+
sendTelemetryEvent(ctx.config, {
|
|
62
|
+
event: "tool_error",
|
|
63
|
+
code: "TIMEOUT",
|
|
64
|
+
tool: toolName,
|
|
65
|
+
cv_version: pkg.version,
|
|
66
|
+
});
|
|
60
67
|
return err(
|
|
61
68
|
"Tool timed out after 60s. Try a simpler query or run `context-vault reindex` first.",
|
|
62
69
|
"TIMEOUT",
|
|
@@ -70,6 +77,12 @@ export function registerTools(server, ctx) {
|
|
|
70
77
|
timestamp: Date.now(),
|
|
71
78
|
};
|
|
72
79
|
}
|
|
80
|
+
sendTelemetryEvent(ctx.config, {
|
|
81
|
+
event: "tool_error",
|
|
82
|
+
code: "UNKNOWN",
|
|
83
|
+
tool: toolName,
|
|
84
|
+
cv_version: pkg.version,
|
|
85
|
+
});
|
|
73
86
|
try {
|
|
74
87
|
await captureAndIndex(ctx, {
|
|
75
88
|
kind: "feedback",
|