@bivy/bivy 0.20.0 → 0.20.1-staging.10
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 -0
- package/bin/access-view.mjs +43 -0
- package/bin/bivy.mjs +300 -18
- package/bin/cli-commands.mjs +7 -2
- package/bin/guides/talk-to-the-user.md +19 -3
- package/bin/install-kind.mjs +20 -0
- package/bin/tailscale.mjs +70 -0
- package/bin/tui/app.mjs +189 -0
- package/bin/tui/model.mjs +112 -0
- package/bin/tui/render.mjs +164 -0
- package/bin/tui/text.mjs +72 -0
- package/bin/tui/transcript.mjs +54 -0
- package/dist/access.js +72 -0
- package/dist/agent-instructions.js +8 -3
- package/dist/apps/listeners.js +31 -3
- package/dist/apps/service.js +8 -1
- package/dist/apps/styles.css +16 -2
- package/dist/apps/tokens.css +40 -0
- package/dist/auth.js +31 -4
- package/dist/controllers/app-commands.js +7 -2
- package/dist/machine-theme.js +118 -0
- package/dist/node-update.js +16 -0
- package/dist/pair-codes.js +41 -0
- package/dist/protocol/client-command-routes.js +2 -0
- package/dist/protocol/client-command-schemas.js +3 -1
- package/dist/remote/direct.js +24 -0
- package/dist/remote/index.js +2 -0
- package/dist/remote/tailnet.js +44 -0
- package/dist/server.js +165 -18
- package/dist/session/run-terminal.js +6 -2
- package/dist/session/session-namer.js +8 -2
- package/dist/session/session-tokens.js +1 -1
- package/dist/session/suggestions.js +5 -1
- package/dist/web-app.js +55 -0
- package/package.json +2 -2
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/AutomationsView-CjTCAznJ.js +51 -0
- package/web/assets/ChangesCard-CBV_oCpv.js +4 -0
- package/web/assets/LibraryView-tZ-SLR74.js +2 -0
- package/web/assets/ProviderConnect-di0nwwfS.js +1 -0
- package/web/assets/ReadinessChecklist-Bbgq2GQ8.js +1 -0
- package/web/assets/Settings-wr27-Utd.js +10 -0
- package/web/assets/Sheet-DyGDs8Ap.js +2 -0
- package/web/assets/StatusDot-dpbztK53.js +1 -0
- package/web/assets/Terminal-B1njb2Cr.css +1 -0
- package/web/assets/Terminal-BnewuT2I.js +96 -0
- package/web/assets/VoiceSettings-gkjVXlwh.js +1 -0
- package/web/assets/abnfDiagram-VCTEODGH-xPExYTbq.js +1 -0
- package/web/assets/arc-Pry-7G1_.js +1 -0
- package/web/assets/architecture-7GRP2DOG-C2VA8oJp.js +1 -0
- package/web/assets/architectureDiagram-5GKGNRK7-DY9aKTXO.js +36 -0
- package/web/assets/array-BifhSqXX.js +1 -0
- package/web/assets/blockDiagram-I7D4REHJ-oAtHEG_S.js +129 -0
- package/web/assets/c4Diagram-7LVT6UL2-DAW5aIn-.js +38 -0
- package/web/assets/channel-DfQdWzMf.js +1 -0
- package/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web/assets/chunk-4HAMMTFA-k55Imkrb.js +62 -0
- package/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web/assets/chunk-75Z2AOVW-9BoEka2V.js +2 -0
- package/web/assets/chunk-DU6HZSFF-v3xjhwis.js +127 -0
- package/web/assets/chunk-F27PBJKO-ukCww9iX.js +1 -0
- package/web/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
- package/web/assets/chunk-GMAD6QVW-eEDdT117.js +72 -0
- package/web/assets/chunk-GVQU2GXP-BJ7btc9s.js +1 -0
- package/web/assets/chunk-IMKFNOWR-Cmcy1775.js +231 -0
- package/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web/assets/chunk-L3NEJ4N5-BjMoq7Ow.js +1 -0
- package/web/assets/chunk-OSK3NFVY-03uwrVwP.js +10 -0
- package/web/assets/chunk-P2QGCYS3-C3poXv15.js +1 -0
- package/web/assets/chunk-POPQ4Y6H-DmAcZLrU.js +1 -0
- package/web/assets/chunk-PWAF6VOD-Bi69-MYG.js +1 -0
- package/web/assets/chunk-SHT3W25Y-DBVBaWqA.js +168 -0
- package/web/assets/chunk-SVP7TREG-DA4UOsHJ.js +88 -0
- package/web/assets/chunk-TICWLB2K-CcUmJxSS.js +206 -0
- package/web/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
- package/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web/assets/classDiagram-ZZMXUADV-DDfEWWro.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-DDfEWWro.js +1 -0
- package/web/assets/common-DvIV9UK2.js +5 -0
- package/web/assets/cose-bilkent-JH36ORCC-BIqBF2fY.js +1 -0
- package/web/assets/cynefin-OW5HDTMX-B-p1iHVa.js +1 -0
- package/web/assets/cynefinDiagram-5FMLGOSQ-D3t5rjlb.js +62 -0
- package/web/assets/cytoscape.esm-NgwPT4ez.js +321 -0
- package/web/assets/dagre-C-rnJpZF.js +1 -0
- package/web/assets/dagre-GXQ25YYZ-CAVvMffj.js +4 -0
- package/web/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/web/assets/diagram-S7CK7UJ4-5avxKhVt.js +30 -0
- package/web/assets/diagram-UQ7AKVKN-Dre36bGM.js +41 -0
- package/web/assets/diagram-VSXAHHWV-DiIGVyBf.js +3 -0
- package/web/assets/diagram-VX7I27RA-J24keGuv.js +24 -0
- package/web/assets/diagram-Z3DM3KII-CO6mpfCn.js +24 -0
- package/web/assets/dist-v5Q1xZ2K.js +1 -0
- package/web/assets/ebnfDiagram-PWID7BFC-Cj8nHw-p.js +1 -0
- package/web/assets/erDiagram-RLTQ6QDP-DCnatJWn.js +99 -0
- package/web/assets/eventmodeling-NTZA5JFV-CUmkoHPv.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-CAMIZvHT.js +1 -0
- package/web/assets/ganttDiagram-EL5Y4UJY-CnoyY5yJ.js +292 -0
- package/web/assets/gitGraph-4MIJSDKK-5QxKBdhV.js +1 -0
- package/web/assets/gitGraphDiagram-WWUBYQGX-m72whYP5.js +106 -0
- package/web/assets/graphlib-DS17s2tU.js +1 -0
- package/web/assets/index-D36pVYmX.css +1 -0
- package/web/assets/index-DiYWjsXW.js +3 -0
- package/web/assets/info-A6RAGUB7-D0yTLuOD.js +1 -0
- package/web/assets/infoDiagram-27XIBGKW-DhMqaJK2.js +2 -0
- package/web/assets/init-C-OQMol4.js +1 -0
- package/web/assets/ishikawaDiagram-5VMMS53U-D43D69Lw.js +70 -0
- package/web/assets/journeyDiagram-3NMN7TZE-BGvdefN8.js +139 -0
- package/web/assets/jsx-runtime-NZYk81nU.js +1 -0
- package/web/assets/kanban-definition-UXKFOSKX-DTLCjXTM.js +89 -0
- package/web/assets/katex-ZlcWpGUi.js +257 -0
- package/web/assets/line-D3e4bvOi.js +1 -0
- package/web/assets/linear-SzVmGSw4.js +1 -0
- package/web/assets/mermaid-parser.core-hF3j4BfN.js +7 -0
- package/web/assets/mermaid.core-Dt4GvTcF.js +44 -0
- package/web/assets/mindmap-definition-YA3MSWOX-kGjt1kFy.js +96 -0
- package/web/assets/mount-BjlUftih.css +1 -0
- package/web/assets/mount-DFFc-s3e.js +27 -0
- package/web/assets/ordinal-BDEzSJ7C.js +1 -0
- package/web/assets/packet-AYTQ26CC-uWunEOCP.js +1 -0
- package/web/assets/path-fybaL0A-.js +1 -0
- package/web/assets/pegDiagram-XKGWAZYB-DHUvSUTF.js +1 -0
- package/web/assets/pie-WAS4IAKB-CDjHTH5e.js +1 -0
- package/web/assets/pieDiagram-E7YTZNPT-BXmHg_oo.js +39 -0
- package/web/assets/quadrantDiagram-AXDQQJYC-wrGDXaUj.js +7 -0
- package/web/assets/radar-RG4KPBEZ-iriLOEHZ.js +1 -0
- package/web/assets/railroad-74A4TZTK-Bji_D229.js +1 -0
- package/web/assets/railroad-abnf-HS5TGJTU-C2rjIz3_.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-D1IwbAAk.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-D6AkTvV0.js +1 -0
- package/web/assets/railroadDiagram-O6MQD6OU-DaMvYvYd.js +1 -0
- package/web/assets/requirementDiagram-BXWQKSXE-CSk6pyHt.js +84 -0
- package/web/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/web/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/web/assets/sankeyDiagram-P5KCCOFB-CDYh3o0x.js +40 -0
- package/web/assets/sequenceDiagram-WJ2MYXX4-Cebdnnr3.js +162 -0
- package/web/assets/settingsRoute-D0Hlh8NA.js +1 -0
- package/web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/web/assets/src-B6xuSHsQ.js +1 -0
- package/web/assets/stateDiagram-D77RDMKH-B9UCBm56.js +1 -0
- package/web/assets/stateDiagram-v2-MP3YSRHH-Btx-OjtR.js +1 -0
- package/web/assets/swimlanes-42K2YHIH-zH5kmbnV.js +1 -0
- package/web/assets/swimlanesDiagram-VR7AAH4N-CZInbEeM.js +8 -0
- package/web/assets/timeline-definition-24CTP7MA-D2tCs5Ws.js +120 -0
- package/web/assets/treeView-Q6P3EWNA-BCo4q2Xs.js +1 -0
- package/web/assets/treemap-WGGIJYW6-C2Xuxw-p.js +1 -0
- package/web/assets/useStore-DECYZPZP.js +27 -0
- package/web/assets/vennDiagram-4TSXK5OY-CMD5TuHM.js +34 -0
- package/web/assets/wardley-WFR3VGLG-0MDv_diU.js +1 -0
- package/web/assets/wardleyDiagram-VM6X3IG4-BOQSNZzn.js +78 -0
- package/web/assets/workbox-window.prod.es5-Bd17z0YL.js +2 -0
- package/web/assets/xychartDiagram-S5SC5T6Z-B79lzllh.js +7 -0
- package/web/fonts/OFL.txt +90 -0
- package/web/fonts/newsreader-italic.woff2 +0 -0
- package/web/fonts/newsreader-roman.woff2 +0 -0
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/icon.svg +16 -0
- package/web/index.html +36 -0
- package/web/manifest.webmanifest +1 -0
- package/web/runtime-config.js +3 -0
- package/web/sw.js +1 -0
- package/web/tent-logo.png +0 -0
package/README.md
CHANGED
|
@@ -11,6 +11,11 @@ Pi, OpenCode and other agents on your own machine. Reach them from a browser or
|
|
|
11
11
|
your phone, with the live session, its terminal, its approvals, and the app
|
|
12
12
|
they're building right in the chat. Try it, mark what's wrong, and send it back.
|
|
13
13
|
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="docs/images/preview-feedback-loop.gif" width="360"
|
|
16
|
+
alt="On a phone: open the app an agent built, circle the packed items, send a note, and compare the result before and after the agent's fix.">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
14
19
|
Bivy is not another coding agent and not a cloud development machine. The agent
|
|
15
20
|
stays local and does the coding with your model provider. Your repos, tools,
|
|
16
21
|
databases, and services stay where they are. Bivy gives you remote access to all
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Terminal views of the node's access report (src/access.ts): the full
|
|
4
|
+
// `bivy access` screen and the one-line summary `bivy status` shows.
|
|
5
|
+
|
|
6
|
+
const REACH_WORD = ["", "on your tailnet", "from anywhere"];
|
|
7
|
+
|
|
8
|
+
/** "Tailscale + Bivy hosted", or "This machine only". */
|
|
9
|
+
export function activeLabel(report) {
|
|
10
|
+
const on = report.setups.filter((s) => s.active && s.id !== "local");
|
|
11
|
+
return on.length ? on.map((s) => s.label).join(" + ") : "This machine only";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function labelOf(report, id) {
|
|
15
|
+
return report.setups.find((s) => s.id === id)?.label ?? id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `access: Tailscale — next: Bivy hosted` for `bivy status`. */
|
|
19
|
+
export function accessLine(report) {
|
|
20
|
+
const next = report.next.filter((n) => n.id !== "server").map((n) => labelOf(report, n.id));
|
|
21
|
+
return `${activeLabel(report)}${next.length ? ` — next: ${next.join(" or ")} ('bivy access')` : ""}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The `bivy access` screen as lines; `paint` supplies colors. */
|
|
25
|
+
export function accessScreen(report, paint) {
|
|
26
|
+
const lines = [`${paint.bold("Access:")} ${activeLabel(report)}`];
|
|
27
|
+
if (report.tailscaleUrl) lines.push(` ${paint.dim("tailnet")} ${report.tailscaleUrl}`);
|
|
28
|
+
if (report.controlPlaneUrl) lines.push(` ${paint.dim("app")} ${report.controlPlaneUrl}`);
|
|
29
|
+
lines.push("");
|
|
30
|
+
for (const feature of report.features) {
|
|
31
|
+
const reach = report.reach[feature.id];
|
|
32
|
+
lines.push(` ${reach ? paint.green("✓") : paint.dim("✗")} ${feature.label}${reach ? paint.dim(` ${REACH_WORD[reach]}`.trimEnd()) : ""}`);
|
|
33
|
+
}
|
|
34
|
+
if (report.next.length) {
|
|
35
|
+
lines.push("", paint.bold("Next:"));
|
|
36
|
+
for (const step of report.next) {
|
|
37
|
+
const setup = report.setups.find((s) => s.id === step.id);
|
|
38
|
+
lines.push(` ${paint.cyan((setup?.command ?? "").padEnd(26))} ${setup?.label}`, ` ${" ".repeat(26)} ${paint.dim(`adds ${step.addsText}`)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lines.push("", paint.dim("Setups stack: adding one never takes away another. 'bivy access local' turns remote access off."));
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
package/bin/bivy.mjs
CHANGED
|
@@ -40,8 +40,10 @@ import { nativeResumeRef, resolveSessionsLimit, truncateSavedSessions } from "./
|
|
|
40
40
|
import { renderManagedBlock, upsertManagedBlock, removeManagedBlock, rcFileForShell } from "./shim-path.mjs";
|
|
41
41
|
import { removeInstallAndState } from "./uninstall-paths.mjs";
|
|
42
42
|
import { findAvailablePort, reconcilePort } from "./port-picker.mjs";
|
|
43
|
+
import { accessLine, accessScreen } from "./access-view.mjs";
|
|
44
|
+
import { parseTailscaleStatus, serveConflict, serveFailureHint, serveOffArgs, serveOnArgs, serveState } from "./tailscale.mjs";
|
|
43
45
|
import { resolveAttachSessionId } from "./attach-session-id.mjs";
|
|
44
|
-
import { detectInstallKind as classifyInstallKind, npmGlobalPrefix } from "./install-kind.mjs";
|
|
46
|
+
import { detectInstallKind as classifyInstallKind, managedInstall, npmGlobalPrefix } from "./install-kind.mjs";
|
|
45
47
|
import { hasConfiguredService as configuredServiceExists } from "./service-state.mjs";
|
|
46
48
|
import { COMMANDS, EXIT, cliError, completionWords, describeCli, describeCommand, describeTools, renderHelp, resolveCommand, subcommandTable, suggestCommands, toolArgv, wantsJson } from "./cli-commands.mjs";
|
|
47
49
|
|
|
@@ -76,6 +78,7 @@ process.env.BIVY_DATA_DIR = appDir;
|
|
|
76
78
|
// - "npx" ephemeral `npx bivy` run (repoRoot under an npm _npx cache)
|
|
77
79
|
// - "npm-global" `npm i -g @bivy/bivy` (repoRoot is below node_modules/@bivy)
|
|
78
80
|
// - "packaged" install.sh tarball tree (user-owned, self-preserving)
|
|
81
|
+
// - "managed" a system package (the AUR's `bivy`), updated by its manager
|
|
79
82
|
function detectInstallKind() {
|
|
80
83
|
return classifyInstallKind(repoRoot);
|
|
81
84
|
}
|
|
@@ -2132,6 +2135,31 @@ async function fetchJson(baseUrl, pathName, token) {
|
|
|
2132
2135
|
// --json machine-readable list, no prompt
|
|
2133
2136
|
// --limit/-n N cap how many saved sessions to show (default: unlimited — all of them)
|
|
2134
2137
|
// <n> | <id> select non-interactively (index in the list, or a session id)
|
|
2138
|
+
// `bivy tui` — every session on this machine and your direct nodes in one
|
|
2139
|
+
// terminal view: live transcripts, approvals, messages to the agent.
|
|
2140
|
+
async function cmdTui(args = []) {
|
|
2141
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2142
|
+
console.log("Usage: bivy tui\n\nA terminal view of the sessions on this machine and on the nodes you added with 'bivy nodes add': live transcripts, approvals and messages to the agent, all from the keyboard. Press ? inside for keys.");
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2146
|
+
console.error(c.red("bivy tui needs an interactive terminal."));
|
|
2147
|
+
process.exitCode = EXIT.usage;
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const config = loadConfig();
|
|
2151
|
+
if (!(await ensureNodeRunning(config))) {
|
|
2152
|
+
console.error(c.red(`Could not start the Bivy node at ${url(config)}.`));
|
|
2153
|
+
process.exitCode = EXIT.unavailable;
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
const token = await localDeviceToken(config);
|
|
2157
|
+
const info = await fetchJson(url(config), "/api/node/info", token).catch(() => ({}));
|
|
2158
|
+
const direct = Object.entries(loadNodes().nodes).map(([name, node]) => ({ name, base: node.url, token: node.token }));
|
|
2159
|
+
const { runTui } = await import("./tui/app.mjs");
|
|
2160
|
+
await runTui({ machines: [{ name: info?.name || "this machine", base: url(config), token }, ...direct] });
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2135
2163
|
async function cmdSessions(args = [], opts = {}) {
|
|
2136
2164
|
if (args.includes("-h") || args.includes("--help")) {
|
|
2137
2165
|
console.log(
|
|
@@ -2521,18 +2549,18 @@ async function cmdContext(args = []) {
|
|
|
2521
2549
|
console.log(`\nEverything else: ${c.cyan("bivy help")} (or ${c.cyan("bivy help --json")}).`);
|
|
2522
2550
|
}
|
|
2523
2551
|
|
|
2524
|
-
// `bivy suggest "<task>" [--title "…"] [--session <id>]`
|
|
2525
|
-
// user can start in one tap,
|
|
2552
|
+
// `bivy suggest "<task>" [--title "…"] [--run here|subagents|new] [--session <id>]`
|
|
2553
|
+
// — propose a task the user can start in one tap, in this session or beside it. For an agent
|
|
2526
2554
|
// offering next steps: write the task as a complete instruction.
|
|
2527
2555
|
async function cmdSuggest(args = []) {
|
|
2528
|
-
const usage = 'Usage: bivy suggest "<task>" [--title "short label"] [--session <id>] [--json]';
|
|
2556
|
+
const usage = 'Usage: bivy suggest "<task>" [--title "short label"] [--run here|subagents|new] [--session <id>] [--json]';
|
|
2529
2557
|
if (args.includes("-h") || args.includes("--help")) {
|
|
2530
|
-
console.log(`${usage}\n\nPost a task the user can start in one tap
|
|
2558
|
+
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"}.`);
|
|
2531
2559
|
return;
|
|
2532
2560
|
}
|
|
2533
2561
|
const json = wantsJson(args);
|
|
2534
2562
|
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2535
|
-
const flagsWithValue = new Set(["--session", "--title"]);
|
|
2563
|
+
const flagsWithValue = new Set(["--session", "--title", "--run"]);
|
|
2536
2564
|
const flag = (name) => {
|
|
2537
2565
|
const i = args.indexOf(name);
|
|
2538
2566
|
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
@@ -2540,10 +2568,12 @@ async function cmdSuggest(args = []) {
|
|
|
2540
2568
|
const text = args.filter((a, i) => !a.startsWith("-") && !(i > 0 && flagsWithValue.has(args[i - 1]))).join(" ").trim();
|
|
2541
2569
|
const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
|
|
2542
2570
|
if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2571
|
+
const run = flag("--run");
|
|
2572
|
+
if (run !== undefined && !["here", "subagents", "new"].includes(run)) return fail({ code: "usage", message: `--run is here, subagents or new (got "${run}").`, exit: EXIT.usage });
|
|
2543
2573
|
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 });
|
|
2544
2574
|
const config = loadConfig();
|
|
2545
2575
|
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 });
|
|
2546
|
-
const res = await sessionPost(config, sessionId, "suggest", { text, title: flag("--title") }).catch((error) => error);
|
|
2576
|
+
const res = await sessionPost(config, sessionId, "suggest", { text, title: flag("--title"), run }).catch((error) => error);
|
|
2547
2577
|
if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
|
|
2548
2578
|
const body = await res.json().catch(() => ({}));
|
|
2549
2579
|
if (!res.ok) return fail(sessionHttpError("Suggest", res.status, body));
|
|
@@ -2551,6 +2581,30 @@ async function cmdSuggest(args = []) {
|
|
|
2551
2581
|
console.log(c.green("Suggested in the chat. The user can start it in one tap."));
|
|
2552
2582
|
}
|
|
2553
2583
|
|
|
2584
|
+
// `bivy title "<title>" [--session <id>]` — rename the session the agent runs in.
|
|
2585
|
+
async function cmdTitle(args = []) {
|
|
2586
|
+
const usage = 'Usage: bivy title "<title>" [--session <id>] [--json]';
|
|
2587
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2588
|
+
console.log(`${usage}\n\nRename this session in the user's session list. Use a short title that says what the work is, when the first message made a poor title or the work changed direction. --json prints {"ok","title"}.`);
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
const json = wantsJson(args);
|
|
2592
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2593
|
+
const sessionFlag = args.indexOf("--session");
|
|
2594
|
+
const title = args.filter((a, i) => !a.startsWith("-") && !(sessionFlag >= 0 && i === sessionFlag + 1)).join(" ").trim();
|
|
2595
|
+
const sessionId = resolveAttachSessionId({ sessionFlag: sessionFlag >= 0 ? args[sessionFlag + 1] : undefined, env: process.env });
|
|
2596
|
+
if (!title) return fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2597
|
+
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 });
|
|
2598
|
+
const config = loadConfig();
|
|
2599
|
+
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 });
|
|
2600
|
+
const res = await sessionPost(config, sessionId, "title", { title }).catch((error) => error);
|
|
2601
|
+
if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
|
|
2602
|
+
const body = await res.json().catch(() => ({}));
|
|
2603
|
+
if (!res.ok) return fail(sessionHttpError("Title", res.status, body));
|
|
2604
|
+
if (json) { console.log(JSON.stringify(body)); return; }
|
|
2605
|
+
console.log(c.green(`Renamed this session to "${body.title}".`));
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2554
2608
|
// Flags shared by the session commands: `--name value` pairs and bare switches,
|
|
2555
2609
|
// with every other word collected as positional text.
|
|
2556
2610
|
function parseSessionArgs(args, valueFlags) {
|
|
@@ -4474,6 +4528,8 @@ async function cmdTokenSetup() {
|
|
|
4474
4528
|
}
|
|
4475
4529
|
|
|
4476
4530
|
async function cmdSetup(args = []) {
|
|
4531
|
+
// Set by the access question; `bivy tailscale` runs once the node is up.
|
|
4532
|
+
let wantTailscale = false;
|
|
4477
4533
|
if (args.includes("-h") || args.includes("--help")) {
|
|
4478
4534
|
console.log("Usage: bivy setup\n\nFirst-run wizard: agent choice, model login, remote access + sign-in, and background service. Workspace and port get safe defaults. Re-run later to change the default agent or remote access.");
|
|
4479
4535
|
return;
|
|
@@ -4581,18 +4637,22 @@ async function cmdSetup(args = []) {
|
|
|
4581
4637
|
// (BIVY_CONTROL_PLANE_URL / BIVY_RELAY_URL then pre-fill the URL prompts below).
|
|
4582
4638
|
const nodeClaimCode = process.env.BIVY_NODE_CLAIM_CODE?.trim();
|
|
4583
4639
|
const selfHostEnv = !nodeClaimCode && Boolean((process.env.BIVY_CONTROL_PLANE_URL || "").trim() || (process.env.BIVY_RELAY_URL || "").trim());
|
|
4640
|
+
// Tailscale is offered only where it's installed; it needs no account.
|
|
4641
|
+
const tailscaleInstalled = runTailscale(["version"]) !== null;
|
|
4584
4642
|
const syncChoice = nodeClaimCode ? "h" : await askChoice(
|
|
4585
|
-
"
|
|
4643
|
+
"How will you reach this machine?",
|
|
4586
4644
|
[
|
|
4587
|
-
{ key: "h", label: "hosted (recommended — sign in with GitHub or email;
|
|
4588
|
-
{ key: "
|
|
4589
|
-
{ key: "
|
|
4645
|
+
{ key: "h", label: "Bivy hosted (recommended — sign in with GitHub or email; every machine, from anywhere)" },
|
|
4646
|
+
...(tailscaleInstalled ? [{ key: "t", label: "Tailscale (your devices over your tailnet; no account, nothing in between)" }] : []),
|
|
4647
|
+
{ key: "s", label: "your own server (a Bivy server you run)" },
|
|
4648
|
+
{ key: "l", label: "this machine only for now (add more any time with 'bivy access')" },
|
|
4590
4649
|
],
|
|
4591
4650
|
selfHostEnv ? "s" : "h",
|
|
4592
4651
|
);
|
|
4652
|
+
wantTailscale = syncChoice === "t";
|
|
4593
4653
|
const relayArgs = [];
|
|
4594
|
-
if (syncChoice === "l") {
|
|
4595
|
-
console.log(c.dim("
|
|
4654
|
+
if (syncChoice === "l" || syncChoice === "t") {
|
|
4655
|
+
console.log(c.dim(`\n${syncChoice === "t" ? "Tailscale is turned on once the node is running." : "Skipping remote access."} 'bivy access' shows every option later.`));
|
|
4596
4656
|
}
|
|
4597
4657
|
if (syncChoice === "s") {
|
|
4598
4658
|
const endpoints = await getHostedEndpoints();
|
|
@@ -4602,7 +4662,7 @@ async function cmdSetup(args = []) {
|
|
|
4602
4662
|
if (relayWs.trim()) relayArgs.push("--relay", relayWs.trim());
|
|
4603
4663
|
}
|
|
4604
4664
|
|
|
4605
|
-
if (syncChoice !== "l") {
|
|
4665
|
+
if (syncChoice !== "l" && syncChoice !== "t") {
|
|
4606
4666
|
if (!nodeClaimCode) {
|
|
4607
4667
|
const loginChoice = await askChoice(
|
|
4608
4668
|
"Remote login",
|
|
@@ -4651,7 +4711,7 @@ async function cmdSetup(args = []) {
|
|
|
4651
4711
|
setupSession = consumeSetupSession();
|
|
4652
4712
|
}
|
|
4653
4713
|
} else {
|
|
4654
|
-
console.log(c.dim("\nRemote access already configured.
|
|
4714
|
+
console.log(c.dim("\nRemote access already configured. 'bivy access' shows it and the other options."));
|
|
4655
4715
|
}
|
|
4656
4716
|
|
|
4657
4717
|
// GitHub App — connect it from the web app (Settings → GitHub App) or with
|
|
@@ -4690,6 +4750,19 @@ async function cmdSetup(args = []) {
|
|
|
4690
4750
|
}
|
|
4691
4751
|
}
|
|
4692
4752
|
|
|
4753
|
+
// The terminal path to the same loop: the agent's own command starts a Bivy
|
|
4754
|
+
// session, so work begun in a terminal shows up in the app and reaches the
|
|
4755
|
+
// phone. Offered after sign-in, which runs the real agent.
|
|
4756
|
+
const shimAgent = setupAgent?.command;
|
|
4757
|
+
if (agentReady && shimAgent && AGENT_INTEGRATIONS.has(shimAgent) && !loadShims().shims[shimAgent]) {
|
|
4758
|
+
console.log(`\nWhen you type '${shimAgent}' in a terminal, Bivy can run it as a session: it shows up in the app, and your phone hears when it finishes or needs you.`);
|
|
4759
|
+
if (await askYesNo(`Start a Bivy session whenever you run '${shimAgent}'?`, true)) {
|
|
4760
|
+
rl.pause();
|
|
4761
|
+
await run(nodeBin, [selfScript, "shim", "install", shimAgent], { env: process.env });
|
|
4762
|
+
rl.resume();
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4693
4766
|
// Materialize canonical typed config before starting/restarting the daemon,
|
|
4694
4767
|
// so this setup run's workspace/port/agent choices are effective immediately.
|
|
4695
4768
|
// Existing config keeps every advanced field.
|
|
@@ -4731,6 +4804,7 @@ async function cmdSetup(args = []) {
|
|
|
4731
4804
|
? Boolean(liveReadiness?.credential?.ok ?? hasModelConfig(finalConfig))
|
|
4732
4805
|
: agentAuthReady;
|
|
4733
4806
|
const repoReady = Boolean(liveReadiness?.repository?.ok);
|
|
4807
|
+
if (wantTailscale) await cmdTailscale(["on"]).catch((error) => console.log(c.yellow(`Tailscale setup didn't finish: ${error?.message || error}. Run 'bivy access tailscale' to retry.`)));
|
|
4734
4808
|
console.log(c.bold(c.green("\n ✓ Node running. Check first-task readiness below.\n")));
|
|
4735
4809
|
console.log(` ${c.green("✓")} node reachable at ${url(finalConfig)}`);
|
|
4736
4810
|
console.log(` ${agentReady ? c.green("✓") : c.yellow("!")} runtime ${agentReady ? `${setupAgent?.label || "Pi"} available` : "not installed — run 'bivy agents:install'"}`);
|
|
@@ -4739,7 +4813,7 @@ async function cmdSetup(args = []) {
|
|
|
4739
4813
|
const ghReady = githubConnected(finalConfig);
|
|
4740
4814
|
console.log(` ${ghReady ? c.green("✓") : c.dim("○")} GitHub ${ghReady ? "connected — your repos will list in the app" : c.dim("optional — connect later in the app under Settings → GitHub App")}`);
|
|
4741
4815
|
console.log(` ${agentReady && modelReady && repoReady ? c.green("✓") : c.yellow("!")} first task ${agentReady && modelReady && repoReady ? "ready to try" : "blocked by the stage above"}`);
|
|
4742
|
-
console.log(` ${fs.existsSync(relayConfigPath) ? c.green("✓") : c.
|
|
4816
|
+
console.log(` ${fs.existsSync(relayConfigPath) || fs.existsSync(tailscaleConfigPath) ? c.green("✓") : c.dim("○")} access ${fs.existsSync(relayConfigPath) || fs.existsSync(tailscaleConfigPath) ? "set up — 'bivy access' shows what it gives you" : "this machine only — 'bivy access' shows the options"}\n`);
|
|
4743
4817
|
// Get the user into the product immediately; terminal commands are the
|
|
4744
4818
|
// fallback/next-step checklist after the remote app has been opened or linked.
|
|
4745
4819
|
await finishSetupRemote(finalConfig, setupSession);
|
|
@@ -4926,8 +5000,10 @@ async function cmdStatus(args = []) {
|
|
|
4926
5000
|
const relay = loadRelayConfig();
|
|
4927
5001
|
const relaySt = status?.relay;
|
|
4928
5002
|
const relayConfigured = Boolean(relaySt?.configured || relay);
|
|
5003
|
+
const access = reachable ? await fetchAccess(config).catch(() => null) : null;
|
|
5004
|
+
if (access) console.log(` access: ${accessLine(access)}`);
|
|
4929
5005
|
if (!relayConfigured) {
|
|
4930
|
-
console.log(` remote: ${c.dim("
|
|
5006
|
+
if (!access) console.log(` remote: ${c.dim("this machine only")} ${c.dim("('bivy access' to see the options)")}`);
|
|
4931
5007
|
} else {
|
|
4932
5008
|
// The live link state is only knowable when the node is running; if it's
|
|
4933
5009
|
// down we can say it's configured but not whether it's currently connected.
|
|
@@ -5122,7 +5198,7 @@ async function cmdDoctor(args = []) {
|
|
|
5122
5198
|
const relayApp = status?.relay?.controlPlaneUrl;
|
|
5123
5199
|
const relayErr = status?.relay?.lastError;
|
|
5124
5200
|
const relayLine = !relayConfigured
|
|
5125
|
-
? c.dim("
|
|
5201
|
+
? c.dim("this machine only — 'bivy access' to see the options")
|
|
5126
5202
|
: relayConnected
|
|
5127
5203
|
? c.green("relay connected") + (relayApp ? c.dim(` ${relayApp}`) : "")
|
|
5128
5204
|
: c.yellow("configured, not connected") + (relayErr ? c.dim(` (${relayErr})`) : "");
|
|
@@ -5287,6 +5363,13 @@ async function cmdUpdate(args = []) {
|
|
|
5287
5363
|
console.log("Usage: bivy update [--force|--no-wait] [--staging|--stable|--channel <name>]\n\nUpdate Bivy + install deps + restart service. Stays on the release channel recorded at install time (default 'latest'); --staging/--stable/--channel switch channels and are remembered for future updates. Waits for active sessions to finish a turn first; --force/--no-wait skips the wait. See 'bivy update:log' for the last run's output.");
|
|
5288
5364
|
return;
|
|
5289
5365
|
}
|
|
5366
|
+
const managed = managedInstall(repoRoot);
|
|
5367
|
+
if (managed) {
|
|
5368
|
+
console.log(`Bivy is installed by ${managed.manager} here, so it updates through it${managed.update ? `: ${c.cyan(managed.update)}` : "."}`);
|
|
5369
|
+
console.log(c.dim("Then run 'bivy restart' to pick up the new version."));
|
|
5370
|
+
process.exitCode = EXIT.failed;
|
|
5371
|
+
return;
|
|
5372
|
+
}
|
|
5290
5373
|
// Inside a Bivy web/PWA terminal the shell is a child of the node's own
|
|
5291
5374
|
// process, so `restartService()` — the final step of an update — tears down
|
|
5292
5375
|
// this very terminal. Run inline and you lose all output the moment it lands.
|
|
@@ -5671,6 +5754,193 @@ async function cmdLinkPhone(args = []) {
|
|
|
5671
5754
|
}
|
|
5672
5755
|
}
|
|
5673
5756
|
|
|
5757
|
+
const tailscaleConfigPath = path.join(appDir, "tailscale.json");
|
|
5758
|
+
const LOCAL_TOKEN_NAMES = ["Bivy CLI", "Local Remote UI"];
|
|
5759
|
+
|
|
5760
|
+
function runTailscale(args) {
|
|
5761
|
+
const res = spawnSync("tailscale", args, { encoding: "utf8" });
|
|
5762
|
+
if (res.error?.code === "ENOENT") return null;
|
|
5763
|
+
return { ok: res.status === 0, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
async function printPairLink(config) {
|
|
5767
|
+
const data = await localApi(config, "/api/auth/pair-codes", { method: "POST", body: "{}" });
|
|
5768
|
+
if (!data?.url) throw new Error("The node did not return a pairing link.");
|
|
5769
|
+
const qr = terminalQr(data.url);
|
|
5770
|
+
if (qr) console.log(qr);
|
|
5771
|
+
console.log(`\nOpen on your phone (single use, 10 minutes):\n${c.cyan(data.url)}\n`);
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
// `bivy tailscale` — reach this machine from anywhere on your tailnet with no
|
|
5775
|
+
// control plane or relay: `tailscale serve` puts HTTPS on the machine's ts.net
|
|
5776
|
+
// name and forwards it to the node's direct listener, which serves the web app.
|
|
5777
|
+
async function cmdTailscale(args = []) {
|
|
5778
|
+
const [verb = "on"] = args;
|
|
5779
|
+
if (args.includes("-h") || args.includes("--help") || !["on", "pair", "status", "devices", "revoke", "off"].includes(verb)) {
|
|
5780
|
+
console.log(`Usage: bivy tailscale [on|pair|status|devices|revoke <id>|off]
|
|
5781
|
+
|
|
5782
|
+
on Serve this machine's Bivy on https://<machine>.<tailnet>.ts.net (tailnet only) and show a pairing link
|
|
5783
|
+
pair Show a new one-time pairing link for another device
|
|
5784
|
+
status Show the Tailscale address and whether it is served
|
|
5785
|
+
devices List the devices holding a token for this node
|
|
5786
|
+
revoke <id> Revoke a device's token and disconnect it
|
|
5787
|
+
off Stop serving Bivy over Tailscale`);
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
const config = loadConfig();
|
|
5791
|
+
if (!(await isReachable(config))) {
|
|
5792
|
+
console.log(c.yellow(`The node is not reachable at ${url(config)}. Start it first: ${c.cyan(config.service ? "bivy restart" : "bivy start")}`));
|
|
5793
|
+
process.exitCode = EXIT.unavailable;
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
const saved = (() => { try { return JSON.parse(fs.readFileSync(tailscaleConfigPath, "utf8")); } catch { return null; } })();
|
|
5797
|
+
|
|
5798
|
+
if (verb === "pair") {
|
|
5799
|
+
if (!saved) { console.log(`Tailscale access is off. Turn it on with ${c.cyan("bivy tailscale")}.`); process.exitCode = EXIT.failed; return; }
|
|
5800
|
+
await printPairLink(config);
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
if (verb === "devices") {
|
|
5804
|
+
// The CLI and loopback web client mint their own tokens on this machine; list them as a count.
|
|
5805
|
+
const all = (await localApi(config, "/api/auth/devices")) ?? [];
|
|
5806
|
+
const devices = all.filter((device) => !LOCAL_TOKEN_NAMES.includes(device.name));
|
|
5807
|
+
for (const device of devices) console.log(`${c.cyan(device.id)} ${device.name}${device.createdAt ? c.dim(` paired ${device.createdAt}`) : ""}`);
|
|
5808
|
+
if (!devices.length) console.log("No paired devices.");
|
|
5809
|
+
if (all.length > devices.length) console.log(c.dim(`(+${all.length - devices.length} tokens used on this machine by the CLI)`));
|
|
5810
|
+
return;
|
|
5811
|
+
}
|
|
5812
|
+
if (verb === "revoke") {
|
|
5813
|
+
const id = args[1];
|
|
5814
|
+
if (!id) { console.log("Usage: bivy tailscale revoke <id> (ids from 'bivy tailscale devices')"); process.exitCode = EXIT.usage; return; }
|
|
5815
|
+
const res = await fetch(`${url(config)}/api/auth/devices/${encodeURIComponent(id)}`, { method: "DELETE", headers: { authorization: `Bearer ${await localDeviceToken(config)}` } });
|
|
5816
|
+
if (!res.ok) { console.log(c.yellow(`No device ${id}.`)); process.exitCode = EXIT.notFound; return; }
|
|
5817
|
+
console.log(`${c.green("✓")} Revoked ${id}.`);
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
if (verb === "status") {
|
|
5821
|
+
console.log(saved ? `${c.green("✓")} Served on ${c.cyan(`https://${saved.hostname}`)} (tailnet only) → 127.0.0.1:${saved.port}` : `Tailscale access is off. Turn it on with ${c.cyan("bivy tailscale")}.`);
|
|
5822
|
+
return;
|
|
5823
|
+
}
|
|
5824
|
+
if (verb === "off") {
|
|
5825
|
+
if (saved) {
|
|
5826
|
+
const state = serveState(runTailscale(["serve", "status", "--json"])?.stdout, saved.hostname);
|
|
5827
|
+
if (state.target === `http://127.0.0.1:${saved.port}`) runTailscale(serveOffArgs());
|
|
5828
|
+
}
|
|
5829
|
+
fs.rmSync(tailscaleConfigPath, { force: true });
|
|
5830
|
+
await localApi(config, "/api/direct/reload", { method: "POST", body: "{}" });
|
|
5831
|
+
console.log(`${c.green("✓")} Bivy is no longer served over Tailscale. Paired devices keep their tokens; see them with 'bivy tailscale devices' and revoke with 'bivy tailscale revoke <id>'.`);
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5834
|
+
|
|
5835
|
+
const status = runTailscale(["status", "--json"]);
|
|
5836
|
+
if (!status) {
|
|
5837
|
+
console.log(`${c.yellow("Tailscale isn't installed.")} Install it from ${c.cyan("https://tailscale.com/download")} (Arch: ${c.cyan("sudo pacman -S tailscale && sudo systemctl enable --now tailscaled")}), sign in with ${c.cyan("sudo tailscale up")}, then run ${c.cyan("bivy tailscale")} again.`);
|
|
5838
|
+
process.exitCode = EXIT.unavailable;
|
|
5839
|
+
return;
|
|
5840
|
+
}
|
|
5841
|
+
const { running, hostname, owner } = parseTailscaleStatus(status.stdout);
|
|
5842
|
+
if (!running || !hostname) {
|
|
5843
|
+
console.log(`${c.yellow("Tailscale isn't connected.")} Sign in with ${c.cyan("sudo tailscale up")}, then run ${c.cyan("bivy tailscale")} again.`);
|
|
5844
|
+
process.exitCode = EXIT.unavailable;
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5847
|
+
const port = saved?.port ?? await findAvailablePort(config.port + 2, "127.0.0.1");
|
|
5848
|
+
const conflict = serveConflict(serveState(runTailscale(["serve", "status", "--json"])?.stdout, hostname), port);
|
|
5849
|
+
if (conflict) {
|
|
5850
|
+
console.log(c.yellow(conflict));
|
|
5851
|
+
process.exitCode = EXIT.conflict;
|
|
5852
|
+
return;
|
|
5853
|
+
}
|
|
5854
|
+
fs.writeFileSync(tailscaleConfigPath, `${JSON.stringify({ port, hostname, ...(owner ? { owner } : {}) }, null, 2)}\n`, { mode: 0o600 });
|
|
5855
|
+
const reload = await localApi(config, "/api/direct/reload", { method: "POST", body: "{}" });
|
|
5856
|
+
const served = runTailscale(serveOnArgs(port));
|
|
5857
|
+
if (!served?.ok) {
|
|
5858
|
+
fs.rmSync(tailscaleConfigPath, { force: true });
|
|
5859
|
+
await localApi(config, "/api/direct/reload", { method: "POST", body: "{}" }).catch(() => {});
|
|
5860
|
+
console.error(c.red(`tailscale serve failed: ${(served?.stderr || served?.stdout || "").trim()}`));
|
|
5861
|
+
const hint = serveFailureHint(served?.stderr);
|
|
5862
|
+
if (hint) console.log(hint);
|
|
5863
|
+
process.exitCode = EXIT.failed;
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
console.log(c.bold(c.green(`\n Bivy is on https://${hostname}\n`)));
|
|
5867
|
+
console.log(c.dim(owner
|
|
5868
|
+
? ` Only devices on your tailnet can reach it. Yours (signed in as ${owner}) are let in without pairing;\n anyone else's pairs once with a link from 'bivy tailscale pair'.`
|
|
5869
|
+
: " Only devices on your tailnet can reach it. This machine is tagged, so each device pairs once with a link from 'bivy tailscale pair'."));
|
|
5870
|
+
if (!reload?.webApp) console.log(c.yellow(" This install has no built web app, so the address serves the API only. Run 'pnpm run build:web' in a source checkout."));
|
|
5871
|
+
console.log("");
|
|
5872
|
+
if (owner) {
|
|
5873
|
+
console.log(`Open ${c.cyan(`https://${hostname}`)} on any of your devices.\n`);
|
|
5874
|
+
return;
|
|
5875
|
+
}
|
|
5876
|
+
await printPairLink(config);
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
async function fetchAccess(config) {
|
|
5880
|
+
return localApi(config, "/api/access/get", { method: "POST", body: "{}" });
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
// `bivy access` — how this machine can be reached (this machine only,
|
|
5884
|
+
// Tailscale, Bivy hosted, your own server), what that gives you, and the next
|
|
5885
|
+
// step. The node owns the table (src/access.ts); this only renders and routes.
|
|
5886
|
+
async function cmdAccess(args = []) {
|
|
5887
|
+
const [verb, target] = args.filter((a) => !a.startsWith("-"));
|
|
5888
|
+
if (args.includes("-h") || args.includes("--help") || (verb && !["local", "tailscale", "hosted", "server"].includes(verb))) {
|
|
5889
|
+
console.log(`Usage: bivy access [--json]
|
|
5890
|
+
bivy access tailscale reach this machine from your devices over Tailscale
|
|
5891
|
+
bivy access hosted sign in to Bivy hosted: every machine, from anywhere
|
|
5892
|
+
bivy access server <url> [--relay <wss-url>] the same, through your own server
|
|
5893
|
+
bivy access local turn remote access off
|
|
5894
|
+
|
|
5895
|
+
Setups stack: Tailscale can stay on alongside hosted or your own server.`);
|
|
5896
|
+
if (verb && !["local", "tailscale", "hosted", "server"].includes(verb)) process.exitCode = EXIT.usage;
|
|
5897
|
+
return;
|
|
5898
|
+
}
|
|
5899
|
+
const config = loadConfig();
|
|
5900
|
+
if (!(await ensureNodeRunning(config))) {
|
|
5901
|
+
console.error(c.red(`Could not start the Bivy node at ${url(config)}.`));
|
|
5902
|
+
process.exitCode = EXIT.unavailable;
|
|
5903
|
+
return;
|
|
5904
|
+
}
|
|
5905
|
+
const report = await fetchAccess(config);
|
|
5906
|
+
|
|
5907
|
+
if (!verb) {
|
|
5908
|
+
if (wantsJson(args)) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
5909
|
+
console.log(`\n${accessScreen(report, c).join("\n")}\n`);
|
|
5910
|
+
return;
|
|
5911
|
+
}
|
|
5912
|
+
if (verb === "tailscale") return cmdTailscale(["on"]);
|
|
5913
|
+
if (verb === "hosted" || verb === "server") {
|
|
5914
|
+
const linked = report.setups.find((s) => s.active && (s.id === "hosted" || s.id === "server"));
|
|
5915
|
+
if (linked && linked.id === verb && verb === "hosted") {
|
|
5916
|
+
console.log(`Already on ${linked.label}${report.controlPlaneUrl ? ` (${report.controlPlaneUrl})` : ""}.`);
|
|
5917
|
+
return;
|
|
5918
|
+
}
|
|
5919
|
+
if (linked) console.log(c.yellow(`This machine is on ${linked.label}${report.controlPlaneUrl ? ` (${report.controlPlaneUrl})` : ""}. Switching keeps everything on this machine; your devices sign in again at the new address.`));
|
|
5920
|
+
if (verb === "hosted") return cmdRelaySetup([]);
|
|
5921
|
+
if (!target) {
|
|
5922
|
+
console.log(`Usage: bivy access server <url> [--relay <wss-url>]\n\nThe address of your Bivy server, e.g. https://bivy.example.com. Set one up with docs/self-host-quickstart.md.`);
|
|
5923
|
+
process.exitCode = EXIT.usage;
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
let appUrl;
|
|
5927
|
+
try { appUrl = new URL(/^https?:\/\//.test(target) ? target : `https://${target}`); } catch { appUrl = null; }
|
|
5928
|
+
if (!appUrl) { console.error(c.red(`Not a URL: ${target}`)); process.exitCode = EXIT.usage; return; }
|
|
5929
|
+
// Bivy's server installs serve the relay at /relay on the app's own domain.
|
|
5930
|
+
const relayUrl = argValue(args, "relay") || `${appUrl.protocol === "http:" ? "ws" : "wss"}://${appUrl.host}/relay`;
|
|
5931
|
+
return cmdRelaySetup(["--control-plane", appUrl.origin, "--relay", relayUrl]);
|
|
5932
|
+
}
|
|
5933
|
+
// local: turn off every remote setup that is on.
|
|
5934
|
+
const on = report.setups.filter((s) => s.active && s.id !== "local");
|
|
5935
|
+
if (!on.length) { console.log("Already this machine only."); return; }
|
|
5936
|
+
const rl = createPrompter();
|
|
5937
|
+
const yes = await rl.askYesNo(`Turn off ${on.map((s) => s.label).join(" and ")}? Sessions and credentials on this machine stay.`, false);
|
|
5938
|
+
rl.close();
|
|
5939
|
+
if (!yes) return;
|
|
5940
|
+
if (on.some((s) => s.id === "tailscale")) await cmdTailscale(["off"]);
|
|
5941
|
+
if (on.some((s) => s.id === "hosted" || s.id === "server")) await cmdAccountLogout([]);
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5674
5944
|
function printUninstallHelp() {
|
|
5675
5945
|
console.log(`
|
|
5676
5946
|
${c.bold("bivy uninstall")} — remove Bivy and all its data from this machine
|
|
@@ -6085,6 +6355,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
6085
6355
|
case "suggest":
|
|
6086
6356
|
await cmdSuggest(args);
|
|
6087
6357
|
break;
|
|
6358
|
+
case "title":
|
|
6359
|
+
await cmdTitle(args);
|
|
6360
|
+
break;
|
|
6088
6361
|
case "notify":
|
|
6089
6362
|
await cmdNotify(args);
|
|
6090
6363
|
break;
|
|
@@ -6268,6 +6541,15 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
|
|
|
6268
6541
|
case "link":
|
|
6269
6542
|
await cmdLinkPhone(args);
|
|
6270
6543
|
break;
|
|
6544
|
+
case "tui":
|
|
6545
|
+
await cmdTui(args);
|
|
6546
|
+
break;
|
|
6547
|
+
case "tailscale":
|
|
6548
|
+
await cmdTailscale(args);
|
|
6549
|
+
break;
|
|
6550
|
+
case "access":
|
|
6551
|
+
await cmdAccess(args);
|
|
6552
|
+
break;
|
|
6271
6553
|
case "relay:setup":
|
|
6272
6554
|
await cmdRelaySetup(args);
|
|
6273
6555
|
break;
|
package/bin/cli-commands.mjs
CHANGED
|
@@ -48,8 +48,10 @@ export const COMMANDS = [
|
|
|
48
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
|
-
{ name: "
|
|
52
|
-
tools: [{ name: "
|
|
51
|
+
{ name: "title", group: "session", scope: "session", json: true, usage: 'title "<title>"', summary: "Rename this session",
|
|
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" }] },
|
|
@@ -63,6 +65,7 @@ export const COMMANDS = [
|
|
|
63
65
|
{ name: "run", group: "sessions", scope: "node", usage: "run <agent> [--chat] [--name …] [--model …] [--node …] [--workspace …]", summary: "Run an agent (native CLI/TUI) as a session the app can see" },
|
|
64
66
|
{ name: "exec", group: "sessions", scope: "node", json: true, usage: 'exec "<prompt>" [--agent …] [--session …]', summary: "One-shot headless session: prints the answer to stdout" },
|
|
65
67
|
{ name: "send", group: "sessions", scope: "node", json: true, usage: 'send <id> "<message>"', summary: "Send a prompt to an existing session and stream the reply" },
|
|
68
|
+
{ name: "tui", group: "sessions", scope: "node", usage: "tui", summary: "Sessions across your machines in the terminal: transcripts, approvals, messages" },
|
|
66
69
|
{ name: "sessions", aliases: ["ls"], group: "sessions", scope: "node", json: true, usage: "sessions [--json]", summary: "List recent sessions (live and saved) and resume one" },
|
|
67
70
|
{ name: "resume", group: "sessions", scope: "node", usage: "resume [n|id]", summary: "Resume a session directly (default: most recent)" },
|
|
68
71
|
{ name: "kill", group: "sessions", scope: "node", usage: "kill <id> [--delete]", summary: "Stop a session or terminal" },
|
|
@@ -103,6 +106,8 @@ export const COMMANDS = [
|
|
|
103
106
|
{ name: "token", group: "node", scope: "node", usage: "token", summary: "Print a device token for this node" },
|
|
104
107
|
{ name: "open", group: "node", scope: "node", usage: "open", summary: "Open the remote web app" },
|
|
105
108
|
{ name: "link", group: "node", scope: "node", usage: "link", summary: "Show a web app link as a QR code in the terminal" },
|
|
109
|
+
{ name: "access", group: "node", scope: "node", json: true, usage: "access [local|tailscale|hosted|server <url>]", summary: "How this machine can be reached, what that gives you, and the next step", subcommands: ["local", "tailscale", "hosted", "server"] },
|
|
110
|
+
{ name: "tailscale", group: "node", scope: "node", usage: "tailscale [on|pair|status|devices|revoke|off]", summary: "Reach this machine over Tailscale, with no server in between", subcommands: ["on", "pair", "status", "devices", "revoke", "off"] },
|
|
106
111
|
{ name: "relay:setup", group: "node", scope: "node", usage: "relay:setup", summary: "Enable secure remote web app access" },
|
|
107
112
|
{ name: "uninstall", group: "node", scope: "node", usage: "uninstall [--keep-sessions] [--dry-run]", summary: "Remove Bivy and all its data" },
|
|
108
113
|
|
|
@@ -28,9 +28,25 @@ Ask only when you can't reasonably decide yourself.
|
|
|
28
28
|
|
|
29
29
|
bivy suggest "Add a GET /version endpoint that returns the package version." --title "Add /version"
|
|
30
30
|
|
|
31
|
-
Each suggestion is a card the user can start in one tap
|
|
32
|
-
session with its own copy of the project. Write it as
|
|
33
|
-
paths relative to the project root. Post one per idea
|
|
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
|
+
|
|
44
|
+
## Name the session: title
|
|
45
|
+
|
|
46
|
+
bivy title "Fix login redirect loop"
|
|
47
|
+
|
|
48
|
+
The session list shows a title taken from the first message. When that doesn't
|
|
49
|
+
say what the work is, or the work changes direction, rename it. Keep it short.
|
|
34
50
|
|
|
35
51
|
## Is anyone there?
|
|
36
52
|
|
package/bin/install-kind.mjs
CHANGED
|
@@ -3,9 +3,29 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
+
/** The file a system package (pacman, …) writes into the install it owns. */
|
|
7
|
+
export const MANAGED_INSTALL_FILE = ".bivy-install.json";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `{ manager, update }` when a system package manager owns this install, so
|
|
11
|
+
* Bivy must not replace its files; null otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export function managedInstall(repoRoot, readFileSync = fs.readFileSync) {
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(readFileSync(path.join(repoRoot, MANAGED_INSTALL_FILE), "utf8"));
|
|
16
|
+
if (typeof data?.manager === "string" && data.manager) {
|
|
17
|
+
return { manager: data.manager, update: typeof data.update === "string" ? data.update : "" };
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
/* not managed */
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
/** Classify a Bivy package root without assuming unscoped npm package layout. */
|
|
7
26
|
export function detectInstallKind(repoRoot, existsSync = fs.existsSync) {
|
|
8
27
|
if (existsSync(path.join(repoRoot, ".git"))) return "git";
|
|
28
|
+
if (existsSync(path.join(repoRoot, MANAGED_INSTALL_FILE))) return "managed";
|
|
9
29
|
|
|
10
30
|
// npm installs an unscoped package at node_modules/name and a scoped package
|
|
11
31
|
// at node_modules/@scope/name. Bivy uses the latter layout.
|