context-vault 3.16.0 → 3.17.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 +886 -436
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +4 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/server.js +3 -436
- package/dist/server.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +34 -0
- package/dist/tools/context-status.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +42 -0
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +2 -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/search.ts +59 -0
- package/node_modules/@context-vault/core/src/types.ts +2 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +26 -1
- package/src/register-tools.ts +4 -0
- package/src/server.ts +3 -432
- package/src/tools/context-status.ts +38 -0
package/bin/cli.js
CHANGED
|
@@ -443,7 +443,6 @@ ${bold('Commands:')}
|
|
|
443
443
|
${cyan('status')} Show vault diagnostics
|
|
444
444
|
${cyan('doctor')} Diagnose and repair common issues
|
|
445
445
|
${cyan('debug')} Generate AI-pasteable debug report
|
|
446
|
-
${cyan('daemon')} start|stop|status Run vault as a shared HTTP daemon (one process, all sessions)
|
|
447
446
|
${cyan('restart')} Stop running MCP server processes (client auto-restarts)
|
|
448
447
|
${cyan('reconnect')} Fix vault path, kill stale servers, re-register MCP, reindex
|
|
449
448
|
${cyan('search')} Search vault entries from CLI
|
|
@@ -452,6 +451,10 @@ ${bold('Commands:')}
|
|
|
452
451
|
${cyan('export')} Export vault entries (JSON, CSV, or portable ZIP)
|
|
453
452
|
${cyan('ingest')} <url> Fetch URL and save as vault entry
|
|
454
453
|
${cyan('ingest-project')} <path> Scan project directory and register as project entity
|
|
454
|
+
${cyan('ingest-comms')} Ingest structured comms from stdin (JSON lines, with dedup)
|
|
455
|
+
${cyan('gmail-bridge')} Fetch recent emails via gmail-cli and ingest into vault
|
|
456
|
+
${cyan('slack-bridge')} Fetch Slack messages from allowed channels and ingest
|
|
457
|
+
${cyan('contacts')} list|show|add Manage contact entities in the vault
|
|
455
458
|
${cyan('reindex')} Rebuild search index from knowledge files
|
|
456
459
|
${cyan('reclassify')} Move prompt-history entries from knowledge to event category
|
|
457
460
|
${cyan('sync')} [dir] Index .context/ files into vault DB (use --dry-run to preview)
|
|
@@ -459,6 +462,7 @@ ${bold('Commands:')}
|
|
|
459
462
|
${cyan('archive')} Archive old ephemeral/event entries (use --dry-run to preview)
|
|
460
463
|
${cyan('restore')} <id> Restore an archived entry back into the vault
|
|
461
464
|
${cyan('prune')} Remove expired entries (use --dry-run to preview)
|
|
465
|
+
${cyan('stale')} List entries with low freshness scores
|
|
462
466
|
${cyan('stats')} recall|co-retrieval Measure recall ratio and co-retrieval graph
|
|
463
467
|
${cyan('remote')} setup|status|sync|pull Connect to hosted vault (cloud sync)
|
|
464
468
|
${cyan('team')} join|leave|status|browse Join or manage a team vault
|
|
@@ -3962,6 +3966,555 @@ async function runIngest() {
|
|
|
3962
3966
|
console.log();
|
|
3963
3967
|
}
|
|
3964
3968
|
|
|
3969
|
+
/**
|
|
3970
|
+
* Shared ingest helper: dedup-check, build entry, save to vault.
|
|
3971
|
+
* Returns 'saved' | 'skipped' | 'error'.
|
|
3972
|
+
*/
|
|
3973
|
+
async function ingestCommLine(parsed, { ctx, db, captureAndIndex, dryRun, verbose, sourceFlag, kindFlag, extraTags }) {
|
|
3974
|
+
if (!parsed.id || !parsed.body) {
|
|
3975
|
+
if (verbose) console.log(` ${red('!')} Missing required field (id, body)`);
|
|
3976
|
+
return 'error';
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
const source = parsed.source || sourceFlag;
|
|
3980
|
+
if (!source) {
|
|
3981
|
+
if (verbose) console.log(` ${red('!')} No source (use --source flag or include in JSON)`);
|
|
3982
|
+
return 'error';
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
const identityKey = `comms:${source}:${parsed.id}`;
|
|
3986
|
+
|
|
3987
|
+
if (!dryRun) {
|
|
3988
|
+
const exists = db.prepare('SELECT 1 FROM vault WHERE identity_key = ? LIMIT 1').get(identityKey);
|
|
3989
|
+
if (exists) {
|
|
3990
|
+
if (verbose) console.log(` ${dim('=')} [${source}] Already exists (${parsed.id})`);
|
|
3991
|
+
return 'skipped';
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
const title = parsed.subject
|
|
3996
|
+
? `[${source}] ${parsed.subject}`
|
|
3997
|
+
: parsed.from
|
|
3998
|
+
? `[${source}] Message from ${parsed.from}`
|
|
3999
|
+
: `[${source}] ${parsed.id}`;
|
|
4000
|
+
|
|
4001
|
+
const lineTags = Array.isArray(parsed.tags) ? parsed.tags : [];
|
|
4002
|
+
const allTags = [...new Set([...lineTags, ...extraTags, `source:${source}`])];
|
|
4003
|
+
|
|
4004
|
+
const meta = {};
|
|
4005
|
+
if (parsed.from) meta.from = parsed.from;
|
|
4006
|
+
if (parsed.to) meta.to = parsed.to;
|
|
4007
|
+
if (parsed.date) meta.date = parsed.date;
|
|
4008
|
+
if (parsed.thread_id) meta.thread_id = parsed.thread_id;
|
|
4009
|
+
if (parsed.channel) meta.channel = parsed.channel;
|
|
4010
|
+
|
|
4011
|
+
if (dryRun) {
|
|
4012
|
+
if (verbose) console.log(` ${green('+')} [${source}] ${parsed.subject || parsed.id}`);
|
|
4013
|
+
return 'saved';
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
try {
|
|
4017
|
+
await captureAndIndex(ctx, {
|
|
4018
|
+
kind: kindFlag,
|
|
4019
|
+
title,
|
|
4020
|
+
body: parsed.body,
|
|
4021
|
+
tags: allTags,
|
|
4022
|
+
source,
|
|
4023
|
+
identity_key: identityKey,
|
|
4024
|
+
meta: Object.keys(meta).length ? meta : null,
|
|
4025
|
+
});
|
|
4026
|
+
if (verbose) console.log(` ${green('+')} [${source}] ${parsed.subject || parsed.id} (${parsed.id})`);
|
|
4027
|
+
return 'saved';
|
|
4028
|
+
} catch (e) {
|
|
4029
|
+
if (verbose) console.log(` ${red('!')} [${source}] Error saving ${parsed.id}: ${e.message}`);
|
|
4030
|
+
return 'error';
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
async function runIngestComms() {
|
|
4035
|
+
if (flags.has('--help')) {
|
|
4036
|
+
console.log(`
|
|
4037
|
+
${bold('context-vault ingest-comms')}
|
|
4038
|
+
|
|
4039
|
+
Read structured communications from stdin (JSON lines) and save to vault with dedup.
|
|
4040
|
+
|
|
4041
|
+
${bold('Flags:')}
|
|
4042
|
+
--source <name> Source identifier (gmail, slack, etc.) — required if not in JSON
|
|
4043
|
+
--kind <kind> Vault entry kind (default: event)
|
|
4044
|
+
--tags t1,t2 Additional tags (merged with per-line tags)
|
|
4045
|
+
--dry-run Parse and validate without saving
|
|
4046
|
+
--verbose Print each entry as it's processed
|
|
4047
|
+
|
|
4048
|
+
${bold('JSON line schema:')}
|
|
4049
|
+
{ "id": "msg-123", "body": "text", "source": "gmail", "subject": "...", "from": "...",
|
|
4050
|
+
"to": [...], "date": "...", "thread_id": "...", "channel": "...", "tags": [...] }
|
|
4051
|
+
Required fields: id, body. source from JSON overrides --source flag.
|
|
4052
|
+
|
|
4053
|
+
${bold('Examples:')}
|
|
4054
|
+
cat emails.jsonl | context-vault ingest-comms --source gmail
|
|
4055
|
+
echo '{"id":"1","body":"hi","source":"slack"}' | context-vault ingest-comms
|
|
4056
|
+
`);
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
const sourceFlag = getFlag('--source');
|
|
4061
|
+
const kindFlag = getFlag('--kind') || 'event';
|
|
4062
|
+
const tagsFlag = getFlag('--tags');
|
|
4063
|
+
const extraTags = tagsFlag ? tagsFlag.split(',').map((t) => t.trim()) : [];
|
|
4064
|
+
const dryRun = flags.has('--dry-run');
|
|
4065
|
+
const verbose = flags.has('--verbose');
|
|
4066
|
+
|
|
4067
|
+
// Read all of stdin
|
|
4068
|
+
let input;
|
|
4069
|
+
if (!process.stdin.isTTY) {
|
|
4070
|
+
input = await new Promise((res) => {
|
|
4071
|
+
let data = '';
|
|
4072
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
4073
|
+
process.stdin.on('end', () => res(data));
|
|
4074
|
+
});
|
|
4075
|
+
} else {
|
|
4076
|
+
console.log(`\n ${bold('context-vault ingest-comms')}`);
|
|
4077
|
+
console.log(`\n Pipe JSON lines to stdin. Example:`);
|
|
4078
|
+
console.log(` cat emails.jsonl | context-vault ingest-comms --source gmail\n`);
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
const lines = input.split('\n').filter((l) => l.trim());
|
|
4083
|
+
if (!lines.length) {
|
|
4084
|
+
console.log(`\n No input lines received.\n`);
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
// Bootstrap DB (unless dry-run)
|
|
4089
|
+
let ctx, db;
|
|
4090
|
+
if (!dryRun) {
|
|
4091
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
4092
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
4093
|
+
await import('@context-vault/core/db');
|
|
4094
|
+
const { embed } = await import('@context-vault/core/embed');
|
|
4095
|
+
|
|
4096
|
+
const config = resolveConfig();
|
|
4097
|
+
if (!config.vaultDirExists) {
|
|
4098
|
+
console.error(red(`\n Vault directory not found: ${config.vaultDir}`));
|
|
4099
|
+
process.exit(1);
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
db = await initDatabase(config.dbPath);
|
|
4103
|
+
const stmts = prepareStatements(db);
|
|
4104
|
+
ctx = {
|
|
4105
|
+
db,
|
|
4106
|
+
config,
|
|
4107
|
+
stmts,
|
|
4108
|
+
embed,
|
|
4109
|
+
insertVec: (r, e) => insertVec(stmts, r, e),
|
|
4110
|
+
deleteVec: (r) => deleteVec(stmts, r),
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
const { captureAndIndex } = dryRun ? {} : await import('@context-vault/core/capture');
|
|
4115
|
+
|
|
4116
|
+
let saved = 0;
|
|
4117
|
+
let skipped = 0;
|
|
4118
|
+
let errors = 0;
|
|
4119
|
+
|
|
4120
|
+
console.log(dim(`\n Processing stdin...`));
|
|
4121
|
+
|
|
4122
|
+
const opts = { ctx, db, captureAndIndex, dryRun, verbose, sourceFlag, kindFlag, extraTags };
|
|
4123
|
+
|
|
4124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4125
|
+
let parsed;
|
|
4126
|
+
try {
|
|
4127
|
+
parsed = JSON.parse(lines[i]);
|
|
4128
|
+
} catch {
|
|
4129
|
+
errors++;
|
|
4130
|
+
if (verbose) console.log(` ${red('!')} Invalid JSON on line ${i + 1}`);
|
|
4131
|
+
continue;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
const result = await ingestCommLine(parsed, opts);
|
|
4135
|
+
if (result === 'saved') saved++;
|
|
4136
|
+
else if (result === 'skipped') skipped++;
|
|
4137
|
+
else errors++;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
if (db) db.close();
|
|
4141
|
+
|
|
4142
|
+
// Summary
|
|
4143
|
+
if (dryRun) {
|
|
4144
|
+
console.log(` Dry run: ${saved} would be saved, ${skipped} duplicates, ${errors} errors\n`);
|
|
4145
|
+
} else {
|
|
4146
|
+
console.log(` ${green('✓')} ${saved} saved, ${skipped} skipped (duplicates), ${errors} errors\n`);
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
const summaryParts = [];
|
|
4150
|
+
if (sourceFlag) summaryParts.push(`Source: ${sourceFlag}`);
|
|
4151
|
+
summaryParts.push(`Kind: ${kindFlag}`);
|
|
4152
|
+
if (extraTags.length) summaryParts.push(`Tags: ${extraTags.join(', ')}`);
|
|
4153
|
+
if (summaryParts.length) console.log(` ${dim(summaryParts.join(' | '))}\n`);
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
async function runGmailBridge() {
|
|
4157
|
+
if (flags.has('--help')) {
|
|
4158
|
+
console.log(`
|
|
4159
|
+
${bold('context-vault gmail-bridge')}
|
|
4160
|
+
|
|
4161
|
+
Fetch recent emails via gmail-cli and ingest into the vault with dedup.
|
|
4162
|
+
|
|
4163
|
+
${bold('Flags:')}
|
|
4164
|
+
--account <name> Gmail account (default: personal)
|
|
4165
|
+
--max <n> Maximum messages to fetch (default: 50)
|
|
4166
|
+
--query <q> Gmail search query (default: newer_than:1d)
|
|
4167
|
+
--dry-run Show what would be ingested without saving
|
|
4168
|
+
--verbose Print each entry as it's processed
|
|
4169
|
+
|
|
4170
|
+
${bold('Examples:')}
|
|
4171
|
+
context-vault gmail-bridge --dry-run --max 3
|
|
4172
|
+
context-vault gmail-bridge --account stormfors --max 10
|
|
4173
|
+
context-vault gmail-bridge --query "is:unread newer_than:3d"
|
|
4174
|
+
`);
|
|
4175
|
+
return;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
const account = getFlag('--account') || 'personal';
|
|
4179
|
+
const max = getFlag('--max') || '50';
|
|
4180
|
+
const query = getFlag('--query') || 'newer_than:1d';
|
|
4181
|
+
const dryRun = flags.has('--dry-run');
|
|
4182
|
+
const verbose = flags.has('--verbose');
|
|
4183
|
+
|
|
4184
|
+
const GMAIL_CLI_PATH = '/Users/admin/omni/workspaces/_archive/agent-tools/gmail-cli/cli.ts';
|
|
4185
|
+
const SKIP_LABELS = new Set([
|
|
4186
|
+
'CATEGORY_PROMOTIONS',
|
|
4187
|
+
'CATEGORY_SOCIAL',
|
|
4188
|
+
'CATEGORY_UPDATES',
|
|
4189
|
+
'CATEGORY_FORUMS',
|
|
4190
|
+
]);
|
|
4191
|
+
const SKIP_FROM_PATTERNS = [/noreply/i, /newsletter/i, /marketing/i, /digest/i];
|
|
4192
|
+
|
|
4193
|
+
console.log(dim(`\n Fetching emails (account: ${account}, max: ${max}, query: ${query})...`));
|
|
4194
|
+
|
|
4195
|
+
let listOutput;
|
|
4196
|
+
try {
|
|
4197
|
+
listOutput = execFileSync('npx', ['tsx', GMAIL_CLI_PATH, 'list', '--account', account, '--max', max, '--query', query], {
|
|
4198
|
+
encoding: 'utf-8',
|
|
4199
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
4200
|
+
timeout: 60000,
|
|
4201
|
+
});
|
|
4202
|
+
} catch (e) {
|
|
4203
|
+
const stderr = e.stderr ? e.stderr.trim() : '';
|
|
4204
|
+
const msg = stderr || e.message;
|
|
4205
|
+
console.error(red(`\n Failed to list emails: ${msg}`));
|
|
4206
|
+
process.exit(1);
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
let listResult;
|
|
4210
|
+
try {
|
|
4211
|
+
listResult = JSON.parse(listOutput);
|
|
4212
|
+
} catch {
|
|
4213
|
+
console.error(red(`\n Failed to parse gmail-cli output as JSON.`));
|
|
4214
|
+
if (verbose) console.error(dim(` Raw output: ${listOutput.slice(0, 500)}`));
|
|
4215
|
+
process.exit(1);
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
if (!listResult.ok || !Array.isArray(listResult.messages)) {
|
|
4219
|
+
console.error(red(`\n gmail-cli returned an error or unexpected format.`));
|
|
4220
|
+
if (listResult.error) console.error(red(` ${listResult.error}`));
|
|
4221
|
+
process.exit(1);
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
const allMessages = listResult.messages;
|
|
4225
|
+
const filtered = allMessages.filter((msg) => {
|
|
4226
|
+
const labels = Array.isArray(msg.labels) ? msg.labels : [];
|
|
4227
|
+
if (labels.some((l) => SKIP_LABELS.has(l))) return false;
|
|
4228
|
+
const from = msg.from || '';
|
|
4229
|
+
if (SKIP_FROM_PATTERNS.some((p) => p.test(from))) return false;
|
|
4230
|
+
return true;
|
|
4231
|
+
});
|
|
4232
|
+
|
|
4233
|
+
const skippedNewsletter = allMessages.length - filtered.length;
|
|
4234
|
+
if (skippedNewsletter > 0) {
|
|
4235
|
+
console.log(dim(` Filtered out ${skippedNewsletter} newsletter/promo messages`));
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
if (!filtered.length) {
|
|
4239
|
+
console.log(`\n No messages to ingest after filtering.\n`);
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
console.log(dim(` ${filtered.length} messages to process...`));
|
|
4244
|
+
|
|
4245
|
+
// Bootstrap DB (unless dry-run)
|
|
4246
|
+
let ctx, db, captureAndIndex;
|
|
4247
|
+
if (!dryRun) {
|
|
4248
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
4249
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
4250
|
+
await import('@context-vault/core/db');
|
|
4251
|
+
const { embed } = await import('@context-vault/core/embed');
|
|
4252
|
+
const captureMod = await import('@context-vault/core/capture');
|
|
4253
|
+
captureAndIndex = captureMod.captureAndIndex;
|
|
4254
|
+
|
|
4255
|
+
const config = resolveConfig();
|
|
4256
|
+
if (!config.vaultDirExists) {
|
|
4257
|
+
console.error(red(`\n Vault directory not found: ${config.vaultDir}`));
|
|
4258
|
+
process.exit(1);
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
db = await initDatabase(config.dbPath);
|
|
4262
|
+
const stmts = prepareStatements(db);
|
|
4263
|
+
ctx = {
|
|
4264
|
+
db,
|
|
4265
|
+
config,
|
|
4266
|
+
stmts,
|
|
4267
|
+
embed,
|
|
4268
|
+
insertVec: (r, e) => insertVec(stmts, r, e),
|
|
4269
|
+
deleteVec: (r) => deleteVec(stmts, r),
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
const extraTags = ['bucket:comms', `account:${account}`];
|
|
4274
|
+
const opts = { ctx, db, captureAndIndex, dryRun, verbose, sourceFlag: 'gmail', kindFlag: 'event', extraTags };
|
|
4275
|
+
|
|
4276
|
+
let saved = 0;
|
|
4277
|
+
let skipped = 0;
|
|
4278
|
+
let errors = 0;
|
|
4279
|
+
|
|
4280
|
+
for (const msg of filtered) {
|
|
4281
|
+
// Fetch full body for each message
|
|
4282
|
+
let body = msg.snippet || '';
|
|
4283
|
+
if (!dryRun) {
|
|
4284
|
+
try {
|
|
4285
|
+
const getOutput = execFileSync('npx', ['tsx', GMAIL_CLI_PATH, 'get', msg.id, '--account', account], {
|
|
4286
|
+
encoding: 'utf-8',
|
|
4287
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
4288
|
+
timeout: 30000,
|
|
4289
|
+
});
|
|
4290
|
+
const fullMsg = JSON.parse(getOutput);
|
|
4291
|
+
if (fullMsg.body) body = fullMsg.body;
|
|
4292
|
+
} catch {
|
|
4293
|
+
if (verbose) console.log(` ${yellow('!')} Could not fetch body for ${msg.id}, using snippet`);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
const parsed = {
|
|
4298
|
+
id: msg.id,
|
|
4299
|
+
source: 'gmail',
|
|
4300
|
+
from: msg.from || '',
|
|
4301
|
+
to: Array.isArray(msg.to) ? msg.to : [],
|
|
4302
|
+
subject: msg.subject || '',
|
|
4303
|
+
body,
|
|
4304
|
+
date: msg.date || '',
|
|
4305
|
+
thread_id: msg.threadId || '',
|
|
4306
|
+
tags: extraTags,
|
|
4307
|
+
};
|
|
4308
|
+
|
|
4309
|
+
const result = await ingestCommLine(parsed, opts);
|
|
4310
|
+
if (result === 'saved') saved++;
|
|
4311
|
+
else if (result === 'skipped') skipped++;
|
|
4312
|
+
else errors++;
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
if (db) db.close();
|
|
4316
|
+
|
|
4317
|
+
if (dryRun) {
|
|
4318
|
+
console.log(`\n Dry run: ${saved} would be saved, ${skipped} duplicates, ${errors} errors\n`);
|
|
4319
|
+
} else {
|
|
4320
|
+
console.log(`\n ${green('✓')} ${saved} saved, ${skipped} skipped (duplicates), ${errors} errors\n`);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
async function runSlackBridge() {
|
|
4325
|
+
const SLACK_CLI_PATH = '/Users/admin/omni/workspaces/_archive/agent-tools/slack-cli/cli.ts';
|
|
4326
|
+
|
|
4327
|
+
if (flags.has('--help') || (!flags.has('--list-channels') && !getFlag('--channels'))) {
|
|
4328
|
+
console.log(`
|
|
4329
|
+
${bold('context-vault slack-bridge')}
|
|
4330
|
+
|
|
4331
|
+
Fetch recent Slack messages from allowed channels and ingest into the vault.
|
|
4332
|
+
|
|
4333
|
+
${bold('Flags:')}
|
|
4334
|
+
--channels <ids> Comma-separated channel IDs (required, explicit allowlist)
|
|
4335
|
+
--limit <n> Messages per channel (default: 50)
|
|
4336
|
+
--dry-run Show what would be ingested without saving
|
|
4337
|
+
--verbose Print each entry as it's processed
|
|
4338
|
+
--list-channels List available channels and exit
|
|
4339
|
+
|
|
4340
|
+
${bold('Examples:')}
|
|
4341
|
+
context-vault slack-bridge --list-channels
|
|
4342
|
+
context-vault slack-bridge --channels C123,C456 --dry-run
|
|
4343
|
+
context-vault slack-bridge --channels C123 --limit 10
|
|
4344
|
+
`);
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
// --list-channels: show available channels and exit
|
|
4349
|
+
if (flags.has('--list-channels')) {
|
|
4350
|
+
console.log(dim('\n Fetching Slack channels...'));
|
|
4351
|
+
let raw;
|
|
4352
|
+
try {
|
|
4353
|
+
raw = execFileSync('npx', ['tsx', SLACK_CLI_PATH, 'channels', '--limit', '100'], {
|
|
4354
|
+
encoding: 'utf-8',
|
|
4355
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
4356
|
+
timeout: 60000,
|
|
4357
|
+
});
|
|
4358
|
+
} catch (e) {
|
|
4359
|
+
const stderr = e.stderr ? e.stderr.trim() : '';
|
|
4360
|
+
const msg = stderr || e.message;
|
|
4361
|
+
console.error(red(`\n Failed to list channels: ${msg}`));
|
|
4362
|
+
process.exit(1);
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
let result;
|
|
4366
|
+
try {
|
|
4367
|
+
result = JSON.parse(raw);
|
|
4368
|
+
} catch {
|
|
4369
|
+
console.error(red('\n Failed to parse slack-cli output as JSON.'));
|
|
4370
|
+
process.exit(1);
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
const channels = Array.isArray(result.channels) ? result.channels : Array.isArray(result) ? result : [];
|
|
4374
|
+
if (!channels.length) {
|
|
4375
|
+
console.log('\n No channels found.\n');
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
console.log(`\n ${bold('Available channels:')}\n`);
|
|
4380
|
+
for (const ch of channels) {
|
|
4381
|
+
const id = ch.id || ch.channel_id || '?';
|
|
4382
|
+
const name = ch.name || ch.channel_name || '?';
|
|
4383
|
+
console.log(` ${cyan(id)} ${name}`);
|
|
4384
|
+
}
|
|
4385
|
+
console.log();
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
// Parse flags
|
|
4390
|
+
const channelsRaw = getFlag('--channels');
|
|
4391
|
+
const channelIds = channelsRaw.split(',').map((c) => c.trim()).filter(Boolean);
|
|
4392
|
+
if (!channelIds.length) {
|
|
4393
|
+
console.error(red('\n --channels requires at least one channel ID.'));
|
|
4394
|
+
process.exit(1);
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
const limit = getFlag('--limit') || '50';
|
|
4398
|
+
const dryRun = flags.has('--dry-run');
|
|
4399
|
+
const verbose = flags.has('--verbose');
|
|
4400
|
+
|
|
4401
|
+
// Bootstrap DB (unless dry-run)
|
|
4402
|
+
let ctx, db, captureAndIndex;
|
|
4403
|
+
if (!dryRun) {
|
|
4404
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
4405
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
4406
|
+
await import('@context-vault/core/db');
|
|
4407
|
+
const { embed } = await import('@context-vault/core/embed');
|
|
4408
|
+
const captureMod = await import('@context-vault/core/capture');
|
|
4409
|
+
captureAndIndex = captureMod.captureAndIndex;
|
|
4410
|
+
|
|
4411
|
+
const config = resolveConfig();
|
|
4412
|
+
if (!config.vaultDirExists) {
|
|
4413
|
+
console.error(red(`\n Vault directory not found: ${config.vaultDir}`));
|
|
4414
|
+
process.exit(1);
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
db = await initDatabase(config.dbPath);
|
|
4418
|
+
const stmts = prepareStatements(db);
|
|
4419
|
+
ctx = {
|
|
4420
|
+
db,
|
|
4421
|
+
config,
|
|
4422
|
+
stmts,
|
|
4423
|
+
embed,
|
|
4424
|
+
insertVec: (r, e) => insertVec(stmts, r, e),
|
|
4425
|
+
deleteVec: (r) => deleteVec(stmts, r),
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
const extraTags = ['bucket:comms'];
|
|
4430
|
+
const opts = { ctx, db, captureAndIndex, dryRun, verbose, sourceFlag: 'slack', kindFlag: 'event', extraTags };
|
|
4431
|
+
|
|
4432
|
+
let totalSaved = 0;
|
|
4433
|
+
let totalSkipped = 0;
|
|
4434
|
+
let totalErrors = 0;
|
|
4435
|
+
|
|
4436
|
+
for (const channelId of channelIds) {
|
|
4437
|
+
console.log(dim(`\n Fetching messages from ${channelId} (limit: ${limit})...`));
|
|
4438
|
+
|
|
4439
|
+
let raw;
|
|
4440
|
+
try {
|
|
4441
|
+
raw = execFileSync('npx', ['tsx', SLACK_CLI_PATH, 'channel-history', channelId, '--limit', limit], {
|
|
4442
|
+
encoding: 'utf-8',
|
|
4443
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
4444
|
+
timeout: 60000,
|
|
4445
|
+
});
|
|
4446
|
+
} catch (e) {
|
|
4447
|
+
const stderr = e.stderr ? e.stderr.trim() : '';
|
|
4448
|
+
const msg = stderr || e.message;
|
|
4449
|
+
console.error(red(` Failed to fetch channel ${channelId}: ${msg}`));
|
|
4450
|
+
totalErrors++;
|
|
4451
|
+
continue;
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
let result;
|
|
4455
|
+
try {
|
|
4456
|
+
result = JSON.parse(raw);
|
|
4457
|
+
} catch {
|
|
4458
|
+
console.error(red(` Failed to parse output for channel ${channelId}.`));
|
|
4459
|
+
totalErrors++;
|
|
4460
|
+
continue;
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
if (!result.ok || !Array.isArray(result.messages)) {
|
|
4464
|
+
console.error(red(` slack-cli returned an error for channel ${channelId}.`));
|
|
4465
|
+
if (result.error) console.error(red(` ${result.error}`));
|
|
4466
|
+
totalErrors++;
|
|
4467
|
+
continue;
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
// Filter out bot messages
|
|
4471
|
+
const messages = result.messages.filter((m) => !m.bot_id);
|
|
4472
|
+
const botCount = result.messages.length - messages.length;
|
|
4473
|
+
if (botCount > 0 && verbose) {
|
|
4474
|
+
console.log(dim(` Filtered out ${botCount} bot messages`));
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
if (!messages.length) {
|
|
4478
|
+
console.log(dim(` No messages to ingest for ${channelId}.`));
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
console.log(dim(` ${messages.length} messages to process...`));
|
|
4483
|
+
|
|
4484
|
+
for (const msg of messages) {
|
|
4485
|
+
const ts = msg.ts || '';
|
|
4486
|
+
const epochSec = parseFloat(ts) || 0;
|
|
4487
|
+
const isoDate = epochSec ? new Date(epochSec * 1000).toISOString() : '';
|
|
4488
|
+
|
|
4489
|
+
const parsed = {
|
|
4490
|
+
id: `${channelId}:${ts}`,
|
|
4491
|
+
source: 'slack',
|
|
4492
|
+
from: msg.user || '',
|
|
4493
|
+
to: [],
|
|
4494
|
+
subject: null,
|
|
4495
|
+
body: msg.text || '',
|
|
4496
|
+
date: isoDate,
|
|
4497
|
+
thread_id: msg.thread_ts || null,
|
|
4498
|
+
channel: channelId,
|
|
4499
|
+
tags: [...extraTags, `channel:${channelId}`],
|
|
4500
|
+
};
|
|
4501
|
+
|
|
4502
|
+
const res = await ingestCommLine(parsed, opts);
|
|
4503
|
+
if (res === 'saved') totalSaved++;
|
|
4504
|
+
else if (res === 'skipped') totalSkipped++;
|
|
4505
|
+
else totalErrors++;
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
if (db) db.close();
|
|
4510
|
+
|
|
4511
|
+
if (dryRun) {
|
|
4512
|
+
console.log(`\n Dry run: ${totalSaved} would be saved, ${totalSkipped} duplicates, ${totalErrors} errors\n`);
|
|
4513
|
+
} else {
|
|
4514
|
+
console.log(`\n ${green('✓')} ${totalSaved} saved, ${totalSkipped} skipped (duplicates), ${totalErrors} errors\n`);
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
|
|
3965
4518
|
async function runIngestProject() {
|
|
3966
4519
|
const rawPath = args[1];
|
|
3967
4520
|
if (!rawPath) {
|
|
@@ -6399,123 +6952,13 @@ async function runHealth() {
|
|
|
6399
6952
|
console.log(red(`context-vault health — FAILED`));
|
|
6400
6953
|
}
|
|
6401
6954
|
|
|
6402
|
-
for (const line of lines) {
|
|
6403
|
-
console.log(line);
|
|
6404
|
-
}
|
|
6405
|
-
|
|
6406
|
-
console.log(` status: ${healthy ? green('healthy') : red('unhealthy')}`);
|
|
6407
|
-
|
|
6408
|
-
if (!healthy) process.exit(1);
|
|
6409
|
-
}
|
|
6410
|
-
|
|
6411
|
-
async function runRestart() {
|
|
6412
|
-
const force = flags.has('--force');
|
|
6413
|
-
|
|
6414
|
-
console.log();
|
|
6415
|
-
console.log(` ${bold('◇ context-vault restart')}`);
|
|
6416
|
-
console.log();
|
|
6417
|
-
|
|
6418
|
-
const isWin = platform() === 'win32';
|
|
6419
|
-
let psOutput;
|
|
6420
|
-
try {
|
|
6421
|
-
const psCmd = isWin
|
|
6422
|
-
? 'wmic process where "CommandLine like \'%context-vault%\'" get ProcessId,CommandLine /format:list'
|
|
6423
|
-
: 'ps aux';
|
|
6424
|
-
psOutput = execSync(psCmd, { encoding: 'utf-8', timeout: 5000 });
|
|
6425
|
-
} catch (e) {
|
|
6426
|
-
console.error(red(` Failed to list processes: ${e.message}`));
|
|
6427
|
-
process.exit(1);
|
|
6428
|
-
}
|
|
6429
|
-
|
|
6430
|
-
const currentPid = process.pid;
|
|
6431
|
-
const serverPids = [];
|
|
6432
|
-
|
|
6433
|
-
if (isWin) {
|
|
6434
|
-
const pidMatches = psOutput.matchAll(/ProcessId=(\d+)/g);
|
|
6435
|
-
for (const m of pidMatches) {
|
|
6436
|
-
const pid = parseInt(m[1], 10);
|
|
6437
|
-
if (pid !== currentPid) serverPids.push(pid);
|
|
6438
|
-
}
|
|
6439
|
-
} else {
|
|
6440
|
-
const lines = psOutput.split('\n');
|
|
6441
|
-
for (const line of lines) {
|
|
6442
|
-
const match = line.match(/^\S+\s+(\d+)\s/);
|
|
6443
|
-
if (!match) continue;
|
|
6444
|
-
const pid = parseInt(match[1], 10);
|
|
6445
|
-
if (pid === currentPid) continue;
|
|
6446
|
-
if (
|
|
6447
|
-
/context-vault.*(serve|stdio|server\/index)/.test(line) ||
|
|
6448
|
-
/server\/index\.js.*context-vault/.test(line)
|
|
6449
|
-
) {
|
|
6450
|
-
serverPids.push(pid);
|
|
6451
|
-
}
|
|
6452
|
-
}
|
|
6453
|
-
}
|
|
6454
|
-
|
|
6455
|
-
if (serverPids.length === 0) {
|
|
6456
|
-
console.log(dim(' No running context-vault MCP server processes found.'));
|
|
6457
|
-
console.log(dim(' The MCP client will start the server automatically on the next tool call.'));
|
|
6458
|
-
console.log();
|
|
6459
|
-
return;
|
|
6460
|
-
}
|
|
6461
|
-
|
|
6462
|
-
console.log(
|
|
6463
|
-
` Found ${serverPids.length} server process${serverPids.length === 1 ? '' : 'es'}: ${dim(serverPids.join(', '))}`
|
|
6464
|
-
);
|
|
6465
|
-
console.log();
|
|
6466
|
-
|
|
6467
|
-
const signal = force ? 'SIGKILL' : 'SIGTERM';
|
|
6468
|
-
const killed = [];
|
|
6469
|
-
const failed = [];
|
|
6470
|
-
|
|
6471
|
-
for (const pid of serverPids) {
|
|
6472
|
-
try {
|
|
6473
|
-
process.kill(pid, signal);
|
|
6474
|
-
killed.push(pid);
|
|
6475
|
-
console.log(` ${green('✓')} Sent ${signal} to PID ${pid}`);
|
|
6476
|
-
} catch (e) {
|
|
6477
|
-
if (e.code === 'ESRCH') {
|
|
6478
|
-
console.log(` ${dim('-')} PID ${pid} already gone`);
|
|
6479
|
-
} else {
|
|
6480
|
-
failed.push(pid);
|
|
6481
|
-
console.log(` ${red('✘')} Failed to signal PID ${pid}: ${e.message}`);
|
|
6482
|
-
}
|
|
6483
|
-
}
|
|
6484
|
-
}
|
|
6485
|
-
|
|
6486
|
-
if (!force && killed.length > 0) {
|
|
6487
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
6488
|
-
|
|
6489
|
-
for (const pid of killed) {
|
|
6490
|
-
try {
|
|
6491
|
-
process.kill(pid, 0);
|
|
6492
|
-
console.log(` ${yellow('!')} PID ${pid} still running — sending SIGKILL`);
|
|
6493
|
-
try {
|
|
6494
|
-
process.kill(pid, 'SIGKILL');
|
|
6495
|
-
} catch {}
|
|
6496
|
-
} catch {
|
|
6497
|
-
// process is gone — expected
|
|
6498
|
-
}
|
|
6499
|
-
}
|
|
6500
|
-
}
|
|
6501
|
-
|
|
6502
|
-
console.log();
|
|
6503
|
-
|
|
6504
|
-
if (failed.length > 0) {
|
|
6505
|
-
console.log(
|
|
6506
|
-
red(
|
|
6507
|
-
` Could not stop ${failed.length} process${failed.length === 1 ? '' : 'es'}. Try --force.`
|
|
6508
|
-
)
|
|
6509
|
-
);
|
|
6510
|
-
process.exit(1);
|
|
6511
|
-
} else {
|
|
6512
|
-
console.log(
|
|
6513
|
-
green(' Server stopped.') +
|
|
6514
|
-
dim(' The MCP client will restart it automatically on the next tool call.')
|
|
6515
|
-
);
|
|
6955
|
+
for (const line of lines) {
|
|
6956
|
+
console.log(line);
|
|
6516
6957
|
}
|
|
6517
6958
|
|
|
6518
|
-
console.log();
|
|
6959
|
+
console.log(` status: ${healthy ? green('healthy') : red('unhealthy')}`);
|
|
6960
|
+
|
|
6961
|
+
if (!healthy) process.exit(1);
|
|
6519
6962
|
}
|
|
6520
6963
|
|
|
6521
6964
|
async function runReconnect() {
|
|
@@ -6857,323 +7300,6 @@ async function runDebug() {
|
|
|
6857
7300
|
console.log(lines.join('\n'));
|
|
6858
7301
|
}
|
|
6859
7302
|
|
|
6860
|
-
async function runDaemon() {
|
|
6861
|
-
const sub = args[1];
|
|
6862
|
-
const pidPath = join(HOME, '.context-mcp', 'daemon.pid');
|
|
6863
|
-
const defaultPort = 3377;
|
|
6864
|
-
|
|
6865
|
-
function readPid() {
|
|
6866
|
-
try {
|
|
6867
|
-
return JSON.parse(readFileSync(pidPath, 'utf-8'));
|
|
6868
|
-
} catch {
|
|
6869
|
-
return null;
|
|
6870
|
-
}
|
|
6871
|
-
}
|
|
6872
|
-
|
|
6873
|
-
function isAlive(pid) {
|
|
6874
|
-
try {
|
|
6875
|
-
process.kill(pid, 0);
|
|
6876
|
-
return true;
|
|
6877
|
-
} catch {
|
|
6878
|
-
return false;
|
|
6879
|
-
}
|
|
6880
|
-
}
|
|
6881
|
-
|
|
6882
|
-
async function pollHealth(port, timeoutMs = 5000) {
|
|
6883
|
-
const start = Date.now();
|
|
6884
|
-
while (Date.now() - start < timeoutMs) {
|
|
6885
|
-
try {
|
|
6886
|
-
const res = await fetch(`http://localhost:${port}/health`);
|
|
6887
|
-
if (res.ok) return await res.json();
|
|
6888
|
-
} catch {}
|
|
6889
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
6890
|
-
}
|
|
6891
|
-
return null;
|
|
6892
|
-
}
|
|
6893
|
-
|
|
6894
|
-
function configureClaudeDaemon(port) {
|
|
6895
|
-
const env = { ...process.env };
|
|
6896
|
-
delete env.CLAUDECODE;
|
|
6897
|
-
|
|
6898
|
-
for (const oldName of ['context-mcp', 'context-vault']) {
|
|
6899
|
-
try {
|
|
6900
|
-
execFileSync('claude', ['mcp', 'remove', oldName, '-s', 'user'], {
|
|
6901
|
-
stdio: 'pipe',
|
|
6902
|
-
env,
|
|
6903
|
-
});
|
|
6904
|
-
} catch {}
|
|
6905
|
-
}
|
|
6906
|
-
|
|
6907
|
-
try {
|
|
6908
|
-
execFileSync(
|
|
6909
|
-
'claude',
|
|
6910
|
-
[
|
|
6911
|
-
'mcp', 'add', '-s', 'user',
|
|
6912
|
-
'--transport', 'http',
|
|
6913
|
-
'context-vault',
|
|
6914
|
-
`http://localhost:${port}/mcp`,
|
|
6915
|
-
],
|
|
6916
|
-
{ stdio: 'pipe', env }
|
|
6917
|
-
);
|
|
6918
|
-
} catch (e) {
|
|
6919
|
-
const stderr = e.stderr?.toString().trim();
|
|
6920
|
-
throw new Error(stderr || e.message);
|
|
6921
|
-
}
|
|
6922
|
-
}
|
|
6923
|
-
|
|
6924
|
-
if (!sub || sub === '--help') {
|
|
6925
|
-
console.log(`
|
|
6926
|
-
${bold('◇ context-vault daemon')} ${dim('— shared HTTP daemon')}
|
|
6927
|
-
|
|
6928
|
-
${bold('Subcommands:')}
|
|
6929
|
-
${cyan('start')} [--port PORT] Start the daemon (default port: ${defaultPort})
|
|
6930
|
-
${cyan('stop')} Stop the running daemon
|
|
6931
|
-
${cyan('status')} Show daemon status
|
|
6932
|
-
${cyan('install')} Start daemon + configure Claude Code to use it
|
|
6933
|
-
${cyan('uninstall')} Stop daemon + revert Claude Code to stdio mode
|
|
6934
|
-
`);
|
|
6935
|
-
return;
|
|
6936
|
-
}
|
|
6937
|
-
|
|
6938
|
-
if (sub === 'start') {
|
|
6939
|
-
const port = parseInt(getFlag('--port') || String(defaultPort), 10);
|
|
6940
|
-
const existing = readPid();
|
|
6941
|
-
|
|
6942
|
-
if (existing && isAlive(existing.pid)) {
|
|
6943
|
-
console.log(` ${green('✓')} Daemon already running (PID ${existing.pid} on port ${existing.port})`);
|
|
6944
|
-
return;
|
|
6945
|
-
}
|
|
6946
|
-
|
|
6947
|
-
if (existing) {
|
|
6948
|
-
try { unlinkSync(pidPath); } catch {}
|
|
6949
|
-
}
|
|
6950
|
-
|
|
6951
|
-
console.log(` Starting daemon on port ${port}...`);
|
|
6952
|
-
|
|
6953
|
-
const vaultDir = getFlag('--vault-dir');
|
|
6954
|
-
const serverArgs = [SERVER_PATH, '--http', '--port', String(port)];
|
|
6955
|
-
if (vaultDir) serverArgs.push('--vault-dir', vaultDir);
|
|
6956
|
-
|
|
6957
|
-
const child = spawn(process.execPath, serverArgs, {
|
|
6958
|
-
detached: true,
|
|
6959
|
-
stdio: 'ignore',
|
|
6960
|
-
env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning' },
|
|
6961
|
-
});
|
|
6962
|
-
child.unref();
|
|
6963
|
-
|
|
6964
|
-
const health = await pollHealth(port);
|
|
6965
|
-
if (health) {
|
|
6966
|
-
console.log(` ${green('✓')} Daemon started on http://localhost:${port}/mcp (PID ${health.pid})`);
|
|
6967
|
-
} else {
|
|
6968
|
-
console.error(red(` Failed to start daemon. Check error log: ~/.context-mcp/error.log`));
|
|
6969
|
-
process.exit(1);
|
|
6970
|
-
}
|
|
6971
|
-
|
|
6972
|
-
} else if (sub === 'stop') {
|
|
6973
|
-
const existing = readPid();
|
|
6974
|
-
if (!existing) {
|
|
6975
|
-
console.log(dim(' No daemon running.'));
|
|
6976
|
-
return;
|
|
6977
|
-
}
|
|
6978
|
-
|
|
6979
|
-
if (!isAlive(existing.pid)) {
|
|
6980
|
-
console.log(dim(' Stale PID file (process not running). Cleaning up.'));
|
|
6981
|
-
try { unlinkSync(pidPath); } catch {}
|
|
6982
|
-
return;
|
|
6983
|
-
}
|
|
6984
|
-
|
|
6985
|
-
console.log(` Stopping daemon (PID ${existing.pid})...`);
|
|
6986
|
-
process.kill(existing.pid, 'SIGTERM');
|
|
6987
|
-
|
|
6988
|
-
const deadline = Date.now() + 3000;
|
|
6989
|
-
while (Date.now() < deadline && isAlive(existing.pid)) {
|
|
6990
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
6991
|
-
}
|
|
6992
|
-
|
|
6993
|
-
if (isAlive(existing.pid)) {
|
|
6994
|
-
console.log(` ${yellow('!')} Still alive, sending SIGKILL...`);
|
|
6995
|
-
try { process.kill(existing.pid, 'SIGKILL'); } catch {}
|
|
6996
|
-
}
|
|
6997
|
-
|
|
6998
|
-
try { unlinkSync(pidPath); } catch {}
|
|
6999
|
-
console.log(` ${green('✓')} Daemon stopped.`);
|
|
7000
|
-
|
|
7001
|
-
} else if (sub === 'status') {
|
|
7002
|
-
const existing = readPid();
|
|
7003
|
-
if (!existing) {
|
|
7004
|
-
console.log(dim(' Not running.'));
|
|
7005
|
-
return;
|
|
7006
|
-
}
|
|
7007
|
-
|
|
7008
|
-
if (!isAlive(existing.pid)) {
|
|
7009
|
-
console.log(` ${yellow('!')} Stale PID file (PID ${existing.pid} not found).`);
|
|
7010
|
-
return;
|
|
7011
|
-
}
|
|
7012
|
-
|
|
7013
|
-
try {
|
|
7014
|
-
const res = await fetch(`http://localhost:${existing.port}/health`);
|
|
7015
|
-
const health = await res.json();
|
|
7016
|
-
const uptimeMin = Math.floor(health.uptime / 60);
|
|
7017
|
-
console.log(
|
|
7018
|
-
` ${green('●')} Running (PID ${health.pid}, port ${existing.port}, v${health.version}, ` +
|
|
7019
|
-
`${health.sessions} session${health.sessions === 1 ? '' : 's'}, uptime ${uptimeMin}m)`
|
|
7020
|
-
);
|
|
7021
|
-
} catch (e) {
|
|
7022
|
-
console.log(` ${yellow('!')} Process alive (PID ${existing.pid}) but health check failed: ${e.message}`);
|
|
7023
|
-
}
|
|
7024
|
-
|
|
7025
|
-
} else if (sub === 'install') {
|
|
7026
|
-
const port = parseInt(getFlag('--port') || String(defaultPort), 10);
|
|
7027
|
-
|
|
7028
|
-
// 1. Install LaunchAgent on macOS for auto-start on login
|
|
7029
|
-
if (platform() === 'darwin') {
|
|
7030
|
-
const launchAgentDir = join(HOME, 'Library', 'LaunchAgents');
|
|
7031
|
-
const plistPath = join(launchAgentDir, 'com.context-vault.daemon.plist');
|
|
7032
|
-
const logPath = join(HOME, '.context-mcp', 'daemon.log');
|
|
7033
|
-
const vaultDir = getFlag('--vault-dir');
|
|
7034
|
-
const progArgs = [process.execPath, SERVER_PATH, '--http', '--port', String(port)];
|
|
7035
|
-
if (vaultDir) progArgs.push('--vault-dir', vaultDir);
|
|
7036
|
-
|
|
7037
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7038
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7039
|
-
<plist version="1.0">
|
|
7040
|
-
<dict>
|
|
7041
|
-
<key>Label</key>
|
|
7042
|
-
<string>com.context-vault.daemon</string>
|
|
7043
|
-
<key>ProgramArguments</key>
|
|
7044
|
-
<array>
|
|
7045
|
-
${progArgs.map(a => ` <string>${a}</string>`).join('\n')}
|
|
7046
|
-
</array>
|
|
7047
|
-
<key>RunAtLoad</key>
|
|
7048
|
-
<true/>
|
|
7049
|
-
<key>KeepAlive</key>
|
|
7050
|
-
<dict>
|
|
7051
|
-
<key>SuccessfulExit</key>
|
|
7052
|
-
<false/>
|
|
7053
|
-
</dict>
|
|
7054
|
-
<key>StandardErrorPath</key>
|
|
7055
|
-
<string>${logPath}</string>
|
|
7056
|
-
<key>StandardOutPath</key>
|
|
7057
|
-
<string>/dev/null</string>
|
|
7058
|
-
<key>EnvironmentVariables</key>
|
|
7059
|
-
<dict>
|
|
7060
|
-
<key>NODE_OPTIONS</key>
|
|
7061
|
-
<string>--no-warnings=ExperimentalWarning</string>
|
|
7062
|
-
<key>CONTEXT_VAULT_NO_DAEMON</key>
|
|
7063
|
-
<string>1</string>
|
|
7064
|
-
</dict>
|
|
7065
|
-
<key>ThrottleInterval</key>
|
|
7066
|
-
<integer>5</integer>
|
|
7067
|
-
</dict>
|
|
7068
|
-
</plist>`;
|
|
7069
|
-
|
|
7070
|
-
mkdirSync(launchAgentDir, { recursive: true });
|
|
7071
|
-
|
|
7072
|
-
// Unload existing agent if present
|
|
7073
|
-
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
7074
|
-
|
|
7075
|
-
writeFileSync(plistPath, plist);
|
|
7076
|
-
try {
|
|
7077
|
-
execSync(`launchctl load -w "${plistPath}"`, { stdio: 'pipe' });
|
|
7078
|
-
console.log(` ${green('✓')} LaunchAgent installed (auto-starts on login, restarts on crash)`);
|
|
7079
|
-
} catch (e) {
|
|
7080
|
-
console.log(` ${yellow('!')} LaunchAgent write succeeded but launchctl load failed: ${e.message}`);
|
|
7081
|
-
}
|
|
7082
|
-
|
|
7083
|
-
// Wait for launchd to start the daemon
|
|
7084
|
-
const health = await pollHealth(port, 8000);
|
|
7085
|
-
if (health) {
|
|
7086
|
-
console.log(` ${green('✓')} Daemon running (PID ${health.pid})`);
|
|
7087
|
-
} else {
|
|
7088
|
-
console.error(red(` Daemon did not start. Check log: ${logPath}`));
|
|
7089
|
-
process.exit(1);
|
|
7090
|
-
}
|
|
7091
|
-
} else {
|
|
7092
|
-
// Non-macOS: direct spawn (no service manager integration yet)
|
|
7093
|
-
const existing = readPid();
|
|
7094
|
-
if (!existing || !isAlive(existing.pid)) {
|
|
7095
|
-
if (existing) try { unlinkSync(pidPath); } catch {}
|
|
7096
|
-
|
|
7097
|
-
console.log(` Starting daemon on port ${port}...`);
|
|
7098
|
-
const vaultDir = getFlag('--vault-dir');
|
|
7099
|
-
const serverArgs = [SERVER_PATH, '--http', '--port', String(port)];
|
|
7100
|
-
if (vaultDir) serverArgs.push('--vault-dir', vaultDir);
|
|
7101
|
-
|
|
7102
|
-
const child = spawn(process.execPath, serverArgs, {
|
|
7103
|
-
detached: true,
|
|
7104
|
-
stdio: 'ignore',
|
|
7105
|
-
env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning' },
|
|
7106
|
-
});
|
|
7107
|
-
child.unref();
|
|
7108
|
-
|
|
7109
|
-
const health = await pollHealth(port);
|
|
7110
|
-
if (!health) {
|
|
7111
|
-
console.error(red(` Failed to start daemon.`));
|
|
7112
|
-
process.exit(1);
|
|
7113
|
-
}
|
|
7114
|
-
console.log(` ${green('✓')} Daemon started (PID ${health.pid})`);
|
|
7115
|
-
} else {
|
|
7116
|
-
console.log(` ${green('✓')} Daemon already running (PID ${existing.pid})`);
|
|
7117
|
-
}
|
|
7118
|
-
}
|
|
7119
|
-
|
|
7120
|
-
// 2. Configure Claude Code for HTTP transport
|
|
7121
|
-
console.log(` Configuring Claude Code to use HTTP transport...`);
|
|
7122
|
-
try {
|
|
7123
|
-
configureClaudeDaemon(port);
|
|
7124
|
-
console.log(` ${green('✓')} Claude Code configured for http://localhost:${port}/mcp`);
|
|
7125
|
-
console.log();
|
|
7126
|
-
console.log(dim(' Restart any open Claude Code sessions for the change to take effect.'));
|
|
7127
|
-
} catch (e) {
|
|
7128
|
-
console.error(red(` Failed to configure Claude Code: ${e.message}`));
|
|
7129
|
-
process.exit(1);
|
|
7130
|
-
}
|
|
7131
|
-
|
|
7132
|
-
} else if (sub === 'uninstall') {
|
|
7133
|
-
// 1. Revert Claude Code to stdio
|
|
7134
|
-
console.log(` Reverting Claude Code to stdio mode...`);
|
|
7135
|
-
try {
|
|
7136
|
-
const vaultDir = getFlag('--vault-dir') || join(HOME, '.vault');
|
|
7137
|
-
const tool = { name: 'Claude Code', configPath: null };
|
|
7138
|
-
await configureClaude(tool, vaultDir);
|
|
7139
|
-
console.log(` ${green('✓')} Claude Code reverted to stdio`);
|
|
7140
|
-
} catch (e) {
|
|
7141
|
-
console.error(red(` Failed to reconfigure Claude Code: ${e.message}`));
|
|
7142
|
-
}
|
|
7143
|
-
|
|
7144
|
-
// 2. Remove LaunchAgent on macOS
|
|
7145
|
-
if (platform() === 'darwin') {
|
|
7146
|
-
const plistPath = join(HOME, 'Library', 'LaunchAgents', 'com.context-vault.daemon.plist');
|
|
7147
|
-
if (existsSync(plistPath)) {
|
|
7148
|
-
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
7149
|
-
try { unlinkSync(plistPath); } catch {}
|
|
7150
|
-
console.log(` ${green('✓')} LaunchAgent removed`);
|
|
7151
|
-
}
|
|
7152
|
-
}
|
|
7153
|
-
|
|
7154
|
-
// 3. Stop daemon if running
|
|
7155
|
-
const existing = readPid();
|
|
7156
|
-
if (existing && isAlive(existing.pid)) {
|
|
7157
|
-
console.log(` Stopping daemon (PID ${existing.pid})...`);
|
|
7158
|
-
process.kill(existing.pid, 'SIGTERM');
|
|
7159
|
-
const deadline = Date.now() + 3000;
|
|
7160
|
-
while (Date.now() < deadline && isAlive(existing.pid)) {
|
|
7161
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
7162
|
-
}
|
|
7163
|
-
if (isAlive(existing.pid)) {
|
|
7164
|
-
try { process.kill(existing.pid, 'SIGKILL'); } catch {}
|
|
7165
|
-
}
|
|
7166
|
-
try { unlinkSync(pidPath); } catch {}
|
|
7167
|
-
console.log(` ${green('✓')} Daemon stopped.`);
|
|
7168
|
-
}
|
|
7169
|
-
|
|
7170
|
-
} else {
|
|
7171
|
-
console.error(red(` Unknown daemon subcommand: ${sub}`));
|
|
7172
|
-
console.error(` Run ${cyan('context-vault daemon --help')} for usage.`);
|
|
7173
|
-
process.exit(1);
|
|
7174
|
-
}
|
|
7175
|
-
}
|
|
7176
|
-
|
|
7177
7303
|
async function runTier() {
|
|
7178
7304
|
const { resolveConfig } = await import('@context-vault/core/config');
|
|
7179
7305
|
const { initDatabase, prepareStatements, insertVec, deleteVec, insertCtxVec, deleteCtxVec } = await import('@context-vault/core/db');
|
|
@@ -8488,6 +8614,313 @@ async function drainOfflineQueue(apiUrl, apiKey) {
|
|
|
8488
8614
|
}
|
|
8489
8615
|
}
|
|
8490
8616
|
|
|
8617
|
+
async function runContacts() {
|
|
8618
|
+
const sub = args[1];
|
|
8619
|
+
|
|
8620
|
+
if (flags.has('--help') || !sub) {
|
|
8621
|
+
console.log(`
|
|
8622
|
+
${bold('context-vault contacts')} <subcommand> [options]
|
|
8623
|
+
|
|
8624
|
+
Manage contact entities in the vault.
|
|
8625
|
+
|
|
8626
|
+
${bold('Subcommands:')}
|
|
8627
|
+
${cyan('list')} List all contacts
|
|
8628
|
+
${cyan('show')} <identity_key> Show a specific contact by identity key
|
|
8629
|
+
${cyan('add')} Add a new contact
|
|
8630
|
+
|
|
8631
|
+
${bold('List options:')}
|
|
8632
|
+
--tags <a,b,c> Filter by tags (comma-separated)
|
|
8633
|
+
--format <fmt> Output format: plain (default), json
|
|
8634
|
+
|
|
8635
|
+
${bold('Show options:')}
|
|
8636
|
+
--format <fmt> Output format: plain (default), json
|
|
8637
|
+
|
|
8638
|
+
${bold('Add options:')}
|
|
8639
|
+
--name <name> Contact name (required)
|
|
8640
|
+
--key <identity_key> Identity key for lookup (required)
|
|
8641
|
+
--email <email> Email address
|
|
8642
|
+
--role <role> Role or title
|
|
8643
|
+
--tags <a,b,c> Additional tags (comma-separated)
|
|
8644
|
+
--notes <text> Free-form notes
|
|
8645
|
+
|
|
8646
|
+
${bold('Examples:')}
|
|
8647
|
+
context-vault contacts list
|
|
8648
|
+
context-vault contacts list --tags "bucket:stormfors" --format json
|
|
8649
|
+
context-vault contacts show felix-hellstrom
|
|
8650
|
+
context-vault contacts add --name "Jane Doe" --key "jane-doe" --email "jane@example.com" --role "Engineer"
|
|
8651
|
+
`);
|
|
8652
|
+
return;
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
const format = getFlag('--format') || 'plain';
|
|
8656
|
+
|
|
8657
|
+
let db;
|
|
8658
|
+
try {
|
|
8659
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
8660
|
+
const config = resolveConfig();
|
|
8661
|
+
if (!config.vaultDirExists) {
|
|
8662
|
+
console.error(red('Error: vault not initialised — run `context-vault setup` first'));
|
|
8663
|
+
process.exit(1);
|
|
8664
|
+
}
|
|
8665
|
+
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
8666
|
+
await import('@context-vault/core/db');
|
|
8667
|
+
db = await initDatabase(config.dbPath);
|
|
8668
|
+
const stmts = prepareStatements(db);
|
|
8669
|
+
|
|
8670
|
+
if (sub === 'list') {
|
|
8671
|
+
const tagsStr = getFlag('--tags');
|
|
8672
|
+
let rows = db.prepare(
|
|
8673
|
+
`SELECT id, title, identity_key, tags, body FROM vault WHERE kind = 'contact' AND superseded_by IS NULL ORDER BY title`
|
|
8674
|
+
).all();
|
|
8675
|
+
|
|
8676
|
+
if (tagsStr) {
|
|
8677
|
+
const filterTags = tagsStr.split(',').map((t) => t.trim().toLowerCase());
|
|
8678
|
+
rows = rows.filter((r) => {
|
|
8679
|
+
const entryTags = r.tags ? JSON.parse(r.tags).map((t) => t.toLowerCase()) : [];
|
|
8680
|
+
return filterTags.some((ft) => entryTags.includes(ft));
|
|
8681
|
+
});
|
|
8682
|
+
}
|
|
8683
|
+
|
|
8684
|
+
if (rows.length === 0) {
|
|
8685
|
+
console.log(dim('No contacts found.'));
|
|
8686
|
+
return;
|
|
8687
|
+
}
|
|
8688
|
+
|
|
8689
|
+
if (format === 'json') {
|
|
8690
|
+
const output = rows.map((r) => ({
|
|
8691
|
+
id: r.id,
|
|
8692
|
+
title: r.title,
|
|
8693
|
+
identity_key: r.identity_key,
|
|
8694
|
+
tags: r.tags ? JSON.parse(r.tags) : [],
|
|
8695
|
+
body: r.body || '',
|
|
8696
|
+
}));
|
|
8697
|
+
console.log(JSON.stringify(output, null, 2));
|
|
8698
|
+
} else {
|
|
8699
|
+
const nameW = 30;
|
|
8700
|
+
const keyW = 25;
|
|
8701
|
+
console.log(` ${bold('Name'.padEnd(nameW))} ${bold('Key'.padEnd(keyW))} ${bold('Tags')}`);
|
|
8702
|
+
console.log(` ${'─'.repeat(nameW)} ${'─'.repeat(keyW)} ${'─'.repeat(30)}`);
|
|
8703
|
+
for (const r of rows) {
|
|
8704
|
+
const name = (r.title || '').slice(0, nameW).padEnd(nameW);
|
|
8705
|
+
const key = (r.identity_key || '').slice(0, keyW).padEnd(keyW);
|
|
8706
|
+
const tags = r.tags ? JSON.parse(r.tags).join(', ') : '';
|
|
8707
|
+
console.log(` ${name} ${dim(key)} ${dim(tags)}`);
|
|
8708
|
+
}
|
|
8709
|
+
console.log(dim(`\n ${rows.length} contact(s)`));
|
|
8710
|
+
}
|
|
8711
|
+
|
|
8712
|
+
} else if (sub === 'show') {
|
|
8713
|
+
const identityKey = args[2];
|
|
8714
|
+
if (!identityKey || identityKey.startsWith('--')) {
|
|
8715
|
+
console.error(red('Error: provide an identity_key, e.g. context-vault contacts show felix-hellstrom'));
|
|
8716
|
+
process.exit(1);
|
|
8717
|
+
}
|
|
8718
|
+
const row = stmts.getByIdentityKey.get('contact', identityKey);
|
|
8719
|
+
if (!row) {
|
|
8720
|
+
console.error(red(`Error: no contact found with identity_key "${identityKey}"`));
|
|
8721
|
+
process.exit(1);
|
|
8722
|
+
}
|
|
8723
|
+
|
|
8724
|
+
if (format === 'json') {
|
|
8725
|
+
console.log(JSON.stringify({
|
|
8726
|
+
id: row.id,
|
|
8727
|
+
title: row.title,
|
|
8728
|
+
identity_key: row.identity_key,
|
|
8729
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
8730
|
+
body: row.body || '',
|
|
8731
|
+
created_at: row.created_at,
|
|
8732
|
+
updated_at: row.updated_at,
|
|
8733
|
+
}, null, 2));
|
|
8734
|
+
} else {
|
|
8735
|
+
console.log(`\n ${bold(row.title || identityKey)}`);
|
|
8736
|
+
console.log(` ${dim('Key:')} ${row.identity_key}`);
|
|
8737
|
+
const tags = row.tags ? JSON.parse(row.tags) : [];
|
|
8738
|
+
if (tags.length) console.log(` ${dim('Tags:')} ${tags.join(', ')}`);
|
|
8739
|
+
if (row.created_at) console.log(` ${dim('Created:')} ${row.created_at}`);
|
|
8740
|
+
if (row.updated_at) console.log(` ${dim('Updated:')} ${row.updated_at}`);
|
|
8741
|
+
console.log(`\n${row.body || dim('(no body)')}\n`);
|
|
8742
|
+
}
|
|
8743
|
+
|
|
8744
|
+
} else if (sub === 'add') {
|
|
8745
|
+
const name = getFlag('--name');
|
|
8746
|
+
const key = getFlag('--key');
|
|
8747
|
+
const email = getFlag('--email');
|
|
8748
|
+
const role = getFlag('--role');
|
|
8749
|
+
const tagsStr = getFlag('--tags');
|
|
8750
|
+
const notes = getFlag('--notes');
|
|
8751
|
+
|
|
8752
|
+
if (!name) {
|
|
8753
|
+
console.error(red('Error: --name is required'));
|
|
8754
|
+
process.exit(1);
|
|
8755
|
+
}
|
|
8756
|
+
if (!key) {
|
|
8757
|
+
console.error(red('Error: --key is required'));
|
|
8758
|
+
process.exit(1);
|
|
8759
|
+
}
|
|
8760
|
+
|
|
8761
|
+
const existing = stmts.getByIdentityKey.get('contact', key);
|
|
8762
|
+
if (existing) {
|
|
8763
|
+
console.error(red(`Error: a contact with identity_key "${key}" already exists (id: ${existing.id}).`));
|
|
8764
|
+
console.error(`Use ${cyan('context-vault save --kind contact --identity-key "' + key + '" --body "..."')} to update.`);
|
|
8765
|
+
process.exit(1);
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
let body = `# ${name}\n`;
|
|
8769
|
+
if (email) body += `\n## Contact\n${email}\n`;
|
|
8770
|
+
if (role) body += `\n## Role\n${role}\n`;
|
|
8771
|
+
if (notes) body += `\n## Notes\n${notes}\n`;
|
|
8772
|
+
|
|
8773
|
+
const parsedTags = ['contact'];
|
|
8774
|
+
if (tagsStr) {
|
|
8775
|
+
for (const t of tagsStr.split(',').map((t) => t.trim()).filter(Boolean)) {
|
|
8776
|
+
if (!parsedTags.includes(t)) parsedTags.push(t);
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
|
|
8780
|
+
const { embed } = await import('@context-vault/core/embed');
|
|
8781
|
+
const { captureAndIndex } = await import('@context-vault/core/capture');
|
|
8782
|
+
const ctx = {
|
|
8783
|
+
db,
|
|
8784
|
+
config,
|
|
8785
|
+
stmts,
|
|
8786
|
+
embed,
|
|
8787
|
+
insertVec: (rowid, embedding) => insertVec(stmts, rowid, embedding),
|
|
8788
|
+
deleteVec: (rowid) => deleteVec(stmts, rowid),
|
|
8789
|
+
};
|
|
8790
|
+
const entry = await captureAndIndex(ctx, {
|
|
8791
|
+
kind: 'contact',
|
|
8792
|
+
title: name,
|
|
8793
|
+
body: body.trim(),
|
|
8794
|
+
tags: parsedTags,
|
|
8795
|
+
source: 'cli',
|
|
8796
|
+
identity_key: key,
|
|
8797
|
+
});
|
|
8798
|
+
console.log(`${green('✓')} Contact saved — id: ${entry.id}, key: ${key}`);
|
|
8799
|
+
|
|
8800
|
+
} else {
|
|
8801
|
+
console.error(red(`Unknown contacts subcommand: ${sub}`));
|
|
8802
|
+
console.error(`Run ${cyan('context-vault contacts --help')} for usage.`);
|
|
8803
|
+
process.exit(1);
|
|
8804
|
+
}
|
|
8805
|
+
} catch (e) {
|
|
8806
|
+
console.error(`${red('x')} contacts ${sub} failed: ${e.message}`);
|
|
8807
|
+
process.exit(1);
|
|
8808
|
+
} finally {
|
|
8809
|
+
try { db?.close(); } catch {}
|
|
8810
|
+
}
|
|
8811
|
+
}
|
|
8812
|
+
|
|
8813
|
+
async function runStale() {
|
|
8814
|
+
if (flags.has('--help')) {
|
|
8815
|
+
console.log(`
|
|
8816
|
+
${bold('context-vault stale')} [options]
|
|
8817
|
+
|
|
8818
|
+
List vault entries with low freshness scores (context that may need attention).
|
|
8819
|
+
|
|
8820
|
+
${bold('Usage:')}
|
|
8821
|
+
context-vault stale # Entries scoring < 50 (stale + dormant)
|
|
8822
|
+
context-vault stale --threshold 25 # Only dormant entries
|
|
8823
|
+
context-vault stale --kind insight # Filter by kind
|
|
8824
|
+
context-vault stale --format json # Structured output
|
|
8825
|
+
|
|
8826
|
+
${bold('Options:')}
|
|
8827
|
+
--threshold <n> Score threshold (default: 50, entries below this are shown)
|
|
8828
|
+
--kind <kind> Filter by kind
|
|
8829
|
+
--format <fmt> Output format: plain (default), json
|
|
8830
|
+
--limit <n> Max entries to show (default: 50)
|
|
8831
|
+
`);
|
|
8832
|
+
return;
|
|
8833
|
+
}
|
|
8834
|
+
|
|
8835
|
+
const threshold = parseInt(getFlag('--threshold') || '50', 10);
|
|
8836
|
+
const kind = getFlag('--kind');
|
|
8837
|
+
const format = getFlag('--format') || 'plain';
|
|
8838
|
+
const limit = parseInt(getFlag('--limit') || '50', 10);
|
|
8839
|
+
|
|
8840
|
+
const { resolveConfig } = await import('@context-vault/core/config');
|
|
8841
|
+
const { initDatabase } = await import('@context-vault/core/db');
|
|
8842
|
+
const { computeFreshnessScore } = await import('@context-vault/core/search');
|
|
8843
|
+
|
|
8844
|
+
const config = resolveConfig();
|
|
8845
|
+
if (!config.vaultDirExists) {
|
|
8846
|
+
console.error(red('No vault found. Run: context-vault setup'));
|
|
8847
|
+
process.exit(1);
|
|
8848
|
+
}
|
|
8849
|
+
|
|
8850
|
+
let db;
|
|
8851
|
+
try {
|
|
8852
|
+
db = await initDatabase(config.dbPath);
|
|
8853
|
+
} catch (e) {
|
|
8854
|
+
console.error(red(`Database error: ${e.message}`));
|
|
8855
|
+
process.exit(1);
|
|
8856
|
+
}
|
|
8857
|
+
|
|
8858
|
+
try {
|
|
8859
|
+
let sql = `SELECT * FROM vault WHERE superseded_by IS NULL AND (expires_at IS NULL OR expires_at > datetime('now'))`;
|
|
8860
|
+
const params = [];
|
|
8861
|
+
if (kind) {
|
|
8862
|
+
sql += ' AND kind = ?';
|
|
8863
|
+
params.push(kind);
|
|
8864
|
+
}
|
|
8865
|
+
|
|
8866
|
+
const rows = db.prepare(sql).all(...params);
|
|
8867
|
+
|
|
8868
|
+
const scored = rows.map((row) => {
|
|
8869
|
+
const { score, label } = computeFreshnessScore(row);
|
|
8870
|
+
return { ...row, freshness_score: score, freshness_label: label };
|
|
8871
|
+
});
|
|
8872
|
+
|
|
8873
|
+
const staleEntries = scored
|
|
8874
|
+
.filter((e) => e.freshness_score < threshold)
|
|
8875
|
+
.sort((a, b) => a.freshness_score - b.freshness_score)
|
|
8876
|
+
.slice(0, limit);
|
|
8877
|
+
|
|
8878
|
+
if (format === 'json') {
|
|
8879
|
+
const output = staleEntries.map((e) => ({
|
|
8880
|
+
id: e.id,
|
|
8881
|
+
kind: e.kind,
|
|
8882
|
+
title: e.title,
|
|
8883
|
+
freshness_score: e.freshness_score,
|
|
8884
|
+
freshness_label: e.freshness_label,
|
|
8885
|
+
created_at: e.created_at,
|
|
8886
|
+
updated_at: e.updated_at,
|
|
8887
|
+
last_accessed_at: e.last_accessed_at,
|
|
8888
|
+
recall_count: e.recall_count,
|
|
8889
|
+
recall_sessions: e.recall_sessions,
|
|
8890
|
+
}));
|
|
8891
|
+
console.log(JSON.stringify(output, null, 2));
|
|
8892
|
+
} else {
|
|
8893
|
+
console.log();
|
|
8894
|
+
console.log(` ${bold('Stale Entries')} ${dim(`(freshness < ${threshold})`)}`);
|
|
8895
|
+
console.log();
|
|
8896
|
+
|
|
8897
|
+
if (staleEntries.length === 0) {
|
|
8898
|
+
console.log(` ${dim('No entries below threshold.')}`);
|
|
8899
|
+
} else {
|
|
8900
|
+
const labelColor = (label) => {
|
|
8901
|
+
if (label === 'dormant') return red(label);
|
|
8902
|
+
if (label === 'stale') return yellow(label);
|
|
8903
|
+
return dim(label);
|
|
8904
|
+
};
|
|
8905
|
+
|
|
8906
|
+
for (const entry of staleEntries) {
|
|
8907
|
+
const title = entry.title || '(untitled)';
|
|
8908
|
+
const score = String(entry.freshness_score).padStart(3);
|
|
8909
|
+
console.log(` ${dim(score)} ${labelColor(entry.freshness_label).padEnd(18)} ${entry.kind.padEnd(12)} ${title}`);
|
|
8910
|
+
}
|
|
8911
|
+
|
|
8912
|
+
console.log();
|
|
8913
|
+
const dormantCount = staleEntries.filter((e) => e.freshness_label === 'dormant').length;
|
|
8914
|
+
const staleCount = staleEntries.filter((e) => e.freshness_label === 'stale').length;
|
|
8915
|
+
console.log(` ${dim('Total:')} ${staleEntries.length} entries (${dormantCount} dormant, ${staleCount} stale)`);
|
|
8916
|
+
}
|
|
8917
|
+
console.log();
|
|
8918
|
+
}
|
|
8919
|
+
} finally {
|
|
8920
|
+
db.close();
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8923
|
+
|
|
8491
8924
|
async function main() {
|
|
8492
8925
|
if (flags.has('--version') || command === 'version') {
|
|
8493
8926
|
console.log(VERSION);
|
|
@@ -8496,7 +8929,7 @@ async function main() {
|
|
|
8496
8929
|
|
|
8497
8930
|
if (flags.has('--help') || command === 'help') {
|
|
8498
8931
|
// Commands with their own --help handling: delegate to them
|
|
8499
|
-
const commandsWithHelp = new Set(['save', 'search', 'rules', 'hooks', '
|
|
8932
|
+
const commandsWithHelp = new Set(['save', 'search', 'rules', 'hooks', 'team', 'remote', 'ingest-comms', 'gmail-bridge', 'slack-bridge', 'contacts']);
|
|
8500
8933
|
if (!command || command === 'help' || !commandsWithHelp.has(command)) {
|
|
8501
8934
|
showHelp(flags.has('--all'));
|
|
8502
8935
|
return;
|
|
@@ -8525,7 +8958,8 @@ async function main() {
|
|
|
8525
8958
|
await runSwitch();
|
|
8526
8959
|
break;
|
|
8527
8960
|
case 'daemon':
|
|
8528
|
-
|
|
8961
|
+
console.log('The daemon command was removed in v3.16.1. context-vault now runs in stdio mode only.');
|
|
8962
|
+
process.exit(0);
|
|
8529
8963
|
break;
|
|
8530
8964
|
case 'serve':
|
|
8531
8965
|
await runServe();
|
|
@@ -8575,6 +9009,18 @@ async function main() {
|
|
|
8575
9009
|
case 'ingest-project':
|
|
8576
9010
|
await runIngestProject();
|
|
8577
9011
|
break;
|
|
9012
|
+
case 'ingest-comms':
|
|
9013
|
+
await runIngestComms();
|
|
9014
|
+
break;
|
|
9015
|
+
case 'gmail-bridge':
|
|
9016
|
+
await runGmailBridge();
|
|
9017
|
+
break;
|
|
9018
|
+
case 'slack-bridge':
|
|
9019
|
+
await runSlackBridge();
|
|
9020
|
+
break;
|
|
9021
|
+
case 'contacts':
|
|
9022
|
+
await runContacts();
|
|
9023
|
+
break;
|
|
8578
9024
|
case 'reindex':
|
|
8579
9025
|
await runReindex();
|
|
8580
9026
|
break;
|
|
@@ -8615,7 +9061,8 @@ async function main() {
|
|
|
8615
9061
|
await runHealth();
|
|
8616
9062
|
break;
|
|
8617
9063
|
case 'restart':
|
|
8618
|
-
|
|
9064
|
+
console.log('The restart command was removed in v3.16.1. Use "context-vault reconnect" instead.');
|
|
9065
|
+
process.exit(0);
|
|
8619
9066
|
break;
|
|
8620
9067
|
case 'reconnect':
|
|
8621
9068
|
await runReconnect();
|
|
@@ -8644,6 +9091,9 @@ async function main() {
|
|
|
8644
9091
|
case 'compact':
|
|
8645
9092
|
await runCompact();
|
|
8646
9093
|
break;
|
|
9094
|
+
case 'stale':
|
|
9095
|
+
await runStale();
|
|
9096
|
+
break;
|
|
8647
9097
|
default:
|
|
8648
9098
|
console.error(red(`Unknown command: ${command}`));
|
|
8649
9099
|
console.error(`Run ${cyan('context-vault --help')} for usage.`);
|