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 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/retrieve/index");
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
- const { main } = await import("../src/hooks/session-end.mjs");
3440
- await main();
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
- const { main } = await import("../src/hooks/post-tool-call.mjs");
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/retrieve/index");
3741
+ const { hybridSearch } = await import("@context-vault/core/search");
3583
3742
 
3584
3743
  db = await initDatabase(config.dbPath);
3585
3744
  const stmts = prepareStatements(db);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "3.0.0",
3
+ "version": "3.0.3",
4
4
  "type": "module",
5
5
  "description": "Pure local engine: capture, index, search, and utilities for context-vault",
6
6
  "main": "dist/main.js",
@@ -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 embeddingText = [title, body].filter(Boolean).join(" ");
96
- const embedding = await ctx.embed(embeddingText);
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.1",
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.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"
@@ -170,4 +170,6 @@ export function registerTools(server, ctx) {
170
170
  tracked((args) => mod.handler(args, ctx, shared), mod.name),
171
171
  );
172
172
  }
173
+
174
+ ensureIndexed().catch(() => {});
173
175
  }
@@ -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
- const queryEmbedding = await ctx.embed(embeddingText);
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);