@openparachute/vault 0.3.3 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parachute/module.json +15 -0
- package/README.md +133 -0
- package/core/src/core.test.ts +2990 -92
- package/core/src/links.ts +1 -1
- package/core/src/mcp.ts +413 -68
- package/core/src/notes.ts +693 -42
- package/core/src/obsidian.ts +3 -3
- package/core/src/paths.ts +1 -1
- package/core/src/query-operators.ts +23 -7
- package/core/src/schema-defaults.ts +331 -0
- package/core/src/schema.ts +467 -11
- package/core/src/store.ts +262 -8
- package/core/src/tag-hierarchy.ts +171 -0
- package/core/src/tag-schemas.ts +242 -42
- package/core/src/types.ts +96 -7
- package/core/src/vault-projection.ts +309 -0
- package/core/src/wikilinks.ts +3 -3
- package/package.json +13 -3
- package/src/admin-spa.test.ts +161 -0
- package/src/admin-spa.ts +161 -0
- package/src/auth-hub-jwt.test.ts +360 -0
- package/src/auth-status.ts +84 -0
- package/src/auth.test.ts +135 -23
- package/src/auth.ts +173 -15
- package/src/backup.ts +4 -7
- package/src/cli.ts +322 -57
- package/src/config.test.ts +44 -0
- package/src/config.ts +68 -40
- package/src/hub-jwt.test.ts +307 -0
- package/src/hub-jwt.ts +88 -0
- package/src/init.test.ts +216 -0
- package/src/mcp-http.ts +33 -29
- package/src/mcp-install.ts +1 -1
- package/src/mcp-tools.ts +318 -19
- package/src/module-config.ts +1 -1
- package/src/oauth.test.ts +345 -0
- package/src/oauth.ts +85 -14
- package/src/owner-auth.ts +57 -1
- package/src/prompt.ts +6 -5
- package/src/routes.ts +796 -61
- package/src/routing.test.ts +466 -1
- package/src/routing.ts +106 -24
- package/src/scopes.test.ts +66 -8
- package/src/scopes.ts +163 -37
- package/src/server.ts +24 -2
- package/src/services-manifest.test.ts +20 -0
- package/src/services-manifest.ts +9 -2
- package/src/stop-signal.test.ts +85 -0
- package/src/storage.test.ts +92 -0
- package/src/tag-scope.ts +118 -0
- package/src/token-store.test.ts +47 -0
- package/src/token-store.ts +128 -13
- package/src/tokens-routes.test.ts +727 -0
- package/src/tokens-routes.ts +392 -0
- package/src/transcription-worker.test.ts +5 -0
- package/src/triggers.ts +1 -1
- package/src/two-factor.ts +2 -2
- package/src/vault-create.test.ts +193 -0
- package/src/vault-name.test.ts +123 -0
- package/src/vault-name.ts +80 -0
- package/src/vault.test.ts +1626 -183
- package/tsconfig.json +8 -1
- package/.claude/settings.local.json +0 -8
- package/.dockerignore +0 -8
- package/.env.example +0 -9
- package/CHANGELOG.md +0 -175
- package/CLAUDE.md +0 -125
- package/Caddyfile +0 -3
- package/Dockerfile +0 -22
- package/bun.lock +0 -219
- package/bunfig.toml +0 -2
- package/deploy/parachute-vault.service +0 -20
- package/docker-compose.yml +0 -50
- package/docs/HTTP_API.md +0 -434
- package/docs/auth-model.md +0 -340
- package/fly.toml +0 -24
- package/package/package.json +0 -32
- package/railway.json +0 -14
- package/scripts/migrate-audio-to-opus.test.ts +0 -237
- 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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
console.log(
|
|
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/
|
|
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 —
|
|
288
|
-
// `.well-known/parachute.json` URL and for `parachute expose
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
353
|
-
(
|
|
354
|
-
|
|
355
|
-
|
|
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("
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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 (
|
|
2105
|
+
} else if (arg === "--obsidian") {
|
|
1866
2106
|
format = "obsidian";
|
|
1867
2107
|
} else {
|
|
1868
|
-
positional.push(
|
|
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
|
-
|
|
1972
|
-
|
|
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(
|
|
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
|
|
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>
|
|
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
|
|
2127
|
-
parachute-vault tokens
|
|
2128
|
-
parachute-vault tokens create
|
|
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
|
|
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
|
|
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
|
}
|