@bivy/bivy 0.19.4-staging.2 → 0.19.4-staging.4

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
@@ -43,6 +43,7 @@ import { findAvailablePort, reconcilePort } from "./port-picker.mjs";
43
43
  import { resolveAttachSessionId } from "./attach-session-id.mjs";
44
44
  import { detectInstallKind as classifyInstallKind, npmGlobalPrefix } from "./install-kind.mjs";
45
45
  import { hasConfiguredService as configuredServiceExists } from "./service-state.mjs";
46
+ import { COMMANDS, EXIT, cliError, completionWords, describeCli, describeCommand, describeTools, renderHelp, resolveCommand, subcommandTable, suggestCommands, toolArgv, wantsJson } from "./cli-commands.mjs";
46
47
 
47
48
  const selfScript = fileURLToPath(import.meta.url);
48
49
  const __dirname = path.dirname(selfScript);
@@ -1822,18 +1823,16 @@ function translateExecAgent(args = []) {
1822
1823
  }
1823
1824
 
1824
1825
  // `bivy completions <bash|zsh|fish>` — print a shell completion script to eval or
1825
- // install. Covers the top-level commands and known integration ids.
1826
+ // install. Commands and their verbs come from bin/cli-commands.mjs; `run`
1827
+ // completes the known integration ids.
1826
1828
  function cmdCompletions(args = []) {
1827
1829
  const shell = (args[0] || "").toLowerCase();
1828
- const commands = [
1829
- "run", "runs", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
1830
- "send", "attach", "suggest", "delegate", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
1831
- "update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
1832
- "github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
1833
- ];
1830
+ const commands = completionWords();
1834
1831
  const agents = [...AGENT_INTEGRATIONS.keys()];
1832
+ const verbs = [{ words: ["run"], subcommands: agents }, ...subcommandTable()];
1835
1833
 
1836
1834
  if (shell === "bash") {
1835
+ const cases = verbs.map((v) => ` ${v.words.join("|")}) COMPREPLY=( $(compgen -W "${v.subcommands.join(" ")}" -- "$cur") );;`).join("\n");
1837
1836
  console.log(`# bivy bash completion — add to ~/.bashrc: eval "$(bivy completions bash)"
1838
1837
  _bivy_completions() {
1839
1838
  local cur prev
@@ -1843,41 +1842,42 @@ _bivy_completions() {
1843
1842
  COMPREPLY=( $(compgen -W "${commands.join(" ")}" -- "$cur") )
1844
1843
  return
1845
1844
  fi
1845
+ [ "$COMP_CWORD" -eq 2 ] || return
1846
1846
  case "$prev" in
1847
- run) COMPREPLY=( $(compgen -W "${agents.join(" ")}" -- "$cur") );;
1848
- provider|model) COMPREPLY=( $(compgen -W "login" -- "$cur") );;
1847
+ ${cases}
1849
1848
  esac
1850
1849
  }
1851
1850
  complete -F _bivy_completions bivy`);
1852
1851
  return;
1853
1852
  }
1854
1853
  if (shell === "zsh") {
1854
+ const cases = verbs.map((v) => ` ${v.words.map((w) => `'${w}'`).join("|")}) compadd -- ${v.subcommands.map((x) => `'${x}'`).join(" ")} ;;`).join("\n");
1855
1855
  console.log(`# bivy zsh completion — add to ~/.zshrc: eval "$(bivy completions zsh)"
1856
1856
  _bivy() {
1857
- local -a cmds agents
1857
+ local -a cmds
1858
1858
  cmds=(${commands.map((x) => `'${x}'`).join(" ")})
1859
- agents=(${agents.map((x) => `'${x}'`).join(" ")})
1860
1859
  if (( CURRENT == 2 )); then
1861
1860
  compadd -- $cmds
1862
- elif [[ \${words[2]} == run ]]; then
1863
- compadd -- $agents
1864
- elif [[ \${words[2]} == provider || \${words[2]} == model ]]; then
1865
- compadd -- login
1861
+ return
1866
1862
  fi
1863
+ (( CURRENT == 3 )) || return
1864
+ case \${words[2]} in
1865
+ ${cases}
1866
+ esac
1867
1867
  }
1868
1868
  compdef _bivy bivy`);
1869
1869
  return;
1870
1870
  }
1871
1871
  if (shell === "fish") {
1872
+ const lines = verbs.map((v) => `complete -c bivy -n '__fish_seen_subcommand_from ${v.words.join(" ")}' -a '${v.subcommands.join(" ")}'`);
1872
1873
  console.log(`# bivy fish completion — save to ~/.config/fish/completions/bivy.fish
1873
1874
  complete -c bivy -f
1874
1875
  complete -c bivy -n '__fish_use_subcommand' -a '${commands.join(" ")}'
1875
- complete -c bivy -n '__fish_seen_subcommand_from run' -a '${agents.join(" ")}'
1876
- complete -c bivy -n '__fish_seen_subcommand_from provider model' -a 'login'`);
1876
+ ${lines.join("\n")}`);
1877
1877
  return;
1878
1878
  }
1879
1879
  console.error(c.red("Usage: bivy completions <bash|zsh|fish>"));
1880
- process.exit(1);
1880
+ process.exit(EXIT.usage);
1881
1881
  }
1882
1882
 
1883
1883
  // `bivy run <agent>` — launch a native agent in a daemon-owned PTY and bind this
@@ -2379,6 +2379,13 @@ async function cmdSend(args = []) {
2379
2379
  // surfacing in the session's Artifacts list) rather than an incidental inline
2380
2380
  // image — see packages/core/src/artifacts.ts.
2381
2381
  async function cmdAttach(args = []) {
2382
+ const usage = 'Usage: bivy attach <file> [--caption "…"] [--artifact] [--name <name>] [--mime <type>] [--session <id>] [--json]';
2383
+ if (args.includes("-h") || args.includes("--help")) {
2384
+ console.log(`${usage}\n\nShow a local file in the chat: images render inline, other files as downloads. The path must be inside the session workspace. --artifact also lists it in the session's Artifacts. --json prints {"ok","name","kind","size","mimeType","hash"}.`);
2385
+ return;
2386
+ }
2387
+ const json = wantsJson(args);
2388
+ const fail = (error) => cliError(error, { json, paint: c.red });
2382
2389
  const flag = (name) => {
2383
2390
  const i = args.indexOf(name);
2384
2391
  return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
@@ -2398,40 +2405,123 @@ async function cmdAttach(args = []) {
2398
2405
  file = a;
2399
2406
  break;
2400
2407
  }
2401
- if (!file) { console.error(c.red('Usage: bivy attach <file> [--caption "…"] [--artifact] [--session <id>]')); process.exit(1); return; }
2402
- if (!sessionId) { console.error(c.red("No session id. Set --session <id> or run inside an agent session ($BIVY_SESSION_ID).")); process.exit(1); return; }
2408
+ if (!file) return fail({ code: "usage", message: usage, exit: EXIT.usage });
2409
+ if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
2403
2410
  const absPath = path.resolve(process.cwd(), file);
2404
- if (!fs.existsSync(absPath)) { console.error(c.red(`File not found: ${file}`)); process.exit(1); return; }
2411
+ if (!fs.existsSync(absPath)) return fail({ code: "file_not_found", message: `File not found: ${file}`, exit: EXIT.notFound });
2405
2412
 
2406
2413
  const config = loadConfig();
2407
- if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
2408
- // A token isn't required on a single-user host (loopback bypasses auth), but
2409
- // include it when available so multi-user hosts work too.
2414
+ 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 });
2415
+ const res = await sessionPost(config, sessionId, "attach", { path: absPath, caption, name, mimeType, artifact }).catch((error) => error);
2416
+ if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
2417
+ const body = await res.json().catch(() => ({}));
2418
+ if (!res.ok) return fail(sessionHttpError("Attach", res.status, body));
2419
+ if (json) { console.log(JSON.stringify(body)); return; }
2420
+ console.log(c.green(`Attached ${body.name} (${body.kind}, ${body.size} bytes) to the chat.`));
2421
+ }
2422
+
2423
+ // POST to a per-session route. A token isn't required on a single-user host
2424
+ // (loopback bypasses auth), but include one when available so multi-user
2425
+ // hosts work too.
2426
+ async function sessionPost(config, sessionId, action, body) {
2410
2427
  let token;
2411
2428
  try { token = await localDeviceToken(config); } catch { token = undefined; }
2412
- const headers = { "content-type": "application/json" };
2413
- if (token) headers.authorization = `Bearer ${token}`;
2414
- let res;
2415
- try {
2416
- res = await fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/attach`, {
2417
- method: "POST",
2418
- headers,
2419
- body: JSON.stringify({ path: absPath, caption, name, mimeType, artifact }),
2420
- });
2421
- } catch (error) {
2422
- console.error(c.red(`Could not reach the Bivy node: ${error?.message || String(error)}`));
2423
- process.exit(1);
2429
+ return fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/${action}`, {
2430
+ method: "POST",
2431
+ headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
2432
+ body: JSON.stringify(body),
2433
+ });
2434
+ }
2435
+
2436
+ // A failed per-session request, in the shared error shape.
2437
+ function sessionHttpError(what, status, body) {
2438
+ const message = `${what} failed (${status}): ${body?.error || "unknown error"}`;
2439
+ 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 };
2441
+ if (status === 400) return { code: "invalid", message, exit: EXIT.usage };
2442
+ return { code: "failed", message, exit: EXIT.failed };
2443
+ }
2444
+
2445
+ // The checkout the agent is standing in, from git itself (no node needed).
2446
+ function gitContext(cwd) {
2447
+ const git = (...gitArgs) => {
2448
+ const out = spawnSync("git", gitArgs, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
2449
+ return out.status === 0 ? out.stdout.trim() : undefined;
2450
+ };
2451
+ const root = git("rev-parse", "--show-toplevel");
2452
+ if (!root) return undefined;
2453
+ const origin = git("remote", "get-url", "origin");
2454
+ const repo = origin?.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/)?.[1];
2455
+ const status = git("status", "--porcelain");
2456
+ return { root, branch: git("branch", "--show-current") || undefined, ...(repo ? { repo } : {}), changedFiles: status ? status.split("\n").length : 0 };
2457
+ }
2458
+
2459
+ // `bivy context [--json]` — the first thing an agent should run: which session,
2460
+ // workspace, branch and machine it is in, what it has published, and the
2461
+ // session commands it can use. Works outside a session too (session: null).
2462
+ async function cmdContext(args = []) {
2463
+ if (args.includes("-h") || args.includes("--help")) {
2464
+ console.log("Usage: bivy context [--json] [--session <id>]\n\nWhere this agent is running: the Bivy session ($BIVY_SESSION_ID), its workspace and git branch, the machine, apps it has published, the device that last drove it, and the commands that reach the user. --json prints it as one object.");
2424
2465
  return;
2425
2466
  }
2426
- const body = await res.json().catch(() => ({}));
2427
- if (!res.ok) { console.error(c.red(`Attach failed (${res.status}): ${body?.error || "unknown error"}`)); process.exit(1); return; }
2428
- console.log(c.green(`Attached ${body.name} (${body.kind}, ${body.size} bytes) to the chat.`));
2467
+ const json = wantsJson(args);
2468
+ const flagAt = args.indexOf("--session");
2469
+ const sessionId = resolveAttachSessionId({ sessionFlag: flagAt >= 0 ? args[flagAt + 1] : undefined, env: process.env });
2470
+ const commands = COMMANDS.filter((command) => command.group === "session" && command.name !== "context")
2471
+ .map((command) => ({ command: `bivy ${command.usage}`, does: command.summary }));
2472
+ let node = { session: null };
2473
+ if (sessionId) {
2474
+ const config = loadConfig();
2475
+ let token;
2476
+ try { token = await localDeviceToken(config); } catch { token = undefined; }
2477
+ try {
2478
+ const res = await fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/context`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
2479
+ const body = await res.json().catch(() => ({}));
2480
+ if (res.status === 404 && body?.error === "Session not found") {
2481
+ cliError({ code: "session_not_found", message: `Session ${sessionId} is not open on this node.`, next: "bivy sessions --json", exit: EXIT.notFound }, { json, paint: c.red });
2482
+ return;
2483
+ }
2484
+ if (res.status === 404) {
2485
+ cliError({ code: "node_outdated", message: "This node is older than 'bivy context'.", hint: "Update it, then try again.", next: "bivy update", exit: EXIT.unavailable }, { json, paint: c.red });
2486
+ return;
2487
+ }
2488
+ if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`);
2489
+ node = body;
2490
+ } catch (error) {
2491
+ cliError({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}: ${error?.message || String(error)}`, next: "bivy status", exit: EXIT.unavailable }, { json, paint: c.red });
2492
+ return;
2493
+ }
2494
+ }
2495
+ const result = { ...node, git: gitContext(process.cwd()) ?? null, commands, more: "bivy help" };
2496
+ if (json) { console.log(JSON.stringify(result, null, 2)); return; }
2497
+ const s = result.session;
2498
+ if (s) {
2499
+ console.log(`${c.bold("Session")} ${s.name ? `${s.name} ` : ""}${c.dim(s.id)} ${s.agentName || s.agent}${s.busy ? " (working)" : ""}`);
2500
+ console.log(`${c.bold("Machine")} ${result.machine.name} (${result.machine.platform}/${result.machine.arch})`);
2501
+ console.log(`${c.bold("Workspace")} ${s.workspace}`);
2502
+ if (s.forkedFrom) console.log(`${c.bold("Forked")} from ${s.forkedFrom}`);
2503
+ if (result.lastDriver) console.log(`${c.bold("User")} last typed from ${result.lastDriver.label} (${result.lastDriver.via}, ${new Date(result.lastDriver.at).toISOString()})`);
2504
+ console.log(`${c.bold("Apps")} ${result.apps.length ? result.apps.map((a) => `${a.name} [${a.views.map((v) => v.kind).join(", ")}]`).join("; ") : c.dim("none published")}${result.previewAvailable ? "" : c.dim(" (previews not available on this machine)")}`);
2505
+ } else {
2506
+ console.log(c.yellow("Not inside a Bivy session ($BIVY_SESSION_ID is unset). Session commands need --session <id>."));
2507
+ }
2508
+ if (result.git) console.log(`${c.bold("Git")} ${result.git.repo ? `${result.git.repo} ` : ""}${result.git.branch ?? "(detached)"}, ${result.git.changedFiles} changed file(s)`);
2509
+ console.log(`\n${c.bold("Reach the user")}`);
2510
+ for (const row of commands) console.log(` ${c.cyan(row.command)}\n ${row.does}`);
2511
+ console.log(`\nEverything else: ${c.cyan("bivy help")} (or ${c.cyan("bivy help --json")}).`);
2429
2512
  }
2430
2513
 
2431
2514
  // `bivy suggest "<task>" [--title "…"] [--session <id>]` — propose a task the
2432
2515
  // user can start in one tap, beside this session or in it. For an agent
2433
2516
  // offering next steps: write the task as a complete instruction.
2434
2517
  async function cmdSuggest(args = []) {
2518
+ const usage = 'Usage: bivy suggest "<task>" [--title "short label"] [--session <id>] [--json]';
2519
+ if (args.includes("-h") || args.includes("--help")) {
2520
+ console.log(`${usage}\n\nPost a task the user can start in one tap, in this session or in a parallel one that works in its own copy of the project. Write it as a complete instruction, with paths relative to the project root. --json prints {"ok","id"}.`);
2521
+ return;
2522
+ }
2523
+ const json = wantsJson(args);
2524
+ const fail = (error) => cliError(error, { json, paint: c.red });
2435
2525
  const flagsWithValue = new Set(["--session", "--title"]);
2436
2526
  const flag = (name) => {
2437
2527
  const i = args.indexOf(name);
@@ -2439,27 +2529,159 @@ async function cmdSuggest(args = []) {
2439
2529
  };
2440
2530
  const text = args.filter((a, i) => !a.startsWith("-") && !(i > 0 && flagsWithValue.has(args[i - 1]))).join(" ").trim();
2441
2531
  const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
2442
- if (!text) { console.error(c.red('Usage: bivy suggest "<task>" [--title "short label"] [--session <id>]')); process.exit(1); return; }
2443
- if (!sessionId) { console.error(c.red("No session id. Set --session <id> or run inside an agent session ($BIVY_SESSION_ID).")); process.exit(1); return; }
2532
+ if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
2533
+ if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
2444
2534
  const config = loadConfig();
2445
- if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
2535
+ 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 });
2536
+ const res = await sessionPost(config, sessionId, "suggest", { text, title: flag("--title") }).catch((error) => error);
2537
+ if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
2538
+ const body = await res.json().catch(() => ({}));
2539
+ if (!res.ok) return fail(sessionHttpError("Suggest", res.status, body));
2540
+ if (json) { console.log(JSON.stringify(body)); return; }
2541
+ console.log(c.green("Suggested in the chat. The user can start it in one tap."));
2542
+ }
2543
+
2544
+ // Flags shared by the session commands: `--name value` pairs and bare switches,
2545
+ // with every other word collected as positional text.
2546
+ function parseSessionArgs(args, valueFlags) {
2547
+ const values = {};
2548
+ const positional = [];
2549
+ for (let i = 0; i < args.length; i++) {
2550
+ const a = args[i];
2551
+ if (valueFlags.includes(a)) {
2552
+ const v = args[i + 1];
2553
+ if (v === undefined) return { error: `${a} requires a value.` };
2554
+ (values[a] ??= []).push(v);
2555
+ i++;
2556
+ } else if (!a.startsWith("--")) positional.push(a);
2557
+ }
2558
+ return { values, positional, has: (flag) => args.includes(flag), one: (flag) => values[flag]?.[0] };
2559
+ }
2560
+
2561
+ // "90", "90s", "10m", "2h" → seconds.
2562
+ function durationSeconds(text) {
2563
+ const m = /^(\d+)(s|m|h)?$/.exec(String(text ?? "").trim());
2564
+ return m ? Number(m[1]) * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1) : NaN;
2565
+ }
2566
+
2567
+ // `bivy notify "<message>" [--urgent]` — reach the user: a card in the chat,
2568
+ // and a push naming the session (never the text) when nobody has the app open.
2569
+ async function cmdNotify(args = []) {
2570
+ const usage = 'Usage: bivy notify "<message>" [--urgent] [--session <id>] [--json]';
2571
+ if (args.includes("-h") || args.includes("--help")) {
2572
+ console.log(`${usage}
2573
+
2574
+ Send the user a message: it appears as a card in the chat, and their devices get
2575
+ a push notification (naming this session, not the text) when nobody has the app
2576
+ open. --urgent pushes even while they do. At most one push a minute per session;
2577
+ later messages still reach the chat. Use it when you finish long work, get
2578
+ blocked, or need the user to look at something. --json prints
2579
+ {"ok","id","push":"sent"|"user_watching"|"rate_limited","userWatching"}.`);
2580
+ return;
2581
+ }
2582
+ const json = wantsJson(args);
2583
+ const fail = (error) => cliError(error, { json, paint: c.red });
2584
+ const parsed = parseSessionArgs(args, ["--session"]);
2585
+ if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
2586
+ const text = parsed.positional.join(" ").trim();
2587
+ if (!text) return fail({ code: "usage", message: usage, exit: EXIT.usage });
2588
+ const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
2589
+ if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
2590
+ const config = loadConfig();
2591
+ 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 });
2592
+ const res = await sessionPost(config, sessionId, "notify", { text, urgent: parsed.has("--urgent") }).catch((error) => error);
2593
+ if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
2594
+ const body = await res.json().catch(() => ({}));
2595
+ if (!res.ok) return fail(sessionHttpError("Notify", res.status, body));
2596
+ if (json) { console.log(JSON.stringify(body)); return; }
2597
+ const pushed = { sent: "and pushed to the user's devices", user_watching: "(no push: the user has the app open; add --urgent to push anyway)", rate_limited: "(no push: this session pushed less than a minute ago)", unavailable: "(no push: this machine isn't signed in to a Bivy account)" }[body.push] ?? "";
2598
+ console.log(c.green(`Posted in the chat ${pushed}.`.replace(" .", ".")));
2599
+ }
2600
+
2601
+ // `bivy ask "<question>" [--option …]` — the question card, for any agent.
2602
+ // Blocks until the user answers (printing the answer), or with --async returns
2603
+ // an id to read later with `bivy ask status|wait <id>`.
2604
+ const ASK_USAGE = `Usage: bivy ask "<question>" [--option <label>]… [--multi] [--header <label>] [--timeout 10m] [--async] [--json]
2605
+ bivy ask status <id> [--json]
2606
+ bivy ask wait <id> [--timeout 10m] [--json]`;
2607
+
2608
+ async function cmdAsk(args = []) {
2609
+ if (args.includes("-h") || args.includes("--help")) {
2610
+ console.log(`${ASK_USAGE}
2611
+
2612
+ Ask the user a question in the chat and wait for the answer. Their devices get a
2613
+ "needs your input" push. With --option (2 to 8 of them) they pick one (or
2614
+ several with --multi) or write their own; without options they type an answer.
2615
+ --header is the short label on the card.
2616
+
2617
+ Prints the answer and exits 0. Exits 1 if the user dismissed the question and 5
2618
+ if --timeout (default 10m, up to 24h) passes first; the card closes then.
2619
+ --async returns the question's id at once; 'bivy ask wait <id>' blocks for the
2620
+ answer later and 'bivy ask status <id>' checks without waiting.
2621
+ --json prints {"id","status":"pending"|"answered"|"dismissed"|"expired","answer"}.`);
2622
+ return;
2623
+ }
2624
+ const json = wantsJson(args);
2625
+ const fail = (error) => cliError(error, { json, paint: c.red });
2626
+ const parsed = parseSessionArgs(args, ["--session", "--option", "--header", "--timeout"]);
2627
+ if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
2628
+ const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
2629
+ if (!sessionId) return fail({ code: "no_session", message: "No session id.", hint: "Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.", next: "bivy sessions --json", exit: EXIT.usage });
2630
+ const timeout = parsed.one("--timeout") === undefined ? 600 : durationSeconds(parsed.one("--timeout"));
2631
+ 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
+ const config = loadConfig();
2633
+ 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 });
2446
2634
  let token;
2447
2635
  try { token = await localDeviceToken(config); } catch { token = undefined; }
2448
- let res;
2449
- try {
2450
- res = await fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/suggest`, {
2451
- method: "POST",
2636
+ const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/ask`;
2637
+ const call = async (suffix, body) => {
2638
+ const res = await fetch(`${base}${suffix}`, {
2639
+ method: body ? "POST" : "GET",
2452
2640
  headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
2453
- body: JSON.stringify({ text, title: flag("--title") }),
2454
- });
2455
- } catch (error) {
2456
- console.error(c.red(`Could not reach the Bivy node: ${error?.message || String(error)}`));
2457
- process.exit(1);
2641
+ ...(body ? { body: JSON.stringify(body) } : {}),
2642
+ }).catch((error) => error);
2643
+ if (res instanceof Error) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable });
2644
+ const data = await res.json().catch(() => ({}));
2645
+ if (!res.ok) return fail(sessionHttpError("Ask", res.status, data));
2646
+ return data;
2647
+ };
2648
+ const answerText = (result) => Object.values(result.answers ?? {}).join("\n");
2649
+ const finish = (result) => {
2650
+ const out = { id: result.id, status: result.status, ...(result.status === "answered" ? { answer: answerText(result) } : {}) };
2651
+ if (result.status === "answered") { console.log(json ? JSON.stringify(out) : out.answer); return; }
2652
+ if (result.status === "pending") { console.log(json ? JSON.stringify(out) : `Still waiting for an answer. Check again with: bivy ask wait ${result.id}`); process.exit(EXIT.timeout); }
2653
+ if (json) console.log(JSON.stringify(out));
2654
+ else console.error(result.status === "expired" ? "The question timed out without an answer." : "The user dismissed the question without answering.");
2655
+ process.exit(result.status === "expired" ? EXIT.timeout : EXIT.failed);
2656
+ };
2657
+ const waitFor = async (id, seconds) => {
2658
+ const deadline = Date.now() + seconds * 1000;
2659
+ let result = await call(`/${encodeURIComponent(id)}`);
2660
+ while (result.status === "pending" && Date.now() < deadline) {
2661
+ result = await call(`/${encodeURIComponent(id)}/wait`, { timeoutSeconds: Math.max(1, Math.min(240, Math.ceil((deadline - Date.now()) / 1000))) });
2662
+ }
2663
+ return result;
2664
+ };
2665
+
2666
+ const [action, id] = parsed.positional;
2667
+ if ((action === "status" || action === "wait") && id && parsed.positional.length === 2) {
2668
+ const result = action === "status" ? await call(`/${encodeURIComponent(id)}`) : await waitFor(id, timeout);
2669
+ if (action === "status" && result.status === "pending") { console.log(json ? JSON.stringify({ id: result.id, status: "pending" }) : "Still waiting for an answer."); return; }
2670
+ return finish(result);
2671
+ }
2672
+ const question = parsed.positional.join(" ").trim();
2673
+ if (!question) return fail({ code: "usage", message: ASK_USAGE, exit: EXIT.usage });
2674
+ const options = parsed.values["--option"] ?? [];
2675
+ if (options.length === 1) return fail({ code: "usage", message: "Offer at least two --option values, or none for a free-text answer.", exit: EXIT.usage });
2676
+ const started = await call("", {
2677
+ questions: [{ question, header: parsed.one("--header"), options, multiSelect: parsed.has("--multi") }],
2678
+ timeoutSeconds: timeout,
2679
+ });
2680
+ if (parsed.has("--async")) {
2681
+ console.log(json ? JSON.stringify({ id: started.id, status: started.status }) : `Asked. Get the answer with: bivy ask wait ${started.id}`);
2458
2682
  return;
2459
2683
  }
2460
- const body = await res.json().catch(() => ({}));
2461
- if (!res.ok) { console.error(c.red(`Suggest failed (${res.status}): ${body?.error || "unknown error"}`)); process.exit(1); return; }
2462
- console.log(c.green("Suggested in the chat. The user can start it in one tap."));
2684
+ finish(await waitFor(started.id, timeout + 5));
2463
2685
  }
2464
2686
 
2465
2687
  // Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
@@ -2590,7 +2812,9 @@ async function cmdDelegate(args = []) {
2590
2812
  // Universal app publishing: every agent that can write JSON and run a command
2591
2813
  // uses the same contract. Relative static directories are manifest-relative.
2592
2814
  async function cmdApp(args = []) {
2593
- const [action, ...rest] = args;
2815
+ // Output is JSON already; --json is accepted so every session command takes it.
2816
+ const json = wantsJson(args);
2817
+ const [action, ...rest] = args.filter((arg) => arg !== "--json");
2594
2818
  if (!action || rest.some((arg) => arg === "--help" || arg === "-h") || ["help", "--help", "-h"].includes(action)) {
2595
2819
  console.log(`Usage: bivy app publish <manifest.json> [--session <id>]
2596
2820
  bivy app list [--session <id>]
@@ -2658,7 +2882,7 @@ Web previews require operator setup; see docs/apps.md.`);
2658
2882
  return;
2659
2883
  }
2660
2884
  if (INPUT_ACTIONS[action]) return appInput(action, rest);
2661
- if (action === "menu") return appMenu(rest);
2885
+ if (action === "menu") return appMenu(rest, { json });
2662
2886
  if (!["publish", "list", "remove", "shot", "run", "present", "share", "notes"].includes(action)) throw new Error("Unknown app command. Run bivy app --help.");
2663
2887
  // `run -- <command> [args…]`: publish a one-window desktop app without a manifest file.
2664
2888
  let runManifest;
@@ -2743,7 +2967,7 @@ Web previews require operator setup; see docs/apps.md.`);
2743
2967
  await appRequest(action === "run" ? "publish" : action, body);
2744
2968
  }
2745
2969
 
2746
- async function appMenu(rest) {
2970
+ async function appMenu(rest, { json = false } = {}) {
2747
2971
  const options = {};
2748
2972
  const positional = [];
2749
2973
  for (let i = 0; i < rest.length; i++) {
@@ -2760,7 +2984,7 @@ async function appMenu(rest) {
2760
2984
  const body = { sessionId, ...(options["--app"] ? { target: options["--app"] } : {}) };
2761
2985
  if (positional[0]) body.path = positional[0].split(">").map((part) => part.trim());
2762
2986
  const result = await appRequest("menu", body, { print: false });
2763
- if (!result.menus) { console.log(JSON.stringify(result, null, 2)); return; }
2987
+ if (json || !result.menus) { console.log(JSON.stringify(result, null, 2)); return; }
2764
2988
  // A tree an agent can read: one item per line, indented by level.
2765
2989
  const lines = [];
2766
2990
  const walk = (items, depth) => {
@@ -5464,81 +5688,104 @@ function readSelfVersion() {
5464
5688
  }
5465
5689
 
5466
5690
  function printHelp() {
5467
- console.log(`
5468
- ${c.bold("bivy")} — Bivy node CLI
5469
-
5470
- ${c.cyan("bivy run claude")} Run a native agent (real CLI/TUI) as a relay-visible session
5471
- ${c.cyan("bivy run <agent>")} ${[...AGENT_INTEGRATIONS.keys()].join(" | ")} | -- <command>
5472
- ${c.cyan("bivy run <agent> --name <label>")} Name the session (shown in 'bivy sessions' and the app)
5473
- ${c.cyan("bivy run <agent> --model <model>")} Run with a specific model (passed to the agent, shown in the cockpit)
5474
- ${c.cyan("bivy run <agent> --node <name>")} Start the session on another registered node
5475
- ${c.cyan("bivy run <agent> --clone [remote]")} Start in a fresh clone (current repo, or a given remote)
5476
- ${c.cyan("bivy run <agent> --workspace <dir>")} Start in an existing directory (default: current repo, else the configured workspace)
5477
- ${c.cyan("bivy rename <name>")} Rename this node (takes effect immediately, no restart)
5478
- ${c.cyan("bivy nodes")} List/add/remove other nodes (add <name> <url> --token <t>)
5479
- ${c.cyan("bivy agents")} List the supported agents and which are installed (--json)
5480
- ${c.cyan("bivy shim install <agent>")} Make interactive '<agent>' launch its native TUI in a Bivy PTY (remote-visible)
5481
- ${c.cyan("bivy shim")} List installed agent shims (install/uninstall/status)
5482
- ${c.cyan("bivy takeover <id>")} Stop a pinned run-terminal's native TUI and continue it as a governed chat
5483
- ${c.cyan("bivy token")} Print a device token for this node (for 'bivy nodes add' elsewhere)
5484
- ${c.cyan("bivy sessions")} List recent sessions (live + saved) and resume one (alias: ls)
5485
- ${c.cyan("bivy resume")} [n|id] Resume a session directly (default: most recent)
5486
- ${c.cyan("bivy app")} Publish and list session apps (web and terminal views)
5487
- ${c.cyan("bivy send <id>")} "..." Send a prompt to an existing session and stream the reply
5488
- ${c.cyan("bivy kill <id>")} Stop a session/terminal (--delete also removes a saved session)
5489
- ${c.cyan("bivy prune")} Delete old sessions/workspaces/worktrees (--keep N, --older-than 7d, --dry-run)
5490
- ${c.cyan("bivy exec")} "<prompt>" One-shot headless session: prints the answer to stdout (pipe-friendly; --agent/--model/--name/--workspace/--session/--json)
5491
- ${c.cyan("bivy runs start")} "<instructions>" Queue a one-off unattended Run with checks and a Receipt
5492
- ${c.cyan("bivy automation")} list | trigger | init | validate | plan | test | apply
5493
- ${c.cyan("bivy config")} init | validate | show | get | set | explain (typed node config)
5494
- ${c.cyan("bivy plugin")} init | validate | doctor | test | install | list | remove
5495
- ${c.cyan("bivy")} Show this help
5496
- ${c.cyan("bivy setup")} First-run wizard: agent, model login, remote sign-in, background service
5497
- ${c.cyan("bivy start")} Run the daemon in the foreground
5498
- ${c.cyan("bivy stop")} Stop the background service
5499
- ${c.cyan("bivy restart")} Restart the background service (waits for active sessions to finish a turn; --force to skip)
5500
- ${c.cyan("bivy status")} Show config and whether the node is reachable
5501
- ${c.cyan("bivy doctor")} Health check: deps, node, model, remote, agents
5502
- ${c.cyan("bivy capabilities")} [--json] What this Machine unlocks: OS, agents, providers, Docker/GPU, plugins, workspaces
5503
- ${c.cyan("bivy logs")} [-f] Tail the node logs (systemd journal, launchd, or background log)
5504
- ${c.cyan("bivy login")} Sign this machine into a Bivy account (GitHub or email)
5505
- ${c.cyan("bivy logout")} Sign this machine out (alias: signout)
5506
- ${c.cyan("bivy provider login")} Sign into a model provider (alias: model login)
5507
- ${c.cyan("bivy auth import")} Import local Claude, Codex, or Grok logins into the vault
5508
- ${c.cyan("bivy update")} Update Bivy + install deps + restart service (waits for active sessions to finish a turn; --force to skip)
5509
- ${c.cyan("bivy update:log")} Show output of the last (or in-progress) update
5510
- ${c.cyan("bivy agent add")} Connect an existing user-owned agent
5511
- ${c.cyan("bivy agents:install")} Install known upstream agents (${KNOWN_AGENT_INSTALLS.map((a) => a.label).join(", ")})
5512
- ${c.cyan("bivy open")} Open the remote web/PWA app
5513
- ${c.cyan("bivy service")} install | uninstall | status
5514
- ${c.cyan("bivy uninstall")} Remove Bivy and all its data (--keep-sessions, --keep-worktrees, --dry-run)
5515
- ${c.cyan("bivy link")} Show a remote web/PWA link QR in the terminal
5516
- ${c.cyan("bivy relay:setup")} Enable secure remote web/PWA access (sign in once)
5517
- ${c.cyan("bivy github:app-create")} One-click: create + connect a GitHub App
5518
- ${c.cyan("bivy github:app-connect")} Connect an existing GitHub App (--app-id --key)
5519
- ${c.cyan("bivy github:app-sync")} [on|off] Sync connected GitHub App keys to this account's other opted-in nodes
5520
- ${c.cyan("bivy github:connect")} [owner/repo] Connect GitHub in the app (or use a configured self-hosted device flow)
5521
- ${c.cyan("bivy secrets")} list | set | ref | delete | doctor | resolve
5522
- ${c.cyan("bivy voice")} Configure speech-to-text: provider | key | remove | status
5523
- ${c.cyan("bivy completions")} <bash|zsh|fish> Print a shell completion script
5524
- ${c.cyan("bivy version")} Print the installed Bivy version (alias: --version, -v)
5525
- `);
5691
+ const details = { run: `Agents: ${[...AGENT_INTEGRATIONS.keys()].join(" | ")}, or -- <command>` };
5692
+ console.log(`\n${renderHelp({ paint: c.cyan, bold: c.bold, details })}\n`);
5693
+ }
5694
+
5695
+ // `bivy help [command] [--json]`: the whole table, or one command's row. The
5696
+ // row's --help has the full flags; JSON is for agents and scripts.
5697
+ function cmdHelp(args = []) {
5698
+ const json = wantsJson(args);
5699
+ const word = args.find((a) => !a.startsWith("-"));
5700
+ if (!word) {
5701
+ if (json) console.log(JSON.stringify(describeCli(), null, 2));
5702
+ else printHelp();
5703
+ return;
5704
+ }
5705
+ const entry = resolveCommand(word);
5706
+ if (!entry) {
5707
+ const close = suggestCommands(word);
5708
+ cliError({ code: "unknown_command", message: `Unknown command: ${word}`, hint: close.length ? `Did you mean: ${close.join(", ")}?` : undefined, next: "bivy help", exit: EXIT.usage }, { json, paint: c.red });
5709
+ return;
5710
+ }
5711
+ const row = describeCommand(entry);
5712
+ if (json) { console.log(JSON.stringify(row, null, 2)); return; }
5713
+ console.log(`Usage: ${row.usage}\n\n${row.summary}${row.aliases.length ? `\nAliases: ${row.aliases.join(", ")}` : ""}${row.subcommands.length ? `\nSubcommands: ${row.subcommands.join(", ")}` : ""}\n\nFull options: ${row.help}`);
5714
+ }
5715
+
5716
+ // `bivy guide [topic]` — short playbooks for agents, one markdown file per
5717
+ // topic in bin/guides (a "# Title" line, then "Summary: …").
5718
+ function readGuides() {
5719
+ const dir = path.join(__dirname, "guides");
5720
+ return fs.readdirSync(dir).filter((file) => file.endsWith(".md")).sort().map((file) => {
5721
+ const markdown = fs.readFileSync(path.join(dir, file), "utf8");
5722
+ return {
5723
+ topic: file.slice(0, -3),
5724
+ title: markdown.match(/^# (.+)$/m)?.[1] ?? file,
5725
+ summary: markdown.match(/^Summary: (.+)$/m)?.[1] ?? "",
5726
+ markdown: markdown.replace(/^Summary: .+\n\n?/m, ""),
5727
+ };
5728
+ });
5729
+ }
5730
+
5731
+ // `bivy tool <name> '<json>'` — how `bivy mcp-serve` runs a tool: map the input
5732
+ // to the command's words and run it with --json, so a tool and its command can't
5733
+ // behave differently.
5734
+ function cmdTool(args = []) {
5735
+ const [name, raw = "{}"] = args;
5736
+ const tool = describeTools().find((t) => t.name === name);
5737
+ if (!tool) return cliError({ code: "unknown_tool", message: `No tool called "${name}".`, next: "bivy help --json", exit: EXIT.usage }, { json: true });
5738
+ let input;
5739
+ try { input = JSON.parse(raw); } catch { return cliError({ code: "usage", message: "Tool input must be a JSON object.", exit: EXIT.usage }, { json: true }); }
5740
+ const result = spawnSync(process.execPath, [selfScript, ...toolArgv(tool.argv, input ?? {}), "--json"], { stdio: "inherit", env: process.env });
5741
+ process.exit(result.status ?? EXIT.failed);
5742
+ }
5743
+
5744
+ function cmdGuide(args = []) {
5745
+ if (args.includes("-h") || args.includes("--help")) {
5746
+ console.log("Usage: bivy guide [topic] [--json]\n\nShort playbooks for agents working in Bivy. Without a topic, lists them.");
5747
+ return;
5748
+ }
5749
+ const json = wantsJson(args);
5750
+ const guides = readGuides();
5751
+ const topic = args.find((a) => !a.startsWith("-"));
5752
+ if (!topic) {
5753
+ if (json) { console.log(JSON.stringify({ guides: guides.map(({ topic: t, title, summary }) => ({ topic: t, title, summary })) }, null, 2)); return; }
5754
+ console.log(`${c.bold("Guides")} (bivy guide <topic>)\n`);
5755
+ for (const guide of guides) console.log(` ${c.cyan(guide.topic.padEnd(18))} ${guide.summary}`);
5756
+ return;
5757
+ }
5758
+ const guide = guides.find((g) => g.topic === topic);
5759
+ if (!guide) {
5760
+ cliError({ code: "unknown_guide", message: `No guide called "${topic}".`, hint: `Guides: ${guides.map((g) => g.topic).join(", ")}.`, next: "bivy guide", exit: EXIT.notFound }, { json, paint: c.red });
5761
+ return;
5762
+ }
5763
+ if (json) { console.log(JSON.stringify({ topic: guide.topic, title: guide.title, markdown: guide.markdown }, null, 2)); return; }
5764
+ console.log(guide.markdown.trimEnd());
5526
5765
  }
5527
5766
 
5528
5767
  async function main() {
5529
5768
  await hydrateCanonicalConfig();
5530
5769
  const argv = process.argv.slice(2);
5531
5770
  const [command, ...args] = argv;
5532
- switch (command) {
5533
- case undefined:
5534
- printHelp();
5535
- break;
5771
+ if (command === undefined) { printHelp(); return; }
5772
+ const entry = resolveCommand(command);
5773
+ if (!entry) {
5774
+ const close = suggestCommands(command);
5775
+ cliError({
5776
+ code: "unknown_command",
5777
+ message: `Unknown command: ${command}`,
5778
+ hint: close.length ? `Did you mean: ${close.map((name) => `bivy ${name}`).join(", ")}?` : undefined,
5779
+ next: "bivy help",
5780
+ exit: EXIT.usage,
5781
+ }, { json: wantsJson(args), paint: c.red });
5782
+ return;
5783
+ }
5784
+ switch (entry.name) {
5536
5785
  case "setup":
5537
- case "init":
5538
5786
  await cmdSetup(args);
5539
5787
  break;
5540
5788
  case "start":
5541
- case "dev":
5542
5789
  await cmdStart(args);
5543
5790
  break;
5544
5791
  case "run":
@@ -5559,7 +5806,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5559
5806
  await cmdRun(args);
5560
5807
  break;
5561
5808
  case "sessions":
5562
- case "ls":
5563
5809
  await cmdSessions(args);
5564
5810
  break;
5565
5811
  case "resume":
@@ -5569,7 +5815,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5569
5815
  await cmdPromote(args);
5570
5816
  break;
5571
5817
  case "rename":
5572
- case "node:rename":
5573
5818
  await cmdRename(args);
5574
5819
  break;
5575
5820
  case "nodes":
@@ -5584,7 +5829,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5584
5829
  cmdAgents(args);
5585
5830
  break;
5586
5831
  case "shim":
5587
- case "listen":
5588
5832
  await cmdShim(args);
5589
5833
  break;
5590
5834
  case "takeover":
@@ -5600,7 +5844,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5600
5844
  await cmdKill(args);
5601
5845
  break;
5602
5846
  case "prune":
5603
- case "clean":
5604
5847
  await cmdPrune(args);
5605
5848
  break;
5606
5849
  case "send":
@@ -5615,11 +5858,16 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5615
5858
  case "suggest":
5616
5859
  await cmdSuggest(args);
5617
5860
  break;
5861
+ case "notify":
5862
+ await cmdNotify(args);
5863
+ break;
5864
+ case "ask":
5865
+ await cmdAsk(args);
5866
+ break;
5618
5867
  case "delegate":
5619
5868
  await cmdDelegate(args);
5620
5869
  break;
5621
5870
  case "completions":
5622
- case "completion":
5623
5871
  cmdCompletions(args);
5624
5872
  break;
5625
5873
  case "runs": {
@@ -5649,8 +5897,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5649
5897
  process.exit(await run(nodeBin, [...nodeScriptArgs(automationEntry), internalAction, ...runArgs], { cwd: process.cwd(), env: process.env }));
5650
5898
  break;
5651
5899
  }
5652
- case "automation":
5653
- case "automations": {
5900
+ case "automation": {
5654
5901
  if (!(await ensureDeps())) process.exit(1);
5655
5902
  process.exit(await run(nodeBin, [...nodeScriptArgs(automationEntry), ...args], { cwd: process.cwd(), env: process.env }));
5656
5903
  break;
@@ -5660,17 +5907,14 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5660
5907
  process.exit(await run(nodeBin, [...nodeScriptArgs(configEntry), ...args], { cwd: process.cwd(), env: process.env }));
5661
5908
  break;
5662
5909
  }
5663
- case "auth":
5664
- case "credentials":
5665
- case "creds": {
5910
+ case "auth": {
5666
5911
  if (!(await ensureDeps())) process.exit(1);
5667
5912
  // Forward a subcommand-position --help to the CLI's first-arg help (like secrets).
5668
5913
  const forwardArgs = (args.includes("-h") || args.includes("--help")) ? ["--help"] : args;
5669
5914
  process.exit(await run(nodeBin, [...nodeScriptArgs(credentialsEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
5670
5915
  break;
5671
5916
  }
5672
- case "plugin":
5673
- case "plugins": {
5917
+ case "plugin": {
5674
5918
  if (!(await ensureDeps())) process.exit(1);
5675
5919
  const pluginEnv = { ...process.env };
5676
5920
  const configuredAgents = loadConfig().env?.BIVY_CUSTOM_AGENTS;
@@ -5724,11 +5968,9 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5724
5968
  await cmdAccountLogin(args);
5725
5969
  break;
5726
5970
  case "logout":
5727
- case "signout":
5728
5971
  await cmdAccountLogout(args);
5729
5972
  break;
5730
- case "provider":
5731
- case "model": {
5973
+ case "provider": {
5732
5974
  const [action, ...providerArgs] = args;
5733
5975
  if (action === "login") {
5734
5976
  await cmdProviderLogin(providerArgs);
@@ -5750,7 +5992,6 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5750
5992
  cmdAudit(args);
5751
5993
  break;
5752
5994
  case "agents:install":
5753
- case "runtimes:install":
5754
5995
  if (args.includes("-h") || args.includes("--help")) {
5755
5996
  console.log(`Usage: bivy agents:install [--bridges]\n\nInstall known agent integrations (${KNOWN_AGENT_INSTALLS.map((a) => a.label).join(", ")}).\n\n --bridges only install the SDK bridges Bivy loads for Claude Code and Pi\n (they otherwise install on first use)`);
5756
5997
  break;
@@ -5787,7 +6028,6 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5787
6028
  await cmdRelaySetup(args);
5788
6029
  break;
5789
6030
  case "github:connect":
5790
- case "connect-repo":
5791
6031
  if (args.includes("-h") || args.includes("--help")) {
5792
6032
  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.");
5793
6033
  break;
@@ -5856,8 +6096,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5856
6096
  }
5857
6097
  break;
5858
6098
  }
5859
- case "secrets":
5860
- case "secret": {
6099
+ case "secrets": {
5861
6100
  if (!(await ensureDeps())) process.exit(1);
5862
6101
  // secrets-cli.ts only recognizes "--help"/"help" as the FIRST argument, so
5863
6102
  // a subcommand-position --help (e.g. 'bivy secrets list --help') would
@@ -5866,8 +6105,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5866
6105
  process.exit(await run(nodeBin, [...nodeScriptArgs(secretsEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
5867
6106
  break;
5868
6107
  }
5869
- case "voice":
5870
- case "stt": {
6108
+ case "voice": {
5871
6109
  if (!(await ensureDeps())) process.exit(1);
5872
6110
  const forwardArgs = (args.includes("-h") || args.includes("--help")) ? ["--help"] : args;
5873
6111
  process.exit(await run(nodeBin, [...nodeScriptArgs(sttEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
@@ -5888,25 +6126,29 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5888
6126
  await run(nodeBin, [...nodeScriptArgs(mcpServeEntry), ...args], { cwd: process.cwd(), env: process.env });
5889
6127
  break;
5890
6128
  case "help":
5891
- case "-h":
5892
- case "--help":
5893
- printHelp();
6129
+ cmdHelp(args);
6130
+ break;
6131
+ case "context":
6132
+ await cmdContext(args);
6133
+ break;
6134
+ case "guide":
6135
+ cmdGuide(args);
6136
+ break;
6137
+ case "tool":
6138
+ cmdTool(args);
5894
6139
  break;
5895
6140
  case "version":
5896
- case "--version":
5897
- case "-v":
5898
6141
  console.log(readSelfVersion());
5899
6142
  break;
5900
6143
  default:
5901
- console.error(c.red(`Unknown command: ${command}`));
5902
- printHelp();
5903
- process.exit(1);
6144
+ throw new Error(`bivy ${entry.name} is listed in bin/cli-commands.mjs but not dispatched.`);
5904
6145
  }
5905
6146
  }
5906
6147
 
5907
6148
  main().catch((error) => {
5908
6149
  // Show a clean message to users; the full stack is only useful with BIVY_DEBUG.
5909
- console.error(c.red(error?.message || String(error)));
6150
+ if (wantsJson(process.argv.slice(2))) process.stderr.write(`${JSON.stringify({ error: { code: "failed", message: error?.message || String(error) } })}\n`);
6151
+ else console.error(c.red(error?.message || String(error)));
5910
6152
  if (process.env.BIVY_DEBUG && error?.stack) console.error(c.dim(error.stack));
5911
6153
  process.exit(1);
5912
6154
  });