@openparachute/vault 0.3.1 → 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.
- package/.parachute/module.json +15 -0
- package/README.md +9 -5
- package/core/src/core.test.ts +2252 -7
- package/core/src/links.ts +1 -1
- package/core/src/mcp.ts +801 -67
- package/core/src/note-schemas.ts +232 -0
- package/core/src/notes.ts +313 -35
- 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 +287 -0
- package/core/src/schema.ts +393 -9
- package/core/src/store.ts +248 -6
- package/core/src/tag-hierarchy.ts +137 -0
- package/core/src/tag-schemas.ts +242 -42
- package/core/src/types.ts +100 -6
- 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 +231 -0
- package/src/auth-status.ts +84 -0
- package/src/auth.test.ts +135 -23
- package/src/auth.ts +144 -15
- package/src/backup.ts +4 -7
- package/src/cli.ts +384 -78
- package/src/config.test.ts +44 -0
- package/src/config.ts +68 -40
- package/src/hub-jwt.test.ts +296 -0
- package/src/hub-jwt.ts +79 -0
- package/src/init-summary.test.ts +133 -0
- package/src/init-summary.ts +90 -0
- package/src/init.test.ts +216 -0
- package/src/mcp-http.ts +30 -28
- package/src/mcp-install.ts +1 -1
- package/src/mcp-tools.ts +294 -6
- 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 +31 -14
- package/src/routes.ts +686 -58
- package/src/routing.test.ts +466 -1
- package/src/routing.ts +108 -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 +720 -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 +868 -3
- 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,11 +46,13 @@ 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";
|
|
50
53
|
import { installAgent, uninstallAgent, isAgentLoaded, restartAgent } from "./launchd.ts";
|
|
51
54
|
import { chooseMcpUrl } from "./mcp-install.ts";
|
|
55
|
+
import { buildInitSummaryLines } from "./init-summary.ts";
|
|
52
56
|
import {
|
|
53
57
|
runBackup,
|
|
54
58
|
readLastBackup,
|
|
@@ -81,6 +85,7 @@ import { resolveBindHostname } from "./bind.ts";
|
|
|
81
85
|
import { generateToken, createToken, listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
|
|
82
86
|
import type { TokenPermission } from "./token-store.ts";
|
|
83
87
|
import { resolveCreateTokenFlags, VAULT_SCOPES } from "./scopes.ts";
|
|
88
|
+
import { validateVaultName, decideInitVaultName } from "./vault-name.ts";
|
|
84
89
|
import { getVaultStore } from "./vault-store.ts";
|
|
85
90
|
import { upsertService, ServicesManifestError } from "./services-manifest.ts";
|
|
86
91
|
import {
|
|
@@ -177,6 +182,9 @@ switch (command) {
|
|
|
177
182
|
case "restart":
|
|
178
183
|
await cmdRestart();
|
|
179
184
|
break;
|
|
185
|
+
case "stop":
|
|
186
|
+
await cmdStop();
|
|
187
|
+
break;
|
|
180
188
|
case "uninstall":
|
|
181
189
|
await cmdUninstall(cmdArgs);
|
|
182
190
|
break;
|
|
@@ -218,6 +226,27 @@ switch (command) {
|
|
|
218
226
|
// Command implementations
|
|
219
227
|
// ---------------------------------------------------------------------------
|
|
220
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
|
+
|
|
221
250
|
async function cmdInit(args: string[] = []) {
|
|
222
251
|
ensureConfigDirSync();
|
|
223
252
|
|
|
@@ -225,8 +254,37 @@ async function cmdInit(args: string[] = []) {
|
|
|
225
254
|
// --no-mcp skips it without prompting. If both passed, --no-mcp wins
|
|
226
255
|
// (safer default). Neither → prompt in a TTY, default-yes in a
|
|
227
256
|
// non-TTY for back-compat with existing piped install scripts.
|
|
257
|
+
//
|
|
258
|
+
// --token / --no-token follow the same pattern for whether the API
|
|
259
|
+
// token is surfaced to the user at the end of init (for pasting into
|
|
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).
|
|
228
264
|
const flagMcpOn = args.includes("--mcp");
|
|
229
265
|
const flagMcpOff = args.includes("--no-mcp");
|
|
266
|
+
const flagTokenOn = args.includes("--token");
|
|
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;
|
|
230
288
|
|
|
231
289
|
const isMac = process.platform === "darwin";
|
|
232
290
|
const isLinux = process.platform === "linux";
|
|
@@ -234,14 +292,23 @@ async function cmdInit(args: string[] = []) {
|
|
|
234
292
|
|
|
235
293
|
console.log("Parachute Vault — self-hosted knowledge graph\n");
|
|
236
294
|
|
|
237
|
-
// 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.
|
|
238
298
|
const vaults = listVaults();
|
|
239
299
|
let apiKey: string | undefined;
|
|
240
300
|
if (vaults.length === 0) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
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}`);
|
|
244
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
|
+
}
|
|
245
312
|
console.log(`Found ${vaults.length} existing vault(s)`);
|
|
246
313
|
}
|
|
247
314
|
|
|
@@ -272,30 +339,28 @@ async function cmdInit(args: string[] = []) {
|
|
|
272
339
|
}
|
|
273
340
|
writeGlobalConfig(globalConfig);
|
|
274
341
|
|
|
275
|
-
// 2a. Register in the shared services manifest so the @openparachute/
|
|
342
|
+
// 2a. Register in the shared services manifest so the @openparachute/hub
|
|
276
343
|
// dispatcher can discover this service and its health endpoint. Upserts
|
|
277
344
|
// by name, preserving entries for other services. Non-fatal on failure —
|
|
278
345
|
// init can complete without the manifest, just with a warning.
|
|
279
346
|
//
|
|
280
|
-
// `paths[0]` is the canonical mount point —
|
|
281
|
-
// `.well-known/parachute.json` URL and for `parachute expose
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
? `/vault/${globalConfig.default_vault}`
|
|
287
|
-
: "/";
|
|
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).
|
|
288
353
|
try {
|
|
289
354
|
upsertService({
|
|
290
355
|
name: "parachute-vault",
|
|
291
356
|
port: globalConfig.port || DEFAULT_PORT,
|
|
292
|
-
paths:
|
|
357
|
+
paths: buildVaultServicePaths(globalConfig.default_vault, allVaults),
|
|
293
358
|
health: "/health",
|
|
294
359
|
version: pkg.version,
|
|
295
360
|
});
|
|
296
361
|
} catch (err) {
|
|
297
362
|
const msg = err instanceof ServicesManifestError ? err.message : String(err);
|
|
298
|
-
console.
|
|
363
|
+
console.error(` Warning: could not update ~/.parachute/services.json: ${msg}`);
|
|
299
364
|
}
|
|
300
365
|
|
|
301
366
|
// 2b. Migrate existing legacy keys into per-vault token tables
|
|
@@ -325,28 +390,67 @@ async function cmdInit(args: string[] = []) {
|
|
|
325
390
|
console.log();
|
|
326
391
|
}
|
|
327
392
|
|
|
328
|
-
// 5b.
|
|
393
|
+
// 5b. Owner password is only needed for OAuth consent (browser-based
|
|
394
|
+
// clients like claude.ai / ChatGPT / Claude Desktop). Those paths are
|
|
395
|
+
// coming in the next few weeks; until then, skip the prompt. Users who
|
|
396
|
+
// want to expose the vault publicly today can set one manually via
|
|
397
|
+
// `parachute-vault set-password`.
|
|
329
398
|
if (!hasOwnerPassword()) {
|
|
330
|
-
|
|
399
|
+
console.log();
|
|
400
|
+
console.log("Public exposure + web-AI connectors (claude.ai, ChatGPT, etc.) are coming soon.");
|
|
401
|
+
console.log(" When you're ready to expose this vault publicly, run:");
|
|
402
|
+
console.log(" parachute-vault set-password # required for OAuth consent");
|
|
331
403
|
}
|
|
332
404
|
|
|
333
405
|
// 6. Install daemon (platform-aware). Idempotent — safe to re-run after
|
|
334
406
|
// a folder move; this refreshes ~/.parachute/server-path and bounces the
|
|
335
407
|
// daemon so the new location takes effect immediately.
|
|
336
|
-
|
|
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
|
+
|
|
337
429
|
let serverPath: string | null = null;
|
|
338
|
-
if (
|
|
339
|
-
(
|
|
340
|
-
|
|
341
|
-
|
|
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");
|
|
342
439
|
} else {
|
|
343
|
-
console.log("
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
|
350
454
|
}
|
|
351
455
|
const bindHost = resolveBindHostname(process.env);
|
|
352
456
|
console.log(` Listening on http://${bindHost}:${globalConfig.port || DEFAULT_PORT}`);
|
|
@@ -361,11 +465,43 @@ async function cmdInit(args: string[] = []) {
|
|
|
361
465
|
} else if (flagMcpOn) {
|
|
362
466
|
addMcp = true;
|
|
363
467
|
} else if (process.stdin.isTTY) {
|
|
364
|
-
addMcp = await confirm("
|
|
468
|
+
addMcp = await confirm("Install Vault as an MCP server in Claude Code (~/.claude.json)?", true);
|
|
365
469
|
} else {
|
|
366
470
|
addMcp = true; // non-interactive: preserve the installable-via-pipe default
|
|
367
471
|
}
|
|
368
472
|
|
|
473
|
+
// 7b. Surface an API token for other clients? (Codex, Goose, OpenCode,
|
|
474
|
+
// Cursor, Zed, Cline, scripts, curl.) Same flag/TTY precedence as MCP.
|
|
475
|
+
// Note: a token is always minted when addMcp is true (it gets baked into
|
|
476
|
+
// the ~/.claude.json entry); this prompt controls whether that token is
|
|
477
|
+
// printed prominently at the end so the user can paste it elsewhere.
|
|
478
|
+
let addToken: boolean;
|
|
479
|
+
if (flagTokenOff) {
|
|
480
|
+
addToken = false;
|
|
481
|
+
} else if (flagTokenOn) {
|
|
482
|
+
addToken = true;
|
|
483
|
+
} else if (process.stdin.isTTY) {
|
|
484
|
+
addToken = await confirm(
|
|
485
|
+
"Generate an API token for other MCP clients (Codex, Goose, OpenCode, Cursor, Zed, Cline), scripts, or curl?",
|
|
486
|
+
true,
|
|
487
|
+
);
|
|
488
|
+
} else {
|
|
489
|
+
addToken = true; // non-interactive: default-yes matches addMcp default
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Mint a token if we need one (for the claude.json entry and/or for
|
|
493
|
+
// prominent display) and don't already have one from vault creation.
|
|
494
|
+
// Re-runs of init that opt in will mint a fresh token — old tokens
|
|
495
|
+
// continue to work; the user can `tokens revoke` the unused ones.
|
|
496
|
+
const defaultVault = globalConfig.default_vault || "default";
|
|
497
|
+
const needToken = addMcp || addToken;
|
|
498
|
+
if (needToken && !apiKey) {
|
|
499
|
+
const store = getVaultStore(defaultVault);
|
|
500
|
+
const { fullToken } = generateToken();
|
|
501
|
+
createToken(store.db, fullToken, { label: "init", permission: "full" });
|
|
502
|
+
apiKey = fullToken;
|
|
503
|
+
}
|
|
504
|
+
|
|
369
505
|
if (addMcp) {
|
|
370
506
|
installMcpConfig(apiKey);
|
|
371
507
|
console.log(` MCP server added to ~/.claude.json`);
|
|
@@ -375,30 +511,31 @@ async function cmdInit(args: string[] = []) {
|
|
|
375
511
|
}
|
|
376
512
|
|
|
377
513
|
// 8. Summary
|
|
378
|
-
console.log("\n---");
|
|
379
514
|
const port = globalConfig.port || DEFAULT_PORT;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
515
|
+
const mcpUrl = `http://127.0.0.1:${port}/vault/${defaultVault}/mcp`;
|
|
516
|
+
const lines = buildInitSummaryLines({
|
|
517
|
+
addMcp,
|
|
518
|
+
addToken,
|
|
519
|
+
apiKey,
|
|
520
|
+
configDir: CONFIG_DIR,
|
|
521
|
+
bindHost,
|
|
522
|
+
port,
|
|
523
|
+
mcpUrl,
|
|
524
|
+
});
|
|
525
|
+
for (const line of lines) console.log(line);
|
|
526
|
+
}
|
|
387
527
|
|
|
388
|
-
console.log(`\nConfig: ${CONFIG_DIR}`);
|
|
389
|
-
console.log(`Server: http://${bindHost}:${port}`);
|
|
390
528
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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}`);
|
|
395
535
|
}
|
|
396
|
-
|
|
397
|
-
console.log(`\nNext steps:`);
|
|
398
|
-
console.log(` parachute-vault status check everything is running`);
|
|
399
|
-
console.log(` parachute-vault config view/edit configuration`);
|
|
400
536
|
}
|
|
401
537
|
|
|
538
|
+
|
|
402
539
|
async function promptForOwnerPassword(purpose: string): Promise<boolean> {
|
|
403
540
|
console.log(`\n${purpose}`);
|
|
404
541
|
console.log(" Used on the OAuth consent page to authorize third-party clients");
|
|
@@ -628,9 +765,20 @@ async function cmd2fa(args: string[]) {
|
|
|
628
765
|
}
|
|
629
766
|
|
|
630
767
|
function cmdCreate(args: string[]) {
|
|
631
|
-
|
|
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];
|
|
632
780
|
if (!name) {
|
|
633
|
-
console.error("Usage: parachute-vault create <name>");
|
|
781
|
+
console.error("Usage: parachute-vault create <name> [--json]");
|
|
634
782
|
process.exit(1);
|
|
635
783
|
}
|
|
636
784
|
|
|
@@ -663,14 +811,49 @@ function cmdCreate(args: string[]) {
|
|
|
663
811
|
const needsDefault = !globalConfig.default_vault
|
|
664
812
|
|| !listVaults().includes(globalConfig.default_vault);
|
|
665
813
|
let defaultNote: string | null = null;
|
|
814
|
+
let setAsDefault = false;
|
|
666
815
|
if (needsDefault) {
|
|
667
816
|
globalConfig.default_vault = name;
|
|
668
817
|
writeGlobalConfig(globalConfig);
|
|
818
|
+
setAsDefault = true;
|
|
669
819
|
defaultNote = wasFirst
|
|
670
820
|
? `Set as default vault (unscoped routes will target "${name}")`
|
|
671
821
|
: `Set as default vault (previous default was missing)`;
|
|
672
822
|
}
|
|
673
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
|
+
|
|
674
857
|
console.log(`Vault "${name}" created.`);
|
|
675
858
|
console.log(` Path: ${vaultDir(name)}`);
|
|
676
859
|
console.log(` API token: ${key}`);
|
|
@@ -815,20 +998,38 @@ async function cmdConfig(args: string[]) {
|
|
|
815
998
|
function cmdTokens(args: string[]) {
|
|
816
999
|
const subcmd = args[0];
|
|
817
1000
|
|
|
818
|
-
// 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.
|
|
819
1004
|
if (!subcmd || subcmd === "list") {
|
|
820
|
-
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();
|
|
821
1012
|
let anyTokens = false;
|
|
822
1013
|
|
|
823
1014
|
for (const vaultName of vaults) {
|
|
824
1015
|
const vc = readVaultConfig(vaultName);
|
|
825
|
-
if (!vc)
|
|
1016
|
+
if (!vc) {
|
|
1017
|
+
if (onlyVault) {
|
|
1018
|
+
console.error(`Vault "${vaultName}" not found.`);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
826
1023
|
const store = getVaultStore(vaultName);
|
|
827
1024
|
// Ensure legacy keys are migrated
|
|
828
1025
|
const globalCfg = readGlobalConfig();
|
|
829
1026
|
migrateVaultKeys(store.db, vc.api_keys, globalCfg.api_keys);
|
|
830
1027
|
|
|
831
|
-
|
|
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 });
|
|
832
1033
|
if (tokens.length === 0) continue;
|
|
833
1034
|
anyTokens = true;
|
|
834
1035
|
|
|
@@ -836,7 +1037,8 @@ function cmdTokens(args: string[]) {
|
|
|
836
1037
|
for (const t of tokens) {
|
|
837
1038
|
const expiry = t.expires_at ? ` (expires: ${t.expires_at})` : "";
|
|
838
1039
|
const lastUsed = t.last_used_at ? ` (last used: ${t.last_used_at})` : "";
|
|
839
|
-
|
|
1040
|
+
const serverWide = t.vault_name === null ? " [server-wide]" : "";
|
|
1041
|
+
console.log(` ${t.id} ${t.label} [${t.permission}]${serverWide}${expiry}${lastUsed}`);
|
|
840
1042
|
}
|
|
841
1043
|
console.log();
|
|
842
1044
|
}
|
|
@@ -847,12 +1049,26 @@ function cmdTokens(args: string[]) {
|
|
|
847
1049
|
return;
|
|
848
1050
|
}
|
|
849
1051
|
|
|
850
|
-
// parachute-vault tokens create --vault <name>
|
|
1052
|
+
// parachute-vault tokens create [--vault <name> | --all]
|
|
851
1053
|
// [--scope vault:read,vault:write | --read | --permission full|read]
|
|
852
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.
|
|
853
1060
|
if (subcmd === "create") {
|
|
854
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
|
+
}
|
|
855
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
|
+
}
|
|
856
1072
|
|
|
857
1073
|
const vc = readVaultConfig(vaultName);
|
|
858
1074
|
if (!vc) {
|
|
@@ -876,6 +1092,10 @@ function cmdTokens(args: string[]) {
|
|
|
876
1092
|
let expiresAt: string | null = null;
|
|
877
1093
|
if (expiresFlag !== -1) {
|
|
878
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
|
+
}
|
|
879
1099
|
expiresAt = parseDuration(dur);
|
|
880
1100
|
if (!expiresAt) {
|
|
881
1101
|
console.error(`Invalid duration: ${dur}. Use format like 7d, 30d, 24h, 1y.`);
|
|
@@ -884,7 +1104,7 @@ function cmdTokens(args: string[]) {
|
|
|
884
1104
|
}
|
|
885
1105
|
|
|
886
1106
|
const labelFlag = args.indexOf("--label");
|
|
887
|
-
const label = labelFlag !== -1 ? args[labelFlag + 1] : "default";
|
|
1107
|
+
const label = (labelFlag !== -1 ? args[labelFlag + 1] : undefined) ?? "default";
|
|
888
1108
|
|
|
889
1109
|
const store = getVaultStore(vaultName);
|
|
890
1110
|
const { fullToken } = generateToken();
|
|
@@ -893,15 +1113,22 @@ function cmdTokens(args: string[]) {
|
|
|
893
1113
|
permission,
|
|
894
1114
|
scopes,
|
|
895
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,
|
|
896
1119
|
});
|
|
897
1120
|
|
|
898
1121
|
const displayScopes = scopes ?? [...VAULT_SCOPES];
|
|
899
|
-
|
|
1122
|
+
const heading = allFlag
|
|
1123
|
+
? `Created server-wide token (authenticates against any vault):`
|
|
1124
|
+
: `Created token for vault "${vaultName}":`;
|
|
1125
|
+
console.log(heading);
|
|
900
1126
|
console.log(` Token: ${fullToken}`);
|
|
901
1127
|
console.log(` Permission: ${permission}`);
|
|
902
1128
|
console.log(` Scopes: ${displayScopes.join(" ")}`);
|
|
903
1129
|
if (expiresAt) console.log(` Expires: ${expiresAt}`);
|
|
904
1130
|
console.log(` Label: ${label}`);
|
|
1131
|
+
if (!allFlag) console.log(` Vault: ${vaultName}`);
|
|
905
1132
|
console.log();
|
|
906
1133
|
console.log("Save this token — it will not be shown again.");
|
|
907
1134
|
return;
|
|
@@ -917,6 +1144,10 @@ function cmdTokens(args: string[]) {
|
|
|
917
1144
|
|
|
918
1145
|
const vaultFlag = args.indexOf("--vault");
|
|
919
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
|
+
}
|
|
920
1151
|
|
|
921
1152
|
const vc = readVaultConfig(vaultName);
|
|
922
1153
|
if (!vc) {
|
|
@@ -942,8 +1173,8 @@ function cmdTokens(args: string[]) {
|
|
|
942
1173
|
function parseDuration(dur: string): string | null {
|
|
943
1174
|
const match = dur.match(/^(\d+)(h|d|w|m|y)$/);
|
|
944
1175
|
if (!match) return null;
|
|
945
|
-
const n = parseInt(match[1]
|
|
946
|
-
const unit = match[2]
|
|
1176
|
+
const n = parseInt(match[1]!, 10);
|
|
1177
|
+
const unit = match[2]!;
|
|
947
1178
|
const now = new Date();
|
|
948
1179
|
switch (unit) {
|
|
949
1180
|
case "h": now.setHours(now.getHours() + n); break;
|
|
@@ -1001,6 +1232,41 @@ async function cmdRestart() {
|
|
|
1001
1232
|
process.exit(1);
|
|
1002
1233
|
}
|
|
1003
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
|
+
|
|
1004
1270
|
async function cmdStatus() {
|
|
1005
1271
|
loadEnvFile();
|
|
1006
1272
|
const globalConfig = readGlobalConfig();
|
|
@@ -1819,16 +2085,27 @@ async function cmdImport(args: string[]) {
|
|
|
1819
2085
|
|
|
1820
2086
|
const positional: string[] = [];
|
|
1821
2087
|
for (let i = 0; i < args.length; i++) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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") {
|
|
1827
2104
|
dryRun = true;
|
|
1828
|
-
} else if (
|
|
2105
|
+
} else if (arg === "--obsidian") {
|
|
1829
2106
|
format = "obsidian";
|
|
1830
2107
|
} else {
|
|
1831
|
-
positional.push(
|
|
2108
|
+
positional.push(arg);
|
|
1832
2109
|
}
|
|
1833
2110
|
}
|
|
1834
2111
|
sourcePath = positional[0] ?? "";
|
|
@@ -1931,10 +2208,16 @@ async function cmdExport(args: string[]) {
|
|
|
1931
2208
|
|
|
1932
2209
|
const positional: string[] = [];
|
|
1933
2210
|
for (let i = 0; i < args.length; i++) {
|
|
1934
|
-
|
|
1935
|
-
|
|
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;
|
|
1936
2219
|
} else {
|
|
1937
|
-
positional.push(
|
|
2220
|
+
positional.push(arg);
|
|
1938
2221
|
}
|
|
1939
2222
|
}
|
|
1940
2223
|
outputPath = positional[0] ?? "";
|
|
@@ -2058,7 +2341,7 @@ function usage() {
|
|
|
2058
2341
|
console.log(`
|
|
2059
2342
|
Parachute Vault — self-hosted knowledge graph
|
|
2060
2343
|
|
|
2061
|
-
If you installed via the Parachute
|
|
2344
|
+
If you installed via the Parachute Hub, prefer the wrapper commands for
|
|
2062
2345
|
lifecycle — \`parachute start vault\`, \`parachute stop vault\`,
|
|
2063
2346
|
\`parachute status\` — and use the vault-direct commands below for setup,
|
|
2064
2347
|
data, and debugging.
|
|
@@ -2066,7 +2349,22 @@ data, and debugging.
|
|
|
2066
2349
|
── Standard use ───────────────────────────────────────────────────────
|
|
2067
2350
|
|
|
2068
2351
|
Setup:
|
|
2069
|
-
parachute-vault init [--mcp
|
|
2352
|
+
parachute-vault init [--mcp|--no-mcp] [--token|--no-token] [--vault-name <name>]
|
|
2353
|
+
[--autostart|--no-autostart]
|
|
2354
|
+
Set up everything (one command, idempotent).
|
|
2355
|
+
--mcp/--no-mcp controls the Claude Code MCP entry;
|
|
2356
|
+
--token/--no-token controls whether an API token is
|
|
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'.
|
|
2070
2368
|
parachute-vault doctor Diagnose install/config issues
|
|
2071
2369
|
parachute-vault uninstall [--wipe] [--yes]
|
|
2072
2370
|
Remove daemon + MCP entry; --wipe also removes vaults, .env,
|
|
@@ -2076,15 +2374,19 @@ Setup:
|
|
|
2076
2374
|
parachute --version Print the installed version (alias: -v, version)
|
|
2077
2375
|
|
|
2078
2376
|
Vaults:
|
|
2079
|
-
parachute-vault create <name>
|
|
2377
|
+
parachute-vault create <name> [--json] Create a new vault (--json: emit { name, token, paths, set_as_default })
|
|
2080
2378
|
parachute-vault list List all vaults
|
|
2081
2379
|
parachute-vault remove <name> [--yes] Remove a vault
|
|
2082
2380
|
parachute-vault mcp-install Add vault MCP to Claude
|
|
2083
2381
|
|
|
2084
2382
|
Tokens:
|
|
2085
|
-
parachute-vault tokens List
|
|
2086
|
-
parachute-vault tokens
|
|
2087
|
-
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.
|
|
2088
2390
|
parachute-vault tokens create --read Read-only token (shorthand for --scope vault:read)
|
|
2089
2391
|
parachute-vault tokens create --scope vault:write
|
|
2090
2392
|
Narrow the token's scopes. Accepts a comma-separated
|
|
@@ -2119,9 +2421,9 @@ Import/Export:
|
|
|
2119
2421
|
|
|
2120
2422
|
── Advanced / standalone ──────────────────────────────────────────────
|
|
2121
2423
|
|
|
2122
|
-
Direct daemon controls. For normal use, prefer the Parachute
|
|
2424
|
+
Direct daemon controls. For normal use, prefer the Parachute Hub wrappers
|
|
2123
2425
|
— they add PID tracking, log rotation, and cross-service \`parachute status\`
|
|
2124
|
-
visibility. Use these when running vault without the
|
|
2426
|
+
visibility. Use these when running vault without the hub or when debugging.
|
|
2125
2427
|
|
|
2126
2428
|
parachute-vault serve Run server in the foreground (no PID tracking).
|
|
2127
2429
|
Prefer \`parachute start vault\` for managed lifecycle.
|
|
@@ -2129,5 +2431,9 @@ visibility. Use these when running vault without the CLI or when debugging.
|
|
|
2129
2431
|
Prefer \`parachute status\` for a cross-service view.
|
|
2130
2432
|
parachute-vault logs Stream server logs
|
|
2131
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).
|
|
2132
2438
|
`);
|
|
2133
2439
|
}
|