@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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// The other machines on your tailnet that run Bivy, so a machine reached over
|
|
5
|
+
// Tailscale can offer the rest. Tailscale lists the peers; each is asked for
|
|
6
|
+
// Bivy's hello at its tailnet address, which only answers where `bivy
|
|
7
|
+
// tailscale` is on.
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
/** Peers from `tailscale status --json`, without the machine itself. */
|
|
10
|
+
export function parseTailnetPeers(text) {
|
|
11
|
+
let status;
|
|
12
|
+
try {
|
|
13
|
+
status = JSON.parse(text);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return Object.values(status?.Peer ?? {})
|
|
19
|
+
.map((p) => ({ dnsName: String(p.DNSName ?? "").replace(/\.$/, ""), hostName: String(p.HostName ?? ""), online: p.Online === true, os: p.OS }))
|
|
20
|
+
.filter((p) => p.dnsName.endsWith(".ts.net"));
|
|
21
|
+
}
|
|
22
|
+
function tailscaleStatus() {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
execFile("tailscale", ["status", "--json"], { timeout: 3000, maxBuffer: 4 * 1024 * 1024 }, (error, stdout) => resolve(error ? "" : stdout));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function hello(peer, fetchImpl) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetchImpl(`https://${peer.dnsName}/api/direct/hello`, { signal: AbortSignal.timeout(2500) });
|
|
30
|
+
const body = await res.json();
|
|
31
|
+
if (!res.ok || body?.bivy !== true)
|
|
32
|
+
return null;
|
|
33
|
+
return { name: typeof body.name === "string" && body.name ? body.name : peer.hostName, url: `https://${peer.dnsName}`, nodeId: typeof body.nodeId === "string" ? body.nodeId : undefined, os: peer.os, online: true, self: false };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Online tailnet peers running Bivy over Tailscale. Offline peers aren't asked. */
|
|
40
|
+
export async function discoverTailnetMachines(deps = {}) {
|
|
41
|
+
const peers = parseTailnetPeers(await (deps.status ?? tailscaleStatus)()).filter((p) => p.online);
|
|
42
|
+
const found = await Promise.all(peers.map((p) => hello(p, deps.fetchImpl ?? fetch)));
|
|
43
|
+
return found.filter((m) => m !== null).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
-
import { createNodeUpdateChecker, updateRegistryUrl } from "./node-update.js";
|
|
4
|
+
import { createNodeUpdateChecker, managedInstall, updateRegistryUrl } from "./node-update.js";
|
|
5
5
|
import { createRemoteSessionAdmission, RemoteSessionAdmissionError } from "./session/remote-session-admission.js";
|
|
6
6
|
import { requiresExistingSession, resolveTargetSession } from "./session/target-session.js";
|
|
7
7
|
import path from "node:path";
|
|
@@ -74,7 +74,7 @@ import { exportProviderAuth, exportAccountApiKeys, exportAccountOAuthCredentials
|
|
|
74
74
|
import { listProviders } from "./runtime/provider-catalog.js";
|
|
75
75
|
import { exportLocalModels, importLocalModels } from "./runtime/local-model-store.js";
|
|
76
76
|
import { sessionLikeFields } from "./session/start-like.js";
|
|
77
|
-
import { MAX_SUGGESTION_TEXT, MAX_SUGGESTION_TITLE, isTaskSuggestion } from "./session/suggestions.js";
|
|
77
|
+
import { MAX_SUGGESTION_TEXT, MAX_SUGGESTION_TITLE, SUGGESTION_RUNS, isTaskSuggestion } from "./session/suggestions.js";
|
|
78
78
|
import { BIVY_AGENT_NOTE, mergeSyncedAgentInstructions, readAgentInstructions, sessionInstructions, writeAgentInstructions, MAX_AGENT_INSTRUCTIONS_BYTES } from "./agent-instructions.js";
|
|
79
79
|
import { execEphemeralRequest } from "./ephemeral-exec.js";
|
|
80
80
|
import { ApprovalManager } from "./approval.js";
|
|
@@ -84,8 +84,14 @@ import { canOpenBrowser, openBrowser } from "./browser-open.js";
|
|
|
84
84
|
import { openOAuthLoginOnNode } from "./runtime/oauth/oauth-node-open.js";
|
|
85
85
|
import { collectNodeStats } from "./node-stats.js";
|
|
86
86
|
import { SessionEventCoalescer } from "./session-event-coalescer.js";
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
87
|
+
import { readMachineTheme, watchMachineTheme } from "./machine-theme.js";
|
|
88
|
+
import { authMiddleware, resolveAuth, isAuthorized, requestOriginAllowed, markRemoteListener } from "./auth.js";
|
|
89
|
+
import { PairCodes } from "./pair-codes.js";
|
|
90
|
+
import { accessReport } from "./access.js";
|
|
91
|
+
import { hostedEndpoints } from "./hosted-endpoints.mjs";
|
|
92
|
+
import http from "node:http";
|
|
93
|
+
import { webAppDirs, webAppRouter } from "./web-app.js";
|
|
94
|
+
import { RelayConnector, loadRelayConfig, loadDirectListenerConfig, discoverTailnetMachines, soloCredentials } from "./remote/index.js";
|
|
89
95
|
import { readEphemeralTeardownConfig, shouldSelfTeardown, snapshotsDurableForTeardown, performSelfTeardown } from "./ephemeral-teardown.js";
|
|
90
96
|
import { buildSessionSnapshot, applySessionSnapshot } from "./session/snapshot.js";
|
|
91
97
|
import { clearTurnActivity } from "./session/turn-activity.js";
|
|
@@ -919,6 +925,10 @@ async function maybeNotifyBivyUpdate() {
|
|
|
919
925
|
// in both. Returns a friendly error instead of throwing when it can't be found
|
|
920
926
|
// (e.g. an unusual layout), so the banner can fall back to the manual command.
|
|
921
927
|
function runBivyUpdate() {
|
|
928
|
+
// A system package (the AUR's `bivy`) owns this install; its manager updates it.
|
|
929
|
+
const managed = managedInstall(repoRoot);
|
|
930
|
+
if (managed)
|
|
931
|
+
return { ok: false, error: `Bivy is installed by ${managed.manager} on this machine. Update it there${managed.update ? `: ${managed.update}` : ""}, then restart Bivy.` };
|
|
922
932
|
const script = path.join(repoRoot, "bin", "bivy.mjs");
|
|
923
933
|
if (!fs.existsSync(script)) {
|
|
924
934
|
return { ok: false, error: "Could not locate the bivy CLI on this node — run `bivy update` in a terminal." };
|
|
@@ -1070,9 +1080,16 @@ const accessDevices = createAccessDeviceController({
|
|
|
1070
1080
|
create: (name) => identity.createDevice(name),
|
|
1071
1081
|
revoke: (id) => identity.revokeDevice(id),
|
|
1072
1082
|
onCreated: (device) => broadcast({ type: "device.created", device }),
|
|
1073
|
-
onRevoked: (id) =>
|
|
1083
|
+
onRevoked: (id) => {
|
|
1084
|
+
broadcast({ type: "device.revoked", id });
|
|
1085
|
+
// A revoked token loses its open sockets now, not at its next reconnect.
|
|
1086
|
+
for (const socket of clients)
|
|
1087
|
+
if (socketDeviceIds.get(socket) === id)
|
|
1088
|
+
socket.close(1008, "Unauthorized");
|
|
1089
|
+
},
|
|
1074
1090
|
});
|
|
1075
1091
|
const clients = new Set();
|
|
1092
|
+
const socketDeviceIds = new WeakMap();
|
|
1076
1093
|
const commandProcesses = new Map();
|
|
1077
1094
|
const oauthLogins = new Map();
|
|
1078
1095
|
// A browser-initiated subscription login parks the node on `manualCodePromise`
|
|
@@ -1712,6 +1729,9 @@ const commands = [
|
|
|
1712
1729
|
// network, background tab); used by both broadcast paths below.
|
|
1713
1730
|
const CLIENT_BACKPRESSURE_BYTES = 8 * 1024 * 1024;
|
|
1714
1731
|
let projectClientMessage;
|
|
1732
|
+
// The machine's desktop theme (Omarchy), so every connected app can match it.
|
|
1733
|
+
let machineTheme = readMachineTheme();
|
|
1734
|
+
const machineThemeEvent = () => ({ type: "node.theme", theme: machineTheme });
|
|
1715
1735
|
function broadcast(payload) {
|
|
1716
1736
|
const p = payload;
|
|
1717
1737
|
if (p?.type === "session.event" && p.sessionId && projectClientMessage)
|
|
@@ -2279,6 +2299,25 @@ appRegistry.on("notes", (viewId) => {
|
|
|
2279
2299
|
});
|
|
2280
2300
|
}, NOTES_NOTIFY_MS).unref?.();
|
|
2281
2301
|
});
|
|
2302
|
+
// Asking every peer takes a moment; a short cache keeps switcher opens instant.
|
|
2303
|
+
let tailnetCache = null;
|
|
2304
|
+
async function tailnetMachines() {
|
|
2305
|
+
const direct = loadDirectListenerConfig(appDir);
|
|
2306
|
+
if (!direct?.hostname)
|
|
2307
|
+
return [];
|
|
2308
|
+
if (!tailnetCache || Date.now() - tailnetCache.at > 15_000)
|
|
2309
|
+
tailnetCache = { at: Date.now(), machines: discoverTailnetMachines() };
|
|
2310
|
+
const self = { name: identity.name, url: `https://${direct.hostname}`, nodeId: identity.nodeId, os: process.platform, online: true, self: true };
|
|
2311
|
+
return [self, ...(await tailnetCache.machines).filter((m) => m.url !== self.url)];
|
|
2312
|
+
}
|
|
2313
|
+
function currentAccess() {
|
|
2314
|
+
const relay = loadRelayConfig(appDir);
|
|
2315
|
+
return accessReport({
|
|
2316
|
+
tailscaleHostname: loadDirectListenerConfig(appDir)?.hostname,
|
|
2317
|
+
relay: relay ? { controlPlaneUrl: relay.controlPlaneUrl, room: relay.room, roomToken: relay.roomToken } : null,
|
|
2318
|
+
hostedControlPlane: hostedEndpoints(process.env).controlPlane,
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2282
2321
|
const RELAY_COMMANDS = {
|
|
2283
2322
|
...createAppCommands(appService, (id) => { const record = resolveSession(id); return record ? harnessDirFor(record) : undefined; }, (app) => {
|
|
2284
2323
|
const record = resolveSession(app.sessionId);
|
|
@@ -2296,6 +2335,14 @@ const RELAY_COMMANDS = {
|
|
|
2296
2335
|
// (see runBivyUpdate). The node restarts itself when the update lands, so the
|
|
2297
2336
|
// client just sees the socket reconnect on the new build; a failure to even
|
|
2298
2337
|
// start reports back so the banner can show the manual command.
|
|
2338
|
+
// The machines on this machine's tailnet that run Bivy, this one first.
|
|
2339
|
+
async "tailnet.machines"(msg, ctx) {
|
|
2340
|
+
ctx.reply({ type: "tailnet.machines.ok", requestId: msg.requestId, machines: await tailnetMachines() });
|
|
2341
|
+
},
|
|
2342
|
+
// How this machine can be reached and what the next setup would add (src/access.ts).
|
|
2343
|
+
"access.get"(msg, ctx) {
|
|
2344
|
+
ctx.reply({ type: "access.get.ok", requestId: msg.requestId, ...currentAccess() });
|
|
2345
|
+
},
|
|
2299
2346
|
"node.update"(_msg, ctx) {
|
|
2300
2347
|
const result = runBivyUpdate();
|
|
2301
2348
|
ctx.reply({ type: "node.update.result", ok: result.ok, error: result.error });
|
|
@@ -2518,10 +2565,7 @@ const RELAY_COMMANDS = {
|
|
|
2518
2565
|
const rec = resolveSession(sid);
|
|
2519
2566
|
if (!rec)
|
|
2520
2567
|
return;
|
|
2521
|
-
|
|
2522
|
-
persistSessionMetadata(rec);
|
|
2523
|
-
ctx.broadcast({ type: "session.renamed", sessionId: rec.id, sessionFile: rec.sessionFile, name: newName });
|
|
2524
|
-
scheduleAdvertise();
|
|
2568
|
+
sessionNamer.setSessionName(rec, newName);
|
|
2525
2569
|
},
|
|
2526
2570
|
abort(msg, ctx) {
|
|
2527
2571
|
const sessionId = String(msg.sessionId ?? "");
|
|
@@ -2661,6 +2705,7 @@ const RELAY_COMMANDS = {
|
|
|
2661
2705
|
// Relay clients request the list on every connect/reconnect. Replay even an
|
|
2662
2706
|
// empty update state so a banner from the previous daemon is cleared.
|
|
2663
2707
|
relay?.sendEvent(nodeUpdates.snapshot());
|
|
2708
|
+
relay?.sendEvent(machineThemeEvent());
|
|
2664
2709
|
void checkBivyUpdate();
|
|
2665
2710
|
relay?.sendEvent({ type: "sessions.list", sessions: await sessionListRows() });
|
|
2666
2711
|
},
|
|
@@ -9337,7 +9382,8 @@ app.use((_req, res, next) => {
|
|
|
9337
9382
|
// (OAuth callback, GitHub App manifest). It does NOT host the web UI — the
|
|
9338
9383
|
// React/Vite PWA (@bivy/web) is served exclusively by the hosted or self-hosted
|
|
9339
9384
|
// control plane. `/` returns a small informational stub for anyone who points a
|
|
9340
|
-
// browser straight at the node.
|
|
9385
|
+
// browser straight at the node. The one exception is the direct listener for
|
|
9386
|
+
// `bivy tailscale` (below), which has no control plane and serves the app itself.
|
|
9341
9387
|
app.get("/", (_req, res) => {
|
|
9342
9388
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
9343
9389
|
res
|
|
@@ -9447,6 +9493,26 @@ app.post("/api/auth/bootstrap", sensitiveRateLimiter, (req, res) => {
|
|
|
9447
9493
|
const { device, token } = accessDevices.create(name);
|
|
9448
9494
|
res.json({ ok: true, device, token });
|
|
9449
9495
|
});
|
|
9496
|
+
// Pairing a device that reaches the node directly (Tailscale): trade a one-time
|
|
9497
|
+
// code minted on this machine for a device token. Registered before the auth
|
|
9498
|
+
// middleware because the caller has no token yet; the code is the credential.
|
|
9499
|
+
const pairCodes = new PairCodes();
|
|
9500
|
+
app.post("/api/auth/pair", sensitiveRateLimiter, (req, res) => {
|
|
9501
|
+
if (!requestOriginAllowed(req))
|
|
9502
|
+
return res.status(403).json({ error: "Forbidden origin" });
|
|
9503
|
+
if (!pairCodes.redeem(String(req.body?.code ?? ""))) {
|
|
9504
|
+
return res.status(401).json({ error: "This pairing link is used or expired. Run 'bivy tailscale pair' on your machine for a new one." });
|
|
9505
|
+
}
|
|
9506
|
+
const name = String(req.body?.name ?? "").trim().slice(0, 80) || "Paired device";
|
|
9507
|
+
const { device, token } = accessDevices.create(name);
|
|
9508
|
+
res.json({ ok: true, device, token });
|
|
9509
|
+
});
|
|
9510
|
+
// Identifies a Bivy node to the other machines on its tailnet, so each can list
|
|
9511
|
+
// the rest (tailnet.machines). Public, like /healthz: it says only that Bivy
|
|
9512
|
+
// runs here and under which name; the direct listener is tailnet-only.
|
|
9513
|
+
app.get("/api/direct/hello", (_req, res) => {
|
|
9514
|
+
res.json({ bivy: true, name: identity.name, nodeId: identity.nodeId });
|
|
9515
|
+
});
|
|
9450
9516
|
// All other /api routes require auth (loopback may bypass, per config and host
|
|
9451
9517
|
// — see loopbackAllowed()/isMultiUserHost() in src/auth.ts).
|
|
9452
9518
|
app.use("/api", authMiddleware(identity, {
|
|
@@ -9457,6 +9523,23 @@ app.use("/api", authMiddleware(identity, {
|
|
|
9457
9523
|
used: (sessionId, call) => { if (call.method !== "GET")
|
|
9458
9524
|
auditLog.record({ kind: "agent.call", session: sessionId, tool: `${call.method} ${call.path}` }); },
|
|
9459
9525
|
}));
|
|
9526
|
+
// Mint a pairing code for a direct (Tailscale) device. Only callers already
|
|
9527
|
+
// trusted by this node get one: the owner's CLI, or an already-paired device.
|
|
9528
|
+
app.post("/api/auth/pair-codes", (_req, res) => {
|
|
9529
|
+
const direct = loadDirectListenerConfig(appDir);
|
|
9530
|
+
const { code, expiresAt } = pairCodes.issue();
|
|
9531
|
+
const url = direct?.hostname ? `https://${direct.hostname}/#pair=${code}` : undefined;
|
|
9532
|
+
res.json({ ok: true, code, expiresAt, url });
|
|
9533
|
+
});
|
|
9534
|
+
// Start, move or stop the direct listener after `bivy tailscale` changes it.
|
|
9535
|
+
app.post("/api/direct/reload", async (_req, res, next) => {
|
|
9536
|
+
try {
|
|
9537
|
+
res.json({ ok: true, ...(await reloadDirectListener()) });
|
|
9538
|
+
}
|
|
9539
|
+
catch (error) {
|
|
9540
|
+
next(error);
|
|
9541
|
+
}
|
|
9542
|
+
});
|
|
9460
9543
|
// Reload relay.json without forcing the user to restart the whole node. This is
|
|
9461
9544
|
// used by `bivy relay:setup` after it enrolls the node.
|
|
9462
9545
|
app.post("/api/relay/reload", (_req, res) => {
|
|
@@ -9823,10 +9906,7 @@ app.post("/api/sessions/rename", (req, res, next) => {
|
|
|
9823
9906
|
const rec = resolveSession(sid);
|
|
9824
9907
|
if (!rec)
|
|
9825
9908
|
return res.status(404).json({ error: "Session not found" });
|
|
9826
|
-
|
|
9827
|
-
persistSessionMetadata(rec);
|
|
9828
|
-
broadcast({ type: "session.renamed", sessionId: rec.id, sessionFile: rec.sessionFile, name: newName });
|
|
9829
|
-
scheduleAdvertise();
|
|
9909
|
+
sessionNamer.setSessionName(rec, newName);
|
|
9830
9910
|
res.json({ ok: true, name: newName });
|
|
9831
9911
|
}
|
|
9832
9912
|
catch (error) {
|
|
@@ -11665,17 +11745,32 @@ app.get("/api/session/:id/automations/apply/:proposalId", (req, res) => {
|
|
|
11665
11745
|
return res.status(404).json({ error: "Proposal not found" });
|
|
11666
11746
|
res.json(proposal);
|
|
11667
11747
|
});
|
|
11748
|
+
// `bivy title "<title>"`: the agent names its own session, e.g. when the first
|
|
11749
|
+
// message made a poor title or the work changed direction.
|
|
11750
|
+
const MAX_SESSION_TITLE = 100;
|
|
11751
|
+
app.post("/api/session/:id/title", (req, res) => {
|
|
11752
|
+
const record = openSessions.get(String(req.params.id));
|
|
11753
|
+
if (!record)
|
|
11754
|
+
return res.status(404).json({ error: "Session not found" });
|
|
11755
|
+
const title = typeof req.body?.title === "string" ? req.body.title.replace(/\s+/g, " ").trim() : "";
|
|
11756
|
+
if (!title || title.length > MAX_SESSION_TITLE)
|
|
11757
|
+
return res.status(400).json({ error: `A title needs 1 to ${MAX_SESSION_TITLE} characters.` });
|
|
11758
|
+
sessionNamer.setSessionName(record, title);
|
|
11759
|
+
res.json({ ok: true, title });
|
|
11760
|
+
});
|
|
11668
11761
|
// `bivy suggest "<task>"`: the agent proposes a task the user can start in one
|
|
11669
|
-
// tap, here or in a parallel session
|
|
11762
|
+
// tap, here, through this agent's sub-agents, or in a parallel session; `run`
|
|
11763
|
+
// is the one the agent recommends (see packages/web SuggestionCard).
|
|
11670
11764
|
app.post("/api/session/:id/suggest", (req, res) => {
|
|
11671
11765
|
const record = openSessions.get(String(req.params.id));
|
|
11672
11766
|
if (!record)
|
|
11673
11767
|
return res.status(404).json({ error: "Session not found" });
|
|
11674
11768
|
const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
|
|
11675
11769
|
const title = typeof req.body?.title === "string" && req.body.title.trim() ? req.body.title.trim() : undefined;
|
|
11676
|
-
const
|
|
11770
|
+
const run = req.body?.run || undefined;
|
|
11771
|
+
const suggestion = { id: `suggestion-${randomBytes(8).toString("hex")}`, text, ...(title ? { title } : {}), ...(run ? { run } : {}) };
|
|
11677
11772
|
if (!isTaskSuggestion(suggestion))
|
|
11678
|
-
return res.status(400).json({ error: `A suggestion needs text (up to ${MAX_SUGGESTION_TEXT} characters)
|
|
11773
|
+
return res.status(400).json({ error: `A suggestion needs text (up to ${MAX_SUGGESTION_TEXT} characters), an optional title (up to ${MAX_SUGGESTION_TITLE}) and an optional run (${SUGGESTION_RUNS.join(", ")}).` });
|
|
11679
11774
|
eventLog.appendSuggestion(record.id, { afterMessageCount: record.session.getMessages().length, suggestion });
|
|
11680
11775
|
broadcast(stampSessionEvent({ type: "session.event", sessionId: record.id, event: { type: "suggestion", id: suggestion.id, suggestion } }));
|
|
11681
11776
|
res.json({ ok: true, id: suggestion.id });
|
|
@@ -12005,6 +12100,11 @@ const server = app.listen(port, host, async () => {
|
|
|
12005
12100
|
console.log(`Agent data dir: ${piDir}`);
|
|
12006
12101
|
console.log(`Workspace: ${defaultWorkspace}`);
|
|
12007
12102
|
startRelayIfConfigured();
|
|
12103
|
+
watchMachineTheme((theme) => {
|
|
12104
|
+
machineTheme = theme;
|
|
12105
|
+
broadcast(machineThemeEvent());
|
|
12106
|
+
});
|
|
12107
|
+
void reloadDirectListener().catch((error) => console.error("[direct] Could not start the direct listener:", error instanceof Error ? error.message : error));
|
|
12008
12108
|
void syncAgentBridges().catch((error) => console.warn("[bridges] Could not install agent bridges; they will install on first use:", error instanceof Error ? error.message : error));
|
|
12009
12109
|
if (appGateway) {
|
|
12010
12110
|
appGateway.server.on("error", (error) => { console.error("[apps] Preview gateway could not start:", error); shutdown("preview gateway failure"); });
|
|
@@ -12071,6 +12171,49 @@ server.on("error", (error) => {
|
|
|
12071
12171
|
process.exit(1);
|
|
12072
12172
|
});
|
|
12073
12173
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
12174
|
+
// Direct listener (`bivy tailscale`): the same API and WebSocket, plus the web
|
|
12175
|
+
// app, on a loopback port `tailscale serve` exposes to the tailnet over HTTPS.
|
|
12176
|
+
// markRemoteListener keeps its proxied, loopback-looking connections from ever
|
|
12177
|
+
// getting loopback trust; every caller needs a device token.
|
|
12178
|
+
const directWebApp = webAppRouter(webAppDirs(repoRoot));
|
|
12179
|
+
let directListener = null;
|
|
12180
|
+
let directOwner;
|
|
12181
|
+
async function reloadDirectListener() {
|
|
12182
|
+
const config = loadDirectListenerConfig(appDir);
|
|
12183
|
+
directOwner = config?.owner;
|
|
12184
|
+
if (directListener && directListener.port !== config?.port) {
|
|
12185
|
+
const previous = directListener;
|
|
12186
|
+
directListener = null;
|
|
12187
|
+
for (const client of previous.wss.clients)
|
|
12188
|
+
client.terminate();
|
|
12189
|
+
previous.wss.close();
|
|
12190
|
+
previous.server.closeAllConnections();
|
|
12191
|
+
await new Promise((resolve) => previous.server.close(() => resolve()));
|
|
12192
|
+
}
|
|
12193
|
+
if (config && !directListener) {
|
|
12194
|
+
const directApp = express();
|
|
12195
|
+
directApp.disable("x-powered-by");
|
|
12196
|
+
if (directWebApp)
|
|
12197
|
+
directApp.use(directWebApp);
|
|
12198
|
+
directApp.use(app);
|
|
12199
|
+
const listener = http.createServer(directApp);
|
|
12200
|
+
// The owner's own tailnet devices come in on their Tailscale identity.
|
|
12201
|
+
markRemoteListener(listener, { trustedLogin: () => directOwner });
|
|
12202
|
+
const directWss = new WebSocketServer({ server: listener, path: "/ws" });
|
|
12203
|
+
directWss.on("connection", (socket, req) => wss.emit("connection", socket, req));
|
|
12204
|
+
await new Promise((resolve, reject) => {
|
|
12205
|
+
listener.once("error", reject);
|
|
12206
|
+
listener.listen(config.port, "127.0.0.1", () => {
|
|
12207
|
+
listener.off("error", reject);
|
|
12208
|
+
resolve();
|
|
12209
|
+
});
|
|
12210
|
+
});
|
|
12211
|
+
listener.on("error", (error) => console.error("[direct] Listener error:", error));
|
|
12212
|
+
directListener = { port: config.port, server: listener, wss: directWss };
|
|
12213
|
+
console.log(`[direct] Listening on 127.0.0.1:${config.port}${config.hostname ? ` for https://${config.hostname}` : ""}${directWebApp ? "" : " (web app not built; API only)"}`);
|
|
12214
|
+
}
|
|
12215
|
+
return { enabled: Boolean(directListener), port: directListener?.port, webApp: Boolean(directWebApp) };
|
|
12216
|
+
}
|
|
12074
12217
|
wss.on("connection", (socket, req) => {
|
|
12075
12218
|
// Reject cross-origin / DNS-rebinding upgrades before auth: loopback bypasses
|
|
12076
12219
|
// auth by default, so a page the user merely visited could otherwise open a
|
|
@@ -12079,11 +12222,14 @@ wss.on("connection", (socket, req) => {
|
|
|
12079
12222
|
socket.close(1008, "Forbidden origin");
|
|
12080
12223
|
return;
|
|
12081
12224
|
}
|
|
12082
|
-
|
|
12225
|
+
const socketAuth = resolveAuth(identity, req);
|
|
12226
|
+
if (!isAuthorized(socketAuth)) {
|
|
12083
12227
|
socket.close(1008, "Unauthorized");
|
|
12084
12228
|
return;
|
|
12085
12229
|
}
|
|
12086
12230
|
clients.add(socket);
|
|
12231
|
+
if (socketAuth.deviceId)
|
|
12232
|
+
socketDeviceIds.set(socket, socketAuth.deviceId);
|
|
12087
12233
|
// Terminals opened on this socket; closed when the socket disconnects so a
|
|
12088
12234
|
// dropped browser tab doesn't leak shells. Output is unicast to this socket.
|
|
12089
12235
|
const ownedTerminals = new Set();
|
|
@@ -12099,6 +12245,7 @@ wss.on("connection", (socket, req) => {
|
|
|
12099
12245
|
// a freshly-opened app surfaces a newly-available update without waiting for a
|
|
12100
12246
|
// session turn.
|
|
12101
12247
|
socket.send(JSON.stringify(nodeUpdates.snapshot()));
|
|
12248
|
+
socket.send(JSON.stringify(machineThemeEvent()));
|
|
12102
12249
|
void checkBivyUpdate();
|
|
12103
12250
|
socket.on("message", (raw) => {
|
|
12104
12251
|
let msg;
|
|
@@ -180,7 +180,7 @@ export function createRunTerminals(deps) {
|
|
|
180
180
|
deps.broadcast({ type: "terminal.idle", termId: id, at: Date.now() });
|
|
181
181
|
broadcastRunTerminalList();
|
|
182
182
|
const who = displayName || "An agent";
|
|
183
|
-
deps.sendNotificationHint({ kind: "agent_waiting", title: `${who} is waiting`, body: `${who} has been quiet for a bit — it may be waiting for your input
|
|
183
|
+
deps.sendNotificationHint({ kind: "agent_waiting", title: `${who} is waiting`, body: `${who} has been quiet for a bit — it may be waiting for your input.`, ...runTarget(id) });
|
|
184
184
|
}, IDLE_NOTIFY_INTERVAL));
|
|
185
185
|
}
|
|
186
186
|
function clearRunIdleNotify(id) {
|
|
@@ -201,7 +201,11 @@ export function createRunTerminals(deps) {
|
|
|
201
201
|
lastBellNotify.set(id, now);
|
|
202
202
|
const meta = terminals.meta(id);
|
|
203
203
|
const who = meta?.name?.trim() || meta?.label?.trim() || "A terminal";
|
|
204
|
-
deps.sendNotificationHint({ kind: "terminal_bell", title: "Terminal bell", body: `${who} rang the terminal bell — it may be waiting for you
|
|
204
|
+
deps.sendNotificationHint({ kind: "terminal_bell", title: "Terminal bell", body: `${who} rang the terminal bell — it may be waiting for you.`, ...runTarget(id) });
|
|
205
|
+
}
|
|
206
|
+
function runTarget(id) {
|
|
207
|
+
const sessionId = terminals.meta(id)?.sessionId;
|
|
208
|
+
return sessionId ? { sessionId, targetSessionId: sessionId } : {};
|
|
205
209
|
}
|
|
206
210
|
function clearBellNotify(id) {
|
|
207
211
|
lastBellNotify.delete(id);
|
|
@@ -180,6 +180,12 @@ export function createSessionNamer(deps) {
|
|
|
180
180
|
catch (error) {
|
|
181
181
|
console.warn("Session naming suggestion failed", error);
|
|
182
182
|
}
|
|
183
|
+
// A title set while the model was thinking (the user, or the agent with
|
|
184
|
+
// `bivy title`) wins over the refinement.
|
|
185
|
+
if (record.session.getName() !== fallback) {
|
|
186
|
+
record.namedFromFirstPrompt = true;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
183
189
|
if (finalName !== fallback) {
|
|
184
190
|
record.session.setName(finalName);
|
|
185
191
|
deps.persistSessionMetadata(record);
|
|
@@ -211,8 +217,8 @@ export function createSessionNamer(deps) {
|
|
|
211
217
|
record.naming = false;
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
|
-
/** Manually set a session's title (
|
|
215
|
-
* first-prompt namer won't overwrite it. */
|
|
220
|
+
/** Manually set a session's title (a rename in the app, `bivy title`, a
|
|
221
|
+
* GitHub-issue pickup) and lock it so the first-prompt namer won't overwrite it. */
|
|
216
222
|
function setSessionName(record, name) {
|
|
217
223
|
const clean = name.trim();
|
|
218
224
|
if (!clean)
|
|
@@ -44,7 +44,7 @@ export function createSessionTokenCodec(key = randomBytes(32)) {
|
|
|
44
44
|
*/
|
|
45
45
|
export const SESSION_TOKEN_ROUTES = [
|
|
46
46
|
{ method: "GET", path: /^\/api\/session\/([^/]+)\/context$/, session: "path" },
|
|
47
|
-
{ method: "POST", path: /^\/api\/session\/([^/]+)\/(attach|suggest|notify)$/, session: "path" },
|
|
47
|
+
{ method: "POST", path: /^\/api\/session\/([^/]+)\/(attach|suggest|notify|title)$/, session: "path" },
|
|
48
48
|
{ method: "POST", path: /^\/api\/session\/([^/]+)\/ask$/, session: "path" },
|
|
49
49
|
{ method: "GET", path: /^\/api\/session\/([^/]+)\/ask\/[^/]+$/, session: "path" },
|
|
50
50
|
{ method: "POST", path: /^\/api\/session\/([^/]+)\/ask\/[^/]+\/wait$/, session: "path" },
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
/** here: this session's agent. subagents: this agent, through its own sub-agents. new: a session per task. */
|
|
4
|
+
export const SUGGESTION_RUNS = ["here", "subagents", "new"];
|
|
5
|
+
const isRun = (value) => value === undefined || SUGGESTION_RUNS.includes(value);
|
|
3
6
|
export const SUGGESTION_BLOCK = "bivy_suggestion";
|
|
4
7
|
export const MAX_SUGGESTION_TEXT = 4000;
|
|
5
8
|
export const MAX_SUGGESTION_TITLE = 120;
|
|
@@ -9,5 +12,6 @@ export function isTaskSuggestion(value) {
|
|
|
9
12
|
const s = value;
|
|
10
13
|
return typeof s.id === "string" && s.id.length > 0
|
|
11
14
|
&& typeof s.text === "string" && s.text.length > 0 && s.text.length <= MAX_SUGGESTION_TEXT
|
|
12
|
-
&& (s.title === undefined || (typeof s.title === "string" && s.title.length <= MAX_SUGGESTION_TITLE))
|
|
15
|
+
&& (s.title === undefined || (typeof s.title === "string" && s.title.length <= MAX_SUGGESTION_TITLE))
|
|
16
|
+
&& isRun(s.run);
|
|
13
17
|
}
|
package/dist/web-app.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// The web app, served by the node itself on its direct listener (Tailscale).
|
|
5
|
+
// Elsewhere the control plane serves it and the node stays a pure data plane;
|
|
6
|
+
// here there is no control plane, so the node hands out the same build and
|
|
7
|
+
// tells it, through /runtime-config.js, to talk to this node directly.
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import express from "express";
|
|
11
|
+
/** Where a packaged install and a source checkout keep the built app and its icons. */
|
|
12
|
+
export function webAppDirs(packageRoot) {
|
|
13
|
+
return [
|
|
14
|
+
path.join(packageRoot, "web"),
|
|
15
|
+
path.join(packageRoot, "packages", "web", "dist"),
|
|
16
|
+
path.join(packageRoot, "services", "control-plane", "public"),
|
|
17
|
+
].filter((dir) => fs.existsSync(dir));
|
|
18
|
+
}
|
|
19
|
+
/** Paths the node itself answers, never the app shell. */
|
|
20
|
+
const NODE_ROUTES = ["/api", "/ws", "/healthz", "/github"];
|
|
21
|
+
function noStore(res) {
|
|
22
|
+
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
23
|
+
}
|
|
24
|
+
/** Serves the app shell, assets and a direct-mode runtime config; passes on anything else. */
|
|
25
|
+
export function webAppRouter(dirs) {
|
|
26
|
+
const indexDir = dirs.find((dir) => fs.existsSync(path.join(dir, "index.html")));
|
|
27
|
+
if (!indexDir)
|
|
28
|
+
return null;
|
|
29
|
+
// Read once: the shell is served from memory, with no file access per request.
|
|
30
|
+
const indexHtml = fs.readFileSync(path.join(indexDir, "index.html"));
|
|
31
|
+
const router = express.Router();
|
|
32
|
+
router.get("/runtime-config.js", (_req, res) => {
|
|
33
|
+
noStore(res);
|
|
34
|
+
res.type("application/javascript").send(`globalThis.__BIVY_RUNTIME_CONFIG__ = ${JSON.stringify({ directNode: true })};\n`);
|
|
35
|
+
});
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
router.use(express.static(dir, {
|
|
38
|
+
index: false,
|
|
39
|
+
setHeaders(res, filePath) {
|
|
40
|
+
const name = path.basename(filePath);
|
|
41
|
+
if (name === "index.html" || name === "sw.js" || name.startsWith("workbox-"))
|
|
42
|
+
noStore(res);
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
// Client-side routes (`/sessions/…`, `/settings`, `/share`, …) load the shell;
|
|
47
|
+
// the node's own routes pass through.
|
|
48
|
+
router.get(/.*/, (req, res, next) => {
|
|
49
|
+
if (NODE_ROUTES.some((prefix) => req.path === prefix || req.path.startsWith(`${prefix}/`)) || !req.accepts("html"))
|
|
50
|
+
return next();
|
|
51
|
+
noStore(res);
|
|
52
|
+
res.type("html").send(indexHtml);
|
|
53
|
+
});
|
|
54
|
+
return router;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bivy/bivy",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1-staging.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"lodash-es": "4.18.1",
|
|
66
66
|
"undici": "8.11.2"
|
|
67
67
|
},
|
|
68
|
-
"readme": "# Bivy\n\n[](https://www.npmjs.com/package/@bivy/bivy)\n[](LICENSE)\n[](https://nodejs.org)\n\n**Local coding agents that show their work. Anywhere.**\n\nBivy is the open-source workspace for coding agents. It runs Claude Code, Codex,\nPi, OpenCode and other agents on your own machine. Reach them from a browser or\nyour phone, with the live session, its terminal, its approvals, and the app\nthey're building right in the chat. Try it, mark what's wrong, and send it back.\n\nBivy is not another coding agent and not a cloud development machine. The agent\nstays local and does the coding with your model provider. Your repos, tools,\ndatabases, and services stay where they are. Bivy gives you remote access to all\nof it, plus live previews, automations, and review.\n\n- **Live previews.** The running app beside the chat. Mark it, send it back,\n share it with someone who has no Bivy account.\n- **Automations.** GitHub issues, failed CI, Linear, Slack, schedules, and\n webhooks start the work. It comes back as a preview link or a pull request.\n- **Any agent.** Hit a usage limit? Fork the session to another agent, or let\n Bivy retry when the limit resets.\n- **Your machine, anywhere.** Start at your desk, pick the session up on your\n phone. The machine dials out, so there are no ports to open, and session\n traffic is end-to-end encrypted.\n\n**[Start free on Bivy Cloud](https://app.bivy.sh)** ·\n**[Quickstart](docs/quickstart.md)** ·\n**[Documentation](docs/README.md)** ·\n**[Self-host](docs/deploy-images.md)** ·\n**[Website](https://bivy.sh)**\n\n**Recommended:** sign in at [app.bivy.sh](https://app.bivy.sh), then copy your\npersonalized **Connect a Machine** command into a terminal on your Mac or Linux\ncomputer. It installs and enrolls the machine without another Bivy login. The\nbrowser connects automatically; choose a repository and send your first task\nright there.\n\nPrefer starting from the terminal?\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # or codex, pi, opencode\nbivy open # continue in the web app (needs remote setup)\n```\n\nBivy Cloud hosts the app, control plane, and relay—not the machines running your\nagents. Connect a Mac, Linux computer, or existing server and bring your own\nagent subscription, model API key, or local model. You can also self-host the\nentire remote-access stack.\n\n> **Bivy is 0.x software.** Claude Code, Codex, Pi, OpenCode, and Grok are the\n> release-tested paths. Credential sync, resume, handoffs, approvals, and\n> sandboxing depend on the runtime. See the\n> [runtime support matrix](docs/runtime-support-matrix.md).\n\n## Don't just read the diff. Try the app.\n\nBivy lets you **use what the agent built and show it what to fix**, without a\nseparate deployment step.\n\n1. **Ask for a change.** The agent works in your repo with your existing tools\n and local services.\n2. **Try the app.** Bivy finds the dev server the agent starts and opens it\n beside the chat: web apps, terminals, and desktop apps. Use the running app,\n not a screenshot of what the agent says it finished.\n3. **Mark what needs work.** Press and hold anything in the preview to mark it,\n or drag to circle it, then say what you want. **Mark another** keeps\n separate notes separate. The marks go to the agent with the element and page\n context. No screenshots to paste.\n4. **Follow each note.** Every mark stays in the chat as a pin that resolves\n itself: **Changed** when a later run changes what you marked, **Element\n gone** when it leaves the page, **Done** when you say so.\n5. **Review the next version.** A new version waits on the preview pill instead\n of reloading under you. Take it when you're ready, compare before and after,\n inspect the diff and checks, and decide when the work is done.\n6. **Get a second opinion.** Share a preview link for 1 hour, 1 day, or 7 days.\n Teammates and clients try the running app and leave notes without a Bivy\n account. Make it view-only, or **Stop sharing** to end every link at once.\n\nOn Bivy Cloud, preview delivery is built in: no per-app domains, certificates,\npublic ports, or tunnels to configure. Self-hosters configure preview delivery\nonce for their Bivy deployment, not for every app. The app still needs its normal\nbuild or dev-server setup, and **the machine serving it must stay awake and\nonline**. These are development previews, not production hosting.\n\n**Share deliberately:** anyone with a preview link can use that app, including\nits live backend, until the link expires or you stop sharing.\nReviewer notes aren't sent to the agent automatically; you send them or explicitly\nallow agent access. Preview traffic uses HTTPS through the preview relay, not\nsession end-to-end encryption; the relay operator can see it.\n\n[App previews, visual feedback, and sharing →](docs/apps.md)\n\n## One workflow, from trigger to review\n\n```text\nPrompt · GitHub issue · CI failure · Linear · Slack · Schedule · Webhook\n │\n ▼\n Choose machine + agent + model\n + supported credentials\n │\n ▼\n Live agent session\n Join · steer · approve · stop\n │\n ▼\n Try app · inspect changes · checks\n │\n ▼\n Mark up · send feedback · iterate\n │\n ▼\n Share preview · review PR\n```\n\nA **Machine** is a computer or server you connect. A **Session** is live agent\nwork on that machine. A **Run** is delegated background work that creates a\nsession and tracks its outcome. An **Automation** is a reusable definition that\ncreates runs when an event matches.\n\nManual and automated work use the same kind of live session. You can join a run\nwhen it needs help rather than wait for a black-box job to finish.\n\n### Let events start the work\n\nAutomations turn recurring or incoming work into sessions you can join,\nsupervise, and review. An agent picks the task up on your machine and posts\nback a preview link or a pull request, so you try the result where the task\nlives and reply in the same session. Choose the repository, machine, agent,\nmodel, approval mode, sandbox setting, and maximum attempts.\n\n| Trigger | Example workflow |\n|---|---|\n| **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |\n| **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |\n| **Linear** | Label an issue to start work without copying its description into an agent. |\n| **Slack** | Send a request from the conversation where the work came up. |\n| **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |\n| **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |\n\nConfigure automations in the app or version them with your repository in\n`.bivy/automations.yaml`:\n\n```bash\nbivy automation init\n# Edit the generated definition for your repository and workflow.\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture\nbivy automation apply\n```\n\nOr delegate a one-off job without creating an automation:\n\n```bash\nbivy runs start \"Review outdated dependencies and propose a small, tested update.\"\nbivy runs wait <id>\n```\n\nRuns keep routing and lifecycle evidence, check results, and output references\nin a reviewable Receipt. For unattended issue work, Bivy runs declared repository\nchecks after the agent's turn; failed required checks fail the run even if the\nagent reports success. A completed process alone is not proof that the task\nsucceeded.\n\n[Automation recipes →](docs/capability-recipes.md#let-events-start-runs) ·\n[Automations as code →](docs/automations-as-code.md) ·\n[Run outcomes and reliability limits →](docs/automation-runs.md)\n\n### Use every agent you pay for. Switch mid-task.\n\nRun Claude Code for one task, Codex for another, and Pi or OpenCode where they\nfit, side by side in one session list. Bivy supplies the shared session,\nremote-access, automation, and review surfaces; your chosen agent still does\nthe coding and uses your model provider.\n\n- **Hit a usage limit? Keep going.** When a turn fails on a limit, fork the\n session, with its code and conversation, to another agent and model, or let\n Bivy retry when the limit resets (the provider has to report a reset time).\n- **Get a second opinion.** `bivy delegate` hands a task to another agent, on\n the same machine or a different one, and brings back its answer, branch, and\n PR. `--to codex,claude@linux` sends one task to several agents to compare.\n- Choose an agent and, where supported, a model for each session or run.\n- Import existing Claude Code and Codex sessions.\n- Fork or move work to another agent or machine when a different setup fits\n better. Continuation fidelity varies: some paths preserve native history,\n while others replay portable turns or hand over a summary with the earlier\n transcript as a local file.\n- Use agent-native logins, Bivy-managed credentials, or local inference.\n Bivy's custom OpenAI-compatible endpoint registry currently feeds Pi;\n other agents may need their own provider configuration.\n- Register your own ACP or headless process agent with `bivy agent add`.\n\nBivy does not replace your agent, provide model inference, or make every agent's\nfeatures identical. Consult the [support matrix](docs/runtime-support-matrix.md)\nand [handoff recipes](docs/capability-recipes.md#fork-or-move-a-session).\n\n### Less signing in. Less copying secrets.\n\nBivy syncs **Bivy-managed API keys and supported OAuth credentials** across\nenrolled machines for compatible runtimes. Connect supported credentials once\nand reuse them where you run work, rather than manually distributing keys to\neach machine.\n\nFor ordinary account sync, credentials are encrypted on the node before upload.\nThe control plane stores ciphertext and wrapped-key metadata; enrolled nodes\nshare access by wrapping the vault key to one another. Bivy Cloud does not\nreceive plaintext credentials through this sync path.\n\nYou can also keep credentials local, use labeled keys and project presets, or\nreference environment variables and 1Password instead of embedding secrets in\nconfiguration:\n\n```bash\nbivy provider login\nbivy credentials add anthropic work\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\n```\n\n**Not every CLI login syncs.** Native agent logins may still be per-machine;\nGitHub App private-key sync is separately opt-in. If you lose every node and\ndevice able to unwrap a vault, you must sign in to providers again. Explicit\nhosted-provisioning custody grants are separate from ordinary encrypted sync.\n\n[Credential sync and runtime coverage →](docs/credential-sync.md) ·\n[Credentials guide →](docs/credentials-guide.md) ·\n[Key storage →](docs/key-management.md)\n\n### Work in the environment you already have\n\nA clean cloud sandbox isn't always enough. Your agent may need the database\nrunning on localhost, an uncommitted change, an internal API behind your VPN,\nor a model running on your GPU. Bivy runs the agent where those things already\nexist, subject to that machine's permissions and the runtime's protection.\n\nConnect several machines to the same account: a laptop for interactive work,\na Linux server for background jobs, or a GPU box for local inference. Choose\nthe machine for each session or pin it in an automation. Repository runs can\nuse isolated Git worktrees without rebuilding the whole development environment.\n\n**The execution machine must stay awake and online.** To close your laptop and\nleave work running, run the agent on a different, always-on machine.\n\n[Environment and multi-machine recipes →](docs/capability-recipes.md)\n\n### Start at your desk. Continue anywhere.\n\nOpen the same session in the browser, phone PWA, or terminal. Watch work live,\nanswer questions, approve supported tool calls, or stop the agent.\n\n- Send screenshots, images, logs, and other files from your phone.\n- Download reports and artifacts the agent creates.\n- Open [session apps](docs/apps.md) from chat or the **Apps** menu: live web\n previews, desktop app views, and CLI/TUI tools. Agents can publish views with\n `bivy app publish bivy.app.json` and create share links with `bivy app share`.\n- Use voice input and read-aloud where supported; provider-backed voice may\n send audio or text to the selected provider.\n- Keep a native terminal workflow or use structured chat, depending on the agent.\n\n```bash\nbivy run claude --no-follow # start without attaching\nbivy open # open the web app\nbivy resume # return to the session in your terminal\nbivy link # pair a device directly via QR\n```\n\nNo phone app installation is required. Open [app.bivy.sh](https://app.bivy.sh)\nin your browser; adding it to your home screen is optional.\n\n[Remote access →](docs/remote-access.md) ·\n[Voice, files, and terminal recipes →](docs/capability-recipes.md)\n\n### Agents can reach you, too\n\nEvery session gets the `bivy` CLI, so any agent with a shell can talk back\nthrough the app, not only agents with their own built-in tools:\n\n```bash\nbivy notify \"Tests are green, PR is up\" # chat card + phone push when you're away\nbivy ask \"Ship to staging?\" --option Yes --option No # waits for your answer\nbivy attach report.png --caption \"Before/after\" # show a file in the chat\nbivy suggest \"Add a dark theme to settings\" # a next task you start in one tap\nbivy context --json # session, workspace, machine, apps\n```\n\n`bivy help --json` lists every command, and with `BIVY_OUTPUT=json` failures\ncome back as structured errors with distinct exit codes, so an agent can tell\nwhat went wrong and what to run next. `bivy guide` prints short playbooks for\nagents, and the same commands are served as MCP tools for agents that prefer\nthem. Add your own standing instructions for\nevery session in **Settings → Agent instructions**.\n\n[Commands for agents inside a session →](docs/cli-reference.md#inside-an-agent-session) ·\n[Agent instructions →](docs/agent-instructions.md)\n\n## Get started\n\n### Install\n\nBivy supports **macOS and Linux with Node.js 20+**. The installer installs the\n`@bivy/bivy` package, runs guided setup, and starts a launchd or systemd service:\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nSetup helps you choose an agent and configure remote access. Existing agents\nkeep their command, login, and configuration. The installer may use `sudo` to\ninstall a missing Node.js, but never for `npm install`. To inspect it first,\ndownload it with `curl -fsSL https://bivy.sh/install.sh -o install.sh`.\n\nAlready have Node.js and want to avoid sudo?\n\n```bash\nnpm install -g @bivy/bivy\nbivy setup\n```\n\nThen try one small task:\n\n```bash\ncd your-repo\nbivy run claude\n# Ask: \"Explain this repo and make one small, safe improvement. Run the relevant checks.\"\nbivy open\n```\n\nOpen that same session on your phone while it runs. For a web app, ask the agent\nto start its dev server, then open **Apps → Running in this workspace → Preview**.\nTry a page, point to something you'd change, and send the feedback back to the\nagent. No separate preview deployment needed.\n\nOnce that works, share a preview for review, connect another machine, or add\nyour first automation.\n\n**Local-only works too.** `bivy run`, `bivy resume`, and `bivy sessions` need no\naccount or server. Choose **local only for now** during setup; use `bivy login`\nlater. Browser and phone access need a hosted or self-hosted control plane;\nthe node itself does not serve a web UI.\n\n[Full quickstart →](docs/quickstart.md) ·\n[Installer options, service management, and uninstall →](docs/install.md)\n\n### Choose hosted or self-hosted\n\n| Option | What you get |\n|---|---|\n| **Free Cloud — $0** | Every launch feature, including automations; 10 new remote sessions per rolling seven days. No credit card required. |\n| **Cloud — $15/month** | The same features with unlimited remote sessions. |\n| **Self-hosted Core** | Operate the app, control plane, and relay yourself, with no Bivy usage limits. |\n\nManual and automated sessions share the Cloud allowance. Resuming existing\nsessions and viewing history do not consume it. Agent subscriptions and model\nprovider charges are separate. See [current pricing](https://bivy.sh#pricing).\n\n**Self-host anywhere:** deploy the public control-plane (including the web app)\nand relay images with Postgres and [a small set of environment variables](docs/deploy-images.md).\nYour server or container platform handles HTTPS. Set up owner access in the\nbrowser—no SSH, external authentication provider, or Bivy Cloud account required.\nFor a bare VPS, the [Compose installer](docs/self-host-quickstart.md) automates the\nsame stack. These onboarding features require a release containing them.\n\nStart on Cloud and self-host later if you prefer. Deploy the stack, reconnect\nmachines with `bivy relay:setup`, and pair devices to your server. This is not a\none-click migration of your Cloud account; your local repos and agent\nconfiguration stay in place.\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nSelf-hosting is community-supported: you own TLS, backups, upgrades, and\nhardening. Public multi-architecture images are available as\n`ghcr.io/bivysh/bivy-control-plane` and `ghcr.io/bivysh/bivy-relay`; pin a release\nversion or full commit SHA.\n\n[Deploy the images anywhere →](docs/deploy-images.md) ·\n[Optional VPS installer →](docs/self-host-quickstart.md) ·\n[Operations reference →](docs/self-host.md)\n\n## Architecture\n\nYour environment, with clear security boundaries:\n\n```text\nYour machine Hosted or self-hosted\n┌──────────────────────┐ ┌──────────────────────┐\n│ Node daemon │──outbound──▶│ Relay │\n│ Agents, repos, tools │ │ Encrypted frames │\n│ Local credentials │ └──────────┬───────────┘\n└──────────────────────┘ │\n Browser / phone\n + control plane\n (app, accounts, metadata)\n```\n\n- **Execution stays on your machine.** Bivy Cloud does not run your agents.\n Your model provider still sees whatever the agent sends it.\n- **Interactive session traffic is end-to-end encrypted** between the node and\n paired devices. The relay forwards opaque session frames; your node dials out,\n so no inbound public port is required.\n- **App previews have a separate security boundary.** Preview traffic uses HTTPS\n and an outbound tunnel, not session E2E encryption. The preview ingress/relay\n operator can see it. Public preview links grant anyone holding them access to\n that view and its live backend until expiry or revocation. See\n [preview security and sharing](docs/apps.md#runtime-and-security-boundaries).\n- **Ordinary credential sync uploads ciphertext, not plaintext keys.**\n Supported credentials and recovery limits are documented separately.\n- **Encryption is not universal across integrations.** Slack commands and\n generic webhook instructions reach the control plane in plaintext. Do not\n put secrets in them. Routing and bounded run metadata are also visible there.\n- **Device authorization matters.** QR pairing authorizes a device directly\n through the node. Hosted account pairing trusts the control plane to authorize\n devices and serve the web app that holds client keys.\n- **Bivy is not an OS-level sandbox.** The default approval mode is\n `autonomous`; protection depends on the runtime. Some agents enforce sandbox\n tiers, while process agents may run with your full user permissions.\n Heuristic tool checks help prevent accidents but are not isolation.\n\nReview the runtime's Protection label and configure approval/sandbox settings\nfor the task, especially before enabling unattended work.\n\n[Security model and known limitations →](docs/security-model.md) ·\n[Runtime protection matrix →](docs/runtime-support-matrix.md) ·\n[Configuration →](docs/configuration.md)\n\n## Agents and everyday commands\n\n**Claude Code, Codex, Pi, OpenCode, and Grok are release-tested.** Additional\nadapters include Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub\nCopilot, Amp, Auggie, Droid, Continue, Kilo Code, and Rovo Dev. Installation,\nresume, model selection, and tool protection vary—see the\n[support matrix](docs/runtime-support-matrix.md) and [agent guides](docs/agents/README.md).\n\nRun an arbitrary command with `bivy run -- ./your-agent --flags`, register a\nreusable entry with `bivy agent add`, or package a declarative integration with\nexperimental [plugins](docs/plugins.md).\n\n```bash\nbivy run claude # launch a durable session; also codex, pi, opencode\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires remote setup)\nbivy nodes # list connected account machines\nbivy runs list # inspect delegated work\nbivy automation init # scaffold repo-owned automations\nbivy provider login # connect supported model credentials\nbivy agent add # register an ACP or process agent\nbivy doctor # check installation and connectivity\nbivy logs -f # follow node logs\nbivy update # update and restart the service\n```\n\n`bivy update` uses your original installation method and waits for an active\nturn to finish before restarting. Use `--force` to skip that wait.\n\n[CLI reference →](docs/cli-reference.md) ·\n[Node and project configuration →](docs/config-as-code.md) ·\n[GitHub setup →](docs/github-setup.md) ·\n[Linear setup →](docs/linear-work-queue.md)\n\n## Development and contributions\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server\n```\n\n| Directory | Contents |\n|---|---|\n| `src/`, `bin/` | Node daemon, CLI, runtime adapters, sessions, approvals, secrets |\n| `packages/core/` | Shared protocol, pairing, and wire format |\n| `packages/web/`, `packages/ui/` | React PWA and shared design system |\n| `services/relay/` | Self-hostable encrypted relay |\n| `services/control-plane/` | Self-hostable control plane |\n| `deploy/` | Deployment examples |\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow. Releases\nare published from CI with provenance attestations; see\n[release verification](docs/releasing.md).\n\n**Found a security issue?** Use\n[GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new),\nnot a public issue. See [SECURITY.md](SECURITY.md).\n\n### In development—not available at launch\n\nAutomatically provisioned, short-lived **ephemeral machines** are in development\nfor hosted and bring-your-own-cloud deployments. Neither path is ready or\nsupported for this launch. Use an existing computer or server you operate.\nExperimental provisioning has different credential-custody and encryption\nboundaries; see the [provisioning trust model](docs/hosted-provisioning-trust-model.md).\n\n## License\n\nEverything in this repository—node, CLI, web/PWA, relay, and control plane—is\nfree and open-source **AGPL-3.0-only Core**, with no Bivy usage limits. You may\nuse, modify, and self-host it under that license. If users interact with your\nmodified version over a network, section 13 requires you to offer its\ncorresponding source. See [LICENSE](LICENSE).\n\nBivy Cloud is the hosted operation of that stack plus billing and plans, in a\nseparate private repository. Contributions use the\n[DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
|
|
68
|
+
"readme": "# Bivy\n\n[](https://www.npmjs.com/package/@bivy/bivy)\n[](LICENSE)\n[](https://nodejs.org)\n\n**Local coding agents that show their work. Anywhere.**\n\nBivy is the open-source workspace for coding agents. It runs Claude Code, Codex,\nPi, OpenCode and other agents on your own machine. Reach them from a browser or\nyour phone, with the live session, its terminal, its approvals, and the app\nthey're building right in the chat. Try it, mark what's wrong, and send it back.\n\n<p align=\"center\">\n <img src=\"docs/images/preview-feedback-loop.gif\" width=\"360\"\n 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.\">\n</p>\n\nBivy is not another coding agent and not a cloud development machine. The agent\nstays local and does the coding with your model provider. Your repos, tools,\ndatabases, and services stay where they are. Bivy gives you remote access to all\nof it, plus live previews, automations, and review.\n\n- **Live previews.** The running app beside the chat. Mark it, send it back,\n share it with someone who has no Bivy account.\n- **Automations.** GitHub issues, failed CI, Linear, Slack, schedules, and\n webhooks start the work. It comes back as a preview link or a pull request.\n- **Any agent.** Hit a usage limit? Fork the session to another agent, or let\n Bivy retry when the limit resets.\n- **Your machine, anywhere.** Start at your desk, pick the session up on your\n phone. The machine dials out, so there are no ports to open, and session\n traffic is end-to-end encrypted.\n\n**[Start free on Bivy Cloud](https://app.bivy.sh)** ·\n**[Quickstart](docs/quickstart.md)** ·\n**[Documentation](docs/README.md)** ·\n**[Self-host](docs/deploy-images.md)** ·\n**[Website](https://bivy.sh)**\n\n**Recommended:** sign in at [app.bivy.sh](https://app.bivy.sh), then copy your\npersonalized **Connect a Machine** command into a terminal on your Mac or Linux\ncomputer. It installs and enrolls the machine without another Bivy login. The\nbrowser connects automatically; choose a repository and send your first task\nright there.\n\nPrefer starting from the terminal?\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # or codex, pi, opencode\nbivy open # continue in the web app (needs remote setup)\n```\n\nBivy Cloud hosts the app, control plane, and relay—not the machines running your\nagents. Connect a Mac, Linux computer, or existing server and bring your own\nagent subscription, model API key, or local model. You can also self-host the\nentire remote-access stack.\n\n> **Bivy is 0.x software.** Claude Code, Codex, Pi, OpenCode, and Grok are the\n> release-tested paths. Credential sync, resume, handoffs, approvals, and\n> sandboxing depend on the runtime. See the\n> [runtime support matrix](docs/runtime-support-matrix.md).\n\n## Don't just read the diff. Try the app.\n\nBivy lets you **use what the agent built and show it what to fix**, without a\nseparate deployment step.\n\n1. **Ask for a change.** The agent works in your repo with your existing tools\n and local services.\n2. **Try the app.** Bivy finds the dev server the agent starts and opens it\n beside the chat: web apps, terminals, and desktop apps. Use the running app,\n not a screenshot of what the agent says it finished.\n3. **Mark what needs work.** Press and hold anything in the preview to mark it,\n or drag to circle it, then say what you want. **Mark another** keeps\n separate notes separate. The marks go to the agent with the element and page\n context. No screenshots to paste.\n4. **Follow each note.** Every mark stays in the chat as a pin that resolves\n itself: **Changed** when a later run changes what you marked, **Element\n gone** when it leaves the page, **Done** when you say so.\n5. **Review the next version.** A new version waits on the preview pill instead\n of reloading under you. Take it when you're ready, compare before and after,\n inspect the diff and checks, and decide when the work is done.\n6. **Get a second opinion.** Share a preview link for 1 hour, 1 day, or 7 days.\n Teammates and clients try the running app and leave notes without a Bivy\n account. Make it view-only, or **Stop sharing** to end every link at once.\n\nOn Bivy Cloud, preview delivery is built in: no per-app domains, certificates,\npublic ports, or tunnels to configure. Self-hosters configure preview delivery\nonce for their Bivy deployment, not for every app. The app still needs its normal\nbuild or dev-server setup, and **the machine serving it must stay awake and\nonline**. These are development previews, not production hosting.\n\n**Share deliberately:** anyone with a preview link can use that app, including\nits live backend, until the link expires or you stop sharing.\nReviewer notes aren't sent to the agent automatically; you send them or explicitly\nallow agent access. Preview traffic uses HTTPS through the preview relay, not\nsession end-to-end encryption; the relay operator can see it.\n\n[App previews, visual feedback, and sharing →](docs/apps.md)\n\n## One workflow, from trigger to review\n\n```text\nPrompt · GitHub issue · CI failure · Linear · Slack · Schedule · Webhook\n │\n ▼\n Choose machine + agent + model\n + supported credentials\n │\n ▼\n Live agent session\n Join · steer · approve · stop\n │\n ▼\n Try app · inspect changes · checks\n │\n ▼\n Mark up · send feedback · iterate\n │\n ▼\n Share preview · review PR\n```\n\nA **Machine** is a computer or server you connect. A **Session** is live agent\nwork on that machine. A **Run** is delegated background work that creates a\nsession and tracks its outcome. An **Automation** is a reusable definition that\ncreates runs when an event matches.\n\nManual and automated work use the same kind of live session. You can join a run\nwhen it needs help rather than wait for a black-box job to finish.\n\n### Let events start the work\n\nAutomations turn recurring or incoming work into sessions you can join,\nsupervise, and review. An agent picks the task up on your machine and posts\nback a preview link or a pull request, so you try the result where the task\nlives and reply in the same session. Choose the repository, machine, agent,\nmodel, approval mode, sandbox setting, and maximum attempts.\n\n| Trigger | Example workflow |\n|---|---|\n| **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |\n| **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |\n| **Linear** | Label an issue to start work without copying its description into an agent. |\n| **Slack** | Send a request from the conversation where the work came up. |\n| **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |\n| **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |\n\nConfigure automations in the app or version them with your repository in\n`.bivy/automations.yaml`:\n\n```bash\nbivy automation init\n# Edit the generated definition for your repository and workflow.\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture\nbivy automation apply\n```\n\nOr delegate a one-off job without creating an automation:\n\n```bash\nbivy runs start \"Review outdated dependencies and propose a small, tested update.\"\nbivy runs wait <id>\n```\n\nRuns keep routing and lifecycle evidence, check results, and output references\nin a reviewable Receipt. For unattended issue work, Bivy runs declared repository\nchecks after the agent's turn; failed required checks fail the run even if the\nagent reports success. A completed process alone is not proof that the task\nsucceeded.\n\n[Automation recipes →](docs/capability-recipes.md#let-events-start-runs) ·\n[Automations as code →](docs/automations-as-code.md) ·\n[Run outcomes and reliability limits →](docs/automation-runs.md)\n\n### Use every agent you pay for. Switch mid-task.\n\nRun Claude Code for one task, Codex for another, and Pi or OpenCode where they\nfit, side by side in one session list. Bivy supplies the shared session,\nremote-access, automation, and review surfaces; your chosen agent still does\nthe coding and uses your model provider.\n\n- **Hit a usage limit? Keep going.** When a turn fails on a limit, fork the\n session, with its code and conversation, to another agent and model, or let\n Bivy retry when the limit resets (the provider has to report a reset time).\n- **Get a second opinion.** `bivy delegate` hands a task to another agent, on\n the same machine or a different one, and brings back its answer, branch, and\n PR. `--to codex,claude@linux` sends one task to several agents to compare.\n- Choose an agent and, where supported, a model for each session or run.\n- Import existing Claude Code and Codex sessions.\n- Fork or move work to another agent or machine when a different setup fits\n better. Continuation fidelity varies: some paths preserve native history,\n while others replay portable turns or hand over a summary with the earlier\n transcript as a local file.\n- Use agent-native logins, Bivy-managed credentials, or local inference.\n Bivy's custom OpenAI-compatible endpoint registry currently feeds Pi;\n other agents may need their own provider configuration.\n- Register your own ACP or headless process agent with `bivy agent add`.\n\nBivy does not replace your agent, provide model inference, or make every agent's\nfeatures identical. Consult the [support matrix](docs/runtime-support-matrix.md)\nand [handoff recipes](docs/capability-recipes.md#fork-or-move-a-session).\n\n### Less signing in. Less copying secrets.\n\nBivy syncs **Bivy-managed API keys and supported OAuth credentials** across\nenrolled machines for compatible runtimes. Connect supported credentials once\nand reuse them where you run work, rather than manually distributing keys to\neach machine.\n\nFor ordinary account sync, credentials are encrypted on the node before upload.\nThe control plane stores ciphertext and wrapped-key metadata; enrolled nodes\nshare access by wrapping the vault key to one another. Bivy Cloud does not\nreceive plaintext credentials through this sync path.\n\nYou can also keep credentials local, use labeled keys and project presets, or\nreference environment variables and 1Password instead of embedding secrets in\nconfiguration:\n\n```bash\nbivy provider login\nbivy credentials add anthropic work\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\n```\n\n**Not every CLI login syncs.** Native agent logins may still be per-machine;\nGitHub App private-key sync is separately opt-in. If you lose every node and\ndevice able to unwrap a vault, you must sign in to providers again. Explicit\nhosted-provisioning custody grants are separate from ordinary encrypted sync.\n\n[Credential sync and runtime coverage →](docs/credential-sync.md) ·\n[Credentials guide →](docs/credentials-guide.md) ·\n[Key storage →](docs/key-management.md)\n\n### Work in the environment you already have\n\nA clean cloud sandbox isn't always enough. Your agent may need the database\nrunning on localhost, an uncommitted change, an internal API behind your VPN,\nor a model running on your GPU. Bivy runs the agent where those things already\nexist, subject to that machine's permissions and the runtime's protection.\n\nConnect several machines to the same account: a laptop for interactive work,\na Linux server for background jobs, or a GPU box for local inference. Choose\nthe machine for each session or pin it in an automation. Repository runs can\nuse isolated Git worktrees without rebuilding the whole development environment.\n\n**The execution machine must stay awake and online.** To close your laptop and\nleave work running, run the agent on a different, always-on machine.\n\n[Environment and multi-machine recipes →](docs/capability-recipes.md)\n\n### Start at your desk. Continue anywhere.\n\nOpen the same session in the browser, phone PWA, or terminal. Watch work live,\nanswer questions, approve supported tool calls, or stop the agent.\n\n- Send screenshots, images, logs, and other files from your phone.\n- Download reports and artifacts the agent creates.\n- Open [session apps](docs/apps.md) from chat or the **Apps** menu: live web\n previews, desktop app views, and CLI/TUI tools. Agents can publish views with\n `bivy app publish bivy.app.json` and create share links with `bivy app share`.\n- Use voice input and read-aloud where supported; provider-backed voice may\n send audio or text to the selected provider.\n- Keep a native terminal workflow or use structured chat, depending on the agent.\n\n```bash\nbivy run claude --no-follow # start without attaching\nbivy open # open the web app\nbivy resume # return to the session in your terminal\nbivy link # pair a device directly via QR\n```\n\nNo phone app installation is required. Open [app.bivy.sh](https://app.bivy.sh)\nin your browser; adding it to your home screen is optional.\n\n[Remote access →](docs/remote-access.md) ·\n[Voice, files, and terminal recipes →](docs/capability-recipes.md)\n\n### Agents can reach you, too\n\nEvery session gets the `bivy` CLI, so any agent with a shell can talk back\nthrough the app, not only agents with their own built-in tools:\n\n```bash\nbivy notify \"Tests are green, PR is up\" # chat card + phone push when you're away\nbivy ask \"Ship to staging?\" --option Yes --option No # waits for your answer\nbivy attach report.png --caption \"Before/after\" # show a file in the chat\nbivy suggest \"Add a dark theme to settings\" # a next task you start in one tap\nbivy context --json # session, workspace, machine, apps\n```\n\n`bivy help --json` lists every command, and with `BIVY_OUTPUT=json` failures\ncome back as structured errors with distinct exit codes, so an agent can tell\nwhat went wrong and what to run next. `bivy guide` prints short playbooks for\nagents, and the same commands are served as MCP tools for agents that prefer\nthem. Add your own standing instructions for\nevery session in **Settings → Agent instructions**.\n\n[Commands for agents inside a session →](docs/cli-reference.md#inside-an-agent-session) ·\n[Agent instructions →](docs/agent-instructions.md)\n\n## Get started\n\n### Install\n\nBivy supports **macOS and Linux with Node.js 20+**. The installer installs the\n`@bivy/bivy` package, runs guided setup, and starts a launchd or systemd service:\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nSetup helps you choose an agent and configure remote access. Existing agents\nkeep their command, login, and configuration. The installer may use `sudo` to\ninstall a missing Node.js, but never for `npm install`. To inspect it first,\ndownload it with `curl -fsSL https://bivy.sh/install.sh -o install.sh`.\n\nAlready have Node.js and want to avoid sudo?\n\n```bash\nnpm install -g @bivy/bivy\nbivy setup\n```\n\nThen try one small task:\n\n```bash\ncd your-repo\nbivy run claude\n# Ask: \"Explain this repo and make one small, safe improvement. Run the relevant checks.\"\nbivy open\n```\n\nOpen that same session on your phone while it runs. For a web app, ask the agent\nto start its dev server, then open **Apps → Running in this workspace → Preview**.\nTry a page, point to something you'd change, and send the feedback back to the\nagent. No separate preview deployment needed.\n\nOnce that works, share a preview for review, connect another machine, or add\nyour first automation.\n\n**Local-only works too.** `bivy run`, `bivy resume`, and `bivy sessions` need no\naccount or server. Choose **local only for now** during setup; use `bivy login`\nlater. Browser and phone access need a hosted or self-hosted control plane;\nthe node itself does not serve a web UI.\n\n[Full quickstart →](docs/quickstart.md) ·\n[Installer options, service management, and uninstall →](docs/install.md)\n\n### Choose hosted or self-hosted\n\n| Option | What you get |\n|---|---|\n| **Free Cloud — $0** | Every launch feature, including automations; 10 new remote sessions per rolling seven days. No credit card required. |\n| **Cloud — $15/month** | The same features with unlimited remote sessions. |\n| **Self-hosted Core** | Operate the app, control plane, and relay yourself, with no Bivy usage limits. |\n\nManual and automated sessions share the Cloud allowance. Resuming existing\nsessions and viewing history do not consume it. Agent subscriptions and model\nprovider charges are separate. See [current pricing](https://bivy.sh#pricing).\n\n**Self-host anywhere:** deploy the public control-plane (including the web app)\nand relay images with Postgres and [a small set of environment variables](docs/deploy-images.md).\nYour server or container platform handles HTTPS. Set up owner access in the\nbrowser—no SSH, external authentication provider, or Bivy Cloud account required.\nFor a bare VPS, the [Compose installer](docs/self-host-quickstart.md) automates the\nsame stack. These onboarding features require a release containing them.\n\nStart on Cloud and self-host later if you prefer. Deploy the stack, reconnect\nmachines with `bivy relay:setup`, and pair devices to your server. This is not a\none-click migration of your Cloud account; your local repos and agent\nconfiguration stay in place.\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nSelf-hosting is community-supported: you own TLS, backups, upgrades, and\nhardening. Public multi-architecture images are available as\n`ghcr.io/bivysh/bivy-control-plane` and `ghcr.io/bivysh/bivy-relay`; pin a release\nversion or full commit SHA.\n\n[Deploy the images anywhere →](docs/deploy-images.md) ·\n[Optional VPS installer →](docs/self-host-quickstart.md) ·\n[Operations reference →](docs/self-host.md)\n\n## Architecture\n\nYour environment, with clear security boundaries:\n\n```text\nYour machine Hosted or self-hosted\n┌──────────────────────┐ ┌──────────────────────┐\n│ Node daemon │──outbound──▶│ Relay │\n│ Agents, repos, tools │ │ Encrypted frames │\n│ Local credentials │ └──────────┬───────────┘\n└──────────────────────┘ │\n Browser / phone\n + control plane\n (app, accounts, metadata)\n```\n\n- **Execution stays on your machine.** Bivy Cloud does not run your agents.\n Your model provider still sees whatever the agent sends it.\n- **Interactive session traffic is end-to-end encrypted** between the node and\n paired devices. The relay forwards opaque session frames; your node dials out,\n so no inbound public port is required.\n- **App previews have a separate security boundary.** Preview traffic uses HTTPS\n and an outbound tunnel, not session E2E encryption. The preview ingress/relay\n operator can see it. Public preview links grant anyone holding them access to\n that view and its live backend until expiry or revocation. See\n [preview security and sharing](docs/apps.md#runtime-and-security-boundaries).\n- **Ordinary credential sync uploads ciphertext, not plaintext keys.**\n Supported credentials and recovery limits are documented separately.\n- **Encryption is not universal across integrations.** Slack commands and\n generic webhook instructions reach the control plane in plaintext. Do not\n put secrets in them. Routing and bounded run metadata are also visible there.\n- **Device authorization matters.** QR pairing authorizes a device directly\n through the node. Hosted account pairing trusts the control plane to authorize\n devices and serve the web app that holds client keys.\n- **Bivy is not an OS-level sandbox.** The default approval mode is\n `autonomous`; protection depends on the runtime. Some agents enforce sandbox\n tiers, while process agents may run with your full user permissions.\n Heuristic tool checks help prevent accidents but are not isolation.\n\nReview the runtime's Protection label and configure approval/sandbox settings\nfor the task, especially before enabling unattended work.\n\n[Security model and known limitations →](docs/security-model.md) ·\n[Runtime protection matrix →](docs/runtime-support-matrix.md) ·\n[Configuration →](docs/configuration.md)\n\n## Agents and everyday commands\n\n**Claude Code, Codex, Pi, OpenCode, and Grok are release-tested.** Additional\nadapters include Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub\nCopilot, Amp, Auggie, Droid, Continue, Kilo Code, and Rovo Dev. Installation,\nresume, model selection, and tool protection vary—see the\n[support matrix](docs/runtime-support-matrix.md) and [agent guides](docs/agents/README.md).\n\nRun an arbitrary command with `bivy run -- ./your-agent --flags`, register a\nreusable entry with `bivy agent add`, or package a declarative integration with\nexperimental [plugins](docs/plugins.md).\n\n```bash\nbivy run claude # launch a durable session; also codex, pi, opencode\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires remote setup)\nbivy nodes # list connected account machines\nbivy runs list # inspect delegated work\nbivy automation init # scaffold repo-owned automations\nbivy provider login # connect supported model credentials\nbivy agent add # register an ACP or process agent\nbivy doctor # check installation and connectivity\nbivy logs -f # follow node logs\nbivy update # update and restart the service\n```\n\n`bivy update` uses your original installation method and waits for an active\nturn to finish before restarting. Use `--force` to skip that wait.\n\n[CLI reference →](docs/cli-reference.md) ·\n[Node and project configuration →](docs/config-as-code.md) ·\n[GitHub setup →](docs/github-setup.md) ·\n[Linear setup →](docs/linear-work-queue.md)\n\n## Development and contributions\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server\n```\n\n| Directory | Contents |\n|---|---|\n| `src/`, `bin/` | Node daemon, CLI, runtime adapters, sessions, approvals, secrets |\n| `packages/core/` | Shared protocol, pairing, and wire format |\n| `packages/web/`, `packages/ui/` | React PWA and shared design system |\n| `services/relay/` | Self-hostable encrypted relay |\n| `services/control-plane/` | Self-hostable control plane |\n| `deploy/` | Deployment examples |\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow. Releases\nare published from CI with provenance attestations; see\n[release verification](docs/releasing.md).\n\n**Found a security issue?** Use\n[GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new),\nnot a public issue. See [SECURITY.md](SECURITY.md).\n\n### In development—not available at launch\n\nAutomatically provisioned, short-lived **ephemeral machines** are in development\nfor hosted and bring-your-own-cloud deployments. Neither path is ready or\nsupported for this launch. Use an existing computer or server you operate.\nExperimental provisioning has different credential-custody and encryption\nboundaries; see the [provisioning trust model](docs/hosted-provisioning-trust-model.md).\n\n## License\n\nEverything in this repository—node, CLI, web/PWA, relay, and control plane—is\nfree and open-source **AGPL-3.0-only Core**, with no Bivy usage limits. You may\nuse, modify, and self-host it under that license. If users interact with your\nmodified version over a network, section 13 requires you to offer its\ncorresponding source. See [LICENSE](LICENSE).\n\nBivy Cloud is the hosted operation of that stack plus billing and plans, in a\nseparate private repository. Contributions use the\n[DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
|
|
69
69
|
"readmeFilename": "README.md",
|
|
70
70
|
"agentBridges": {
|
|
71
71
|
"@anthropic-ai/claude-agent-sdk": "0.3.285",
|