@bivy/bivy 0.10.1-staging.176 → 0.10.1-staging.178

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,7 @@ Environment variables passed to the one-line installer change what it does:
62
62
  | Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |
63
63
  | Pin an exact version | `BIVY_VERSION=0.1.0` |
64
64
  | Install without sudo, into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |
65
- | Preinstall every bundled agent runtime | `BIVY_INSTALL_ALL_AGENTS=1` |
65
+ | Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |
66
66
 
67
67
  For example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.
68
68
 
@@ -146,8 +146,8 @@ the rest are explicitly Beta in the picker and support matrix:
146
146
 
147
147
  | Agent | Command | Notes |
148
148
  |---|---|---|
149
- | Pi | `bivy run pi` | Default; bundled |
150
- | Claude Code | `bivy run claude` | Bundled SDK |
149
+ | Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |
150
+ | Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |
151
151
  | Codex | `bivy run codex` | Installs `@openai/codex` |
152
152
  | OpenCode | `bivy run opencode` | Installs `opencode-ai` |
153
153
  | Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |
@@ -169,9 +169,10 @@ the rest are explicitly Beta in the picker and support matrix:
169
169
  Any other command works via `bivy run -- ./your-agent --flags`. ACP-capable
170
170
  agents can be promoted to Bivy's governed protocol path for per-tool approvals
171
171
  and native resume. To add a reusable process or ACP agent to both the CLI and web
172
- picker without changing Bivy, scaffold and install a declarative
173
- [plugin manifest](docs/plugins.md) with `bivy plugin init`; the plugin SDK,
174
- JSON Schema, diagnostics, and ACP conformance test support external authors.
172
+ picker without changing Bivy, run `bivy agent add`, or scaffold and install a
173
+ declarative [plugin manifest](docs/plugins.md) with `bivy plugin init`. Both use
174
+ the same schema/store; the plugin SDK, diagnostics, and ACP conformance fixtures
175
+ support distributable or custom protocol bridges.
175
176
 
176
177
  [`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly
177
178
  what each agent supports — resume, model selection, approvals, sandboxing.
@@ -186,7 +187,8 @@ bivy sessions # list live and saved sessions
186
187
  bivy resume # resume the most recent session
187
188
  bivy open # open the web app (requires relay setup)
188
189
  bivy automation init # create .bivy/automations.yaml
189
- bivy plugin list # installed declarative agent plugins
190
+ bivy agent add # connect an existing ACP or process agent
191
+ bivy plugin list # installed declarative integration packages
190
192
  bivy status # config summary and node reachability
191
193
  bivy doctor # health check
192
194
  bivy logs -f # tail node logs
package/bin/bivy.mjs CHANGED
@@ -60,7 +60,7 @@ function resolveAppDir() {
60
60
  return path.join(os.homedir(), ".bivy");
61
61
  }
62
62
  const appDir = resolveAppDir();
63
- // Propagate the resolved data dir to every child process (daemon, native-pi,
63
+ // Propagate the resolved data dir to every Bivy child process (daemon, helpers,
64
64
  // exec, …) via the environment so none of them independently fall back to
65
65
  // <repoRoot>/.bivy. The daemon reads BIVY_DATA_DIR (see src/server.ts).
66
66
  process.env.BIVY_DATA_DIR = appDir;
@@ -142,12 +142,11 @@ function resolveUpdateChannel(args) {
142
142
  }
143
143
  const packaged = fs.existsSync(path.join(repoRoot, "dist", "server.js"));
144
144
  const serverEntry = path.join(repoRoot, packaged ? "dist/server.js" : "src/server.ts");
145
- const nativePiEntry = path.join(repoRoot, packaged ? "dist/native-pi.js" : "src/native-pi.ts");
146
145
  const bivyLoginEntry = path.join(repoRoot, packaged ? "dist/bivy-login.js" : "src/bivy-login.ts");
147
- const credentialIngestEntry = path.join(repoRoot, packaged ? "dist/credential-ingest-cli.js" : "src/credential-ingest-cli.ts");
148
146
  const automationEntry = path.join(repoRoot, packaged ? "dist/automation-cli.js" : "src/automation-cli.ts");
149
147
  const configEntry = path.join(repoRoot, packaged ? "dist/config-cli.js" : "src/config-cli.ts");
150
148
  const pluginEntry = path.join(repoRoot, packaged ? "dist/plugin-cli.js" : "src/plugin-cli.ts");
149
+ const agentEntry = path.join(repoRoot, packaged ? "dist/agent-cli.js" : "src/agent-cli.ts");
151
150
  const relaySetupEntry = path.join(repoRoot, packaged ? "dist/relay-setup.js" : "src/relay-setup.ts");
152
151
  // Dependency-free hosted-endpoint helper. Shipped to dist/ in the release
153
152
  // artifact (src/ is not packaged), so resolve it the same packaged-aware way as
@@ -432,7 +431,7 @@ function hasModelConfig(config) {
432
431
  }
433
432
 
434
433
  const SETUP_AGENT_CHOICES = [
435
- { key: "p", label: "Pi (default, sign in to ChatGPT/Claude/Copilot or paste a model key)", runtimeId: "pi", needsBivyModel: true },
434
+ { key: "p", label: "Pi (default; uses your existing login and configuration)", runtimeId: "pi", command: "pi", needsBivyModel: false, loginHint: "Sign in through Pi (/login)" },
436
435
  { key: "c", label: "Claude Code", runtimeId: "claude-code-sdk", command: "claude", authProbe: ["auth", "status"], needsBivyModel: false, loginHint: "Sign in through Claude Code" },
437
436
  { key: "x", label: "Codex", runtimeId: "codex", command: "codex", authProbe: ["login", "status"], needsBivyModel: false, loginHint: "Sign in through Codex" },
438
437
  { key: "o", label: "OpenCode", runtimeId: "opencode", command: "opencode", needsBivyModel: false },
@@ -463,20 +462,13 @@ function nativeAgentAuthDetected(choice) {
463
462
  // Conservative file fallbacks for older CLI versions without a status command.
464
463
  if (choice.command === "codex") return fs.existsSync(path.join(os.homedir(), ".codex", "auth.json"));
465
464
  if (choice.command === "claude") return fs.existsSync(path.join(os.homedir(), ".claude", ".credentials.json"));
465
+ if (choice.command === "pi") {
466
+ const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
467
+ return fs.existsSync(path.join(agentDir, "auth.json"));
468
+ }
466
469
  return false;
467
470
  }
468
471
 
469
- async function ingestSetupAgentLogin(choice) {
470
- if (!choice || !["claude-code-sdk", "codex"].includes(choice.runtimeId)) return false;
471
- const code = await run(nodeBin, [
472
- ...nodeScriptArgs(credentialIngestEntry),
473
- choice.runtimeId,
474
- path.join(appDir, "credentials"),
475
- path.join(appDir, "pi"),
476
- ], { cwd: repoRoot, env: process.env, stdio: "ignore" });
477
- return code === 0;
478
- }
479
-
480
472
  function url(config) {
481
473
  return `http://localhost:${config.port}`;
482
474
  }
@@ -626,7 +618,8 @@ async function ensurePythonCommand(command, packageName, label) {
626
618
 
627
619
  // Single source of truth for what `bivy agents:install` installs, so its help
628
620
  // text (see printHelp) can never drift from what it actually does (#113).
629
- const BUNDLED_AGENTS = [
621
+ const KNOWN_AGENT_INSTALLS = [
622
+ { command: "pi", npmPackage: "@earendil-works/pi-coding-agent", label: "Pi" },
630
623
  { command: "claude", npmPackage: "@anthropic-ai/claude-code", label: "Claude Code" },
631
624
  { command: "codex", npmPackage: "@openai/codex", label: "Codex" },
632
625
  { command: "opencode", npmPackage: "opencode-ai/opencode", label: "OpenCode" },
@@ -635,11 +628,11 @@ const BUNDLED_AGENTS = [
635
628
  { command: "gemini", npmPackage: "@google/gemini-cli", label: "Gemini CLI" },
636
629
  ];
637
630
 
638
- async function ensureBundledAgents() {
631
+ async function ensureKnownAgents() {
639
632
  if (process.env.BIVY_SKIP_AGENT_PREINSTALL === "1") return true;
640
- console.log(c.dim("Ensuring bundled agent runtimes are installed…"));
633
+ console.log(c.dim("Ensuring known agent integrations are installed…"));
641
634
  const results = [await ensureNodePackage("@anthropic-ai/claude-agent-sdk")];
642
- for (const agent of BUNDLED_AGENTS) {
635
+ for (const agent of KNOWN_AGENT_INSTALLS) {
643
636
  results.push(
644
637
  agent.pythonPackage
645
638
  ? await ensurePythonCommand(agent.command, agent.pythonPackage, agent.label)
@@ -652,7 +645,8 @@ async function ensureBundledAgents() {
652
645
  }
653
646
 
654
647
  async function ensureSetupAgent(choice) {
655
- if (!choice || choice.runtimeId === "pi") return true;
648
+ if (!choice) return true;
649
+ if (choice.runtimeId === "pi") return ensureNpmCommand("pi", "@earendil-works/pi-coding-agent", "Pi");
656
650
  if (choice.runtimeId === "claude-code-sdk") {
657
651
  const sdk = await ensureNodePackage("@anthropic-ai/claude-agent-sdk");
658
652
  const cli = await ensureNpmCommand("claude", "@anthropic-ai/claude-code", "Claude Code");
@@ -669,9 +663,9 @@ async function ensureSetupAgent(choice) {
669
663
  }
670
664
 
671
665
  // The CLI-agent rows come from bin/agent-manifest.json — generated from
672
- // CLI_AGENT_SPECS (`npm run gen:agent-manifest`), so the terminal `bivy run`
673
- // agents never drift from the web picker's. A sync test guards the JSON. The two
674
- // native rows (Pi, Claude Code) aren't CLI specs and stay defined here.
666
+ // AGENT_PROFILES (`npm run gen:agent-manifest`), so the terminal `bivy run`
667
+ // agents never drift from the web picker's. A sync test guards the JSON. Agent
668
+ // integrations that need a bridge still point at the operator's installed CLI.
675
669
  function loadAgentManifest() {
676
670
  try {
677
671
  const raw = fs.readFileSync(path.join(__dirname, "agent-manifest.json"), "utf8");
@@ -758,8 +752,8 @@ function manifestHeadlessFlags(id) {
758
752
  return plugin?.headlessFlags?.length ? plugin.headlessFlags : undefined;
759
753
  }
760
754
 
761
- const BUILTIN_TERMINAL_AGENTS = new Map([
762
- ["pi", { label: "Pi", type: "native-pi" }],
755
+ const AGENT_INTEGRATIONS = new Map([
756
+ ["pi", { label: "Pi", type: "command", command: "pi", npmPackage: "@earendil-works/pi-coding-agent" }],
763
757
  ["claude", { label: "Claude Code", type: "command", command: "claude", npmPackage: "@anthropic-ai/claude-code" }],
764
758
  ["openclaw", { label: "OpenClaw", type: "command", command: process.env.BIVY_OPENCLAW_COMMAND || "openclaw" }],
765
759
  ...loadAgentManifest().map((a) => [
@@ -817,7 +811,7 @@ function customTerminalAgent(agentId) {
817
811
 
818
812
  function terminalAgent(agentId) {
819
813
  const id = (agentId || resolveDefaultAgent()).toLowerCase();
820
- return { id, agent: BUILTIN_TERMINAL_AGENTS.get(id) ?? customTerminalAgent(id) };
814
+ return { id, agent: AGENT_INTEGRATIONS.get(id) ?? customTerminalAgent(id) };
821
815
  }
822
816
 
823
817
  async function waitForNode(config, timeoutMs = 8000) {
@@ -880,10 +874,7 @@ async function resolveRunSpec(agentId, extraArgs) {
880
874
  }
881
875
  const { id, agent } = terminalAgent(agentId);
882
876
  if (!agent) {
883
- return { error: `Unknown agent: ${agentId}. Built-ins: ${[...BUILTIN_TERMINAL_AGENTS.keys()].join(", ")}. Or: bivy run -- <command>.` };
884
- }
885
- if (agent.type === "native-pi") {
886
- return { spec: { agent: id, label: agent.label, command: nodeBin, args: [...nodeScriptArgs(nativePiEntry), ...extraArgs] } };
877
+ return { error: `Unknown agent: ${agentId}. Known integrations: ${[...AGENT_INTEGRATIONS.keys()].join(", ")}. Or: bivy run -- <command>.` };
887
878
  }
888
879
  const command = await ensureTerminalCommand(agent);
889
880
  if (!command) {
@@ -1420,19 +1411,15 @@ async function cmdNodes(args = []) {
1420
1411
  console.log("");
1421
1412
  }
1422
1413
 
1423
- // `bivy agents` — list the agents Bivy can launch (its built-in terminal agents),
1424
- // showing which are installed on PATH. `bivy run <agent>` starts one; the bundled
1425
- // ones are installed with `bivy agents:install`. `--json` for machine-readable output.
1414
+ // `bivy agents` — list integrations and the upstream agents found on PATH.
1415
+ // `bivy run <agent>` starts the user's native agent; `--json` is machine-readable.
1426
1416
  function cmdAgents(args = []) {
1427
1417
  if (args.includes("-h") || args.includes("--help")) {
1428
- console.log("Usage: bivy agents [--json]\n\nList the agents Bivy can launch ('bivy run <agent>') and which are installed on PATH. 'bivy agents:install' installs the bundled ones.");
1418
+ console.log("Usage: bivy agents [--json]\n\nList the agents Bivy can launch ('bivy run <agent>') and which are installed on PATH. 'bivy agents:install' installs the known upstream agents.");
1429
1419
  return;
1430
1420
  }
1431
1421
  const asJson = args.includes("--json");
1432
- const rows = [...BUILTIN_TERMINAL_AGENTS.entries()].map(([id, meta]) => {
1433
- if (meta.type === "native-pi") {
1434
- return { id, label: meta.label, type: meta.type, command: null, installed: true, path: null };
1435
- }
1422
+ const rows = [...AGENT_INTEGRATIONS.entries()].map(([id, meta]) => {
1436
1423
  const command = meta.command || id;
1437
1424
  const resolved = whichOnPath(command);
1438
1425
  return { id, label: meta.label, type: meta.type, command, installed: Boolean(resolved), path: resolved || null, ...(meta.plugin ? { plugin: meta.plugin } : {}) };
@@ -1443,11 +1430,9 @@ function cmdAgents(args = []) {
1443
1430
  return;
1444
1431
  }
1445
1432
 
1446
- console.log(c.bold("\n Agents") + c.dim(" (bivy run <agent> — 'bivy agents:install' adds the bundled ones)\n"));
1433
+ console.log(c.bold("\n Agents") + c.dim(" (bivy run <agent> — 'bivy agents:install' adds the known upstream agents)\n"));
1447
1434
  for (const row of rows) {
1448
- const status = row.type === "native-pi"
1449
- ? c.green("● built-in")
1450
- : row.installed ? c.green("● installed") : c.dim("○ not installed");
1435
+ const status = row.installed ? c.green("● installed") : c.dim("○ not installed");
1451
1436
  const where = row.path ? c.dim(` ${row.path}`) : "";
1452
1437
  const source = row.plugin ? c.dim(` plugin:${row.plugin}`) : "";
1453
1438
  console.log(` ${c.cyan(row.id.padEnd(12))} ${String(row.label).padEnd(16)} ${status}${source}${where}`);
@@ -1502,13 +1487,8 @@ async function cmdShim(args = []) {
1502
1487
  process.exit(1);
1503
1488
  return;
1504
1489
  }
1505
- const builtin = BUILTIN_TERMINAL_AGENTS.get(agent);
1506
- if (builtin && builtin.type === "native-pi") {
1507
- console.error(c.red(`"${agent}" is Bivy's own native runtime, not a standalone binary — nothing to shim. Just run 'bivy -a ${agent}'.`));
1508
- process.exit(1);
1509
- return;
1510
- }
1511
- const agentCmd = builtin?.command || agent;
1490
+ const integration = AGENT_INTEGRATIONS.get(agent);
1491
+ const agentCmd = integration?.command || agent;
1512
1492
  const shimDir = path.resolve(argValue(rest, "dir") || defaultShimDir());
1513
1493
  const force = rest.includes("--force");
1514
1494
  const headlessOverride = argValue(rest, "headless");
@@ -1694,16 +1674,16 @@ async function cmdExec(args = []) {
1694
1674
  }
1695
1675
 
1696
1676
  // `bivy completions <bash|zsh|fish>` — print a shell completion script to eval or
1697
- // install. Covers the top-level commands and the built-in agent ids.
1677
+ // install. Covers the top-level commands and known integration ids.
1698
1678
  function cmdCompletions(args = []) {
1699
1679
  const shell = (args[0] || "").toLowerCase();
1700
1680
  const commands = [
1701
- "run", "sessions", "ls", "resume", "promote", "rename", "nodes", "agents", "agents:install", "shim", "takeover", "token", "exec",
1681
+ "run", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
1702
1682
  "send", "attach", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "logs", "login",
1703
1683
  "update", "update:log", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
1704
1684
  "github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
1705
1685
  ];
1706
- const agents = [...BUILTIN_TERMINAL_AGENTS.keys()];
1686
+ const agents = [...AGENT_INTEGRATIONS.keys()];
1707
1687
 
1708
1688
  if (shell === "bash") {
1709
1689
  console.log(`# bivy bash completion — add to ~/.bashrc: eval "$(bivy completions bash)"
@@ -1806,11 +1786,6 @@ async function cmdRun(args = []) {
1806
1786
  // path a phone uses). The command must resolve on the REMOTE node's PATH, so
1807
1787
  // send the agent's bare command rather than this machine's absolute path.
1808
1788
  if (target.source === "relay") {
1809
- if (agentId !== "--" && terminalAgent(agentId).agent?.type === "native-pi") {
1810
- console.error(c.red("Pi runs only on the local node. For --node, pick an installed agent (e.g. claude, codex)."));
1811
- process.exit(1);
1812
- return;
1813
- }
1814
1789
  const remoteCommand = agentId === "--" ? resolved.spec.command : (terminalAgent(agentId).agent?.command || resolved.spec.command);
1815
1790
  const spec = { ...resolved.spec, command: remoteCommand, name, model, workspace: undefined };
1816
1791
  console.log(c.dim(`Starting on ${c.cyan(target.name)} over the relay…`));
@@ -3473,8 +3448,8 @@ async function cmdSetup(args = []) {
3473
3448
  // `bivy github:app-create` / `github:app-connect`. One app covers every repo,
3474
3449
  // and the node mints its own tokens, so there's no per-repo token to set up here.
3475
3450
 
3476
- // Model access is part of activation, not a post-success footnote. Pi/Aider use
3477
- // Bivy's provider login; offer it inline so setup cannot imply the first task
3451
+ // Model access is part of activation, not a post-success footnote. Integrations
3452
+ // marked needsBivyModel use Bivy's provider login; offer it inline so setup cannot imply the first task
3478
3453
  // is ready while the required credential is still absent. Agent-native auth is
3479
3454
  // explained in the readiness checklist below because those CLIs own the flow.
3480
3455
  let agentAuthReady = setupAgent?.needsBivyModel ? hasModelConfig(config) : nativeAgentAuthDetected(setupAgent);
@@ -3492,17 +3467,15 @@ async function cmdSetup(args = []) {
3492
3467
  }
3493
3468
  } else if (setupAgent && !setupAgent.needsBivyModel) {
3494
3469
  if (agentAuthReady) {
3495
- const imported = await ingestSetupAgentLogin(setupAgent);
3496
- console.log(c.green(`\n ✓ Existing ${setupAgent.label} login detected — Bivy will reuse it in the terminal and PWA${imported ? " and stored it in the encrypted vault" : ""}.`));
3470
+ console.log(c.green(`\n ✓ Existing ${setupAgent.label} login detected — Bivy will connect without replacing the agent's credential store.`));
3497
3471
  } else if (setupAgent.command) {
3498
- console.log(`\n${setupAgent.label} owns its login. After sign-in, Bivy stores compatible credential fields in its encrypted vault so the terminal, PWA, and your other Bivy nodes can reuse them.`);
3472
+ console.log(`\n${setupAgent.label} owns its login and configuration. Bivy connects to that existing agent state rather than copying it into a separate default credential store.`);
3499
3473
  const signInNow = await askYesNo(`Open ${setupAgent.label} now to sign in? (Exit it when sign-in is complete.)`, true);
3500
3474
  if (signInNow) {
3501
3475
  rl.pause();
3502
3476
  const loginCode = await run(setupAgent.command, [], { cwd: config.workspace, env: startEnv(config) });
3503
3477
  rl.resume();
3504
3478
  agentAuthReady = loginCode === 0 || nativeAgentAuthDetected(setupAgent);
3505
- if (agentAuthReady) await ingestSetupAgentLogin(setupAgent);
3506
3479
  }
3507
3480
  }
3508
3481
  }
@@ -3857,11 +3830,11 @@ async function cmdDoctor(args = []) {
3857
3830
  ? c.green("relay connected") + (relayApp ? c.dim(` ${relayApp}`) : "")
3858
3831
  : c.yellow("configured, not connected") + (relayErr ? c.dim(` (${relayErr})`) : "");
3859
3832
  console.log(` ${relayConfigured ? (relayConnected ? ok : warn) : c.dim("○")} remote ${relayLine}`);
3860
- // Derived from BUILTIN_TERMINAL_AGENTS (the same list 'bivy agents'/'bivy run'
3833
+ // Derived from AGENT_INTEGRATIONS (the same list 'bivy agents'/'bivy run'
3861
3834
  // use) rather than a hand-maintained list, so it can't drift out of sync (#113).
3862
- const agentCommands = [...BUILTIN_TERMINAL_AGENTS.values()].filter((a) => a.type === "command").map((a) => a.command);
3835
+ const agentCommands = [...AGENT_INTEGRATIONS.values()].filter((a) => a.type === "command").map((a) => a.command);
3863
3836
  const agents = agentCommands.filter((a) => commandExists(a));
3864
- console.log(` ${mark(agents.length > 0, true)} agents on PATH: ${agents.length ? c.cyan(agents.join(", ")) : c.dim("none (built-in Pi still works; 'bivy agents:install')")}`);
3837
+ console.log(` ${mark(agents.length > 0, true)} agents on PATH: ${agents.length ? c.cyan(agents.join(", ")) : c.dim("none ('bivy agents:install' can install known upstream agents)")}`);
3865
3838
  if (status?.eventLog) {
3866
3839
  const healthy = status.eventLog.ok !== false;
3867
3840
  const mib = Number(status.eventLog.bytes || 0) / (1024 * 1024);
@@ -4024,7 +3997,7 @@ async function runUpdate(args = []) {
4024
3997
  console.log(c.yellow(`npm reported an issue (exit ${code}). Try: sudo npm i -g @bivy/bivy@${channel}`));
4025
3998
  process.exit(code);
4026
3999
  }
4027
- await ensureBundledAgents();
4000
+ await ensureKnownAgents();
4028
4001
  const config = loadConfig();
4029
4002
  await waitForIdleSessions(config, { skip: skipWait });
4030
4003
  if (config.service && (await restartServiceReconciled(config))) {
@@ -4060,7 +4033,7 @@ async function runUpdate(args = []) {
4060
4033
  const pull = await run("git", ["pull", "--ff-only", "origin", branch || "main"], { cwd: repoRoot });
4061
4034
  if (pull !== 0) console.log(c.yellow("git pull reported an issue; continuing."));
4062
4035
  await run("npm", [fs.existsSync(path.join(repoRoot, "package-lock.json")) ? "ci" : "install", "--no-audit", "--no-fund"], { cwd: repoRoot });
4063
- await ensureBundledAgents();
4036
+ await ensureKnownAgents();
4064
4037
  const config = loadConfig();
4065
4038
  await waitForIdleSessions(config, { skip: skipWait });
4066
4039
  if (config.service && (await restartServiceReconciled(config))) {
@@ -4377,7 +4350,7 @@ function printHelp() {
4377
4350
  ${c.bold("bivy")} — Bivy node CLI
4378
4351
 
4379
4352
  ${c.cyan("bivy run claude")} Run a native agent (real CLI/TUI) as a relay-visible session
4380
- ${c.cyan("bivy run <agent>")} ${[...BUILTIN_TERMINAL_AGENTS.keys()].join(" | ")} | -- <command>
4353
+ ${c.cyan("bivy run <agent>")} ${[...AGENT_INTEGRATIONS.keys()].join(" | ")} | -- <command>
4381
4354
  ${c.cyan("bivy run <agent> --name <label>")} Name the session (shown in 'bivy sessions' and the app)
4382
4355
  ${c.cyan("bivy run <agent> --model <model>")} Run with a specific model (passed to the agent, shown in the cockpit)
4383
4356
  ${c.cyan("bivy run <agent> --node <name>")} Start the session on another registered node
@@ -4410,7 +4383,8 @@ ${c.bold("bivy")} — Bivy node CLI
4410
4383
  ${c.cyan("bivy login")} Sign into a model provider (Pi /login)
4411
4384
  ${c.cyan("bivy update")} Update Bivy + install deps + restart service (waits for active sessions to finish a turn; --force to skip)
4412
4385
  ${c.cyan("bivy update:log")} Show output of the last (or in-progress) update
4413
- ${c.cyan("bivy agents:install")} Install bundled agents (${BUNDLED_AGENTS.map((a) => a.label).join(", ")})
4386
+ ${c.cyan("bivy agent add")} Connect an existing user-owned agent
4387
+ ${c.cyan("bivy agents:install")} Install known upstream agents (${KNOWN_AGENT_INSTALLS.map((a) => a.label).join(", ")})
4414
4388
  ${c.cyan("bivy open")} Open the remote web/PWA app
4415
4389
  ${c.cyan("bivy service")} install | uninstall | status
4416
4390
  ${c.cyan("bivy uninstall")} Remove Bivy and all its data (--keep-sessions, --keep-worktrees, --dry-run)
@@ -4452,7 +4426,7 @@ async function main() {
4452
4426
  console.log(`Usage: bivy run <agent> [--name <label>] [--model <model>] [--node <name>] [--clone [remote]] [--workspace <dir>] | -- <command>
4453
4427
 
4454
4428
  Run a native agent (real CLI/TUI) as a relay-visible session.
4455
- Agents: ${[...BUILTIN_TERMINAL_AGENTS.keys()].join(", ")}, or -- <command> for anything else.
4429
+ Agents: ${[...AGENT_INTEGRATIONS.keys()].join(", ")}, or -- <command> for anything else.
4456
4430
  An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4457
4431
  break;
4458
4432
  }
@@ -4475,6 +4449,11 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4475
4449
  case "nodes":
4476
4450
  await cmdNodes(args);
4477
4451
  break;
4452
+ case "agent": {
4453
+ if (!(await ensureDeps())) process.exit(1);
4454
+ process.exit(await run(nodeBin, [...nodeScriptArgs(agentEntry), ...args], { cwd: process.cwd(), env: process.env }));
4455
+ break;
4456
+ }
4478
4457
  case "agents":
4479
4458
  cmdAgents(args);
4480
4459
  break;
@@ -4579,11 +4558,11 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
4579
4558
  case "agents:install":
4580
4559
  case "runtimes:install":
4581
4560
  if (args.includes("-h") || args.includes("--help")) {
4582
- console.log(`Usage: bivy agents:install\n\nInstall bundled agent runtimes (${BUNDLED_AGENTS.map((a) => a.label).join(", ")}).`);
4561
+ console.log(`Usage: bivy agents:install\n\nInstall known agent integrations (${KNOWN_AGENT_INSTALLS.map((a) => a.label).join(", ")}).`);
4583
4562
  break;
4584
4563
  }
4585
4564
  if (!(await ensureDeps())) process.exit(1);
4586
- await ensureBundledAgents();
4565
+ await ensureKnownAgents();
4587
4566
  break;
4588
4567
  case "open": {
4589
4568
  if (args.includes("-h") || args.includes("--help")) {
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // Copyright (c) 2026 Petter André Sjulstad
4
+ /** First-class BYO-agent convenience CLI backed by the ordinary plugin manifest. */
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { stringify as stringifyYaml } from "yaml";
9
+ import { currentBivyVersion } from "./app-version.js";
10
+ import { defaultDataDir } from "./data-dir.js";
11
+ import { recommendedBivyRange } from "./plugin-sdk/index.js";
12
+ import { installPlugin, listInstalledPlugins, removePlugin } from "./plugins/store.js";
13
+ function option(args, name) {
14
+ const index = args.indexOf(name);
15
+ return index >= 0 ? args[index + 1] : undefined;
16
+ }
17
+ function positional(args) {
18
+ const valued = new Set(["--command", "--transport", "--adapter", "--args", "--name", "--prompt-mode", "--data-dir"]);
19
+ const out = [];
20
+ for (let index = 0; index < args.length; index += 1) {
21
+ if (valued.has(args[index])) {
22
+ index += 1;
23
+ continue;
24
+ }
25
+ if (!args[index].startsWith("-"))
26
+ out.push(args[index]);
27
+ }
28
+ return out;
29
+ }
30
+ function fail(message, json) {
31
+ if (json)
32
+ console.log(JSON.stringify({ ok: false, error: message }));
33
+ else
34
+ console.error(`error: ${message}`);
35
+ process.exit(1);
36
+ }
37
+ function parseArgs(value) {
38
+ if (!value)
39
+ return [];
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(value);
43
+ }
44
+ catch {
45
+ throw new Error("--args must be a JSON string array");
46
+ }
47
+ if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
48
+ throw new Error("--args must be a JSON string array");
49
+ }
50
+ return parsed;
51
+ }
52
+ function localManifest(args) {
53
+ const id = positional(args)[1]?.trim().toLowerCase() ?? "";
54
+ if (!/^[a-z][a-z0-9-]{1,47}$/.test(id))
55
+ throw new Error("Agent id must be a lowercase slug of 2-48 characters");
56
+ const command = option(args, "--command")?.trim();
57
+ if (!command)
58
+ throw new Error("--command is required");
59
+ const transport = (option(args, "--transport") ?? option(args, "--adapter") ?? "acp").toLowerCase();
60
+ if (transport !== "acp" && transport !== "process")
61
+ throw new Error("--transport must be acp or process");
62
+ const adapterArgs = parseArgs(option(args, "--args"));
63
+ const name = option(args, "--name")?.trim() || id.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
64
+ const promptModeValue = option(args, "--prompt-mode") ?? "stdin";
65
+ if (promptModeValue !== "stdin" && promptModeValue !== "argv")
66
+ throw new Error("--prompt-mode must be stdin or argv");
67
+ const promptMode = promptModeValue;
68
+ return {
69
+ apiVersion: "bivy.sh/v1alpha1",
70
+ kind: "Plugin",
71
+ metadata: {
72
+ id,
73
+ name,
74
+ version: "0.1.0",
75
+ description: `Local integration for the ${name} agent.`,
76
+ },
77
+ requires: { bivy: recommendedBivyRange(currentBivyVersion()) },
78
+ contributes: {
79
+ agents: [{
80
+ id,
81
+ name,
82
+ authOwner: "agent",
83
+ adapter: transport === "acp"
84
+ ? { kind: "acp", command, ...(adapterArgs.length ? { args: adapterArgs } : {}) }
85
+ : { kind: "process", command, ...(adapterArgs.length ? { args: adapterArgs } : {}), promptMode },
86
+ }],
87
+ },
88
+ };
89
+ }
90
+ async function main() {
91
+ const args = process.argv.slice(2);
92
+ const command = args[0];
93
+ const json = args.includes("--json");
94
+ const dataDir = path.resolve(option(args, "--data-dir") ?? defaultDataDir());
95
+ if (!command || command === "help" || args.includes("--help") || args.includes("-h")) {
96
+ console.log(`Usage:
97
+ bivy agent add <id> --command <command> [--transport acp|process] [--args '["..."]']
98
+ bivy agent list [--json]
99
+ bivy agent remove <integration-id>
100
+
101
+ Adds an existing user-owned agent through the same declarative package contract used by every external integration.`);
102
+ return;
103
+ }
104
+ try {
105
+ if (command === "add") {
106
+ const manifest = localManifest(args);
107
+ const existingPackage = listInstalledPlugins(dataDir).some((plugin) => plugin.id === manifest.metadata.id);
108
+ if (!existingPackage) {
109
+ const runtimeModule = await import("./runtime/index.js");
110
+ const retained = runtimeModule.canonicalAgentId(manifest.contributes.agents[0].id);
111
+ if (retained)
112
+ throw new Error(`Agent id ${manifest.contributes.agents[0].id} conflicts with retained integration ${retained}; choose a unique id`);
113
+ }
114
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "bivy-agent-add-"));
115
+ const file = path.join(temp, "bivy.plugin.yaml");
116
+ try {
117
+ fs.writeFileSync(file, stringifyYaml(manifest), { mode: 0o600 });
118
+ const result = installPlugin(file, { dataDir, force: args.includes("--force"), bivyVersion: currentBivyVersion() });
119
+ const body = { ok: true, id: manifest.metadata.id, path: result.path, replaced: result.replaced, restartRequired: true };
120
+ if (json)
121
+ console.log(JSON.stringify(body, null, 2));
122
+ else {
123
+ console.log(`${result.replaced ? "Updated" : "Added"} agent integration ${manifest.metadata.name} (${manifest.metadata.id}).`);
124
+ console.log("Restart Bivy to activate it: bivy restart");
125
+ }
126
+ }
127
+ finally {
128
+ fs.rmSync(temp, { recursive: true, force: true });
129
+ }
130
+ return;
131
+ }
132
+ if (command === "list" || command === "ls") {
133
+ const rows = listInstalledPlugins(dataDir).flatMap((plugin) => plugin.manifest?.contributes.agents.map((agent) => ({
134
+ id: agent.id,
135
+ name: agent.name,
136
+ package: plugin.id,
137
+ version: plugin.manifest.metadata.version,
138
+ transport: agent.adapter.kind,
139
+ command: agent.adapter.command,
140
+ status: plugin.errors.length ? "invalid" : "installed",
141
+ })) ?? []);
142
+ if (json)
143
+ console.log(JSON.stringify({ ok: true, agents: rows }, null, 2));
144
+ else if (!rows.length)
145
+ console.log("No installed agent integration packages.");
146
+ else
147
+ for (const row of rows)
148
+ console.log(`${row.id}\t${row.transport}\t${row.command}\t${row.package}@${row.version}`);
149
+ return;
150
+ }
151
+ if (command === "remove" || command === "rm") {
152
+ const id = positional(args)[1];
153
+ if (!id)
154
+ fail("Usage: bivy agent remove <integration-id>", json);
155
+ const installed = listInstalledPlugins(dataDir);
156
+ const direct = installed.find((plugin) => plugin.id === id);
157
+ const matches = direct ? [direct] : installed.filter((plugin) => plugin.manifest?.contributes.agents.some((agent) => agent.id === id));
158
+ if (matches.length > 1)
159
+ throw new Error(`Agent id ${id} is contributed by multiple invalid packages; remove one with bivy plugin remove <package-id>`);
160
+ const packageId = matches[0]?.id ?? id;
161
+ const removed = removePlugin(packageId, dataDir);
162
+ if (json)
163
+ console.log(JSON.stringify({ ok: true, id, packageId, removed }));
164
+ else
165
+ console.log(removed ? `Removed ${id} (${packageId}). Restart Bivy to deactivate it.` : `Agent integration ${id} is not installed.`);
166
+ return;
167
+ }
168
+ fail(`Unknown agent command: ${command}`, json);
169
+ }
170
+ catch (error) {
171
+ fail(error instanceof Error ? error.message : String(error), json);
172
+ }
173
+ }
174
+ await main();
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { defineAgentIntegration } from "../definition.js";
4
+ import { withExactCapabilitySurface } from "../../runtime/types.js";
5
+ import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled, } from "./runtime.js";
6
+ export const CLAUDE_TESTED_VERSION = "0.3.220";
7
+ const CLAUDE_CAPABILITIES = withExactCapabilitySurface({
8
+ toolInterception: true,
9
+ modelSelection: true,
10
+ packages: false,
11
+ resume: true,
12
+ fork: true,
13
+ usageReporting: true,
14
+ nativeSessionDiscovery: true,
15
+ nativeSessionAdoption: true,
16
+ streamingBehaviors: ["steer"],
17
+ });
18
+ export function claudeCodeIntegration(origin) {
19
+ return defineAgentIntegration({
20
+ // Compatibility id retained for existing sessions; aliases expose the agent's
21
+ // ordinary product/command names without making SDK transport part of the UX.
22
+ id: "claude-code-sdk",
23
+ aliases: ["claude", "claude-code"],
24
+ visible: true,
25
+ origin,
26
+ describe: () => {
27
+ const bridgeInstalled = claudeSdkInstalled();
28
+ const options = claudeRuntimeFromEnv();
29
+ const agentInstalled = Boolean(options.executablePath);
30
+ const installed = bridgeInstalled && agentInstalled;
31
+ return {
32
+ id: "claude-code-sdk",
33
+ executionMode: "protocol",
34
+ displayName: "Claude Code",
35
+ description: "The operator-installed Claude Code agent, connected through Anthropic's SDK bridge for streaming, model selection, and governed tool calls.",
36
+ status: installed ? "available" : "external",
37
+ packageName: "@anthropic-ai/claude-code",
38
+ language: "TypeScript",
39
+ capabilities: { ...CLAUDE_CAPABILITIES, interactiveTui: agentInstalled },
40
+ nativeSandbox: true,
41
+ supportTier: "supported",
42
+ testedVersion: CLAUDE_TESTED_VERSION,
43
+ source: origin,
44
+ authOwner: "agent",
45
+ notes: installed
46
+ ? "Uses the Claude Code executable already on this node, including its native auth, configuration, and sessions. Set BIVY_CLAUDE_MODEL to pick a default model."
47
+ : agentInstalled
48
+ ? "The Claude Code agent is installed, but this Bivy distribution is missing its SDK bridge dependency."
49
+ : "Install and sign in to Claude Code on this node; Bivy will connect to that existing agent.",
50
+ install: installed || agentInstalled ? undefined : {
51
+ label: "Install Claude Code",
52
+ description: "Installs Anthropic's Claude Code agent on this node.",
53
+ command: "npm install --global @anthropic-ai/claude-code",
54
+ },
55
+ };
56
+ },
57
+ create: (sessionOptions) => {
58
+ const options = claudeRuntimeFromEnv();
59
+ if (!options.executablePath)
60
+ throw new Error("Claude Code command not found on PATH: claude");
61
+ return new ClaudeCodeRuntime({
62
+ ...options,
63
+ sandbox: sessionOptions.sandbox,
64
+ attachToChat: sessionOptions.attachToChat,
65
+ });
66
+ },
67
+ install: (prefix) => ({
68
+ command: "npm",
69
+ args: ["install", "--global", "--prefix", prefix, "@anthropic-ai/claude-code"],
70
+ display: `npm install --global --prefix ${prefix} @anthropic-ai/claude-code`,
71
+ }),
72
+ });
73
+ }