context-vault 3.8.0 → 3.10.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/assets/agent-rules.md +28 -1
- package/assets/setup-prompt.md +16 -1
- package/bin/cli.js +1187 -4
- package/dist/auto-memory.d.ts +52 -0
- package/dist/auto-memory.d.ts.map +1 -0
- package/dist/auto-memory.js +142 -0
- package/dist/auto-memory.js.map +1 -0
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +2 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/remote.d.ts +186 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +372 -0
- package/dist/remote.js.map +1 -0
- package/dist/remote.test.d.ts +2 -0
- package/dist/remote.test.d.ts.map +1 -0
- package/dist/remote.test.js +107 -0
- package/dist/remote.test.js.map +1 -0
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +19 -0
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +70 -0
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/publish-to-team.d.ts +11 -0
- package/dist/tools/publish-to-team.d.ts.map +1 -0
- package/dist/tools/publish-to-team.js +91 -0
- package/dist/tools/publish-to-team.js.map +1 -0
- package/dist/tools/publish-to-team.test.d.ts +2 -0
- package/dist/tools/publish-to-team.test.d.ts.map +1 -0
- package/dist/tools/publish-to-team.test.js +95 -0
- package/dist/tools/publish-to-team.test.js.map +1 -0
- package/dist/tools/recall.d.ts +1 -1
- package/dist/tools/recall.d.ts.map +1 -1
- package/dist/tools/recall.js +120 -1
- package/dist/tools/recall.js.map +1 -1
- package/dist/tools/save-context.d.ts +5 -1
- package/dist/tools/save-context.d.ts.map +1 -1
- package/dist/tools/save-context.js +163 -2
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +134 -86
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts +3 -1
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +48 -2
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +7 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/config.ts +50 -3
- package/node_modules/@context-vault/core/src/main.ts +1 -0
- package/node_modules/@context-vault/core/src/types.ts +8 -0
- package/package.json +2 -2
- package/src/auto-memory.ts +169 -0
- package/src/register-tools.ts +2 -0
- package/src/remote.test.ts +123 -0
- package/src/remote.ts +470 -0
- package/src/tools/context-status.ts +19 -0
- package/src/tools/get-context.ts +72 -0
- package/src/tools/publish-to-team.test.ts +115 -0
- package/src/tools/publish-to-team.ts +112 -0
- package/src/tools/recall.ts +113 -1
- package/src/tools/save-context.ts +167 -1
- package/src/tools/session-start.ts +133 -100
package/bin/cli.js
CHANGED
|
@@ -414,6 +414,9 @@ ${bold('Commands:')}
|
|
|
414
414
|
${cyan('restore')} <id> Restore an archived entry back into the vault
|
|
415
415
|
${cyan('prune')} Remove expired entries (use --dry-run to preview)
|
|
416
416
|
${cyan('stats')} recall|co-retrieval Measure recall ratio and co-retrieval graph
|
|
417
|
+
${cyan('remote')} setup|status|sync|pull Connect to hosted vault (cloud sync)
|
|
418
|
+
${cyan('team')} join|leave|status|browse Join or manage a team vault
|
|
419
|
+
${cyan('public')} create|seed|list|add Manage and consume public vaults
|
|
417
420
|
${cyan('update')} Check for and install updates
|
|
418
421
|
${cyan('uninstall')} Remove MCP configs and optionally data
|
|
419
422
|
`);
|
|
@@ -2783,8 +2786,8 @@ async function runStatus() {
|
|
|
2783
2786
|
const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
|
|
2784
2787
|
const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
2785
2788
|
const countStr = String(c).padStart(4);
|
|
2786
|
-
const IRREGULAR_PLURALS = { activity: 'activities', inbox: 'inboxes', index: 'indexes', match: 'matches' };
|
|
2787
|
-
const plural = IRREGULAR_PLURALS[kind] || (kind.endsWith('s') ? kind : kind + 's');
|
|
2789
|
+
const IRREGULAR_PLURALS = { activity: 'activities', inbox: 'inboxes', index: 'indexes', match: 'matches', entity: 'entities', summary: 'summaries' };
|
|
2790
|
+
const plural = IRREGULAR_PLURALS[kind] || (kind.endsWith('y') ? kind.slice(0, -1) + 'ies' : kind.endsWith('s') || kind.endsWith('x') || kind.endsWith('ch') || kind.endsWith('sh') ? kind + 'es' : kind + 's');
|
|
2788
2791
|
console.log(` ${dim(bar)} ${countStr} ${plural}`);
|
|
2789
2792
|
}
|
|
2790
2793
|
} else {
|
|
@@ -4390,6 +4393,35 @@ async function runPostToolCall() {
|
|
|
4390
4393
|
}
|
|
4391
4394
|
|
|
4392
4395
|
async function runSave() {
|
|
4396
|
+
if (flags.has('--help')) {
|
|
4397
|
+
console.log(`
|
|
4398
|
+
${bold('context-vault save')}
|
|
4399
|
+
|
|
4400
|
+
Save an entry to the vault from CLI.
|
|
4401
|
+
|
|
4402
|
+
${bold('Required:')}
|
|
4403
|
+
--kind <kind> Entry kind (insight, decision, pattern, reference, event)
|
|
4404
|
+
--title <title> Entry title
|
|
4405
|
+
|
|
4406
|
+
${bold('Content')} (one of):
|
|
4407
|
+
--body <text> Inline body text
|
|
4408
|
+
--file <path> Read body from a file
|
|
4409
|
+
(stdin) Pipe content via stdin
|
|
4410
|
+
|
|
4411
|
+
${bold('Optional:')}
|
|
4412
|
+
--tags <a,b,c> Comma-separated tags
|
|
4413
|
+
--tier <tier> working (default) or durable
|
|
4414
|
+
--source <source> Source label (default: cli)
|
|
4415
|
+
--identity-key <key> Identity key (for entity kinds)
|
|
4416
|
+
--meta <json> Additional metadata as JSON
|
|
4417
|
+
|
|
4418
|
+
${bold('Examples:')}
|
|
4419
|
+
context-vault save --kind insight --title "Express 5 gotcha" --body "body parser changed"
|
|
4420
|
+
echo "content" | context-vault save --kind reference --title "API notes"
|
|
4421
|
+
`);
|
|
4422
|
+
return;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4393
4425
|
const kind = getFlag('--kind');
|
|
4394
4426
|
const title = getFlag('--title');
|
|
4395
4427
|
const tags = getFlag('--tags');
|
|
@@ -4487,6 +4519,30 @@ async function runSave() {
|
|
|
4487
4519
|
}
|
|
4488
4520
|
|
|
4489
4521
|
async function runSearch() {
|
|
4522
|
+
if (flags.has('--help')) {
|
|
4523
|
+
console.log(`
|
|
4524
|
+
${bold('context-vault search')} <query> [options]
|
|
4525
|
+
|
|
4526
|
+
Search vault entries from CLI using hybrid semantic + full-text search.
|
|
4527
|
+
|
|
4528
|
+
${bold('Usage:')}
|
|
4529
|
+
context-vault search "express middleware"
|
|
4530
|
+
context-vault search --kind insight --tags "bucket:myproject"
|
|
4531
|
+
context-vault search "auth" --limit 20 --format json
|
|
4532
|
+
|
|
4533
|
+
${bold('Options:')}
|
|
4534
|
+
--kind <kind> Filter by kind (insight, decision, pattern, reference, event)
|
|
4535
|
+
--tags <a,b,c> Filter by tags (comma-separated)
|
|
4536
|
+
--limit <n> Max results (default: 10)
|
|
4537
|
+
--sort <mode> Sort by: relevance (default), recent
|
|
4538
|
+
--format <fmt> Output format: plain (default), json
|
|
4539
|
+
--scope <scope> Search scope: hot (default), events, all
|
|
4540
|
+
--full Show full entry body (not truncated)
|
|
4541
|
+
--query <text> Explicit query (alternative to positional args)
|
|
4542
|
+
`);
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4490
4546
|
const kind = getFlag('--kind');
|
|
4491
4547
|
const tagsStr = getFlag('--tags');
|
|
4492
4548
|
const limit = parseInt(getFlag('--limit') || '10', 10);
|
|
@@ -7040,6 +7096,1119 @@ async function runServe() {
|
|
|
7040
7096
|
await import('../dist/server.js');
|
|
7041
7097
|
}
|
|
7042
7098
|
|
|
7099
|
+
async function runTeam() {
|
|
7100
|
+
const subcommand = args[1];
|
|
7101
|
+
const { getRemoteConfig, saveRemoteConfig } = await import('@context-vault/core/config');
|
|
7102
|
+
const dataDir = join(HOME, '.context-mcp');
|
|
7103
|
+
|
|
7104
|
+
if (subcommand === 'join') {
|
|
7105
|
+
const teamId = args[2];
|
|
7106
|
+
if (!teamId) {
|
|
7107
|
+
console.error(`\n ${red('Usage:')} context-vault team join <team-id>\n`);
|
|
7108
|
+
process.exit(1);
|
|
7109
|
+
}
|
|
7110
|
+
|
|
7111
|
+
const remote = getRemoteConfig(dataDir);
|
|
7112
|
+
if (!remote || !remote.enabled || !remote.apiKey) {
|
|
7113
|
+
console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
|
|
7114
|
+
process.exit(1);
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7117
|
+
console.log(`\n Testing connection to team ${dim(teamId)}...`);
|
|
7118
|
+
try {
|
|
7119
|
+
const res = await fetch(`${remote.url.replace(/\/$/, '')}/api/team/${teamId}/status`, {
|
|
7120
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7121
|
+
signal: AbortSignal.timeout(10000),
|
|
7122
|
+
});
|
|
7123
|
+
if (res.ok) {
|
|
7124
|
+
const data = await res.json();
|
|
7125
|
+
saveRemoteConfig({ teamId }, dataDir);
|
|
7126
|
+
console.log(` ${green('✓')} Joined team: ${bold(data.name || teamId)}`);
|
|
7127
|
+
if (data.entry_count != null) {
|
|
7128
|
+
console.log(` ${dim(`${data.entry_count} entries, ${data.member_count || '?'} members`)}`);
|
|
7129
|
+
}
|
|
7130
|
+
} else {
|
|
7131
|
+
const text = await res.text().catch(() => '');
|
|
7132
|
+
console.error(` ${red('✘')} Failed to connect to team: HTTP ${res.status}`);
|
|
7133
|
+
if (text) console.error(` ${dim(text.slice(0, 200))}`);
|
|
7134
|
+
process.exit(1);
|
|
7135
|
+
}
|
|
7136
|
+
} catch (e) {
|
|
7137
|
+
console.error(` ${red('✘')} Connection failed: ${e.message}`);
|
|
7138
|
+
process.exit(1);
|
|
7139
|
+
}
|
|
7140
|
+
console.log();
|
|
7141
|
+
return;
|
|
7142
|
+
}
|
|
7143
|
+
|
|
7144
|
+
if (subcommand === 'leave') {
|
|
7145
|
+
const remote = getRemoteConfig(dataDir);
|
|
7146
|
+
if (!remote?.teamId) {
|
|
7147
|
+
console.log(`\n ${dim('Not currently in a team.')}\n`);
|
|
7148
|
+
return;
|
|
7149
|
+
}
|
|
7150
|
+
saveRemoteConfig({ teamId: '' }, dataDir);
|
|
7151
|
+
console.log(`\n ${green('✓')} Left team. Team vault queries disabled.\n`);
|
|
7152
|
+
return;
|
|
7153
|
+
}
|
|
7154
|
+
|
|
7155
|
+
if (subcommand === 'status') {
|
|
7156
|
+
const remote = getRemoteConfig(dataDir);
|
|
7157
|
+
console.log();
|
|
7158
|
+
console.log(` ${bold('◇ Team Status')}`);
|
|
7159
|
+
console.log();
|
|
7160
|
+
|
|
7161
|
+
if (!remote || !remote.teamId) {
|
|
7162
|
+
console.log(` Team: ${dim('not joined')}`);
|
|
7163
|
+
console.log(` ${dim('Run')} ${cyan('context-vault team join <team-id>')} ${dim('to connect.')}`);
|
|
7164
|
+
console.log();
|
|
7165
|
+
return;
|
|
7166
|
+
}
|
|
7167
|
+
|
|
7168
|
+
console.log(` Team ID: ${remote.teamId}`);
|
|
7169
|
+
|
|
7170
|
+
if (remote.enabled && remote.apiKey) {
|
|
7171
|
+
console.log(` Checking status...`);
|
|
7172
|
+
try {
|
|
7173
|
+
const res = await fetch(`${remote.url.replace(/\/$/, '')}/api/team/${remote.teamId}/status`, {
|
|
7174
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7175
|
+
signal: AbortSignal.timeout(10000),
|
|
7176
|
+
});
|
|
7177
|
+
if (res.ok) {
|
|
7178
|
+
const data = await res.json();
|
|
7179
|
+
console.log(` Name: ${data.name || dim('(unnamed)')}`);
|
|
7180
|
+
console.log(` Members: ${data.member_count ?? dim('?')}`);
|
|
7181
|
+
console.log(` Entries: ${data.entry_count ?? dim('?')}`);
|
|
7182
|
+
console.log(` Health: ${green('connected')}`);
|
|
7183
|
+
} else {
|
|
7184
|
+
console.log(` Health: ${red('error')} (HTTP ${res.status})`);
|
|
7185
|
+
}
|
|
7186
|
+
} catch (e) {
|
|
7187
|
+
console.log(` Health: ${red('unreachable')} (${e.message})`);
|
|
7188
|
+
}
|
|
7189
|
+
} else {
|
|
7190
|
+
console.log(` Health: ${yellow('remote not configured')}`);
|
|
7191
|
+
}
|
|
7192
|
+
console.log();
|
|
7193
|
+
return;
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
if (subcommand === 'seed') {
|
|
7197
|
+
const teamIdFlag = getFlag('--team');
|
|
7198
|
+
const tagsFlag = getFlag('--tags');
|
|
7199
|
+
const remote = getRemoteConfig(dataDir);
|
|
7200
|
+
|
|
7201
|
+
if (!remote || !remote.enabled || !remote.apiKey) {
|
|
7202
|
+
console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
|
|
7203
|
+
process.exit(1);
|
|
7204
|
+
}
|
|
7205
|
+
|
|
7206
|
+
const teamId = teamIdFlag || remote.teamId;
|
|
7207
|
+
if (!teamId) {
|
|
7208
|
+
console.error(`\n ${red('✘')} No team ID. Pass ${cyan('--team <id>')} or join a team first.\n`);
|
|
7209
|
+
process.exit(1);
|
|
7210
|
+
}
|
|
7211
|
+
|
|
7212
|
+
// Load local vault DB
|
|
7213
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
7214
|
+
const config = resolveConfig();
|
|
7215
|
+
|
|
7216
|
+
let DatabaseSync;
|
|
7217
|
+
try {
|
|
7218
|
+
DatabaseSync = (await import('node:sqlite')).DatabaseSync;
|
|
7219
|
+
} catch {
|
|
7220
|
+
console.error(`\n ${red('✘')} Node.js SQLite not available. Requires Node >= 22.5.\n`);
|
|
7221
|
+
process.exit(1);
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
const db = new DatabaseSync(config.dbPath, { open: true });
|
|
7225
|
+
|
|
7226
|
+
// Build query based on tag filter
|
|
7227
|
+
let sql = 'SELECT id, kind, category, title, body, tags, meta, source, identity_key, tier FROM vault WHERE superseded_by IS NULL';
|
|
7228
|
+
const params = [];
|
|
7229
|
+
|
|
7230
|
+
if (tagsFlag) {
|
|
7231
|
+
sql += ' AND tags LIKE ?';
|
|
7232
|
+
params.push(`%"${tagsFlag}"%`);
|
|
7233
|
+
}
|
|
7234
|
+
|
|
7235
|
+
const rows = db.prepare(sql).all(...params);
|
|
7236
|
+
db.close();
|
|
7237
|
+
|
|
7238
|
+
let publishedKnowledge = 0;
|
|
7239
|
+
let federatedEntities = 0;
|
|
7240
|
+
let skippedEvents = 0;
|
|
7241
|
+
let blockedPrivacy = 0;
|
|
7242
|
+
const blockedEntries = [];
|
|
7243
|
+
let errors = 0;
|
|
7244
|
+
|
|
7245
|
+
const apiUrl = remote.url.replace(/\/$/, '');
|
|
7246
|
+
|
|
7247
|
+
console.log();
|
|
7248
|
+
console.log(` ${bold('◇ Team Seed')}`);
|
|
7249
|
+
console.log(` Team: ${teamId}`);
|
|
7250
|
+
console.log(` Filter: ${tagsFlag || dim('(all entries)')}`);
|
|
7251
|
+
console.log(` Entries: ${rows.length}`);
|
|
7252
|
+
console.log();
|
|
7253
|
+
|
|
7254
|
+
if (isDryRun) {
|
|
7255
|
+
for (const row of rows) {
|
|
7256
|
+
if (row.category === 'event') {
|
|
7257
|
+
skippedEvents++;
|
|
7258
|
+
} else if (row.category === 'entity') {
|
|
7259
|
+
federatedEntities++;
|
|
7260
|
+
} else {
|
|
7261
|
+
publishedKnowledge++;
|
|
7262
|
+
}
|
|
7263
|
+
}
|
|
7264
|
+
console.log(` ${dim('[dry run]')}`);
|
|
7265
|
+
console.log(` Would publish: ${green(publishedKnowledge)} knowledge, ${cyan(federatedEntities)} entities`);
|
|
7266
|
+
console.log(` Would skip: ${dim(skippedEvents)} events (private)`);
|
|
7267
|
+
console.log();
|
|
7268
|
+
return;
|
|
7269
|
+
}
|
|
7270
|
+
|
|
7271
|
+
for (const row of rows) {
|
|
7272
|
+
if (row.category === 'event') {
|
|
7273
|
+
skippedEvents++;
|
|
7274
|
+
continue;
|
|
7275
|
+
}
|
|
7276
|
+
|
|
7277
|
+
const tags = row.tags ? JSON.parse(row.tags) : [];
|
|
7278
|
+
const meta = row.meta ? JSON.parse(row.meta) : {};
|
|
7279
|
+
|
|
7280
|
+
try {
|
|
7281
|
+
const res = await fetch(`${apiUrl}/api/vault/publish`, {
|
|
7282
|
+
method: 'POST',
|
|
7283
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7284
|
+
signal: AbortSignal.timeout(15000),
|
|
7285
|
+
body: JSON.stringify({
|
|
7286
|
+
entryId: row.id,
|
|
7287
|
+
teamId,
|
|
7288
|
+
visibility: 'team',
|
|
7289
|
+
kind: row.kind,
|
|
7290
|
+
title: row.title,
|
|
7291
|
+
body: row.body,
|
|
7292
|
+
tags,
|
|
7293
|
+
meta,
|
|
7294
|
+
source: row.source,
|
|
7295
|
+
identity_key: row.identity_key,
|
|
7296
|
+
tier: row.tier,
|
|
7297
|
+
category: row.category,
|
|
7298
|
+
}),
|
|
7299
|
+
});
|
|
7300
|
+
|
|
7301
|
+
if (res.ok) {
|
|
7302
|
+
if (row.category === 'entity') {
|
|
7303
|
+
federatedEntities++;
|
|
7304
|
+
} else {
|
|
7305
|
+
publishedKnowledge++;
|
|
7306
|
+
}
|
|
7307
|
+
} else if (res.status === 422) {
|
|
7308
|
+
const data = await res.json().catch(() => ({}));
|
|
7309
|
+
if (data.code === 'PRIVACY_SCAN_FAILED') {
|
|
7310
|
+
const matchTypes = Array.isArray(data.matches) ? data.matches.map(m => m.type) : [];
|
|
7311
|
+
const uniqueTypes = [...new Set(matchTypes)];
|
|
7312
|
+
console.log(` ${yellow('!')} Blocked: "${row.title || row.id}" (${uniqueTypes.join(', ') || 'sensitive content'})`);
|
|
7313
|
+
blockedPrivacy++;
|
|
7314
|
+
blockedEntries.push({ title: row.title || row.id, types: uniqueTypes });
|
|
7315
|
+
} else {
|
|
7316
|
+
errors++;
|
|
7317
|
+
console.error(` ${red('!')} 422 for "${row.title || row.id}": ${data.error || 'unknown'}`);
|
|
7318
|
+
}
|
|
7319
|
+
} else {
|
|
7320
|
+
const data = await res.json().catch(() => ({}));
|
|
7321
|
+
if (data.conflict) {
|
|
7322
|
+
console.log(` ${yellow('!')} Conflict for "${row.title || row.id}": ${data.conflict.suggestion || 'similar entry exists'}`);
|
|
7323
|
+
}
|
|
7324
|
+
if (row.category === 'entity') {
|
|
7325
|
+
federatedEntities++;
|
|
7326
|
+
} else {
|
|
7327
|
+
publishedKnowledge++;
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
} catch (e) {
|
|
7331
|
+
errors++;
|
|
7332
|
+
console.error(` ${red('✘')} Failed: ${row.title || row.id} (${e.message})`);
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
// Progress indicator every 10 entries
|
|
7336
|
+
const processed = publishedKnowledge + federatedEntities + skippedEvents + blockedPrivacy + errors;
|
|
7337
|
+
if (processed % 10 === 0) {
|
|
7338
|
+
process.stdout.write(` ${dim(`${processed}/${rows.length}...`)}\r`);
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
|
|
7342
|
+
console.log(` Seeded: ${green(publishedKnowledge)} published, ${cyan(federatedEntities)} federated, ${dim(skippedEvents)} skipped (events), ${blockedPrivacy > 0 ? yellow(blockedPrivacy) : dim(blockedPrivacy)} blocked (privacy scan)`);
|
|
7343
|
+
if (errors > 0) {
|
|
7344
|
+
console.log(` Errors: ${red(errors)}`);
|
|
7345
|
+
}
|
|
7346
|
+
if (blockedEntries.length > 0) {
|
|
7347
|
+
console.log();
|
|
7348
|
+
console.log(` ${yellow('Blocked entries (review and clean before re-seeding):')}`);
|
|
7349
|
+
for (const entry of blockedEntries) {
|
|
7350
|
+
console.log(` - ${entry.title} [${entry.types.join(', ')}]`);
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
console.log();
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
|
|
7357
|
+
// Default: show team help
|
|
7358
|
+
console.log();
|
|
7359
|
+
console.log(` ${bold('◇ context-vault team')}`);
|
|
7360
|
+
console.log();
|
|
7361
|
+
console.log(` ${cyan('join <team-id>')} Join a team vault`);
|
|
7362
|
+
console.log(` ${cyan('leave')} Leave the current team`);
|
|
7363
|
+
console.log(` ${cyan('status')} Show team connection status`);
|
|
7364
|
+
console.log(` ${cyan('seed')} Publish local entries to team vault`);
|
|
7365
|
+
console.log(` ${dim('--team <id>')} Team ID (defaults to joined team)`);
|
|
7366
|
+
console.log(` ${dim('--tags <filter>')} Filter entries by tag (e.g. bucket:stormfors)`);
|
|
7367
|
+
console.log(` ${dim('--dry-run')} Preview without publishing`);
|
|
7368
|
+
console.log();
|
|
7369
|
+
}
|
|
7370
|
+
|
|
7371
|
+
async function runPublic() {
|
|
7372
|
+
const subcommand = args[1];
|
|
7373
|
+
const { getRemoteConfig, saveRemoteConfig } = await import('@context-vault/core/config');
|
|
7374
|
+
const dataDir = join(HOME, '.context-mcp');
|
|
7375
|
+
|
|
7376
|
+
if (subcommand === 'create') {
|
|
7377
|
+
const name = args[2];
|
|
7378
|
+
const slug = getFlag('--slug') || (name ? name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') : null);
|
|
7379
|
+
const description = getFlag('--description') || '';
|
|
7380
|
+
const domainTags = getFlag('--domains') ? getFlag('--domains').split(',').map(s => s.trim()) : [];
|
|
7381
|
+
const visibility = getFlag('--visibility') || 'free';
|
|
7382
|
+
|
|
7383
|
+
if (!name || !slug) {
|
|
7384
|
+
console.error(`\n ${red('Usage:')} context-vault public create <name> [--slug <slug>] [--description <desc>] [--domains <tag1,tag2>] [--visibility free|pro]\n`);
|
|
7385
|
+
process.exit(1);
|
|
7386
|
+
}
|
|
7387
|
+
|
|
7388
|
+
const remote = getRemoteConfig(dataDir);
|
|
7389
|
+
if (!remote || !remote.enabled || !remote.apiKey) {
|
|
7390
|
+
console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
|
|
7391
|
+
process.exit(1);
|
|
7392
|
+
}
|
|
7393
|
+
|
|
7394
|
+
console.log(`\n Creating public vault "${bold(name)}" (${slug})...`);
|
|
7395
|
+
try {
|
|
7396
|
+
const res = await fetch(`${remote.url.replace(/\/$/, '')}/api/public/vaults`, {
|
|
7397
|
+
method: 'POST',
|
|
7398
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7399
|
+
signal: AbortSignal.timeout(15000),
|
|
7400
|
+
body: JSON.stringify({ name, slug, description, domain_tags: domainTags, visibility }),
|
|
7401
|
+
});
|
|
7402
|
+
if (res.ok) {
|
|
7403
|
+
const data = await res.json();
|
|
7404
|
+
console.log(` ${green('✓')} Created public vault: ${bold(data.slug || slug)}`);
|
|
7405
|
+
console.log(` Visibility: ${visibility}`);
|
|
7406
|
+
if (domainTags.length) console.log(` Domains: ${domainTags.join(', ')}`);
|
|
7407
|
+
} else {
|
|
7408
|
+
const data = await res.json().catch(() => ({}));
|
|
7409
|
+
console.error(` ${red('✘')} Failed: ${data.error || `HTTP ${res.status}`}`);
|
|
7410
|
+
process.exit(1);
|
|
7411
|
+
}
|
|
7412
|
+
} catch (e) {
|
|
7413
|
+
console.error(` ${red('✘')} Connection failed: ${e.message}`);
|
|
7414
|
+
process.exit(1);
|
|
7415
|
+
}
|
|
7416
|
+
console.log();
|
|
7417
|
+
return;
|
|
7418
|
+
}
|
|
7419
|
+
|
|
7420
|
+
if (subcommand === 'seed') {
|
|
7421
|
+
const vaultSlug = getFlag('--vault');
|
|
7422
|
+
const tagsFlag = getFlag('--tags');
|
|
7423
|
+
const remote = getRemoteConfig(dataDir);
|
|
7424
|
+
|
|
7425
|
+
if (!remote || !remote.enabled || !remote.apiKey) {
|
|
7426
|
+
console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
|
|
7427
|
+
process.exit(1);
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
if (!vaultSlug) {
|
|
7431
|
+
console.error(`\n ${red('Usage:')} context-vault public seed --vault <slug> [--tags <filter>] [--dry-run]\n`);
|
|
7432
|
+
process.exit(1);
|
|
7433
|
+
}
|
|
7434
|
+
|
|
7435
|
+
// Load local vault DB
|
|
7436
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
7437
|
+
const config = resolveConfig();
|
|
7438
|
+
|
|
7439
|
+
let DatabaseSync;
|
|
7440
|
+
try {
|
|
7441
|
+
DatabaseSync = (await import('node:sqlite')).DatabaseSync;
|
|
7442
|
+
} catch {
|
|
7443
|
+
console.error(`\n ${red('✘')} Node.js SQLite not available. Requires Node >= 22.5.\n`);
|
|
7444
|
+
process.exit(1);
|
|
7445
|
+
}
|
|
7446
|
+
|
|
7447
|
+
const db = new DatabaseSync(config.dbPath, { open: true });
|
|
7448
|
+
|
|
7449
|
+
let sql = 'SELECT id, kind, category, title, body, tags, meta, source, identity_key, tier FROM vault WHERE superseded_by IS NULL';
|
|
7450
|
+
const params = [];
|
|
7451
|
+
if (tagsFlag) {
|
|
7452
|
+
sql += ' AND tags LIKE ?';
|
|
7453
|
+
params.push(`%"${tagsFlag}"%`);
|
|
7454
|
+
}
|
|
7455
|
+
|
|
7456
|
+
const rows = db.prepare(sql).all(...params);
|
|
7457
|
+
db.close();
|
|
7458
|
+
|
|
7459
|
+
let publishedKnowledge = 0;
|
|
7460
|
+
let publishedEntities = 0;
|
|
7461
|
+
let skippedEvents = 0;
|
|
7462
|
+
let blockedPrivacy = 0;
|
|
7463
|
+
const blockedEntries = [];
|
|
7464
|
+
let errors = 0;
|
|
7465
|
+
|
|
7466
|
+
const apiUrl = remote.url.replace(/\/$/, '');
|
|
7467
|
+
|
|
7468
|
+
console.log();
|
|
7469
|
+
console.log(` ${bold('◇ Public Vault Seed')}`);
|
|
7470
|
+
console.log(` Vault: ${vaultSlug}`);
|
|
7471
|
+
console.log(` Filter: ${tagsFlag || dim('(all entries)')}`);
|
|
7472
|
+
console.log(` Entries: ${rows.length}`);
|
|
7473
|
+
console.log();
|
|
7474
|
+
|
|
7475
|
+
if (isDryRun) {
|
|
7476
|
+
for (const row of rows) {
|
|
7477
|
+
if (row.category === 'event') skippedEvents++;
|
|
7478
|
+
else if (row.category === 'entity') publishedEntities++;
|
|
7479
|
+
else publishedKnowledge++;
|
|
7480
|
+
}
|
|
7481
|
+
console.log(` ${dim('[dry run]')}`);
|
|
7482
|
+
console.log(` Would publish: ${green(publishedKnowledge)} knowledge, ${cyan(publishedEntities)} entities`);
|
|
7483
|
+
console.log(` Would skip: ${dim(skippedEvents)} events (blocked for public)`);
|
|
7484
|
+
console.log();
|
|
7485
|
+
return;
|
|
7486
|
+
}
|
|
7487
|
+
|
|
7488
|
+
for (const row of rows) {
|
|
7489
|
+
if (row.category === 'event') {
|
|
7490
|
+
skippedEvents++;
|
|
7491
|
+
continue;
|
|
7492
|
+
}
|
|
7493
|
+
|
|
7494
|
+
const tags = row.tags ? JSON.parse(row.tags) : [];
|
|
7495
|
+
const meta = row.meta ? JSON.parse(row.meta) : {};
|
|
7496
|
+
|
|
7497
|
+
try {
|
|
7498
|
+
const res = await fetch(`${apiUrl}/api/public/${vaultSlug}/entries`, {
|
|
7499
|
+
method: 'POST',
|
|
7500
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7501
|
+
signal: AbortSignal.timeout(15000),
|
|
7502
|
+
body: JSON.stringify({
|
|
7503
|
+
kind: row.kind,
|
|
7504
|
+
title: row.title,
|
|
7505
|
+
body: row.body,
|
|
7506
|
+
tags,
|
|
7507
|
+
meta,
|
|
7508
|
+
source: row.source,
|
|
7509
|
+
identity_key: row.identity_key,
|
|
7510
|
+
tier: row.tier,
|
|
7511
|
+
category: row.category,
|
|
7512
|
+
}),
|
|
7513
|
+
});
|
|
7514
|
+
|
|
7515
|
+
if (res.ok) {
|
|
7516
|
+
if (row.category === 'entity') publishedEntities++;
|
|
7517
|
+
else publishedKnowledge++;
|
|
7518
|
+
} else if (res.status === 422) {
|
|
7519
|
+
const data = await res.json().catch(() => ({}));
|
|
7520
|
+
if (data.code === 'PRIVACY_SCAN_FAILED') {
|
|
7521
|
+
const matchTypes = Array.isArray(data.matches) ? data.matches.map(m => m.type) : [];
|
|
7522
|
+
const uniqueTypes = [...new Set(matchTypes)];
|
|
7523
|
+
console.log(` ${yellow('!')} Blocked: "${row.title || row.id}" (${uniqueTypes.join(', ') || 'sensitive content'})`);
|
|
7524
|
+
blockedPrivacy++;
|
|
7525
|
+
blockedEntries.push({ title: row.title || row.id, types: uniqueTypes });
|
|
7526
|
+
} else {
|
|
7527
|
+
errors++;
|
|
7528
|
+
console.error(` ${red('!')} 422 for "${row.title || row.id}": ${data.error || 'unknown'}`);
|
|
7529
|
+
}
|
|
7530
|
+
} else {
|
|
7531
|
+
errors++;
|
|
7532
|
+
const text = await res.text().catch(() => '');
|
|
7533
|
+
console.error(` ${red('!')} HTTP ${res.status} for "${row.title || row.id}": ${text.slice(0, 200)}`);
|
|
7534
|
+
}
|
|
7535
|
+
} catch (e) {
|
|
7536
|
+
errors++;
|
|
7537
|
+
console.error(` ${red('✘')} Failed: ${row.title || row.id} (${e.message})`);
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
const processed = publishedKnowledge + publishedEntities + skippedEvents + blockedPrivacy + errors;
|
|
7541
|
+
if (processed % 10 === 0) {
|
|
7542
|
+
process.stdout.write(` ${dim(`${processed}/${rows.length}...`)}\r`);
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
|
|
7546
|
+
console.log(` Seeded: ${green(publishedKnowledge)} knowledge, ${cyan(publishedEntities)} entities, ${dim(skippedEvents)} skipped (events), ${blockedPrivacy > 0 ? yellow(blockedPrivacy) : dim(blockedPrivacy)} blocked (privacy)`);
|
|
7547
|
+
if (errors > 0) console.log(` Errors: ${red(errors)}`);
|
|
7548
|
+
if (blockedEntries.length > 0) {
|
|
7549
|
+
console.log();
|
|
7550
|
+
console.log(` ${yellow('Blocked entries (review and clean before re-seeding):')}`);
|
|
7551
|
+
for (const entry of blockedEntries) {
|
|
7552
|
+
console.log(` - ${entry.title} [${entry.types.join(', ')}]`);
|
|
7553
|
+
}
|
|
7554
|
+
}
|
|
7555
|
+
console.log();
|
|
7556
|
+
return;
|
|
7557
|
+
}
|
|
7558
|
+
|
|
7559
|
+
if (subcommand === 'list') {
|
|
7560
|
+
const domain = getFlag('--domain');
|
|
7561
|
+
const remote = getRemoteConfig(dataDir);
|
|
7562
|
+
|
|
7563
|
+
const apiUrl = remote?.url?.replace(/\/$/, '') || 'https://api.context-vault.com';
|
|
7564
|
+
const headers = {};
|
|
7565
|
+
if (remote?.apiKey) headers['Authorization'] = `Bearer ${remote.apiKey}`;
|
|
7566
|
+
headers['Content-Type'] = 'application/json';
|
|
7567
|
+
|
|
7568
|
+
const query = new URLSearchParams();
|
|
7569
|
+
if (domain) query.set('domain', domain);
|
|
7570
|
+
|
|
7571
|
+
console.log();
|
|
7572
|
+
console.log(` ${bold('◇ Public Vaults')}`);
|
|
7573
|
+
console.log();
|
|
7574
|
+
|
|
7575
|
+
try {
|
|
7576
|
+
const res = await fetch(`${apiUrl}/api/public/vaults?${query.toString()}`, {
|
|
7577
|
+
headers,
|
|
7578
|
+
signal: AbortSignal.timeout(10000),
|
|
7579
|
+
});
|
|
7580
|
+
if (res.ok) {
|
|
7581
|
+
const data = await res.json();
|
|
7582
|
+
const vaults = Array.isArray(data.vaults) ? data.vaults : Array.isArray(data) ? data : [];
|
|
7583
|
+
if (vaults.length === 0) {
|
|
7584
|
+
console.log(` ${dim('No public vaults found.')}`);
|
|
7585
|
+
} else {
|
|
7586
|
+
for (const v of vaults) {
|
|
7587
|
+
const domains = Array.isArray(v.domain_tags) ? v.domain_tags.join(', ') : '';
|
|
7588
|
+
console.log(` ${bold(v.name || v.slug)} ${dim(`(${v.slug})`)}`);
|
|
7589
|
+
if (v.description) console.log(` ${v.description}`);
|
|
7590
|
+
const stats = [`${v.consumer_count ?? 0} consumers`, `${v.total_recalls ?? 0} recalls`];
|
|
7591
|
+
if (domains) stats.push(domains);
|
|
7592
|
+
console.log(` ${dim(stats.join(' · '))}`);
|
|
7593
|
+
console.log();
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
} else {
|
|
7597
|
+
console.error(` ${red('✘')} Failed to list vaults: HTTP ${res.status}`);
|
|
7598
|
+
}
|
|
7599
|
+
} catch (e) {
|
|
7600
|
+
console.error(` ${red('✘')} Connection failed: ${e.message}`);
|
|
7601
|
+
}
|
|
7602
|
+
console.log();
|
|
7603
|
+
return;
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
if (subcommand === 'add') {
|
|
7607
|
+
const slug = args[2];
|
|
7608
|
+
if (!slug) {
|
|
7609
|
+
console.error(`\n ${red('Usage:')} context-vault public add <slug>\n`);
|
|
7610
|
+
process.exit(1);
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
const configPath = join(dataDir, 'config.json');
|
|
7614
|
+
let config = {};
|
|
7615
|
+
try {
|
|
7616
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
7617
|
+
} catch {}
|
|
7618
|
+
|
|
7619
|
+
if (!config.remote) config.remote = {};
|
|
7620
|
+
if (!Array.isArray(config.remote.publicVaults)) config.remote.publicVaults = [];
|
|
7621
|
+
|
|
7622
|
+
if (config.remote.publicVaults.includes(slug)) {
|
|
7623
|
+
console.log(`\n ${dim('Already added:')} ${slug}\n`);
|
|
7624
|
+
return;
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
config.remote.publicVaults.push(slug);
|
|
7628
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
7629
|
+
console.log(`\n ${green('✓')} Added public vault: ${bold(slug)}`);
|
|
7630
|
+
console.log(` Your agent will now query this vault in get_context, session_start, and recall.\n`);
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
if (subcommand === 'remove') {
|
|
7635
|
+
const slug = args[2];
|
|
7636
|
+
if (!slug) {
|
|
7637
|
+
console.error(`\n ${red('Usage:')} context-vault public remove <slug>\n`);
|
|
7638
|
+
process.exit(1);
|
|
7639
|
+
}
|
|
7640
|
+
|
|
7641
|
+
const configPath = join(dataDir, 'config.json');
|
|
7642
|
+
let config = {};
|
|
7643
|
+
try {
|
|
7644
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
7645
|
+
} catch {}
|
|
7646
|
+
|
|
7647
|
+
if (!config.remote?.publicVaults || !config.remote.publicVaults.includes(slug)) {
|
|
7648
|
+
console.log(`\n ${dim('Not found:')} ${slug}\n`);
|
|
7649
|
+
return;
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
config.remote.publicVaults = config.remote.publicVaults.filter(s => s !== slug);
|
|
7653
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
7654
|
+
console.log(`\n ${green('✓')} Removed public vault: ${bold(slug)}\n`);
|
|
7655
|
+
return;
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7658
|
+
// Default: show public help
|
|
7659
|
+
console.log();
|
|
7660
|
+
console.log(` ${bold('◇ context-vault public')}`);
|
|
7661
|
+
console.log();
|
|
7662
|
+
console.log(` ${cyan('create <name>')} Create a new public vault`);
|
|
7663
|
+
console.log(` ${dim('--slug <slug>')} URL-friendly identifier`);
|
|
7664
|
+
console.log(` ${dim('--description <d>')} Vault description`);
|
|
7665
|
+
console.log(` ${dim('--domains <tags>')} Comma-separated domain tags`);
|
|
7666
|
+
console.log(` ${dim('--visibility <v>')} free (default) or pro`);
|
|
7667
|
+
console.log(` ${cyan('seed')} Publish local entries to a public vault`);
|
|
7668
|
+
console.log(` ${dim('--vault <slug>')} Target public vault slug (required)`);
|
|
7669
|
+
console.log(` ${dim('--tags <filter>')} Filter entries by tag`);
|
|
7670
|
+
console.log(` ${dim('--dry-run')} Preview without publishing`);
|
|
7671
|
+
console.log(` ${cyan('list')} Browse available public vaults`);
|
|
7672
|
+
console.log(` ${dim('--domain <tag>')} Filter by domain tag`);
|
|
7673
|
+
console.log(` ${cyan('add <slug>')} Add a public vault to your agent config`);
|
|
7674
|
+
console.log(` ${cyan('remove <slug>')} Remove a public vault from your config`);
|
|
7675
|
+
console.log();
|
|
7676
|
+
}
|
|
7677
|
+
|
|
7678
|
+
async function runRemote() {
|
|
7679
|
+
const subcommand = args[1];
|
|
7680
|
+
const { getRemoteConfig, saveRemoteConfig } = await import('@context-vault/core/config');
|
|
7681
|
+
const dataDir = join(HOME, '.context-mcp');
|
|
7682
|
+
|
|
7683
|
+
if (subcommand === 'setup') {
|
|
7684
|
+
const readline = await import('node:readline');
|
|
7685
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7686
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
7687
|
+
|
|
7688
|
+
console.log();
|
|
7689
|
+
console.log(` ${bold('◇ Remote Vault Setup')}`);
|
|
7690
|
+
console.log();
|
|
7691
|
+
|
|
7692
|
+
const defaultUrl = 'https://api.context-vault.com';
|
|
7693
|
+
const urlInput = await ask(` API URL ${dim(`(${defaultUrl})`)}: `);
|
|
7694
|
+
const url = urlInput.trim() || defaultUrl;
|
|
7695
|
+
|
|
7696
|
+
const apiKey = await ask(' API Key: ');
|
|
7697
|
+
rl.close();
|
|
7698
|
+
|
|
7699
|
+
if (!apiKey.trim()) {
|
|
7700
|
+
console.error(`\n ${red('✘')} API key is required.`);
|
|
7701
|
+
process.exit(1);
|
|
7702
|
+
}
|
|
7703
|
+
|
|
7704
|
+
console.log(`\n Testing connection to ${dim(url)}...`);
|
|
7705
|
+
try {
|
|
7706
|
+
const res = await fetch(`${url.replace(/\/$/, '')}/api/vault/status`, {
|
|
7707
|
+
headers: { 'Authorization': `Bearer ${apiKey.trim()}`, 'Content-Type': 'application/json' },
|
|
7708
|
+
signal: AbortSignal.timeout(10000),
|
|
7709
|
+
});
|
|
7710
|
+
if (res.ok) {
|
|
7711
|
+
saveRemoteConfig({ enabled: true, url, apiKey: apiKey.trim() }, dataDir);
|
|
7712
|
+
console.log(` ${green('✓')} Connected successfully. Remote sync enabled.`);
|
|
7713
|
+
console.log(dim(` Config saved to ${join(dataDir, 'config.json')}`));
|
|
7714
|
+
} else {
|
|
7715
|
+
const text = await res.text().catch(() => '');
|
|
7716
|
+
console.error(` ${red('✘')} Connection failed: HTTP ${res.status}`);
|
|
7717
|
+
if (text) console.error(` ${dim(text.slice(0, 200))}`);
|
|
7718
|
+
process.exit(1);
|
|
7719
|
+
}
|
|
7720
|
+
} catch (e) {
|
|
7721
|
+
console.error(` ${red('✘')} Connection failed: ${e.message}`);
|
|
7722
|
+
process.exit(1);
|
|
7723
|
+
}
|
|
7724
|
+
console.log();
|
|
7725
|
+
return;
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7728
|
+
if (subcommand === 'status') {
|
|
7729
|
+
const remote = getRemoteConfig(dataDir);
|
|
7730
|
+
console.log();
|
|
7731
|
+
console.log(` ${bold('◇ Remote Status')}`);
|
|
7732
|
+
console.log();
|
|
7733
|
+
if (!remote) {
|
|
7734
|
+
console.log(` Remote: ${dim('not configured')}`);
|
|
7735
|
+
console.log(` ${dim('Run')} ${cyan('context-vault remote setup')} ${dim('to connect.')}`);
|
|
7736
|
+
} else {
|
|
7737
|
+
const keyPreview = remote.apiKey ? remote.apiKey.slice(0, 6) + '...' : dim('(none)');
|
|
7738
|
+
console.log(` Enabled: ${remote.enabled ? green('yes') : red('no')}`);
|
|
7739
|
+
console.log(` URL: ${remote.url}`);
|
|
7740
|
+
console.log(` API Key: ${keyPreview}`);
|
|
7741
|
+
|
|
7742
|
+
if (remote.enabled && remote.apiKey) {
|
|
7743
|
+
console.log(`\n Testing connection...`);
|
|
7744
|
+
try {
|
|
7745
|
+
const res = await fetch(`${remote.url.replace(/\/$/, '')}/api/vault/status`, {
|
|
7746
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7747
|
+
signal: AbortSignal.timeout(10000),
|
|
7748
|
+
});
|
|
7749
|
+
if (res.ok) {
|
|
7750
|
+
console.log(` ${green('✓')} Remote is reachable.`);
|
|
7751
|
+
} else {
|
|
7752
|
+
console.log(` ${red('✘')} HTTP ${res.status}`);
|
|
7753
|
+
}
|
|
7754
|
+
} catch (e) {
|
|
7755
|
+
console.log(` ${red('✘')} ${e.message}`);
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7759
|
+
console.log();
|
|
7760
|
+
return;
|
|
7761
|
+
}
|
|
7762
|
+
|
|
7763
|
+
if (subcommand === 'disconnect') {
|
|
7764
|
+
const remote = getRemoteConfig(dataDir);
|
|
7765
|
+
if (!remote || !remote.enabled) {
|
|
7766
|
+
console.log(`\n ${dim('Remote sync is already disabled.')}\n`);
|
|
7767
|
+
return;
|
|
7768
|
+
}
|
|
7769
|
+
saveRemoteConfig({ enabled: false }, dataDir);
|
|
7770
|
+
console.log(`\n ${green('✓')} Remote sync disabled. API key preserved (re-enable with ${cyan('context-vault remote setup')}).\n`);
|
|
7771
|
+
return;
|
|
7772
|
+
}
|
|
7773
|
+
|
|
7774
|
+
if (subcommand === 'sync') {
|
|
7775
|
+
await runRemoteSync(getRemoteConfig, dataDir);
|
|
7776
|
+
return;
|
|
7777
|
+
}
|
|
7778
|
+
|
|
7779
|
+
if (subcommand === 'pull') {
|
|
7780
|
+
await runRemotePull(getRemoteConfig, dataDir);
|
|
7781
|
+
return;
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
console.log();
|
|
7785
|
+
console.log(` ${bold('◇ context-vault remote')}`);
|
|
7786
|
+
console.log();
|
|
7787
|
+
console.log(` ${cyan('setup')} Connect to a hosted vault API`);
|
|
7788
|
+
console.log(` ${cyan('status')} Show remote config and test connection`);
|
|
7789
|
+
console.log(` ${cyan('sync')} ${dim('[--full] [--dry-run]')} Sync local vault to hosted`);
|
|
7790
|
+
console.log(` ${cyan('pull')} Pull remote entries to local`);
|
|
7791
|
+
console.log(` ${cyan('disconnect')} Disable remote sync (preserves API key)`);
|
|
7792
|
+
console.log();
|
|
7793
|
+
}
|
|
7794
|
+
|
|
7795
|
+
// ---------------------------------------------------------------------------
|
|
7796
|
+
// remote sync — push local vault to hosted
|
|
7797
|
+
// ---------------------------------------------------------------------------
|
|
7798
|
+
async function runRemoteSync(getRemoteConfig, dataDir) {
|
|
7799
|
+
const remote = getRemoteConfig(dataDir);
|
|
7800
|
+
if (!remote || !remote.enabled || !remote.apiKey) {
|
|
7801
|
+
console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
|
|
7802
|
+
process.exit(1);
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7805
|
+
const isFullSync = flags.has('--full');
|
|
7806
|
+
const { createHash } = await import('node:crypto');
|
|
7807
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
7808
|
+
const config = resolveConfig();
|
|
7809
|
+
|
|
7810
|
+
let DatabaseSync;
|
|
7811
|
+
try {
|
|
7812
|
+
DatabaseSync = (await import('node:sqlite')).DatabaseSync;
|
|
7813
|
+
} catch {
|
|
7814
|
+
console.error(`\n ${red('✘')} Node.js SQLite not available. Requires Node >= 22.5.\n`);
|
|
7815
|
+
process.exit(1);
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7818
|
+
const db = new DatabaseSync(config.dbPath, { open: true });
|
|
7819
|
+
const apiUrl = remote.url.replace(/\/$/, '');
|
|
7820
|
+
|
|
7821
|
+
function contentHash(entry) {
|
|
7822
|
+
const data = (entry.title || '') + (entry.body || '') + JSON.stringify(entry.tags || []) + JSON.stringify(entry.meta || {});
|
|
7823
|
+
return createHash('sha256').update(data).digest('hex');
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7826
|
+
console.log();
|
|
7827
|
+
console.log(` ${bold('◇ Remote Sync')}`);
|
|
7828
|
+
console.log();
|
|
7829
|
+
|
|
7830
|
+
// Read all local entries
|
|
7831
|
+
const localRows = db.prepare(
|
|
7832
|
+
'SELECT id, kind, category, title, body, tags, meta, source, identity_key, tier, expires_at, created_at, updated_at FROM vault WHERE superseded_by IS NULL'
|
|
7833
|
+
).all();
|
|
7834
|
+
db.close();
|
|
7835
|
+
|
|
7836
|
+
const localEntries = localRows.map((row) => ({
|
|
7837
|
+
...row,
|
|
7838
|
+
tags: typeof row.tags === 'string' ? JSON.parse(row.tags) : (row.tags || []),
|
|
7839
|
+
meta: typeof row.meta === 'string' ? JSON.parse(row.meta) : (row.meta || {}),
|
|
7840
|
+
}));
|
|
7841
|
+
|
|
7842
|
+
console.log(` Local entries: ${localEntries.length}`);
|
|
7843
|
+
|
|
7844
|
+
let toUpload;
|
|
7845
|
+
|
|
7846
|
+
if (isFullSync) {
|
|
7847
|
+
console.log(` Mode: ${cyan('full')} (uploading all entries)`);
|
|
7848
|
+
toUpload = localEntries;
|
|
7849
|
+
} else {
|
|
7850
|
+
// Fetch remote manifest
|
|
7851
|
+
console.log(` Fetching remote manifest...`);
|
|
7852
|
+
let manifest;
|
|
7853
|
+
try {
|
|
7854
|
+
const res = await fetch(`${apiUrl}/api/vault/manifest`, {
|
|
7855
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
7856
|
+
signal: AbortSignal.timeout(30000),
|
|
7857
|
+
});
|
|
7858
|
+
if (!res.ok) {
|
|
7859
|
+
const text = await res.text().catch(() => '');
|
|
7860
|
+
console.error(` ${red('✘')} Manifest fetch failed: HTTP ${res.status}`);
|
|
7861
|
+
if (text) console.error(` ${dim(text.slice(0, 200))}`);
|
|
7862
|
+
process.exit(1);
|
|
7863
|
+
}
|
|
7864
|
+
manifest = await res.json();
|
|
7865
|
+
} catch (e) {
|
|
7866
|
+
console.error(` ${red('✘')} Manifest fetch failed: ${e.message}`);
|
|
7867
|
+
process.exit(1);
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
const remoteEntries = manifest.entries || [];
|
|
7871
|
+
const remoteMap = new Map();
|
|
7872
|
+
for (const entry of remoteEntries) {
|
|
7873
|
+
remoteMap.set(entry.id, entry);
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
// If remote manifest has content_hash, do a hash diff. Otherwise fall back to full sync.
|
|
7877
|
+
const hasContentHash = remoteEntries.length > 0 && remoteEntries[0].content_hash;
|
|
7878
|
+
|
|
7879
|
+
if (!hasContentHash && remoteEntries.length > 0) {
|
|
7880
|
+
console.log(` ${yellow('!')} Remote manifest lacks content_hash. Falling back to full upload.`);
|
|
7881
|
+
toUpload = localEntries;
|
|
7882
|
+
} else {
|
|
7883
|
+
toUpload = [];
|
|
7884
|
+
let unchanged = 0;
|
|
7885
|
+
for (const entry of localEntries) {
|
|
7886
|
+
const remoteEntry = remoteMap.get(entry.id);
|
|
7887
|
+
if (!remoteEntry) {
|
|
7888
|
+
toUpload.push(entry);
|
|
7889
|
+
} else if (remoteEntry.content_hash !== contentHash(entry)) {
|
|
7890
|
+
toUpload.push(entry);
|
|
7891
|
+
} else {
|
|
7892
|
+
unchanged++;
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
|
|
7899
|
+
const newCount = toUpload.length;
|
|
7900
|
+
console.log(` To upload: ${newCount}`);
|
|
7901
|
+
|
|
7902
|
+
if (newCount === 0) {
|
|
7903
|
+
console.log(`\n ${green('✓')} Everything is in sync.\n`);
|
|
7904
|
+
await drainOfflineQueue(apiUrl, remote.apiKey);
|
|
7905
|
+
return;
|
|
7906
|
+
}
|
|
7907
|
+
|
|
7908
|
+
if (isDryRun) {
|
|
7909
|
+
console.log();
|
|
7910
|
+
for (const entry of toUpload.slice(0, 20)) {
|
|
7911
|
+
console.log(` ${dim(entry.id)} ${entry.kind || '?'} ${entry.title || dim('(untitled)')}`);
|
|
7912
|
+
}
|
|
7913
|
+
if (toUpload.length > 20) {
|
|
7914
|
+
console.log(` ${dim(`... and ${toUpload.length - 20} more`)}`);
|
|
7915
|
+
}
|
|
7916
|
+
console.log(`\n ${dim('Dry run. No changes made.')}\n`);
|
|
7917
|
+
return;
|
|
7918
|
+
}
|
|
7919
|
+
|
|
7920
|
+
// Stream as NDJSON
|
|
7921
|
+
console.log(` Uploading...`);
|
|
7922
|
+
const ndjsonBody = toUpload.map((entry) => JSON.stringify(entry)).join('\n') + '\n';
|
|
7923
|
+
|
|
7924
|
+
let jobId, entriesUploaded;
|
|
7925
|
+
try {
|
|
7926
|
+
const res = await fetch(`${apiUrl}/api/vault/import/stream`, {
|
|
7927
|
+
method: 'POST',
|
|
7928
|
+
headers: {
|
|
7929
|
+
'Authorization': `Bearer ${remote.apiKey}`,
|
|
7930
|
+
'Content-Type': 'application/x-ndjson',
|
|
7931
|
+
},
|
|
7932
|
+
body: ndjsonBody,
|
|
7933
|
+
signal: AbortSignal.timeout(120000),
|
|
7934
|
+
});
|
|
7935
|
+
if (!res.ok) {
|
|
7936
|
+
const text = await res.text().catch(() => '');
|
|
7937
|
+
console.error(` ${red('✘')} Upload failed: HTTP ${res.status}`);
|
|
7938
|
+
if (text) console.error(` ${dim(text.slice(0, 200))}`);
|
|
7939
|
+
process.exit(1);
|
|
7940
|
+
}
|
|
7941
|
+
const data = await res.json();
|
|
7942
|
+
jobId = data.job_id;
|
|
7943
|
+
entriesUploaded = data.entries_uploaded || newCount;
|
|
7944
|
+
} catch (e) {
|
|
7945
|
+
console.error(` ${red('✘')} Upload failed: ${e.message}`);
|
|
7946
|
+
process.exit(1);
|
|
7947
|
+
}
|
|
7948
|
+
|
|
7949
|
+
console.log(` ${green('✓')} Uploaded ${entriesUploaded} entries.`);
|
|
7950
|
+
|
|
7951
|
+
if (!jobId) {
|
|
7952
|
+
console.log(`\n ${green('✓')} Sync complete (no job tracking available).\n`);
|
|
7953
|
+
await drainOfflineQueue(apiUrl, remote.apiKey);
|
|
7954
|
+
return;
|
|
7955
|
+
}
|
|
7956
|
+
|
|
7957
|
+
// Poll job status until embeddings complete
|
|
7958
|
+
console.log(` Job: ${dim(jobId)}`);
|
|
7959
|
+
console.log(` Waiting for embeddings...`);
|
|
7960
|
+
|
|
7961
|
+
let attempts = 0;
|
|
7962
|
+
const maxAttempts = 300; // 10 min max
|
|
7963
|
+
while (attempts < maxAttempts) {
|
|
7964
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
7965
|
+
attempts++;
|
|
7966
|
+
try {
|
|
7967
|
+
const res = await fetch(`${apiUrl}/api/vault/jobs/${jobId}`, {
|
|
7968
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}` },
|
|
7969
|
+
signal: AbortSignal.timeout(10000),
|
|
7970
|
+
});
|
|
7971
|
+
if (!res.ok) {
|
|
7972
|
+
if (res.status === 404) {
|
|
7973
|
+
console.log(` ${yellow('!')} Job not found (server may not support job tracking yet).`);
|
|
7974
|
+
break;
|
|
7975
|
+
}
|
|
7976
|
+
continue;
|
|
7977
|
+
}
|
|
7978
|
+
const job = await res.json();
|
|
7979
|
+
const embedded = job.entries_embedded || 0;
|
|
7980
|
+
const total = job.total_entries || entriesUploaded;
|
|
7981
|
+
const pct = total > 0 ? Math.round((embedded / total) * 100) : 0;
|
|
7982
|
+
process.stdout.write(`\r Embeddings: ${embedded}/${total} (${pct}%) `);
|
|
7983
|
+
|
|
7984
|
+
if (job.status === 'complete') {
|
|
7985
|
+
process.stdout.write('\n');
|
|
7986
|
+
console.log(` ${green('✓')} Embeddings complete.`);
|
|
7987
|
+
break;
|
|
7988
|
+
}
|
|
7989
|
+
if (job.status === 'failed') {
|
|
7990
|
+
process.stdout.write('\n');
|
|
7991
|
+
console.error(` ${red('✘')} Embedding job failed.`);
|
|
7992
|
+
if (job.errors) console.error(` ${dim(JSON.stringify(job.errors).slice(0, 300))}`);
|
|
7993
|
+
break;
|
|
7994
|
+
}
|
|
7995
|
+
} catch {
|
|
7996
|
+
// Transient error, keep polling
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
|
|
8000
|
+
if (attempts >= maxAttempts) {
|
|
8001
|
+
console.log(`\n ${yellow('!')} Timed out waiting for embeddings. They will complete in the background.`);
|
|
8002
|
+
}
|
|
8003
|
+
|
|
8004
|
+
console.log(`\n ${green('✓')} Synced ${entriesUploaded} entries.\n`);
|
|
8005
|
+
await drainOfflineQueue(apiUrl, remote.apiKey);
|
|
8006
|
+
}
|
|
8007
|
+
|
|
8008
|
+
// ---------------------------------------------------------------------------
|
|
8009
|
+
// remote pull — fetch remote entries to local
|
|
8010
|
+
// ---------------------------------------------------------------------------
|
|
8011
|
+
async function runRemotePull(getRemoteConfig, dataDir) {
|
|
8012
|
+
const remote = getRemoteConfig(dataDir);
|
|
8013
|
+
if (!remote || !remote.enabled || !remote.apiKey) {
|
|
8014
|
+
console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
|
|
8015
|
+
process.exit(1);
|
|
8016
|
+
}
|
|
8017
|
+
|
|
8018
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
8019
|
+
const config = resolveConfig();
|
|
8020
|
+
|
|
8021
|
+
let DatabaseSync;
|
|
8022
|
+
try {
|
|
8023
|
+
DatabaseSync = (await import('node:sqlite')).DatabaseSync;
|
|
8024
|
+
} catch {
|
|
8025
|
+
console.error(`\n ${red('✘')} Node.js SQLite not available. Requires Node >= 22.5.\n`);
|
|
8026
|
+
process.exit(1);
|
|
8027
|
+
}
|
|
8028
|
+
|
|
8029
|
+
const db = new DatabaseSync(config.dbPath, { open: true });
|
|
8030
|
+
const apiUrl = remote.url.replace(/\/$/, '');
|
|
8031
|
+
|
|
8032
|
+
console.log();
|
|
8033
|
+
console.log(` ${bold('◇ Remote Pull')}`);
|
|
8034
|
+
console.log();
|
|
8035
|
+
|
|
8036
|
+
// Fetch remote manifest
|
|
8037
|
+
console.log(` Fetching remote manifest...`);
|
|
8038
|
+
let manifest;
|
|
8039
|
+
try {
|
|
8040
|
+
const res = await fetch(`${apiUrl}/api/vault/manifest`, {
|
|
8041
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
8042
|
+
signal: AbortSignal.timeout(30000),
|
|
8043
|
+
});
|
|
8044
|
+
if (!res.ok) {
|
|
8045
|
+
const text = await res.text().catch(() => '');
|
|
8046
|
+
console.error(` ${red('✘')} Manifest fetch failed: HTTP ${res.status}`);
|
|
8047
|
+
if (text) console.error(` ${dim(text.slice(0, 200))}`);
|
|
8048
|
+
db.close();
|
|
8049
|
+
process.exit(1);
|
|
8050
|
+
}
|
|
8051
|
+
manifest = await res.json();
|
|
8052
|
+
} catch (e) {
|
|
8053
|
+
console.error(` ${red('✘')} Manifest fetch failed: ${e.message}`);
|
|
8054
|
+
db.close();
|
|
8055
|
+
process.exit(1);
|
|
8056
|
+
}
|
|
8057
|
+
|
|
8058
|
+
const remoteEntries = manifest.entries || [];
|
|
8059
|
+
console.log(` Remote entries: ${remoteEntries.length}`);
|
|
8060
|
+
|
|
8061
|
+
// Build local manifest
|
|
8062
|
+
const localRows = db.prepare('SELECT id, updated_at FROM vault').all();
|
|
8063
|
+
const localMap = new Map();
|
|
8064
|
+
for (const row of localRows) {
|
|
8065
|
+
localMap.set(row.id, row.updated_at);
|
|
8066
|
+
}
|
|
8067
|
+
|
|
8068
|
+
// Diff: remote entries not in local, or remote.updated_at > local.updated_at
|
|
8069
|
+
const toPull = [];
|
|
8070
|
+
for (const entry of remoteEntries) {
|
|
8071
|
+
const localUpdated = localMap.get(entry.id);
|
|
8072
|
+
if (!localUpdated) {
|
|
8073
|
+
toPull.push(entry.id);
|
|
8074
|
+
} else if (entry.updated_at && entry.updated_at > localUpdated) {
|
|
8075
|
+
toPull.push(entry.id);
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
|
|
8079
|
+
console.log(` To pull: ${toPull.length}`);
|
|
8080
|
+
|
|
8081
|
+
if (toPull.length === 0) {
|
|
8082
|
+
console.log(`\n ${green('✓')} Local vault is up to date.\n`);
|
|
8083
|
+
db.close();
|
|
8084
|
+
await drainOfflineQueue(apiUrl, remote.apiKey);
|
|
8085
|
+
return;
|
|
8086
|
+
}
|
|
8087
|
+
|
|
8088
|
+
if (isDryRun) {
|
|
8089
|
+
for (const id of toPull.slice(0, 20)) {
|
|
8090
|
+
const remoteEntry = remoteEntries.find((e) => e.id === id);
|
|
8091
|
+
console.log(` ${dim(id)} ${remoteEntry?.kind || '?'} ${remoteEntry?.title || dim('(untitled)')}`);
|
|
8092
|
+
}
|
|
8093
|
+
if (toPull.length > 20) {
|
|
8094
|
+
console.log(` ${dim(`... and ${toPull.length - 20} more`)}`);
|
|
8095
|
+
}
|
|
8096
|
+
console.log(`\n ${dim('Dry run. No changes made.')}\n`);
|
|
8097
|
+
db.close();
|
|
8098
|
+
return;
|
|
8099
|
+
}
|
|
8100
|
+
|
|
8101
|
+
// Batch fetch in groups of 100
|
|
8102
|
+
let pulled = 0;
|
|
8103
|
+
|
|
8104
|
+
// Ensure vault table has the columns we need for INSERT OR REPLACE
|
|
8105
|
+
const insertStmt = db.prepare(`
|
|
8106
|
+
INSERT OR REPLACE INTO vault (id, kind, category, title, body, tags, meta, source, identity_key, tier, expires_at, created_at, updated_at)
|
|
8107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8108
|
+
`);
|
|
8109
|
+
|
|
8110
|
+
for (let i = 0; i < toPull.length; i += 100) {
|
|
8111
|
+
const batch = toPull.slice(i, i + 100);
|
|
8112
|
+
const idsParam = batch.join(',');
|
|
8113
|
+
try {
|
|
8114
|
+
const res = await fetch(`${apiUrl}/api/vault/entries?ids=${encodeURIComponent(idsParam)}`, {
|
|
8115
|
+
headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
|
|
8116
|
+
signal: AbortSignal.timeout(30000),
|
|
8117
|
+
});
|
|
8118
|
+
if (!res.ok) {
|
|
8119
|
+
console.error(` ${red('✘')} Batch fetch failed: HTTP ${res.status}`);
|
|
8120
|
+
continue;
|
|
8121
|
+
}
|
|
8122
|
+
const data = await res.json();
|
|
8123
|
+
const entries = data.entries || data;
|
|
8124
|
+
for (const entry of (Array.isArray(entries) ? entries : [])) {
|
|
8125
|
+
const tags = typeof entry.tags === 'string' ? entry.tags : JSON.stringify(entry.tags || []);
|
|
8126
|
+
const meta = typeof entry.meta === 'string' ? entry.meta : JSON.stringify(entry.meta || {});
|
|
8127
|
+
insertStmt.run(
|
|
8128
|
+
entry.id,
|
|
8129
|
+
entry.kind || null,
|
|
8130
|
+
entry.category || 'knowledge',
|
|
8131
|
+
entry.title || null,
|
|
8132
|
+
entry.body || '',
|
|
8133
|
+
tags,
|
|
8134
|
+
meta,
|
|
8135
|
+
entry.source || null,
|
|
8136
|
+
entry.identity_key || null,
|
|
8137
|
+
entry.tier || 'working',
|
|
8138
|
+
entry.expires_at || null,
|
|
8139
|
+
entry.created_at || new Date().toISOString(),
|
|
8140
|
+
entry.updated_at || new Date().toISOString()
|
|
8141
|
+
);
|
|
8142
|
+
pulled++;
|
|
8143
|
+
}
|
|
8144
|
+
} catch (e) {
|
|
8145
|
+
console.error(` ${yellow('!')} Batch fetch error: ${e.message}`);
|
|
8146
|
+
}
|
|
8147
|
+
process.stdout.write(`\r Progress: ${Math.min(i + 100, toPull.length)}/${toPull.length} `);
|
|
8148
|
+
}
|
|
8149
|
+
|
|
8150
|
+
db.close();
|
|
8151
|
+
process.stdout.write('\n');
|
|
8152
|
+
console.log(`\n ${green('✓')} Pulled ${pulled} entries from remote.\n`);
|
|
8153
|
+
await drainOfflineQueue(apiUrl, remote.apiKey);
|
|
8154
|
+
}
|
|
8155
|
+
|
|
8156
|
+
// ---------------------------------------------------------------------------
|
|
8157
|
+
// Offline queue drain — best-effort, runs after any successful remote call
|
|
8158
|
+
// ---------------------------------------------------------------------------
|
|
8159
|
+
async function drainOfflineQueue(apiUrl, apiKey) {
|
|
8160
|
+
const queuePath = join(HOME, '.context-mcp', 'sync-queue.jsonl');
|
|
8161
|
+
if (!existsSync(queuePath)) return;
|
|
8162
|
+
|
|
8163
|
+
let lines;
|
|
8164
|
+
try {
|
|
8165
|
+
const content = readFileSync(queuePath, 'utf-8').trim();
|
|
8166
|
+
if (!content) {
|
|
8167
|
+
unlinkSync(queuePath);
|
|
8168
|
+
return;
|
|
8169
|
+
}
|
|
8170
|
+
lines = content.split('\n').filter(Boolean);
|
|
8171
|
+
} catch {
|
|
8172
|
+
return;
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
if (lines.length === 0) {
|
|
8176
|
+
try { unlinkSync(queuePath); } catch {}
|
|
8177
|
+
return;
|
|
8178
|
+
}
|
|
8179
|
+
|
|
8180
|
+
console.log(` ${dim(`Draining offline queue (${lines.length} entries)...`)}`);
|
|
8181
|
+
const remaining = [];
|
|
8182
|
+
|
|
8183
|
+
for (const line of lines) {
|
|
8184
|
+
try {
|
|
8185
|
+
const entry = JSON.parse(line);
|
|
8186
|
+
const res = await fetch(`${apiUrl}/api/vault/entries`, {
|
|
8187
|
+
method: 'POST',
|
|
8188
|
+
headers: {
|
|
8189
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
8190
|
+
'Content-Type': 'application/json',
|
|
8191
|
+
},
|
|
8192
|
+
body: JSON.stringify(entry),
|
|
8193
|
+
signal: AbortSignal.timeout(10000),
|
|
8194
|
+
});
|
|
8195
|
+
if (!res.ok) {
|
|
8196
|
+
remaining.push(line);
|
|
8197
|
+
}
|
|
8198
|
+
} catch {
|
|
8199
|
+
remaining.push(line);
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
|
|
8203
|
+
if (remaining.length > 0) {
|
|
8204
|
+
writeFileSync(queuePath, remaining.join('\n') + '\n');
|
|
8205
|
+
console.log(` ${yellow('!')} ${remaining.length} entries still queued (will retry next sync).`);
|
|
8206
|
+
} else {
|
|
8207
|
+
try { unlinkSync(queuePath); } catch {}
|
|
8208
|
+
console.log(` ${green('✓')} Offline queue drained.`);
|
|
8209
|
+
}
|
|
8210
|
+
}
|
|
8211
|
+
|
|
7043
8212
|
async function main() {
|
|
7044
8213
|
if (flags.has('--version') || command === 'version') {
|
|
7045
8214
|
console.log(VERSION);
|
|
@@ -7047,8 +8216,13 @@ async function main() {
|
|
|
7047
8216
|
}
|
|
7048
8217
|
|
|
7049
8218
|
if (flags.has('--help') || command === 'help') {
|
|
7050
|
-
|
|
7051
|
-
|
|
8219
|
+
// Commands with their own --help handling: delegate to them
|
|
8220
|
+
const commandsWithHelp = new Set(['save', 'search', 'rules', 'hooks', 'daemon', 'team', 'remote']);
|
|
8221
|
+
if (!command || command === 'help' || !commandsWithHelp.has(command)) {
|
|
8222
|
+
showHelp(flags.has('--all'));
|
|
8223
|
+
return;
|
|
8224
|
+
}
|
|
8225
|
+
// Fall through to command switch for commands with built-in --help
|
|
7052
8226
|
}
|
|
7053
8227
|
|
|
7054
8228
|
if (!command) {
|
|
@@ -7173,6 +8347,15 @@ async function main() {
|
|
|
7173
8347
|
case 'stats':
|
|
7174
8348
|
await runStats();
|
|
7175
8349
|
break;
|
|
8350
|
+
case 'remote':
|
|
8351
|
+
await runRemote();
|
|
8352
|
+
break;
|
|
8353
|
+
case 'team':
|
|
8354
|
+
await runTeam();
|
|
8355
|
+
break;
|
|
8356
|
+
case 'public':
|
|
8357
|
+
await runPublic();
|
|
8358
|
+
break;
|
|
7176
8359
|
default:
|
|
7177
8360
|
console.error(red(`Unknown command: ${command}`));
|
|
7178
8361
|
console.error(`Run ${cyan('context-vault --help')} for usage.`);
|