@bivy/bivy 0.6.0 → 0.7.0-staging.101

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 CHANGED
@@ -199,24 +199,33 @@ Every environment variable, config file, and precedence rule:
199
199
  ## Approvals and sandboxing
200
200
 
201
201
  The default approval mode is **`autonomous`**: agents act without per-action
202
- prompts. Safety comes from a floor that applies in *every* mode — catastrophic
203
- commands and writes outside the workspace are refused outright, and a backstop
204
- set (force-push, publish, deploy, sudo) always pauses for a human.
202
+ prompts. The actual protection depends on the selected runtime. Native-sandbox
203
+ agents enforce the chosen access tier; structured runtimes also pass tool calls
204
+ through Bivy's policy and approval layer. Process agents that Bivy cannot
205
+ intercept run with your OS user permissions. The picker shows this distinction
206
+ and requires confirmation before selecting that limited path.
207
+
208
+ Where Bivy receives structured shell/file calls, a heuristic floor blocks known
209
+ catastrophic commands and structured writes outside the workspace, and a
210
+ backstop set (force-push, publish, deploy, sudo) pauses for a human. This catches
211
+ accidents; it is not an adversarial isolation boundary.
205
212
 
206
213
  If you want to be asked about more, set the mode explicitly:
207
214
 
208
215
  ```bash
209
216
  BIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits
210
217
  BIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits
211
- BIVY_APPROVAL_MODE=never # no prompts beyond the hard floor
218
+ BIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available
212
219
  ```
213
220
 
214
221
  Approve from the terminal, browser, or phone.
215
222
 
216
223
  Sandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced
217
224
  natively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.
218
- Agents without a native sandbox are governed at the filesystem, MCP, and network
219
- layer. **Bivy does not ship its own OS-level jail in 0.1.**
225
+ Agents without a native sandbox may expose structured tool or MCP controls, but
226
+ those controls do not cover activity the agent performs outside those channels;
227
+ some process adapters run entirely with your user permissions. Check the
228
+ picker's Protection label. **Bivy does not currently ship its own OS-level jail.**
220
229
 
221
230
  ## Credentials
222
231
 
package/bin/acp-shim.mjs CHANGED
@@ -57,45 +57,112 @@ function bivy(obj) {
57
57
  // --- ACP agent (child JSON-RPC over its stdio) ------------------------------
58
58
  const agent = spawn(agentCmd, agentArgs, { stdio: ["pipe", "pipe", "pipe"] });
59
59
  agent.stderr.on("data", (d) => process.stderr.write(`[acp-agent] ${d}`));
60
- agent.on("error", (e) => bivy({ type: "session.error", error: `acp agent spawn failed: ${e.message}` }));
61
- agent.on("exit", (code) => {
62
- if (code && code !== 0) bivy({ type: "session.error", error: `acp agent exited (${code})` });
63
- });
64
-
65
60
  let nextId = 1;
66
61
  const pending = new Map(); // jsonrpc id -> {resolve, reject}
67
- function agentRequest(method, params) {
62
+ let agentDead = null; // set to an Error once the child is gone
63
+
64
+ /**
65
+ * The child is gone (spawn failure or exit). Every in-flight request must be
66
+ * rejected: without this, a CLI whose ACP mode doesn't exist leaves `initialize`
67
+ * pending forever and the daemon waits out its whole session.create timeout instead
68
+ * of surfacing the real reason. Fail fast, with the reason.
69
+ */
70
+ function killPending(reason) {
71
+ if (agentDead) return;
72
+ agentDead = reason instanceof Error ? reason : new Error(String(reason));
73
+ bivy({ type: "session.error", error: agentDead.message });
74
+ for (const [id, p] of [...pending]) { pending.delete(id); p.reject(agentDead); }
75
+ }
76
+ agent.on("error", (e) => killPending(`acp agent spawn failed: ${e.message}`));
77
+ agent.on("exit", (code, signal) => {
78
+ if (code || signal) killPending(`acp agent exited (${code ?? signal})`);
79
+ });
80
+ // Writing to a dead child's stdin raises EPIPE; with no listener that's an uncaught
81
+ // exception that takes the shim down mid-turn instead of reporting the cause.
82
+ agent.stdin.on("error", (e) => killPending(`acp agent stdin closed: ${e.message}`));
83
+
84
+ function agentWrite(payload) {
85
+ if (agentDead) throw agentDead;
86
+ agent.stdin.write(`${JSON.stringify(payload)}\n`);
87
+ }
88
+ function agentRequest(method, params, { timeoutMs } = {}) {
68
89
  const id = nextId++;
69
- agent.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
70
- return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
90
+ return new Promise((resolve, reject) => {
91
+ let timer;
92
+ pending.set(id, {
93
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
94
+ reject: (e) => { clearTimeout(timer); reject(e); },
95
+ });
96
+ if (timeoutMs) {
97
+ timer = setTimeout(() => {
98
+ if (pending.delete(id)) reject(new Error(`acp ${method} timed out after ${timeoutMs}ms`));
99
+ }, timeoutMs);
100
+ }
101
+ try { agentWrite({ jsonrpc: "2.0", id, method, params }); }
102
+ catch (e) { clearTimeout(timer); pending.delete(id); reject(e); }
103
+ });
71
104
  }
72
105
  function agentReply(id, result) {
73
- agent.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
106
+ try { agentWrite({ jsonrpc: "2.0", id, result }); } catch { /* child gone; killPending already reported it */ }
74
107
  }
75
108
  function agentReplyError(id, code, message) {
76
- agent.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })}\n`);
109
+ try { agentWrite({ jsonrpc: "2.0", id, error: { code, message } }); } catch { /* child gone */ }
77
110
  }
78
111
  function agentNotify(method, params) {
79
- agent.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
112
+ try { agentWrite({ jsonrpc: "2.0", method, params }); } catch { /* child gone */ }
80
113
  }
81
114
 
82
115
  // --- session state ----------------------------------------------------------
83
116
  let sessionId = null;
84
117
  let cwd = process.cwd();
85
118
  let initialized = false;
119
+ // The ACP config-option id that selects the model (usually "model"), learned from
120
+ // session/new; used for the session/set_config_option fallback.
121
+ let modelConfigId = "model";
122
+ // A model chosen before the ACP session existed, applied once it does.
123
+ let pendingModel = null;
86
124
  // toolCallId -> { requestId, options } so a later bivy tool.decision answers the
87
125
  // right ACP permission request with a concrete optionId.
88
126
  const permissionRequests = new Map();
89
127
 
90
128
  async function ensureInitialized() {
91
129
  if (initialized) return;
130
+ // Bounded: a binary that accepts the launch args but never speaks ACP would
131
+ // otherwise hang here until the daemon's own session timeout, hiding the cause.
92
132
  await agentRequest("initialize", {
93
133
  protocolVersion: 1,
94
134
  clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
95
- });
135
+ }, { timeoutMs: 20_000 });
96
136
  initialized = true;
97
137
  }
98
138
 
139
+ /**
140
+ * ACP exposes a session's selectable models as a `select` config option on the
141
+ * session/new|load result (opencode: `configOptions: [{id:"model", currentValue,
142
+ * options:[{value,name}]}]`). Models are per-NODE — they depend on which providers
143
+ * the user has authenticated in the agent — so a hardcoded list would offer models
144
+ * the agent rejects. Publish what the agent actually reports, as a post-hello
145
+ * `runtime.models` event ProtocolRuntime folds into its picker.
146
+ */
147
+ function publishModels(result) {
148
+ const options = Array.isArray(result?.configOptions) ? result.configOptions : [];
149
+ const modelOption = options.find((o) => String(o?.id ?? "") === "model" || String(o?.category ?? "") === "model");
150
+ const choices = Array.isArray(modelOption?.options) ? modelOption.options : [];
151
+ const models = choices
152
+ .map((o) => ({ id: String(o?.value ?? ""), name: String(o?.name ?? o?.value ?? "") }))
153
+ .filter((m) => m.id)
154
+ // ACP model ids are `provider/model`; split the provider so Bivy can group and
155
+ // scope provider-specific settings the same way it does for other runtimes.
156
+ .map((m) => ({ ...m, provider: m.id.includes("/") ? m.id.split("/")[0] : "agent" }));
157
+ if (!models.length) return;
158
+ modelConfigId = String(modelOption?.id ?? "model");
159
+ bivy({
160
+ type: "runtime.models",
161
+ models,
162
+ ...(modelOption?.currentValue ? { currentModel: String(modelOption.currentValue) } : {}),
163
+ });
164
+ }
165
+
99
166
  // --- ACP → bivy: streamed session/update notifications ----------------------
100
167
  function onSessionUpdate(params) {
101
168
  const u = params?.update;
@@ -211,6 +278,33 @@ createInterface({ input: agent.stdout }).on("line", (line) => {
211
278
  if (msg.method === "session/update") onSessionUpdate(msg.params);
212
279
  });
213
280
 
281
+ /**
282
+ * Select a model on the live ACP session. `session/set_model` is the direct form;
283
+ * agents that only expose the generic config-option surface take the same choice as
284
+ * `session/set_config_option`. A rejection propagates: ProtocolRuntime only commits
285
+ * the selection once we ack, so a model the agent won't accept must not look applied.
286
+ */
287
+ async function setAgentModel(model) {
288
+ try {
289
+ await agentRequest("session/set_model", { sessionId, modelId: model }, { timeoutMs: 15_000 });
290
+ } catch (primary) {
291
+ try {
292
+ await agentRequest("session/set_config_option", { sessionId, configId: modelConfigId, value: model }, { timeoutMs: 15_000 });
293
+ } catch {
294
+ throw primary;
295
+ }
296
+ }
297
+ }
298
+
299
+ async function applyPendingModel() {
300
+ if (!pendingModel || !sessionId) return;
301
+ const model = pendingModel;
302
+ pendingModel = null;
303
+ // Best-effort: a stale pick shouldn't block the session from opening.
304
+ try { await setAgentModel(model); }
305
+ catch (e) { bivy({ type: "runtime.debug", message: `acp set_model failed: ${e instanceof Error ? e.message : String(e)}` }); }
306
+ }
307
+
214
308
  // --- bivy commands in (daemon → us) -----------------------------------------
215
309
  async function onBivyCommand(msg) {
216
310
  const type = String(msg.type || "");
@@ -224,6 +318,8 @@ async function onBivyCommand(msg) {
224
318
  cwd = String(msg.cwd || msg.workspace || cwd);
225
319
  const res = await agentRequest("session/new", { cwd, mcpServers: [] });
226
320
  sessionId = res?.sessionId ?? res?.session?.id ?? null;
321
+ publishModels(res);
322
+ await applyPendingModel();
227
323
  bivy({ replyTo: id, ok: true, runtimeSessionRef: sessionId });
228
324
  return;
229
325
  }
@@ -235,11 +331,14 @@ async function onBivyCommand(msg) {
235
331
  try {
236
332
  const res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers: [] });
237
333
  sessionId = res?.sessionId ?? ref;
334
+ publishModels(res);
238
335
  } catch {
239
336
  // Agent doesn't support session/load — start fresh so the chat still opens.
240
337
  const res = await agentRequest("session/new", { cwd, mcpServers: [] });
241
338
  sessionId = res?.sessionId ?? null;
339
+ publishModels(res);
242
340
  }
341
+ await applyPendingModel();
243
342
  bivy({ replyTo: id, ok: true, runtimeSessionRef: sessionId });
244
343
  return;
245
344
  }
@@ -270,6 +369,19 @@ async function onBivyCommand(msg) {
270
369
  }
271
370
  return;
272
371
  }
372
+ case "model.set": {
373
+ const model = String(msg.model ?? "").trim();
374
+ if (!model) { bivy({ replyTo: id, ok: true }); return; }
375
+ if (!sessionId) {
376
+ // Chosen before the session exists — remember and apply at session/new.
377
+ pendingModel = model;
378
+ bivy({ replyTo: id, ok: true });
379
+ return;
380
+ }
381
+ await setAgentModel(model);
382
+ bivy({ replyTo: id, ok: true });
383
+ return;
384
+ }
273
385
  case "session.abort": {
274
386
  if (sessionId) agentNotify("session/cancel", { sessionId });
275
387
  if (id !== undefined) bivy({ replyTo: id, ok: true });
@@ -286,8 +398,10 @@ async function onBivyCommand(msg) {
286
398
  }
287
399
 
288
400
  // Announce capabilities: ACP agents are governed (per-tool permission) and
289
- // resumable (session/load). Models aren't part of the core ACP surface, so we
290
- // don't advertise a picker we can't drive.
401
+ // resumable (session/load). modelSelection starts FALSE and is upgraded later by a
402
+ // `runtime.models` event if the session reports selectable models — the list is
403
+ // per-node (it depends on the providers the user has authenticated in the agent)
404
+ // and only arrives with session/new, so claiming a picker here would be a guess.
291
405
  bivy({ type: "hello", runtime: { capabilities: { toolInterception: true, modelSelection: false, resume: true } } });
292
406
 
293
407
  createInterface({ input: process.stdin }).on("line", (line) => {
@@ -5,6 +5,8 @@
5
5
  "label": "Codex",
6
6
  "command": "codex",
7
7
  "hidden": true,
8
+ "supportTier": "beta",
9
+ "certification": "adapter-tested",
8
10
  "headlessFlags": [
9
11
  "exec"
10
12
  ],
@@ -18,6 +20,9 @@
18
20
  "label": "OpenCode",
19
21
  "command": "opencode",
20
22
  "hidden": false,
23
+ "supportTier": "supported",
24
+ "certification": "release-tested",
25
+ "testedVersion": "1.18.13",
21
26
  "headlessFlags": [
22
27
  "run",
23
28
  "-s"
@@ -32,6 +37,8 @@
32
37
  "label": "Aider",
33
38
  "command": "aider",
34
39
  "hidden": false,
40
+ "supportTier": "beta",
41
+ "certification": "adapter-tested",
35
42
  "headlessFlags": [
36
43
  "--yes-always",
37
44
  "--message"
@@ -46,6 +53,8 @@
46
53
  "label": "Hermes",
47
54
  "command": "hermes",
48
55
  "hidden": true,
56
+ "supportTier": "beta",
57
+ "certification": "adapter-tested",
49
58
  "headlessFlags": [],
50
59
  "install": {
51
60
  "kind": "npm",
@@ -57,6 +66,8 @@
57
66
  "label": "Goose",
58
67
  "command": "goose",
59
68
  "hidden": false,
69
+ "supportTier": "beta",
70
+ "certification": "adapter-tested",
60
71
  "headlessFlags": [
61
72
  "run",
62
73
  "-t",
@@ -76,6 +87,8 @@
76
87
  "label": "Gemini CLI",
77
88
  "command": "gemini",
78
89
  "hidden": false,
90
+ "supportTier": "beta",
91
+ "certification": "adapter-tested",
79
92
  "headlessFlags": [
80
93
  "-p",
81
94
  "-o",
@@ -92,6 +105,8 @@
92
105
  "label": "Qwen Code",
93
106
  "command": "qwen",
94
107
  "hidden": false,
108
+ "supportTier": "beta",
109
+ "certification": "adapter-tested",
95
110
  "headlessFlags": [
96
111
  "-p",
97
112
  "--output-format",
@@ -108,6 +123,8 @@
108
123
  "label": "Cline",
109
124
  "command": "cline",
110
125
  "hidden": false,
126
+ "supportTier": "beta",
127
+ "certification": "adapter-tested",
111
128
  "headlessFlags": [
112
129
  "-y",
113
130
  "--id"
@@ -122,6 +139,8 @@
122
139
  "label": "Crush",
123
140
  "command": "crush",
124
141
  "hidden": false,
142
+ "supportTier": "beta",
143
+ "certification": "adapter-tested",
125
144
  "headlessFlags": [
126
145
  "run",
127
146
  "-q"
@@ -136,6 +155,8 @@
136
155
  "label": "Cursor",
137
156
  "command": "cursor-agent",
138
157
  "hidden": false,
158
+ "supportTier": "beta",
159
+ "certification": "adapter-tested",
139
160
  "headlessFlags": [
140
161
  "--force",
141
162
  "-p"
@@ -151,6 +172,8 @@
151
172
  "label": "GitHub Copilot",
152
173
  "command": "copilot",
153
174
  "hidden": false,
175
+ "supportTier": "beta",
176
+ "certification": "adapter-tested",
154
177
  "headlessFlags": [
155
178
  "--allow-all-tools",
156
179
  "-p"
@@ -165,6 +188,8 @@
165
188
  "label": "Grok",
166
189
  "command": "grok",
167
190
  "hidden": false,
191
+ "supportTier": "beta",
192
+ "certification": "adapter-tested",
168
193
  "headlessFlags": [
169
194
  "-p"
170
195
  ],
@@ -178,6 +203,8 @@
178
203
  "label": "Amp",
179
204
  "command": "amp",
180
205
  "hidden": false,
206
+ "supportTier": "beta",
207
+ "certification": "adapter-tested",
181
208
  "headlessFlags": [
182
209
  "-x",
183
210
  "threads",
@@ -193,6 +220,8 @@
193
220
  "label": "Auggie",
194
221
  "command": "auggie",
195
222
  "hidden": false,
223
+ "supportTier": "beta",
224
+ "certification": "adapter-tested",
196
225
  "headlessFlags": [
197
226
  "--quiet",
198
227
  "--print"
@@ -207,6 +236,8 @@
207
236
  "label": "Droid",
208
237
  "command": "droid",
209
238
  "hidden": false,
239
+ "supportTier": "beta",
240
+ "certification": "adapter-tested",
210
241
  "headlessFlags": [
211
242
  "exec",
212
243
  "--auto",
@@ -223,6 +254,8 @@
223
254
  "label": "Continue",
224
255
  "command": "cn",
225
256
  "hidden": false,
257
+ "supportTier": "beta",
258
+ "certification": "adapter-tested",
226
259
  "headlessFlags": [
227
260
  "--auto",
228
261
  "-p"
@@ -237,6 +270,8 @@
237
270
  "label": "Kilo Code",
238
271
  "command": "kilo",
239
272
  "hidden": false,
273
+ "supportTier": "beta",
274
+ "certification": "adapter-tested",
240
275
  "headlessFlags": [
241
276
  "run",
242
277
  "--auto",
@@ -252,6 +287,8 @@
252
287
  "label": "Rovo Dev",
253
288
  "command": "acli",
254
289
  "hidden": false,
290
+ "supportTier": "beta",
291
+ "certification": "adapter-tested",
255
292
  "headlessFlags": [
256
293
  "rovodev",
257
294
  "run",
@@ -265,6 +302,8 @@
265
302
  "label": "Codebuff",
266
303
  "command": "codebuff",
267
304
  "hidden": true,
305
+ "supportTier": "experimental",
306
+ "certification": "unverified",
268
307
  "headlessFlags": [
269
308
  "--continue"
270
309
  ],
package/bin/bivy.mjs CHANGED
@@ -425,6 +425,25 @@ function run(cmd, args, opts = {}) {
425
425
  });
426
426
  }
427
427
 
428
+ /** Fixed executable + fixed entry point for setup's inline model-auth stage.
429
+ * Keep this separate from the generic CLI forwarding helper: no user-provided
430
+ * command or argv value reaches this process boundary. */
431
+ function runSetupModelLogin(config) {
432
+ return new Promise((resolve) => {
433
+ const child = spawn(process.execPath, nodeScriptArgs(bivyLoginEntry), {
434
+ stdio: "inherit",
435
+ cwd: repoRoot,
436
+ env: startEnv(config),
437
+ shell: false,
438
+ });
439
+ child.on("exit", (code) => resolve(code ?? 0));
440
+ child.on("error", (error) => {
441
+ console.error(c.red(`Failed to start model login: ${error.message}`));
442
+ resolve(1);
443
+ });
444
+ });
445
+ }
446
+
428
447
  function runQuiet(cmd, args, opts = {}) {
429
448
  const res = spawnSync(cmd, args, { encoding: "utf8", ...opts });
430
449
  return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
@@ -1530,7 +1549,7 @@ function cmdCompletions(args = []) {
1530
1549
  const shell = (args[0] || "").toLowerCase();
1531
1550
  const commands = [
1532
1551
  "run", "sessions", "ls", "resume", "promote", "rename", "nodes", "agents", "agents:install", "shim", "takeover", "token", "exec",
1533
- "send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "logs", "login",
1552
+ "send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "logs", "login",
1534
1553
  "update", "update:log", "open", "service", "secrets", "voice", "link", "relay:setup",
1535
1554
  "github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
1536
1555
  ];
@@ -3194,9 +3213,10 @@ async function cmdSetup(args = []) {
3194
3213
  config.env = { ...config.env, BIVY_RUNTIME: setupAgent.runtimeId };
3195
3214
  saveConfig(config);
3196
3215
  }
3216
+ let agentReady = true;
3197
3217
  if (setupAgent && setupAgent.runtimeId !== "pi") {
3198
- const installed = await ensureSetupAgent(setupAgent);
3199
- if (!installed) console.log(c.yellow(`${setupAgent.label} was not fully installed. Install it later from the app or with 'bivy agents:install'.`));
3218
+ agentReady = await ensureSetupAgent(setupAgent);
3219
+ if (!agentReady) console.log(c.yellow(`${setupAgent.label} was not fully installed. Install it later from the app or with 'bivy agents:install'.`));
3200
3220
  }
3201
3221
  console.log(c.dim(`Default agent: ${setupAgent?.label || "Pi"} (change in Settings; sign into your model from the agent's CLI/TUI or Settings → Keys & OAuth)`));
3202
3222
 
@@ -3211,13 +3231,17 @@ async function cmdSetup(args = []) {
3211
3231
  console.log(c.bold("\n Remote access\n"));
3212
3232
 
3213
3233
  console.log("Bivy uses remote access to make agent sessions visible and steerable from your other devices.");
3234
+ // If self-host endpoints are already provided via the environment, default to
3235
+ // self-hosted so a scripted or self-hosted install doesn't have to re-pick it
3236
+ // (BIVY_CONTROL_PLANE_URL / BIVY_RELAY_URL then pre-fill the URL prompts below).
3237
+ const selfHostEnv = Boolean((process.env.BIVY_CONTROL_PLANE_URL || "").trim() || (process.env.BIVY_RELAY_URL || "").trim());
3214
3238
  const syncChoice = await askChoice(
3215
3239
  "Remote access",
3216
3240
  [
3217
3241
  { key: "h", label: "hosted (recommended — one node is free)" },
3218
3242
  { key: "s", label: "self-hosted (your own control plane + relay)" },
3219
3243
  ],
3220
- "h",
3244
+ selfHostEnv ? "s" : "h",
3221
3245
  );
3222
3246
  const relayArgs = [];
3223
3247
  if (syncChoice === "s") {
@@ -3278,6 +3302,22 @@ async function cmdSetup(args = []) {
3278
3302
  // `bivy github:app-create` / `github:app-connect`. One app covers every repo,
3279
3303
  // and the node mints its own tokens, so there's no per-repo token to set up here.
3280
3304
 
3305
+ // Model access is part of activation, not a post-success footnote. Pi/Aider use
3306
+ // Bivy's provider login; offer it inline so setup cannot imply the first task
3307
+ // is ready while the required credential is still absent. Agent-native auth is
3308
+ // explained in the readiness checklist below because those CLIs own the flow.
3309
+ if (setupAgent?.needsBivyModel && !hasModelConfig(config)) {
3310
+ const signInNow = await askYesNo("Sign in to a model now so your first task can run?", true);
3311
+ if (signInNow) {
3312
+ rl.pause();
3313
+ const loginCode = await runSetupModelLogin(config);
3314
+ rl.resume();
3315
+ if (loginCode !== 0 || !hasModelConfig(loadConfig())) {
3316
+ console.log(c.yellow("Model sign-in did not complete. The node can start, but an agent reply still requires 'bivy login'."));
3317
+ }
3318
+ }
3319
+ }
3320
+
3281
3321
  // 4. Background service — always installed so the node keeps running (and stays
3282
3322
  // reachable remotely) after you close this terminal. No prompt.
3283
3323
  let started = false;
@@ -3296,9 +3336,17 @@ async function cmdSetup(args = []) {
3296
3336
  return;
3297
3337
  }
3298
3338
 
3299
- console.log(c.bold(c.green("\n ✓ Your node is running.\n")));
3300
- printFirstRunSteps();
3301
- await finishSetupRemote(config, setupSession);
3339
+ const finalConfig = loadConfig();
3340
+ const modelReady = !setupAgent?.needsBivyModel || hasModelConfig(finalConfig);
3341
+ console.log(c.bold(c.green("\n ✓ Node running. Check first-task readiness below.\n")));
3342
+ console.log(` ${c.green("✓")} node reachable at ${url(finalConfig)}`);
3343
+ console.log(` ${agentReady ? c.green("✓") : c.yellow("!")} runtime ${agentReady ? `${setupAgent?.label || "Pi"} available` : "not installed — run 'bivy agents:install'"}`);
3344
+ console.log(` ${modelReady ? (setupAgent?.needsBivyModel ? c.green("✓") : c.dim("○")) : c.yellow("!")} model ${modelReady ? (setupAgent?.needsBivyModel ? "credential configured" : "agent-managed — verified by the first task") : "not configured — run 'bivy login'"}`);
3345
+ console.log(` ${c.dim("○")} repository chosen from the directory where you start Bivy`);
3346
+ console.log(` ${agentReady && modelReady ? c.green("✓") : c.yellow("!")} first task ${agentReady && modelReady ? "ready to try" : "blocked by the stage above"}`);
3347
+ console.log(` ${fs.existsSync(relayConfigPath) ? c.green("✓") : c.yellow("!")} remote ${fs.existsSync(relayConfigPath) ? "configured" : "not configured — run 'bivy relay:setup'"}\n`);
3348
+ printFirstRunSteps(modelReady);
3349
+ await finishSetupRemote(finalConfig, setupSession);
3302
3350
  }
3303
3351
 
3304
3352
  // Read and delete the one-time account-session handoff written by relay:setup
@@ -3385,11 +3433,11 @@ async function openRemoteApp(config, { setupSession = null, open = true } = {})
3385
3433
  return { relay, remoteBase, accountUrl, pairedUrl, openUrl };
3386
3434
  }
3387
3435
 
3388
- function printFirstRunSteps() {
3436
+ function printFirstRunSteps(modelReady = false) {
3389
3437
  console.log(" Run your first task:");
3390
- console.log(` 1. Model access: ${c.cyan("bivy login")} ${c.dim("(for Pi; other agents use their own login)")}`);
3391
- console.log(` 2. Start chatting: ${c.cyan("bivy")}`);
3392
- console.log(` One-shot task: ${c.cyan('bivy exec "explain this repository"')}\n`);
3438
+ if (!modelReady) console.log(` 1. Model access: ${c.cyan("bivy login")} ${c.dim("(for Pi; other agents use their own login)")}`);
3439
+ console.log(` ${modelReady ? "1" : "2"}. Start chatting: ${c.cyan("bivy")}`);
3440
+ console.log(` Starter task: ${c.cyan('bivy exec "explain this repository and identify one low-risk improvement"')}\n`);
3393
3441
  }
3394
3442
 
3395
3443
  async function finishSetupRemote(config, setupSession = null) {
@@ -3506,7 +3554,7 @@ async function cmdStatus(args = []) {
3506
3554
  console.log(` sessions: ${status.sessions?.open ?? 0} open, ${status.sessions?.indexed ?? 0} indexed${status.sessions?.active ? `, active ${status.sessions.active}` : ""}`);
3507
3555
  console.log(` devices: ${status.devices?.paired ?? 0} paired remote, ${status.devices?.localTokens ?? 0} local token(s)`);
3508
3556
  console.log(` approvals: ${status.approvals?.pending ?? 0} pending`);
3509
- console.log(` guard: ${status.approvalMode || "autonomous"} (${status.guardrails?.workspaceBoundary ? "workspace boundary on" : "boundary unknown"})`);
3557
+ console.log(` guard: ${status.approvalMode || "autonomous"} · ${status.guardrails?.protection || (status.guardrails?.workspaceBoundary ? "structured workspace controls" : "runs with user permissions")}`);
3510
3558
  if (status.updatedAt) {
3511
3559
  const when = new Date(status.updatedAt);
3512
3560
  console.log(` updated: ${Number.isNaN(when.getTime()) ? status.updatedAt : when.toLocaleString()}`);
@@ -3517,9 +3565,34 @@ async function cmdStatus(args = []) {
3517
3565
 
3518
3566
  // `bivy doctor` — one health screen: runtime deps, node reachability, model auth,
3519
3567
  // remote/relay, and agents on PATH.
3568
+ // `bivy diagnostics [--out <file>]` — fetch the node's redacted diagnostics
3569
+ // bundle (versions, health counters, whitelisted config, activation record — no
3570
+ // secrets/prompts/transcripts) and print it, or write it to a file to attach to a
3571
+ // support request. See src/diagnostics.ts for exactly what is (and isn't) included.
3572
+ async function cmdDiagnostics(args = []) {
3573
+ if (args.includes("-h") || args.includes("--help")) {
3574
+ console.log('Usage: bivy diagnostics [--out <file>]\n\nPrint a redacted, shareable diagnostics bundle (no secrets, prompts, transcripts, or repo content). --out writes it to a file instead of stdout.');
3575
+ return;
3576
+ }
3577
+ const config = loadConfig();
3578
+ if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
3579
+ let report;
3580
+ try { report = await localApi(config, "/api/diagnostics"); }
3581
+ catch (error) { console.error(c.red(`Could not fetch diagnostics: ${error?.message || String(error)}`)); process.exit(1); return; }
3582
+ const json = JSON.stringify(report, null, 2);
3583
+ const outIdx = args.indexOf("--out");
3584
+ const out = outIdx >= 0 && outIdx + 1 < args.length ? args[outIdx + 1] : undefined;
3585
+ if (out) {
3586
+ fs.writeFileSync(out, json + "\n");
3587
+ console.log(c.green(`Wrote redacted diagnostics to ${out}`));
3588
+ } else {
3589
+ console.log(json);
3590
+ }
3591
+ }
3592
+
3520
3593
  async function cmdDoctor(args = []) {
3521
3594
  if (args.includes("-h") || args.includes("--help")) {
3522
- console.log("Usage: bivy doctor\n\nHealth check: runtime deps, node reachability, model auth, remote/relay, and agents on PATH. Exits non-zero if Node is unsupported or the node is unreachable, so it can gate CI/monitoring.");
3595
+ console.log("Usage: bivy doctor\n\nHealth check: runtime deps, node reachability, model auth, remote/relay, and agents on PATH. Exits non-zero if Node is unsupported or the node is unreachable, so it can gate CI/monitoring. See also 'bivy diagnostics' for a shareable redacted bundle.");
3523
3596
  return;
3524
3597
  }
3525
3598
  if (!(await ensureDeps())) process.exit(1);
@@ -3565,6 +3638,16 @@ async function cmdDoctor(args = []) {
3565
3638
  const agentCommands = [...BUILTIN_TERMINAL_AGENTS.values()].filter((a) => a.type === "command").map((a) => a.command);
3566
3639
  const agents = agentCommands.filter((a) => commandExists(a));
3567
3640
  console.log(` ${mark(agents.length > 0, true)} agents on PATH: ${agents.length ? c.cyan(agents.join(", ")) : c.dim("none (built-in Pi still works; 'bivy agents:install')")}`);
3641
+ if (status?.eventLog) {
3642
+ const healthy = status.eventLog.ok !== false;
3643
+ const mib = Number(status.eventLog.bytes || 0) / (1024 * 1024);
3644
+ console.log(` ${mark(healthy, true)} event log ${healthy ? "writable" : `${status.eventLog.affectedSessions ?? 0} session(s) need attention`} · ${mib.toFixed(1)} MiB`);
3645
+ }
3646
+ if (status?.attachments) {
3647
+ const mib = Number(status.attachments.bytes || 0) / (1024 * 1024);
3648
+ const over = Number(status.attachments.overCapBytes || 0);
3649
+ console.log(` ${over > 0 ? warn : ok} attachments ${status.attachments.blobs ?? 0} blob(s), ${mib.toFixed(1)} MiB${over > 0 ? c.dim(" (over cap; referenced history retained)") : ""}`);
3650
+ }
3568
3651
  console.log("");
3569
3652
 
3570
3653
  // Fail the command when a hard check is red (unsupported Node or an
@@ -4234,6 +4317,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4234
4317
  case "doctor":
4235
4318
  await cmdDoctor(args);
4236
4319
  break;
4320
+ case "diagnostics":
4321
+ await cmdDiagnostics(args);
4322
+ break;
4237
4323
  case "logs":
4238
4324
  await cmdLogs(args);
4239
4325
  break;