context-vault 3.0.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/bin/cli.js +165 -6
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/capture.ts +2 -2
- package/node_modules/@context-vault/core/src/index.ts +4 -2
- package/package.json +2 -2
- package/src/register-tools.js +2 -0
- package/src/tools/save-context.js +7 -4
package/bin/cli.js
CHANGED
|
@@ -3308,7 +3308,7 @@ async function runRecall() {
|
|
|
3308
3308
|
const { initDatabase, prepareStatements } =
|
|
3309
3309
|
await import("@context-vault/core/db");
|
|
3310
3310
|
const { embed } = await import("@context-vault/core/embed");
|
|
3311
|
-
const { hybridSearch } = await import("@context-vault/core/
|
|
3311
|
+
const { hybridSearch } = await import("@context-vault/core/search");
|
|
3312
3312
|
|
|
3313
3313
|
db = await initDatabase(config.dbPath);
|
|
3314
3314
|
const stmts = prepareStatements(db);
|
|
@@ -3436,13 +3436,158 @@ async function runSessionCapture() {
|
|
|
3436
3436
|
}
|
|
3437
3437
|
|
|
3438
3438
|
async function runSessionEnd() {
|
|
3439
|
-
|
|
3440
|
-
|
|
3439
|
+
let db;
|
|
3440
|
+
try {
|
|
3441
|
+
const raw = await new Promise((resolve) => {
|
|
3442
|
+
let data = "";
|
|
3443
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
3444
|
+
process.stdin.on("end", () => resolve(data));
|
|
3445
|
+
});
|
|
3446
|
+
if (!raw.trim()) return;
|
|
3447
|
+
let input;
|
|
3448
|
+
try {
|
|
3449
|
+
input = JSON.parse(raw);
|
|
3450
|
+
} catch {
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
const { session_id, transcript_path, cwd } = input ?? {};
|
|
3454
|
+
if (!transcript_path || !cwd) return;
|
|
3455
|
+
|
|
3456
|
+
// Read transcript (JSONL)
|
|
3457
|
+
let turns = [];
|
|
3458
|
+
try {
|
|
3459
|
+
const transcriptRaw = readFileSync(transcript_path, "utf-8");
|
|
3460
|
+
for (const line of transcriptRaw.split("\n")) {
|
|
3461
|
+
const trimmed = line.trim();
|
|
3462
|
+
if (!trimmed) continue;
|
|
3463
|
+
try { turns.push(JSON.parse(trimmed)); } catch {}
|
|
3464
|
+
}
|
|
3465
|
+
} catch { return; }
|
|
3466
|
+
|
|
3467
|
+
const extractText = (turn) => {
|
|
3468
|
+
if (typeof turn.content === "string") return turn.content;
|
|
3469
|
+
if (Array.isArray(turn.content))
|
|
3470
|
+
return turn.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
3471
|
+
return "";
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
const userTurns = turns.filter((t) => t.role === "user");
|
|
3475
|
+
if (userTurns.length === 0) return;
|
|
3476
|
+
|
|
3477
|
+
// Tool use blocks
|
|
3478
|
+
const allToolUse = [];
|
|
3479
|
+
for (const turn of turns) {
|
|
3480
|
+
if (!Array.isArray(turn.content)) continue;
|
|
3481
|
+
for (const block of turn.content) {
|
|
3482
|
+
if (block.type === "tool_use") allToolUse.push(block);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// Files modified
|
|
3487
|
+
const seenFiles = new Set();
|
|
3488
|
+
const filesModified = [];
|
|
3489
|
+
for (const block of allToolUse) {
|
|
3490
|
+
if (block.name === "Write" || block.name === "Edit") {
|
|
3491
|
+
const path = block.input?.file_path ?? block.input?.path ?? null;
|
|
3492
|
+
if (path && !seenFiles.has(path)) { seenFiles.add(path); filesModified.push(path); }
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
// Commands run
|
|
3497
|
+
const commandsRun = [];
|
|
3498
|
+
for (const block of allToolUse) {
|
|
3499
|
+
if (block.name === "Bash") {
|
|
3500
|
+
const cmd = block.input?.command ?? block.input?.cmd ?? null;
|
|
3501
|
+
if (cmd) commandsRun.push(cmd.slice(0, 100));
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
// Tool counts
|
|
3506
|
+
const toolCounts = {};
|
|
3507
|
+
for (const block of allToolUse) {
|
|
3508
|
+
const name = block.name ?? "unknown";
|
|
3509
|
+
toolCounts[name] = (toolCounts[name] ?? 0) + 1;
|
|
3510
|
+
}
|
|
3511
|
+
const toolSummary = Object.entries(toolCounts)
|
|
3512
|
+
.sort((a, b) => b[1] - a[1])
|
|
3513
|
+
.map(([name, count]) => `${name}: ${count}`)
|
|
3514
|
+
.join(", ");
|
|
3515
|
+
|
|
3516
|
+
// Duration
|
|
3517
|
+
let durationStr = null;
|
|
3518
|
+
const timestampedTurns = turns.filter((t) => t.timestamp != null);
|
|
3519
|
+
if (timestampedTurns.length >= 2) {
|
|
3520
|
+
const diffMs = new Date(timestampedTurns[timestampedTurns.length - 1].timestamp) - new Date(timestampedTurns[0].timestamp);
|
|
3521
|
+
if (!isNaN(diffMs) && diffMs >= 0) {
|
|
3522
|
+
const totalSec = Math.round(diffMs / 1000);
|
|
3523
|
+
const hours = Math.floor(totalSec / 3600);
|
|
3524
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
3525
|
+
const seconds = totalSec % 60;
|
|
3526
|
+
durationStr = hours > 0 ? `${hours}h ${minutes}m` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
const message_count = userTurns.length;
|
|
3531
|
+
const project = cwd.split("/").pop() || "unknown";
|
|
3532
|
+
const first_prompt = extractText(userTurns[0]).slice(0, 200);
|
|
3533
|
+
const last_prompt = message_count > 1 ? extractText(userTurns[message_count - 1]).slice(0, 200) : first_prompt;
|
|
3534
|
+
|
|
3535
|
+
// Build body
|
|
3536
|
+
const durationPart = durationStr ? `, ~${durationStr}` : "";
|
|
3537
|
+
const bodyLines = [
|
|
3538
|
+
`Session in ${project} (${message_count} exchange${message_count !== 1 ? "s" : ""}${durationPart}).`,
|
|
3539
|
+
"", "## What was done",
|
|
3540
|
+
`Opened with: ${first_prompt}`, "",
|
|
3541
|
+
`Closed with: ${last_prompt}`,
|
|
3542
|
+
];
|
|
3543
|
+
const limitedFiles = filesModified.slice(0, 20);
|
|
3544
|
+
if (limitedFiles.length > 0) {
|
|
3545
|
+
bodyLines.push("", "## Files modified");
|
|
3546
|
+
for (const f of limitedFiles) bodyLines.push(`- ${f}`);
|
|
3547
|
+
if (filesModified.length > 20) bodyLines.push(`- ... and ${filesModified.length - 20} more`);
|
|
3548
|
+
}
|
|
3549
|
+
const limitedCmds = commandsRun.slice(0, 10);
|
|
3550
|
+
if (limitedCmds.length > 0) {
|
|
3551
|
+
bodyLines.push("", "## Key commands");
|
|
3552
|
+
for (const c of limitedCmds) bodyLines.push(`- ${c}`);
|
|
3553
|
+
if (commandsRun.length > 10) bodyLines.push(`- ... and ${commandsRun.length - 10} more`);
|
|
3554
|
+
}
|
|
3555
|
+
if (toolSummary) bodyLines.push("", "## Tools used", toolSummary);
|
|
3556
|
+
const body = bodyLines.join("\n");
|
|
3557
|
+
|
|
3558
|
+
// Save via core APIs
|
|
3559
|
+
const { resolveConfig } = await import("@context-vault/core/config");
|
|
3560
|
+
const config = resolveConfig();
|
|
3561
|
+
if (!config.vaultDirExists) return;
|
|
3562
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
3563
|
+
await import("@context-vault/core/db");
|
|
3564
|
+
const { captureAndIndex } = await import("@context-vault/core/capture");
|
|
3565
|
+
db = await initDatabase(config.dbPath);
|
|
3566
|
+
const stmts = prepareStatements(db);
|
|
3567
|
+
const ctx = {
|
|
3568
|
+
db, config, stmts,
|
|
3569
|
+
embed: async () => null,
|
|
3570
|
+
insertVec: (rowid, embedding) => insertVec(stmts, rowid, embedding),
|
|
3571
|
+
deleteVec: (rowid) => deleteVec(stmts, rowid),
|
|
3572
|
+
};
|
|
3573
|
+
const entry = await captureAndIndex(ctx, {
|
|
3574
|
+
kind: "session",
|
|
3575
|
+
title: `Session — ${project} ${new Date().toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" })}`,
|
|
3576
|
+
body,
|
|
3577
|
+
tags: ["session-end", "session-summary", project],
|
|
3578
|
+
source: "claude-code",
|
|
3579
|
+
meta: { session_id: session_id ?? null, cwd, message_count },
|
|
3580
|
+
});
|
|
3581
|
+
console.log(`context-vault session captured — id: ${entry.id}`);
|
|
3582
|
+
} catch {
|
|
3583
|
+
// fail silently — never block session end
|
|
3584
|
+
} finally {
|
|
3585
|
+
try { db?.close(); } catch {}
|
|
3586
|
+
}
|
|
3441
3587
|
}
|
|
3442
3588
|
|
|
3443
3589
|
async function runPostToolCall() {
|
|
3444
|
-
|
|
3445
|
-
await main();
|
|
3590
|
+
// Removed in v3 — post-tool-call hooks are no longer supported
|
|
3446
3591
|
}
|
|
3447
3592
|
|
|
3448
3593
|
async function runSave() {
|
|
@@ -3453,6 +3598,8 @@ async function runSave() {
|
|
|
3453
3598
|
const tier = getFlag("--tier");
|
|
3454
3599
|
const filePath = getFlag("--file");
|
|
3455
3600
|
const bodyFlag = getFlag("--body");
|
|
3601
|
+
const identityKey = getFlag("--identity-key");
|
|
3602
|
+
const metaRaw = getFlag("--meta");
|
|
3456
3603
|
|
|
3457
3604
|
if (!kind) {
|
|
3458
3605
|
console.error(red("Error: --kind is required"));
|
|
@@ -3463,6 +3610,16 @@ async function runSave() {
|
|
|
3463
3610
|
process.exit(1);
|
|
3464
3611
|
}
|
|
3465
3612
|
|
|
3613
|
+
let meta;
|
|
3614
|
+
if (metaRaw) {
|
|
3615
|
+
try {
|
|
3616
|
+
meta = JSON.parse(metaRaw);
|
|
3617
|
+
} catch {
|
|
3618
|
+
console.error(red("Error: --meta must be valid JSON"));
|
|
3619
|
+
process.exit(1);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3466
3623
|
let body;
|
|
3467
3624
|
if (bodyFlag) {
|
|
3468
3625
|
body = bodyFlag;
|
|
@@ -3520,6 +3677,8 @@ async function runSave() {
|
|
|
3520
3677
|
tags: parsedTags,
|
|
3521
3678
|
source,
|
|
3522
3679
|
...(tier ? { tier } : {}),
|
|
3680
|
+
...(identityKey ? { identity_key: identityKey } : {}),
|
|
3681
|
+
...(meta !== undefined ? { meta } : {}),
|
|
3523
3682
|
});
|
|
3524
3683
|
console.log(`${green("✓")} Saved ${kind} — id: ${entry.id}`);
|
|
3525
3684
|
} catch (e) {
|
|
@@ -3579,7 +3738,7 @@ async function runSearch() {
|
|
|
3579
3738
|
const { initDatabase, prepareStatements } =
|
|
3580
3739
|
await import("@context-vault/core/db");
|
|
3581
3740
|
const { embed } = await import("@context-vault/core/embed");
|
|
3582
|
-
const { hybridSearch } = await import("@context-vault/core/
|
|
3741
|
+
const { hybridSearch } = await import("@context-vault/core/search");
|
|
3583
3742
|
|
|
3584
3743
|
db = await initDatabase(config.dbPath);
|
|
3585
3744
|
const stmts = prepareStatements(db);
|
|
@@ -266,7 +266,7 @@ export function updateEntryFile(
|
|
|
266
266
|
};
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
export async function captureAndIndex(ctx: BaseCtx, data: CaptureInput): Promise<CaptureResult> {
|
|
269
|
+
export async function captureAndIndex(ctx: BaseCtx, data: CaptureInput, precomputedEmbedding?: Float32Array | null): Promise<CaptureResult> {
|
|
270
270
|
let previousContent: string | null = null;
|
|
271
271
|
if (categoryFor(data.kind) === "entity" && data.identity_key) {
|
|
272
272
|
const identitySlug = slugify(data.identity_key);
|
|
@@ -279,7 +279,7 @@ export async function captureAndIndex(ctx: BaseCtx, data: CaptureInput): Promise
|
|
|
279
279
|
|
|
280
280
|
const entry = writeEntry(ctx, data);
|
|
281
281
|
try {
|
|
282
|
-
await indexEntry(ctx, entry);
|
|
282
|
+
await indexEntry(ctx, entry, precomputedEmbedding);
|
|
283
283
|
if (entry.supersedes?.length && ctx.stmts.updateSupersededBy) {
|
|
284
284
|
for (const supersededId of entry.supersedes) {
|
|
285
285
|
if (typeof supersededId === "string" && supersededId.trim()) {
|
|
@@ -13,6 +13,7 @@ const EMBED_BATCH_SIZE = 32;
|
|
|
13
13
|
export async function indexEntry(
|
|
14
14
|
ctx: BaseCtx,
|
|
15
15
|
entry: IndexEntryInput & { supersedes?: string[] | null; related_to?: string[] | null },
|
|
16
|
+
precomputedEmbedding?: Float32Array | null,
|
|
16
17
|
): Promise<void> {
|
|
17
18
|
const {
|
|
18
19
|
id, kind, category, title, body, meta, tags, source,
|
|
@@ -92,8 +93,9 @@ export async function indexEntry(
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
if (cat !== "event") {
|
|
95
|
-
const
|
|
96
|
-
|
|
96
|
+
const embedding = precomputedEmbedding !== undefined
|
|
97
|
+
? precomputedEmbedding
|
|
98
|
+
: await ctx.embed([title, body].filter(Boolean).join(" "));
|
|
97
99
|
|
|
98
100
|
if (embedding) {
|
|
99
101
|
try { ctx.deleteVec(rowid); } catch { /* no-op */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@context-vault/core"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@context-vault/core": "^3.0.
|
|
60
|
+
"@context-vault/core": "^3.0.3",
|
|
61
61
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
62
62
|
"adm-zip": "^0.5.16",
|
|
63
63
|
"sqlite-vec": "^0.1.0"
|
package/src/register-tools.js
CHANGED
|
@@ -484,17 +484,18 @@ export async function handler(
|
|
|
484
484
|
// ── Similarity check (knowledge + event only) ────────────────────────────
|
|
485
485
|
const category = categoryFor(normalizedKind);
|
|
486
486
|
let similarEntries = [];
|
|
487
|
+
let queryEmbedding = null;
|
|
487
488
|
|
|
488
489
|
if (category === "knowledge" || category === "event") {
|
|
489
490
|
const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
490
491
|
const embeddingText = [title, body].filter(Boolean).join(" ");
|
|
491
|
-
|
|
492
|
+
queryEmbedding = await ctx.embed(embeddingText);
|
|
492
493
|
if (queryEmbedding) {
|
|
493
494
|
similarEntries = await findSimilar(
|
|
494
495
|
ctx,
|
|
495
496
|
queryEmbedding,
|
|
496
497
|
threshold,
|
|
497
|
-
|
|
498
|
+
|
|
498
499
|
{ hydrate: suggestMode },
|
|
499
500
|
);
|
|
500
501
|
}
|
|
@@ -540,6 +541,8 @@ export async function handler(
|
|
|
540
541
|
|
|
541
542
|
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
542
543
|
|
|
544
|
+
const embeddingToReuse = category === "knowledge" ? queryEmbedding : null;
|
|
545
|
+
|
|
543
546
|
const entry = await captureAndIndex(ctx, {
|
|
544
547
|
kind: normalizedKind,
|
|
545
548
|
title,
|
|
@@ -553,9 +556,9 @@ export async function handler(
|
|
|
553
556
|
supersedes,
|
|
554
557
|
related_to,
|
|
555
558
|
source_files,
|
|
556
|
-
|
|
559
|
+
|
|
557
560
|
tier: effectiveTier,
|
|
558
|
-
});
|
|
561
|
+
}, embeddingToReuse);
|
|
559
562
|
|
|
560
563
|
if (ctx.config?.dataDir) {
|
|
561
564
|
maybeShowFeedbackPrompt(ctx.config.dataDir);
|