context-vault 2.8.4 → 2.8.6

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
@@ -21,9 +21,8 @@ import {
21
21
  } from "node:fs";
22
22
  import { join, resolve, dirname } from "node:path";
23
23
  import { homedir, platform } from "node:os";
24
- import { execSync, execFile, fork } from "node:child_process";
24
+ import { execSync, execFile } from "node:child_process";
25
25
  import { fileURLToPath } from "node:url";
26
- import { createServer as createNetServer } from "node:net";
27
26
 
28
27
  const __filename = fileURLToPath(import.meta.url);
29
28
  const __dirname = dirname(__filename);
@@ -222,8 +221,8 @@ ${bold("Usage:")}
222
221
  ${bold("Commands:")}
223
222
  ${cyan("setup")} Interactive MCP server installer
224
223
  ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
224
+ ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
225
225
  ${cyan("serve")} Start the MCP server (used by AI clients)
226
- ${cyan("ui")} [--port 3141] Launch web dashboard
227
226
  ${cyan("reindex")} Rebuild search index from knowledge files
228
227
  ${cyan("status")} Show vault diagnostics
229
228
  ${cyan("update")} Check for and install updates
@@ -231,8 +230,6 @@ ${bold("Commands:")}
231
230
  ${cyan("import")} <path> Import entries from file or directory
232
231
  ${cyan("export")} Export vault to JSON or CSV
233
232
  ${cyan("ingest")} <url> Fetch URL and save as vault entry
234
- ${cyan("link")} --key cv_... Link local vault to hosted account
235
- ${cyan("sync")} Sync entries between local and hosted
236
233
  ${cyan("migrate")} Migrate vault between local and hosted
237
234
 
238
235
  ${bold("Options:")}
@@ -451,6 +448,7 @@ async function runSetup() {
451
448
  vaultConfig.dataDir = dataDir;
452
449
  vaultConfig.dbPath = join(dataDir, "vault.db");
453
450
  vaultConfig.devDir = join(HOME, "dev");
451
+ vaultConfig.mode = "local";
454
452
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
455
453
  console.log(`\n ${green("+")} Wrote ${configPath}`);
456
454
 
@@ -557,17 +555,6 @@ async function runSetup() {
557
555
  );
558
556
  }
559
557
 
560
- // Offer to launch UI
561
- console.log();
562
- if (!isNonInteractive) {
563
- const launchUi = await prompt(` Launch web dashboard? (y/N):`, "N");
564
- if (launchUi.toLowerCase() === "y") {
565
- console.log();
566
- runUi();
567
- return;
568
- }
569
- }
570
-
571
558
  // Health check
572
559
  console.log(`\n ${dim("[5/5]")}${bold(" Health check...")}\n`);
573
560
  const okResults = results.filter((r) => r.ok);
@@ -606,7 +593,6 @@ async function runSetup() {
606
593
  ``,
607
594
  ` ${bold("CLI Commands:")}`,
608
595
  ` context-vault status Show vault health`,
609
- ` context-vault ui Launch web dashboard`,
610
596
  ` context-vault update Check for updates`,
611
597
  ];
612
598
  const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
@@ -635,10 +621,11 @@ async function configureClaude(tool, vaultDir) {
635
621
 
636
622
  try {
637
623
  if (isInstalledPackage()) {
638
- const cmdArgs = ["serve"];
624
+ const launcherPath = join(HOME, ".context-mcp", "server.mjs");
625
+ const cmdArgs = [`"${launcherPath}"`];
639
626
  if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
640
627
  execSync(
641
- `claude mcp add -s user context-vault -- context-vault ${cmdArgs.join(" ")}`,
628
+ `claude mcp add -s user context-vault -- node ${cmdArgs.join(" ")}`,
642
629
  { stdio: "pipe", env },
643
630
  );
644
631
  } else {
@@ -667,12 +654,12 @@ async function configureCodex(tool, vaultDir) {
667
654
 
668
655
  try {
669
656
  if (isInstalledPackage()) {
670
- const cmdArgs = ["serve"];
657
+ const launcherPath = join(HOME, ".context-mcp", "server.mjs");
658
+ const cmdArgs = [`"${launcherPath}"`];
671
659
  if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
672
- execSync(
673
- `codex mcp add context-vault -- context-vault ${cmdArgs.join(" ")}`,
674
- { stdio: "pipe" },
675
- );
660
+ execSync(`codex mcp add context-vault -- node ${cmdArgs.join(" ")}`, {
661
+ stdio: "pipe",
662
+ });
676
663
  } else {
677
664
  const cmdArgs = [`"${SERVER_PATH}"`];
678
665
  if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
@@ -715,11 +702,12 @@ function configureJsonTool(tool, vaultDir) {
715
702
  delete config[tool.configKey]["context-mcp"];
716
703
 
717
704
  if (isInstalledPackage()) {
718
- const serverArgs = ["serve"];
705
+ const launcherPath = join(HOME, ".context-mcp", "server.mjs");
706
+ const serverArgs = [];
719
707
  if (vaultDir) serverArgs.push("--vault-dir", vaultDir);
720
708
  config[tool.configKey]["context-vault"] = {
721
- command: "context-vault",
722
- args: serverArgs,
709
+ command: "node",
710
+ args: [launcherPath, ...serverArgs],
723
711
  };
724
712
  } else {
725
713
  const serverArgs = [SERVER_PATH];
@@ -948,6 +936,19 @@ async function runConnect() {
948
936
  }
949
937
  }
950
938
 
939
+ // Persist mode in config
940
+ const modeConfigPath = join(HOME, ".context-mcp", "config.json");
941
+ let modeConfig = {};
942
+ if (existsSync(modeConfigPath)) {
943
+ try {
944
+ modeConfig = JSON.parse(readFileSync(modeConfigPath, "utf-8"));
945
+ } catch {}
946
+ }
947
+ modeConfig.mode = "hosted";
948
+ modeConfig.hostedUrl = hostedUrl;
949
+ mkdirSync(join(HOME, ".context-mcp"), { recursive: true });
950
+ writeFileSync(modeConfigPath, JSON.stringify(modeConfig, null, 2) + "\n");
951
+
951
952
  console.log();
952
953
  console.log(
953
954
  green(" ✓ Connected! Your AI tools can now access your hosted vault."),
@@ -1033,85 +1034,133 @@ function configureJsonToolHosted(tool, apiKey, hostedUrl) {
1033
1034
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1034
1035
  }
1035
1036
 
1036
- function runUi() {
1037
- // Try bundled path first (npm install), then workspace path (local dev)
1038
- const bundledDist = resolve(ROOT, "app-dist");
1039
- const workspaceDist = resolve(ROOT, "..", "app", "dist");
1040
- const appDist = existsSync(join(bundledDist, "index.html"))
1041
- ? bundledDist
1042
- : existsSync(join(workspaceDist, "index.html"))
1043
- ? workspaceDist
1044
- : null;
1037
+ async function runSwitch() {
1038
+ const target = args[1];
1039
+ if (target !== "local" && target !== "hosted") {
1040
+ console.log(`\n ${bold("context-vault switch")} <local|hosted>\n`);
1041
+ console.log(` Switch between local and hosted MCP modes.\n`);
1042
+ console.log(
1043
+ ` ${cyan("switch local")} Use local vault (SQLite + files on this device)`,
1044
+ );
1045
+ console.log(
1046
+ ` ${cyan("switch hosted")} Use hosted vault (requires API key)\n`,
1047
+ );
1048
+ console.log(` Options:`);
1049
+ console.log(` --key <key> API key for hosted mode (cv_...)`);
1050
+ console.log(
1051
+ ` --url <url> Hosted server URL (default: https://api.context-vault.com)\n`,
1052
+ );
1053
+ return;
1054
+ }
1045
1055
 
1046
- if (!appDist) {
1047
- const cloudUrl = "https://app.context-vault.com";
1048
- console.log(`Opening ${cloudUrl}`);
1049
- const open =
1050
- PLATFORM === "darwin"
1051
- ? "open"
1052
- : PLATFORM === "win32"
1053
- ? "start"
1054
- : "xdg-open";
1056
+ const dataDir = join(HOME, ".context-mcp");
1057
+ const configPath = join(dataDir, "config.json");
1058
+ let vaultConfig = {};
1059
+ if (existsSync(configPath)) {
1055
1060
  try {
1056
- execSync(`${open} ${cloudUrl}`, { stdio: "ignore" });
1061
+ vaultConfig = JSON.parse(readFileSync(configPath, "utf-8"));
1057
1062
  } catch {}
1058
- return;
1059
1063
  }
1060
1064
 
1061
- const port = parseInt(getFlag("--port") || "3141", 10);
1062
- const localServer = join(ROOT, "scripts", "local-server.js");
1063
- if (!existsSync(localServer)) {
1064
- console.error(red("Local server not found."));
1065
- process.exit(1);
1066
- }
1065
+ const { detected } = await detectAllTools();
1066
+
1067
+ if (target === "local") {
1068
+ const launcherPath = join(dataDir, "server.mjs");
1069
+ if (!existsSync(launcherPath)) {
1070
+ const serverAbs = resolve(ROOT, "src", "server", "index.js");
1071
+ mkdirSync(dataDir, { recursive: true });
1072
+ writeFileSync(launcherPath, `import "${serverAbs}";\n`);
1073
+ }
1074
+
1075
+ vaultConfig.mode = "local";
1076
+ mkdirSync(dataDir, { recursive: true });
1077
+ writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
1067
1078
 
1068
- // Probe the port before forking
1069
- const probe = createNetServer();
1070
- probe.once("error", (e) => {
1071
- if (e.code === "EADDRINUSE") {
1072
- console.error(red(` Port ${port} is already in use.`));
1073
- console.error(` Try: ${cyan(`context-vault ui --port ${port + 1}`)}`);
1079
+ console.log();
1080
+ console.log(` ${bold("◇ context-vault")} ${dim("switch → local")}`);
1081
+ console.log();
1082
+
1083
+ const defaultVDir = join(HOME, "vault");
1084
+ const customVaultDir =
1085
+ vaultConfig.vaultDir &&
1086
+ resolve(vaultConfig.vaultDir) !== resolve(defaultVDir)
1087
+ ? vaultConfig.vaultDir
1088
+ : null;
1089
+
1090
+ for (const tool of detected) {
1091
+ try {
1092
+ if (tool.configType === "cli" && tool.id === "codex") {
1093
+ await configureCodex(tool, customVaultDir);
1094
+ } else if (tool.configType === "cli") {
1095
+ await configureClaude(tool, customVaultDir);
1096
+ } else {
1097
+ configureJsonTool(tool, customVaultDir);
1098
+ }
1099
+ console.log(` ${green("+")} ${tool.name} — switched to local`);
1100
+ } catch (e) {
1101
+ console.log(` ${red("x")} ${tool.name} — ${e.message}`);
1102
+ }
1103
+ }
1104
+ console.log();
1105
+ console.log(green(" ✓ Switched to local mode."));
1106
+ console.log(dim(` Server: node ${launcherPath}`));
1107
+ console.log();
1108
+ } else {
1109
+ const hostedUrl =
1110
+ getFlag("--url") ||
1111
+ vaultConfig.hostedUrl ||
1112
+ "https://api.context-vault.com";
1113
+ const apiKey = getFlag("--key") || vaultConfig.apiKey;
1114
+
1115
+ if (!apiKey) {
1116
+ console.error(
1117
+ red(` --key <api_key> required. Get yours at ${hostedUrl}/dashboard`),
1118
+ );
1074
1119
  process.exit(1);
1075
1120
  }
1076
- // Other error — let the fork handle it
1077
- probe.close();
1078
- launchServer(port, localServer);
1079
- });
1080
- probe.listen(port, () => {
1081
- probe.close(() => {
1082
- launchServer(port, localServer);
1083
- });
1084
- });
1085
- }
1086
1121
 
1087
- function launchServer(port, localServer) {
1088
- const child = fork(localServer, [`--port=${port}`], { stdio: "inherit" });
1089
- child.on("exit", (code) => process.exit(code ?? 0));
1122
+ console.log();
1123
+ console.log(` ${bold("◇ context-vault")} ${dim("switch hosted")}`);
1124
+ console.log();
1125
+ console.log(dim(" Verifying API key..."));
1090
1126
 
1091
- // Open browser after a short delay — prefer hosted UI if reachable
1092
- setTimeout(async () => {
1093
1127
  try {
1094
- let url = `http://localhost:${port}`;
1128
+ const response = await fetch(`${hostedUrl}/api/me`, {
1129
+ headers: { Authorization: `Bearer ${apiKey}` },
1130
+ });
1131
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1132
+ const user = await response.json();
1133
+ console.log(` ${green("+")} Verified — ${user.email}\n`);
1134
+ } catch (e) {
1135
+ console.error(red(` Verification failed: ${e.message}`));
1136
+ process.exit(1);
1137
+ }
1138
+
1139
+ vaultConfig.mode = "hosted";
1140
+ vaultConfig.hostedUrl = hostedUrl;
1141
+ vaultConfig.apiKey = apiKey;
1142
+ mkdirSync(dataDir, { recursive: true });
1143
+ writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
1144
+
1145
+ for (const tool of detected) {
1095
1146
  try {
1096
- const controller = new AbortController();
1097
- const timeout = setTimeout(() => controller.abort(), 3000);
1098
- await fetch("https://context-vault.com", {
1099
- method: "HEAD",
1100
- signal: controller.signal,
1101
- });
1102
- clearTimeout(timeout);
1103
- url = `https://context-vault.com?local=${port}`;
1104
- } catch {}
1105
- console.log(`Opening ${url}`);
1106
- const open =
1107
- PLATFORM === "darwin"
1108
- ? "open"
1109
- : PLATFORM === "win32"
1110
- ? "start"
1111
- : "xdg-open";
1112
- execSync(`${open} ${url}`, { stdio: "ignore" });
1113
- } catch {}
1114
- }, 1500);
1147
+ if (tool.configType === "cli" && tool.id === "codex") {
1148
+ configureCodexHosted(apiKey, hostedUrl);
1149
+ } else if (tool.configType === "cli") {
1150
+ configureClaudeHosted(apiKey, hostedUrl);
1151
+ } else {
1152
+ configureJsonToolHosted(tool, apiKey, hostedUrl);
1153
+ }
1154
+ console.log(` ${green("+")} ${tool.name} — switched to hosted`);
1155
+ } catch (e) {
1156
+ console.log(` ${red("x")} ${tool.name} — ${e.message}`);
1157
+ }
1158
+ }
1159
+ console.log();
1160
+ console.log(green(" ✓ Switched to hosted mode."));
1161
+ console.log(dim(` Endpoint: ${hostedUrl}/mcp`));
1162
+ console.log();
1163
+ }
1115
1164
  }
1116
1165
 
1117
1166
  async function runReindex() {
@@ -1157,6 +1206,24 @@ async function runStatus() {
1157
1206
  const { gatherVaultStatus } = await import("@context-vault/core/core/status");
1158
1207
 
1159
1208
  const config = resolveConfig();
1209
+
1210
+ let mode = "local";
1211
+ let modeDetail = "";
1212
+ const rawConfigPath = join(HOME, ".context-mcp", "config.json");
1213
+ if (existsSync(rawConfigPath)) {
1214
+ try {
1215
+ const raw = JSON.parse(readFileSync(rawConfigPath, "utf-8"));
1216
+ mode = raw.mode || "local";
1217
+ if (mode === "hosted" && raw.hostedUrl) {
1218
+ const email = raw.email ? ` · ${raw.email}` : "";
1219
+ modeDetail = ` (${raw.hostedUrl}${email})`;
1220
+ } else {
1221
+ const launcherPath = join(HOME, ".context-mcp", "server.mjs");
1222
+ modeDetail = ` (node ${launcherPath})`;
1223
+ }
1224
+ } catch {}
1225
+ }
1226
+
1160
1227
  const db = await initDatabase(config.dbPath);
1161
1228
 
1162
1229
  const status = gatherVaultStatus({ db, config });
@@ -1166,6 +1233,7 @@ async function runStatus() {
1166
1233
  console.log();
1167
1234
  console.log(` ${bold("◇ context-vault")} ${dim(`v${VERSION}`)}`);
1168
1235
  console.log();
1236
+ console.log(` Mode: ${mode}${dim(modeDetail)}`);
1169
1237
  console.log(
1170
1238
  ` Vault: ${config.vaultDir} ${dim(`(${config.vaultDirExists ? status.fileCount + " files" : "missing"})`)}`,
1171
1239
  );
@@ -1715,185 +1783,6 @@ async function runIngest() {
1715
1783
  console.log();
1716
1784
  }
1717
1785
 
1718
- async function runLink() {
1719
- const apiKey = getFlag("--key");
1720
- const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1721
-
1722
- if (!apiKey) {
1723
- console.log(`\n ${bold("context-vault link")} --key cv_...\n`);
1724
- console.log(` Link your local vault to a hosted Context Vault account.\n`);
1725
- console.log(` Options:`);
1726
- console.log(` --key <key> API key (required)`);
1727
- console.log(
1728
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
1729
- );
1730
- console.log();
1731
- return;
1732
- }
1733
-
1734
- console.log(dim(" Verifying API key..."));
1735
-
1736
- let user;
1737
- try {
1738
- const response = await fetch(`${hostedUrl}/api/me`, {
1739
- headers: { Authorization: `Bearer ${apiKey}` },
1740
- });
1741
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1742
- user = await response.json();
1743
- } catch (e) {
1744
- console.error(red(` Verification failed: ${e.message}`));
1745
- console.error(dim(` Check your API key and server URL.`));
1746
- process.exit(1);
1747
- }
1748
-
1749
- // Store credentials in config
1750
- const dataDir = join(HOME, ".context-mcp");
1751
- const configPath = join(dataDir, "config.json");
1752
- let config = {};
1753
- if (existsSync(configPath)) {
1754
- try {
1755
- config = JSON.parse(readFileSync(configPath, "utf-8"));
1756
- } catch {}
1757
- }
1758
-
1759
- config.hostedUrl = hostedUrl;
1760
- config.apiKey = apiKey;
1761
- config.userId = user.userId || user.id;
1762
- config.email = user.email;
1763
- config.linkedAt = new Date().toISOString();
1764
-
1765
- mkdirSync(dataDir, { recursive: true });
1766
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1767
-
1768
- console.log();
1769
- console.log(green(` ✓ Linked to ${user.email}`));
1770
- console.log(dim(` Tier: ${user.tier || "free"}`));
1771
- console.log(dim(` Server: ${hostedUrl}`));
1772
- console.log(dim(` Config: ${configPath}`));
1773
- console.log();
1774
- }
1775
-
1776
- async function runSync() {
1777
- const dryRun = flags.has("--dry-run");
1778
- const pushOnly = flags.has("--push-only");
1779
- const pullOnly = flags.has("--pull-only");
1780
-
1781
- // Read credentials
1782
- const dataDir = join(HOME, ".context-mcp");
1783
- const configPath = join(dataDir, "config.json");
1784
- let storedConfig = {};
1785
- if (existsSync(configPath)) {
1786
- try {
1787
- storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
1788
- } catch {}
1789
- }
1790
-
1791
- const apiKey = getFlag("--key") || storedConfig.apiKey;
1792
- const hostedUrl =
1793
- getFlag("--url") ||
1794
- storedConfig.hostedUrl ||
1795
- "https://api.context-vault.com";
1796
-
1797
- if (!apiKey) {
1798
- console.error(
1799
- red(" Not linked. Run `context-vault link --key cv_...` first."),
1800
- );
1801
- process.exit(1);
1802
- }
1803
-
1804
- const { resolveConfig } = await import("@context-vault/core/core/config");
1805
- const { initDatabase, prepareStatements, insertVec, deleteVec } =
1806
- await import("@context-vault/core/index/db");
1807
- const { embed } = await import("@context-vault/core/index/embed");
1808
- const {
1809
- buildLocalManifest,
1810
- fetchRemoteManifest,
1811
- computeSyncPlan,
1812
- executeSync,
1813
- } = await import("@context-vault/core/sync");
1814
-
1815
- const config = resolveConfig();
1816
- if (!config.vaultDirExists) {
1817
- console.error(red(` Vault directory not found: ${config.vaultDir}`));
1818
- process.exit(1);
1819
- }
1820
-
1821
- const db = await initDatabase(config.dbPath);
1822
- const stmts = prepareStatements(db);
1823
- const ctx = {
1824
- db,
1825
- config,
1826
- stmts,
1827
- embed,
1828
- insertVec: (r, e) => insertVec(stmts, r, e),
1829
- deleteVec: (r) => deleteVec(stmts, r),
1830
- };
1831
-
1832
- console.log(dim(" Building manifests..."));
1833
- const local = buildLocalManifest(ctx);
1834
-
1835
- let remote;
1836
- try {
1837
- remote = await fetchRemoteManifest(hostedUrl, apiKey);
1838
- } catch (e) {
1839
- db.close();
1840
- console.error(red(` Failed to fetch remote manifest: ${e.message}`));
1841
- process.exit(1);
1842
- }
1843
-
1844
- const plan = computeSyncPlan(local, remote);
1845
-
1846
- // Apply push-only / pull-only filters
1847
- if (pushOnly) plan.toPull = [];
1848
- if (pullOnly) plan.toPush = [];
1849
-
1850
- console.log();
1851
- console.log(` ${bold("Sync Plan")}`);
1852
- console.log(` Push (local → remote): ${plan.toPush.length} entries`);
1853
- console.log(` Pull (remote → local): ${plan.toPull.length} entries`);
1854
- console.log(` Up to date: ${plan.upToDate.length} entries`);
1855
-
1856
- if (plan.toPush.length === 0 && plan.toPull.length === 0) {
1857
- db.close();
1858
- console.log(green("\n ✓ Everything in sync."));
1859
- console.log();
1860
- return;
1861
- }
1862
-
1863
- if (dryRun) {
1864
- db.close();
1865
- console.log(dim("\n Dry run — no changes were made."));
1866
- console.log();
1867
- return;
1868
- }
1869
-
1870
- console.log(dim("\n Syncing..."));
1871
-
1872
- const result = await executeSync(ctx, {
1873
- hostedUrl,
1874
- apiKey,
1875
- plan,
1876
- onProgress: (phase, current, total) => {
1877
- process.stdout.write(
1878
- `\r ${phase === "push" ? "Pushing" : "Pulling"}... ${current}/${total}`,
1879
- );
1880
- },
1881
- });
1882
-
1883
- db.close();
1884
-
1885
- console.log(`\r ${green("✓")} Sync complete `);
1886
- console.log(` ${green("↑")} ${result.pushed} pushed`);
1887
- console.log(` ${green("↓")} ${result.pulled} pulled`);
1888
- if (result.failed > 0) {
1889
- console.log(` ${red("x")} ${result.failed} failed`);
1890
- for (const err of result.errors.slice(0, 5)) {
1891
- console.log(` ${dim(err)}`);
1892
- }
1893
- }
1894
- console.log();
1895
- }
1896
-
1897
1786
  async function runServe() {
1898
1787
  await import("../src/server/index.js");
1899
1788
  }
@@ -1916,12 +1805,12 @@ async function main() {
1916
1805
  case "connect":
1917
1806
  await runConnect();
1918
1807
  break;
1808
+ case "switch":
1809
+ await runSwitch();
1810
+ break;
1919
1811
  case "serve":
1920
1812
  await runServe();
1921
1813
  break;
1922
- case "ui":
1923
- runUi();
1924
- break;
1925
1814
  case "import":
1926
1815
  await runImport();
1927
1816
  break;
@@ -1931,12 +1820,6 @@ async function main() {
1931
1820
  case "ingest":
1932
1821
  await runIngest();
1933
1822
  break;
1934
- case "link":
1935
- await runLink();
1936
- break;
1937
- case "sync":
1938
- await runSync();
1939
- break;
1940
1823
  case "reindex":
1941
1824
  await runReindex();
1942
1825
  break;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -16,8 +16,8 @@ const VEC_WEIGHT = 0.6;
16
16
  */
17
17
  export function buildFtsQuery(query) {
18
18
  const words = query
19
- .split(/\s+/)
20
- .map((w) => w.replace(/[*"()\-:^~{}]/g, ""))
19
+ .split(/[\s-]+/)
20
+ .map((w) => w.replace(/[*"():^~{}]/g, ""))
21
21
  .filter((w) => w.length > 0);
22
22
  if (!words.length) return null;
23
23
  return words.map((w) => `"${w}"`).join(" AND ");
@@ -26,6 +26,7 @@ function validateSaveInput({
26
26
  meta,
27
27
  source,
28
28
  identity_key,
29
+ expires_at,
29
30
  }) {
30
31
  if (kind !== undefined && kind !== null) {
31
32
  if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
@@ -93,6 +94,14 @@ function validateSaveInput({
93
94
  );
94
95
  }
95
96
  }
97
+ if (expires_at !== undefined && expires_at !== null) {
98
+ if (
99
+ typeof expires_at !== "string" ||
100
+ isNaN(new Date(expires_at).getTime())
101
+ ) {
102
+ return err("expires_at must be a valid ISO date string", "INVALID_INPUT");
103
+ }
104
+ }
96
105
  return null;
97
106
  }
98
107
 
@@ -178,6 +187,7 @@ export async function handler(
178
187
  meta,
179
188
  source,
180
189
  identity_key,
190
+ expires_at,
181
191
  });
182
192
  if (inputErr) return inputErr;
183
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -20,7 +20,6 @@
20
20
  "bin/",
21
21
  "src/",
22
22
  "scripts/",
23
- "app-dist/",
24
23
  "README.md",
25
24
  "LICENSE"
26
25
  ],
@@ -56,7 +55,7 @@
56
55
  "@context-vault/core"
57
56
  ],
58
57
  "dependencies": {
59
- "@context-vault/core": "^2.8.4",
58
+ "@context-vault/core": "^2.8.6",
60
59
  "@modelcontextprotocol/sdk": "^1.26.0",
61
60
  "better-sqlite3": "^12.6.2",
62
61
  "sqlite-vec": "^0.1.0"
@@ -10,9 +10,10 @@
10
10
  */
11
11
 
12
12
  import { execSync } from "node:child_process";
13
- import { existsSync } from "node:fs";
13
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
14
14
  import { join, dirname } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { homedir } from "node:os";
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
  const PKG_ROOT = join(__dirname, "..");
@@ -88,6 +89,14 @@ async function main() {
88
89
  );
89
90
  }
90
91
  }
92
+
93
+ // ── 3. Write local server launcher ───────────────────────────────────
94
+ const SERVER_ABS = join(PKG_ROOT, "src", "server", "index.js");
95
+ const DATA_DIR = join(homedir(), ".context-mcp");
96
+ const LAUNCHER = join(DATA_DIR, "server.mjs");
97
+ mkdirSync(DATA_DIR, { recursive: true });
98
+ writeFileSync(LAUNCHER, `import "${SERVER_ABS}";\n`);
99
+ console.log("[context-vault] Local server launcher written to " + LAUNCHER);
91
100
  }
92
101
 
93
102
  main().catch(() => {});