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

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, renderHelp, resolveCommand, subcommandTable, suggestCommands, 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,26 +2529,15 @@ 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; }
2446
- let token;
2447
- 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",
2452
- 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);
2458
- return;
2459
- }
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 });
2460
2538
  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; }
2539
+ if (!res.ok) return fail(sessionHttpError("Suggest", res.status, body));
2540
+ if (json) { console.log(JSON.stringify(body)); return; }
2462
2541
  console.log(c.green("Suggested in the chat. The user can start it in one tap."));
2463
2542
  }
2464
2543
 
@@ -2590,7 +2669,9 @@ async function cmdDelegate(args = []) {
2590
2669
  // Universal app publishing: every agent that can write JSON and run a command
2591
2670
  // uses the same contract. Relative static directories are manifest-relative.
2592
2671
  async function cmdApp(args = []) {
2593
- const [action, ...rest] = args;
2672
+ // Output is JSON already; --json is accepted so every session command takes it.
2673
+ const json = wantsJson(args);
2674
+ const [action, ...rest] = args.filter((arg) => arg !== "--json");
2594
2675
  if (!action || rest.some((arg) => arg === "--help" || arg === "-h") || ["help", "--help", "-h"].includes(action)) {
2595
2676
  console.log(`Usage: bivy app publish <manifest.json> [--session <id>]
2596
2677
  bivy app list [--session <id>]
@@ -2658,7 +2739,7 @@ Web previews require operator setup; see docs/apps.md.`);
2658
2739
  return;
2659
2740
  }
2660
2741
  if (INPUT_ACTIONS[action]) return appInput(action, rest);
2661
- if (action === "menu") return appMenu(rest);
2742
+ if (action === "menu") return appMenu(rest, { json });
2662
2743
  if (!["publish", "list", "remove", "shot", "run", "present", "share", "notes"].includes(action)) throw new Error("Unknown app command. Run bivy app --help.");
2663
2744
  // `run -- <command> [args…]`: publish a one-window desktop app without a manifest file.
2664
2745
  let runManifest;
@@ -2743,7 +2824,7 @@ Web previews require operator setup; see docs/apps.md.`);
2743
2824
  await appRequest(action === "run" ? "publish" : action, body);
2744
2825
  }
2745
2826
 
2746
- async function appMenu(rest) {
2827
+ async function appMenu(rest, { json = false } = {}) {
2747
2828
  const options = {};
2748
2829
  const positional = [];
2749
2830
  for (let i = 0; i < rest.length; i++) {
@@ -2760,7 +2841,7 @@ async function appMenu(rest) {
2760
2841
  const body = { sessionId, ...(options["--app"] ? { target: options["--app"] } : {}) };
2761
2842
  if (positional[0]) body.path = positional[0].split(">").map((part) => part.trim());
2762
2843
  const result = await appRequest("menu", body, { print: false });
2763
- if (!result.menus) { console.log(JSON.stringify(result, null, 2)); return; }
2844
+ if (json || !result.menus) { console.log(JSON.stringify(result, null, 2)); return; }
2764
2845
  // A tree an agent can read: one item per line, indented by level.
2765
2846
  const lines = [];
2766
2847
  const walk = (items, depth) => {
@@ -5464,81 +5545,53 @@ function readSelfVersion() {
5464
5545
  }
5465
5546
 
5466
5547
  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
- `);
5548
+ const details = { run: `Agents: ${[...AGENT_INTEGRATIONS.keys()].join(" | ")}, or -- <command>` };
5549
+ console.log(`\n${renderHelp({ paint: c.cyan, bold: c.bold, details })}\n`);
5550
+ }
5551
+
5552
+ // `bivy help [command] [--json]`: the whole table, or one command's row. The
5553
+ // row's --help has the full flags; JSON is for agents and scripts.
5554
+ function cmdHelp(args = []) {
5555
+ const json = wantsJson(args);
5556
+ const word = args.find((a) => !a.startsWith("-"));
5557
+ if (!word) {
5558
+ if (json) console.log(JSON.stringify(describeCli(), null, 2));
5559
+ else printHelp();
5560
+ return;
5561
+ }
5562
+ const entry = resolveCommand(word);
5563
+ if (!entry) {
5564
+ const close = suggestCommands(word);
5565
+ 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 });
5566
+ return;
5567
+ }
5568
+ const row = describeCommand(entry);
5569
+ if (json) { console.log(JSON.stringify(row, null, 2)); return; }
5570
+ 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}`);
5526
5571
  }
5527
5572
 
5528
5573
  async function main() {
5529
5574
  await hydrateCanonicalConfig();
5530
5575
  const argv = process.argv.slice(2);
5531
5576
  const [command, ...args] = argv;
5532
- switch (command) {
5533
- case undefined:
5534
- printHelp();
5535
- break;
5577
+ if (command === undefined) { printHelp(); return; }
5578
+ const entry = resolveCommand(command);
5579
+ if (!entry) {
5580
+ const close = suggestCommands(command);
5581
+ cliError({
5582
+ code: "unknown_command",
5583
+ message: `Unknown command: ${command}`,
5584
+ hint: close.length ? `Did you mean: ${close.map((name) => `bivy ${name}`).join(", ")}?` : undefined,
5585
+ next: "bivy help",
5586
+ exit: EXIT.usage,
5587
+ }, { json: wantsJson(args), paint: c.red });
5588
+ return;
5589
+ }
5590
+ switch (entry.name) {
5536
5591
  case "setup":
5537
- case "init":
5538
5592
  await cmdSetup(args);
5539
5593
  break;
5540
5594
  case "start":
5541
- case "dev":
5542
5595
  await cmdStart(args);
5543
5596
  break;
5544
5597
  case "run":
@@ -5559,7 +5612,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5559
5612
  await cmdRun(args);
5560
5613
  break;
5561
5614
  case "sessions":
5562
- case "ls":
5563
5615
  await cmdSessions(args);
5564
5616
  break;
5565
5617
  case "resume":
@@ -5569,7 +5621,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5569
5621
  await cmdPromote(args);
5570
5622
  break;
5571
5623
  case "rename":
5572
- case "node:rename":
5573
5624
  await cmdRename(args);
5574
5625
  break;
5575
5626
  case "nodes":
@@ -5584,7 +5635,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5584
5635
  cmdAgents(args);
5585
5636
  break;
5586
5637
  case "shim":
5587
- case "listen":
5588
5638
  await cmdShim(args);
5589
5639
  break;
5590
5640
  case "takeover":
@@ -5600,7 +5650,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5600
5650
  await cmdKill(args);
5601
5651
  break;
5602
5652
  case "prune":
5603
- case "clean":
5604
5653
  await cmdPrune(args);
5605
5654
  break;
5606
5655
  case "send":
@@ -5619,7 +5668,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5619
5668
  await cmdDelegate(args);
5620
5669
  break;
5621
5670
  case "completions":
5622
- case "completion":
5623
5671
  cmdCompletions(args);
5624
5672
  break;
5625
5673
  case "runs": {
@@ -5649,8 +5697,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5649
5697
  process.exit(await run(nodeBin, [...nodeScriptArgs(automationEntry), internalAction, ...runArgs], { cwd: process.cwd(), env: process.env }));
5650
5698
  break;
5651
5699
  }
5652
- case "automation":
5653
- case "automations": {
5700
+ case "automation": {
5654
5701
  if (!(await ensureDeps())) process.exit(1);
5655
5702
  process.exit(await run(nodeBin, [...nodeScriptArgs(automationEntry), ...args], { cwd: process.cwd(), env: process.env }));
5656
5703
  break;
@@ -5660,17 +5707,14 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5660
5707
  process.exit(await run(nodeBin, [...nodeScriptArgs(configEntry), ...args], { cwd: process.cwd(), env: process.env }));
5661
5708
  break;
5662
5709
  }
5663
- case "auth":
5664
- case "credentials":
5665
- case "creds": {
5710
+ case "auth": {
5666
5711
  if (!(await ensureDeps())) process.exit(1);
5667
5712
  // Forward a subcommand-position --help to the CLI's first-arg help (like secrets).
5668
5713
  const forwardArgs = (args.includes("-h") || args.includes("--help")) ? ["--help"] : args;
5669
5714
  process.exit(await run(nodeBin, [...nodeScriptArgs(credentialsEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
5670
5715
  break;
5671
5716
  }
5672
- case "plugin":
5673
- case "plugins": {
5717
+ case "plugin": {
5674
5718
  if (!(await ensureDeps())) process.exit(1);
5675
5719
  const pluginEnv = { ...process.env };
5676
5720
  const configuredAgents = loadConfig().env?.BIVY_CUSTOM_AGENTS;
@@ -5724,11 +5768,9 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5724
5768
  await cmdAccountLogin(args);
5725
5769
  break;
5726
5770
  case "logout":
5727
- case "signout":
5728
5771
  await cmdAccountLogout(args);
5729
5772
  break;
5730
- case "provider":
5731
- case "model": {
5773
+ case "provider": {
5732
5774
  const [action, ...providerArgs] = args;
5733
5775
  if (action === "login") {
5734
5776
  await cmdProviderLogin(providerArgs);
@@ -5750,7 +5792,6 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5750
5792
  cmdAudit(args);
5751
5793
  break;
5752
5794
  case "agents:install":
5753
- case "runtimes:install":
5754
5795
  if (args.includes("-h") || args.includes("--help")) {
5755
5796
  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
5797
  break;
@@ -5787,7 +5828,6 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5787
5828
  await cmdRelaySetup(args);
5788
5829
  break;
5789
5830
  case "github:connect":
5790
- case "connect-repo":
5791
5831
  if (args.includes("-h") || args.includes("--help")) {
5792
5832
  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
5833
  break;
@@ -5856,8 +5896,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5856
5896
  }
5857
5897
  break;
5858
5898
  }
5859
- case "secrets":
5860
- case "secret": {
5899
+ case "secrets": {
5861
5900
  if (!(await ensureDeps())) process.exit(1);
5862
5901
  // secrets-cli.ts only recognizes "--help"/"help" as the FIRST argument, so
5863
5902
  // a subcommand-position --help (e.g. 'bivy secrets list --help') would
@@ -5866,8 +5905,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5866
5905
  process.exit(await run(nodeBin, [...nodeScriptArgs(secretsEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
5867
5906
  break;
5868
5907
  }
5869
- case "voice":
5870
- case "stt": {
5908
+ case "voice": {
5871
5909
  if (!(await ensureDeps())) process.exit(1);
5872
5910
  const forwardArgs = (args.includes("-h") || args.includes("--help")) ? ["--help"] : args;
5873
5911
  process.exit(await run(nodeBin, [...nodeScriptArgs(sttEntry), ...forwardArgs], { cwd: repoRoot, env: process.env }));
@@ -5888,25 +5926,23 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5888
5926
  await run(nodeBin, [...nodeScriptArgs(mcpServeEntry), ...args], { cwd: process.cwd(), env: process.env });
5889
5927
  break;
5890
5928
  case "help":
5891
- case "-h":
5892
- case "--help":
5893
- printHelp();
5929
+ cmdHelp(args);
5930
+ break;
5931
+ case "context":
5932
+ await cmdContext(args);
5894
5933
  break;
5895
5934
  case "version":
5896
- case "--version":
5897
- case "-v":
5898
5935
  console.log(readSelfVersion());
5899
5936
  break;
5900
5937
  default:
5901
- console.error(c.red(`Unknown command: ${command}`));
5902
- printHelp();
5903
- process.exit(1);
5938
+ throw new Error(`bivy ${entry.name} is listed in bin/cli-commands.mjs but not dispatched.`);
5904
5939
  }
5905
5940
  }
5906
5941
 
5907
5942
  main().catch((error) => {
5908
5943
  // Show a clean message to users; the full stack is only useful with BIVY_DEBUG.
5909
- console.error(c.red(error?.message || String(error)));
5944
+ if (wantsJson(process.argv.slice(2))) process.stderr.write(`${JSON.stringify({ error: { code: "failed", message: error?.message || String(error) } })}\n`);
5945
+ else console.error(c.red(error?.message || String(error)));
5910
5946
  if (process.env.BIVY_DEBUG && error?.stack) console.error(c.dim(error.stack));
5911
5947
  process.exit(1);
5912
5948
  });
@@ -0,0 +1,214 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // The `bivy` command table: one row per top-level command. bin/bivy.mjs
4
+ // dispatches only commands found here, and `bivy help`, `bivy help --json`,
5
+ // shell completions, "did you mean" and `pnpm run check:cli` (which keeps
6
+ // docs/cli-reference.md in step) all read these rows. Adding a command means
7
+ // adding a row; a command without one cannot be dispatched.
8
+ //
9
+ // Fields:
10
+ // name canonical name (what the dispatcher switches on)
11
+ // aliases other spellings that dispatch to it
12
+ // group help section (GROUPS below, in display order)
13
+ // usage one-line synopsis after `bivy`
14
+ // summary what it does, one line
15
+ // scope "session" (acts on $BIVY_SESSION_ID), "node" (this machine) or "account"
16
+ // json true when it accepts --json (or honors BIVY_OUTPUT=json)
17
+ // subcommands first-argument verbs, for completion and discovery
18
+ // hidden plumbing launched by Bivy itself; left out of help and completions
19
+
20
+ /** Exit codes shared by the commands agents call. 0 and 1 hold everywhere. */
21
+ export const EXIT = Object.freeze({ ok: 0, failed: 1, usage: 2, notFound: 3, denied: 4, timeout: 5, conflict: 6, unavailable: 75 });
22
+
23
+ export const GROUPS = [
24
+ ["session", "Inside an agent session (defaults to $BIVY_SESSION_ID)"],
25
+ ["sessions", "Agents and sessions"],
26
+ ["runs", "Runs and automations"],
27
+ ["node", "This node"],
28
+ ["accounts", "Accounts, providers and credentials"],
29
+ ["config", "Configuration and extensions"],
30
+ ["help", "Help and discovery"],
31
+ ];
32
+
33
+ export const COMMANDS = [
34
+ // Inside an agent session
35
+ { name: "context", group: "session", scope: "session", json: true, usage: "context [--json]", summary: "Where this agent is running: session, workspace, machine, apps, and what to run next" },
36
+ { name: "attach", group: "session", scope: "session", json: true, usage: 'attach <file> [--caption "…"] [--artifact]', summary: "Show a local file or image to the user in the chat" },
37
+ { name: "suggest", group: "session", scope: "session", json: true, usage: 'suggest "<task>" [--title "label"]', summary: "Propose a task the user can start in one tap" },
38
+ { name: "app", group: "session", scope: "session", json: true, usage: "app <publish|shot|present|share|run|…>", summary: "Live previews: publish a web/terminal/desktop app, screenshot it, present it", subcommands: ["publish", "list", "remove", "shot", "present", "share", "notes", "run", "click", "type", "key", "scroll", "drag", "move", "menu"] },
39
+ { name: "delegate", group: "session", scope: "session", json: true, usage: 'delegate "<task>" [--agent <id>] [--machine <name>] [--wait]', summary: "Hand a task to another agent or machine and get its answer back", subcommands: ["machines", "status", "wait"] },
40
+
41
+ // Agents and sessions
42
+ { name: "run", group: "sessions", scope: "node", usage: "run <agent> [--chat] [--name …] [--model …] [--node …] [--workspace …]", summary: "Run an agent (native CLI/TUI) as a session the app can see" },
43
+ { name: "exec", group: "sessions", scope: "node", json: true, usage: 'exec "<prompt>" [--agent …] [--session …]', summary: "One-shot headless session: prints the answer to stdout" },
44
+ { name: "send", group: "sessions", scope: "node", json: true, usage: 'send <id> "<message>"', summary: "Send a prompt to an existing session and stream the reply" },
45
+ { name: "sessions", aliases: ["ls"], group: "sessions", scope: "node", json: true, usage: "sessions [--json]", summary: "List recent sessions (live and saved) and resume one" },
46
+ { name: "resume", group: "sessions", scope: "node", usage: "resume [n|id]", summary: "Resume a session directly (default: most recent)" },
47
+ { name: "kill", group: "sessions", scope: "node", usage: "kill <id> [--delete]", summary: "Stop a session or terminal" },
48
+ { name: "takeover", group: "sessions", scope: "node", usage: "takeover <id>", summary: "Stop a run-terminal's native TUI and continue it as a governed chat" },
49
+ { name: "promote", group: "sessions", scope: "node", usage: "promote <session-id>", summary: "Continue a warm-replicated session on this node" },
50
+ { name: "prune", aliases: ["clean"], group: "sessions", scope: "node", json: true, usage: "prune [--keep N] [--older-than 7d] [--dry-run]", summary: "Delete old sessions, workspaces and worktrees" },
51
+ { name: "agents", group: "sessions", scope: "node", json: true, usage: "agents [--json]", summary: "List supported agents and which are installed" },
52
+ { name: "agent", group: "sessions", scope: "node", json: true, usage: "agent <add|list|remove>", summary: "Connect, list or remove a user-owned agent", subcommands: ["add", "list", "remove"] },
53
+ { name: "agents:install", aliases: ["runtimes:install"], group: "sessions", scope: "node", usage: "agents:install [--bridges]", summary: "Install known upstream agents" },
54
+ { name: "shim", aliases: ["listen"], group: "sessions", scope: "node", usage: "shim [install|uninstall|status] <agent>", summary: "Make typing '<agent>' open its TUI in a session the app can see", subcommands: ["install", "uninstall", "status"] },
55
+
56
+ // Runs and automations
57
+ { name: "runs", group: "runs", scope: "account", json: true, usage: 'runs <start|list|status|wait>', summary: "Unattended Runs with checks, evidence and a Receipt", subcommands: ["start", "list", "status", "wait"] },
58
+ { name: "automation", aliases: ["automations"], group: "runs", scope: "account", json: true, usage: "automation <list|trigger|init|validate|plan|test|apply>", summary: "Automations as code: validate, plan, apply, trigger", subcommands: ["list", "trigger", "init", "validate", "plan", "test", "test-filter", "apply"] },
59
+
60
+ // This node
61
+ { name: "setup", aliases: ["init"], group: "node", scope: "node", usage: "setup", summary: "First-run wizard: agent, model login, remote sign-in, background service" },
62
+ { name: "start", aliases: ["dev"], group: "node", scope: "node", usage: "start", summary: "Run the daemon in the foreground" },
63
+ { name: "stop", group: "node", scope: "node", usage: "stop", summary: "Stop the background service" },
64
+ { name: "restart", group: "node", scope: "node", usage: "restart [--force]", summary: "Restart the background service after active turns finish" },
65
+ { name: "status", group: "node", scope: "node", json: true, usage: "status [--json]", summary: "Show config and whether the node is reachable" },
66
+ { name: "doctor", group: "node", scope: "node", usage: "doctor", summary: "Health check: deps, node, model, remote, agents" },
67
+ { name: "diagnostics", group: "node", scope: "node", usage: "diagnostics [--out <file>]", summary: "Print a redacted, shareable diagnostics bundle" },
68
+ { name: "capabilities", group: "node", scope: "node", json: true, usage: "capabilities [--json]", summary: "What this machine unlocks: OS, agents, providers, Docker/GPU, plugins" },
69
+ { name: "logs", group: "node", scope: "node", usage: "logs [-f]", summary: "Tail the node logs" },
70
+ { name: "audit", group: "node", scope: "node", json: true, usage: "audit [--session <id>] [--verify] [--json]", summary: "Show and verify the node's governance audit trail" },
71
+ { name: "update", group: "node", scope: "node", usage: "update [--force]", summary: "Update Bivy, install deps and restart the service" },
72
+ { name: "update:log", group: "node", scope: "node", usage: "update:log", summary: "Show output of the last (or in-progress) update" },
73
+ { name: "service", group: "node", scope: "node", usage: "service <install|uninstall|status>", summary: "Manage the background service", subcommands: ["install", "uninstall", "status"] },
74
+ { name: "rename", aliases: ["node:rename"], group: "node", scope: "node", usage: "rename <name>", summary: "Rename this node (no restart)" },
75
+ { name: "nodes", group: "node", scope: "node", usage: "nodes [list|add|remove]", summary: "List, add or remove other nodes", subcommands: ["list", "add", "remove"] },
76
+ { name: "token", group: "node", scope: "node", usage: "token", summary: "Print a device token for this node" },
77
+ { name: "open", group: "node", scope: "node", usage: "open", summary: "Open the remote web app" },
78
+ { name: "link", group: "node", scope: "node", usage: "link", summary: "Show a web app link as a QR code in the terminal" },
79
+ { name: "relay:setup", group: "node", scope: "node", usage: "relay:setup", summary: "Enable secure remote web app access" },
80
+ { name: "uninstall", group: "node", scope: "node", usage: "uninstall [--keep-sessions] [--dry-run]", summary: "Remove Bivy and all its data" },
81
+
82
+ // Accounts, providers and credentials
83
+ { name: "login", group: "accounts", scope: "account", usage: "login", summary: "Sign this machine into a Bivy account" },
84
+ { name: "logout", aliases: ["signout"], group: "accounts", scope: "account", usage: "logout", summary: "Sign this machine out" },
85
+ { name: "provider", aliases: ["model"], group: "accounts", scope: "node", usage: "provider login [provider]", summary: "Sign into a model provider", subcommands: ["login"] },
86
+ { name: "auth", aliases: ["credentials", "creds"], group: "accounts", scope: "node", usage: "auth <list|add|remove|import|sync|…>", summary: "Model credentials: import local logins, add, sync", subcommands: ["list", "add", "remove", "sync", "preset", "ingest", "config", "import"] },
87
+ { name: "secrets", aliases: ["secret"], group: "accounts", scope: "node", usage: "secrets <list|set|ref|delete|doctor|resolve>", summary: "Store tokens in the node vault", subcommands: ["list", "set", "ref", "delete", "doctor", "resolve"] },
88
+ { name: "github:connect", aliases: ["connect-repo"], group: "accounts", scope: "node", usage: "github:connect [owner/repo]", summary: "Connect GitHub" },
89
+ { name: "github:app-create", group: "accounts", scope: "node", usage: "github:app-create [--org <org>]", summary: "One-click: create and connect a GitHub App" },
90
+ { name: "github:app-connect", group: "accounts", scope: "node", usage: "github:app-connect --app-id <id> --key <path.pem>", summary: "Connect an existing GitHub App" },
91
+ { name: "github:app-sync", group: "accounts", scope: "account", usage: "github:app-sync [on|off]", summary: "Sync GitHub App keys to this account's other nodes" },
92
+
93
+ // Configuration and extensions
94
+ { name: "config", group: "config", scope: "node", json: true, usage: "config <show|get|set|unset|explain|validate|init|path>", summary: "Typed node configuration", subcommands: ["init", "validate", "show", "get", "set", "unset", "explain", "path"] },
95
+ { name: "plugin", aliases: ["plugins"], group: "config", scope: "node", json: true, usage: "plugin <init|validate|doctor|test|install|list|remove>", summary: "Build and install plugins", subcommands: ["init", "validate", "doctor", "test", "install", "list", "remove"] },
96
+ { name: "voice", aliases: ["stt"], group: "config", scope: "node", usage: "voice <provider|key|remove|status>", summary: "Configure speech-to-text", subcommands: ["provider", "key", "remove", "status"] },
97
+ { name: "completions", aliases: ["completion"], group: "config", scope: "node", usage: "completions <bash|zsh|fish>", summary: "Print a shell completion script", subcommands: ["bash", "zsh", "fish"] },
98
+
99
+ // Help and discovery
100
+ { name: "help", aliases: ["-h", "--help"], group: "help", scope: "node", json: true, usage: "help [command] [--json]", summary: "Show this help, one command's row, or every command as JSON" },
101
+ { name: "version", aliases: ["--version", "-v"], group: "help", scope: "node", usage: "version", summary: "Print the installed Bivy version" },
102
+
103
+ // Plumbing
104
+ { name: "mcp-serve", group: "help", scope: "session", hidden: true, usage: "mcp-serve", summary: "Bivy's MCP server for agents (stdio; injected into agent MCP config)" },
105
+ { name: "mcp-proxy", group: "help", scope: "session", hidden: true, usage: "mcp-proxy -- <server command>", summary: "MCP proxy in front of an agent's own MCP servers (stdio)" },
106
+ ];
107
+
108
+ const byWord = new Map();
109
+ for (const command of COMMANDS) for (const word of [command.name, ...(command.aliases ?? [])]) byWord.set(word, command);
110
+
111
+ /** The row for a typed command word (name or alias), or undefined. */
112
+ export function resolveCommand(word) {
113
+ return typeof word === "string" ? byWord.get(word) : undefined;
114
+ }
115
+
116
+ // Edit distance where swapping two neighbors counts once ("hepl" → "help").
117
+ function distance(a, b) {
118
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
119
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
120
+ for (let i = 1; i <= a.length; i++) {
121
+ for (let j = 1; j <= b.length; j++) {
122
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
123
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
124
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
125
+ }
126
+ }
127
+ return d[a.length][b.length];
128
+ }
129
+
130
+ /** Closest visible command names to a mistyped word, best first (at most 3). */
131
+ export function suggestCommands(word) {
132
+ const typed = String(word ?? "").toLowerCase();
133
+ if (!typed) return [];
134
+ const scored = [];
135
+ for (const command of COMMANDS) {
136
+ if (command.hidden) continue;
137
+ const words = [command.name, ...(command.aliases ?? [])].filter((w) => !w.startsWith("-"));
138
+ const best = Math.min(...words.map((w) => (w.startsWith(typed) || typed.startsWith(w) ? 0 : distance(typed, w))));
139
+ if (best <= Math.max(1, Math.floor(typed.length / 3))) scored.push([best, command.name]);
140
+ }
141
+ return scored.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1])).slice(0, 3).map(([, name]) => name);
142
+ }
143
+
144
+ /** The words shells should complete after `bivy`. */
145
+ export function completionWords() {
146
+ return COMMANDS.filter((command) => !command.hidden).flatMap((command) => [command.name, ...(command.aliases ?? []).filter((a) => !a.startsWith("-"))]);
147
+ }
148
+
149
+ /** Commands with first-argument verbs, for completing `bivy <cmd> <verb>`. */
150
+ export function subcommandTable() {
151
+ return COMMANDS.filter((command) => !command.hidden && command.subcommands?.length).map((command) => ({
152
+ words: [command.name, ...(command.aliases ?? [])],
153
+ subcommands: command.subcommands,
154
+ }));
155
+ }
156
+
157
+ /** One command as plain data: what `bivy help --json` and `bivy help <cmd> --json` print. */
158
+ export function describeCommand(command) {
159
+ const { name, aliases = [], group, usage, summary, scope, json = false, subcommands = [] } = command;
160
+ return { name, aliases, group, usage: `bivy ${usage}`, summary, scope, json, subcommands, help: `bivy ${name} --help` };
161
+ }
162
+
163
+ export function describeCli() {
164
+ return {
165
+ exitCodes: EXIT,
166
+ output: "Commands marked json accept --json; BIVY_OUTPUT=json turns it on for every such command.",
167
+ errors: "With --json, failures print {\"error\":{\"code\",\"message\",\"hint\"?,\"next\"?}} to stderr.",
168
+ commands: COMMANDS.filter((command) => !command.hidden).map(describeCommand),
169
+ };
170
+ }
171
+
172
+ /** `bivy help` text. `paint` colors a command synopsis (the CLI passes its cyan);
173
+ * `details` adds a line under a command, for lists only known at runtime. */
174
+ export function renderHelp({ paint = (s) => s, bold = (s) => s, details = {} } = {}) {
175
+ const visible = COMMANDS.filter((command) => !command.hidden);
176
+ const width = Math.min(44, Math.max(...visible.map((command) => command.usage.length)) + 5);
177
+ const lines = [`${bold("bivy")} — Bivy node CLI`, ""];
178
+ for (const [group, title] of GROUPS) {
179
+ const rows = visible.filter((command) => command.group === group);
180
+ if (!rows.length) continue;
181
+ lines.push(bold(title));
182
+ for (const command of rows) {
183
+ const synopsis = `bivy ${command.usage}`;
184
+ const pad = synopsis.length < width ? " ".repeat(width - synopsis.length) : "\n" + " ".repeat(width + 2);
185
+ lines.push(` ${paint(synopsis)}${pad}${command.summary}`);
186
+ if (details[command.name]) lines.push(`${" ".repeat(width + 2)}${details[command.name]}`);
187
+ }
188
+ lines.push("");
189
+ }
190
+ lines.push("Every command takes --help. 'bivy help --json' lists all commands, flags that take --json, and exit codes.");
191
+ lines.push("Agents: start with 'bivy context'.");
192
+ return lines.join("\n");
193
+ }
194
+
195
+ /** `--json` was passed, or BIVY_OUTPUT=json asks for JSON everywhere. */
196
+ export function wantsJson(args = [], env = process.env) {
197
+ return args.includes("--json") || String(env?.BIVY_OUTPUT ?? "").toLowerCase() === "json";
198
+ }
199
+
200
+ /**
201
+ * The one error shape for commands agents call: JSON on stderr under --json, a
202
+ * readable line otherwise, then exit with a code from EXIT. `next` names the
203
+ * command that gets the caller unstuck.
204
+ */
205
+ export function cliError({ code, message, hint, next, exit = EXIT.failed }, { json = false, paint = (s) => s } = {}) {
206
+ if (json) {
207
+ process.stderr.write(`${JSON.stringify({ error: { code, message, ...(hint ? { hint } : {}), ...(next ? { next } : {}) } })}\n`);
208
+ } else {
209
+ process.stderr.write(`${paint(message)}\n`);
210
+ if (hint) process.stderr.write(`${hint}\n`);
211
+ if (next) process.stderr.write(`Try: ${next}\n`);
212
+ }
213
+ process.exit(exit);
214
+ }
@@ -46,6 +46,7 @@ export const BIVY_AGENT_NOTE = [
46
46
  '`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
47
47
  "its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
48
48
  "lists machines and their agents. Use your own sub-agents for everything else.",
49
+ "- `bivy context` shows your session, workspace and published apps; `bivy help` lists every command (`--json` for all).",
49
50
  ].join("\n");
50
51
  const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
51
52
  "instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
package/dist/server.js CHANGED
@@ -126,6 +126,7 @@ import { createSessionControlCommands } from "./controllers/session-control.js";
126
126
  import { createPresenceCommands } from "./controllers/presence-commands.js";
127
127
  import { createArtifactCommands } from "./controllers/artifact-commands.js";
128
128
  import { PresenceBook, deviceFrom } from "./session/presence.js";
129
+ import { buildAgentContext } from "./session/agent-context.js";
129
130
  import { createForkCommands } from "./controllers/fork-commands.js";
130
131
  import { createGithubCommands } from "./controllers/github-commands.js";
131
132
  import { createCredentialCommands } from "./controllers/credential-commands.js";
@@ -11505,6 +11506,31 @@ app.post("/api/session/:id/attach", (req, res) => {
11505
11506
  const { hash, name, mimeType, size, kind } = result.ref;
11506
11507
  res.json({ ok: true, hash, name, mimeType, size, kind });
11507
11508
  });
11509
+ // `bivy context`: what an agent in this session needs to orient itself.
11510
+ app.get("/api/session/:id/context", (req, res) => {
11511
+ const record = openSessions.get(String(req.params.id));
11512
+ if (!record)
11513
+ return res.status(404).json({ error: "Session not found" });
11514
+ const { apps, previewAvailable } = appService.list(record.id);
11515
+ res.json(buildAgentContext({
11516
+ session: {
11517
+ id: record.id,
11518
+ name: record.session.getName() || undefined,
11519
+ agent: record.runtimeId,
11520
+ agentName: getRuntime(record.runtimeId).displayName,
11521
+ workspace: harnessDirFor(record),
11522
+ branch: record.worktree?.branch,
11523
+ prUrl: record.prUrl,
11524
+ forkedFrom: record.forkedFrom,
11525
+ delegatedFrom: record.delegatedFrom?.sessionId,
11526
+ busy: sessionBusy(record),
11527
+ },
11528
+ machine: { name: identity.name, platform: process.platform, arch: process.arch },
11529
+ presence: presenceBook.get(record.id),
11530
+ apps,
11531
+ previewAvailable,
11532
+ }));
11533
+ });
11508
11534
  // `bivy suggest "<task>"`: the agent proposes a task the user can start in one
11509
11535
  // tap, here or in a parallel session (see packages/web SuggestionCard).
11510
11536
  app.post("/api/session/:id/suggest", (req, res) => {
@@ -0,0 +1,14 @@
1
+ export function buildAgentContext(input) {
2
+ const driver = input.presence.driver;
3
+ return {
4
+ session: input.session,
5
+ machine: input.machine,
6
+ ...(driver ? { lastDriver: { label: driver.label, via: driver.via, at: driver.at } } : {}),
7
+ apps: input.apps.map((app) => ({
8
+ id: app.id,
9
+ name: app.name,
10
+ views: app.views.map((view) => ({ id: view.id, name: view.name, kind: view.kind })),
11
+ })),
12
+ previewAvailable: input.previewAvailable,
13
+ };
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.19.4-staging.2",
3
+ "version": "0.19.4-staging.3",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",