keymaxxer 0.1.2 → 0.2.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 (3) hide show
  1. package/README.md +73 -54
  2. package/dist/cli.mjs +530 -484
  3. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // packages/cli/src/index.ts
5
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
5
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
6
6
 
7
7
  // packages/sdk/src/scrubber.ts
8
8
  var MIN_SCRUB_LEN = 4;
@@ -59,7 +59,7 @@ function runWithSecrets(opts) {
59
59
  }
60
60
  // packages/sdk/src/store.ts
61
61
  import { connect } from "@tursodatabase/database";
62
- import { mkdirSync } from "node:fs";
62
+ import { chmodSync, mkdirSync } from "node:fs";
63
63
  import { homedir } from "node:os";
64
64
  import { dirname, join } from "node:path";
65
65
  var DEFAULT_CIPHER = "aes256gcm";
@@ -82,6 +82,9 @@ class SecretStore {
82
82
  static async open(opts) {
83
83
  const path = opts.path ?? defaultVaultPath();
84
84
  mkdirSync(dirname(path), { recursive: true });
85
+ try {
86
+ chmodSync(dirname(path), 448);
87
+ } catch {}
85
88
  try {
86
89
  const db = await connect(path, {
87
90
  encryption: { cipher: opts.cipher ?? DEFAULT_CIPHER, hexkey: opts.hexkey },
@@ -291,9 +294,8 @@ function newPassphraseMeta(cipher, salt) {
291
294
  function newExternalKeyMeta(cipher) {
292
295
  return { version: 1, cipher, kdf: "none" };
293
296
  }
294
- // packages/cli/src/agent.ts
295
- import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
296
- import { createServer } from "node:net";
297
+ // packages/cli/src/client.ts
298
+ import { existsSync as existsSync2 } from "node:fs";
297
299
  // packages/cli/src/approver.ts
298
300
  import { spawn as spawn2 } from "node:child_process";
299
301
  function isSensitive(m) {
@@ -310,32 +312,43 @@ function describe(req) {
310
312
  const names = req.secrets.map((s) => `${s.name} (${[s.provider, s.environment, s.access].filter(Boolean).join(" / ")})`).join(", ");
311
313
  const cmd = req.command.length > 200 ? req.command.slice(0, 197) + "..." : req.command;
312
314
  return [
313
- "An agent wants to USE a sensitive secret:",
314
315
  `Secret: ${names}`,
315
316
  `Command: ${cmd}`,
316
- `Dir: ${req.cwd}`
317
+ `Dir: ${req.cwd}`,
318
+ "",
319
+ "Allow once = this command only. Allow session = until the vault locks."
317
320
  ];
318
321
  }
319
322
  function approveViaOsascript(req) {
320
323
  return new Promise((resolve) => {
321
324
  const body = describe(req).map(asQuote).join(" & return & ");
322
- const script = `display dialog ${body} with title "keymaxxer approve secret use" ` + `buttons {"Deny", "Allow"} default button "Deny" cancel button "Deny" giving up after 60`;
325
+ const script = `display alert "An agent wants to use a sensitive secret" message (${body}) as critical ` + `buttons {"Deny", "Allow once", "Allow session"} default button "Allow once" ` + `cancel button "Deny" giving up after 60`;
323
326
  const proc = spawn2("osascript", ["-e", script]);
324
327
  let out = "";
325
328
  proc.stdout.on("data", (c) => out += c.toString());
326
- proc.on("error", () => resolve(false));
327
- proc.on("close", (code) => resolve(code === 0 && /button returned:Allow/.test(out)));
329
+ proc.on("error", () => resolve("deny"));
330
+ proc.on("close", (code) => {
331
+ if (code !== 0)
332
+ return resolve("deny");
333
+ if (/button returned:Allow session/.test(out))
334
+ return resolve("session");
335
+ if (/button returned:Allow once/.test(out))
336
+ return resolve("once");
337
+ return resolve("deny");
338
+ });
328
339
  });
329
340
  }
330
341
  async function requestApproval(req) {
331
342
  const override = (process.env.KEYMAXXER_APPROVE ?? "").toLowerCase();
332
- if (override === "allow")
333
- return true;
334
343
  if (override === "deny")
335
- return false;
344
+ return "deny";
345
+ if (override === "session")
346
+ return "session";
347
+ if (override === "allow" || override === "once")
348
+ return "once";
336
349
  if (process.platform === "darwin")
337
350
  return approveViaOsascript(req);
338
- return false;
351
+ return "deny";
339
352
  }
340
353
 
341
354
  // packages/cli/src/paths.ts
@@ -347,320 +360,178 @@ function keymaxxerDir() {
347
360
  function vaultPath() {
348
361
  return join3(keymaxxerDir(), "vault.db");
349
362
  }
350
- function socketPath() {
351
- return join3(keymaxxerDir(), "agent.sock");
352
- }
353
- function pidPath() {
354
- return join3(keymaxxerDir(), "agent.pid");
363
+
364
+ // packages/cli/src/prompt.ts
365
+ import { spawn as spawn3 } from "node:child_process";
366
+ var stdinAttached = false;
367
+ var buffer = "";
368
+ var ended = false;
369
+ var queued = [];
370
+ var waiters = [];
371
+ function attachStdinLines() {
372
+ if (stdinAttached)
373
+ return;
374
+ stdinAttached = true;
375
+ process.stdin.on("data", (d) => {
376
+ buffer += d.toString();
377
+ let nl;
378
+ while ((nl = buffer.indexOf(`
379
+ `)) >= 0) {
380
+ const line = buffer.slice(0, nl).replace(/\r$/, "");
381
+ buffer = buffer.slice(nl + 1);
382
+ const w = waiters.shift();
383
+ if (w)
384
+ w(line);
385
+ else
386
+ queued.push(line);
387
+ }
388
+ });
389
+ process.stdin.on("end", () => {
390
+ ended = true;
391
+ if (buffer.length) {
392
+ queued.push(buffer);
393
+ buffer = "";
394
+ }
395
+ while (waiters.length)
396
+ waiters.shift()(queued.shift() ?? null);
397
+ });
398
+ process.stdin.resume();
355
399
  }
356
- function agentLogPath() {
357
- return join3(keymaxxerDir(), "agent.log");
400
+ function nextLine() {
401
+ attachStdinLines();
402
+ const q = queued.shift();
403
+ if (q !== undefined)
404
+ return Promise.resolve(q);
405
+ if (ended)
406
+ return Promise.resolve(null);
407
+ return new Promise((res) => waiters.push(res));
358
408
  }
359
-
360
- // packages/cli/src/agent.ts
361
- function log(msg) {
362
- try {
363
- appendFileSync(agentLogPath(), `${new Date().toISOString()} ${msg}
409
+ function readHiddenLine(promptText) {
410
+ return new Promise((resolve) => {
411
+ const stdin = process.stdin;
412
+ process.stderr.write(promptText);
413
+ stdin.setRawMode(true);
414
+ stdin.resume();
415
+ let buf = "";
416
+ const cleanup = () => {
417
+ stdin.setRawMode(false);
418
+ stdin.pause();
419
+ stdin.off("data", onData);
420
+ };
421
+ const onData = (d) => {
422
+ for (const ch of d.toString("utf8")) {
423
+ const code = ch.charCodeAt(0);
424
+ if (code === 13 || code === 10) {
425
+ cleanup();
426
+ process.stderr.write(`
364
427
  `);
365
- } catch {}
366
- }
367
- function readKeyFromStdin() {
368
- return new Promise((resolve, reject) => {
369
- let data = "";
370
- const onData = (chunk) => {
371
- data += chunk.toString();
372
- const nl = data.indexOf(`
428
+ resolve(buf);
429
+ return;
430
+ } else if (code === 3) {
431
+ cleanup();
432
+ process.stderr.write(`
373
433
  `);
374
- if (nl >= 0) {
375
- process.stdin.off("data", onData);
376
- process.stdin.pause();
377
- resolve(data.slice(0, nl).trim());
434
+ process.exit(130);
435
+ } else if (code === 127 || code === 8) {
436
+ buf = buf.slice(0, -1);
437
+ } else if (code >= 32) {
438
+ buf += ch;
439
+ }
378
440
  }
379
441
  };
380
- process.stdin.on("data", onData);
381
- process.stdin.on("end", () => reject(new Error("stdin closed before key was received")));
382
- process.stdin.on("error", reject);
442
+ stdin.on("data", onData);
383
443
  });
384
444
  }
385
- async function runAgent() {
386
- const sock = socketPath();
387
- const idleMs = Math.max(1, Number(process.env.KEYMAXXER_IDLE_MINUTES) || 15) * 60000;
388
- const hexkey = await readKeyFromStdin();
389
- let store;
390
- try {
391
- store = await SecretStore.open({ path: vaultPath(), hexkey });
392
- } catch (err) {
393
- log(`failed to open vault: ${err instanceof Error ? err.message : String(err)}`);
394
- process.exit(1);
395
- }
396
- let lastActivity = Date.now();
397
- const unlockedAt = Date.now();
398
- const shutdown = (reason) => {
399
- log(`locking (${reason})`);
400
- if (existsSync2(sock))
401
- unlinkSync(sock);
402
- if (existsSync2(pidPath()))
403
- unlinkSync(pidPath());
404
- process.exit(0);
405
- };
406
- const idleTimer = setInterval(() => {
407
- if (Date.now() - lastActivity > idleMs)
408
- shutdown("idle timeout");
409
- }, 5000);
410
- idleTimer.unref();
411
- async function handle(req) {
412
- lastActivity = Date.now();
413
- try {
414
- switch (req.op) {
415
- case "status": {
416
- const result = {
417
- unlocked: true,
418
- vault: vaultPath(),
419
- idleSeconds: Math.floor((Date.now() - lastActivity) / 1000),
420
- idleTimeoutSeconds: Math.floor(idleMs / 1000)
421
- };
422
- return { ok: true, result };
423
- }
424
- case "list":
425
- return { ok: true, result: await store.list() };
426
- case "run": {
427
- const metas = await store.list();
428
- const sensitive = req.req.secrets.map((n) => metas.find((m) => m.name === n)).filter((m) => !!m && isSensitive(m));
429
- if (sensitive.length > 0) {
430
- const names = sensitive.map((s) => s.name).join(", ");
431
- const ok = await requestApproval({
432
- secrets: sensitive,
433
- command: req.req.command,
434
- cwd: req.req.cwd ?? process.cwd()
435
- });
436
- if (!ok) {
437
- await store.auditDenied(req.req.secrets, req.req.command, req.req.cwd ?? process.cwd());
438
- log(`DENIED [${names}]: ${req.req.command}`);
439
- return { ok: false, error: `Denied: use of ${names} was not approved by the user.` };
440
- }
441
- log(`approved [${names}]`);
442
- }
443
- return { ok: true, result: await store.run(req.req) };
444
- }
445
- case "set":
446
- await store.set(req.name, req.value, req.fields);
447
- return { ok: true, result: null };
448
- case "remove":
449
- return { ok: true, result: await store.remove(req.name) };
450
- case "audit":
451
- return { ok: true, result: await store.recentAudit(req.limit) };
452
- case "lock":
453
- setTimeout(() => shutdown("explicit lock"), 10);
454
- return { ok: true, result: null };
455
- default:
456
- return { ok: false, error: `unknown op` };
457
- }
458
- } catch (err) {
459
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
460
- }
461
- }
462
- const server = createServer((conn) => {
463
- let data = "";
464
- conn.on("data", async (chunk) => {
465
- data += chunk.toString();
466
- const nl = data.indexOf(`
467
- `);
468
- if (nl < 0)
469
- return;
470
- let req;
471
- try {
472
- req = JSON.parse(data.slice(0, nl));
473
- } catch {
474
- conn.end(JSON.stringify({ ok: false, error: "malformed request" }) + `
475
- `);
476
- return;
477
- }
478
- const res = await handle(req);
479
- conn.end(JSON.stringify(res) + `
480
- `);
481
- });
482
- conn.on("error", () => conn.destroy());
483
- });
484
- mkdirSync3(keymaxxerDir(), { recursive: true });
485
- try {
486
- chmodSync(keymaxxerDir(), 448);
487
- } catch {}
488
- if (existsSync2(sock))
489
- unlinkSync(sock);
490
- server.listen(sock, () => {
491
- try {
492
- chmodSync(sock, 384);
493
- } catch {}
494
- writeFileSync2(pidPath(), String(process.pid));
495
- log(`unlocked, listening on ${sock} (idle ${idleMs / 60000}m)`);
496
- });
497
- server.on("error", (err) => {
498
- log(`server error: ${err.message}`);
499
- process.exit(1);
500
- });
501
- for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
502
- process.on(sig, () => shutdown(sig));
503
- }
445
+ function asAppleScript(s) {
446
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ") + '"';
504
447
  }
505
-
506
- // packages/cli/src/client.ts
507
- import { spawn as spawn3 } from "node:child_process";
508
- import { existsSync as existsSync3, unlinkSync as unlinkSync2 } from "node:fs";
509
-
510
- // packages/cli/src/protocol.ts
511
- import { connect as connect2 } from "node:net";
512
- function sendRequest(socket, req, timeoutMs = 0) {
513
- return new Promise((resolve, reject) => {
514
- const conn = connect2(socket);
515
- let data = "";
516
- if (timeoutMs > 0)
517
- conn.setTimeout(timeoutMs, () => conn.destroy(new Error("request timed out")));
518
- conn.on("connect", () => conn.write(JSON.stringify(req) + `
519
- `));
520
- conn.on("data", (chunk) => {
521
- data += chunk.toString();
522
- const nl = data.indexOf(`
523
- `);
524
- if (nl >= 0) {
525
- conn.end();
526
- try {
527
- resolve(JSON.parse(data.slice(0, nl)));
528
- } catch (err) {
529
- reject(new Error("malformed response from agent"));
530
- }
531
- }
448
+ function promptPassphraseGui(message) {
449
+ if (process.platform !== "darwin")
450
+ return Promise.resolve(null);
451
+ return new Promise((resolve) => {
452
+ const script = `display dialog ${asAppleScript(message)} default answer "" with hidden answer ` + `with icon note with title "keymaxxer — unlock vault" ` + `buttons {"Cancel", "Unlock"} default button "Unlock" cancel button "Cancel" ` + `giving up after 120`;
453
+ const proc = spawn3("osascript", ["-e", script]);
454
+ let out = "";
455
+ proc.stdout.on("data", (c) => out += c.toString());
456
+ proc.on("error", () => resolve(null));
457
+ proc.on("close", (code) => {
458
+ if (code !== 0)
459
+ return resolve(null);
460
+ if (/gave up:true/.test(out))
461
+ return resolve(null);
462
+ const m = out.match(/text returned:([\s\S]*?)(?:, gave up:[^,]*)?$/);
463
+ resolve(m ? m[1].replace(/\n$/, "") : "");
532
464
  });
533
- conn.on("error", reject);
534
465
  });
535
466
  }
536
-
537
- // packages/cli/src/self.ts
538
- function selfCommand(extraArgs) {
539
- const entry = process.argv[1];
540
- if (entry)
541
- return { cmd: process.execPath, args: [entry, ...extraArgs] };
542
- return { cmd: process.execPath, args: [...extraArgs] };
467
+ async function readPassphrase(promptText) {
468
+ const env = process.env.KEYMAXXER_PASSPHRASE;
469
+ if (env)
470
+ return env;
471
+ if (process.stdin.isTTY)
472
+ return readHiddenLine(promptText);
473
+ const piped = await nextLine();
474
+ if (piped)
475
+ return piped;
476
+ const gui = await promptPassphraseGui(promptText.replace(/:\s*$/, ""));
477
+ if (gui)
478
+ return gui;
479
+ throw new Error("no passphrase provided (no TTY, no piped input, no GUI available).");
543
480
  }
544
481
 
545
482
  // packages/cli/src/client.ts
546
483
  var HEX64 = /^[0-9a-fA-F]{64}$/;
547
- function unwrap(res) {
548
- if (!res.ok)
549
- throw new Error(res.error);
550
- return res.result;
551
- }
552
-
553
- class DaemonClient {
554
- call(req) {
555
- return sendRequest(socketPath(), req).then(unwrap);
556
- }
557
- list() {
558
- return this.call({ op: "list" });
559
- }
560
- run(req) {
561
- return this.call({ op: "run", req });
562
- }
563
- async set(name, value, fields) {
564
- await this.call({ op: "set", name, value, fields });
565
- }
566
- remove(name) {
567
- return this.call({ op: "remove", name });
568
- }
569
- audit(limit) {
570
- return this.call({ op: "audit", limit });
571
- }
572
- async close() {}
573
- }
574
-
575
- class DirectStore {
576
- store;
577
- constructor(store) {
578
- this.store = store;
579
- }
580
- static async open(hexkey) {
581
- return new DirectStore(await SecretStore.open({ path: vaultPath(), hexkey }));
582
- }
583
- list() {
584
- return this.store.list();
585
- }
586
- run(req) {
587
- return this.store.run(req);
588
- }
589
- set(name, value, fields) {
590
- return this.store.set(name, value, fields);
591
- }
592
- remove(name) {
593
- return this.store.remove(name);
594
- }
595
- audit(limit) {
596
- return this.store.recentAudit(limit);
597
- }
598
- close() {
599
- return this.store.close();
600
- }
601
- }
602
- async function isAgentAlive() {
603
- if (!existsSync3(socketPath()))
604
- return false;
605
- try {
606
- const res = await sendRequest(socketPath(), { op: "status" }, 2000);
607
- return res.ok;
608
- } catch {
609
- for (const p of [socketPath(), pidPath()])
610
- if (existsSync3(p))
611
- unlinkSync2(p);
612
- return false;
613
- }
614
- }
615
- async function agentStatus() {
616
- if (!await isAgentAlive())
617
- return null;
618
- const res = await sendRequest(socketPath(), { op: "status" });
619
- return res.ok ? res.result : null;
620
- }
621
- async function getClient() {
484
+ async function openVault(getPassphrase) {
622
485
  const envKey = process.env.KEYMAXXER_MASTER_KEY;
623
486
  if (envKey) {
624
487
  if (!HEX64.test(envKey))
625
488
  throw new Error("KEYMAXXER_MASTER_KEY must be 64 hex characters.");
626
- return DirectStore.open(envKey.toLowerCase());
489
+ return SecretStore.open({ path: vaultPath(), hexkey: envKey.toLowerCase() });
627
490
  }
628
- if (await isAgentAlive())
629
- return new DaemonClient;
630
- throw new Error("vault is locked. Run `keymaxxer unlock` first (or set KEYMAXXER_MASTER_KEY).");
631
- }
632
- async function lockAgent() {
633
- if (!await isAgentAlive())
634
- return false;
491
+ const meta = loadMeta(vaultPath());
492
+ if (!meta || !existsSync2(vaultPath()))
493
+ throw new Error("no vault found. Run `keymaxxer init` first.");
494
+ if (meta.kdf !== "scrypt" || !meta.salt) {
495
+ throw new Error("this vault uses an external key — set KEYMAXXER_MASTER_KEY.");
496
+ }
497
+ const passphrase = await getPassphrase();
498
+ if (!passphrase)
499
+ throw new Error("no passphrase provided — vault stays locked.");
500
+ const hexkey = deriveKey(passphrase, meta.salt, meta.scrypt);
635
501
  try {
636
- await sendRequest(socketPath(), { op: "lock" });
637
- } catch {}
638
- return true;
502
+ return await SecretStore.open({ path: vaultPath(), hexkey });
503
+ } catch (err) {
504
+ if (err instanceof WrongKeyError)
505
+ throw new Error("wrong passphrase.");
506
+ throw err;
507
+ }
639
508
  }
640
- async function spawnAgent(hexkey, idleMinutes) {
641
- if (await isAgentAlive())
642
- throw new Error("vault is already unlocked.");
643
- const { cmd, args } = selfCommand(["__agent"]);
644
- const env = { ...process.env };
645
- delete env.KEYMAXXER_MASTER_KEY;
646
- if (idleMinutes)
647
- env.KEYMAXXER_IDLE_MINUTES = String(idleMinutes);
648
- const child = spawn3(cmd, args, { detached: true, stdio: ["pipe", "ignore", "ignore"], env });
649
- child.stdin.write(hexkey + `
650
- `);
651
- child.stdin.end();
652
- child.unref();
653
- const deadline = Date.now() + 8000;
654
- while (Date.now() < deadline) {
655
- if (await isAgentAlive())
656
- return;
657
- await new Promise((r) => setTimeout(r, 100));
509
+ function openVaultCli() {
510
+ return openVault(() => readPassphrase("Vault passphrase: "));
511
+ }
512
+ async function runGated(store, req, approved) {
513
+ const metas = await store.list();
514
+ const sensitive = req.secrets.map((n) => metas.find((m) => m.name === n)).filter((m) => !!m && isSensitive(m));
515
+ const needPrompt = sensitive.filter((s) => !approved.has(s.name));
516
+ if (needPrompt.length > 0) {
517
+ const names = needPrompt.map((s) => s.name).join(", ");
518
+ const decision = await requestApproval({
519
+ secrets: needPrompt,
520
+ command: req.command,
521
+ cwd: req.cwd ?? process.cwd()
522
+ });
523
+ if (decision === "deny") {
524
+ await store.auditDenied(req.secrets, req.command, req.cwd ?? process.cwd());
525
+ throw new Error(`Denied: use of ${names} was not approved by the user.`);
526
+ }
527
+ if (decision === "session")
528
+ needPrompt.forEach((s) => approved.add(s.name));
658
529
  }
659
- throw new Error("agent did not start in time — check ~/.keymaxxer/agent.log");
530
+ return store.run(req);
660
531
  }
661
532
 
662
533
  // packages/cli/src/init.ts
663
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
534
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
664
535
  import { join as join4 } from "node:path";
665
536
  function keymaxxerServerEntry() {
666
537
  const entry = process.argv[1] ?? "";
@@ -672,7 +543,7 @@ function keymaxxerServerEntry() {
672
543
  function wireMcpJson(dir) {
673
544
  const file = join4(dir, ".mcp.json");
674
545
  let config = {};
675
- if (existsSync4(file)) {
546
+ if (existsSync3(file)) {
676
547
  try {
677
548
  config = JSON.parse(readFileSync2(file, "utf8"));
678
549
  } catch {
@@ -682,7 +553,7 @@ function wireMcpJson(dir) {
682
553
  config.mcpServers ??= {};
683
554
  const existed = "keymaxxer" in config.mcpServers;
684
555
  config.mcpServers.keymaxxer = keymaxxerServerEntry();
685
- writeFileSync3(file, JSON.stringify(config, null, 2) + `
556
+ writeFileSync2(file, JSON.stringify(config, null, 2) + `
686
557
  `);
687
558
  return `${existed ? "Updated" : "Added"} 'keymaxxer' server in ${file}`;
688
559
  }
@@ -695,118 +566,262 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
695
566
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
696
567
  import { z } from "zod";
697
568
 
698
- // packages/cli/src/client.ts
699
- import { existsSync as existsSync5, unlinkSync as unlinkSync3 } from "node:fs";
700
- var HEX642 = /^[0-9a-fA-F]{64}$/;
701
- function unwrap2(res) {
702
- if (!res.ok)
703
- throw new Error(res.error);
704
- return res.result;
569
+ // packages/cli/src/addsecret.ts
570
+ import { spawn as spawn4 } from "node:child_process";
571
+ function asAppleScript2(s) {
572
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ") + '"';
705
573
  }
706
-
707
- class DaemonClient2 {
708
- call(req) {
709
- return sendRequest(socketPath(), req).then(unwrap2);
710
- }
711
- list() {
712
- return this.call({ op: "list" });
713
- }
714
- run(req) {
715
- return this.call({ op: "run", req });
716
- }
717
- async set(name, value, fields) {
718
- await this.call({ op: "set", name, value, fields });
719
- }
720
- remove(name) {
721
- return this.call({ op: "remove", name });
722
- }
723
- audit(limit) {
724
- return this.call({ op: "audit", limit });
725
- }
726
- async close() {}
574
+ function dialog(message, prefill, saveLabel) {
575
+ if (process.platform !== "darwin")
576
+ return Promise.resolve(null);
577
+ return new Promise((resolve) => {
578
+ const script = `display dialog ${asAppleScript2(message)} default answer ${asAppleScript2(prefill)} ` + `with title "keymaxxer — add secret" with icon note ` + `buttons {"Cancel", ${asAppleScript2(saveLabel)}} default button ${asAppleScript2(saveLabel)} ` + `cancel button "Cancel" giving up after 180`;
579
+ const proc = spawn4("osascript", ["-e", script]);
580
+ let out = "";
581
+ proc.stdout.on("data", (c) => out += c.toString());
582
+ proc.on("error", () => resolve(null));
583
+ proc.on("close", (code) => {
584
+ if (code !== 0)
585
+ return resolve(null);
586
+ if (/gave up:true/.test(out))
587
+ return resolve(null);
588
+ const m = out.match(/text returned:([\s\S]*?)(?:, gave up:[^,]*)?$/);
589
+ resolve(m ? m[1].replace(/\n$/, "") : "");
590
+ });
591
+ });
727
592
  }
728
-
729
- class DirectStore2 {
730
- store;
731
- constructor(store) {
732
- this.store = store;
733
- }
734
- static async open(hexkey) {
735
- return new DirectStore2(await SecretStore.open({ path: vaultPath(), hexkey }));
736
- }
737
- list() {
738
- return this.store.list();
739
- }
740
- run(req) {
741
- return this.store.run(req);
742
- }
743
- set(name, value, fields) {
744
- return this.store.set(name, value, fields);
745
- }
746
- remove(name) {
747
- return this.store.remove(name);
748
- }
749
- audit(limit) {
750
- return this.store.recentAudit(limit);
593
+ function confirm(message) {
594
+ if (process.platform !== "darwin")
595
+ return Promise.resolve(null);
596
+ return new Promise((resolve) => {
597
+ const lines = message.split(`
598
+ `);
599
+ const heading = asAppleScript2(lines[0] ?? "");
600
+ const rest = lines.slice(1).map(asAppleScript2).join(" & return & ") || '""';
601
+ const script = `display alert ${heading} message (${rest}) as informational ` + `buttons {"Edit", "Save"} default button "Save" giving up after 180`;
602
+ const proc = spawn4("osascript", ["-e", script]);
603
+ let out = "";
604
+ proc.stdout.on("data", (c) => out += c.toString());
605
+ proc.on("error", () => resolve(null));
606
+ proc.on("close", (code) => {
607
+ if (code !== 0)
608
+ return resolve(null);
609
+ if (/gave up:true/.test(out))
610
+ return resolve(null);
611
+ if (/button returned:Save/.test(out))
612
+ return resolve("save");
613
+ if (/button returned:Edit/.test(out))
614
+ return resolve("edit");
615
+ return resolve(null);
616
+ });
617
+ });
618
+ }
619
+ function tokenize(s) {
620
+ const out = [];
621
+ let cur = "";
622
+ let inQuote = false;
623
+ let started = false;
624
+ for (const ch of s) {
625
+ if (ch === '"') {
626
+ inQuote = !inQuote;
627
+ started = true;
628
+ } else if (ch === " " && !inQuote) {
629
+ if (started || cur)
630
+ out.push(cur);
631
+ cur = "";
632
+ started = false;
633
+ } else {
634
+ cur += ch;
635
+ started = true;
636
+ }
751
637
  }
752
- close() {
753
- return this.store.close();
638
+ if (started || cur)
639
+ out.push(cur);
640
+ return out;
641
+ }
642
+ function tokensToFields(tokens) {
643
+ const flags = {};
644
+ for (let i = 0;i < tokens.length; i++) {
645
+ const t = tokens[i];
646
+ if (!t.startsWith("--"))
647
+ continue;
648
+ const key = t.slice(2);
649
+ const next = tokens[i + 1];
650
+ if (next !== undefined && !next.startsWith("--")) {
651
+ flags[key] = next;
652
+ i++;
653
+ } else {
654
+ flags[key] = "";
655
+ }
754
656
  }
657
+ const fields = {};
658
+ const pick = (...keys) => keys.map((k) => flags[k]).find((v) => v);
659
+ const tag = pick("tag", "tags");
660
+ if (tag)
661
+ fields.tags = tag.split(",");
662
+ const provider = pick("provider");
663
+ if (provider)
664
+ fields.provider = provider;
665
+ const account = pick("account");
666
+ if (account)
667
+ fields.account = account;
668
+ const environment = pick("env", "environment");
669
+ if (environment)
670
+ fields.environment = environment;
671
+ const access = pick("access");
672
+ if (access)
673
+ fields.access = access;
674
+ const description = pick("description", "desc");
675
+ if (description)
676
+ fields.description = description;
677
+ return fields;
755
678
  }
756
- async function isAgentAlive2() {
757
- if (!existsSync5(socketPath()))
758
- return false;
759
- try {
760
- const res = await sendRequest(socketPath(), { op: "status" }, 2000);
761
- return res.ok;
762
- } catch {
763
- for (const p of [socketPath(), pidPath()])
764
- if (existsSync5(p))
765
- unlinkSync3(p);
766
- return false;
679
+ function suggestionLine(s) {
680
+ const q = (v) => `"${v.replace(/"/g, "'")}"`;
681
+ const parts = [s.name];
682
+ if (s.provider)
683
+ parts.push(`--provider ${q(s.provider)}`);
684
+ if (s.account)
685
+ parts.push(`--account ${q(s.account)}`);
686
+ if (s.environment)
687
+ parts.push(`--env ${q(s.environment)}`);
688
+ if (s.access)
689
+ parts.push(`--access ${q(s.access)}`);
690
+ if (s.tags)
691
+ parts.push(`--tag ${q(s.tags)}`);
692
+ if (s.description)
693
+ parts.push(`--description ${q(s.description)}`);
694
+ return parts.join(" ");
695
+ }
696
+ function parseSuggestionLine(line) {
697
+ const tokens = tokenize(line.trim());
698
+ const name = tokens[0];
699
+ if (!name || name.startsWith("--"))
700
+ throw new Error("a secret name is required.");
701
+ return { name, fields: tokensToFields(tokens.slice(1)) };
702
+ }
703
+ async function promptAddSecret(s) {
704
+ const edited = await dialog("Edit the new secret's name and attributes. Available flags: --provider --account --env --access --tag --description", suggestionLine(s), "Next");
705
+ if (edited === null)
706
+ return null;
707
+ const { name, fields } = parseSuggestionLine(edited);
708
+ let value = "";
709
+ for (;; ) {
710
+ const entered = await dialog(`Value for ${name} — saved to the vault, never shared with the agent:`, value, "Review");
711
+ if (entered === null)
712
+ return null;
713
+ if (!entered)
714
+ throw new Error("no value provided.");
715
+ value = entered;
716
+ const choice = await confirm(`Save this value for ${name}?
717
+
718
+ ${value}`);
719
+ if (choice === "save")
720
+ break;
721
+ if (choice === null)
722
+ return null;
767
723
  }
724
+ return { name, value, fields };
768
725
  }
769
- async function getClient2() {
726
+
727
+ // packages/cli/src/client.ts
728
+ import { existsSync as existsSync4 } from "node:fs";
729
+ var HEX642 = /^[0-9a-fA-F]{64}$/;
730
+ async function openVault2(getPassphrase) {
770
731
  const envKey = process.env.KEYMAXXER_MASTER_KEY;
771
732
  if (envKey) {
772
733
  if (!HEX642.test(envKey))
773
734
  throw new Error("KEYMAXXER_MASTER_KEY must be 64 hex characters.");
774
- return DirectStore2.open(envKey.toLowerCase());
735
+ return SecretStore.open({ path: vaultPath(), hexkey: envKey.toLowerCase() });
736
+ }
737
+ const meta = loadMeta(vaultPath());
738
+ if (!meta || !existsSync4(vaultPath()))
739
+ throw new Error("no vault found. Run `keymaxxer init` first.");
740
+ if (meta.kdf !== "scrypt" || !meta.salt) {
741
+ throw new Error("this vault uses an external key — set KEYMAXXER_MASTER_KEY.");
742
+ }
743
+ const passphrase = await getPassphrase();
744
+ if (!passphrase)
745
+ throw new Error("no passphrase provided — vault stays locked.");
746
+ const hexkey = deriveKey(passphrase, meta.salt, meta.scrypt);
747
+ try {
748
+ return await SecretStore.open({ path: vaultPath(), hexkey });
749
+ } catch (err) {
750
+ if (err instanceof WrongKeyError)
751
+ throw new Error("wrong passphrase.");
752
+ throw err;
775
753
  }
776
- if (await isAgentAlive2())
777
- return new DaemonClient2;
778
- throw new Error("vault is locked. Run `keymaxxer unlock` first (or set KEYMAXXER_MASTER_KEY).");
754
+ }
755
+ function openVaultServe(message) {
756
+ return openVault2(() => process.env.KEYMAXXER_PASSPHRASE ? Promise.resolve(process.env.KEYMAXXER_PASSPHRASE) : promptPassphraseGui(message));
757
+ }
758
+ async function runGated2(store, req, approved) {
759
+ const metas = await store.list();
760
+ const sensitive = req.secrets.map((n) => metas.find((m) => m.name === n)).filter((m) => !!m && isSensitive(m));
761
+ const needPrompt = sensitive.filter((s) => !approved.has(s.name));
762
+ if (needPrompt.length > 0) {
763
+ const names = needPrompt.map((s) => s.name).join(", ");
764
+ const decision = await requestApproval({
765
+ secrets: needPrompt,
766
+ command: req.command,
767
+ cwd: req.cwd ?? process.cwd()
768
+ });
769
+ if (decision === "deny") {
770
+ await store.auditDenied(req.secrets, req.command, req.cwd ?? process.cwd());
771
+ throw new Error(`Denied: use of ${names} was not approved by the user.`);
772
+ }
773
+ if (decision === "session")
774
+ needPrompt.forEach((s) => approved.add(s.name));
775
+ }
776
+ return store.run(req);
779
777
  }
780
778
 
781
779
  // packages/cli/src/mcp.ts
782
780
  async function serve() {
783
- const server = new McpServer({ name: "keymaxxer", version: "0.1.0" });
781
+ let store = null;
782
+ const approved = new Set;
783
+ let lastActivity = Date.now();
784
+ const idleMs = Math.max(0, Number(process.env.KEYMAXXER_IDLE_MINUTES) || 0) * 60000;
785
+ if (idleMs > 0) {
786
+ const timer = setInterval(() => {
787
+ if (store && Date.now() - lastActivity > idleMs) {
788
+ store.close().catch(() => {});
789
+ store = null;
790
+ approved.clear();
791
+ }
792
+ }, 5000);
793
+ timer.unref();
794
+ }
795
+ async function vault() {
796
+ lastActivity = Date.now();
797
+ if (!store) {
798
+ store = await openVaultServe("An agent wants to use a secret. Enter your keymaxxer passphrase to unlock the vault:");
799
+ }
800
+ return store;
801
+ }
802
+ const server = new McpServer({ name: "keymaxxer", version: "0.2.0" });
784
803
  server.registerTool("keymaxxer_list", {
785
804
  description: "List the secrets in the vault with their attributes — name, provider (e.g. github/orb/stripe), account, environment (prod/dev/staging), access level (read-only/read-write/admin), tags, and description. Returns NO secret values. Call this first to discover which secrets exist AND to choose the correct one: match the provider/account the task targets, prefer the right environment, and prefer the least-privileged credential that can do the job (e.g. a read-only key when you're only reading).",
786
805
  inputSchema: {}
787
806
  }, async () => {
788
807
  try {
789
- const client = await getClient2();
790
- const metas = await client.list();
791
- await client.close();
808
+ const metas = await (await vault()).list();
792
809
  return { content: [{ type: "text", text: JSON.stringify(metas, null, 2) }] };
793
810
  } catch (err) {
794
811
  return { content: [{ type: "text", text: errText(err) }], isError: true };
795
812
  }
796
813
  });
797
814
  server.registerTool("keymaxxer_run", {
798
- description: 'Run a shell command with secrets injected as environment variables. Reference each secret as $NAME (e.g. "gh api /user" with GITHUB_TOKEN, or curl -H "Authorization: Bearer $TOKEN"). Secret values are injected into the child process only; they are scrubbed from the returned output and never exposed to you. Use keymaxxer_list to find available names. Note: read-write or production secrets are gated — the human is asked to approve the use, so the call may pause briefly and can be denied; if denied, pick a less-privileged secret or ask the user.',
815
+ description: `Run a shell command with secrets injected as environment variables. Reference each secret as $NAME (e.g. "gh api /user" with GITHUB_TOKEN, or curl -H "Authorization: Bearer $TOKEN"). Secret values are injected into the child process only; they are scrubbed from the returned output and never exposed to you. Use keymaxxer_list to find available names. Note: if the vault is locked the human is prompted to unlock it (the call may pause). Read-write or production secrets are gated — the human approves the use and may allow it just once or for the whole session; the call may pause and can be denied. If denied, pick a less-privileged secret or ask the user. Don't tell the user to unlock manually — just make the call and it will prompt them.`,
799
816
  inputSchema: {
800
817
  command: z.string().describe("Shell command to run. Reference secrets as $NAME."),
801
818
  secrets: z.array(z.string()).describe("Names of secrets to inject as environment variables."),
802
- cwd: z.string().optional().describe("Working directory. Defaults to the agent's cwd."),
819
+ cwd: z.string().optional().describe("Working directory. Defaults to the server's cwd."),
803
820
  timeoutMs: z.number().optional().describe("Kill the command after this many milliseconds.")
804
821
  }
805
822
  }, async (args) => {
806
823
  try {
807
- const client = await getClient2();
808
- const res = await client.run(args);
809
- await client.close();
824
+ const res = await runGated2(await vault(), args, approved);
810
825
  const lines = [
811
826
  `exit_code: ${res.exitCode}`,
812
827
  res.redactions > 0 ? `[keymaxxer] redacted ${res.redactions} secret occurrence(s)` : null,
@@ -821,6 +836,37 @@ async function serve() {
821
836
  return { content: [{ type: "text", text: errText(err) }], isError: true };
822
837
  }
823
838
  });
839
+ server.registerTool("keymaxxer_add", {
840
+ description: "Ask the human to add a new secret to the vault. Suggest the name and any attributes you can infer (provider, account, environment, access, description, tags); the human reviews/edits them in a dialog and types the secret VALUE. The value is saved straight to the encrypted vault and is NEVER returned to you. Use this when a task needs a credential that isn't in keymaxxer_list yet — never ask the user to paste a secret into the chat.",
841
+ inputSchema: {
842
+ name: z.string().describe("Suggested secret name, e.g. GITHUB_TOKEN or ORB_PROD_TOKEN."),
843
+ provider: z.string().optional().describe("e.g. github, orb, stripe"),
844
+ account: z.string().optional().describe("which account/org the credential belongs to"),
845
+ environment: z.string().optional().describe("prod / dev / staging / test"),
846
+ access: z.string().optional().describe("read-only / read-write / admin"),
847
+ description: z.string().optional(),
848
+ tags: z.string().optional().describe("comma-separated")
849
+ }
850
+ }, async (args) => {
851
+ try {
852
+ const store2 = await vault();
853
+ const result = await promptAddSecret(args);
854
+ if (!result) {
855
+ return { content: [{ type: "text", text: "The user cancelled — no secret was added." }] };
856
+ }
857
+ await store2.set(result.name, result.value, result.fields);
858
+ return {
859
+ content: [
860
+ {
861
+ type: "text",
862
+ text: `Stored '${result.name}'. The value was entered by the user and is not shown to you — use it with keymaxxer_run as $${result.name}.`
863
+ }
864
+ ]
865
+ };
866
+ } catch (err) {
867
+ return { content: [{ type: "text", text: errText(err) }], isError: true };
868
+ }
869
+ });
824
870
  await server.connect(new StdioServerTransport);
825
871
  }
826
872
  function errText(err) {
@@ -838,37 +884,51 @@ function vaultPath2() {
838
884
  }
839
885
 
840
886
  // packages/cli/src/prompt.ts
841
- var stdinAttached = false;
842
- var buffer = "";
843
- var queued = [];
844
- var waiters = [];
845
- function attachStdinLines() {
846
- if (stdinAttached)
887
+ import { spawn as spawn5 } from "node:child_process";
888
+ var stdinAttached2 = false;
889
+ var buffer2 = "";
890
+ var ended2 = false;
891
+ var queued2 = [];
892
+ var waiters2 = [];
893
+ function attachStdinLines2() {
894
+ if (stdinAttached2)
847
895
  return;
848
- stdinAttached = true;
896
+ stdinAttached2 = true;
849
897
  process.stdin.on("data", (d) => {
850
- buffer += d.toString();
898
+ buffer2 += d.toString();
851
899
  let nl;
852
- while ((nl = buffer.indexOf(`
900
+ while ((nl = buffer2.indexOf(`
853
901
  `)) >= 0) {
854
- const line = buffer.slice(0, nl).replace(/\r$/, "");
855
- buffer = buffer.slice(nl + 1);
856
- const w = waiters.shift();
902
+ const line = buffer2.slice(0, nl).replace(/\r$/, "");
903
+ buffer2 = buffer2.slice(nl + 1);
904
+ const w = waiters2.shift();
857
905
  if (w)
858
906
  w(line);
859
907
  else
860
- queued.push(line);
908
+ queued2.push(line);
909
+ }
910
+ });
911
+ process.stdin.on("end", () => {
912
+ ended2 = true;
913
+ if (buffer2.length) {
914
+ queued2.push(buffer2);
915
+ buffer2 = "";
861
916
  }
917
+ while (waiters2.length)
918
+ waiters2.shift()(queued2.shift() ?? null);
862
919
  });
920
+ process.stdin.resume();
863
921
  }
864
- function nextLine() {
865
- attachStdinLines();
866
- const q = queued.shift();
922
+ function nextLine2() {
923
+ attachStdinLines2();
924
+ const q = queued2.shift();
867
925
  if (q !== undefined)
868
926
  return Promise.resolve(q);
869
- return new Promise((res) => waiters.push(res));
927
+ if (ended2)
928
+ return Promise.resolve(null);
929
+ return new Promise((res) => waiters2.push(res));
870
930
  }
871
- function readHiddenLine(promptText) {
931
+ function readHiddenLine2(promptText) {
872
932
  return new Promise((resolve) => {
873
933
  const stdin = process.stdin;
874
934
  process.stderr.write(promptText);
@@ -904,16 +964,47 @@ function readHiddenLine(promptText) {
904
964
  stdin.on("data", onData);
905
965
  });
906
966
  }
907
- function readPassphrase(promptText) {
908
- if (!process.stdin.isTTY)
909
- return nextLine();
910
- return readHiddenLine(promptText);
967
+ function asAppleScript3(s) {
968
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ") + '"';
969
+ }
970
+ function promptPassphraseGui2(message) {
971
+ if (process.platform !== "darwin")
972
+ return Promise.resolve(null);
973
+ return new Promise((resolve) => {
974
+ const script = `display dialog ${asAppleScript3(message)} default answer "" with hidden answer ` + `with icon note with title "keymaxxer — unlock vault" ` + `buttons {"Cancel", "Unlock"} default button "Unlock" cancel button "Cancel" ` + `giving up after 120`;
975
+ const proc = spawn5("osascript", ["-e", script]);
976
+ let out = "";
977
+ proc.stdout.on("data", (c) => out += c.toString());
978
+ proc.on("error", () => resolve(null));
979
+ proc.on("close", (code) => {
980
+ if (code !== 0)
981
+ return resolve(null);
982
+ if (/gave up:true/.test(out))
983
+ return resolve(null);
984
+ const m = out.match(/text returned:([\s\S]*?)(?:, gave up:[^,]*)?$/);
985
+ resolve(m ? m[1].replace(/\n$/, "") : "");
986
+ });
987
+ });
988
+ }
989
+ async function readPassphrase2(promptText) {
990
+ const env = process.env.KEYMAXXER_PASSPHRASE;
991
+ if (env)
992
+ return env;
993
+ if (process.stdin.isTTY)
994
+ return readHiddenLine2(promptText);
995
+ const piped = await nextLine2();
996
+ if (piped)
997
+ return piped;
998
+ const gui = await promptPassphraseGui2(promptText.replace(/:\s*$/, ""));
999
+ if (gui)
1000
+ return gui;
1001
+ throw new Error("no passphrase provided (no TTY, no piped input, no GUI available).");
911
1002
  }
912
1003
  async function readNewPassphrase() {
913
- const a = await readPassphrase("Create a vault passphrase: ");
1004
+ const a = await readPassphrase2("Create a vault passphrase: ");
914
1005
  if (a.length < 8)
915
1006
  throw new Error("passphrase must be at least 8 characters.");
916
- const b = await readPassphrase("Confirm passphrase: ");
1007
+ const b = await readPassphrase2("Confirm passphrase: ");
917
1008
  if (a !== b)
918
1009
  throw new Error("passphrases did not match.");
919
1010
  return a;
@@ -923,12 +1014,9 @@ async function readNewPassphrase() {
923
1014
  var HELP = `keymaxxer \u2014 a secret manager for coding agents
924
1015
 
925
1016
  Setup:
926
- keymaxxer init Create the encrypted vault (prompts for a passphrase)
927
- keymaxxer unlock [--timeout m] Unlock the vault into the background agent (default 15m idle)
928
- keymaxxer lock Lock the vault and stop the agent
929
- keymaxxer status Show whether the vault is unlocked
1017
+ keymaxxer init Create the encrypted vault (prompts for a passphrase)
930
1018
 
931
- Secrets (require an unlocked vault, or KEYMAXXER_MASTER_KEY):
1019
+ Secrets:
932
1020
  keymaxxer set <NAME> [attrs] Store a secret; prompts to paste the value (hidden),
933
1021
  or pipe it in. attrs: --provider --account --env --access --tag --description
934
1022
  keymaxxer import <file> Import KEY=VALUE lines from a .env-style file
@@ -938,10 +1026,10 @@ Secrets (require an unlocked vault, or KEYMAXXER_MASTER_KEY):
938
1026
  keymaxxer audit [--limit N] Show recent secret-access log
939
1027
 
940
1028
  Agent integration:
941
- keymaxxer serve Start the MCP server on stdio (proxies to the agent)
1029
+ keymaxxer serve Start the MCP server on stdio (holds the key for the session)
942
1030
 
943
- The encryption key is derived from your passphrase and never stored at rest.
944
- For CI, set KEYMAXXER_MASTER_KEY (64-hex) and skip unlock entirely.`;
1031
+ The key is derived from your passphrase and never stored. Each command unlocks on
1032
+ demand; set KEYMAXXER_PASSPHRASE (or KEYMAXXER_MASTER_KEY, 64-hex) for non-interactive use.`;
945
1033
  async function readStdin() {
946
1034
  const chunks = [];
947
1035
  for await (const chunk of process.stdin)
@@ -1014,28 +1102,23 @@ async function main() {
1014
1102
  case "--help":
1015
1103
  console.log(HELP);
1016
1104
  return;
1017
- case "__agent":
1018
- await runAgent();
1019
- return;
1020
1105
  case "init": {
1021
- if (loadMeta(vaultPath2()) && existsSync6(vaultPath2())) {
1106
+ if (loadMeta(vaultPath2()) && existsSync5(vaultPath2())) {
1022
1107
  die("a vault already exists at " + vaultPath2());
1023
1108
  }
1024
1109
  const envKey = process.env.KEYMAXXER_MASTER_KEY;
1025
1110
  if (envKey) {
1026
- await SecretStore.open({ path: vaultPath2(), hexkey: envKey.toLowerCase() });
1111
+ const store = await SecretStore.open({ path: vaultPath2(), hexkey: envKey.toLowerCase() });
1112
+ await store.close();
1027
1113
  saveMeta(vaultPath2(), newExternalKeyMeta(DEFAULT_CIPHER));
1028
1114
  console.log(`\u2713 Vault created at ${vaultPath2()} using KEYMAXXER_MASTER_KEY (AES-256-GCM).`);
1029
1115
  } else {
1030
1116
  const passphrase = await readNewPassphrase();
1031
1117
  const salt = generateSalt();
1032
- const hexkey = deriveKey(passphrase, salt);
1033
- const store = await SecretStore.open({ path: vaultPath2(), hexkey });
1118
+ const store = await SecretStore.open({ path: vaultPath2(), hexkey: deriveKey(passphrase, salt) });
1034
1119
  await store.close();
1035
1120
  saveMeta(vaultPath2(), newPassphraseMeta(DEFAULT_CIPHER, salt));
1036
1121
  console.log(`\u2713 Vault created at ${vaultPath2()} (key derived from your passphrase, AES-256-GCM).`);
1037
- await spawnAgent(hexkey);
1038
- console.log("\u2713 Vault unlocked into the background agent.");
1039
1122
  }
1040
1123
  console.log(wireMcpJson(process.cwd()));
1041
1124
  console.log(`
@@ -1043,64 +1126,23 @@ For editors not auto-configured, add this MCP server:`);
1043
1126
  console.log(manualSnippet());
1044
1127
  return;
1045
1128
  }
1046
- case "unlock": {
1047
- const meta = loadMeta(vaultPath2());
1048
- if (!meta || !existsSync6(vaultPath2()))
1049
- die("no vault found. Run `keymaxxer init` first.");
1050
- if (meta.kdf === "none") {
1051
- die("this vault uses an external key \u2014 set KEYMAXXER_MASTER_KEY; no unlock needed.");
1052
- }
1053
- if (await isAgentAlive()) {
1054
- console.log("Vault is already unlocked.");
1055
- return;
1056
- }
1057
- const passphrase = await readPassphrase("Vault passphrase: ");
1058
- const hexkey = deriveKey(passphrase, meta.salt, meta.scrypt);
1059
- try {
1060
- const probe = await SecretStore.open({ path: vaultPath2(), hexkey });
1061
- await probe.close();
1062
- } catch (err) {
1063
- if (err instanceof WrongKeyError)
1064
- die("wrong passphrase.");
1065
- throw err;
1066
- }
1067
- const timeout = typeof flags.timeout === "string" ? Number(flags.timeout) : undefined;
1068
- await spawnAgent(hexkey, timeout);
1069
- const st = await agentStatus();
1070
- console.log(`\u2713 Vault unlocked (auto-locks after ${st ? st.idleTimeoutSeconds / 60 : 15}m idle).`);
1071
- return;
1072
- }
1073
- case "lock": {
1074
- const stopped = await lockAgent();
1075
- console.log(stopped ? "\u2713 Vault locked." : "Vault was not unlocked.");
1076
- return;
1077
- }
1078
- case "status": {
1079
- const st = await agentStatus();
1080
- if (!st) {
1081
- console.log(process.env.KEYMAXXER_MASTER_KEY ? "Unlocked via KEYMAXXER_MASTER_KEY." : "Locked.");
1082
- return;
1083
- }
1084
- console.log(`Unlocked \u2014 vault ${st.vault}, idle ${st.idleSeconds}s / ${st.idleTimeoutSeconds}s timeout.`);
1085
- return;
1086
- }
1087
1129
  case "set": {
1088
1130
  const name = positionals[0] ?? die("usage: keymaxxer set <NAME> [--provider p] [--account a] [--env e] [--access a] [--tag t] [--description d]");
1089
- const value = process.stdin.isTTY ? await readHiddenLine(`Value for ${name} (paste \u2014 input is hidden): `) : await readStdin();
1131
+ const value = process.stdin.isTTY ? await readHiddenLine2(`Value for ${name} (paste \u2014 input is hidden): `) : await readStdin();
1090
1132
  if (!value)
1091
1133
  die("no value provided.");
1092
- const client = await getClient();
1093
- await client.set(name, value, fieldsFromFlags(flags));
1094
- await client.close();
1134
+ const store = await openVaultCli();
1135
+ await store.set(name, value, fieldsFromFlags(flags));
1136
+ await store.close();
1095
1137
  console.log(`\u2713 Stored '${name}'.`);
1096
1138
  return;
1097
1139
  }
1098
1140
  case "import": {
1099
1141
  const file = positionals[0] ?? die("usage: keymaxxer import <file>");
1100
- if (!existsSync6(file))
1142
+ if (!existsSync5(file))
1101
1143
  die(`file not found: ${file}`);
1102
1144
  const text = readFileSync3(file, "utf8");
1103
- const client = await getClient();
1145
+ const store = await openVaultCli();
1104
1146
  let count = 0;
1105
1147
  for (const raw of text.split(`
1106
1148
  `)) {
@@ -1115,19 +1157,19 @@ For editors not auto-configured, add this MCP server:`);
1115
1157
  if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
1116
1158
  val = val.slice(1, -1);
1117
1159
  }
1118
- await client.set(key, val, {});
1160
+ await store.set(key, val, {});
1119
1161
  count++;
1120
1162
  }
1121
- await client.close();
1163
+ await store.close();
1122
1164
  console.log(`\u2713 Imported ${count} secret(s) from ${file}.`);
1123
1165
  return;
1124
1166
  }
1125
1167
  case "list": {
1126
- const client = await getClient();
1127
- const metas = await client.list();
1128
- await client.close();
1168
+ const store = await openVaultCli();
1169
+ const metas = await store.list();
1170
+ await store.close();
1129
1171
  if (metas.length === 0) {
1130
- console.log("No secrets yet. Add one: printf %s 'value' | keymaxxer set NAME");
1172
+ console.log("No secrets yet. Add one: keymaxxer set NAME");
1131
1173
  return;
1132
1174
  }
1133
1175
  for (const m of metas) {
@@ -1143,9 +1185,9 @@ For editors not auto-configured, add this MCP server:`);
1143
1185
  }
1144
1186
  case "rm": {
1145
1187
  const name = positionals[0] ?? die("usage: keymaxxer rm <NAME>");
1146
- const client = await getClient();
1147
- const removed = await client.remove(name);
1148
- await client.close();
1188
+ const store = await openVaultCli();
1189
+ const removed = await store.remove(name);
1190
+ await store.close();
1149
1191
  console.log(removed ? `\u2713 Removed '${name}'.` : `'${name}' not found.`);
1150
1192
  return;
1151
1193
  }
@@ -1155,9 +1197,13 @@ For editors not auto-configured, add this MCP server:`);
1155
1197
  die("usage: keymaxxer run --secrets a,b -- <command...>");
1156
1198
  const secrets = typeof flags.secrets === "string" && flags.secrets.length ? flags.secrets.split(",") : [];
1157
1199
  const timeoutMs = typeof flags.timeout === "string" ? Number(flags.timeout) : undefined;
1158
- const client = await getClient();
1159
- const res = await client.run({ command, secrets, timeoutMs });
1160
- await client.close();
1200
+ const store = await openVaultCli();
1201
+ let res;
1202
+ try {
1203
+ res = await runGated(store, { command, secrets, timeoutMs }, new Set);
1204
+ } finally {
1205
+ await store.close();
1206
+ }
1161
1207
  if (res.stdout)
1162
1208
  process.stdout.write(res.stdout.endsWith(`
1163
1209
  `) ? res.stdout : res.stdout + `
@@ -1172,9 +1218,9 @@ For editors not auto-configured, add this MCP server:`);
1172
1218
  }
1173
1219
  case "audit": {
1174
1220
  const limit = typeof flags.limit === "string" ? Number(flags.limit) : 20;
1175
- const client = await getClient();
1176
- const entries = await client.audit(limit);
1177
- await client.close();
1221
+ const store = await openVaultCli();
1222
+ const entries = await store.recentAudit(limit);
1223
+ await store.close();
1178
1224
  if (entries.length === 0) {
1179
1225
  console.log("No audit entries yet.");
1180
1226
  return;