context-vault 2.4.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -109,13 +109,18 @@ function vscodeDataDir() {
109
109
  function commandExists(bin) {
110
110
  try {
111
111
  const cmd = PLATFORM === "win32" ? `where ${bin}` : `which ${bin}`;
112
- execSync(cmd, { stdio: "pipe" });
112
+ execSync(cmd, { stdio: "pipe", timeout: 5000 });
113
113
  return true;
114
114
  } catch {
115
115
  return false;
116
116
  }
117
117
  }
118
118
 
119
+ /** Check if a directory exists at any of the given paths */
120
+ function anyDirExists(...paths) {
121
+ return paths.some((p) => existsSync(p));
122
+ }
123
+
119
124
  const TOOLS = [
120
125
  {
121
126
  id: "claude-code",
@@ -140,7 +145,7 @@ const TOOLS = [
140
145
  {
141
146
  id: "cursor",
142
147
  name: "Cursor",
143
- detect: () => existsSync(join(HOME, ".cursor")),
148
+ detect: () => anyDirExists(join(HOME, ".cursor"), join(appDataDir(), "Cursor")),
144
149
  configType: "json",
145
150
  configPath: join(HOME, ".cursor", "mcp.json"),
146
151
  configKey: "mcpServers",
@@ -148,15 +153,21 @@ const TOOLS = [
148
153
  {
149
154
  id: "windsurf",
150
155
  name: "Windsurf",
151
- detect: () => existsSync(join(HOME, ".codeium", "windsurf")),
156
+ detect: () => anyDirExists(
157
+ join(HOME, ".codeium", "windsurf"),
158
+ join(HOME, ".windsurf"),
159
+ ),
152
160
  configType: "json",
153
161
  configPath: join(HOME, ".codeium", "windsurf", "mcp_config.json"),
154
162
  configKey: "mcpServers",
155
163
  },
156
164
  {
157
165
  id: "antigravity",
158
- name: "Antigravity",
159
- detect: () => existsSync(join(HOME, ".gemini", "antigravity")),
166
+ name: "Antigravity (Gemini CLI)",
167
+ detect: () => anyDirExists(
168
+ join(HOME, ".gemini", "antigravity"),
169
+ join(HOME, ".gemini"),
170
+ ),
160
171
  configType: "json",
161
172
  configPath: join(HOME, ".gemini", "antigravity", "mcp_config.json"),
162
173
  configKey: "mcpServers",
@@ -175,6 +186,20 @@ const TOOLS = [
175
186
  ),
176
187
  configKey: "mcpServers",
177
188
  },
189
+ {
190
+ id: "roo-code",
191
+ name: "Roo Code (VS Code)",
192
+ detect: () =>
193
+ existsSync(join(vscodeDataDir(), "rooveterinaryinc.roo-cline", "settings")),
194
+ configType: "json",
195
+ configPath: join(
196
+ vscodeDataDir(),
197
+ "rooveterinaryinc.roo-cline",
198
+ "settings",
199
+ "cline_mcp_settings.json"
200
+ ),
201
+ configKey: "mcpServers",
202
+ },
178
203
  ];
179
204
 
180
205
  // ─── Help ────────────────────────────────────────────────────────────────────
@@ -196,12 +221,18 @@ ${bold("Commands:")}
196
221
  ${cyan("status")} Show vault diagnostics
197
222
  ${cyan("update")} Check for and install updates
198
223
  ${cyan("uninstall")} Remove MCP configs and optionally data
224
+ ${cyan("import")} <path> Import entries from file or directory
225
+ ${cyan("export")} Export vault to JSON or CSV
226
+ ${cyan("ingest")} <url> Fetch URL and save as vault entry
227
+ ${cyan("link")} --key cv_... Link local vault to hosted account
228
+ ${cyan("sync")} Sync entries between local and hosted
199
229
  ${cyan("migrate")} Migrate vault between local and hosted
200
230
 
201
231
  ${bold("Options:")}
202
232
  --help Show this help
203
233
  --version Show version
204
234
  --yes Non-interactive mode (accept all defaults)
235
+ --skip-embeddings Skip embedding model download (FTS-only mode)
205
236
  `);
206
237
  }
207
238
 
@@ -425,29 +456,36 @@ async function runSetup() {
425
456
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + "\n");
426
457
  console.log(`\n ${green("+")} Wrote ${configPath}`);
427
458
 
428
- // Pre-download embedding model with spinner
429
- console.log(`\n ${dim("[3/5]")}${bold(" Downloading embedding model...")}`);
430
- console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)\n"));
431
- {
432
- const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
433
- let frame = 0;
434
- const start = Date.now();
435
- const spinner = setInterval(() => {
436
- const elapsed = ((Date.now() - start) / 1000).toFixed(0);
437
- process.stdout.write(`\r ${spinnerFrames[frame++ % spinnerFrames.length]} Downloading... ${dim(`${elapsed}s`)}`);
438
- }, 100);
439
-
440
- try {
441
- const { embed } = await import("@context-vault/core/index/embed");
442
- await embed("warmup");
459
+ // Pre-download embedding model with spinner (skip with --skip-embeddings)
460
+ const skipEmbeddings = flags.has("--skip-embeddings");
461
+ if (skipEmbeddings) {
462
+ console.log(`\n ${dim("[3/5]")}${bold(" Embedding model")} ${dim("(skipped)")}`);
463
+ console.log(dim(" FTS-only mode full-text search works, semantic search disabled."));
464
+ console.log(dim(" To enable later: context-vault setup (without --skip-embeddings)"));
465
+ } else {
466
+ console.log(`\n ${dim("[3/5]")}${bold(" Downloading embedding model...")}`);
467
+ console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)\n"));
468
+ {
469
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
470
+ let frame = 0;
471
+ const start = Date.now();
472
+ const spinner = setInterval(() => {
473
+ const elapsed = ((Date.now() - start) / 1000).toFixed(0);
474
+ process.stdout.write(`\r ${spinnerFrames[frame++ % spinnerFrames.length]} Downloading... ${dim(`${elapsed}s`)}`);
475
+ }, 100);
443
476
 
444
- clearInterval(spinner);
445
- process.stdout.write(`\r ${green("+")} Embedding model ready \n`);
446
- } catch (e) {
447
- clearInterval(spinner);
448
- process.stdout.write(`\r ${yellow("!")} Model download failed: ${e.message} \n`);
449
- console.log(dim(` Retry: context-vault setup`));
450
- console.log(dim(` Semantic search disabled — full-text search still works.`));
477
+ try {
478
+ const { embed } = await import("@context-vault/core/index/embed");
479
+ await embed("warmup");
480
+
481
+ clearInterval(spinner);
482
+ process.stdout.write(`\r ${green("+")} Embedding model ready \n`);
483
+ } catch (e) {
484
+ clearInterval(spinner);
485
+ process.stdout.write(`\r ${yellow("!")} Model download failed: ${e.message} \n`);
486
+ console.log(dim(` Retry: context-vault setup`));
487
+ console.log(dim(` Semantic search disabled — full-text search still works.`));
488
+ }
451
489
  }
452
490
  }
453
491
 
@@ -727,7 +765,7 @@ This is an example entry showing the decision format. Feel free to delete it.
727
765
 
728
766
  async function runConnect() {
729
767
  const apiKey = getFlag("--key");
730
- const hostedUrl = getFlag("--url") || "https://www.context-vault.com";
768
+ const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
731
769
 
732
770
  if (!apiKey) {
733
771
  console.log(`\n ${bold("context-vault connect")}\n`);
@@ -736,15 +774,56 @@ async function runConnect() {
736
774
  console.log(` context-vault connect --key cv_...\n`);
737
775
  console.log(` Options:`);
738
776
  console.log(` --key <key> API key (required)`);
739
- console.log(` --url <url> Hosted server URL (default: https://www.context-vault.com)`);
777
+ console.log(` --url <url> Hosted server URL (default: https://api.context-vault.com)`);
740
778
  console.log();
741
779
  return;
742
780
  }
743
781
 
782
+ // Validate key format
783
+ if (!apiKey.startsWith("cv_") || apiKey.length < 10) {
784
+ console.error(`\n ${red("Invalid API key format.")}`);
785
+ console.error(dim(` Keys start with "cv_" and are 43 characters long.`));
786
+ console.error(dim(` Get yours at ${hostedUrl}/register\n`));
787
+ process.exit(1);
788
+ }
789
+
744
790
  console.log();
745
791
  console.log(` ${bold("◇ context-vault")} ${dim("connect")}`);
746
792
  console.log();
747
793
 
794
+ // Validate key against server before configuring tools
795
+ console.log(dim(" Verifying API key..."));
796
+ let user;
797
+ try {
798
+ const response = await fetch(`${hostedUrl}/api/me`, {
799
+ headers: { Authorization: `Bearer ${apiKey}` },
800
+ });
801
+ if (response.status === 401) {
802
+ console.error(`\n ${red("Invalid or expired API key.")}`);
803
+ console.error(dim(` Check your key and try again.`));
804
+ console.error(dim(` Get a new key at ${hostedUrl}/register\n`));
805
+ process.exit(1);
806
+ }
807
+ if (!response.ok) {
808
+ throw new Error(`Server returned HTTP ${response.status}`);
809
+ }
810
+ user = await response.json();
811
+ console.log(` ${green("+")} Verified — ${user.email} (${user.tier})\n`);
812
+ } catch (e) {
813
+ if (e.code === "ECONNREFUSED" || e.code === "ENOTFOUND" || e.cause?.code === "ECONNREFUSED" || e.cause?.code === "ENOTFOUND") {
814
+ console.error(`\n ${red("Cannot reach server.")}`);
815
+ console.error(dim(` URL: ${hostedUrl}`));
816
+ console.error(dim(` Check your internet connection or try --url <url>\n`));
817
+ } else if (e.message?.includes("Invalid or expired")) {
818
+ // Already handled above
819
+ } else {
820
+ console.error(`\n ${red(`Verification failed: ${e.message}`)}`);
821
+ console.error(dim(` Server: ${hostedUrl}`));
822
+ console.error(dim(` Check your API key and internet connection.\n`));
823
+ }
824
+ process.exit(1);
825
+ }
826
+
748
827
  // Detect tools
749
828
  console.log(dim(` [1/2]`) + bold(" Detecting tools...\n"));
750
829
  const detected = [];
@@ -893,11 +972,21 @@ function configureJsonToolHosted(tool, apiKey, hostedUrl) {
893
972
  // ─── UI Command ──────────────────────────────────────────────────────────────
894
973
 
895
974
  function runUi() {
896
- const appDist = resolve(ROOT, "..", "app", "dist");
897
- if (!existsSync(appDist) || !existsSync(join(appDist, "index.html"))) {
975
+ // Try bundled path first (npm install), then workspace path (local dev)
976
+ const bundledDist = resolve(ROOT, "app-dist");
977
+ const workspaceDist = resolve(ROOT, "..", "app", "dist");
978
+ const appDist = existsSync(join(bundledDist, "index.html")) ? bundledDist
979
+ : existsSync(join(workspaceDist, "index.html")) ? workspaceDist
980
+ : null;
981
+
982
+ if (!appDist) {
898
983
  console.error(red("Web dashboard not found."));
899
- console.error(dim(" From repo: npm run build --workspace=packages/app"));
900
- console.error(dim(" Then run: context-vault ui"));
984
+ if (isInstalledPackage()) {
985
+ console.error(dim(" Try reinstalling: npm install -g context-vault@latest"));
986
+ } else {
987
+ console.error(dim(" From repo: npm run build --workspace=packages/app"));
988
+ console.error(dim(" Then run: context-vault ui"));
989
+ }
901
990
  process.exit(1);
902
991
  }
903
992
 
@@ -1173,13 +1262,13 @@ async function runMigrate() {
1173
1262
  console.log(` context-vault migrate --to-hosted Upload local vault to hosted service`);
1174
1263
  console.log(` context-vault migrate --to-local Download hosted vault to local files`);
1175
1264
  console.log(`\n Options:`);
1176
- console.log(` --url <url> Hosted server URL (default: https://vault.contextvault.dev)`);
1265
+ console.log(` --url <url> Hosted server URL (default: https://api.context-vault.com)`);
1177
1266
  console.log(` --key <key> API key (cv_...)`);
1178
1267
  console.log();
1179
1268
  return;
1180
1269
  }
1181
1270
 
1182
- const hostedUrl = getFlag("--url") || "https://vault.contextvault.dev";
1271
+ const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1183
1272
  const apiKey = getFlag("--key");
1184
1273
 
1185
1274
  if (!apiKey) {
@@ -1234,6 +1323,404 @@ async function runMigrate() {
1234
1323
  console.log();
1235
1324
  }
1236
1325
 
1326
+ // ─── Import Command ─────────────────────────────────────────────────────────
1327
+
1328
+ async function runImport() {
1329
+ const target = args[1];
1330
+ if (!target) {
1331
+ console.log(`\n ${bold("context-vault import")} <path>\n`);
1332
+ console.log(` Import entries from a file or directory.\n`);
1333
+ console.log(` Supported formats: .md, .csv, .tsv, .json, .txt\n`);
1334
+ console.log(` Options:`);
1335
+ console.log(` --kind <kind> Default kind (default: insight)`);
1336
+ console.log(` --source <src> Default source (default: cli-import)`);
1337
+ console.log(` --dry-run Show parsed entries without importing`);
1338
+ console.log();
1339
+ return;
1340
+ }
1341
+
1342
+ const { resolveConfig } = await import("@context-vault/core/core/config");
1343
+ const { initDatabase, prepareStatements, insertVec, deleteVec } = await import("@context-vault/core/index/db");
1344
+ const { embed } = await import("@context-vault/core/index/embed");
1345
+ const { parseFile, parseDirectory } = await import("@context-vault/core/capture/importers");
1346
+ const { importEntries } = await import("@context-vault/core/capture/import-pipeline");
1347
+ const { readFileSync, statSync } = await import("node:fs");
1348
+
1349
+ const kind = getFlag("--kind") || undefined;
1350
+ const source = getFlag("--source") || "cli-import";
1351
+ const dryRun = flags.has("--dry-run");
1352
+
1353
+ const targetPath = resolve(target);
1354
+ if (!existsSync(targetPath)) {
1355
+ console.error(red(` Path not found: ${targetPath}`));
1356
+ process.exit(1);
1357
+ }
1358
+
1359
+ const stat = statSync(targetPath);
1360
+ let entries;
1361
+
1362
+ if (stat.isDirectory()) {
1363
+ entries = parseDirectory(targetPath, { kind, source });
1364
+ } else {
1365
+ const content = readFileSync(targetPath, "utf-8");
1366
+ entries = parseFile(targetPath, content, { kind, source });
1367
+ }
1368
+
1369
+ if (entries.length === 0) {
1370
+ console.log(yellow(" No entries found to import."));
1371
+ return;
1372
+ }
1373
+
1374
+ console.log(`\n Found ${bold(String(entries.length))} entries to import\n`);
1375
+
1376
+ if (dryRun) {
1377
+ for (let i = 0; i < Math.min(entries.length, 20); i++) {
1378
+ const e = entries[i];
1379
+ console.log(` ${dim(`[${i + 1}]`)} ${e.kind} — ${e.title || e.body.slice(0, 60)}${e.tags?.length ? ` ${dim(`[${e.tags.join(", ")}]`)}` : ""}`);
1380
+ }
1381
+ if (entries.length > 20) {
1382
+ console.log(dim(` ... and ${entries.length - 20} more`));
1383
+ }
1384
+ console.log(dim("\n Dry run — no entries were imported."));
1385
+ return;
1386
+ }
1387
+
1388
+ const config = resolveConfig();
1389
+ if (!config.vaultDirExists) {
1390
+ console.error(red(` Vault directory not found: ${config.vaultDir}`));
1391
+ console.error(` Run ${cyan("context-vault setup")} to configure.`);
1392
+ process.exit(1);
1393
+ }
1394
+
1395
+ const db = await initDatabase(config.dbPath);
1396
+ const stmts = prepareStatements(db);
1397
+ const ctx = {
1398
+ db, config, stmts, embed,
1399
+ insertVec: (r, e) => insertVec(stmts, r, e),
1400
+ deleteVec: (r) => deleteVec(stmts, r),
1401
+ };
1402
+
1403
+ const result = await importEntries(ctx, entries, {
1404
+ source,
1405
+ onProgress: (current, total) => {
1406
+ process.stdout.write(`\r Importing... ${current}/${total}`);
1407
+ },
1408
+ });
1409
+
1410
+ db.close();
1411
+
1412
+ console.log(`\r ${green("✓")} Import complete `);
1413
+ console.log(` ${green("+")} ${result.imported} imported`);
1414
+ if (result.failed > 0) {
1415
+ console.log(` ${red("x")} ${result.failed} failed`);
1416
+ for (const err of result.errors.slice(0, 5)) {
1417
+ console.log(` ${dim(err.error)}`);
1418
+ }
1419
+ }
1420
+ console.log();
1421
+ }
1422
+
1423
+ // ─── Export Command ─────────────────────────────────────────────────────────
1424
+
1425
+ async function runExport() {
1426
+ const format = getFlag("--format") || "json";
1427
+ const output = getFlag("--output");
1428
+
1429
+ const { resolveConfig } = await import("@context-vault/core/core/config");
1430
+ const { initDatabase, prepareStatements } = await import("@context-vault/core/index/db");
1431
+ const { writeFileSync } = await import("node:fs");
1432
+
1433
+ const config = resolveConfig();
1434
+ if (!config.vaultDirExists) {
1435
+ console.error(red(` Vault directory not found: ${config.vaultDir}`));
1436
+ process.exit(1);
1437
+ }
1438
+
1439
+ const db = await initDatabase(config.dbPath);
1440
+
1441
+ const rows = db.prepare(
1442
+ "SELECT * FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC"
1443
+ ).all();
1444
+
1445
+ db.close();
1446
+
1447
+ const entries = rows.map((row) => ({
1448
+ id: row.id,
1449
+ kind: row.kind,
1450
+ category: row.category,
1451
+ title: row.title || null,
1452
+ body: row.body || null,
1453
+ tags: row.tags ? JSON.parse(row.tags) : [],
1454
+ meta: row.meta ? JSON.parse(row.meta) : {},
1455
+ source: row.source || null,
1456
+ identity_key: row.identity_key || null,
1457
+ expires_at: row.expires_at || null,
1458
+ created_at: row.created_at,
1459
+ }));
1460
+
1461
+ let content;
1462
+
1463
+ if (format === "csv") {
1464
+ const headers = ["id", "kind", "category", "title", "body", "tags", "source", "identity_key", "expires_at", "created_at"];
1465
+ const csvLines = [headers.join(",")];
1466
+ for (const e of entries) {
1467
+ const row = headers.map((h) => {
1468
+ let val = e[h];
1469
+ if (Array.isArray(val)) val = val.join(", ");
1470
+ if (val == null) val = "";
1471
+ val = String(val);
1472
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) {
1473
+ val = '"' + val.replace(/"/g, '""') + '"';
1474
+ }
1475
+ return val;
1476
+ });
1477
+ csvLines.push(row.join(","));
1478
+ }
1479
+ content = csvLines.join("\n");
1480
+ } else {
1481
+ content = JSON.stringify({ entries, total: entries.length, exported_at: new Date().toISOString() }, null, 2);
1482
+ }
1483
+
1484
+ if (output) {
1485
+ writeFileSync(resolve(output), content);
1486
+ console.log(green(` ✓ Exported ${entries.length} entries to ${output}`));
1487
+ } else {
1488
+ process.stdout.write(content);
1489
+ }
1490
+ }
1491
+
1492
+ // ─── Ingest Command ─────────────────────────────────────────────────────────
1493
+
1494
+ async function runIngest() {
1495
+ const url = args[1];
1496
+ if (!url) {
1497
+ console.log(`\n ${bold("context-vault ingest")} <url>\n`);
1498
+ console.log(` Fetch a URL and save as a vault entry.\n`);
1499
+ console.log(` Options:`);
1500
+ console.log(` --kind <kind> Entry kind (default: reference)`);
1501
+ console.log(` --tags t1,t2 Comma-separated tags`);
1502
+ console.log(` --dry-run Show extracted content without saving`);
1503
+ console.log();
1504
+ return;
1505
+ }
1506
+
1507
+ const { ingestUrl } = await import("@context-vault/core/capture/ingest-url");
1508
+ const kind = getFlag("--kind") || undefined;
1509
+ const tagsStr = getFlag("--tags");
1510
+ const tags = tagsStr ? tagsStr.split(",").map((t) => t.trim()) : undefined;
1511
+ const dryRun = flags.has("--dry-run");
1512
+
1513
+ console.log(dim(` Fetching ${url}...`));
1514
+
1515
+ let entry;
1516
+ try {
1517
+ entry = await ingestUrl(url, { kind, tags });
1518
+ } catch (e) {
1519
+ console.error(red(` Failed: ${e.message}`));
1520
+ process.exit(1);
1521
+ }
1522
+
1523
+ console.log(`\n ${bold(entry.title)}`);
1524
+ console.log(` ${dim(`kind: ${entry.kind} | source: ${entry.source} | ${entry.body.length} chars`)}`);
1525
+ if (entry.tags?.length) console.log(` ${dim(`tags: ${entry.tags.join(", ")}`)}`);
1526
+
1527
+ if (dryRun) {
1528
+ console.log(`\n${dim(" Preview (first 500 chars):")}`);
1529
+ console.log(dim(" " + entry.body.slice(0, 500).split("\n").join("\n ")));
1530
+ console.log(dim("\n Dry run — entry was not saved."));
1531
+ return;
1532
+ }
1533
+
1534
+ const { resolveConfig } = await import("@context-vault/core/core/config");
1535
+ const { initDatabase, prepareStatements, insertVec, deleteVec } = await import("@context-vault/core/index/db");
1536
+ const { embed } = await import("@context-vault/core/index/embed");
1537
+ const { captureAndIndex } = await import("@context-vault/core/capture");
1538
+ const { indexEntry } = await import("@context-vault/core/index");
1539
+
1540
+ const config = resolveConfig();
1541
+ if (!config.vaultDirExists) {
1542
+ console.error(red(`\n Vault directory not found: ${config.vaultDir}`));
1543
+ process.exit(1);
1544
+ }
1545
+
1546
+ const db = await initDatabase(config.dbPath);
1547
+ const stmts = prepareStatements(db);
1548
+ const ctx = {
1549
+ db, config, stmts, embed,
1550
+ insertVec: (r, e) => insertVec(stmts, r, e),
1551
+ deleteVec: (r) => deleteVec(stmts, r),
1552
+ };
1553
+
1554
+ const result = await captureAndIndex(ctx, entry, indexEntry);
1555
+ db.close();
1556
+
1557
+ const relPath = result.filePath.replace(config.vaultDir + "/", "");
1558
+ console.log(`\n ${green("✓")} Saved → ${relPath}`);
1559
+ console.log(` id: ${result.id}`);
1560
+ console.log();
1561
+ }
1562
+
1563
+ // ─── Link Command ───────────────────────────────────────────────────────────
1564
+
1565
+ async function runLink() {
1566
+ const apiKey = getFlag("--key");
1567
+ const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
1568
+
1569
+ if (!apiKey) {
1570
+ console.log(`\n ${bold("context-vault link")} --key cv_...\n`);
1571
+ console.log(` Link your local vault to a hosted Context Vault account.\n`);
1572
+ console.log(` Options:`);
1573
+ console.log(` --key <key> API key (required)`);
1574
+ console.log(` --url <url> Hosted server URL (default: https://api.context-vault.com)`);
1575
+ console.log();
1576
+ return;
1577
+ }
1578
+
1579
+ console.log(dim(" Verifying API key..."));
1580
+
1581
+ let user;
1582
+ try {
1583
+ const response = await fetch(`${hostedUrl}/api/me`, {
1584
+ headers: { Authorization: `Bearer ${apiKey}` },
1585
+ });
1586
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1587
+ user = await response.json();
1588
+ } catch (e) {
1589
+ console.error(red(` Verification failed: ${e.message}`));
1590
+ console.error(dim(` Check your API key and server URL.`));
1591
+ process.exit(1);
1592
+ }
1593
+
1594
+ // Store credentials in config
1595
+ const dataDir = join(HOME, ".context-mcp");
1596
+ const configPath = join(dataDir, "config.json");
1597
+ let config = {};
1598
+ if (existsSync(configPath)) {
1599
+ try { config = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
1600
+ }
1601
+
1602
+ config.hostedUrl = hostedUrl;
1603
+ config.apiKey = apiKey;
1604
+ config.userId = user.userId || user.id;
1605
+ config.email = user.email;
1606
+ config.linkedAt = new Date().toISOString();
1607
+
1608
+ mkdirSync(dataDir, { recursive: true });
1609
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1610
+
1611
+ console.log();
1612
+ console.log(green(` ✓ Linked to ${user.email}`));
1613
+ console.log(dim(` Tier: ${user.tier || "free"}`));
1614
+ console.log(dim(` Server: ${hostedUrl}`));
1615
+ console.log(dim(` Config: ${configPath}`));
1616
+ console.log();
1617
+ }
1618
+
1619
+ // ─── Sync Command ───────────────────────────────────────────────────────────
1620
+
1621
+ async function runSync() {
1622
+ const dryRun = flags.has("--dry-run");
1623
+ const pushOnly = flags.has("--push-only");
1624
+ const pullOnly = flags.has("--pull-only");
1625
+
1626
+ // Read credentials
1627
+ const dataDir = join(HOME, ".context-mcp");
1628
+ const configPath = join(dataDir, "config.json");
1629
+ let storedConfig = {};
1630
+ if (existsSync(configPath)) {
1631
+ try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
1632
+ }
1633
+
1634
+ const apiKey = getFlag("--key") || storedConfig.apiKey;
1635
+ const hostedUrl = getFlag("--url") || storedConfig.hostedUrl || "https://api.context-vault.com";
1636
+
1637
+ if (!apiKey) {
1638
+ console.error(red(" Not linked. Run `context-vault link --key cv_...` first."));
1639
+ process.exit(1);
1640
+ }
1641
+
1642
+ const { resolveConfig } = await import("@context-vault/core/core/config");
1643
+ const { initDatabase, prepareStatements, insertVec, deleteVec } = await import("@context-vault/core/index/db");
1644
+ const { embed } = await import("@context-vault/core/index/embed");
1645
+ const { buildLocalManifest, fetchRemoteManifest, computeSyncPlan, executeSync } = await import("@context-vault/core/sync");
1646
+
1647
+ const config = resolveConfig();
1648
+ if (!config.vaultDirExists) {
1649
+ console.error(red(` Vault directory not found: ${config.vaultDir}`));
1650
+ process.exit(1);
1651
+ }
1652
+
1653
+ const db = await initDatabase(config.dbPath);
1654
+ const stmts = prepareStatements(db);
1655
+ const ctx = {
1656
+ db, config, stmts, embed,
1657
+ insertVec: (r, e) => insertVec(stmts, r, e),
1658
+ deleteVec: (r) => deleteVec(stmts, r),
1659
+ };
1660
+
1661
+ console.log(dim(" Building manifests..."));
1662
+ const local = buildLocalManifest(ctx);
1663
+
1664
+ let remote;
1665
+ try {
1666
+ remote = await fetchRemoteManifest(hostedUrl, apiKey);
1667
+ } catch (e) {
1668
+ db.close();
1669
+ console.error(red(` Failed to fetch remote manifest: ${e.message}`));
1670
+ process.exit(1);
1671
+ }
1672
+
1673
+ const plan = computeSyncPlan(local, remote);
1674
+
1675
+ // Apply push-only / pull-only filters
1676
+ if (pushOnly) plan.toPull = [];
1677
+ if (pullOnly) plan.toPush = [];
1678
+
1679
+ console.log();
1680
+ console.log(` ${bold("Sync Plan")}`);
1681
+ console.log(` Push (local → remote): ${plan.toPush.length} entries`);
1682
+ console.log(` Pull (remote → local): ${plan.toPull.length} entries`);
1683
+ console.log(` Up to date: ${plan.upToDate.length} entries`);
1684
+
1685
+ if (plan.toPush.length === 0 && plan.toPull.length === 0) {
1686
+ db.close();
1687
+ console.log(green("\n ✓ Everything in sync."));
1688
+ console.log();
1689
+ return;
1690
+ }
1691
+
1692
+ if (dryRun) {
1693
+ db.close();
1694
+ console.log(dim("\n Dry run — no changes were made."));
1695
+ console.log();
1696
+ return;
1697
+ }
1698
+
1699
+ console.log(dim("\n Syncing..."));
1700
+
1701
+ const result = await executeSync(ctx, {
1702
+ hostedUrl,
1703
+ apiKey,
1704
+ plan,
1705
+ onProgress: (phase, current, total) => {
1706
+ process.stdout.write(`\r ${phase === "push" ? "Pushing" : "Pulling"}... ${current}/${total}`);
1707
+ },
1708
+ });
1709
+
1710
+ db.close();
1711
+
1712
+ console.log(`\r ${green("✓")} Sync complete `);
1713
+ console.log(` ${green("↑")} ${result.pushed} pushed`);
1714
+ console.log(` ${green("↓")} ${result.pulled} pulled`);
1715
+ if (result.failed > 0) {
1716
+ console.log(` ${red("x")} ${result.failed} failed`);
1717
+ for (const err of result.errors.slice(0, 5)) {
1718
+ console.log(` ${dim(err)}`);
1719
+ }
1720
+ }
1721
+ console.log();
1722
+ }
1723
+
1237
1724
  // ─── Serve Command ──────────────────────────────────────────────────────────
1238
1725
 
1239
1726
  async function runServe() {
@@ -1267,8 +1754,19 @@ async function main() {
1267
1754
  runUi();
1268
1755
  break;
1269
1756
  case "import":
1757
+ await runImport();
1758
+ break;
1270
1759
  case "export":
1271
- console.log(`Import/export removed. Add .md files to vault/ and run \`context-vault reindex\`.`);
1760
+ await runExport();
1761
+ break;
1762
+ case "ingest":
1763
+ await runIngest();
1764
+ break;
1765
+ case "link":
1766
+ await runLink();
1767
+ break;
1768
+ case "sync":
1769
+ await runSync();
1272
1770
  break;
1273
1771
  case "reindex":
1274
1772
  await runReindex();