@bivy/bivy 0.15.0-staging.4 → 0.15.0-staging.41
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 +23 -9
- package/bin/agent-manifest.json +18 -18
- package/bin/bivy.mjs +39 -8
- package/dist/agents/pi/capabilities.js +42 -0
- package/dist/agents/pi/integration.js +19 -15
- package/dist/agents/pi/runtime.js +4 -23
- package/dist/agents/profiles.js +16 -15
- package/dist/certification/index.js +7 -6
- package/dist/linear-tasks.js +3 -3
- package/dist/metadata.js +8 -1
- package/dist/runtime/auth-errors.js +9 -0
- package/dist/runtime/index.js +3 -3
- package/dist/server.js +72 -24
- package/dist/session/session-contract-values.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -117,10 +117,16 @@ curl -fsSL https://bivy.sh/install.sh | bash
|
|
|
117
117
|
|
|
118
118
|
macOS and Linux. Requires Node.js 20 or newer. The installer puts the
|
|
119
119
|
[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) npm package and the
|
|
120
|
-
`bivy` command on your `PATH` with optional bridges skipped for speed, then runs
|
|
121
|
-
choice, selected-agent install if needed, remote access,
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
`bivy` command on your `PATH` with optional bridges skipped for speed, then runs
|
|
121
|
+
`bivy setup` — agent choice, selected-agent install if needed, remote access,
|
|
122
|
+
and an auto-start background service (launchd on macOS, systemd on Linux).
|
|
123
|
+
|
|
124
|
+
If Claude Code, Codex, OpenCode, Gemini, or another agent is already installed,
|
|
125
|
+
setup says so and uses that existing CLI, login, and configuration. Bivy does
|
|
126
|
+
not replace the agent or copy its native credentials; it adds remote continuity
|
|
127
|
+
and governance around the agent you already use. Re-running the installer on a
|
|
128
|
+
machine that already has Bivy just applies the latest build and restarts the
|
|
129
|
+
service.
|
|
124
130
|
|
|
125
131
|
**What needs an account, and what doesn't.** The CLI alone — `bivy run`,
|
|
126
132
|
`bivy resume`, `bivy sessions` — needs no account and no server; `bivy setup`
|
|
@@ -131,11 +137,19 @@ UI needs a control plane, because the node hosts none: use the hosted one at
|
|
|
131
137
|
[self-host your own](docs/self-host-quickstart.md). Switch any time with
|
|
132
138
|
`bivy relay:setup`.
|
|
133
139
|
|
|
140
|
+
Prefer to inspect the installer first?
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
curl -fsSL https://bivy.sh/install.sh -o install.sh
|
|
144
|
+
less install.sh
|
|
145
|
+
bash install.sh
|
|
146
|
+
```
|
|
147
|
+
|
|
134
148
|
**What the installer does with sudo.** It escalates only when it must, and
|
|
135
149
|
tells you when it does:
|
|
136
150
|
|
|
137
|
-
- Debian/Ubuntu without a suitable Node.js: `sudo apt-get install
|
|
138
|
-
|
|
151
|
+
- Debian/Ubuntu without a suitable Node.js: `sudo apt-get install curl
|
|
152
|
+
ca-certificates`, then NodeSource's Node 22 setup script via `sudo`.
|
|
139
153
|
- Other Linux, or macOS, without a suitable Node.js: downloads the official
|
|
140
154
|
Node 22 tarball from nodejs.org (sha256-checked) and installs it under
|
|
141
155
|
`/usr/local` with `sudo`.
|
|
@@ -267,9 +281,9 @@ See [`docs/remote-access.md`](docs/remote-access.md) and
|
|
|
267
281
|
|
|
268
282
|
## Supported agents
|
|
269
283
|
|
|
270
|
-
**
|
|
271
|
-
|
|
272
|
-
vary by runtime.
|
|
284
|
+
**Bivy maintains wrappers for the popular coding agents below.** Claude Code,
|
|
285
|
+
Codex, Pi, and OpenCode have the richest release-tested capability sets today;
|
|
286
|
+
capabilities and fidelity still vary by runtime.
|
|
273
287
|
|
|
274
288
|
| Agent | Command | Notes |
|
|
275
289
|
|---|---|---|
|
package/bin/agent-manifest.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"command": "codex",
|
|
7
7
|
"hidden": true,
|
|
8
8
|
"supportTier": "supported",
|
|
9
|
-
"certification": "
|
|
9
|
+
"certification": "adapter-tested",
|
|
10
10
|
"headlessFlags": [
|
|
11
11
|
"exec"
|
|
12
12
|
],
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"label": "Aider",
|
|
38
38
|
"command": "aider",
|
|
39
39
|
"hidden": false,
|
|
40
|
-
"supportTier": "
|
|
40
|
+
"supportTier": "supported",
|
|
41
41
|
"certification": "adapter-tested",
|
|
42
42
|
"headlessFlags": [
|
|
43
43
|
"--yes-always",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"label": "Hermes",
|
|
54
54
|
"command": "hermes",
|
|
55
55
|
"hidden": true,
|
|
56
|
-
"supportTier": "
|
|
57
|
-
"certification": "
|
|
56
|
+
"supportTier": "experimental",
|
|
57
|
+
"certification": "unverified",
|
|
58
58
|
"headlessFlags": [],
|
|
59
59
|
"install": {
|
|
60
60
|
"kind": "npm",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"label": "Goose",
|
|
67
67
|
"command": "goose",
|
|
68
68
|
"hidden": false,
|
|
69
|
-
"supportTier": "
|
|
69
|
+
"supportTier": "supported",
|
|
70
70
|
"certification": "adapter-tested",
|
|
71
71
|
"headlessFlags": [
|
|
72
72
|
"run",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"label": "Gemini CLI",
|
|
88
88
|
"command": "gemini",
|
|
89
89
|
"hidden": false,
|
|
90
|
-
"supportTier": "
|
|
90
|
+
"supportTier": "supported",
|
|
91
91
|
"certification": "adapter-tested",
|
|
92
92
|
"headlessFlags": [
|
|
93
93
|
"-p",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"label": "Qwen Code",
|
|
106
106
|
"command": "qwen",
|
|
107
107
|
"hidden": false,
|
|
108
|
-
"supportTier": "
|
|
108
|
+
"supportTier": "supported",
|
|
109
109
|
"certification": "adapter-tested",
|
|
110
110
|
"headlessFlags": [
|
|
111
111
|
"-p",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"label": "Cline",
|
|
124
124
|
"command": "cline",
|
|
125
125
|
"hidden": false,
|
|
126
|
-
"supportTier": "
|
|
126
|
+
"supportTier": "supported",
|
|
127
127
|
"certification": "adapter-tested",
|
|
128
128
|
"headlessFlags": [
|
|
129
129
|
"-y",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"label": "Crush",
|
|
140
140
|
"command": "crush",
|
|
141
141
|
"hidden": false,
|
|
142
|
-
"supportTier": "
|
|
142
|
+
"supportTier": "supported",
|
|
143
143
|
"certification": "adapter-tested",
|
|
144
144
|
"headlessFlags": [
|
|
145
145
|
"run",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"label": "Cursor",
|
|
156
156
|
"command": "cursor-agent",
|
|
157
157
|
"hidden": false,
|
|
158
|
-
"supportTier": "
|
|
158
|
+
"supportTier": "supported",
|
|
159
159
|
"certification": "adapter-tested",
|
|
160
160
|
"headlessFlags": [
|
|
161
161
|
"--force",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"label": "GitHub Copilot",
|
|
173
173
|
"command": "copilot",
|
|
174
174
|
"hidden": false,
|
|
175
|
-
"supportTier": "
|
|
175
|
+
"supportTier": "supported",
|
|
176
176
|
"certification": "adapter-tested",
|
|
177
177
|
"headlessFlags": [
|
|
178
178
|
"--allow-all-tools",
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"label": "Grok",
|
|
189
189
|
"command": "grok",
|
|
190
190
|
"hidden": false,
|
|
191
|
-
"supportTier": "
|
|
191
|
+
"supportTier": "supported",
|
|
192
192
|
"certification": "adapter-tested",
|
|
193
193
|
"headlessFlags": [
|
|
194
194
|
"-p",
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"label": "Amp",
|
|
206
206
|
"command": "amp",
|
|
207
207
|
"hidden": false,
|
|
208
|
-
"supportTier": "
|
|
208
|
+
"supportTier": "supported",
|
|
209
209
|
"certification": "adapter-tested",
|
|
210
210
|
"headlessFlags": [
|
|
211
211
|
"-x",
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
"label": "Auggie",
|
|
223
223
|
"command": "auggie",
|
|
224
224
|
"hidden": false,
|
|
225
|
-
"supportTier": "
|
|
225
|
+
"supportTier": "supported",
|
|
226
226
|
"certification": "adapter-tested",
|
|
227
227
|
"headlessFlags": [
|
|
228
228
|
"--quiet",
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
"label": "Droid",
|
|
239
239
|
"command": "droid",
|
|
240
240
|
"hidden": false,
|
|
241
|
-
"supportTier": "
|
|
241
|
+
"supportTier": "supported",
|
|
242
242
|
"certification": "adapter-tested",
|
|
243
243
|
"headlessFlags": [
|
|
244
244
|
"exec",
|
|
@@ -256,7 +256,7 @@
|
|
|
256
256
|
"label": "Continue",
|
|
257
257
|
"command": "cn",
|
|
258
258
|
"hidden": false,
|
|
259
|
-
"supportTier": "
|
|
259
|
+
"supportTier": "supported",
|
|
260
260
|
"certification": "adapter-tested",
|
|
261
261
|
"headlessFlags": [
|
|
262
262
|
"--auto",
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
"label": "Kilo Code",
|
|
273
273
|
"command": "kilo",
|
|
274
274
|
"hidden": false,
|
|
275
|
-
"supportTier": "
|
|
275
|
+
"supportTier": "supported",
|
|
276
276
|
"certification": "adapter-tested",
|
|
277
277
|
"headlessFlags": [
|
|
278
278
|
"run",
|
|
@@ -289,7 +289,7 @@
|
|
|
289
289
|
"label": "Rovo Dev",
|
|
290
290
|
"command": "acli",
|
|
291
291
|
"hidden": false,
|
|
292
|
-
"supportTier": "
|
|
292
|
+
"supportTier": "supported",
|
|
293
293
|
"certification": "adapter-tested",
|
|
294
294
|
"headlessFlags": [
|
|
295
295
|
"rovodev",
|
package/bin/bivy.mjs
CHANGED
|
@@ -543,6 +543,11 @@ function commandExists(cmd) {
|
|
|
543
543
|
return runQuiet("sh", ["-lc", "command -v -- \"$1\" >/dev/null 2>&1", "sh", cmd]).code === 0;
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
function commandOnPath(cmd) {
|
|
547
|
+
const result = runQuiet("sh", ["-lc", "command -v -- \"$1\"", "sh", cmd]);
|
|
548
|
+
return result.code === 0 ? result.stdout.trim().split(/\r?\n/)[0] || "" : "";
|
|
549
|
+
}
|
|
550
|
+
|
|
546
551
|
function npmGlobalBinCommand(cmd) {
|
|
547
552
|
if (!commandExists("npm")) return "";
|
|
548
553
|
const prefix = runQuiet("npm", ["prefix", "-g"]);
|
|
@@ -552,9 +557,13 @@ function npmGlobalBinCommand(cmd) {
|
|
|
552
557
|
return fs.existsSync(candidate) ? candidate : "";
|
|
553
558
|
}
|
|
554
559
|
|
|
560
|
+
function nodeAtLeast(minMajor, minMinor = 0) {
|
|
561
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
562
|
+
return major > minMajor || (major === minMajor && minor >= minMinor);
|
|
563
|
+
}
|
|
564
|
+
|
|
555
565
|
function hasSupportedNode() {
|
|
556
|
-
|
|
557
|
-
return major >= 20;
|
|
566
|
+
return nodeAtLeast(20);
|
|
558
567
|
}
|
|
559
568
|
|
|
560
569
|
async function ensureDeps() {
|
|
@@ -612,8 +621,15 @@ function nodePackageInstalled(packageName) {
|
|
|
612
621
|
return runQuiet(nodeBin, ["-e", "require.resolve(process.argv[1], { paths: [process.argv[2]] })", packageName, repoRoot]).code === 0;
|
|
613
622
|
}
|
|
614
623
|
|
|
624
|
+
function nodePackageLoadable(packageName) {
|
|
625
|
+
return runQuiet(nodeBin, ["-e", "require(require.resolve(process.argv[1], { paths: [process.argv[2]] }))", packageName, repoRoot]).code === 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
615
628
|
async function ensureNodePackage(packageName) {
|
|
616
|
-
if (nodePackageInstalled(packageName))
|
|
629
|
+
if (nodePackageInstalled(packageName)) {
|
|
630
|
+
console.log(c.green(` ✓ Found Bivy bridge package ${packageName}`));
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
617
633
|
// Add with the package manager that owns this tree. Running `npm install` in a
|
|
618
634
|
// pnpm workspace would write a competing package-lock.json and a hoisted
|
|
619
635
|
// node_modules over pnpm's symlink layout, leaving the checkout in a state
|
|
@@ -626,7 +642,7 @@ async function ensureNodePackage(packageName) {
|
|
|
626
642
|
console.error(c.red(`${cmd} is required to install ${packageName}.`));
|
|
627
643
|
return false;
|
|
628
644
|
}
|
|
629
|
-
console.log(c.dim(`Installing ${packageName}…`));
|
|
645
|
+
console.log(c.dim(`Installing Bivy bridge package ${packageName}…`));
|
|
630
646
|
const code = await run(cmd, [...baseArgs, packageName], { cwd: repoRoot });
|
|
631
647
|
return code === 0 && nodePackageInstalled(packageName);
|
|
632
648
|
}
|
|
@@ -634,13 +650,18 @@ async function ensureNodePackage(packageName) {
|
|
|
634
650
|
const userLocalPrefix = process.env.BIVY_NPM_GLOBAL_PREFIX || path.join(os.homedir(), ".local");
|
|
635
651
|
|
|
636
652
|
async function ensureNpmCommand(command, packageName, label) {
|
|
637
|
-
|
|
653
|
+
const existing = commandOnPath(command);
|
|
654
|
+
if (existing) {
|
|
655
|
+
console.log(c.green(` ✓ Found existing ${label}: ${existing}`));
|
|
656
|
+
console.log(c.dim(` Bivy will use your installed ${label}; it will not replace its auth or configuration.`));
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
638
659
|
if (!commandExists("npm")) {
|
|
639
660
|
console.log(c.yellow(`Skipping ${label}: npm is not available.`));
|
|
640
661
|
return false;
|
|
641
662
|
}
|
|
642
663
|
fs.mkdirSync(path.join(userLocalPrefix, "bin"), { recursive: true });
|
|
643
|
-
console.log(c.dim(`Installing ${label} (${packageName})…`));
|
|
664
|
+
console.log(c.dim(`Installing ${label} (${packageName}) because it was not found on PATH…`));
|
|
644
665
|
const code = await run("npm", ["install", "--global", "--prefix", userLocalPrefix, packageName, "--no-audit", "--no-fund"]);
|
|
645
666
|
return code === 0 && commandExists(command);
|
|
646
667
|
}
|
|
@@ -4121,6 +4142,14 @@ async function cmdDoctor(args = []) {
|
|
|
4121
4142
|
|
|
4122
4143
|
console.log(c.bold("\n Bivy doctor\n"));
|
|
4123
4144
|
console.log(` ${mark(hasSupportedNode())} Node ${process.version}${hasSupportedNode() ? "" : c.dim(" (needs >= 20.0.0)")}`);
|
|
4145
|
+
const ptyInstalled = nodePackageInstalled("node-pty");
|
|
4146
|
+
const ptyUsable = ptyInstalled && nodePackageLoadable("node-pty");
|
|
4147
|
+
console.log(` ${mark(ptyUsable, true)} terminal PTY ${ptyUsable ? c.green("available") : ptyInstalled ? c.yellow("installed but not loadable — reinstall after installing build tools") : c.dim("optional dependency missing — interactive terminals unavailable; chat/exec still work")}`);
|
|
4148
|
+
const claudeBridge = nodePackageInstalled("@anthropic-ai/claude-agent-sdk");
|
|
4149
|
+
console.log(` ${mark(claudeBridge, true)} Claude bridge ${claudeBridge ? c.green("installed") : c.dim("optional — installed on first Claude setup/use")}`);
|
|
4150
|
+
const piBridge = nodePackageInstalled("@earendil-works/pi-coding-agent");
|
|
4151
|
+
const piNodeOk = nodeAtLeast(22, 19);
|
|
4152
|
+
console.log(` ${mark(piBridge && piNodeOk, true)} Pi bridge ${piBridge ? (piNodeOk ? c.green("installed") : c.yellow("installed but needs Node >=22.19")) : c.dim("optional — installed only if you choose Pi")}`);
|
|
4124
4153
|
console.log(` ${mark(commandExists("git"), true)} git${commandExists("git") ? "" : c.dim(" (recommended for repo-backed sessions)")}`);
|
|
4125
4154
|
// GitHub is optional (a "No repo" session needs none), so this only ever warns.
|
|
4126
4155
|
// `gh` is NOT required — it's a token fallback; the primary path is Bivy's own
|
|
@@ -5091,8 +5120,10 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
|
|
|
5091
5120
|
}
|
|
5092
5121
|
const remote = await openRemoteApp();
|
|
5093
5122
|
if (!remote) {
|
|
5094
|
-
console.log("
|
|
5095
|
-
console.log(
|
|
5123
|
+
console.log(c.yellow("Remote access is not configured on this machine yet."));
|
|
5124
|
+
console.log("The local node hosts only an API/WebSocket, not the Bivy PWA.");
|
|
5125
|
+
console.log(`Run ${c.cyan("bivy relay:setup")} to connect this node to the hosted or self-hosted remote web/PWA app, then run ${c.cyan("bivy open")} again.`);
|
|
5126
|
+
process.exitCode = 1;
|
|
5096
5127
|
} else if (!canOpenBrowser()) {
|
|
5097
5128
|
console.log(`Open the Bivy app here: ${c.cyan(remote.remoteBase)}`);
|
|
5098
5129
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { withExactCapabilitySurface } from "../../runtime/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The ONE capability table for Pi, shared by the real `PiRuntime` and the
|
|
6
|
+
* `LazyPiRuntime` facade the registry hands out before the SDK is loaded.
|
|
7
|
+
*
|
|
8
|
+
* This module deliberately imports nothing from the pi SDK so the lazy facade
|
|
9
|
+
* can use it without defeating its purpose. Keeping a single source of truth is
|
|
10
|
+
* a correctness requirement, not tidiness: the daemon reads flags such as
|
|
11
|
+
* `sessionRefIsPath` off whichever runtime object the registry returns — the
|
|
12
|
+
* facade — *before* any session is opened. When the facade's copy of this table
|
|
13
|
+
* drifted from the real one and lost `sessionRefIsPath`, the daemon treated every
|
|
14
|
+
* pi resume ref as an opaque id, stripped the transcript path to its basename,
|
|
15
|
+
* and pi created a brand-new empty session for every message sent to a closed
|
|
16
|
+
* chat (the "sending to a closed session creates a new one" regression).
|
|
17
|
+
*/
|
|
18
|
+
export const PI_CAPABILITIES = withExactCapabilitySurface({
|
|
19
|
+
toolInterception: true,
|
|
20
|
+
modelSelection: true,
|
|
21
|
+
packages: true,
|
|
22
|
+
resume: true,
|
|
23
|
+
fork: false,
|
|
24
|
+
// pi resumes by session-file path (under the node's sessions dir), so the
|
|
25
|
+
// daemon applies its path-traversal guard to these refs.
|
|
26
|
+
sessionRefIsPath: true,
|
|
27
|
+
// pi's TUI ships with the node, so the chat<->TUI hand-off is always offered.
|
|
28
|
+
interactiveTui: true,
|
|
29
|
+
usageReporting: true,
|
|
30
|
+
// pi can find a prior on-disk session by cwd + start time, so a `bivy run`
|
|
31
|
+
// terminal with no pinned sessionId can still be continued as a governed chat.
|
|
32
|
+
sessionDiscovery: true,
|
|
33
|
+
// pi transcripts are structured messages that round-trip through the session
|
|
34
|
+
// store, so a pi->pi fork is full fidelity (see exportForFork/importForFork).
|
|
35
|
+
forkTransport: true,
|
|
36
|
+
// pi can also stand up a session from portable {role,text} history, so a fork
|
|
37
|
+
// FROM another agent INTO pi is a true replay, not a seeded summary.
|
|
38
|
+
forkHistoryImport: true,
|
|
39
|
+
// The pi-coding-agent SDK implements both explicitly: prompting mid-turn
|
|
40
|
+
// with no streamingBehavior hint throws, forcing every caller to choose.
|
|
41
|
+
streamingBehaviors: ["steer", "followUp"],
|
|
42
|
+
});
|
|
@@ -4,19 +4,8 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { defineAgentIntegration } from "../definition.js";
|
|
7
|
-
import {
|
|
7
|
+
import { PI_CAPABILITIES } from "./capabilities.js";
|
|
8
8
|
export const PI_TESTED_VERSION = "0.84.3";
|
|
9
|
-
const PI_CAPABILITIES = withExactCapabilitySurface({
|
|
10
|
-
toolInterception: true,
|
|
11
|
-
modelSelection: true,
|
|
12
|
-
packages: true,
|
|
13
|
-
resume: true,
|
|
14
|
-
fork: false,
|
|
15
|
-
interactiveTui: true,
|
|
16
|
-
usageReporting: true,
|
|
17
|
-
sessionDiscovery: true,
|
|
18
|
-
streamingBehaviors: ["steer", "followUp"],
|
|
19
|
-
});
|
|
20
9
|
export function piAgentDir() {
|
|
21
10
|
return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
22
11
|
}
|
|
@@ -40,6 +29,15 @@ export function piCommandAvailable() {
|
|
|
40
29
|
export function invalidatePiCommandProbe() {
|
|
41
30
|
PI_COMMAND_CACHE.clear();
|
|
42
31
|
}
|
|
32
|
+
export function piBridgeInstalled() {
|
|
33
|
+
try {
|
|
34
|
+
import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
43
41
|
function unsupportedNodeMessage() {
|
|
44
42
|
return `Pi requires Node.js 22.19+ (found ${process.version}). Upgrade Node, or select another agent such as Claude Code/Codex/OpenCode.`;
|
|
45
43
|
}
|
|
@@ -87,13 +85,15 @@ export function piIntegration(origin) {
|
|
|
87
85
|
visible: true,
|
|
88
86
|
origin,
|
|
89
87
|
describe: () => {
|
|
90
|
-
const
|
|
88
|
+
const commandInstalled = piCommandAvailable();
|
|
89
|
+
const bridgeInstalled = piBridgeInstalled();
|
|
90
|
+
const installed = commandInstalled && bridgeInstalled && nodeSupportsPi();
|
|
91
91
|
return {
|
|
92
92
|
id: "pi",
|
|
93
93
|
executionMode: "protocol",
|
|
94
94
|
displayName: "Pi",
|
|
95
95
|
description: "The operator-installed Pi coding agent connected to Bivy for durable sessions, governance, packages, and model selection.",
|
|
96
|
-
status: installed
|
|
96
|
+
status: installed ? "available" : "external",
|
|
97
97
|
packageName: "@earendil-works/pi-coding-agent",
|
|
98
98
|
language: "TypeScript",
|
|
99
99
|
capabilities: PI_CAPABILITIES,
|
|
@@ -105,7 +105,9 @@ export function piIntegration(origin) {
|
|
|
105
105
|
? unsupportedNodeMessage()
|
|
106
106
|
: installed
|
|
107
107
|
? "Uses the Pi command and agent-owned auth/configuration already on this node, and hands sessions back to that native TUI."
|
|
108
|
-
:
|
|
108
|
+
: commandInstalled
|
|
109
|
+
? "Pi is on PATH, but Bivy's optional Pi bridge is not installed. Select Pi in setup or run 'bivy agents:install'."
|
|
110
|
+
: "Install and sign in to Pi on this node; Bivy will connect to that existing agent.",
|
|
109
111
|
install: installed ? undefined : {
|
|
110
112
|
label: "Install Pi",
|
|
111
113
|
description: "Installs the upstream Pi coding agent on this node.",
|
|
@@ -116,6 +118,8 @@ export function piIntegration(origin) {
|
|
|
116
118
|
create: (options) => {
|
|
117
119
|
if (!piCommandAvailable())
|
|
118
120
|
throw new Error(`Pi command not found on PATH: ${piCommand()}`);
|
|
121
|
+
if (!piBridgeInstalled())
|
|
122
|
+
throw new Error("Bivy's optional Pi bridge is not installed. Run 'bivy setup' and choose Pi, or run 'bivy agents:install'.");
|
|
119
123
|
// Vault-backed credentials (see catalogRuntimes): the daemon-hosted Pi
|
|
120
124
|
// session reads the shared vault the user signed in to, not Pi's own
|
|
121
125
|
// plaintext auth.json. The agent dir still supplies config/models/packages.
|
|
@@ -15,7 +15,7 @@ import { toModelInfo as sharedToModelInfo } from "../../runtime/normalize.js";
|
|
|
15
15
|
import { provisionPiAuthJson } from "../../runtime/credential-provisioning.js";
|
|
16
16
|
import { isNativeOAuthProvider } from "../../runtime/oauth/model-oauth-providers.js";
|
|
17
17
|
import { bivySessionEnv } from "../../runtime/session-env.js";
|
|
18
|
-
import {
|
|
18
|
+
import { PI_CAPABILITIES } from "./capabilities.js";
|
|
19
19
|
import { mapToolCall, mapToolResult } from "../../runtime/tool-call-map.js";
|
|
20
20
|
import { BackgroundShellTracker, createBackgroundAwareBashOperations } from "./background-shell.js";
|
|
21
21
|
/**
|
|
@@ -406,28 +406,9 @@ export class PiRuntime {
|
|
|
406
406
|
options;
|
|
407
407
|
id = "pi";
|
|
408
408
|
displayName = "Pi";
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
packages: true,
|
|
413
|
-
resume: true,
|
|
414
|
-
fork: false,
|
|
415
|
-
// pi resumes by session-file path (under the node's sessions dir), so the
|
|
416
|
-
// daemon applies its path-traversal guard to these refs.
|
|
417
|
-
sessionRefIsPath: true,
|
|
418
|
-
// pi's TUI ships with the node, so the chat<->TUI hand-off is always offered.
|
|
419
|
-
interactiveTui: true,
|
|
420
|
-
usageReporting: true,
|
|
421
|
-
// pi transcripts are structured messages that round-trip through the session
|
|
422
|
-
// store, so a pi->pi fork is full fidelity (see exportForFork/importForFork).
|
|
423
|
-
forkTransport: true,
|
|
424
|
-
// pi can also stand up a session from portable {role,text} history, so a fork
|
|
425
|
-
// FROM another agent INTO pi is a true replay, not a seeded summary.
|
|
426
|
-
forkHistoryImport: true,
|
|
427
|
-
// The pi-coding-agent SDK implements both explicitly: prompting mid-turn
|
|
428
|
-
// with no streamingBehavior hint throws, forcing every caller to choose.
|
|
429
|
-
streamingBehaviors: ["steer", "followUp"],
|
|
430
|
-
});
|
|
409
|
+
// Shared with LazyPiRuntime (the facade the registry actually hands out) —
|
|
410
|
+
// see capabilities.ts for why the two must never drift.
|
|
411
|
+
capabilities = PI_CAPABILITIES;
|
|
431
412
|
constructor(options) {
|
|
432
413
|
this.options = options;
|
|
433
414
|
}
|
package/dist/agents/profiles.js
CHANGED
|
@@ -82,7 +82,7 @@ export const AGENT_PROFILES = {
|
|
|
82
82
|
// git-aware turn and exits instead of dropping into the REPL.
|
|
83
83
|
args: ["--yes-always", "--message"],
|
|
84
84
|
promptMode: "argv",
|
|
85
|
-
supportTier: "
|
|
85
|
+
supportTier: "supported",
|
|
86
86
|
authOwner: "mixed",
|
|
87
87
|
blurb: "Popular git-native pair-programming agent (Aider).",
|
|
88
88
|
// `aider --model <id> …` — a leading option (insertAt: 0). Aider resolves its
|
|
@@ -116,6 +116,7 @@ export const AGENT_PROFILES = {
|
|
|
116
116
|
// Hidden from the picker: dumb-pipe adapter with no validated JSON parser or
|
|
117
117
|
// documented session/resume flag (still runnable via BIVY_RUNTIME=hermes).
|
|
118
118
|
hidden: true,
|
|
119
|
+
supportTier: "experimental",
|
|
119
120
|
// No `resume`: no documented session/resume flag.
|
|
120
121
|
install: { kind: "npm", pkg: "hermes-agent" },
|
|
121
122
|
},
|
|
@@ -137,7 +138,7 @@ export const AGENT_PROFILES = {
|
|
|
137
138
|
// BIVY_GOOSE_ACP=1 (or global BIVY_PREFER_ACP=1); off by default until validated.
|
|
138
139
|
acp: { args: ["acp"] },
|
|
139
140
|
promptMode: "argv",
|
|
140
|
-
supportTier: "
|
|
141
|
+
supportTier: "supported",
|
|
141
142
|
blurb: "Block's open-source agent with a structured stream-json protocol (Goose).",
|
|
142
143
|
// Homebrew isn't present on stock Linux nodes (brew → ENOENT); the official
|
|
143
144
|
// download script installs the goose binary on both Linux and macOS.
|
|
@@ -154,7 +155,7 @@ export const AGENT_PROFILES = {
|
|
|
154
155
|
displayName: "Gemini CLI",
|
|
155
156
|
command: "gemini",
|
|
156
157
|
packageName: "@google/gemini-cli",
|
|
157
|
-
supportTier: "
|
|
158
|
+
supportTier: "supported",
|
|
158
159
|
blurb: "Google's terminal coding agent (Gemini CLI).",
|
|
159
160
|
// `gemini -m <id> … -p "<prompt>"` — a leading option before the trailing `-p`
|
|
160
161
|
// (insertAt: 0). The prompt flag stays last, so prepending is safe.
|
|
@@ -197,7 +198,7 @@ export const AGENT_PROFILES = {
|
|
|
197
198
|
displayName: "Qwen Code",
|
|
198
199
|
command: "qwen",
|
|
199
200
|
packageName: "@qwen-code/qwen-code",
|
|
200
|
-
supportTier: "
|
|
201
|
+
supportTier: "supported",
|
|
201
202
|
blurb: "Alibaba's Qwen Code CLI (a Gemini-CLI fork tuned for Qwen-Coder models).",
|
|
202
203
|
// Gemini-CLI fork: same `-m <id> … -p` model flag (insertAt: 0).
|
|
203
204
|
model: {
|
|
@@ -241,7 +242,7 @@ export const AGENT_PROFILES = {
|
|
|
241
242
|
displayName: "Cline",
|
|
242
243
|
command: "cline",
|
|
243
244
|
packageName: "cline",
|
|
244
|
-
supportTier: "
|
|
245
|
+
supportTier: "supported",
|
|
245
246
|
blurb: "Cline's standalone terminal agent (the CLI sibling of the Cline IDE extension).",
|
|
246
247
|
// `cline -y "<prompt>"` runs one autonomous, non-interactive task (‑y/‑‑yolo
|
|
247
248
|
// skips per-tool prompts so a piped run doesn't wedge on approval). Bivy's
|
|
@@ -264,7 +265,7 @@ export const AGENT_PROFILES = {
|
|
|
264
265
|
displayName: "Crush",
|
|
265
266
|
command: "crush",
|
|
266
267
|
packageName: "@charmland/crush",
|
|
267
|
-
supportTier: "
|
|
268
|
+
supportTier: "supported",
|
|
268
269
|
blurb: "Charm's glamourous open-source coding agent (Crush).",
|
|
269
270
|
// `crush run "<prompt>"` runs a single non-interactive prompt and exits;
|
|
270
271
|
// `-q/--quiet` suppresses the spinner UI so stdout is just the reply.
|
|
@@ -285,7 +286,7 @@ export const AGENT_PROFILES = {
|
|
|
285
286
|
displayName: "Cursor",
|
|
286
287
|
command: "cursor-agent",
|
|
287
288
|
packageName: "cursor (curl https://cursor.com/install)",
|
|
288
|
-
supportTier: "
|
|
289
|
+
supportTier: "supported",
|
|
289
290
|
blurb: "Cursor's standalone terminal coding agent (cursor-agent) — the editor's engine on the CLI.",
|
|
290
291
|
// `cursor-agent --force -p "<prompt>"` runs one non-interactive print turn and
|
|
291
292
|
// exits (`-p/--print`); `--force` auto-approves tool/command execution so a
|
|
@@ -320,7 +321,7 @@ export const AGENT_PROFILES = {
|
|
|
320
321
|
displayName: "GitHub Copilot",
|
|
321
322
|
command: "copilot",
|
|
322
323
|
packageName: "@github/copilot",
|
|
323
|
-
supportTier: "
|
|
324
|
+
supportTier: "supported",
|
|
324
325
|
blurb: "GitHub's official terminal coding agent (Copilot CLI).",
|
|
325
326
|
// `copilot --allow-all-tools -p "<prompt>"` runs one programmatic turn and
|
|
326
327
|
// exits; --allow-all-tools skips per-tool approval so a piped run doesn't
|
|
@@ -352,7 +353,7 @@ export const AGENT_PROFILES = {
|
|
|
352
353
|
behaviors: { preflight: "grok", prepare: "grok-auth", nativeSessions: "grok" },
|
|
353
354
|
command: "grok",
|
|
354
355
|
packageName: "grok (curl -fsSL https://x.ai/cli/install.sh | bash)",
|
|
355
|
-
supportTier: "
|
|
356
|
+
supportTier: "supported",
|
|
356
357
|
authOwner: "mixed",
|
|
357
358
|
blurb: "xAI's official Grok coding agent (Grok CLI) — SuperGrok/X subscription or API key.",
|
|
358
359
|
// Official CLI: `grok -p "<prompt>"` (alias `--single`) runs one headless
|
|
@@ -392,7 +393,7 @@ export const AGENT_PROFILES = {
|
|
|
392
393
|
displayName: "Amp",
|
|
393
394
|
command: "amp",
|
|
394
395
|
packageName: "@sourcegraph/amp",
|
|
395
|
-
supportTier: "
|
|
396
|
+
supportTier: "supported",
|
|
396
397
|
blurb: "Sourcegraph's autonomous coding agent with persistent threads (Amp).",
|
|
397
398
|
// `amp -x "<prompt>"` (`--execute`) runs one thread turn and streams to stdout;
|
|
398
399
|
// Amp doesn't gate tools per-run (governed by its own allowlist config), so no
|
|
@@ -414,7 +415,7 @@ export const AGENT_PROFILES = {
|
|
|
414
415
|
displayName: "Auggie",
|
|
415
416
|
command: "auggie",
|
|
416
417
|
packageName: "@augmentcode/auggie",
|
|
417
|
-
supportTier: "
|
|
418
|
+
supportTier: "supported",
|
|
418
419
|
blurb: "Augment Code's terminal agent backed by its codebase context engine (Auggie).",
|
|
419
420
|
// `auggie --quiet --print "<prompt>"` runs one non-interactive turn and prints
|
|
420
421
|
// the final reply (`--print`); `--quiet` drops the UI chatter. Prompt trails.
|
|
@@ -428,7 +429,7 @@ export const AGENT_PROFILES = {
|
|
|
428
429
|
displayName: "Droid",
|
|
429
430
|
command: "droid",
|
|
430
431
|
packageName: "droid (curl https://app.factory.ai/cli)",
|
|
431
|
-
supportTier: "
|
|
432
|
+
supportTier: "supported",
|
|
432
433
|
blurb: "Factory AI's autonomous terminal coding agent (Droid).",
|
|
433
434
|
// `droid exec --auto high "<prompt>"` runs one headless task at high autonomy
|
|
434
435
|
// (auto-approves) and streams to stdout. Prompt trails the `exec` subcommand.
|
|
@@ -456,7 +457,7 @@ export const AGENT_PROFILES = {
|
|
|
456
457
|
displayName: "Continue",
|
|
457
458
|
command: "cn",
|
|
458
459
|
packageName: "@continuedev/cli",
|
|
459
|
-
supportTier: "
|
|
460
|
+
supportTier: "supported",
|
|
460
461
|
blurb: "Continue's headless terminal agent (cn) driving configurable assistants.",
|
|
461
462
|
// `cn --auto -p "<prompt>"` runs one headless turn (`-p` = no TUI) and prints
|
|
462
463
|
// the final response; `--auto` allows all tools without prompting. Prompt
|
|
@@ -484,7 +485,7 @@ export const AGENT_PROFILES = {
|
|
|
484
485
|
displayName: "Kilo Code",
|
|
485
486
|
command: "kilo",
|
|
486
487
|
packageName: "@kilocode/cli",
|
|
487
|
-
supportTier: "
|
|
488
|
+
supportTier: "supported",
|
|
488
489
|
blurb: "Kilo Code's terminal CLI (an OpenCode fork) for pipeline-friendly agentic coding.",
|
|
489
490
|
// `kilo run --auto "<prompt>"` runs one non-interactive turn (`run`) with
|
|
490
491
|
// auto-approved permissions (`--auto`) and streams to stdout. Prompt trails.
|
|
@@ -518,7 +519,7 @@ export const AGENT_PROFILES = {
|
|
|
518
519
|
displayName: "Rovo Dev",
|
|
519
520
|
command: "acli",
|
|
520
521
|
packageName: "atlassian acli (rovodev)",
|
|
521
|
-
supportTier: "
|
|
522
|
+
supportTier: "supported",
|
|
522
523
|
blurb: "Atlassian's Rovo Dev terminal coding agent, run through the acli CLI.",
|
|
523
524
|
// `acli rovodev run --yolo "<prompt>"` runs one instruction headlessly; --yolo
|
|
524
525
|
// skips tool-approval prompts. Prompt trails the `rovodev run` subcommand.
|
|
@@ -3,9 +3,11 @@ export function certificationEntry(id) {
|
|
|
3
3
|
return CERTIFICATION_MATRIX.agents.find((entry) => entry.id === id);
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Supported means Bivy maintains a wrapper for this integration. Certification is
|
|
7
|
+
* a separate fidelity signal: an active matrix entry verifies the richer release-
|
|
8
|
+
* tested capability set for a pinned adapter version, while drift or a different
|
|
9
|
+
* configured path keeps the wrapper Supported but marks the capability surface as
|
|
10
|
+
* adapter-tested instead of release-tested.
|
|
9
11
|
*/
|
|
10
12
|
export function applyCertification(runtime) {
|
|
11
13
|
if (runtime.supportTier !== "supported")
|
|
@@ -18,9 +20,8 @@ export function applyCertification(runtime) {
|
|
|
18
20
|
if (!eligible) {
|
|
19
21
|
return {
|
|
20
22
|
...runtime,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
notes: `${runtime.notes ? `${runtime.notes} ` : ""}This configured path is not the release-certified execution mode.`,
|
|
23
|
+
certification: runtime.certification ?? "adapter-tested",
|
|
24
|
+
notes: `${runtime.notes ? `${runtime.notes} ` : ""}This wrapper is maintained by Bivy; its current configured path is adapter-tested rather than release-tested.`,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
return { ...runtime, certification: "release-tested", testedVersion: entry.pinnedVersion };
|
package/dist/linear-tasks.js
CHANGED
|
@@ -34,7 +34,7 @@ export function linearBranchName(identifier) {
|
|
|
34
34
|
const slug = identifier.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
35
35
|
return `bivy/linear-${slug || "issue"}`;
|
|
36
36
|
}
|
|
37
|
-
export function buildLinearTaskPrompt(issue) {
|
|
37
|
+
export function buildLinearTaskPrompt(issue, instructions) {
|
|
38
38
|
return [
|
|
39
39
|
`You are working on Linear issue ${issue.identifier}: ${issue.title}`,
|
|
40
40
|
"",
|
|
@@ -42,8 +42,8 @@ export function buildLinearTaskPrompt(issue) {
|
|
|
42
42
|
"",
|
|
43
43
|
issue.url ? `Issue: ${issue.url}` : "",
|
|
44
44
|
"",
|
|
45
|
-
"Understand the issue and surrounding codebase, implement it completely, and run the project's tests, linter, and type-checker.",
|
|
45
|
+
instructions?.trim() || "Understand the issue and surrounding codebase, implement it completely, and run the project's tests, linter, and type-checker.",
|
|
46
46
|
"",
|
|
47
|
-
`When finished, commit and push your changes and open a pull request yourself. Include a link to ${issue.identifier} in the pull request description.`,
|
|
47
|
+
instructions?.trim() ? "" : `When finished, commit and push your changes and open a pull request yourself. Include a link to ${issue.identifier} in the pull request description.`,
|
|
48
48
|
].filter((part, index, all) => part !== "" || all[index - 1] !== "").join("\n");
|
|
49
49
|
}
|
package/dist/metadata.js
CHANGED
|
@@ -103,7 +103,14 @@ export class MetadataStore {
|
|
|
103
103
|
const prev = this.data.sessions[input.id];
|
|
104
104
|
const createdAt = input.createdAt ?? prev?.createdAt ?? nowIso();
|
|
105
105
|
const updatedAt = input.updatedAt ?? nowIso();
|
|
106
|
-
|
|
106
|
+
// Runtime adapters do not all expose the same resume fields. In particular,
|
|
107
|
+
// an adapter can temporarily have no sessionFile while it is being rebuilt.
|
|
108
|
+
// Do not let that partial snapshot erase the durable resume ref (or any
|
|
109
|
+
// other field) already recorded for the session. The old object spread did
|
|
110
|
+
// exactly that: `{ path: undefined }` replaced the Pi transcript path, so
|
|
111
|
+
// reopening the saved row fell through to a fresh session.
|
|
112
|
+
const defined = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
113
|
+
this.data.sessions[input.id] = { ...prev, ...defined, createdAt, updatedAt };
|
|
107
114
|
this.save();
|
|
108
115
|
}
|
|
109
116
|
touchSession(id, status) {
|
|
@@ -77,3 +77,12 @@ export function authProviderForSession(runtimeId, modelProvider) {
|
|
|
77
77
|
return provider;
|
|
78
78
|
return undefined;
|
|
79
79
|
}
|
|
80
|
+
/** Convert a provider failure into the safe wire shape consumed by clients.
|
|
81
|
+
* Raw provider text can contain stack frames, paths, or tokens and must never
|
|
82
|
+
* be used as the user-facing auth error. */
|
|
83
|
+
export function classifyModelAuthError(raw, runtimeId, modelProvider) {
|
|
84
|
+
if (!isModelAuthError(raw))
|
|
85
|
+
return null;
|
|
86
|
+
const provider = authProviderForSession(runtimeId, modelProvider);
|
|
87
|
+
return provider ? { kind: "model_auth", provider } : null;
|
|
88
|
+
}
|
package/dist/runtime/index.js
CHANGED
|
@@ -277,8 +277,8 @@ export function cliAgentManifest() {
|
|
|
277
277
|
label: spec.displayName,
|
|
278
278
|
command: spec.command,
|
|
279
279
|
hidden: Boolean(spec.hidden),
|
|
280
|
-
supportTier: spec.supportTier ?? "
|
|
281
|
-
certification: spec.testedVersion ? "release-tested" : (spec.supportTier ?? "
|
|
280
|
+
supportTier: spec.supportTier ?? "supported",
|
|
281
|
+
certification: spec.testedVersion ? "release-tested" : (spec.supportTier ?? "supported") === "supported" || (spec.supportTier ?? "supported") === "beta" ? "adapter-tested" : "unverified",
|
|
282
282
|
...(spec.testedVersion ? { testedVersion: spec.testedVersion } : {}),
|
|
283
283
|
headlessFlags: [...headless].filter((a) => !a.includes("{")),
|
|
284
284
|
install: spec.install ?? null,
|
|
@@ -1015,7 +1015,7 @@ function runtimeCertification(runtime) {
|
|
|
1015
1015
|
};
|
|
1016
1016
|
if (runtime.testedVersion)
|
|
1017
1017
|
return { certification: "release-tested", testedVersion: runtime.testedVersion };
|
|
1018
|
-
return { certification: runtime.supportTier === "beta" ? "adapter-tested" : "unverified" };
|
|
1018
|
+
return { certification: runtime.supportTier === "supported" || runtime.supportTier === "beta" ? "adapter-tested" : "unverified" };
|
|
1019
1019
|
}
|
|
1020
1020
|
function runtimeProtection(runtime) {
|
|
1021
1021
|
// Native SDK/CLI sandboxes receive the requested read-only/workspace/full tier
|
package/dist/server.js
CHANGED
|
@@ -34,7 +34,7 @@ import { InMemoryLocationRegistry } from "./runtime/location-registry.js";
|
|
|
34
34
|
import { ControlPlaneSessionLocationRegistry, LayeredSessionLocationRegistry } from "./runtime/control-plane-location.js";
|
|
35
35
|
import { attachAdoptedSessions, classifyAttachFailure } from "./runtime/adoption.js";
|
|
36
36
|
import { createCredentialStore, testProviderCredential } from "./runtime/credentials.js";
|
|
37
|
-
import { isModelAuthError, authProviderForSession } from "./runtime/auth-errors.js";
|
|
37
|
+
import { isModelAuthError, authProviderForSession, classifyModelAuthError } from "./runtime/auth-errors.js";
|
|
38
38
|
import { createCredentialVault, migrateVaultDir } from "./runtime/credential-store.js";
|
|
39
39
|
import { probeAnthropicAccess } from "./runtime/anthropic-preflight.js";
|
|
40
40
|
import { createSessionNamer, fallbackSessionName } from "./session/session-namer.js";
|
|
@@ -930,6 +930,7 @@ function harnessDirFor(record) {
|
|
|
930
930
|
function harnessBeginTurn(record) {
|
|
931
931
|
const dir = harnessDirFor(record);
|
|
932
932
|
record.harnessTurnReady = undefined;
|
|
933
|
+
record.harnessTurnFinished = false;
|
|
933
934
|
if (!dir)
|
|
934
935
|
return;
|
|
935
936
|
const previous = record.workspaceState === "dirty" ? "dirty" : "clean";
|
|
@@ -954,6 +955,14 @@ function harnessBeginTurn(record) {
|
|
|
954
955
|
})();
|
|
955
956
|
}
|
|
956
957
|
/** After a turn, snapshot again and broadcast the structured diff it produced. */
|
|
958
|
+
function finishHarnessTurn(record) {
|
|
959
|
+
if (record.harnessTurnFinished)
|
|
960
|
+
return;
|
|
961
|
+
record.harnessTurnFinished = true;
|
|
962
|
+
void harnessEndTurn(record).finally(() => {
|
|
963
|
+
void replication.onTurnComplete(record.id);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
957
966
|
async function harnessEndTurn(record) {
|
|
958
967
|
const previous = record.workspaceState === "dirty" ? "dirty" : "clean";
|
|
959
968
|
try {
|
|
@@ -1643,11 +1652,14 @@ function stampSessionEvent(payload) {
|
|
|
1643
1652
|
return payload;
|
|
1644
1653
|
}
|
|
1645
1654
|
// Collapse the burst of assistant `message_update`s (one per agent stdout line,
|
|
1646
|
-
// each carrying the FULL text so far)
|
|
1647
|
-
//
|
|
1655
|
+
// each carrying the FULL text so far) to a human-smooth 4 fps. A 16 ms window
|
|
1656
|
+
// still allowed up to 62 cumulative snapshots/second, making a long answer
|
|
1657
|
+
// quadratic and surprisingly expensive over a phone's relay connection. The
|
|
1658
|
+
// latest snapshot supersedes every skipped one, and non-update events flush it
|
|
1659
|
+
// immediately, so 250 ms changes neither final content nor event ordering.
|
|
1648
1660
|
// The coalescer's emit stamps the surviving (latest) update, so only ~one seq is
|
|
1649
|
-
// spent per
|
|
1650
|
-
const SESSION_UPDATE_COALESCE_MS =
|
|
1661
|
+
// spent per window — the ring holds turn-scale events, not every stdout line.
|
|
1662
|
+
const SESSION_UPDATE_COALESCE_MS = 250;
|
|
1651
1663
|
const sessionEvents = new SessionEventCoalescer({
|
|
1652
1664
|
coalesceMs: SESSION_UPDATE_COALESCE_MS,
|
|
1653
1665
|
emit: (payload) => broadcastCoalesced(stampSessionEvent(payload)),
|
|
@@ -2842,7 +2854,7 @@ const RELAY_COMMANDS = {
|
|
|
2842
2854
|
// this the relay client (PWA) is stranded on "Working…" forever with
|
|
2843
2855
|
// only a session.error toast. Clear working so a terminal state reaches it.
|
|
2844
2856
|
clearSessionWorking(record);
|
|
2845
|
-
broadcast({ type: "session.error", sessionId: record.id, error:
|
|
2857
|
+
broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, error) });
|
|
2846
2858
|
});
|
|
2847
2859
|
},
|
|
2848
2860
|
async "session.new"(msg) {
|
|
@@ -4212,7 +4224,7 @@ async function runIssueTaskInner(cfg, issue, source, overrides = {}) {
|
|
|
4212
4224
|
try {
|
|
4213
4225
|
recordRunAuditCorrelation(record, overrides.correlation);
|
|
4214
4226
|
emit(record, "started", `Started work on ${cfg.owner}/${cfg.repo}#${issue.number}.`);
|
|
4215
|
-
await runSessionTurn(record, buildTaskPrompt(issue, nodeGithubIssuePrompt()), overrides.signal);
|
|
4227
|
+
await runSessionTurn(record, buildTaskPrompt(issue, overrides.instructions ?? nodeGithubIssuePrompt()), overrides.signal);
|
|
4216
4228
|
if (overrides.signal?.aborted)
|
|
4217
4229
|
throw overrides.signal.reason ?? new Error("Run cancelled");
|
|
4218
4230
|
emit(record, "agent_done", `Agent finished issue #${issue.number}; running deterministic checks.`);
|
|
@@ -4918,6 +4930,7 @@ async function runWorkItem(item, report, signal) {
|
|
|
4918
4930
|
sandbox: normalizeSandboxTier(item.sandbox),
|
|
4919
4931
|
approvalMode: approvalModeFrom(item.approvalMode),
|
|
4920
4932
|
onEvidence: report,
|
|
4933
|
+
instructions: item.body,
|
|
4921
4934
|
signal,
|
|
4922
4935
|
correlation: { runId: item.id, attempt: item.attempt ?? 1, machineId: identity.nodeId },
|
|
4923
4936
|
});
|
|
@@ -4938,7 +4951,7 @@ async function runWorkItem(item, report, signal) {
|
|
|
4938
4951
|
throw new Error(`Linear work item has an invalid repo "${repoSlug}"`);
|
|
4939
4952
|
// Case B: a re-dispatch the control plane correlated to an existing session
|
|
4940
4953
|
// continues it as a normal chat instead of starting cold (mirrors GitHub).
|
|
4941
|
-
if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue), report, { resumeOnMissing: item.targetKind === "existing_session", signal }))
|
|
4954
|
+
if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue, item.body), report, { resumeOnMissing: item.targetKind === "existing_session", signal }))
|
|
4942
4955
|
return;
|
|
4943
4956
|
const githubToken = await resolveGitHubToken();
|
|
4944
4957
|
if (!githubToken)
|
|
@@ -4965,7 +4978,7 @@ async function runWorkItem(item, report, signal) {
|
|
|
4965
4978
|
catch { }
|
|
4966
4979
|
}
|
|
4967
4980
|
await report({ output: { sessionId: record.id, branch }, events: [{ at: new Date().toISOString(), kind: "branch", summary: "Linear issue working branch and session created.", ref: branch, url: issue.url }] });
|
|
4968
|
-
await runSessionTurn(record, buildLinearTaskPrompt(issue), signal);
|
|
4981
|
+
await runSessionTurn(record, buildLinearTaskPrompt(issue, item.body), signal);
|
|
4969
4982
|
if (signal.aborted)
|
|
4970
4983
|
throw signal.reason ?? new Error("Run cancelled");
|
|
4971
4984
|
await branchPublish.maybePushWorktreeBranch(record);
|
|
@@ -5958,6 +5971,7 @@ async function sessionListRows() {
|
|
|
5958
5971
|
agent: meta?.runtimeId ?? s.agent,
|
|
5959
5972
|
agentName: meta?.agentName ?? s.agentName,
|
|
5960
5973
|
source: rec?.source ?? meta?.source,
|
|
5974
|
+
bivyCreated: Boolean(meta),
|
|
5961
5975
|
forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
|
|
5962
5976
|
branch: rec?.worktree?.branch ?? meta?.branch,
|
|
5963
5977
|
sandbox: rec?.sandbox ?? normalizeSandboxTier(meta?.sandbox),
|
|
@@ -7174,6 +7188,11 @@ function actionableAgentError(runtimeId, error) {
|
|
|
7174
7188
|
}
|
|
7175
7189
|
return raw;
|
|
7176
7190
|
}
|
|
7191
|
+
function sessionAgentError(record, error) {
|
|
7192
|
+
const raw = humanizeAgentError(error instanceof Error ? error.message : String(error));
|
|
7193
|
+
return classifyModelAuthError(raw, record.runtimeId, record.session.getCurrentModel()?.provider)
|
|
7194
|
+
?? actionableAgentError(record.runtimeId, error);
|
|
7195
|
+
}
|
|
7177
7196
|
/**
|
|
7178
7197
|
* A turn that ended in a *terminal* model/provider failure the runtime would
|
|
7179
7198
|
* otherwise swallow. `agent_end` carries the turn's messages and whether the
|
|
@@ -7230,7 +7249,7 @@ function attachSessionListeners(record) {
|
|
|
7230
7249
|
policy: sessionRunPolicy,
|
|
7231
7250
|
onNotice: (n) => broadcast({ type: "session.notice", sessionId: record.id, level: n.level, message: n.message }),
|
|
7232
7251
|
onModelChanged: () => broadcast({ type: "model.updated", sessionId: record.id, model: publicModel(record.session.getCurrentModel(), record.session.getCurrentModel()) }),
|
|
7233
|
-
onFailed: (message) => broadcast({ type: "session.error", sessionId: record.id, error: message }),
|
|
7252
|
+
onFailed: (message) => broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, message) }),
|
|
7234
7253
|
});
|
|
7235
7254
|
}
|
|
7236
7255
|
record.unsubscribe = record.session.subscribe((event) => {
|
|
@@ -7327,9 +7346,14 @@ function attachSessionListeners(record) {
|
|
|
7327
7346
|
transcripts.resolveInlineImages(record);
|
|
7328
7347
|
}
|
|
7329
7348
|
// Durably persist the throttled sidecars at the turn boundary so a crash
|
|
7330
|
-
// loses at most the in-flight turn's UI detail, not the whole turn.
|
|
7331
|
-
|
|
7349
|
+
// loses at most the in-flight turn's UI detail, not the whole turn. A
|
|
7350
|
+
// runtime may keep its agent process alive and never emit agent_end (Pi),
|
|
7351
|
+
// so turn_end is also authoritative for the interactive working state.
|
|
7352
|
+
if (event.type === "turn_end") {
|
|
7332
7353
|
eventLog.flush(record.id);
|
|
7354
|
+
clearSessionWorking(record);
|
|
7355
|
+
finishHarnessTurn(record);
|
|
7356
|
+
}
|
|
7333
7357
|
// AskUserQuestion is intercepted and answered by the daemon's guardian /
|
|
7334
7358
|
// QuestionManager (see guardianInterceptor), which broadcasts
|
|
7335
7359
|
// session.question(.resolved) from its own listeners — runtimes no longer
|
|
@@ -7364,14 +7388,9 @@ function attachSessionListeners(record) {
|
|
|
7364
7388
|
transcripts.clearLiveIntermediate(record.id);
|
|
7365
7389
|
clearSessionWorking(record);
|
|
7366
7390
|
void refreshSessionUsage(record);
|
|
7367
|
-
// Snapshot
|
|
7368
|
-
//
|
|
7369
|
-
|
|
7370
|
-
// (so the shipped frame carries this turn's transcript AND its checkpoint).
|
|
7371
|
-
// Gated on session sync — inert by default.
|
|
7372
|
-
void harnessEndTurn(record).finally(() => {
|
|
7373
|
-
void replication.onTurnComplete(record.id);
|
|
7374
|
-
});
|
|
7391
|
+
// Snapshot/diff at whichever completion boundary arrived first. Pi keeps
|
|
7392
|
+
// its process alive after turn_end; other runtimes also emit agent_end.
|
|
7393
|
+
finishHarnessTurn(record);
|
|
7375
7394
|
// A turn that ended in a terminal model/provider error (e.g. an expired
|
|
7376
7395
|
// credential or a 4xx from the API) otherwise vanished: working cleared,
|
|
7377
7396
|
// no reply, no signal. Surface it as a session-scoped error so the client
|
|
@@ -7433,7 +7452,7 @@ function attachSessionListeners(record) {
|
|
|
7433
7452
|
scheduleAdvertise();
|
|
7434
7453
|
broadcast({ type: "session.failed", sessionId: record.id, failedAt: record.lastFailureAt });
|
|
7435
7454
|
if (messageError)
|
|
7436
|
-
broadcast({ type: "session.error", sessionId: record.id, error:
|
|
7455
|
+
broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, messageError) });
|
|
7437
7456
|
// If the terminal error is an auth failure (expired key/token → 4xx),
|
|
7438
7457
|
// also raise the sign-in sheet for the failing provider.
|
|
7439
7458
|
maybeSignalAuthRequired(record, turnError);
|
|
@@ -8057,7 +8076,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
8057
8076
|
void refreshSessionUsage(record);
|
|
8058
8077
|
if (makeActive)
|
|
8059
8078
|
active = record;
|
|
8060
|
-
broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
|
|
8079
|
+
broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, bivyCreated: true, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
|
|
8061
8080
|
void maybeNotifyBivyUpdate();
|
|
8062
8081
|
scheduleAdvertise();
|
|
8063
8082
|
return record;
|
|
@@ -8074,6 +8093,12 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
8074
8093
|
* of the same session onto one promise fixes both.
|
|
8075
8094
|
*/
|
|
8076
8095
|
const resumingSessions = new Map();
|
|
8096
|
+
// Keep an id index as well as the resolved runtime ref. A prompt can arrive
|
|
8097
|
+
// while session.open is still resolving and may not carry the session's path;
|
|
8098
|
+
// without this index it cannot discover the in-flight open from metadata and
|
|
8099
|
+
// is incorrectly reported as "Session not found". Both commands must join the
|
|
8100
|
+
// same resume operation by the identity the client actually supplied.
|
|
8101
|
+
const resumingSessionsById = new Map();
|
|
8077
8102
|
/**
|
|
8078
8103
|
* Resolve the session a client command targets, resuming it from durable
|
|
8079
8104
|
* metadata when it isn't currently held in memory. A session survives being
|
|
@@ -8096,6 +8121,9 @@ async function resolveOrResumeSession(sessionId, sessionPath) {
|
|
|
8096
8121
|
const open = openSessions.get(id);
|
|
8097
8122
|
if (open)
|
|
8098
8123
|
return open;
|
|
8124
|
+
const byId = resumingSessionsById.get(id);
|
|
8125
|
+
if (byId)
|
|
8126
|
+
return byId;
|
|
8099
8127
|
const pathRef = typeof sessionPath === "string" && sessionPath.trim() ? sessionPath.trim() : undefined;
|
|
8100
8128
|
const meta = metadata.getSession(id) ?? (pathRef ? metadata.getSession(pathRef) : undefined);
|
|
8101
8129
|
// A run that only kept its terminal log has nothing to resume — it opens as a
|
|
@@ -8151,15 +8179,23 @@ async function resolveOrResumeSession(sessionId, sessionPath) {
|
|
|
8151
8179
|
}
|
|
8152
8180
|
}
|
|
8153
8181
|
const inflight = resumingSessions.get(key);
|
|
8154
|
-
if (inflight)
|
|
8182
|
+
if (inflight) {
|
|
8183
|
+
resumingSessionsById.set(id, inflight);
|
|
8155
8184
|
return inflight;
|
|
8185
|
+
}
|
|
8156
8186
|
const resume = createSession(defaultWorkspace, ref, { runtimeId, makeActive: false });
|
|
8157
8187
|
resumingSessions.set(key, resume);
|
|
8188
|
+
resumingSessionsById.set(id, resume);
|
|
8158
8189
|
try {
|
|
8159
8190
|
return await resume;
|
|
8160
8191
|
}
|
|
8161
8192
|
finally {
|
|
8162
8193
|
resumingSessions.delete(key);
|
|
8194
|
+
// Only delete our promise. A future implementation may replace an entry
|
|
8195
|
+
// after a failed resume, and must not have its newer attempt removed by an
|
|
8196
|
+
// older finally block.
|
|
8197
|
+
if (resumingSessionsById.get(id) === resume)
|
|
8198
|
+
resumingSessionsById.delete(id);
|
|
8163
8199
|
}
|
|
8164
8200
|
}
|
|
8165
8201
|
// Enumerating a runtime's models needs a live session (the model registry lives
|
|
@@ -9835,6 +9871,7 @@ app.get("/api/sessions", async (_req, res, next) => {
|
|
|
9835
9871
|
agent: meta?.runtimeId ?? s.agent,
|
|
9836
9872
|
agentName: meta?.agentName ?? s.agentName,
|
|
9837
9873
|
source: rec?.source ?? meta?.source,
|
|
9874
|
+
bivyCreated: Boolean(meta),
|
|
9838
9875
|
forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
|
|
9839
9876
|
branch: rec?.worktree?.branch ?? meta?.branch,
|
|
9840
9877
|
prUrl: rec?.prUrl ?? meta?.prUrl,
|
|
@@ -9856,7 +9893,18 @@ app.get("/api/sessions", async (_req, res, next) => {
|
|
|
9856
9893
|
});
|
|
9857
9894
|
app.post("/api/sessions/open", async (req, res, next) => {
|
|
9858
9895
|
try {
|
|
9859
|
-
|
|
9896
|
+
// Direct transports use this endpoint for both opening a saved session and
|
|
9897
|
+
// the legacy path-only open flow. The old path-only implementation ignored
|
|
9898
|
+
// sessionId, so a direct client sending into a closed chat opened a brand-new
|
|
9899
|
+
// session instead of reopening the selected conversation.
|
|
9900
|
+
const requestedId = typeof req.body?.sessionId === "string" && req.body.sessionId.trim()
|
|
9901
|
+
? req.body.sessionId.trim()
|
|
9902
|
+
: undefined;
|
|
9903
|
+
const session = requestedId
|
|
9904
|
+
? await resolveOrResumeSession(requestedId, req.body?.path)
|
|
9905
|
+
: await createSession(defaultWorkspace, String(req.body?.path ?? ""), { runtimeId: agentFrom(req.body ?? {}) });
|
|
9906
|
+
if (!session)
|
|
9907
|
+
return res.status(404).json({ error: "Session not found" });
|
|
9860
9908
|
res.json({
|
|
9861
9909
|
id: session.id,
|
|
9862
9910
|
workspace: session.workspace,
|
|
@@ -104,7 +104,7 @@ export function resolveSessionContract(input) {
|
|
|
104
104
|
};
|
|
105
105
|
const supportTier = input.supportTier ?? "experimental";
|
|
106
106
|
const hasProtectionDegradation = degradedReasons.some((r) => PROTECTION_AREAS.has(r.area));
|
|
107
|
-
const requiresAcknowledgement =
|
|
107
|
+
const requiresAcknowledgement = input.certification === "release-tested" && hasProtectionDegradation && !input.acknowledgedAt;
|
|
108
108
|
return {
|
|
109
109
|
schemaVersion: SESSION_CONTRACT_SCHEMA_VERSION,
|
|
110
110
|
resolvedAt: input.now,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bivy/bivy",
|
|
3
|
-
"version": "0.15.0-staging.
|
|
3
|
+
"version": "0.15.0-staging.41",
|
|
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.",
|
|
@@ -66,6 +66,6 @@
|
|
|
66
66
|
"nanoid": "3.3.18",
|
|
67
67
|
"undici": "8.10.0"
|
|
68
68
|
},
|
|
69
|
-
"readme": "# Bivy\n\n[](https://www.npmjs.com/package/@bivy/bivy)\n[](LICENSE)\n[](https://nodejs.org)\n\n**Run coding agents on the machines you already own — then reach them from your\nphone, browser, or another terminal.**\n\nStart Claude Code on your workstation, right where the repo, the running dev\nserver, and the staging database already live. Walk away. On the train, open\nyour phone: read what the agent did, answer its question, approve the migration\n— over a link only your devices can decrypt. The work never left your machine.\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # start an agent where your work lives\nbivy open # pick it up from your phone or browser\n```\n\nFirst thing to try: ask the agent to explain the repository, make one small safe\nchange, then open the same Session in the web app or on your phone while it runs.\n\n**[Quickstart](docs/quickstart.md)** ·\n**[Docs](docs/README.md)** ·\n**[Why Bivy](docs/why-bivy.md)** ·\n**[Security model](docs/security-model.md)** ·\n**[bivy.sh](https://bivy.sh)**\n\n> **0.x software.** The core loop is solid and used daily. Interfaces and\n> cross-runtime fidelity still change between releases — check the\n> [runtime support matrix](docs/runtime-support-matrix.md) before you depend on\n> a specific agent capability.\n\n## Why not just a cloud sandbox?\n\nA hosted sandbox starts from an *approximation* of your environment. A Bivy\nMachine **is** your environment — the actual working tree, the services already\nrunning, the caches already warm.\n\n| | Cloud sandbox | Bivy Machine |\n|---|---|---|\n| Your repository | a cloned copy | the real working tree, uncommitted changes and all |\n| Dev server & database | mocked, or absent | already running, right beside the agent |\n| Private networks & internal APIs | out of reach | reachable |\n| Toolchains, package caches | cold, reinstalled each time | warm, already installed |\n| GPUs / local inference | rented separately | the ones on your box |\n| Where your code sits | someone else's infrastructure | the machine you already trust |\n\nYou keep the environment. Bivy adds the part that was missing: **reaching that\nenvironment from anywhere, and leaving it working while you're gone.**\n\n## What you can do\n\nBivy gives you two ways to put an agent to work.\n\n### Sessions — interactive, and portable\n\nStart an agent, watch it work, jump in to steer, stop, or approve. Then leave\nyour desk and keep going:\n\n```bash\nbivy run claude # or codex, pi, gemini, and a dozen more\nbivy open # continue the same session in the browser or PWA\nbivy resume # pick it back up in the terminal\nbivy run claude --no-follow # start it in the background instead of attaching\nbivy run claude --chat # start the governed app session and open it in the browser\n```\n\n- **Reconnect from anywhere** — phone, browser, or another terminal — to the\n same live Session. The PWA adds voice input, read-aloud, phone-to-agent\n file/image uploads, and agent-to-phone attachments.\n- **Move work without starting over.** Import existing Claude Code and Codex\n Sessions, or fork, copy, and move a Bivy Session to another agent, model, or\n Machine.\n- **Run more than one Machine** — a workstation, a private-network server, a GPU\n box — on one account, and pick the environment each Session needs.\n\n### Runs — unattended, and accountable\n\nQueue one on demand, or let an event kick it off — either way it returns\nimmediately and reports back:\n\n```bash\nbivy runs start \"...\" # queue a one-off unattended Run, then `bivy runs wait <id>`\nbivy automation init # or define governed jobs in .bivy/automations.yaml\n```\n\n- **Trigger from real events** — a failed CI job, a GitHub or Linear issue,\n Slack, a schedule, or a signed webhook.\n- **Pin the guardrails** — Machine, agent, model, sandbox, approval mode, and a\n hard attempt ceiling — right next to the job.\n- **Get a Receipt** — every Run reports the checks it ran and how it turned out,\n not just a wall of output.\n\nTry the [capability recipes](docs/capability-recipes.md) to see each of these\nend to end, or the [runtime support matrix](docs/runtime-support-matrix.md) for\nexactly what each agent supports.\n\n## Bring your own stack\n\nUse provider subscriptions through native agent logins, API keys stored in\nBivy's vault, or local / OpenAI-compatible inference. Claude Code, Codex, and Pi\nhave first-class SDK integrations; any other ACP or headless agent needs no\nadapter at all — it's a data row you add with one command:\n\n```bash\nbivy agent add # register an existing ACP or process agent\n```\n\n## Install\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nmacOS and Linux. Requires Node.js 20 or newer. The installer puts the\n[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) npm package and the\n`bivy` command on your `PATH` with optional bridges skipped for speed, then runs the guided `bivy setup` wizard — agent\nchoice, selected-agent install if needed, remote access, and an auto-start background service (launchd on macOS,\nsystemd on Linux). Re-running it on a machine that already has Bivy just applies\nthe latest build and restarts the service.\n\n**What needs an account, and what doesn't.** The CLI alone — `bivy run`,\n`bivy resume`, `bivy sessions` — needs no account and no server; `bivy setup`\nlets you pick **local only for now** and skip remote access. A browser or phone\nUI needs a control plane, because the node hosts none: use the hosted one at\n`app.bivy.sh` (sign in with GitHub or email; free tier plus a paid plan — see\n[bivy.sh#pricing](https://bivy.sh#pricing)) or\n[self-host your own](docs/self-host-quickstart.md). Switch any time with\n`bivy relay:setup`.\n\n**What the installer does with sudo.** It escalates only when it must, and\ntells you when it does:\n\n- Debian/Ubuntu without a suitable Node.js: `sudo apt-get install build-essential\n python3 curl`, then NodeSource's Node 22 setup script via `sudo`.\n- Other Linux, or macOS, without a suitable Node.js: downloads the official\n Node 22 tarball from nodejs.org (sha256-checked) and installs it under\n `/usr/local` with `sudo`.\n- If npm's global prefix isn't writable it falls back to `~/.local` — it never\n runs `npm install` under `sudo`.\n- It appends a marked PATH block to `~/.bashrc` or `~/.zshrc`\n (`BIVY_NO_RC_UPDATE=1` to opt out).\n\nWant no sudo at all? Bring your own Node.js 20+ and skip the script:\n\n```bash\nnpm install -g @bivy/bivy && bivy setup # install globally\nnpx @bivy/bivy setup # or try it once, no install\n```\n\nReleases are published from CI with provenance attestations; verify a build's\norigin with `npm audit signatures`. See [`docs/releasing.md`](docs/releasing.md).\n\n### Your first session\n\nOnce `bivy setup` finishes, use Bivy from inside an existing repo. The first win\nis simple: start the local agent, give it a real task in that environment, then\nreopen the same Session from another surface.\n\n```bash\ncd your-repo\nbivy run claude # start an agent as a durable session in the current repo\n# Try: \"Explain this repo and suggest one small, safe improvement.\"\nbivy open # open that same session in the web app (needs relay setup)\nbivy resume # or pick it back up here in the terminal\n```\n\nFrom here the [quickstart](docs/quickstart.md) walks through Runs, multiple\nMachines, and automations.\n\n### Install options\n\nEnvironment variables passed to the one-line installer change what it does:\n\n| Goal | Variable |\n|---|---|\n| Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |\n| Pin an exact version | `BIVY_VERSION=0.1.0` |\n| Install the npm package into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |\n| Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |\n| Install optional Bivy bridges/native terminal dependency up front | `BIVY_INSTALL_OPTIONAL_DEPS=1` |\n| Don't touch `~/.bashrc` / `~/.zshrc`; print the PATH line instead | `BIVY_NO_RC_UPDATE=1` |\n\nFor example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.\n\nWorking from a checkout of this repository instead:\n\n```bash\npnpm install\npnpm run setup\n```\n\nSee [`docs/install.md`](docs/install.md) for where data lives, service\nmanagement, and uninstall.\n\n## Updating\n\n```bash\nbivy update\n```\n\n`bivy update` detects how Bivy was installed and does the right thing, then\nwaits for any active session to finish its current turn and restarts the\nbackground service so the node reconnects on the new build:\n\n| Install kind | What `bivy update` does |\n|---|---|\n| npm global (`npm i -g`) | `npm install -g @bivy/bivy@<channel>`, then restart the service |\n| installer / packaged | re-runs `install.sh` (migrating to npm if needed), then restart |\n| git checkout | `git pull --ff-only` + `pnpm install --frozen-lockfile`, then restart |\n| `npx` run | nothing to update — each run already fetches the latest |\n\nUpdates follow the release **channel** recorded at install time — `latest`\n(production) by default, or `staging` if you installed with\n`BIVY_CHANNEL=staging`. Switch channels (the choice is remembered for next\ntime), or skip the wait for a busy session:\n\n```bash\nbivy update --staging # move to the dev channel\nbivy update --stable # move back to production (latest)\nbivy update --force # don't wait for an in-flight turn to finish\n```\n\nThe daemon also checks the registry periodically and posts an in-session notice\nwhen a newer build is available.\n\n## Architecture\n\nBivy has three parts. **Only the first one holds your data.**\n\n```text\n your machine hosted or self-hosted\n\n ┌──────────────┐ ┌─────────┐ ┌───────────────┐\n │ node daemon │ ──dials──▶ │ relay │ ◀────▶ │ control plane │\n │ agents, keys │ outbound │ opaque │ │ accounts, web │\n │ repo, tools │ │ frames │ │ app, metadata │\n └──────────────┘ └─────────┘ └───────────────┘\n ▲ ▲\n └────────── end-to-end encrypted session ───────────┘\n phone · browser · another terminal\n```\n\n- **Node** — a daemon on your machine. Owns the workspace, credentials, and agent\n processes. Serves an API and WebSocket on `http://localhost:4317` plus a\n `/healthz` probe. **It hosts no web UI.**\n- **Relay** — forwards encrypted frames between your node and your devices. Your\n node dials out, so no inbound port is opened. The relay cannot read the frames.\n- **Control plane** — holds your account, node registry, and session index, and\n serves the web/PWA client. Use the hosted one or run your own.\n\nBecause the node serves no UI, a browser or phone needs a control plane — hosted\nat `app.bivy.sh`, or one you deploy yourself. The terminal CLI needs neither.\nInteractive Session traffic is end-to-end encrypted between a Machine and its\npaired devices: the relay never sees plaintext and cannot decrypt it. Who can\n*authorize* a device depends on how you pair — with a QR / `bivy link` pairing,\nor on a self-hosted deployment, the control plane can't read your Sessions\neither; with hosted account sign-in you trust the control plane to authorize\ndevices and to serve the web app that holds the keys. See\n[known limitations](docs/security-model.md#known-limitations-for-0x).\n\nSee [`docs/remote-access.md`](docs/remote-access.md) and\n[`docs/security-model.md`](docs/security-model.md).\n\n## Supported agents\n\n**Claude Code and Codex are the recommended, release-certified paths.** The\nbroader catalog stays available under **More agents**; capabilities and fidelity\nvary by runtime.\n\n| Agent | Command | Notes |\n|---|---|---|\n| Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |\n| Codex | `bivy run codex` | Installs `@openai/codex` |\n| Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |\n| OpenCode | `bivy run opencode` | Installs `opencode-ai` |\n| Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |\n| Qwen Code | `bivy run qwen` | Installs `@qwen-code/qwen-code` |\n| Goose | `bivy run goose` | Requires `goose` on PATH |\n| Aider | `bivy run aider` | No session resume (upstream gap) |\n| Cline | `bivy run cline` | Installs `cline` |\n| Crush | `bivy run crush` | No session resume (upstream gap) |\n| Cursor | `bivy run cursor` | ACP-capable |\n| GitHub Copilot | `bivy run copilot` | ACP-capable |\n| Grok | `bivy run grok` | Model selection |\n| Amp | `bivy run amp` | Native thread resume |\n| Auggie | `bivy run auggie` | Headless CLI |\n| Droid | `bivy run droid` | Model selection |\n| Continue | `bivy run continue` | Headless CLI |\n| Kilo Code | `bivy run kilocode` | ACP-capable |\n| Rovo Dev | `bivy run rovodev` | Installed out of band |\n\nAlso defined but hidden from the picker as *Experimental* — runnable via\n`BIVY_RUNTIME=<id>`: Codebuff (`codebuff`, no verified headless mode upstream\nyet), Hermes (`hermes`, generic process adapter), and OpenClaw (`openclaw`,\nCLI adapter only, no resume yet).\n\nAny other command works via `bivy run -- ./your-agent --flags`. ACP-capable\nagents can be promoted to Bivy's governed protocol path for per-tool approvals\nand native resume. To add a reusable process or ACP agent to both the CLI and web\npicker without changing Bivy, run `bivy agent add`, or scaffold and install a\ndeclarative [plugin manifest](docs/plugins.md) with `bivy plugin init`\n(declarative plugins are Experimental, `v1alpha1`, and run out of process).\n\n[`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly\nwhat each agent supports — resume, model selection, approvals, sandboxing.\n\n## Common commands\n\n```bash\nbivy # show the command overview\nbivy run claude # launch Claude Code as a durable session\nbivy run codex # run a different agent\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires relay setup)\nbivy automation init # create .bivy/automations.yaml\nbivy agent add # connect an existing ACP or process agent\nbivy plugin list # installed declarative integration packages\nbivy status # config summary and node reachability\nbivy doctor # health check\nbivy logs -f # tail node logs\nbivy update # update Bivy and restart the service\n```\n\nFull command list, flags, and examples: [`docs/cli-reference.md`](docs/cli-reference.md).\n\n## Configuration\n\nThe common knobs:\n\n```bash\nBIVY_WORKSPACE=/path/to/repo # default workspace\nBIVY_SANDBOX=read-only # read-only | workspace-write (default) | danger-full-access\nBIVY_APPROVAL_MODE=risky # never | risky | always | autonomous (default)\n```\n\nCreate and inspect the typed node configuration, or add repository-owned\nsafety/check/retry policy:\n\n```bash\nbivy config init\nbivy config set defaults.agent codex\nbivy config explain defaults.sandbox\nbivy config init --project # .bivy/policy.yaml\n```\n\nSee [`docs/config-as-code.md`](docs/config-as-code.md). Every environment\nvariable and precedence rule lives in\n[`docs/configuration.md`](docs/configuration.md).\n\n## Approvals and sandboxing\n\nThe default approval mode is **`autonomous`**: agents act without per-action\nprompts. How much that actually protects you depends on the runtime. Native-sandbox\nagents enforce the chosen access tier; structured runtimes also pass tool calls\nthrough Bivy's policy and approval layer. Process agents that Bivy cannot\nintercept run with your OS user permissions — the picker flags this and requires\nconfirmation before you pick that path.\n\nWhere Bivy receives structured shell/file calls, a heuristic floor blocks known\ncatastrophic commands and structured writes outside the workspace, and a\nbackstop set (force-push, publish, deploy, sudo) pauses for a human. This catches\naccidents; **it is not an adversarial isolation boundary.**\n\nWant to be asked about more? Set the mode explicitly:\n\n```bash\nBIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits\nBIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits\nBIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available\n```\n\nApprove from the terminal, browser, or phone.\n\nSandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced\nnatively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.\nAgents without a native sandbox may expose structured tool or MCP controls, but\nthose don't cover activity the agent performs outside those channels; some\nprocess adapters run entirely with your user permissions. Check the picker's\nProtection label. **Bivy does not currently ship its own OS-level jail.**\n\n## Credentials\n\nInteractive prompts, transcripts, and workspace files stay encrypted across the\nrelay. Credentials can remain on a Machine or in a vault you control:\n\n```bash\nbivy secrets list\nbivy secrets set github.repo-token\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\nbivy secrets doctor\n```\n\n`secret://`, `env://`, and `op://` (1Password) references resolve on demand when\nthe daemon provisions an agent run, so raw values never sit in your config.\n\n**One deliberate exception to relay blindness:** if you explicitly enable hosted\nunattended provisioning, Bivy Cloud may store encrypted cloud, repository,\nmodel, or key-escrow material that the service can technically access. Treat this\nas an explicit hosted-custody mode. See the\n[security model](docs/security-model.md#what-the-control-plane-sees) and\n[`docs/key-management.md`](docs/key-management.md).\n\n## Automations as code\n\nDefine governed jobs in `.bivy/automations.yaml`, validate them, and simulate\ntrigger events locally before applying anything:\n\n```bash\nbivy automation init\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml\nbivy automation apply\n```\n\nInstructions are encrypted on the applying node before upload. Safety policy\nlives beside the job — sandbox, approval mode, and a hard attempt ceiling that\nretry/fallback rules cannot exceed. See\n[`docs/automations-as-code.md`](docs/automations-as-code.md).\n\n## GitHub Runs\n\nLabel an issue `bivy` (or `bivy/<machine>` to target a Machine), or mention the\nBivy GitHub App in a comment. Bivy creates a Run on the selected Machine, uses an\nisolated worktree, executes configured checks, and reports an explicit outcome.\n\nCore applies no commercial usage limits. Bivy Cloud billing and commercial\npolicy live in the separate Cloud repository.\n\nA private GitHub App only installs on the account that owns it, so connect one\napp per GitHub account — one for your personal repos, one per organization\n(`bivy github:app-create --org <org>`). A node can serve several at once, each\nwith its own key and `@`-mention handle.\n\nSee [`docs/github-work-queue.md`](docs/github-work-queue.md).\n\n## Linear Runs\n\nApply `bivy` or `bivy/<machine>` to a Linear issue to create a Run on the selected\nMachine. The Machine fetches issue content directly from Linear, works in an\nisolated GitHub worktree, and asks the agent to open a pull request. See\n[`docs/linear-work-queue.md`](docs/linear-work-queue.md).\n\n## Development\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server (proxies /api and /ws to the node)\n```\n\nChecks — all of these run in CI:\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\nRepository layout:\n\n- `src/` — node daemon, runtime adapters, approvals, secrets, sessions\n- `bin/` — the `bivy` CLI\n- `packages/core` — shared protocol, pairing, wire format\n- `packages/web` — the React/Vite PWA client (`@bivy/web`)\n- `services/relay` — self-hostable relay\n- `services/control-plane` — self-hostable control plane\n- `deploy/` — self-host deployment examples\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## Self-hosting\n\nNode, relay, and control plane are all in this repository. Point a node at your\nown deployment by passing URLs to `bivy relay:setup` — re-running it switches an\nexisting node over to the new endpoints:\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nEach URL has a flag and an environment-variable equivalent (the flag wins):\n\n| Flag | Environment variable | Points at | Default |\n|---|---|---|---|\n| `--control-plane <url>` | `BIVY_CONTROL_PLANE_URL` | accounts, node registry, and the web-app API | hosted (`app.bivy.sh`) |\n| `--relay <wss-url>` | `BIVY_RELAY_URL` | the encrypted-frame relay your node dials out to | hosted |\n| `--client <url>` | `BIVY_CLIENT_BASE_URL` | base URL used when building app/PWA links | the `--control-plane` URL |\n\nSign-in defaults to GitHub device login (`--github`); pass\n`--email you@example.com` for an email magic-link, or `--session-token <token>`\nto skip interactive sign-in. `relay:setup` checks the control plane is reachable,\nenrolls this node, and writes the endpoints to `.bivy/relay.json`, so `bivy open`,\n`bivy link`, and `bivy update` all keep using your deployment afterwards.\n\n**Self-hosting is community-supported** — no SLA, best-effort help via GitHub\nissues. You own TLS, backups, upgrades, and hardening. Start with the\none-command VPS path in\n[`docs/self-host-quickstart.md`](docs/self-host-quickstart.md); the ops\nreference (backups, rotation, security boundary) is\n[`docs/self-host.md`](docs/self-host.md).\n\n## Security\n\nReport vulnerabilities through [GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new).\nPlease don't open a public issue. See [`SECURITY.md`](SECURITY.md) for scope,\nresponse times, and safe harbour, and [`docs/security-model.md`](docs/security-model.md)\nfor the trust model and known limitations.\n\n## License\n\nBivy Core is free and open-source software under the GNU Affero General Public\nLicense, version 3.0 only (AGPL-3.0-only). You may use, study, modify, and\nself-host it under that license. If you modify Bivy and let users interact with\nit over a network, section 13 requires you to offer them the corresponding\nsource code. See [`LICENSE`](LICENSE).\n\n**Where the open-core line is.** Everything in this repository — node, CLI,\nrelay, control plane, and the web/PWA client — is AGPL Core, with no usage\nlimits. **Bivy Cloud** is the hosted operation of that stack plus billing and\nplans, and lives in a separate private repository. Contributions are accepted\nunder the [DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
|
|
69
|
+
"readme": "# Bivy\n\n[](https://www.npmjs.com/package/@bivy/bivy)\n[](LICENSE)\n[](https://nodejs.org)\n\n**Run coding agents on the machines you already own — then reach them from your\nphone, browser, or another terminal.**\n\nStart Claude Code on your workstation, right where the repo, the running dev\nserver, and the staging database already live. Walk away. On the train, open\nyour phone: read what the agent did, answer its question, approve the migration\n— over a link only your devices can decrypt. The work never left your machine.\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # start an agent where your work lives\nbivy open # pick it up from your phone or browser\n```\n\nFirst thing to try: ask the agent to explain the repository, make one small safe\nchange, then open the same Session in the web app or on your phone while it runs.\n\n**[Quickstart](docs/quickstart.md)** ·\n**[Docs](docs/README.md)** ·\n**[Why Bivy](docs/why-bivy.md)** ·\n**[Security model](docs/security-model.md)** ·\n**[bivy.sh](https://bivy.sh)**\n\n> **0.x software.** The core loop is solid and used daily. Interfaces and\n> cross-runtime fidelity still change between releases — check the\n> [runtime support matrix](docs/runtime-support-matrix.md) before you depend on\n> a specific agent capability.\n\n## Why not just a cloud sandbox?\n\nA hosted sandbox starts from an *approximation* of your environment. A Bivy\nMachine **is** your environment — the actual working tree, the services already\nrunning, the caches already warm.\n\n| | Cloud sandbox | Bivy Machine |\n|---|---|---|\n| Your repository | a cloned copy | the real working tree, uncommitted changes and all |\n| Dev server & database | mocked, or absent | already running, right beside the agent |\n| Private networks & internal APIs | out of reach | reachable |\n| Toolchains, package caches | cold, reinstalled each time | warm, already installed |\n| GPUs / local inference | rented separately | the ones on your box |\n| Where your code sits | someone else's infrastructure | the machine you already trust |\n\nYou keep the environment. Bivy adds the part that was missing: **reaching that\nenvironment from anywhere, and leaving it working while you're gone.**\n\n## What you can do\n\nBivy gives you two ways to put an agent to work.\n\n### Sessions — interactive, and portable\n\nStart an agent, watch it work, jump in to steer, stop, or approve. Then leave\nyour desk and keep going:\n\n```bash\nbivy run claude # or codex, pi, gemini, and a dozen more\nbivy open # continue the same session in the browser or PWA\nbivy resume # pick it back up in the terminal\nbivy run claude --no-follow # start it in the background instead of attaching\nbivy run claude --chat # start the governed app session and open it in the browser\n```\n\n- **Reconnect from anywhere** — phone, browser, or another terminal — to the\n same live Session. The PWA adds voice input, read-aloud, phone-to-agent\n file/image uploads, and agent-to-phone attachments.\n- **Move work without starting over.** Import existing Claude Code and Codex\n Sessions, or fork, copy, and move a Bivy Session to another agent, model, or\n Machine.\n- **Run more than one Machine** — a workstation, a private-network server, a GPU\n box — on one account, and pick the environment each Session needs.\n\n### Runs — unattended, and accountable\n\nQueue one on demand, or let an event kick it off — either way it returns\nimmediately and reports back:\n\n```bash\nbivy runs start \"...\" # queue a one-off unattended Run, then `bivy runs wait <id>`\nbivy automation init # or define governed jobs in .bivy/automations.yaml\n```\n\n- **Trigger from real events** — a failed CI job, a GitHub or Linear issue,\n Slack, a schedule, or a signed webhook.\n- **Pin the guardrails** — Machine, agent, model, sandbox, approval mode, and a\n hard attempt ceiling — right next to the job.\n- **Get a Receipt** — every Run reports the checks it ran and how it turned out,\n not just a wall of output.\n\nTry the [capability recipes](docs/capability-recipes.md) to see each of these\nend to end, or the [runtime support matrix](docs/runtime-support-matrix.md) for\nexactly what each agent supports.\n\n## Bring your own stack\n\nUse provider subscriptions through native agent logins, API keys stored in\nBivy's vault, or local / OpenAI-compatible inference. Claude Code, Codex, and Pi\nhave first-class SDK integrations; any other ACP or headless agent needs no\nadapter at all — it's a data row you add with one command:\n\n```bash\nbivy agent add # register an existing ACP or process agent\n```\n\n## Install\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nmacOS and Linux. Requires Node.js 20 or newer. The installer puts the\n[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) npm package and the\n`bivy` command on your `PATH` with optional bridges skipped for speed, then runs\n`bivy setup` — agent choice, selected-agent install if needed, remote access,\nand an auto-start background service (launchd on macOS, systemd on Linux).\n\nIf Claude Code, Codex, OpenCode, Gemini, or another agent is already installed,\nsetup says so and uses that existing CLI, login, and configuration. Bivy does\nnot replace the agent or copy its native credentials; it adds remote continuity\nand governance around the agent you already use. Re-running the installer on a\nmachine that already has Bivy just applies the latest build and restarts the\nservice.\n\n**What needs an account, and what doesn't.** The CLI alone — `bivy run`,\n`bivy resume`, `bivy sessions` — needs no account and no server; `bivy setup`\nlets you pick **local only for now** and skip remote access. A browser or phone\nUI needs a control plane, because the node hosts none: use the hosted one at\n`app.bivy.sh` (sign in with GitHub or email; free tier plus a paid plan — see\n[bivy.sh#pricing](https://bivy.sh#pricing)) or\n[self-host your own](docs/self-host-quickstart.md). Switch any time with\n`bivy relay:setup`.\n\nPrefer to inspect the installer first?\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh -o install.sh\nless install.sh\nbash install.sh\n```\n\n**What the installer does with sudo.** It escalates only when it must, and\ntells you when it does:\n\n- Debian/Ubuntu without a suitable Node.js: `sudo apt-get install curl\n ca-certificates`, then NodeSource's Node 22 setup script via `sudo`.\n- Other Linux, or macOS, without a suitable Node.js: downloads the official\n Node 22 tarball from nodejs.org (sha256-checked) and installs it under\n `/usr/local` with `sudo`.\n- If npm's global prefix isn't writable it falls back to `~/.local` — it never\n runs `npm install` under `sudo`.\n- It appends a marked PATH block to `~/.bashrc` or `~/.zshrc`\n (`BIVY_NO_RC_UPDATE=1` to opt out).\n\nWant no sudo at all? Bring your own Node.js 20+ and skip the script:\n\n```bash\nnpm install -g @bivy/bivy && bivy setup # install globally\nnpx @bivy/bivy setup # or try it once, no install\n```\n\nReleases are published from CI with provenance attestations; verify a build's\norigin with `npm audit signatures`. See [`docs/releasing.md`](docs/releasing.md).\n\n### Your first session\n\nOnce `bivy setup` finishes, use Bivy from inside an existing repo. The first win\nis simple: start the local agent, give it a real task in that environment, then\nreopen the same Session from another surface.\n\n```bash\ncd your-repo\nbivy run claude # start an agent as a durable session in the current repo\n# Try: \"Explain this repo and suggest one small, safe improvement.\"\nbivy open # open that same session in the web app (needs relay setup)\nbivy resume # or pick it back up here in the terminal\n```\n\nFrom here the [quickstart](docs/quickstart.md) walks through Runs, multiple\nMachines, and automations.\n\n### Install options\n\nEnvironment variables passed to the one-line installer change what it does:\n\n| Goal | Variable |\n|---|---|\n| Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |\n| Pin an exact version | `BIVY_VERSION=0.1.0` |\n| Install the npm package into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |\n| Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |\n| Install optional Bivy bridges/native terminal dependency up front | `BIVY_INSTALL_OPTIONAL_DEPS=1` |\n| Don't touch `~/.bashrc` / `~/.zshrc`; print the PATH line instead | `BIVY_NO_RC_UPDATE=1` |\n\nFor example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.\n\nWorking from a checkout of this repository instead:\n\n```bash\npnpm install\npnpm run setup\n```\n\nSee [`docs/install.md`](docs/install.md) for where data lives, service\nmanagement, and uninstall.\n\n## Updating\n\n```bash\nbivy update\n```\n\n`bivy update` detects how Bivy was installed and does the right thing, then\nwaits for any active session to finish its current turn and restarts the\nbackground service so the node reconnects on the new build:\n\n| Install kind | What `bivy update` does |\n|---|---|\n| npm global (`npm i -g`) | `npm install -g @bivy/bivy@<channel>`, then restart the service |\n| installer / packaged | re-runs `install.sh` (migrating to npm if needed), then restart |\n| git checkout | `git pull --ff-only` + `pnpm install --frozen-lockfile`, then restart |\n| `npx` run | nothing to update — each run already fetches the latest |\n\nUpdates follow the release **channel** recorded at install time — `latest`\n(production) by default, or `staging` if you installed with\n`BIVY_CHANNEL=staging`. Switch channels (the choice is remembered for next\ntime), or skip the wait for a busy session:\n\n```bash\nbivy update --staging # move to the dev channel\nbivy update --stable # move back to production (latest)\nbivy update --force # don't wait for an in-flight turn to finish\n```\n\nThe daemon also checks the registry periodically and posts an in-session notice\nwhen a newer build is available.\n\n## Architecture\n\nBivy has three parts. **Only the first one holds your data.**\n\n```text\n your machine hosted or self-hosted\n\n ┌──────────────┐ ┌─────────┐ ┌───────────────┐\n │ node daemon │ ──dials──▶ │ relay │ ◀────▶ │ control plane │\n │ agents, keys │ outbound │ opaque │ │ accounts, web │\n │ repo, tools │ │ frames │ │ app, metadata │\n └──────────────┘ └─────────┘ └───────────────┘\n ▲ ▲\n └────────── end-to-end encrypted session ───────────┘\n phone · browser · another terminal\n```\n\n- **Node** — a daemon on your machine. Owns the workspace, credentials, and agent\n processes. Serves an API and WebSocket on `http://localhost:4317` plus a\n `/healthz` probe. **It hosts no web UI.**\n- **Relay** — forwards encrypted frames between your node and your devices. Your\n node dials out, so no inbound port is opened. The relay cannot read the frames.\n- **Control plane** — holds your account, node registry, and session index, and\n serves the web/PWA client. Use the hosted one or run your own.\n\nBecause the node serves no UI, a browser or phone needs a control plane — hosted\nat `app.bivy.sh`, or one you deploy yourself. The terminal CLI needs neither.\nInteractive Session traffic is end-to-end encrypted between a Machine and its\npaired devices: the relay never sees plaintext and cannot decrypt it. Who can\n*authorize* a device depends on how you pair — with a QR / `bivy link` pairing,\nor on a self-hosted deployment, the control plane can't read your Sessions\neither; with hosted account sign-in you trust the control plane to authorize\ndevices and to serve the web app that holds the keys. See\n[known limitations](docs/security-model.md#known-limitations-for-0x).\n\nSee [`docs/remote-access.md`](docs/remote-access.md) and\n[`docs/security-model.md`](docs/security-model.md).\n\n## Supported agents\n\n**Bivy maintains wrappers for the popular coding agents below.** Claude Code,\nCodex, Pi, and OpenCode have the richest release-tested capability sets today;\ncapabilities and fidelity still vary by runtime.\n\n| Agent | Command | Notes |\n|---|---|---|\n| Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |\n| Codex | `bivy run codex` | Installs `@openai/codex` |\n| Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |\n| OpenCode | `bivy run opencode` | Installs `opencode-ai` |\n| Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |\n| Qwen Code | `bivy run qwen` | Installs `@qwen-code/qwen-code` |\n| Goose | `bivy run goose` | Requires `goose` on PATH |\n| Aider | `bivy run aider` | No session resume (upstream gap) |\n| Cline | `bivy run cline` | Installs `cline` |\n| Crush | `bivy run crush` | No session resume (upstream gap) |\n| Cursor | `bivy run cursor` | ACP-capable |\n| GitHub Copilot | `bivy run copilot` | ACP-capable |\n| Grok | `bivy run grok` | Model selection |\n| Amp | `bivy run amp` | Native thread resume |\n| Auggie | `bivy run auggie` | Headless CLI |\n| Droid | `bivy run droid` | Model selection |\n| Continue | `bivy run continue` | Headless CLI |\n| Kilo Code | `bivy run kilocode` | ACP-capable |\n| Rovo Dev | `bivy run rovodev` | Installed out of band |\n\nAlso defined but hidden from the picker as *Experimental* — runnable via\n`BIVY_RUNTIME=<id>`: Codebuff (`codebuff`, no verified headless mode upstream\nyet), Hermes (`hermes`, generic process adapter), and OpenClaw (`openclaw`,\nCLI adapter only, no resume yet).\n\nAny other command works via `bivy run -- ./your-agent --flags`. ACP-capable\nagents can be promoted to Bivy's governed protocol path for per-tool approvals\nand native resume. To add a reusable process or ACP agent to both the CLI and web\npicker without changing Bivy, run `bivy agent add`, or scaffold and install a\ndeclarative [plugin manifest](docs/plugins.md) with `bivy plugin init`\n(declarative plugins are Experimental, `v1alpha1`, and run out of process).\n\n[`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly\nwhat each agent supports — resume, model selection, approvals, sandboxing.\n\n## Common commands\n\n```bash\nbivy # show the command overview\nbivy run claude # launch Claude Code as a durable session\nbivy run codex # run a different agent\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires relay setup)\nbivy automation init # create .bivy/automations.yaml\nbivy agent add # connect an existing ACP or process agent\nbivy plugin list # installed declarative integration packages\nbivy status # config summary and node reachability\nbivy doctor # health check\nbivy logs -f # tail node logs\nbivy update # update Bivy and restart the service\n```\n\nFull command list, flags, and examples: [`docs/cli-reference.md`](docs/cli-reference.md).\n\n## Configuration\n\nThe common knobs:\n\n```bash\nBIVY_WORKSPACE=/path/to/repo # default workspace\nBIVY_SANDBOX=read-only # read-only | workspace-write (default) | danger-full-access\nBIVY_APPROVAL_MODE=risky # never | risky | always | autonomous (default)\n```\n\nCreate and inspect the typed node configuration, or add repository-owned\nsafety/check/retry policy:\n\n```bash\nbivy config init\nbivy config set defaults.agent codex\nbivy config explain defaults.sandbox\nbivy config init --project # .bivy/policy.yaml\n```\n\nSee [`docs/config-as-code.md`](docs/config-as-code.md). Every environment\nvariable and precedence rule lives in\n[`docs/configuration.md`](docs/configuration.md).\n\n## Approvals and sandboxing\n\nThe default approval mode is **`autonomous`**: agents act without per-action\nprompts. How much that actually protects you depends on the runtime. Native-sandbox\nagents enforce the chosen access tier; structured runtimes also pass tool calls\nthrough Bivy's policy and approval layer. Process agents that Bivy cannot\nintercept run with your OS user permissions — the picker flags this and requires\nconfirmation before you pick that path.\n\nWhere Bivy receives structured shell/file calls, a heuristic floor blocks known\ncatastrophic commands and structured writes outside the workspace, and a\nbackstop set (force-push, publish, deploy, sudo) pauses for a human. This catches\naccidents; **it is not an adversarial isolation boundary.**\n\nWant to be asked about more? Set the mode explicitly:\n\n```bash\nBIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits\nBIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits\nBIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available\n```\n\nApprove from the terminal, browser, or phone.\n\nSandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced\nnatively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.\nAgents without a native sandbox may expose structured tool or MCP controls, but\nthose don't cover activity the agent performs outside those channels; some\nprocess adapters run entirely with your user permissions. Check the picker's\nProtection label. **Bivy does not currently ship its own OS-level jail.**\n\n## Credentials\n\nInteractive prompts, transcripts, and workspace files stay encrypted across the\nrelay. Credentials can remain on a Machine or in a vault you control:\n\n```bash\nbivy secrets list\nbivy secrets set github.repo-token\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\nbivy secrets doctor\n```\n\n`secret://`, `env://`, and `op://` (1Password) references resolve on demand when\nthe daemon provisions an agent run, so raw values never sit in your config.\n\n**One deliberate exception to relay blindness:** if you explicitly enable hosted\nunattended provisioning, Bivy Cloud may store encrypted cloud, repository,\nmodel, or key-escrow material that the service can technically access. Treat this\nas an explicit hosted-custody mode. See the\n[security model](docs/security-model.md#what-the-control-plane-sees) and\n[`docs/key-management.md`](docs/key-management.md).\n\n## Automations as code\n\nDefine governed jobs in `.bivy/automations.yaml`, validate them, and simulate\ntrigger events locally before applying anything:\n\n```bash\nbivy automation init\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml\nbivy automation apply\n```\n\nInstructions are encrypted on the applying node before upload. Safety policy\nlives beside the job — sandbox, approval mode, and a hard attempt ceiling that\nretry/fallback rules cannot exceed. See\n[`docs/automations-as-code.md`](docs/automations-as-code.md).\n\n## GitHub Runs\n\nLabel an issue `bivy` (or `bivy/<machine>` to target a Machine), or mention the\nBivy GitHub App in a comment. Bivy creates a Run on the selected Machine, uses an\nisolated worktree, executes configured checks, and reports an explicit outcome.\n\nCore applies no commercial usage limits. Bivy Cloud billing and commercial\npolicy live in the separate Cloud repository.\n\nA private GitHub App only installs on the account that owns it, so connect one\napp per GitHub account — one for your personal repos, one per organization\n(`bivy github:app-create --org <org>`). A node can serve several at once, each\nwith its own key and `@`-mention handle.\n\nSee [`docs/github-work-queue.md`](docs/github-work-queue.md).\n\n## Linear Runs\n\nApply `bivy` or `bivy/<machine>` to a Linear issue to create a Run on the selected\nMachine. The Machine fetches issue content directly from Linear, works in an\nisolated GitHub worktree, and asks the agent to open a pull request. See\n[`docs/linear-work-queue.md`](docs/linear-work-queue.md).\n\n## Development\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server (proxies /api and /ws to the node)\n```\n\nChecks — all of these run in CI:\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\nRepository layout:\n\n- `src/` — node daemon, runtime adapters, approvals, secrets, sessions\n- `bin/` — the `bivy` CLI\n- `packages/core` — shared protocol, pairing, wire format\n- `packages/web` — the React/Vite PWA client (`@bivy/web`)\n- `services/relay` — self-hostable relay\n- `services/control-plane` — self-hostable control plane\n- `deploy/` — self-host deployment examples\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## Self-hosting\n\nNode, relay, and control plane are all in this repository. Point a node at your\nown deployment by passing URLs to `bivy relay:setup` — re-running it switches an\nexisting node over to the new endpoints:\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nEach URL has a flag and an environment-variable equivalent (the flag wins):\n\n| Flag | Environment variable | Points at | Default |\n|---|---|---|---|\n| `--control-plane <url>` | `BIVY_CONTROL_PLANE_URL` | accounts, node registry, and the web-app API | hosted (`app.bivy.sh`) |\n| `--relay <wss-url>` | `BIVY_RELAY_URL` | the encrypted-frame relay your node dials out to | hosted |\n| `--client <url>` | `BIVY_CLIENT_BASE_URL` | base URL used when building app/PWA links | the `--control-plane` URL |\n\nSign-in defaults to GitHub device login (`--github`); pass\n`--email you@example.com` for an email magic-link, or `--session-token <token>`\nto skip interactive sign-in. `relay:setup` checks the control plane is reachable,\nenrolls this node, and writes the endpoints to `.bivy/relay.json`, so `bivy open`,\n`bivy link`, and `bivy update` all keep using your deployment afterwards.\n\n**Self-hosting is community-supported** — no SLA, best-effort help via GitHub\nissues. You own TLS, backups, upgrades, and hardening. Start with the\none-command VPS path in\n[`docs/self-host-quickstart.md`](docs/self-host-quickstart.md); the ops\nreference (backups, rotation, security boundary) is\n[`docs/self-host.md`](docs/self-host.md).\n\n## Security\n\nReport vulnerabilities through [GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new).\nPlease don't open a public issue. See [`SECURITY.md`](SECURITY.md) for scope,\nresponse times, and safe harbour, and [`docs/security-model.md`](docs/security-model.md)\nfor the trust model and known limitations.\n\n## License\n\nBivy Core is free and open-source software under the GNU Affero General Public\nLicense, version 3.0 only (AGPL-3.0-only). You may use, study, modify, and\nself-host it under that license. If you modify Bivy and let users interact with\nit over a network, section 13 requires you to offer them the corresponding\nsource code. See [`LICENSE`](LICENSE).\n\n**Where the open-core line is.** Everything in this repository — node, CLI,\nrelay, control plane, and the web/PWA client — is AGPL Core, with no usage\nlimits. **Bivy Cloud** is the hosted operation of that stack plus billing and\nplans, and lives in a separate private repository. Contributions are accepted\nunder the [DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
|
|
70
70
|
"readmeFilename": "README.md"
|
|
71
71
|
}
|