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/README.md +3 -3
- package/app-dist/assets/index-DjXoWapE.css +1 -0
- package/app-dist/assets/index-R4n9Qz4U.js +380 -0
- package/app-dist/index.html +16 -0
- package/bin/cli.js +534 -36
- package/node_modules/@context-vault/core/package.json +8 -4
- package/node_modules/@context-vault/core/src/capture/file-ops.js +1 -1
- package/node_modules/@context-vault/core/src/capture/import-pipeline.js +85 -0
- package/node_modules/@context-vault/core/src/capture/importers.js +360 -0
- package/node_modules/@context-vault/core/src/capture/ingest-url.js +216 -0
- package/node_modules/@context-vault/core/src/core/config.js +13 -3
- package/node_modules/@context-vault/core/src/index/db.js +18 -4
- package/node_modules/@context-vault/core/src/retrieve/index.js +12 -7
- package/node_modules/@context-vault/core/src/server/tools.js +149 -15
- package/node_modules/@context-vault/core/src/sync/sync.js +230 -0
- package/package.json +6 -5
- package/scripts/local-server.js +204 -3
- package/scripts/prepack.js +13 -1
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: () =>
|
|
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: () =>
|
|
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: () =>
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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://
|
|
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://
|
|
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
|
-
|
|
897
|
-
|
|
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
|
-
|
|
900
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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();
|