context-vault 2.3.0 → 2.4.1
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/LICENSE +21 -0
- package/bin/cli.js +39 -31
- package/node_modules/@context-vault/core/LICENSE +21 -0
- package/node_modules/@context-vault/core/package.json +2 -2
- package/node_modules/@context-vault/core/src/capture/index.js +3 -2
- package/node_modules/@context-vault/core/src/core/config.js +5 -0
- package/node_modules/@context-vault/core/src/core/status.js +32 -8
- package/node_modules/@context-vault/core/src/index/db.js +163 -57
- package/node_modules/@context-vault/core/src/index/embed.js +35 -14
- package/node_modules/@context-vault/core/src/index/index.js +51 -12
- package/node_modules/@context-vault/core/src/retrieve/index.js +52 -43
- package/node_modules/@context-vault/core/src/server/tools.js +105 -11
- package/package.json +11 -5
- package/scripts/postinstall.js +45 -0
- package/scripts/prepack.js +31 -0
- package/src/server/index.js +119 -77
- package/ui/Context.applescript +0 -36
- package/ui/index.html +0 -1377
- package/ui/serve.js +0 -474
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Hellstrom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
unlinkSync,
|
|
21
21
|
} from "node:fs";
|
|
22
22
|
import { join, resolve, dirname } from "node:path";
|
|
23
|
-
import { homedir } from "node:os";
|
|
23
|
+
import { homedir, platform } from "node:os";
|
|
24
24
|
import { execSync, fork } from "node:child_process";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
|
|
@@ -73,6 +73,36 @@ function prompt(question, defaultVal) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// ─── Platform Helpers ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const PLATFORM = platform();
|
|
79
|
+
|
|
80
|
+
/** Get the platform-specific application data directory */
|
|
81
|
+
function appDataDir() {
|
|
82
|
+
switch (PLATFORM) {
|
|
83
|
+
case "win32":
|
|
84
|
+
return process.env.APPDATA || join(HOME, "AppData", "Roaming");
|
|
85
|
+
case "darwin":
|
|
86
|
+
return join(HOME, "Library", "Application Support");
|
|
87
|
+
case "linux":
|
|
88
|
+
default:
|
|
89
|
+
return process.env.XDG_CONFIG_HOME || join(HOME, ".config");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Get the platform-specific VS Code extensions directory */
|
|
94
|
+
function vscodeDataDir() {
|
|
95
|
+
switch (PLATFORM) {
|
|
96
|
+
case "win32":
|
|
97
|
+
return join(appDataDir(), "Code", "User", "globalStorage");
|
|
98
|
+
case "darwin":
|
|
99
|
+
return join(appDataDir(), "Code", "User", "globalStorage");
|
|
100
|
+
case "linux":
|
|
101
|
+
default:
|
|
102
|
+
return join(HOME, ".config", "Code", "User", "globalStorage");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
// ─── Tool Detection ──────────────────────────────────────────────────────────
|
|
77
107
|
|
|
78
108
|
const TOOLS = [
|
|
@@ -81,7 +111,8 @@ const TOOLS = [
|
|
|
81
111
|
name: "Claude Code",
|
|
82
112
|
detect: () => {
|
|
83
113
|
try {
|
|
84
|
-
|
|
114
|
+
const cmd = PLATFORM === "win32" ? "where claude" : "which claude";
|
|
115
|
+
execSync(cmd, { stdio: "pipe" });
|
|
85
116
|
return true;
|
|
86
117
|
} catch {
|
|
87
118
|
return false;
|
|
@@ -92,16 +123,9 @@ const TOOLS = [
|
|
|
92
123
|
{
|
|
93
124
|
id: "claude-desktop",
|
|
94
125
|
name: "Claude Desktop",
|
|
95
|
-
detect: () =>
|
|
96
|
-
existsSync(join(HOME, "Library", "Application Support", "Claude")),
|
|
126
|
+
detect: () => existsSync(join(appDataDir(), "Claude")),
|
|
97
127
|
configType: "json",
|
|
98
|
-
configPath: join(
|
|
99
|
-
HOME,
|
|
100
|
-
"Library",
|
|
101
|
-
"Application Support",
|
|
102
|
-
"Claude",
|
|
103
|
-
"claude_desktop_config.json"
|
|
104
|
-
),
|
|
128
|
+
configPath: join(appDataDir(), "Claude", "claude_desktop_config.json"),
|
|
105
129
|
configKey: "mcpServers",
|
|
106
130
|
},
|
|
107
131
|
{
|
|
@@ -124,26 +148,10 @@ const TOOLS = [
|
|
|
124
148
|
id: "cline",
|
|
125
149
|
name: "Cline (VS Code)",
|
|
126
150
|
detect: () =>
|
|
127
|
-
existsSync(
|
|
128
|
-
join(
|
|
129
|
-
HOME,
|
|
130
|
-
"Library",
|
|
131
|
-
"Application Support",
|
|
132
|
-
"Code",
|
|
133
|
-
"User",
|
|
134
|
-
"globalStorage",
|
|
135
|
-
"saoudrizwan.claude-dev",
|
|
136
|
-
"settings"
|
|
137
|
-
)
|
|
138
|
-
),
|
|
151
|
+
existsSync(join(vscodeDataDir(), "saoudrizwan.claude-dev", "settings")),
|
|
139
152
|
configType: "json",
|
|
140
153
|
configPath: join(
|
|
141
|
-
|
|
142
|
-
"Library",
|
|
143
|
-
"Application Support",
|
|
144
|
-
"Code",
|
|
145
|
-
"User",
|
|
146
|
-
"globalStorage",
|
|
154
|
+
vscodeDataDir(),
|
|
147
155
|
"saoudrizwan.claude-dev",
|
|
148
156
|
"settings",
|
|
149
157
|
"cline_mcp_settings.json"
|
|
@@ -672,7 +680,7 @@ async function runReindex() {
|
|
|
672
680
|
process.exit(1);
|
|
673
681
|
}
|
|
674
682
|
|
|
675
|
-
const db = initDatabase(config.dbPath);
|
|
683
|
+
const db = await initDatabase(config.dbPath);
|
|
676
684
|
const stmts = prepareStatements(db);
|
|
677
685
|
const ctx = {
|
|
678
686
|
db,
|
|
@@ -701,7 +709,7 @@ async function runStatus() {
|
|
|
701
709
|
const { gatherVaultStatus } = await import("@context-vault/core/core/status");
|
|
702
710
|
|
|
703
711
|
const config = resolveConfig();
|
|
704
|
-
const db = initDatabase(config.dbPath);
|
|
712
|
+
const db = await initDatabase(config.dbPath);
|
|
705
713
|
|
|
706
714
|
const status = gatherVaultStatus({ db, config });
|
|
707
715
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Hellstrom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@context-vault/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"engines": { "node": ">=20" },
|
|
27
27
|
"author": "Felix Hellstrom",
|
|
28
|
-
"repository": { "type": "git", "url": "https://github.com/fellanH/context-mcp.git", "directory": "packages/core" },
|
|
28
|
+
"repository": { "type": "git", "url": "git+https://github.com/fellanH/context-mcp.git", "directory": "packages/core" },
|
|
29
29
|
"homepage": "https://github.com/fellanH/context-mcp",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@huggingface/transformers": "^3.0.0",
|
|
@@ -15,7 +15,7 @@ import { parseFrontmatter, formatFrontmatter } from "../core/frontmatter.js";
|
|
|
15
15
|
import { formatBody } from "./formatters.js";
|
|
16
16
|
import { writeEntryFile } from "./file-ops.js";
|
|
17
17
|
|
|
18
|
-
export function writeEntry(ctx, { kind, title, body, meta, tags, source, folder, identity_key, expires_at }) {
|
|
18
|
+
export function writeEntry(ctx, { kind, title, body, meta, tags, source, folder, identity_key, expires_at, userId }) {
|
|
19
19
|
if (!kind || typeof kind !== "string") {
|
|
20
20
|
throw new Error("writeEntry: kind is required (non-empty string)");
|
|
21
21
|
}
|
|
@@ -59,7 +59,7 @@ export function writeEntry(ctx, { kind, title, body, meta, tags, source, folder,
|
|
|
59
59
|
category, identity_key, expires_at,
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
return { id, filePath, kind, category, title, body, meta, tags, source, createdAt, identity_key, expires_at };
|
|
62
|
+
return { id, filePath, kind, category, title, body, meta, tags, source, createdAt, identity_key, expires_at, userId: userId || null };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
@@ -123,6 +123,7 @@ export function updateEntryFile(ctx, existing, updates) {
|
|
|
123
123
|
createdAt: fmMeta.created || existing.created_at,
|
|
124
124
|
identity_key: existing.identity_key,
|
|
125
125
|
expires_at,
|
|
126
|
+
userId: existing.user_id || null,
|
|
126
127
|
};
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -19,6 +19,7 @@ export function parseArgs(argv) {
|
|
|
19
19
|
else if (argv[i] === "--data-dir" && argv[i + 1]) args.dataDir = argv[++i];
|
|
20
20
|
else if (argv[i] === "--db-path" && argv[i + 1]) args.dbPath = argv[++i];
|
|
21
21
|
else if (argv[i] === "--dev-dir" && argv[i + 1]) args.devDir = argv[++i];
|
|
22
|
+
else if (argv[i] === "--event-decay-days" && argv[i + 1]) args.eventDecayDays = Number(argv[++i]);
|
|
22
23
|
}
|
|
23
24
|
return args;
|
|
24
25
|
}
|
|
@@ -34,6 +35,7 @@ export function resolveConfig() {
|
|
|
34
35
|
dataDir,
|
|
35
36
|
dbPath: join(dataDir, "vault.db"),
|
|
36
37
|
devDir: join(HOME, "dev"),
|
|
38
|
+
eventDecayDays: 30,
|
|
37
39
|
resolvedFrom: "defaults",
|
|
38
40
|
};
|
|
39
41
|
|
|
@@ -46,6 +48,7 @@ export function resolveConfig() {
|
|
|
46
48
|
if (fc.dataDir) { config.dataDir = fc.dataDir; config.dbPath = join(resolve(fc.dataDir), "vault.db"); }
|
|
47
49
|
if (fc.dbPath) config.dbPath = fc.dbPath;
|
|
48
50
|
if (fc.devDir) config.devDir = fc.devDir;
|
|
51
|
+
if (fc.eventDecayDays) config.eventDecayDays = fc.eventDecayDays;
|
|
49
52
|
config.resolvedFrom = "config file";
|
|
50
53
|
} catch (e) {
|
|
51
54
|
throw new Error(`[context-mcp] Invalid config at ${configPath}: ${e.message}`);
|
|
@@ -57,11 +60,13 @@ export function resolveConfig() {
|
|
|
57
60
|
if (process.env.CONTEXT_MCP_VAULT_DIR) { config.vaultDir = process.env.CONTEXT_MCP_VAULT_DIR; config.resolvedFrom = "env"; }
|
|
58
61
|
if (process.env.CONTEXT_MCP_DB_PATH) { config.dbPath = process.env.CONTEXT_MCP_DB_PATH; config.resolvedFrom = "env"; }
|
|
59
62
|
if (process.env.CONTEXT_MCP_DEV_DIR) { config.devDir = process.env.CONTEXT_MCP_DEV_DIR; config.resolvedFrom = "env"; }
|
|
63
|
+
if (process.env.CONTEXT_MCP_EVENT_DECAY_DAYS) { config.eventDecayDays = Number(process.env.CONTEXT_MCP_EVENT_DECAY_DAYS); config.resolvedFrom = "env"; }
|
|
60
64
|
|
|
61
65
|
// 4. CLI arg overrides (highest priority)
|
|
62
66
|
if (cliArgs.vaultDir) { config.vaultDir = cliArgs.vaultDir; config.resolvedFrom = "CLI args"; }
|
|
63
67
|
if (cliArgs.dbPath) { config.dbPath = cliArgs.dbPath; config.resolvedFrom = "CLI args"; }
|
|
64
68
|
if (cliArgs.devDir) { config.devDir = cliArgs.devDir; config.resolvedFrom = "CLI args"; }
|
|
69
|
+
if (cliArgs.eventDecayDays) { config.eventDecayDays = cliArgs.eventDecayDays; config.resolvedFrom = "CLI args"; }
|
|
65
70
|
|
|
66
71
|
// Resolve all paths to absolute
|
|
67
72
|
config.vaultDir = resolve(config.vaultDir);
|
|
@@ -5,17 +5,26 @@
|
|
|
5
5
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { walkDir } from "./files.js";
|
|
8
|
+
import { isEmbedAvailable } from "../index/embed.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Gather raw vault status data for formatting by consumers.
|
|
11
12
|
*
|
|
12
13
|
* @param {{ db, config }} ctx
|
|
14
|
+
* @param {{ userId?: string }} opts — optional userId for per-user stats
|
|
13
15
|
* @returns {{ fileCount, subdirs, kindCounts, dbSize, stalePaths, resolvedFrom, embeddingStatus, errors }}
|
|
14
16
|
*/
|
|
15
|
-
export function gatherVaultStatus(ctx) {
|
|
17
|
+
export function gatherVaultStatus(ctx, opts = {}) {
|
|
16
18
|
const { db, config } = ctx;
|
|
19
|
+
const { userId } = opts;
|
|
17
20
|
const errors = [];
|
|
18
21
|
|
|
22
|
+
// Build user filter clause for DB queries
|
|
23
|
+
const hasUser = userId !== undefined;
|
|
24
|
+
const userWhere = hasUser ? "WHERE user_id = ?" : "";
|
|
25
|
+
const userAnd = hasUser ? "AND user_id = ?" : "";
|
|
26
|
+
const userParams = hasUser ? [userId] : [];
|
|
27
|
+
|
|
19
28
|
// Count files in vault subdirs (auto-discover)
|
|
20
29
|
let fileCount = 0;
|
|
21
30
|
const subdirs = [];
|
|
@@ -37,7 +46,7 @@ export function gatherVaultStatus(ctx) {
|
|
|
37
46
|
// Count DB rows by kind
|
|
38
47
|
let kindCounts = [];
|
|
39
48
|
try {
|
|
40
|
-
kindCounts = db.prepare(
|
|
49
|
+
kindCounts = db.prepare(`SELECT kind, COUNT(*) as c FROM vault ${userWhere} GROUP BY kind`).all(...userParams);
|
|
41
50
|
} catch (e) {
|
|
42
51
|
errors.push(`Kind count query failed: ${e.message}`);
|
|
43
52
|
}
|
|
@@ -45,7 +54,7 @@ export function gatherVaultStatus(ctx) {
|
|
|
45
54
|
// Count DB rows by category
|
|
46
55
|
let categoryCounts = [];
|
|
47
56
|
try {
|
|
48
|
-
categoryCounts = db.prepare(
|
|
57
|
+
categoryCounts = db.prepare(`SELECT category, COUNT(*) as c FROM vault ${userWhere} GROUP BY category`).all(...userParams);
|
|
49
58
|
} catch (e) {
|
|
50
59
|
errors.push(`Category count query failed: ${e.message}`);
|
|
51
60
|
}
|
|
@@ -69,26 +78,39 @@ export function gatherVaultStatus(ctx) {
|
|
|
69
78
|
let staleCount = 0;
|
|
70
79
|
try {
|
|
71
80
|
const result = db.prepare(
|
|
72
|
-
|
|
73
|
-
).get(config.vaultDir);
|
|
81
|
+
`SELECT COUNT(*) as c FROM vault WHERE file_path NOT LIKE ? || '%' ${userAnd}`
|
|
82
|
+
).get(config.vaultDir, ...userParams);
|
|
74
83
|
staleCount = result.c;
|
|
75
84
|
stalePaths = staleCount > 0;
|
|
76
85
|
} catch (e) {
|
|
77
86
|
errors.push(`Stale path check failed: ${e.message}`);
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
// Count expired entries pending pruning
|
|
90
|
+
let expiredCount = 0;
|
|
91
|
+
try {
|
|
92
|
+
expiredCount = db.prepare(
|
|
93
|
+
`SELECT COUNT(*) as c FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now') ${userAnd}`
|
|
94
|
+
).get(...userParams).c;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
errors.push(`Expired count failed: ${e.message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
// Embedding/vector status
|
|
81
100
|
let embeddingStatus = null;
|
|
82
101
|
try {
|
|
83
|
-
const total = db.prepare(
|
|
102
|
+
const total = db.prepare(`SELECT COUNT(*) as c FROM vault ${userWhere}`).get(...userParams).c;
|
|
84
103
|
const indexed = db.prepare(
|
|
85
|
-
|
|
86
|
-
).get().c;
|
|
104
|
+
`SELECT COUNT(*) as c FROM vault WHERE rowid IN (SELECT rowid FROM vault_vec) ${userAnd}`
|
|
105
|
+
).get(...userParams).c;
|
|
87
106
|
embeddingStatus = { indexed, total, missing: total - indexed };
|
|
88
107
|
} catch (e) {
|
|
89
108
|
errors.push(`Embedding status check failed: ${e.message}`);
|
|
90
109
|
}
|
|
91
110
|
|
|
111
|
+
// Embedding model availability
|
|
112
|
+
const embedModelAvailable = isEmbedAvailable();
|
|
113
|
+
|
|
92
114
|
return {
|
|
93
115
|
fileCount,
|
|
94
116
|
subdirs,
|
|
@@ -98,7 +120,9 @@ export function gatherVaultStatus(ctx) {
|
|
|
98
120
|
dbSizeBytes,
|
|
99
121
|
stalePaths,
|
|
100
122
|
staleCount,
|
|
123
|
+
expiredCount,
|
|
101
124
|
embeddingStatus,
|
|
125
|
+
embedModelAvailable,
|
|
102
126
|
resolvedFrom: config.resolvedFrom,
|
|
103
127
|
errors,
|
|
104
128
|
};
|
|
@@ -2,32 +2,101 @@
|
|
|
2
2
|
* db.js — Database schema, initialization, and prepared statements
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import * as sqliteVec from "sqlite-vec";
|
|
7
|
-
import { unlinkSync } from "node:fs";
|
|
5
|
+
import { unlinkSync, copyFileSync, existsSync } from "node:fs";
|
|
8
6
|
|
|
9
|
-
// ───
|
|
7
|
+
// ─── Native Module Error ────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export class NativeModuleError extends Error {
|
|
10
|
+
constructor(originalError) {
|
|
11
|
+
const diagnostic = formatNativeModuleError(originalError);
|
|
12
|
+
super(diagnostic);
|
|
13
|
+
this.name = "NativeModuleError";
|
|
14
|
+
this.originalError = originalError;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatNativeModuleError(err) {
|
|
19
|
+
const msg = err.message || "";
|
|
20
|
+
const versionMatch = msg.match(
|
|
21
|
+
/was compiled against a different Node\.js version using\s+NODE_MODULE_VERSION (\d+)\. This version of Node\.js requires\s+NODE_MODULE_VERSION (\d+)/
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const lines = [
|
|
25
|
+
`Native module failed to load: ${msg}`,
|
|
26
|
+
"",
|
|
27
|
+
` Running Node.js: ${process.version} (${process.execPath})`,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (versionMatch) {
|
|
31
|
+
lines.push(` Module compiled for: NODE_MODULE_VERSION ${versionMatch[1]}`);
|
|
32
|
+
lines.push(` Current runtime: NODE_MODULE_VERSION ${versionMatch[2]}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
lines.push(
|
|
36
|
+
"",
|
|
37
|
+
" Fix: Rebuild native modules for your current Node.js:",
|
|
38
|
+
" npm rebuild better-sqlite3 sqlite-vec",
|
|
39
|
+
"",
|
|
40
|
+
" Or reinstall:",
|
|
41
|
+
" npm install -g context-vault",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Lazy Native Module Loading ─────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
let _Database = null;
|
|
50
|
+
let _sqliteVec = null;
|
|
51
|
+
|
|
52
|
+
async function loadNativeModules() {
|
|
53
|
+
if (_Database && _sqliteVec) return { Database: _Database, sqliteVec: _sqliteVec };
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const dbMod = await import("better-sqlite3");
|
|
57
|
+
_Database = dbMod.default;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
throw new NativeModuleError(e);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const vecMod = await import("sqlite-vec");
|
|
64
|
+
_sqliteVec = vecMod;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new NativeModuleError(e);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { Database: _Database, sqliteVec: _sqliteVec };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Schema DDL (v6 — multi-tenancy + encryption) ──────────────────────────
|
|
10
73
|
|
|
11
74
|
export const SCHEMA_DDL = `
|
|
12
75
|
CREATE TABLE IF NOT EXISTS vault (
|
|
13
|
-
id
|
|
14
|
-
kind
|
|
15
|
-
category
|
|
16
|
-
title
|
|
17
|
-
body
|
|
18
|
-
meta
|
|
19
|
-
tags
|
|
20
|
-
source
|
|
21
|
-
file_path
|
|
22
|
-
identity_key
|
|
23
|
-
expires_at
|
|
24
|
-
created_at
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
kind TEXT NOT NULL,
|
|
78
|
+
category TEXT NOT NULL DEFAULT 'knowledge',
|
|
79
|
+
title TEXT,
|
|
80
|
+
body TEXT NOT NULL,
|
|
81
|
+
meta TEXT,
|
|
82
|
+
tags TEXT,
|
|
83
|
+
source TEXT,
|
|
84
|
+
file_path TEXT UNIQUE,
|
|
85
|
+
identity_key TEXT,
|
|
86
|
+
expires_at TEXT,
|
|
87
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
88
|
+
user_id TEXT,
|
|
89
|
+
body_encrypted BLOB,
|
|
90
|
+
title_encrypted BLOB,
|
|
91
|
+
meta_encrypted BLOB,
|
|
92
|
+
iv BLOB
|
|
25
93
|
);
|
|
26
94
|
|
|
27
95
|
CREATE INDEX IF NOT EXISTS idx_vault_kind ON vault(kind);
|
|
28
96
|
CREATE INDEX IF NOT EXISTS idx_vault_category ON vault(category);
|
|
29
97
|
CREATE INDEX IF NOT EXISTS idx_vault_category_created ON vault(category, created_at DESC);
|
|
30
|
-
CREATE
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_vault_user ON vault(user_id);
|
|
99
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(user_id, kind, identity_key) WHERE identity_key IS NOT NULL;
|
|
31
100
|
|
|
32
101
|
-- Single FTS5 table
|
|
33
102
|
CREATE VIRTUAL TABLE IF NOT EXISTS vault_fts USING fts5(
|
|
@@ -57,34 +126,15 @@ export const SCHEMA_DDL = `
|
|
|
57
126
|
|
|
58
127
|
// ─── Database Init ───────────────────────────────────────────────────────────
|
|
59
128
|
|
|
60
|
-
export function initDatabase(dbPath) {
|
|
61
|
-
const
|
|
62
|
-
db.pragma("journal_mode = WAL");
|
|
63
|
-
db.pragma("foreign_keys = ON");
|
|
64
|
-
try {
|
|
65
|
-
sqliteVec.load(db);
|
|
66
|
-
} catch (e) {
|
|
67
|
-
console.error(`[context-mcp] Failed to load sqlite-vec native module.`);
|
|
68
|
-
console.error(`[context-mcp] This usually means prebuilt binaries aren't available for your platform.`);
|
|
69
|
-
console.error(`[context-mcp] Try: npm rebuild sqlite-vec`);
|
|
70
|
-
console.error(`[context-mcp] Error: ${e.message}`);
|
|
71
|
-
throw e;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const version = db.pragma("user_version", { simple: true });
|
|
129
|
+
export async function initDatabase(dbPath) {
|
|
130
|
+
const { Database, sqliteVec } = await loadNativeModules();
|
|
75
131
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
db.
|
|
80
|
-
unlinkSync(dbPath);
|
|
81
|
-
try { unlinkSync(dbPath + "-wal"); } catch {}
|
|
82
|
-
try { unlinkSync(dbPath + "-shm"); } catch {}
|
|
83
|
-
const freshDb = new Database(dbPath);
|
|
84
|
-
freshDb.pragma("journal_mode = WAL");
|
|
85
|
-
freshDb.pragma("foreign_keys = ON");
|
|
132
|
+
function createDb(path) {
|
|
133
|
+
const db = new Database(path);
|
|
134
|
+
db.pragma("journal_mode = WAL");
|
|
135
|
+
db.pragma("foreign_keys = ON");
|
|
86
136
|
try {
|
|
87
|
-
sqliteVec.load(
|
|
137
|
+
sqliteVec.load(db);
|
|
88
138
|
} catch (e) {
|
|
89
139
|
console.error(`[context-mcp] Failed to load sqlite-vec native module.`);
|
|
90
140
|
console.error(`[context-mcp] This usually means prebuilt binaries aren't available for your platform.`);
|
|
@@ -92,14 +142,61 @@ export function initDatabase(dbPath) {
|
|
|
92
142
|
console.error(`[context-mcp] Error: ${e.message}`);
|
|
93
143
|
throw e;
|
|
94
144
|
}
|
|
145
|
+
return db;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const db = createDb(dbPath);
|
|
149
|
+
const version = db.pragma("user_version", { simple: true });
|
|
150
|
+
|
|
151
|
+
// Enforce fresh-DB-only — old schemas get a full rebuild (with backup)
|
|
152
|
+
if (version > 0 && version < 5) {
|
|
153
|
+
console.error(`[context-mcp] Schema v${version} is outdated. Rebuilding database...`);
|
|
154
|
+
|
|
155
|
+
// Backup old DB before destroying it
|
|
156
|
+
const backupPath = `${dbPath}.v${version}.backup`;
|
|
157
|
+
try {
|
|
158
|
+
db.close();
|
|
159
|
+
if (existsSync(dbPath)) {
|
|
160
|
+
copyFileSync(dbPath, backupPath);
|
|
161
|
+
console.error(`[context-mcp] Backed up old database to: ${backupPath}`);
|
|
162
|
+
}
|
|
163
|
+
} catch (backupErr) {
|
|
164
|
+
console.error(`[context-mcp] Warning: could not backup old database: ${backupErr.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
unlinkSync(dbPath);
|
|
168
|
+
try { unlinkSync(dbPath + "-wal"); } catch {}
|
|
169
|
+
try { unlinkSync(dbPath + "-shm"); } catch {}
|
|
170
|
+
|
|
171
|
+
const freshDb = createDb(dbPath);
|
|
95
172
|
freshDb.exec(SCHEMA_DDL);
|
|
96
|
-
freshDb.pragma("user_version =
|
|
173
|
+
freshDb.pragma("user_version = 6");
|
|
97
174
|
return freshDb;
|
|
98
175
|
}
|
|
99
176
|
|
|
100
177
|
if (version < 5) {
|
|
101
178
|
db.exec(SCHEMA_DDL);
|
|
102
|
-
db.pragma("user_version =
|
|
179
|
+
db.pragma("user_version = 6");
|
|
180
|
+
} else if (version === 5) {
|
|
181
|
+
// v5 -> v6 migration: add multi-tenancy + encryption columns
|
|
182
|
+
// Wrapped in transaction with duplicate-column guards for idempotent retry
|
|
183
|
+
const migrate = db.transaction(() => {
|
|
184
|
+
const addColumnSafe = (sql) => {
|
|
185
|
+
try { db.exec(sql); } catch (e) {
|
|
186
|
+
if (!e.message.includes("duplicate column")) throw e;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
addColumnSafe(`ALTER TABLE vault ADD COLUMN user_id TEXT`);
|
|
190
|
+
addColumnSafe(`ALTER TABLE vault ADD COLUMN body_encrypted BLOB`);
|
|
191
|
+
addColumnSafe(`ALTER TABLE vault ADD COLUMN title_encrypted BLOB`);
|
|
192
|
+
addColumnSafe(`ALTER TABLE vault ADD COLUMN meta_encrypted BLOB`);
|
|
193
|
+
addColumnSafe(`ALTER TABLE vault ADD COLUMN iv BLOB`);
|
|
194
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_vault_user ON vault(user_id)`);
|
|
195
|
+
db.exec(`DROP INDEX IF EXISTS idx_vault_identity`);
|
|
196
|
+
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(user_id, kind, identity_key) WHERE identity_key IS NOT NULL`);
|
|
197
|
+
db.pragma("user_version = 6");
|
|
198
|
+
});
|
|
199
|
+
migrate();
|
|
103
200
|
}
|
|
104
201
|
|
|
105
202
|
return db;
|
|
@@ -108,18 +205,27 @@ export function initDatabase(dbPath) {
|
|
|
108
205
|
// ─── Prepared Statements Factory ─────────────────────────────────────────────
|
|
109
206
|
|
|
110
207
|
export function prepareStatements(db) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
208
|
+
try {
|
|
209
|
+
return {
|
|
210
|
+
insertEntry: db.prepare(`INSERT INTO vault (id, user_id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
211
|
+
insertEntryEncrypted: db.prepare(`INSERT INTO vault (id, user_id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, body_encrypted, title_encrypted, meta_encrypted, iv) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
212
|
+
updateEntry: db.prepare(`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, identity_key = ?, expires_at = ? WHERE file_path = ?`),
|
|
213
|
+
deleteEntry: db.prepare(`DELETE FROM vault WHERE id = ?`),
|
|
214
|
+
getRowid: db.prepare(`SELECT rowid FROM vault WHERE id = ?`),
|
|
215
|
+
getRowidByPath: db.prepare(`SELECT rowid FROM vault WHERE file_path = ?`),
|
|
216
|
+
getEntryById: db.prepare(`SELECT * FROM vault WHERE id = ?`),
|
|
217
|
+
getByIdentityKey: db.prepare(`SELECT * FROM vault WHERE kind = ? AND identity_key = ? AND user_id IS ?`),
|
|
218
|
+
upsertByIdentityKey: db.prepare(`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, file_path = ?, expires_at = ? WHERE kind = ? AND identity_key = ? AND user_id IS ?`),
|
|
219
|
+
insertVecStmt: db.prepare(`INSERT INTO vault_vec (rowid, embedding) VALUES (?, ?)`),
|
|
220
|
+
deleteVecStmt: db.prepare(`DELETE FROM vault_vec WHERE rowid = ?`),
|
|
221
|
+
};
|
|
222
|
+
} catch (e) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Failed to prepare database statements. The database may be corrupted.\n` +
|
|
225
|
+
`Try deleting and rebuilding: rm "${db.name}" && context-mcp reindex\n` +
|
|
226
|
+
`Original error: ${e.message}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
123
229
|
}
|
|
124
230
|
|
|
125
231
|
// ─── Vector Helpers (parameterized rowid via cached statements) ──────────────
|