context-vault 2.17.1 → 3.0.2
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 +795 -71
- package/node_modules/@context-vault/core/dist/capture.d.ts +21 -0
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/capture.js +269 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -0
- package/node_modules/@context-vault/core/dist/categories.d.ts +6 -0
- package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/categories.js +50 -0
- package/node_modules/@context-vault/core/dist/categories.js.map +1 -0
- package/node_modules/@context-vault/core/dist/config.d.ts +4 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/config.js +190 -0
- package/node_modules/@context-vault/core/dist/config.js.map +1 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts +33 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/constants.js +23 -0
- package/node_modules/@context-vault/core/dist/constants.js.map +1 -0
- package/node_modules/@context-vault/core/dist/db.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/db.js +191 -0
- package/node_modules/@context-vault/core/dist/db.js.map +1 -0
- package/node_modules/@context-vault/core/dist/embed.d.ts +5 -0
- package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/embed.js +78 -0
- package/node_modules/@context-vault/core/dist/embed.js.map +1 -0
- package/node_modules/@context-vault/core/dist/files.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/files.js +66 -0
- package/node_modules/@context-vault/core/dist/files.js.map +1 -0
- package/node_modules/@context-vault/core/dist/formatters.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/formatters.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/formatters.js +18 -0
- package/node_modules/@context-vault/core/dist/formatters.js.map +1 -0
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts +12 -0
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/frontmatter.js +101 -0
- package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -0
- package/node_modules/@context-vault/core/dist/index.d.ts +10 -0
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/index.js +297 -0
- package/node_modules/@context-vault/core/dist/index.js.map +1 -0
- package/node_modules/@context-vault/core/dist/ingest-url.d.ts +20 -0
- package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/ingest-url.js +113 -0
- package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -0
- package/node_modules/@context-vault/core/dist/main.d.ts +14 -0
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/main.js +25 -0
- package/node_modules/@context-vault/core/dist/main.js.map +1 -0
- package/node_modules/@context-vault/core/dist/search.d.ts +18 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/search.js +238 -0
- package/node_modules/@context-vault/core/dist/search.js.map +1 -0
- package/node_modules/@context-vault/core/dist/types.d.ts +176 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/types.js +2 -0
- package/node_modules/@context-vault/core/dist/types.js.map +1 -0
- package/node_modules/@context-vault/core/package.json +66 -16
- package/node_modules/@context-vault/core/src/capture.ts +308 -0
- package/node_modules/@context-vault/core/src/categories.ts +54 -0
- package/node_modules/@context-vault/core/src/{core/config.js → config.ts} +34 -33
- package/node_modules/@context-vault/core/src/{constants.js → constants.ts} +6 -3
- package/node_modules/@context-vault/core/src/db.ts +229 -0
- package/node_modules/@context-vault/core/src/{index/embed.js → embed.ts} +10 -35
- package/node_modules/@context-vault/core/src/{core/files.js → files.ts} +15 -20
- package/node_modules/@context-vault/core/src/{capture/formatters.js → formatters.ts} +13 -11
- package/node_modules/@context-vault/core/src/{core/frontmatter.js → frontmatter.ts} +26 -33
- package/node_modules/@context-vault/core/src/index.ts +351 -0
- package/node_modules/@context-vault/core/src/ingest-url.ts +99 -0
- package/node_modules/@context-vault/core/src/main.ts +111 -0
- package/node_modules/@context-vault/core/src/{retrieve/index.js → search.ts} +62 -150
- package/node_modules/@context-vault/core/src/types.ts +166 -0
- package/package.json +12 -7
- package/scripts/postinstall.js +1 -1
- package/{node_modules/@context-vault/core/src/core → src}/error-log.js +1 -15
- package/{node_modules/@context-vault/core/src/server → src}/helpers.js +9 -4
- package/src/linking.js +100 -0
- package/{node_modules/@context-vault/core/src/server/tools.js → src/register-tools.js} +14 -13
- package/src/{server/index.js → server.js} +10 -38
- package/src/status.js +235 -0
- package/{node_modules/@context-vault/core/src/core → src}/telemetry.js +9 -19
- package/src/temporal.js +97 -0
- package/{node_modules/@context-vault/core/src/server → src}/tools/context-status.js +3 -4
- package/{node_modules/@context-vault/core/src/server → src}/tools/create-snapshot.js +6 -7
- package/{node_modules/@context-vault/core/src/server → src}/tools/delete-context.js +0 -2
- package/{node_modules/@context-vault/core/src/server → src}/tools/get-context.js +17 -21
- package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-project.js +5 -6
- package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-url.js +3 -4
- package/{node_modules/@context-vault/core/src/server → src}/tools/list-buckets.js +4 -5
- package/{node_modules/@context-vault/core/src/server → src}/tools/list-context.js +3 -6
- package/{node_modules/@context-vault/core/src/server → src}/tools/save-context.js +17 -20
- package/{node_modules/@context-vault/core/src/server → src}/tools/session-start.js +9 -16
- package/node_modules/@context-vault/core/src/capture/file-ops.js +0 -99
- package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -46
- package/node_modules/@context-vault/core/src/capture/importers.js +0 -387
- package/node_modules/@context-vault/core/src/capture/index.js +0 -250
- package/node_modules/@context-vault/core/src/capture/ingest-url.js +0 -252
- package/node_modules/@context-vault/core/src/consolidation/index.js +0 -112
- package/node_modules/@context-vault/core/src/core/categories.js +0 -73
- package/node_modules/@context-vault/core/src/core/linking.js +0 -161
- package/node_modules/@context-vault/core/src/core/migrate-dirs.js +0 -196
- package/node_modules/@context-vault/core/src/core/status.js +0 -350
- package/node_modules/@context-vault/core/src/core/temporal.js +0 -146
- package/node_modules/@context-vault/core/src/index/db.js +0 -586
- package/node_modules/@context-vault/core/src/index/index.js +0 -583
- package/node_modules/@context-vault/core/src/index.js +0 -71
- package/node_modules/@context-vault/core/src/sync/sync.js +0 -235
- package/src/hooks/post-tool-call.mjs +0 -62
- package/src/hooks/session-end.mjs +0 -492
- /package/{node_modules/@context-vault/core/src/server → src}/tools/clear-context.js +0 -0
package/src/linking.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
|
|
3
|
+
export function parseRelatedTo(raw: string | null | undefined): string[] {
|
|
4
|
+
if (!raw) return [];
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
if (!Array.isArray(parsed)) return [];
|
|
8
|
+
return parsed.filter((id: unknown) => typeof id === "string" && (id as string).trim());
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveLinks(
|
|
15
|
+
db: DatabaseSync,
|
|
16
|
+
ids: string[],
|
|
17
|
+
): Record<string, unknown>[] {
|
|
18
|
+
if (!ids.length) return [];
|
|
19
|
+
const unique = [...new Set(ids)];
|
|
20
|
+
const placeholders = unique.map(() => "?").join(",");
|
|
21
|
+
try {
|
|
22
|
+
return db
|
|
23
|
+
.prepare(
|
|
24
|
+
`SELECT * FROM vault
|
|
25
|
+
WHERE id IN (${placeholders})
|
|
26
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
27
|
+
AND superseded_by IS NULL`,
|
|
28
|
+
)
|
|
29
|
+
.all(...unique) as unknown as Record<string, unknown>[];
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveBacklinks(
|
|
36
|
+
db: DatabaseSync,
|
|
37
|
+
entryId: string,
|
|
38
|
+
): Record<string, unknown>[] {
|
|
39
|
+
if (!entryId) return [];
|
|
40
|
+
const likePattern = `%"${entryId}"%`;
|
|
41
|
+
try {
|
|
42
|
+
return db
|
|
43
|
+
.prepare(
|
|
44
|
+
`SELECT * FROM vault
|
|
45
|
+
WHERE related_to LIKE ?
|
|
46
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
47
|
+
AND superseded_by IS NULL`,
|
|
48
|
+
)
|
|
49
|
+
.all(likePattern) as unknown as Record<string, unknown>[];
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function collectLinkedEntries(
|
|
56
|
+
db: DatabaseSync,
|
|
57
|
+
primaryEntries: Record<string, unknown>[],
|
|
58
|
+
): { forward: Record<string, unknown>[]; backward: Record<string, unknown>[] } {
|
|
59
|
+
const primaryIds = new Set(primaryEntries.map((e) => e.id as string));
|
|
60
|
+
|
|
61
|
+
const forwardIds: string[] = [];
|
|
62
|
+
for (const entry of primaryEntries) {
|
|
63
|
+
const ids = parseRelatedTo(entry.related_to as string);
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
if (!primaryIds.has(id)) forwardIds.push(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const forwardEntries = resolveLinks(db, forwardIds).filter(
|
|
69
|
+
(e) => !primaryIds.has(e.id as string),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const backwardSeen = new Set<string>();
|
|
73
|
+
const backwardEntries: Record<string, unknown>[] = [];
|
|
74
|
+
for (const entry of primaryEntries) {
|
|
75
|
+
const backlinks = resolveBacklinks(db, entry.id as string);
|
|
76
|
+
for (const bl of backlinks) {
|
|
77
|
+
if (!primaryIds.has(bl.id as string) && !backwardSeen.has(bl.id as string)) {
|
|
78
|
+
backwardSeen.add(bl.id as string);
|
|
79
|
+
backwardEntries.push(bl);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { forward: forwardEntries, backward: backwardEntries };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateRelatedTo(relatedTo: unknown): string | null {
|
|
88
|
+
if (relatedTo === undefined || relatedTo === null) return null;
|
|
89
|
+
if (!Array.isArray(relatedTo))
|
|
90
|
+
return "related_to must be an array of entry IDs";
|
|
91
|
+
for (const id of relatedTo) {
|
|
92
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
93
|
+
return "each related_to entry must be a non-empty string ID";
|
|
94
|
+
}
|
|
95
|
+
if (id.length > 32) {
|
|
96
|
+
return `related_to ID too long (max 32 chars): "${id.slice(0, 32)}..."`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { reindex } from "
|
|
2
|
-
import { captureAndIndex } from "
|
|
1
|
+
import { reindex } from "@context-vault/core/index";
|
|
2
|
+
import { captureAndIndex } from "@context-vault/core/capture";
|
|
3
3
|
import { err } from "./helpers.js";
|
|
4
|
-
import { sendTelemetryEvent } from "
|
|
5
|
-
import
|
|
4
|
+
import { sendTelemetryEvent } from "./telemetry.js";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkg = JSON.parse(
|
|
11
|
+
readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"),
|
|
12
|
+
);
|
|
6
13
|
|
|
7
14
|
import * as getContext from "./tools/get-context.js";
|
|
8
15
|
import * as saveContext from "./tools/save-context.js";
|
|
@@ -33,8 +40,6 @@ const toolModules = [
|
|
|
33
40
|
const TOOL_TIMEOUT_MS = 60_000;
|
|
34
41
|
|
|
35
42
|
export function registerTools(server, ctx) {
|
|
36
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
37
|
-
|
|
38
43
|
function tracked(handler, toolName) {
|
|
39
44
|
return async (...args) => {
|
|
40
45
|
if (ctx.activeOps) ctx.activeOps.count++;
|
|
@@ -55,8 +60,6 @@ export function registerTools(server, ctx) {
|
|
|
55
60
|
return result;
|
|
56
61
|
} catch (e) {
|
|
57
62
|
if (e.message === "TOOL_TIMEOUT") {
|
|
58
|
-
// Suppress any late rejection from the still-running handler to
|
|
59
|
-
// prevent unhandled promise rejection warnings in the host process.
|
|
60
63
|
handlerPromise?.catch(() => {});
|
|
61
64
|
if (ctx.toolStats) {
|
|
62
65
|
ctx.toolStats.errors++;
|
|
@@ -105,7 +108,7 @@ export function registerTools(server, ctx) {
|
|
|
105
108
|
auto: true,
|
|
106
109
|
},
|
|
107
110
|
});
|
|
108
|
-
} catch {}
|
|
111
|
+
} catch {}
|
|
109
112
|
throw e;
|
|
110
113
|
} finally {
|
|
111
114
|
clearTimeout(timer);
|
|
@@ -114,8 +117,7 @@ export function registerTools(server, ctx) {
|
|
|
114
117
|
};
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
let reindexDone = userId !== undefined ? true : false;
|
|
120
|
+
let reindexDone = false;
|
|
119
121
|
let reindexPromise = null;
|
|
120
122
|
let reindexAttempts = 0;
|
|
121
123
|
let reindexFailed = false;
|
|
@@ -124,7 +126,6 @@ export function registerTools(server, ctx) {
|
|
|
124
126
|
async function ensureIndexed() {
|
|
125
127
|
if (reindexDone) return;
|
|
126
128
|
if (reindexPromise) return reindexPromise;
|
|
127
|
-
// Assign promise synchronously to prevent concurrent calls from both entering reindex()
|
|
128
129
|
const promise = reindex(ctx, { fullSync: true })
|
|
129
130
|
.then((stats) => {
|
|
130
131
|
reindexDone = true;
|
|
@@ -147,7 +148,7 @@ export function registerTools(server, ctx) {
|
|
|
147
148
|
reindexDone = true;
|
|
148
149
|
reindexFailed = true;
|
|
149
150
|
} else {
|
|
150
|
-
reindexPromise = null;
|
|
151
|
+
reindexPromise = null;
|
|
151
152
|
}
|
|
152
153
|
});
|
|
153
154
|
reindexPromise = promise;
|
|
@@ -18,24 +18,19 @@ const pkg = JSON.parse(
|
|
|
18
18
|
readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"),
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
import { resolveConfig } from "@context-vault/core/
|
|
22
|
-
import { appendErrorLog } from "
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
maybeShowTelemetryNotice,
|
|
26
|
-
} from "@context-vault/core/core/telemetry";
|
|
27
|
-
import { embed } from "@context-vault/core/index/embed";
|
|
21
|
+
import { resolveConfig } from "@context-vault/core/config";
|
|
22
|
+
import { appendErrorLog } from "./error-log.js";
|
|
23
|
+
import { sendTelemetryEvent, maybeShowTelemetryNotice } from "./telemetry.js";
|
|
24
|
+
import { embed } from "@context-vault/core/embed";
|
|
28
25
|
import {
|
|
29
26
|
initDatabase,
|
|
30
27
|
NativeModuleError,
|
|
31
28
|
prepareStatements,
|
|
32
29
|
insertVec,
|
|
33
30
|
deleteVec,
|
|
34
|
-
} from "@context-vault/core/
|
|
35
|
-
import { registerTools } from "
|
|
36
|
-
import { pruneExpired } from "@context-vault/core/index
|
|
37
|
-
|
|
38
|
-
// ─── Phased Startup ─────────────────────────────────────────────────────────
|
|
31
|
+
} from "@context-vault/core/db";
|
|
32
|
+
import { registerTools } from "./register-tools.js";
|
|
33
|
+
import { pruneExpired } from "@context-vault/core/index";
|
|
39
34
|
|
|
40
35
|
async function main() {
|
|
41
36
|
let phase = "CONFIG";
|
|
@@ -43,16 +38,13 @@ async function main() {
|
|
|
43
38
|
let config;
|
|
44
39
|
|
|
45
40
|
try {
|
|
46
|
-
// ── Phase: CONFIG ────────────────────────────────────────────────────────
|
|
47
41
|
config = resolveConfig();
|
|
48
42
|
|
|
49
|
-
// ── Phase: DIRS ──────────────────────────────────────────────────────────
|
|
50
43
|
phase = "DIRS";
|
|
51
44
|
mkdirSync(config.dataDir, { recursive: true });
|
|
52
45
|
mkdirSync(config.vaultDir, { recursive: true });
|
|
53
46
|
maybeShowTelemetryNotice(config.dataDir);
|
|
54
47
|
|
|
55
|
-
// Verify vault directory is writable (catch permission issues early)
|
|
56
48
|
try {
|
|
57
49
|
const probe = join(config.vaultDir, ".write-probe");
|
|
58
50
|
writeFileSync(probe, "");
|
|
@@ -68,7 +60,6 @@ async function main() {
|
|
|
68
60
|
process.exit(1);
|
|
69
61
|
}
|
|
70
62
|
|
|
71
|
-
// Write .context-mcp marker (non-fatal)
|
|
72
63
|
try {
|
|
73
64
|
const markerPath = join(config.vaultDir, ".context-mcp");
|
|
74
65
|
const markerData = existsSync(markerPath)
|
|
@@ -93,7 +84,6 @@ async function main() {
|
|
|
93
84
|
|
|
94
85
|
config.vaultDirExists = existsSync(config.vaultDir);
|
|
95
86
|
|
|
96
|
-
// Startup diagnostics
|
|
97
87
|
console.error(`[context-vault] Vault: ${config.vaultDir}`);
|
|
98
88
|
console.error(`[context-vault] Database: ${config.dbPath}`);
|
|
99
89
|
console.error(`[context-vault] Dev dir: ${config.devDir}`);
|
|
@@ -101,11 +91,9 @@ async function main() {
|
|
|
101
91
|
console.error(`[context-vault] WARNING: Vault directory not found!`);
|
|
102
92
|
}
|
|
103
93
|
|
|
104
|
-
// ── Phase: DB ────────────────────────────────────────────────────────────
|
|
105
94
|
phase = "DB";
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const stmts = prepareStatements(db, mode);
|
|
95
|
+
db = await initDatabase(config.dbPath);
|
|
96
|
+
const stmts = prepareStatements(db);
|
|
109
97
|
|
|
110
98
|
const ctx = {
|
|
111
99
|
db,
|
|
@@ -118,7 +106,6 @@ async function main() {
|
|
|
118
106
|
toolStats: { ok: 0, errors: 0, lastError: null },
|
|
119
107
|
};
|
|
120
108
|
|
|
121
|
-
// ── Phase: PRUNE ─────────────────────────────────────────────────────────
|
|
122
109
|
try {
|
|
123
110
|
const pruned = await pruneExpired(ctx);
|
|
124
111
|
if (pruned > 0) {
|
|
@@ -132,16 +119,12 @@ async function main() {
|
|
|
132
119
|
);
|
|
133
120
|
}
|
|
134
121
|
|
|
135
|
-
// ── Phase: SERVER ────────────────────────────────────────────────────────
|
|
136
122
|
phase = "SERVER";
|
|
137
123
|
const server = new McpServer(
|
|
138
124
|
{ name: "context-vault", version: pkg.version },
|
|
139
125
|
{ capabilities: { tools: {} } },
|
|
140
126
|
);
|
|
141
127
|
|
|
142
|
-
// Hot-reload config.json on every tool call (Option C from #144).
|
|
143
|
-
// resolveConfig() re-reads the small file each time — negligible I/O
|
|
144
|
-
// compared to DB queries and embedding operations that follow.
|
|
145
128
|
let lastVaultDir = config.vaultDir;
|
|
146
129
|
Object.defineProperty(ctx, "config", {
|
|
147
130
|
get() {
|
|
@@ -160,7 +143,6 @@ async function main() {
|
|
|
160
143
|
|
|
161
144
|
registerTools(server, ctx);
|
|
162
145
|
|
|
163
|
-
// ── Graceful Shutdown ────────────────────────────────────────────────────
|
|
164
146
|
function closeDb() {
|
|
165
147
|
try {
|
|
166
148
|
if (db.inTransaction) {
|
|
@@ -189,7 +171,6 @@ async function main() {
|
|
|
189
171
|
closeDb();
|
|
190
172
|
}
|
|
191
173
|
}, 100);
|
|
192
|
-
// Force shutdown after 5 seconds even if ops are still running
|
|
193
174
|
setTimeout(() => {
|
|
194
175
|
clearInterval(check);
|
|
195
176
|
console.error(
|
|
@@ -204,12 +185,10 @@ async function main() {
|
|
|
204
185
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
205
186
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
206
187
|
|
|
207
|
-
// ── Phase: CONNECTED ─────────────────────────────────────────────────────
|
|
208
188
|
phase = "CONNECTED";
|
|
209
189
|
const transport = new StdioServerTransport();
|
|
210
190
|
await server.connect(transport);
|
|
211
191
|
|
|
212
|
-
// ── Non-blocking Update Check ────────────────────────────────────────────
|
|
213
192
|
setTimeout(() => {
|
|
214
193
|
import("node:child_process")
|
|
215
194
|
.then(({ execSync }) => {
|
|
@@ -251,7 +230,6 @@ async function main() {
|
|
|
251
230
|
});
|
|
252
231
|
|
|
253
232
|
if (err instanceof NativeModuleError) {
|
|
254
|
-
// Boxed diagnostic for native module mismatch
|
|
255
233
|
console.error("");
|
|
256
234
|
console.error(
|
|
257
235
|
"╔══════════════════════════════════════════════════════════════╗",
|
|
@@ -269,7 +247,7 @@ async function main() {
|
|
|
269
247
|
console.error(` Node.js version: ${process.version}`);
|
|
270
248
|
console.error(` Error log: ${join(dataDir, "error.log")}`);
|
|
271
249
|
console.error("");
|
|
272
|
-
process.exit(78);
|
|
250
|
+
process.exit(78);
|
|
273
251
|
}
|
|
274
252
|
|
|
275
253
|
console.error(
|
|
@@ -285,12 +263,6 @@ async function main() {
|
|
|
285
263
|
}
|
|
286
264
|
}
|
|
287
265
|
|
|
288
|
-
// ─── Top-level Safety Net ────────────────────────────────────────────────────
|
|
289
|
-
// Catch any errors that escape the main() try/catch (e.g. thrown in MCP
|
|
290
|
-
// transport callbacks or in unrelated async chains). Claude Code surfaces
|
|
291
|
-
// stderr when a server exits unexpectedly, so every message written here will
|
|
292
|
-
// be visible to the user.
|
|
293
|
-
|
|
294
266
|
process.on("uncaughtException", (err) => {
|
|
295
267
|
const dataDir = join(homedir(), ".context-mcp");
|
|
296
268
|
const logEntry = {
|
package/src/status.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { walkDir } from "@context-vault/core/files";
|
|
4
|
+
import { isEmbedAvailable } from "@context-vault/core/embed";
|
|
5
|
+
import { KIND_STALENESS_DAYS } from "@context-vault/core/categories";
|
|
6
|
+
|
|
7
|
+
function countArchivedEntries(vaultDir) {
|
|
8
|
+
const archRoot = join(vaultDir, "_archive");
|
|
9
|
+
if (!existsSync(archRoot)) return 0;
|
|
10
|
+
try {
|
|
11
|
+
return walkDir(archRoot).length;
|
|
12
|
+
} catch {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function gatherVaultStatus(ctx, opts = {}) {
|
|
18
|
+
const { db, config } = ctx;
|
|
19
|
+
const errors = [];
|
|
20
|
+
|
|
21
|
+
let fileCount = 0;
|
|
22
|
+
const subdirs = [];
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(config.vaultDir)) {
|
|
25
|
+
for (const d of readdirSync(config.vaultDir, { withFileTypes: true })) {
|
|
26
|
+
if (d.isDirectory()) {
|
|
27
|
+
const dir = join(config.vaultDir, d.name);
|
|
28
|
+
const count = walkDir(dir).length;
|
|
29
|
+
fileCount += count;
|
|
30
|
+
if (count > 0) subdirs.push({ name: d.name, count });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
errors.push(`File scan failed: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let kindCounts = [];
|
|
39
|
+
try {
|
|
40
|
+
kindCounts = db
|
|
41
|
+
.prepare(`SELECT kind, COUNT(*) as c FROM vault GROUP BY kind`)
|
|
42
|
+
.all();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
errors.push(`Kind count query failed: ${e.message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let categoryCounts = [];
|
|
48
|
+
try {
|
|
49
|
+
categoryCounts = db
|
|
50
|
+
.prepare(`SELECT category, COUNT(*) as c FROM vault GROUP BY category`)
|
|
51
|
+
.all();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
errors.push(`Category count query failed: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let dbSize = "n/a";
|
|
57
|
+
let dbSizeBytes = 0;
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(config.dbPath)) {
|
|
60
|
+
dbSizeBytes = statSync(config.dbPath).size;
|
|
61
|
+
dbSize =
|
|
62
|
+
dbSizeBytes > 1024 * 1024
|
|
63
|
+
? `${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB`
|
|
64
|
+
: `${(dbSizeBytes / 1024).toFixed(1)}KB`;
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
errors.push(`DB size check failed: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let stalePaths = false;
|
|
71
|
+
let staleCount = 0;
|
|
72
|
+
try {
|
|
73
|
+
const result = db
|
|
74
|
+
.prepare(`SELECT COUNT(*) as c FROM vault WHERE file_path NOT LIKE ? || '%'`)
|
|
75
|
+
.get(config.vaultDir);
|
|
76
|
+
staleCount = result.c;
|
|
77
|
+
stalePaths = staleCount > 0;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
errors.push(`Stale path check failed: ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let expiredCount = 0;
|
|
83
|
+
try {
|
|
84
|
+
expiredCount = db
|
|
85
|
+
.prepare(`SELECT COUNT(*) as c FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')`)
|
|
86
|
+
.get().c;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
errors.push(`Expired count failed: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let eventCount = 0;
|
|
92
|
+
try {
|
|
93
|
+
eventCount = db
|
|
94
|
+
.prepare(`SELECT COUNT(*) as c FROM vault WHERE category = 'event'`)
|
|
95
|
+
.get().c;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
errors.push(`Event count failed: ${e.message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let eventsWithoutTtlCount = 0;
|
|
101
|
+
try {
|
|
102
|
+
eventsWithoutTtlCount = db
|
|
103
|
+
.prepare(`SELECT COUNT(*) as c FROM vault WHERE category = 'event' AND expires_at IS NULL`)
|
|
104
|
+
.get().c;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
errors.push(`Events without TTL count failed: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let embeddingStatus = null;
|
|
110
|
+
try {
|
|
111
|
+
const total = db.prepare(`SELECT COUNT(*) as c FROM vault`).get().c;
|
|
112
|
+
const indexed = db
|
|
113
|
+
.prepare(`SELECT COUNT(*) as c FROM vault WHERE rowid IN (SELECT rowid FROM vault_vec)`)
|
|
114
|
+
.get().c;
|
|
115
|
+
embeddingStatus = { indexed, total, missing: total - indexed };
|
|
116
|
+
} catch (e) {
|
|
117
|
+
errors.push(`Embedding status check failed: ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const embedModelAvailable = isEmbedAvailable();
|
|
121
|
+
|
|
122
|
+
let autoCapturedFeedbackCount = 0;
|
|
123
|
+
try {
|
|
124
|
+
autoCapturedFeedbackCount = db
|
|
125
|
+
.prepare(`SELECT COUNT(*) as c FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%'`)
|
|
126
|
+
.get().c;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
errors.push(`Auto-captured feedback count failed: ${e.message}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let archivedCount = 0;
|
|
132
|
+
try {
|
|
133
|
+
archivedCount = countArchivedEntries(config.vaultDir);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
errors.push(`Archived count failed: ${e.message}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let staleKnowledge = [];
|
|
139
|
+
try {
|
|
140
|
+
const stalenessKinds = Object.entries(KIND_STALENESS_DAYS);
|
|
141
|
+
if (stalenessKinds.length > 0) {
|
|
142
|
+
const kindClauses = stalenessKinds
|
|
143
|
+
.map(
|
|
144
|
+
([kind, days]) =>
|
|
145
|
+
`(kind = '${kind}' AND COALESCE(updated_at, created_at) <= datetime('now', '-${days} days'))`,
|
|
146
|
+
)
|
|
147
|
+
.join(" OR ");
|
|
148
|
+
staleKnowledge = db
|
|
149
|
+
.prepare(
|
|
150
|
+
`SELECT kind, title, COALESCE(updated_at, created_at) as last_updated FROM vault WHERE category = 'knowledge' AND (${kindClauses}) AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY last_updated ASC LIMIT 10`,
|
|
151
|
+
)
|
|
152
|
+
.all();
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
errors.push(`Stale knowledge check failed: ${e.message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
fileCount,
|
|
160
|
+
subdirs,
|
|
161
|
+
kindCounts,
|
|
162
|
+
categoryCounts,
|
|
163
|
+
dbSize,
|
|
164
|
+
dbSizeBytes,
|
|
165
|
+
stalePaths,
|
|
166
|
+
staleCount,
|
|
167
|
+
expiredCount,
|
|
168
|
+
eventCount,
|
|
169
|
+
eventsWithoutTtlCount,
|
|
170
|
+
embeddingStatus,
|
|
171
|
+
embedModelAvailable,
|
|
172
|
+
autoCapturedFeedbackCount,
|
|
173
|
+
archivedCount,
|
|
174
|
+
staleKnowledge,
|
|
175
|
+
resolvedFrom: config.resolvedFrom,
|
|
176
|
+
errors,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function computeGrowthWarnings(status, thresholds) {
|
|
181
|
+
if (!thresholds)
|
|
182
|
+
return { warnings: [], hasCritical: false, hasWarnings: false, actions: [], kindBreakdown: [] };
|
|
183
|
+
|
|
184
|
+
const t = thresholds;
|
|
185
|
+
const warnings = [];
|
|
186
|
+
const actions = [];
|
|
187
|
+
|
|
188
|
+
const total = status.embeddingStatus?.total ?? 0;
|
|
189
|
+
const { eventCount = 0, eventsWithoutTtlCount = 0, expiredCount = 0, dbSizeBytes = 0 } = status;
|
|
190
|
+
|
|
191
|
+
let totalExceeded = false;
|
|
192
|
+
|
|
193
|
+
if (t.totalEntries?.critical != null && total >= t.totalEntries.critical) {
|
|
194
|
+
totalExceeded = true;
|
|
195
|
+
warnings.push({ level: "critical", message: `Total entries: ${total.toLocaleString()} (exceeds critical limit of ${t.totalEntries.critical.toLocaleString()})` });
|
|
196
|
+
} else if (t.totalEntries?.warn != null && total >= t.totalEntries.warn) {
|
|
197
|
+
totalExceeded = true;
|
|
198
|
+
warnings.push({ level: "warn", message: `Total entries: ${total.toLocaleString()} (exceeds recommended ${t.totalEntries.warn.toLocaleString()})` });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (t.eventEntries?.critical != null && eventCount >= t.eventEntries.critical) {
|
|
202
|
+
warnings.push({ level: "critical", message: `Event entries: ${eventCount.toLocaleString()} (exceeds critical limit of ${t.eventEntries.critical.toLocaleString()})` });
|
|
203
|
+
} else if (t.eventEntries?.warn != null && eventCount >= t.eventEntries.warn) {
|
|
204
|
+
const ttlNote = eventsWithoutTtlCount > 0 ? ` (${eventsWithoutTtlCount.toLocaleString()} without TTL)` : "";
|
|
205
|
+
warnings.push({ level: "warn", message: `Event entries: ${eventCount.toLocaleString()}${ttlNote} (exceeds recommended ${t.eventEntries.warn.toLocaleString()})` });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (t.vaultSizeBytes?.critical != null && dbSizeBytes >= t.vaultSizeBytes.critical) {
|
|
209
|
+
warnings.push({ level: "critical", message: `Database size: ${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB (exceeds critical limit of ${(t.vaultSizeBytes.critical / 1024 / 1024).toFixed(0)}MB)` });
|
|
210
|
+
} else if (t.vaultSizeBytes?.warn != null && dbSizeBytes >= t.vaultSizeBytes.warn) {
|
|
211
|
+
warnings.push({ level: "warn", message: `Database size: ${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB (exceeds recommended ${(t.vaultSizeBytes.warn / 1024 / 1024).toFixed(0)}MB)` });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (t.eventsWithoutTtl?.warn != null && eventsWithoutTtlCount >= t.eventsWithoutTtl.warn) {
|
|
215
|
+
warnings.push({ level: "warn", message: `Event entries without expires_at: ${eventsWithoutTtlCount.toLocaleString()} (exceeds recommended ${t.eventsWithoutTtl.warn.toLocaleString()})` });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const hasCritical = warnings.some((w) => w.level === "critical");
|
|
219
|
+
|
|
220
|
+
if (expiredCount > 0) {
|
|
221
|
+
actions.push(`Run \`context-vault prune\` to remove ${expiredCount} expired event entr${expiredCount === 1 ? "y" : "ies"}`);
|
|
222
|
+
}
|
|
223
|
+
if (eventsWithoutTtlCount > 0 && (eventCount >= (t.eventEntries?.warn ?? Infinity) || eventsWithoutTtlCount >= (t.eventsWithoutTtl?.warn ?? Infinity))) {
|
|
224
|
+
actions.push("Add `expires_at` to event/session entries to enable automatic cleanup");
|
|
225
|
+
}
|
|
226
|
+
if (total >= (t.totalEntries?.warn ?? Infinity)) {
|
|
227
|
+
actions.push("Run `context-vault archive` to move old ephemeral/event entries to _archive/");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const kindBreakdown = totalExceeded && status.kindCounts?.length
|
|
231
|
+
? [...status.kindCounts].sort((a, b) => b.c - a.c).map(({ kind, c }) => ({ kind, count: c, pct: Math.round((c / total) * 100) }))
|
|
232
|
+
: [];
|
|
233
|
+
|
|
234
|
+
return { warnings, hasCritical, hasWarnings: warnings.length > 0, actions, kindBreakdown };
|
|
235
|
+
}
|
|
@@ -1,23 +1,22 @@
|
|
|
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 "
|
|
3
|
+
import { API_URL, MARKETING_URL, GITHUB_ISSUES_URL } from "@context-vault/core/constants";
|
|
4
|
+
import type { VaultConfig } from "@context-vault/core/types";
|
|
4
5
|
|
|
5
6
|
const TELEMETRY_ENDPOINT = `${API_URL}/telemetry`;
|
|
6
7
|
const NOTICE_MARKER = ".telemetry-notice-shown";
|
|
7
8
|
const FEEDBACK_PROMPT_MARKER = ".feedback-prompt-shown";
|
|
8
9
|
|
|
9
|
-
export function isTelemetryEnabled(config) {
|
|
10
|
+
export function isTelemetryEnabled(config: VaultConfig | undefined): boolean {
|
|
10
11
|
const envVal = process.env.CONTEXT_VAULT_TELEMETRY;
|
|
11
12
|
if (envVal !== undefined) return envVal === "1" || envVal === "true";
|
|
12
13
|
return config?.telemetry === true;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
20
|
-
export function sendTelemetryEvent(config, payload) {
|
|
16
|
+
export function sendTelemetryEvent(
|
|
17
|
+
config: VaultConfig | undefined,
|
|
18
|
+
payload: { event: string; code?: string | null; tool?: string | null; cv_version: string },
|
|
19
|
+
): void {
|
|
21
20
|
if (!isTelemetryEnabled(config)) return;
|
|
22
21
|
|
|
23
22
|
const event = {
|
|
@@ -39,11 +38,7 @@ export function sendTelemetryEvent(config, payload) {
|
|
|
39
38
|
}).catch(() => {});
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
* Print the one-time telemetry notice to stderr.
|
|
44
|
-
* Uses a marker file in dataDir to ensure it's only shown once.
|
|
45
|
-
*/
|
|
46
|
-
export function maybeShowTelemetryNotice(dataDir) {
|
|
41
|
+
export function maybeShowTelemetryNotice(dataDir: string): void {
|
|
47
42
|
try {
|
|
48
43
|
const markerPath = join(dataDir, NOTICE_MARKER);
|
|
49
44
|
if (existsSync(markerPath)) return;
|
|
@@ -65,12 +60,7 @@ export function maybeShowTelemetryNotice(dataDir) {
|
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
62
|
|
|
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) {
|
|
63
|
+
export function maybeShowFeedbackPrompt(dataDir: string): void {
|
|
74
64
|
try {
|
|
75
65
|
const markerPath = join(dataDir, FEEDBACK_PROMPT_MARKER);
|
|
76
66
|
if (existsSync(markerPath)) return;
|