context-vault 2.7.0 → 2.8.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.
@@ -7,13 +7,26 @@
7
7
  */
8
8
 
9
9
  import { createServer } from "node:http";
10
- import { createReadStream, existsSync, statSync, unlinkSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import {
11
+ createReadStream,
12
+ existsSync,
13
+ statSync,
14
+ unlinkSync,
15
+ readFileSync,
16
+ writeFileSync,
17
+ mkdirSync,
18
+ } from "node:fs";
11
19
  import { join, resolve, dirname, extname } from "node:path";
12
20
  import { fileURLToPath } from "node:url";
13
21
  import { homedir, platform } from "node:os";
14
22
  import { execSync } from "node:child_process";
15
23
  import { resolveConfig } from "@context-vault/core/core/config";
16
- import { initDatabase, prepareStatements, insertVec, deleteVec } from "@context-vault/core/index/db";
24
+ import {
25
+ initDatabase,
26
+ prepareStatements,
27
+ insertVec,
28
+ deleteVec,
29
+ } from "@context-vault/core/index/db";
17
30
  import { embed } from "@context-vault/core/index/embed";
18
31
  import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
19
32
  import { indexEntry } from "@context-vault/core/index";
@@ -24,7 +37,12 @@ import { categoryFor } from "@context-vault/core/core/categories";
24
37
  import { parseFile } from "@context-vault/core/capture/importers";
25
38
  import { importEntries } from "@context-vault/core/capture/import-pipeline";
26
39
  import { ingestUrl } from "@context-vault/core/capture/ingest-url";
27
- import { buildLocalManifest, fetchRemoteManifest, computeSyncPlan, executeSync } from "@context-vault/core/sync";
40
+ import {
41
+ buildLocalManifest,
42
+ fetchRemoteManifest,
43
+ computeSyncPlan,
44
+ executeSync,
45
+ } from "@context-vault/core/sync";
28
46
 
29
47
  const __dirname = dirname(fileURLToPath(import.meta.url));
30
48
  const LOCAL_ROOT = resolve(__dirname, "..");
@@ -32,7 +50,9 @@ const LOCAL_ROOT = resolve(__dirname, "..");
32
50
  // Try bundled path first (npm install), then workspace path (local dev)
33
51
  const bundledDist = resolve(LOCAL_ROOT, "app-dist");
34
52
  const workspaceDist = resolve(LOCAL_ROOT, "..", "app", "dist");
35
- const APP_DIST = existsSync(join(bundledDist, "index.html")) ? bundledDist : workspaceDist;
53
+ const APP_DIST = existsSync(join(bundledDist, "index.html"))
54
+ ? bundledDist
55
+ : workspaceDist;
36
56
 
37
57
  const MIME = {
38
58
  ".html": "text/html",
@@ -51,7 +71,11 @@ function formatEntry(row) {
51
71
  title: row.title || null,
52
72
  body: row.body || null,
53
73
  tags: row.tags ? JSON.parse(row.tags) : [],
54
- meta: row.meta ? (typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta) : {},
74
+ meta: row.meta
75
+ ? typeof row.meta === "string"
76
+ ? JSON.parse(row.meta)
77
+ : row.meta
78
+ : {},
55
79
  source: row.source || null,
56
80
  identity_key: row.identity_key || null,
57
81
  expires_at: row.expires_at || null,
@@ -60,17 +84,30 @@ function formatEntry(row) {
60
84
  }
61
85
 
62
86
  function validateEntry(data, { requireKind = true, requireBody = true } = {}) {
63
- if (requireKind && !data.kind) return { error: "kind is required", status: 400 };
64
- if (data.kind && !/^[a-z0-9-]+$/.test(data.kind)) return { error: "kind must be lowercase alphanumeric/hyphens", status: 400 };
65
- if (requireBody && !data.body) return { error: "body is required", status: 400 };
66
- if (data.body && data.body.length > 100 * 1024) return { error: "body max 100KB", status: 400 };
67
- if (categoryFor(data.kind) === "entity" && !data.identity_key) return { error: `Entity kind "${data.kind}" requires identity_key`, status: 400 };
87
+ if (requireKind && !data.kind)
88
+ return { error: "kind is required", status: 400 };
89
+ if (data.kind && !/^[a-z0-9-]+$/.test(data.kind))
90
+ return {
91
+ error: "kind must be lowercase alphanumeric/hyphens",
92
+ status: 400,
93
+ };
94
+ if (requireBody && !data.body)
95
+ return { error: "body is required", status: 400 };
96
+ if (data.body && data.body.length > 100 * 1024)
97
+ return { error: "body max 100KB", status: 400 };
98
+ if (categoryFor(data.kind) === "entity" && !data.identity_key)
99
+ return {
100
+ error: `Entity kind "${data.kind}" requires identity_key`,
101
+ status: 400,
102
+ };
68
103
  return null;
69
104
  }
70
105
 
71
106
  async function main() {
72
107
  const portArg = process.argv.find((a) => a.startsWith("--port="));
73
- const portVal = portArg ? portArg.split("=")[1] : process.argv[process.argv.indexOf("--port") + 1];
108
+ const portVal = portArg
109
+ ? portArg.split("=")[1]
110
+ : process.argv[process.argv.indexOf("--port") + 1];
74
111
  const port = parseInt(portVal || "3141", 10);
75
112
 
76
113
  const config = resolveConfig();
@@ -143,15 +180,18 @@ async function main() {
143
180
  if (os === "darwin") {
144
181
  selected = execSync(
145
182
  `osascript -e 'POSIX path of (choose folder with prompt "Select vault folder")'`,
146
- { encoding: "utf-8", timeout: 30000 }
183
+ { encoding: "utf-8", timeout: 30000 },
147
184
  ).trim();
148
185
  } else if (os === "linux") {
149
186
  selected = execSync(
150
187
  `zenity --file-selection --directory --title="Select vault folder" 2>/dev/null`,
151
- { encoding: "utf-8", timeout: 30000 }
188
+ { encoding: "utf-8", timeout: 30000 },
152
189
  ).trim();
153
190
  } else {
154
- return json({ error: "Folder picker not supported on this platform" }, 501);
191
+ return json(
192
+ { error: "Folder picker not supported on this platform" },
193
+ 501,
194
+ );
155
195
  }
156
196
 
157
197
  if (selected) {
@@ -167,14 +207,33 @@ async function main() {
167
207
  // ─── API: POST /api/local/connect — switch to a local vault folder ───────
168
208
  if (url === "/api/local/connect" && req.method === "POST") {
169
209
  const data = await readBody();
170
- if (!data?.vaultDir?.trim()) return json({ error: "vaultDir is required", code: "INVALID_INPUT" }, 400);
210
+ if (!data?.vaultDir?.trim())
211
+ return json(
212
+ { error: "vaultDir is required", code: "INVALID_INPUT" },
213
+ 400,
214
+ );
171
215
  let vaultPath = data.vaultDir.trim().replace(/^~/, homedir());
172
216
  vaultPath = resolve(vaultPath);
173
- if (!existsSync(vaultPath)) return json({ error: "Vault folder not found", code: "NOT_FOUND" }, 404);
174
- if (!statSync(vaultPath).isDirectory()) return json({ error: "Path is not a directory", code: "INVALID_INPUT" }, 400);
217
+ if (!existsSync(vaultPath))
218
+ return json(
219
+ { error: "Vault folder not found", code: "NOT_FOUND" },
220
+ 404,
221
+ );
222
+ if (!statSync(vaultPath).isDirectory())
223
+ return json(
224
+ { error: "Path is not a directory", code: "INVALID_INPUT" },
225
+ 400,
226
+ );
175
227
  try {
176
- try { state.db.close(); } catch {}
177
- const newConfig = { ...state.config, vaultDir: vaultPath, dbPath: join(vaultPath, ".context-vault.db"), vaultDirExists: true };
228
+ try {
229
+ state.db.close();
230
+ } catch {}
231
+ const newConfig = {
232
+ ...state.config,
233
+ vaultDir: vaultPath,
234
+ dbPath: join(vaultPath, ".context-vault.db"),
235
+ vaultDirExists: true,
236
+ };
178
237
  state.config = newConfig;
179
238
  state.db = await initDatabase(newConfig.dbPath);
180
239
  state.stmts = prepareStatements(state.db);
@@ -188,7 +247,10 @@ async function main() {
188
247
  });
189
248
  } catch (e) {
190
249
  console.error(`[local-server] Connect error: ${e.message}`);
191
- return json({ error: `Failed to connect: ${e.message}`, code: "CONNECT_FAILED" }, 500);
250
+ return json(
251
+ { error: `Failed to connect: ${e.message}`, code: "CONNECT_FAILED" },
252
+ 500,
253
+ );
192
254
  }
193
255
  }
194
256
 
@@ -207,10 +269,16 @@ async function main() {
207
269
  if (url === "/api/billing/usage" && req.method === "GET") {
208
270
  const status = gatherVaultStatus(ctx, {});
209
271
  const total = status.kindCounts.reduce((s, k) => s + k.c, 0);
210
- const storageMb = Math.round((status.dbSizeBytes / (1024 * 1024)) * 100) / 100;
272
+ const storageMb =
273
+ Math.round((status.dbSizeBytes / (1024 * 1024)) * 100) / 100;
211
274
  return json({
212
275
  tier: "free",
213
- limits: { maxEntries: "unlimited", requestsPerDay: "unlimited", storageMb: 1024, exportEnabled: true },
276
+ limits: {
277
+ maxEntries: "unlimited",
278
+ requestsPerDay: "unlimited",
279
+ storageMb: 1024,
280
+ exportEnabled: true,
281
+ },
214
282
  usage: { requestsToday: 0, entriesUsed: total, storageMb },
215
283
  });
216
284
  }
@@ -226,14 +294,24 @@ async function main() {
226
294
  return json({
227
295
  entries: {
228
296
  total: status.kindCounts.reduce((s, k) => s + k.c, 0),
229
- by_kind: Object.fromEntries(status.kindCounts.map((k) => [k.kind, k.c])),
230
- by_category: Object.fromEntries(status.categoryCounts.map((k) => [k.category, k.c])),
297
+ by_kind: Object.fromEntries(
298
+ status.kindCounts.map((k) => [k.kind, k.c]),
299
+ ),
300
+ by_category: Object.fromEntries(
301
+ status.categoryCounts.map((k) => [k.category, k.c]),
302
+ ),
231
303
  },
232
304
  files: { total: status.fileCount, directories: status.subdirs },
233
- database: { size: status.dbSize, size_bytes: status.dbSizeBytes, stale_paths: status.staleCount, expired: status.expiredCount },
305
+ database: {
306
+ size: status.dbSize,
307
+ size_bytes: status.dbSizeBytes,
308
+ stale_paths: status.staleCount,
309
+ expired: status.expiredCount,
310
+ },
234
311
  embeddings: status.embeddingStatus,
235
312
  embed_model_available: status.embedModelAvailable,
236
- health: status.errors.length === 0 && !status.stalePaths ? "ok" : "degraded",
313
+ health:
314
+ status.errors.length === 0 && !status.stalePaths ? "ok" : "degraded",
237
315
  errors: status.errors,
238
316
  });
239
317
  }
@@ -243,13 +321,17 @@ async function main() {
243
321
  const idMatch = url.match(/\/api\/vault\/entries\/([^/]+)$/);
244
322
  if (idMatch) {
245
323
  const entry = stmts.getEntryById.get(idMatch[1]);
246
- if (!entry) return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
324
+ if (!entry)
325
+ return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
247
326
  return json(formatEntry(entry));
248
327
  }
249
328
  const u = new URL(req.url || "", "http://localhost");
250
329
  const kind = u.searchParams.get("kind") || null;
251
330
  const category = u.searchParams.get("category") || null;
252
- const limit = Math.min(parseInt(u.searchParams.get("limit") || "20", 10) || 20, 100);
331
+ const limit = Math.min(
332
+ parseInt(u.searchParams.get("limit") || "20", 10) || 20,
333
+ 100,
334
+ );
253
335
  const offset = parseInt(u.searchParams.get("offset") || "0", 10) || 0;
254
336
  const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
255
337
  const params = [];
@@ -262,37 +344,44 @@ async function main() {
262
344
  params.push(category);
263
345
  }
264
346
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
265
- const total = ctx.db.prepare(`SELECT COUNT(*) as c FROM vault ${where}`).get(...params).c;
266
- const rows = ctx.db.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
347
+ const total = ctx.db
348
+ .prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
349
+ .get(...params).c;
350
+ const rows = ctx.db
351
+ .prepare(
352
+ `SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
353
+ )
354
+ .all(...params, limit, offset);
267
355
  return json({ entries: rows.map(formatEntry), total, limit, offset });
268
356
  }
269
357
 
270
358
  // ─── API: POST /api/vault/entries ────────────────────────────────────────
271
359
  if (url === "/api/vault/entries" && req.method === "POST") {
272
360
  const data = await readBody();
273
- if (!data) return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
361
+ if (!data)
362
+ return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
274
363
  const err = validateEntry(data);
275
- if (err) return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
364
+ if (err)
365
+ return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
276
366
  try {
277
- const entry = await captureAndIndex(
278
- ctx,
279
- {
280
- kind: data.kind,
281
- title: data.title,
282
- body: data.body,
283
- meta: data.meta,
284
- tags: data.tags,
285
- source: data.source || "rest-api",
286
- identity_key: data.identity_key,
287
- expires_at: data.expires_at,
288
- userId: null,
289
- },
290
- indexEntry
291
- );
367
+ const entry = await captureAndIndex(ctx, {
368
+ kind: data.kind,
369
+ title: data.title,
370
+ body: data.body,
371
+ meta: data.meta,
372
+ tags: data.tags,
373
+ source: data.source || "rest-api",
374
+ identity_key: data.identity_key,
375
+ expires_at: data.expires_at,
376
+ userId: null,
377
+ });
292
378
  return json(formatEntry(stmts.getEntryById.get(entry.id)), 201);
293
379
  } catch (e) {
294
380
  console.error(`[local-server] Create error: ${e.message}`);
295
- return json({ error: "Failed to create entry", code: "CREATE_FAILED" }, 500);
381
+ return json(
382
+ { error: "Failed to create entry", code: "CREATE_FAILED" },
383
+ 500,
384
+ );
296
385
  }
297
386
  }
298
387
 
@@ -300,11 +389,17 @@ async function main() {
300
389
  if (url.match(/^\/api\/vault\/entries\/[^/]+$/) && req.method === "PUT") {
301
390
  const id = url.split("/").pop();
302
391
  const data = await readBody();
303
- if (!data) return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
304
- const err = validateEntry(data, { requireKind: false, requireBody: false });
305
- if (err) return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
392
+ if (!data)
393
+ return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
394
+ const err = validateEntry(data, {
395
+ requireKind: false,
396
+ requireBody: false,
397
+ });
398
+ if (err)
399
+ return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
306
400
  const existing = stmts.getEntryById.get(id);
307
- if (!existing) return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
401
+ if (!existing)
402
+ return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
308
403
  try {
309
404
  const entry = updateEntryFile(ctx, existing, {
310
405
  title: data.title,
@@ -318,15 +413,22 @@ async function main() {
318
413
  return json(formatEntry(stmts.getEntryById.get(id)));
319
414
  } catch (e) {
320
415
  console.error(`[local-server] Update error: ${e.message}`);
321
- return json({ error: "Failed to update entry", code: "UPDATE_FAILED" }, 500);
416
+ return json(
417
+ { error: "Failed to update entry", code: "UPDATE_FAILED" },
418
+ 500,
419
+ );
322
420
  }
323
421
  }
324
422
 
325
423
  // ─── API: DELETE /api/vault/entries/:id ──────────────────────────────────
326
- if (url.match(/^\/api\/vault\/entries\/[^/]+$/) && req.method === "DELETE") {
424
+ if (
425
+ url.match(/^\/api\/vault\/entries\/[^/]+$/) &&
426
+ req.method === "DELETE"
427
+ ) {
327
428
  const id = url.split("/").pop();
328
429
  const entry = stmts.getEntryById.get(id);
329
- if (!entry) return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
430
+ if (!entry)
431
+ return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
330
432
  if (entry.file_path) {
331
433
  try {
332
434
  unlinkSync(entry.file_path);
@@ -339,13 +441,19 @@ async function main() {
339
441
  } catch {}
340
442
  }
341
443
  stmts.deleteEntry.run(id);
342
- return json({ deleted: true, id, kind: entry.kind, title: entry.title || null });
444
+ return json({
445
+ deleted: true,
446
+ id,
447
+ kind: entry.kind,
448
+ title: entry.title || null,
449
+ });
343
450
  }
344
451
 
345
452
  // ─── API: POST /api/vault/search ─────────────────────────────────────────
346
453
  if (url === "/api/vault/search" && req.method === "POST") {
347
454
  const data = await readBody();
348
- if (!data || !data.query?.trim()) return json({ error: "query is required", code: "INVALID_INPUT" }, 400);
455
+ if (!data || !data.query?.trim())
456
+ return json({ error: "query is required", code: "INVALID_INPUT" }, 400);
349
457
  const limit = Math.min(parseInt(data.limit || 20, 10) || 20, 100);
350
458
  const offset = parseInt(data.offset || 0, 10) || 0;
351
459
  try {
@@ -357,8 +465,15 @@ async function main() {
357
465
  decayDays: ctx.config.eventDecayDays || 30,
358
466
  userIdFilter: undefined,
359
467
  });
360
- const formatted = results.map((row) => ({ ...formatEntry(row), score: Math.round(row.score * 1000) / 1000 }));
361
- return json({ results: formatted, count: formatted.length, query: data.query });
468
+ const formatted = results.map((row) => ({
469
+ ...formatEntry(row),
470
+ score: Math.round(row.score * 1000) / 1000,
471
+ }));
472
+ return json({
473
+ results: formatted,
474
+ count: formatted.length,
475
+ query: data.query,
476
+ });
362
477
  } catch (e) {
363
478
  console.error(`[local-server] Search error: ${e.message}`);
364
479
  return json({ error: "Search failed", code: "SEARCH_FAILED" }, 500);
@@ -369,28 +484,60 @@ async function main() {
369
484
  if (url === "/api/vault/import/bulk" && req.method === "POST") {
370
485
  const data = await readBody();
371
486
  if (!data || !Array.isArray(data.entries)) {
372
- return json({ error: "Invalid body — expected { entries: [...] }", code: "INVALID_INPUT" }, 400);
487
+ return json(
488
+ {
489
+ error: "Invalid body — expected { entries: [...] }",
490
+ code: "INVALID_INPUT",
491
+ },
492
+ 400,
493
+ );
373
494
  }
374
495
  if (data.entries.length > 500) {
375
- return json({ error: "Maximum 500 entries per request", code: "LIMIT_EXCEEDED" }, 400);
496
+ return json(
497
+ { error: "Maximum 500 entries per request", code: "LIMIT_EXCEEDED" },
498
+ 400,
499
+ );
376
500
  }
377
501
 
378
- const result = await importEntries(ctx, data.entries, { source: "bulk-import" });
379
- return json({ imported: result.imported, failed: result.failed, errors: result.errors.slice(0, 10).map((e) => e.error) });
502
+ const result = await importEntries(ctx, data.entries, {
503
+ source: "bulk-import",
504
+ });
505
+ return json({
506
+ imported: result.imported,
507
+ failed: result.failed,
508
+ errors: result.errors.slice(0, 10).map((e) => e.error),
509
+ });
380
510
  }
381
511
 
382
512
  // ─── API: POST /api/vault/import/file — Import from file content ────────
383
513
  if (url === "/api/vault/import/file" && req.method === "POST") {
384
514
  const data = await readBody();
385
515
  if (!data?.filename || !data?.content) {
386
- return json({ error: "filename and content are required", code: "INVALID_INPUT" }, 400);
516
+ return json(
517
+ { error: "filename and content are required", code: "INVALID_INPUT" },
518
+ 400,
519
+ );
387
520
  }
388
521
 
389
- const entries = parseFile(data.filename, data.content, { kind: data.kind, source: data.source || "file-import" });
390
- if (!entries.length) return json({ imported: 0, failed: 0, errors: ["No entries parsed from file"] });
522
+ const entries = parseFile(data.filename, data.content, {
523
+ kind: data.kind,
524
+ source: data.source || "file-import",
525
+ });
526
+ if (!entries.length)
527
+ return json({
528
+ imported: 0,
529
+ failed: 0,
530
+ errors: ["No entries parsed from file"],
531
+ });
391
532
 
392
- const result = await importEntries(ctx, entries, { source: data.source || "file-import" });
393
- return json({ imported: result.imported, failed: result.failed, errors: result.errors.slice(0, 10).map((e) => e.error) });
533
+ const result = await importEntries(ctx, entries, {
534
+ source: data.source || "file-import",
535
+ });
536
+ return json({
537
+ imported: result.imported,
538
+ failed: result.failed,
539
+ errors: result.errors.slice(0, 10).map((e) => e.error),
540
+ });
394
541
  }
395
542
 
396
543
  // ─── API: GET /api/vault/export — Export all entries ─────────────────────
@@ -398,14 +545,27 @@ async function main() {
398
545
  const u = new URL(req.url || "", "http://localhost");
399
546
  const format = u.searchParams.get("format") || "json";
400
547
 
401
- const rows = ctx.db.prepare(
402
- "SELECT * FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC"
403
- ).all();
548
+ const rows = ctx.db
549
+ .prepare(
550
+ "SELECT * FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC",
551
+ )
552
+ .all();
404
553
 
405
554
  const entries = rows.map(formatEntry);
406
555
 
407
556
  if (format === "csv") {
408
- const headers = ["id", "kind", "category", "title", "body", "tags", "source", "identity_key", "expires_at", "created_at"];
557
+ const headers = [
558
+ "id",
559
+ "kind",
560
+ "category",
561
+ "title",
562
+ "body",
563
+ "tags",
564
+ "source",
565
+ "identity_key",
566
+ "expires_at",
567
+ "created_at",
568
+ ];
409
569
  const csvLines = [headers.join(",")];
410
570
  for (const e of entries) {
411
571
  const row = headers.map((h) => {
@@ -420,34 +580,57 @@ async function main() {
420
580
  });
421
581
  csvLines.push(row.join(","));
422
582
  }
423
- res.writeHead(200, { "Content-Type": "text/csv", "Access-Control-Allow-Origin": "*" });
583
+ res.writeHead(200, {
584
+ "Content-Type": "text/csv",
585
+ "Access-Control-Allow-Origin": "*",
586
+ });
424
587
  res.end(csvLines.join("\n"));
425
588
  return;
426
589
  }
427
590
 
428
- return json({ entries, total: entries.length, exported_at: new Date().toISOString() });
591
+ return json({
592
+ entries,
593
+ total: entries.length,
594
+ exported_at: new Date().toISOString(),
595
+ });
429
596
  }
430
597
 
431
598
  // ─── API: POST /api/vault/ingest — Fetch URL and save as entry ──────────
432
599
  if (url === "/api/vault/ingest" && req.method === "POST") {
433
600
  const data = await readBody();
434
- if (!data?.url) return json({ error: "url is required", code: "INVALID_INPUT" }, 400);
601
+ if (!data?.url)
602
+ return json({ error: "url is required", code: "INVALID_INPUT" }, 400);
435
603
 
436
604
  try {
437
- const entry = await ingestUrl(data.url, { kind: data.kind, tags: data.tags });
438
- const result = await captureAndIndex(ctx, entry, indexEntry);
605
+ const entry = await ingestUrl(data.url, {
606
+ kind: data.kind,
607
+ tags: data.tags,
608
+ });
609
+ const result = await captureAndIndex(ctx, entry);
439
610
  return json(formatEntry(ctx.stmts.getEntryById.get(result.id)), 201);
440
611
  } catch (e) {
441
- return json({ error: `Ingestion failed: ${e.message}`, code: "INGEST_FAILED" }, 500);
612
+ return json(
613
+ { error: `Ingestion failed: ${e.message}`, code: "INGEST_FAILED" },
614
+ 500,
615
+ );
442
616
  }
443
617
  }
444
618
 
445
619
  // ─── API: GET /api/vault/manifest — Lightweight entry list for sync ─────
446
620
  if (url === "/api/vault/manifest" && req.method === "GET") {
447
- const rows = ctx.db.prepare(
448
- "SELECT id, kind, title, created_at FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC"
449
- ).all();
450
- return json({ entries: rows.map((r) => ({ id: r.id, kind: r.kind, title: r.title || null, created_at: r.created_at })) });
621
+ const rows = ctx.db
622
+ .prepare(
623
+ "SELECT id, kind, title, created_at FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC",
624
+ )
625
+ .all();
626
+ return json({
627
+ entries: rows.map((r) => ({
628
+ id: r.id,
629
+ kind: r.kind,
630
+ title: r.title || null,
631
+ created_at: r.created_at,
632
+ })),
633
+ });
451
634
  }
452
635
 
453
636
  // ─── API: GET /api/local/link — Get link status ─────────────────────────
@@ -456,7 +639,9 @@ async function main() {
456
639
  const configPath = join(dataDir, "config.json");
457
640
  let storedConfig = {};
458
641
  if (existsSync(configPath)) {
459
- try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
642
+ try {
643
+ storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
644
+ } catch {}
460
645
  }
461
646
  return json({
462
647
  linked: !!storedConfig.apiKey,
@@ -475,7 +660,9 @@ async function main() {
475
660
 
476
661
  let storedConfig = {};
477
662
  if (existsSync(configPath)) {
478
- try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
663
+ try {
664
+ storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
665
+ } catch {}
479
666
  }
480
667
 
481
668
  if (!data?.apiKey) {
@@ -508,9 +695,16 @@ async function main() {
508
695
  mkdirSync(dataDir, { recursive: true });
509
696
  writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
510
697
 
511
- return json({ linked: true, email: user.email, tier: user.tier || "free" });
698
+ return json({
699
+ linked: true,
700
+ email: user.email,
701
+ tier: user.tier || "free",
702
+ });
512
703
  } catch (e) {
513
- return json({ error: `Verification failed: ${e.message}`, code: "AUTH_FAILED" }, 401);
704
+ return json(
705
+ { error: `Verification failed: ${e.message}`, code: "AUTH_FAILED" },
706
+ 401,
707
+ );
514
708
  }
515
709
  }
516
710
 
@@ -520,16 +714,24 @@ async function main() {
520
714
  const configPath = join(dataDir, "config.json");
521
715
  let storedConfig = {};
522
716
  if (existsSync(configPath)) {
523
- try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
717
+ try {
718
+ storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
719
+ } catch {}
524
720
  }
525
721
 
526
722
  if (!storedConfig.apiKey) {
527
- return json({ error: "Not linked. Use link endpoint first.", code: "NOT_LINKED" }, 400);
723
+ return json(
724
+ { error: "Not linked. Use link endpoint first.", code: "NOT_LINKED" },
725
+ 400,
726
+ );
528
727
  }
529
728
 
530
729
  try {
531
730
  const local = buildLocalManifest(ctx);
532
- const remote = await fetchRemoteManifest(storedConfig.hostedUrl, storedConfig.apiKey);
731
+ const remote = await fetchRemoteManifest(
732
+ storedConfig.hostedUrl,
733
+ storedConfig.apiKey,
734
+ );
533
735
  const plan = computeSyncPlan(local, remote);
534
736
  const result = await executeSync(ctx, {
535
737
  hostedUrl: storedConfig.hostedUrl,
@@ -538,12 +740,18 @@ async function main() {
538
740
  });
539
741
  return json(result);
540
742
  } catch (e) {
541
- return json({ error: `Sync failed: ${e.message}`, code: "SYNC_FAILED" }, 500);
743
+ return json(
744
+ { error: `Sync failed: ${e.message}`, code: "SYNC_FAILED" },
745
+ 500,
746
+ );
542
747
  }
543
748
  }
544
749
 
545
750
  // ─── Static files ───────────────────────────────────────────────────────
546
- const filePath = join(APP_DIST, url === "/" ? "index.html" : url.replace(/^\//, ""));
751
+ const filePath = join(
752
+ APP_DIST,
753
+ url === "/" ? "index.html" : url.replace(/^\//, ""),
754
+ );
547
755
  if (!filePath.startsWith(APP_DIST)) {
548
756
  res.writeHead(403);
549
757
  res.end();
@@ -572,11 +780,15 @@ async function main() {
572
780
  });
573
781
 
574
782
  process.on("SIGINT", () => {
575
- try { state.db.close(); } catch {}
783
+ try {
784
+ state.db.close();
785
+ } catch {}
576
786
  process.exit(0);
577
787
  });
578
788
  process.on("SIGTERM", () => {
579
- try { state.db.close(); } catch {}
789
+ try {
790
+ state.db.close();
791
+ } catch {}
580
792
  process.exit(0);
581
793
  });
582
794
  }