context-vault 3.0.2 → 3.1.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/bin/cli.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // Node.js version guard — must run before any ESM imports
4
4
  const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
5
- if (nodeVersion < 20) {
5
+ if (nodeVersion < 22) {
6
6
  process.stderr.write(
7
- `\ncontext-vault requires Node.js >= 20 (you have ${process.versions.node}).\n` +
7
+ `\ncontext-vault requires Node.js >= 22 (you have ${process.versions.node}).\n` +
8
8
  `Install a newer version: https://nodejs.org/\n\n`,
9
9
  );
10
10
  process.exit(1);
@@ -303,6 +303,7 @@ ${bold("Commands:")}
303
303
  ${cyan("health")} Quick health check — vault, DB, entry count
304
304
  ${cyan("status")} Show vault diagnostics
305
305
  ${cyan("doctor")} Diagnose and repair common issues
306
+ ${cyan("debug")} Generate AI-pasteable debug report
306
307
  ${cyan("restart")} Stop running MCP server processes (client auto-restarts)
307
308
  ${cyan("search")} Search vault entries from CLI
308
309
  ${cyan("save")} Save an entry to the vault from CLI
@@ -3598,6 +3599,8 @@ async function runSave() {
3598
3599
  const tier = getFlag("--tier");
3599
3600
  const filePath = getFlag("--file");
3600
3601
  const bodyFlag = getFlag("--body");
3602
+ const identityKey = getFlag("--identity-key");
3603
+ const metaRaw = getFlag("--meta");
3601
3604
 
3602
3605
  if (!kind) {
3603
3606
  console.error(red("Error: --kind is required"));
@@ -3608,6 +3611,16 @@ async function runSave() {
3608
3611
  process.exit(1);
3609
3612
  }
3610
3613
 
3614
+ let meta;
3615
+ if (metaRaw) {
3616
+ try {
3617
+ meta = JSON.parse(metaRaw);
3618
+ } catch {
3619
+ console.error(red("Error: --meta must be valid JSON"));
3620
+ process.exit(1);
3621
+ }
3622
+ }
3623
+
3611
3624
  let body;
3612
3625
  if (bodyFlag) {
3613
3626
  body = bodyFlag;
@@ -3665,6 +3678,8 @@ async function runSave() {
3665
3678
  tags: parsedTags,
3666
3679
  source,
3667
3680
  ...(tier ? { tier } : {}),
3681
+ ...(identityKey ? { identity_key: identityKey } : {}),
3682
+ ...(meta !== undefined ? { meta } : {}),
3668
3683
  });
3669
3684
  console.log(`${green("✓")} Saved ${kind} — id: ${entry.id}`);
3670
3685
  } catch (e) {
@@ -5265,6 +5280,86 @@ async function runConsolidate() {
5265
5280
  console.log();
5266
5281
  }
5267
5282
 
5283
+ async function runDebug() {
5284
+ const { resolveConfig } = await import("@context-vault/core/config");
5285
+ const { errorLogPath, errorLogCount } = await import("../src/error-log.js");
5286
+
5287
+ let config;
5288
+ try {
5289
+ config = resolveConfig();
5290
+ } catch {
5291
+ config = null;
5292
+ }
5293
+
5294
+ const dataDir = config?.dataDir || join(HOME, ".context-mcp");
5295
+ const vaultDir = config?.vaultDir || join(HOME, "vault");
5296
+ const dbPath = config?.dbPath || join(dataDir, "vault.db");
5297
+ const configPath = config?.configPath || join(dataDir, "config.json");
5298
+
5299
+ const vaultExists = existsSync(vaultDir);
5300
+ let vaultWritable = false;
5301
+ if (vaultExists) {
5302
+ try {
5303
+ const probe = join(vaultDir, ".write-probe");
5304
+ writeFileSync(probe, "");
5305
+ unlinkSync(probe);
5306
+ vaultWritable = true;
5307
+ } catch {}
5308
+ }
5309
+
5310
+ let dbAccessible = false;
5311
+ let dbEntryCount = "n/a";
5312
+ try {
5313
+ const { initDatabase } = await import("@context-vault/core/db");
5314
+ const db = await initDatabase(dbPath);
5315
+ dbEntryCount = db.prepare("SELECT COUNT(*) as c FROM vault").get().c;
5316
+ db.close();
5317
+ dbAccessible = true;
5318
+ } catch {}
5319
+
5320
+ const logCount = errorLogCount(dataDir);
5321
+ const logPath = errorLogPath(dataDir);
5322
+ let lastLogLines = [];
5323
+ if (logCount > 0) {
5324
+ try {
5325
+ const lines = readFileSync(logPath, "utf-8")
5326
+ .split("\n")
5327
+ .filter((l) => l.trim());
5328
+ lastLogLines = lines.slice(-5);
5329
+ } catch {}
5330
+ }
5331
+
5332
+ const lines = [
5333
+ "```",
5334
+ `context-vault debug report`,
5335
+ `Generated: ${new Date().toISOString()}`,
5336
+ ``,
5337
+ `Node.js: ${process.versions.node} (${process.execPath})`,
5338
+ `Platform: ${process.platform} ${process.arch}`,
5339
+ `cv version: ${VERSION}`,
5340
+ ``,
5341
+ `Config: ${configPath} (${existsSync(configPath) ? "found" : "missing"})`,
5342
+ `Vault dir: ${vaultDir} (${vaultExists ? "exists" : "missing"}${vaultExists ? `, writable: ${vaultWritable}` : ""})`,
5343
+ `DB path: ${dbPath} (${existsSync(dbPath) ? "exists" : "missing"})`,
5344
+ `DB access: ${dbAccessible ? `ok (${dbEntryCount} entries)` : "failed"}`,
5345
+ ``,
5346
+ `Error log: ${logPath} (${logCount} entries)`,
5347
+ ];
5348
+
5349
+ if (lastLogLines.length) {
5350
+ lines.push(``, `Last 5 error log entries:`);
5351
+ for (const l of lastLogLines) lines.push(` ${l}`);
5352
+ }
5353
+
5354
+ lines.push("```");
5355
+ lines.push(
5356
+ ``,
5357
+ `Paste the above into Claude Code or your AI assistant to diagnose issues.`,
5358
+ );
5359
+
5360
+ console.log(lines.join("\n"));
5361
+ }
5362
+
5268
5363
  async function runServe() {
5269
5364
  await import("../src/server.js");
5270
5365
  }
@@ -5384,6 +5479,9 @@ async function main() {
5384
5479
  case "consolidate":
5385
5480
  await runConsolidate();
5386
5481
  break;
5482
+ case "debug":
5483
+ await runDebug();
5484
+ break;
5387
5485
  default:
5388
5486
  console.error(red(`Unknown command: ${command}`));
5389
5487
  console.error(`Run ${cyan("context-vault --help")} for usage.`);
@@ -5392,6 +5490,15 @@ async function main() {
5392
5490
  }
5393
5491
 
5394
5492
  main().catch((e) => {
5395
- console.error(red(e.message));
5493
+ const dataDir = join(HOME, ".context-mcp");
5494
+ const logPath = join(dataDir, "error.log");
5495
+ console.error(red(`Error: ${e.message}`));
5496
+ console.error(dim(`Error log: ${logPath}`));
5497
+ console.error(dim(`Run: context-vault doctor`));
5498
+ console.error(
5499
+ dim(
5500
+ `Debug with AI: "context-vault exited with: ${e.message} — how do I fix this?"`,
5501
+ ),
5502
+ );
5396
5503
  process.exit(1);
5397
5504
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
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()) {
@@ -107,6 +107,7 @@ export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
107
107
  const db = new DatabaseSync(path, { allowExtension: true });
108
108
  db.exec("PRAGMA journal_mode = WAL");
109
109
  db.exec("PRAGMA foreign_keys = ON");
110
+ db.exec("PRAGMA busy_timeout = 3000");
110
111
  try {
111
112
  sqliteVec.load(db);
112
113
  } catch (e) {
@@ -124,6 +125,7 @@ export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
124
125
  );
125
126
 
126
127
  const backupPath = `${dbPath}.v${version}.backup`;
128
+ let backupSucceeded = false;
127
129
  try {
128
130
  db.close();
129
131
  if (existsSync(dbPath)) {
@@ -131,6 +133,9 @@ export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
131
133
  console.error(
132
134
  `[context-vault] Backed up old database to: ${backupPath}`,
133
135
  );
136
+ backupSucceeded = true;
137
+ } else {
138
+ backupSucceeded = true;
134
139
  }
135
140
  } catch (backupErr) {
136
141
  console.error(
@@ -138,6 +143,13 @@ export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
138
143
  );
139
144
  }
140
145
 
146
+ if (!backupSucceeded) {
147
+ throw new Error(
148
+ `[context-vault] Aborting schema migration: backup failed for ${dbPath}. ` +
149
+ `Fix the backup issue or manually back up the file before upgrading.`,
150
+ );
151
+ }
152
+
141
153
  unlinkSync(dbPath);
142
154
  try { unlinkSync(dbPath + "-wal"); } catch {}
143
155
  try { unlinkSync(dbPath + "-shm"); } catch {}
@@ -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,16 @@ 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
+ let embedding: Float32Array | null = null;
97
+ if (precomputedEmbedding !== undefined) {
98
+ embedding = precomputedEmbedding;
99
+ } else {
100
+ try {
101
+ embedding = await ctx.embed([title, body].filter(Boolean).join(" "));
102
+ } catch (embedErr) {
103
+ console.warn(`[context-vault] embed() failed for entry ${id} — skipping vec insert: ${(embedErr as Error).message}`);
104
+ }
105
+ }
97
106
 
98
107
  if (embedding) {
99
108
  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.2",
3
+ "version": "3.1.0",
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.1.0",
61
61
  "@modelcontextprotocol/sdk": "^1.26.0",
62
62
  "adm-zip": "^0.5.16",
63
63
  "sqlite-vec": "^0.1.0"
package/src/helpers.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
 
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  const pkg = JSON.parse(
7
- readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"),
7
+ readFileSync(join(__dirname, "..", "package.json"), "utf-8"),
8
8
  );
9
9
 
10
10
  export function ok(text) {
@@ -26,11 +26,19 @@ export function err(text, code = "UNKNOWN", meta = {}) {
26
26
  };
27
27
  }
28
28
 
29
+ export function errWithHint(text, code, hint) {
30
+ const prompt = hint
31
+ ? `\n\n**Debug with AI:** Paste this into Claude Code or your AI assistant:\n> "${hint}"`
32
+ : "";
33
+ return err(text + prompt, code);
34
+ }
35
+
29
36
  export function ensureVaultExists(config) {
30
37
  if (!config.vaultDirExists) {
31
- return err(
38
+ return errWithHint(
32
39
  `Vault directory not found: ${config.vaultDir}. Run context-status for diagnostics.`,
33
40
  "VAULT_NOT_FOUND",
41
+ "My context-vault can't find the vault directory. Run `context-vault doctor` and help me fix it.",
34
42
  );
35
43
  }
36
44
  return null;
@@ -109,7 +109,7 @@ export function registerTools(server, ctx) {
109
109
  },
110
110
  });
111
111
  } catch {}
112
- throw e;
112
+ return err(e.message, "INTERNAL_ERROR");
113
113
  } finally {
114
114
  clearTimeout(timer);
115
115
  if (ctx.activeOps) ctx.activeOps.count--;
@@ -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
  }
package/src/server.js CHANGED
@@ -221,6 +221,13 @@ async function main() {
221
221
  phase,
222
222
  };
223
223
  appendErrorLog(dataDir, logEntry);
224
+ try {
225
+ mkdirSync(dataDir, { recursive: true });
226
+ writeFileSync(
227
+ join(dataDir, ".last-error"),
228
+ `${logEntry.timestamp} [${phase}] ${err.message}`,
229
+ );
230
+ } catch {}
224
231
 
225
232
  sendTelemetryEvent(config, {
226
233
  event: "startup_error",
@@ -1,6 +1,8 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { gatherVaultStatus, computeGrowthWarnings } from "../status.js";
2
4
  import { errorLogPath, errorLogCount } from "../error-log.js";
3
- import { ok } from "../helpers.js";
5
+ import { ok, err } from "../helpers.js";
4
6
 
5
7
  function relativeTime(ts) {
6
8
  const secs = Math.floor((Date.now() - ts) / 1000);
@@ -23,6 +25,7 @@ export const inputSchema = {};
23
25
  * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
24
26
  */
25
27
  export function handler(_args, ctx) {
28
+ try {
26
29
  const { config } = ctx;
27
30
 
28
31
  const status = gatherVaultStatus(ctx);
@@ -120,6 +123,18 @@ export function handler(_args, ctx) {
120
123
  lines.push(`- Entries: ${logCount} (share this file for support)`);
121
124
  }
122
125
 
126
+ // Last startup error
127
+ const lastErrorPath = join(config.dataDir, ".last-error");
128
+ if (existsSync(lastErrorPath)) {
129
+ try {
130
+ const lastError = readFileSync(lastErrorPath, "utf-8").trim();
131
+ lines.push(``, `### Last Startup Error`);
132
+ lines.push(`\`\`\``);
133
+ lines.push(lastError);
134
+ lines.push(`\`\`\``);
135
+ } catch {}
136
+ }
137
+
123
138
  // Health: session-level tool call stats
124
139
  const ts = ctx.toolStats;
125
140
  if (ts) {
@@ -178,4 +193,7 @@ export function handler(_args, ctx) {
178
193
  }
179
194
 
180
195
  return ok(lines.join("\n"));
196
+ } catch (e) {
197
+ return err(e.message, "STATUS_FAILED");
198
+ }
181
199
  }
@@ -105,28 +105,32 @@ export async function handler(
105
105
 
106
106
  let candidates = [];
107
107
 
108
- if (normalizedKinds.length > 0) {
109
- for (const kindFilter of normalizedKinds) {
110
- const rows = await hybridSearch(ctx, topic, {
111
- kindFilter,
112
- limit: Math.ceil(MAX_ENTRIES_FOR_GATHER / normalizedKinds.length),
113
-
108
+ try {
109
+ if (normalizedKinds.length > 0) {
110
+ for (const kindFilter of normalizedKinds) {
111
+ const rows = await hybridSearch(ctx, topic, {
112
+ kindFilter,
113
+ limit: Math.ceil(MAX_ENTRIES_FOR_GATHER / normalizedKinds.length),
114
+
115
+ includeSuperseeded: false,
116
+ });
117
+ candidates.push(...rows);
118
+ }
119
+ const seen = new Set();
120
+ candidates = candidates.filter((r) => {
121
+ if (seen.has(r.id)) return false;
122
+ seen.add(r.id);
123
+ return true;
124
+ });
125
+ } else {
126
+ candidates = await hybridSearch(ctx, topic, {
127
+ limit: MAX_ENTRIES_FOR_GATHER,
128
+
114
129
  includeSuperseeded: false,
115
130
  });
116
- candidates.push(...rows);
117
131
  }
118
- const seen = new Set();
119
- candidates = candidates.filter((r) => {
120
- if (seen.has(r.id)) return false;
121
- seen.add(r.id);
122
- return true;
123
- });
124
- } else {
125
- candidates = await hybridSearch(ctx, topic, {
126
- limit: MAX_ENTRIES_FOR_GATHER,
127
-
128
- includeSuperseeded: false,
129
- });
132
+ } catch (e) {
133
+ return err(e.message, "SEARCH_FAILED");
130
134
  }
131
135
 
132
136
  if (effectiveTags.length) {
@@ -162,22 +166,27 @@ export async function handler(
162
166
 
163
167
  const supersedes = noiseIds.length > 0 ? noiseIds : undefined;
164
168
 
165
- const entry = await captureAndIndex(ctx, {
166
- kind: "brief",
167
- title: `${topic} Context Brief`,
168
- body: briefBody,
169
- tags: briefTags,
170
- source: "create_snapshot",
171
- identity_key: effectiveIdentityKey,
172
- supersedes,
173
-
174
- meta: {
175
- topic,
176
- entry_count: gatherEntries.length,
177
- noise_superseded: noiseIds.length,
178
- synthesized_from: gatherEntries.map((e) => e.id),
179
- },
180
- });
169
+ let entry;
170
+ try {
171
+ entry = await captureAndIndex(ctx, {
172
+ kind: "brief",
173
+ title: `${topic} — Context Brief`,
174
+ body: briefBody,
175
+ tags: briefTags,
176
+ source: "create_snapshot",
177
+ identity_key: effectiveIdentityKey,
178
+ supersedes,
179
+
180
+ meta: {
181
+ topic,
182
+ entry_count: gatherEntries.length,
183
+ noise_superseded: noiseIds.length,
184
+ synthesized_from: gatherEntries.map((e) => e.id),
185
+ },
186
+ });
187
+ } catch (e) {
188
+ return err(e.message, "SAVE_FAILED");
189
+ }
181
190
 
182
191
  const parts = [
183
192
  `✓ Snapshot created → id: ${entry.id}`,
@@ -17,7 +17,6 @@ export const inputSchema = {
17
17
  * @param {import('../types.js').ToolShared} shared
18
18
  */
19
19
  export async function handler({ id }, ctx, { ensureIndexed }) {
20
-
21
20
  if (!id?.trim())
22
21
  return err("Required: id (non-empty string)", "INVALID_INPUT");
23
22
  await ensureIndexed();
@@ -25,34 +24,31 @@ export async function handler({ id }, ctx, { ensureIndexed }) {
25
24
  const entry = ctx.stmts.getEntryById.get(id);
26
25
  if (!entry) return err(`Entry not found: ${id}`, "NOT_FOUND");
27
26
 
28
- // Ownership check: don't leak existence across users
29
- return err(`Entry not found: ${id}`, "NOT_FOUND");
30
- }
31
-
32
- // Delete file from disk first (source of truth)
33
- let fileWarning = null;
34
- if (entry.file_path) {
35
- try {
36
- unlinkSync(entry.file_path);
37
- } catch (e) {
38
- // ENOENT = already gone not an error worth surfacing
39
- if (e.code !== "ENOENT") {
40
- fileWarning = `file could not be removed from disk (${e.code}): ${entry.file_path}`;
27
+ try {
28
+ // Delete DB record first — if this fails, the file stays and no orphan is created
29
+ const rowidResult = ctx.stmts.getRowid.get(id);
30
+ if (rowidResult?.rowid) {
31
+ try {
32
+ ctx.deleteVec(Number(rowidResult.rowid));
33
+ } catch {}
34
+ }
35
+ ctx.stmts.deleteEntry.run(id);
36
+
37
+ // Delete file from disk after successful DB delete
38
+ let fileWarning = null;
39
+ if (entry.file_path) {
40
+ try {
41
+ unlinkSync(entry.file_path);
42
+ } catch (e) {
43
+ if (e.code !== "ENOENT") {
44
+ fileWarning = `file could not be removed from disk (${e.code}): ${entry.file_path}`;
45
+ }
41
46
  }
42
47
  }
43
- }
44
48
 
45
- // Delete vector embedding
46
- const rowidResult = ctx.stmts.getRowid.get(id);
47
- if (rowidResult?.rowid) {
48
- try {
49
- ctx.deleteVec(Number(rowidResult.rowid));
50
- } catch {}
49
+ const msg = `Deleted ${entry.kind}: ${entry.title || "(untitled)"} [${id}]`;
50
+ return ok(fileWarning ? `${msg}\nWarning: ${fileWarning}` : msg);
51
+ } catch (e) {
52
+ return err(e.message, "DELETE_FAILED");
51
53
  }
52
-
53
- // Delete DB row (FTS trigger handles FTS cleanup)
54
- ctx.stmts.deleteEntry.run(id);
55
-
56
- const msg = `Deleted ${entry.kind}: ${entry.title || "(untitled)"} [${id}]`;
57
- return ok(fileWarning ? `${msg}\nWarning: ${fileWarning}` : msg);
58
54
  }
@@ -7,7 +7,7 @@ import { categoryFor } from "@context-vault/core/categories";
7
7
  import { normalizeKind } from "@context-vault/core/files";
8
8
  import { resolveTemporalParams } from "../temporal.js";
9
9
  import { collectLinkedEntries } from "../linking.js";
10
- import { ok, err } from "../helpers.js";
10
+ import { ok, err, errWithHint } from "../helpers.js";
11
11
  import { isEmbedAvailable } from "@context-vault/core/embed";
12
12
 
13
13
  const STALE_DUPLICATE_DAYS = 7;
@@ -290,7 +290,7 @@ export const inputSchema = {
290
290
  .describe(
291
291
  "Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday.",
292
292
  ),
293
- limit: z.number().optional().describe("Max results to return (default 10)"),
293
+ limit: z.number().max(500).optional().describe("Max results to return (default 10)"),
294
294
  include_superseded: z
295
295
  .boolean()
296
296
  .optional()
@@ -305,6 +305,7 @@ export const inputSchema = {
305
305
  ),
306
306
  max_tokens: z
307
307
  .number()
308
+ .max(100000)
308
309
  .optional()
309
310
  .describe(
310
311
  "Limit output to entries that fit within this token budget (rough estimate: 1 token ≈ 4 chars). Entries are packed greedily by relevance rank. At least 1 result is always returned. Response metadata includes tokens_used and tokens_budget.",
@@ -415,16 +416,7 @@ export async function handler(
415
416
  if (identity_key) {
416
417
  if (!kindFilter)
417
418
  return err("identity_key requires kind to be specified", "INVALID_INPUT");
418
- // Local mode: getByIdentityKey takes 2 params (no user_id).
419
- // Hosted mode: 3 params — (kind, identity_key).
420
- const match =
421
- ctx.stmts._mode === "local"
422
- ? ctx.stmts.getByIdentityKey.get(kindFilter, identity_key)
423
- : ctx.stmts.getByIdentityKey.get(
424
- kindFilter,
425
- identity_key,
426
- null,
427
- );
419
+ const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key);
428
420
  if (match) {
429
421
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
430
422
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
@@ -514,9 +506,18 @@ export async function handler(
514
506
  }
515
507
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
516
508
  params.push(fetchLimit);
517
- const rows = ctx.db
518
- .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
519
- .all(...params);
509
+ let rows;
510
+ try {
511
+ rows = ctx.db
512
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
513
+ .all(...params);
514
+ } catch (e) {
515
+ return errWithHint(
516
+ e.message,
517
+ "DB_ERROR",
518
+ "context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
519
+ );
520
+ }
520
521
 
521
522
  // Post-filter by tags if provided, then apply requested limit
522
523
  filtered = effectiveTags.length
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { normalizeKind } from "@context-vault/core/files";
3
3
  import { categoryFor } from "@context-vault/core/categories";
4
- import { ok } from "../helpers.js";
4
+ import { ok, err, errWithHint } from "../helpers.js";
5
+ import { resolveTemporalParams } from "../temporal.js";
5
6
 
6
7
  export const name = "list_context";
7
8
 
@@ -50,6 +51,10 @@ export async function handler(
50
51
 
51
52
  await ensureIndexed();
52
53
 
54
+ const resolved = resolveTemporalParams({ since, until });
55
+ since = resolved.since;
56
+ until = resolved.until;
57
+
53
58
  const kindFilter = kind ? normalizeKind(kind) : null;
54
59
  const effectiveCategory =
55
60
  category || (kindFilter ? categoryFor(kindFilter) : null);
@@ -64,8 +69,6 @@ export async function handler(
64
69
  const clauses = [];
65
70
  const params = [];
66
71
 
67
- if (false) {
68
- }
69
72
  if (kindFilter) {
70
73
  clauses.push("kind = ?");
71
74
  params.push(kindFilter);
@@ -91,16 +94,26 @@ export async function handler(
91
94
  const fetchLimit = tags?.length ? effectiveLimit * 10 : effectiveLimit;
92
95
 
93
96
  const countParams = [...params];
94
- const total = ctx.db
95
- .prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
96
- .get(...countParams).c;
97
-
98
- params.push(fetchLimit, effectiveOffset);
99
- const rows = ctx.db
100
- .prepare(
101
- `SELECT id, title, kind, category, tags, created_at, updated_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
102
- )
103
- .all(...params);
97
+ let total;
98
+ let rows;
99
+ try {
100
+ total = ctx.db
101
+ .prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
102
+ .get(...countParams).c;
103
+
104
+ params.push(fetchLimit, effectiveOffset);
105
+ rows = ctx.db
106
+ .prepare(
107
+ `SELECT id, title, kind, category, tags, created_at, updated_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
108
+ )
109
+ .all(...params);
110
+ } catch (e) {
111
+ return errWithHint(
112
+ e.message,
113
+ "DB_ERROR",
114
+ "context-vault list_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
115
+ );
116
+ }
104
117
 
105
118
  // Post-filter by tags if provided, then apply requested limit
106
119
  const filtered = tags?.length
@@ -3,7 +3,7 @@ import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
3
3
  import { indexEntry } from "@context-vault/core/index";
4
4
  import { categoryFor, defaultTierFor } from "@context-vault/core/categories";
5
5
  import { normalizeKind } from "@context-vault/core/files";
6
- import { ok, err, ensureVaultExists, ensureValidKind } from "../helpers.js";
6
+ import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from "../helpers.js";
7
7
  import { maybeShowFeedbackPrompt } from "../telemetry.js";
8
8
  import { validateRelatedTo } from "../linking.js";
9
9
  import {
@@ -319,12 +319,6 @@ export const inputSchema = {
319
319
  .describe(
320
320
  "Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.",
321
321
  ),
322
- tier: z
323
- .enum(["ephemeral", "working", "durable"])
324
- .optional()
325
- .describe(
326
- "Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified.",
327
- ),
328
322
  dry_run: z
329
323
  .boolean()
330
324
  .optional()
@@ -409,10 +403,6 @@ export async function handler(
409
403
  const existing = ctx.stmts.getEntryById.get(id);
410
404
  if (!existing) return err(`Entry not found: ${id}`, "NOT_FOUND");
411
405
 
412
- // Ownership check: don't leak existence across users
413
- return err(`Entry not found: ${id}`, "NOT_FOUND");
414
- }
415
-
416
406
  if (kind && normalizeKind(kind) !== existing.kind) {
417
407
  return err(
418
408
  `Cannot change kind (current: "${existing.kind}"). Delete and re-create instead.`,
@@ -434,18 +424,27 @@ export async function handler(
434
424
  if (decrypted.meta) existing.meta = JSON.stringify(decrypted.meta);
435
425
  }
436
426
 
437
- const entry = updateEntryFile(ctx, existing, {
438
- title,
439
- body,
440
- tags,
441
- meta,
442
- source,
443
- expires_at,
444
- supersedes,
445
- related_to,
446
- source_files,
447
- });
448
- await indexEntry(ctx, entry);
427
+ let entry;
428
+ try {
429
+ entry = updateEntryFile(ctx, existing, {
430
+ title,
431
+ body,
432
+ tags,
433
+ meta,
434
+ source,
435
+ expires_at,
436
+ supersedes,
437
+ related_to,
438
+ source_files,
439
+ });
440
+ await indexEntry(ctx, entry);
441
+ } catch (e) {
442
+ return errWithHint(
443
+ e.message,
444
+ "UPDATE_FAILED",
445
+ "context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
446
+ );
447
+ }
449
448
  if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
450
449
  ctx.stmts.updateRelatedTo.run(JSON.stringify(entry.related_to), entry.id);
451
450
  } else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
@@ -484,17 +483,22 @@ export async function handler(
484
483
  // ── Similarity check (knowledge + event only) ────────────────────────────
485
484
  const category = categoryFor(normalizedKind);
486
485
  let similarEntries = [];
486
+ let queryEmbedding = null;
487
487
 
488
488
  if (category === "knowledge" || category === "event") {
489
489
  const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
490
490
  const embeddingText = [title, body].filter(Boolean).join(" ");
491
- const queryEmbedding = await ctx.embed(embeddingText);
491
+ try {
492
+ queryEmbedding = await ctx.embed(embeddingText);
493
+ } catch {
494
+ queryEmbedding = null;
495
+ }
492
496
  if (queryEmbedding) {
493
497
  similarEntries = await findSimilar(
494
498
  ctx,
495
499
  queryEmbedding,
496
500
  threshold,
497
-
501
+
498
502
  { hydrate: suggestMode },
499
503
  );
500
504
  }
@@ -540,22 +544,29 @@ export async function handler(
540
544
 
541
545
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);
542
546
 
543
- const entry = await captureAndIndex(ctx, {
544
- kind: normalizedKind,
545
- title,
546
- body,
547
- meta: finalMeta,
548
- tags,
549
- source,
550
- folder,
551
- identity_key,
552
- expires_at,
553
- supersedes,
554
- related_to,
555
- source_files,
556
-
557
- tier: effectiveTier,
558
- });
547
+ const embeddingToReuse = category === "knowledge" ? queryEmbedding : null;
548
+
549
+ let entry;
550
+ try {
551
+ entry = await captureAndIndex(ctx, {
552
+ kind: normalizedKind,
553
+ title,
554
+ body,
555
+ meta: finalMeta,
556
+ tags,
557
+ source,
558
+ folder,
559
+ identity_key,
560
+ expires_at,
561
+ supersedes,
562
+ related_to,
563
+ source_files,
564
+
565
+ tier: effectiveTier,
566
+ }, embeddingToReuse);
567
+ } catch (e) {
568
+ return errWithHint(e.message, "SAVE_FAILED", "context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.");
569
+ }
559
570
 
560
571
  if (ctx.config?.dataDir) {
561
572
  maybeShowFeedbackPrompt(ctx.config.dataDir);
@@ -610,12 +621,8 @@ export async function handler(
610
621
  if (criticalLimit != null) {
611
622
  try {
612
623
  const countRow = ctx.db
613
- .prepare(
614
- false
615
-
616
- : "SELECT COUNT(*) as c FROM vault",
617
- )
618
- .get(...([]));
624
+ .prepare("SELECT COUNT(*) as c FROM vault")
625
+ .get();
619
626
  if (countRow.c >= criticalLimit) {
620
627
  parts.push(
621
628
  ``,