@openparachute/vault 0.3.1 → 0.3.3

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 CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  **Parachute Vault is a self-hosted knowledge graph that any AI can read and write, over the open [MCP](https://modelcontextprotocol.io) protocol.** Your notes, tags, links, and attachments live on your machine — in plain SQLite databases under `~/.parachute/`, not in a vendor's cloud.
4
4
 
5
- Works with Claude, ChatGPT, Gemini, or any future MCP-capable AI. Switch tools without losing your knowledge. No vendor lock-in, no re-import step when the next model lands. One command to install; one OAuth consent to connect each AI client.
5
+ Today it works with **Claude Code, Codex, Goose, OpenCode, and any other local MCP client** same endpoint, your vault. Claude Code auto-wires on install; for the rest, point them at `http://127.0.0.1:1940/vault/default/mcp`.
6
+
7
+ Web AI connectors — **claude.ai**, **ChatGPT**, and **Gemini** — are coming in the next few weeks. Switch tools, keep your knowledge. No vendor lock-in, no re-import step when the next model lands.
6
8
 
7
9
  ## Quick start
8
10
 
@@ -20,7 +22,7 @@ bun install
20
22
  bun src/cli.ts vault init
21
23
  ```
22
24
 
23
- `vault init` creates a vault, generates an API key, starts a background daemon (launchd on Mac, systemd on Linux), and configures Claude Code's MCP — all in one command. Your API key is printed once at init; save it for connecting from other tools.
25
+ `vault init` creates a vault, generates an API key, starts a background daemon (launchd on Mac, systemd on Linux), and configures Claude Code's MCP — all in one command. Start a new Claude Code session and your vault's tools show up. For other local MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline, your own agent), point them at `http://127.0.0.1:1940/vault/default/mcp` — the API key is printed once at init; save it for anything that isn't Claude Code.
24
26
 
25
27
  For remote access from Claude Desktop or mobile apps, see [Deployment](#deployment) below.
26
28
 
@@ -88,11 +90,13 @@ The daemon binds `0.0.0.0:1940` (or whatever you set in `PORT`) and serves REST,
88
90
 
89
91
  ### Your API token
90
92
 
91
- The `pvt_...` token printed at init is the one baked into `~/.claude.json`. It's not stored anywhere retrievable save it if you need it for `curl`, cron, or any other script. Lost it? Just mint a new one: `parachute-vault tokens create`. Tokens are SHA-256 hashed at rest in each vault's `vault.db`.
93
+ `vault init` asks two explicit questions: (1) install vault as an MCP server in `~/.claude.json`? (2) also surface the API token so you can paste it into other MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline), scripts, or `curl`? Both default yes. Pass `--mcp` / `--no-mcp` and `--token` / `--no-token` for non-interactive installs.
94
+
95
+ If you said yes to (2), the `pvt_...` token is printed prominently at the end — it's the same token baked into `~/.claude.json` (if you also said yes to (1)). It's not stored anywhere retrievable — save it if you need it for `curl`, cron, or any other script. Lost it? Just mint a new one: `parachute-vault tokens create`. Tokens are SHA-256 hashed at rest in each vault's `vault.db`.
92
96
 
93
- ### Owner password prompt
97
+ ### Owner password (for OAuth, coming soon)
94
98
 
95
- Init pauses for one interactive prompt: "Set an owner password for OAuth consent?" The password is what the consent page asks for when Claude Desktop / Parachute Daily / any browser-OAuth client connects. You can skip it and set it later with `parachute-vault set-password`; without it, the consent page falls back to pasting a vault token. See [Connecting a client → Owner password](#owner-password-needed-for-oauth).
99
+ `vault init` doesn't prompt for an owner password — the password is only needed for OAuth consent, which is what browser-based clients (claude.ai, ChatGPT, Claude Desktop) use, and those paths are coming in the next few weeks. When you're ready to expose the vault publicly, set one with `parachute-vault set-password` (and optionally `parachute-vault 2fa enroll`). See [Connecting a client → Owner password](#owner-password-needed-for-oauth).
96
100
 
97
101
  ## Connecting a client
98
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -49,6 +49,7 @@ import type { VaultConfig } from "./config.ts";
49
49
  import { DATA_DIR } from "./config.ts";
50
50
  import { installAgent, uninstallAgent, isAgentLoaded, restartAgent } from "./launchd.ts";
51
51
  import { chooseMcpUrl } from "./mcp-install.ts";
52
+ import { buildInitSummaryLines } from "./init-summary.ts";
52
53
  import {
53
54
  runBackup,
54
55
  readLastBackup,
@@ -225,8 +226,14 @@ async function cmdInit(args: string[] = []) {
225
226
  // --no-mcp skips it without prompting. If both passed, --no-mcp wins
226
227
  // (safer default). Neither → prompt in a TTY, default-yes in a
227
228
  // non-TTY for back-compat with existing piped install scripts.
229
+ //
230
+ // --token / --no-token follow the same pattern for whether the API
231
+ // token is surfaced to the user at the end of init (for pasting into
232
+ // other MCP clients, scripts, or curl).
228
233
  const flagMcpOn = args.includes("--mcp");
229
234
  const flagMcpOff = args.includes("--no-mcp");
235
+ const flagTokenOn = args.includes("--token");
236
+ const flagTokenOff = args.includes("--no-token");
230
237
 
231
238
  const isMac = process.platform === "darwin";
232
239
  const isLinux = process.platform === "linux";
@@ -325,9 +332,16 @@ async function cmdInit(args: string[] = []) {
325
332
  console.log();
326
333
  }
327
334
 
328
- // 5b. Offer to set an owner password for OAuth consent, unless one is already set.
335
+ // 5b. Owner password is only needed for OAuth consent (browser-based
336
+ // clients like claude.ai / ChatGPT / Claude Desktop). Those paths are
337
+ // coming in the next few weeks; until then, skip the prompt. Users who
338
+ // want to expose the vault publicly today can set one manually via
339
+ // `parachute-vault set-password`.
329
340
  if (!hasOwnerPassword()) {
330
- await promptForOwnerPassword("Set an owner password for OAuth consent?");
341
+ console.log();
342
+ console.log("Public exposure + web-AI connectors (claude.ai, ChatGPT, etc.) are coming soon.");
343
+ console.log(" When you're ready to expose this vault publicly, run:");
344
+ console.log(" parachute-vault set-password # required for OAuth consent");
331
345
  }
332
346
 
333
347
  // 6. Install daemon (platform-aware). Idempotent — safe to re-run after
@@ -361,11 +375,43 @@ async function cmdInit(args: string[] = []) {
361
375
  } else if (flagMcpOn) {
362
376
  addMcp = true;
363
377
  } else if (process.stdin.isTTY) {
364
- addMcp = await confirm("Add Vault MCP to Claude Code (~/.claude.json)?", true);
378
+ addMcp = await confirm("Install Vault as an MCP server in Claude Code (~/.claude.json)?", true);
365
379
  } else {
366
380
  addMcp = true; // non-interactive: preserve the installable-via-pipe default
367
381
  }
368
382
 
383
+ // 7b. Surface an API token for other clients? (Codex, Goose, OpenCode,
384
+ // Cursor, Zed, Cline, scripts, curl.) Same flag/TTY precedence as MCP.
385
+ // Note: a token is always minted when addMcp is true (it gets baked into
386
+ // the ~/.claude.json entry); this prompt controls whether that token is
387
+ // printed prominently at the end so the user can paste it elsewhere.
388
+ let addToken: boolean;
389
+ if (flagTokenOff) {
390
+ addToken = false;
391
+ } else if (flagTokenOn) {
392
+ addToken = true;
393
+ } else if (process.stdin.isTTY) {
394
+ addToken = await confirm(
395
+ "Generate an API token for other MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline), scripts, or curl?",
396
+ true,
397
+ );
398
+ } else {
399
+ addToken = true; // non-interactive: default-yes matches addMcp default
400
+ }
401
+
402
+ // Mint a token if we need one (for the claude.json entry and/or for
403
+ // prominent display) and don't already have one from vault creation.
404
+ // Re-runs of init that opt in will mint a fresh token — old tokens
405
+ // continue to work; the user can `tokens revoke` the unused ones.
406
+ const defaultVault = globalConfig.default_vault || "default";
407
+ const needToken = addMcp || addToken;
408
+ if (needToken && !apiKey) {
409
+ const store = getVaultStore(defaultVault);
410
+ const { fullToken } = generateToken();
411
+ createToken(store.db, fullToken, { label: "init", permission: "full" });
412
+ apiKey = fullToken;
413
+ }
414
+
369
415
  if (addMcp) {
370
416
  installMcpConfig(apiKey);
371
417
  console.log(` MCP server added to ~/.claude.json`);
@@ -375,30 +421,21 @@ async function cmdInit(args: string[] = []) {
375
421
  }
376
422
 
377
423
  // 8. Summary
378
- console.log("\n---");
379
424
  const port = globalConfig.port || DEFAULT_PORT;
380
- if (apiKey) {
381
- console.log(`\nYour API token: ${apiKey}`);
382
- console.log(" Use this in Claude Desktop, curl, or any client.");
383
- console.log(" Pass via: Authorization: Bearer <token>");
384
- console.log(" Or via: X-API-Key: <token>");
385
- console.log("\nSave this — it will not be shown again.");
386
- }
387
-
388
- console.log(`\nConfig: ${CONFIG_DIR}`);
389
- console.log(`Server: http://${bindHost}:${port}`);
390
-
391
- console.log(`\nUsage examples:`);
392
- console.log(` curl http://localhost:${port}/health`);
393
- if (apiKey) {
394
- console.log(` curl -H "Authorization: Bearer ${apiKey}" http://localhost:${port}/api/notes`);
395
- }
396
-
397
- console.log(`\nNext steps:`);
398
- console.log(` parachute-vault status check everything is running`);
399
- console.log(` parachute-vault config view/edit configuration`);
425
+ const mcpUrl = `http://127.0.0.1:${port}/vault/${defaultVault}/mcp`;
426
+ const lines = buildInitSummaryLines({
427
+ addMcp,
428
+ addToken,
429
+ apiKey,
430
+ configDir: CONFIG_DIR,
431
+ bindHost,
432
+ port,
433
+ mcpUrl,
434
+ });
435
+ for (const line of lines) console.log(line);
400
436
  }
401
437
 
438
+
402
439
  async function promptForOwnerPassword(purpose: string): Promise<boolean> {
403
440
  console.log(`\n${purpose}`);
404
441
  console.log(" Used on the OAuth consent page to authorize third-party clients");
@@ -2066,7 +2103,11 @@ data, and debugging.
2066
2103
  ── Standard use ───────────────────────────────────────────────────────
2067
2104
 
2068
2105
  Setup:
2069
- parachute-vault init [--mcp | --no-mcp] Set up everything (one command, idempotent)
2106
+ parachute-vault init [--mcp|--no-mcp] [--token|--no-token]
2107
+ Set up everything (one command, idempotent).
2108
+ --mcp/--no-mcp controls the Claude Code MCP entry;
2109
+ --token/--no-token controls whether an API token is
2110
+ printed for pasting into other MCP clients / scripts.
2070
2111
  parachute-vault doctor Diagnose install/config issues
2071
2112
  parachute-vault uninstall [--wipe] [--yes]
2072
2113
  Remove daemon + MCP entry; --wipe also removes vaults, .env,
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Tests for `buildInitSummaryLines` — the post-install summary printed at the
3
+ * end of `vault init`. The summary branches on the (addMcp, addToken) decision
4
+ * matrix; these tests cover all four cells plus the token surfacing /
5
+ * Bearer-example rules.
6
+ */
7
+
8
+ import { describe, test, expect } from "bun:test";
9
+ import { buildInitSummaryLines } from "./init-summary.ts";
10
+
11
+ const baseInput = {
12
+ configDir: "/tmp/parachute",
13
+ bindHost: "127.0.0.1",
14
+ port: 1940,
15
+ mcpUrl: "http://127.0.0.1:1940/vault/default/mcp",
16
+ };
17
+
18
+ function lines(addMcp: boolean, addToken: boolean, apiKey: string | undefined) {
19
+ return buildInitSummaryLines({ ...baseInput, addMcp, addToken, apiKey });
20
+ }
21
+
22
+ describe("buildInitSummaryLines", () => {
23
+ describe("MCP=Y + token=Y (most common)", () => {
24
+ const out = lines(true, true, "pvt_abc123").join("\n");
25
+
26
+ test("prints token prominently", () => {
27
+ expect(out).toContain("Your API token: pvt_abc123");
28
+ });
29
+
30
+ test("notes token is baked into ~/.claude.json", () => {
31
+ expect(out).toContain("Baked into ~/.claude.json for Claude Code");
32
+ });
33
+
34
+ test("includes save-it-now warning", () => {
35
+ expect(out).toContain("Won't be shown again — save it now.");
36
+ });
37
+
38
+ test("includes Bearer curl example", () => {
39
+ expect(out).toContain(
40
+ 'curl -H "Authorization: Bearer pvt_abc123" http://localhost:1940/api/notes',
41
+ );
42
+ });
43
+
44
+ test("Next steps mentions starting a Claude Code session", () => {
45
+ expect(out).toContain("Start a new Claude Code session");
46
+ });
47
+ });
48
+
49
+ describe("MCP=Y + token=N (MCP wired, token not surfaced)", () => {
50
+ const out = lines(true, false, "pvt_secret").join("\n");
51
+
52
+ test("does not print the token prominently", () => {
53
+ expect(out).not.toContain("pvt_secret");
54
+ });
55
+
56
+ test("does not include the 'Baked into' bullet", () => {
57
+ expect(out).not.toContain("Baked into ~/.claude.json");
58
+ });
59
+
60
+ test("includes the tokens-create-later hint", () => {
61
+ expect(out).toContain("Token in ~/.claude.json");
62
+ expect(out).toContain("parachute vault tokens create");
63
+ });
64
+
65
+ test("omits the Bearer curl example", () => {
66
+ expect(out).not.toContain("Authorization: Bearer");
67
+ });
68
+
69
+ test("still shows the Claude-Code-session next step", () => {
70
+ expect(out).toContain("Start a new Claude Code session");
71
+ });
72
+ });
73
+
74
+ describe("MCP=N + token=Y (token only)", () => {
75
+ const out = lines(false, true, "pvt_xyz").join("\n");
76
+
77
+ test("prints token prominently", () => {
78
+ expect(out).toContain("Your API token: pvt_xyz");
79
+ });
80
+
81
+ test("omits the 'Baked into' bullet (no claude.json entry written)", () => {
82
+ expect(out).not.toContain("Baked into ~/.claude.json");
83
+ });
84
+
85
+ test("includes Bearer curl example", () => {
86
+ expect(out).toContain('Authorization: Bearer pvt_xyz');
87
+ });
88
+
89
+ test("Next steps points at any local MCP client", () => {
90
+ expect(out).toContain("Point any local MCP client");
91
+ expect(out).toContain("http://127.0.0.1:1940/vault/default/mcp");
92
+ });
93
+
94
+ test("Next steps offers mcp-install as a way back", () => {
95
+ expect(out).toContain("parachute-vault mcp-install");
96
+ });
97
+ });
98
+
99
+ describe("MCP=N + token=N (unreachable)", () => {
100
+ const out = lines(false, false, undefined).join("\n");
101
+
102
+ test("warns the vault is unreachable", () => {
103
+ expect(out).toContain("your vault isn't reachable by any client");
104
+ });
105
+
106
+ test("points to both recovery paths", () => {
107
+ expect(out).toContain("parachute-vault mcp-install");
108
+ expect(out).toContain("parachute vault tokens create");
109
+ });
110
+
111
+ test("does not print any token", () => {
112
+ expect(out).not.toContain("Your API token:");
113
+ expect(out).not.toMatch(/pvt_/);
114
+ });
115
+
116
+ test("omits the Bearer curl example", () => {
117
+ expect(out).not.toContain("Authorization: Bearer");
118
+ });
119
+ });
120
+
121
+ test("always prints Config: and Server: lines", () => {
122
+ for (const [addMcp, addToken] of [
123
+ [true, true],
124
+ [true, false],
125
+ [false, true],
126
+ [false, false],
127
+ ] as const) {
128
+ const out = lines(addMcp, addToken, addMcp || addToken ? "pvt_k" : undefined).join("\n");
129
+ expect(out).toContain("Config: /tmp/parachute");
130
+ expect(out).toContain("Server: http://127.0.0.1:1940");
131
+ }
132
+ });
133
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Pure helper for `vault init`'s post-install summary. Extracted from cli.ts
3
+ * so the (addMcp, addToken) decision-matrix branches can be unit-tested
4
+ * without side-effects from importing the CLI entrypoint.
5
+ */
6
+
7
+ export type InitSummaryInput = {
8
+ addMcp: boolean;
9
+ addToken: boolean;
10
+ apiKey: string | undefined;
11
+ configDir: string;
12
+ bindHost: string;
13
+ port: number;
14
+ mcpUrl: string;
15
+ };
16
+
17
+ /**
18
+ * Build the post-install summary lines for `vault init`, branched on the
19
+ * (addMcp, addToken) decision matrix:
20
+ *
21
+ * Y, Y → token baked into claude.json + printed prominently
22
+ * Y, N → token baked into claude.json, hint about `tokens create`
23
+ * N, Y → token printed prominently, no claude.json entry
24
+ * N, N → warning: vault unreachable; both recovery paths listed
25
+ */
26
+ export function buildInitSummaryLines(input: InitSummaryInput): string[] {
27
+ const { addMcp, addToken, apiKey, configDir, bindHost, port, mcpUrl } = input;
28
+ const lines: string[] = [];
29
+ lines.push("");
30
+ lines.push("---");
31
+
32
+ if (addMcp && addToken && apiKey) {
33
+ lines.push("");
34
+ lines.push(`Your API token: ${apiKey}`);
35
+ lines.push(` - Baked into ~/.claude.json for Claude Code ✓`);
36
+ lines.push(` - Paste into your other MCP client's config, or use as Authorization: Bearer <token>`);
37
+ lines.push(` - Won't be shown again — save it now.`);
38
+ } else if (addMcp && !addToken) {
39
+ lines.push("");
40
+ lines.push(
41
+ "Token in ~/.claude.json; run `parachute vault tokens create` later if you need one for other clients.",
42
+ );
43
+ } else if (!addMcp && addToken && apiKey) {
44
+ lines.push("");
45
+ lines.push(`Your API token: ${apiKey}`);
46
+ lines.push(` - Paste into your other MCP client's config, or use as Authorization: Bearer <token>`);
47
+ lines.push(` - Won't be shown again — save it now.`);
48
+ } else if (!addMcp && !addToken) {
49
+ lines.push("");
50
+ lines.push(
51
+ "You've skipped both MCP install and token generation — your vault isn't reachable by any client.",
52
+ );
53
+ lines.push(
54
+ " Add Claude Code later with `parachute-vault mcp-install`, or mint a token with `parachute vault tokens create`.",
55
+ );
56
+ }
57
+
58
+ lines.push("");
59
+ lines.push(`Config: ${configDir}`);
60
+ lines.push(`Server: http://${bindHost}:${port}`);
61
+
62
+ lines.push("");
63
+ lines.push(`Usage examples:`);
64
+ lines.push(` curl http://localhost:${port}/health`);
65
+ if (addToken && apiKey) {
66
+ lines.push(` curl -H "Authorization: Bearer ${apiKey}" http://localhost:${port}/api/notes`);
67
+ }
68
+
69
+ lines.push("");
70
+ lines.push(`Next steps:`);
71
+ if (addMcp) {
72
+ lines.push(` - Start a new Claude Code session — your Vault is already wired in. Try:`);
73
+ lines.push(` claude "Help me set up my parachute vault"`);
74
+ lines.push(` - Or point any other local MCP client (Codex, Goose, OpenCode, Cursor,`);
75
+ lines.push(` Zed, Cline, your own agent) at:`);
76
+ lines.push(` ${mcpUrl}`);
77
+ } else if (addToken) {
78
+ lines.push(` - Point any local MCP client (Codex, Goose, OpenCode, Cursor, Zed,`);
79
+ lines.push(` Cline, your own agent) at:`);
80
+ lines.push(` ${mcpUrl}`);
81
+ lines.push(` - Or add Claude Code back anytime: parachute-vault mcp-install`);
82
+ } else {
83
+ lines.push(` - Add Claude Code: parachute-vault mcp-install`);
84
+ lines.push(` - Mint a token: parachute vault tokens create`);
85
+ }
86
+ lines.push(` - Check status: parachute-vault status`);
87
+ lines.push(` - Edit config: parachute-vault config`);
88
+
89
+ return lines;
90
+ }
package/src/prompt.ts CHANGED
@@ -74,36 +74,52 @@ export async function askPassword(question: string): Promise<string> {
74
74
  }
75
75
  };
76
76
 
77
+ // Batch visible output per data event. On Bun 1.2.x, per-char writes
78
+ // can appear in bursts (keystrokes echoing late or out of order);
79
+ // coalescing to a single write per data event keeps the visible
80
+ // stream in lock-step with the captured input.
77
81
  const onData = (data: string) => {
78
82
  try {
83
+ let toWrite = "";
84
+ let done = false;
85
+ let aborted = false;
79
86
  for (const ch of data) {
80
87
  // Enter — done
81
88
  if (ch === "\r" || ch === "\n") {
82
- process.stdout.write("\n");
83
- cleanup();
84
- resolve(buf);
85
- return;
89
+ done = true;
90
+ break;
86
91
  }
87
92
  // Ctrl-C — abort
88
93
  if (ch === "\u0003") {
89
- process.stdout.write("\n");
90
- cleanup();
91
- process.exit(130);
94
+ aborted = true;
95
+ break;
92
96
  }
93
97
  // Backspace / DEL
94
98
  if (ch === "\u0008" || ch === "\u007f") {
95
99
  if (buf.length > 0) {
96
100
  buf = buf.slice(0, -1);
97
- process.stdout.write("\b \b");
101
+ toWrite += "\b \b";
98
102
  }
99
103
  continue;
100
104
  }
101
105
  // Printable
102
106
  if (ch >= " ") {
103
107
  buf += ch;
104
- process.stdout.write("*");
108
+ toWrite += "*";
105
109
  }
106
110
  }
111
+ if (toWrite) process.stdout.write(toWrite);
112
+ if (done) {
113
+ process.stdout.write("\n");
114
+ cleanup();
115
+ resolve(buf);
116
+ return;
117
+ }
118
+ if (aborted) {
119
+ process.stdout.write("\n");
120
+ cleanup();
121
+ process.exit(130);
122
+ }
107
123
  } catch (err) {
108
124
  cleanup();
109
125
  reject(err);