@openparachute/vault 0.2.3 → 0.3.0-rc.1

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 (58) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/CHANGELOG.md +70 -0
  3. package/CLAUDE.md +17 -7
  4. package/README.md +169 -136
  5. package/core/src/core.test.ts +603 -19
  6. package/core/src/indexed-fields.test.ts +285 -0
  7. package/core/src/indexed-fields.ts +238 -0
  8. package/core/src/mcp.ts +127 -6
  9. package/core/src/notes.ts +157 -11
  10. package/core/src/query-operators.ts +174 -0
  11. package/core/src/schema.ts +69 -2
  12. package/core/src/store.ts +92 -0
  13. package/core/src/tag-schemas.ts +5 -0
  14. package/core/src/types.ts +29 -1
  15. package/docs/HTTP_API.md +105 -1
  16. package/package/package.json +32 -0
  17. package/package.json +2 -2
  18. package/src/auth.test.ts +83 -114
  19. package/src/auth.ts +68 -6
  20. package/src/backup-launchd.ts +1 -1
  21. package/src/backup.test.ts +1 -1
  22. package/src/backup.ts +18 -17
  23. package/src/cli.ts +179 -121
  24. package/src/config-triggers.test.ts +49 -0
  25. package/src/config.test.ts +317 -2
  26. package/src/config.ts +420 -40
  27. package/src/context.test.ts +136 -0
  28. package/src/context.ts +115 -0
  29. package/src/daemon.ts +17 -16
  30. package/src/doctor.test.ts +9 -7
  31. package/src/launchd.test.ts +1 -1
  32. package/src/launchd.ts +6 -6
  33. package/src/mcp-http.ts +75 -21
  34. package/src/mcp-install.test.ts +125 -0
  35. package/src/mcp-install.ts +60 -0
  36. package/src/mcp-tools.ts +34 -96
  37. package/src/module-config.ts +109 -0
  38. package/src/oauth.test.ts +345 -57
  39. package/src/oauth.ts +155 -35
  40. package/src/published.test.ts +2 -2
  41. package/src/routes.ts +209 -33
  42. package/src/routing.test.ts +817 -300
  43. package/src/routing.ts +204 -202
  44. package/src/scopes.test.ts +136 -0
  45. package/src/scopes.ts +105 -0
  46. package/src/scribe-env.test.ts +49 -0
  47. package/src/scribe-env.ts +33 -0
  48. package/src/server.ts +57 -5
  49. package/src/services-manifest.test.ts +140 -0
  50. package/src/services-manifest.ts +99 -0
  51. package/src/systemd.ts +3 -3
  52. package/src/token-store.ts +42 -9
  53. package/src/transcription-worker.test.ts +583 -0
  54. package/src/transcription-worker.ts +346 -0
  55. package/src/triggers.test.ts +191 -1
  56. package/src/triggers.ts +17 -2
  57. package/src/vault.test.ts +693 -77
  58. package/src/version.test.ts +1 -1
package/src/cli.ts CHANGED
@@ -4,16 +4,16 @@
4
4
  * Parachute Vault CLI.
5
5
  *
6
6
  * Usage:
7
- * parachute vault init — set up everything, one command
8
- * parachute vault create <name> — create a new vault
9
- * parachute vault list — list all vaults
10
- * parachute vault mcp-install <name> — add vault MCP to ~/.claude.json
11
- * parachute vault remove <name> — remove a vault
12
- * parachute vault config — show all config
13
- * parachute vault config set <key> <val> — set a config value
14
- * parachute vault config unset <key> — remove a config value
15
- * parachute vault serve — run the server (foreground)
16
- * parachute vault status — show full status
7
+ * parachute-vault init — set up everything, one command
8
+ * parachute-vault create <name> — create a new vault
9
+ * parachute-vault list — list all vaults
10
+ * parachute-vault mcp-install <name> — add vault MCP to ~/.claude.json
11
+ * parachute-vault remove <name> — remove a vault
12
+ * parachute-vault config — show all config
13
+ * parachute-vault config set <key> <val> — set a config value
14
+ * parachute-vault config unset <key> — remove a config value
15
+ * parachute-vault serve — run the server (foreground)
16
+ * parachute-vault status — show full status
17
17
  */
18
18
 
19
19
  import { resolve } from "path";
@@ -25,6 +25,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "fs";
25
25
  import pkg from "../package.json" with { type: "json" };
26
26
  import {
27
27
  ensureConfigDirSync,
28
+ migrateFromLegacyLayout,
28
29
  readVaultConfig,
29
30
  writeVaultConfig,
30
31
  readGlobalConfig,
@@ -45,8 +46,9 @@ import {
45
46
  GLOBAL_CONFIG_PATH,
46
47
  } from "./config.ts";
47
48
  import type { VaultConfig } from "./config.ts";
48
- import { VAULTS_DIR } from "./config.ts";
49
+ import { DATA_DIR } from "./config.ts";
49
50
  import { installAgent, uninstallAgent, isAgentLoaded, restartAgent } from "./launchd.ts";
51
+ import { chooseMcpUrl } from "./mcp-install.ts";
50
52
  import {
51
53
  runBackup,
52
54
  readLastBackup,
@@ -78,6 +80,7 @@ import { confirm, ask, askPassword, choose } from "./prompt.ts";
78
80
  import { generateToken, createToken, listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
79
81
  import type { TokenPermission } from "./token-store.ts";
80
82
  import { getVaultStore } from "./vault-store.ts";
83
+ import { upsertService, ServicesManifestError } from "./services-manifest.ts";
81
84
  import {
82
85
  hasOwnerPassword,
83
86
  setOwnerPassword,
@@ -103,7 +106,7 @@ import {
103
106
 
104
107
  const args = process.argv.slice(2);
105
108
 
106
- // Support both `parachute vault <cmd>` and `parachute <cmd>` patterns
109
+ // Support both `parachute-vault <cmd>` and `parachute <cmd>` patterns
107
110
  let command: string;
108
111
  let cmdArgs: string[];
109
112
 
@@ -119,6 +122,17 @@ if (args[0] === "vault") {
119
122
  // Commands
120
123
  // ---------------------------------------------------------------------------
121
124
 
125
+ // Pre-0.3 installs kept vault state directly under ~/.parachute/. Run the
126
+ // migration unconditionally so any command (including read-only ones like
127
+ // `doctor` and `url`) picks up the relocated state on first post-upgrade run.
128
+ // No-op when no legacy paths exist. Skipped for `help` and `version`, which
129
+ // are expected to produce *only* their documented output — and because scripts
130
+ // piping `parachute-vault --version` shouldn't get migration chatter on stderr.
131
+ const SKIP_MIGRATION = new Set(["help", "--help", "-h", "version", "--version", "-v"]);
132
+ if (!SKIP_MIGRATION.has(command)) {
133
+ migrateFromLegacyLayout();
134
+ }
135
+
122
136
  switch (command) {
123
137
  case "init":
124
138
  await cmdInit();
@@ -188,7 +202,7 @@ switch (command) {
188
202
  case "--version":
189
203
  case "-v":
190
204
  // Intentionally minimal — just the version string on stdout. Scripts
191
- // (and `parachute vault doctor` in a future check) rely on this being
205
+ // (and `parachute-vault doctor` in a future check) rely on this being
192
206
  // a bare-number line; anything else belongs in `vault status`.
193
207
  console.log(pkg.version);
194
208
  break;
@@ -249,6 +263,32 @@ async function cmdInit() {
249
263
  }
250
264
  writeGlobalConfig(globalConfig);
251
265
 
266
+ // 2a. Register in the shared services manifest so the @openparachute/cli
267
+ // dispatcher can discover this service and its health endpoint. Upserts
268
+ // by name, preserving entries for other services. Non-fatal on failure —
269
+ // init can complete without the manifest, just with a warning.
270
+ //
271
+ // `paths[0]` is the canonical mount point — CLI uses it for the
272
+ // `.well-known/parachute.json` URL and for `parachute expose`. Advertise
273
+ // `/vault/<default_vault>` so MCP clients land at the scoped endpoint.
274
+ // When no default vault exists yet (multi-vault, no fallback), fall back
275
+ // to "/" — the CLI can detect and prompt.
276
+ const servicePath = globalConfig.default_vault
277
+ ? `/vault/${globalConfig.default_vault}`
278
+ : "/";
279
+ try {
280
+ upsertService({
281
+ name: "parachute-vault",
282
+ port: globalConfig.port || DEFAULT_PORT,
283
+ paths: [servicePath],
284
+ health: "/health",
285
+ version: pkg.version,
286
+ });
287
+ } catch (err) {
288
+ const msg = err instanceof ServicesManifestError ? err.message : String(err);
289
+ console.log(` Warning: could not update ~/.parachute/services.json: ${msg}`);
290
+ }
291
+
252
292
  // 2b. Migrate existing legacy keys into per-vault token tables
253
293
  for (const v of listVaults()) {
254
294
  try {
@@ -297,7 +337,7 @@ async function cmdInit() {
297
337
  }
298
338
  if (serverPath) {
299
339
  console.log(` Server path: ${serverPath}`);
300
- console.log(` Wrapper: ~/.parachute/start.sh`);
340
+ console.log(` Wrapper: ~/.parachute/vault/start.sh`);
301
341
  }
302
342
  console.log(` Listening on http://0.0.0.0:${globalConfig.port || DEFAULT_PORT}`);
303
343
 
@@ -326,8 +366,8 @@ async function cmdInit() {
326
366
  }
327
367
 
328
368
  console.log(`\nNext steps:`);
329
- console.log(` parachute vault status check everything is running`);
330
- console.log(` parachute vault config view/edit configuration`);
369
+ console.log(` parachute-vault status check everything is running`);
370
+ console.log(` parachute-vault config view/edit configuration`);
331
371
  }
332
372
 
333
373
  async function promptForOwnerPassword(purpose: string): Promise<boolean> {
@@ -339,7 +379,7 @@ async function promptForOwnerPassword(purpose: string): Promise<boolean> {
339
379
  while (true) {
340
380
  const pw = await askPassword(" Password (or leave blank to skip)");
341
381
  if (!pw) {
342
- console.log(" Skipped — you can set one later with `parachute vault set-password`.");
382
+ console.log(" Skipped — you can set one later with `parachute-vault set-password`.");
343
383
  return false;
344
384
  }
345
385
 
@@ -391,13 +431,13 @@ async function cmdSetPassword(args: string[]) {
391
431
  }
392
432
 
393
433
  // ---------------------------------------------------------------------------
394
- // 2FA — parachute vault 2fa [enroll | disable | backup-codes | status]
434
+ // 2FA — parachute-vault 2fa [enroll | disable | backup-codes | status]
395
435
  // ---------------------------------------------------------------------------
396
436
 
397
437
  async function confirmOwnerPassword(purpose: string): Promise<boolean> {
398
438
  const hash = getOwnerPasswordHash();
399
439
  if (!hash) {
400
- console.error("No owner password is set. Run: parachute vault set-password");
440
+ console.error("No owner password is set. Run: parachute-vault set-password");
401
441
  return false;
402
442
  }
403
443
  console.log(purpose);
@@ -460,14 +500,14 @@ async function cmd2fa(args: string[]) {
460
500
  console.log(`2FA: enabled (${getBackupCodeCount()} backup code(s) remaining)`);
461
501
  } else {
462
502
  console.log("2FA: not enabled");
463
- console.log(" Enable with: parachute vault 2fa enroll");
503
+ console.log(" Enable with: parachute-vault 2fa enroll");
464
504
  }
465
505
  return;
466
506
  }
467
507
 
468
508
  if (sub === "enroll") {
469
509
  if (!hasOwnerPassword()) {
470
- console.error("Set an owner password first: parachute vault set-password");
510
+ console.error("Set an owner password first: parachute-vault set-password");
471
511
  process.exit(1);
472
512
  }
473
513
  if (hasTotpEnrolled()) {
@@ -511,7 +551,7 @@ async function cmd2fa(args: string[]) {
511
551
  console.log(` Incorrect code. (${2 - attempt} attempt(s) left)`);
512
552
  }
513
553
  if (!confirmed) {
514
- console.error("Enrollment failed — rolling back. Re-run `parachute vault 2fa enroll` to try again.");
554
+ console.error("Enrollment failed — rolling back. Re-run `parachute-vault 2fa enroll` to try again.");
515
555
  disableTotp();
516
556
  process.exit(1);
517
557
  }
@@ -539,7 +579,7 @@ async function cmd2fa(args: string[]) {
539
579
 
540
580
  if (sub === "backup-codes") {
541
581
  if (!hasTotpEnrolled()) {
542
- console.error("2FA is not enabled. Run: parachute vault 2fa enroll");
582
+ console.error("2FA is not enabled. Run: parachute-vault 2fa enroll");
543
583
  process.exit(1);
544
584
  }
545
585
  if (!(await confirmForTwoFactor("Confirm ownership to regenerate backup codes:"))) {
@@ -554,14 +594,14 @@ async function cmd2fa(args: string[]) {
554
594
  }
555
595
 
556
596
  console.error(`Unknown 2fa command: ${sub}`);
557
- console.error("Usage: parachute vault 2fa [status | enroll | disable | backup-codes]");
597
+ console.error("Usage: parachute-vault 2fa [status | enroll | disable | backup-codes]");
558
598
  process.exit(1);
559
599
  }
560
600
 
561
601
  function cmdCreate(args: string[]) {
562
602
  const name = args[0];
563
603
  if (!name) {
564
- console.error("Usage: parachute vault create <name>");
604
+ console.error("Usage: parachute-vault create <name>");
565
605
  process.exit(1);
566
606
  }
567
607
 
@@ -570,9 +610,9 @@ function cmdCreate(args: string[]) {
570
610
  process.exit(1);
571
611
  }
572
612
  if (name === "list") {
573
- // Reserved — /vaults/list is the public discovery endpoint. Allowing a
574
- // vault with this name would let its routes (/vaults/list/mcp, etc.) be
575
- // shadowed by the discovery handler.
613
+ // Reserved — keeps the "list" vault name out of play even though per-vault
614
+ // routes now live under /vault/<name>/ and no longer collide with the
615
+ // /vaults/list discovery endpoint.
576
616
  console.error(`"list" is a reserved vault name.`);
577
617
  process.exit(1);
578
618
  }
@@ -610,13 +650,13 @@ function cmdCreate(args: string[]) {
610
650
  console.log(` ${defaultNote}`);
611
651
  }
612
652
  console.log();
613
- console.log(`To add MCP to Claude: parachute vault mcp-install ${name}`);
653
+ console.log(`To add MCP to Claude: parachute-vault mcp-install ${name}`);
614
654
  }
615
655
 
616
656
  function cmdList() {
617
657
  const vaults = listVaults();
618
658
  if (vaults.length === 0) {
619
- console.log("No vaults. Run: parachute vault init");
659
+ console.log("No vaults. Run: parachute-vault init");
620
660
  return;
621
661
  }
622
662
 
@@ -637,7 +677,7 @@ function cmdMcpInstall(_args: string[]) {
637
677
  function cmdRemove(args: string[]) {
638
678
  const name = args[0];
639
679
  if (!name) {
640
- console.error("Usage: parachute vault remove <name>");
680
+ console.error("Usage: parachute-vault remove <name>");
641
681
  process.exit(1);
642
682
  }
643
683
 
@@ -651,7 +691,7 @@ function cmdRemove(args: string[]) {
651
691
  if (!force) {
652
692
  console.log(`This will permanently delete vault "${name}" and all its data.`);
653
693
  console.log(` Path: ${vaultDir(name)}`);
654
- console.log(`\nTo confirm: parachute vault remove ${name} --yes`);
694
+ console.log(`\nTo confirm: parachute-vault remove ${name} --yes`);
655
695
  return;
656
696
  }
657
697
 
@@ -680,7 +720,7 @@ function cmdRemove(args: string[]) {
680
720
  async function cmdConfig(args: string[]) {
681
721
  const subcmd = args[0];
682
722
 
683
- // parachute vault config — show current config
723
+ // parachute-vault config — show current config
684
724
  if (!subcmd) {
685
725
  loadEnvFile();
686
726
  const env = readEnvFile();
@@ -693,7 +733,7 @@ async function cmdConfig(args: string[]) {
693
733
  console.log();
694
734
 
695
735
  if (Object.keys(env).length === 0) {
696
- console.log(" No env vars set. Use: parachute vault config set <key> <value>");
736
+ console.log(" No env vars set. Use: parachute-vault config set <key> <value>");
697
737
  } else {
698
738
  for (const [key, val] of Object.entries(env)) {
699
739
  // Mask sensitive values
@@ -707,46 +747,46 @@ async function cmdConfig(args: string[]) {
707
747
  return;
708
748
  }
709
749
 
710
- // parachute vault config set <key> <value>
750
+ // parachute-vault config set <key> <value>
711
751
  if (subcmd === "set") {
712
752
  const key = args[1];
713
753
  const value = args.slice(2).join(" ");
714
754
  if (!key || !value) {
715
- console.error("Usage: parachute vault config set <key> <value>");
755
+ console.error("Usage: parachute-vault config set <key> <value>");
716
756
  process.exit(1);
717
757
  }
718
758
  setEnvVar(key, value);
719
759
  console.log(`Set ${key}=${key.includes("KEY") ? value.slice(0, 8) + "..." : value}`);
720
- console.log("Restart the daemon to apply: parachute vault restart");
760
+ console.log("Restart the daemon to apply: parachute-vault restart");
721
761
  return;
722
762
  }
723
763
 
724
- // parachute vault config unset <key>
764
+ // parachute-vault config unset <key>
725
765
  if (subcmd === "unset") {
726
766
  const key = args[1];
727
767
  if (!key) {
728
- console.error("Usage: parachute vault config unset <key>");
768
+ console.error("Usage: parachute-vault config unset <key>");
729
769
  process.exit(1);
730
770
  }
731
771
  unsetEnvVar(key);
732
772
  console.log(`Removed ${key}`);
733
- console.log("Restart the daemon to apply: parachute vault restart");
773
+ console.log("Restart the daemon to apply: parachute-vault restart");
734
774
  return;
735
775
  }
736
776
 
737
777
  console.error(`Unknown config command: ${subcmd}`);
738
- console.error("Usage: parachute vault config [set <key> <value> | unset <key>]");
778
+ console.error("Usage: parachute-vault config [set <key> <value> | unset <key>]");
739
779
  process.exit(1);
740
780
  }
741
781
 
742
782
  // ---------------------------------------------------------------------------
743
- // Tokens — parachute vault tokens [create | list | revoke]
783
+ // Tokens — parachute-vault tokens [create | list | revoke]
744
784
  // ---------------------------------------------------------------------------
745
785
 
746
786
  function cmdTokens(args: string[]) {
747
787
  const subcmd = args[0];
748
788
 
749
- // parachute vault tokens — list all tokens (across all vaults)
789
+ // parachute-vault tokens — list all tokens (across all vaults)
750
790
  if (!subcmd || subcmd === "list") {
751
791
  const vaults = listVaults();
752
792
  let anyTokens = false;
@@ -773,12 +813,12 @@ function cmdTokens(args: string[]) {
773
813
  }
774
814
 
775
815
  if (!anyTokens) {
776
- console.log("No tokens found. Create one: parachute vault tokens create");
816
+ console.log("No tokens found. Create one: parachute-vault tokens create");
777
817
  }
778
818
  return;
779
819
  }
780
820
 
781
- // parachute vault tokens create --vault <name> [--permission full|read]
821
+ // parachute-vault tokens create --vault <name> [--permission full|read]
782
822
  // [--expires <duration>] [--label <label>]
783
823
  if (subcmd === "create") {
784
824
  const vaultFlag = args.indexOf("--vault");
@@ -832,11 +872,11 @@ function cmdTokens(args: string[]) {
832
872
  return;
833
873
  }
834
874
 
835
- // parachute vault tokens revoke <token-id> --vault <name>
875
+ // parachute-vault tokens revoke <token-id> --vault <name>
836
876
  if (subcmd === "revoke") {
837
877
  const tokenId = args[1];
838
878
  if (!tokenId) {
839
- console.error("Usage: parachute vault tokens revoke <token-id> --vault <name>");
879
+ console.error("Usage: parachute-vault tokens revoke <token-id> --vault <name>");
840
880
  process.exit(1);
841
881
  }
842
882
 
@@ -860,7 +900,7 @@ function cmdTokens(args: string[]) {
860
900
  }
861
901
 
862
902
  console.error(`Unknown tokens command: ${subcmd}`);
863
- console.error("Usage: parachute vault tokens [create | list | revoke <id>]");
903
+ console.error("Usage: parachute-vault tokens [create | list | revoke <id>]");
864
904
  process.exit(1);
865
905
  }
866
906
 
@@ -1029,7 +1069,7 @@ async function cmdUninstall(argsList: string[]) {
1029
1069
  if (wipe) {
1030
1070
  console.log("`--wipe` will ALSO remove vaults, .env, config.yaml, and daemon logs.\n");
1031
1071
  } else {
1032
- console.log("User data (~/.parachute/vaults, ~/.parachute/.env) is left alone.\n");
1072
+ console.log("User data (~/.parachute/vault/) is left alone.\n");
1033
1073
  }
1034
1074
 
1035
1075
  // Scripted `--yes --wipe` bypasses both interactive confirms. That's the
@@ -1039,7 +1079,7 @@ async function cmdUninstall(argsList: string[]) {
1039
1079
  // miss this; interactive users already see the prompts.
1040
1080
  if (skipPrompts && wipe) {
1041
1081
  const ts = new Date().toISOString();
1042
- const targets = [VAULTS_DIR, ENV_PATH, GLOBAL_CONFIG_PATH, LOG_PATH, ERR_PATH].join(", ");
1082
+ const targets = [DATA_DIR, ENV_PATH, GLOBAL_CONFIG_PATH, LOG_PATH, ERR_PATH].join(", ");
1043
1083
  console.log(`[${ts}] scripted destructive wipe: ${targets}`);
1044
1084
  }
1045
1085
 
@@ -1083,7 +1123,7 @@ async function cmdUninstall(argsList: string[]) {
1083
1123
  // Inventory what's actually on disk. Paths that don't exist are a
1084
1124
  // silent no-op on removal, but we also skip listing them so the
1085
1125
  // "would be removed" summary doesn't lie to the user.
1086
- const vaultsExist = existsSync(VAULTS_DIR);
1126
+ const vaultsExist = existsSync(DATA_DIR);
1087
1127
  const envExists = existsSync(ENV_PATH);
1088
1128
  const configExists = existsSync(GLOBAL_CONFIG_PATH);
1089
1129
  const logExists = existsSync(LOG_PATH);
@@ -1094,7 +1134,7 @@ async function cmdUninstall(argsList: string[]) {
1094
1134
  console.log("No user data to remove.");
1095
1135
  } else {
1096
1136
  console.log("\nUser data that would be removed:");
1097
- if (vaultsExist) console.log(` ${VAULTS_DIR} (SQLite vaults)`);
1137
+ if (vaultsExist) console.log(` ${DATA_DIR} (per-vault SQLite data)`);
1098
1138
  if (envExists) console.log(` ${ENV_PATH} (.env config + secrets)`);
1099
1139
  if (configExists) console.log(` ${GLOBAL_CONFIG_PATH} (global config)`);
1100
1140
  if (logExists) console.log(` ${LOG_PATH} (daemon log)`);
@@ -1108,7 +1148,7 @@ async function cmdUninstall(argsList: string[]) {
1108
1148
  doWipe = await confirm("Delete this data? (cannot be undone)", false);
1109
1149
  }
1110
1150
  if (doWipe) {
1111
- if (vaultsExist) rmSync(VAULTS_DIR, { recursive: true, force: true });
1151
+ if (vaultsExist) rmSync(DATA_DIR, { recursive: true, force: true });
1112
1152
  if (envExists) rmSync(ENV_PATH, { force: true });
1113
1153
  if (configExists) rmSync(GLOBAL_CONFIG_PATH, { force: true });
1114
1154
  if (logExists) rmSync(LOG_PATH, { force: true });
@@ -1120,7 +1160,7 @@ async function cmdUninstall(argsList: string[]) {
1120
1160
  }
1121
1161
  }
1122
1162
 
1123
- console.log("\nDone. To reinstall: `parachute vault init`.");
1163
+ console.log("\nDone. To reinstall: `parachute-vault init`.");
1124
1164
  }
1125
1165
 
1126
1166
  interface DoctorCheck {
@@ -1139,7 +1179,7 @@ async function cmdDoctor() {
1139
1179
  name: "server-path pointer",
1140
1180
  status: "fail",
1141
1181
  detail: `missing: ${SERVER_PATH_FILE}`,
1142
- fix: "Run `parachute vault init` to create it.",
1182
+ fix: "Run `parachute-vault init` to create it.",
1143
1183
  });
1144
1184
  } else {
1145
1185
  const pointed = readServerPathPointer();
@@ -1148,14 +1188,14 @@ async function cmdDoctor() {
1148
1188
  name: "server-path pointer",
1149
1189
  status: "fail",
1150
1190
  detail: `empty: ${SERVER_PATH_FILE}`,
1151
- fix: "Run `parachute vault init` to rewrite it.",
1191
+ fix: "Run `parachute-vault init` to rewrite it.",
1152
1192
  });
1153
1193
  } else if (!existsSync(pointed)) {
1154
1194
  checks.push({
1155
1195
  name: "server.ts at pointer target",
1156
1196
  status: "fail",
1157
1197
  detail: `points to ${pointed}, which does not exist`,
1158
- fix: "Run `parachute vault init` from the current repo location.",
1198
+ fix: "Run `parachute-vault init` from the current repo location.",
1159
1199
  });
1160
1200
  } else {
1161
1201
  checks.push({
@@ -1173,7 +1213,7 @@ async function cmdDoctor() {
1173
1213
  name: "wrapper script",
1174
1214
  status: "fail",
1175
1215
  detail: `missing: ${WRAPPER_PATH}`,
1176
- fix: "Run `parachute vault init`.",
1216
+ fix: "Run `parachute-vault init`.",
1177
1217
  });
1178
1218
  } else {
1179
1219
  checks.push({ name: "wrapper script", status: "pass", detail: WRAPPER_PATH });
@@ -1186,7 +1226,7 @@ async function cmdDoctor() {
1186
1226
  name: "launchd agent",
1187
1227
  status: loaded ? "pass" : "warn",
1188
1228
  detail: loaded ? "loaded" : "not loaded",
1189
- fix: loaded ? undefined : "Run `parachute vault init` or `parachute vault restart`.",
1229
+ fix: loaded ? undefined : "Run `parachute-vault init` or `parachute-vault restart`.",
1190
1230
  });
1191
1231
  } else if (isSystemdAvailable()) {
1192
1232
  const active = await isServiceActive();
@@ -1194,7 +1234,7 @@ async function cmdDoctor() {
1194
1234
  name: "systemd service",
1195
1235
  status: active ? "pass" : "warn",
1196
1236
  detail: active ? "active" : "not active",
1197
- fix: active ? undefined : "Run `parachute vault init` or `parachute vault restart`.",
1237
+ fix: active ? undefined : "Run `parachute-vault init` or `parachute-vault restart`.",
1198
1238
  });
1199
1239
  }
1200
1240
 
@@ -1225,7 +1265,7 @@ async function cmdDoctor() {
1225
1265
  name: "MCP entry in ~/.claude.json",
1226
1266
  status: "warn",
1227
1267
  detail: mcpEntry.reason,
1228
- fix: "Run `parachute vault mcp-install` to register the vault with Claude.",
1268
+ fix: "Run `parachute-vault mcp-install` to register the vault with Claude.",
1229
1269
  });
1230
1270
  } else {
1231
1271
  checks.push({
@@ -1248,7 +1288,7 @@ async function cmdDoctor() {
1248
1288
  name: "MCP URL port matches vault",
1249
1289
  status: "warn",
1250
1290
  detail: `MCP URL port ${mcpEntry.port ?? "(unparseable)"} ≠ vault port ${port}`,
1251
- fix: "Re-run `parachute vault mcp-install` to refresh the MCP URL.",
1291
+ fix: "Re-run `parachute-vault mcp-install` to refresh the MCP URL.",
1252
1292
  });
1253
1293
  }
1254
1294
 
@@ -1268,7 +1308,7 @@ async function cmdDoctor() {
1268
1308
  name: "MCP URL reachable",
1269
1309
  status: "warn",
1270
1310
  detail: reach.detail,
1271
- fix: "Start the daemon: `parachute vault restart` (or `init` if not yet installed).",
1311
+ fix: "Start the daemon: `parachute-vault restart` (or `init` if not yet installed).",
1272
1312
  });
1273
1313
  }
1274
1314
  }
@@ -1297,7 +1337,7 @@ async function cmdDoctor() {
1297
1337
  name: `port ${port} availability`,
1298
1338
  status: "warn",
1299
1339
  detail: `port in use by non-vault process: ${collision.detail}`,
1300
- fix: "Stop the conflicting process, or set a different PORT in ~/.parachute/.env and re-run `parachute vault init`.",
1340
+ fix: "Stop the conflicting process, or set a different PORT in ~/.parachute/vault/.env and re-run `parachute-vault init`.",
1301
1341
  });
1302
1342
  break;
1303
1343
  case "unknown":
@@ -1319,7 +1359,7 @@ async function cmdDoctor() {
1319
1359
  name: "backup agent",
1320
1360
  status: loaded ? "pass" : "warn",
1321
1361
  detail: loaded ? `loaded (schedule: ${backupCfg.schedule})` : `not loaded (schedule: ${backupCfg.schedule})`,
1322
- fix: loaded ? undefined : `Re-run \`parachute vault backup --schedule ${backupCfg.schedule}\` to reinstall the agent.`,
1362
+ fix: loaded ? undefined : `Re-run \`parachute-vault backup --schedule ${backupCfg.schedule}\` to reinstall the agent.`,
1323
1363
  });
1324
1364
  }
1325
1365
 
@@ -1332,7 +1372,7 @@ async function cmdDoctor() {
1332
1372
  name: "backup destinations",
1333
1373
  status: "warn",
1334
1374
  detail: "schedule is active but no destinations configured",
1335
- fix: "Edit ~/.parachute/config.yaml and add at least one destination under `backup.destinations`.",
1375
+ fix: "Edit ~/.parachute/vault/config.yaml and add at least one destination under `backup.destinations`.",
1336
1376
  });
1337
1377
  } else {
1338
1378
  for (const dest of backupCfg.destinations) {
@@ -1341,7 +1381,7 @@ async function cmdDoctor() {
1341
1381
  name: `backup destination (${dest.kind})`,
1342
1382
  status: res.ok ? "pass" : "warn",
1343
1383
  detail: res.ok ? res.path : `${res.path}: ${res.error}`,
1344
- fix: res.ok ? undefined : "Ensure the path exists and is writable, or update it in ~/.parachute/config.yaml.",
1384
+ fix: res.ok ? undefined : "Ensure the path exists and is writable, or update it in ~/.parachute/vault/config.yaml.",
1345
1385
  });
1346
1386
  }
1347
1387
  }
@@ -1361,12 +1401,12 @@ async function cmdDoctor() {
1361
1401
  const hasWarn = checks.some((c) => c.status === "warn");
1362
1402
  console.log();
1363
1403
  if (hasFailure) {
1364
- console.log("doctor: problems found (exit 1). See `parachute vault status` for runtime details.");
1404
+ console.log("doctor: problems found (exit 1). See `parachute-vault status` for runtime details.");
1365
1405
  process.exit(1);
1366
1406
  } else if (hasWarn) {
1367
- console.log("doctor: warnings only. `parachute vault status` has live runtime detail.");
1407
+ console.log("doctor: warnings only. `parachute-vault status` has live runtime detail.");
1368
1408
  } else {
1369
- console.log("doctor: all checks passed. For live runtime state: `parachute vault status`.");
1409
+ console.log("doctor: all checks passed. For live runtime state: `parachute-vault status`.");
1370
1410
  }
1371
1411
  }
1372
1412
 
@@ -1377,7 +1417,7 @@ function cmdUrl() {
1377
1417
 
1378
1418
  /**
1379
1419
  * Resolve the vault's port the way `status`, `restart`, `url`, and `doctor`
1380
- * all need to agree on: env override (~/.parachute/.env) wins, then
1420
+ * all need to agree on: env override (~/.parachute/vault/.env) wins, then
1381
1421
  * config.yaml, then DEFAULT_PORT. Sources .env as a side effect so callers
1382
1422
  * running this before any env read still see PORT.
1383
1423
  */
@@ -1398,7 +1438,7 @@ type McpEntryLookup =
1398
1438
  /**
1399
1439
  * Read `~/.claude.json` and return the shape of the `parachute-vault` MCP
1400
1440
  * entry if present. The entry is always an HTTP MCP pointing at the local
1401
- * daemon — `{ type: "http", url: "http://127.0.0.1:<port>/vaults/<name>/mcp" }`
1441
+ * daemon — `{ type: "http", url: "http://127.0.0.1:<port>/vault/<name>/mcp" }`
1402
1442
  * — so we parse the URL's port for the port-match check.
1403
1443
  *
1404
1444
  * Invariant: the check is NON-fatal. A missing ~/.claude.json is a warn,
@@ -1573,7 +1613,7 @@ async function describeProcess(pid: number): Promise<string | null> {
1573
1613
  }
1574
1614
 
1575
1615
  // ---------------------------------------------------------------------------
1576
- // Backup — parachute vault backup [--schedule <freq> | status]
1616
+ // Backup — parachute-vault backup [--schedule <freq> | status]
1577
1617
  // ---------------------------------------------------------------------------
1578
1618
 
1579
1619
  async function cmdBackup(args: string[]) {
@@ -1588,7 +1628,7 @@ async function cmdBackup(args: string[]) {
1588
1628
  if (schedFlag !== -1) {
1589
1629
  const raw = args[schedFlag + 1];
1590
1630
  if (!raw) {
1591
- console.error("Usage: parachute vault backup --schedule <hourly|daily|weekly|manual>");
1631
+ console.error("Usage: parachute-vault backup --schedule <hourly|daily|weekly|manual>");
1592
1632
  process.exit(1);
1593
1633
  }
1594
1634
  if (raw !== "hourly" && raw !== "daily" && raw !== "weekly" && raw !== "manual") {
@@ -1606,7 +1646,7 @@ async function cmdBackup(args: string[]) {
1606
1646
  async function cmdBackupRun() {
1607
1647
  const cfg = readGlobalConfig().backup ?? defaultBackupConfig();
1608
1648
  if (cfg.destinations.length === 0) {
1609
- console.error("No backup destinations configured. Edit ~/.parachute/config.yaml:");
1649
+ console.error("No backup destinations configured. Edit ~/.parachute/vault/config.yaml:");
1610
1650
  console.error(" backup:");
1611
1651
  console.error(" destinations:");
1612
1652
  console.error(" - kind: local");
@@ -1648,7 +1688,7 @@ async function cmdBackupSchedule(schedule: BackupSchedule) {
1648
1688
  if (schedule === "manual") {
1649
1689
  await uninstallBackupAgent();
1650
1690
  console.log("Schedule: manual — backup agent removed.");
1651
- console.log("Run `parachute vault backup` to trigger a backup on demand.");
1691
+ console.log("Run `parachute-vault backup` to trigger a backup on demand.");
1652
1692
  return;
1653
1693
  }
1654
1694
 
@@ -1656,7 +1696,7 @@ async function cmdBackupSchedule(schedule: BackupSchedule) {
1656
1696
  console.log(`Schedule set to: ${schedule}`);
1657
1697
  console.log();
1658
1698
  console.log("WARNING: no destinations configured — scheduled runs will fail.");
1659
- console.log("Edit ~/.parachute/config.yaml and add at least one destination under `backup.destinations`.");
1699
+ console.log("Edit ~/.parachute/vault/config.yaml and add at least one destination under `backup.destinations`.");
1660
1700
  }
1661
1701
 
1662
1702
  await installBackupAgent(schedule);
@@ -1759,7 +1799,7 @@ async function cmdImport(args: string[]) {
1759
1799
  sourcePath = positional[0] ?? "";
1760
1800
 
1761
1801
  if (!sourcePath) {
1762
- console.error("Usage: parachute vault import <path> [--vault <name>] [--dry-run]");
1802
+ console.error("Usage: parachute-vault import <path> [--vault <name>] [--dry-run]");
1763
1803
  console.error("\nImports an Obsidian vault into Parachute Vault.");
1764
1804
  console.error("\nOptions:");
1765
1805
  console.error(" --vault <name> Target vault (default: 'default')");
@@ -1778,7 +1818,7 @@ async function cmdImport(args: string[]) {
1778
1818
  // Verify vault exists
1779
1819
  const config = readVaultConfig(vaultName);
1780
1820
  if (!config) {
1781
- console.error(`Vault "${vaultName}" not found. Run: parachute vault create ${vaultName}`);
1821
+ console.error(`Vault "${vaultName}" not found. Run: parachute-vault create ${vaultName}`);
1782
1822
  process.exit(1);
1783
1823
  }
1784
1824
 
@@ -1865,7 +1905,7 @@ async function cmdExport(args: string[]) {
1865
1905
  outputPath = positional[0] ?? "";
1866
1906
 
1867
1907
  if (!outputPath) {
1868
- console.error("Usage: parachute vault export <output-path> [--vault <name>]");
1908
+ console.error("Usage: parachute-vault export <output-path> [--vault <name>]");
1869
1909
  console.error("\nExports a Parachute Vault as Obsidian-compatible markdown files.");
1870
1910
  process.exit(1);
1871
1911
  }
@@ -1945,12 +1985,16 @@ function installMcpConfig(apiKey?: string) {
1945
1985
  }
1946
1986
  }
1947
1987
 
1948
- // Single HTTP MCP entry — use per-vault endpoint so pvt_ tokens work
1988
+ // Single HTTP MCP entry — use per-vault endpoint so pvt_ tokens work.
1989
+ // Pick the URL that matches the OAuth issuer vault will advertise, in this
1990
+ // order: explicit hub origin env > active tailnet/public exposure >
1991
+ // loopback. Otherwise a strict MCP client (Claude Code) hits a loopback URL
1992
+ // whose discovery issuer points at the hub and rejects on origin mismatch
1993
+ // (RFC 8414).
1949
1994
  const defaultVault = globalConfig.default_vault || "default";
1950
- const mcpEntry: Record<string, unknown> = {
1951
- type: "http",
1952
- url: `http://127.0.0.1:${port}/vaults/${defaultVault}/mcp`,
1953
- };
1995
+ const { url: mcpUrl, source } = chooseMcpUrl(defaultVault, port);
1996
+ console.log(`MCP URL: ${mcpUrl} (${source})`);
1997
+ const mcpEntry: Record<string, unknown> = { type: "http", url: mcpUrl };
1954
1998
  if (apiKey) {
1955
1999
  mcpEntry.headers = { Authorization: `Bearer ${apiKey}` };
1956
2000
  }
@@ -1979,58 +2023,72 @@ function usage() {
1979
2023
  console.log(`
1980
2024
  Parachute Vault — self-hosted knowledge graph
1981
2025
 
2026
+ If you installed via the Parachute CLI, prefer the wrapper commands for
2027
+ lifecycle — \`parachute start vault\`, \`parachute stop vault\`,
2028
+ \`parachute status\` — and use the vault-direct commands below for setup,
2029
+ data, and debugging.
2030
+
2031
+ ── Standard use ───────────────────────────────────────────────────────
2032
+
1982
2033
  Setup:
1983
- parachute vault init Set up everything (one command, idempotent)
1984
- parachute vault status Check what's running
1985
- parachute vault doctor Diagnose install/config issues
1986
- parachute vault uninstall [--wipe] [--yes]
2034
+ parachute-vault init Set up everything (one command, idempotent)
2035
+ parachute-vault doctor Diagnose install/config issues
2036
+ parachute-vault uninstall [--wipe] [--yes]
1987
2037
  Remove daemon + MCP entry; --wipe also removes vaults, .env,
1988
2038
  config.yaml, and daemon logs (vault.log, vault.err).
1989
2039
  --yes skips prompts (DANGEROUS with --wipe: no confirmation).
1990
- parachute vault url Print the local server URL (for scripts)
2040
+ parachute-vault url Print the local server URL (for scripts)
1991
2041
  parachute --version Print the installed version (alias: -v, version)
1992
2042
 
1993
2043
  Vaults:
1994
- parachute vault create <name> Create a new vault
1995
- parachute vault list List all vaults
1996
- parachute vault remove <name> [--yes] Remove a vault
1997
- parachute vault mcp-install Add vault MCP to Claude
2044
+ parachute-vault create <name> Create a new vault
2045
+ parachute-vault list List all vaults
2046
+ parachute-vault remove <name> [--yes] Remove a vault
2047
+ parachute-vault mcp-install Add vault MCP to Claude
1998
2048
 
1999
2049
  Tokens:
2000
- parachute vault tokens List all tokens
2001
- parachute vault tokens create Create a full-access token in the default vault
2002
- parachute vault tokens create --vault <name> Create a token in a specific vault
2003
- parachute vault tokens create --read Read-only token
2004
- parachute vault tokens create --label x Set a label
2005
- parachute vault tokens create --expires 30d Expiring token
2006
- parachute vault tokens revoke <token-id> Revoke a token (default vault)
2050
+ parachute-vault tokens List all tokens
2051
+ parachute-vault tokens create Create a full-access token in the default vault
2052
+ parachute-vault tokens create --vault <name> Create a token in a specific vault
2053
+ parachute-vault tokens create --read Read-only token
2054
+ parachute-vault tokens create --label x Set a label
2055
+ parachute-vault tokens create --expires 30d Expiring token
2056
+ parachute-vault tokens revoke <token-id> Revoke a token (default vault)
2007
2057
 
2008
2058
  OAuth:
2009
- parachute vault set-password Set/change the owner password (for consent page)
2010
- parachute vault set-password --clear Remove the owner password
2011
- parachute vault 2fa status Show 2FA state
2012
- parachute vault 2fa enroll Enable TOTP 2FA (QR + backup codes)
2013
- parachute vault 2fa disable Disable 2FA (requires password)
2014
- parachute vault 2fa backup-codes Regenerate backup codes
2059
+ parachute-vault set-password Set/change the owner password (for consent page)
2060
+ parachute-vault set-password --clear Remove the owner password
2061
+ parachute-vault 2fa status Show 2FA state
2062
+ parachute-vault 2fa enroll Enable TOTP 2FA (QR + backup codes)
2063
+ parachute-vault 2fa disable Disable 2FA (requires password)
2064
+ parachute-vault 2fa backup-codes Regenerate backup codes
2015
2065
 
2016
2066
  Config:
2017
- parachute vault config Show current configuration
2018
- parachute vault config set <key> <val> Set a config value
2019
- parachute vault config unset <key> Remove a config value
2067
+ parachute-vault config Show current configuration
2068
+ parachute-vault config set <key> <val> Set a config value
2069
+ parachute-vault config unset <key> Remove a config value
2020
2070
 
2021
2071
  Backup:
2022
- parachute vault backup One-shot backup to configured destinations
2023
- parachute vault backup --schedule <freq> hourly | daily | weekly | manual (macOS launchd)
2024
- parachute vault backup status Show schedule, last run, destinations, next run
2072
+ parachute-vault backup One-shot backup to configured destinations
2073
+ parachute-vault backup --schedule <freq> hourly | daily | weekly | manual (macOS launchd)
2074
+ parachute-vault backup status Show schedule, last run, destinations, next run
2025
2075
 
2026
2076
  Import/Export:
2027
- parachute vault import <path> Import an Obsidian vault
2028
- parachute vault import <path> --dry-run Preview import without writing
2029
- parachute vault export <path> Export vault as Obsidian markdown
2030
-
2031
- Server:
2032
- parachute vault serve Run server (foreground)
2033
- parachute vault logs Stream server logs
2034
- parachute vault restart Restart the daemon
2077
+ parachute-vault import <path> Import an Obsidian vault
2078
+ parachute-vault import <path> --dry-run Preview import without writing
2079
+ parachute-vault export <path> Export vault as Obsidian markdown
2080
+
2081
+ ── Advanced / standalone ──────────────────────────────────────────────
2082
+
2083
+ Direct daemon controls. For normal use, prefer the Parachute CLI wrappers
2084
+ they add PID tracking, log rotation, and cross-service \`parachute status\`
2085
+ visibility. Use these when running vault without the CLI or when debugging.
2086
+
2087
+ parachute-vault serve Run server in the foreground (no PID tracking).
2088
+ Prefer \`parachute start vault\` for managed lifecycle.
2089
+ parachute-vault status Vault-only daemon status.
2090
+ Prefer \`parachute status\` for a cross-service view.
2091
+ parachute-vault logs Stream server logs
2092
+ parachute-vault restart Restart the daemon
2035
2093
  `);
2036
2094
  }