context-vault 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -13
- package/bin/cli.js +417 -20
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/constants.js +6 -0
- package/node_modules/@context-vault/core/src/core/telemetry.js +28 -2
- package/node_modules/@context-vault/core/src/server/tools/clear-context.js +47 -0
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +109 -0
- package/node_modules/@context-vault/core/src/server/tools/save-context.js +6 -0
- package/node_modules/@context-vault/core/src/server/tools.js +2 -0
- package/package.json +2 -2
- package/src/server/index.js +44 -0
package/README.md
CHANGED
|
@@ -40,19 +40,63 @@ Entries are organized by `kind` (insight, decision, pattern, reference, contact,
|
|
|
40
40
|
|
|
41
41
|
## CLI
|
|
42
42
|
|
|
43
|
-
| Command | Description
|
|
44
|
-
| ----------------------------- |
|
|
45
|
-
| `context-vault setup` | Interactive installer — detects tools, writes MCP configs
|
|
46
|
-
| `context-vault connect --key` | Connect AI tools to hosted vault
|
|
47
|
-
| `context-vault switch` | Switch between local and hosted MCP modes
|
|
48
|
-
| `context-vault serve` | Start the MCP server (used by AI clients)
|
|
49
|
-
| `context-vault status` | Vault health, paths, entry counts
|
|
50
|
-
| `context-vault
|
|
51
|
-
| `context-vault
|
|
52
|
-
| `context-vault
|
|
53
|
-
| `context-vault
|
|
54
|
-
| `context-vault
|
|
55
|
-
| `context-vault
|
|
43
|
+
| Command | Description |
|
|
44
|
+
| ----------------------------- | ---------------------------------------------------------- |
|
|
45
|
+
| `context-vault setup` | Interactive installer — detects tools, writes MCP configs |
|
|
46
|
+
| `context-vault connect --key` | Connect AI tools to hosted vault |
|
|
47
|
+
| `context-vault switch` | Switch between local and hosted MCP modes |
|
|
48
|
+
| `context-vault serve` | Start the MCP server (used by AI clients) |
|
|
49
|
+
| `context-vault status` | Vault health, paths, entry counts |
|
|
50
|
+
| `context-vault flush` | Confirm DB is accessible; prints entry count and last save |
|
|
51
|
+
| `context-vault hooks install` | Install Claude Code memory and optional session flush hook |
|
|
52
|
+
| `context-vault hooks remove` | Remove the recall and session flush hooks |
|
|
53
|
+
| `context-vault reindex` | Rebuild search index |
|
|
54
|
+
| `context-vault import <path>` | Import .md, .csv, .json, .txt |
|
|
55
|
+
| `context-vault export` | Export to JSON or CSV |
|
|
56
|
+
| `context-vault ingest <url>` | Fetch URL and save as vault entry |
|
|
57
|
+
| `context-vault update` | Check for updates |
|
|
58
|
+
| `context-vault uninstall` | Remove MCP configs |
|
|
59
|
+
|
|
60
|
+
## Claude Code Lifecycle Hooks
|
|
61
|
+
|
|
62
|
+
Claude Code exposes shell hooks that fire on session events. context-vault integrates with two of them:
|
|
63
|
+
|
|
64
|
+
**UserPromptSubmit** — runs `context-vault recall` on every prompt, injecting relevant vault entries as context (installed via `hooks install`).
|
|
65
|
+
|
|
66
|
+
**SessionEnd** — runs `context-vault flush` when a session ends, confirming the vault is healthy and logging the current entry count. Install it when prompted by `hooks install`, or add it manually to `~/.claude/settings.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"hooks": {
|
|
71
|
+
"SessionEnd": [
|
|
72
|
+
{
|
|
73
|
+
"hooks": [
|
|
74
|
+
{
|
|
75
|
+
"type": "command",
|
|
76
|
+
"command": "npx context-vault flush",
|
|
77
|
+
"timeout": 10
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `flush` command reads the DB, prints a one-line status (`context-vault ok — N entries, last save: <timestamp>`), and exits 0. It is intentionally a no-op write — its purpose is to confirm reachability at session boundaries.
|
|
87
|
+
|
|
88
|
+
To install both hooks at once:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
context-vault hooks install
|
|
92
|
+
# Follow the second prompt: "Install session auto-flush hook? (y/N)"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
To remove both hooks:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
context-vault hooks remove
|
|
99
|
+
```
|
|
56
100
|
|
|
57
101
|
## Manual MCP Config
|
|
58
102
|
|
|
@@ -73,6 +117,8 @@ If you prefer manual setup over `context-vault setup`:
|
|
|
73
117
|
|
|
74
118
|
No Node.js required — sign up at [app.context-vault.com](https://app.context-vault.com), get an API key, connect in 2 minutes.
|
|
75
119
|
|
|
120
|
+
Full setup instructions for Claude Code, Cursor, and GPT Actions: [docs/distribution/connect-in-2-minutes.md](../../docs/distribution/connect-in-2-minutes.md)
|
|
121
|
+
|
|
76
122
|
## Troubleshooting
|
|
77
123
|
|
|
78
124
|
**Install fails (native modules):**
|
package/bin/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ import { join, resolve, dirname } from "node:path";
|
|
|
25
25
|
import { homedir, platform } from "node:os";
|
|
26
26
|
import { execSync, execFile, execFileSync } from "node:child_process";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { APP_URL, API_URL, MARKETING_URL } from "@context-vault/core/constants";
|
|
28
29
|
|
|
29
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
31
|
const __dirname = dirname(__filename);
|
|
@@ -235,10 +236,12 @@ ${bold("Commands:")}
|
|
|
235
236
|
${cyan("switch")} local|hosted Switch between local and hosted MCP modes
|
|
236
237
|
${cyan("serve")} Start the MCP server (used by AI clients)
|
|
237
238
|
${cyan("hooks")} install|remove Install or remove Claude Code memory hook
|
|
239
|
+
${cyan("flush")} Check vault health and confirm DB is accessible
|
|
238
240
|
${cyan("recall")} Search vault from a Claude Code hook (reads stdin)
|
|
239
241
|
${cyan("reindex")} Rebuild search index from knowledge files
|
|
240
242
|
${cyan("prune")} Remove expired entries (use --dry-run to preview)
|
|
241
243
|
${cyan("status")} Show vault diagnostics
|
|
244
|
+
${cyan("doctor")} Diagnose and repair common issues
|
|
242
245
|
${cyan("update")} Check for and install updates
|
|
243
246
|
${cyan("uninstall")} Remove MCP configs and optionally data
|
|
244
247
|
${cyan("import")} <path> Import entries from file or directory
|
|
@@ -272,9 +275,39 @@ async function runSetup() {
|
|
|
272
275
|
existingVault = cfg.vaultDir || existingVault;
|
|
273
276
|
} catch {}
|
|
274
277
|
|
|
278
|
+
// Version check against npm registry (5s timeout, fail silently if offline)
|
|
279
|
+
let latestVersion = null;
|
|
280
|
+
try {
|
|
281
|
+
latestVersion = execSync("npm view context-vault version", {
|
|
282
|
+
encoding: "utf-8",
|
|
283
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
284
|
+
timeout: 5000,
|
|
285
|
+
}).trim();
|
|
286
|
+
} catch {}
|
|
287
|
+
|
|
288
|
+
if (latestVersion === VERSION) {
|
|
289
|
+
console.log(
|
|
290
|
+
green(` ✓ context-vault v${VERSION} is up to date`) +
|
|
291
|
+
dim(` (vault: ${existingVault})`),
|
|
292
|
+
);
|
|
293
|
+
console.log();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
275
297
|
console.log(yellow(` Existing installation detected`));
|
|
276
298
|
console.log(dim(` Vault: ${existingVault}`));
|
|
277
|
-
|
|
299
|
+
if (latestVersion) {
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(` Current: ${dim(VERSION)}`);
|
|
302
|
+
console.log(` Latest: ${green(latestVersion)}`);
|
|
303
|
+
const upgradeCmd = isNpx()
|
|
304
|
+
? "npx context-vault@latest setup"
|
|
305
|
+
: "npm install -g context-vault";
|
|
306
|
+
console.log();
|
|
307
|
+
console.log(dim(` To upgrade: ${upgradeCmd}`));
|
|
308
|
+
} else {
|
|
309
|
+
console.log(dim(` Config: ${existingConfig}`));
|
|
310
|
+
}
|
|
278
311
|
console.log();
|
|
279
312
|
console.log(` 1) Full reconfigure`);
|
|
280
313
|
console.log(` 2) Update tool configs only ${dim("(skip vault setup)")}`);
|
|
@@ -354,6 +387,7 @@ async function runSetup() {
|
|
|
354
387
|
|
|
355
388
|
console.log();
|
|
356
389
|
console.log(green(" ✓ Tool configs updated."));
|
|
390
|
+
console.log(dim(" Restart your AI tools to apply the changes."));
|
|
357
391
|
console.log();
|
|
358
392
|
return;
|
|
359
393
|
}
|
|
@@ -501,7 +535,7 @@ async function runSetup() {
|
|
|
501
535
|
console.log(
|
|
502
536
|
dim(" file paths, or personal data is ever sent. Off by default."),
|
|
503
537
|
);
|
|
504
|
-
console.log(dim(
|
|
538
|
+
console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
|
|
505
539
|
console.log();
|
|
506
540
|
|
|
507
541
|
let telemetryEnabled = vaultConfig.telemetry === true;
|
|
@@ -541,7 +575,12 @@ async function runSetup() {
|
|
|
541
575
|
console.log(
|
|
542
576
|
`\n ${dim("[4/6]")}${bold(" Downloading embedding model...")}`,
|
|
543
577
|
);
|
|
544
|
-
console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)
|
|
578
|
+
console.log(dim(" all-MiniLM-L6-v2 (~22MB, one-time download)"));
|
|
579
|
+
console.log(
|
|
580
|
+
dim(
|
|
581
|
+
` Slow connection? Re-run with --skip-embeddings (enables FTS-only mode)\n`,
|
|
582
|
+
),
|
|
583
|
+
);
|
|
545
584
|
{
|
|
546
585
|
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
547
586
|
let frame = 0;
|
|
@@ -731,7 +770,9 @@ async function runSetup() {
|
|
|
731
770
|
const boxLines = [
|
|
732
771
|
` ✓ Setup complete — ${passed}/${checks.length} checks passed (${elapsed}s)`,
|
|
733
772
|
``,
|
|
734
|
-
` ${bold("
|
|
773
|
+
` ${bold("Next:")} restart ${toolName} to activate the vault`,
|
|
774
|
+
``,
|
|
775
|
+
` ${bold("AI Tools")} — once active, try:`,
|
|
735
776
|
` "Search my vault for getting started"`,
|
|
736
777
|
` "Save an insight about [topic]"`,
|
|
737
778
|
` "Show my vault status"`,
|
|
@@ -979,7 +1020,7 @@ This is an example entry showing the decision format. Feel free to delete it.
|
|
|
979
1020
|
|
|
980
1021
|
async function runConnect() {
|
|
981
1022
|
const apiKey = getFlag("--key");
|
|
982
|
-
const hostedUrl = getFlag("--url") ||
|
|
1023
|
+
const hostedUrl = getFlag("--url") || API_URL;
|
|
983
1024
|
|
|
984
1025
|
if (!apiKey) {
|
|
985
1026
|
console.log(`\n ${bold("context-vault connect")}\n`);
|
|
@@ -988,9 +1029,7 @@ async function runConnect() {
|
|
|
988
1029
|
console.log(` context-vault connect --key cv_...\n`);
|
|
989
1030
|
console.log(` Options:`);
|
|
990
1031
|
console.log(` --key <key> API key (required)`);
|
|
991
|
-
console.log(
|
|
992
|
-
` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
|
|
993
|
-
);
|
|
1032
|
+
console.log(` --url <url> Hosted server URL (default: ${API_URL})`);
|
|
994
1033
|
console.log();
|
|
995
1034
|
return;
|
|
996
1035
|
}
|
|
@@ -1232,9 +1271,7 @@ async function runSwitch() {
|
|
|
1232
1271
|
);
|
|
1233
1272
|
console.log(` Options:`);
|
|
1234
1273
|
console.log(` --key <key> API key for hosted mode (cv_...)`);
|
|
1235
|
-
console.log(
|
|
1236
|
-
` --url <url> Hosted server URL (default: https://api.context-vault.com)\n`,
|
|
1237
|
-
);
|
|
1274
|
+
console.log(` --url <url> Hosted server URL (default: ${API_URL})\n`);
|
|
1238
1275
|
return;
|
|
1239
1276
|
}
|
|
1240
1277
|
|
|
@@ -1291,10 +1328,7 @@ async function runSwitch() {
|
|
|
1291
1328
|
console.log(dim(` Server: node ${launcherPath}`));
|
|
1292
1329
|
console.log();
|
|
1293
1330
|
} else {
|
|
1294
|
-
const hostedUrl =
|
|
1295
|
-
getFlag("--url") ||
|
|
1296
|
-
vaultConfig.hostedUrl ||
|
|
1297
|
-
"https://api.context-vault.com";
|
|
1331
|
+
const hostedUrl = getFlag("--url") || vaultConfig.hostedUrl || API_URL;
|
|
1298
1332
|
const apiKey = getFlag("--key") || vaultConfig.apiKey;
|
|
1299
1333
|
|
|
1300
1334
|
if (!apiKey) {
|
|
@@ -1452,6 +1486,8 @@ async function runStatus() {
|
|
|
1452
1486
|
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
1453
1487
|
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
1454
1488
|
const { gatherVaultStatus } = await import("@context-vault/core/core/status");
|
|
1489
|
+
const { errorLogPath, errorLogCount } =
|
|
1490
|
+
await import("@context-vault/core/core/error-log");
|
|
1455
1491
|
|
|
1456
1492
|
const config = resolveConfig();
|
|
1457
1493
|
|
|
@@ -1534,6 +1570,18 @@ async function runStatus() {
|
|
|
1534
1570
|
console.log(yellow(" Stale paths detected in DB."));
|
|
1535
1571
|
console.log(` Run ${cyan("context-vault reindex")} to update.`);
|
|
1536
1572
|
}
|
|
1573
|
+
|
|
1574
|
+
const logCount = errorLogCount(config.dataDir);
|
|
1575
|
+
if (logCount > 0) {
|
|
1576
|
+
const logPath = errorLogPath(config.dataDir);
|
|
1577
|
+
console.log();
|
|
1578
|
+
console.log(
|
|
1579
|
+
yellow(
|
|
1580
|
+
` ${logCount} startup error${logCount === 1 ? "" : "s"} logged — run ${cyan("context-vault doctor")} for details`,
|
|
1581
|
+
),
|
|
1582
|
+
);
|
|
1583
|
+
console.log(` ${dim(logPath)}`);
|
|
1584
|
+
}
|
|
1537
1585
|
console.log();
|
|
1538
1586
|
}
|
|
1539
1587
|
|
|
@@ -1674,15 +1722,13 @@ async function runMigrate() {
|
|
|
1674
1722
|
` context-vault migrate --to-local Download hosted vault to local files`,
|
|
1675
1723
|
);
|
|
1676
1724
|
console.log(`\n Options:`);
|
|
1677
|
-
console.log(
|
|
1678
|
-
` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
|
|
1679
|
-
);
|
|
1725
|
+
console.log(` --url <url> Hosted server URL (default: ${API_URL})`);
|
|
1680
1726
|
console.log(` --key <key> API key (cv_...)`);
|
|
1681
1727
|
console.log();
|
|
1682
1728
|
return;
|
|
1683
1729
|
}
|
|
1684
1730
|
|
|
1685
|
-
const hostedUrl = getFlag("--url") ||
|
|
1731
|
+
const hostedUrl = getFlag("--url") || API_URL;
|
|
1686
1732
|
const apiKey = getFlag("--key");
|
|
1687
1733
|
|
|
1688
1734
|
if (!apiKey) {
|
|
@@ -2099,6 +2145,37 @@ async function runRecall() {
|
|
|
2099
2145
|
}
|
|
2100
2146
|
}
|
|
2101
2147
|
|
|
2148
|
+
async function runFlush() {
|
|
2149
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2150
|
+
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
2151
|
+
|
|
2152
|
+
let db;
|
|
2153
|
+
try {
|
|
2154
|
+
const config = resolveConfig();
|
|
2155
|
+
db = await initDatabase(config.dbPath);
|
|
2156
|
+
|
|
2157
|
+
const { c: entryCount } = db
|
|
2158
|
+
.prepare("SELECT COUNT(*) as c FROM vault")
|
|
2159
|
+
.get();
|
|
2160
|
+
|
|
2161
|
+
const lastSaveRow = db
|
|
2162
|
+
.prepare("SELECT MAX(COALESCE(updated_at, created_at)) as ts FROM vault")
|
|
2163
|
+
.get();
|
|
2164
|
+
const lastSave = lastSaveRow?.ts ?? "n/a";
|
|
2165
|
+
|
|
2166
|
+
console.log(
|
|
2167
|
+
`context-vault ok — ${entryCount} ${entryCount === 1 ? "entry" : "entries"}, last save: ${lastSave}`,
|
|
2168
|
+
);
|
|
2169
|
+
} catch (e) {
|
|
2170
|
+
console.error(red(`context-vault flush failed: ${e.message}`));
|
|
2171
|
+
process.exit(1);
|
|
2172
|
+
} finally {
|
|
2173
|
+
try {
|
|
2174
|
+
db?.close();
|
|
2175
|
+
} catch {}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2102
2179
|
/** Returns the path to Claude Code's global settings.json */
|
|
2103
2180
|
function claudeSettingsPath() {
|
|
2104
2181
|
return join(HOME, ".claude", "settings.json");
|
|
@@ -2146,6 +2223,76 @@ function installClaudeHook() {
|
|
|
2146
2223
|
return true;
|
|
2147
2224
|
}
|
|
2148
2225
|
|
|
2226
|
+
/**
|
|
2227
|
+
* Writes a SessionEnd hook entry for context-vault flush to ~/.claude/settings.json.
|
|
2228
|
+
* Returns true if installed, false if already present.
|
|
2229
|
+
*/
|
|
2230
|
+
function installSessionEndHook() {
|
|
2231
|
+
const settingsPath = claudeSettingsPath();
|
|
2232
|
+
let settings = {};
|
|
2233
|
+
|
|
2234
|
+
if (existsSync(settingsPath)) {
|
|
2235
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
2236
|
+
try {
|
|
2237
|
+
settings = JSON.parse(raw);
|
|
2238
|
+
} catch {
|
|
2239
|
+
const bak = settingsPath + ".bak";
|
|
2240
|
+
copyFileSync(settingsPath, bak);
|
|
2241
|
+
console.log(yellow(` Backed up corrupted settings to ${bak}`));
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (!settings.hooks) settings.hooks = {};
|
|
2246
|
+
if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
|
|
2247
|
+
|
|
2248
|
+
const alreadyInstalled = settings.hooks.SessionEnd.some((h) =>
|
|
2249
|
+
h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
|
|
2250
|
+
);
|
|
2251
|
+
if (alreadyInstalled) return false;
|
|
2252
|
+
|
|
2253
|
+
settings.hooks.SessionEnd.push({
|
|
2254
|
+
hooks: [
|
|
2255
|
+
{
|
|
2256
|
+
type: "command",
|
|
2257
|
+
command: "npx context-vault flush",
|
|
2258
|
+
timeout: 10,
|
|
2259
|
+
},
|
|
2260
|
+
],
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
2264
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/**
|
|
2269
|
+
* Removes the context-vault flush SessionEnd hook from ~/.claude/settings.json.
|
|
2270
|
+
* Returns true if removed, false if not found.
|
|
2271
|
+
*/
|
|
2272
|
+
function removeSessionEndHook() {
|
|
2273
|
+
const settingsPath = claudeSettingsPath();
|
|
2274
|
+
if (!existsSync(settingsPath)) return false;
|
|
2275
|
+
|
|
2276
|
+
let settings;
|
|
2277
|
+
try {
|
|
2278
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
2279
|
+
} catch {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if (!settings.hooks?.SessionEnd) return false;
|
|
2284
|
+
|
|
2285
|
+
const before = settings.hooks.SessionEnd.length;
|
|
2286
|
+
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
|
|
2287
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes("context-vault flush")),
|
|
2288
|
+
);
|
|
2289
|
+
|
|
2290
|
+
if (settings.hooks.SessionEnd.length === before) return false;
|
|
2291
|
+
|
|
2292
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2149
2296
|
/**
|
|
2150
2297
|
* Removes the context-vault recall hook from ~/.claude/settings.json.
|
|
2151
2298
|
* Returns true if removed, false if not found.
|
|
@@ -2203,6 +2350,44 @@ async function runHooks() {
|
|
|
2203
2350
|
process.exit(1);
|
|
2204
2351
|
}
|
|
2205
2352
|
console.log();
|
|
2353
|
+
|
|
2354
|
+
// Prompt for optional session auto-flush (SessionEnd) hook
|
|
2355
|
+
const installFlush =
|
|
2356
|
+
flags.has("--flush") ||
|
|
2357
|
+
(await prompt(
|
|
2358
|
+
" Install session auto-flush hook? (runs context-vault flush at session end) (y/N):",
|
|
2359
|
+
"n",
|
|
2360
|
+
));
|
|
2361
|
+
const shouldInstallFlush =
|
|
2362
|
+
installFlush === true ||
|
|
2363
|
+
(typeof installFlush === "string" &&
|
|
2364
|
+
installFlush.toLowerCase().startsWith("y"));
|
|
2365
|
+
|
|
2366
|
+
if (shouldInstallFlush) {
|
|
2367
|
+
try {
|
|
2368
|
+
const flushInstalled = installSessionEndHook();
|
|
2369
|
+
if (flushInstalled) {
|
|
2370
|
+
console.log(
|
|
2371
|
+
`\n ${green("✓")} Session auto-flush hook installed (SessionEnd).\n`,
|
|
2372
|
+
);
|
|
2373
|
+
console.log(
|
|
2374
|
+
dim(
|
|
2375
|
+
" At the end of each session, context-vault flush confirms the vault is healthy.",
|
|
2376
|
+
),
|
|
2377
|
+
);
|
|
2378
|
+
} else {
|
|
2379
|
+
console.log(
|
|
2380
|
+
`\n ${yellow("!")} Session auto-flush hook already installed.\n`,
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
console.error(
|
|
2385
|
+
`\n ${red("x")} Failed to install session flush hook: ${e.message}\n`,
|
|
2386
|
+
);
|
|
2387
|
+
process.exit(1);
|
|
2388
|
+
}
|
|
2389
|
+
console.log();
|
|
2390
|
+
}
|
|
2206
2391
|
} else if (sub === "remove") {
|
|
2207
2392
|
try {
|
|
2208
2393
|
const removed = removeClaudeHook();
|
|
@@ -2215,6 +2400,19 @@ async function runHooks() {
|
|
|
2215
2400
|
console.error(`\n ${red("x")} Failed to remove hook: ${e.message}\n`);
|
|
2216
2401
|
process.exit(1);
|
|
2217
2402
|
}
|
|
2403
|
+
|
|
2404
|
+
try {
|
|
2405
|
+
const flushRemoved = removeSessionEndHook();
|
|
2406
|
+
if (flushRemoved) {
|
|
2407
|
+
console.log(
|
|
2408
|
+
`\n ${green("✓")} Session auto-flush hook removed (SessionEnd).\n`,
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
} catch (e) {
|
|
2412
|
+
console.error(
|
|
2413
|
+
`\n ${red("x")} Failed to remove session flush hook: ${e.message}\n`,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2218
2416
|
} else {
|
|
2219
2417
|
console.log(`
|
|
2220
2418
|
${bold("context-vault hooks")} <install|remove>
|
|
@@ -2225,11 +2423,204 @@ async function runHooks() {
|
|
|
2225
2423
|
|
|
2226
2424
|
${bold("Commands:")}
|
|
2227
2425
|
${cyan("hooks install")} Write UserPromptSubmit hook to ~/.claude/settings.json
|
|
2228
|
-
|
|
2426
|
+
Also prompts to install a SessionEnd auto-flush hook
|
|
2427
|
+
${cyan("hooks remove")} Remove the recall hook and SessionEnd flush hook
|
|
2229
2428
|
`);
|
|
2230
2429
|
}
|
|
2231
2430
|
}
|
|
2232
2431
|
|
|
2432
|
+
async function runDoctor() {
|
|
2433
|
+
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
2434
|
+
const { errorLogPath, errorLogCount } =
|
|
2435
|
+
await import("@context-vault/core/core/error-log");
|
|
2436
|
+
|
|
2437
|
+
console.log();
|
|
2438
|
+
console.log(` ${bold("◇ context-vault doctor")} ${dim(`v${VERSION}`)}`);
|
|
2439
|
+
console.log();
|
|
2440
|
+
|
|
2441
|
+
let allOk = true;
|
|
2442
|
+
|
|
2443
|
+
// ── Node.js version ──────────────────────────────────────────────────────
|
|
2444
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
2445
|
+
if (nodeMajor < 20) {
|
|
2446
|
+
console.log(
|
|
2447
|
+
` ${red("✘")} Node.js ${process.versions.node} — requires >= 20`,
|
|
2448
|
+
);
|
|
2449
|
+
console.log(
|
|
2450
|
+
` ${dim("Fix: install a newer Node.js from https://nodejs.org/")}`,
|
|
2451
|
+
);
|
|
2452
|
+
allOk = false;
|
|
2453
|
+
} else {
|
|
2454
|
+
console.log(
|
|
2455
|
+
` ${green("✓")} Node.js ${process.versions.node} ${dim(`(${process.execPath})`)}`,
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
2460
|
+
let config;
|
|
2461
|
+
try {
|
|
2462
|
+
config = resolveConfig();
|
|
2463
|
+
const configExists = existsSync(config.configPath);
|
|
2464
|
+
console.log(
|
|
2465
|
+
` ${green("✓")} Config ${dim(`(${configExists ? "exists" : "using defaults"}: ${config.configPath})`)}`,
|
|
2466
|
+
);
|
|
2467
|
+
} catch (e) {
|
|
2468
|
+
console.log(` ${red("✘")} Config parse error: ${e.message}`);
|
|
2469
|
+
console.log(
|
|
2470
|
+
` ${dim(`Fix: delete or repair ${join(HOME, ".context-mcp", "config.json")}`)}`,
|
|
2471
|
+
);
|
|
2472
|
+
allOk = false;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
if (config) {
|
|
2476
|
+
// ── Data dir ───────────────────────────────────────────────────────────
|
|
2477
|
+
if (existsSync(config.dataDir)) {
|
|
2478
|
+
console.log(` ${green("✓")} Data dir ${dim(config.dataDir)}`);
|
|
2479
|
+
} else {
|
|
2480
|
+
console.log(
|
|
2481
|
+
` ${yellow("!")} Data dir missing — will be created on next start`,
|
|
2482
|
+
);
|
|
2483
|
+
console.log(` ${dim(`mkdir -p "${config.dataDir}"`)}`);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// ── Vault dir ─────────────────────────────────────────────────────────
|
|
2487
|
+
if (existsSync(config.vaultDir)) {
|
|
2488
|
+
try {
|
|
2489
|
+
const probe = join(config.vaultDir, ".write-probe");
|
|
2490
|
+
writeFileSync(probe, "");
|
|
2491
|
+
unlinkSync(probe);
|
|
2492
|
+
console.log(` ${green("✓")} Vault dir ${dim(config.vaultDir)}`);
|
|
2493
|
+
} catch {
|
|
2494
|
+
console.log(` ${red("✘")} Vault dir not writable: ${config.vaultDir}`);
|
|
2495
|
+
console.log(` ${dim(`Fix: chmod u+w "${config.vaultDir}"`)}`);
|
|
2496
|
+
allOk = false;
|
|
2497
|
+
}
|
|
2498
|
+
} else {
|
|
2499
|
+
console.log(
|
|
2500
|
+
` ${yellow("!")} Vault dir missing — will be created on next start`,
|
|
2501
|
+
);
|
|
2502
|
+
console.log(` ${dim(`mkdir -p "${config.vaultDir}"`)}`);
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
2506
|
+
if (existsSync(config.dbPath)) {
|
|
2507
|
+
try {
|
|
2508
|
+
const { initDatabase } = await import("@context-vault/core/index/db");
|
|
2509
|
+
const db = await initDatabase(config.dbPath);
|
|
2510
|
+
db.close();
|
|
2511
|
+
console.log(` ${green("✓")} Database ${dim(config.dbPath)}`);
|
|
2512
|
+
} catch (e) {
|
|
2513
|
+
console.log(` ${red("✘")} Database error: ${e.message}`);
|
|
2514
|
+
console.log(
|
|
2515
|
+
` ${dim(`Fix: rm "${config.dbPath}" (data will be lost)`)}`,
|
|
2516
|
+
);
|
|
2517
|
+
allOk = false;
|
|
2518
|
+
}
|
|
2519
|
+
} else {
|
|
2520
|
+
console.log(
|
|
2521
|
+
` ${yellow("!")} Database missing — will be created on next start`,
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// ── Launcher (server.mjs) ─────────────────────────────────────────────
|
|
2526
|
+
const launcherPath = join(HOME, ".context-mcp", "server.mjs");
|
|
2527
|
+
if (existsSync(launcherPath)) {
|
|
2528
|
+
const launcherContent = readFileSync(launcherPath, "utf-8");
|
|
2529
|
+
const match = launcherContent.match(/import "(.+?)"/);
|
|
2530
|
+
if (match) {
|
|
2531
|
+
const serverEntryPath = match[1];
|
|
2532
|
+
if (existsSync(serverEntryPath)) {
|
|
2533
|
+
console.log(
|
|
2534
|
+
` ${green("✓")} Launcher ${dim(`→ ${serverEntryPath}`)}`,
|
|
2535
|
+
);
|
|
2536
|
+
} else {
|
|
2537
|
+
console.log(
|
|
2538
|
+
` ${red("✘")} Launcher points to missing server: ${serverEntryPath}`,
|
|
2539
|
+
);
|
|
2540
|
+
console.log(
|
|
2541
|
+
` ${dim("Fix: run context-vault setup to reinstall")}`,
|
|
2542
|
+
);
|
|
2543
|
+
allOk = false;
|
|
2544
|
+
}
|
|
2545
|
+
} else {
|
|
2546
|
+
console.log(` ${green("✓")} Launcher exists ${dim(launcherPath)}`);
|
|
2547
|
+
}
|
|
2548
|
+
} else {
|
|
2549
|
+
console.log(` ${yellow("!")} Launcher not found at ${launcherPath}`);
|
|
2550
|
+
console.log(` ${dim("Fix: run context-vault setup")}`);
|
|
2551
|
+
allOk = false;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// ── Error log ─────────────────────────────────────────────────────────
|
|
2555
|
+
const logPath = errorLogPath(config.dataDir);
|
|
2556
|
+
const logCount = errorLogCount(config.dataDir);
|
|
2557
|
+
if (logCount > 0) {
|
|
2558
|
+
console.log();
|
|
2559
|
+
console.log(
|
|
2560
|
+
` ${yellow("!")} Error log has ${logCount} entr${logCount === 1 ? "y" : "ies"}: ${dim(logPath)}`,
|
|
2561
|
+
);
|
|
2562
|
+
try {
|
|
2563
|
+
const lines = readFileSync(logPath, "utf-8")
|
|
2564
|
+
.split("\n")
|
|
2565
|
+
.filter((l) => l.trim());
|
|
2566
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
2567
|
+
console.log(` Last error: ${red(last.message)}`);
|
|
2568
|
+
console.log(
|
|
2569
|
+
` Phase: ${dim(last.phase || "unknown")} Time: ${dim(last.timestamp)}`,
|
|
2570
|
+
);
|
|
2571
|
+
} catch {}
|
|
2572
|
+
console.log(` ${dim(`To clear: rm "${logPath}"`)}`);
|
|
2573
|
+
allOk = false;
|
|
2574
|
+
} else {
|
|
2575
|
+
console.log(` ${green("✓")} No startup errors logged`);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// ── MCP tool configs ──────────────────────────────────────────────────────
|
|
2580
|
+
console.log();
|
|
2581
|
+
console.log(bold(" Tool Configurations"));
|
|
2582
|
+
const claudeConfigPath = join(HOME, ".claude.json");
|
|
2583
|
+
if (existsSync(claudeConfigPath)) {
|
|
2584
|
+
try {
|
|
2585
|
+
const claudeConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
|
|
2586
|
+
const servers = claudeConfig?.mcpServers || {};
|
|
2587
|
+
if (servers["context-vault"]) {
|
|
2588
|
+
const srv = servers["context-vault"];
|
|
2589
|
+
const cmd = [srv.command, ...(srv.args || [])].join(" ");
|
|
2590
|
+
console.log(` ${green("+")} Claude Code: ${dim(cmd)}`);
|
|
2591
|
+
} else {
|
|
2592
|
+
console.log(` ${dim("-")} Claude Code: context-vault not configured`);
|
|
2593
|
+
console.log(` ${dim("Fix: run context-vault setup")}`);
|
|
2594
|
+
}
|
|
2595
|
+
} catch {
|
|
2596
|
+
console.log(
|
|
2597
|
+
` ${yellow("!")} Claude Code: could not read ~/.claude.json`,
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
} else {
|
|
2601
|
+
console.log(` ${dim("-")} Claude Code: ~/.claude.json not found`);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
2605
|
+
console.log();
|
|
2606
|
+
if (allOk) {
|
|
2607
|
+
console.log(
|
|
2608
|
+
` ${green("✓ All checks passed.")} If the MCP server still fails, try:`,
|
|
2609
|
+
);
|
|
2610
|
+
console.log(
|
|
2611
|
+
` ${dim("context-vault setup")} — reconfigure tool integrations`,
|
|
2612
|
+
);
|
|
2613
|
+
} else {
|
|
2614
|
+
console.log(
|
|
2615
|
+
` ${yellow("Some issues found.")} Address the items above, then restart your AI tool.`,
|
|
2616
|
+
);
|
|
2617
|
+
console.log(
|
|
2618
|
+
` ${dim("context-vault setup")} — reconfigure and repair installation`,
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
console.log();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2233
2624
|
async function runServe() {
|
|
2234
2625
|
await import("../src/server/index.js");
|
|
2235
2626
|
}
|
|
@@ -2261,6 +2652,9 @@ async function main() {
|
|
|
2261
2652
|
case "hooks":
|
|
2262
2653
|
await runHooks();
|
|
2263
2654
|
break;
|
|
2655
|
+
case "flush":
|
|
2656
|
+
await runFlush();
|
|
2657
|
+
break;
|
|
2264
2658
|
case "recall":
|
|
2265
2659
|
await runRecall();
|
|
2266
2660
|
break;
|
|
@@ -2291,6 +2685,9 @@ async function main() {
|
|
|
2291
2685
|
case "migrate":
|
|
2292
2686
|
await runMigrate();
|
|
2293
2687
|
break;
|
|
2688
|
+
case "doctor":
|
|
2689
|
+
await runDoctor();
|
|
2690
|
+
break;
|
|
2294
2691
|
default:
|
|
2295
2692
|
console.error(red(`Unknown command: ${command}`));
|
|
2296
2693
|
console.error(`Run ${cyan("context-vault --help")} for usage.`);
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const APP_URL = "https://app.context-vault.com";
|
|
2
|
+
export const API_URL = "https://api.context-vault.com";
|
|
3
|
+
export const MARKETING_URL = "https://contextvault.dev";
|
|
4
|
+
export const GITHUB_ISSUES_URL =
|
|
5
|
+
"https://github.com/fellanH/context-vault/issues";
|
|
6
|
+
|
|
1
7
|
export const MAX_BODY_LENGTH = 100 * 1024; // 100KB
|
|
2
8
|
export const MAX_TITLE_LENGTH = 500;
|
|
3
9
|
export const MAX_KIND_LENGTH = 64;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { API_URL, MARKETING_URL, GITHUB_ISSUES_URL } from "../constants.js";
|
|
3
4
|
|
|
4
|
-
const TELEMETRY_ENDPOINT =
|
|
5
|
+
const TELEMETRY_ENDPOINT = `${API_URL}/telemetry`;
|
|
5
6
|
const NOTICE_MARKER = ".telemetry-notice-shown";
|
|
7
|
+
const FEEDBACK_PROMPT_MARKER = ".feedback-prompt-shown";
|
|
6
8
|
|
|
7
9
|
export function isTelemetryEnabled(config) {
|
|
8
10
|
const envVal = process.env.CONTEXT_VAULT_TELEMETRY;
|
|
@@ -56,7 +58,31 @@ export function maybeShowTelemetryNotice(dataDir) {
|
|
|
56
58
|
"[context-vault] Reports contain only: event type, error code, tool name, version, node version, platform, arch, timestamp.",
|
|
57
59
|
"[context-vault] No vault content, file paths, or personal data is ever sent.",
|
|
58
60
|
'[context-vault] Opt in: set "telemetry": true in ~/.context-mcp/config.json or set CONTEXT_VAULT_TELEMETRY=1.',
|
|
59
|
-
|
|
61
|
+
`[context-vault] Full payload schema: ${MARKETING_URL}/telemetry`,
|
|
62
|
+
];
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
process.stderr.write(line + "\n");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Print a one-time feedback prompt after the user's first successful save.
|
|
70
|
+
* Uses a marker file in dataDir to ensure it's only shown once.
|
|
71
|
+
* Never throws, never blocks.
|
|
72
|
+
*/
|
|
73
|
+
export function maybeShowFeedbackPrompt(dataDir) {
|
|
74
|
+
try {
|
|
75
|
+
const markerPath = join(dataDir, FEEDBACK_PROMPT_MARKER);
|
|
76
|
+
if (existsSync(markerPath)) return;
|
|
77
|
+
writeFileSync(markerPath, new Date().toISOString() + "\n");
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const lines = [
|
|
83
|
+
"[context-vault] First entry saved — nice work!",
|
|
84
|
+
"[context-vault] Got feedback, a bug, or a feature request?",
|
|
85
|
+
`[context-vault] Open an issue: ${GITHUB_ISSUES_URL}`,
|
|
60
86
|
];
|
|
61
87
|
for (const line of lines) {
|
|
62
88
|
process.stderr.write(line + "\n");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok } from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
export const name = "clear_context";
|
|
5
|
+
|
|
6
|
+
export const description =
|
|
7
|
+
"Reset active in-memory session context without deleting vault entries. Call this when switching projects or topics mid-session. With `scope`, all subsequent get_context calls should filter to that tag/project. Vault data is never modified.";
|
|
8
|
+
|
|
9
|
+
export const inputSchema = {
|
|
10
|
+
scope: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
"Optional tag or project name to focus on going forward. When provided, treat subsequent get_context calls as if filtered to this tag.",
|
|
15
|
+
),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} args
|
|
20
|
+
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} _ctx
|
|
21
|
+
*/
|
|
22
|
+
export function handler({ scope } = {}) {
|
|
23
|
+
const lines = [
|
|
24
|
+
"## Context Reset",
|
|
25
|
+
"",
|
|
26
|
+
"Active session context has been cleared. All previous context from this session should be disregarded.",
|
|
27
|
+
"",
|
|
28
|
+
"Vault entries are unchanged — no data was deleted.",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (scope?.trim()) {
|
|
32
|
+
const trimmed = scope.trim();
|
|
33
|
+
lines.push(
|
|
34
|
+
"",
|
|
35
|
+
`### Active Scope: \`${trimmed}\``,
|
|
36
|
+
"",
|
|
37
|
+
`Going forward, treat \`get_context\` calls as scoped to the tag or project **"${trimmed}"** unless the user explicitly requests a different scope or passes their own tag filters.`,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
lines.push(
|
|
41
|
+
"",
|
|
42
|
+
"No scope set. Use `get_context` normally — all vault entries are accessible.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ok(lines.join("\n"));
|
|
47
|
+
}
|
|
@@ -5,6 +5,87 @@ import { normalizeKind } from "../../core/files.js";
|
|
|
5
5
|
import { ok, err } from "../helpers.js";
|
|
6
6
|
import { isEmbedAvailable } from "../../index/embed.js";
|
|
7
7
|
|
|
8
|
+
const STALE_DUPLICATE_DAYS = 7;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect conflicts among a set of search result entries.
|
|
12
|
+
*
|
|
13
|
+
* Two checks are performed:
|
|
14
|
+
* 1. Supersession: if entry A's `superseded_by` points to any entry B in the
|
|
15
|
+
* result set, A is stale and should be discarded in favour of B.
|
|
16
|
+
* 2. Stale duplicate: two entries share the same kind and at least one common
|
|
17
|
+
* tag, but their `updated_at` timestamps differ by more than
|
|
18
|
+
* STALE_DUPLICATE_DAYS days — suggesting the older one may be outdated.
|
|
19
|
+
*
|
|
20
|
+
* No LLM calls, no new dependencies — pure in-memory set operations on the
|
|
21
|
+
* rows already fetched from the DB.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array} entries - Result rows (as returned by hybridSearch / filter-only mode)
|
|
24
|
+
* @param {import('../types.js').BaseCtx} _ctx - Unused for now; reserved for future DB look-ups
|
|
25
|
+
* @returns {Array<{entry_a_id: string, entry_b_id: string, reason: string, recommendation: string}>}
|
|
26
|
+
*/
|
|
27
|
+
export function detectConflicts(entries, _ctx) {
|
|
28
|
+
const conflicts = [];
|
|
29
|
+
const idSet = new Set(entries.map((e) => e.id));
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.superseded_by && idSet.has(entry.superseded_by)) {
|
|
33
|
+
conflicts.push({
|
|
34
|
+
entry_a_id: entry.id,
|
|
35
|
+
entry_b_id: entry.superseded_by,
|
|
36
|
+
reason: "superseded",
|
|
37
|
+
recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const supersededConflictPairs = new Set(
|
|
43
|
+
conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < entries.length; i++) {
|
|
47
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
48
|
+
const a = entries[i];
|
|
49
|
+
const b = entries[j];
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
supersededConflictPairs.has(`${a.id}|${b.id}`) ||
|
|
53
|
+
supersededConflictPairs.has(`${b.id}|${a.id}`)
|
|
54
|
+
) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (a.kind !== b.kind) continue;
|
|
59
|
+
|
|
60
|
+
const tagsA = a.tags ? JSON.parse(a.tags) : [];
|
|
61
|
+
const tagsB = b.tags ? JSON.parse(b.tags) : [];
|
|
62
|
+
|
|
63
|
+
if (!tagsA.length || !tagsB.length) continue;
|
|
64
|
+
|
|
65
|
+
const tagsSetA = new Set(tagsA);
|
|
66
|
+
const sharedTag = tagsB.some((t) => tagsSetA.has(t));
|
|
67
|
+
if (!sharedTag) continue;
|
|
68
|
+
|
|
69
|
+
const dateA = new Date(a.updated_at || a.created_at);
|
|
70
|
+
const dateB = new Date(b.updated_at || b.created_at);
|
|
71
|
+
if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) continue;
|
|
72
|
+
|
|
73
|
+
const diffDays = Math.abs(dateA - dateB) / 86400000;
|
|
74
|
+
if (diffDays <= STALE_DUPLICATE_DAYS) continue;
|
|
75
|
+
|
|
76
|
+
const [older, newer] = dateA < dateB ? [a, b] : [b, a];
|
|
77
|
+
conflicts.push({
|
|
78
|
+
entry_a_id: older.id,
|
|
79
|
+
entry_b_id: newer.id,
|
|
80
|
+
reason: "stale_duplicate",
|
|
81
|
+
recommendation: `Verify \`${older.id}\` is still accurate — it shares kind "${older.kind}" and tags with \`${newer.id}\` but was last updated ${Math.round(diffDays)} days earlier.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return conflicts;
|
|
87
|
+
}
|
|
88
|
+
|
|
8
89
|
export const name = "get_context";
|
|
9
90
|
|
|
10
91
|
export const description =
|
|
@@ -48,6 +129,12 @@ export const inputSchema = {
|
|
|
48
129
|
.describe(
|
|
49
130
|
"If true, include entries that have been superseded by newer ones. Default: false.",
|
|
50
131
|
),
|
|
132
|
+
detect_conflicts: z
|
|
133
|
+
.boolean()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe(
|
|
136
|
+
"If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.",
|
|
137
|
+
),
|
|
51
138
|
};
|
|
52
139
|
|
|
53
140
|
/**
|
|
@@ -66,6 +153,7 @@ export async function handler(
|
|
|
66
153
|
until,
|
|
67
154
|
limit,
|
|
68
155
|
include_superseded,
|
|
156
|
+
detect_conflicts,
|
|
69
157
|
},
|
|
70
158
|
ctx,
|
|
71
159
|
{ ensureIndexed, reindexFailed },
|
|
@@ -227,6 +315,9 @@ export async function handler(
|
|
|
227
315
|
}
|
|
228
316
|
}
|
|
229
317
|
|
|
318
|
+
// Conflict detection
|
|
319
|
+
const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
|
|
320
|
+
|
|
230
321
|
const lines = [];
|
|
231
322
|
if (reindexFailed)
|
|
232
323
|
lines.push(
|
|
@@ -265,5 +356,23 @@ export async function handler(
|
|
|
265
356
|
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
|
|
266
357
|
lines.push("");
|
|
267
358
|
}
|
|
359
|
+
|
|
360
|
+
if (detect_conflicts) {
|
|
361
|
+
if (conflicts.length === 0) {
|
|
362
|
+
lines.push(
|
|
363
|
+
`## Conflict Detection\n\nNo conflicts detected among results.\n`,
|
|
364
|
+
);
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
|
|
367
|
+
for (const c of conflicts) {
|
|
368
|
+
lines.push(
|
|
369
|
+
`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
|
|
370
|
+
);
|
|
371
|
+
lines.push(` Recommendation: ${c.recommendation}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push("");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
268
377
|
return ok(lines.join("\n"));
|
|
269
378
|
}
|
|
@@ -4,6 +4,7 @@ import { indexEntry } from "../../index/index.js";
|
|
|
4
4
|
import { categoryFor } from "../../core/categories.js";
|
|
5
5
|
import { normalizeKind } from "../../core/files.js";
|
|
6
6
|
import { ok, err, ensureVaultExists, ensureValidKind } from "../helpers.js";
|
|
7
|
+
import { maybeShowFeedbackPrompt } from "../../core/telemetry.js";
|
|
7
8
|
import {
|
|
8
9
|
MAX_BODY_LENGTH,
|
|
9
10
|
MAX_TITLE_LENGTH,
|
|
@@ -398,6 +399,11 @@ export async function handler(
|
|
|
398
399
|
supersedes,
|
|
399
400
|
userId,
|
|
400
401
|
});
|
|
402
|
+
|
|
403
|
+
if (ctx.config?.dataDir) {
|
|
404
|
+
maybeShowFeedbackPrompt(ctx.config.dataDir);
|
|
405
|
+
}
|
|
406
|
+
|
|
401
407
|
const relPath = entry.filePath
|
|
402
408
|
? entry.filePath.replace(config.vaultDir + "/", "")
|
|
403
409
|
: entry.filePath;
|
|
@@ -11,6 +11,7 @@ import * as deleteContext from "./tools/delete-context.js";
|
|
|
11
11
|
import * as submitFeedback from "./tools/submit-feedback.js";
|
|
12
12
|
import * as ingestUrl from "./tools/ingest-url.js";
|
|
13
13
|
import * as contextStatus from "./tools/context-status.js";
|
|
14
|
+
import * as clearContext from "./tools/clear-context.js";
|
|
14
15
|
|
|
15
16
|
const toolModules = [
|
|
16
17
|
getContext,
|
|
@@ -20,6 +21,7 @@ const toolModules = [
|
|
|
20
21
|
submitFeedback,
|
|
21
22
|
ingestUrl,
|
|
22
23
|
contextStatus,
|
|
24
|
+
clearContext,
|
|
23
25
|
];
|
|
24
26
|
|
|
25
27
|
const TOOL_TIMEOUT_MS = 60_000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@context-vault/core"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@context-vault/core": "^2.
|
|
58
|
+
"@context-vault/core": "^2.13.0",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
60
|
"sqlite-vec": "^0.1.0"
|
|
61
61
|
}
|
package/src/server/index.js
CHANGED
|
@@ -265,6 +265,50 @@ async function main() {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// ─── Top-level Safety Net ────────────────────────────────────────────────────
|
|
269
|
+
// Catch any errors that escape the main() try/catch (e.g. thrown in MCP
|
|
270
|
+
// transport callbacks or in unrelated async chains). Claude Code surfaces
|
|
271
|
+
// stderr when a server exits unexpectedly, so every message written here will
|
|
272
|
+
// be visible to the user.
|
|
273
|
+
|
|
274
|
+
process.on("uncaughtException", (err) => {
|
|
275
|
+
const dataDir = join(homedir(), ".context-mcp");
|
|
276
|
+
const logEntry = {
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
error_type: "uncaughtException",
|
|
279
|
+
message: err.message,
|
|
280
|
+
stack: err.stack?.split("\n").slice(0, 5).join(" | "),
|
|
281
|
+
node_version: process.version,
|
|
282
|
+
platform: process.platform,
|
|
283
|
+
arch: process.arch,
|
|
284
|
+
cv_version: pkg.version,
|
|
285
|
+
};
|
|
286
|
+
appendErrorLog(dataDir, logEntry);
|
|
287
|
+
console.error(`[context-vault] Uncaught exception: ${err.message}`);
|
|
288
|
+
console.error(`[context-vault] Error log: ${join(dataDir, "error.log")}`);
|
|
289
|
+
console.error(`[context-vault] Run: context-vault doctor`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
process.on("unhandledRejection", (reason) => {
|
|
294
|
+
const dataDir = join(homedir(), ".context-mcp");
|
|
295
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
296
|
+
const logEntry = {
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
298
|
+
error_type: "unhandledRejection",
|
|
299
|
+
message,
|
|
300
|
+
node_version: process.version,
|
|
301
|
+
platform: process.platform,
|
|
302
|
+
arch: process.arch,
|
|
303
|
+
cv_version: pkg.version,
|
|
304
|
+
};
|
|
305
|
+
appendErrorLog(dataDir, logEntry);
|
|
306
|
+
console.error(`[context-vault] Unhandled rejection: ${message}`);
|
|
307
|
+
console.error(`[context-vault] Error log: ${join(dataDir, "error.log")}`);
|
|
308
|
+
console.error(`[context-vault] Run: context-vault doctor`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
268
312
|
main().catch((err) => {
|
|
269
313
|
console.error(`[context-vault] Unexpected fatal error: ${err.message}`);
|
|
270
314
|
process.exit(1);
|