@openparachute/vault 0.6.0-rc.1 → 0.6.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 (91) hide show
  1. package/.parachute/module.json +14 -3
  2. package/README.md +7 -7
  3. package/core/src/core.test.ts +279 -26
  4. package/core/src/expand-visibility.test.ts +102 -0
  5. package/core/src/expand.ts +31 -3
  6. package/core/src/indexed-fields.ts +1 -1
  7. package/core/src/link-count.test.ts +301 -0
  8. package/core/src/links.ts +97 -2
  9. package/core/src/mcp.ts +201 -33
  10. package/core/src/notes.ts +44 -8
  11. package/core/src/obsidian-alignment.test.ts +375 -0
  12. package/core/src/obsidian.ts +234 -14
  13. package/core/src/portable-md.test.ts +40 -0
  14. package/core/src/portable-md.ts +142 -16
  15. package/core/src/schema.ts +58 -11
  16. package/core/src/store.ts +69 -22
  17. package/core/src/tag-expand-axis.test.ts +301 -0
  18. package/core/src/tag-hierarchy.ts +80 -0
  19. package/core/src/tag-schemas.ts +61 -46
  20. package/core/src/triggers-store.test.ts +100 -0
  21. package/core/src/triggers-store.ts +165 -0
  22. package/core/src/types.ts +68 -4
  23. package/core/src/vault-projection.ts +20 -0
  24. package/core/src/wikilinks.ts +2 -2
  25. package/package.json +2 -3
  26. package/src/admin-spa.test.ts +100 -10
  27. package/src/admin-spa.ts +48 -3
  28. package/src/auth-hub-jwt.test.ts +8 -1
  29. package/src/auth-status.ts +2 -2
  30. package/src/auth.test.ts +39 -3
  31. package/src/auth.ts +31 -2
  32. package/src/auto-transcribe.test.ts +51 -0
  33. package/src/auto-transcribe.ts +24 -6
  34. package/src/autostart.test.ts +75 -0
  35. package/src/autostart.ts +84 -0
  36. package/src/cli.ts +434 -140
  37. package/src/config.test.ts +109 -0
  38. package/src/config.ts +157 -10
  39. package/src/export-watch.test.ts +23 -0
  40. package/src/export-watch.ts +14 -0
  41. package/src/git-preflight.test.ts +70 -0
  42. package/src/git-preflight.ts +68 -0
  43. package/src/hub-jwt.test.ts +75 -2
  44. package/src/hub-jwt.ts +43 -6
  45. package/src/init-summary.test.ts +120 -5
  46. package/src/init-summary.ts +67 -25
  47. package/src/live-match.test.ts +198 -0
  48. package/src/live-match.ts +310 -0
  49. package/src/mcp-install.test.ts +93 -0
  50. package/src/mcp-install.ts +106 -0
  51. package/src/mcp-tools.ts +80 -7
  52. package/src/mirror-config.test.ts +14 -0
  53. package/src/mirror-config.ts +11 -0
  54. package/src/mirror-import.test.ts +110 -0
  55. package/src/mirror-import.ts +71 -13
  56. package/src/mirror-manager.test.ts +51 -0
  57. package/src/mirror-manager.ts +73 -11
  58. package/src/mirror-routes.test.ts +463 -1
  59. package/src/mirror-routes.ts +474 -4
  60. package/src/oauth-discovery.test.ts +55 -0
  61. package/src/oauth-discovery.ts +24 -5
  62. package/src/routes.ts +696 -121
  63. package/src/routing.test.ts +451 -5
  64. package/src/routing.ts +113 -5
  65. package/src/scopes.ts +1 -1
  66. package/src/server.ts +66 -4
  67. package/src/storage.test.ts +162 -0
  68. package/src/subscribe.test.ts +588 -0
  69. package/src/subscribe.ts +248 -0
  70. package/src/subscriptions.ts +295 -0
  71. package/src/tag-expand-routes.test.ts +45 -0
  72. package/src/tag-scope.ts +68 -1
  73. package/src/token-store.ts +7 -7
  74. package/src/transcription-worker.test.ts +471 -5
  75. package/src/transcription-worker.ts +212 -44
  76. package/src/triggers-api.test.ts +533 -0
  77. package/src/triggers-api.ts +295 -0
  78. package/src/triggers.ts +93 -7
  79. package/src/usage.test.ts +362 -0
  80. package/src/usage.ts +318 -0
  81. package/src/vault-create.test.ts +340 -12
  82. package/src/vault-name.test.ts +61 -3
  83. package/src/vault-name.ts +62 -14
  84. package/src/vault-remove.test.ts +187 -0
  85. package/src/vault-store.ts +10 -3
  86. package/src/vault.test.ts +1353 -62
  87. package/web/ui/dist/assets/index-CGL256oe.js +60 -0
  88. package/web/ui/dist/assets/index-J0pVP7I-.css +1 -0
  89. package/web/ui/dist/index.html +2 -2
  90. package/web/ui/dist/assets/index-DBe8Xiah.css +0 -1
  91. package/web/ui/dist/assets/index-DDRo6F4u.js +0 -60
package/src/cli.ts CHANGED
@@ -56,8 +56,11 @@ import {
56
56
  buildMcpConfigJson,
57
57
  buildMcpEntryPlan,
58
58
  chooseHubOrigin,
59
+ chooseMcpUrl,
60
+ detectHubPresence,
59
61
  detectInstallContext,
60
62
  mintHubJwt,
63
+ noOperatorTokenGuidance,
61
64
  readOperatorToken,
62
65
  removeMcpConfig,
63
66
  resolveInstallTarget,
@@ -101,7 +104,15 @@ import { resolveBindHostname } from "./bind.ts";
101
104
  import { listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
102
105
  import { VAULT_SCOPES } from "./scopes.ts";
103
106
  import { validateVaultName, decideInitVaultName } from "./vault-name.ts";
107
+ import { decideAutostart } from "./autostart.ts";
104
108
  import { getVaultStore } from "./vault-store.ts";
109
+ import {
110
+ defaultMirrorConfig,
111
+ resolveMirrorPath,
112
+ writeMirrorConfigForVault,
113
+ type MirrorConfig,
114
+ } from "./mirror-config.ts";
115
+ import { bootstrapInternalMirror } from "./mirror-manager.ts";
105
116
  import { selfRegister } from "./self-register.ts";
106
117
  import {
107
118
  hasOwnerPassword,
@@ -265,12 +276,16 @@ async function cmdInit(args: string[] = []) {
265
276
  const flagMcpOff = args.includes("--no-mcp");
266
277
  const flagTokenOn = args.includes("--token");
267
278
  const flagTokenOff = args.includes("--no-token");
268
- // --autostart / --no-autostart toggle daemon registration. Default is on
269
- // (preserves historical behavior). When --no-autostart is passed, init
270
- // skips registering with launchd / systemd AND removes any prior
271
- // registration for CI, dev sandboxes, Docker, or any environment where
272
- // another supervisor manages the process. --no-autostart wins over
273
- // --autostart on the same command line (safer-default precedence).
279
+ // --autostart / --no-autostart toggle daemon registration. The default is
280
+ // context-aware (resolved by decideAutostart below): ON for standalone
281
+ // deploys, but OFF when a hub supervisor is detected, since the hub owns
282
+ // vault's lifecycle and a launchd/systemd unit would race it for :1940
283
+ // (ParachuteComputer/parachute-hub#580). --no-autostart always skips
284
+ // registering AND removes any prior registration for CI, dev sandboxes,
285
+ // Docker, or any environment where another supervisor manages the process.
286
+ // --autostart forces registration even under a hub (logged with a warning).
287
+ // --no-autostart wins over --autostart on the same command line
288
+ // (safer-default precedence).
274
289
  const flagAutostartOn = args.includes("--autostart");
275
290
  const flagAutostartOff = args.includes("--no-autostart");
276
291
 
@@ -407,37 +422,73 @@ async function cmdInit(args: string[] = []) {
407
422
  // a folder move; this refreshes ~/.parachute/server-path and bounces the
408
423
  // daemon so the new location takes effect immediately.
409
424
  //
410
- // Autostart precedence (resolved here so the user's prior config is
411
- // honored on re-runs that don't pass a flag):
412
- // 1. --no-autostart on this run false (and persisted)
413
- // 2. --autostart on this run → true (and persisted)
414
- // 3. Existing config.autostart → that value
415
- // 4. Default true (historical behavior)
425
+ // Autostart precedence is resolved by `decideAutostart` (pure, unit-tested):
426
+ // 1. --no-autostart on this run → false (and persisted)
427
+ // 2. --autostart on this run true (and persisted; warns
428
+ // if a supervised hub was seen)
429
+ // 3. Existing config.autostart → that value
430
+ // 4. Hub present, no flag, no persisted val false (the hub supervisor
431
+ // owns the lifecycle — #580)
432
+ // 5. Default → true (standalone deploys
433
+ // genuinely need a daemon)
416
434
  // When false: skip register AND uninstall any prior registration so the
417
- // flag's intent ("don't auto-start / don't auto-restart") matches reality
435
+ // decision's intent ("don't auto-start / don't auto-restart") matches reality
418
436
  // even if a previous run had registered a daemon.
419
- let autostartEnabled: boolean;
420
- if (flagAutostartOff) autostartEnabled = false;
421
- else if (flagAutostartOn) autostartEnabled = true;
422
- else if (typeof globalConfig.autostart === "boolean") autostartEnabled = globalConfig.autostart;
423
- else autostartEnabled = true;
437
+ //
438
+ // The hub probe runs only when neither flag was passed AND no value is
439
+ // persisted i.e. only when the hub signal can actually change the outcome.
440
+ // It targets the hub's fixed loopback port (1939 / $PARACHUTE_HUB_PORT) and
441
+ // never throws (see detectHubPresence). We skip it when a flag/persisted
442
+ // value already decides, to avoid an 800ms wait on a flagged run.
443
+ //
444
+ // False-positive risk: a stale expose-state / leftover PARACHUTE_HUB_ORIGIN
445
+ // makes detectHubPresence return true on a genuinely hubless box, so init
446
+ // silently skips registering a daemon. Narrow + accepted — recover with
447
+ // `parachute-vault init --autostart`. The pre-decided guard below means any
448
+ // explicit flag or persisted value never even reaches the probe.
449
+ const autostartPreDecided =
450
+ flagAutostartOff || flagAutostartOn || typeof globalConfig.autostart === "boolean";
451
+ const hubPresentForAutostart = autostartPreDecided ? false : await detectHubPresence();
452
+ const autostartDecision = decideAutostart({
453
+ flagOn: flagAutostartOn,
454
+ flagOff: flagAutostartOff,
455
+ persisted: globalConfig.autostart,
456
+ hubPresent: hubPresentForAutostart,
457
+ });
458
+ const autostartEnabled = autostartDecision.enabled;
424
459
 
425
- if (flagAutostartOff || flagAutostartOn) {
460
+ if (autostartDecision.persist) {
426
461
  globalConfig.autostart = autostartEnabled;
427
462
  writeGlobalConfig(globalConfig);
428
463
  }
429
464
 
430
465
  let serverPath: string | null = null;
431
466
  if (!autostartEnabled) {
432
- console.log("Autostart disabled — skipping daemon registration.");
467
+ if (autostartDecision.reason === "hub-default-off") {
468
+ console.log(
469
+ "Hub supervisor detected — not registering a separate daemon. The hub manages vault's lifecycle.",
470
+ );
471
+ console.log(" To force a standalone daemon anyway: parachute-vault init --autostart");
472
+ } else {
473
+ console.log("Autostart disabled — skipping daemon registration.");
474
+ }
433
475
  if (isMac) {
434
476
  await uninstallAgent();
435
477
  } else if (isLinux && isSystemdAvailable()) {
436
478
  await uninstallSystemdService();
437
479
  }
438
480
  console.log(" To run vault: parachute-vault serve (or use your own supervisor)");
439
- console.log(" To re-enable: parachute-vault init --autostart");
481
+ if (autostartDecision.reason !== "hub-default-off") {
482
+ console.log(" To re-enable: parachute-vault init --autostart");
483
+ }
440
484
  } else {
485
+ if (autostartDecision.overrodeHub) {
486
+ console.log(
487
+ "Warning: a supervised hub was detected, but --autostart was passed — registering a "
488
+ + "standalone daemon anyway. This can race the hub supervisor for the vault port; "
489
+ + "prefer letting the hub manage vault unless you know you need both.",
490
+ );
491
+ }
441
492
  console.log("Installing daemon...");
442
493
  if (isMac) {
443
494
  ({ serverPath } = await installAgent());
@@ -471,11 +522,16 @@ async function cmdInit(args: string[] = []) {
471
522
  addMcp = true; // non-interactive: preserve the installable-via-pipe default
472
523
  }
473
524
 
474
- // 7b. Surface an API token for other clients? (Codex, Goose, OpenCode,
475
- // Cursor, Zed, Cline, scripts, curl.) Same flag/TTY precedence as MCP.
476
- // Note: a token is always minted when addMcp is true (it gets baked into
477
- // the ~/.claude.json entry); this prompt controls whether that token is
478
- // printed prominently at the end so the user can paste it elsewhere.
525
+ // 7b. Mint an API token for the header-auth / script use case? (Codex,
526
+ // Goose, OpenCode, Cursor, Zed, Cline, scripts, curl.)
527
+ //
528
+ // vault#442: default vault auth is per-user OAuth the Claude Code MCP entry
529
+ // is written WITHOUT a baked bearer, so the first connection does browser
530
+ // sign-in. We mint a token ONLY when the operator explicitly opts in
531
+ // (`--token`, or "yes" at the prompt), and then it's scope-narrow
532
+ // (`vault:<name>:read`), NEVER admin. `--no-token` (and the non-interactive
533
+ // default) skips minting entirely — no auto-mint, no noisy mint-failure on a
534
+ // fresh vault.
479
535
  let addToken: boolean;
480
536
  if (flagTokenOff) {
481
537
  addToken = false;
@@ -483,25 +539,22 @@ async function cmdInit(args: string[] = []) {
483
539
  addToken = true;
484
540
  } else if (process.stdin.isTTY) {
485
541
  addToken = await confirm(
486
- "Generate an API token for other MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline), scripts, or curl?",
487
- true,
542
+ "Also mint a header-auth API token for non-OAuth clients / scripts (Codex, Goose, curl)? (OAuth works without one)",
543
+ false,
488
544
  );
489
545
  } else {
490
- addToken = true; // non-interactive: default-yes matches addMcp default
546
+ addToken = false; // non-interactive default: OAuth-first, no auto-mint
491
547
  }
492
548
 
493
- // Mint a token if we need one (for the claude.json entry and/or for
494
- // prominent display) and don't already have one from vault creation
495
- // e.g. a re-run of init against an existing vault. vault#282 Stage 2: vault
496
- // no longer mints pvt_* tokens, so this is a hub JWT via the operator.token
497
- // hub mint-token path (`mintBootstrapCredential`). When no hub is
498
- // reachable, `apiKey` stays undefined and we carry the guidance to the
499
- // summary — the operator runs `mcp-install` once a hub is up, or sets
500
- // VAULT_AUTH_TOKEN.
549
+ // Mint a scope-narrow token ONLY when explicitly opted in and we don't
550
+ // already have one from vault creation. vault#282 Stage 2: vault no longer
551
+ // mints pvt_* tokens this is a hub JWT via the operator.token → hub
552
+ // mint-token path (`mintBootstrapCredential`), scoped `vault:<name>:read`.
553
+ // When no hub is reachable, `apiKey` stays undefined and we carry the
554
+ // guidance to the summary. The default (OAuth) path never reaches here.
501
555
  const defaultVault = globalConfig.default_vault || "default";
502
- const needToken = addMcp || addToken;
503
- if (needToken && !apiKey) {
504
- const credential = await mintBootstrapCredential(defaultVault);
556
+ if (addToken && !apiKey) {
557
+ const credential = await mintBootstrapCredential(defaultVault, "read");
505
558
  apiKey = credential.token ?? undefined;
506
559
  credentialGuidance = credential.guidance;
507
560
  if (!apiKey) console.log(` ${credential.guidance}`);
@@ -510,10 +563,9 @@ async function cmdInit(args: string[] = []) {
510
563
  if (addMcp) {
511
564
  // Goes through `buildMcpEntryPlan` for entryKey + url so this path shares
512
565
  // the writer-side invariant with `executeMcpInstall` — a future URL-shape
513
- // change can't drift between init and mcp-install. The bearer is the hub
514
- // JWT minted above (omitted when no hub was reachable the entry is then
515
- // written unauthenticated, and the operator re-runs `mcp-install` once a
516
- // hub is up).
566
+ // change can't drift between init and mcp-install. By default NO bearer is
567
+ // baked (vault#442 OAuth on first connect); a bearer is embedded only
568
+ // when the operator explicitly opted into a scope-narrow token above.
517
569
  const target = resolveInstallTarget("user");
518
570
  const { entryKey, url, source } = buildMcpEntryPlan({
519
571
  vaultName: defaultVault,
@@ -528,6 +580,9 @@ async function cmdInit(args: string[] = []) {
528
580
  });
529
581
  console.log(`MCP URL: ${url} (${source})`);
530
582
  console.log(` MCP server added to ~/.claude.json`);
583
+ if (!apiKey) {
584
+ console.log(` No token baked in — you'll sign in via OAuth on first connect.`);
585
+ }
531
586
  } else {
532
587
  console.log(" Skipped adding MCP to ~/.claude.json.");
533
588
  console.log(" Run `parachute-vault mcp-install` later if you want it.");
@@ -536,6 +591,11 @@ async function cmdInit(args: string[] = []) {
536
591
  // 8. Summary
537
592
  const port = globalConfig.port || DEFAULT_PORT;
538
593
  const mcpUrl = `http://127.0.0.1:${port}/vault/${defaultVault}/mcp`;
594
+ // Probe whether a hub is present so the summary's "opted into a token but
595
+ // none minted" copy reflects reality: under a hub the vault is reachable via
596
+ // browser OAuth even with no header-auth token (#445). Only matters for the
597
+ // !apiKey branches; cheap + best-effort (never throws).
598
+ const hubPresent = !apiKey ? await detectHubPresence() : true;
539
599
  const lines = buildInitSummaryLines({
540
600
  addMcp,
541
601
  addToken,
@@ -544,7 +604,9 @@ async function cmdInit(args: string[] = []) {
544
604
  bindHost,
545
605
  port,
546
606
  mcpUrl,
607
+ vaultName: defaultVault,
547
608
  noTokenGuidance: credentialGuidance,
609
+ hubPresent,
548
610
  });
549
611
  for (const line of lines) console.log(line);
550
612
  }
@@ -814,27 +876,73 @@ async function cmdCreate(args: string[]) {
814
876
  // POST /vaults shells out to this CLI and parses stdout). Errors still go
815
877
  // to stderr as plain text and exit nonzero — callers branch on exit code.
816
878
  const jsonMode = args.includes("--json");
817
- // Greedy strip of any `--*` token to recover the positional vault name.
818
- // Today only `--json` is recognized; any other `--foo` is silently dropped.
819
- // If a future flag (e.g. `--force`, `--dry-run`) is added, the parsing
820
- // here needs to whitelist it — otherwise an invalid flag becomes a silent
821
- // no-op rather than a usage error.
822
- const positional = args.filter((a) => !a.startsWith("--"));
879
+ // `--no-mirror` opts THIS create out of the default internal live mirror
880
+ // even when the server-wide `default_mirror` knob is `internal`. Parity for
881
+ // operators who want one bare vault without flipping the global default.
882
+ const noMirror = args.includes("--no-mirror");
883
+
884
+ // --- Auth opt-in (vault#442). Default = per-user OAuth, NO token minted. ---
885
+ // `--mint` opts into a scope-narrow hub JWT for the header-auth / script
886
+ // use case; `--scope read|write` (default read) picks the verb. `:admin` is
887
+ // intentionally NOT accepted from the create flow. `--token <bearer>` is the
888
+ // paste path — use an existing bearer instead of minting. The two are
889
+ // mutually exclusive.
890
+ const wantMint = args.includes("--mint");
891
+ const createTokenArg = takeArgValue(args, "--token");
892
+ if (createTokenArg.missingValue) {
893
+ console.error("--token requires a value (the bearer token to embed).");
894
+ process.exit(1);
895
+ }
896
+ const pastedToken = createTokenArg.value;
897
+ if (wantMint && pastedToken !== undefined) {
898
+ console.error("--mint and --token are mutually exclusive.");
899
+ process.exit(1);
900
+ }
901
+ const createScopeArg = takeArgValue(args, "--scope");
902
+ if (createScopeArg.missingValue) {
903
+ console.error("--scope requires a value: read or write.");
904
+ process.exit(1);
905
+ }
906
+ const rawCreateVerb = createScopeArg.value ?? "read";
907
+ if (rawCreateVerb !== "read" && rawCreateVerb !== "write") {
908
+ console.error(
909
+ `--scope must be "read" or "write" for create (admin is minted out-of-band via \`mcp-install --scope vault:admin\`). Got: ${rawCreateVerb}.`,
910
+ );
911
+ process.exit(1);
912
+ }
913
+ const mintVerb: "read" | "write" | undefined = wantMint ? rawCreateVerb : undefined;
914
+
915
+ // Greedy strip of any `--*` token (and any `--flag value` pairs we consumed
916
+ // above) to recover the positional vault name. `--json`, `--no-mirror`,
917
+ // `--mint`, `--token <v>`, `--scope <v>` are recognized; any other `--foo` is
918
+ // silently dropped, and the value following a recognized value-flag is
919
+ // skipped so it can't be mistaken for the vault name.
920
+ const VALUE_FLAGS = new Set(["--token", "--scope"]);
921
+ const positional: string[] = [];
922
+ for (let i = 0; i < args.length; i++) {
923
+ const a = args[i]!;
924
+ if (a.startsWith("--")) {
925
+ if (VALUE_FLAGS.has(a)) i++; // skip the flag's value
926
+ continue;
927
+ }
928
+ positional.push(a);
929
+ }
823
930
  const name = positional[0];
824
931
  if (!name) {
825
932
  console.error("Usage: parachute-vault create <name> [--json]");
826
933
  process.exit(1);
827
934
  }
828
935
 
829
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
830
- console.error("Vault name must contain only letters, numbers, hyphens, and underscores.");
831
- process.exit(1);
832
- }
833
- if (name === "list") {
834
- // Reserved keeps the "list" vault name out of play even though per-vault
835
- // routes now live under /vault/<name>/ and no longer collide with the
836
- // /vaults/list discovery endpoint.
837
- console.error(`"list" is a reserved vault name.`);
936
+ // One validator for every name-minting edge (2026-06-09 hub-module-boundary
937
+ // migration B2). cmdCreate used to carry its own inline charset check plus a
938
+ // hardcoded `"list"` reservation that had drifted from `validateVaultName`'s
939
+ // set — a vault named `admin`/`new`/`assets` could enter through `create`
940
+ // and capture a reserved route (`/vault/admin` is the daemon-level admin
941
+ // mount as of B3). Consuming the shared validator also picks up its 2–32
942
+ // length rule, aligning `create` with `init`, the env var, and hub's wizard.
943
+ const nameValidation = validateVaultName(name);
944
+ if (!nameValidation.ok) {
945
+ console.error(nameValidation.error);
838
946
  process.exit(1);
839
947
  }
840
948
 
@@ -846,7 +954,18 @@ async function cmdCreate(args: string[]) {
846
954
 
847
955
  ensureConfigDirSync();
848
956
  const wasFirst = listVaults().length === 0;
849
- const credential = await createVault(name);
957
+ const credential = await createVault(name, {
958
+ ...(noMirror ? { enableMirror: false } : {}),
959
+ ...(mintVerb ? { mintVerb } : {}),
960
+ });
961
+
962
+ // `--token <bearer>` paste path: the operator supplied their own bearer
963
+ // instead of minting one. Surface it as the credential token so the
964
+ // downstream JSON / human / summary copy treats it the same as a minted one.
965
+ const effectiveToken = pastedToken ?? credential.token;
966
+ const effectiveGuidance = pastedToken
967
+ ? "Using the bearer you supplied via --token."
968
+ : credential.guidance;
850
969
 
851
970
  // If this is the only vault now, make it the default so unscoped routes
852
971
  // (/mcp, /api/*, /oauth/*) target it. Avoids the "single vault named
@@ -880,17 +999,19 @@ async function cmdCreate(args: string[]) {
880
999
  log: () => {}, // CLI create has its own status lines.
881
1000
  });
882
1001
 
1002
+ const endpoint = chooseMcpUrl(name, globalConfig.port || DEFAULT_PORT).url;
1003
+
883
1004
  if (jsonMode) {
884
1005
  // Contract (hub's admin-vaults.ts requires `typeof token === "string"`):
885
- // emit the minted hub JWT when present. When no hub was reachable
886
- // (standalone create — no operator.token / no hub origin), `token` is the
887
- // empty string and `token_guidance` carries the operator's next step. Hub
888
- // only shells out to `create --json` while IT is the orchestrator (a hub is
889
- // running, operator.token present), so that path always mints a real JWT.
1006
+ // emit a token string when one was minted/pasted. vault#442 default is
1007
+ // per-user OAuth — no token is minted so `token` is the empty string and
1008
+ // `token_guidance` carries the OAuth-first connect path. (Hub's admin SPA
1009
+ // already handles the empty-token case + has its own session-cookie admin
1010
+ // mint path, so it doesn't depend on create minting a token.)
890
1011
  const payload = {
891
1012
  name,
892
- token: credential.token ?? "",
893
- token_guidance: credential.guidance,
1013
+ token: effectiveToken ?? "",
1014
+ token_guidance: effectiveGuidance,
894
1015
  paths: {
895
1016
  vault_dir: vaultDir(name),
896
1017
  vault_db: vaultDbPath(name),
@@ -904,18 +1025,20 @@ async function cmdCreate(args: string[]) {
904
1025
 
905
1026
  console.log(`Vault "${name}" created.`);
906
1027
  console.log(` Path: ${vaultDir(name)}`);
907
- if (credential.token) {
908
- console.log(` API token: ${credential.token}`);
909
- console.log(` ${credential.guidance}`);
1028
+ if (effectiveToken) {
1029
+ console.log(` API token: ${effectiveToken}`);
1030
+ console.log(` ${effectiveGuidance}`);
910
1031
  console.log(` Save this — it will not be shown again.`);
911
1032
  } else {
912
- console.log(` ${credential.guidance}`);
1033
+ console.log(` ${effectiveGuidance}`);
913
1034
  }
914
1035
  if (defaultNote) {
915
1036
  console.log(` ${defaultNote}`);
916
1037
  }
917
1038
  console.log();
918
- console.log(`To add MCP to Claude: parachute-vault mcp-install ${name}`);
1039
+ console.log(`Connect your AI: claude mcp add --transport http parachute-${name} ${endpoint}`);
1040
+ console.log(` (no token needed — you'll sign in on first use)`);
1041
+ console.log(`Need a header-auth token for a script? parachute auth mint-token --scope vault:${name}:read`);
919
1042
  }
920
1043
 
921
1044
  function cmdList() {
@@ -1447,20 +1570,52 @@ function cmdRemove(args: string[]) {
1447
1570
  // Keep default_vault in sync. If the removed vault was the default, either
1448
1571
  // promote the remaining vault (if exactly one) or clear the setting.
1449
1572
  const globalConfig = readGlobalConfig();
1573
+ const remaining = listVaults();
1574
+ let configDirty = false;
1450
1575
  if (globalConfig.default_vault === name) {
1451
- const remaining = listVaults();
1452
1576
  if (remaining.length === 1) {
1453
1577
  globalConfig.default_vault = remaining[0];
1454
- writeGlobalConfig(globalConfig);
1455
1578
  console.log(` Default vault is now "${remaining[0]}".`);
1456
1579
  } else {
1457
1580
  delete globalConfig.default_vault;
1458
- writeGlobalConfig(globalConfig);
1459
1581
  if (remaining.length > 1) {
1460
1582
  console.log(` Cleared default_vault — set one with: editor ${CONFIG_DIR}/config.yaml`);
1461
1583
  }
1462
1584
  }
1585
+ configDirty = true;
1463
1586
  }
1587
+
1588
+ // Last-vault marker (2026-06-09 hub-module-boundary migration, B1's
1589
+ // CLI-side improvement). Server boot auto-creates `default` when zero
1590
+ // vaults exist — without the marker, an operator who explicitly emptied
1591
+ // the server would find a freshly-credentialed `default` resurrected on
1592
+ // the next restart. Fresh installs never carry the marker (no config.yaml
1593
+ // at all), so Docker / hub-install first-run auto-create is preserved.
1594
+ if (remaining.length === 0 && globalConfig.auto_create !== false) {
1595
+ globalConfig.auto_create = false;
1596
+ configDirty = true;
1597
+ console.log(
1598
+ ` Last vault removed — wrote auto_create: false to ${GLOBAL_CONFIG_PATH} so the` +
1599
+ ` server won't auto-recreate "default" on next boot. Create a vault with:` +
1600
+ ` parachute-vault create <name>`,
1601
+ );
1602
+ }
1603
+ if (configDirty) writeGlobalConfig(globalConfig);
1604
+
1605
+ // Refresh services.json so the removed vault's /vault/<name> path drops
1606
+ // out of the parachute-vault row immediately — the same selfRegister
1607
+ // refresh cmdCreate does (#208). Without this, the hub's well-known
1608
+ // fan-out kept advertising the deleted vault until the next server boot.
1609
+ // Note: with zero vaults remaining, selfRegister falls back to the
1610
+ // manifest's canonical paths (`/vault/default`) — the same row a
1611
+ // subsequent boot would write — so CLI-remove and boot agree on the
1612
+ // zero-vault registration shape. Warnings go to stderr; status lines stay
1613
+ // ours.
1614
+ selfRegister({
1615
+ version: pkg.version,
1616
+ warn: (msg) => console.error(`Warning: ${msg}`),
1617
+ log: () => {},
1618
+ });
1464
1619
  }
1465
1620
 
1466
1621
  async function cmdConfig(args: string[]) {
@@ -1586,15 +1741,15 @@ function cmdTokens(args: string[]) {
1586
1741
  return;
1587
1742
  }
1588
1743
 
1589
- // `tokens create` was removed at 0.6.0 (vault#282 Stage 2). Vault no longer
1744
+ // `tokens create` was removed at 0.5.0 (vault#282 Stage 2). Vault no longer
1590
1745
  // mints its own (pvt_*) tokens — it's a pure hub resource-server. Tokens are
1591
1746
  // now hub-issued JWTs: run `parachute-vault mcp-install` to mint + wire one
1592
1747
  // for an MCP client, or `parachute auth mint-token --scope vault:<name>:<verb>`
1593
1748
  // for scripts. `tokens list` / `tokens revoke` remain for cleaning up any
1594
- // vestigial pre-0.6.0 rows.
1749
+ // vestigial pre-0.5.0 rows.
1595
1750
  if (subcmd === "create") {
1596
1751
  console.error(
1597
- "`parachute-vault tokens create` was removed at 0.6.0 — vault no longer mints its own tokens.\n" +
1752
+ "`parachute-vault tokens create` was removed at 0.5.0 — vault no longer mints its own tokens.\n" +
1598
1753
  " Mint a hub-issued JWT instead:\n" +
1599
1754
  " parachute-vault mcp-install --scope vault:<verb> # wire an MCP client\n" +
1600
1755
  " parachute auth mint-token --scope vault:<name>:<verb> # for scripts\n" +
@@ -2760,34 +2915,17 @@ async function cmdImport(args: string[]) {
2760
2915
  return;
2761
2916
  }
2762
2917
 
2763
- // Import into vault — use createNoteRaw to skip per-note wikilink sync,
2764
- // then do a single pass after all notes are imported (much faster for large vaults).
2918
+ // Import into vault — use the shared `importObsidianNotes` adapter
2919
+ // (obsidian.ts). It uses createNoteRaw to skip per-note wikilink sync,
2920
+ // id-aware upsert with a path-conflict guard, intra-batch collision
2921
+ // dedup, per-note error isolation, and timestamp preservation. The
2922
+ // single wikilink pass runs below, after all notes exist.
2923
+ const { importObsidianNotes } = await import("../core/src/obsidian.ts");
2765
2924
  const store = getVaultStore(vaultName);
2766
- let imported = 0;
2767
- let skipped = 0;
2768
-
2769
- for (const note of notes) {
2770
- // Skip if a note with this path already exists
2771
- const existing = await store.getNoteByPath(note.path);
2772
- if (existing) {
2773
- skipped++;
2774
- continue;
2775
- }
2925
+ const { imported, skipped } = await importObsidianNotes(store, notes);
2776
2926
 
2777
- // Build metadata from frontmatter (excluding tags, already extracted)
2778
- const metadata = Object.keys(note.frontmatter).length > 0 ? note.frontmatter : undefined;
2779
-
2780
- await store.createNoteRaw(note.content, {
2781
- path: note.path,
2782
- tags: note.tags.length > 0 ? note.tags : undefined,
2783
- metadata: metadata as Record<string, unknown>,
2784
- });
2785
- imported++;
2786
- }
2787
-
2788
- // Single-pass wikilink sync after all notes exist
2789
2927
  console.log(`\nImported ${imported} notes into vault "${vaultName}"`);
2790
- if (skipped > 0) console.log(`Skipped ${skipped} notes (path already exists)`);
2928
+ if (skipped > 0) console.log(`Skipped ${skipped} notes (path already exists or conflict)`);
2791
2929
 
2792
2930
  if (imported > 0) {
2793
2931
  const linkResult = await store.syncAllWikilinks();
@@ -3274,38 +3412,58 @@ async function firstChangedNoteTitle(
3274
3412
  /**
3275
3413
  * Outcome of bootstrapping a fresh vault's first credential (vault#282 Stage 2).
3276
3414
  *
3277
- * Vault no longer mints `pvt_*` tokens. The first credential for a new vault is
3278
- * a hub-issued JWT, minted via the same operator.token → hub mint-token path
3279
- * `mcp-install --mint` uses (cli.ts ~`cmdMcpInstall`). When no hub is reachable
3280
- * (standalone install, no operator.token, or no real hub origin), `token` is
3281
- * null and `guidance` carries the operator's next step.
3415
+ * Vault no longer mints `pvt_*` tokens. When a token IS minted (explicit opt-in
3416
+ * only — vault#442), it's a hub-issued JWT scoped narrow (`vault:<name>:read`
3417
+ * or `:write`, NEVER `:admin`), minted via the same operator.token → hub
3418
+ * mint-token path `mcp-install --mint` uses (cli.ts ~`cmdMcpInstall`). When no
3419
+ * hub is reachable (standalone install, no operator.token, or no real hub
3420
+ * origin), `token` is null and `guidance` carries the operator's next step.
3282
3421
  */
3283
3422
  interface VaultCredential {
3284
- /** Hub-issued JWT scoped to `vault:<name>:admin`, or null when no hub is reachable. */
3423
+ /** Hub-issued JWT scoped narrow (read/write), or null when not minted / no hub reachable. */
3285
3424
  token: string | null;
3286
3425
  /** Human-readable note: how the token was issued, or why it wasn't. */
3287
3426
  guidance: string;
3288
3427
  }
3289
3428
 
3290
3429
  /**
3291
- * Mint the first credential for a freshly-created vault.
3430
+ * Mint a scope-narrow credential for a vault (explicit opt-in vault#442).
3431
+ *
3432
+ * Default vault auth is per-user OAuth (browser sign-in on first MCP connect);
3433
+ * tokens are only for the header-auth / script use case and are minted ONLY
3434
+ * when explicitly requested. Decision (vault#442): the create/init flow NEVER
3435
+ * auto-mints, and when a token IS requested it's scope-narrow — `verb` is
3436
+ * `read` (default) or `write`, NEVER `admin`. (Admin tokens, when truly
3437
+ * needed, are minted out-of-band via `mcp-install --scope vault:admin` against
3438
+ * a hub running hub#449, or the hub admin SPA's own session-cookie path.)
3292
3439
  *
3293
- * Decision (vault#282 Stage 2): when a hub is reachable (operator.token present
3294
- * AND a real hub origin resolves), mint a `vault:<name>:admin` hub JWT and
3295
- * return it as the bootstrap credential — preserving the `create --json`
3296
- * `token` string contract hub's admin-vaults.ts requires. When no hub is
3440
+ * When a hub is reachable (operator.token present AND a real hub origin
3441
+ * resolves), mint a `vault:<name>:<verb>` hub JWT and return it. When no hub is
3297
3442
  * reachable, return `token: null` plus explicit standalone guidance. There is
3298
3443
  * no local pvt_* fallback anymore.
3299
3444
  */
3300
- async function mintBootstrapCredential(name: string): Promise<VaultCredential> {
3445
+ async function mintBootstrapCredential(
3446
+ name: string,
3447
+ verb: "read" | "write" = "read",
3448
+ /**
3449
+ * Test seam — injectable hub-presence probe. Defaults to the live
3450
+ * `detectHubPresence` (loopback `/health` + configured-origin check). Lets
3451
+ * tests drive both branches of the no-operator-token copy without a real hub.
3452
+ */
3453
+ detectHub: typeof detectHubPresence = detectHubPresence,
3454
+ ): Promise<VaultCredential> {
3301
3455
  const operatorToken = readOperatorToken();
3302
3456
  if (!operatorToken) {
3457
+ // No operator.token. Two very different worlds, identical symptom:
3458
+ // (a) Hub running on a fresh box — the token isn't minted until the
3459
+ // admin wizard creates the first admin user (hub init Step 1.5 is a
3460
+ // no-op until then). NOTHING to do here; the old "install the hub …"
3461
+ // copy is circular (this very flow was spawned *by* the hub). #445.
3462
+ // (b) Genuinely standalone — no hub at all. The original guidance holds.
3463
+ const hubPresent = await detectHub();
3303
3464
  return {
3304
3465
  token: null,
3305
- guidance:
3306
- "No token issued — no hub operator token at ~/.parachute/operator.token. " +
3307
- "Install the hub (`bun add -g @openparachute/hub` + `parachute init`) and re-run, " +
3308
- "or set VAULT_AUTH_TOKEN for an operator-channel bearer.",
3466
+ guidance: noOperatorTokenGuidance(hubPresent),
3309
3467
  };
3310
3468
  }
3311
3469
  const port = readGlobalConfig().port || DEFAULT_PORT;
@@ -3322,7 +3480,7 @@ async function mintBootstrapCredential(name: string): Promise<VaultCredential> {
3322
3480
  const result = await mintHubJwt({
3323
3481
  hubOrigin: hub.url,
3324
3482
  operatorToken,
3325
- scope: `vault:${name}:admin`,
3483
+ scope: `vault:${name}:${verb}`,
3326
3484
  subject: "parachute-vault-bootstrap",
3327
3485
  });
3328
3486
  if ("kind" in result) {
@@ -3333,7 +3491,7 @@ async function mintBootstrapCredential(name: string): Promise<VaultCredential> {
3333
3491
  return {
3334
3492
  token: null,
3335
3493
  guidance:
3336
- `No token issued — ${detail}. Verify the hub is running (hub#449 for vault:admin mint), ` +
3494
+ `No token issued — ${detail}. Verify the hub is running, ` +
3337
3495
  "then run `parachute-vault mcp-install`, or set VAULT_AUTH_TOKEN.",
3338
3496
  };
3339
3497
  }
@@ -3344,13 +3502,76 @@ async function mintBootstrapCredential(name: string): Promise<VaultCredential> {
3344
3502
  }
3345
3503
 
3346
3504
  /**
3347
- * Create a vault's config + DB and mint its first credential.
3505
+ * Create a vault's config + DB.
3348
3506
  *
3349
- * Returns the bootstrap credential (a hub JWT, or null + guidance when no hub
3350
- * is reachable vault#282 Stage 2). The DB is created lazily via
3351
- * `getVaultStore` so migrations + schema run; we no longer write any pvt_* row.
3507
+ * Default vault auth is per-user OAuth (vault#442) create does NOT mint or
3508
+ * bake in any token. Returns a `VaultCredential` whose `token` is null and
3509
+ * whose `guidance` points at the OAuth-first connect path. A scope-narrow
3510
+ * token is minted only when the caller passes `mintVerb` (the explicit
3511
+ * header-auth / script opt-in: `read` default or `write`, NEVER `admin`). The
3512
+ * DB is created lazily via `getVaultStore` so migrations + schema run; we never
3513
+ * write any pvt_* row.
3514
+ */
3515
+ interface CreateVaultOptions {
3516
+ /**
3517
+ * Opt-in token mint (vault#442). Unset → no token is minted; the vault uses
3518
+ * per-user OAuth on first MCP connect. Set to `read`/`write` → mint a
3519
+ * scope-narrow `vault:<name>:<verb>` hub JWT for the header-auth / script
3520
+ * use case. `admin` is intentionally NOT accepted here.
3521
+ */
3522
+ mintVerb?: "read" | "write";
3523
+ /**
3524
+ * Override the server-wide `default_mirror` knob for this one create.
3525
+ * `--no-mirror` on `parachute-vault create` sets this to `false` so the
3526
+ * vault is created with no mirror config even when the knob is `internal`.
3527
+ * Unset → fall back to the `default_mirror` global config knob (default
3528
+ * `internal`).
3529
+ */
3530
+ enableMirror?: boolean;
3531
+ /**
3532
+ * Test seam threaded straight into `bootstrapInternalMirror` (default
3533
+ * `Bun.which`). Inject a fn returning `null` to exercise the
3534
+ * git-not-installed best-effort path without uninstalling git from the
3535
+ * test host.
3536
+ */
3537
+ which?: (cmd: string) => string | null;
3538
+ }
3539
+
3540
+ /**
3541
+ * The History / "Live Mirror" preset, written at create time when the
3542
+ * `default_mirror` knob resolves to `internal`. Matches the History preset
3543
+ * the admin SPA's VaultMirror page applies:
3544
+ * `{enabled:true, location:internal, sync_mode:events, auto_commit:true,
3545
+ * auto_push:false}`.
3546
+ * Built on top of `defaultMirrorConfig()` so the non-preset fields
3547
+ * (commit_template, safety_net_seconds) stay canonical.
3548
+ */
3549
+ function historyPresetMirrorConfig(): MirrorConfig {
3550
+ return {
3551
+ ...defaultMirrorConfig(),
3552
+ enabled: true,
3553
+ location: "internal",
3554
+ sync_mode: "events",
3555
+ auto_commit: true,
3556
+ auto_push: false,
3557
+ };
3558
+ }
3559
+
3560
+ /**
3561
+ * Resolve whether a freshly created vault should get the internal mirror.
3562
+ * Precedence: explicit per-create override (`--no-mirror`) → server-wide
3563
+ * `default_mirror` knob (default `internal`).
3352
3564
  */
3353
- async function createVault(name: string): Promise<VaultCredential> {
3565
+ function shouldEnableCreateTimeMirror(opts: CreateVaultOptions): boolean {
3566
+ if (opts.enableMirror !== undefined) return opts.enableMirror;
3567
+ // Default to "internal" when the knob is unset — backup-on-by-default.
3568
+ return (readGlobalConfig().default_mirror ?? "internal") === "internal";
3569
+ }
3570
+
3571
+ async function createVault(
3572
+ name: string,
3573
+ opts: CreateVaultOptions = {},
3574
+ ): Promise<VaultCredential> {
3354
3575
  const config: VaultConfig = {
3355
3576
  name,
3356
3577
  api_keys: [],
@@ -3359,9 +3580,62 @@ async function createVault(name: string): Promise<VaultCredential> {
3359
3580
  writeVaultConfig(config);
3360
3581
 
3361
3582
  // Touch the store so the vault's SQLite DB + schema are created. No token
3362
- // row is written — vault is a pure hub resource-server post-0.6.0.
3583
+ // row is written — vault is a pure hub resource-server post-0.5.0.
3363
3584
  getVaultStore(name);
3364
- return mintBootstrapCredential(name);
3585
+
3586
+ // Default new vaults to an internal live mirror (local git backup of the
3587
+ // markdown projection). Backup-on-by-default; GitHub off-site backup is an
3588
+ // opt-in upgrade layered on top later. Opt out via the `default_mirror: off`
3589
+ // global knob (operators on git-less / disk-constrained / cloud boxes) or
3590
+ // the `--no-mirror` flag (this one create only).
3591
+ //
3592
+ // BEST-EFFORT, NON-FATAL: write the mirror config first (so the operator's
3593
+ // intent persists even if git is absent), then attempt the bootstrap. A
3594
+ // git-less box leaves the config written but inactive + logs an actionable
3595
+ // hint — it must NEVER fail the vault create. Create-time ONLY: existing
3596
+ // vaults are never retroactively migrated.
3597
+ if (shouldEnableCreateTimeMirror(opts)) {
3598
+ const mirrorConfig = historyPresetMirrorConfig();
3599
+ writeMirrorConfigForVault(name, mirrorConfig);
3600
+ const mirrorPath = resolveMirrorPath(vaultDir(name), mirrorConfig);
3601
+ if (mirrorPath) {
3602
+ try {
3603
+ const result = await bootstrapInternalMirror(mirrorPath, opts.which);
3604
+ if (!result.ok) {
3605
+ // git-not-installed (or refuse-to-clobber) — config stays written,
3606
+ // mirror just isn't active yet. Surface an actionable line; the
3607
+ // vault create succeeds regardless.
3608
+ console.error(
3609
+ `Note: local git backup configured but not yet active — ${result.error} ` +
3610
+ `Install git to activate; the backup turns on automatically on the next vault restart.`,
3611
+ );
3612
+ }
3613
+ } catch (err) {
3614
+ // Defense-in-depth: bootstrapInternalMirror already converts the
3615
+ // git-missing case into a non-throwing { ok:false } result, but a
3616
+ // truly unexpected throw must still not fail the create.
3617
+ console.error(
3618
+ `Note: local git backup configured but bootstrap hit an unexpected error ` +
3619
+ `(${(err as Error).message ?? err}). The vault was still created; ` +
3620
+ `the backup will retry on the next vault restart.`,
3621
+ );
3622
+ }
3623
+ }
3624
+ }
3625
+
3626
+ // vault#442: default to per-user OAuth — do NOT auto-mint or bake in a
3627
+ // shared token. Only mint when the caller explicitly opted in (header-auth /
3628
+ // script use case), and then scope-narrow (read/write, never admin).
3629
+ if (opts.mintVerb) {
3630
+ return mintBootstrapCredential(name, opts.mintVerb);
3631
+ }
3632
+ return {
3633
+ token: null,
3634
+ guidance:
3635
+ "No token minted — this vault uses per-user OAuth (sign in on first connect). " +
3636
+ "Need a header-auth token for a script? Run " +
3637
+ `\`parachute auth mint-token --scope vault:${name}:read\` (or \`:write\`).`,
3638
+ };
3365
3639
  }
3366
3640
 
3367
3641
  interface InstallMcpConfigOpts {
@@ -3443,19 +3717,28 @@ Setup:
3443
3717
  parachute-vault init [--mcp|--no-mcp] [--token|--no-token] [--vault-name <name>]
3444
3718
  [--autostart|--no-autostart]
3445
3719
  Set up everything (one command, idempotent).
3446
- --mcp/--no-mcp controls the Claude Code MCP entry;
3447
- --token/--no-token controls whether an API token is
3448
- printed for pasting into other MCP clients / scripts.
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
3723
+ header-auth token (vault:<name>:read) for non-OAuth clients /
3724
+ scripts; --no-token (the default) skips minting entirely.
3449
3725
  --vault-name skips the prompt and names the vault
3450
3726
  (lowercase alphanumeric, hyphens, underscores;
3451
3727
  omit to be prompted interactively, default "default").
3452
- --autostart (default) registers vault with launchd /
3453
- systemd so it starts on boot AND auto-restarts on
3454
- crash. --no-autostart skips daemon registration AND
3455
- uninstalls any prior registration for CI, dev
3456
- sandboxes, Docker, or environments where another
3457
- supervisor manages the process. Persists in
3458
- config.yaml as 'autostart: true|false'.
3728
+ --autostart registers vault with launchd / systemd so
3729
+ it starts on boot AND auto-restarts on crash; it forces
3730
+ registration even when a hub supervisor is detected
3731
+ (logged with a warning). --no-autostart skips daemon
3732
+ registration AND uninstalls any prior registration — for
3733
+ CI, dev sandboxes, Docker, or environments where another
3734
+ supervisor manages the process. Default: register when
3735
+ standalone, but skip when a hub is detected (the hub
3736
+ supervisor owns vault's lifecycle). An explicit flag
3737
+ persists in config.yaml as 'autostart: true|false'.
3738
+ Upgrade note: a box with a persisted 'autostart: true'
3739
+ (from an earlier explicit --autostart) keeps registering
3740
+ even under a hub — run init --no-autostart once to clear
3741
+ it and let the hub manage vault.
3459
3742
  parachute-vault doctor Diagnose install/config issues
3460
3743
  parachute-vault uninstall [--wipe] [--yes]
3461
3744
  Remove daemon + MCP entry; --wipe also removes vaults, .env,
@@ -3465,7 +3748,18 @@ Setup:
3465
3748
  parachute --version Print the installed version (alias: -v, version)
3466
3749
 
3467
3750
  Vaults:
3468
- parachute-vault create <name> [--json] Create a new vault (--json: emit { name, token, paths, set_as_default })
3751
+ parachute-vault create <name> [--json] [--no-mirror] [--mint [--scope read|write]] [--token <bearer>]
3752
+ Create a new vault (--json: emit { name, token, paths, set_as_default }).
3753
+ Default auth is per-user OAuth — NO token is minted; connect with
3754
+ "claude mcp add --transport http parachute-<name> <endpoint>" and sign in
3755
+ on first use. --mint opts into a scope-narrow hub JWT for the header-auth /
3756
+ script case (--scope read [default] | write — admin is NOT mintable from
3757
+ create); --token <bearer> pastes an existing bearer instead of minting.
3758
+ New vaults default to an internal live mirror — a local git backup of
3759
+ the markdown projection (backup on by default; GitHub off-site is an
3760
+ opt-in upgrade). --no-mirror creates a bare vault with no mirror config.
3761
+ Operators can flip the server-wide default with 'default_mirror: off' in
3762
+ config.yaml (recommended for cloud / disk-constrained boxes).
3469
3763
  parachute-vault list List all vaults
3470
3764
  parachute-vault remove <name> [--yes] Remove a vault
3471
3765
  parachute-vault mcp-install [--mint|--token <t>]
@@ -3539,7 +3833,7 @@ Vaults:
3539
3833
  Tokens (vault#282 Stage 2 — vault is a pure hub resource-server; it no longer
3540
3834
  mints its own tokens. Mint a hub-issued JWT with \`parachute-vault mcp-install\`
3541
3835
  or \`parachute auth mint-token --scope vault:<name>:<verb>\`. \`list\` / \`revoke\`
3542
- below operate on any vestigial pre-0.6.0 rows for cleanup.):
3836
+ below operate on any vestigial pre-0.5.0 rows for cleanup.):
3543
3837
  parachute-vault tokens List vault-DB tokens (every vault)
3544
3838
  parachute-vault tokens list --vault <name> List tokens for one vault only
3545
3839
  parachute-vault tokens revoke <token-id> Revoke a vestigial token (default vault)