@bivy/bivy 0.20.9-staging.12 → 0.20.9-staging.2
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/README.md +5 -6
- package/bin/bivy.mjs +56 -13
- package/bin/cli-commands.mjs +4 -2
- package/bin/guides/talk-to-the-user.md +24 -13
- package/dist/agent-instructions.js +10 -4
- package/dist/agents/pi/integration.js +1 -1
- package/dist/apps/styles.css +47 -14
- package/dist/certification/generated.js +1 -1
- package/dist/ephemeral-exec.js +108 -0
- package/dist/ephemeral-teardown.js +1 -11
- package/dist/exec.js +1 -4
- package/dist/runtime/model-catalog.js +0 -16
- package/dist/server.js +53 -21
- package/dist/session/event-log.js +17 -2
- package/dist/session/inline-image-fetch.js +3 -1
- package/dist/session/session-tokens.js +1 -1
- package/dist/session/suggestions.js +17 -0
- package/package.json +6 -6
- package/web/assets/AutomationsView-DEild20v.js +51 -0
- package/web/assets/{ChangesCard-g8Lhp8-o.js → ChangesCard-DcSRy5g_.js} +1 -1
- package/web/assets/{LibraryView-2DXscx59.js → LibraryView-Cu0tU0gU.js} +1 -1
- package/web/assets/{ProviderConnect-Cl2HBpl_.js → ProviderConnect-cpGwBMrU.js} +1 -1
- package/web/assets/Settings-BoTU83-i.js +10 -0
- package/web/assets/{Sheet-BERX6Dsv.js → Sheet-D-5t3lJ0.js} +1 -1
- package/web/assets/{Terminal-CJcgCagB.js → Terminal-CjMjKB1f.js} +1 -1
- package/web/assets/{VoiceSettings-DkSdCkze.js → VoiceSettings-xv6rjaY2.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-BHD5JTM6.js → abnfDiagram-VCTEODGH-BLT3MLyK.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-s28sMQ0-.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-C_GKL4K7.js → architectureDiagram-5GKGNRK7-BGinLETW.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-CRq2fdX9.js → blockDiagram-I7D4REHJ-BNiY2HLB.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-CvXSE3hG.js → c4Diagram-7LVT6UL2-46aGfRMd.js} +1 -1
- package/web/assets/channel-CPSM7rj7.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-Ds9o7yrU.js → chunk-4HAMMTFA-CRzer-vm.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-BwiC6QFm.js → chunk-75Z2AOVW-MFkgf2Ar.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-236r-frX.js → chunk-DU6HZSFF-Bpze6ID8.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-CguQ8WaZ.js → chunk-F27PBJKO-Dq3OmNXG.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-DmBwXsGy.js → chunk-GMAD6QVW-umDWdG9g.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-DnkJJSYL.js → chunk-GVQU2GXP-DuT1Awt9.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-YG1z0eu5.js → chunk-IMKFNOWR-CpFFccVb.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-D39eIpWM.js → chunk-L3NEJ4N5-0jRWrASa.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-CGrQqCBq.js → chunk-OSK3NFVY-CBErW4-p.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-CnzAU-Mz.js → chunk-P2QGCYS3-BPMii7LM.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CWalH36f.js → chunk-POPQ4Y6H-CHTRMb_C.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BQT5ioTL.js → chunk-PWAF6VOD-BOcNKrmu.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-wfqek-d7.js → chunk-SHT3W25Y-Dvb-oEDN.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-CBp3IjhM.js → chunk-SVP7TREG-B4AQIUTK.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-ags6QsSk.js → chunk-TICWLB2K-CWzLTsHW.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-CZ-So67n.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-CZ-So67n.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-DO3oi7vt.js → cynefin-OW5HDTMX-BPkKT48j.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-CIXw7Cc6.js → cynefinDiagram-5FMLGOSQ-Ck2qLmpR.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-BLXWq9eV.js → dagre-GXQ25YYZ-BPKEs_49.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-bA0zwJY_.js → diagram-S7CK7UJ4-ou-L5HdJ.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-DKogFgPR.js → diagram-UQ7AKVKN-DihVn6fF.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-qkLzNlr2.js → diagram-VSXAHHWV-BkLtxbP7.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-_osxbKbn.js → diagram-VX7I27RA-BZ-Z-CwP.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-CQljxyfX.js → diagram-Z3DM3KII-BQN-bGqD.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-C8Y5lz7G.js → ebnfDiagram-PWID7BFC-aKK1a_it.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-DjUCgPos.js → erDiagram-RLTQ6QDP-BGsXtJl-.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-Dd2mk6iz.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-D7f1jYIL.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-CMCiD5IY.js → ganttDiagram-EL5Y4UJY-lrYuyNAe.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-BRcacSQ-.js → gitGraph-4MIJSDKK-raj_IxZ8.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-0js7hKj5.js → gitGraphDiagram-WWUBYQGX-DzqRGXuk.js} +1 -1
- package/web/assets/{index-BdEB2YRe.js → index-B6GPr2Pt.js} +2 -2
- package/web/assets/index-D76QD5dl.css +1 -0
- package/web/assets/{info-A6RAGUB7-CsrRX5La.js → info-A6RAGUB7-DJVi9SAN.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-DDP3W2Tm.js → infoDiagram-27XIBGKW-B2HrGywP.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-BMu0M7NV.js → ishikawaDiagram-5VMMS53U-CczIkvWw.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-BPr-VSMD.js → journeyDiagram-3NMN7TZE-D-h-kwep.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-BM-VHNhy.js → kanban-definition-UXKFOSKX-GClrFWfH.js} +1 -1
- package/web/assets/{line-Bd9NaoV1.js → line-pweWV0oc.js} +1 -1
- package/web/assets/{mermaid-parser.core-hutbDo6D.js → mermaid-parser.core-C1vHZCtC.js} +3 -3
- package/web/assets/{mermaid.core-gqG1ONr1.js → mermaid.core-Df8DJ7AE.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-DxVb-AvM.js → mindmap-definition-YA3MSWOX-BpqA307n.js} +1 -1
- package/web/assets/mount-ISRAcaLu.js +33 -0
- package/web/assets/{packet-AYTQ26CC-dFLVGDcI.js → packet-AYTQ26CC-afE6T0Hc.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-Bazytp0F.js → pegDiagram-XKGWAZYB-AAEw7Khy.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-CTkqaK_f.js → pie-WAS4IAKB-vtwSUVVK.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-CDvZ4aIB.js → pieDiagram-E7YTZNPT-CDg0SkhS.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-DTSeVxbf.js → quadrantDiagram-AXDQQJYC-_o0rOR26.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-CKfc3MS_.js → radar-RG4KPBEZ-_Tf7tpz8.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-Cxcv3-64.js → railroad-74A4TZTK-DM_a0HtL.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-BzFZIgti.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-BkKCtTE4.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-CuXWl9dG.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-DevTuLL1.js → railroadDiagram-O6MQD6OU-BpQZeQ-f.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-BYjGKExS.js → requirementDiagram-BXWQKSXE-DC4US4K_.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-CfVa0PNX.js → sankeyDiagram-P5KCCOFB-BUJ9KIqV.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-Dl5bPCHn.js → sequenceDiagram-WJ2MYXX4-By7D5XFY.js} +1 -1
- package/web/assets/settingsRoute-DSj-6Pdo.js +1 -0
- package/web/assets/{stateDiagram-D77RDMKH-BtU_gQEb.js → stateDiagram-D77RDMKH-D0BA3EeM.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-2_bDqGCB.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-DIe9GxhW.js → swimlanes-42K2YHIH-BNUI3ghm.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-SmXx3nAj.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-B9g7nrL1.js → timeline-definition-24CTP7MA-DVzeoz4F.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-DdT2XETS.js → treeView-Q6P3EWNA-CnvdZw-U.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-h4WmocS0.js → treemap-WGGIJYW6-Ax_RXzPk.js} +1 -1
- package/web/assets/useStore-BznE40dl.js +29 -0
- package/web/assets/{vennDiagram-4TSXK5OY-gk44eWcf.js → vennDiagram-4TSXK5OY-CNauH8S4.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-uJbKN4qW.js → wardley-WFR3VGLG-q8WnmSMu.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-Diy79JCP.js → wardleyDiagram-VM6X3IG4-DKOY4P11.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-CbeKz8Bk.js → xychartDiagram-S5SC5T6Z-m8KaPfml.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/web/assets/AutomationsView-BWARomwT.js +0 -51
- package/web/assets/Settings-DUh6WdcC.js +0 -10
- package/web/assets/architecture-7GRP2DOG-BK3P2Dfz.js +0 -1
- package/web/assets/channel-Bsrodtv-.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BBOjgswF.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BBOjgswF.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-CKIRww6K.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-C9kJw-vn.js +0 -1
- package/web/assets/index-DZb12uRj.css +0 -1
- package/web/assets/mount-DShkRPG0.js +0 -31
- package/web/assets/railroad-abnf-HS5TGJTU-CDmyp3sp.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-CbiTHSV5.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-CVsfoDy0.js +0 -1
- package/web/assets/settingsRoute-d6enyGGs.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-CGVAIFE8.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-CY0bvLUP.js +0 -8
- package/web/assets/useStore-DzDtTF5N.js +0 -22
package/README.md
CHANGED
|
@@ -285,9 +285,10 @@ Every session gets the `bivy` CLI, so any agent with a shell can talk back
|
|
|
285
285
|
through the app, not only agents with their own built-in tools:
|
|
286
286
|
|
|
287
287
|
```bash
|
|
288
|
-
bivy notify
|
|
288
|
+
bivy notify "Tests are green, PR is up" # chat card + phone push when you're away
|
|
289
289
|
bivy ask "Ship to staging?" --option Yes --option No # waits for your answer
|
|
290
290
|
bivy attach report.png --caption "Before/after" # show a file in the chat
|
|
291
|
+
bivy suggest "Add a dark theme to settings" # a next task you start in one tap
|
|
291
292
|
bivy context --json # session, workspace, machine, apps
|
|
292
293
|
```
|
|
293
294
|
|
|
@@ -526,8 +527,6 @@ use, modify, and self-host it under that license. If users interact with your
|
|
|
526
527
|
modified version over a network, section 13 requires you to offer its
|
|
527
528
|
corresponding source. See [LICENSE](LICENSE).
|
|
528
529
|
|
|
529
|
-
Bivy Cloud is the hosted operation of that stack
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
[Contributor License Agreement](CLA.md). The Bivy name and logo are covered by
|
|
533
|
-
the [trademark policy](TRADEMARKS.md).
|
|
530
|
+
Bivy Cloud is the hosted operation of that stack plus billing and plans, in a
|
|
531
|
+
separate private repository. Contributions use the
|
|
532
|
+
[DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.
|
package/bin/bivy.mjs
CHANGED
|
@@ -2595,6 +2595,38 @@ async function cmdContext(args = []) {
|
|
|
2595
2595
|
console.log(`\nEverything else: ${c.cyan("bivy help")} (or ${c.cyan("bivy help --json")}).`);
|
|
2596
2596
|
}
|
|
2597
2597
|
|
|
2598
|
+
// `bivy suggest "<task>" [--title "…"] [--run here|subagents|new] [--session <id>]`
|
|
2599
|
+
// — propose a task the user can start in one tap, in this session or beside it. For an agent
|
|
2600
|
+
// offering next steps: write the task as a complete instruction.
|
|
2601
|
+
async function cmdSuggest(args = []) {
|
|
2602
|
+
const usage = 'Usage: bivy suggest "<task>" [--title "short label"] [--run here|subagents|new] [--session <id>] [--json]';
|
|
2603
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2604
|
+
console.log(`${usage}\n\nPost a task the user can start in one tap: in this session, through your sub-agents, or in a parallel session that works in its own copy of the project. Write it as a complete instruction, with paths relative to the project root.\n\n--run says where you recommend running it, which becomes the card's main button: here (builds on this conversation), subagents (independent tasks you can split across your own sub-agents; only if you have them), or new (bigger independent work the user will want to follow in its own session). Without it, one card recommends here and several recommend new.\n\n--json prints {"ok","id"}.`);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
const json = wantsJson(args);
|
|
2608
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2609
|
+
const flagsWithValue = new Set(["--session", "--title", "--run"]);
|
|
2610
|
+
const flag = (name) => {
|
|
2611
|
+
const i = args.indexOf(name);
|
|
2612
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
2613
|
+
};
|
|
2614
|
+
const text = args.filter((a, i) => !a.startsWith("-") && !(i > 0 && flagsWithValue.has(args[i - 1]))).join(" ").trim();
|
|
2615
|
+
const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
|
|
2616
|
+
if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2617
|
+
const run = flag("--run");
|
|
2618
|
+
if (run !== undefined && !["here", "subagents", "new"].includes(run)) return fail({ code: "usage", message: `--run is here, subagents or new (got "${run}").`, exit: EXIT.usage });
|
|
2619
|
+
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 });
|
|
2620
|
+
const config = loadConfig();
|
|
2621
|
+
if (!(await ensureNodeRunning(config))) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable });
|
|
2622
|
+
const res = await sessionPost(config, sessionId, "suggest", { text, title: flag("--title"), run }).catch((error) => error);
|
|
2623
|
+
if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
|
|
2624
|
+
const body = await res.json().catch(() => ({}));
|
|
2625
|
+
if (!res.ok) return fail(sessionHttpError("Suggest", res.status, body));
|
|
2626
|
+
if (json) { console.log(JSON.stringify(body)); return; }
|
|
2627
|
+
console.log(c.green("Suggested in the chat. The user can start it in one tap."));
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2598
2630
|
// `bivy title "<title>" [--session <id>]` — rename the session the agent runs in.
|
|
2599
2631
|
async function cmdTitle(args = []) {
|
|
2600
2632
|
const usage = 'Usage: bivy title "<title>" [--session <id>] [--json]';
|
|
@@ -2642,37 +2674,45 @@ function durationSeconds(text) {
|
|
|
2642
2674
|
return m ? Number(m[1]) * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1) : NaN;
|
|
2643
2675
|
}
|
|
2644
2676
|
|
|
2645
|
-
// `bivy notify [--urgent]` —
|
|
2646
|
-
// (never
|
|
2677
|
+
// `bivy notify "<message>" [--urgent]` — reach the user: a card in the chat,
|
|
2678
|
+
// and a push naming the session (never the text) when nobody has the app open.
|
|
2647
2679
|
async function cmdNotify(args = []) {
|
|
2648
|
-
const usage =
|
|
2680
|
+
const usage = 'Usage: bivy notify "<message>" [--urgent] [--session <id>] [--json]';
|
|
2649
2681
|
if (args.includes("-h") || args.includes("--help")) {
|
|
2650
2682
|
console.log(`${usage}
|
|
2651
2683
|
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
{"ok","push":"sent"|"user_watching"|"rate_limited"
|
|
2684
|
+
Send the user a message: it appears as a card in the chat, and their devices get
|
|
2685
|
+
a push notification (naming this session, not the text) when nobody has the app
|
|
2686
|
+
open. --urgent pushes even while they do. At most one push a minute per session;
|
|
2687
|
+
later messages still reach the chat. Use it when you finish long work, get
|
|
2688
|
+
blocked, or need the user to look at something. --json prints
|
|
2689
|
+
{"ok","id","push":"sent"|"user_watching"|"rate_limited","userWatching"}.`);
|
|
2658
2690
|
return;
|
|
2659
2691
|
}
|
|
2660
2692
|
const json = wantsJson(args);
|
|
2661
2693
|
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2662
2694
|
const parsed = parseSessionArgs(args, ["--session"]);
|
|
2663
2695
|
if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
|
|
2696
|
+
const text = parsed.positional.join(" ").trim();
|
|
2697
|
+
if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2664
2698
|
const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
|
|
2665
2699
|
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 });
|
|
2666
2700
|
const config = loadConfig();
|
|
2667
2701
|
if (!(await ensureNodeRunning(config))) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable });
|
|
2668
|
-
const res = await sessionPost(config, sessionId, "notify", { urgent: parsed.has("--urgent") }).catch((error) => error);
|
|
2702
|
+
const res = await sessionPost(config, sessionId, "notify", { text, urgent: parsed.has("--urgent") }).catch((error) => error);
|
|
2669
2703
|
if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
|
|
2670
2704
|
const body = await res.json().catch(() => ({}));
|
|
2671
2705
|
if (!res.ok) return fail(sessionHttpError("Notify", res.status, body));
|
|
2672
2706
|
if (json) { console.log(JSON.stringify(body)); return; }
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2707
|
+
const noPush = { user_watching: "the user has the app open; add --urgent to push anyway", rate_limited: "this session pushed less than a minute ago", unavailable: "this machine isn't signed in to a Bivy account" }[body.push];
|
|
2708
|
+
// A terminal run (`bivy run`) has no chat to hold a card: the push is the notice.
|
|
2709
|
+
if (body.posted === false) {
|
|
2710
|
+
if (body.push === "sent") console.log(c.green("Pushed to the user's devices."));
|
|
2711
|
+
else console.log(c.yellow(`Not delivered: a terminal run has no chat to post in, and no push was sent (${noPush ?? body.push}).`));
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
const pushed = body.push === "sent" ? "and pushed to the user's devices" : noPush ? `(no push: ${noPush})` : "";
|
|
2715
|
+
console.log(c.green(`Posted in the chat ${pushed}.`.replace(" .", ".")));
|
|
2676
2716
|
}
|
|
2677
2717
|
|
|
2678
2718
|
// `bivy ask "<question>" [--option …]` — the question card, for any agent.
|
|
@@ -6475,6 +6515,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
6475
6515
|
case "attach":
|
|
6476
6516
|
await cmdAttach(args);
|
|
6477
6517
|
break;
|
|
6518
|
+
case "suggest":
|
|
6519
|
+
await cmdSuggest(args);
|
|
6520
|
+
break;
|
|
6478
6521
|
case "title":
|
|
6479
6522
|
await cmdTitle(args);
|
|
6480
6523
|
break;
|
package/bin/cli-commands.mjs
CHANGED
|
@@ -44,12 +44,14 @@ export const COMMANDS = [
|
|
|
44
44
|
{ name: "context", group: "session", scope: "session", json: true, usage: "context [--json]", summary: "Where this agent is running: session, workspace, machine, apps, and what to run next",
|
|
45
45
|
tools: [{ name: "bivy_context", description: "Where you are running inside Bivy: your session, agent, workspace and git branch, the machine, apps you have published, whether the user has the app open, and the commands that reach them.", input: {}, argv: ["context"] }] },
|
|
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
|
-
{ name: "notify", group: "session", scope: "session", json: true, usage:
|
|
48
|
-
tools: [{ name: "notify_user", description: "
|
|
47
|
+
{ name: "notify", group: "session", scope: "session", json: true, usage: 'notify "<message>" [--urgent]', summary: "Message the user: a card in the chat, and a push when they're away",
|
|
48
|
+
tools: [{ name: "notify_user", description: "Send the user a message: a card in the chat, and a push to their phone when nobody has the app open (the push names the session, not the text). Use it when long work finishes, you are blocked, or something needs their attention. At most one push a minute per session.", input: { message: { type: "string", description: "What to tell the user.", required: true }, urgent: { type: "boolean", description: "Push even while the user has the app open." } }, argv: ["notify", { arg: "message" }, { when: "urgent", flag: "--urgent" }] }] },
|
|
49
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
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
51
|
{ name: "title", group: "session", scope: "session", json: true, usage: 'title "<title>"', summary: "Rename this session",
|
|
52
52
|
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
|
+
{ name: "suggest", group: "session", scope: "session", json: true, usage: 'suggest "<task>" [--title "label"] [--run here|subagents|new]', summary: "Propose a task the user can start in one tap",
|
|
54
|
+
tools: [{ name: "suggest_task", description: "Propose a next step the user can start in one tap: in this session, through your sub-agents, or in a parallel session with its own copy of the project. Write the task as a complete instruction with paths relative to the project root. Post one per idea instead of listing them.", input: { task: { type: "string", description: "The complete instruction.", required: true }, title: { type: "string", description: "A short label for the card." }, run: { type: "string", enum: ["here", "subagents", "new"], description: "Where you recommend running it: here (builds on this conversation), subagents (independent tasks you can split across your own sub-agents; only if you have them), or new (bigger independent work in its own session). Defaults: here for one card, new for several." } }, argv: ["suggest", { arg: "task" }, { flag: "--title", from: "title" }, { flag: "--run", from: "run" }] }] },
|
|
53
55
|
{ 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"],
|
|
54
56
|
tools: [
|
|
55
57
|
{ name: "app_publish", description: "Give the user a live preview of something with a UI, from an app manifest file (web server port, static build, terminal, or desktop app). Run `bivy app --help` for the manifest format.", input: { manifest_path: { type: "string", description: "Path to the manifest JSON.", required: true } }, argv: ["app", "publish", { arg: "manifest_path" }] },
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
# Talk to the user
|
|
2
2
|
|
|
3
|
-
Summary: When to answer in chat, when to notify,
|
|
3
|
+
Summary: When to answer in chat, when to notify, when to ask and wait, and how to offer next steps.
|
|
4
4
|
|
|
5
5
|
Your chat reply is the default. Reach for these when the chat alone won't do.
|
|
6
6
|
|
|
7
7
|
## They may be away: notify
|
|
8
8
|
|
|
9
|
-
bivy notify
|
|
10
|
-
bivy notify --urgent
|
|
9
|
+
bivy notify "Migration finished: 3 tables rewritten, all tests pass."
|
|
10
|
+
bivy notify --urgent "Production is serving the old build."
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
You don't need it when you finish: Bivy already pushes when a turn ends while
|
|
18
|
-
they are away. Use it while you keep working and need them, for example when a
|
|
19
|
-
long run is blocked on something only they can do.
|
|
20
|
-
|
|
21
|
-
Next steps and options go in your reply as plain text.
|
|
12
|
+
Posts a card in the chat. When nobody has the app open, their devices get a push
|
|
13
|
+
naming this session (the text stays in the chat). `--urgent` pushes even while
|
|
14
|
+
they are looking. One push a minute per session; don't send one per step.
|
|
15
|
+
Good moments: long work finished, you are blocked, something needs a look.
|
|
22
16
|
|
|
23
17
|
## You need a decision: ask
|
|
24
18
|
|
|
@@ -30,6 +24,23 @@ dismissed it (use your judgment), exit 5 that `--timeout` (default 10m) passed.
|
|
|
30
24
|
For long waits use `--async`, keep working, and later `bivy ask wait <id>`.
|
|
31
25
|
Ask only when you can't reasonably decide yourself.
|
|
32
26
|
|
|
27
|
+
## Offer next steps: suggest
|
|
28
|
+
|
|
29
|
+
bivy suggest "Add a GET /version endpoint that returns the package version." --title "Add /version"
|
|
30
|
+
|
|
31
|
+
Each suggestion is a card the user can start in one tap: here, through your
|
|
32
|
+
sub-agents, or in a parallel session with its own copy of the project. Write it as
|
|
33
|
+
a complete instruction with paths relative to the project root. Post one per idea
|
|
34
|
+
instead of a bulleted list.
|
|
35
|
+
|
|
36
|
+
`--run` picks the card's main button; the others stay one tap away:
|
|
37
|
+
|
|
38
|
+
| `--run` | When |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `here` | It builds on this conversation, or it's small. Default for a single card. |
|
|
41
|
+
| `subagents` | Independent tasks you can split across your own sub-agents and report back on. Only if you have sub-agents. |
|
|
42
|
+
| `new` | Bigger independent work the user will want to follow, review or merge on its own. Default for several cards. |
|
|
43
|
+
|
|
33
44
|
## Name the session: title
|
|
34
45
|
|
|
35
46
|
bivy title "Fix login redirect loop"
|
|
@@ -39,12 +39,18 @@ export const BIVY_AGENT_NOTE = [
|
|
|
39
39
|
"can check your work; `bivy app present` tells the user a visible change is ready to look at, and " +
|
|
40
40
|
"`.bivy/scenarios/*.json` files let them open it in the states that matter (errors, empty, slow) with `--try`. " +
|
|
41
41
|
"`bivy app --help` has the details.",
|
|
42
|
+
"- Proposing tasks the user could hand you (next steps, ideas, options): post each with " +
|
|
43
|
+
'`bivy suggest "<complete instruction>" [--title "short label"] [--run here|subagents|new]` instead of only ' +
|
|
44
|
+
"listing them. The user can start each in one tap, in this session, through your sub-agents, or in a parallel " +
|
|
45
|
+
"one that works in its own copy of the project, so write it to stand on its own, with paths relative to the " +
|
|
46
|
+
"project root. `--run` is the one you recommend: here when it builds on this conversation, subagents for " +
|
|
47
|
+
"independent tasks you can split and supervise (only if you have sub-agents), new for bigger work worth its " +
|
|
48
|
+
"own session.",
|
|
42
49
|
'- If this session\'s title (taken from the first message) doesn\'t say what the work is, or the work changes ' +
|
|
43
50
|
'direction: `bivy title "<short title>"`.',
|
|
44
|
-
"-
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
"ask-the-user tool).",
|
|
51
|
+
"- When you finish long work, get blocked, or need the user to look at something: " +
|
|
52
|
+
'`bivy notify "<message>"` (a chat card, plus a push to their phone when they are away). To ask and wait for ' +
|
|
53
|
+
'an answer: `bivy ask "<question>" [--option A --option B]` prints their answer (or use your own ask-the-user tool).',
|
|
48
54
|
"- Only when the user asks for another agent or another of their machines to take part: " +
|
|
49
55
|
'`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
|
|
50
56
|
"its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { defineAgentIntegration } from "../definition.js";
|
|
7
7
|
import { PI_CAPABILITIES } from "./capabilities.js";
|
|
8
8
|
import { bridgeInstalled, enableBridgeResolution } from "../../agent-bridges.mjs";
|
|
9
|
-
export const PI_TESTED_VERSION = "
|
|
9
|
+
export const PI_TESTED_VERSION = "0.99.2";
|
|
10
10
|
export function piAgentDir() {
|
|
11
11
|
return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
12
12
|
}
|
package/dist/apps/styles.css
CHANGED
|
@@ -485,12 +485,6 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
|
|
|
485
485
|
}
|
|
486
486
|
.transcript-turn { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
|
|
487
487
|
.transcript-standalone { display: flex; justify-content: center; min-width: 0; }
|
|
488
|
-
/* A centered flex row lets a child grow past the column on one long unbroken
|
|
489
|
-
token (a path in an error); cap every standalone item to the column. */
|
|
490
|
-
.transcript-standalone > * { min-width: 0; max-width: 100%; }
|
|
491
|
-
/* Errors span the column so their edges line up with the turn content. */
|
|
492
|
-
.transcript-error { flex: 1; display: flex; flex-direction: column; gap: var(--space-2); overflow-wrap: anywhere; }
|
|
493
|
-
.transcript-error .settings-disclosure-body { margin: 0; padding: 0; white-space: pre-wrap; font-size: var(--text-xs); color: var(--muted); }
|
|
494
488
|
.turn-response-body { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
|
|
495
489
|
.jump-latest {
|
|
496
490
|
position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
|
|
@@ -661,6 +655,20 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
|
|
|
661
655
|
.entry-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); font-style: normal; }
|
|
662
656
|
.msg.system .entry-actions { justify-content: center; }
|
|
663
657
|
.msg.thinking { align-self: flex-start; color: var(--muted); font-size: 13px; border-left: 2px solid var(--line-strong); padding-left: 10px; }
|
|
658
|
+
.msg.error {
|
|
659
|
+
align-self: stretch; display: flex; gap: 8px; align-items: flex-start;
|
|
660
|
+
background: var(--danger-soft); border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
|
|
661
|
+
border-left: 3px solid var(--danger); border-radius: var(--radius-sm); padding: 10px 12px;
|
|
662
|
+
}
|
|
663
|
+
.msg-error-icon {
|
|
664
|
+
flex: none; width: 18px; height: 18px; margin-top: 1px; border-radius: var(--radius-full);
|
|
665
|
+
background: var(--danger); color: var(--danger-contrast); font-weight: 700; font-size: 12px;
|
|
666
|
+
display: grid; place-items: center;
|
|
667
|
+
}
|
|
668
|
+
.msg-error-text {
|
|
669
|
+
flex: 1; min-width: 0; font-size: 13px; line-height: 1.45; color: var(--ink);
|
|
670
|
+
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
|
|
671
|
+
}
|
|
664
672
|
.msg.streaming { position: relative; }
|
|
665
673
|
.msg.streaming::after { content: "▍"; color: var(--accent); animation: blink 1s steps(2) infinite; }
|
|
666
674
|
@keyframes blink { 50% { opacity: 0; } }
|
|
@@ -1469,8 +1477,10 @@ button.sheet-action:last-child { border-bottom: 0; }
|
|
|
1469
1477
|
}
|
|
1470
1478
|
/* Password-manager-style provider credential vault. Saved items are the primary
|
|
1471
1479
|
list; the provider catalog appears only inside Add. */
|
|
1472
|
-
/*
|
|
1473
|
-
|
|
1480
|
+
/* Wide settings panels that read as a shallow "vault": the credential vault and
|
|
1481
|
+
the isolated-machine-profiles panel share this container + header layout. */
|
|
1482
|
+
.credential-vault, .machine-profiles { gap: 14px; max-width: 680px; width: 100%; }
|
|
1483
|
+
.credential-vault { overflow-wrap: anywhere; }
|
|
1474
1484
|
.vault-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
|
|
1475
1485
|
.vault-title-row > div { min-width: 0; }
|
|
1476
1486
|
/* The header action (e.g. "+ Add profile") never shrinks or wraps — the title
|
|
@@ -2607,6 +2617,15 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
|
|
|
2607
2617
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
2608
2618
|
}
|
|
2609
2619
|
.connect-option:hover { border-color: var(--line-strong); box-shadow: var(--shadow-md); }
|
|
2620
|
+
.connect-option-head { display: flex; align-items: flex-start; gap: 12px; }
|
|
2621
|
+
.connect-option-badge {
|
|
2622
|
+
flex: none; width: 36px; height: 36px; display: grid; place-items: center;
|
|
2623
|
+
border-radius: var(--radius-md); color: var(--accent);
|
|
2624
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
2625
|
+
}
|
|
2626
|
+
.connect-option-copy { min-width: 0; }
|
|
2627
|
+
.connect-option-copy h3 { margin: 1px 0 3px; font-size: 15px; }
|
|
2628
|
+
.connect-option-copy p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
|
|
2610
2629
|
|
|
2611
2630
|
.connect-command {
|
|
2612
2631
|
display: flex; align-items: center; gap: 8px; padding: 4px 4px 4px 12px;
|
|
@@ -2626,8 +2645,9 @@ a.queue-item:hover { border-color: var(--line-strong); background: var(--surface
|
|
|
2626
2645
|
.connect-option-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
|
|
2627
2646
|
.connect-option-link { align-self: flex-start; color: var(--accent); font-size: 12.5px; font-weight: 600; text-decoration: none; }
|
|
2628
2647
|
.connect-option-link:hover { text-decoration: underline; }
|
|
2648
|
+
.connect-option-cta { align-self: flex-start; }
|
|
2629
2649
|
/* A one-time "next step" card above the composer (automations, notifications). */
|
|
2630
|
-
.next-step {
|
|
2650
|
+
.next-step { margin: 0 var(--space-4) var(--space-3); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
|
|
2631
2651
|
.next-step p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
|
|
2632
2652
|
.next-step .banner { margin-top: var(--space-2); }
|
|
2633
2653
|
.next-step .next-step-ok { margin: 0; color: var(--ok); font-weight: var(--weight-medium); }
|
|
@@ -2980,7 +3000,7 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
|
|
|
2980
3000
|
.node-switcher-btn.locked { cursor: default; }
|
|
2981
3001
|
.node-switcher-name { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2982
3002
|
.node-switcher-caret { color: var(--muted); font-size: 9px; }
|
|
2983
|
-
.node-menu { position: absolute; left: 0; top: calc(100% + 6px); min-width: 220px;
|
|
3003
|
+
.node-menu { position: absolute; left: 0; top: calc(100% + 6px); min-width: 220px; z-index: var(--z-dropdown); }
|
|
2984
3004
|
/* "Reconnecting…" line at the top of the open switcher, under the node the
|
|
2985
3005
|
button shows — the dropdown's spelled-out counterpart to the spinner dot. */
|
|
2986
3006
|
.node-menu-status { display: flex; align-items: center; gap: 8px; padding: 10px 14px; font-size: 13px; color: var(--muted); border-bottom: 1px solid var(--line); }
|
|
@@ -2995,8 +3015,6 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
|
|
|
2995
3015
|
.node-menu-item.active { font-weight: 600; }
|
|
2996
3016
|
.node-menu-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2997
3017
|
.node-menu-check { color: var(--accent); }
|
|
2998
|
-
/* Quiet trailing hint on a cloud row, e.g. "starts when you send". */
|
|
2999
|
-
.node-menu-meta { flex: none; color: var(--muted); font-size: var(--text-xs); }
|
|
3000
3018
|
.node-menu-glyph { color: var(--accent); flex: none; }
|
|
3001
3019
|
.eph-steps { margin: 4px 0; padding-left: 18px; color: var(--muted); font-size: 13px; display: grid; gap: 4px; }
|
|
3002
3020
|
.code-snippet { margin: 6px 0 0; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-md); padding: 10px; background: var(--surface); font-size: 12px; }
|
|
@@ -3229,9 +3247,24 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
|
|
|
3229
3247
|
.app-row-meta { color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
|
|
3230
3248
|
.app-row-action { flex: none; display: flex; align-items: center; gap: var(--space-1); }
|
|
3231
3249
|
.app-message { margin-block: var(--space-2); }
|
|
3250
|
+
/* Suggested task (SuggestionCard.tsx): a task the agent proposed, one tap to
|
|
3251
|
+
start in its own session. Shell from .card; layout only here. */
|
|
3252
|
+
.suggestion-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
|
|
3232
3253
|
/* The small label above a chat card's content, shared by every agent-posted card. */
|
|
3233
|
-
.delegation-eyebrow, .notice-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
|
|
3234
|
-
|
|
3254
|
+
.suggestion-eyebrow, .delegation-eyebrow, .notice-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
|
|
3255
|
+
.suggestion-title { margin: 0; font-size: var(--text-base); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
|
|
3256
|
+
.suggestion-text { margin: 0; color: var(--muted); font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
|
3257
|
+
.suggestion-text[data-expanded="true"] { display: block; overflow: visible; }
|
|
3258
|
+
.suggestion-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
|
|
3259
|
+
.suggestion-bar, .suggestion-all { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-2); }
|
|
3260
|
+
.suggestion-all { padding-top: var(--space-2); border-top: 1px solid var(--line); }
|
|
3261
|
+
.suggestion-bar .suggestion-actions, .suggestion-all .suggestion-actions { margin-top: 0; }
|
|
3262
|
+
.suggestion-hint { margin: 0; color: var(--muted); font-size: var(--text-xs); }
|
|
3263
|
+
/* In a set, the title is the checkbox's label, so the whole line is the tap target. */
|
|
3264
|
+
.suggestion-pick { display: flex; align-items: flex-start; gap: var(--space-2); cursor: pointer; }
|
|
3265
|
+
.suggestion-pick input { flex: none; inline-size: 18px; block-size: 18px; margin: 2px 0 0; accent-color: var(--accent); }
|
|
3266
|
+
.suggestion-status { margin: var(--space-2) 0 0; display: flex; align-items: center; gap: var(--space-2); color: var(--ok); font-size: var(--text-sm); font-weight: var(--weight-medium); }
|
|
3267
|
+
/* Notice (NoticeCard.tsx): a message the agent sent with `bivy notify`.
|
|
3235
3268
|
Shell and tone from .card, the urgent marker from .badge. */
|
|
3236
3269
|
.notice-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
|
|
3237
3270
|
.notice-eyebrow { display: flex; align-items: center; gap: var(--space-2); }
|
|
@@ -5,7 +5,7 @@ export const CERTIFICATION_MATRIX = {
|
|
|
5
5
|
agents: [
|
|
6
6
|
{ id: "claude-code-sdk", status: "active", executionMode: "protocol", pinnedVersion: "0.3.286", capabilities: ["toolInterception", "modelSelection", "resume"] },
|
|
7
7
|
{ id: "codex-approvals", status: "active", executionMode: "protocol", pinnedVersion: "0.159.2", capabilities: ["toolInterception", "modelSelection", "resume"] },
|
|
8
|
-
{ id: "pi", status: "active", executionMode: "protocol", pinnedVersion: "
|
|
8
|
+
{ id: "pi", status: "active", executionMode: "protocol", pinnedVersion: "0.99.2", capabilities: ["toolInterception", "modelSelection", "resume"] },
|
|
9
9
|
{ id: "opencode", status: "active", executionMode: "protocol", pinnedVersion: "1.18.33", capabilities: ["toolInterception", "modelSelection", "resume"] },
|
|
10
10
|
{ id: "grok", status: "active", executionMode: "protocol", pinnedVersion: "1.0.44", capabilities: ["toolInterception", "modelSelection", "resume"] },
|
|
11
11
|
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Ephemeral provisioning proxy (node-broker path).
|
|
4
|
+
//
|
|
5
|
+
// A remote device that holds the user's cloud token asks this node to make ONE
|
|
6
|
+
// allowlisted HTTPS request to a provider (Fly/Hetzner/AWS/...) on its behalf —
|
|
7
|
+
// the browser can't call those APIs directly (no CORS). The token/credentials
|
|
8
|
+
// ride in the request headers and are used transiently; nothing is persisted
|
|
9
|
+
// here, so the provisioning stays end-to-end (the control plane never sees
|
|
10
|
+
// it). The host allowlist is the SSRF guard and must be kept in lock-step with
|
|
11
|
+
// the two other copies: `ALLOWED_HOSTS` in packages/core/src/ephemeral.ts
|
|
12
|
+
// (the browser-side adapter) and the control-plane's cold-start relay in
|
|
13
|
+
// services/control-plane/src/index.ts.
|
|
14
|
+
export const EPHEMERAL_ALLOWED_HOSTS = new Set([
|
|
15
|
+
"api.hetzner.cloud",
|
|
16
|
+
"api.machines.dev",
|
|
17
|
+
"api.fly.io",
|
|
18
|
+
"ec2.us-east-1.amazonaws.com",
|
|
19
|
+
"ec2.us-west-2.amazonaws.com",
|
|
20
|
+
"ec2.eu-west-1.amazonaws.com",
|
|
21
|
+
"ec2.eu-central-1.amazonaws.com",
|
|
22
|
+
"ec2.ap-southeast-1.amazonaws.com",
|
|
23
|
+
"ec2.ap-northeast-1.amazonaws.com",
|
|
24
|
+
"ssm.us-east-1.amazonaws.com",
|
|
25
|
+
"ssm.us-west-2.amazonaws.com",
|
|
26
|
+
"ssm.eu-west-1.amazonaws.com",
|
|
27
|
+
"ssm.eu-central-1.amazonaws.com",
|
|
28
|
+
"ssm.ap-southeast-1.amazonaws.com",
|
|
29
|
+
"ssm.ap-northeast-1.amazonaws.com",
|
|
30
|
+
]);
|
|
31
|
+
/** Hard cap on redirect hops, so a provider (or a MITM) can't wedge us in a loop. */
|
|
32
|
+
const MAX_REDIRECTS = 5;
|
|
33
|
+
function checkAllowedHost(url) {
|
|
34
|
+
let host;
|
|
35
|
+
try {
|
|
36
|
+
host = new URL(url).host;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(`Bad provider URL: ${url}`);
|
|
40
|
+
}
|
|
41
|
+
if (!EPHEMERAL_ALLOWED_HOSTS.has(host))
|
|
42
|
+
throw new Error(`Refusing to proxy to non-provider host: ${host}`);
|
|
43
|
+
return host;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* How a redirect changes the follow-up request, mirroring the WHATWG fetch spec's
|
|
47
|
+
* own `redirect: "follow"` behavior (which we're replicating manually below so we
|
|
48
|
+
* can re-validate the target host on every hop): 303 always downgrades to a
|
|
49
|
+
* bodyless GET; 301/302 do the same but only for a non-GET/HEAD method; 307/308
|
|
50
|
+
* preserve the original method and body.
|
|
51
|
+
*/
|
|
52
|
+
function nextHopRequest(status, method, payload) {
|
|
53
|
+
if (status === 303 || ((status === 301 || status === 302) && method !== "GET" && method !== "HEAD")) {
|
|
54
|
+
return { method: "GET", payload: undefined };
|
|
55
|
+
}
|
|
56
|
+
return { method, payload };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Perform one allowlisted provider request for a remote device. Rejects any
|
|
60
|
+
* non-provider host — including a redirect target: the allowlist check ran only
|
|
61
|
+
* on the *initial* URL, but a plain `fetch` follows redirects transparently, so a
|
|
62
|
+
* provider response (or anyone able to influence it) could 302 us — Authorization
|
|
63
|
+
* header and all — to an arbitrary host. `redirect: "manual"` disables that
|
|
64
|
+
* auto-follow so every hop's Location is re-checked against the same allowlist
|
|
65
|
+
* before it's requested.
|
|
66
|
+
*/
|
|
67
|
+
export async function execEphemeralRequest(request, fetchImpl = fetch) {
|
|
68
|
+
let url = String(request?.url ?? "");
|
|
69
|
+
let method = String(request?.method ?? "GET").toUpperCase();
|
|
70
|
+
const headers = { ...(request?.headers ?? {}) };
|
|
71
|
+
let payload;
|
|
72
|
+
if (request?.body !== undefined && request?.body !== null && method !== "GET" && method !== "HEAD") {
|
|
73
|
+
payload = typeof request.body === "string" ? request.body : JSON.stringify(request.body);
|
|
74
|
+
if (!Object.keys(headers).some((h) => h.toLowerCase() === "content-type"))
|
|
75
|
+
headers["content-type"] = "application/json";
|
|
76
|
+
}
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
79
|
+
try {
|
|
80
|
+
for (let hop = 0;; hop++) {
|
|
81
|
+
const host = checkAllowedHost(url);
|
|
82
|
+
const res = await fetchImpl(url, { method, headers, body: payload, signal: controller.signal, redirect: "manual" });
|
|
83
|
+
if (res.status >= 300 && res.status < 400 && res.status !== 304) {
|
|
84
|
+
if (hop >= MAX_REDIRECTS)
|
|
85
|
+
throw new Error(`Too many redirects proxying to ${host}`);
|
|
86
|
+
const location = res.headers.get("location");
|
|
87
|
+
if (!location)
|
|
88
|
+
throw new Error(`Redirect from ${host} (${res.status}) had no Location header`);
|
|
89
|
+
// Resolve relative to the current hop; the loop's next iteration
|
|
90
|
+
// re-validates this new host before it's ever requested — the whole
|
|
91
|
+
// point of this loop.
|
|
92
|
+
url = new URL(location, url).toString();
|
|
93
|
+
({ method, payload } = nextHopRequest(res.status, method, payload));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const text = await res.text();
|
|
97
|
+
let body = text;
|
|
98
|
+
try {
|
|
99
|
+
body = text ? JSON.parse(text) : null;
|
|
100
|
+
}
|
|
101
|
+
catch { /* non-JSON provider response */ }
|
|
102
|
+
return { status: res.status, body };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -9,7 +9,6 @@ export function readEphemeralTeardownConfig(env = process.env) {
|
|
|
9
9
|
onFinish: env.BIVY_TEARDOWN_ON_FINISH === "1",
|
|
10
10
|
finishGraceMs: Number(env.BIVY_TEARDOWN_FINISH_GRACE_MS) || 10_000,
|
|
11
11
|
idleGraceMs: Number(env.BIVY_SESSION_IDLE_CLOSE_MS) || 30 * 60 * 1000,
|
|
12
|
-
sleep: env.BIVY_EPHEMERAL_SLEEP === "1",
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
@@ -21,10 +20,8 @@ export function readEphemeralTeardownConfig(env = process.env) {
|
|
|
21
20
|
export function shouldSelfTeardown(cfg, state) {
|
|
22
21
|
if (!cfg.enabled)
|
|
23
22
|
return false;
|
|
24
|
-
// A fresh box waits for its first work. A sleeping machine woken for
|
|
25
|
-
// nothing goes back to sleep after the idle window instead of running to TTL.
|
|
26
23
|
if (!state.everBusy)
|
|
27
|
-
return
|
|
24
|
+
return false;
|
|
28
25
|
if (state.anyWorking || state.anyRemoteActive || state.inFlightWork > 0)
|
|
29
26
|
return false;
|
|
30
27
|
const grace = cfg.onFinish ? cfg.finishGraceMs : cfg.idleGraceMs;
|
|
@@ -53,13 +50,6 @@ export async function performSelfTeardown(deps) {
|
|
|
53
50
|
torndown = true;
|
|
54
51
|
const log = deps.log ?? ((m) => console.log(`[ephemeral-teardown] ${m}`));
|
|
55
52
|
const exit = deps.exit ?? ((c) => process.exit(c));
|
|
56
|
-
if (deps.sleep) {
|
|
57
|
-
// Exit only: the provider keeps the stopped machine and its disk, and a
|
|
58
|
-
// wake starts it again. Signalling settled would get it reaped.
|
|
59
|
-
log(`machine idle — sleeping (provider=${deps.provider})`);
|
|
60
|
-
exit(0);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
53
|
log(`machine idle — self-teardown (provider=${deps.provider})`);
|
|
64
54
|
try {
|
|
65
55
|
await deps.signalSettled?.();
|
package/dist/exec.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
// A turn that ends without any assistant text because the agent FAILED (e.g. an
|
|
15
15
|
// unauthenticated CLI) exits non-zero with the error on stderr, rather than
|
|
16
16
|
// reporting a silent empty success.
|
|
17
|
-
import { randomUUID } from "node:crypto";
|
|
18
17
|
import { WebSocket } from "ws";
|
|
19
18
|
const USAGE = `Usage: bivy exec [options] "<prompt>" (or pipe the prompt on stdin, or pass -)
|
|
20
19
|
|
|
@@ -224,9 +223,7 @@ async function main() {
|
|
|
224
223
|
try {
|
|
225
224
|
await api(args.url, args.token, "/api/session/prompt", {
|
|
226
225
|
method: "POST",
|
|
227
|
-
|
|
228
|
-
// for a day, so an id shared by every run in a session ate follow-ups.
|
|
229
|
-
body: JSON.stringify({ sessionId, text: prompt, clientMessageId: `exec-${randomUUID()}` }),
|
|
226
|
+
body: JSON.stringify({ sessionId, text: prompt, clientMessageId: `exec-${sessionId}` }),
|
|
230
227
|
});
|
|
231
228
|
}
|
|
232
229
|
catch (error) {
|
|
@@ -147,19 +147,3 @@ export async function catalogReplyEvent(load, sessionId) {
|
|
|
147
147
|
return { type: "session.error", sessionId, error: `Couldn't read this machine's model catalog: ${error instanceof Error ? error.message : String(error)}` };
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Give a session's agent up to `timeoutMs` to publish its real model catalog
|
|
152
|
-
* before a caller reads it. A not-yet-started agent otherwise answers from a
|
|
153
|
-
* placeholder list. Never throws; a runtime without warmModels() is a no-op.
|
|
154
|
-
*/
|
|
155
|
-
export async function warmSessionCatalog(session, timeoutMs) {
|
|
156
|
-
if (!session.warmModels)
|
|
157
|
-
return;
|
|
158
|
-
let timer;
|
|
159
|
-
await Promise.race([
|
|
160
|
-
session.warmModels().catch(() => { }),
|
|
161
|
-
new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); }),
|
|
162
|
-
]);
|
|
163
|
-
if (timer)
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
}
|