@openparachute/vault 0.1.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.
Files changed (103) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/.dockerignore +8 -0
  3. package/.env.example +9 -0
  4. package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +2 -0
  5. package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +1 -0
  6. package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +2 -0
  7. package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +2 -0
  8. package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +1 -0
  10. package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +211 -0
  11. package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +59 -0
  12. package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +232 -0
  13. package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +182 -0
  14. package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +91 -0
  15. package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +70 -0
  16. package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +59 -0
  17. package/CLAUDE.md +115 -0
  18. package/Caddyfile +3 -0
  19. package/Dockerfile +22 -0
  20. package/LICENSE +661 -0
  21. package/README.md +356 -0
  22. package/bun.lock +219 -0
  23. package/bunfig.toml +2 -0
  24. package/core/package.json +7 -0
  25. package/core/src/core.test.ts +940 -0
  26. package/core/src/hooks.test.ts +361 -0
  27. package/core/src/hooks.ts +234 -0
  28. package/core/src/links.ts +352 -0
  29. package/core/src/mcp.ts +672 -0
  30. package/core/src/notes.ts +520 -0
  31. package/core/src/obsidian.test.ts +380 -0
  32. package/core/src/obsidian.ts +322 -0
  33. package/core/src/paths.test.ts +197 -0
  34. package/core/src/paths.ts +53 -0
  35. package/core/src/schema.ts +331 -0
  36. package/core/src/store.ts +303 -0
  37. package/core/src/tag-schemas.ts +104 -0
  38. package/core/src/test-preload.ts +8 -0
  39. package/core/src/types.ts +140 -0
  40. package/core/src/wikilinks.test.ts +277 -0
  41. package/core/src/wikilinks.ts +402 -0
  42. package/deploy/parachute-vault.service +20 -0
  43. package/docker-compose.yml +50 -0
  44. package/docs/HTTP_API.md +328 -0
  45. package/fly.toml +24 -0
  46. package/package.json +32 -0
  47. package/railway.json +14 -0
  48. package/religions-abrahamic-filter.png +0 -0
  49. package/religions-buddhism-v2.png +0 -0
  50. package/religions-buddhism.png +0 -0
  51. package/religions-final.png +0 -0
  52. package/religions-v1.png +0 -0
  53. package/religions-v2.png +0 -0
  54. package/religions-zen.png +0 -0
  55. package/scripts/migrate-audio-to-opus.test.ts +237 -0
  56. package/scripts/migrate-audio-to-opus.ts +499 -0
  57. package/src/auth.ts +170 -0
  58. package/src/cli.ts +1131 -0
  59. package/src/config-triggers.test.ts +83 -0
  60. package/src/config.test.ts +125 -0
  61. package/src/config.ts +716 -0
  62. package/src/db.ts +14 -0
  63. package/src/launchd.ts +109 -0
  64. package/src/mcp-http.ts +113 -0
  65. package/src/mcp-tools.ts +155 -0
  66. package/src/oauth.test.ts +1242 -0
  67. package/src/oauth.ts +729 -0
  68. package/src/owner-auth.ts +159 -0
  69. package/src/prompt.ts +141 -0
  70. package/src/published.test.ts +214 -0
  71. package/src/qrcode-terminal.d.ts +9 -0
  72. package/src/routes.ts +822 -0
  73. package/src/server.ts +450 -0
  74. package/src/systemd.ts +84 -0
  75. package/src/token-store.test.ts +174 -0
  76. package/src/token-store.ts +241 -0
  77. package/src/triggers.test.ts +397 -0
  78. package/src/triggers.ts +412 -0
  79. package/src/two-factor.test.ts +246 -0
  80. package/src/two-factor.ts +222 -0
  81. package/src/vault-store.ts +47 -0
  82. package/src/vault.test.ts +1309 -0
  83. package/tsconfig.json +29 -0
  84. package/web/README.md +73 -0
  85. package/web/bun.lock +827 -0
  86. package/web/eslint.config.js +23 -0
  87. package/web/index.html +15 -0
  88. package/web/package.json +36 -0
  89. package/web/public/favicon.svg +1 -0
  90. package/web/public/icons.svg +24 -0
  91. package/web/src/App.tsx +149 -0
  92. package/web/src/Graph.tsx +200 -0
  93. package/web/src/NoteView.tsx +155 -0
  94. package/web/src/Sidebar.tsx +186 -0
  95. package/web/src/api.ts +21 -0
  96. package/web/src/index.css +50 -0
  97. package/web/src/main.tsx +10 -0
  98. package/web/src/types.ts +37 -0
  99. package/web/src/utils.ts +107 -0
  100. package/web/tsconfig.app.json +25 -0
  101. package/web/tsconfig.json +7 -0
  102. package/web/tsconfig.node.json +24 -0
  103. package/web/vite.config.ts +15 -0
package/src/db.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Vault database — opens bun:sqlite databases for vaults.
3
+ */
4
+
5
+ import { Database } from "bun:sqlite";
6
+ import { vaultDbPath, vaultDir } from "./config.ts";
7
+ import { mkdirSync } from "fs";
8
+
9
+ /** Open (or create) a vault's SQLite database. */
10
+ export function openVaultDb(name: string): Database {
11
+ const dir = vaultDir(name);
12
+ mkdirSync(dir, { recursive: true });
13
+ return new Database(vaultDbPath(name));
14
+ }
package/src/launchd.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * macOS launchd agent management for the vault daemon.
3
+ *
4
+ * The plist runs a wrapper script that sources ~/.parachute/.env
5
+ * before starting the server, so env vars (API keys, providers)
6
+ * are available to the daemon.
7
+ */
8
+
9
+ import { homedir } from "os";
10
+ import { join, resolve, dirname } from "path";
11
+ import { writeFile, unlink } from "fs/promises";
12
+ import { $ } from "bun";
13
+ import { CONFIG_DIR, ENV_PATH, LOG_PATH, ERR_PATH } from "./config.ts";
14
+
15
+ const LABEL = "computer.parachute.vault";
16
+ 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
+
41
+ export function generatePlist(): string {
42
+ return `<?xml version="1.0" encoding="UTF-8"?>
43
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
44
+ <plist version="1.0">
45
+ <dict>
46
+ <key>Label</key>
47
+ <string>${LABEL}</string>
48
+ <key>ProgramArguments</key>
49
+ <array>
50
+ <string>/bin/bash</string>
51
+ <string>${WRAPPER_PATH}</string>
52
+ </array>
53
+ <key>KeepAlive</key>
54
+ <true/>
55
+ <key>RunAtLoad</key>
56
+ <true/>
57
+ <key>StandardOutPath</key>
58
+ <string>${LOG_PATH}</string>
59
+ <key>StandardErrorPath</key>
60
+ <string>${ERR_PATH}</string>
61
+ <key>WorkingDirectory</key>
62
+ <string>${CONFIG_DIR}</string>
63
+ </dict>
64
+ </plist>`;
65
+ }
66
+
67
+ export async function installAgent(): Promise<void> {
68
+ if (process.platform !== "darwin") {
69
+ throw new Error("launchd is only available on macOS. Use systemd on Linux.");
70
+ }
71
+ const serverPath = resolve(dirname(import.meta.path), "server.ts");
72
+ const bunPath = Bun.which("bun") || join(homedir(), ".bun", "bin", "bun");
73
+
74
+ // Write the wrapper script
75
+ await writeFile(WRAPPER_PATH, generateWrapper(serverPath, bunPath), { mode: 0o755 });
76
+
77
+ // Write and load the plist
78
+ const plist = generatePlist();
79
+ await writeFile(PLIST_PATH, plist);
80
+ await $`launchctl load ${PLIST_PATH}`.quiet();
81
+ }
82
+
83
+ export async function uninstallAgent(): Promise<void> {
84
+ try {
85
+ await $`launchctl unload ${PLIST_PATH}`.quiet();
86
+ } catch {}
87
+ try {
88
+ await unlink(PLIST_PATH);
89
+ } catch {}
90
+ try {
91
+ await unlink(WRAPPER_PATH);
92
+ } catch {}
93
+ }
94
+
95
+ export async function isAgentLoaded(): Promise<boolean> {
96
+ try {
97
+ const result = await $`launchctl list ${LABEL}`.quiet();
98
+ return result.exitCode === 0;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ export async function restartAgent(): Promise<void> {
105
+ try {
106
+ await $`launchctl unload ${PLIST_PATH}`.quiet();
107
+ } catch {}
108
+ await $`launchctl load ${PLIST_PATH}`.quiet();
109
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Streamable HTTP MCP transport — stateless mode.
3
+ *
4
+ * Each request gets a fresh transport+server pair with no session ID
5
+ * generator. The SDK skips session validation when sessionIdGenerator
6
+ * is undefined, so clients can send `tools/call` or `tools/list`
7
+ * directly without a prior `initialize` handshake.
8
+ *
9
+ * This means server restarts never break existing MCP clients — the
10
+ * root cause of vault#56. The `initialize` method still works if a
11
+ * client sends it (the Server class handles it natively).
12
+ *
13
+ * Two modes:
14
+ * /mcp — unified, all vaults via `vault` param + list-vaults
15
+ * /vaults/{name}/mcp — scoped to one vault, no vault param
16
+ *
17
+ * Vault description is sent as the MCP server instruction.
18
+ * Read-only keys see fewer tools.
19
+ */
20
+
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
23
+ import {
24
+ ListToolsRequestSchema,
25
+ CallToolRequestSchema,
26
+ } from "@modelcontextprotocol/sdk/types.js";
27
+ import { generateUnifiedMcpTools, generateScopedMcpTools, getServerInstruction } from "./mcp-tools.ts";
28
+ import { isToolAllowed } from "./auth.ts";
29
+ import type { AuthResult } from "./auth.ts";
30
+ import type { McpToolDef } from "../core/src/mcp.ts";
31
+
32
+ /** Handle unified MCP at /mcp (all vaults). */
33
+ export async function handleUnifiedMcp(req: Request, auth: AuthResult): Promise<Response> {
34
+ const instruction = getServerInstruction();
35
+ return handleMcp(req, () => generateUnifiedMcpTools(), "parachute-vault", auth, instruction);
36
+ }
37
+
38
+ /** Handle scoped MCP at /vaults/{name}/mcp (single vault). */
39
+ export async function handleScopedMcp(req: Request, vaultName: string, auth: AuthResult): Promise<Response> {
40
+ const instruction = getServerInstruction(vaultName);
41
+ return handleMcp(req, () => generateScopedMcpTools(vaultName), `parachute-vault/${vaultName}`, auth, instruction);
42
+ }
43
+
44
+ async function handleMcp(
45
+ req: Request,
46
+ getTools: () => McpToolDef[],
47
+ serverName: string,
48
+ auth: AuthResult,
49
+ instruction: string,
50
+ ): Promise<Response> {
51
+ const { permission } = auth;
52
+ const transport = new WebStandardStreamableHTTPServerTransport({
53
+ sessionIdGenerator: undefined,
54
+ enableJsonResponse: true,
55
+ });
56
+
57
+ const server = new Server(
58
+ { name: serverName, version: "0.1.0" },
59
+ {
60
+ capabilities: { tools: {} },
61
+ instructions: instruction,
62
+ },
63
+ );
64
+
65
+ const mcpTools = getTools();
66
+
67
+ // For read-only keys, only list readable tools
68
+ const visibleTools = permission === "read"
69
+ ? mcpTools.filter((t) => isToolAllowed(t.name, "read"))
70
+ : mcpTools;
71
+
72
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
73
+ tools: visibleTools.map((t) => ({
74
+ name: t.name,
75
+ description: t.description,
76
+ inputSchema: t.inputSchema,
77
+ })),
78
+ }));
79
+
80
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
81
+ const { name, arguments: args } = request.params;
82
+
83
+ if (!isToolAllowed(name, permission)) {
84
+ return {
85
+ content: [{ type: "text" as const, text: `Forbidden: insufficient permissions to call ${name}` }],
86
+ isError: true,
87
+ };
88
+ }
89
+
90
+ const tool = mcpTools.find((t) => t.name === name);
91
+ if (!tool) {
92
+ return {
93
+ content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
94
+ isError: true,
95
+ };
96
+ }
97
+ try {
98
+ const result = tool.execute((args ?? {}) as Record<string, unknown>);
99
+ return {
100
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
101
+ };
102
+ } catch (err) {
103
+ const message = err instanceof Error ? err.message : "Unknown error";
104
+ return {
105
+ content: [{ type: "text" as const, text: `Error: ${message}` }],
106
+ isError: true,
107
+ };
108
+ }
109
+ });
110
+
111
+ await server.connect(transport);
112
+ return transport.handleRequest(req);
113
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * MCP tool generation for multi-vault.
3
+ *
4
+ * Wraps core tools with vault resolution (optional `vault` param) and
5
+ * overrides vault-info with actual vault config access.
6
+ */
7
+
8
+ import { generateMcpTools } from "../core/src/mcp.ts";
9
+ import type { McpToolDef } from "../core/src/mcp.ts";
10
+ import { readVaultConfig, writeVaultConfig, readGlobalConfig, listVaults as getVaultNames } from "./config.ts";
11
+ import { getVaultStore } from "./vault-store.ts";
12
+
13
+ /**
14
+ * Get the MCP server instruction for a vault (or the default vault).
15
+ * Sent once at session init — not per tool.
16
+ */
17
+ export function getServerInstruction(vaultName?: string): string {
18
+ const globalConfig = readGlobalConfig();
19
+ const name = vaultName ?? globalConfig.default_vault ?? "default";
20
+ const config = readVaultConfig(name);
21
+
22
+ const parts: string[] = [
23
+ `You are connected to Parachute Vault "${name}".`,
24
+ ];
25
+
26
+ if (config?.description) {
27
+ parts.push("", config.description);
28
+ }
29
+
30
+ return parts.join("\n");
31
+ }
32
+
33
+ /**
34
+ * Generate the unified MCP tool set.
35
+ * Each tool has an optional `vault` param that defaults to the default vault.
36
+ */
37
+ export function generateUnifiedMcpTools(): McpToolDef[] {
38
+ const globalConfig = readGlobalConfig();
39
+ const defaultVault = globalConfig.default_vault ?? "default";
40
+ const vaultNames = getVaultNames();
41
+ const multiVault = vaultNames.length > 1;
42
+
43
+ // Get tool definitions from core (using default vault for schema)
44
+ const defaultStore = getVaultStore(defaultVault);
45
+ const coreTools = generateMcpTools(defaultStore);
46
+
47
+ // Wrap each core tool with vault resolution
48
+ const tools: McpToolDef[] = coreTools.map((coreTool) => {
49
+ let description = coreTool.description;
50
+ if (multiVault) {
51
+ description += `\n\nMulti-vault: pass 'vault' to target a specific vault. Default: "${defaultVault}". Available: ${vaultNames.join(", ")}`;
52
+ }
53
+
54
+ const inputSchema = {
55
+ ...coreTool.inputSchema,
56
+ properties: {
57
+ vault: {
58
+ type: "string",
59
+ description: `Vault name (default: "${defaultVault}")`,
60
+ },
61
+ ...(coreTool.inputSchema as any).properties,
62
+ },
63
+ };
64
+
65
+ return {
66
+ name: coreTool.name,
67
+ description,
68
+ inputSchema,
69
+ execute: (params) => {
70
+ const vaultName = (params.vault as string) ?? defaultVault;
71
+ const config = readVaultConfig(vaultName);
72
+ if (!config) {
73
+ throw new Error(`Vault "${vaultName}" not found. Available: ${getVaultNames().join(", ")}`);
74
+ }
75
+ const store = getVaultStore(vaultName);
76
+ const vaultTools = generateMcpTools(store);
77
+ const tool = vaultTools.find((t) => t.name === coreTool.name)!;
78
+ const { vault: _, ...rest } = params;
79
+ return tool.execute(rest);
80
+ },
81
+ };
82
+ });
83
+
84
+ // Override vault-info with actual vault config access
85
+ overrideVaultInfo(tools, defaultVault);
86
+
87
+ // Add list-vaults (multi-vault only, not in core)
88
+ if (multiVault) {
89
+ tools.push({
90
+ name: "list-vaults",
91
+ description: "List all available vaults with their descriptions.",
92
+ inputSchema: { type: "object", properties: {} },
93
+ execute: () => {
94
+ const names = getVaultNames();
95
+ return names.map((name) => {
96
+ const config = readVaultConfig(name);
97
+ return {
98
+ name,
99
+ description: config?.description,
100
+ created_at: config?.created_at,
101
+ is_default: name === defaultVault,
102
+ };
103
+ });
104
+ },
105
+ });
106
+ }
107
+
108
+ return tools;
109
+ }
110
+
111
+ /**
112
+ * Generate MCP tools scoped to a single vault.
113
+ * No vault param — tools operate on that vault only.
114
+ */
115
+ export function generateScopedMcpTools(vaultName: string): McpToolDef[] {
116
+ const store = getVaultStore(vaultName);
117
+ const tools = generateMcpTools(store);
118
+
119
+ // Override vault-info with actual vault config access
120
+ overrideVaultInfo(tools, vaultName);
121
+
122
+ return tools;
123
+ }
124
+
125
+ /**
126
+ * Override vault-info's placeholder execute with real vault config access.
127
+ */
128
+ function overrideVaultInfo(tools: McpToolDef[], defaultVault: string): void {
129
+ const vaultInfo = tools.find((t) => t.name === "vault-info");
130
+ if (!vaultInfo) return;
131
+
132
+ vaultInfo.execute = (params) => {
133
+ const vaultName = (params.vault as string) ?? defaultVault;
134
+ const config = readVaultConfig(vaultName);
135
+ if (!config) throw new Error(`Vault "${vaultName}" not found`);
136
+
137
+ // Update description if provided
138
+ if (params.description !== undefined) {
139
+ config.description = params.description as string;
140
+ writeVaultConfig(config);
141
+ }
142
+
143
+ const result: any = {
144
+ name: config.name,
145
+ description: config.description ?? null,
146
+ };
147
+
148
+ if (params.include_stats) {
149
+ const store = getVaultStore(vaultName);
150
+ result.stats = store.getVaultStats();
151
+ }
152
+
153
+ return result;
154
+ };
155
+ }