context-vault 2.8.5 → 2.8.7
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 +15 -16
- package/bin/cli.js +28 -254
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/retrieve/index.js +2 -2
- package/node_modules/@context-vault/core/src/server/tools/save-context.js +10 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +12 -7
- package/scripts/local-server.js +0 -793
package/README.md
CHANGED
|
@@ -10,16 +10,13 @@ Persistent memory for AI agents — saves and searches knowledge across sessions
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
14
|
-
context-vault setup
|
|
13
|
+
npx context-vault setup
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
Setup
|
|
16
|
+
One command — no global install required. Setup detects your AI tools (Claude Code, Codex, Claude Desktop, Cursor, Windsurf, Cline, and more), downloads the embedding model (~22MB), seeds your vault, and configures MCP.
|
|
18
17
|
|
|
19
18
|
Then open your AI tool and try: **"Search my vault for getting started"**
|
|
20
19
|
|
|
21
|
-
> `context-mcp` still works as a CLI alias — `context-vault` is the primary command.
|
|
22
|
-
|
|
23
20
|
## What It Does
|
|
24
21
|
|
|
25
22
|
- **Save** — insights, decisions, patterns, contacts. Your AI agent writes them as you work.
|
|
@@ -43,17 +40,19 @@ Entries are organized by `kind` (insight, decision, pattern, reference, contact,
|
|
|
43
40
|
|
|
44
41
|
## CLI
|
|
45
42
|
|
|
46
|
-
| Command
|
|
47
|
-
|
|
|
48
|
-
| `context-vault setup`
|
|
49
|
-
| `context-vault
|
|
50
|
-
| `context-vault
|
|
51
|
-
| `context-vault
|
|
52
|
-
| `context-vault
|
|
53
|
-
| `context-vault
|
|
54
|
-
| `context-vault
|
|
55
|
-
| `context-vault
|
|
56
|
-
| `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 reindex` | Rebuild search index |
|
|
51
|
+
| `context-vault import <path>` | Import .md, .csv, .json, .txt |
|
|
52
|
+
| `context-vault export` | Export to JSON or CSV |
|
|
53
|
+
| `context-vault ingest <url>` | Fetch URL and save as vault entry |
|
|
54
|
+
| `context-vault update` | Check for updates |
|
|
55
|
+
| `context-vault uninstall` | Remove MCP configs |
|
|
57
56
|
|
|
58
57
|
## Manual MCP Config
|
|
59
58
|
|
package/bin/cli.js
CHANGED
|
@@ -21,9 +21,8 @@ import {
|
|
|
21
21
|
} from "node:fs";
|
|
22
22
|
import { join, resolve, dirname } from "node:path";
|
|
23
23
|
import { homedir, platform } from "node:os";
|
|
24
|
-
import { execSync, execFile
|
|
24
|
+
import { execSync, execFile } from "node:child_process";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { createServer as createNetServer } from "node:net";
|
|
27
26
|
|
|
28
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
28
|
const __dirname = dirname(__filename);
|
|
@@ -39,6 +38,11 @@ function isInstalledPackage() {
|
|
|
39
38
|
return ROOT.includes("/node_modules/") || ROOT.includes("\\node_modules\\");
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
/** Detect if running via npx (ephemeral cache — paths won't survive cache eviction) */
|
|
42
|
+
function isNpx() {
|
|
43
|
+
return ROOT.includes("/_npx/") || ROOT.includes("\\_npx\\");
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
43
47
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
44
48
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
@@ -224,7 +228,6 @@ ${bold("Commands:")}
|
|
|
224
228
|
${cyan("connect")} --key cv_... Connect AI tools to hosted vault
|
|
225
229
|
${cyan("switch")} local|hosted Switch between local and hosted MCP modes
|
|
226
230
|
${cyan("serve")} Start the MCP server (used by AI clients)
|
|
227
|
-
${cyan("ui")} [--port 3141] Launch web dashboard
|
|
228
231
|
${cyan("reindex")} Rebuild search index from knowledge files
|
|
229
232
|
${cyan("status")} Show vault diagnostics
|
|
230
233
|
${cyan("update")} Check for and install updates
|
|
@@ -232,8 +235,6 @@ ${bold("Commands:")}
|
|
|
232
235
|
${cyan("import")} <path> Import entries from file or directory
|
|
233
236
|
${cyan("export")} Export vault to JSON or CSV
|
|
234
237
|
${cyan("ingest")} <url> Fetch URL and save as vault entry
|
|
235
|
-
${cyan("link")} --key cv_... Link local vault to hosted account
|
|
236
|
-
${cyan("sync")} Sync entries between local and hosted
|
|
237
238
|
${cyan("migrate")} Migrate vault between local and hosted
|
|
238
239
|
|
|
239
240
|
${bold("Options:")}
|
|
@@ -559,17 +560,6 @@ async function runSetup() {
|
|
|
559
560
|
);
|
|
560
561
|
}
|
|
561
562
|
|
|
562
|
-
// Offer to launch UI
|
|
563
|
-
console.log();
|
|
564
|
-
if (!isNonInteractive) {
|
|
565
|
-
const launchUi = await prompt(` Launch web dashboard? (y/N):`, "N");
|
|
566
|
-
if (launchUi.toLowerCase() === "y") {
|
|
567
|
-
console.log();
|
|
568
|
-
runUi();
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
563
|
// Health check
|
|
574
564
|
console.log(`\n ${dim("[5/5]")}${bold(" Health check...")}\n`);
|
|
575
565
|
const okResults = results.filter((r) => r.ok);
|
|
@@ -608,7 +598,6 @@ async function runSetup() {
|
|
|
608
598
|
``,
|
|
609
599
|
` ${bold("CLI Commands:")}`,
|
|
610
600
|
` context-vault status Show vault health`,
|
|
611
|
-
` context-vault ui Launch web dashboard`,
|
|
612
601
|
` context-vault update Check for updates`,
|
|
613
602
|
];
|
|
614
603
|
const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
|
|
@@ -636,7 +625,14 @@ async function configureClaude(tool, vaultDir) {
|
|
|
636
625
|
} catch {}
|
|
637
626
|
|
|
638
627
|
try {
|
|
639
|
-
if (
|
|
628
|
+
if (isNpx()) {
|
|
629
|
+
const cmdArgs = ["-y", "context-vault", "serve"];
|
|
630
|
+
if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
|
|
631
|
+
execSync(
|
|
632
|
+
`claude mcp add -s user context-vault -- npx ${cmdArgs.join(" ")}`,
|
|
633
|
+
{ stdio: "pipe", env },
|
|
634
|
+
);
|
|
635
|
+
} else if (isInstalledPackage()) {
|
|
640
636
|
const launcherPath = join(HOME, ".context-mcp", "server.mjs");
|
|
641
637
|
const cmdArgs = [`"${launcherPath}"`];
|
|
642
638
|
if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
|
|
@@ -669,7 +665,13 @@ async function configureCodex(tool, vaultDir) {
|
|
|
669
665
|
} catch {}
|
|
670
666
|
|
|
671
667
|
try {
|
|
672
|
-
if (
|
|
668
|
+
if (isNpx()) {
|
|
669
|
+
const cmdArgs = ["-y", "context-vault", "serve"];
|
|
670
|
+
if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
|
|
671
|
+
execSync(`codex mcp add context-vault -- npx ${cmdArgs.join(" ")}`, {
|
|
672
|
+
stdio: "pipe",
|
|
673
|
+
});
|
|
674
|
+
} else if (isInstalledPackage()) {
|
|
673
675
|
const launcherPath = join(HOME, ".context-mcp", "server.mjs");
|
|
674
676
|
const cmdArgs = [`"${launcherPath}"`];
|
|
675
677
|
if (vaultDir) cmdArgs.push("--vault-dir", `"${vaultDir}"`);
|
|
@@ -717,7 +719,13 @@ function configureJsonTool(tool, vaultDir) {
|
|
|
717
719
|
// Clean up old "context-mcp" key
|
|
718
720
|
delete config[tool.configKey]["context-mcp"];
|
|
719
721
|
|
|
720
|
-
if (
|
|
722
|
+
if (isNpx()) {
|
|
723
|
+
const serverArgs = vaultDir ? ["--vault-dir", vaultDir] : [];
|
|
724
|
+
config[tool.configKey]["context-vault"] = {
|
|
725
|
+
command: "npx",
|
|
726
|
+
args: ["-y", "context-vault", "serve", ...serverArgs],
|
|
727
|
+
};
|
|
728
|
+
} else if (isInstalledPackage()) {
|
|
721
729
|
const launcherPath = join(HOME, ".context-mcp", "server.mjs");
|
|
722
730
|
const serverArgs = [];
|
|
723
731
|
if (vaultDir) serverArgs.push("--vault-dir", vaultDir);
|
|
@@ -1179,52 +1187,6 @@ async function runSwitch() {
|
|
|
1179
1187
|
}
|
|
1180
1188
|
}
|
|
1181
1189
|
|
|
1182
|
-
function runUi() {
|
|
1183
|
-
const port = parseInt(getFlag("--port") || "3141", 10);
|
|
1184
|
-
const localServer = join(ROOT, "scripts", "local-server.js");
|
|
1185
|
-
if (!existsSync(localServer)) {
|
|
1186
|
-
console.error(red("Local server not found."));
|
|
1187
|
-
process.exit(1);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Probe the port before forking
|
|
1191
|
-
const probe = createNetServer();
|
|
1192
|
-
probe.once("error", (e) => {
|
|
1193
|
-
if (e.code === "EADDRINUSE") {
|
|
1194
|
-
console.error(red(` Port ${port} is already in use.`));
|
|
1195
|
-
console.error(` Try: ${cyan(`context-vault ui --port ${port + 1}`)}`);
|
|
1196
|
-
process.exit(1);
|
|
1197
|
-
}
|
|
1198
|
-
// Other error — let the fork handle it
|
|
1199
|
-
probe.close();
|
|
1200
|
-
launchServer(port, localServer);
|
|
1201
|
-
});
|
|
1202
|
-
probe.listen(port, () => {
|
|
1203
|
-
probe.close(() => {
|
|
1204
|
-
launchServer(port, localServer);
|
|
1205
|
-
});
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function launchServer(port, localServer) {
|
|
1210
|
-
const child = fork(localServer, [`--port=${port}`], { stdio: "inherit" });
|
|
1211
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
1212
|
-
|
|
1213
|
-
setTimeout(() => {
|
|
1214
|
-
try {
|
|
1215
|
-
const url = `https://app.context-vault.com?local=${port}`;
|
|
1216
|
-
console.log(`Opening ${url}`);
|
|
1217
|
-
const open =
|
|
1218
|
-
PLATFORM === "darwin"
|
|
1219
|
-
? "open"
|
|
1220
|
-
: PLATFORM === "win32"
|
|
1221
|
-
? "start"
|
|
1222
|
-
: "xdg-open";
|
|
1223
|
-
execSync(`${open} ${url}`, { stdio: "ignore" });
|
|
1224
|
-
} catch {}
|
|
1225
|
-
}, 1500);
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
1190
|
async function runReindex() {
|
|
1229
1191
|
console.log(dim("Loading vault..."));
|
|
1230
1192
|
|
|
@@ -1845,185 +1807,6 @@ async function runIngest() {
|
|
|
1845
1807
|
console.log();
|
|
1846
1808
|
}
|
|
1847
1809
|
|
|
1848
|
-
async function runLink() {
|
|
1849
|
-
const apiKey = getFlag("--key");
|
|
1850
|
-
const hostedUrl = getFlag("--url") || "https://api.context-vault.com";
|
|
1851
|
-
|
|
1852
|
-
if (!apiKey) {
|
|
1853
|
-
console.log(`\n ${bold("context-vault link")} --key cv_...\n`);
|
|
1854
|
-
console.log(` Link your local vault to a hosted Context Vault account.\n`);
|
|
1855
|
-
console.log(` Options:`);
|
|
1856
|
-
console.log(` --key <key> API key (required)`);
|
|
1857
|
-
console.log(
|
|
1858
|
-
` --url <url> Hosted server URL (default: https://api.context-vault.com)`,
|
|
1859
|
-
);
|
|
1860
|
-
console.log();
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
console.log(dim(" Verifying API key..."));
|
|
1865
|
-
|
|
1866
|
-
let user;
|
|
1867
|
-
try {
|
|
1868
|
-
const response = await fetch(`${hostedUrl}/api/me`, {
|
|
1869
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1870
|
-
});
|
|
1871
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1872
|
-
user = await response.json();
|
|
1873
|
-
} catch (e) {
|
|
1874
|
-
console.error(red(` Verification failed: ${e.message}`));
|
|
1875
|
-
console.error(dim(` Check your API key and server URL.`));
|
|
1876
|
-
process.exit(1);
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
// Store credentials in config
|
|
1880
|
-
const dataDir = join(HOME, ".context-mcp");
|
|
1881
|
-
const configPath = join(dataDir, "config.json");
|
|
1882
|
-
let config = {};
|
|
1883
|
-
if (existsSync(configPath)) {
|
|
1884
|
-
try {
|
|
1885
|
-
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1886
|
-
} catch {}
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
config.hostedUrl = hostedUrl;
|
|
1890
|
-
config.apiKey = apiKey;
|
|
1891
|
-
config.userId = user.userId || user.id;
|
|
1892
|
-
config.email = user.email;
|
|
1893
|
-
config.linkedAt = new Date().toISOString();
|
|
1894
|
-
|
|
1895
|
-
mkdirSync(dataDir, { recursive: true });
|
|
1896
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1897
|
-
|
|
1898
|
-
console.log();
|
|
1899
|
-
console.log(green(` ✓ Linked to ${user.email}`));
|
|
1900
|
-
console.log(dim(` Tier: ${user.tier || "free"}`));
|
|
1901
|
-
console.log(dim(` Server: ${hostedUrl}`));
|
|
1902
|
-
console.log(dim(` Config: ${configPath}`));
|
|
1903
|
-
console.log();
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
async function runSync() {
|
|
1907
|
-
const dryRun = flags.has("--dry-run");
|
|
1908
|
-
const pushOnly = flags.has("--push-only");
|
|
1909
|
-
const pullOnly = flags.has("--pull-only");
|
|
1910
|
-
|
|
1911
|
-
// Read credentials
|
|
1912
|
-
const dataDir = join(HOME, ".context-mcp");
|
|
1913
|
-
const configPath = join(dataDir, "config.json");
|
|
1914
|
-
let storedConfig = {};
|
|
1915
|
-
if (existsSync(configPath)) {
|
|
1916
|
-
try {
|
|
1917
|
-
storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1918
|
-
} catch {}
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
const apiKey = getFlag("--key") || storedConfig.apiKey;
|
|
1922
|
-
const hostedUrl =
|
|
1923
|
-
getFlag("--url") ||
|
|
1924
|
-
storedConfig.hostedUrl ||
|
|
1925
|
-
"https://api.context-vault.com";
|
|
1926
|
-
|
|
1927
|
-
if (!apiKey) {
|
|
1928
|
-
console.error(
|
|
1929
|
-
red(" Not linked. Run `context-vault link --key cv_...` first."),
|
|
1930
|
-
);
|
|
1931
|
-
process.exit(1);
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
const { resolveConfig } = await import("@context-vault/core/core/config");
|
|
1935
|
-
const { initDatabase, prepareStatements, insertVec, deleteVec } =
|
|
1936
|
-
await import("@context-vault/core/index/db");
|
|
1937
|
-
const { embed } = await import("@context-vault/core/index/embed");
|
|
1938
|
-
const {
|
|
1939
|
-
buildLocalManifest,
|
|
1940
|
-
fetchRemoteManifest,
|
|
1941
|
-
computeSyncPlan,
|
|
1942
|
-
executeSync,
|
|
1943
|
-
} = await import("@context-vault/core/sync");
|
|
1944
|
-
|
|
1945
|
-
const config = resolveConfig();
|
|
1946
|
-
if (!config.vaultDirExists) {
|
|
1947
|
-
console.error(red(` Vault directory not found: ${config.vaultDir}`));
|
|
1948
|
-
process.exit(1);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
const db = await initDatabase(config.dbPath);
|
|
1952
|
-
const stmts = prepareStatements(db);
|
|
1953
|
-
const ctx = {
|
|
1954
|
-
db,
|
|
1955
|
-
config,
|
|
1956
|
-
stmts,
|
|
1957
|
-
embed,
|
|
1958
|
-
insertVec: (r, e) => insertVec(stmts, r, e),
|
|
1959
|
-
deleteVec: (r) => deleteVec(stmts, r),
|
|
1960
|
-
};
|
|
1961
|
-
|
|
1962
|
-
console.log(dim(" Building manifests..."));
|
|
1963
|
-
const local = buildLocalManifest(ctx);
|
|
1964
|
-
|
|
1965
|
-
let remote;
|
|
1966
|
-
try {
|
|
1967
|
-
remote = await fetchRemoteManifest(hostedUrl, apiKey);
|
|
1968
|
-
} catch (e) {
|
|
1969
|
-
db.close();
|
|
1970
|
-
console.error(red(` Failed to fetch remote manifest: ${e.message}`));
|
|
1971
|
-
process.exit(1);
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
const plan = computeSyncPlan(local, remote);
|
|
1975
|
-
|
|
1976
|
-
// Apply push-only / pull-only filters
|
|
1977
|
-
if (pushOnly) plan.toPull = [];
|
|
1978
|
-
if (pullOnly) plan.toPush = [];
|
|
1979
|
-
|
|
1980
|
-
console.log();
|
|
1981
|
-
console.log(` ${bold("Sync Plan")}`);
|
|
1982
|
-
console.log(` Push (local → remote): ${plan.toPush.length} entries`);
|
|
1983
|
-
console.log(` Pull (remote → local): ${plan.toPull.length} entries`);
|
|
1984
|
-
console.log(` Up to date: ${plan.upToDate.length} entries`);
|
|
1985
|
-
|
|
1986
|
-
if (plan.toPush.length === 0 && plan.toPull.length === 0) {
|
|
1987
|
-
db.close();
|
|
1988
|
-
console.log(green("\n ✓ Everything in sync."));
|
|
1989
|
-
console.log();
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
if (dryRun) {
|
|
1994
|
-
db.close();
|
|
1995
|
-
console.log(dim("\n Dry run — no changes were made."));
|
|
1996
|
-
console.log();
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
console.log(dim("\n Syncing..."));
|
|
2001
|
-
|
|
2002
|
-
const result = await executeSync(ctx, {
|
|
2003
|
-
hostedUrl,
|
|
2004
|
-
apiKey,
|
|
2005
|
-
plan,
|
|
2006
|
-
onProgress: (phase, current, total) => {
|
|
2007
|
-
process.stdout.write(
|
|
2008
|
-
`\r ${phase === "push" ? "Pushing" : "Pulling"}... ${current}/${total}`,
|
|
2009
|
-
);
|
|
2010
|
-
},
|
|
2011
|
-
});
|
|
2012
|
-
|
|
2013
|
-
db.close();
|
|
2014
|
-
|
|
2015
|
-
console.log(`\r ${green("✓")} Sync complete `);
|
|
2016
|
-
console.log(` ${green("↑")} ${result.pushed} pushed`);
|
|
2017
|
-
console.log(` ${green("↓")} ${result.pulled} pulled`);
|
|
2018
|
-
if (result.failed > 0) {
|
|
2019
|
-
console.log(` ${red("x")} ${result.failed} failed`);
|
|
2020
|
-
for (const err of result.errors.slice(0, 5)) {
|
|
2021
|
-
console.log(` ${dim(err)}`);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
console.log();
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
1810
|
async function runServe() {
|
|
2028
1811
|
await import("../src/server/index.js");
|
|
2029
1812
|
}
|
|
@@ -2052,9 +1835,6 @@ async function main() {
|
|
|
2052
1835
|
case "serve":
|
|
2053
1836
|
await runServe();
|
|
2054
1837
|
break;
|
|
2055
|
-
case "ui":
|
|
2056
|
-
runUi();
|
|
2057
|
-
break;
|
|
2058
1838
|
case "import":
|
|
2059
1839
|
await runImport();
|
|
2060
1840
|
break;
|
|
@@ -2064,12 +1844,6 @@ async function main() {
|
|
|
2064
1844
|
case "ingest":
|
|
2065
1845
|
await runIngest();
|
|
2066
1846
|
break;
|
|
2067
|
-
case "link":
|
|
2068
|
-
await runLink();
|
|
2069
|
-
break;
|
|
2070
|
-
case "sync":
|
|
2071
|
-
await runSync();
|
|
2072
|
-
break;
|
|
2073
1847
|
case "reindex":
|
|
2074
1848
|
await runReindex();
|
|
2075
1849
|
break;
|
|
@@ -16,8 +16,8 @@ const VEC_WEIGHT = 0.6;
|
|
|
16
16
|
*/
|
|
17
17
|
export function buildFtsQuery(query) {
|
|
18
18
|
const words = query
|
|
19
|
-
.split(
|
|
20
|
-
.map((w) => w.replace(/[*"()
|
|
19
|
+
.split(/[\s-]+/)
|
|
20
|
+
.map((w) => w.replace(/[*"():^~{}]/g, ""))
|
|
21
21
|
.filter((w) => w.length > 0);
|
|
22
22
|
if (!words.length) return null;
|
|
23
23
|
return words.map((w) => `"${w}"`).join(" AND ");
|
|
@@ -26,6 +26,7 @@ function validateSaveInput({
|
|
|
26
26
|
meta,
|
|
27
27
|
source,
|
|
28
28
|
identity_key,
|
|
29
|
+
expires_at,
|
|
29
30
|
}) {
|
|
30
31
|
if (kind !== undefined && kind !== null) {
|
|
31
32
|
if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
|
|
@@ -93,6 +94,14 @@ function validateSaveInput({
|
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
if (expires_at !== undefined && expires_at !== null) {
|
|
98
|
+
if (
|
|
99
|
+
typeof expires_at !== "string" ||
|
|
100
|
+
isNaN(new Date(expires_at).getTime())
|
|
101
|
+
) {
|
|
102
|
+
return err("expires_at must be a valid ISO date string", "INVALID_INPUT");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
96
105
|
return null;
|
|
97
106
|
}
|
|
98
107
|
|
|
@@ -178,6 +187,7 @@ export async function handler(
|
|
|
178
187
|
meta,
|
|
179
188
|
source,
|
|
180
189
|
identity_key,
|
|
190
|
+
expires_at,
|
|
181
191
|
});
|
|
182
192
|
if (inputErr) return inputErr;
|
|
183
193
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.7",
|
|
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.8.
|
|
58
|
+
"@context-vault/core": "^2.8.7",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
60
|
"better-sqlite3": "^12.6.2",
|
|
61
61
|
"sqlite-vec": "^0.1.0"
|
package/scripts/postinstall.js
CHANGED
|
@@ -90,13 +90,18 @@ async function main() {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// ── 3. Write local server launcher
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
// ── 3. Write local server launcher (global installs only) ────────────
|
|
94
|
+
// Under npx the path would be stale after cache eviction — configs use
|
|
95
|
+
// `npx context-vault serve` instead, so skip writing the launcher.
|
|
96
|
+
const isNpx = PKG_ROOT.includes("/_npx/") || PKG_ROOT.includes("\\_npx\\");
|
|
97
|
+
if (!isNpx) {
|
|
98
|
+
const SERVER_ABS = join(PKG_ROOT, "src", "server", "index.js");
|
|
99
|
+
const DATA_DIR = join(homedir(), ".context-mcp");
|
|
100
|
+
const LAUNCHER = join(DATA_DIR, "server.mjs");
|
|
101
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
102
|
+
writeFileSync(LAUNCHER, `import "${SERVER_ABS}";\n`);
|
|
103
|
+
console.log("[context-vault] Local server launcher written to " + LAUNCHER);
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
main().catch(() => {});
|
package/scripts/local-server.js
DELETED
|
@@ -1,793 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* local-server.js — Local mode: serves app + vault API with no auth.
|
|
4
|
-
*
|
|
5
|
-
* Uses local SQLite vault. No authentication required.
|
|
6
|
-
* Usage: node local-server.js [--port 3141]
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { createServer } from "node:http";
|
|
10
|
-
import {
|
|
11
|
-
createReadStream,
|
|
12
|
-
existsSync,
|
|
13
|
-
statSync,
|
|
14
|
-
unlinkSync,
|
|
15
|
-
readFileSync,
|
|
16
|
-
writeFileSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
} from "node:fs";
|
|
19
|
-
import { join, resolve, dirname, extname } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
import { homedir, platform } from "node:os";
|
|
22
|
-
import { execSync } from "node:child_process";
|
|
23
|
-
import { resolveConfig } from "@context-vault/core/core/config";
|
|
24
|
-
import {
|
|
25
|
-
initDatabase,
|
|
26
|
-
prepareStatements,
|
|
27
|
-
insertVec,
|
|
28
|
-
deleteVec,
|
|
29
|
-
} from "@context-vault/core/index/db";
|
|
30
|
-
import { embed } from "@context-vault/core/index/embed";
|
|
31
|
-
import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
|
|
32
|
-
import { indexEntry } from "@context-vault/core/index";
|
|
33
|
-
import { hybridSearch } from "@context-vault/core/retrieve";
|
|
34
|
-
import { gatherVaultStatus } from "@context-vault/core/core/status";
|
|
35
|
-
import { normalizeKind } from "@context-vault/core/core/files";
|
|
36
|
-
import { categoryFor } from "@context-vault/core/core/categories";
|
|
37
|
-
import { parseFile } from "@context-vault/core/capture/importers";
|
|
38
|
-
import { importEntries } from "@context-vault/core/capture/import-pipeline";
|
|
39
|
-
import { ingestUrl } from "@context-vault/core/capture/ingest-url";
|
|
40
|
-
import {
|
|
41
|
-
buildLocalManifest,
|
|
42
|
-
fetchRemoteManifest,
|
|
43
|
-
computeSyncPlan,
|
|
44
|
-
executeSync,
|
|
45
|
-
} from "@context-vault/core/sync";
|
|
46
|
-
|
|
47
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
-
const LOCAL_ROOT = resolve(__dirname, "..");
|
|
49
|
-
const APP_DIST = resolve(LOCAL_ROOT, "app-dist");
|
|
50
|
-
|
|
51
|
-
const MIME = {
|
|
52
|
-
".html": "text/html",
|
|
53
|
-
".js": "application/javascript",
|
|
54
|
-
".css": "text/css",
|
|
55
|
-
".json": "application/json",
|
|
56
|
-
".ico": "image/x-icon",
|
|
57
|
-
".svg": "image/svg+xml",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
function formatEntry(row) {
|
|
61
|
-
return {
|
|
62
|
-
id: row.id,
|
|
63
|
-
kind: row.kind,
|
|
64
|
-
category: row.category,
|
|
65
|
-
title: row.title || null,
|
|
66
|
-
body: row.body || null,
|
|
67
|
-
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
68
|
-
meta: row.meta
|
|
69
|
-
? typeof row.meta === "string"
|
|
70
|
-
? JSON.parse(row.meta)
|
|
71
|
-
: row.meta
|
|
72
|
-
: {},
|
|
73
|
-
source: row.source || null,
|
|
74
|
-
identity_key: row.identity_key || null,
|
|
75
|
-
expires_at: row.expires_at || null,
|
|
76
|
-
created_at: row.created_at,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function validateEntry(data, { requireKind = true, requireBody = true } = {}) {
|
|
81
|
-
if (requireKind && !data.kind)
|
|
82
|
-
return { error: "kind is required", status: 400 };
|
|
83
|
-
if (data.kind && !/^[a-z0-9-]+$/.test(data.kind))
|
|
84
|
-
return {
|
|
85
|
-
error: "kind must be lowercase alphanumeric/hyphens",
|
|
86
|
-
status: 400,
|
|
87
|
-
};
|
|
88
|
-
if (requireBody && !data.body)
|
|
89
|
-
return { error: "body is required", status: 400 };
|
|
90
|
-
if (data.body && data.body.length > 100 * 1024)
|
|
91
|
-
return { error: "body max 100KB", status: 400 };
|
|
92
|
-
if (categoryFor(data.kind) === "entity" && !data.identity_key)
|
|
93
|
-
return {
|
|
94
|
-
error: `Entity kind "${data.kind}" requires identity_key`,
|
|
95
|
-
status: 400,
|
|
96
|
-
};
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getAllowedOrigin(req) {
|
|
101
|
-
const origin = req.headers["origin"];
|
|
102
|
-
if (!origin) return null;
|
|
103
|
-
const lower = origin.toLowerCase();
|
|
104
|
-
if (
|
|
105
|
-
lower === "https://context-vault.com" ||
|
|
106
|
-
lower === "https://www.context-vault.com"
|
|
107
|
-
)
|
|
108
|
-
return origin;
|
|
109
|
-
if (/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(lower)) return origin;
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function main() {
|
|
114
|
-
const portArg = process.argv.find((a) => a.startsWith("--port="));
|
|
115
|
-
const portVal = portArg
|
|
116
|
-
? portArg.split("=")[1]
|
|
117
|
-
: process.argv[process.argv.indexOf("--port") + 1];
|
|
118
|
-
const port = parseInt(portVal || "3141", 10);
|
|
119
|
-
|
|
120
|
-
const config = resolveConfig();
|
|
121
|
-
config.vaultDirExists = existsSync(config.vaultDir);
|
|
122
|
-
let db = await initDatabase(config.dbPath);
|
|
123
|
-
let stmts = prepareStatements(db);
|
|
124
|
-
|
|
125
|
-
const state = {
|
|
126
|
-
db,
|
|
127
|
-
config,
|
|
128
|
-
stmts,
|
|
129
|
-
embed,
|
|
130
|
-
get ctx() {
|
|
131
|
-
return {
|
|
132
|
-
db: state.db,
|
|
133
|
-
config: state.config,
|
|
134
|
-
stmts: state.stmts,
|
|
135
|
-
embed: state.embed,
|
|
136
|
-
insertVec: (r, e) => insertVec(state.stmts, r, e),
|
|
137
|
-
deleteVec: (r) => deleteVec(state.stmts, r),
|
|
138
|
-
userId: null,
|
|
139
|
-
};
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const server = createServer(async (req, res) => {
|
|
144
|
-
const url = req.url?.replace(/\?.*$/, "") || "/";
|
|
145
|
-
|
|
146
|
-
const allowedOrigin = getAllowedOrigin(req);
|
|
147
|
-
const corsHeaders = allowedOrigin
|
|
148
|
-
? {
|
|
149
|
-
"Access-Control-Allow-Origin": allowedOrigin,
|
|
150
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
151
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
152
|
-
Vary: "Origin",
|
|
153
|
-
}
|
|
154
|
-
: { Vary: "Origin" };
|
|
155
|
-
|
|
156
|
-
if (req.method === "OPTIONS") {
|
|
157
|
-
res.writeHead(204, corsHeaders);
|
|
158
|
-
res.end();
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const json = (data, status = 200) => {
|
|
163
|
-
res.writeHead(status, {
|
|
164
|
-
"Content-Type": "application/json",
|
|
165
|
-
...corsHeaders,
|
|
166
|
-
});
|
|
167
|
-
res.end(JSON.stringify(data));
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const readBody = () =>
|
|
171
|
-
new Promise((resolve) => {
|
|
172
|
-
let body = "";
|
|
173
|
-
req.on("data", (c) => (body += c));
|
|
174
|
-
req.on("end", () => {
|
|
175
|
-
try {
|
|
176
|
-
resolve(body ? JSON.parse(body) : null);
|
|
177
|
-
} catch {
|
|
178
|
-
resolve(null);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const ctx = state.ctx;
|
|
184
|
-
|
|
185
|
-
if (url === "/api/local/browse" && req.method === "POST") {
|
|
186
|
-
const os = platform();
|
|
187
|
-
try {
|
|
188
|
-
let selected;
|
|
189
|
-
if (os === "darwin") {
|
|
190
|
-
selected = execSync(
|
|
191
|
-
`osascript -e 'POSIX path of (choose folder with prompt "Select vault folder")'`,
|
|
192
|
-
{ encoding: "utf-8", timeout: 30000 },
|
|
193
|
-
).trim();
|
|
194
|
-
} else if (os === "linux") {
|
|
195
|
-
selected = execSync(
|
|
196
|
-
`zenity --file-selection --directory --title="Select vault folder" 2>/dev/null`,
|
|
197
|
-
{ encoding: "utf-8", timeout: 30000 },
|
|
198
|
-
).trim();
|
|
199
|
-
} else {
|
|
200
|
-
return json(
|
|
201
|
-
{ error: "Folder picker not supported on this platform" },
|
|
202
|
-
501,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (selected) {
|
|
207
|
-
return json({ path: selected });
|
|
208
|
-
}
|
|
209
|
-
return json({ path: null, cancelled: true });
|
|
210
|
-
} catch {
|
|
211
|
-
// User cancelled the dialog or command failed
|
|
212
|
-
return json({ path: null, cancelled: true });
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (url === "/api/local/connect" && req.method === "POST") {
|
|
217
|
-
const data = await readBody();
|
|
218
|
-
if (!data?.vaultDir?.trim())
|
|
219
|
-
return json(
|
|
220
|
-
{ error: "vaultDir is required", code: "INVALID_INPUT" },
|
|
221
|
-
400,
|
|
222
|
-
);
|
|
223
|
-
let vaultPath = data.vaultDir.trim().replace(/^~/, homedir());
|
|
224
|
-
vaultPath = resolve(vaultPath);
|
|
225
|
-
if (!existsSync(vaultPath))
|
|
226
|
-
return json(
|
|
227
|
-
{ error: "Vault folder not found", code: "NOT_FOUND" },
|
|
228
|
-
404,
|
|
229
|
-
);
|
|
230
|
-
if (!statSync(vaultPath).isDirectory())
|
|
231
|
-
return json(
|
|
232
|
-
{ error: "Path is not a directory", code: "INVALID_INPUT" },
|
|
233
|
-
400,
|
|
234
|
-
);
|
|
235
|
-
try {
|
|
236
|
-
try {
|
|
237
|
-
state.db.close();
|
|
238
|
-
} catch {}
|
|
239
|
-
const newConfig = {
|
|
240
|
-
...state.config,
|
|
241
|
-
vaultDir: vaultPath,
|
|
242
|
-
dbPath: join(vaultPath, ".context-vault.db"),
|
|
243
|
-
vaultDirExists: true,
|
|
244
|
-
};
|
|
245
|
-
state.config = newConfig;
|
|
246
|
-
state.db = await initDatabase(newConfig.dbPath);
|
|
247
|
-
state.stmts = prepareStatements(state.db);
|
|
248
|
-
console.log(`[context-vault] Switched to vault: ${vaultPath}`);
|
|
249
|
-
return json({
|
|
250
|
-
userId: "local",
|
|
251
|
-
email: "local@localhost",
|
|
252
|
-
name: "Local",
|
|
253
|
-
tier: "free",
|
|
254
|
-
createdAt: new Date().toISOString(),
|
|
255
|
-
});
|
|
256
|
-
} catch (e) {
|
|
257
|
-
console.error(`[local-server] Connect error: ${e.message}`);
|
|
258
|
-
return json(
|
|
259
|
-
{ error: `Failed to connect: ${e.message}`, code: "CONNECT_FAILED" },
|
|
260
|
-
500,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (url === "/api/health" && req.method === "GET") {
|
|
266
|
-
return json({ ok: true, mode: "local" });
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (url === "/api/me" && req.method === "GET") {
|
|
270
|
-
return json({
|
|
271
|
-
userId: "local",
|
|
272
|
-
email: "local@localhost",
|
|
273
|
-
name: "Local",
|
|
274
|
-
tier: "free",
|
|
275
|
-
createdAt: new Date().toISOString(),
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (url === "/api/billing/usage" && req.method === "GET") {
|
|
280
|
-
const status = gatherVaultStatus(ctx, {});
|
|
281
|
-
const total = status.kindCounts.reduce((s, k) => s + k.c, 0);
|
|
282
|
-
const storageMb =
|
|
283
|
-
Math.round((status.dbSizeBytes / (1024 * 1024)) * 100) / 100;
|
|
284
|
-
return json({
|
|
285
|
-
tier: "free",
|
|
286
|
-
limits: {
|
|
287
|
-
maxEntries: "unlimited",
|
|
288
|
-
requestsPerDay: "unlimited",
|
|
289
|
-
storageMb: 1024,
|
|
290
|
-
exportEnabled: true,
|
|
291
|
-
},
|
|
292
|
-
usage: { requestsToday: 0, entriesUsed: total, storageMb },
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (url === "/api/keys" && req.method === "GET") {
|
|
297
|
-
return json({ keys: [] });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (url === "/api/vault/status" && req.method === "GET") {
|
|
301
|
-
const status = gatherVaultStatus(ctx, {});
|
|
302
|
-
return json({
|
|
303
|
-
entries: {
|
|
304
|
-
total: status.kindCounts.reduce((s, k) => s + k.c, 0),
|
|
305
|
-
by_kind: Object.fromEntries(
|
|
306
|
-
status.kindCounts.map((k) => [k.kind, k.c]),
|
|
307
|
-
),
|
|
308
|
-
by_category: Object.fromEntries(
|
|
309
|
-
status.categoryCounts.map((k) => [k.category, k.c]),
|
|
310
|
-
),
|
|
311
|
-
},
|
|
312
|
-
files: { total: status.fileCount, directories: status.subdirs },
|
|
313
|
-
database: {
|
|
314
|
-
size: status.dbSize,
|
|
315
|
-
size_bytes: status.dbSizeBytes,
|
|
316
|
-
stale_paths: status.staleCount,
|
|
317
|
-
expired: status.expiredCount,
|
|
318
|
-
},
|
|
319
|
-
embeddings: status.embeddingStatus,
|
|
320
|
-
embed_model_available: status.embedModelAvailable,
|
|
321
|
-
health:
|
|
322
|
-
status.errors.length === 0 && !status.stalePaths ? "ok" : "degraded",
|
|
323
|
-
errors: status.errors,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (url.startsWith("/api/vault/entries") && req.method === "GET") {
|
|
328
|
-
const idMatch = url.match(/\/api\/vault\/entries\/([^/]+)$/);
|
|
329
|
-
if (idMatch) {
|
|
330
|
-
const entry = stmts.getEntryById.get(idMatch[1]);
|
|
331
|
-
if (!entry)
|
|
332
|
-
return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
|
|
333
|
-
return json(formatEntry(entry));
|
|
334
|
-
}
|
|
335
|
-
const u = new URL(req.url || "", "http://localhost");
|
|
336
|
-
const kind = u.searchParams.get("kind") || null;
|
|
337
|
-
const category = u.searchParams.get("category") || null;
|
|
338
|
-
const limit = Math.min(
|
|
339
|
-
parseInt(u.searchParams.get("limit") || "20", 10) || 20,
|
|
340
|
-
100,
|
|
341
|
-
);
|
|
342
|
-
const offset = parseInt(u.searchParams.get("offset") || "0", 10) || 0;
|
|
343
|
-
const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
|
|
344
|
-
const params = [];
|
|
345
|
-
if (kind) {
|
|
346
|
-
clauses.push("kind = ?");
|
|
347
|
-
params.push(normalizeKind(kind));
|
|
348
|
-
}
|
|
349
|
-
if (category) {
|
|
350
|
-
clauses.push("category = ?");
|
|
351
|
-
params.push(category);
|
|
352
|
-
}
|
|
353
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
354
|
-
const total = ctx.db
|
|
355
|
-
.prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
|
|
356
|
-
.get(...params).c;
|
|
357
|
-
const rows = ctx.db
|
|
358
|
-
.prepare(
|
|
359
|
-
`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
360
|
-
)
|
|
361
|
-
.all(...params, limit, offset);
|
|
362
|
-
return json({ entries: rows.map(formatEntry), total, limit, offset });
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (url === "/api/vault/entries" && req.method === "POST") {
|
|
366
|
-
const data = await readBody();
|
|
367
|
-
if (!data)
|
|
368
|
-
return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
|
|
369
|
-
const err = validateEntry(data);
|
|
370
|
-
if (err)
|
|
371
|
-
return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
|
|
372
|
-
try {
|
|
373
|
-
const entry = await captureAndIndex(ctx, {
|
|
374
|
-
kind: data.kind,
|
|
375
|
-
title: data.title,
|
|
376
|
-
body: data.body,
|
|
377
|
-
meta: data.meta,
|
|
378
|
-
tags: data.tags,
|
|
379
|
-
source: data.source || "rest-api",
|
|
380
|
-
identity_key: data.identity_key,
|
|
381
|
-
expires_at: data.expires_at,
|
|
382
|
-
userId: null,
|
|
383
|
-
});
|
|
384
|
-
return json(formatEntry(stmts.getEntryById.get(entry.id)), 201);
|
|
385
|
-
} catch (e) {
|
|
386
|
-
console.error(`[local-server] Create error: ${e.message}`);
|
|
387
|
-
return json(
|
|
388
|
-
{ error: "Failed to create entry", code: "CREATE_FAILED" },
|
|
389
|
-
500,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (url.match(/^\/api\/vault\/entries\/[^/]+$/) && req.method === "PUT") {
|
|
395
|
-
const id = url.split("/").pop();
|
|
396
|
-
const data = await readBody();
|
|
397
|
-
if (!data)
|
|
398
|
-
return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
|
|
399
|
-
const err = validateEntry(data, {
|
|
400
|
-
requireKind: false,
|
|
401
|
-
requireBody: false,
|
|
402
|
-
});
|
|
403
|
-
if (err)
|
|
404
|
-
return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
|
|
405
|
-
const existing = stmts.getEntryById.get(id);
|
|
406
|
-
if (!existing)
|
|
407
|
-
return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
|
|
408
|
-
try {
|
|
409
|
-
const entry = updateEntryFile(ctx, existing, {
|
|
410
|
-
title: data.title,
|
|
411
|
-
body: data.body,
|
|
412
|
-
tags: data.tags,
|
|
413
|
-
meta: data.meta,
|
|
414
|
-
source: data.source,
|
|
415
|
-
expires_at: data.expires_at,
|
|
416
|
-
});
|
|
417
|
-
await indexEntry(ctx, entry);
|
|
418
|
-
return json(formatEntry(stmts.getEntryById.get(id)));
|
|
419
|
-
} catch (e) {
|
|
420
|
-
console.error(`[local-server] Update error: ${e.message}`);
|
|
421
|
-
return json(
|
|
422
|
-
{ error: "Failed to update entry", code: "UPDATE_FAILED" },
|
|
423
|
-
500,
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
url.match(/^\/api\/vault\/entries\/[^/]+$/) &&
|
|
430
|
-
req.method === "DELETE"
|
|
431
|
-
) {
|
|
432
|
-
const id = url.split("/").pop();
|
|
433
|
-
const entry = stmts.getEntryById.get(id);
|
|
434
|
-
if (!entry)
|
|
435
|
-
return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
|
|
436
|
-
if (entry.file_path) {
|
|
437
|
-
try {
|
|
438
|
-
unlinkSync(entry.file_path);
|
|
439
|
-
} catch {}
|
|
440
|
-
}
|
|
441
|
-
const rowidResult = stmts.getRowid.get(id);
|
|
442
|
-
if (rowidResult?.rowid) {
|
|
443
|
-
try {
|
|
444
|
-
deleteVec(stmts, Number(rowidResult.rowid));
|
|
445
|
-
} catch {}
|
|
446
|
-
}
|
|
447
|
-
stmts.deleteEntry.run(id);
|
|
448
|
-
return json({
|
|
449
|
-
deleted: true,
|
|
450
|
-
id,
|
|
451
|
-
kind: entry.kind,
|
|
452
|
-
title: entry.title || null,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (url === "/api/vault/search" && req.method === "POST") {
|
|
457
|
-
const data = await readBody();
|
|
458
|
-
if (!data || !data.query?.trim())
|
|
459
|
-
return json({ error: "query is required", code: "INVALID_INPUT" }, 400);
|
|
460
|
-
const limit = Math.min(parseInt(data.limit || 20, 10) || 20, 100);
|
|
461
|
-
const offset = parseInt(data.offset || 0, 10) || 0;
|
|
462
|
-
try {
|
|
463
|
-
const results = await hybridSearch(ctx, data.query, {
|
|
464
|
-
kindFilter: data.kind ? normalizeKind(data.kind) : null,
|
|
465
|
-
categoryFilter: data.category || null,
|
|
466
|
-
limit,
|
|
467
|
-
offset,
|
|
468
|
-
decayDays: ctx.config.eventDecayDays || 30,
|
|
469
|
-
userIdFilter: undefined,
|
|
470
|
-
});
|
|
471
|
-
const formatted = results.map((row) => ({
|
|
472
|
-
...formatEntry(row),
|
|
473
|
-
score: Math.round(row.score * 1000) / 1000,
|
|
474
|
-
}));
|
|
475
|
-
return json({
|
|
476
|
-
results: formatted,
|
|
477
|
-
count: formatted.length,
|
|
478
|
-
query: data.query,
|
|
479
|
-
});
|
|
480
|
-
} catch (e) {
|
|
481
|
-
console.error(`[local-server] Search error: ${e.message}`);
|
|
482
|
-
return json({ error: "Search failed", code: "SEARCH_FAILED" }, 500);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (url === "/api/vault/import/bulk" && req.method === "POST") {
|
|
487
|
-
const data = await readBody();
|
|
488
|
-
if (!data || !Array.isArray(data.entries)) {
|
|
489
|
-
return json(
|
|
490
|
-
{
|
|
491
|
-
error: "Invalid body — expected { entries: [...] }",
|
|
492
|
-
code: "INVALID_INPUT",
|
|
493
|
-
},
|
|
494
|
-
400,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
if (data.entries.length > 500) {
|
|
498
|
-
return json(
|
|
499
|
-
{ error: "Maximum 500 entries per request", code: "LIMIT_EXCEEDED" },
|
|
500
|
-
400,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const result = await importEntries(ctx, data.entries, {
|
|
505
|
-
source: "bulk-import",
|
|
506
|
-
});
|
|
507
|
-
return json({
|
|
508
|
-
imported: result.imported,
|
|
509
|
-
failed: result.failed,
|
|
510
|
-
errors: result.errors.slice(0, 10).map((e) => e.error),
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (url === "/api/vault/import/file" && req.method === "POST") {
|
|
515
|
-
const data = await readBody();
|
|
516
|
-
if (!data?.filename || !data?.content) {
|
|
517
|
-
return json(
|
|
518
|
-
{ error: "filename and content are required", code: "INVALID_INPUT" },
|
|
519
|
-
400,
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const entries = parseFile(data.filename, data.content, {
|
|
524
|
-
kind: data.kind,
|
|
525
|
-
source: data.source || "file-import",
|
|
526
|
-
});
|
|
527
|
-
if (!entries.length)
|
|
528
|
-
return json({
|
|
529
|
-
imported: 0,
|
|
530
|
-
failed: 0,
|
|
531
|
-
errors: ["No entries parsed from file"],
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
const result = await importEntries(ctx, entries, {
|
|
535
|
-
source: data.source || "file-import",
|
|
536
|
-
});
|
|
537
|
-
return json({
|
|
538
|
-
imported: result.imported,
|
|
539
|
-
failed: result.failed,
|
|
540
|
-
errors: result.errors.slice(0, 10).map((e) => e.error),
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (url.startsWith("/api/vault/export") && req.method === "GET") {
|
|
545
|
-
const u = new URL(req.url || "", "http://localhost");
|
|
546
|
-
const format = u.searchParams.get("format") || "json";
|
|
547
|
-
|
|
548
|
-
const rows = ctx.db
|
|
549
|
-
.prepare(
|
|
550
|
-
"SELECT * FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC",
|
|
551
|
-
)
|
|
552
|
-
.all();
|
|
553
|
-
|
|
554
|
-
const entries = rows.map(formatEntry);
|
|
555
|
-
|
|
556
|
-
if (format === "csv") {
|
|
557
|
-
const headers = [
|
|
558
|
-
"id",
|
|
559
|
-
"kind",
|
|
560
|
-
"category",
|
|
561
|
-
"title",
|
|
562
|
-
"body",
|
|
563
|
-
"tags",
|
|
564
|
-
"source",
|
|
565
|
-
"identity_key",
|
|
566
|
-
"expires_at",
|
|
567
|
-
"created_at",
|
|
568
|
-
];
|
|
569
|
-
const csvLines = [headers.join(",")];
|
|
570
|
-
for (const e of entries) {
|
|
571
|
-
const row = headers.map((h) => {
|
|
572
|
-
let val = e[h];
|
|
573
|
-
if (Array.isArray(val)) val = val.join(", ");
|
|
574
|
-
if (val == null) val = "";
|
|
575
|
-
val = String(val);
|
|
576
|
-
if (val.includes(",") || val.includes('"') || val.includes("\n")) {
|
|
577
|
-
val = '"' + val.replace(/"/g, '""') + '"';
|
|
578
|
-
}
|
|
579
|
-
return val;
|
|
580
|
-
});
|
|
581
|
-
csvLines.push(row.join(","));
|
|
582
|
-
}
|
|
583
|
-
res.writeHead(200, {
|
|
584
|
-
"Content-Type": "text/csv",
|
|
585
|
-
...corsHeaders,
|
|
586
|
-
});
|
|
587
|
-
res.end(csvLines.join("\n"));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return json({
|
|
592
|
-
entries,
|
|
593
|
-
total: entries.length,
|
|
594
|
-
exported_at: new Date().toISOString(),
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (url === "/api/vault/ingest" && req.method === "POST") {
|
|
599
|
-
const data = await readBody();
|
|
600
|
-
if (!data?.url)
|
|
601
|
-
return json({ error: "url is required", code: "INVALID_INPUT" }, 400);
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
const entry = await ingestUrl(data.url, {
|
|
605
|
-
kind: data.kind,
|
|
606
|
-
tags: data.tags,
|
|
607
|
-
});
|
|
608
|
-
const result = await captureAndIndex(ctx, entry);
|
|
609
|
-
return json(formatEntry(ctx.stmts.getEntryById.get(result.id)), 201);
|
|
610
|
-
} catch (e) {
|
|
611
|
-
return json(
|
|
612
|
-
{ error: `Ingestion failed: ${e.message}`, code: "INGEST_FAILED" },
|
|
613
|
-
500,
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (url === "/api/vault/manifest" && req.method === "GET") {
|
|
619
|
-
const rows = ctx.db
|
|
620
|
-
.prepare(
|
|
621
|
-
"SELECT id, kind, title, created_at FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC",
|
|
622
|
-
)
|
|
623
|
-
.all();
|
|
624
|
-
return json({
|
|
625
|
-
entries: rows.map((r) => ({
|
|
626
|
-
id: r.id,
|
|
627
|
-
kind: r.kind,
|
|
628
|
-
title: r.title || null,
|
|
629
|
-
created_at: r.created_at,
|
|
630
|
-
})),
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (url === "/api/local/link" && req.method === "GET") {
|
|
635
|
-
const dataDir = join(homedir(), ".context-mcp");
|
|
636
|
-
const configPath = join(dataDir, "config.json");
|
|
637
|
-
let storedConfig = {};
|
|
638
|
-
if (existsSync(configPath)) {
|
|
639
|
-
try {
|
|
640
|
-
storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
641
|
-
} catch {}
|
|
642
|
-
}
|
|
643
|
-
return json({
|
|
644
|
-
linked: !!storedConfig.apiKey,
|
|
645
|
-
email: storedConfig.email || null,
|
|
646
|
-
hostedUrl: storedConfig.hostedUrl || null,
|
|
647
|
-
linkedAt: storedConfig.linkedAt || null,
|
|
648
|
-
tier: storedConfig.tier || null,
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (url === "/api/local/link" && req.method === "POST") {
|
|
653
|
-
const data = await readBody();
|
|
654
|
-
const dataDir = join(homedir(), ".context-mcp");
|
|
655
|
-
const configPath = join(dataDir, "config.json");
|
|
656
|
-
|
|
657
|
-
let storedConfig = {};
|
|
658
|
-
if (existsSync(configPath)) {
|
|
659
|
-
try {
|
|
660
|
-
storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
661
|
-
} catch {}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (!data?.apiKey) {
|
|
665
|
-
// Unlink
|
|
666
|
-
delete storedConfig.apiKey;
|
|
667
|
-
delete storedConfig.hostedUrl;
|
|
668
|
-
delete storedConfig.userId;
|
|
669
|
-
delete storedConfig.email;
|
|
670
|
-
delete storedConfig.linkedAt;
|
|
671
|
-
delete storedConfig.tier;
|
|
672
|
-
writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
|
|
673
|
-
return json({ linked: false });
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const hostedUrl = data.hostedUrl || "https://api.context-vault.com";
|
|
677
|
-
try {
|
|
678
|
-
const response = await fetch(`${hostedUrl}/api/me`, {
|
|
679
|
-
headers: { Authorization: `Bearer ${data.apiKey}` },
|
|
680
|
-
});
|
|
681
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
682
|
-
const user = await response.json();
|
|
683
|
-
|
|
684
|
-
storedConfig.apiKey = data.apiKey;
|
|
685
|
-
storedConfig.hostedUrl = hostedUrl;
|
|
686
|
-
storedConfig.userId = user.userId || user.id;
|
|
687
|
-
storedConfig.email = user.email;
|
|
688
|
-
storedConfig.tier = user.tier || "free";
|
|
689
|
-
storedConfig.linkedAt = new Date().toISOString();
|
|
690
|
-
|
|
691
|
-
mkdirSync(dataDir, { recursive: true });
|
|
692
|
-
writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
|
|
693
|
-
|
|
694
|
-
return json({
|
|
695
|
-
linked: true,
|
|
696
|
-
email: user.email,
|
|
697
|
-
tier: user.tier || "free",
|
|
698
|
-
});
|
|
699
|
-
} catch (e) {
|
|
700
|
-
return json(
|
|
701
|
-
{ error: `Verification failed: ${e.message}`, code: "AUTH_FAILED" },
|
|
702
|
-
401,
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (url === "/api/local/sync" && req.method === "POST") {
|
|
708
|
-
const dataDir = join(homedir(), ".context-mcp");
|
|
709
|
-
const configPath = join(dataDir, "config.json");
|
|
710
|
-
let storedConfig = {};
|
|
711
|
-
if (existsSync(configPath)) {
|
|
712
|
-
try {
|
|
713
|
-
storedConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
714
|
-
} catch {}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (!storedConfig.apiKey) {
|
|
718
|
-
return json(
|
|
719
|
-
{ error: "Not linked. Use link endpoint first.", code: "NOT_LINKED" },
|
|
720
|
-
400,
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
try {
|
|
725
|
-
const local = buildLocalManifest(ctx);
|
|
726
|
-
const remote = await fetchRemoteManifest(
|
|
727
|
-
storedConfig.hostedUrl,
|
|
728
|
-
storedConfig.apiKey,
|
|
729
|
-
);
|
|
730
|
-
const plan = computeSyncPlan(local, remote);
|
|
731
|
-
const result = await executeSync(ctx, {
|
|
732
|
-
hostedUrl: storedConfig.hostedUrl,
|
|
733
|
-
apiKey: storedConfig.apiKey,
|
|
734
|
-
plan,
|
|
735
|
-
});
|
|
736
|
-
return json(result);
|
|
737
|
-
} catch (e) {
|
|
738
|
-
return json(
|
|
739
|
-
{ error: `Sync failed: ${e.message}`, code: "SYNC_FAILED" },
|
|
740
|
-
500,
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const filePath = join(
|
|
746
|
-
APP_DIST,
|
|
747
|
-
url === "/" ? "index.html" : url.replace(/^\//, ""),
|
|
748
|
-
);
|
|
749
|
-
if (!filePath.startsWith(APP_DIST)) {
|
|
750
|
-
res.writeHead(403);
|
|
751
|
-
res.end();
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
755
|
-
const fallback = join(APP_DIST, "index.html");
|
|
756
|
-
if (existsSync(fallback)) {
|
|
757
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
758
|
-
createReadStream(fallback).pipe(res);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
res.writeHead(404);
|
|
762
|
-
res.end();
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
const type = MIME[extname(filePath)] || "application/octet-stream";
|
|
766
|
-
res.writeHead(200, { "Content-Type": type });
|
|
767
|
-
createReadStream(filePath).pipe(res);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
server.listen(port, () => {
|
|
771
|
-
console.log(`[context-vault] Local mode: http://localhost:${port}`);
|
|
772
|
-
console.log(`[context-vault] Vault: ${config.vaultDir}`);
|
|
773
|
-
console.log(`[context-vault] No authentication required`);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
process.on("SIGINT", () => {
|
|
777
|
-
try {
|
|
778
|
-
state.db.close();
|
|
779
|
-
} catch {}
|
|
780
|
-
process.exit(0);
|
|
781
|
-
});
|
|
782
|
-
process.on("SIGTERM", () => {
|
|
783
|
-
try {
|
|
784
|
-
state.db.close();
|
|
785
|
-
} catch {}
|
|
786
|
-
process.exit(0);
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
main().catch((e) => {
|
|
791
|
-
console.error(`[local-server] Fatal: ${e.message}`);
|
|
792
|
-
process.exit(1);
|
|
793
|
-
});
|