@openparachute/vault 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/CLAUDE.md +2 -2
- package/README.md +289 -44
- package/core/src/core.test.ts +802 -346
- package/core/src/expand.ts +140 -0
- package/core/src/hooks.test.ts +27 -27
- package/core/src/hooks.ts +1 -1
- package/core/src/mcp.ts +102 -39
- package/core/src/notes.ts +82 -4
- package/core/src/obsidian.test.ts +11 -11
- package/core/src/paths.test.ts +46 -46
- package/core/src/schema.ts +18 -2
- package/core/src/store.ts +51 -51
- package/core/src/types.ts +29 -29
- package/core/src/wikilinks.test.ts +61 -61
- package/docs/HTTP_API.md +4 -2
- package/package.json +1 -1
- package/src/auth.test.ts +319 -0
- package/src/backup-launchd.test.ts +90 -0
- package/src/backup-launchd.ts +169 -0
- package/src/backup.test.ts +715 -0
- package/src/backup.ts +699 -0
- package/src/cli.ts +923 -31
- package/src/config.test.ts +173 -0
- package/src/config.ts +345 -15
- package/src/daemon.ts +136 -0
- package/src/doctor.test.ts +356 -0
- package/src/health.test.ts +201 -0
- package/src/health.ts +115 -0
- package/src/launchd.test.ts +91 -0
- package/src/launchd.ts +37 -40
- package/src/mcp-http.ts +1 -1
- package/src/mcp-tools.ts +7 -9
- package/src/oauth.test.ts +289 -8
- package/src/oauth.ts +57 -12
- package/src/published.test.ts +21 -21
- package/src/routes.ts +152 -70
- package/src/routing.test.ts +347 -0
- package/src/routing.ts +365 -0
- package/src/server.ts +7 -278
- package/src/systemd.test.ts +15 -0
- package/src/systemd.ts +18 -11
- package/src/triggers.test.ts +7 -7
- package/src/triggers.ts +6 -6
- package/src/vault-store.ts +20 -3
- package/src/vault.test.ts +356 -262
- package/.claude/settings.local.json +0 -31
- package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +0 -2
- package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +0 -1
- package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +0 -2
- package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +0 -2
- package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +0 -1
- package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +0 -1
- package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +0 -211
- package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +0 -59
- package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +0 -232
- package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +0 -182
- package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +0 -91
- package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +0 -70
- package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +0 -59
- package/religions-abrahamic-filter.png +0 -0
- package/religions-buddhism-v2.png +0 -0
- package/religions-buddhism.png +0 -0
- package/religions-final.png +0 -0
- package/religions-v1.png +0 -0
- package/religions-v2.png +0 -0
- package/religions-zen.png +0 -0
- package/web/README.md +0 -73
- package/web/bun.lock +0 -827
- package/web/eslint.config.js +0 -23
- package/web/index.html +0 -15
- package/web/package.json +0 -36
- package/web/public/favicon.svg +0 -1
- package/web/public/icons.svg +0 -24
- package/web/src/App.tsx +0 -149
- package/web/src/Graph.tsx +0 -200
- package/web/src/NoteView.tsx +0 -155
- package/web/src/Sidebar.tsx +0 -186
- package/web/src/api.ts +0 -21
- package/web/src/index.css +0 -50
- package/web/src/main.tsx +0 -10
- package/web/src/types.ts +0 -37
- package/web/src/utils.ts +0 -107
- package/web/tsconfig.app.json +0 -25
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -24
- package/web/vite.config.ts +0 -15
package/src/health.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Healthcheck + error-log helpers for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Used by `vault status`, `vault restart`, and `vault doctor` to distinguish
|
|
5
|
+
* three distinct failure modes that production wedged us on once already:
|
|
6
|
+
*
|
|
7
|
+
* 1. Launchd says the agent is loaded, but nothing is bound to the port.
|
|
8
|
+
* 2. Something is bound to the port, but /health doesn't return 200.
|
|
9
|
+
* 3. Everything is fine.
|
|
10
|
+
*
|
|
11
|
+
* The original CLI conflated these and reported "running" in cases where
|
|
12
|
+
* start.sh was failing and the daemon was respawning in a crash loop.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from "fs";
|
|
16
|
+
|
|
17
|
+
export type HealthStatus =
|
|
18
|
+
| "healthy" // port bound + /health 200
|
|
19
|
+
| "unhealthy" // port bound but /health not 200
|
|
20
|
+
| "not-listening" // port closed (nothing accepting connections)
|
|
21
|
+
| "error"; // other fetch failure
|
|
22
|
+
|
|
23
|
+
export interface HealthResult {
|
|
24
|
+
status: HealthStatus;
|
|
25
|
+
statusCode?: number;
|
|
26
|
+
error?: string;
|
|
27
|
+
latencyMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Probe http://127.0.0.1:<port>/health once. Short timeout so callers can
|
|
32
|
+
* poll without hanging. Treats ECONNREFUSED as "not listening" explicitly so
|
|
33
|
+
* the CLI can tell "daemon crashed" apart from "daemon running but wedged."
|
|
34
|
+
*/
|
|
35
|
+
export async function checkHealth(port: number, timeoutMs = 2000): Promise<HealthResult> {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
+
try {
|
|
40
|
+
const resp = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
const latencyMs = Date.now() - start;
|
|
44
|
+
if (resp.ok) {
|
|
45
|
+
return { status: "healthy", statusCode: resp.status, latencyMs };
|
|
46
|
+
}
|
|
47
|
+
return { status: "unhealthy", statusCode: resp.status, latencyMs };
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
const latencyMs = Date.now() - start;
|
|
50
|
+
const msg = String(err?.message ?? err);
|
|
51
|
+
// Bun surfaces ECONNREFUSED as "Unable to connect" / error code
|
|
52
|
+
// "ConnectionRefused" depending on the version. Also catch DNS failures.
|
|
53
|
+
if (
|
|
54
|
+
/ECONNREFUSED|ConnectionRefused|Unable to connect|refused/i.test(msg) ||
|
|
55
|
+
err?.code === "ECONNREFUSED"
|
|
56
|
+
) {
|
|
57
|
+
return { status: "not-listening", error: msg, latencyMs };
|
|
58
|
+
}
|
|
59
|
+
if (err?.name === "AbortError" || /aborted|timeout/i.test(msg)) {
|
|
60
|
+
return { status: "error", error: `timeout after ${timeoutMs}ms`, latencyMs };
|
|
61
|
+
}
|
|
62
|
+
return { status: "error", error: msg, latencyMs };
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Poll /health until it returns healthy or the overall budget expires.
|
|
70
|
+
* Used by `vault restart` to turn a fire-and-forget launchctl bounce into a
|
|
71
|
+
* blocking operation with a clear success/failure signal.
|
|
72
|
+
*/
|
|
73
|
+
export async function waitForHealthy(
|
|
74
|
+
port: number,
|
|
75
|
+
opts: { totalMs?: number; intervalMs?: number; perProbeTimeoutMs?: number } = {},
|
|
76
|
+
): Promise<HealthResult> {
|
|
77
|
+
const totalMs = opts.totalMs ?? 10_000;
|
|
78
|
+
const intervalMs = opts.intervalMs ?? 500;
|
|
79
|
+
const perProbeTimeoutMs = opts.perProbeTimeoutMs ?? 1_500;
|
|
80
|
+
const deadline = Date.now() + totalMs;
|
|
81
|
+
|
|
82
|
+
let last: HealthResult = { status: "error", error: "never probed" };
|
|
83
|
+
while (Date.now() < deadline) {
|
|
84
|
+
// Never let a single probe's timeout push us past the total budget —
|
|
85
|
+
// otherwise the worst case is totalMs + perProbeTimeoutMs, and the
|
|
86
|
+
// "10s" the user sees from the CLI wouldn't match reality.
|
|
87
|
+
const remainingBeforeProbe = deadline - Date.now();
|
|
88
|
+
const probeBudget = Math.min(perProbeTimeoutMs, remainingBeforeProbe);
|
|
89
|
+
if (probeBudget <= 0) break;
|
|
90
|
+
last = await checkHealth(port, probeBudget);
|
|
91
|
+
if (last.status === "healthy") return last;
|
|
92
|
+
const remaining = deadline - Date.now();
|
|
93
|
+
if (remaining <= 0) break;
|
|
94
|
+
await new Promise((r) => setTimeout(r, Math.min(intervalMs, remaining)));
|
|
95
|
+
}
|
|
96
|
+
return last;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Return the last `n` lines of a file. Safe on missing files (returns null
|
|
101
|
+
* so callers can render "no log file" rather than propagating ENOENT).
|
|
102
|
+
*/
|
|
103
|
+
export function tailFile(path: string, n: number): string | null {
|
|
104
|
+
if (!existsSync(path)) return null;
|
|
105
|
+
try {
|
|
106
|
+
const content = readFileSync(path, "utf8");
|
|
107
|
+
if (!content) return "";
|
|
108
|
+
const lines = content.split("\n");
|
|
109
|
+
// Drop trailing empty line from a terminating \n so the tail isn't blank.
|
|
110
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
111
|
+
return lines.slice(-n).join("\n");
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { $ } from "bun";
|
|
6
|
+
import { generateWrapper, WRAPPER_PATH } from "./daemon.ts";
|
|
7
|
+
import { generatePlist } from "./launchd.ts";
|
|
8
|
+
|
|
9
|
+
describe("generateWrapper", () => {
|
|
10
|
+
// The incident that triggered this module: start.sh had the server path
|
|
11
|
+
// baked in and went stale when the repo moved. These tests assert the new
|
|
12
|
+
// contract — NO absolute server.ts path in the wrapper, pointer read at
|
|
13
|
+
// runtime, env override respected, and graceful failure when missing.
|
|
14
|
+
|
|
15
|
+
test("does not hardcode an absolute server.ts path", () => {
|
|
16
|
+
const wrapper = generateWrapper({
|
|
17
|
+
bunPath: "/Users/alice/.bun/bin/bun",
|
|
18
|
+
serverPathFile: "/Users/alice/.parachute/server-path",
|
|
19
|
+
envPath: "/Users/alice/.parachute/.env",
|
|
20
|
+
});
|
|
21
|
+
// No absolute path ending in server.ts — the whole point. Error messages
|
|
22
|
+
// may legitimately mention "server.ts" in text; we only forbid baked-in
|
|
23
|
+
// absolute paths that would go stale when the repo moves.
|
|
24
|
+
expect(wrapper).not.toMatch(/\/[^\s"]+\/server\.ts/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("reads the pointer file at boot, with PARACHUTE_VAULT_SERVER_PATH override", () => {
|
|
28
|
+
const wrapper = generateWrapper({
|
|
29
|
+
bunPath: "/bin/bun",
|
|
30
|
+
serverPathFile: "/x/.parachute/server-path",
|
|
31
|
+
});
|
|
32
|
+
// Env override precedes the pointer fallback.
|
|
33
|
+
expect(wrapper).toContain('SERVER_PATH="${PARACHUTE_VAULT_SERVER_PATH:-}"');
|
|
34
|
+
expect(wrapper).toContain('[ -f "/x/.parachute/server-path" ]');
|
|
35
|
+
expect(wrapper).toContain('cat "/x/.parachute/server-path"');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("fails loudly (non-zero exit) when neither env nor pointer supplies a path", () => {
|
|
39
|
+
const wrapper = generateWrapper({ bunPath: "/bin/bun" });
|
|
40
|
+
expect(wrapper).toContain('if [ -z "$SERVER_PATH" ]; then');
|
|
41
|
+
expect(wrapper).toContain("exit 1");
|
|
42
|
+
// Actionable message — user needs to know what to run.
|
|
43
|
+
expect(wrapper).toMatch(/parachute vault init/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("fails loudly when pointer target no longer exists (moved repo)", () => {
|
|
47
|
+
const wrapper = generateWrapper({ bunPath: "/bin/bun" });
|
|
48
|
+
expect(wrapper).toContain('if [ ! -f "$SERVER_PATH" ]; then');
|
|
49
|
+
expect(wrapper).toMatch(/repo may have moved/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("sources .env at the configured path", () => {
|
|
53
|
+
const wrapper = generateWrapper({
|
|
54
|
+
bunPath: "/bin/bun",
|
|
55
|
+
envPath: "/custom/.env",
|
|
56
|
+
});
|
|
57
|
+
expect(wrapper).toContain('[ -f "/custom/.env" ]');
|
|
58
|
+
expect(wrapper).toContain('source "/custom/.env"');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("execs bun with the resolved path (not a literal server path)", () => {
|
|
62
|
+
const wrapper = generateWrapper({ bunPath: "/Users/alice/.bun/bin/bun" });
|
|
63
|
+
// The exec line uses $SERVER_PATH (dereferenced at boot), not a literal.
|
|
64
|
+
expect(wrapper).toMatch(/exec "\/Users\/alice\/\.bun\/bin\/bun" "\$SERVER_PATH"/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("output is syntactically valid bash (bash -n passes)", async () => {
|
|
68
|
+
// Catch any shell-quoting regressions before they become crash-looping
|
|
69
|
+
// daemons in production. `bash -n` parses without executing.
|
|
70
|
+
const wrapper = generateWrapper({ bunPath: "/bin/bun" });
|
|
71
|
+
const dir = mkdtempSync(join(tmpdir(), "vault-wrapper-"));
|
|
72
|
+
const path = join(dir, "start.sh");
|
|
73
|
+
try {
|
|
74
|
+
writeFileSync(path, wrapper);
|
|
75
|
+
const result = await $`bash -n ${path}`.quiet().nothrow();
|
|
76
|
+
expect(result.exitCode).toBe(0);
|
|
77
|
+
} finally {
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("generatePlist", () => {
|
|
84
|
+
test("references the shared wrapper script, not server.ts directly", () => {
|
|
85
|
+
const plist = generatePlist();
|
|
86
|
+
// The plist launches /bin/bash with the wrapper — it must not name
|
|
87
|
+
// server.ts so the plist stays valid across repo moves.
|
|
88
|
+
expect(plist).toContain(`<string>${WRAPPER_PATH}</string>`);
|
|
89
|
+
expect(plist).not.toMatch(/server\.ts/);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/launchd.ts
CHANGED
|
@@ -1,42 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* macOS launchd agent management for the vault daemon.
|
|
3
3
|
*
|
|
4
|
-
* The plist runs
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The plist runs `~/.parachute/start.sh` (the shared wrapper from
|
|
5
|
+
* daemon.ts). The wrapper reads the pointer file at every boot, so
|
|
6
|
+
* moving the repo only requires re-running `parachute vault init`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { homedir } from "os";
|
|
10
|
-
import { join
|
|
10
|
+
import { join } from "path";
|
|
11
11
|
import { writeFile, unlink } from "fs/promises";
|
|
12
12
|
import { $ } from "bun";
|
|
13
|
-
import { CONFIG_DIR,
|
|
13
|
+
import { CONFIG_DIR, LOG_PATH, ERR_PATH } from "./config.ts";
|
|
14
|
+
import { WRAPPER_PATH, writeDaemonWrapper } from "./daemon.ts";
|
|
14
15
|
|
|
15
16
|
const LABEL = "computer.parachute.vault";
|
|
16
17
|
const PLIST_PATH = join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
17
|
-
const WRAPPER_PATH = join(CONFIG_DIR, "start.sh");
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Generate a shell wrapper that loads .env before starting the server.
|
|
21
|
-
*/
|
|
22
|
-
function generateWrapper(serverPath: string, bunPath: string): string {
|
|
23
|
-
return `#!/bin/bash
|
|
24
|
-
# Auto-generated by parachute vault init
|
|
25
|
-
# Loads user PATH + ~/.parachute/.env then starts the vault server
|
|
26
|
-
|
|
27
|
-
# Source user shell profile for PATH (needed for parakeet-mlx, ffmpeg, etc.)
|
|
28
|
-
[ -f "$HOME/.zprofile" ] && source "$HOME/.zprofile" 2>/dev/null
|
|
29
|
-
[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc" 2>/dev/null
|
|
30
|
-
|
|
31
|
-
if [ -f "${ENV_PATH}" ]; then
|
|
32
|
-
set -a
|
|
33
|
-
source "${ENV_PATH}"
|
|
34
|
-
set +a
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
exec "${bunPath}" "${serverPath}"
|
|
38
|
-
`;
|
|
39
|
-
}
|
|
40
18
|
|
|
41
19
|
export function generatePlist(): string {
|
|
42
20
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -64,20 +42,37 @@ export function generatePlist(): string {
|
|
|
64
42
|
</plist>`;
|
|
65
43
|
}
|
|
66
44
|
|
|
67
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Install or re-install the launchd agent. Idempotent: if the agent is
|
|
47
|
+
* already loaded, it's unloaded first so the new wrapper + pointer take
|
|
48
|
+
* effect. This is what makes `parachute vault init` safe to re-run after
|
|
49
|
+
* a folder move — the incident that motivated this PR.
|
|
50
|
+
*/
|
|
51
|
+
export async function installAgent(): Promise<{ serverPath: string }> {
|
|
68
52
|
if (process.platform !== "darwin") {
|
|
69
53
|
throw new Error("launchd is only available on macOS. Use systemd on Linux.");
|
|
70
54
|
}
|
|
71
|
-
const serverPath = resolve(dirname(import.meta.path), "server.ts");
|
|
72
|
-
const bunPath = Bun.which("bun") || join(homedir(), ".bun", "bin", "bun");
|
|
73
55
|
|
|
74
|
-
|
|
75
|
-
await writeFile(
|
|
56
|
+
const { serverPath } = await writeDaemonWrapper();
|
|
57
|
+
await writeFile(PLIST_PATH, generatePlist());
|
|
76
58
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
// Bounce launchd so it picks up a refreshed wrapper + pointer. `load`
|
|
60
|
+
// alone fails with "Operation already in progress" if we're already
|
|
61
|
+
// registered, which used to silently leave stale config in place. If
|
|
62
|
+
// two `vault init` calls race, the second `load` may also see the
|
|
63
|
+
// service as still-loaded; swallow it so re-runs don't blow up.
|
|
64
|
+
try {
|
|
65
|
+
await $`launchctl unload ${PLIST_PATH}`.quiet();
|
|
66
|
+
} catch {
|
|
67
|
+
// Not loaded yet — fine.
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await $`launchctl load ${PLIST_PATH}`.quiet();
|
|
71
|
+
} catch {
|
|
72
|
+
// A concurrent init already reloaded it — fine.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { serverPath };
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
export async function uninstallAgent(): Promise<void> {
|
|
@@ -87,9 +82,11 @@ export async function uninstallAgent(): Promise<void> {
|
|
|
87
82
|
try {
|
|
88
83
|
await unlink(PLIST_PATH);
|
|
89
84
|
} catch {}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
// Wrapper + pointer removal lives in daemon.ts so it's shared with the
|
|
86
|
+
// Linux uninstall path. Callers that want a fully-clean teardown must
|
|
87
|
+
// also call `removeDaemonWrapper()` — the CLI's `uninstall` command in
|
|
88
|
+
// PR 3 wires that up. Leaving them here programmatically would strand
|
|
89
|
+
// orphaned files in `~/.parachute/`.
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
export async function isAgentLoaded(): Promise<boolean> {
|
package/src/mcp-http.ts
CHANGED
|
@@ -95,7 +95,7 @@ async function handleMcp(
|
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
try {
|
|
98
|
-
const result = tool.execute((args ?? {}) as Record<string, unknown>);
|
|
98
|
+
const result = await tool.execute((args ?? {}) as Record<string, unknown>);
|
|
99
99
|
return {
|
|
100
100
|
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
101
101
|
};
|
package/src/mcp-tools.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { generateMcpTools } from "../core/src/mcp.ts";
|
|
9
9
|
import type { McpToolDef } from "../core/src/mcp.ts";
|
|
10
|
-
import { readVaultConfig, writeVaultConfig,
|
|
10
|
+
import { readVaultConfig, writeVaultConfig, listVaults as getVaultNames, resolveDefaultVault } from "./config.ts";
|
|
11
11
|
import { getVaultStore } from "./vault-store.ts";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -15,8 +15,7 @@ import { getVaultStore } from "./vault-store.ts";
|
|
|
15
15
|
* Sent once at session init — not per tool.
|
|
16
16
|
*/
|
|
17
17
|
export function getServerInstruction(vaultName?: string): string {
|
|
18
|
-
const
|
|
19
|
-
const name = vaultName ?? globalConfig.default_vault ?? "default";
|
|
18
|
+
const name = vaultName ?? resolveDefaultVault() ?? "default";
|
|
20
19
|
const config = readVaultConfig(name);
|
|
21
20
|
|
|
22
21
|
const parts: string[] = [
|
|
@@ -35,9 +34,8 @@ export function getServerInstruction(vaultName?: string): string {
|
|
|
35
34
|
* Each tool has an optional `vault` param that defaults to the default vault.
|
|
36
35
|
*/
|
|
37
36
|
export function generateUnifiedMcpTools(): McpToolDef[] {
|
|
38
|
-
const globalConfig = readGlobalConfig();
|
|
39
|
-
const defaultVault = globalConfig.default_vault ?? "default";
|
|
40
37
|
const vaultNames = getVaultNames();
|
|
38
|
+
const defaultVault = resolveDefaultVault() ?? "default";
|
|
41
39
|
const multiVault = vaultNames.length > 1;
|
|
42
40
|
|
|
43
41
|
// Get tool definitions from core (using default vault for schema)
|
|
@@ -66,7 +64,7 @@ export function generateUnifiedMcpTools(): McpToolDef[] {
|
|
|
66
64
|
name: coreTool.name,
|
|
67
65
|
description,
|
|
68
66
|
inputSchema,
|
|
69
|
-
execute: (params) => {
|
|
67
|
+
execute: async (params) => {
|
|
70
68
|
const vaultName = (params.vault as string) ?? defaultVault;
|
|
71
69
|
const config = readVaultConfig(vaultName);
|
|
72
70
|
if (!config) {
|
|
@@ -76,7 +74,7 @@ export function generateUnifiedMcpTools(): McpToolDef[] {
|
|
|
76
74
|
const vaultTools = generateMcpTools(store);
|
|
77
75
|
const tool = vaultTools.find((t) => t.name === coreTool.name)!;
|
|
78
76
|
const { vault: _, ...rest } = params;
|
|
79
|
-
return tool.execute(rest);
|
|
77
|
+
return await tool.execute(rest);
|
|
80
78
|
},
|
|
81
79
|
};
|
|
82
80
|
});
|
|
@@ -129,7 +127,7 @@ function overrideVaultInfo(tools: McpToolDef[], defaultVault: string): void {
|
|
|
129
127
|
const vaultInfo = tools.find((t) => t.name === "vault-info");
|
|
130
128
|
if (!vaultInfo) return;
|
|
131
129
|
|
|
132
|
-
vaultInfo.execute = (params) => {
|
|
130
|
+
vaultInfo.execute = async (params) => {
|
|
133
131
|
const vaultName = (params.vault as string) ?? defaultVault;
|
|
134
132
|
const config = readVaultConfig(vaultName);
|
|
135
133
|
if (!config) throw new Error(`Vault "${vaultName}" not found`);
|
|
@@ -147,7 +145,7 @@ function overrideVaultInfo(tools: McpToolDef[], defaultVault: string): void {
|
|
|
147
145
|
|
|
148
146
|
if (params.include_stats) {
|
|
149
147
|
const store = getVaultStore(vaultName);
|
|
150
|
-
result.stats = store.getVaultStats();
|
|
148
|
+
result.stats = await store.getVaultStats();
|
|
151
149
|
}
|
|
152
150
|
|
|
153
151
|
return result;
|