@bivy/bivy 0.19.4-staging.5 → 0.19.4-staging.7
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 +133 -105
- package/bin/bivy.mjs +114 -16
- package/bin/cli-commands.mjs +5 -1
- package/bin/guides/automate.md +28 -4
- package/dist/agent-instructions.js +2 -0
- package/dist/approval.js +1 -1
- package/dist/apps/styles.css +1 -1
- package/dist/auth.js +20 -1
- package/dist/automation-cli.js +23 -1
- package/dist/certification/agent-ux.js +50 -0
- package/dist/harness/mcp-config.js +4 -1
- package/dist/harness/mcp-serve-cli.js +6 -5
- package/dist/runtime/session-env.js +9 -1
- package/dist/server.js +77 -1
- package/dist/session/automation-proposals.js +87 -0
- package/dist/session/session-tokens.js +76 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,22 +4,27 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
**Local coding agents that show their work. Anywhere.**
|
|
8
|
+
|
|
9
|
+
Bivy is the open-source workspace for coding agents. It runs Claude Code, Codex,
|
|
10
|
+
Pi, OpenCode and other agents on your own machine. Reach them from a browser or
|
|
11
|
+
your phone, with the live session, its terminal, its approvals, and the app
|
|
12
|
+
they're building right in the chat. Try it, mark what's wrong, and send it back.
|
|
13
|
+
|
|
14
|
+
Bivy is not another coding agent and not a cloud development machine. The agent
|
|
15
|
+
stays local and does the coding with your model provider. Your repos, tools,
|
|
16
|
+
databases, and services stay where they are. Bivy gives you remote access to all
|
|
17
|
+
of it, plus live previews, automations, and review.
|
|
18
|
+
|
|
19
|
+
- **Live previews.** The running app beside the chat. Mark it, send it back,
|
|
20
|
+
share it with someone who has no Bivy account.
|
|
21
|
+
- **Automations.** GitHub issues, failed CI, Linear, Slack, schedules, and
|
|
22
|
+
webhooks start the work. It comes back as a preview link or a pull request.
|
|
23
|
+
- **Any agent.** Hit a usage limit? Fork the session to another agent, or let
|
|
24
|
+
Bivy retry when the limit resets.
|
|
25
|
+
- **Your machine, anywhere.** Start at your desk, pick the session up on your
|
|
26
|
+
phone. The machine dials out, so there are no ports to open, and session
|
|
27
|
+
traffic is end-to-end encrypted.
|
|
23
28
|
|
|
24
29
|
**[Start free on Bivy Cloud](https://app.bivy.sh)** ·
|
|
25
30
|
**[Quickstart](docs/quickstart.md)** ·
|
|
@@ -47,30 +52,34 @@ agents. Connect a Mac, Linux computer, or existing server and bring your own
|
|
|
47
52
|
agent subscription, model API key, or local model. You can also self-host the
|
|
48
53
|
entire remote-access stack.
|
|
49
54
|
|
|
50
|
-
> **Bivy is 0.x software.** Claude Code, Codex, Pi, and
|
|
55
|
+
> **Bivy is 0.x software.** Claude Code, Codex, Pi, OpenCode, and Grok are the
|
|
51
56
|
> release-tested paths. Credential sync, resume, handoffs, approvals, and
|
|
52
57
|
> sandboxing depend on the runtime. See the
|
|
53
58
|
> [runtime support matrix](docs/runtime-support-matrix.md).
|
|
54
59
|
|
|
55
|
-
##
|
|
60
|
+
## Don't just read the diff. Try the app.
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
Bivy lets you **use what the agent built and show it what to fix**, without a
|
|
63
|
+
separate deployment step.
|
|
59
64
|
|
|
60
65
|
1. **Ask for a change.** The agent works in your repo with your existing tools
|
|
61
66
|
and local services.
|
|
62
|
-
2. **Try the app.**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
3. **Mark
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
4. **
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
2. **Try the app.** Bivy finds the dev server the agent starts and opens it
|
|
68
|
+
beside the chat: web apps, terminals, and desktop apps. Use the running app,
|
|
69
|
+
not a screenshot of what the agent says it finished.
|
|
70
|
+
3. **Mark what needs work.** Press and hold anything in the preview to mark it,
|
|
71
|
+
or drag to circle it, then say what you want. **Mark another** keeps
|
|
72
|
+
separate notes separate. The marks go to the agent with the element and page
|
|
73
|
+
context. No screenshots to paste.
|
|
74
|
+
4. **Follow each note.** Every mark stays in the chat as a pin that resolves
|
|
75
|
+
itself: **Changed** when a later run changes what you marked, **Element
|
|
76
|
+
gone** when it leaves the page, **Done** when you say so.
|
|
77
|
+
5. **Review the next version.** A new version waits on the preview pill instead
|
|
78
|
+
of reloading under you. Take it when you're ready, compare before and after,
|
|
79
|
+
inspect the diff and checks, and decide when the work is done.
|
|
80
|
+
6. **Get a second opinion.** Share a preview link for 1 hour, 1 day, or 7 days.
|
|
81
|
+
Teammates and clients try the running app and leave notes without a Bivy
|
|
82
|
+
account. Make it view-only, or **Stop sharing** to end every link at once.
|
|
74
83
|
|
|
75
84
|
On Bivy Cloud, preview delivery is built in: no per-app domains, certificates,
|
|
76
85
|
public ports, or tunnels to configure. Self-hosters configure preview delivery
|
|
@@ -79,27 +88,13 @@ build or dev-server setup, and **the machine serving it must stay awake and
|
|
|
79
88
|
online**. These are development previews, not production hosting.
|
|
80
89
|
|
|
81
90
|
**Share deliberately:** anyone with a preview link can use that app, including
|
|
82
|
-
its live backend
|
|
91
|
+
its live backend, until the link expires or you stop sharing.
|
|
83
92
|
Reviewer notes aren't sent to the agent automatically; you send them or explicitly
|
|
84
93
|
allow agent access. Preview traffic uses HTTPS through the preview relay, not
|
|
85
94
|
session end-to-end encryption; the relay operator can see it.
|
|
86
95
|
|
|
87
96
|
[App previews, visual feedback, and sharing →](docs/apps.md)
|
|
88
97
|
|
|
89
|
-
## Your agents and machines, one workspace
|
|
90
|
-
|
|
91
|
-
| Capability | What it means for you |
|
|
92
|
-
|---|---|
|
|
93
|
-
| **One workspace, multiple agents** | Use different agents and models for different tasks without maintaining a separate workflow for each. |
|
|
94
|
-
| **Your machines and environment** | Work beside your existing repos, dev servers, databases, private networks, toolchains, and GPUs. |
|
|
95
|
-
| **Automations and triggers** | Let issues, failed CI, messages, schedules, and webhooks start work instead of copying requests into a chat. |
|
|
96
|
-
| **Encrypted key and OAuth sync** | Reuse Bivy-managed provider credentials across enrolled machines and compatible runtimes, with less repeated setup. |
|
|
97
|
-
| **Live sessions from anywhere** | Build and review from your phone, browser, or terminal; answer questions and approve supported actions in the same session. |
|
|
98
|
-
| **Live app previews** | Try the running app without deploying it to a separate hosting service. |
|
|
99
|
-
| **Visual feedback and sharing** | Point, draw, and send specific feedback to the agent; share a preview link for outside review. |
|
|
100
|
-
| **Reviewable results** | Inspect the app, changes, declared checks, artifacts, and pull requests—not just an agent's claim that it finished. |
|
|
101
|
-
| **Hosted convenience or self-hosting** | Use Bivy Cloud for managed remote access, or run the same open-source core yourself. |
|
|
102
|
-
|
|
103
98
|
## One workflow, from trigger to review
|
|
104
99
|
|
|
105
100
|
```text
|
|
@@ -131,34 +126,70 @@ creates runs when an event matches.
|
|
|
131
126
|
Manual and automated work use the same kind of live session. You can join a run
|
|
132
127
|
when it needs help rather than wait for a black-box job to finish.
|
|
133
128
|
|
|
134
|
-
###
|
|
129
|
+
### Let events start the work
|
|
135
130
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
Automations turn recurring or incoming work into sessions you can join,
|
|
132
|
+
supervise, and review. An agent picks the task up on your machine and posts
|
|
133
|
+
back a preview link or a pull request, so you try the result where the task
|
|
134
|
+
lives and reply in the same session. Choose the repository, machine, agent,
|
|
135
|
+
model, approval mode, sandbox setting, and maximum attempts.
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
| Trigger | Example workflow |
|
|
138
|
+
|---|---|
|
|
139
|
+
| **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |
|
|
140
|
+
| **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |
|
|
141
|
+
| **Linear** | Label an issue to start work without copying its description into an agent. |
|
|
142
|
+
| **Slack** | Send a request from the conversation where the work came up. |
|
|
143
|
+
| **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |
|
|
144
|
+
| **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
Configure automations in the app or version them with your repository in
|
|
147
|
+
`.bivy/automations.yaml`:
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
```bash
|
|
150
|
+
bivy automation init
|
|
151
|
+
# Edit the generated definition for your repository and workflow.
|
|
152
|
+
bivy automation validate
|
|
153
|
+
bivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture
|
|
154
|
+
bivy automation apply
|
|
155
|
+
```
|
|
150
156
|
|
|
151
|
-
|
|
157
|
+
Or delegate a one-off job without creating an automation:
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
```bash
|
|
160
|
+
bivy runs start "Review outdated dependencies and propose a small, tested update."
|
|
161
|
+
bivy runs wait <id>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Runs keep routing and lifecycle evidence, check results, and output references
|
|
165
|
+
in a reviewable Receipt. For unattended issue work, Bivy runs declared repository
|
|
166
|
+
checks after the agent's turn; failed required checks fail the run even if the
|
|
167
|
+
agent reports success. A completed process alone is not proof that the task
|
|
168
|
+
succeeded.
|
|
156
169
|
|
|
170
|
+
[Automation recipes →](docs/capability-recipes.md#let-events-start-runs) ·
|
|
171
|
+
[Automations as code →](docs/automations-as-code.md) ·
|
|
172
|
+
[Run outcomes and reliability limits →](docs/automation-runs.md)
|
|
173
|
+
|
|
174
|
+
### Use every agent you pay for. Switch mid-task.
|
|
175
|
+
|
|
176
|
+
Run Claude Code for one task, Codex for another, and Pi or OpenCode where they
|
|
177
|
+
fit, side by side in one session list. Bivy supplies the shared session,
|
|
178
|
+
remote-access, automation, and review surfaces; your chosen agent still does
|
|
179
|
+
the coding and uses your model provider.
|
|
180
|
+
|
|
181
|
+
- **Hit a usage limit? Keep going.** When a turn fails on a limit, fork the
|
|
182
|
+
session, with its code and conversation, to another agent and model, or let
|
|
183
|
+
Bivy retry when the limit resets (the provider has to report a reset time).
|
|
184
|
+
- **Get a second opinion.** `bivy delegate` hands a task to another agent, on
|
|
185
|
+
the same machine or a different one, and brings back its answer, branch, and
|
|
186
|
+
PR. `--to codex,claude@linux` sends one task to several agents to compare.
|
|
157
187
|
- Choose an agent and, where supported, a model for each session or run.
|
|
158
188
|
- Import existing Claude Code and Codex sessions.
|
|
159
189
|
- Fork or move work to another agent or machine when a different setup fits
|
|
160
190
|
better. Continuation fidelity varies: some paths preserve native history,
|
|
161
|
-
while others replay portable turns or
|
|
191
|
+
while others replay portable turns or hand over a summary with the earlier
|
|
192
|
+
transcript as a local file.
|
|
162
193
|
- Use agent-native logins, Bivy-managed credentials, or local inference.
|
|
163
194
|
Bivy's custom OpenAI-compatible endpoint registry currently feeds Pi;
|
|
164
195
|
other agents may need their own provider configuration.
|
|
@@ -199,48 +230,22 @@ hosted-provisioning custody grants are separate from ordinary encrypted sync.
|
|
|
199
230
|
[Credentials guide →](docs/credentials-guide.md) ·
|
|
200
231
|
[Key storage →](docs/key-management.md)
|
|
201
232
|
|
|
202
|
-
###
|
|
203
|
-
|
|
204
|
-
Automations turn recurring or incoming work into sessions you can join,
|
|
205
|
-
supervise, and review. Choose the repository, machine, agent, model, approval
|
|
206
|
-
mode, sandbox setting, and maximum attempts.
|
|
207
|
-
|
|
208
|
-
| Trigger | Example workflow |
|
|
209
|
-
|---|---|
|
|
210
|
-
| **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |
|
|
211
|
-
| **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |
|
|
212
|
-
| **Linear** | Label an issue to start work without copying its description into an agent. |
|
|
213
|
-
| **Slack** | Send a request from the conversation where the work came up. |
|
|
214
|
-
| **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |
|
|
215
|
-
| **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |
|
|
216
|
-
|
|
217
|
-
Configure automations in the app or version them with your repository in
|
|
218
|
-
`.bivy/automations.yaml`:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
bivy automation init
|
|
222
|
-
# Edit the generated definition for your repository and workflow.
|
|
223
|
-
bivy automation validate
|
|
224
|
-
bivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture
|
|
225
|
-
bivy automation apply
|
|
226
|
-
```
|
|
233
|
+
### Work in the environment you already have
|
|
227
234
|
|
|
228
|
-
|
|
235
|
+
A clean cloud sandbox isn't always enough. Your agent may need the database
|
|
236
|
+
running on localhost, an uncommitted change, an internal API behind your VPN,
|
|
237
|
+
or a model running on your GPU. Bivy runs the agent where those things already
|
|
238
|
+
exist, subject to that machine's permissions and the runtime's protection.
|
|
229
239
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
Connect several machines to the same account: a laptop for interactive work,
|
|
241
|
+
a Linux server for background jobs, or a GPU box for local inference. Choose
|
|
242
|
+
the machine for each session or pin it in an automation. Repository runs can
|
|
243
|
+
use isolated Git worktrees without rebuilding the whole development environment.
|
|
234
244
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
checks after the agent's turn; failed required checks fail the run even if the
|
|
238
|
-
agent reports success. A completed process alone is not proof that the task
|
|
239
|
-
succeeded.
|
|
245
|
+
**The execution machine must stay awake and online.** To close your laptop and
|
|
246
|
+
leave work running, run the agent on a different, always-on machine.
|
|
240
247
|
|
|
241
|
-
[
|
|
242
|
-
[Automations as code →](docs/automations-as-code.md) ·
|
|
243
|
-
[Run outcomes and reliability limits →](docs/automation-runs.md)
|
|
248
|
+
[Environment and multi-machine recipes →](docs/capability-recipes.md)
|
|
244
249
|
|
|
245
250
|
### Start at your desk. Continue anywhere.
|
|
246
251
|
|
|
@@ -269,6 +274,29 @@ in your browser; adding it to your home screen is optional.
|
|
|
269
274
|
[Remote access →](docs/remote-access.md) ·
|
|
270
275
|
[Voice, files, and terminal recipes →](docs/capability-recipes.md)
|
|
271
276
|
|
|
277
|
+
### Agents can reach you, too
|
|
278
|
+
|
|
279
|
+
Every session gets the `bivy` CLI, so any agent with a shell can talk back
|
|
280
|
+
through the app, not only agents with their own built-in tools:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
bivy notify "Tests are green, PR is up" # chat card + phone push when you're away
|
|
284
|
+
bivy ask "Ship to staging?" --option Yes --option No # waits for your answer
|
|
285
|
+
bivy attach report.png --caption "Before/after" # show a file in the chat
|
|
286
|
+
bivy suggest "Add a dark theme to settings" # a next task you start in one tap
|
|
287
|
+
bivy context --json # session, workspace, machine, apps
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`bivy help --json` lists every command, and with `BIVY_OUTPUT=json` failures
|
|
291
|
+
come back as structured errors with distinct exit codes, so an agent can tell
|
|
292
|
+
what went wrong and what to run next. `bivy guide` prints short playbooks for
|
|
293
|
+
agents, and the same commands are served as MCP tools for agents that prefer
|
|
294
|
+
them. Add your own standing instructions for
|
|
295
|
+
every session in **Settings → Agent instructions**.
|
|
296
|
+
|
|
297
|
+
[Commands for agents inside a session →](docs/cli-reference.md#inside-an-agent-session) ·
|
|
298
|
+
[Agent instructions →](docs/agent-instructions.md)
|
|
299
|
+
|
|
272
300
|
## Get started
|
|
273
301
|
|
|
274
302
|
### Install
|
|
@@ -404,9 +432,9 @@ for the task, especially before enabling unattended work.
|
|
|
404
432
|
|
|
405
433
|
## Agents and everyday commands
|
|
406
434
|
|
|
407
|
-
**Claude Code, Codex, Pi, and
|
|
408
|
-
include Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub
|
|
409
|
-
|
|
435
|
+
**Claude Code, Codex, Pi, OpenCode, and Grok are release-tested.** Additional
|
|
436
|
+
adapters include Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub
|
|
437
|
+
Copilot, Amp, Auggie, Droid, Continue, Kilo Code, and Rovo Dev. Installation,
|
|
410
438
|
resume, model selection, and tool protection vary—see the
|
|
411
439
|
[support matrix](docs/runtime-support-matrix.md) and [agent guides](docs/agents/README.md).
|
|
412
440
|
|
package/bin/bivy.mjs
CHANGED
|
@@ -511,7 +511,19 @@ function nativeAgentAuthDetected(choice) {
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
function url(config) {
|
|
514
|
-
|
|
514
|
+
// BIVY_NODE_URL: set by `bivy mcp-serve` for the CLI it runs, so tools reach
|
|
515
|
+
// the node that started the agent whatever data dir this process resolves.
|
|
516
|
+
const override = process.env.BIVY_NODE_URL?.trim().replace(/\/+$/, "");
|
|
517
|
+
return override || `http://localhost:${config.port}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// The credential for a request about `sessionId`: the agent's own session token
|
|
521
|
+
// ($BIVY_SESSION_TOKEN) when it is that session, else a device token when this
|
|
522
|
+
// user can mint one, else none (loopback on a single-user host needs none).
|
|
523
|
+
async function sessionAuth(config, sessionId) {
|
|
524
|
+
const own = resolveAttachSessionId({ env: process.env });
|
|
525
|
+
if (process.env.BIVY_SESSION_TOKEN && sessionId && sessionId === own) return process.env.BIVY_SESSION_TOKEN;
|
|
526
|
+
try { return await localDeviceToken(config); } catch { return undefined; }
|
|
515
527
|
}
|
|
516
528
|
|
|
517
529
|
// The host the node will bind (mirrors src/server.ts). We probe on this same
|
|
@@ -2424,8 +2436,7 @@ async function cmdAttach(args = []) {
|
|
|
2424
2436
|
// (loopback bypasses auth), but include one when available so multi-user
|
|
2425
2437
|
// hosts work too.
|
|
2426
2438
|
async function sessionPost(config, sessionId, action, body) {
|
|
2427
|
-
|
|
2428
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2439
|
+
const token = await sessionAuth(config, sessionId);
|
|
2429
2440
|
return fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/${action}`, {
|
|
2430
2441
|
method: "POST",
|
|
2431
2442
|
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
@@ -2437,7 +2448,7 @@ async function sessionPost(config, sessionId, action, body) {
|
|
|
2437
2448
|
function sessionHttpError(what, status, body) {
|
|
2438
2449
|
const message = `${what} failed (${status}): ${body?.error || "unknown error"}`;
|
|
2439
2450
|
if (status === 404) return { code: "session_not_found", message, hint: "The session must be open on this node.", next: "bivy context", exit: EXIT.notFound };
|
|
2440
|
-
if (status === 401 || status === 403) return { code: "denied", message, exit: EXIT.denied };
|
|
2451
|
+
if (status === 401 || status === 403) return { code: "denied", message, hint: "A session's token only reaches that session; run it from the session's own shell, or from a terminal signed in to this node.", exit: EXIT.denied };
|
|
2441
2452
|
if (status === 400) return { code: "invalid", message, exit: EXIT.usage };
|
|
2442
2453
|
return { code: "failed", message, exit: EXIT.failed };
|
|
2443
2454
|
}
|
|
@@ -2472,8 +2483,7 @@ async function cmdContext(args = []) {
|
|
|
2472
2483
|
let node = { session: null };
|
|
2473
2484
|
if (sessionId) {
|
|
2474
2485
|
const config = loadConfig();
|
|
2475
|
-
|
|
2476
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2486
|
+
const token = await sessionAuth(config, sessionId);
|
|
2477
2487
|
try {
|
|
2478
2488
|
const res = await fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/context`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
|
|
2479
2489
|
const body = await res.json().catch(() => ({}));
|
|
@@ -2631,8 +2641,7 @@ answer later and 'bivy ask status <id>' checks without waiting.
|
|
|
2631
2641
|
if (!Number.isFinite(timeout) || timeout < 1) return fail({ code: "usage", message: "--timeout takes seconds or a duration like 90s, 10m or 2h.", exit: EXIT.usage });
|
|
2632
2642
|
const config = loadConfig();
|
|
2633
2643
|
if (!(await ensureNodeRunning(config))) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable });
|
|
2634
|
-
|
|
2635
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2644
|
+
const token = await sessionAuth(config, sessionId);
|
|
2636
2645
|
const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/ask`;
|
|
2637
2646
|
const call = async (suffix, body) => {
|
|
2638
2647
|
const res = await fetch(`${base}${suffix}`, {
|
|
@@ -2686,11 +2695,10 @@ answer later and 'bivy ask status <id>' checks without waiting.
|
|
|
2686
2695
|
|
|
2687
2696
|
// One JSON request to the local node for the commands below: starts the node if
|
|
2688
2697
|
// needed and returns { status, body }, or fails in the shared error shape.
|
|
2689
|
-
async function nodeRequest(method, pathname, body, { json = false } = {}) {
|
|
2698
|
+
async function nodeRequest(method, pathname, body, { json = false, sessionId } = {}) {
|
|
2690
2699
|
const config = loadConfig();
|
|
2691
2700
|
if (!(await ensureNodeRunning(config))) return cliError({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable }, { json, paint: c.red });
|
|
2692
|
-
|
|
2693
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2701
|
+
const token = await sessionAuth(config, sessionId);
|
|
2694
2702
|
const res = await fetch(`${url(config)}${pathname}`, {
|
|
2695
2703
|
method,
|
|
2696
2704
|
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
@@ -2719,7 +2727,7 @@ async function cmdFork(args = []) {
|
|
|
2719
2727
|
const modelArg = parsed.one("--model");
|
|
2720
2728
|
const slash = modelArg?.indexOf("/") ?? -1;
|
|
2721
2729
|
const model = modelArg ? (slash > 0 ? { provider: modelArg.slice(0, slash), id: modelArg.slice(slash + 1) } : { provider: "", id: modelArg }) : undefined;
|
|
2722
|
-
const { status, body } = await nodeRequest("POST", "/api/session/fork", { sessionId, ...(model ? { model } : {}) }, { json });
|
|
2730
|
+
const { status, body } = await nodeRequest("POST", "/api/session/fork", { sessionId, ...(model ? { model } : {}) }, { json, sessionId });
|
|
2723
2731
|
if (status === 404) return fail({ code: "node_outdated", message: "This node is older than 'bivy fork'.", next: "bivy update", exit: EXIT.unavailable });
|
|
2724
2732
|
if (status >= 400 || body.error) return fail({ code: /not found/i.test(body.error ?? "") ? "session_not_found" : "failed", message: `Fork failed: ${body.error || `HTTP ${status}`}`, exit: /not found/i.test(body.error ?? "") ? EXIT.notFound : EXIT.failed });
|
|
2725
2733
|
if (json) { console.log(JSON.stringify({ sessionId: body.sessionId, fidelity: body.fidelity, ...(body.missing?.length ? { missing: body.missing } : {}) })); return; }
|
|
@@ -2818,6 +2826,62 @@ async function cmdInstructions(args = []) {
|
|
|
2818
2826
|
fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2819
2827
|
}
|
|
2820
2828
|
|
|
2829
|
+
// `bivy automation apply` inside an agent session: the node shows the user what
|
|
2830
|
+
// would change as an approval card and applies it only once they approve, with
|
|
2831
|
+
// this machine's account credential. `proposal <id> [--wait]` follows one.
|
|
2832
|
+
async function cmdAutomationProposal(args = []) {
|
|
2833
|
+
const usage = "Usage (in an agent session): bivy automation apply [path] [--prune] [--timeout 90] [--json]\n bivy automation proposal <id> [--wait] [--timeout 90] [--json]";
|
|
2834
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2835
|
+
console.log(`${usage}\n\nInside an agent session, apply goes through this machine's approval mode: usually the user gets an approval card listing what would change and decides; with "never ask" it applies at once. The chat records what was applied. Waits up to --timeout seconds (default 90) for the answer; if it hasn't come, prints the proposal id to check later with 'bivy automation proposal <id> --wait'. The card stays open for 30 minutes. Validate and plan first ('bivy automation validate', 'plan').`);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
const json = wantsJson(args);
|
|
2839
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2840
|
+
const parsed = parseSessionArgs(args, ["--timeout", "--session"]);
|
|
2841
|
+
if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
|
|
2842
|
+
const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
|
|
2843
|
+
if (!sessionId) return fail({ code: "no_session", message: "No session id.", next: "bivy sessions --json", exit: EXIT.usage });
|
|
2844
|
+
const timeout = parsed.one("--timeout") === undefined ? 90 : durationSeconds(parsed.one("--timeout"));
|
|
2845
|
+
if (!Number.isFinite(timeout) || timeout < 0) return fail({ code: "usage", message: "--timeout takes seconds or a duration like 90s or 10m.", exit: EXIT.usage });
|
|
2846
|
+
const base = `/api/session/${encodeURIComponent(sessionId)}/automations/apply`;
|
|
2847
|
+
const [action, target] = parsed.positional;
|
|
2848
|
+
let proposal;
|
|
2849
|
+
if (action === "proposal") {
|
|
2850
|
+
if (!target) return fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2851
|
+
const { status, body } = await nodeRequest("GET", `${base}/${encodeURIComponent(target)}`, undefined, { json, sessionId });
|
|
2852
|
+
if (status >= 400) return fail({ code: status === 404 ? "not_found" : "failed", message: body.error || `HTTP ${status}`, exit: status === 404 ? EXIT.notFound : EXIT.failed });
|
|
2853
|
+
proposal = body;
|
|
2854
|
+
if (!parsed.has("--wait")) { if (json) console.log(JSON.stringify(proposal)); else console.log(`Proposal ${proposal.id}: ${proposal.status}`); return; }
|
|
2855
|
+
} else {
|
|
2856
|
+
const file = path.resolve(process.cwd(), target ?? ".bivy/automations.yaml");
|
|
2857
|
+
const { status, body } = await nodeRequest("POST", base, { file, prune: parsed.has("--prune") }, { json, sessionId });
|
|
2858
|
+
if (status === 404 && !body.error?.includes("Session")) return fail({ code: "node_outdated", message: "This node is older than proposing automations from a session.", next: "bivy update", exit: EXIT.unavailable });
|
|
2859
|
+
if (status >= 400) return fail(sessionHttpError("Proposing the automations", status, body));
|
|
2860
|
+
proposal = body;
|
|
2861
|
+
if (!json) {
|
|
2862
|
+
console.error(`Changes to apply (${proposal.file}):`);
|
|
2863
|
+
for (const change of proposal.changes) console.error(` ${change.action === "create" ? "+" : change.action === "remove" ? "-" : "~"} ${change.id}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
const deadline = Date.now() + timeout * 1000;
|
|
2867
|
+
let polls = 0;
|
|
2868
|
+
while (proposal.status === "pending" && Date.now() < deadline) {
|
|
2869
|
+
await new Promise((resolve) => setTimeout(resolve, polls === 0 ? 500 : 2000));
|
|
2870
|
+
if (polls++ === 1 && !json) console.error("Waiting for the user to approve them…");
|
|
2871
|
+
const { status, body } = await nodeRequest("GET", `${base}/${encodeURIComponent(proposal.id)}`, undefined, { json, sessionId });
|
|
2872
|
+
if (status < 400) proposal = body;
|
|
2873
|
+
}
|
|
2874
|
+
if (json) console.log(JSON.stringify(proposal));
|
|
2875
|
+
if (proposal.status === "applied") { if (!json) console.log(proposal.output || "Applied."); return; }
|
|
2876
|
+
if (proposal.status === "pending") {
|
|
2877
|
+
if (!json) console.log(`The user hasn't answered yet. Check later with: bivy automation proposal ${proposal.id} --wait`);
|
|
2878
|
+
process.exit(EXIT.timeout);
|
|
2879
|
+
}
|
|
2880
|
+
const message = { rejected: "The user declined these automation changes.", expired: "Nobody answered within 30 minutes; nothing was applied.", failed: `Approved, but applying failed: ${proposal.error || "unknown error"}` }[proposal.status] ?? proposal.status;
|
|
2881
|
+
if (!json) console.error(message);
|
|
2882
|
+
process.exit(proposal.status === "expired" ? EXIT.timeout : EXIT.failed);
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2821
2885
|
// Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
|
|
2822
2886
|
// self-contained task from this session to another agent, optionally on another
|
|
2823
2887
|
// account machine, and get its answer back. Agent-agnostic — any agent with a
|
|
@@ -2854,8 +2918,7 @@ async function cmdDelegate(args = []) {
|
|
|
2854
2918
|
if (!sessionId) { console.error(c.red("No session id. Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.")); process.exit(1); return; }
|
|
2855
2919
|
const config = loadConfig();
|
|
2856
2920
|
if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
|
|
2857
|
-
|
|
2858
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2921
|
+
const token = await sessionAuth(config, sessionId);
|
|
2859
2922
|
const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/delegated-runs`;
|
|
2860
2923
|
const call = async (path, body) => {
|
|
2861
2924
|
const res = await fetch(`${base}${path}`, {
|
|
@@ -3136,9 +3199,9 @@ async function appMenu(rest, { json = false } = {}) {
|
|
|
3136
3199
|
async function appRequest(action, body, { print = true } = {}) {
|
|
3137
3200
|
const config = loadConfig();
|
|
3138
3201
|
if (!(await ensureNodeRunning(config))) throw new Error("Could not reach the Bivy node.");
|
|
3139
|
-
const token = await
|
|
3202
|
+
const token = await sessionAuth(config, body.sessionId);
|
|
3140
3203
|
const response = await fetch(`${url(config)}/api/apps/${action}`, {
|
|
3141
|
-
method: "POST", headers: { authorization: `Bearer ${token}
|
|
3204
|
+
method: "POST", headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), "content-type": "application/json" }, body: JSON.stringify(body),
|
|
3142
3205
|
});
|
|
3143
3206
|
const result = await response.json();
|
|
3144
3207
|
if (!response.ok || result.error) throw new Error(result.error || `App command failed (${response.status}).`);
|
|
@@ -5821,6 +5884,22 @@ function readSelfVersion() {
|
|
|
5821
5884
|
}
|
|
5822
5885
|
}
|
|
5823
5886
|
|
|
5887
|
+
// A command an agent tried that doesn't exist is the best hint for what it
|
|
5888
|
+
// expected Bivy to have. Inside a session, keep the word (never its arguments)
|
|
5889
|
+
// in <data-dir>/agent-cli-misses.jsonl for `pnpm run eval:agent-ux` and
|
|
5890
|
+
// whoever names the next command. Bounded: one rotation at 256 KiB.
|
|
5891
|
+
function recordAgentMiss(word) {
|
|
5892
|
+
const session = resolveAttachSessionId({ env: process.env });
|
|
5893
|
+
if (!session || typeof word !== "string" || word.length > 64) return;
|
|
5894
|
+
try {
|
|
5895
|
+
const file = path.join(appDir, "agent-cli-misses.jsonl");
|
|
5896
|
+
if (fs.existsSync(file) && fs.statSync(file).size > 256 * 1024) fs.renameSync(file, `${file}.1`);
|
|
5897
|
+
fs.appendFileSync(file, `${JSON.stringify({ ts: Date.now(), session, word })}\n`, { mode: 0o600 });
|
|
5898
|
+
} catch {
|
|
5899
|
+
// Best-effort: a miss log must never change what the command does.
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5824
5903
|
function printHelp() {
|
|
5825
5904
|
const details = { run: `Agents: ${[...AGENT_INTEGRATIONS.keys()].join(" | ")}, or -- <command>` };
|
|
5826
5905
|
console.log(`\n${renderHelp({ paint: c.cyan, bold: c.bold, details })}\n`);
|
|
@@ -5898,13 +5977,27 @@ function cmdGuide(args = []) {
|
|
|
5898
5977
|
console.log(guide.markdown.trimEnd());
|
|
5899
5978
|
}
|
|
5900
5979
|
|
|
5980
|
+
// Inside an agent session the daemon names its own CLI ($BIVY_NODE_CLI). When
|
|
5981
|
+
// `bivy` was found on PATH (every installer links it by that name) and is
|
|
5982
|
+
// another file, run the node's, so the agent gets the commands its node
|
|
5983
|
+
// documents. Running a file on purpose (`node …/bin/bivy.mjs`) is left alone.
|
|
5984
|
+
function nodeCliToRun() {
|
|
5985
|
+
const nodeCli = process.env.BIVY_NODE_CLI;
|
|
5986
|
+
if (path.basename(process.argv[1] ?? "") !== "bivy") return undefined;
|
|
5987
|
+
if (!nodeCli || !path.isAbsolute(nodeCli) || !fs.existsSync(nodeCli)) return undefined;
|
|
5988
|
+
try { return fs.realpathSync(nodeCli) === fs.realpathSync(selfScript) ? undefined : nodeCli; } catch { return undefined; }
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5901
5991
|
async function main() {
|
|
5992
|
+
const nodeCli = nodeCliToRun();
|
|
5993
|
+
if (nodeCli) process.exit(spawnSync(process.execPath, [nodeCli, ...process.argv.slice(2)], { stdio: "inherit", env: process.env }).status ?? 1);
|
|
5902
5994
|
await hydrateCanonicalConfig();
|
|
5903
5995
|
const argv = process.argv.slice(2);
|
|
5904
5996
|
const [command, ...args] = argv;
|
|
5905
5997
|
if (command === undefined) { printHelp(); return; }
|
|
5906
5998
|
const entry = resolveCommand(command);
|
|
5907
5999
|
if (!entry) {
|
|
6000
|
+
recordAgentMiss(command);
|
|
5908
6001
|
const close = suggestCommands(command);
|
|
5909
6002
|
cliError({
|
|
5910
6003
|
code: "unknown_command",
|
|
@@ -6044,6 +6137,11 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
|
|
|
6044
6137
|
break;
|
|
6045
6138
|
}
|
|
6046
6139
|
case "automation": {
|
|
6140
|
+
// An agent's apply is a proposal the user approves (see cmdAutomationProposal).
|
|
6141
|
+
if ((args[0] === "apply" && !args.includes("--dry-run") && !process.env.BIVY_AUTOMATION_DIRECT && resolveAttachSessionId({ env: process.env })) || args[0] === "proposal") {
|
|
6142
|
+
await cmdAutomationProposal(args);
|
|
6143
|
+
break;
|
|
6144
|
+
}
|
|
6047
6145
|
if (!(await ensureDeps())) process.exit(1);
|
|
6048
6146
|
process.exit(await run(nodeBin, [...nodeScriptArgs(automationEntry), ...args], { cwd: process.cwd(), env: process.env }));
|
|
6049
6147
|
break;
|
package/bin/cli-commands.mjs
CHANGED
|
@@ -78,7 +78,11 @@ export const COMMANDS = [
|
|
|
78
78
|
|
|
79
79
|
// Runs and automations
|
|
80
80
|
{ name: "runs", group: "runs", scope: "account", json: true, usage: 'runs <start|list|status|wait>', summary: "Unattended Runs with checks, evidence and a Receipt", subcommands: ["start", "list", "status", "wait"] },
|
|
81
|
-
{ name: "automation", aliases: ["automations"], group: "runs", scope: "account", json: true, usage: "automation <list|trigger|init|validate|plan|test|apply>", summary: "Automations as code: validate, plan, apply, trigger", subcommands: ["list", "trigger", "init", "validate", "plan", "test", "test-filter", "apply"]
|
|
81
|
+
{ name: "automation", aliases: ["automations"], group: "runs", scope: "account", json: true, usage: "automation <list|trigger|init|validate|plan|test|apply>", summary: "Automations as code: validate, plan, apply, trigger", subcommands: ["list", "trigger", "init", "validate", "plan", "test", "test-filter", "apply", "proposal"],
|
|
82
|
+
tools: [
|
|
83
|
+
{ name: "automation_plan", description: "Check an automations file (.bivy/automations.yaml) and show what each automation would do: trigger, routing, effective safety. Run it before automation_apply.", input: { path: { type: "string", description: "The automations file (default .bivy/automations.yaml)." } }, argv: ["automation", "plan", { arg: "path" }] },
|
|
84
|
+
{ name: "automation_apply", description: "Propose applying an automations file to the user's account. They get an approval card listing each change; nothing is applied unless they approve. Returns the proposal: status applied, rejected, expired, failed, or pending (check later with bivy automation proposal <id> --wait).", input: { path: { type: "string", description: "The automations file (default .bivy/automations.yaml)." }, prune: { type: "boolean", description: "Also remove automations this file no longer has." } }, argv: ["automation", "apply", { arg: "path" }, { when: "prune", flag: "--prune" }] },
|
|
85
|
+
] },
|
|
82
86
|
|
|
83
87
|
// This node
|
|
84
88
|
{ name: "setup", aliases: ["init"], group: "node", scope: "node", usage: "setup", summary: "First-run wizard: agent, model login, remote sign-in, background service" },
|
package/bin/guides/automate.md
CHANGED
|
@@ -9,11 +9,35 @@ infrastructure code:
|
|
|
9
9
|
bivy automation validate # check it
|
|
10
10
|
bivy automation plan --json # triggers, routing, effective safety
|
|
11
11
|
bivy automation test --event event.json # which automation an event would start
|
|
12
|
-
bivy automation apply #
|
|
12
|
+
bivy automation apply # ask the user, then apply
|
|
13
13
|
bivy automation list --json
|
|
14
14
|
bivy automation trigger <id-or-key> # run one now
|
|
15
15
|
|
|
16
|
+
A scheduled one (triggers: schedule, github, linear, webhook, manual; the
|
|
17
|
+
format is in docs/automations-as-code.md):
|
|
18
|
+
|
|
19
|
+
version: 1
|
|
20
|
+
automations:
|
|
21
|
+
- id: nightly-tests
|
|
22
|
+
name: Nightly tests
|
|
23
|
+
trigger: schedule
|
|
24
|
+
schedule: { cron: "0 2 * * *", timezone: UTC } # or { at: "2026-10-01T09:00:00Z" }
|
|
25
|
+
repo: owner/name # required for schedules
|
|
26
|
+
instructions: |
|
|
27
|
+
Run the test suite. If it fails, open an issue with the failing tests.
|
|
28
|
+
safety:
|
|
29
|
+
approval: risky
|
|
30
|
+
sandbox: workspace-write
|
|
31
|
+
|
|
32
|
+
From a session, `apply` goes through the machine's approval mode. Usually the
|
|
33
|
+
user gets a card listing every change and decides. With "never ask" it applies
|
|
34
|
+
at once, and the chat records what changed either way. The command waits up to
|
|
35
|
+
90 seconds for an answer and prints apply's output once it's applied (exit 0). Exit 1 means they declined
|
|
36
|
+
(don't retry the same thing), and exit 5 means they haven't answered yet: carry
|
|
37
|
+
on, and check later with `bivy automation proposal <id> --wait`.
|
|
38
|
+
`apply --dry-run` shows the changes without asking anyone.
|
|
39
|
+
|
|
16
40
|
To pause one, set `enabled: false` on it and apply. Always `validate` and
|
|
17
|
-
`plan`
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
`plan` first, and say in the chat what the automation will do and when.
|
|
42
|
+
`apply --prune` also removes automations the file no longer has.
|
|
43
|
+
`bivy automation --help` has every flag.
|
|
@@ -49,6 +49,8 @@ export const BIVY_AGENT_NOTE = [
|
|
|
49
49
|
'`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
|
|
50
50
|
"its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
|
|
51
51
|
"lists machines and their agents. Use your own sub-agents for everything else.",
|
|
52
|
+
"- Work that should run on its own later (on a schedule, or when CI fails or an issue arrives) is a Bivy automation: " +
|
|
53
|
+
"`bivy guide automate`. Applying one may ask the user first.",
|
|
52
54
|
"- `bivy context` shows your session, workspace and published apps; `bivy guide` has short playbooks; " +
|
|
53
55
|
"`bivy help` lists every command (`--json` for all).",
|
|
54
56
|
].join("\n");
|