context-vault 2.17.0 → 3.0.1

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.
Files changed (110) hide show
  1. package/bin/cli.js +783 -108
  2. package/node_modules/@context-vault/core/dist/capture.d.ts +21 -0
  3. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -0
  4. package/node_modules/@context-vault/core/dist/capture.js +269 -0
  5. package/node_modules/@context-vault/core/dist/capture.js.map +1 -0
  6. package/node_modules/@context-vault/core/dist/categories.d.ts +6 -0
  7. package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -0
  8. package/node_modules/@context-vault/core/dist/categories.js +50 -0
  9. package/node_modules/@context-vault/core/dist/categories.js.map +1 -0
  10. package/node_modules/@context-vault/core/dist/config.d.ts +4 -0
  11. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -0
  12. package/node_modules/@context-vault/core/dist/config.js +190 -0
  13. package/node_modules/@context-vault/core/dist/config.js.map +1 -0
  14. package/node_modules/@context-vault/core/dist/constants.d.ts +33 -0
  15. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -0
  16. package/node_modules/@context-vault/core/dist/constants.js +23 -0
  17. package/node_modules/@context-vault/core/dist/constants.js.map +1 -0
  18. package/node_modules/@context-vault/core/dist/db.d.ts +13 -0
  19. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -0
  20. package/node_modules/@context-vault/core/dist/db.js +191 -0
  21. package/node_modules/@context-vault/core/dist/db.js.map +1 -0
  22. package/node_modules/@context-vault/core/dist/embed.d.ts +5 -0
  23. package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -0
  24. package/node_modules/@context-vault/core/dist/embed.js +78 -0
  25. package/node_modules/@context-vault/core/dist/embed.js.map +1 -0
  26. package/node_modules/@context-vault/core/dist/files.d.ts +13 -0
  27. package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -0
  28. package/node_modules/@context-vault/core/dist/files.js +66 -0
  29. package/node_modules/@context-vault/core/dist/files.js.map +1 -0
  30. package/node_modules/@context-vault/core/dist/formatters.d.ts +8 -0
  31. package/node_modules/@context-vault/core/dist/formatters.d.ts.map +1 -0
  32. package/node_modules/@context-vault/core/dist/formatters.js +18 -0
  33. package/node_modules/@context-vault/core/dist/formatters.js.map +1 -0
  34. package/node_modules/@context-vault/core/dist/frontmatter.d.ts +12 -0
  35. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -0
  36. package/node_modules/@context-vault/core/dist/frontmatter.js +101 -0
  37. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -0
  38. package/node_modules/@context-vault/core/dist/index.d.ts +10 -0
  39. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -0
  40. package/node_modules/@context-vault/core/dist/index.js +297 -0
  41. package/node_modules/@context-vault/core/dist/index.js.map +1 -0
  42. package/node_modules/@context-vault/core/dist/ingest-url.d.ts +20 -0
  43. package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -0
  44. package/node_modules/@context-vault/core/dist/ingest-url.js +113 -0
  45. package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -0
  46. package/node_modules/@context-vault/core/dist/main.d.ts +14 -0
  47. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -0
  48. package/node_modules/@context-vault/core/dist/main.js +25 -0
  49. package/node_modules/@context-vault/core/dist/main.js.map +1 -0
  50. package/node_modules/@context-vault/core/dist/search.d.ts +18 -0
  51. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -0
  52. package/node_modules/@context-vault/core/dist/search.js +238 -0
  53. package/node_modules/@context-vault/core/dist/search.js.map +1 -0
  54. package/node_modules/@context-vault/core/dist/types.d.ts +176 -0
  55. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -0
  56. package/node_modules/@context-vault/core/dist/types.js +2 -0
  57. package/node_modules/@context-vault/core/dist/types.js.map +1 -0
  58. package/node_modules/@context-vault/core/package.json +66 -16
  59. package/node_modules/@context-vault/core/src/capture.ts +308 -0
  60. package/node_modules/@context-vault/core/src/categories.ts +54 -0
  61. package/node_modules/@context-vault/core/src/{core/config.js → config.ts} +34 -33
  62. package/node_modules/@context-vault/core/src/{constants.js → constants.ts} +6 -3
  63. package/node_modules/@context-vault/core/src/db.ts +229 -0
  64. package/node_modules/@context-vault/core/src/{index/embed.js → embed.ts} +10 -35
  65. package/node_modules/@context-vault/core/src/files.ts +80 -0
  66. package/node_modules/@context-vault/core/src/{capture/formatters.js → formatters.ts} +13 -11
  67. package/node_modules/@context-vault/core/src/{core/frontmatter.js → frontmatter.ts} +27 -33
  68. package/node_modules/@context-vault/core/src/index.ts +351 -0
  69. package/node_modules/@context-vault/core/src/ingest-url.ts +99 -0
  70. package/node_modules/@context-vault/core/src/main.ts +111 -0
  71. package/node_modules/@context-vault/core/src/search.ts +285 -0
  72. package/node_modules/@context-vault/core/src/types.ts +166 -0
  73. package/package.json +12 -7
  74. package/scripts/postinstall.js +1 -1
  75. package/{node_modules/@context-vault/core/src/core → src}/error-log.js +1 -15
  76. package/{node_modules/@context-vault/core/src/server → src}/helpers.js +9 -4
  77. package/src/linking.js +100 -0
  78. package/{node_modules/@context-vault/core/src/server/tools.js → src/register-tools.js} +14 -15
  79. package/src/{server/index.js → server.js} +8 -35
  80. package/src/status.js +235 -0
  81. package/{node_modules/@context-vault/core/src/core → src}/telemetry.js +9 -19
  82. package/src/temporal.js +97 -0
  83. package/{node_modules/@context-vault/core/src/server → src}/tools/context-status.js +3 -4
  84. package/{node_modules/@context-vault/core/src/server → src}/tools/create-snapshot.js +43 -75
  85. package/{node_modules/@context-vault/core/src/server → src}/tools/delete-context.js +0 -2
  86. package/{node_modules/@context-vault/core/src/server → src}/tools/get-context.js +118 -35
  87. package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-project.js +5 -6
  88. package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-url.js +3 -4
  89. package/{node_modules/@context-vault/core/src/server → src}/tools/list-buckets.js +4 -5
  90. package/{node_modules/@context-vault/core/src/server → src}/tools/list-context.js +3 -6
  91. package/{node_modules/@context-vault/core/src/server → src}/tools/save-context.js +41 -21
  92. package/{node_modules/@context-vault/core/src/server → src}/tools/session-start.js +9 -16
  93. package/node_modules/@context-vault/core/src/capture/file-ops.js +0 -97
  94. package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -46
  95. package/node_modules/@context-vault/core/src/capture/importers.js +0 -387
  96. package/node_modules/@context-vault/core/src/capture/index.js +0 -236
  97. package/node_modules/@context-vault/core/src/capture/ingest-url.js +0 -252
  98. package/node_modules/@context-vault/core/src/consolidation/index.js +0 -112
  99. package/node_modules/@context-vault/core/src/core/categories.js +0 -72
  100. package/node_modules/@context-vault/core/src/core/files.js +0 -108
  101. package/node_modules/@context-vault/core/src/core/status.js +0 -350
  102. package/node_modules/@context-vault/core/src/index/db.js +0 -416
  103. package/node_modules/@context-vault/core/src/index/index.js +0 -522
  104. package/node_modules/@context-vault/core/src/index.js +0 -66
  105. package/node_modules/@context-vault/core/src/retrieve/index.js +0 -500
  106. package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +0 -55
  107. package/node_modules/@context-vault/core/src/sync/sync.js +0 -235
  108. package/src/hooks/post-tool-call.mjs +0 -62
  109. package/src/hooks/session-end.mjs +0 -492
  110. /package/{node_modules/@context-vault/core/src/server → src}/tools/clear-context.js +0 -0
package/bin/cli.js CHANGED
@@ -34,7 +34,7 @@ const HOME = homedir();
34
34
 
35
35
  const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
36
36
  const VERSION = pkg.version;
37
- const SERVER_PATH = resolve(ROOT, "src", "server", "index.js");
37
+ const SERVER_PATH = resolve(ROOT, "src", "server.js");
38
38
 
39
39
  /** Detect if running as an npm-installed package (global or local) vs local dev clone */
40
40
  function isInstalledPackage() {
@@ -282,7 +282,7 @@ function printDetectionResults(results) {
282
282
  }
283
283
  }
284
284
 
285
- function showHelp() {
285
+ function showHelp(showAll = false) {
286
286
  console.log(`
287
287
  ${bold("◇ context-vault")} ${dim(`v${VERSION}`)}
288
288
  ${dim("Persistent memory for AI agents")}
@@ -293,37 +293,50 @@ ${bold("Usage:")}
293
293
  ${dim("No command → runs setup (first time) or shows status (existing vault)")}
294
294
 
295
295
  ${bold("Commands:")}
296
- ${cyan("setup")} Interactive MCP server installer
297
- ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
298
- ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
299
- ${cyan("serve")} Start the MCP server (used by AI clients)
300
- ${cyan("hooks")} install|uninstall Install or remove Claude Code memory hook
301
- ${cyan("claude")} install|uninstall Alias for hooks install|uninstall
302
- ${cyan("skills")} install Install bundled Claude Code skills
303
- ${cyan("health")} Quick health check — vault, DB, entry count
304
- ${cyan("restart")} Stop running MCP server processes (client auto-restarts)
305
- ${cyan("flush")} Check vault health and confirm DB is accessible
306
- ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
307
- ${cyan("session-capture")} Save a session summary entry (reads JSON from stdin)
308
- ${cyan("session-end")} Run session-end hook (parse transcript + capture)
309
- ${cyan("post-tool-call")} Run post-tool-call hook (log tool usage)
310
- ${cyan("save")} Save an entry to the vault from CLI
311
- ${cyan("search")} Search vault entries from CLI
312
- ${cyan("reindex")} Rebuild search index from knowledge files
313
- ${cyan("prune")} Remove expired entries (use --dry-run to preview)
314
- ${cyan("status")} Show vault diagnostics
315
- ${cyan("doctor")} Diagnose and repair common issues
316
- ${cyan("update")} Check for and install updates
317
- ${cyan("uninstall")} Remove MCP configs and optionally data
318
- ${cyan("import")} <path> Import entries from file or directory
319
- ${cyan("export")} Export vault to JSON or CSV
320
- ${cyan("ingest")} <url> Fetch URL and save as vault entry
321
- ${cyan("ingest-project")} <path> Scan project directory and register as project entity
322
- ${cyan("migrate")} Migrate vault between local and hosted
323
- ${cyan("consolidate")} Find hot tags and cold entries for maintenance
324
-
325
- ${bold("Options:")}
296
+ ${cyan("setup")} Interactive MCP server installer
297
+ ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
298
+ ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
299
+ ${cyan("serve")} Start the MCP server (used by AI clients)
300
+ ${cyan("hooks")} install|uninstall Install or remove Claude Code memory hook
301
+ ${cyan("claude")} install|uninstall Alias for hooks install|uninstall
302
+ ${cyan("skills")} install Install bundled Claude Code skills
303
+ ${cyan("health")} Quick health check — vault, DB, entry count
304
+ ${cyan("status")} Show vault diagnostics
305
+ ${cyan("doctor")} Diagnose and repair common issues
306
+ ${cyan("restart")} Stop running MCP server processes (client auto-restarts)
307
+ ${cyan("search")} Search vault entries from CLI
308
+ ${cyan("save")} Save an entry to the vault from CLI
309
+ ${cyan("import")} <path> Import entries from file, directory, or .zip archive
310
+ ${cyan("export")} Export vault entries (JSON, CSV, or portable ZIP)
311
+ ${cyan("ingest")} <url> Fetch URL and save as vault entry
312
+ ${cyan("ingest-project")} <path> Scan project directory and register as project entity
313
+ ${cyan("reindex")} Rebuild search index from knowledge files
314
+ ${cyan("migrate-dirs")} [--dry-run] Rename plural vault dirs to singular (post-2.18.0)
315
+ ${cyan("archive")} Archive old ephemeral/event entries (use --dry-run to preview)
316
+ ${cyan("restore")} <id> Restore an archived entry back into the vault
317
+ ${cyan("prune")} Remove expired entries (use --dry-run to preview)
318
+ ${cyan("update")} Check for and install updates
319
+ ${cyan("uninstall")} Remove MCP configs and optionally data
320
+ `);
321
+
322
+ if (showAll) {
323
+ console.log(`${bold("Plumbing")} ${dim("(internal hook implementations and maintenance utilities):")}
324
+ ${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
325
+ ${cyan("session-capture")} Save a session summary entry (reads JSON from stdin)
326
+ ${cyan("session-end")} Run session-end hook (parse transcript + capture)
327
+ ${cyan("post-tool-call")} Run post-tool-call hook (log tool usage)
328
+ ${cyan("flush")} Check vault health and confirm DB is accessible
329
+ ${cyan("consolidate")} Find hot tags and cold entries for maintenance
330
+ ${cyan("migrate")} Migrate vault between local and hosted
331
+ `);
332
+ } else {
333
+ console.log(` ${dim("Run")} ${dim("context-vault --help --all")} ${dim("to show internal plumbing commands.")}
334
+ `);
335
+ }
336
+
337
+ console.log(`${bold("Options:")}
326
338
  --help Show this help
339
+ --help --all Show all commands including internal plumbing
327
340
  --version Show version
328
341
  --vault-dir <path> Set vault directory (setup/serve)
329
342
  --yes Non-interactive mode (accept all defaults)
@@ -361,6 +374,26 @@ async function runSetup() {
361
374
  } catch {}
362
375
 
363
376
  if (latestVersion === VERSION) {
377
+ // Even when "up to date", ensure the launcher points to a valid server
378
+ const dataDir = join(HOME, ".context-mcp");
379
+ const launcherPath = join(dataDir, "server.mjs");
380
+ let launcherOk = false;
381
+ if (existsSync(launcherPath)) {
382
+ const content = readFileSync(launcherPath, "utf-8");
383
+ const m = content.match(/import "(.+?)"/);
384
+ if (m && existsSync(m[1])) launcherOk = true;
385
+ }
386
+ if (!launcherOk && !isNpx()) {
387
+ mkdirSync(dataDir, { recursive: true });
388
+ writeFileSync(launcherPath, `import "${SERVER_PATH}";\n`);
389
+ console.log(
390
+ green(` ✓ context-vault v${VERSION} is up to date`) +
391
+ dim(` (vault: ${existingVault})`),
392
+ );
393
+ console.log(dim(` ↳ Repaired server launcher → ${SERVER_PATH}`));
394
+ console.log();
395
+ return;
396
+ }
364
397
  console.log(
365
398
  green(` ✓ context-vault v${VERSION} is up to date`) +
366
399
  dim(` (vault: ${existingVault})`),
@@ -788,7 +821,7 @@ async function runSetup() {
788
821
  }, 100);
789
822
 
790
823
  try {
791
- const { embed } = await import("@context-vault/core/index/embed");
824
+ const { embed } = await import("@context-vault/core/embed");
792
825
  let timeoutHandle;
793
826
  const timeout = new Promise((_, reject) => {
794
827
  timeoutHandle = setTimeout(
@@ -1023,7 +1056,7 @@ async function runSetup() {
1023
1056
  // Verify DB is accessible
1024
1057
  let dbAccessible = false;
1025
1058
  try {
1026
- const { initDatabase } = await import("@context-vault/core/index/db");
1059
+ const { initDatabase } = await import("@context-vault/core/db");
1027
1060
  const db = await initDatabase(vaultConfig.dbPath);
1028
1061
  db.prepare("SELECT 1").get();
1029
1062
  db.close();
@@ -1566,7 +1599,7 @@ async function runSwitch() {
1566
1599
  if (target === "local") {
1567
1600
  const launcherPath = join(dataDir, "server.mjs");
1568
1601
  if (!existsSync(launcherPath)) {
1569
- const serverAbs = resolve(ROOT, "src", "server", "index.js");
1602
+ const serverAbs = resolve(ROOT, "src", "server.js");
1570
1603
  mkdirSync(dataDir, { recursive: true });
1571
1604
  writeFileSync(launcherPath, `import "${serverAbs}";\n`);
1572
1605
  }
@@ -1662,10 +1695,10 @@ async function runSwitch() {
1662
1695
  async function runReindex() {
1663
1696
  console.log(dim("Loading vault..."));
1664
1697
 
1665
- const { resolveConfig } = await import("@context-vault/core/core/config");
1698
+ const { resolveConfig } = await import("@context-vault/core/config");
1666
1699
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
1667
- await import("@context-vault/core/index/db");
1668
- const { embed } = await import("@context-vault/core/index/embed");
1700
+ await import("@context-vault/core/db");
1701
+ const { embed } = await import("@context-vault/core/embed");
1669
1702
  const { reindex } = await import("@context-vault/core/index");
1670
1703
 
1671
1704
  const config = resolveConfig();
@@ -1696,12 +1729,86 @@ async function runReindex() {
1696
1729
  console.log(` ${dim("·")} ${stats.unchanged} unchanged`);
1697
1730
  }
1698
1731
 
1732
+ async function runMigrateDirs() {
1733
+ const dryRun = flags.has("--dry-run");
1734
+
1735
+ // Vault dir: positional arg (skip --flags), or fall back to configured vault
1736
+ const positional = args.slice(1).find((a) => !a.startsWith("--"));
1737
+ let vaultDir = positional;
1738
+
1739
+ if (!vaultDir) {
1740
+ const { resolveConfig } = await import("@context-vault/core/config");
1741
+ const config = resolveConfig();
1742
+ if (!config.vaultDirExists) {
1743
+ console.error(red(`Vault directory not found: ${config.vaultDir}`));
1744
+ console.error("Run " + cyan("context-vault setup") + " to configure.");
1745
+ process.exit(1);
1746
+ }
1747
+ vaultDir = config.vaultDir;
1748
+ }
1749
+
1750
+ if (!existsSync(vaultDir) || !statSync(vaultDir).isDirectory()) {
1751
+ console.error(red(`Error: ${vaultDir} is not a directory`));
1752
+ process.exit(1);
1753
+ }
1754
+
1755
+ const { planMigration, executeMigration } =
1756
+ await import("@context-vault/core/migrate-dirs");
1757
+
1758
+ const ops = planMigration(vaultDir);
1759
+
1760
+ if (ops.length === 0) {
1761
+ console.log(green("✓ No plural directories found — vault is up to date."));
1762
+ return;
1763
+ }
1764
+
1765
+ if (dryRun) {
1766
+ console.log(dim("Dry run — no files will be moved.\n"));
1767
+ }
1768
+
1769
+ for (const op of ops) {
1770
+ const fileLabel = `${op.fileCount} ${op.fileCount === 1 ? "file" : "files"}`;
1771
+ const actionLabel = op.action === "rename" ? "RENAME" : "MERGE";
1772
+ const suffix = dryRun ? dim(" [dry-run]") : "";
1773
+ console.log(
1774
+ ` ${cyan(actionLabel)}: ${op.pluralName}/ → ${op.singularName}/ (${fileLabel})${suffix}`,
1775
+ );
1776
+ }
1777
+
1778
+ if (dryRun) {
1779
+ console.log();
1780
+ console.log(
1781
+ dim(
1782
+ ` ${ops.length} ${ops.length === 1 ? "directory" : "directories"} would be renamed/merged.`,
1783
+ ),
1784
+ );
1785
+ console.log(dim(" Remove --dry-run to apply."));
1786
+ return;
1787
+ }
1788
+
1789
+ const { renamed, merged, errors } = executeMigration(ops);
1790
+
1791
+ console.log();
1792
+ if (renamed > 0) console.log(green(`✓ Renamed: ${renamed}`));
1793
+ if (merged > 0) console.log(green(`✓ Merged: ${merged}`));
1794
+ if (errors.length > 0) {
1795
+ for (const e of errors) console.log(red(` ✗ ${e}`));
1796
+ }
1797
+
1798
+ if (renamed + merged > 0) {
1799
+ console.log();
1800
+ console.log(
1801
+ dim("Run `context-vault reindex` to rebuild the search index."),
1802
+ );
1803
+ }
1804
+ }
1805
+
1699
1806
  async function runPrune() {
1700
1807
  const dryRun = flags.has("--dry-run");
1701
1808
 
1702
- const { resolveConfig } = await import("@context-vault/core/core/config");
1809
+ const { resolveConfig } = await import("@context-vault/core/config");
1703
1810
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
1704
- await import("@context-vault/core/index/db");
1811
+ await import("@context-vault/core/db");
1705
1812
  const { pruneExpired } = await import("@context-vault/core/index");
1706
1813
 
1707
1814
  const config = resolveConfig();
@@ -1759,12 +1866,169 @@ async function runPrune() {
1759
1866
  }
1760
1867
  }
1761
1868
 
1869
+ async function runArchive() {
1870
+ const dryRun = flags.has("--dry-run");
1871
+
1872
+ const { resolveConfig } = await import("@context-vault/core/config");
1873
+ const { initDatabase, prepareStatements, insertVec, deleteVec } =
1874
+ await import("@context-vault/core/db");
1875
+ const { findArchiveCandidates, archiveEntries } =
1876
+ await import("@context-vault/core/archive");
1877
+
1878
+ const config = resolveConfig();
1879
+ if (!config.vaultDirExists) {
1880
+ console.error(red(`Vault directory not found: ${config.vaultDir}`));
1881
+ console.error("Run " + cyan("context-vault setup") + " to configure.");
1882
+ process.exit(1);
1883
+ }
1884
+
1885
+ const db = await initDatabase(config.dbPath);
1886
+
1887
+ if (dryRun) {
1888
+ const ctx = {
1889
+ db,
1890
+ config,
1891
+ stmts: prepareStatements(db),
1892
+ embed: async () => null,
1893
+ insertVec: () => {},
1894
+ deleteVec: () => {},
1895
+ };
1896
+ const candidates = findArchiveCandidates(ctx);
1897
+ db.close();
1898
+
1899
+ if (candidates.length === 0) {
1900
+ console.log(green(" No entries eligible for archiving."));
1901
+ const lifecycle = config.lifecycle || {};
1902
+ console.log(dim("\n Retention windows:"));
1903
+ for (const [tier, rules] of Object.entries(lifecycle)) {
1904
+ if (rules?.archiveAfterDays) {
1905
+ console.log(dim(` ${tier}: archive after ${rules.archiveAfterDays} days`));
1906
+ }
1907
+ }
1908
+ return;
1909
+ }
1910
+
1911
+ console.log(
1912
+ `\n ${bold(String(candidates.length))} ${candidates.length === 1 ? "entry" : "entries"} eligible for archiving:\n`,
1913
+ );
1914
+ for (const e of candidates) {
1915
+ const label = e.title ? `${e.kind}: ${e.title}` : `${e.kind} (${e.id})`;
1916
+ const age = e.updated_at || e.created_at;
1917
+ console.log(
1918
+ ` ${dim("-")} ${label} ${dim(`(tier=${e.tier}, last updated ${age})`)}`,
1919
+ );
1920
+ }
1921
+ console.log(dim("\n Dry run — no entries were archived."));
1922
+ console.log(dim(" Remove --dry-run to archive."));
1923
+ return;
1924
+ }
1925
+
1926
+ const stmts = prepareStatements(db);
1927
+ const ctx = {
1928
+ db,
1929
+ config,
1930
+ stmts,
1931
+ embed: async () => null,
1932
+ insertVec: (r, e) => insertVec(stmts, r, e),
1933
+ deleteVec: (r) => deleteVec(stmts, r),
1934
+ };
1935
+
1936
+ const result = await archiveEntries(ctx);
1937
+ db.close();
1938
+
1939
+ if (result.count === 0) {
1940
+ console.log(green(" No entries eligible for archiving."));
1941
+ } else {
1942
+ console.log(
1943
+ green(
1944
+ ` ✓ Archived ${result.count} ${result.count === 1 ? "entry" : "entries"} to _archive/`,
1945
+ ),
1946
+ );
1947
+ console.log(
1948
+ dim(` Files moved to: ${join(config.vaultDir, "_archive")}`),
1949
+ );
1950
+ console.log(
1951
+ dim(" Restore with: context-vault restore <id>"),
1952
+ );
1953
+ }
1954
+ }
1955
+
1956
+ async function runRestore() {
1957
+ const entryId = args[1];
1958
+
1959
+ if (!entryId || entryId.startsWith("--")) {
1960
+ const { resolveConfig } = await import("@context-vault/core/config");
1961
+ const { listArchivedEntries } =
1962
+ await import("@context-vault/core/archive");
1963
+
1964
+ const config = resolveConfig();
1965
+
1966
+ console.log(`\n ${bold("context-vault restore")} <id>\n`);
1967
+ console.log(` Restore an archived entry back into the active vault.\n`);
1968
+
1969
+ if (config.vaultDirExists) {
1970
+ const entries = listArchivedEntries(config.vaultDir);
1971
+ if (entries.length > 0) {
1972
+ console.log(` ${bold("Archived entries")} (${entries.length}):\n`);
1973
+ for (const e of entries.slice(0, 20)) {
1974
+ const label = e.title
1975
+ ? `${e.kind}: ${e.title.slice(0, 60)}`
1976
+ : `${e.kind} (${e.id})`;
1977
+ console.log(` ${dim(e.id || "?")} ${label}`);
1978
+ }
1979
+ if (entries.length > 20) {
1980
+ console.log(dim(`\n ... and ${entries.length - 20} more`));
1981
+ }
1982
+ } else {
1983
+ console.log(dim(" No archived entries found."));
1984
+ }
1985
+ }
1986
+ console.log();
1987
+ return;
1988
+ }
1989
+
1990
+ const { resolveConfig } = await import("@context-vault/core/config");
1991
+ const { initDatabase, prepareStatements, insertVec, deleteVec } =
1992
+ await import("@context-vault/core/db");
1993
+ const { embed } = await import("@context-vault/core/embed");
1994
+ const { restoreEntry } = await import("@context-vault/core/archive");
1995
+
1996
+ const config = resolveConfig();
1997
+ if (!config.vaultDirExists) {
1998
+ console.error(red(`Vault directory not found: ${config.vaultDir}`));
1999
+ console.error("Run " + cyan("context-vault setup") + " to configure.");
2000
+ process.exit(1);
2001
+ }
2002
+
2003
+ const db = await initDatabase(config.dbPath);
2004
+ const stmts = prepareStatements(db);
2005
+ const ctx = {
2006
+ db,
2007
+ config,
2008
+ stmts,
2009
+ embed,
2010
+ insertVec: (r, e) => insertVec(stmts, r, e),
2011
+ deleteVec: (r) => deleteVec(stmts, r),
2012
+ };
2013
+
2014
+ const result = await restoreEntry(ctx, entryId);
2015
+ db.close();
2016
+
2017
+ if (result.restored) {
2018
+ console.log(green(` ✓ Restored ${result.kind} entry: ${result.id}`));
2019
+ console.log(dim(` File: ${result.filePath}`));
2020
+ } else {
2021
+ console.error(red(` ✗ ${result.reason}`));
2022
+ process.exit(1);
2023
+ }
2024
+ }
2025
+
1762
2026
  async function runStatus() {
1763
- const { resolveConfig } = await import("@context-vault/core/core/config");
1764
- const { initDatabase } = await import("@context-vault/core/index/db");
1765
- const { gatherVaultStatus } = await import("@context-vault/core/core/status");
2027
+ const { resolveConfig } = await import("@context-vault/core/config");
2028
+ const { initDatabase } = await import("@context-vault/core/db");
2029
+ const { gatherVaultStatus } = await import("../src/status.js");
1766
2030
  const { errorLogPath, errorLogCount } =
1767
- await import("@context-vault/core/core/error-log");
2031
+ await import("../src/error-log.js");
1768
2032
 
1769
2033
  const config = resolveConfig();
1770
2034
 
@@ -1827,7 +2091,7 @@ async function runStatus() {
1827
2091
  const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
1828
2092
  const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
1829
2093
  const countStr = String(c).padStart(4);
1830
- console.log(` ${countStr} ${kind}s ${dim(bar)}`);
2094
+ console.log(` ${dim(bar)} ${countStr} ${kind}s`);
1831
2095
  }
1832
2096
  } else {
1833
2097
  console.log(`\n ${dim("(empty — no entries indexed)")}`);
@@ -1853,6 +2117,15 @@ async function runStatus() {
1853
2117
  }
1854
2118
  }
1855
2119
 
2120
+ if (status.archivedCount > 0) {
2121
+ console.log();
2122
+ console.log(
2123
+ dim(
2124
+ ` ${status.archivedCount} archived ${status.archivedCount === 1 ? "entry" : "entries"} in _archive/ (excluded from search)`,
2125
+ ),
2126
+ );
2127
+ }
2128
+
1856
2129
  if (status.stalePaths) {
1857
2130
  console.log();
1858
2131
  console.log(yellow(" Stale paths detected in DB."));
@@ -2044,7 +2317,7 @@ async function runMigrate() {
2044
2317
  return;
2045
2318
  }
2046
2319
 
2047
- const { resolveConfig } = await import("@context-vault/core/core/config");
2320
+ const { resolveConfig } = await import("@context-vault/core/config");
2048
2321
  const config = resolveConfig();
2049
2322
 
2050
2323
  if (direction === "to-hosted") {
@@ -2098,20 +2371,33 @@ async function runImport() {
2098
2371
  const target = args[1];
2099
2372
  if (!target) {
2100
2373
  console.log(`\n ${bold("context-vault import")} <path>\n`);
2101
- console.log(` Import entries from a file or directory.\n`);
2102
- console.log(` Supported formats: .md, .csv, .tsv, .json, .txt\n`);
2374
+ console.log(` Import entries from a file, directory, or portable archive.\n`);
2375
+ console.log(` Supported formats: .md, .csv, .tsv, .json, .txt, .zip\n`);
2103
2376
  console.log(` Options:`);
2104
2377
  console.log(` --kind <kind> Default kind (default: insight)`);
2105
2378
  console.log(` --source <src> Default source (default: cli-import)`);
2106
2379
  console.log(` --dry-run Show parsed entries without importing`);
2380
+ console.log(` --vault <path> Target vault directory (default: configured vault)`);
2107
2381
  console.log();
2108
2382
  return;
2109
2383
  }
2110
2384
 
2111
- const { resolveConfig } = await import("@context-vault/core/core/config");
2385
+ const dryRun = flags.has("--dry-run");
2386
+ const targetPath = resolve(target);
2387
+
2388
+ if (!existsSync(targetPath)) {
2389
+ console.error(red(` Path not found: ${targetPath}`));
2390
+ process.exit(1);
2391
+ }
2392
+
2393
+ if (targetPath.endsWith(".zip")) {
2394
+ return runImportZip(targetPath, dryRun);
2395
+ }
2396
+
2397
+ const { resolveConfig } = await import("@context-vault/core/config");
2112
2398
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
2113
- await import("@context-vault/core/index/db");
2114
- const { embed } = await import("@context-vault/core/index/embed");
2399
+ await import("@context-vault/core/db");
2400
+ const { embed } = await import("@context-vault/core/embed");
2115
2401
  const { parseFile, parseDirectory } =
2116
2402
  await import("@context-vault/core/capture/importers");
2117
2403
  const { importEntries } =
@@ -2120,13 +2406,6 @@ async function runImport() {
2120
2406
 
2121
2407
  const kind = getFlag("--kind") || undefined;
2122
2408
  const source = getFlag("--source") || "cli-import";
2123
- const dryRun = flags.has("--dry-run");
2124
-
2125
- const targetPath = resolve(target);
2126
- if (!existsSync(targetPath)) {
2127
- console.error(red(` Path not found: ${targetPath}`));
2128
- process.exit(1);
2129
- }
2130
2409
 
2131
2410
  const stat = statSync(targetPath);
2132
2411
  let entries;
@@ -2197,17 +2476,215 @@ async function runImport() {
2197
2476
  console.log();
2198
2477
  }
2199
2478
 
2479
+ async function runImportZip(zipPath, dryRun) {
2480
+ const AdmZip = (await import("adm-zip")).default;
2481
+ const { resolveConfig } = await import("@context-vault/core/config");
2482
+ const { initDatabase, prepareStatements, insertVec, deleteVec } =
2483
+ await import("@context-vault/core/db");
2484
+ const { embed } = await import("@context-vault/core/embed");
2485
+ const { indexEntry } = await import("@context-vault/core/index");
2486
+ const { parseFrontmatter } = await import("@context-vault/core/frontmatter");
2487
+ const { categoryDirFor } = await import("@context-vault/core/categories");
2488
+ const { mkdirSync, writeFileSync, existsSync: existsFn } = await import("node:fs");
2489
+ const { join: joinPath, basename: baseName } = await import("node:path");
2490
+
2491
+ let zip;
2492
+ try {
2493
+ zip = new AdmZip(zipPath);
2494
+ } catch (e) {
2495
+ console.error(red(`\n Failed to open archive: ${e.message}\n`));
2496
+ process.exit(1);
2497
+ }
2498
+
2499
+ const manifestEntry = zip.getEntry("manifest.json");
2500
+ if (!manifestEntry) {
2501
+ console.error(red("\n Invalid archive: missing manifest.json\n"));
2502
+ process.exit(1);
2503
+ }
2504
+
2505
+ let manifest;
2506
+ try {
2507
+ manifest = JSON.parse(zip.readAsText("manifest.json"));
2508
+ } catch {
2509
+ console.error(red("\n Invalid archive: corrupt manifest.json\n"));
2510
+ process.exit(1);
2511
+ }
2512
+
2513
+ const indexEntry_ = zip.getEntry("index.json");
2514
+ if (!indexEntry_) {
2515
+ console.error(red("\n Invalid archive: missing index.json\n"));
2516
+ process.exit(1);
2517
+ }
2518
+
2519
+ let index;
2520
+ try {
2521
+ index = JSON.parse(zip.readAsText("index.json"));
2522
+ } catch {
2523
+ console.error(red("\n Invalid archive: corrupt index.json\n"));
2524
+ process.exit(1);
2525
+ }
2526
+
2527
+ const entries = index.entries || [];
2528
+ if (entries.length === 0) {
2529
+ console.log(yellow("\n Archive contains no entries.\n"));
2530
+ return;
2531
+ }
2532
+
2533
+ console.log(`\n ${bold("◇ context-vault import")} ${dim(baseName(zipPath))}`);
2534
+ console.log(dim(` Archive: v${manifest.version} · ${manifest.entry_count} entries · ${manifest.context_vault_version || "?"}`));
2535
+
2536
+ const kindCounts = {};
2537
+ for (const e of entries) {
2538
+ kindCounts[e.kind] = (kindCounts[e.kind] || 0) + 1;
2539
+ }
2540
+ console.log();
2541
+ for (const [k, count] of Object.entries(kindCounts).sort((a, b) => b[1] - a[1])) {
2542
+ console.log(` ${k}: ${count}`);
2543
+ }
2544
+
2545
+ const vaultDirOverride = getFlag("--vault");
2546
+ const config = (await import("@context-vault/core/config")).resolveConfig();
2547
+ const targetVaultDir = vaultDirOverride ? resolve(vaultDirOverride) : config.vaultDir;
2548
+
2549
+ if (!existsFn(targetVaultDir)) {
2550
+ console.error(red(`\n Vault directory not found: ${targetVaultDir}`));
2551
+ console.error(` Run ${cyan("context-vault setup")} to configure.`);
2552
+ process.exit(1);
2553
+ }
2554
+
2555
+ const db = await initDatabase(config.dbPath);
2556
+ const stmts = prepareStatements(db);
2557
+ const ctx = {
2558
+ db,
2559
+ config: { ...config, vaultDir: targetVaultDir },
2560
+ stmts,
2561
+ embed,
2562
+ insertVec: (r, e) => insertVec(stmts, r, e),
2563
+ deleteVec: (r) => deleteVec(stmts, r),
2564
+ };
2565
+
2566
+ const existingIds = new Set();
2567
+ const allIds = db.prepare("SELECT id FROM vault").all();
2568
+ for (const row of allIds) existingIds.add(row.id);
2569
+
2570
+ let imported = 0;
2571
+ let skippedDuplicate = 0;
2572
+ let skippedMissing = 0;
2573
+ let failed = 0;
2574
+ const errors = [];
2575
+
2576
+ if (dryRun) {
2577
+ for (let i = 0; i < Math.min(entries.length, 25); i++) {
2578
+ const e = entries[i];
2579
+ const isDuplicate = existingIds.has(e.id);
2580
+ const tagStr = e.tags?.length ? ` ${dim(`[${e.tags.join(", ")}]`)}` : "";
2581
+ const statusIcon = isDuplicate ? yellow("~") : green("+");
2582
+ const statusText = isDuplicate ? dim(" (duplicate, would skip)") : "";
2583
+ console.log(`\n ${statusIcon} ${dim(`[${i + 1}]`)} ${e.kind} — ${e.title || e.id}${tagStr}${statusText}`);
2584
+ }
2585
+ if (entries.length > 25) {
2586
+ console.log(dim(`\n ... and ${entries.length - 25} more`));
2587
+ }
2588
+ const wouldSkip = entries.filter((e) => existingIds.has(e.id)).length;
2589
+ console.log(`\n ${dim(`Would import ${entries.length - wouldSkip}, skip ${wouldSkip} duplicates.`)}`);
2590
+ console.log(dim(" Dry run — no entries were imported.\n"));
2591
+ db.close();
2592
+ return;
2593
+ }
2594
+
2595
+ for (let i = 0; i < entries.length; i++) {
2596
+ const entryMeta = entries[i];
2597
+ process.stdout.write(`\r Importing... ${i + 1}/${entries.length}`);
2598
+
2599
+ if (existingIds.has(entryMeta.id)) {
2600
+ skippedDuplicate++;
2601
+ continue;
2602
+ }
2603
+
2604
+ const zipEntry = zip.getEntry(entryMeta.file);
2605
+ if (!zipEntry) {
2606
+ skippedMissing++;
2607
+ continue;
2608
+ }
2609
+
2610
+ const mdContent = zip.readAsText(entryMeta.file);
2611
+ const { meta: fmMeta, body: rawBody } = parseFrontmatter(mdContent);
2612
+
2613
+ const kind = entryMeta.kind || fmMeta.kind || "insight";
2614
+ const categoryDir = categoryDirFor(kind);
2615
+ const targetDir = joinPath(targetVaultDir, categoryDir, kind);
2616
+
2617
+ try {
2618
+ mkdirSync(targetDir, { recursive: true });
2619
+
2620
+ const fileName = baseName(entryMeta.file);
2621
+ const filePath = joinPath(targetDir, fileName);
2622
+ writeFileSync(filePath, mdContent);
2623
+
2624
+ const id = fmMeta.id || entryMeta.id;
2625
+ const tags = Array.isArray(fmMeta.tags) ? fmMeta.tags : entryMeta.tags || [];
2626
+ const title = fmMeta.title || entryMeta.title || null;
2627
+ const source = fmMeta.source || entryMeta.source || "archive-import";
2628
+ const identity_key = fmMeta.identity_key || entryMeta.identity_key || null;
2629
+ const expires_at = fmMeta.expires_at || entryMeta.expires_at || null;
2630
+ const createdAt = fmMeta.created || entryMeta.created_at || new Date().toISOString();
2631
+
2632
+ await indexEntry(ctx, {
2633
+ id,
2634
+ kind,
2635
+ category: entryMeta.category || undefined,
2636
+ title,
2637
+ body: rawBody,
2638
+ meta: null,
2639
+ tags,
2640
+ source,
2641
+ filePath,
2642
+ createdAt,
2643
+ identity_key,
2644
+ expires_at,
2645
+ });
2646
+
2647
+ imported++;
2648
+ } catch (e) {
2649
+ failed++;
2650
+ errors.push({ id: entryMeta.id, error: e.message });
2651
+ }
2652
+ }
2653
+
2654
+ db.close();
2655
+
2656
+ console.log(`\r ${green("✓")} Import complete `);
2657
+ console.log(` ${green("+")} ${imported} imported`);
2658
+ if (skippedDuplicate > 0) {
2659
+ console.log(` ${dim("~")} ${skippedDuplicate} skipped (already exist)`);
2660
+ }
2661
+ if (skippedMissing > 0) {
2662
+ console.log(` ${yellow("!")} ${skippedMissing} skipped (file missing in archive)`);
2663
+ }
2664
+ if (failed > 0) {
2665
+ console.log(` ${red("x")} ${failed} failed`);
2666
+ for (const e of errors.slice(0, 5)) {
2667
+ console.log(` ${dim(e.error)}`);
2668
+ }
2669
+ }
2670
+ console.log();
2671
+ }
2672
+
2200
2673
  async function runExport() {
2201
2674
  const format = getFlag("--format") || "json";
2202
- const output = getFlag("--output");
2675
+ const output = getFlag("--output") || getFlag("-o");
2203
2676
  const rawPageSize = getFlag("--page-size");
2204
2677
  const pageSize = rawPageSize
2205
2678
  ? Math.max(1, parseInt(rawPageSize, 10) || 100)
2206
2679
  : null;
2207
2680
 
2208
- const { resolveConfig } = await import("@context-vault/core/core/config");
2681
+ if (format === "zip") {
2682
+ return runExportZip();
2683
+ }
2684
+
2685
+ const { resolveConfig } = await import("@context-vault/core/config");
2209
2686
  const { initDatabase, prepareStatements } =
2210
- await import("@context-vault/core/index/db");
2687
+ await import("@context-vault/core/db");
2211
2688
  const { writeFileSync } = await import("node:fs");
2212
2689
 
2213
2690
  const config = resolveConfig();
@@ -2223,7 +2700,6 @@ async function runExport() {
2223
2700
 
2224
2701
  let entries;
2225
2702
  if (pageSize) {
2226
- // Paginated: fetch in chunks to avoid loading everything into memory
2227
2703
  entries = [];
2228
2704
  let offset = 0;
2229
2705
  const stmt = db.prepare(
@@ -2293,6 +2769,188 @@ async function runExport() {
2293
2769
  }
2294
2770
  }
2295
2771
 
2772
+ async function runExportZip() {
2773
+ const output = getFlag("--output") || getFlag("-o");
2774
+ const dryRun = flags.has("--dry-run");
2775
+ const tagsRaw = getFlag("--tags");
2776
+ const kindRaw = getFlag("--kind");
2777
+ const since = getFlag("--since");
2778
+ const until = getFlag("--until");
2779
+ const exportAll = flags.has("--all");
2780
+
2781
+ const tagsFilter = tagsRaw
2782
+ ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean)
2783
+ : null;
2784
+ const kindFilter = kindRaw
2785
+ ? kindRaw.split(",").map((k) => k.trim()).filter(Boolean)
2786
+ : null;
2787
+
2788
+ if (!exportAll && !tagsFilter && !kindFilter && !since && !until) {
2789
+ console.log(`\n ${bold("context-vault export --format zip")} [options]\n`);
2790
+ console.log(` Export vault entries as a portable ZIP archive.\n`);
2791
+ console.log(` ${bold("Filters (at least one required, or use --all):")}`);
2792
+ console.log(` --tags <t1,t2> Filter by tags (comma-separated)`);
2793
+ console.log(` --kind <k1,k2> Filter by kind (comma-separated)`);
2794
+ console.log(` --since <YYYY-MM-DD> Entries created on or after date`);
2795
+ console.log(` --until <YYYY-MM-DD> Entries created on or before date`);
2796
+ console.log(` --all Export all entries\n`);
2797
+ console.log(` ${bold("Options:")}`);
2798
+ console.log(` --output, -o <path> Output file path`);
2799
+ console.log(` --dry-run Show what would be exported\n`);
2800
+ console.log(` ${bold("Examples:")}`);
2801
+ console.log(` context-vault export --tags stormfors --format zip -o stormfors.zip`);
2802
+ console.log(` context-vault export --kind decision,pattern --format zip`);
2803
+ console.log(` context-vault export --since 2026-01-01 --until 2026-02-28 --format zip`);
2804
+ console.log(` context-vault export --all --format zip --dry-run\n`);
2805
+ return;
2806
+ }
2807
+
2808
+ const { resolveConfig } = await import("@context-vault/core/config");
2809
+ const { initDatabase } = await import("@context-vault/core/db");
2810
+ const { readFileSync: readFs, existsSync: existsFn } = await import("node:fs");
2811
+ const { basename } = await import("node:path");
2812
+
2813
+ const config = resolveConfig();
2814
+ if (!config.vaultDirExists) {
2815
+ console.error(red(` Vault directory not found: ${config.vaultDir}`));
2816
+ process.exit(1);
2817
+ }
2818
+
2819
+ const db = await initDatabase(config.dbPath);
2820
+
2821
+ const conditions = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
2822
+ const params = [];
2823
+
2824
+ if (tagsFilter) {
2825
+ const tagClauses = tagsFilter.map(() =>
2826
+ "EXISTS (SELECT 1 FROM json_each(vault.tags) WHERE json_each.value = ?)"
2827
+ );
2828
+ conditions.push(`(${tagClauses.join(" OR ")})`);
2829
+ params.push(...tagsFilter);
2830
+ }
2831
+
2832
+ if (kindFilter) {
2833
+ const placeholders = kindFilter.map(() => "?").join(", ");
2834
+ conditions.push(`kind IN (${placeholders})`);
2835
+ params.push(...kindFilter);
2836
+ }
2837
+
2838
+ if (since) {
2839
+ conditions.push("created_at >= ?");
2840
+ params.push(since.includes("T") ? since : `${since}T00:00:00.000Z`);
2841
+ }
2842
+
2843
+ if (until) {
2844
+ conditions.push("created_at <= ?");
2845
+ params.push(until.includes("T") ? until : `${until}T23:59:59.999Z`);
2846
+ }
2847
+
2848
+ const sql = `SELECT * FROM vault WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
2849
+ const rows = db.prepare(sql).all(...params);
2850
+ db.close();
2851
+
2852
+ if (rows.length === 0) {
2853
+ console.log(yellow("\n No entries match the given filters.\n"));
2854
+ return;
2855
+ }
2856
+
2857
+ const kindCounts = {};
2858
+ for (const row of rows) {
2859
+ kindCounts[row.kind] = (kindCounts[row.kind] || 0) + 1;
2860
+ }
2861
+
2862
+ console.log(`\n ${bold(String(rows.length))} entries match filters:\n`);
2863
+ for (const [k, count] of Object.entries(kindCounts).sort((a, b) => b[1] - a[1])) {
2864
+ console.log(` ${k}: ${count}`);
2865
+ }
2866
+
2867
+ const earliest = rows[rows.length - 1]?.created_at;
2868
+ const latest = rows[0]?.created_at;
2869
+ console.log(dim(`\n Date range: ${earliest?.slice(0, 10) || "?"} → ${latest?.slice(0, 10) || "?"}`));
2870
+
2871
+ if (dryRun) {
2872
+ console.log();
2873
+ for (let i = 0; i < Math.min(rows.length, 25); i++) {
2874
+ const r = rows[i];
2875
+ const tags = safeJsonParse(r.tags, []);
2876
+ const tagStr = tags.length ? ` ${dim(`[${tags.join(", ")}]`)}` : "";
2877
+ console.log(` ${dim(`[${i + 1}]`)} ${r.kind} — ${r.title || (r.body || "").slice(0, 60)}${tagStr}`);
2878
+ }
2879
+ if (rows.length > 25) {
2880
+ console.log(dim(` ... and ${rows.length - 25} more`));
2881
+ }
2882
+ console.log(dim("\n Dry run — no archive created.\n"));
2883
+ return;
2884
+ }
2885
+
2886
+ const AdmZip = (await import("adm-zip")).default;
2887
+ const zip = new AdmZip();
2888
+
2889
+ const indexEntries = [];
2890
+ let filesSkipped = 0;
2891
+
2892
+ for (const row of rows) {
2893
+ const entryPath = `entries/${row.kind}/${basename(row.file_path || `${row.id}.md`)}`;
2894
+
2895
+ let fileContent = null;
2896
+ if (row.file_path && existsFn(row.file_path)) {
2897
+ fileContent = readFs(row.file_path);
2898
+ }
2899
+
2900
+ if (!fileContent) {
2901
+ filesSkipped++;
2902
+ continue;
2903
+ }
2904
+
2905
+ zip.addFile(entryPath, fileContent);
2906
+
2907
+ indexEntries.push({
2908
+ id: row.id,
2909
+ kind: row.kind,
2910
+ category: row.category,
2911
+ title: row.title || null,
2912
+ tags: safeJsonParse(row.tags, []),
2913
+ source: row.source || null,
2914
+ identity_key: row.identity_key || null,
2915
+ expires_at: row.expires_at || null,
2916
+ created_at: row.created_at,
2917
+ file: entryPath,
2918
+ });
2919
+ }
2920
+
2921
+ const manifest = {
2922
+ version: 1,
2923
+ created_at: new Date().toISOString(),
2924
+ context_vault_version: VERSION,
2925
+ entry_count: indexEntries.length,
2926
+ date_range: { earliest, latest },
2927
+ filters: {
2928
+ tags: tagsFilter || null,
2929
+ kind: kindFilter || null,
2930
+ since: since || null,
2931
+ until: until || null,
2932
+ all: exportAll || false,
2933
+ },
2934
+ };
2935
+
2936
+ zip.addFile("manifest.json", Buffer.from(JSON.stringify(manifest, null, 2)));
2937
+ zip.addFile("index.json", Buffer.from(JSON.stringify({ entries: indexEntries }, null, 2)));
2938
+
2939
+ const today = new Date().toISOString().slice(0, 10);
2940
+ const defaultName = tagsFilter
2941
+ ? `vault-${tagsFilter[0]}-${today}.zip`
2942
+ : `vault-export-${today}.zip`;
2943
+ const outputPath = resolve(output || defaultName);
2944
+
2945
+ zip.writeZip(outputPath);
2946
+
2947
+ console.log(`\n ${green("✓")} Exported ${indexEntries.length} entries to ${outputPath}`);
2948
+ if (filesSkipped > 0) {
2949
+ console.log(yellow(` ⚠ ${filesSkipped} entries skipped (file not found on disk)`));
2950
+ }
2951
+ console.log();
2952
+ }
2953
+
2296
2954
  function safeJsonParse(str, fallback) {
2297
2955
  if (!str) return fallback;
2298
2956
  try {
@@ -2361,10 +3019,10 @@ async function runIngest() {
2361
3019
  return;
2362
3020
  }
2363
3021
 
2364
- const { resolveConfig } = await import("@context-vault/core/core/config");
3022
+ const { resolveConfig } = await import("@context-vault/core/config");
2365
3023
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
2366
- await import("@context-vault/core/index/db");
2367
- const { embed } = await import("@context-vault/core/index/embed");
3024
+ await import("@context-vault/core/db");
3025
+ const { embed } = await import("@context-vault/core/embed");
2368
3026
  const { captureAndIndex } = await import("@context-vault/core/capture");
2369
3027
 
2370
3028
  const config = resolveConfig();
@@ -2428,10 +3086,10 @@ async function runIngestProject() {
2428
3086
 
2429
3087
  console.log(dim(` Scanning ${projectPath}...`));
2430
3088
 
2431
- const { resolveConfig } = await import("@context-vault/core/core/config");
3089
+ const { resolveConfig } = await import("@context-vault/core/config");
2432
3090
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
2433
- await import("@context-vault/core/index/db");
2434
- const { embed } = await import("@context-vault/core/index/embed");
3091
+ await import("@context-vault/core/db");
3092
+ const { embed } = await import("@context-vault/core/embed");
2435
3093
  const { captureAndIndex } = await import("@context-vault/core/capture");
2436
3094
  const { existsSync: fsExists, readFileSync: fsRead } =
2437
3095
  await import("node:fs");
@@ -2642,21 +3300,21 @@ async function runRecall() {
2642
3300
 
2643
3301
  let db;
2644
3302
  try {
2645
- const { resolveConfig } = await import("@context-vault/core/core/config");
3303
+ const { resolveConfig } = await import("@context-vault/core/config");
2646
3304
  const config = resolveConfig();
2647
3305
 
2648
3306
  if (!config.vaultDirExists) return;
2649
3307
 
2650
3308
  const { initDatabase, prepareStatements } =
2651
- await import("@context-vault/core/index/db");
2652
- const { embed } = await import("@context-vault/core/index/embed");
3309
+ await import("@context-vault/core/db");
3310
+ const { embed } = await import("@context-vault/core/embed");
2653
3311
  const { hybridSearch } = await import("@context-vault/core/retrieve/index");
2654
3312
 
2655
3313
  db = await initDatabase(config.dbPath);
2656
3314
  const stmts = prepareStatements(db);
2657
3315
  const ctx = { db, config, stmts, embed };
2658
3316
 
2659
- const { categoryFor } = await import("@context-vault/core/core/categories");
3317
+ const { categoryFor } = await import("@context-vault/core/categories");
2660
3318
  const recall = config.recall;
2661
3319
 
2662
3320
  const results = await hybridSearch(ctx, query, {
@@ -2696,8 +3354,8 @@ async function runRecall() {
2696
3354
  }
2697
3355
 
2698
3356
  async function runFlush() {
2699
- const { resolveConfig } = await import("@context-vault/core/core/config");
2700
- const { initDatabase } = await import("@context-vault/core/index/db");
3357
+ const { resolveConfig } = await import("@context-vault/core/config");
3358
+ const { initDatabase } = await import("@context-vault/core/db");
2701
3359
 
2702
3360
  let db;
2703
3361
  try {
@@ -2743,12 +3401,12 @@ async function runSessionCapture() {
2743
3401
  }
2744
3402
  const { kind, title, body, tags, source } = payload;
2745
3403
  if (!kind || !body) return;
2746
- const { resolveConfig } = await import("@context-vault/core/core/config");
3404
+ const { resolveConfig } = await import("@context-vault/core/config");
2747
3405
  const config = resolveConfig();
2748
3406
  if (!config.vaultDirExists) return;
2749
3407
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
2750
- await import("@context-vault/core/index/db");
2751
- const { embed } = await import("@context-vault/core/index/embed");
3408
+ await import("@context-vault/core/db");
3409
+ const { embed } = await import("@context-vault/core/embed");
2752
3410
  const { captureAndIndex } = await import("@context-vault/core/capture");
2753
3411
  db = await initDatabase(config.dbPath);
2754
3412
  const stmts = prepareStatements(db);
@@ -2827,7 +3485,7 @@ async function runSave() {
2827
3485
 
2828
3486
  let db;
2829
3487
  try {
2830
- const { resolveConfig } = await import("@context-vault/core/core/config");
3488
+ const { resolveConfig } = await import("@context-vault/core/config");
2831
3489
  const config = resolveConfig();
2832
3490
  if (!config.vaultDirExists) {
2833
3491
  console.error(
@@ -2836,8 +3494,8 @@ async function runSave() {
2836
3494
  process.exit(1);
2837
3495
  }
2838
3496
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
2839
- await import("@context-vault/core/index/db");
2840
- const { embed } = await import("@context-vault/core/index/embed");
3497
+ await import("@context-vault/core/db");
3498
+ const { embed } = await import("@context-vault/core/embed");
2841
3499
  const { captureAndIndex } = await import("@context-vault/core/capture");
2842
3500
  db = await initDatabase(config.dbPath);
2843
3501
  const stmts = prepareStatements(db);
@@ -2881,6 +3539,7 @@ async function runSearch() {
2881
3539
  const sort = getFlag("--sort") || "relevance";
2882
3540
  const format = getFlag("--format") || "plain";
2883
3541
  const showFull = flags.has("--full");
3542
+ const scopeArg = getFlag("--scope"); // "hot" | "events" | "all"
2884
3543
 
2885
3544
  const valuedFlags = new Set([
2886
3545
  "--kind",
@@ -2888,6 +3547,7 @@ async function runSearch() {
2888
3547
  "--limit",
2889
3548
  "--sort",
2890
3549
  "--format",
3550
+ "--scope",
2891
3551
  ]);
2892
3552
 
2893
3553
  const queryParts = [];
@@ -2909,7 +3569,7 @@ async function runSearch() {
2909
3569
 
2910
3570
  let db;
2911
3571
  try {
2912
- const { resolveConfig } = await import("@context-vault/core/core/config");
3572
+ const { resolveConfig } = await import("@context-vault/core/config");
2913
3573
  const config = resolveConfig();
2914
3574
  if (!config.vaultDirExists) {
2915
3575
  console.error(red("No vault found. Run: context-vault setup"));
@@ -2917,8 +3577,8 @@ async function runSearch() {
2917
3577
  }
2918
3578
 
2919
3579
  const { initDatabase, prepareStatements } =
2920
- await import("@context-vault/core/index/db");
2921
- const { embed } = await import("@context-vault/core/index/embed");
3580
+ await import("@context-vault/core/db");
3581
+ const { embed } = await import("@context-vault/core/embed");
2922
3582
  const { hybridSearch } = await import("@context-vault/core/retrieve/index");
2923
3583
 
2924
3584
  db = await initDatabase(config.dbPath);
@@ -2927,8 +3587,18 @@ async function runSearch() {
2927
3587
 
2928
3588
  let results;
2929
3589
 
3590
+ // Resolve scope → category/exclude filter
3591
+ const validScopes = new Set(["hot", "events", "all"]);
3592
+ const resolvedScope = validScopes.has(scopeArg) ? scopeArg : "hot";
3593
+ const scopeCategoryFilter = resolvedScope === "events" ? "event" : null;
3594
+ const scopeExcludeEvents = resolvedScope === "hot";
3595
+
2930
3596
  if (query) {
2931
- results = await hybridSearch(ctx, query, { limit: limit * 2 });
3597
+ results = await hybridSearch(ctx, query, {
3598
+ limit: limit * 2,
3599
+ categoryFilter: scopeCategoryFilter,
3600
+ excludeEvents: scopeExcludeEvents,
3601
+ });
2932
3602
 
2933
3603
  if (kind) {
2934
3604
  results = results.filter((r) => r.kind === kind);
@@ -2941,6 +3611,10 @@ async function runSearch() {
2941
3611
  sql += " AND kind = ?";
2942
3612
  params.push(kind);
2943
3613
  }
3614
+ if (scopeCategoryFilter) {
3615
+ sql += " AND category = ?";
3616
+ params.push(scopeCategoryFilter);
3617
+ }
2944
3618
  sql += " ORDER BY COALESCE(updated_at, created_at) DESC LIMIT ?";
2945
3619
  params.push(limit);
2946
3620
  results = db.prepare(sql).all(...params);
@@ -3662,9 +4336,9 @@ ${bold("Commands:")}
3662
4336
  }
3663
4337
 
3664
4338
  async function runDoctor() {
3665
- const { resolveConfig } = await import("@context-vault/core/core/config");
4339
+ const { resolveConfig } = await import("@context-vault/core/config");
3666
4340
  const { errorLogPath, errorLogCount } =
3667
- await import("@context-vault/core/core/error-log");
4341
+ await import("../src/error-log.js");
3668
4342
 
3669
4343
  console.log();
3670
4344
  console.log(` ${bold("◇ context-vault doctor")} ${dim(`v${VERSION}`)}`);
@@ -3738,7 +4412,7 @@ async function runDoctor() {
3738
4412
  let db;
3739
4413
  if (existsSync(config.dbPath)) {
3740
4414
  try {
3741
- const { initDatabase } = await import("@context-vault/core/index/db");
4415
+ const { initDatabase } = await import("@context-vault/core/db");
3742
4416
  db = await initDatabase(config.dbPath);
3743
4417
  const schemaRow = db.prepare("PRAGMA user_version").get();
3744
4418
  const schemaVersion = schemaRow?.user_version ?? "unknown";
@@ -3760,7 +4434,7 @@ async function runDoctor() {
3760
4434
 
3761
4435
  // ── Embedding model ──────────────────────────────────────────────────
3762
4436
  try {
3763
- const { embed } = await import("@context-vault/core/index/embed");
4437
+ const { embed } = await import("@context-vault/core/embed");
3764
4438
  const vec = await embed("doctor check");
3765
4439
  if (vec && vec.length > 0) {
3766
4440
  console.log(
@@ -3840,7 +4514,7 @@ async function runDoctor() {
3840
4514
  console.log(` ${dim(`${row.created_at} — ${row.title}`)}`);
3841
4515
  }
3842
4516
  console.log(
3843
- ` ${dim('Review: context-vault search --kind feedback --tag auto-captured')}`,
4517
+ ` ${dim("Review: context-vault search --kind feedback --tag auto-captured")}`,
3844
4518
  );
3845
4519
  }
3846
4520
  } catch {
@@ -3953,9 +4627,7 @@ async function runDoctor() {
3953
4627
  console.log(
3954
4628
  ` ${yellow("!")} ${tool.name}: using old name "context-mcp"`,
3955
4629
  );
3956
- console.log(
3957
- ` ${dim("Fix: run context-vault setup to update")}`,
3958
- );
4630
+ console.log(` ${dim("Fix: run context-vault setup to update")}`);
3959
4631
  anyToolConfigured = true;
3960
4632
  }
3961
4633
  } catch {
@@ -3979,9 +4651,7 @@ async function runDoctor() {
3979
4651
  }
3980
4652
 
3981
4653
  if (!anyToolConfigured) {
3982
- console.log(
3983
- ` ${yellow("!")} No AI tools have context-vault configured`,
3984
- );
4654
+ console.log(` ${yellow("!")} No AI tools have context-vault configured`);
3985
4655
  console.log(` ${dim("Fix: run context-vault setup")}`);
3986
4656
  allOk = false;
3987
4657
  }
@@ -4106,14 +4776,10 @@ async function runDoctor() {
4106
4776
 
4107
4777
  if (hookCount === 0) {
4108
4778
  console.log(` ${dim("-")} No context-vault hooks installed`);
4109
- console.log(
4110
- ` ${dim("Optional: run context-vault hooks install")}`,
4111
- );
4779
+ console.log(` ${dim("Optional: run context-vault hooks install")}`);
4112
4780
  }
4113
4781
  } catch {
4114
- console.log(
4115
- ` ${yellow("!")} Could not read ${settingsPath}`,
4116
- );
4782
+ console.log(` ${yellow("!")} Could not read ${settingsPath}`);
4117
4783
  }
4118
4784
  } else {
4119
4785
  console.log(` ${dim("-")} No Claude Code settings found`);
@@ -4134,9 +4800,9 @@ async function runDoctor() {
4134
4800
  }
4135
4801
 
4136
4802
  async function runHealth() {
4137
- const { resolveConfig } = await import("@context-vault/core/core/config");
4803
+ const { resolveConfig } = await import("@context-vault/core/config");
4138
4804
  const { initDatabase, testConnection } =
4139
- await import("@context-vault/core/index/db");
4805
+ await import("@context-vault/core/db");
4140
4806
 
4141
4807
  let config;
4142
4808
  let healthy = true;
@@ -4325,8 +4991,8 @@ async function runConsolidate() {
4325
4991
  const dryRun = flags.has("--dry-run");
4326
4992
  const tagArg = getFlag("--tag");
4327
4993
 
4328
- const { resolveConfig } = await import("@context-vault/core/core/config");
4329
- const { initDatabase } = await import("@context-vault/core/index/db");
4994
+ const { resolveConfig } = await import("@context-vault/core/config");
4995
+ const { initDatabase } = await import("@context-vault/core/db");
4330
4996
  const { findHotTags, findColdEntries } =
4331
4997
  await import("@context-vault/core/consolidation/index");
4332
4998
 
@@ -4445,7 +5111,7 @@ async function runConsolidate() {
4445
5111
  );
4446
5112
  console.log(
4447
5113
  dim(
4448
- ` To archive: use context-vault search --kind <kind> and review manually.`,
5114
+ ` To archive: run context-vault archive (or --dry-run to preview).`,
4449
5115
  ),
4450
5116
  );
4451
5117
  }
@@ -4455,7 +5121,7 @@ async function runConsolidate() {
4455
5121
  }
4456
5122
 
4457
5123
  async function runServe() {
4458
- await import("../src/server/index.js");
5124
+ await import("../src/server.js");
4459
5125
  }
4460
5126
 
4461
5127
  async function main() {
@@ -4465,7 +5131,7 @@ async function main() {
4465
5131
  }
4466
5132
 
4467
5133
  if (flags.has("--help") || command === "help") {
4468
- showHelp();
5134
+ showHelp(flags.has("--all"));
4469
5135
  return;
4470
5136
  }
4471
5137
 
@@ -4537,6 +5203,15 @@ async function main() {
4537
5203
  case "reindex":
4538
5204
  await runReindex();
4539
5205
  break;
5206
+ case "migrate-dirs":
5207
+ await runMigrateDirs();
5208
+ break;
5209
+ case "archive":
5210
+ await runArchive();
5211
+ break;
5212
+ case "restore":
5213
+ await runRestore();
5214
+ break;
4540
5215
  case "prune":
4541
5216
  await runPrune();
4542
5217
  break;