@openparachute/vault 0.3.3 → 0.4.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 (79) hide show
  1. package/.parachute/module.json +15 -0
  2. package/core/src/core.test.ts +2252 -7
  3. package/core/src/links.ts +1 -1
  4. package/core/src/mcp.ts +801 -67
  5. package/core/src/note-schemas.ts +232 -0
  6. package/core/src/notes.ts +313 -35
  7. package/core/src/obsidian.ts +3 -3
  8. package/core/src/paths.ts +1 -1
  9. package/core/src/query-operators.ts +23 -7
  10. package/core/src/schema-defaults.ts +287 -0
  11. package/core/src/schema.ts +393 -9
  12. package/core/src/store.ts +248 -6
  13. package/core/src/tag-hierarchy.ts +137 -0
  14. package/core/src/tag-schemas.ts +242 -42
  15. package/core/src/types.ts +100 -6
  16. package/core/src/wikilinks.ts +3 -3
  17. package/package.json +13 -3
  18. package/src/admin-spa.test.ts +161 -0
  19. package/src/admin-spa.ts +161 -0
  20. package/src/auth-hub-jwt.test.ts +231 -0
  21. package/src/auth-status.ts +84 -0
  22. package/src/auth.test.ts +135 -23
  23. package/src/auth.ts +144 -15
  24. package/src/backup.ts +4 -7
  25. package/src/cli.ts +322 -57
  26. package/src/config.test.ts +44 -0
  27. package/src/config.ts +68 -40
  28. package/src/hub-jwt.test.ts +296 -0
  29. package/src/hub-jwt.ts +79 -0
  30. package/src/init.test.ts +216 -0
  31. package/src/mcp-http.ts +30 -28
  32. package/src/mcp-install.ts +1 -1
  33. package/src/mcp-tools.ts +294 -6
  34. package/src/module-config.ts +1 -1
  35. package/src/oauth.test.ts +345 -0
  36. package/src/oauth.ts +85 -14
  37. package/src/owner-auth.ts +57 -1
  38. package/src/prompt.ts +6 -5
  39. package/src/routes.ts +686 -58
  40. package/src/routing.test.ts +466 -1
  41. package/src/routing.ts +108 -24
  42. package/src/scopes.test.ts +66 -8
  43. package/src/scopes.ts +163 -37
  44. package/src/server.ts +24 -2
  45. package/src/services-manifest.test.ts +20 -0
  46. package/src/services-manifest.ts +9 -2
  47. package/src/stop-signal.test.ts +85 -0
  48. package/src/storage.test.ts +92 -0
  49. package/src/tag-scope.ts +118 -0
  50. package/src/token-store.test.ts +47 -0
  51. package/src/token-store.ts +128 -13
  52. package/src/tokens-routes.test.ts +720 -0
  53. package/src/tokens-routes.ts +392 -0
  54. package/src/transcription-worker.test.ts +5 -0
  55. package/src/triggers.ts +1 -1
  56. package/src/two-factor.ts +2 -2
  57. package/src/vault-create.test.ts +193 -0
  58. package/src/vault-name.test.ts +123 -0
  59. package/src/vault-name.ts +80 -0
  60. package/src/vault.test.ts +868 -3
  61. package/tsconfig.json +8 -1
  62. package/.claude/settings.local.json +0 -8
  63. package/.dockerignore +0 -8
  64. package/.env.example +0 -9
  65. package/CHANGELOG.md +0 -175
  66. package/CLAUDE.md +0 -125
  67. package/Caddyfile +0 -3
  68. package/Dockerfile +0 -22
  69. package/bun.lock +0 -219
  70. package/bunfig.toml +0 -2
  71. package/deploy/parachute-vault.service +0 -20
  72. package/docker-compose.yml +0 -50
  73. package/docs/HTTP_API.md +0 -434
  74. package/docs/auth-model.md +0 -340
  75. package/fly.toml +0 -24
  76. package/package/package.json +0 -32
  77. package/railway.json +0 -14
  78. package/scripts/migrate-audio-to-opus.test.ts +0 -237
  79. package/scripts/migrate-audio-to-opus.ts +0 -499
package/src/cli.ts CHANGED
@@ -37,6 +37,8 @@ import {
37
37
  loadEnvFile,
38
38
  listVaults,
39
39
  vaultDir,
40
+ vaultDbPath,
41
+ vaultConfigPath,
40
42
  DEFAULT_PORT,
41
43
  CONFIG_DIR,
42
44
  ASSETS_DIR,
@@ -44,6 +46,7 @@ import {
44
46
  LOG_PATH,
45
47
  ERR_PATH,
46
48
  GLOBAL_CONFIG_PATH,
49
+ stopSignalPath,
47
50
  } from "./config.ts";
48
51
  import type { VaultConfig } from "./config.ts";
49
52
  import { DATA_DIR } from "./config.ts";
@@ -82,6 +85,7 @@ import { resolveBindHostname } from "./bind.ts";
82
85
  import { generateToken, createToken, listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
83
86
  import type { TokenPermission } from "./token-store.ts";
84
87
  import { resolveCreateTokenFlags, VAULT_SCOPES } from "./scopes.ts";
88
+ import { validateVaultName, decideInitVaultName } from "./vault-name.ts";
85
89
  import { getVaultStore } from "./vault-store.ts";
86
90
  import { upsertService, ServicesManifestError } from "./services-manifest.ts";
87
91
  import {
@@ -178,6 +182,9 @@ switch (command) {
178
182
  case "restart":
179
183
  await cmdRestart();
180
184
  break;
185
+ case "stop":
186
+ await cmdStop();
187
+ break;
181
188
  case "uninstall":
182
189
  await cmdUninstall(cmdArgs);
183
190
  break;
@@ -219,6 +226,27 @@ switch (command) {
219
226
  // Command implementations
220
227
  // ---------------------------------------------------------------------------
221
228
 
229
+ /**
230
+ * Compute the `paths` array for the parachute-vault entry in services.json.
231
+ * One entry advertises every vault on this server; `paths[0]` is the
232
+ * canonical mount the hub stamps into `.well-known/parachute.json`, so the
233
+ * default vault sorts first when one is set. With no vaults yet, fall back
234
+ * to "/" so an early-init registration is still well-formed.
235
+ */
236
+ function buildVaultServicePaths(
237
+ defaultVault: string | undefined,
238
+ vaults: string[],
239
+ ): string[] {
240
+ if (vaults.length === 0) return ["/"];
241
+ if (defaultVault && vaults.includes(defaultVault)) {
242
+ return [
243
+ `/vault/${defaultVault}`,
244
+ ...vaults.filter((v) => v !== defaultVault).map((v) => `/vault/${v}`),
245
+ ];
246
+ }
247
+ return vaults.map((v) => `/vault/${v}`);
248
+ }
249
+
222
250
  async function cmdInit(args: string[] = []) {
223
251
  ensureConfigDirSync();
224
252
 
@@ -230,10 +258,33 @@ async function cmdInit(args: string[] = []) {
230
258
  // --token / --no-token follow the same pattern for whether the API
231
259
  // token is surfaced to the user at the end of init (for pasting into
232
260
  // other MCP clients, scripts, or curl).
261
+ //
262
+ // --vault-name <name> skips the name prompt for non-interactive installs
263
+ // (validated up front; exits non-zero on invalid input).
233
264
  const flagMcpOn = args.includes("--mcp");
234
265
  const flagMcpOff = args.includes("--no-mcp");
235
266
  const flagTokenOn = args.includes("--token");
236
267
  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).
274
+ const flagAutostartOn = args.includes("--autostart");
275
+ const flagAutostartOff = args.includes("--no-autostart");
276
+
277
+ const nameDecision = decideInitVaultName(args, {
278
+ isTTY: !!process.stdin.isTTY,
279
+ });
280
+ if (nameDecision.kind === "error") {
281
+ console.error(nameDecision.message);
282
+ process.exit(1);
283
+ }
284
+ // Whether the user explicitly supplied --vault-name. We use this both to
285
+ // pick the chosen name on first init AND to print a friendly notice if
286
+ // they pass --vault-name on a re-run where vaults already exist.
287
+ const vaultNameFlagSupplied = args.indexOf("--vault-name") !== -1;
237
288
 
238
289
  const isMac = process.platform === "darwin";
239
290
  const isLinux = process.platform === "linux";
@@ -241,14 +292,23 @@ async function cmdInit(args: string[] = []) {
241
292
 
242
293
  console.log("Parachute Vault — self-hosted knowledge graph\n");
243
294
 
244
- // 1. Create default vault if none exist
295
+ // 1. Create the vault if none exist. The name is decided here — flag wins,
296
+ // else prompt in a TTY (default "default"), else fall back to "default" so
297
+ // piped installs keep working unchanged.
245
298
  const vaults = listVaults();
246
299
  let apiKey: string | undefined;
247
300
  if (vaults.length === 0) {
248
- console.log("Creating default vault...");
249
- apiKey = createVault("default");
250
- console.log(" Created vault: default");
301
+ const chosenName =
302
+ nameDecision.kind === "name" ? nameDecision.name : await promptVaultName();
303
+ console.log(`Creating vault "${chosenName}"...`);
304
+ apiKey = createVault(chosenName);
305
+ console.log(` Created vault: ${chosenName}`);
251
306
  } else {
307
+ if (vaultNameFlagSupplied) {
308
+ console.log(
309
+ ` --vault-name ignored: ${vaults.length} vault(s) already exist. Use \`parachute-vault create\` to add another.`,
310
+ );
311
+ }
252
312
  console.log(`Found ${vaults.length} existing vault(s)`);
253
313
  }
254
314
 
@@ -279,30 +339,28 @@ async function cmdInit(args: string[] = []) {
279
339
  }
280
340
  writeGlobalConfig(globalConfig);
281
341
 
282
- // 2a. Register in the shared services manifest so the @openparachute/cli
342
+ // 2a. Register in the shared services manifest so the @openparachute/hub
283
343
  // dispatcher can discover this service and its health endpoint. Upserts
284
344
  // by name, preserving entries for other services. Non-fatal on failure —
285
345
  // init can complete without the manifest, just with a warning.
286
346
  //
287
- // `paths[0]` is the canonical mount point — CLI uses it for the
288
- // `.well-known/parachute.json` URL and for `parachute expose`. Advertise
289
- // `/vault/<default_vault>` so MCP clients land at the scoped endpoint.
290
- // When no default vault exists yet (multi-vault, no fallback), fall back
291
- // to "/" the CLI can detect and prompt.
292
- const servicePath = globalConfig.default_vault
293
- ? `/vault/${globalConfig.default_vault}`
294
- : "/";
347
+ // `paths[0]` is the canonical mount point — the hub uses it for the
348
+ // `.well-known/parachute.json` URL and for `parachute expose`, so the
349
+ // default vault always sorts first. Remaining vaults follow so the hub
350
+ // well-known and paraclaw's attach picker see every vault on this server.
351
+ // Re-running init re-registers the full set; that doubles as the
352
+ // recovery path for installs whose services.json is stale (#208).
295
353
  try {
296
354
  upsertService({
297
355
  name: "parachute-vault",
298
356
  port: globalConfig.port || DEFAULT_PORT,
299
- paths: [servicePath],
357
+ paths: buildVaultServicePaths(globalConfig.default_vault, allVaults),
300
358
  health: "/health",
301
359
  version: pkg.version,
302
360
  });
303
361
  } catch (err) {
304
362
  const msg = err instanceof ServicesManifestError ? err.message : String(err);
305
- console.log(` Warning: could not update ~/.parachute/services.json: ${msg}`);
363
+ console.error(` Warning: could not update ~/.parachute/services.json: ${msg}`);
306
364
  }
307
365
 
308
366
  // 2b. Migrate existing legacy keys into per-vault token tables
@@ -347,20 +405,52 @@ async function cmdInit(args: string[] = []) {
347
405
  // 6. Install daemon (platform-aware). Idempotent — safe to re-run after
348
406
  // a folder move; this refreshes ~/.parachute/server-path and bounces the
349
407
  // daemon so the new location takes effect immediately.
350
- console.log("Installing daemon...");
408
+ //
409
+ // Autostart precedence (resolved here so the user's prior config is
410
+ // honored on re-runs that don't pass a flag):
411
+ // 1. --no-autostart on this run → false (and persisted)
412
+ // 2. --autostart on this run → true (and persisted)
413
+ // 3. Existing config.autostart → that value
414
+ // 4. Default → true (historical behavior)
415
+ // When false: skip register AND uninstall any prior registration so the
416
+ // flag's intent ("don't auto-start / don't auto-restart") matches reality
417
+ // even if a previous run had registered a daemon.
418
+ let autostartEnabled: boolean;
419
+ if (flagAutostartOff) autostartEnabled = false;
420
+ else if (flagAutostartOn) autostartEnabled = true;
421
+ else if (typeof globalConfig.autostart === "boolean") autostartEnabled = globalConfig.autostart;
422
+ else autostartEnabled = true;
423
+
424
+ if (flagAutostartOff || flagAutostartOn) {
425
+ globalConfig.autostart = autostartEnabled;
426
+ writeGlobalConfig(globalConfig);
427
+ }
428
+
351
429
  let serverPath: string | null = null;
352
- if (isMac) {
353
- ({ serverPath } = await installAgent());
354
- } else if (isLinux && isSystemdAvailable()) {
355
- ({ serverPath } = await installSystemdService());
430
+ if (!autostartEnabled) {
431
+ console.log("Autostart disabled skipping daemon registration.");
432
+ if (isMac) {
433
+ await uninstallAgent();
434
+ } else if (isLinux && isSystemdAvailable()) {
435
+ await uninstallSystemdService();
436
+ }
437
+ console.log(" To run vault: parachute-vault serve (or use your own supervisor)");
438
+ console.log(" To re-enable: parachute-vault init --autostart");
356
439
  } else {
357
- console.log(" Auto-start not available on this platform.");
358
- console.log(" Run manually: bun src/server.ts");
359
- console.log(" Or use Docker: docker compose up -d");
360
- }
361
- if (serverPath) {
362
- console.log(` Server path: ${serverPath}`);
363
- console.log(` Wrapper: ~/.parachute/vault/start.sh`);
440
+ console.log("Installing daemon...");
441
+ if (isMac) {
442
+ ({ serverPath } = await installAgent());
443
+ } else if (isLinux && isSystemdAvailable()) {
444
+ ({ serverPath } = await installSystemdService());
445
+ } else {
446
+ console.log(" Auto-start not available on this platform.");
447
+ console.log(" Run manually: bun src/server.ts");
448
+ console.log(" Or use Docker: docker compose up -d");
449
+ }
450
+ if (serverPath) {
451
+ console.log(` Server path: ${serverPath}`);
452
+ console.log(` Wrapper: ~/.parachute/vault/start.sh`);
453
+ }
364
454
  }
365
455
  const bindHost = resolveBindHostname(process.env);
366
456
  console.log(` Listening on http://${bindHost}:${globalConfig.port || DEFAULT_PORT}`);
@@ -436,6 +526,16 @@ async function cmdInit(args: string[] = []) {
436
526
  }
437
527
 
438
528
 
529
+ async function promptVaultName(): Promise<string> {
530
+ while (true) {
531
+ const answer = await ask("What would you like to call this vault?", "default");
532
+ const v = validateVaultName(answer);
533
+ if (v.ok) return v.name;
534
+ console.log(` ${v.error}`);
535
+ }
536
+ }
537
+
538
+
439
539
  async function promptForOwnerPassword(purpose: string): Promise<boolean> {
440
540
  console.log(`\n${purpose}`);
441
541
  console.log(" Used on the OAuth consent page to authorize third-party clients");
@@ -665,9 +765,20 @@ async function cmd2fa(args: string[]) {
665
765
  }
666
766
 
667
767
  function cmdCreate(args: string[]) {
668
- const name = args[0];
768
+ // --json: emit a single machine-readable object on stdout instead of the
769
+ // human-friendly multi-line print. Designed for orchestrators (the hub's
770
+ // POST /vaults shells out to this CLI and parses stdout). Errors still go
771
+ // to stderr as plain text and exit nonzero — callers branch on exit code.
772
+ const jsonMode = args.includes("--json");
773
+ // Greedy strip of any `--*` token to recover the positional vault name.
774
+ // Today only `--json` is recognized; any other `--foo` is silently dropped.
775
+ // If a future flag (e.g. `--force`, `--dry-run`) is added, the parsing
776
+ // here needs to whitelist it — otherwise an invalid flag becomes a silent
777
+ // no-op rather than a usage error.
778
+ const positional = args.filter((a) => !a.startsWith("--"));
779
+ const name = positional[0];
669
780
  if (!name) {
670
- console.error("Usage: parachute-vault create <name>");
781
+ console.error("Usage: parachute-vault create <name> [--json]");
671
782
  process.exit(1);
672
783
  }
673
784
 
@@ -700,14 +811,49 @@ function cmdCreate(args: string[]) {
700
811
  const needsDefault = !globalConfig.default_vault
701
812
  || !listVaults().includes(globalConfig.default_vault);
702
813
  let defaultNote: string | null = null;
814
+ let setAsDefault = false;
703
815
  if (needsDefault) {
704
816
  globalConfig.default_vault = name;
705
817
  writeGlobalConfig(globalConfig);
818
+ setAsDefault = true;
706
819
  defaultNote = wasFirst
707
820
  ? `Set as default vault (unscoped routes will target "${name}")`
708
821
  : `Set as default vault (previous default was missing)`;
709
822
  }
710
823
 
824
+ // Re-register in services.json so the hub well-known and paraclaw's
825
+ // attach picker see this vault. cmdInit registers on first run; cmdCreate
826
+ // adds the new path on every subsequent vault. Without this, vaults
827
+ // created after init were invisible to the hub (#208).
828
+ // Warnings go to stderr to keep --json stdout clean for the orchestrator.
829
+ try {
830
+ upsertService({
831
+ name: "parachute-vault",
832
+ port: globalConfig.port || DEFAULT_PORT,
833
+ paths: buildVaultServicePaths(globalConfig.default_vault, listVaults()),
834
+ health: "/health",
835
+ version: pkg.version,
836
+ });
837
+ } catch (err) {
838
+ const msg = err instanceof ServicesManifestError ? err.message : String(err);
839
+ console.error(`Warning: could not update ~/.parachute/services.json: ${msg}`);
840
+ }
841
+
842
+ if (jsonMode) {
843
+ const payload = {
844
+ name,
845
+ token: key,
846
+ paths: {
847
+ vault_dir: vaultDir(name),
848
+ vault_db: vaultDbPath(name),
849
+ vault_config: vaultConfigPath(name),
850
+ },
851
+ set_as_default: setAsDefault,
852
+ };
853
+ console.log(JSON.stringify(payload));
854
+ return;
855
+ }
856
+
711
857
  console.log(`Vault "${name}" created.`);
712
858
  console.log(` Path: ${vaultDir(name)}`);
713
859
  console.log(` API token: ${key}`);
@@ -852,20 +998,38 @@ async function cmdConfig(args: string[]) {
852
998
  function cmdTokens(args: string[]) {
853
999
  const subcmd = args[0];
854
1000
 
855
- // parachute-vault tokens list all tokens (across all vaults)
1001
+ // parachute-vault tokens [list] [--vault <name>]
1002
+ // Default: every vault's tokens, grouped by vault.
1003
+ // --vault <name>: only that vault.
856
1004
  if (!subcmd || subcmd === "list") {
857
- const vaults = listVaults();
1005
+ const vaultFlag = args.indexOf("--vault");
1006
+ const onlyVault = vaultFlag !== -1 ? args[vaultFlag + 1] : null;
1007
+ if (vaultFlag !== -1 && !onlyVault) {
1008
+ console.error("--vault requires a value.");
1009
+ process.exit(1);
1010
+ }
1011
+ const vaults = onlyVault ? [onlyVault] : listVaults();
858
1012
  let anyTokens = false;
859
1013
 
860
1014
  for (const vaultName of vaults) {
861
1015
  const vc = readVaultConfig(vaultName);
862
- if (!vc) continue;
1016
+ if (!vc) {
1017
+ if (onlyVault) {
1018
+ console.error(`Vault "${vaultName}" not found.`);
1019
+ process.exit(1);
1020
+ }
1021
+ continue;
1022
+ }
863
1023
  const store = getVaultStore(vaultName);
864
1024
  // Ensure legacy keys are migrated
865
1025
  const globalCfg = readGlobalConfig();
866
1026
  migrateVaultKeys(store.db, vc.api_keys, globalCfg.api_keys);
867
1027
 
868
- const tokens = listTokens(store.db);
1028
+ // Per-vault filter (v16): only tokens bound to this vault, plus
1029
+ // legacy NULL-bound (server-wide) rows. The `[server-wide]` annotation
1030
+ // surfaces the latter so an operator listing one vault still sees
1031
+ // tokens that authenticate cross-vault.
1032
+ const tokens = listTokens(store.db, { vaultName });
869
1033
  if (tokens.length === 0) continue;
870
1034
  anyTokens = true;
871
1035
 
@@ -873,7 +1037,8 @@ function cmdTokens(args: string[]) {
873
1037
  for (const t of tokens) {
874
1038
  const expiry = t.expires_at ? ` (expires: ${t.expires_at})` : "";
875
1039
  const lastUsed = t.last_used_at ? ` (last used: ${t.last_used_at})` : "";
876
- console.log(` ${t.id} ${t.label} [${t.permission}]${expiry}${lastUsed}`);
1040
+ const serverWide = t.vault_name === null ? " [server-wide]" : "";
1041
+ console.log(` ${t.id} ${t.label} [${t.permission}]${serverWide}${expiry}${lastUsed}`);
877
1042
  }
878
1043
  console.log();
879
1044
  }
@@ -884,12 +1049,26 @@ function cmdTokens(args: string[]) {
884
1049
  return;
885
1050
  }
886
1051
 
887
- // parachute-vault tokens create --vault <name>
1052
+ // parachute-vault tokens create [--vault <name> | --all]
888
1053
  // [--scope vault:read,vault:write | --read | --permission full|read]
889
1054
  // [--expires <duration>] [--label <label>]
1055
+ //
1056
+ // Per-vault binding (v16): the minted token is pinned to <vaultName>
1057
+ // unless --all is passed, in which case the token is server-wide
1058
+ // (vault_name = NULL) and authenticates against any vault. --all is
1059
+ // the explicit opt-out — there's no implicit fall-through to server-wide.
890
1060
  if (subcmd === "create") {
891
1061
  const vaultFlag = args.indexOf("--vault");
1062
+ const allFlag = args.includes("--all");
1063
+ if (allFlag && vaultFlag !== -1) {
1064
+ console.error("--vault and --all are mutually exclusive.");
1065
+ process.exit(1);
1066
+ }
892
1067
  const vaultName = vaultFlag !== -1 ? args[vaultFlag + 1] : (readGlobalConfig().default_vault || "default");
1068
+ if (!vaultName) {
1069
+ console.error("--vault requires a value.");
1070
+ process.exit(1);
1071
+ }
893
1072
 
894
1073
  const vc = readVaultConfig(vaultName);
895
1074
  if (!vc) {
@@ -913,6 +1092,10 @@ function cmdTokens(args: string[]) {
913
1092
  let expiresAt: string | null = null;
914
1093
  if (expiresFlag !== -1) {
915
1094
  const dur = args[expiresFlag + 1];
1095
+ if (!dur) {
1096
+ console.error("--expires requires a value (e.g. 7d, 30d, 24h, 1y).");
1097
+ process.exit(1);
1098
+ }
916
1099
  expiresAt = parseDuration(dur);
917
1100
  if (!expiresAt) {
918
1101
  console.error(`Invalid duration: ${dur}. Use format like 7d, 30d, 24h, 1y.`);
@@ -921,7 +1104,7 @@ function cmdTokens(args: string[]) {
921
1104
  }
922
1105
 
923
1106
  const labelFlag = args.indexOf("--label");
924
- const label = labelFlag !== -1 ? args[labelFlag + 1] : "default";
1107
+ const label = (labelFlag !== -1 ? args[labelFlag + 1] : undefined) ?? "default";
925
1108
 
926
1109
  const store = getVaultStore(vaultName);
927
1110
  const { fullToken } = generateToken();
@@ -930,15 +1113,22 @@ function cmdTokens(args: string[]) {
930
1113
  permission,
931
1114
  scopes,
932
1115
  expires_at: expiresAt,
1116
+ // v16 binding: pin to the vault we minted in unless --all was passed
1117
+ // (which leaves vault_name NULL = legacy server-wide).
1118
+ vault_name: allFlag ? null : vaultName,
933
1119
  });
934
1120
 
935
1121
  const displayScopes = scopes ?? [...VAULT_SCOPES];
936
- console.log(`Created token for vault "${vaultName}":`);
1122
+ const heading = allFlag
1123
+ ? `Created server-wide token (authenticates against any vault):`
1124
+ : `Created token for vault "${vaultName}":`;
1125
+ console.log(heading);
937
1126
  console.log(` Token: ${fullToken}`);
938
1127
  console.log(` Permission: ${permission}`);
939
1128
  console.log(` Scopes: ${displayScopes.join(" ")}`);
940
1129
  if (expiresAt) console.log(` Expires: ${expiresAt}`);
941
1130
  console.log(` Label: ${label}`);
1131
+ if (!allFlag) console.log(` Vault: ${vaultName}`);
942
1132
  console.log();
943
1133
  console.log("Save this token — it will not be shown again.");
944
1134
  return;
@@ -954,6 +1144,10 @@ function cmdTokens(args: string[]) {
954
1144
 
955
1145
  const vaultFlag = args.indexOf("--vault");
956
1146
  const vaultName = vaultFlag !== -1 ? args[vaultFlag + 1] : (readGlobalConfig().default_vault || "default");
1147
+ if (!vaultName) {
1148
+ console.error("--vault requires a value.");
1149
+ process.exit(1);
1150
+ }
957
1151
 
958
1152
  const vc = readVaultConfig(vaultName);
959
1153
  if (!vc) {
@@ -979,8 +1173,8 @@ function cmdTokens(args: string[]) {
979
1173
  function parseDuration(dur: string): string | null {
980
1174
  const match = dur.match(/^(\d+)(h|d|w|m|y)$/);
981
1175
  if (!match) return null;
982
- const n = parseInt(match[1], 10);
983
- const unit = match[2];
1176
+ const n = parseInt(match[1]!, 10);
1177
+ const unit = match[2]!;
984
1178
  const now = new Date();
985
1179
  switch (unit) {
986
1180
  case "h": now.setHours(now.getHours() + n); break;
@@ -1038,6 +1232,41 @@ async function cmdRestart() {
1038
1232
  process.exit(1);
1039
1233
  }
1040
1234
 
1235
+ async function cmdStop() {
1236
+ loadEnvFile();
1237
+ const port = readGlobalConfig().port || DEFAULT_PORT;
1238
+ const sentinel = stopSignalPath();
1239
+
1240
+ // Health check first: avoid leaving a stale sentinel that would kill the
1241
+ // *next* server boot. The server clears any pre-existing sentinel on
1242
+ // startup, but only after it loads — a sentinel written between launch
1243
+ // and the first poll could still win the race. Skipping the write when
1244
+ // nothing is listening is the cheap, obvious guard.
1245
+ const health = await checkHealth(port);
1246
+ if (health.status === "not-listening" || health.status === "error") {
1247
+ console.log(`Vault is not running (${health.status}${health.error ? `: ${health.error}` : ""}).`);
1248
+ return;
1249
+ }
1250
+
1251
+ ensureConfigDirSync();
1252
+ writeFileSync(sentinel, `${new Date().toISOString()}\n`);
1253
+ console.log(`Stop signal written: ${sentinel}`);
1254
+
1255
+ // Wait briefly for the server to pick up the sentinel and stop responding.
1256
+ // Polls match the server's 500ms cadence; give it ~5s before giving up.
1257
+ const start = Date.now();
1258
+ while (Date.now() - start < 5_000) {
1259
+ await new Promise((r) => setTimeout(r, 600));
1260
+ const h = await checkHealth(port);
1261
+ if (h.status === "not-listening" || h.status === "error") {
1262
+ console.log(`Vault stopped (${Math.round((Date.now() - start) / 100) / 10}s).`);
1263
+ return;
1264
+ }
1265
+ }
1266
+ console.error("Vault did not stop within 5s. Check vault logs or `parachute-vault status`.");
1267
+ process.exit(1);
1268
+ }
1269
+
1041
1270
  async function cmdStatus() {
1042
1271
  loadEnvFile();
1043
1272
  const globalConfig = readGlobalConfig();
@@ -1856,16 +2085,27 @@ async function cmdImport(args: string[]) {
1856
2085
 
1857
2086
  const positional: string[] = [];
1858
2087
  for (let i = 0; i < args.length; i++) {
1859
- if (args[i] === "--format") {
1860
- format = args[++i];
1861
- } else if (args[i] === "--vault") {
1862
- vaultName = args[++i];
1863
- } else if (args[i] === "--dry-run") {
2088
+ const arg = args[i]!;
2089
+ if (arg === "--format") {
2090
+ const v = args[++i];
2091
+ if (!v) {
2092
+ console.error("--format requires a value.");
2093
+ process.exit(1);
2094
+ }
2095
+ format = v;
2096
+ } else if (arg === "--vault") {
2097
+ const v = args[++i];
2098
+ if (!v) {
2099
+ console.error("--vault requires a value.");
2100
+ process.exit(1);
2101
+ }
2102
+ vaultName = v;
2103
+ } else if (arg === "--dry-run") {
1864
2104
  dryRun = true;
1865
- } else if (args[i] === "--obsidian") {
2105
+ } else if (arg === "--obsidian") {
1866
2106
  format = "obsidian";
1867
2107
  } else {
1868
- positional.push(args[i]);
2108
+ positional.push(arg);
1869
2109
  }
1870
2110
  }
1871
2111
  sourcePath = positional[0] ?? "";
@@ -1968,10 +2208,16 @@ async function cmdExport(args: string[]) {
1968
2208
 
1969
2209
  const positional: string[] = [];
1970
2210
  for (let i = 0; i < args.length; i++) {
1971
- if (args[i] === "--vault") {
1972
- vaultName = args[++i];
2211
+ const arg = args[i]!;
2212
+ if (arg === "--vault") {
2213
+ const v = args[++i];
2214
+ if (!v) {
2215
+ console.error("--vault requires a value.");
2216
+ process.exit(1);
2217
+ }
2218
+ vaultName = v;
1973
2219
  } else {
1974
- positional.push(args[i]);
2220
+ positional.push(arg);
1975
2221
  }
1976
2222
  }
1977
2223
  outputPath = positional[0] ?? "";
@@ -2095,7 +2341,7 @@ function usage() {
2095
2341
  console.log(`
2096
2342
  Parachute Vault — self-hosted knowledge graph
2097
2343
 
2098
- If you installed via the Parachute CLI, prefer the wrapper commands for
2344
+ If you installed via the Parachute Hub, prefer the wrapper commands for
2099
2345
  lifecycle — \`parachute start vault\`, \`parachute stop vault\`,
2100
2346
  \`parachute status\` — and use the vault-direct commands below for setup,
2101
2347
  data, and debugging.
@@ -2103,11 +2349,22 @@ data, and debugging.
2103
2349
  ── Standard use ───────────────────────────────────────────────────────
2104
2350
 
2105
2351
  Setup:
2106
- parachute-vault init [--mcp|--no-mcp] [--token|--no-token]
2352
+ parachute-vault init [--mcp|--no-mcp] [--token|--no-token] [--vault-name <name>]
2353
+ [--autostart|--no-autostart]
2107
2354
  Set up everything (one command, idempotent).
2108
2355
  --mcp/--no-mcp controls the Claude Code MCP entry;
2109
2356
  --token/--no-token controls whether an API token is
2110
2357
  printed for pasting into other MCP clients / scripts.
2358
+ --vault-name skips the prompt and names the vault
2359
+ (lowercase alphanumeric, hyphens, underscores;
2360
+ omit to be prompted interactively, default "default").
2361
+ --autostart (default) registers vault with launchd /
2362
+ systemd so it starts on boot AND auto-restarts on
2363
+ crash. --no-autostart skips daemon registration AND
2364
+ uninstalls any prior registration — for CI, dev
2365
+ sandboxes, Docker, or environments where another
2366
+ supervisor manages the process. Persists in
2367
+ config.yaml as 'autostart: true|false'.
2111
2368
  parachute-vault doctor Diagnose install/config issues
2112
2369
  parachute-vault uninstall [--wipe] [--yes]
2113
2370
  Remove daemon + MCP entry; --wipe also removes vaults, .env,
@@ -2117,15 +2374,19 @@ Setup:
2117
2374
  parachute --version Print the installed version (alias: -v, version)
2118
2375
 
2119
2376
  Vaults:
2120
- parachute-vault create <name> Create a new vault
2377
+ parachute-vault create <name> [--json] Create a new vault (--json: emit { name, token, paths, set_as_default })
2121
2378
  parachute-vault list List all vaults
2122
2379
  parachute-vault remove <name> [--yes] Remove a vault
2123
2380
  parachute-vault mcp-install Add vault MCP to Claude
2124
2381
 
2125
2382
  Tokens:
2126
- parachute-vault tokens List all tokens
2127
- parachute-vault tokens create Create a full-access token in the default vault
2128
- parachute-vault tokens create --vault <name> Create a token in a specific vault
2383
+ parachute-vault tokens List tokens (every vault)
2384
+ parachute-vault tokens list --vault <name> List tokens for one vault only
2385
+ parachute-vault tokens create Create a vault-bound token in the default vault
2386
+ parachute-vault tokens create --vault <name> Create a token bound to a specific vault
2387
+ parachute-vault tokens create --all Create a server-wide token (vault_name=NULL).
2388
+ Authenticates against any vault — use sparingly,
2389
+ for cross-vault automation only.
2129
2390
  parachute-vault tokens create --read Read-only token (shorthand for --scope vault:read)
2130
2391
  parachute-vault tokens create --scope vault:write
2131
2392
  Narrow the token's scopes. Accepts a comma-separated
@@ -2160,9 +2421,9 @@ Import/Export:
2160
2421
 
2161
2422
  ── Advanced / standalone ──────────────────────────────────────────────
2162
2423
 
2163
- Direct daemon controls. For normal use, prefer the Parachute CLI wrappers
2424
+ Direct daemon controls. For normal use, prefer the Parachute Hub wrappers
2164
2425
  — they add PID tracking, log rotation, and cross-service \`parachute status\`
2165
- visibility. Use these when running vault without the CLI or when debugging.
2426
+ visibility. Use these when running vault without the hub or when debugging.
2166
2427
 
2167
2428
  parachute-vault serve Run server in the foreground (no PID tracking).
2168
2429
  Prefer \`parachute start vault\` for managed lifecycle.
@@ -2170,5 +2431,9 @@ visibility. Use these when running vault without the CLI or when debugging.
2170
2431
  Prefer \`parachute status\` for a cross-service view.
2171
2432
  parachute-vault logs Stream server logs
2172
2433
  parachute-vault restart Restart the daemon
2434
+ parachute-vault stop Signal a graceful shutdown of the running server
2435
+ (writes \`~/.parachute/vault/stop.signal\` — useful
2436
+ when no signal channel is available, e.g. Docker
2437
+ exec or unmanaged foreground runs).
2173
2438
  `);
2174
2439
  }