@bivy/bivy 0.8.3-staging.117 → 0.8.3-staging.119

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/bin/bivy.mjs CHANGED
@@ -131,6 +131,7 @@ const packaged = fs.existsSync(path.join(repoRoot, "dist", "server.js"));
131
131
  const serverEntry = path.join(repoRoot, packaged ? "dist/server.js" : "src/server.ts");
132
132
  const nativePiEntry = path.join(repoRoot, packaged ? "dist/native-pi.js" : "src/native-pi.ts");
133
133
  const bivyLoginEntry = path.join(repoRoot, packaged ? "dist/bivy-login.js" : "src/bivy-login.ts");
134
+ const credentialIngestEntry = path.join(repoRoot, packaged ? "dist/credential-ingest-cli.js" : "src/credential-ingest-cli.ts");
134
135
  const relaySetupEntry = path.join(repoRoot, packaged ? "dist/relay-setup.js" : "src/relay-setup.ts");
135
136
  // Dependency-free hosted-endpoint helper. Shipped to dist/ in the release
136
137
  // artifact (src/ is not packaged), so resolve it the same packaged-aware way as
@@ -421,6 +422,17 @@ function nativeAgentAuthDetected(choice) {
421
422
  return false;
422
423
  }
423
424
 
425
+ async function ingestSetupAgentLogin(choice) {
426
+ if (!choice || !["claude-code-sdk", "codex"].includes(choice.runtimeId)) return false;
427
+ const code = await run(nodeBin, [
428
+ ...nodeScriptArgs(credentialIngestEntry),
429
+ choice.runtimeId,
430
+ path.join(appDir, "credentials"),
431
+ path.join(appDir, "pi"),
432
+ ], { cwd: repoRoot, env: process.env, stdio: "ignore" });
433
+ return code === 0;
434
+ }
435
+
424
436
  function url(config) {
425
437
  return `http://localhost:${config.port}`;
426
438
  }
@@ -2655,9 +2667,11 @@ function createPrompter() {
2655
2667
  };
2656
2668
 
2657
2669
  const askChoice = async (question, choices, fallback) => {
2658
- const labels = choices.map((choice) => `${choice.key}=${choice.label}`).join(", ");
2659
2670
  for (;;) {
2660
- const answer = (await ask(`${question} (${labels})`, fallback)).toLowerCase();
2671
+ const menu = choices.map((choice) => ` ${c.cyan(choice.key)} ${choice.label}`).join("\n");
2672
+ const suffix = fallback ? c.dim(` [default: ${fallback}]`) : "";
2673
+ process.stdout.write(`\n${c.cyan("›")} ${question}\n${menu}\n >${suffix} `);
2674
+ const answer = String(await nextLine()).trim().toLowerCase() || fallback || "";
2661
2675
  const match = choices.find((choice) => answer === choice.key || answer === choice.label.toLowerCase());
2662
2676
  if (match) return match.key;
2663
2677
  console.log(c.yellow(`Please choose one of: ${choices.map((choice) => choice.key).join(", ")}`));
@@ -3266,7 +3280,7 @@ async function cmdSetup(args = []) {
3266
3280
  const syncChoice = await askChoice(
3267
3281
  "Remote access",
3268
3282
  [
3269
- { key: "h", label: "hosted (recommended — one node is free)" },
3283
+ { key: "h", label: "hosted (recommended — first 25 remotely accessible sessions are free; nothing caps your local usage)" },
3270
3284
  { key: "s", label: "self-hosted (your own control plane + relay)" },
3271
3285
  ],
3272
3286
  selfHostEnv ? "s" : "h",
@@ -3349,15 +3363,17 @@ async function cmdSetup(args = []) {
3349
3363
  }
3350
3364
  } else if (setupAgent && !setupAgent.needsBivyModel) {
3351
3365
  if (agentAuthReady) {
3352
- console.log(c.green(`\n ✓ Existing ${setupAgent.label} login detected — Bivy will reuse it in the terminal and PWA.`));
3366
+ const imported = await ingestSetupAgentLogin(setupAgent);
3367
+ console.log(c.green(`\n ✓ Existing ${setupAgent.label} login detected — Bivy will reuse it in the terminal and PWA${imported ? " and stored it in the encrypted vault" : ""}.`));
3353
3368
  } else if (setupAgent.command) {
3354
- console.log(`\n${setupAgent.label} owns its login; Bivy reuses that native login and does not copy it into the shared vault.`);
3369
+ console.log(`\n${setupAgent.label} owns its login. After sign-in, Bivy stores compatible credential fields in its encrypted vault so the terminal, PWA, and your other Bivy nodes can reuse them.`);
3355
3370
  const signInNow = await askYesNo(`Open ${setupAgent.label} now to sign in? (Exit it when sign-in is complete.)`, true);
3356
3371
  if (signInNow) {
3357
3372
  rl.pause();
3358
3373
  const loginCode = await run(setupAgent.command, [], { cwd: config.workspace, env: startEnv(config) });
3359
3374
  rl.resume();
3360
3375
  agentAuthReady = loginCode === 0 || nativeAgentAuthDetected(setupAgent);
3376
+ if (agentAuthReady) await ingestSetupAgentLogin(setupAgent);
3361
3377
  }
3362
3378
  }
3363
3379
  }
@@ -3394,13 +3410,13 @@ async function cmdSetup(args = []) {
3394
3410
  console.log(` ${modelReady ? c.green("✓") : c.yellow("!")} model ${modelReady ? (setupAgent?.needsBivyModel ? "credential configured" : "native agent login ready") : (setupAgent?.needsBivyModel ? "not configured — run 'bivy login'" : `${setupAgent?.loginHint || "sign in through the selected agent"}`)}`);
3395
3411
  console.log(` ${c.dim("○")} repository chosen from the directory where you start Bivy`);
3396
3412
  const ghReady = githubConnected(finalConfig);
3397
- console.log(` ${ghReady ? c.green("✓") : c.dim("○")} GitHub ${ghReady ? "connected — your repos will list in the app" : c.dim("not connected — 'bivy github:connect' to list repos (optional)")}`);
3413
+ console.log(` ${ghReady ? c.green("✓") : c.dim("○")} GitHub ${ghReady ? "connected — your repos will list in the app" : c.dim("optional — connect later in the app under Settings → GitHub App")}`);
3398
3414
  console.log(` ${agentReady && modelReady ? c.green("✓") : c.yellow("!")} first task ${agentReady && modelReady ? "ready to try" : "blocked by the stage above"}`);
3399
3415
  console.log(` ${fs.existsSync(relayConfigPath) ? c.green("✓") : c.yellow("!")} remote ${fs.existsSync(relayConfigPath) ? "configured" : "not configured — run 'bivy relay:setup'"}\n`);
3400
3416
  // Get the user into the product immediately; terminal commands are the
3401
3417
  // fallback/next-step checklist after the remote app has been opened or linked.
3402
3418
  await finishSetupRemote(finalConfig, setupSession);
3403
- printFirstRunSteps(modelReady, finalConfig, setupAgent);
3419
+ printFirstRunSteps(modelReady, setupAgent);
3404
3420
  }
3405
3421
 
3406
3422
  // Read and delete the one-time account-session handoff written by relay:setup
@@ -3429,20 +3445,18 @@ function consumeSetupSession() {
3429
3445
  // `/nodes` return only this one node, so a user with other nodes appeared to land
3430
3446
  // on a different/empty account until they signed out and back in.
3431
3447
  //
3432
- // The separately minted node-scoped paired link (E2E key embedded) is reserved
3433
- // for the QR below, which is meant to be scanned by *another* device to pair it
3434
- // with this node — there, node-scoping is the right least-privilege choice.
3435
- // Everything is also printed as text so servers without a browser can copy it.
3436
3448
  // Open this node's REMOTE control-plane app in a local browser (best effort) and
3437
- // return the URLs involved. The node no longer hosts a UI, so the web/PWA app
3449
+ // return the URL involved. The node no longer hosts a UI, so the web/PWA app
3438
3450
  // always comes from the hosted or self-hosted control plane. Prefers, in order:
3439
3451
  // an account sign-in URL (only available right after `relay:setup`, when a
3440
- // setupSession is supplied) → a freshly minted node-scoped paired link → the
3441
- // plain remote base URL. Prints a clean "Opening <base>" line — never the
3442
- // tokenized fragment, so no session/pairing secret lands in terminal scrollback.
3452
+ // setupSession is supplied) → the plain remote base URL, where the user signs
3453
+ // in normally. Pairing is deliberately reserved for the explicit `bivy link`
3454
+ // command; ordinary setup/open should establish the full account experience.
3455
+ // Prints a clean "Opening <base>" line — never the tokenized fragment, so no
3456
+ // account secret lands in terminal scrollback.
3443
3457
  // Returns null when no relay is configured (caller should send the user to
3444
3458
  // `bivy relay:setup`).
3445
- async function openRemoteApp(config, { setupSession = null, open = true } = {}) {
3459
+ async function openRemoteApp({ setupSession = null, open = true, remotePath = "" } = {}) {
3446
3460
  const relay = loadRelayConfig();
3447
3461
  if (!relay) return null;
3448
3462
 
@@ -3460,31 +3474,15 @@ async function openRemoteApp(config, { setupSession = null, open = true } = {})
3460
3474
  session: setupSession.session,
3461
3475
  ...(setupSession.nodeId ? { node: { id: setupSession.nodeId } } : {}),
3462
3476
  };
3463
- accountUrl = `${remoteBase}/#${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
3464
- }
3465
-
3466
- // Try to mint a node-scoped paired link (safe to embed in a QR another device
3467
- // scans). Needs the node reachable, so wait briefly first.
3468
- let pairedUrl = "";
3469
- await waitForNode(config).catch(() => {});
3470
- try {
3471
- const token = await localDeviceToken(config);
3472
- const data = await localApi(config, "/api/relay/link", {
3473
- method: "POST",
3474
- headers: { authorization: `Bearer ${token}` },
3475
- body: "{}",
3476
- });
3477
- if (data?.url) pairedUrl = data.url;
3478
- } catch {
3479
- // fall back to the plain remote app URL below
3477
+ accountUrl = `${remoteBase}${remotePath}/#${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
3480
3478
  }
3481
3479
 
3482
- const openUrl = accountUrl || pairedUrl || remoteBase;
3480
+ const openUrl = accountUrl || `${remoteBase}${remotePath}`;
3483
3481
  if (open && canOpenBrowser() && openUrl) {
3484
3482
  console.log(` Opening ${c.cyan(remoteBase || openUrl)} …`);
3485
3483
  openBrowser(openUrl);
3486
3484
  }
3487
- return { relay, remoteBase, accountUrl, pairedUrl, openUrl };
3485
+ return { relay, remoteBase, accountUrl, openUrl };
3488
3486
  }
3489
3487
 
3490
3488
  // Whether GitHub is connected for repo listing/cloning. Bivy's own connect flow
@@ -3500,27 +3498,24 @@ function githubConnected(config = null) {
3500
3498
  return Boolean(token);
3501
3499
  }
3502
3500
 
3503
- function printFirstRunSteps(modelReady = false, config = null, setupAgent = null) {
3504
- console.log(" Run your first task:");
3505
- let n = 0;
3501
+ function printFirstRunSteps(modelReady = false, setupAgent = null) {
3502
+ console.log(" Start your first session:");
3506
3503
  if (!modelReady) {
3507
3504
  const login = setupAgent?.needsBivyModel
3508
3505
  ? `${c.cyan("bivy login")} ${c.dim("(stored in Bivy's encrypted vault)")}`
3509
3506
  : c.cyan(setupAgent?.command || "the selected agent's native CLI");
3510
- console.log(` ${++n}. Model access: ${login}`);
3507
+ console.log(` Model access: ${login}`);
3511
3508
  }
3512
- // GitHub is optional — "No repo" sessions work without it — so this only shows
3513
- // when nothing is connected yet, and never blocks the flow.
3514
- if (!githubConnected(config)) {
3515
- console.log(` ${++n}. GitHub ${c.dim("(optional)")}: ${c.cyan("bivy github:connect")} ${c.dim("— lets the app list your repos; required for private ones")}`);
3516
- }
3517
- console.log(` ${++n}. Start chatting: ${c.cyan("bivy")}`);
3518
- console.log(` Starter task: ${c.cyan('bivy exec "explain this repository and identify one low-risk improvement"')}\n`);
3509
+ const agent = setupAgent?.command || setupAgent?.runtimeId || resolveDefaultAgent();
3510
+ const remoteApp = String(loadRelayConfig()?.clientBaseUrl || "https://app.bivy.sh").replace(/\/+$/, "");
3511
+ console.log(` • In the terminal: ${c.cyan(`bivy run ${agent}`)}`);
3512
+ console.log(` Then use the remote app to watch the session or take over in chat.`);
3513
+ console.log(` • Or start in chat: ${c.cyan(remoteApp)}\n`);
3519
3514
  }
3520
3515
 
3521
3516
  async function finishSetupRemote(config, setupSession = null) {
3522
3517
  const openable = canOpenBrowser();
3523
- const remote = await openRemoteApp(config, { setupSession });
3518
+ const remote = await openRemoteApp({ setupSession });
3524
3519
 
3525
3520
  if (!remote) {
3526
3521
  console.log("\n Almost there — enable remote access to open the Bivy app:");
@@ -3529,19 +3524,14 @@ async function finishSetupRemote(config, setupSession = null) {
3529
3524
  return;
3530
3525
  }
3531
3526
 
3532
- const { remoteBase, pairedUrl } = remote;
3533
- if (pairedUrl) {
3534
- const qr = terminalQr(pairedUrl);
3535
- if (qr) console.log(qr + "\n");
3536
- }
3527
+ const { remoteBase } = remote;
3537
3528
 
3538
3529
  console.log("\n Access Bivy from anywhere:");
3539
3530
  if (remoteBase) console.log(` • Remote app: ${c.cyan(remoteBase)} (sign in with the same GitHub/email you just used)`);
3540
- console.log(` • Link a device: ${c.cyan("bivy link")} (prints a QR to pair a phone/laptop with this node)`);
3541
3531
  console.log(` • Check status: ${c.cyan("bivy status")}`);
3542
3532
  if (!openable) {
3543
3533
  console.log(c.dim("\n No browser on this machine (headless server)? Open the Remote app URL above"));
3544
- console.log(c.dim(" on your phone or laptop and sign in, or scan the QR above to pair a device with this node."));
3534
+ console.log(c.dim(" on your phone or laptop and sign in with the same GitHub account or email."));
3545
3535
  }
3546
3536
  console.log("");
3547
3537
  }
@@ -4262,7 +4252,7 @@ ${c.bold("bivy")} — Bivy node CLI
4262
4252
  ${c.cyan("bivy kill <id>")} Stop a session/terminal (--delete also removes a saved session)
4263
4253
  ${c.cyan("bivy prune")} Delete old sessions/workspaces/worktrees (--keep N, --older-than 7d, --dry-run)
4264
4254
  ${c.cyan("bivy exec")} "<prompt>" One-shot headless run: prints the answer to stdout (pipe-friendly)
4265
- ${c.cyan("bivy")} Launch the default agent (pi) as a managed, relay-visible session
4255
+ ${c.cyan("bivy")} Show this help
4266
4256
  ${c.cyan("bivy setup")} First-run wizard: workspace, remote access + sign-in, background service
4267
4257
  ${c.cyan("bivy start")} Run the daemon in the foreground
4268
4258
  ${c.cyan("bivy stop")} Stop the background service
@@ -4282,7 +4272,7 @@ ${c.bold("bivy")} — Bivy node CLI
4282
4272
  ${c.cyan("bivy github:app-create")} One-click: create + connect a GitHub App
4283
4273
  ${c.cyan("bivy github:app-connect")} Connect an existing GitHub App (--app-id --key)
4284
4274
  ${c.cyan("bivy github:app-sync")} [on|off] Sync connected GitHub App keys to this account's other opted-in nodes
4285
- ${c.cyan("bivy github:connect")} [owner/repo] Authorize repo access for the repo picker (device flow)
4275
+ ${c.cyan("bivy github:connect")} [owner/repo] Connect GitHub in the app (or use a configured self-hosted device flow)
4286
4276
  ${c.cyan("bivy secrets")} list | set | ref | delete | doctor | resolve
4287
4277
  ${c.cyan("bivy voice")} Configure speech-to-text: provider | key | remove | status
4288
4278
  ${c.cyan("bivy completions")} <bash|zsh|fish> Print a shell completion script
@@ -4295,10 +4285,7 @@ async function main() {
4295
4285
  const [command, ...args] = argv;
4296
4286
  switch (command) {
4297
4287
  case undefined:
4298
- // First run opens the guided setup; after setup, bare `bivy` launches the
4299
- // default agent's native CLI/TUI as a managed, relay-visible `bivy run`.
4300
- if (fs.existsSync(cliConfigPath)) await cmdRun([]);
4301
- else await cmdSetup();
4288
+ printHelp();
4302
4289
  break;
4303
4290
  case "setup":
4304
4291
  case "init":
@@ -4435,8 +4422,7 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4435
4422
  console.log("Usage: bivy open\n\nOpen the remote web/PWA app in your browser (requires 'bivy relay:setup' first).");
4436
4423
  break;
4437
4424
  }
4438
- const config = loadConfig();
4439
- const remote = await openRemoteApp(config);
4425
+ const remote = await openRemoteApp();
4440
4426
  if (!remote) {
4441
4427
  console.log("No remote access configured yet.");
4442
4428
  console.log(`Run ${c.cyan("bivy relay:setup")} to enable the web/PWA app, then ${c.cyan("bivy open")}.`);
@@ -4460,7 +4446,17 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4460
4446
  case "github:connect":
4461
4447
  case "connect-repo":
4462
4448
  if (args.includes("-h") || args.includes("--help")) {
4463
- console.log("Usage: bivy github:connect [owner/repo]\n\nAuthorize repo access for the repo picker via GitHub's device flow. The resulting repo-scoped token is stored in this node's encrypted local vault.");
4449
+ console.log("Usage: bivy github:connect [owner/repo]\n\nOpen Settings → GitHub App in the remote app. Self-hosted deployments with BIVY_GITHUB_OAUTH_CLIENT_ID configured use GitHub's device flow instead.");
4450
+ break;
4451
+ }
4452
+ if (!String(process.env.BIVY_GITHUB_OAUTH_CLIENT_ID || loadConfig().env?.BIVY_GITHUB_OAUTH_CLIENT_ID || "").trim()) {
4453
+ const remote = await openRemoteApp({ remotePath: "/settings/github" });
4454
+ if (!remote) {
4455
+ console.log("Remote access is not configured yet.");
4456
+ console.log(`Run ${c.cyan("bivy relay:setup")}, then connect GitHub in the app under Settings → GitHub App.`);
4457
+ } else if (!canOpenBrowser()) {
4458
+ console.log(`Open GitHub setup in the Bivy app: ${c.cyan(remote.openUrl)}`);
4459
+ }
4464
4460
  break;
4465
4461
  }
4466
4462
  if (!(await ensureDeps())) process.exit(1);
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Small setup-time entry point: normalize a native agent login into Bivy's
4
+ // encrypted model credential vault without passing credential material on argv.
5
+ import { ingestAgentCredentials } from "./runtime/credential-ingest.js";
6
+ const [agentId, credsDir, piDir] = process.argv.slice(2);
7
+ if (!agentId || !credsDir || !piDir)
8
+ process.exit(2);
9
+ try {
10
+ const imported = await ingestAgentCredentials(agentId, credsDir, piDir);
11
+ process.exit(imported > 0 ? 0 : 3);
12
+ }
13
+ catch (error) {
14
+ if (process.env.BIVY_DEBUG)
15
+ console.error(error);
16
+ process.exit(1);
17
+ }
package/dist/server.js CHANGED
@@ -3045,6 +3045,13 @@ const RELAY_COMMANDS = {
3045
3045
  messages: conversationMessages(record),
3046
3046
  cursor: historyCursorFrom(msg),
3047
3047
  }));
3048
+ // A client opening this session after the TUI was already live (a deep link
3049
+ // or reload on another device) missed the original terminal.tui broadcast,
3050
+ // so its composer would render unlocked and every send would be refused
3051
+ // server-side. Re-assert the single-writer lock here so it shows the
3052
+ // "running in the terminal" window instead of an unusable chat.
3053
+ if (record.tuiTermId || record.tuiRefreshing)
3054
+ broadcastTuiState(record.id, Boolean(record.tuiTermId));
3048
3055
  // A reconnecting/opening client missed the one-shot card broadcast; put
3049
3056
  // any still-pending question/approval back so it can be answered.
3050
3057
  replayPendingInteractions(record.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.8.3-staging.117",
3
+ "version": "0.8.3-staging.119",
4
4
  "type": "module",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",