@bivy/bivy 0.21.2-staging.10 → 0.21.2-staging.11

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 (109) hide show
  1. package/bin/bivy.mjs +15 -4
  2. package/bin/cli-commands.mjs +4 -3
  3. package/bin/guides/talk-to-the-user.md +13 -0
  4. package/dist/apps/styles.css +5 -0
  5. package/dist/redact.js +23 -1
  6. package/dist/secrets-cli.js +24 -1
  7. package/dist/secrets.js +38 -0
  8. package/dist/server.js +13 -2
  9. package/dist/session/agent-questions.js +42 -2
  10. package/package.json +1 -1
  11. package/web/assets/{AutomationsView-DRwMTpsz.js → AutomationsView-DdynRz7Z.js} +1 -1
  12. package/web/assets/{ChangesCard-C5rvJ8Cq.js → ChangesCard-BKMC_psO.js} +1 -1
  13. package/web/assets/{LibraryView-CIJ-Pfv-.js → LibraryView-DP15D9C2.js} +1 -1
  14. package/web/assets/{ProviderConnect-DiFSOT6L.js → ProviderConnect-CTmnEedT.js} +1 -1
  15. package/web/assets/{SessionList-CxRibKT5.js → SessionList-B3ypZDqf.js} +1 -1
  16. package/web/assets/{Settings-BukJBTwA.js → Settings-V4Kt_5_L.js} +3 -3
  17. package/web/assets/{Sheet-CfbcQeWy.js → Sheet-DTaiHzIs.js} +1 -1
  18. package/web/assets/{Terminal-7Q0Os9n9.js → Terminal-BLmaxri5.js} +1 -1
  19. package/web/assets/{VoiceSettings-DwIi92pR.js → VoiceSettings-BHsSLcee.js} +1 -1
  20. package/web/assets/{abnfDiagram-VCTEODGH-Czjfvfve.js → abnfDiagram-VCTEODGH-QbtUqhRb.js} +1 -1
  21. package/web/assets/architecture-7GRP2DOG-DiaeDSoh.js +1 -0
  22. package/web/assets/{architectureDiagram-5GKGNRK7-DYay_IWI.js → architectureDiagram-5GKGNRK7-Cl6WlvIW.js} +1 -1
  23. package/web/assets/{blockDiagram-I7D4REHJ-CPlmINAT.js → blockDiagram-I7D4REHJ-C88c1PEQ.js} +1 -1
  24. package/web/assets/{c4Diagram-7LVT6UL2-DYcLdJNN.js → c4Diagram-7LVT6UL2-zDGGT6qM.js} +1 -1
  25. package/web/assets/channel-BPU-KLWx.js +1 -0
  26. package/web/assets/{chunk-4HAMMTFA-CdyB2GrZ.js → chunk-4HAMMTFA-CQnMoxAB.js} +1 -1
  27. package/web/assets/{chunk-75Z2AOVW-D7h9tbn8.js → chunk-75Z2AOVW-Cs6qWmdz.js} +1 -1
  28. package/web/assets/{chunk-DU6HZSFF-BISdfBlT.js → chunk-DU6HZSFF-DzJRFHiz.js} +1 -1
  29. package/web/assets/{chunk-F27PBJKO-2JkB2zU9.js → chunk-F27PBJKO-OZCska6x.js} +1 -1
  30. package/web/assets/{chunk-GMAD6QVW-BfxS2p5H.js → chunk-GMAD6QVW-B_8pNlzK.js} +1 -1
  31. package/web/assets/{chunk-GVQU2GXP-vBPS78dh.js → chunk-GVQU2GXP-4y2rTA33.js} +1 -1
  32. package/web/assets/{chunk-IMKFNOWR-C1PvbEPt.js → chunk-IMKFNOWR-BKGXOlEy.js} +1 -1
  33. package/web/assets/{chunk-L3NEJ4N5-wug0ZtiI.js → chunk-L3NEJ4N5-Brxn-p9g.js} +1 -1
  34. package/web/assets/{chunk-OSK3NFVY-CSx22WTp.js → chunk-OSK3NFVY-DwpuG7o9.js} +1 -1
  35. package/web/assets/{chunk-P2QGCYS3-BEJVL1Uw.js → chunk-P2QGCYS3-CCpNvmac.js} +1 -1
  36. package/web/assets/{chunk-POPQ4Y6H-DuEsyMhL.js → chunk-POPQ4Y6H-CfTwiW4G.js} +1 -1
  37. package/web/assets/{chunk-PWAF6VOD-DVLOKu0r.js → chunk-PWAF6VOD-91WR941x.js} +1 -1
  38. package/web/assets/{chunk-SHT3W25Y-Opayr0C_.js → chunk-SHT3W25Y-Ddcd2kTs.js} +1 -1
  39. package/web/assets/{chunk-SVP7TREG-DgHJUbyu.js → chunk-SVP7TREG-DibOBVM1.js} +1 -1
  40. package/web/assets/{chunk-TICWLB2K-DOOCLkvY.js → chunk-TICWLB2K-DKZ7iFXW.js} +1 -1
  41. package/web/assets/classDiagram-ZZMXUADV-CCh6R8Ty.js +1 -0
  42. package/web/assets/classDiagram-v2-VYDZK3BY-CCh6R8Ty.js +1 -0
  43. package/web/assets/{cynefin-OW5HDTMX-1MBj3btC.js → cynefin-OW5HDTMX-C3vikVDV.js} +1 -1
  44. package/web/assets/{cynefinDiagram-5FMLGOSQ-Cc6rrG7K.js → cynefinDiagram-5FMLGOSQ-BD720KJ2.js} +1 -1
  45. package/web/assets/{dagre-GXQ25YYZ-DFG3Ndtk.js → dagre-GXQ25YYZ-D_Yh2THu.js} +1 -1
  46. package/web/assets/{diagram-S7CK7UJ4-SiM6_pqS.js → diagram-S7CK7UJ4-DM39wEUv.js} +1 -1
  47. package/web/assets/{diagram-UQ7AKVKN-BUI0Ju7Z.js → diagram-UQ7AKVKN-CUw8UoIl.js} +1 -1
  48. package/web/assets/{diagram-VSXAHHWV-CXMYZkbm.js → diagram-VSXAHHWV-mcdz_Uo6.js} +1 -1
  49. package/web/assets/{diagram-VX7I27RA-CNCbmY76.js → diagram-VX7I27RA-BaNPXU0X.js} +1 -1
  50. package/web/assets/{diagram-Z3DM3KII-Cf3mrdyV.js → diagram-Z3DM3KII-DfkwhJNu.js} +1 -1
  51. package/web/assets/{ebnfDiagram-PWID7BFC-Ban7TN8N.js → ebnfDiagram-PWID7BFC-DVWHyztc.js} +1 -1
  52. package/web/assets/{erDiagram-RLTQ6QDP-yDvXNUaN.js → erDiagram-RLTQ6QDP-HHGMhQu-.js} +1 -1
  53. package/web/assets/eventmodeling-NTZA5JFV-Ds3fojiM.js +1 -0
  54. package/web/assets/flowDiagram-HODETNUW-DU8X798l.js +1 -0
  55. package/web/assets/{ganttDiagram-EL5Y4UJY-CRtk9R_T.js → ganttDiagram-EL5Y4UJY-wpA-VyQv.js} +1 -1
  56. package/web/assets/{gitGraph-4MIJSDKK-DVCE-pa5.js → gitGraph-4MIJSDKK-aKMK4MF-.js} +1 -1
  57. package/web/assets/{gitGraphDiagram-WWUBYQGX-BGNEoPtQ.js → gitGraphDiagram-WWUBYQGX-DTlc_fKF.js} +1 -1
  58. package/web/assets/{index-Bkcd452U.js → index-CUeorpuq.js} +2 -2
  59. package/web/assets/index-PhGoZUBH.css +1 -0
  60. package/web/assets/{info-A6RAGUB7-CmlSnIUY.js → info-A6RAGUB7-Cv7R3QMt.js} +1 -1
  61. package/web/assets/{infoDiagram-27XIBGKW-CV6e39nj.js → infoDiagram-27XIBGKW-DQ8M1hlm.js} +1 -1
  62. package/web/assets/{ishikawaDiagram-5VMMS53U-BZ317iz5.js → ishikawaDiagram-5VMMS53U-CTph8eqY.js} +1 -1
  63. package/web/assets/{journeyDiagram-3NMN7TZE-aRfKE67B.js → journeyDiagram-3NMN7TZE-Drb7u1Nc.js} +1 -1
  64. package/web/assets/{kanban-definition-UXKFOSKX-DRAkPvek.js → kanban-definition-UXKFOSKX-kvg2Aspv.js} +1 -1
  65. package/web/assets/{mermaid-parser.core-BzKTg0uA.js → mermaid-parser.core-CdLewUsw.js} +3 -3
  66. package/web/assets/{mermaid.core-cF9385Jc.js → mermaid.core-CT3K8cBa.js} +4 -4
  67. package/web/assets/{mindmap-definition-YA3MSWOX-CLiDn2-4.js → mindmap-definition-YA3MSWOX-BjmfoV0m.js} +1 -1
  68. package/web/assets/{mount-DdwNnK_L.js → mount-B3Wovsbl.js} +6 -6
  69. package/web/assets/{packet-AYTQ26CC-C_A2hl_f.js → packet-AYTQ26CC-DGvzP9jp.js} +1 -1
  70. package/web/assets/{pegDiagram-XKGWAZYB-DzWlf-iX.js → pegDiagram-XKGWAZYB-BiC2EyGL.js} +1 -1
  71. package/web/assets/{pie-WAS4IAKB-C8_wbVGt.js → pie-WAS4IAKB-q7EN-Jqj.js} +1 -1
  72. package/web/assets/{pieDiagram-E7YTZNPT-CqlAZb6C.js → pieDiagram-E7YTZNPT-czADSo8s.js} +1 -1
  73. package/web/assets/{quadrantDiagram-AXDQQJYC-BmL5gqum.js → quadrantDiagram-AXDQQJYC-3gtQRACG.js} +1 -1
  74. package/web/assets/{radar-RG4KPBEZ-wXteP-ND.js → radar-RG4KPBEZ-DCdM3lNO.js} +1 -1
  75. package/web/assets/{railroad-74A4TZTK-CiAiQ98V.js → railroad-74A4TZTK-CqRLEVSm.js} +1 -1
  76. package/web/assets/railroad-abnf-HS5TGJTU-BUseZVfJ.js +1 -0
  77. package/web/assets/railroad-ebnf-LZEXJU2U-Bvq_27zd.js +1 -0
  78. package/web/assets/railroad-peg-WCYAUIDC-RMMi8Jbw.js +1 -0
  79. package/web/assets/{railroadDiagram-O6MQD6OU-m83Fr2E6.js → railroadDiagram-O6MQD6OU-CLZ30gwQ.js} +1 -1
  80. package/web/assets/{requirementDiagram-BXWQKSXE-DdiUlbNn.js → requirementDiagram-BXWQKSXE-lpSX7i6e.js} +1 -1
  81. package/web/assets/{sankeyDiagram-P5KCCOFB-DgRmQoC3.js → sankeyDiagram-P5KCCOFB-Ce2dosbC.js} +1 -1
  82. package/web/assets/{sequenceDiagram-WJ2MYXX4-CLvzUry_.js → sequenceDiagram-WJ2MYXX4-CuEkX8By.js} +1 -1
  83. package/web/assets/{settingsRoute-CY886TNp.js → settingsRoute-AXRXVGb_.js} +1 -1
  84. package/web/assets/{stateDiagram-D77RDMKH-CXEsod-X.js → stateDiagram-D77RDMKH-DexcaE4u.js} +1 -1
  85. package/web/assets/stateDiagram-v2-MP3YSRHH-C8nB07ri.js +1 -0
  86. package/web/assets/{swimlanes-42K2YHIH-BIXrBhfr.js → swimlanes-42K2YHIH-BCXmgqmT.js} +1 -1
  87. package/web/assets/swimlanesDiagram-VR7AAH4N-B2JbsdGO.js +8 -0
  88. package/web/assets/{timeline-definition-24CTP7MA-CHNGDuRJ.js → timeline-definition-24CTP7MA-_FwHQA_5.js} +1 -1
  89. package/web/assets/{treeView-Q6P3EWNA-B5PrKFUx.js → treeView-Q6P3EWNA-BBOBdfSv.js} +1 -1
  90. package/web/assets/{treemap-WGGIJYW6-CyfiLDp3.js → treemap-WGGIJYW6-CSsG-a9v.js} +1 -1
  91. package/web/assets/{useStore-BirjskHz.js → useStore-BGuS31a3.js} +1 -1
  92. package/web/assets/{vennDiagram-4TSXK5OY-DsQXSlTg.js → vennDiagram-4TSXK5OY-BWHgnsaP.js} +1 -1
  93. package/web/assets/{wardley-WFR3VGLG-BzGhnDYD.js → wardley-WFR3VGLG-YhQuPDiS.js} +1 -1
  94. package/web/assets/{wardleyDiagram-VM6X3IG4-BIxMxWJH.js → wardleyDiagram-VM6X3IG4-8OaNtyFJ.js} +1 -1
  95. package/web/assets/{xychartDiagram-S5SC5T6Z-B6m5ol80.js → xychartDiagram-S5SC5T6Z-D-_svy0O.js} +1 -1
  96. package/web/index.html +2 -2
  97. package/web/sw.js +1 -1
  98. package/web/assets/architecture-7GRP2DOG-DD5qRKOE.js +0 -1
  99. package/web/assets/channel-BUg-Y_4E.js +0 -1
  100. package/web/assets/classDiagram-ZZMXUADV-DPpdYhAI.js +0 -1
  101. package/web/assets/classDiagram-v2-VYDZK3BY-DPpdYhAI.js +0 -1
  102. package/web/assets/eventmodeling-NTZA5JFV-BktxSOvh.js +0 -1
  103. package/web/assets/flowDiagram-HODETNUW-CuT_k3cJ.js +0 -1
  104. package/web/assets/index-CkoStrAe.css +0 -1
  105. package/web/assets/railroad-abnf-HS5TGJTU-7KYSR92O.js +0 -1
  106. package/web/assets/railroad-ebnf-LZEXJU2U-Dt8zrJqM.js +0 -1
  107. package/web/assets/railroad-peg-WCYAUIDC-NioqZSpU.js +0 -1
  108. package/web/assets/stateDiagram-v2-MP3YSRHH-sOazOE8m.js +0 -1
  109. package/web/assets/swimlanesDiagram-VR7AAH4N-CEQROFH_.js +0 -8
package/bin/bivy.mjs CHANGED
@@ -2685,6 +2685,7 @@ finish: Bivy already pushes when a turn ends while the user is away. --json prin
2685
2685
  // Blocks until the user answers (printing the answer), or with --async returns
2686
2686
  // an id to read later with `bivy ask status|wait <id>`.
2687
2687
  const ASK_USAGE = `Usage: bivy ask "<question>" [--option <label>]… [--multi] [--header <label>] [--timeout 10m] [--async] [--json]
2688
+ bivy ask "<question>" --secret <NAME> [--url <https://…>] [--timeout 10m] [--json]
2688
2689
  bivy ask status <id> [--json]
2689
2690
  bivy ask wait <id> [--timeout 10m] [--json]`;
2690
2691
 
@@ -2697,6 +2698,11 @@ Ask the user a question in the chat and wait for the answer. Their devices get a
2697
2698
  several with --multi) or write their own; without options they type an answer.
2698
2699
  --header is the short label on the card.
2699
2700
 
2701
+ --secret NAME asks for an API key, token or password instead: the card takes a
2702
+ hidden value, the node stores it in its vault as NAME, and this prints only
2703
+ secret://NAME, never the value. --url is where the user finds it. Use it with
2704
+ 'bivy secrets exec --env NAME -- <command>'.
2705
+
2700
2706
  Prints the answer and exits 0. Exits 1 if the user dismissed the question and 5
2701
2707
  if --timeout (default 10m, up to 24h) passes first; the card closes then.
2702
2708
  --async returns the question's id at once; 'bivy ask wait <id>' blocks for the
@@ -2706,7 +2712,7 @@ answer later and 'bivy ask status <id>' checks without waiting.
2706
2712
  }
2707
2713
  const json = wantsJson(args);
2708
2714
  const fail = (error) => cliError(error, { json, paint: c.red });
2709
- const parsed = parseSessionArgs(args, ["--session", "--option", "--header", "--timeout"]);
2715
+ const parsed = parseSessionArgs(args, ["--session", "--option", "--header", "--timeout", "--secret", "--url"]);
2710
2716
  if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
2711
2717
  const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
2712
2718
  if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
@@ -2729,6 +2735,7 @@ answer later and 'bivy ask status <id>' checks without waiting.
2729
2735
  };
2730
2736
  const answerText = (result) => Object.values(result.answers ?? {}).join("\n");
2731
2737
  const finish = (result) => {
2738
+ if (result.status === "answered" && !answerText(result)) return fail({ code: "failed", message: "The secret was not stored on this node.", next: "bivy secrets doctor", exit: EXIT.failed });
2732
2739
  const out = { id: result.id, status: result.status, ...(result.status === "answered" ? { answer: answerText(result) } : {}) };
2733
2740
  if (result.status === "answered") { console.log(json ? JSON.stringify(out) : out.answer); return; }
2734
2741
  if (result.status === "pending") { console.log(json ? JSON.stringify(out) : `Still waiting for an answer. Check again with: bivy ask wait ${result.id}`); process.exit(EXIT.timeout); }
@@ -2755,8 +2762,10 @@ answer later and 'bivy ask status <id>' checks without waiting.
2755
2762
  if (!question) return fail({ code: "usage", message: ASK_USAGE, exit: EXIT.usage });
2756
2763
  const options = parsed.values["--option"] ?? [];
2757
2764
  if (options.length === 1) return fail({ code: "usage", message: "Offer at least two --option values, or none for a free-text answer.", exit: EXIT.usage });
2765
+ const secretName = parsed.one("--secret");
2766
+ if (parsed.one("--url") !== undefined && secretName === undefined) return fail({ code: "usage", message: "--url goes with --secret.", exit: EXIT.usage });
2758
2767
  const started = await call("", {
2759
- questions: [{ question, header: parsed.one("--header"), options, multiSelect: parsed.has("--multi") }],
2768
+ questions: [{ question, header: parsed.one("--header"), options, multiSelect: parsed.has("--multi"), ...(secretName !== undefined ? { secret: { name: secretName, url: parsed.one("--url") } } : {}) }],
2760
2769
  timeoutSeconds: timeout,
2761
2770
  });
2762
2771
  if (parsed.has("--async")) {
@@ -6806,8 +6815,10 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
6806
6815
  // secrets-cli.ts only recognizes "--help"/"help" as the FIRST argument, so
6807
6816
  // a subcommand-position --help (e.g. 'bivy secrets list --help') would
6808
6817
  // otherwise be ignored and the live action would run anyway (#113).
6809
- const forwardArgs = (args.includes("-h") || args.includes("--help")) ? ["--help"] : args;
6810
- process.exit(await run(nodeBin, [...nodeScriptArgs(secretsEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
6818
+ // Words after `--` belong to `secrets exec`'s command, not to us.
6819
+ const own = args.includes("--") ? args.slice(0, args.indexOf("--")) : args;
6820
+ const forwardArgs = (own.includes("-h") || own.includes("--help")) ? ["--help"] : args;
6821
+ process.exit(await run(nodeBin, [...nodeScriptArgs(secretsEntry), ...forwardArgs], { cwd: own[0] === "exec" ? process.cwd() : repoRoot, env: process.env }));
6811
6822
  break;
6812
6823
  }
6813
6824
  case "voice": {
@@ -46,8 +46,9 @@ export const COMMANDS = [
46
46
  { name: "attach", group: "session", scope: "session", json: true, usage: 'attach <file> [--caption "…"] [--artifact]', summary: "Show a local file or image to the user in the chat" },
47
47
  { name: "notify", group: "session", scope: "session", json: true, usage: "notify [--urgent]", summary: "Push to the user's phone so they come back (nothing posted in the chat)",
48
48
  tools: [{ name: "notify_user", description: "Push to the user's phone so they come back to this session, e.g. when you are blocked on them while still working. The push names the session only and nothing is posted in the chat, so say what you need in your reply. Not needed when you finish: Bivy already pushes when a turn ends while they are away. Sent only when nobody has the app open, at most one a minute per session.", input: { urgent: { type: "boolean", description: "Push even while the user has the app open." } }, argv: ["notify", { when: "urgent", flag: "--urgent" }] }] },
49
- { name: "ask", group: "session", scope: "session", json: true, usage: 'ask "<question>" [--option A --option B] [--async]', summary: "Ask the user a question and wait for the answer", subcommands: ["status", "wait"],
50
- tools: [{ name: "ask_user", description: "Ask the user a question in the chat and wait for their answer (their phone gets a push). Offer 2-8 options, or none for a free-text answer; they can always write their own. Returns {status, answer}: status is answered, dismissed (decide yourself) or expired.", input: { question: { type: "string", description: "The question.", required: true }, options: { type: "array", items: { type: "string" }, description: "Choices to pick from (2-8)." }, multiple: { type: "boolean", description: "Allow several choices." }, header: { type: "string", description: "A short label for the card." }, timeout_seconds: { type: "number", description: "How long to wait (default 600)." } }, argv: ["ask", { arg: "question" }, { flag: "--option", from: "options" }, { when: "multiple", flag: "--multi" }, { flag: "--header", from: "header" }, { flag: "--timeout", from: "timeout_seconds" }] }] },
49
+ { name: "ask", group: "session", scope: "session", json: true, usage: 'ask "<question>" [--option A --option B] [--secret NAME] [--async]', summary: "Ask the user a question (or for a secret) and wait for the answer", subcommands: ["status", "wait"],
50
+ tools: [{ name: "ask_user", description: "Ask the user a question in the chat and wait for their answer (their phone gets a push). Offer 2-8 options, or none for a free-text answer; they can always write their own. Returns {status, answer}: status is answered, dismissed (decide yourself) or expired.", input: { question: { type: "string", description: "The question.", required: true }, options: { type: "array", items: { type: "string" }, description: "Choices to pick from (2-8)." }, multiple: { type: "boolean", description: "Allow several choices." }, header: { type: "string", description: "A short label for the card." }, timeout_seconds: { type: "number", description: "How long to wait (default 600)." } }, argv: ["ask", { arg: "question" }, { flag: "--option", from: "options" }, { when: "multiple", flag: "--multi" }, { flag: "--header", from: "header" }, { flag: "--timeout", from: "timeout_seconds" }] },
51
+ { name: "request_secret", description: "Ask the user for an API key, token or password. Their card takes a hidden value that is stored in this machine's vault as `name`; you get back only secret://<name>, never the value. Use it with `bivy secrets exec --env <name> -- <command>`. Never ask for a secret with ask_user. Returns {status, answer}.", input: { name: { type: "string", description: "Vault name, env-var style, e.g. STRIPE_API_KEY.", required: true }, question: { type: "string", description: "What to paste and why, e.g. \"Paste your Stripe secret key so I can create the products.\"", required: true }, url: { type: "string", description: "https:// page where the user finds or creates it." }, timeout_seconds: { type: "number", description: "How long to wait (default 600)." } }, argv: ["ask", { arg: "question" }, { flag: "--secret", from: "name" }, { flag: "--url", from: "url" }, { flag: "--timeout", from: "timeout_seconds" }] }] },
51
52
  { name: "title", group: "session", scope: "session", json: true, usage: 'title "<title>"', summary: "Rename this session",
52
53
  tools: [{ name: "set_session_title", description: "Rename this session in the user's session list. Use a short title (under 60 characters) that says what the work is, when the first message made a poor title or the work changed direction.", input: { title: { type: "string", description: "The new title.", required: true } }, argv: ["title", { arg: "title" }] }] },
53
54
  { name: "app", group: "session", scope: "session", json: true, usage: "app <publish|shot|present|share|run|…>", summary: "Live previews: publish a web/terminal/desktop app, screenshot it, present it", subcommands: ["publish", "list", "remove", "shot", "present", "share", "notes", "run", "click", "type", "key", "scroll", "drag", "move", "menu"],
@@ -114,7 +115,7 @@ export const COMMANDS = [
114
115
  { name: "logout", aliases: ["signout"], group: "accounts", scope: "account", usage: "logout", summary: "Sign this machine out" },
115
116
  { name: "provider", aliases: ["model"], group: "accounts", scope: "node", usage: "provider login [provider]", summary: "Sign into a model provider", subcommands: ["login"] },
116
117
  { name: "auth", aliases: ["credentials", "creds"], group: "accounts", scope: "node", usage: "auth <list|add|remove|import|sync|…>", summary: "Model credentials: import local logins, add, sync", subcommands: ["list", "add", "remove", "sync", "preset", "ingest", "config", "import"] },
117
- { name: "secrets", aliases: ["secret"], group: "accounts", scope: "node", usage: "secrets <list|set|ref|delete|doctor|resolve>", summary: "Store tokens in the node vault", subcommands: ["list", "set", "ref", "delete", "doctor", "resolve"] },
118
+ { name: "secrets", aliases: ["secret"], group: "accounts", scope: "node", usage: "secrets <list|set|ref|delete|doctor|resolve|exec>", summary: "Store tokens in the node vault, and run a command with them", subcommands: ["list", "set", "ref", "delete", "doctor", "resolve", "exec"] },
118
119
  { name: "github:connect", aliases: ["connect-repo"], group: "accounts", scope: "node", usage: "github:connect [owner/repo]", summary: "Connect GitHub" },
119
120
  { name: "github:app-create", group: "accounts", scope: "node", usage: "github:app-create [--org <org>]", summary: "One-click: create and connect a GitHub App" },
120
121
  { name: "github:app-connect", group: "accounts", scope: "node", usage: "github:app-connect --app-id <id> --key <path.pem>", summary: "Connect an existing GitHub App" },
@@ -30,6 +30,19 @@ dismissed it (use your judgment), exit 5 that `--timeout` (default 10m) passed.
30
30
  For long waits use `--async`, keep working, and later `bivy ask wait <id>`.
31
31
  Ask only when you can't reasonably decide yourself.
32
32
 
33
+ ## You need a key or password: ask for a secret
34
+
35
+ bivy ask "Paste your Stripe secret key so I can create the products" \
36
+ --secret STRIPE_API_KEY --url https://dashboard.stripe.com/apikeys
37
+ bivy secrets exec --env STRIPE_API_KEY -- stripe products list
38
+
39
+ The card takes a hidden value and stores it in this machine's vault; you get
40
+ back `secret://STRIPE_API_KEY`, never the value. Use it through
41
+ `bivy secrets exec`, which puts it in the command's environment. It stays
42
+ stored for later sessions, so try `bivy secrets exec` first and ask only when
43
+ it says the secret is missing. Never ask for a secret with a plain `bivy ask`
44
+ or in chat, and never print it: the answer would land in the transcript.
45
+
33
46
  ## Name the session: title
34
47
 
35
48
  bivy title "Fix login redirect loop"
@@ -884,6 +884,11 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
884
884
  and anything below 16px makes iOS Safari auto-zoom the viewport on focus. */
885
885
  .question-other-input { box-sizing: border-box; resize: vertical; min-height: 40px; }
886
886
  .question-other-input:disabled { opacity: 0.6; }
887
+ /* A secret request's fine print: where the value goes, and where to find it. */
888
+ .question-note { margin: 0; font-size: 12px; color: var(--muted); }
889
+ .question-note code { font-size: 12px; }
890
+ .question-note a { color: var(--accent); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
891
+ .question-note a:hover { color: var(--accent-hover); }
887
892
 
888
893
  /* ---- GitHub pill + action sheet ---- */
889
894
  .github-pill { flex: none; background: transparent; font: inherit; cursor: pointer; }
package/dist/redact.js CHANGED
@@ -79,11 +79,33 @@ const KNOWN_SECRET_FIELDS = [
79
79
  "privateKeyPem",
80
80
  ];
81
81
  const KNOWN_SECRET_FIELD_VALUE = new RegExp(`("(?:${KNOWN_SECRET_FIELDS.join("|")})"\\s*:\\s*")[^"\\\\]*(?:\\\\.[^"\\\\]*)*(")`, "g");
82
+ // Exact values the node knows are secret: what the user typed into a secret
83
+ // card, and the node vault's local entries. These have no shape to match on (a
84
+ // random API key, a password), so the vault registers them here and every
85
+ // persisted write masks them wherever they appear. Short values are skipped:
86
+ // masking "1234" everywhere would mangle ordinary text.
87
+ const MIN_KNOWN_SECRET = 8;
88
+ const knownSecrets = new Set();
89
+ /** Mask `value` in everything persisted from now on. */
90
+ export function registerSecretValue(value) {
91
+ const trimmed = String(value ?? "").trim();
92
+ if (trimmed.length >= MIN_KNOWN_SECRET)
93
+ knownSecrets.add(trimmed);
94
+ }
82
95
  /** Mask credentials in `text`. Safe on non-string / empty input. */
83
96
  export function redactSecrets(text) {
84
97
  if (!text)
85
98
  return text;
86
- let out = text
99
+ let out = text;
100
+ for (const value of knownSecrets) {
101
+ if (out.includes(value))
102
+ out = out.split(value).join(REDACTED);
103
+ // Persisted text is JSON: a value with quotes, backslashes or newlines appears escaped.
104
+ const escaped = JSON.stringify(value).slice(1, -1);
105
+ if (escaped !== value && out.includes(escaped))
106
+ out = out.split(escaped).join(REDACTED);
107
+ }
108
+ out = out
87
109
  .replace(URL_CREDENTIAL, `$1:${REDACTED}@`)
88
110
  .replace(GH_TOKEN, REDACTED)
89
111
  .replace(GH_FINE_PAT, REDACTED)
@@ -1,10 +1,11 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
+ import { spawn } from "node:child_process";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { stdin as input, stdout as output } from "node:process";
5
6
  import path from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
- import { SecretVault } from "./secrets.js";
8
+ import { SecretVault, secretExecPlan } from "./secrets.js";
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
  const repoRoot = path.resolve(__dirname, "..");
10
11
  const appDir = process.env.BIVY_DATA_DIR ?? path.join(repoRoot, ".bivy");
@@ -59,6 +60,9 @@ Commands:
59
60
  bivy secrets delete <id>
60
61
  bivy secrets doctor
61
62
  bivy secrets resolve <id> Verify a secret resolves without printing it
63
+ bivy secrets exec --env NAME[=ref]… -- <command> [args…]
64
+ Run a command with secrets in its environment
65
+ (NAME alone means secret://NAME), never printing them
62
66
 
63
67
  Common ids:
64
68
  github.repo-token
@@ -67,8 +71,27 @@ Common ids:
67
71
  integration.github
68
72
  `);
69
73
  }
74
+ async function exec(args) {
75
+ const plan = secretExecPlan(args);
76
+ const env = { ...process.env };
77
+ for (const [name, ref] of plan.env) {
78
+ const value = await vault.resolve(ref);
79
+ if (!value)
80
+ throw new Error(`${ref} did not resolve. Ask the user for it with: bivy ask "<what to paste>" --secret ${name}`);
81
+ env[name] = value;
82
+ }
83
+ return new Promise((resolve, reject) => {
84
+ const child = spawn(plan.command[0], plan.command.slice(1), { stdio: "inherit", env });
85
+ child.on("error", reject);
86
+ child.on("exit", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
87
+ });
88
+ }
70
89
  async function main() {
71
90
  const [cmd, id, ...rest] = process.argv.slice(2);
91
+ if (cmd === "exec") {
92
+ process.exitCode = await exec(process.argv.slice(3));
93
+ return;
94
+ }
72
95
  if (!cmd || cmd === "help" || cmd === "--help") {
73
96
  usage();
74
97
  return;
package/dist/secrets.js CHANGED
@@ -125,6 +125,23 @@ export class SecretVault {
125
125
  return this.decryptRecord(record);
126
126
  return undefined;
127
127
  }
128
+ /** Every value stored in this vault itself (not references), for redaction.
129
+ * A record that no longer decrypts is skipped. */
130
+ localValues() {
131
+ this.reload();
132
+ const values = [];
133
+ for (const record of Object.values(this.data.records)) {
134
+ if (record.backend !== "local")
135
+ continue;
136
+ try {
137
+ const value = this.decryptRecord(record);
138
+ if (value)
139
+ values.push(value);
140
+ }
141
+ catch { }
142
+ }
143
+ return values;
144
+ }
128
145
  setReference(id, ref, description) {
129
146
  const normalized = normalizeSecretId(id);
130
147
  const trimmed = String(ref || "").trim();
@@ -291,6 +308,27 @@ function modeIsPrivate(file) {
291
308
  return false;
292
309
  }
293
310
  }
311
+ /** `--env NAME` or `--env NAME=<secret://…|op://…|env://…|cmd://…|id>` pairs before `--`. */
312
+ export function secretExecPlan(args) {
313
+ const split = args.indexOf("--");
314
+ if (split < 0 || split === args.length - 1)
315
+ throw new Error("Usage: bivy secrets exec --env NAME[=ref]… -- <command> [args…]");
316
+ const env = [];
317
+ const own = args.slice(0, split);
318
+ for (let i = 0; i < own.length; i++) {
319
+ const spec = own[i] === "--env" ? own[++i] : own[i].startsWith("--env=") ? own[i].slice("--env=".length) : undefined;
320
+ if (!spec)
321
+ throw new Error(`Unexpected argument ${own[i]}; pass secrets as --env NAME[=ref].`);
322
+ const eq = spec.indexOf("=");
323
+ const name = eq < 0 ? spec : spec.slice(0, eq);
324
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
325
+ throw new Error(`${name || "(empty)"} is not an environment variable name.`);
326
+ env.push([name, eq < 0 ? `secret://${name}` : spec.slice(eq + 1)]);
327
+ }
328
+ if (!env.length)
329
+ throw new Error("Name at least one secret: --env NAME.");
330
+ return { env, command: args.slice(split + 1) };
331
+ }
294
332
  export async function resolveSecret(value, appDir) {
295
333
  return new SecretVault(appDir).resolve(value);
296
334
  }
package/dist/server.js CHANGED
@@ -171,7 +171,7 @@ import { discoverGithubRepos, listGithubUserRepos } from "./github-repos.js";
171
171
  import { loadGitHubAppConfigs, orderAppsForOwner, listGitHubApps, removeGitHubApp, upsertGitHubApp, privateKeyIdFor, } from "./github-apps.js";
172
172
  import { buildAppManifest, convertManifest, renderManifestForm } from "./github-app-manifest.js";
173
173
  import { encryptGithubAppEnvelope, decryptGithubAppEnvelope, readLocalGithubAppVaultKey, writeLocalGithubAppVaultKey, forgetLocalGithubAppVaultKey, mintLocalGithubAppVaultKey, } from "./github-app-vault.js";
174
- import { redactSecrets } from "./redact.js";
174
+ import { redactSecrets, registerSecretValue } from "./redact.js";
175
175
  import { getSttConfig, isSttProvider, removeSttKey, setSttKey, setSttProvider, transcribeAudio, MAX_AUDIO_BYTES, } from "./stt.js";
176
176
  import { synthesizeOpenAiSpeech } from "./tts.js";
177
177
  import { seal, open } from "./e2e.js";
@@ -423,7 +423,18 @@ setSessionTokenSigner(sessionTokens.sign);
423
423
  // clarifying question across sessions; the guardian interceptor below raises
424
424
  // them, and session.question(.resolved) is broadcast from its listeners.
425
425
  const questionManager = new QuestionManager();
426
- const agentQuestions = new AgentQuestions(questionManager);
426
+ // A secret the user types into a `bivy ask --secret` card goes to the node
427
+ // vault; the agent gets its secret:// reference. Vault values are masked in
428
+ // every persisted transcript write (redact.ts), including ones stored before.
429
+ const agentQuestions = new AgentQuestions(questionManager, Date.now, (name, value, description) => {
430
+ new SecretVault(appDir).setLocal(name, value, description);
431
+ registerSecretValue(value);
432
+ });
433
+ try {
434
+ for (const value of new SecretVault(appDir).localValues())
435
+ registerSecretValue(value);
436
+ }
437
+ catch { }
427
438
  // `bivy automation apply` from an agent: an approval request like any other, so
428
439
  // the node's approval mode decides whether the user is asked ("never" applies
429
440
  // at once; autonomous asks, as for deploys). Either way the result lands in the
@@ -4,6 +4,10 @@ import { randomUUID } from "node:crypto";
4
4
  export const MAX_ASK_TIMEOUT_MS = 24 * 60 * 60 * 1000;
5
5
  const KEEP_SETTLED_MS = 24 * 60 * 60 * 1000;
6
6
  const KEEP_MAX = 200;
7
+ /** Secret names are env-var shaped, so a request can never overwrite one of the
8
+ * node's own dotted vault ids (github.repo-token, integration.<id>.api-key, …). */
9
+ const SECRET_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
10
+ export const secretRef = (name) => `secret://${name}`;
7
11
  /** Validates `bivy ask` input: 1–4 questions, each with text, a short header and
8
12
  * none (a free-text answer) or 2–8 options. Returns an error message, or the questions. */
9
13
  export function askQuestionsFrom(value) {
@@ -23,6 +27,19 @@ export function askQuestionsFrom(value) {
23
27
  return "Each option needs a label (up to 200 characters).";
24
28
  if (new Set(labels).size !== labels.length)
25
29
  return "Options must be different.";
30
+ const secret = raw.secret;
31
+ if (secret !== undefined) {
32
+ const name = typeof secret?.name === "string" ? secret.name.trim() : "";
33
+ if (!SECRET_NAME.test(name))
34
+ return "A secret name is a letter, then letters, digits or underscores (up to 64), like STRIPE_API_KEY.";
35
+ if (labels.length)
36
+ return "A secret question takes no options.";
37
+ const url = typeof secret.url === "string" ? secret.url.trim() : "";
38
+ if (url && !/^https:\/\/\S+$/.test(url))
39
+ return "The secret's link must be an https:// URL.";
40
+ out.push({ question, header: typeof raw.header === "string" && raw.header.trim() ? header : "Secret", options: [], secret: { name, ...(url ? { url } : {}) } });
41
+ continue;
42
+ }
26
43
  out.push({ question, header, options: labels.map((label) => ({ label })), ...(raw.multiSelect === true ? { multiSelect: true } : {}) });
27
44
  }
28
45
  if (new Set(out.map((q) => q.question)).size !== out.length)
@@ -32,14 +49,18 @@ export function askQuestionsFrom(value) {
32
49
  export class AgentQuestions {
33
50
  questions;
34
51
  now;
52
+ storeSecret;
35
53
  results = new Map();
36
54
  waiters = new Map();
37
55
  /** How each of our questions settled. QuestionManager reports it to onResolved
38
56
  * listeners synchronously, before the request's promise callbacks run. */
39
57
  settled = new Map();
40
- constructor(questions, now = Date.now) {
58
+ constructor(questions, now = Date.now,
59
+ /** Keeps a secret answer; the agent only ever sees its reference. */
60
+ storeSecret = () => { throw new Error("No secret store on this node."); }) {
41
61
  this.questions = questions;
42
62
  this.now = now;
63
+ this.storeSecret = storeSecret;
43
64
  questions.onResolved((request) => { if (this.results.has(request.id))
44
65
  this.settled.set(request.id, request.status); });
45
66
  }
@@ -54,7 +75,7 @@ export class AgentQuestions {
54
75
  this.settled.delete(id);
55
76
  result.status = answer.behavior === "completed" ? "answered" : status === "expired" ? "expired" : "dismissed";
56
77
  if (answer.behavior === "completed")
57
- result.answers = answer.answers;
78
+ result.answers = this.keepSecrets(items, answer.answers);
58
79
  result.settledAt = this.now();
59
80
  for (const wake of this.waiters.get(id) ?? [])
60
81
  wake();
@@ -62,6 +83,25 @@ export class AgentQuestions {
62
83
  });
63
84
  return this.view(result);
64
85
  }
86
+ /** Moves secret answers into the store, leaving their references in their place.
87
+ * An empty secret, or one the store refuses, reads back as no answer. */
88
+ keepSecrets(items, answers) {
89
+ const out = { ...answers };
90
+ for (const item of items) {
91
+ if (!item.secret)
92
+ continue;
93
+ const value = out[item.question] ?? "";
94
+ out[item.question] = "";
95
+ if (!value.trim())
96
+ continue;
97
+ try {
98
+ this.storeSecret(item.secret.name, value.trim(), item.question);
99
+ out[item.question] = secretRef(item.secret.name);
100
+ }
101
+ catch { /* left empty: the agent sees no answer and can ask again */ }
102
+ }
103
+ return out;
104
+ }
65
105
  get(sessionId, id) {
66
106
  const result = this.results.get(id);
67
107
  return result && result.sessionId === sessionId ? this.view(result) : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.21.2-staging.10",
3
+ "version": "0.21.2-staging.11",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",