@openparachute/vault 0.6.1 → 0.6.2

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
@@ -22,7 +22,7 @@ bun install
22
22
  bun src/cli.ts vault init
23
23
  ```
24
24
 
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.
25
+ `vault init` creates a vault, starts a background daemon (launchd on Mac, systemd on Linux), and prints how to connect your AI — the web setup wizard URL, your vault's connector URL (`http://127.0.0.1:1940/vault/default/mcp`), and a ready-to-paste `claude mcp add` command. It does **not** write any client config for you by default. Pass `--configure-claude-code` if you want init to add the Claude Code MCP entry; pass `--token` if you also need a header-auth API token for non-OAuth clients (Codex, Goose, OpenCode, Cursor, Zed, Cline, scripts, `curl`). OAuth-capable clients just need the connector URL and sign in on first connect.
26
26
 
27
27
  For remote access from Claude Desktop or mobile apps, see [Deployment](#deployment) below.
28
28
 
@@ -93,11 +93,11 @@ The daemon binds `0.0.0.0:1940` (or whatever you set in `PORT`) and serves REST,
93
93
 
94
94
  ### `~/.claude.json`
95
95
 
96
- `vault init` adds one entry — `mcpServers["parachute-vault"]` — pointing at `http://127.0.0.1:<port>/vault/<default-vault>/mcp` with a baked-in `Authorization: Bearer <hub-jwt>` header (a hub-minted JWT — vault#282 Stage 2). Next Claude Code session picks it up; there's no further wiring. See [Connecting a client](#connecting-a-client) for rotating that token or pointing it elsewhere.
96
+ When you opt in (`--configure-claude-code`), `vault init` adds one entry — `mcpServers["parachute-vault"]` — pointing at `http://127.0.0.1:<port>/vault/<default-vault>/mcp`. By default that entry uses OAuth (browser sign-in on first connect); add `--token` and init bakes a scope-narrow `Authorization: Bearer <hub-jwt>` header instead (a hub-minted JWT — vault#282 Stage 2). Next Claude Code session picks it up. See [Connecting a client](#connecting-a-client) for rotating that token or pointing it elsewhere.
97
97
 
98
98
  ### Your API token
99
99
 
100
- `vault init` asks two explicit questions: (1) install vault as an MCP server in `~/.claude.json`? (2) also surface the access 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.
100
+ `vault init` asks two explicit questions: (1) write the Claude Code MCP entry in `~/.claude.json`? (2) also mint + surface a header-auth API token so you can paste it into non-OAuth MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline), scripts, or `curl`? **Both default no** — init's job is to get you to the web wizard and print the connector URL + a paste-ready `claude mcp add` command, not to write client config behind your back. Opt in with `--configure-claude-code` (alias `--mcp`) and/or `--token`; `--no-mcp` / `--no-token` are the explicit opt-outs for non-interactive installs.
101
101
 
102
102
  If you said yes to (2), the hub-issued JWT 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? Mint a fresh one with `parachute auth mint-token --scope vault:<name>:<verb>` (or rewire an MCP client with `parachute-vault mcp-install`, or use the admin SPA Tokens page). As of vault 0.5.0 (vault#282 Stage 2) vault no longer mints its own `pvt_*` tokens — minting is the hub's job.
103
103
 
@@ -126,7 +126,7 @@ As of 0.5.0 (vault#282 Stage 2) vault is a **pure hub resource-server**: both pa
126
126
 
127
127
  ### Claude Code
128
128
 
129
- `vault init` fully auto-configures `~/.claude.json` — there's nothing else to do. The entry it writes bakes in a hub-minted JWT rather than running the interactive OAuth browser flow:
129
+ `vault init` does **not** touch `~/.claude.json` by default connecting is self-serve (paste the `claude mcp add` command init prints, or add the connector in your client). To have init write the entry for you, pass `--configure-claude-code`. The entry it writes uses OAuth by default (browser sign-in on first connect); add `--token` and it bakes a scope-narrow hub-minted JWT instead:
130
130
 
131
131
  ```json
132
132
  {
@@ -140,9 +140,9 @@ As of 0.5.0 (vault#282 Stage 2) vault is a **pure hub resource-server**: both pa
140
140
  }
141
141
  ```
142
142
 
143
- Where `{name}` is `default` on a fresh install, or whatever vault you pointed `vault init` at. **First MCP call after `vault init` requires no browser handoff — Claude Code uses the baked-in token and the vault's tools show up in your next session.** This is intentional: for an owner connecting their own machine's vault to their own Claude Code, the token is already there and the OAuth browser handshake would add friction.
143
+ Where `{name}` is `default` on a fresh install, or whatever vault you pointed `vault init` at. **With `--configure-claude-code --token`, the first MCP call needs no browser handoff** — Claude Code uses the baked-in token and the vault's tools show up in your next session. Without `--token`, the opted-in entry uses OAuth and Claude Code does a one-time browser sign-in on first connect. This is a deliberate trade: OAuth-first by default (no long-lived token sitting in a dotfile), with the baked-token path one flag away for an owner wiring their own machine.
144
144
 
145
- To re-point Claude Code at a different vault, change `default_vault` in `~/.parachute/vault/config.yaml` and re-run `parachute-vault init` — which re-mints an API token and re-writes the `~/.claude.json` entry end-to-end. To rotate the token only, run `parachute-vault mcp-install` (defaults to `--mint`, which mints a fresh scope-narrow hub JWT via `~/.parachute/operator.token` and writes it into `~/.claude.json` with an `Authorization: Bearer …` header). See the [cookbook](#install-vault-mcp-into-a-client-config) section below for the full flag surface — token paste, scope narrowing, project-level install, multi-vault.
145
+ To re-point Claude Code at a different vault, change `default_vault` in `~/.parachute/vault/config.yaml` and re-run `parachute-vault init --configure-claude-code` (add `--token` to bake a fresh token). To rotate the token only, run `parachute-vault mcp-install` (defaults to `--mint`, which mints a fresh scope-narrow hub JWT via `~/.parachute/operator.token` and writes it into `~/.claude.json` with an `Authorization: Bearer …` header). See the [cookbook](#install-vault-mcp-into-a-client-config) section below for the full flag surface — token paste, scope narrowing, project-level install, multi-vault.
146
146
 
147
147
  ### Claude Desktop (OAuth)
148
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@modelcontextprotocol/sdk": "^1.12.1",
29
- "@openparachute/scope-guard": "^0.4.1-rc.1",
29
+ "@openparachute/scope-guard": "^0.4.1",
30
30
  "jose": "^6.2.2",
31
31
  "otpauth": "^9.5.0",
32
32
  "qrcode-terminal": "^0.12.0"
package/src/cli.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  buildMcpEntryPlan,
58
58
  chooseHubOrigin,
59
59
  chooseMcpUrl,
60
+ DEFAULT_HUB_LOOPBACK_PORT,
60
61
  detectHubPresence,
61
62
  detectInstallContext,
62
63
  mintHubJwt,
@@ -258,21 +259,60 @@ switch (command) {
258
259
  // Command implementations
259
260
  // ---------------------------------------------------------------------------
260
261
 
262
+ /**
263
+ * Resolve the origin to use for the web setup wizard link (`<origin>/admin/setup`).
264
+ *
265
+ * The wizard is served by the HUB, not by vault, so the loopback fallback must
266
+ * target the hub's fixed loopback port (1939 / $PARACHUTE_HUB_PORT) — NOT
267
+ * vault's listen port. `chooseHubOrigin` returns vault's loopback as its
268
+ * fallback, so we only reuse it when a real (env / expose-state) hub origin is
269
+ * configured; otherwise we synthesize the hub's loopback URL.
270
+ *
271
+ * `vaultPort` is vault's listen port — passed only so `chooseHubOrigin`'s
272
+ * loopback branch is well-formed; we discard that loopback URL in favor of the
273
+ * hub-port one.
274
+ */
275
+ function resolveHubOriginForWizard(vaultPort: number): string {
276
+ const { url, source } = chooseHubOrigin(vaultPort);
277
+ if (source === "loopback") {
278
+ // Guard against a non-numeric PARACHUTE_HUB_PORT producing
279
+ // `http://127.0.0.1:NaN` — mirror detectHubPresence's Number.isFinite guard.
280
+ const envPort = process.env.PARACHUTE_HUB_PORT
281
+ ? Number(process.env.PARACHUTE_HUB_PORT)
282
+ : undefined;
283
+ const hubPort = Number.isFinite(envPort) ? (envPort as number) : DEFAULT_HUB_LOOPBACK_PORT;
284
+ return `http://127.0.0.1:${hubPort}`;
285
+ }
286
+ return url;
287
+ }
288
+
261
289
  async function cmdInit(args: string[] = []) {
262
290
  ensureConfigDirSync();
263
291
 
264
- // Flags: --mcp installs MCP in ~/.claude.json without prompting;
265
- // --no-mcp skips it without prompting. If both passed, --no-mcp wins
266
- // (safer default). Neither prompt in a TTY, default-yes in a
267
- // non-TTY for back-compat with existing piped install scripts.
292
+ // Writing the Claude Code MCP config (~/.claude.json) is now OPT-IN
293
+ // (2026-06-23). init's primary job is to get the operator to the hub's
294
+ // web setup wizard and SURFACE the self-serve connection info (connector
295
+ // URL + a ready-to-paste `claude mcp add` command), NOT to silently write
296
+ // a config file as a side effect of setup. The site no longer claims
297
+ // "Claude Code is auto-configured," so the install code stops doing it by
298
+ // default.
299
+ //
300
+ // Opt in with --configure-claude-code (aliases --mcp-install, --mcp) to
301
+ // have init write the entry for you. --no-mcp is retained as the explicit
302
+ // "definitely don't" form (and wins if both are passed — safer default).
303
+ // The standalone `parachute-vault mcp-install` command is unchanged — it
304
+ // remains the canonical explicit opt-in path.
268
305
  //
269
- // --token / --no-token follow the same pattern for whether the API
270
- // token is surfaced to the user at the end of init (for pasting into
271
- // other MCP clients, scripts, or curl).
306
+ // --token / --no-token control whether init ALSO mints + surfaces a
307
+ // header-auth API token (for pasting into non-OAuth MCP clients, scripts,
308
+ // or curl). Default stays off.
272
309
  //
273
310
  // --vault-name <name> skips the name prompt for non-interactive installs
274
311
  // (validated up front; exits non-zero on invalid input).
275
- const flagMcpOn = args.includes("--mcp");
312
+ const flagMcpOn =
313
+ args.includes("--configure-claude-code") ||
314
+ args.includes("--mcp-install") ||
315
+ args.includes("--mcp");
276
316
  const flagMcpOff = args.includes("--no-mcp");
277
317
  const flagTokenOn = args.includes("--token");
278
318
  const flagTokenOff = args.includes("--no-token");
@@ -507,19 +547,28 @@ async function cmdInit(args: string[] = []) {
507
547
  const bindHost = resolveBindHostname(process.env);
508
548
  console.log(` Listening on http://${bindHost}:${globalConfig.port || DEFAULT_PORT}`);
509
549
 
510
- // 7. Install MCP for Claude Code (with token for auth) user confirms
511
- // unless --mcp / --no-mcp explicitly passed. Writing to ~/.claude.json
512
- // is a side effect some users don't want; default-yes in a TTY since
513
- // most users installing vault want Claude Code to see it, but ask.
550
+ // 7. Optionally write the Claude Code MCP config (~/.claude.json). This is
551
+ // OPT-IN as of 2026-06-23 (see the flag-parsing note above). init's job is
552
+ // to point the operator at the web wizard + surface the self-serve connect
553
+ // info; it does NOT write ~/.claude.json by default. Resolution:
554
+ // --no-mcp → false (explicit "don't")
555
+ // --configure-claude-code / --mcp → true (explicit opt-in)
556
+ // TTY, no flag → ask (default NO — opt-in, not -out)
557
+ // non-TTY, no flag → false (no silent side effect in
558
+ // piped installs; the connect info is
559
+ // printed for copy-paste instead)
514
560
  let addMcp: boolean;
515
561
  if (flagMcpOff) {
516
562
  addMcp = false;
517
563
  } else if (flagMcpOn) {
518
564
  addMcp = true;
519
565
  } else if (process.stdin.isTTY) {
520
- addMcp = await confirm("Install Vault as an MCP server in Claude Code (~/.claude.json)?", true);
566
+ addMcp = await confirm(
567
+ "Also write the Claude Code MCP config now (~/.claude.json)? (you can always copy-paste the command below later)",
568
+ false,
569
+ );
521
570
  } else {
522
- addMcp = true; // non-interactive: preserve the installable-via-pipe default
571
+ addMcp = false; // non-interactive: no silent ~/.claude.json write
523
572
  }
524
573
 
525
574
  // 7b. Mint an API token for the header-auth / script use case? (Codex,
@@ -583,14 +632,22 @@ async function cmdInit(args: string[] = []) {
583
632
  if (!apiKey) {
584
633
  console.log(` No token baked in — you'll sign in via OAuth on first connect.`);
585
634
  }
586
- } else {
587
- console.log(" Skipped adding MCP to ~/.claude.json.");
588
- console.log(" Run `parachute-vault mcp-install` later if you want it.");
589
635
  }
636
+ // No else: when the operator didn't opt in, the init summary below surfaces
637
+ // the connector URL + a copy-paste `claude mcp add` command instead of a
638
+ // "skipped" line — that's the self-serve path.
590
639
 
591
640
  // 8. Summary
592
641
  const port = globalConfig.port || DEFAULT_PORT;
593
- const mcpUrl = `http://127.0.0.1:${port}/vault/${defaultVault}/mcp`;
642
+ // Connector URL surfaced for self-serve copy-paste. Hub-origin / expose-state
643
+ // aware (chooseMcpUrl), so it's the URL a remote Claude Code / other client
644
+ // actually reaches, not a bare loopback. The init-summary prints a
645
+ // ready-to-paste `claude mcp add ...` built from this.
646
+ const { url: connectorUrl } = chooseMcpUrl(defaultVault, port);
647
+ // Web setup wizard lives on the hub at <hub-origin>/admin/setup. Resolve the
648
+ // hub origin the same way (env / expose-state / loopback); the loopback form
649
+ // points at the co-located hub's fixed port (1939), not vault's listen port.
650
+ const wizardUrl = `${resolveHubOriginForWizard(port)}/admin/setup`;
594
651
  // Probe whether a hub is present so the summary's "opted into a token but
595
652
  // none minted" copy reflects reality: under a hub the vault is reachable via
596
653
  // browser OAuth even with no header-auth token (#445). Only matters for the
@@ -603,7 +660,8 @@ async function cmdInit(args: string[] = []) {
603
660
  configDir: CONFIG_DIR,
604
661
  bindHost,
605
662
  port,
606
- mcpUrl,
663
+ mcpUrl: connectorUrl,
664
+ wizardUrl,
607
665
  vaultName: defaultVault,
608
666
  noTokenGuidance: credentialGuidance,
609
667
  hubPresent,
@@ -3714,12 +3772,19 @@ data, and debugging.
3714
3772
  ── Standard use ───────────────────────────────────────────────────────
3715
3773
 
3716
3774
  Setup:
3717
- parachute-vault init [--mcp|--no-mcp] [--token|--no-token] [--vault-name <name>]
3718
- [--autostart|--no-autostart]
3719
- Set up everything (one command, idempotent).
3720
- --mcp/--no-mcp controls the Claude Code MCP entry (written
3721
- for per-user OAuth by default no baked token; sign in on
3722
- first connect). --token opts into ALSO minting a scope-narrow
3775
+ parachute-vault init [--configure-claude-code|--no-mcp] [--token|--no-token]
3776
+ [--vault-name <name>] [--autostart|--no-autostart]
3777
+ Set up everything (one command, idempotent). init's
3778
+ job is to get you to the web setup wizard and surface
3779
+ your connector URL + a ready-to-paste \`claude mcp add\`
3780
+ command it does NOT write the Claude Code MCP config
3781
+ (~/.claude.json) by default. Pass --configure-claude-code
3782
+ (alias --mcp-install / --mcp) to opt in and have init
3783
+ write that entry for you (per-user OAuth — no baked token;
3784
+ sign in on first connect); --no-mcp is the explicit "don't".
3785
+ The standalone \`parachute-vault mcp-install\` command remains
3786
+ the canonical way to wire Claude Code anytime.
3787
+ --token opts into ALSO minting a scope-narrow
3723
3788
  header-auth token (vault:<name>:read) for non-OAuth clients /
3724
3789
  scripts; --no-token (the default) skips minting entirely.
3725
3790
  --vault-name skips the prompt and names the vault
@@ -1,8 +1,14 @@
1
1
  /**
2
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.
3
+ * end of `vault init`.
4
+ *
5
+ * 2026-06-23 messaging realignment: the site no longer claims "Claude Code is
6
+ * auto-configured," and init no longer writes ~/.claude.json by default. The
7
+ * summary now (1) leads with the web setup wizard hand-off and (2) always
8
+ * surfaces the self-serve connect info — the connector URL plus a
9
+ * ready-to-paste `claude mcp add` command — so a Claude Code user opts in by
10
+ * copy-paste rather than via a silent side effect. These tests pin that copy
11
+ * across the (addMcp, addToken) decision matrix.
6
12
  */
7
13
 
8
14
  import { describe, test, expect } from "bun:test";
@@ -13,6 +19,7 @@ const baseInput = {
13
19
  bindHost: "127.0.0.1",
14
20
  port: 1940,
15
21
  mcpUrl: "http://127.0.0.1:1940/vault/default/mcp",
22
+ wizardUrl: "http://127.0.0.1:1939/admin/setup",
16
23
  vaultName: "default",
17
24
  };
18
25
 
@@ -21,161 +28,154 @@ function lines(addMcp: boolean, addToken: boolean, apiKey: string | undefined) {
21
28
  }
22
29
 
23
30
  describe("buildInitSummaryLines", () => {
24
- describe("MCP=Y + token=Y (most common)", () => {
25
- const out = lines(true, true, "pvt_abc123").join("\n");
31
+ // The wizard hand-off + self-serve connect info are the load-bearing pieces
32
+ // of the new messaging — they must appear in every branch.
33
+ describe("always surfaces the wizard + the copy-paste connect info", () => {
34
+ for (const [addMcp, addToken] of [
35
+ [true, true],
36
+ [true, false],
37
+ [false, true],
38
+ [false, false],
39
+ ] as const) {
40
+ const out = lines(addMcp, addToken, addMcp || addToken ? "pvt_k" : undefined).join("\n");
26
41
 
27
- test("prints token prominently", () => {
28
- expect(out).toContain("Your API token: pvt_abc123");
29
- });
42
+ test(`(addMcp=${addMcp}, addToken=${addToken}) prints the web wizard URL prominently`, () => {
43
+ expect(out).toContain("Finish setup in your browser:");
44
+ expect(out).toContain("http://127.0.0.1:1939/admin/setup");
45
+ });
30
46
 
31
- test("notes token is baked into ~/.claude.json", () => {
32
- expect(out).toContain("Baked into ~/.claude.json for Claude Code");
33
- });
47
+ test(`(addMcp=${addMcp}, addToken=${addToken}) surfaces the connector URL`, () => {
48
+ expect(out).toContain("http://127.0.0.1:1940/vault/default/mcp");
49
+ });
34
50
 
35
- test("includes save-it-now warning", () => {
36
- expect(out).toContain("Won't be shown again — save it now.");
51
+ test(`(addMcp=${addMcp}, addToken=${addToken}) always prints Config: and Server: lines`, () => {
52
+ expect(out).toContain("Config: /tmp/parachute");
53
+ expect(out).toContain("Server: http://127.0.0.1:1940");
54
+ });
55
+ }
56
+ });
57
+
58
+ // The new DEFAULT init path — no MCP write, no token minted (per-user OAuth).
59
+ // init pointed the operator at the wizard and surfaced the copy-paste connect
60
+ // info; it did NOT write ~/.claude.json.
61
+ describe("DEFAULT (addMcp=N, token=N) — wizard hand-off + copy-paste opt-in", () => {
62
+ const out = lines(false, false, undefined).join("\n");
63
+
64
+ test("does NOT claim Claude Code is already wired in", () => {
65
+ expect(out).not.toContain("already wired in");
66
+ expect(out).not.toContain("Baked into ~/.claude.json");
37
67
  });
38
68
 
39
- test("includes Bearer curl example", () => {
69
+ test("offers the ready-to-paste `claude mcp add` opt-in command", () => {
40
70
  expect(out).toContain(
41
- 'curl -H "Authorization: Bearer pvt_abc123" http://localhost:1940/api/notes',
71
+ "claude mcp add --transport http parachute-vault http://127.0.0.1:1940/vault/default/mcp",
42
72
  );
43
73
  });
44
74
 
45
- test("Next steps mentions starting a Claude Code session", () => {
46
- expect(out).toContain("Start a new Claude Code session");
47
- });
48
- });
49
-
50
- describe("MCP=Y + token=N (MCP wired, token not surfaced)", () => {
51
- const out = lines(true, false, "pvt_secret").join("\n");
52
-
53
- test("does not print the token prominently", () => {
54
- expect(out).not.toContain("pvt_secret");
75
+ test("points at the guided installer as an alternative", () => {
76
+ expect(out).toContain("parachute-vault mcp-install");
55
77
  });
56
78
 
57
- test("does not include the 'Baked into' bullet", () => {
58
- expect(out).not.toContain("Baked into ~/.claude.json");
79
+ test("frames OAuth-first connect no token needed", () => {
80
+ expect(out).toContain("no token needed, you'll sign in on first use");
59
81
  });
60
82
 
61
- test("includes the mcp-install-later hint", () => {
62
- expect(out).toContain("Token in ~/.claude.json");
63
- expect(out).toContain("parachute-vault mcp-install");
83
+ test("offers the scope-narrow opt-in mint for scripts (full vault:<name>:read, never admin)", () => {
84
+ // Must be the three-segment named-resource form the hub mint-token model
85
+ // requires — a bare `vault:read` would mint a malformed scope (vault#443).
86
+ expect(out).toContain("parachute auth mint-token --scope vault:default:read");
87
+ expect(out).not.toContain("vault:admin");
64
88
  });
65
89
 
66
- test("omits the Bearer curl example", () => {
90
+ test("does not print any token", () => {
91
+ expect(out).not.toContain("Your API token:");
92
+ expect(out).not.toMatch(/pvt_/);
67
93
  expect(out).not.toContain("Authorization: Bearer");
68
94
  });
69
95
 
70
- test("still shows the Claude-Code-session next step", () => {
71
- expect(out).toContain("Start a new Claude Code session");
96
+ test("threads a non-default vault name into the mint-token scope + connector URL", () => {
97
+ const out2 = buildInitSummaryLines({
98
+ ...baseInput,
99
+ vaultName: "journal",
100
+ mcpUrl: "http://127.0.0.1:1940/vault/journal/mcp",
101
+ addMcp: false,
102
+ addToken: false,
103
+ apiKey: undefined,
104
+ }).join("\n");
105
+ expect(out2).toContain("parachute auth mint-token --scope vault:journal:read");
106
+ expect(out2).toContain("http://127.0.0.1:1940/vault/journal/mcp");
72
107
  });
73
108
  });
74
109
 
75
- describe("MCP=N + token=Y (token only)", () => {
76
- const out = lines(false, true, "pvt_xyz").join("\n");
77
-
78
- test("prints token prominently", () => {
79
- expect(out).toContain("Your API token: pvt_xyz");
80
- });
81
-
82
- test("omits the 'Baked into' bullet (no claude.json entry written)", () => {
83
- expect(out).not.toContain("Baked into ~/.claude.json");
84
- });
110
+ // Opt-in: operator passed --configure-claude-code, so init DID write the entry.
111
+ describe("opted into MCP write (addMcp=Y, token=N, OAuth)", () => {
112
+ const out = lines(true, false, undefined).join("\n");
85
113
 
86
- test("includes Bearer curl example", () => {
87
- expect(out).toContain('Authorization: Bearer pvt_xyz');
114
+ test("tells the user Claude Code is already wired in", () => {
115
+ expect(out).toContain("Claude Code is already wired in");
88
116
  });
89
117
 
90
- test("Next steps points at any local MCP client", () => {
91
- expect(out).toContain("Point any local MCP client");
118
+ test("still surfaces the connector URL for other clients", () => {
92
119
  expect(out).toContain("http://127.0.0.1:1940/vault/default/mcp");
93
120
  });
94
121
 
95
- test("Next steps offers mcp-install as a way back", () => {
96
- expect(out).toContain("parachute-vault mcp-install");
122
+ test("does NOT print or imply any minted token", () => {
123
+ expect(out).not.toContain("Your API token:");
124
+ expect(out).not.toContain("Authorization: Bearer");
97
125
  });
98
126
  });
99
127
 
100
- // vault#442: the DEFAULT init path MCP wired, NO token minted (per-user
101
- // OAuth). The summary must LEAD with the OAuth connect path, never mint, and
102
- // never surface the old "no token issued" failure copy.
103
- describe("MCP=Y + no token (vault#442 OAuth default)", () => {
104
- const out = lines(true, false, undefined).join("\n");
105
-
106
- test("leads with the OAuth connect message — no token needed", () => {
107
- expect(out).toContain("no token needed, you'll sign in on first use");
108
- });
109
-
110
- test("tells the user Claude Code is already wired in", () => {
111
- expect(out).toContain("Claude Code is already wired in");
112
- });
128
+ describe("opted into MCP write + token minted (addMcp=Y, token=Y)", () => {
129
+ const out = lines(true, true, "pvt_abc123").join("\n");
113
130
 
114
- test("shows the OAuth `claude mcp add` command for other clients", () => {
115
- expect(out).toContain(
116
- "claude mcp add --transport http parachute-vault http://127.0.0.1:1940/vault/default/mcp",
117
- );
131
+ test("prints token prominently", () => {
132
+ expect(out).toContain("Your API token: pvt_abc123");
118
133
  });
119
134
 
120
- test("offers the scope-narrow opt-in mint for scripts (full vault:<name>:read, never admin)", () => {
121
- // Must be the three-segment named-resource form the hub mint-token model
122
- // requires — a bare `vault:read` would mint a malformed scope (vault#443).
123
- expect(out).toContain("parachute auth mint-token --scope vault:default:read");
124
- expect(out).not.toContain("--scope vault:read ");
125
- expect(out).not.toMatch(/--scope vault:read$/m);
126
- expect(out).not.toContain("vault:admin");
135
+ test("notes token is baked into ~/.claude.json", () => {
136
+ expect(out).toContain("Baked into ~/.claude.json for Claude Code");
127
137
  });
128
138
 
129
- test("does NOT print or imply any minted token", () => {
130
- expect(out).not.toContain("Your API token:");
131
- expect(out).not.toContain("Baked into ~/.claude.json");
132
- expect(out).not.toContain("Authorization: Bearer");
139
+ test("includes save-it-now warning", () => {
140
+ expect(out).toContain("Won't be shown again — save it now.");
133
141
  });
134
142
 
135
- test("does NOT surface the old no-token-issued failure copy", () => {
136
- expect(out).not.toContain("No token issued");
143
+ test("includes Bearer curl example", () => {
144
+ expect(out).toContain(
145
+ 'curl -H "Authorization: Bearer pvt_abc123" http://localhost:1940/api/notes',
146
+ );
137
147
  });
138
148
 
139
- test("threads a non-default vault name into the mint-token scope", () => {
140
- const out2 = buildInitSummaryLines({
141
- ...baseInput,
142
- vaultName: "journal",
143
- mcpUrl: "http://127.0.0.1:1940/vault/journal/mcp",
144
- addMcp: true,
145
- addToken: false,
146
- apiKey: undefined,
147
- }).join("\n");
148
- expect(out2).toContain("parachute auth mint-token --scope vault:journal:read");
149
- expect(out2).not.toContain("vault:default:read");
149
+ test("Next steps mentions starting a Claude Code session", () => {
150
+ expect(out).toContain("Start a new Claude Code session");
150
151
  });
151
152
  });
152
153
 
153
- describe("MCP=N + token=N (OAuth default, Claude Code not wired)", () => {
154
- const out = lines(false, false, undefined).join("\n");
154
+ describe("token only, no MCP write (addMcp=N, token=Y, minted)", () => {
155
+ const out = lines(false, true, "pvt_xyz").join("\n");
155
156
 
156
- test("frames skipping the MCP entry as OAuth-first, not 'unreachable'", () => {
157
- expect(out).toContain("uses per-user OAuth, no token needed");
158
- expect(out).not.toContain("your vault isn't reachable by any client");
157
+ test("prints token prominently", () => {
158
+ expect(out).toContain("Your API token: pvt_xyz");
159
159
  });
160
160
 
161
- test("points to mcp-install (no token-minting framing)", () => {
162
- expect(out).toContain("parachute-vault mcp-install");
163
- expect(out).not.toContain("mints a hub JWT");
161
+ test("omits the 'Baked into' bullet (no claude.json entry written)", () => {
162
+ expect(out).not.toContain("Baked into ~/.claude.json");
164
163
  });
165
164
 
166
- test("does not print any token", () => {
167
- expect(out).not.toContain("Your API token:");
168
- expect(out).not.toMatch(/pvt_/);
165
+ test("includes Bearer curl example", () => {
166
+ expect(out).toContain("Authorization: Bearer pvt_xyz");
169
167
  });
170
168
 
171
- test("omits the Bearer curl example", () => {
172
- expect(out).not.toContain("Authorization: Bearer");
169
+ test("surfaces the connector URL + a copy-paste Claude Code opt-in", () => {
170
+ expect(out).toContain("http://127.0.0.1:1940/vault/default/mcp");
171
+ expect(out).toContain(
172
+ "claude mcp add --transport http parachute-vault http://127.0.0.1:1940/vault/default/mcp",
173
+ );
173
174
  });
174
175
  });
175
176
 
176
- // Explicit opt-in but no hub reachable to mint (vault#282 Stage 2 path,
177
- // reached only when the operator passes --token without a hub).
178
- describe("MCP=N + token=Y but no hub (opt-in mint failed, standalone)", () => {
177
+ // Explicit opt-in to a token but no hub reachable to mint (vault#282 Stage 2).
178
+ describe("token opt-in but no hub (standalone, mint failed)", () => {
179
179
  const out = buildInitSummaryLines({
180
180
  ...baseInput,
181
181
  addMcp: false,
@@ -198,12 +198,14 @@ describe("buildInitSummaryLines", () => {
198
198
  test("does NOT claim the vault is reachable (no hub present)", () => {
199
199
  expect(out).not.toContain("Your vault is still reachable");
200
200
  });
201
+
202
+ test("still surfaces the connector URL for self-serve connect", () => {
203
+ expect(out).toContain("http://127.0.0.1:1940/vault/default/mcp");
204
+ });
201
205
  });
202
206
 
203
- // #445: opted into a token, none minted, but a HUB IS PRESENT. The vault is
204
- // reachable via the hub's browser OAuth flow even with no header-auth token,
205
- // so the standalone "isn't reachable" framing would be false here.
206
- describe("MCP=N + token=Y, no token minted, but hub present (#445)", () => {
207
+ // #445: opted into a token, none minted, but a HUB IS PRESENT.
208
+ describe("token opt-in, none minted, hub present (#445)", () => {
207
209
  const out = buildInitSummaryLines({
208
210
  ...baseInput,
209
211
  addMcp: false,
@@ -227,22 +229,20 @@ describe("buildInitSummaryLines", () => {
227
229
  expect(out).not.toContain("Once a hub is running");
228
230
  expect(out).not.toContain("VAULT_AUTH_TOKEN");
229
231
  });
230
-
231
- test("never claims the vault isn't reachable by any client", () => {
232
- expect(out).not.toContain("isn't reachable by any client");
233
- });
234
232
  });
235
233
 
236
- test("always prints Config: and Server: lines", () => {
237
- for (const [addMcp, addToken] of [
238
- [true, true],
239
- [true, false],
240
- [false, true],
241
- [false, false],
242
- ] as const) {
243
- const out = lines(addMcp, addToken, addMcp || addToken ? "pvt_k" : undefined).join("\n");
244
- expect(out).toContain("Config: /tmp/parachute");
245
- expect(out).toContain("Server: http://127.0.0.1:1940");
246
- }
234
+ // Defensive: the summary must still render coherently if no wizard URL is
235
+ // supplied (e.g. an older caller / a hub-origin resolution failure).
236
+ test("omits the wizard hand-off cleanly when wizardUrl is absent", () => {
237
+ const out = buildInitSummaryLines({
238
+ ...baseInput,
239
+ wizardUrl: undefined,
240
+ addMcp: false,
241
+ addToken: false,
242
+ apiKey: undefined,
243
+ }).join("\n");
244
+ expect(out).not.toContain("Finish setup in your browser:");
245
+ // The connect info is still surfaced.
246
+ expect(out).toContain("http://127.0.0.1:1940/vault/default/mcp");
247
247
  });
248
248
  });