context-vault 2.8.5 → 2.8.7

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/README.md CHANGED
@@ -10,16 +10,13 @@ Persistent memory for AI agents — saves and searches knowledge across sessions
10
10
  ## Quick Start
11
11
 
12
12
  ```bash
13
- npm install -g context-vault
14
- context-vault setup
13
+ npx context-vault setup
15
14
  ```
16
15
 
17
- Setup auto-detects your AI tools (Claude Code, Codex, Claude Desktop, Cursor, Windsurf, Cline), downloads the embedding model (~22MB), seeds your vault with a starter entry, and verifies everything works.
16
+ One command — no global install required. Setup detects your AI tools (Claude Code, Codex, Claude Desktop, Cursor, Windsurf, Cline, and more), downloads the embedding model (~22MB), seeds your vault, and configures MCP.
18
17
 
19
18
  Then open your AI tool and try: **"Search my vault for getting started"**
20
19
 
21
- > `context-mcp` still works as a CLI alias — `context-vault` is the primary command.
22
-
23
20
  ## What It Does
24
21
 
25
22
  - **Save** — insights, decisions, patterns, contacts. Your AI agent writes them as you work.
@@ -43,17 +40,19 @@ Entries are organized by `kind` (insight, decision, pattern, reference, contact,
43
40
 
44
41
  ## CLI
45
42
 
46
- | Command | Description |
47
- | -------------------------------- | --------------------------------------------------------- |
48
- | `context-vault setup` | Interactive installer — detects tools, writes MCP configs |
49
- | `context-vault ui [--port 3141]` | Web dashboard |
50
- | `context-vault status` | Vault health, paths, entry counts |
51
- | `context-vault reindex` | Rebuild search index |
52
- | `context-vault import <path>` | Import .md, .csv, .json, .txt |
53
- | `context-vault export` | Export to JSON or CSV |
54
- | `context-vault sync` | Sync with hosted account |
55
- | `context-vault update` | Check for updates |
56
- | `context-vault uninstall` | Remove MCP configs |
43
+ | Command | Description |
44
+ | ----------------------------- | --------------------------------------------------------- |
45
+ | `context-vault setup` | Interactive installer — detects tools, writes MCP configs |
46
+ | `context-vault connect --key` | Connect AI tools to hosted vault |
47
+ | `context-vault switch` | Switch between local and hosted MCP modes |
48
+ | `context-vault serve` | Start the MCP server (used by AI clients) |
49
+ | `context-vault status` | Vault health, paths, entry counts |
50
+ | `context-vault reindex` | Rebuild search index |
51
+ | `context-vault import <path>` | Import .md, .csv, .json, .txt |
52
+ | `context-vault export` | Export to JSON or CSV |
53
+ | `context-vault ingest <url>` | Fetch URL and save as vault entry |
54
+ | `context-vault update` | Check for updates |
55
+ | `context-vault uninstall` | Remove MCP configs |
57
56
 
58
57
  ## Manual MCP Config
59
58
 
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);
@@ -39,6 +38,11 @@ function isInstalledPackage() {
39
38
  return ROOT.includes("/node_modules/") || ROOT.includes("\\node_modules\\");
40
39
  }
41
40
 
41
+ /** Detect if running via npx (ephemeral cache — paths won't survive cache eviction) */
42
+ function isNpx() {
43
+ return ROOT.includes("/_npx/") || ROOT.includes("\\_npx\\");
44
+ }
45
+
42
46
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
43
47
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
44
48
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -224,7 +228,6 @@ ${bold("Commands:")}
224
228
  ${cyan("connect")} --key cv_... Connect AI tools to hosted vault
225
229
  ${cyan("switch")} local|hosted Switch between local and hosted MCP modes
226
230
  ${cyan("serve")} Start the MCP server (used by AI clients)
227
- ${cyan("ui")} [--port 3141] Launch web dashboard
228
231
  ${cyan("reindex")} Rebuild search index from knowledge files
229
232
  ${cyan("status")} Show vault diagnostics
230
233
  ${cyan("update")} Check for and install updates
@@ -232,8 +235,6 @@ ${bold("Commands:")}
232
235
  ${cyan("import")} <path> Import entries from file or directory
233
236
  ${cyan("export")} Export vault to JSON or CSV
234
237
  ${cyan("ingest")} <url> Fetch URL and save as vault entry
235
- ${cyan("link")} --key cv_... Link local vault to hosted account
236
- ${cyan("sync")} Sync entries between local and hosted
237
238
  ${cyan("migrate")} Migrate vault between local and hosted
238
239
 
239
240
  ${bold("Options:")}
@@ -559,17 +560,6 @@ async function runSetup() {
559
560
  );
560
561
  }
561
562
 
562
- // Offer to launch UI
563
- console.log();
564
- if (!isNonInteractive) {
565
- const launchUi = await prompt(` Launch web dashboard? (y/N):`, "N");
566
- if (launchUi.toLowerCase() === "y") {
567
- console.log();
568
- runUi();
569
- return;
570
- }
571
- }
572
-
573
563
  // Health check
574
564
  console.log(`\n ${dim("[5/5]")}${bold(" Health check...")}\n`);
575
565
  const okResults = results.filter((r) => r.ok);
@@ -608,7 +598,6 @@ async function runSetup() {
608
598
  ``,
609
599
  ` ${bold("CLI Commands:")}`,
610
600
  ` context-vault status Show vault health`,
611
- ` context-vault ui Launch web dashboard`,
612
601
  ` context-vault update Check for updates`,
613
602
  ];
614
603
  const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
@@ -636,7 +625,14 @@ async function configureClaude(tool, vaultDir) {
636
625
  } catch {}
637
626
 
638
627
  try {
639
- if (isInstalledPackage()) {
628
+ if (isNpx()) {
629
+ const cmdArgs = ["-y", "context-vault", "serve"];
630
+ if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
631
+ execSync(
632
+ `claude mcp add -s user context-vault -- npx ${cmdArgs.join(" ")}`,
633
+ { stdio: "pipe", env },
634
+ );
635
+ } else if (isInstalledPackage()) {
640
636
  const launcherPath = join(HOME, ".context-mcp", "server.mjs");
641
637
  const cmdArgs = [`"${launcherPath}"`];
642
638
  if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
@@ -669,7 +665,13 @@ async function configureCodex(tool, vaultDir) {
669
665
  } catch {}
670
666
 
671
667
  try {
672
- if (isInstalledPackage()) {
668
+ if (isNpx()) {
669
+ const cmdArgs = ["-y", "context-vault", "serve"];
670
+ if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
671
+ execSync(`codex mcp add context-vault -- npx ${cmdArgs.join(" ")}`, {
672
+ stdio: "pipe",
673
+ });
674
+ } else if (isInstalledPackage()) {
673
675
  const launcherPath = join(HOME, ".context-mcp", "server.mjs");
674
676
  const cmdArgs = [`"${launcherPath}"`];
675
677
  if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
@@ -717,7 +719,13 @@ function configureJsonTool(tool, vaultDir) {
717
719
  // Clean up old "context-mcp" key
718
720
  delete config[tool.configKey]["context-mcp"];
719
721
 
720
- if (isInstalledPackage()) {
722
+ if (isNpx()) {
723
+ const serverArgs = vaultDir ? ["--vault-dir", vaultDir] : [];
724
+ config[tool.configKey]["context-vault"] = {
725
+ command: "npx",
726
+ args: ["-y", "context-vault", "serve", ...serverArgs],
727
+ };
728
+ } else if (isInstalledPackage()) {
721
729
  const launcherPath = join(HOME, ".context-mcp", "server.mjs");
722
730
  const serverArgs = [];
723
731
  if (vaultDir) serverArgs.push("--vault-dir", vaultDir);
@@ -1179,52 +1187,6 @@ async function runSwitch() {
1179
1187
  }
1180
1188
  }
1181
1189
 
1182
- function runUi() {
1183
- const port = parseInt(getFlag("--port") || "3141", 10);
1184
- const localServer = join(ROOT, "scripts", "local-server.js");
1185
- if (!existsSync(localServer)) {
1186
- console.error(red("Local server not found."));
1187
- process.exit(1);
1188
- }
1189
-
1190
- // Probe the port before forking
1191
- const probe = createNetServer();
1192
- probe.once("error", (e) => {
1193
- if (e.code === "EADDRINUSE") {
1194
- console.error(red(` Port ${port} is already in use.`));
1195
- console.error(` Try: ${cyan(`context-vault ui --port ${port + 1}`)}`);
1196
- process.exit(1);
1197
- }
1198
- // Other error — let the fork handle it
1199
- probe.close();
1200
- launchServer(port, localServer);
1201
- });
1202
- probe.listen(port, () => {
1203
- probe.close(() => {
1204
- launchServer(port, localServer);
1205
- });
1206
- });
1207
- }
1208
-
1209
- function launchServer(port, localServer) {
1210
- const child = fork(localServer, [`--port=${port}`], { stdio: "inherit" });
1211
- child.on("exit", (code) => process.exit(code ?? 0));
1212
-
1213
- setTimeout(() => {
1214
- try {
1215
- const url = `https://app.context-vault.com?local=${port}`;
1216
- console.log(`Opening ${url}`);
1217
- const open =
1218
- PLATFORM === "darwin"
1219
- ? "open"
1220
- : PLATFORM === "win32"
1221
- ? "start"
1222
- : "xdg-open";
1223
- execSync(`${open} ${url}`, { stdio: "ignore" });
1224
- } catch {}
1225
- }, 1500);
1226
- }
1227
-
1228
1190
  async function runReindex() {
1229
1191
  console.log(dim("Loading vault..."));
1230
1192
 
@@ -1845,185 +1807,6 @@ async function runIngest() {
1845
1807
  console.log();
1846
1808
  }
1847
1809
 
1848
- async function runLink() {
1849
- const apiKey = getFlag("--key");
1850
- const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1851
-
1852
- if (!apiKey) {
1853
- console.log(`\n ${bold("context-vault link")} --key cv_...\n`);
1854
- console.log(` Link your local vault to a hosted Context Vault account.\n`);
1855
- console.log(` Options:`);
1856
- console.log(` --key <key> API key (required)`);
1857
- console.log(
1858
- ` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
1859
- );
1860
- console.log();
1861
- return;
1862
- }
1863
-
1864
- console.log(dim(" Verifying API key..."));
1865
-
1866
- let user;
1867
- try {
1868
- const response = await fetch(`${hostedUrl}/api/me`, {
1869
- headers: { Authorization: `Bearer ${apiKey}` },
1870
- });
1871
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1872
- user = await response.json();
1873
- } catch (e) {
1874
- console.error(red(` Verification failed: ${e.message}`));
1875
- console.error(dim(` Check your API key and server URL.`));
1876
- process.exit(1);
1877
- }
1878
-
1879
- // Store credentials in config
1880
- const dataDir = join(HOME, ".context-mcp");
1881
- const configPath = join(dataDir, "config.json");
1882
- let config = {};
1883
- if (existsSync(configPath)) {
1884
- try {
1885
- config = JSON.parse(readFileSync(configPath, "utf-8"));
1886
- } catch {}
1887
- }
1888
-
1889
- config.hostedUrl = hostedUrl;
1890
- config.apiKey = apiKey;
1891
- config.userId = user.userId || user.id;
1892
- config.email = user.email;
1893
- config.linkedAt = new Date().toISOString();
1894
-
1895
- mkdirSync(dataDir, { recursive: true });
1896
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1897
-
1898
- console.log();
1899
- console.log(green(` ✓ Linked to ${user.email}`));
1900
- console.log(dim(` Tier: ${user.tier || "free"}`));
1901
- console.log(dim(` Server: ${hostedUrl}`));
1902
- console.log(dim(` Config: ${configPath}`));
1903
- console.log();
1904
- }
1905
-
1906
- async function runSync() {
1907
- const dryRun = flags.has("--dry-run");
1908
- const pushOnly = flags.has("--push-only");
1909
- const pullOnly = flags.has("--pull-only");
1910
-
1911
- // Read credentials
1912
- const dataDir = join(HOME, ".context-mcp");
1913
- const configPath = join(dataDir, "config.json");
1914
- let storedConfig = {};
1915
- if (existsSync(configPath)) {
1916
- try {
1917
- storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
1918
- } catch {}
1919
- }
1920
-
1921
- const apiKey = getFlag("--key") || storedConfig.apiKey;
1922
- const hostedUrl =
1923
- getFlag("--url") ||
1924
- storedConfig.hostedUrl ||
1925
- "https://api.context-vault.com";
1926
-
1927
- if (!apiKey) {
1928
- console.error(
1929
- red(" Not linked. Run `context-vault link --key cv_...` first."),
1930
- );
1931
- process.exit(1);
1932
- }
1933
-
1934
- const { resolveConfig } = await import("@context-vault/core/core/config");
1935
- const { initDatabase, prepareStatements, insertVec, deleteVec } =
1936
- await import("@context-vault/core/index/db");
1937
- const { embed } = await import("@context-vault/core/index/embed");
1938
- const {
1939
- buildLocalManifest,
1940
- fetchRemoteManifest,
1941
- computeSyncPlan,
1942
- executeSync,
1943
- } = await import("@context-vault/core/sync");
1944
-
1945
- const config = resolveConfig();
1946
- if (!config.vaultDirExists) {
1947
- console.error(red(` Vault directory not found: ${config.vaultDir}`));
1948
- process.exit(1);
1949
- }
1950
-
1951
- const db = await initDatabase(config.dbPath);
1952
- const stmts = prepareStatements(db);
1953
- const ctx = {
1954
- db,
1955
- config,
1956
- stmts,
1957
- embed,
1958
- insertVec: (r, e) => insertVec(stmts, r, e),
1959
- deleteVec: (r) => deleteVec(stmts, r),
1960
- };
1961
-
1962
- console.log(dim(" Building manifests..."));
1963
- const local = buildLocalManifest(ctx);
1964
-
1965
- let remote;
1966
- try {
1967
- remote = await fetchRemoteManifest(hostedUrl, apiKey);
1968
- } catch (e) {
1969
- db.close();
1970
- console.error(red(` Failed to fetch remote manifest: ${e.message}`));
1971
- process.exit(1);
1972
- }
1973
-
1974
- const plan = computeSyncPlan(local, remote);
1975
-
1976
- // Apply push-only / pull-only filters
1977
- if (pushOnly) plan.toPull = [];
1978
- if (pullOnly) plan.toPush = [];
1979
-
1980
- console.log();
1981
- console.log(` ${bold("Sync Plan")}`);
1982
- console.log(` Push (local → remote): ${plan.toPush.length} entries`);
1983
- console.log(` Pull (remote → local): ${plan.toPull.length} entries`);
1984
- console.log(` Up to date: ${plan.upToDate.length} entries`);
1985
-
1986
- if (plan.toPush.length === 0 && plan.toPull.length === 0) {
1987
- db.close();
1988
- console.log(green("\n ✓ Everything in sync."));
1989
- console.log();
1990
- return;
1991
- }
1992
-
1993
- if (dryRun) {
1994
- db.close();
1995
- console.log(dim("\n Dry run — no changes were made."));
1996
- console.log();
1997
- return;
1998
- }
1999
-
2000
- console.log(dim("\n Syncing..."));
2001
-
2002
- const result = await executeSync(ctx, {
2003
- hostedUrl,
2004
- apiKey,
2005
- plan,
2006
- onProgress: (phase, current, total) => {
2007
- process.stdout.write(
2008
- `\r ${phase === "push" ? "Pushing" : "Pulling"}... ${current}/${total}`,
2009
- );
2010
- },
2011
- });
2012
-
2013
- db.close();
2014
-
2015
- console.log(`\r ${green("✓")} Sync complete `);
2016
- console.log(` ${green("↑")} ${result.pushed} pushed`);
2017
- console.log(` ${green("↓")} ${result.pulled} pulled`);
2018
- if (result.failed > 0) {
2019
- console.log(` ${red("x")} ${result.failed} failed`);
2020
- for (const err of result.errors.slice(0, 5)) {
2021
- console.log(` ${dim(err)}`);
2022
- }
2023
- }
2024
- console.log();
2025
- }
2026
-
2027
1810
  async function runServe() {
2028
1811
  await import("../src/server/index.js");
2029
1812
  }
@@ -2052,9 +1835,6 @@ async function main() {
2052
1835
  case "serve":
2053
1836
  await runServe();
2054
1837
  break;
2055
- case "ui":
2056
- runUi();
2057
- break;
2058
1838
  case "import":
2059
1839
  await runImport();
2060
1840
  break;
@@ -2064,12 +1844,6 @@ async function main() {
2064
1844
  case "ingest":
2065
1845
  await runIngest();
2066
1846
  break;
2067
- case "link":
2068
- await runLink();
2069
- break;
2070
- case "sync":
2071
- await runSync();
2072
- break;
2073
1847
  case "reindex":
2074
1848
  await runReindex();
2075
1849
  break;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.8.5",
3
+ "version": "2.8.7",
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.5",
3
+ "version": "2.8.7",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  "@context-vault/core"
56
56
  ],
57
57
  "dependencies": {
58
- "@context-vault/core": "^2.8.5",
58
+ "@context-vault/core": "^2.8.7",
59
59
  "@modelcontextprotocol/sdk": "^1.26.0",
60
60
  "better-sqlite3": "^12.6.2",
61
61
  "sqlite-vec": "^0.1.0"
@@ -90,13 +90,18 @@ async function main() {
90
90
  }
91
91
  }
92
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);
93
+ // ── 3. Write local server launcher (global installs only) ────────────
94
+ // Under npx the path would be stale after cache eviction — configs use
95
+ // `npx context-vault serve` instead, so skip writing the launcher.
96
+ const isNpx = PKG_ROOT.includes("/_npx/") || PKG_ROOT.includes("\\_npx\\");
97
+ if (!isNpx) {
98
+ const SERVER_ABS = join(PKG_ROOT, "src", "server", "index.js");
99
+ const DATA_DIR = join(homedir(), ".context-mcp");
100
+ const LAUNCHER = join(DATA_DIR, "server.mjs");
101
+ mkdirSync(DATA_DIR, { recursive: true });
102
+ writeFileSync(LAUNCHER, `import "${SERVER_ABS}";\n`);
103
+ console.log("[context-vault] Local server launcher written to " + LAUNCHER);
104
+ }
100
105
  }
101
106
 
102
107
  main().catch(() => {});
@@ -1,793 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * local-server.js — Local mode: serves app + vault API with no auth.
4
- *
5
- * Uses local SQLite vault. No authentication required.
6
- * Usage: node local-server.js [--port 3141]
7
- */
8
-
9
- import { createServer } from "node:http";
10
- import {
11
- createReadStream,
12
- existsSync,
13
- statSync,
14
- unlinkSync,
15
- readFileSync,
16
- writeFileSync,
17
- mkdirSync,
18
- } from "node:fs";
19
- import { join, resolve, dirname, extname } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
- import { homedir, platform } from "node:os";
22
- import { execSync } from "node:child_process";
23
- import { resolveConfig } from "@context-vault/core/core/config";
24
- import {
25
- initDatabase,
26
- prepareStatements,
27
- insertVec,
28
- deleteVec,
29
- } from "@context-vault/core/index/db";
30
- import { embed } from "@context-vault/core/index/embed";
31
- import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
32
- import { indexEntry } from "@context-vault/core/index";
33
- import { hybridSearch } from "@context-vault/core/retrieve";
34
- import { gatherVaultStatus } from "@context-vault/core/core/status";
35
- import { normalizeKind } from "@context-vault/core/core/files";
36
- import { categoryFor } from "@context-vault/core/core/categories";
37
- import { parseFile } from "@context-vault/core/capture/importers";
38
- import { importEntries } from "@context-vault/core/capture/import-pipeline";
39
- import { ingestUrl } from "@context-vault/core/capture/ingest-url";
40
- import {
41
- buildLocalManifest,
42
- fetchRemoteManifest,
43
- computeSyncPlan,
44
- executeSync,
45
- } from "@context-vault/core/sync";
46
-
47
- const __dirname = dirname(fileURLToPath(import.meta.url));
48
- const LOCAL_ROOT = resolve(__dirname, "..");
49
- const APP_DIST = resolve(LOCAL_ROOT, "app-dist");
50
-
51
- const MIME = {
52
- ".html": "text/html",
53
- ".js": "application/javascript",
54
- ".css": "text/css",
55
- ".json": "application/json",
56
- ".ico": "image/x-icon",
57
- ".svg": "image/svg+xml",
58
- };
59
-
60
- function formatEntry(row) {
61
- return {
62
- id: row.id,
63
- kind: row.kind,
64
- category: row.category,
65
- title: row.title || null,
66
- body: row.body || null,
67
- tags: row.tags ? JSON.parse(row.tags) : [],
68
- meta: row.meta
69
- ? typeof row.meta === "string"
70
- ? JSON.parse(row.meta)
71
- : row.meta
72
- : {},
73
- source: row.source || null,
74
- identity_key: row.identity_key || null,
75
- expires_at: row.expires_at || null,
76
- created_at: row.created_at,
77
- };
78
- }
79
-
80
- function validateEntry(data, { requireKind = true, requireBody = true } = {}) {
81
- if (requireKind && !data.kind)
82
- return { error: "kind is required", status: 400 };
83
- if (data.kind && !/^[a-z0-9-]+$/.test(data.kind))
84
- return {
85
- error: "kind must be lowercase alphanumeric/hyphens",
86
- status: 400,
87
- };
88
- if (requireBody && !data.body)
89
- return { error: "body is required", status: 400 };
90
- if (data.body && data.body.length > 100 * 1024)
91
- return { error: "body max 100KB", status: 400 };
92
- if (categoryFor(data.kind) === "entity" && !data.identity_key)
93
- return {
94
- error: `Entity kind "${data.kind}" requires identity_key`,
95
- status: 400,
96
- };
97
- return null;
98
- }
99
-
100
- function getAllowedOrigin(req) {
101
- const origin = req.headers["origin"];
102
- if (!origin) return null;
103
- const lower = origin.toLowerCase();
104
- if (
105
- lower === "https://context-vault.com" ||
106
- lower === "https://www.context-vault.com"
107
- )
108
- return origin;
109
- if (/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(lower)) return origin;
110
- return null;
111
- }
112
-
113
- async function main() {
114
- const portArg = process.argv.find((a) => a.startsWith("--port="));
115
- const portVal = portArg
116
- ? portArg.split("=")[1]
117
- : process.argv[process.argv.indexOf("--port") + 1];
118
- const port = parseInt(portVal || "3141", 10);
119
-
120
- const config = resolveConfig();
121
- config.vaultDirExists = existsSync(config.vaultDir);
122
- let db = await initDatabase(config.dbPath);
123
- let stmts = prepareStatements(db);
124
-
125
- const state = {
126
- db,
127
- config,
128
- stmts,
129
- embed,
130
- get ctx() {
131
- return {
132
- db: state.db,
133
- config: state.config,
134
- stmts: state.stmts,
135
- embed: state.embed,
136
- insertVec: (r, e) => insertVec(state.stmts, r, e),
137
- deleteVec: (r) => deleteVec(state.stmts, r),
138
- userId: null,
139
- };
140
- },
141
- };
142
-
143
- const server = createServer(async (req, res) => {
144
- const url = req.url?.replace(/\?.*$/, "") || "/";
145
-
146
- const allowedOrigin = getAllowedOrigin(req);
147
- const corsHeaders = allowedOrigin
148
- ? {
149
- "Access-Control-Allow-Origin": allowedOrigin,
150
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
151
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
152
- Vary: "Origin",
153
- }
154
- : { Vary: "Origin" };
155
-
156
- if (req.method === "OPTIONS") {
157
- res.writeHead(204, corsHeaders);
158
- res.end();
159
- return;
160
- }
161
-
162
- const json = (data, status = 200) => {
163
- res.writeHead(status, {
164
- "Content-Type": "application/json",
165
- ...corsHeaders,
166
- });
167
- res.end(JSON.stringify(data));
168
- };
169
-
170
- const readBody = () =>
171
- new Promise((resolve) => {
172
- let body = "";
173
- req.on("data", (c) => (body += c));
174
- req.on("end", () => {
175
- try {
176
- resolve(body ? JSON.parse(body) : null);
177
- } catch {
178
- resolve(null);
179
- }
180
- });
181
- });
182
-
183
- const ctx = state.ctx;
184
-
185
- if (url === "/api/local/browse" && req.method === "POST") {
186
- const os = platform();
187
- try {
188
- let selected;
189
- if (os === "darwin") {
190
- selected = execSync(
191
- `osascript -e 'POSIX path of (choose folder with prompt "Select vault folder")'`,
192
- { encoding: "utf-8", timeout: 30000 },
193
- ).trim();
194
- } else if (os === "linux") {
195
- selected = execSync(
196
- `zenity --file-selection --directory --title="Select vault folder" 2>/dev/null`,
197
- { encoding: "utf-8", timeout: 30000 },
198
- ).trim();
199
- } else {
200
- return json(
201
- { error: "Folder picker not supported on this platform" },
202
- 501,
203
- );
204
- }
205
-
206
- if (selected) {
207
- return json({ path: selected });
208
- }
209
- return json({ path: null, cancelled: true });
210
- } catch {
211
- // User cancelled the dialog or command failed
212
- return json({ path: null, cancelled: true });
213
- }
214
- }
215
-
216
- if (url === "/api/local/connect" && req.method === "POST") {
217
- const data = await readBody();
218
- if (!data?.vaultDir?.trim())
219
- return json(
220
- { error: "vaultDir is required", code: "INVALID_INPUT" },
221
- 400,
222
- );
223
- let vaultPath = data.vaultDir.trim().replace(/^~/, homedir());
224
- vaultPath = resolve(vaultPath);
225
- if (!existsSync(vaultPath))
226
- return json(
227
- { error: "Vault folder not found", code: "NOT_FOUND" },
228
- 404,
229
- );
230
- if (!statSync(vaultPath).isDirectory())
231
- return json(
232
- { error: "Path is not a directory", code: "INVALID_INPUT" },
233
- 400,
234
- );
235
- try {
236
- try {
237
- state.db.close();
238
- } catch {}
239
- const newConfig = {
240
- ...state.config,
241
- vaultDir: vaultPath,
242
- dbPath: join(vaultPath, ".context-vault.db"),
243
- vaultDirExists: true,
244
- };
245
- state.config = newConfig;
246
- state.db = await initDatabase(newConfig.dbPath);
247
- state.stmts = prepareStatements(state.db);
248
- console.log(`[context-vault] Switched to vault: ${vaultPath}`);
249
- return json({
250
- userId: "local",
251
- email: "local@localhost",
252
- name: "Local",
253
- tier: "free",
254
- createdAt: new Date().toISOString(),
255
- });
256
- } catch (e) {
257
- console.error(`[local-server] Connect error: ${e.message}`);
258
- return json(
259
- { error: `Failed to connect: ${e.message}`, code: "CONNECT_FAILED" },
260
- 500,
261
- );
262
- }
263
- }
264
-
265
- if (url === "/api/health" && req.method === "GET") {
266
- return json({ ok: true, mode: "local" });
267
- }
268
-
269
- if (url === "/api/me" && req.method === "GET") {
270
- return json({
271
- userId: "local",
272
- email: "local@localhost",
273
- name: "Local",
274
- tier: "free",
275
- createdAt: new Date().toISOString(),
276
- });
277
- }
278
-
279
- if (url === "/api/billing/usage" && req.method === "GET") {
280
- const status = gatherVaultStatus(ctx, {});
281
- const total = status.kindCounts.reduce((s, k) => s + k.c, 0);
282
- const storageMb =
283
- Math.round((status.dbSizeBytes / (1024 * 1024)) * 100) / 100;
284
- return json({
285
- tier: "free",
286
- limits: {
287
- maxEntries: "unlimited",
288
- requestsPerDay: "unlimited",
289
- storageMb: 1024,
290
- exportEnabled: true,
291
- },
292
- usage: { requestsToday: 0, entriesUsed: total, storageMb },
293
- });
294
- }
295
-
296
- if (url === "/api/keys" && req.method === "GET") {
297
- return json({ keys: [] });
298
- }
299
-
300
- if (url === "/api/vault/status" && req.method === "GET") {
301
- const status = gatherVaultStatus(ctx, {});
302
- return json({
303
- entries: {
304
- total: status.kindCounts.reduce((s, k) => s + k.c, 0),
305
- by_kind: Object.fromEntries(
306
- status.kindCounts.map((k) => [k.kind, k.c]),
307
- ),
308
- by_category: Object.fromEntries(
309
- status.categoryCounts.map((k) => [k.category, k.c]),
310
- ),
311
- },
312
- files: { total: status.fileCount, directories: status.subdirs },
313
- database: {
314
- size: status.dbSize,
315
- size_bytes: status.dbSizeBytes,
316
- stale_paths: status.staleCount,
317
- expired: status.expiredCount,
318
- },
319
- embeddings: status.embeddingStatus,
320
- embed_model_available: status.embedModelAvailable,
321
- health:
322
- status.errors.length === 0 && !status.stalePaths ? "ok" : "degraded",
323
- errors: status.errors,
324
- });
325
- }
326
-
327
- if (url.startsWith("/api/vault/entries") && req.method === "GET") {
328
- const idMatch = url.match(/\/api\/vault\/entries\/([^/]+)$/);
329
- if (idMatch) {
330
- const entry = stmts.getEntryById.get(idMatch[1]);
331
- if (!entry)
332
- return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
333
- return json(formatEntry(entry));
334
- }
335
- const u = new URL(req.url || "", "http://localhost");
336
- const kind = u.searchParams.get("kind") || null;
337
- const category = u.searchParams.get("category") || null;
338
- const limit = Math.min(
339
- parseInt(u.searchParams.get("limit") || "20", 10) || 20,
340
- 100,
341
- );
342
- const offset = parseInt(u.searchParams.get("offset") || "0", 10) || 0;
343
- const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
344
- const params = [];
345
- if (kind) {
346
- clauses.push("kind = ?");
347
- params.push(normalizeKind(kind));
348
- }
349
- if (category) {
350
- clauses.push("category = ?");
351
- params.push(category);
352
- }
353
- const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
354
- const total = ctx.db
355
- .prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
356
- .get(...params).c;
357
- const rows = ctx.db
358
- .prepare(
359
- `SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
360
- )
361
- .all(...params, limit, offset);
362
- return json({ entries: rows.map(formatEntry), total, limit, offset });
363
- }
364
-
365
- if (url === "/api/vault/entries" && req.method === "POST") {
366
- const data = await readBody();
367
- if (!data)
368
- return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
369
- const err = validateEntry(data);
370
- if (err)
371
- return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
372
- try {
373
- const entry = await captureAndIndex(ctx, {
374
- kind: data.kind,
375
- title: data.title,
376
- body: data.body,
377
- meta: data.meta,
378
- tags: data.tags,
379
- source: data.source || "rest-api",
380
- identity_key: data.identity_key,
381
- expires_at: data.expires_at,
382
- userId: null,
383
- });
384
- return json(formatEntry(stmts.getEntryById.get(entry.id)), 201);
385
- } catch (e) {
386
- console.error(`[local-server] Create error: ${e.message}`);
387
- return json(
388
- { error: "Failed to create entry", code: "CREATE_FAILED" },
389
- 500,
390
- );
391
- }
392
- }
393
-
394
- if (url.match(/^\/api\/vault\/entries\/[^/]+$/) && req.method === "PUT") {
395
- const id = url.split("/").pop();
396
- const data = await readBody();
397
- if (!data)
398
- return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
399
- const err = validateEntry(data, {
400
- requireKind: false,
401
- requireBody: false,
402
- });
403
- if (err)
404
- return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
405
- const existing = stmts.getEntryById.get(id);
406
- if (!existing)
407
- return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
408
- try {
409
- const entry = updateEntryFile(ctx, existing, {
410
- title: data.title,
411
- body: data.body,
412
- tags: data.tags,
413
- meta: data.meta,
414
- source: data.source,
415
- expires_at: data.expires_at,
416
- });
417
- await indexEntry(ctx, entry);
418
- return json(formatEntry(stmts.getEntryById.get(id)));
419
- } catch (e) {
420
- console.error(`[local-server] Update error: ${e.message}`);
421
- return json(
422
- { error: "Failed to update entry", code: "UPDATE_FAILED" },
423
- 500,
424
- );
425
- }
426
- }
427
-
428
- if (
429
- url.match(/^\/api\/vault\/entries\/[^/]+$/) &&
430
- req.method === "DELETE"
431
- ) {
432
- const id = url.split("/").pop();
433
- const entry = stmts.getEntryById.get(id);
434
- if (!entry)
435
- return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
436
- if (entry.file_path) {
437
- try {
438
- unlinkSync(entry.file_path);
439
- } catch {}
440
- }
441
- const rowidResult = stmts.getRowid.get(id);
442
- if (rowidResult?.rowid) {
443
- try {
444
- deleteVec(stmts, Number(rowidResult.rowid));
445
- } catch {}
446
- }
447
- stmts.deleteEntry.run(id);
448
- return json({
449
- deleted: true,
450
- id,
451
- kind: entry.kind,
452
- title: entry.title || null,
453
- });
454
- }
455
-
456
- if (url === "/api/vault/search" && req.method === "POST") {
457
- const data = await readBody();
458
- if (!data || !data.query?.trim())
459
- return json({ error: "query is required", code: "INVALID_INPUT" }, 400);
460
- const limit = Math.min(parseInt(data.limit || 20, 10) || 20, 100);
461
- const offset = parseInt(data.offset || 0, 10) || 0;
462
- try {
463
- const results = await hybridSearch(ctx, data.query, {
464
- kindFilter: data.kind ? normalizeKind(data.kind) : null,
465
- categoryFilter: data.category || null,
466
- limit,
467
- offset,
468
- decayDays: ctx.config.eventDecayDays || 30,
469
- userIdFilter: undefined,
470
- });
471
- const formatted = results.map((row) => ({
472
- ...formatEntry(row),
473
- score: Math.round(row.score * 1000) / 1000,
474
- }));
475
- return json({
476
- results: formatted,
477
- count: formatted.length,
478
- query: data.query,
479
- });
480
- } catch (e) {
481
- console.error(`[local-server] Search error: ${e.message}`);
482
- return json({ error: "Search failed", code: "SEARCH_FAILED" }, 500);
483
- }
484
- }
485
-
486
- if (url === "/api/vault/import/bulk" && req.method === "POST") {
487
- const data = await readBody();
488
- if (!data || !Array.isArray(data.entries)) {
489
- return json(
490
- {
491
- error: "Invalid body — expected { entries: [...] }",
492
- code: "INVALID_INPUT",
493
- },
494
- 400,
495
- );
496
- }
497
- if (data.entries.length > 500) {
498
- return json(
499
- { error: "Maximum 500 entries per request", code: "LIMIT_EXCEEDED" },
500
- 400,
501
- );
502
- }
503
-
504
- const result = await importEntries(ctx, data.entries, {
505
- source: "bulk-import",
506
- });
507
- return json({
508
- imported: result.imported,
509
- failed: result.failed,
510
- errors: result.errors.slice(0, 10).map((e) => e.error),
511
- });
512
- }
513
-
514
- if (url === "/api/vault/import/file" && req.method === "POST") {
515
- const data = await readBody();
516
- if (!data?.filename || !data?.content) {
517
- return json(
518
- { error: "filename and content are required", code: "INVALID_INPUT" },
519
- 400,
520
- );
521
- }
522
-
523
- const entries = parseFile(data.filename, data.content, {
524
- kind: data.kind,
525
- source: data.source || "file-import",
526
- });
527
- if (!entries.length)
528
- return json({
529
- imported: 0,
530
- failed: 0,
531
- errors: ["No entries parsed from file"],
532
- });
533
-
534
- const result = await importEntries(ctx, entries, {
535
- source: data.source || "file-import",
536
- });
537
- return json({
538
- imported: result.imported,
539
- failed: result.failed,
540
- errors: result.errors.slice(0, 10).map((e) => e.error),
541
- });
542
- }
543
-
544
- if (url.startsWith("/api/vault/export") && req.method === "GET") {
545
- const u = new URL(req.url || "", "http://localhost");
546
- const format = u.searchParams.get("format") || "json";
547
-
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();
553
-
554
- const entries = rows.map(formatEntry);
555
-
556
- if (format === "csv") {
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
- ];
569
- const csvLines = [headers.join(",")];
570
- for (const e of entries) {
571
- const row = headers.map((h) => {
572
- let val = e[h];
573
- if (Array.isArray(val)) val = val.join(", ");
574
- if (val == null) val = "";
575
- val = String(val);
576
- if (val.includes(",") || val.includes('"') || val.includes("\n")) {
577
- val = '"' + val.replace(/"/g, '""') + '"';
578
- }
579
- return val;
580
- });
581
- csvLines.push(row.join(","));
582
- }
583
- res.writeHead(200, {
584
- "Content-Type": "text/csv",
585
- ...corsHeaders,
586
- });
587
- res.end(csvLines.join("\n"));
588
- return;
589
- }
590
-
591
- return json({
592
- entries,
593
- total: entries.length,
594
- exported_at: new Date().toISOString(),
595
- });
596
- }
597
-
598
- if (url === "/api/vault/ingest" && req.method === "POST") {
599
- const data = await readBody();
600
- if (!data?.url)
601
- return json({ error: "url is required", code: "INVALID_INPUT" }, 400);
602
-
603
- try {
604
- const entry = await ingestUrl(data.url, {
605
- kind: data.kind,
606
- tags: data.tags,
607
- });
608
- const result = await captureAndIndex(ctx, entry);
609
- return json(formatEntry(ctx.stmts.getEntryById.get(result.id)), 201);
610
- } catch (e) {
611
- return json(
612
- { error: `Ingestion failed: ${e.message}`, code: "INGEST_FAILED" },
613
- 500,
614
- );
615
- }
616
- }
617
-
618
- if (url === "/api/vault/manifest" && req.method === "GET") {
619
- const rows = ctx.db
620
- .prepare(
621
- "SELECT id, kind, title, created_at FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC",
622
- )
623
- .all();
624
- return json({
625
- entries: rows.map((r) => ({
626
- id: r.id,
627
- kind: r.kind,
628
- title: r.title || null,
629
- created_at: r.created_at,
630
- })),
631
- });
632
- }
633
-
634
- if (url === "/api/local/link" && req.method === "GET") {
635
- const dataDir = join(homedir(), ".context-mcp");
636
- const configPath = join(dataDir, "config.json");
637
- let storedConfig = {};
638
- if (existsSync(configPath)) {
639
- try {
640
- storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
641
- } catch {}
642
- }
643
- return json({
644
- linked: !!storedConfig.apiKey,
645
- email: storedConfig.email || null,
646
- hostedUrl: storedConfig.hostedUrl || null,
647
- linkedAt: storedConfig.linkedAt || null,
648
- tier: storedConfig.tier || null,
649
- });
650
- }
651
-
652
- if (url === "/api/local/link" && req.method === "POST") {
653
- const data = await readBody();
654
- const dataDir = join(homedir(), ".context-mcp");
655
- const configPath = join(dataDir, "config.json");
656
-
657
- let storedConfig = {};
658
- if (existsSync(configPath)) {
659
- try {
660
- storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
661
- } catch {}
662
- }
663
-
664
- if (!data?.apiKey) {
665
- // Unlink
666
- delete storedConfig.apiKey;
667
- delete storedConfig.hostedUrl;
668
- delete storedConfig.userId;
669
- delete storedConfig.email;
670
- delete storedConfig.linkedAt;
671
- delete storedConfig.tier;
672
- writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
673
- return json({ linked: false });
674
- }
675
-
676
- const hostedUrl = data.hostedUrl || "https://api.context-vault.com";
677
- try {
678
- const response = await fetch(`${hostedUrl}/api/me`, {
679
- headers: { Authorization: `Bearer ${data.apiKey}` },
680
- });
681
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
682
- const user = await response.json();
683
-
684
- storedConfig.apiKey = data.apiKey;
685
- storedConfig.hostedUrl = hostedUrl;
686
- storedConfig.userId = user.userId || user.id;
687
- storedConfig.email = user.email;
688
- storedConfig.tier = user.tier || "free";
689
- storedConfig.linkedAt = new Date().toISOString();
690
-
691
- mkdirSync(dataDir, { recursive: true });
692
- writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
693
-
694
- return json({
695
- linked: true,
696
- email: user.email,
697
- tier: user.tier || "free",
698
- });
699
- } catch (e) {
700
- return json(
701
- { error: `Verification failed: ${e.message}`, code: "AUTH_FAILED" },
702
- 401,
703
- );
704
- }
705
- }
706
-
707
- if (url === "/api/local/sync" && req.method === "POST") {
708
- const dataDir = join(homedir(), ".context-mcp");
709
- const configPath = join(dataDir, "config.json");
710
- let storedConfig = {};
711
- if (existsSync(configPath)) {
712
- try {
713
- storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
714
- } catch {}
715
- }
716
-
717
- if (!storedConfig.apiKey) {
718
- return json(
719
- { error: "Not linked. Use link endpoint first.", code: "NOT_LINKED" },
720
- 400,
721
- );
722
- }
723
-
724
- try {
725
- const local = buildLocalManifest(ctx);
726
- const remote = await fetchRemoteManifest(
727
- storedConfig.hostedUrl,
728
- storedConfig.apiKey,
729
- );
730
- const plan = computeSyncPlan(local, remote);
731
- const result = await executeSync(ctx, {
732
- hostedUrl: storedConfig.hostedUrl,
733
- apiKey: storedConfig.apiKey,
734
- plan,
735
- });
736
- return json(result);
737
- } catch (e) {
738
- return json(
739
- { error: `Sync failed: ${e.message}`, code: "SYNC_FAILED" },
740
- 500,
741
- );
742
- }
743
- }
744
-
745
- const filePath = join(
746
- APP_DIST,
747
- url === "/" ? "index.html" : url.replace(/^\//, ""),
748
- );
749
- if (!filePath.startsWith(APP_DIST)) {
750
- res.writeHead(403);
751
- res.end();
752
- return;
753
- }
754
- if (!existsSync(filePath) || !statSync(filePath).isFile()) {
755
- const fallback = join(APP_DIST, "index.html");
756
- if (existsSync(fallback)) {
757
- res.writeHead(200, { "Content-Type": "text/html" });
758
- createReadStream(fallback).pipe(res);
759
- return;
760
- }
761
- res.writeHead(404);
762
- res.end();
763
- return;
764
- }
765
- const type = MIME[extname(filePath)] || "application/octet-stream";
766
- res.writeHead(200, { "Content-Type": type });
767
- createReadStream(filePath).pipe(res);
768
- });
769
-
770
- server.listen(port, () => {
771
- console.log(`[context-vault] Local mode: http://localhost:${port}`);
772
- console.log(`[context-vault] Vault: ${config.vaultDir}`);
773
- console.log(`[context-vault] No authentication required`);
774
- });
775
-
776
- process.on("SIGINT", () => {
777
- try {
778
- state.db.close();
779
- } catch {}
780
- process.exit(0);
781
- });
782
- process.on("SIGTERM", () => {
783
- try {
784
- state.db.close();
785
- } catch {}
786
- process.exit(0);
787
- });
788
- }
789
-
790
- main().catch((e) => {
791
- console.error(`[local-server] Fatal: ${e.message}`);
792
- process.exit(1);
793
- });