@bivy/bivy 0.0.0 → 0.1.0-staging.2
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/LICENSE +105 -0
- package/README.md +265 -5
- package/bin/acp-shim.mjs +298 -0
- package/bin/agent-manifest.json +277 -0
- package/bin/bivy.mjs +4100 -0
- package/bin/codex-app-server-shim.mjs +447 -0
- package/bin/patch-pi-dependencies.mjs +44 -0
- package/bin/prune-sessions.mjs +52 -0
- package/bin/sessions-list.mjs +27 -0
- package/bin/shim-path.mjs +126 -0
- package/bin/uninstall-paths.mjs +48 -0
- package/dist/approval.js +87 -0
- package/dist/attach.js +248 -0
- package/dist/auth.js +258 -0
- package/dist/bivy-login.js +180 -0
- package/dist/browser-open.js +50 -0
- package/dist/control-plane-tasks.js +236 -0
- package/dist/data-dir.js +25 -0
- package/dist/device-registry.js +201 -0
- package/dist/e2e.js +70 -0
- package/dist/ephemeral-exec.js +109 -0
- package/dist/exec.js +209 -0
- package/dist/git-auth.js +155 -0
- package/dist/github-app-auth.js +107 -0
- package/dist/github-app-connect.js +235 -0
- package/dist/github-app-manifest.js +82 -0
- package/dist/github-app-sync-cli.js +93 -0
- package/dist/github-app-vault.js +106 -0
- package/dist/github-apps.js +121 -0
- package/dist/github-connect-repo.js +74 -0
- package/dist/github-device-auth.js +109 -0
- package/dist/github-tasks.js +650 -0
- package/dist/guard.js +109 -0
- package/dist/harness/cache-evict.js +88 -0
- package/dist/harness/checkpoint.js +0 -0
- package/dist/harness/cow-clone.js +84 -0
- package/dist/harness/dep-cache.js +78 -0
- package/dist/harness/disk-admission.js +46 -0
- package/dist/harness/egress.js +30 -0
- package/dist/harness/manager.js +97 -0
- package/dist/harness/mcp-config-formats.js +164 -0
- package/dist/harness/mcp-config.js +111 -0
- package/dist/harness/mcp-inject.js +134 -0
- package/dist/harness/mcp-proxy-cli.js +88 -0
- package/dist/harness/mcp-proxy.js +150 -0
- package/dist/harness/net-proxy.js +120 -0
- package/dist/harness/sandbox.js +96 -0
- package/dist/history-sync.js +26 -0
- package/dist/hosted-endpoints.d.mts +14 -0
- package/dist/hosted-endpoints.mjs +35 -0
- package/dist/identity.js +153 -0
- package/dist/integrations/index.js +4 -0
- package/dist/integrations/manager.js +279 -0
- package/dist/integrations/oauth.js +78 -0
- package/dist/integrations/registry.js +239 -0
- package/dist/integrations/store.js +54 -0
- package/dist/integrations/types.js +1 -0
- package/dist/linear-tasks.js +49 -0
- package/dist/metadata.js +226 -0
- package/dist/multiplexer.js +79 -0
- package/dist/native-pi.js +38 -0
- package/dist/node-stats.js +237 -0
- package/dist/pairing-crypto.js +105 -0
- package/dist/policy/conditions.js +103 -0
- package/dist/policy/policy-engine.js +20 -0
- package/dist/policy/risk.js +18 -0
- package/dist/policy/ruleset.js +113 -0
- package/dist/policy/run-policy.js +108 -0
- package/dist/policy/session-reroute.js +96 -0
- package/dist/pty-runner.py +95 -0
- package/dist/question.js +146 -0
- package/dist/redact.js +97 -0
- package/dist/relay-attach.js +345 -0
- package/dist/relay-chunk.js +73 -0
- package/dist/relay-cli-crypto.js +70 -0
- package/dist/relay-client.js +344 -0
- package/dist/relay-setup.js +262 -0
- package/dist/repo-workspace.js +208 -0
- package/dist/runtime/adoption.js +45 -0
- package/dist/runtime/agent-service-bin.js +149 -0
- package/dist/runtime/agent-service.js +439 -0
- package/dist/runtime/ansi.js +27 -0
- package/dist/runtime/anthropic-preflight.js +80 -0
- package/dist/runtime/claude-code.js +1364 -0
- package/dist/runtime/cli-parsers.js +647 -0
- package/dist/runtime/codex-auth.js +168 -0
- package/dist/runtime/codex-preflight.js +60 -0
- package/dist/runtime/codex-sessions.js +229 -0
- package/dist/runtime/control-plane-location.js +74 -0
- package/dist/runtime/credential-ingest.js +122 -0
- package/dist/runtime/credential-provisioning.js +79 -0
- package/dist/runtime/credential-store.js +435 -0
- package/dist/runtime/credentials.js +153 -0
- package/dist/runtime/host.js +153 -0
- package/dist/runtime/index.js +1548 -0
- package/dist/runtime/local-model-store.js +194 -0
- package/dist/runtime/location-registry.js +28 -0
- package/dist/runtime/model-catalog.js +97 -0
- package/dist/runtime/model-namer.js +85 -0
- package/dist/runtime/native-process-scan.js +102 -0
- package/dist/runtime/native-session-discovery.js +103 -0
- package/dist/runtime/normalize.js +75 -0
- package/dist/runtime/oauth/model-oauth-providers.js +75 -0
- package/dist/runtime/oauth/model-oauth.js +324 -0
- package/dist/runtime/opencode-preflight.js +55 -0
- package/dist/runtime/pi-auth.js +82 -0
- package/dist/runtime/pi-oauth.js +52 -0
- package/dist/runtime/pi-session-discovery.js +42 -0
- package/dist/runtime/pi.js +518 -0
- package/dist/runtime/process.js +499 -0
- package/dist/runtime/protocol.js +630 -0
- package/dist/runtime/remote.js +541 -0
- package/dist/runtime/rpc-protocol.js +56 -0
- package/dist/runtime/ruleset-store.js +117 -0
- package/dist/runtime/session-location.js +50 -0
- package/dist/runtime/types.js +17 -0
- package/dist/secrets-cli.js +134 -0
- package/dist/secrets.js +264 -0
- package/dist/server.js +9411 -0
- package/dist/session/bivy-session.js +1 -0
- package/dist/session/checkpoint-pack.js +133 -0
- package/dist/session/event-log.js +340 -0
- package/dist/session/fork-dirty.js +73 -0
- package/dist/session/fork-prereqs.js +61 -0
- package/dist/session/fork.js +57 -0
- package/dist/session/native-import.js +56 -0
- package/dist/session/reconnect.js +168 -0
- package/dist/session/replication-service.js +236 -0
- package/dist/session/replication.js +106 -0
- package/dist/session/replicator.js +140 -0
- package/dist/session/session-new-dedupe.js +42 -0
- package/dist/session/sibling-client.js +201 -0
- package/dist/session/transcript-merge.js +131 -0
- package/dist/session/transcript-normal.js +130 -0
- package/dist/session/workspace-context.js +1 -0
- package/dist/session-event-coalescer.js +50 -0
- package/dist/session-identity.js +34 -0
- package/dist/session-ref.js +65 -0
- package/dist/stt-cli.js +131 -0
- package/dist/stt.js +168 -0
- package/dist/terminal.js +409 -0
- package/dist/wire-format.js +67 -0
- package/dist/worktree-provision.js +118 -0
- package/dist/worktree.js +117 -0
- package/package.json +40 -6
- package/public/qr.js +464 -0
|
@@ -0,0 +1,1548 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Runtime registry. Selects the agent runtime by BIVY_RUNTIME (default "pi").
|
|
4
|
+
// This is the seam where additional runtimes (Claude Agent SDK, generic RPC,
|
|
5
|
+
// …) are registered without touching the daemon.
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled } from "./claude-code.js";
|
|
10
|
+
import { deleteCodexSession, discoverNativeCodexSessions, loadCodexTranscript } from "./codex-sessions.js";
|
|
11
|
+
import { createCredentialStore } from "./credentials.js";
|
|
12
|
+
// Args that continue an existing Codex session each prompt. Codex assigns its own
|
|
13
|
+
// session id (no launch-time pin), so a resumed run threads it via `exec resume`.
|
|
14
|
+
// The exact flags vary by Codex version, so `BIVY_CODEX_RESUME_TEMPLATE` (a JSON
|
|
15
|
+
// array with `{id}` / `{tier}` placeholders) overrides this without a code change.
|
|
16
|
+
function codexResumeArgs(sessionId, tier) {
|
|
17
|
+
const raw = process.env.BIVY_CODEX_RESUME_TEMPLATE?.trim();
|
|
18
|
+
if (raw) {
|
|
19
|
+
try {
|
|
20
|
+
const tpl = JSON.parse(raw);
|
|
21
|
+
if (Array.isArray(tpl))
|
|
22
|
+
return tpl.map((a) => String(a).replace(/\{id\}/g, sessionId).replace(/\{tier\}/g, tier));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// malformed override — fall through to the default
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// `--json` and `--sandbox` are options of `codex exec`, not of the `resume`
|
|
29
|
+
// subcommand, so they must precede `resume` — otherwise clap rejects them with
|
|
30
|
+
// "unexpected argument '--sandbox'". Verified against codex-cli 0.142.5 and
|
|
31
|
+
// 0.144.1. Bivy's SandboxTier values are exactly Codex's --sandbox modes
|
|
32
|
+
// (read-only | workspace-write | danger-full-access), so `tier` needs no mapping.
|
|
33
|
+
return ["exec", "--json", "--sandbox", tier, "resume", sessionId];
|
|
34
|
+
}
|
|
35
|
+
import { PiRuntime } from "./pi.js";
|
|
36
|
+
import { ProcessRuntime, processRuntimeFromEnv } from "./process.js";
|
|
37
|
+
import { codexCredentialPreflight } from "./codex-preflight.js";
|
|
38
|
+
import { opencodeCredentialPreflight } from "./opencode-preflight.js";
|
|
39
|
+
import { ensureCodexAuth } from "./codex-auth.js";
|
|
40
|
+
import { parserFactoryFor } from "./cli-parsers.js";
|
|
41
|
+
import { sandboxTier, sandboxArgsFor, codexSandboxPolicy } from "../harness/sandbox.js";
|
|
42
|
+
import { ProtocolRuntime, protocolRuntimeFromEnv, protocolCommandsFromEnv } from "./protocol.js";
|
|
43
|
+
export * from "./types.js";
|
|
44
|
+
export { NodeCredentialResolver, createCredentialStore } from "./credentials.js";
|
|
45
|
+
const PI_CAPABILITIES = {
|
|
46
|
+
toolInterception: true,
|
|
47
|
+
modelSelection: true,
|
|
48
|
+
packages: true,
|
|
49
|
+
resume: true,
|
|
50
|
+
fork: false,
|
|
51
|
+
interactiveTui: true,
|
|
52
|
+
usageReporting: true,
|
|
53
|
+
sessionDiscovery: true,
|
|
54
|
+
// Must match PiRuntime.capabilities (src/runtime/pi.ts) — the composer reads
|
|
55
|
+
// steer support from the session-less runtimes.list catalog, so if this drifts
|
|
56
|
+
// from the live runtime the client never learns steering is available and
|
|
57
|
+
// force-queues every mid-turn message instead of offering an immediate send.
|
|
58
|
+
streamingBehaviors: ["steer", "followUp"],
|
|
59
|
+
};
|
|
60
|
+
const CLAUDE_CAPABILITIES = {
|
|
61
|
+
toolInterception: true,
|
|
62
|
+
modelSelection: true,
|
|
63
|
+
packages: false,
|
|
64
|
+
resume: true,
|
|
65
|
+
fork: true,
|
|
66
|
+
usageReporting: true,
|
|
67
|
+
// Sessions started outside Bivy (a bare `claude` in a terminal) are
|
|
68
|
+
// discoverable and adoptable with a true native resume — see issue #156 and
|
|
69
|
+
// ClaudeCodeRuntime.discoverNativeSessions.
|
|
70
|
+
nativeSessionDiscovery: true,
|
|
71
|
+
nativeSessionAdoption: true,
|
|
72
|
+
// Must match ClaudeCodeRuntime.capabilities (src/runtime/claude-code.ts): a
|
|
73
|
+
// mid-turn prompt re-enters the SDK streaming-input queue and behaves as an
|
|
74
|
+
// immediate steer. Advertised here too so the composer offers "send now"
|
|
75
|
+
// straight from runtimes.list — before any session-capabilities merge, which
|
|
76
|
+
// won't fire on a reconnect to an already-running session (the mobile case).
|
|
77
|
+
streamingBehaviors: ["steer"],
|
|
78
|
+
};
|
|
79
|
+
function claudeCodeInfo() {
|
|
80
|
+
const installed = claudeSdkInstalled();
|
|
81
|
+
return {
|
|
82
|
+
id: "claude-code-sdk",
|
|
83
|
+
displayName: "Claude Code SDK",
|
|
84
|
+
description: "Anthropic's Claude Agent SDK driven as a Bivy runtime: streaming turns, model picker, and tool approvals via the SDK permission callback.",
|
|
85
|
+
status: installed ? "available" : "planned",
|
|
86
|
+
packageName: "@anthropic-ai/claude-agent-sdk",
|
|
87
|
+
language: "TypeScript",
|
|
88
|
+
// The interactive TUI is a chat<->CLI hand-off; only advertise it when the
|
|
89
|
+
// standalone `claude` CLI is actually installed on this node.
|
|
90
|
+
capabilities: { ...CLAUDE_CAPABILITIES, interactiveTui: commandAvailable("claude") },
|
|
91
|
+
supportTier: "supported",
|
|
92
|
+
authOwner: "mixed",
|
|
93
|
+
notes: installed
|
|
94
|
+
? "Multi-turn sessions over a streaming-input query(); approvals map to canUseTool. Set BIVY_CLAUDE_MODEL to pick a default model."
|
|
95
|
+
: "Install @anthropic-ai/claude-agent-sdk to enable this runtime (npm install @anthropic-ai/claude-agent-sdk).",
|
|
96
|
+
install: installed ? undefined : {
|
|
97
|
+
label: "Install Claude Code SDK",
|
|
98
|
+
description: "Installs the optional Anthropic SDK package into this Bivy node.",
|
|
99
|
+
command: "npm install @anthropic-ai/claude-agent-sdk",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function commandAvailable(command) {
|
|
104
|
+
if (!command.trim())
|
|
105
|
+
return false;
|
|
106
|
+
const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
|
|
107
|
+
shell: process.platform !== "win32",
|
|
108
|
+
stdio: "ignore",
|
|
109
|
+
});
|
|
110
|
+
return result.status === 0;
|
|
111
|
+
}
|
|
112
|
+
function genericCliInfo() {
|
|
113
|
+
const options = processRuntimeFromEnv();
|
|
114
|
+
const configured = Boolean(options);
|
|
115
|
+
// Same generic primitive as the built-in CLI agents: honest only when
|
|
116
|
+
// BIVY_AGENT_RESUME_TEMPLATE actually wired resumeArgs (see processRuntimeFromEnv).
|
|
117
|
+
const resume = Boolean(options?.resumeArgs);
|
|
118
|
+
return {
|
|
119
|
+
id: "generic-cli",
|
|
120
|
+
displayName: process.env.BIVY_AGENT_NAME?.trim() || "Generic CLI Agent",
|
|
121
|
+
description: "Run any local agent CLI underneath Bivy by spawning a configured process and streaming stdout/stderr.",
|
|
122
|
+
status: configured ? "available" : "planned",
|
|
123
|
+
packageName: process.env.BIVY_AGENT_COMMAND?.trim() || "Set BIVY_AGENT_COMMAND",
|
|
124
|
+
language: "Process",
|
|
125
|
+
capabilities: { toolInterception: false, modelSelection: false, resume, packages: false, fork: false },
|
|
126
|
+
supportTier: "experimental",
|
|
127
|
+
authOwner: "agent",
|
|
128
|
+
notes: configured
|
|
129
|
+
? `Configured through BIVY_AGENT_COMMAND / BIVY_AGENT_ARGS / BIVY_AGENT_PROMPT_MODE. Provides universal streaming but not structured approvals unless the agent speaks Bivy protocol.${resume ? " Resumable via BIVY_AGENT_RESUME_TEMPLATE." : " Set BIVY_AGENT_RESUME_TEMPLATE (a JSON arg array with {id}) if the configured agent has its own \"continue session <id>\" flag."}`
|
|
130
|
+
: "Set BIVY_AGENT_COMMAND to enable this universal CLI runtime.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const CLI_AGENT_SPECS = {
|
|
134
|
+
codex: {
|
|
135
|
+
displayName: "Codex",
|
|
136
|
+
command: "codex",
|
|
137
|
+
// Bare `codex "<prompt>"` launches the interactive TUI, which needs a real
|
|
138
|
+
// TTY and dies with "stdin is not a terminal" when driven over a pipe. The
|
|
139
|
+
// `exec` subcommand is Codex's non-interactive mode: it takes the prompt as
|
|
140
|
+
// an argument, runs headless, and streams the result to stdout.
|
|
141
|
+
args: ["exec"],
|
|
142
|
+
// `codex exec --json` streams a thread/turn/item JSONL event model.
|
|
143
|
+
jsonArgs: ["exec", "--json"],
|
|
144
|
+
parserId: "codex-json",
|
|
145
|
+
// `codex exec --json --sandbox <tier> <prompt>` — native OS sandbox.
|
|
146
|
+
composeArgs: ({ structured, tier }) => ["exec", ...(structured ? ["--json"] : []), "--sandbox", tier],
|
|
147
|
+
// Reasoning effort via a config override, after the `exec` subcommand.
|
|
148
|
+
// `codex exec -c model_reasoning_effort=<level> …`.
|
|
149
|
+
thinking: { levels: ["minimal", "low", "medium", "high"], default: "medium", template: ["-c", "model_reasoning_effort={level}"], insertAt: 1 },
|
|
150
|
+
packageName: "@openai/codex",
|
|
151
|
+
promptMode: "argv",
|
|
152
|
+
// Hidden from the picker: the governed `codex-approvals` app-server shim
|
|
153
|
+
// supersedes this plain exec path (still runnable via BIVY_RUNTIME=codex).
|
|
154
|
+
hidden: true,
|
|
155
|
+
install: { kind: "npm", pkg: "@openai/codex" },
|
|
156
|
+
},
|
|
157
|
+
opencode: {
|
|
158
|
+
displayName: "OpenCode",
|
|
159
|
+
command: "opencode",
|
|
160
|
+
packageName: "opencode-ai",
|
|
161
|
+
// `opencode run "<prompt>"` runs one non-interactive turn and streams the
|
|
162
|
+
// reply to stdout (the TUI needs a real TTY and would hang over a pipe).
|
|
163
|
+
args: ["run"],
|
|
164
|
+
promptMode: "argv",
|
|
165
|
+
supportTier: "beta",
|
|
166
|
+
blurb: "The most widely used open-source coding harness (OpenCode CLI).",
|
|
167
|
+
// `opencode run -s <id> "<prompt>"` continues a prior session by its own id
|
|
168
|
+
// (`-s, --session session id to continue`, per `opencode run --help`).
|
|
169
|
+
resume: { template: ["run", "-s", "{id}"] },
|
|
170
|
+
// `opencode run --model <provider/model> "<prompt>"` — the flag follows the
|
|
171
|
+
// `run` subcommand (insertAt: 1). Models are `provider/model` ids.
|
|
172
|
+
model: {
|
|
173
|
+
flag: "--model",
|
|
174
|
+
insertAt: 1,
|
|
175
|
+
models: [
|
|
176
|
+
{ id: "anthropic/claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
177
|
+
{ id: "openai/gpt-5", name: "GPT-5", provider: "openai" },
|
|
178
|
+
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro", provider: "google" },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
// OpenCode ships a native ACP server (`opencode acp`, per opencode.ai/docs/acp),
|
|
182
|
+
// so it can be driven through the governed ProtocolRuntime instead of the pipe —
|
|
183
|
+
// per-tool approvals + streaming + resume. Opt in with BIVY_OPENCODE_ACP=1 (or
|
|
184
|
+
// global BIVY_PREFER_ACP=1); off by default until validated for your version.
|
|
185
|
+
acp: { args: ["acp"] },
|
|
186
|
+
install: { kind: "npm", pkg: "opencode-ai" },
|
|
187
|
+
},
|
|
188
|
+
aider: {
|
|
189
|
+
displayName: "Aider",
|
|
190
|
+
command: "aider",
|
|
191
|
+
packageName: "aider-chat",
|
|
192
|
+
// `aider --message "<prompt>" --yes-always` runs a single non-interactive,
|
|
193
|
+
// git-aware turn and exits instead of dropping into the REPL.
|
|
194
|
+
args: ["--yes-always", "--message"],
|
|
195
|
+
promptMode: "argv",
|
|
196
|
+
supportTier: "beta",
|
|
197
|
+
authOwner: "mixed",
|
|
198
|
+
blurb: "Popular git-native pair-programming agent (Aider).",
|
|
199
|
+
// `aider --model <id> …` — a leading option (insertAt: 0). Aider resolves its
|
|
200
|
+
// own short aliases (sonnet/opus/gpt-4o/…) to concrete provider models.
|
|
201
|
+
model: {
|
|
202
|
+
flag: "--model",
|
|
203
|
+
models: [
|
|
204
|
+
{ id: "sonnet", name: "Claude Sonnet (alias)", provider: "anthropic" },
|
|
205
|
+
{ id: "opus", name: "Claude Opus (alias)", provider: "anthropic" },
|
|
206
|
+
{ id: "gpt-5", name: "GPT-5", provider: "openai" },
|
|
207
|
+
{ id: "o3", name: "OpenAI o3", provider: "openai" },
|
|
208
|
+
{ id: "gemini", name: "Gemini (alias)", provider: "google" },
|
|
209
|
+
{ id: "deepseek", name: "DeepSeek (alias)", provider: "deepseek" },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
// No `resume`: stock aider-chat has no "continue session <id>" flag. Its own
|
|
213
|
+
// continuity is `--restore-chat-history` reading `.aider.chat.history.md`,
|
|
214
|
+
// scoped to the cwd rather than to a Bivy session id — orthogonal to the
|
|
215
|
+
// generic id-based primitive, and unsafe to bolt on generically (a second,
|
|
216
|
+
// unrelated session opened in the same workspace would inherit that file's
|
|
217
|
+
// history). See docs/agents-not-fully-supported.md.
|
|
218
|
+
install: { kind: "pip", pkg: "aider-chat" },
|
|
219
|
+
},
|
|
220
|
+
hermes: {
|
|
221
|
+
displayName: "Hermes",
|
|
222
|
+
command: "hermes",
|
|
223
|
+
// The npm package is `hermes-agent` (ships the `hermes` bin); the bare
|
|
224
|
+
// `hermes` package is an unrelated abandoned segmentio lib.
|
|
225
|
+
packageName: "hermes-agent",
|
|
226
|
+
promptMode: "argv",
|
|
227
|
+
// Hidden from the picker: dumb-pipe adapter with no validated JSON parser or
|
|
228
|
+
// documented session/resume flag (still runnable via BIVY_RUNTIME=hermes).
|
|
229
|
+
hidden: true,
|
|
230
|
+
// No `resume`: no documented session/resume flag.
|
|
231
|
+
install: { kind: "npm", pkg: "hermes-agent" },
|
|
232
|
+
},
|
|
233
|
+
goose: {
|
|
234
|
+
displayName: "Goose",
|
|
235
|
+
command: "goose",
|
|
236
|
+
packageName: "block/goose",
|
|
237
|
+
args: ["run", "-t"],
|
|
238
|
+
// `goose run --output-format stream-json` streams message/complete envelopes.
|
|
239
|
+
jsonArgs: ["run", "--output-format", "stream-json", "-t"],
|
|
240
|
+
parserId: "goose-stream-json",
|
|
241
|
+
// Goose has no CLI sandbox flag; governed by the FS/MCP/network channels.
|
|
242
|
+
composeArgs: ({ structured }) => (structured ? ["run", "--output-format", "stream-json", "-t"] : ["run", "-t"]),
|
|
243
|
+
// `goose run --resume --session-id <id> -t "<prompt>"` continues a prior
|
|
244
|
+
// session by id (`--session-id` "Requires --resume", per `goose run --help`).
|
|
245
|
+
resume: { template: ["run", "--output-format", "stream-json", "--resume", "--session-id", "{id}", "-t"] },
|
|
246
|
+
// Goose exposes a native ACP server (`goose acp`, per the Goose "ACP clients"
|
|
247
|
+
// guide), so it can be driven through the governed ProtocolRuntime instead of the
|
|
248
|
+
// stream-json pipe — per-tool approvals + streaming + resume. Opt in with
|
|
249
|
+
// BIVY_GOOSE_ACP=1 (or global BIVY_PREFER_ACP=1); off by default until validated.
|
|
250
|
+
acp: { args: ["acp"] },
|
|
251
|
+
promptMode: "argv",
|
|
252
|
+
supportTier: "beta",
|
|
253
|
+
blurb: "Block's open-source agent with a structured stream-json protocol (Goose).",
|
|
254
|
+
// Homebrew isn't present on stock Linux nodes (brew → ENOENT); the official
|
|
255
|
+
// download script installs the goose binary on both Linux and macOS.
|
|
256
|
+
// `brew install block/tap/goose` ENOENTs on any node without Homebrew; the
|
|
257
|
+
// official download script installs the binary into `{bin}` (on PATH) on both
|
|
258
|
+
// Linux and macOS. `{bin}` expands to `<prefix>/bin` at install time.
|
|
259
|
+
install: {
|
|
260
|
+
kind: "curl",
|
|
261
|
+
display: "curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash",
|
|
262
|
+
shell: 'mkdir -p "{bin}" && curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false GOOSE_BIN_DIR="{bin}" bash',
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
gemini: {
|
|
266
|
+
displayName: "Gemini CLI",
|
|
267
|
+
command: "gemini",
|
|
268
|
+
packageName: "@google/gemini-cli",
|
|
269
|
+
supportTier: "beta",
|
|
270
|
+
blurb: "Google's terminal coding agent (Gemini CLI).",
|
|
271
|
+
// `gemini -m <id> … -p "<prompt>"` — a leading option before the trailing `-p`
|
|
272
|
+
// (insertAt: 0). The prompt flag stays last, so prepending is safe.
|
|
273
|
+
model: {
|
|
274
|
+
flag: "-m",
|
|
275
|
+
models: [
|
|
276
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", provider: "google" },
|
|
277
|
+
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", provider: "google" },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
args: ["-p"],
|
|
281
|
+
// `gemini -o json` returns one final {session_id,response,stats,error} object.
|
|
282
|
+
jsonArgs: ["-o", "json", "-p"],
|
|
283
|
+
parserId: "gemini-json",
|
|
284
|
+
// Gemini contains via --approval-mode; -p must stay last so the prompt
|
|
285
|
+
// (appended by ProcessRuntime) lands as its value.
|
|
286
|
+
composeArgs: ({ structured, tier }) => [...(structured ? ["-o", "json"] : []), ...sandboxArgsFor("gemini", tier), "-p"],
|
|
287
|
+
// `gemini -o json --approval-mode <mode> -r <id> -p "<prompt>"` continues a
|
|
288
|
+
// previous session (`-r, --resume Resume a previous session. Use "latest" for
|
|
289
|
+
// most recent or index number (e.g. --resume 5)`, per `gemini --help`; a
|
|
290
|
+
// session UUID also works). `{sandbox}` re-derives --approval-mode from the
|
|
291
|
+
// tier so a resumed turn stays as contained as a fresh one.
|
|
292
|
+
resume: { template: ["-o", "json", "{sandbox}", "-r", "{id}", "-p"] },
|
|
293
|
+
// Gemini CLI speaks ACP (`--experimental-acp`), so it can be driven through the
|
|
294
|
+
// governed ProtocolRuntime instead of the one-shot pipe — per-tool approvals +
|
|
295
|
+
// streaming + resume. Opt in with BIVY_GEMINI_ACP=1 (or global BIVY_PREFER_ACP=1);
|
|
296
|
+
// off by default until validated for your Gemini version.
|
|
297
|
+
acp: { args: ["--experimental-acp"] },
|
|
298
|
+
promptMode: "argv",
|
|
299
|
+
install: { kind: "npm", pkg: "@google/gemini-cli" },
|
|
300
|
+
},
|
|
301
|
+
qwen: {
|
|
302
|
+
displayName: "Qwen Code",
|
|
303
|
+
command: "qwen",
|
|
304
|
+
packageName: "@qwen-code/qwen-code",
|
|
305
|
+
supportTier: "beta",
|
|
306
|
+
blurb: "Alibaba's Qwen Code CLI (a Gemini-CLI fork tuned for Qwen-Coder models).",
|
|
307
|
+
// Gemini-CLI fork: same `-m <id> … -p` model flag (insertAt: 0).
|
|
308
|
+
model: {
|
|
309
|
+
flag: "-m",
|
|
310
|
+
models: [
|
|
311
|
+
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus", provider: "qwen" },
|
|
312
|
+
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash", provider: "qwen" },
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
// Qwen Code is a Gemini-CLI fork: `-p` runs headless and `--output-format json`
|
|
316
|
+
// emits the same {response,stats,error} envelope Gemini does, so it reuses the
|
|
317
|
+
// gemini-json parser. (Per the Qwen Code headless docs.)
|
|
318
|
+
args: ["-p"],
|
|
319
|
+
jsonArgs: ["--output-format", "json", "-p"],
|
|
320
|
+
parserId: "gemini-json",
|
|
321
|
+
// Shares Gemini's `--approval-mode` containment (see sandboxArgsFor("qwen")).
|
|
322
|
+
composeArgs: ({ structured, tier }) => [...(structured ? ["--output-format", "json"] : []), ...sandboxArgsFor("qwen", tier), "-p"],
|
|
323
|
+
// Gemini-CLI fork: same `--resume <id>` headless resume form (Qwen Code docs,
|
|
324
|
+
// "Headless Mode"). `{sandbox}` re-derives --approval-mode from the tier.
|
|
325
|
+
resume: { template: ["--output-format", "json", "{sandbox}", "--resume", "{id}", "-p"] },
|
|
326
|
+
// Qwen Code inherits Gemini CLI's ACP server (packages/cli/src/acp-integration),
|
|
327
|
+
// so it can be driven through the governed ProtocolRuntime instead of the pipe —
|
|
328
|
+
// per-tool approvals + streaming + resume. Newer builds graduated the flag to
|
|
329
|
+
// `--acp`, but `--experimental-acp` remains a backward-compatible alias across
|
|
330
|
+
// versions (deprecation warning goes to stderr, which the shim logs separately,
|
|
331
|
+
// so it can't corrupt the JSON-RPC stream). Opt in with BIVY_QWEN_ACP=1 (or
|
|
332
|
+
// global BIVY_PREFER_ACP=1); off by default until validated for your version.
|
|
333
|
+
// Zed's ACP registry lists qwen-code with `args: ["--acp"]`.
|
|
334
|
+
acp: { args: ["--experimental-acp"] },
|
|
335
|
+
promptMode: "argv",
|
|
336
|
+
install: { kind: "npm", pkg: "@qwen-code/qwen-code" },
|
|
337
|
+
},
|
|
338
|
+
cline: {
|
|
339
|
+
displayName: "Cline",
|
|
340
|
+
command: "cline",
|
|
341
|
+
packageName: "cline",
|
|
342
|
+
supportTier: "beta",
|
|
343
|
+
blurb: "Cline's standalone terminal agent (the CLI sibling of the Cline IDE extension).",
|
|
344
|
+
// `cline -y "<prompt>"` runs one autonomous, non-interactive task (‑y/‑‑yolo
|
|
345
|
+
// skips per-tool prompts so a piped run doesn't wedge on approval). Bivy's
|
|
346
|
+
// sandbox tier still bounds real effects. Flags are best-effort against the
|
|
347
|
+
// Cline CLI docs — override with BIVY_CLINE_ARGS if a version differs.
|
|
348
|
+
args: ["-y"],
|
|
349
|
+
// `cline --id <id> "<prompt>" -y` resumes an existing session by id
|
|
350
|
+
// (`--id <session-id>` "Resume an existing session by ID", per the Cline CLI
|
|
351
|
+
// reference). No native sandbox/approval-mode flag, so no `{sandbox}` here.
|
|
352
|
+
resume: { template: ["--id", "{id}", "-y"] },
|
|
353
|
+
// The Cline CLI (>2.0.0) speaks ACP via `cline --acp` (per docs.cline.bot ACP
|
|
354
|
+
// editor integrations), so it can be driven through the governed ProtocolRuntime
|
|
355
|
+
// instead of the `-y` pipe — per-tool approvals + streaming + resume. Opt in with
|
|
356
|
+
// BIVY_CLINE_ACP=1 (or global BIVY_PREFER_ACP=1); off by default until validated.
|
|
357
|
+
acp: { args: ["--acp"] },
|
|
358
|
+
promptMode: "argv",
|
|
359
|
+
install: { kind: "npm", pkg: "cline" },
|
|
360
|
+
},
|
|
361
|
+
crush: {
|
|
362
|
+
displayName: "Crush",
|
|
363
|
+
command: "crush",
|
|
364
|
+
packageName: "@charmland/crush",
|
|
365
|
+
supportTier: "beta",
|
|
366
|
+
blurb: "Charm's glamourous open-source coding agent (Crush).",
|
|
367
|
+
// `crush run "<prompt>"` runs a single non-interactive prompt and exits;
|
|
368
|
+
// `-q/--quiet` suppresses the spinner UI so stdout is just the reply.
|
|
369
|
+
// Override with BIVY_CRUSH_ARGS if a version differs.
|
|
370
|
+
args: ["run", "-q"],
|
|
371
|
+
// No `resume`: `crush run` has no session/continue flag upstream yet
|
|
372
|
+
// (charmbracelet/crush#1982, #1015 track adding one) — see
|
|
373
|
+
// docs/agents-not-fully-supported.md.
|
|
374
|
+
promptMode: "argv",
|
|
375
|
+
install: { kind: "npm", pkg: "@charmland/crush" },
|
|
376
|
+
},
|
|
377
|
+
// ---- Second wave (the next-most-used coding-agent CLIs) --------------------
|
|
378
|
+
// Each is pure data on the shared ProcessRuntime path — the same "add an agent
|
|
379
|
+
// = add a spec, not code" mechanism as the block above. Launch/resume/model
|
|
380
|
+
// flags are validated against each CLI's current docs; every one is overridable
|
|
381
|
+
// per node with BIVY_<ID>_ARGS / _RESUME_TEMPLATE / _MODELS.
|
|
382
|
+
cursor: {
|
|
383
|
+
displayName: "Cursor",
|
|
384
|
+
command: "cursor-agent",
|
|
385
|
+
packageName: "cursor (curl https://cursor.com/install)",
|
|
386
|
+
supportTier: "beta",
|
|
387
|
+
blurb: "Cursor's standalone terminal coding agent (cursor-agent) — the editor's engine on the CLI.",
|
|
388
|
+
// `cursor-agent --force -p "<prompt>"` runs one non-interactive print turn and
|
|
389
|
+
// exits (`-p/--print`); `--force` auto-approves tool/command execution so a
|
|
390
|
+
// piped run never blocks on approvals. Prompt is the trailing positional arg.
|
|
391
|
+
args: ["--force", "-p"],
|
|
392
|
+
// Cursor's `--output-format stream-json` emits a streaming JSON event log; the
|
|
393
|
+
// tolerant generic parser reads it. Opt-in (unverified schema) — see below.
|
|
394
|
+
jsonArgs: ["--output-format", "stream-json", "--force", "-p"],
|
|
395
|
+
parserId: "generic-stream-json",
|
|
396
|
+
parserUnverified: true,
|
|
397
|
+
// `cursor-agent --resume=<chatId> …` continues a prior chat by its own id.
|
|
398
|
+
resume: { template: ["--force", "--resume={id}", "-p"] },
|
|
399
|
+
// `cursor-agent -m <id> …` — a leading option (insertAt: 0).
|
|
400
|
+
model: {
|
|
401
|
+
flag: "-m",
|
|
402
|
+
models: [
|
|
403
|
+
{ id: "sonnet-4.5", name: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
404
|
+
{ id: "opus-4.1", name: "Claude Opus 4.1", provider: "anthropic" },
|
|
405
|
+
{ id: "gpt-5", name: "GPT-5", provider: "openai" },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
// Cursor's agent speaks ACP (`cursor-agent acp`, per cursor.com/docs/cli/acp),
|
|
409
|
+
// so it can be driven through the governed ProtocolRuntime instead of the
|
|
410
|
+
// `--force -p` pipe — per-tool approvals + streaming + resume. Opt in with
|
|
411
|
+
// BIVY_CURSOR_ACP=1 (or global BIVY_PREFER_ACP=1); off by default until validated.
|
|
412
|
+
acp: { args: ["acp"] },
|
|
413
|
+
promptMode: "argv",
|
|
414
|
+
// Not on npm — Cursor ships a curl installer that drops `cursor-agent` on PATH.
|
|
415
|
+
install: { kind: "curl", display: "curl https://cursor.com/install -fsS | bash", shell: "curl https://cursor.com/install -fsS | bash" },
|
|
416
|
+
},
|
|
417
|
+
copilot: {
|
|
418
|
+
displayName: "GitHub Copilot",
|
|
419
|
+
command: "copilot",
|
|
420
|
+
packageName: "@github/copilot",
|
|
421
|
+
supportTier: "beta",
|
|
422
|
+
blurb: "GitHub's official terminal coding agent (Copilot CLI).",
|
|
423
|
+
// `copilot --allow-all-tools -p "<prompt>"` runs one programmatic turn and
|
|
424
|
+
// exits; --allow-all-tools skips per-tool approval so a piped run doesn't
|
|
425
|
+
// wedge. `-p/--prompt` takes the prompt as its value (kept last so the
|
|
426
|
+
// trailing prompt lands there).
|
|
427
|
+
args: ["--allow-all-tools", "-p"],
|
|
428
|
+
// `copilot --model <id> …`
|
|
429
|
+
model: {
|
|
430
|
+
flag: "--model",
|
|
431
|
+
models: [
|
|
432
|
+
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
433
|
+
{ id: "gpt-5", name: "GPT-5", provider: "openai" },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
// No `resume`: Copilot's resume flag isn't pinned to a stable by-id form yet;
|
|
437
|
+
// wire one with BIVY_COPILOT_RESUME_TEMPLATE if your version documents it.
|
|
438
|
+
// Copilot CLI ships an ACP server (`copilot --acp`; public preview Jan 2026, per
|
|
439
|
+
// docs.github.com Copilot CLI reference), so it can be driven through the governed
|
|
440
|
+
// ProtocolRuntime instead of the `--allow-all-tools -p` pipe — per-tool approvals
|
|
441
|
+
// + streaming + resume (ACP `session/load` covers the resume the pipe lacks). Opt
|
|
442
|
+
// in with BIVY_COPILOT_ACP=1 (or global BIVY_PREFER_ACP=1); off by default until
|
|
443
|
+
// validated for your version.
|
|
444
|
+
acp: { args: ["--acp"] },
|
|
445
|
+
promptMode: "argv",
|
|
446
|
+
install: { kind: "npm", pkg: "@github/copilot" },
|
|
447
|
+
},
|
|
448
|
+
grok: {
|
|
449
|
+
displayName: "Grok",
|
|
450
|
+
command: "grok",
|
|
451
|
+
packageName: "@vibe-kit/grok-cli",
|
|
452
|
+
supportTier: "beta",
|
|
453
|
+
blurb: "Open-source terminal agent for xAI's Grok models (Grok CLI).",
|
|
454
|
+
// `grok -p "<prompt>"` runs one prompt and exits (headless); `-m <id>` picks
|
|
455
|
+
// the model. (The widely-installed @vibe-kit/grok-cli has no by-id resume or
|
|
456
|
+
// JSON flag; the superagent `grok-dev` fork does — override via env if you run
|
|
457
|
+
// that one.)
|
|
458
|
+
args: ["-p"],
|
|
459
|
+
model: {
|
|
460
|
+
flag: "-m",
|
|
461
|
+
models: [
|
|
462
|
+
{ id: "grok-code-fast-1", name: "Grok Code Fast 1", provider: "xai" },
|
|
463
|
+
{ id: "grok-4-latest", name: "Grok 4", provider: "xai" },
|
|
464
|
+
{ id: "grok-3-fast", name: "Grok 3 Fast", provider: "xai" },
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
promptMode: "argv",
|
|
468
|
+
install: { kind: "npm", pkg: "@vibe-kit/grok-cli" },
|
|
469
|
+
},
|
|
470
|
+
amp: {
|
|
471
|
+
displayName: "Amp",
|
|
472
|
+
command: "amp",
|
|
473
|
+
packageName: "@sourcegraph/amp",
|
|
474
|
+
supportTier: "beta",
|
|
475
|
+
blurb: "Sourcegraph's autonomous coding agent with persistent threads (Amp).",
|
|
476
|
+
// `amp -x "<prompt>"` (`--execute`) runs one thread turn and streams to stdout;
|
|
477
|
+
// Amp doesn't gate tools per-run (governed by its own allowlist config), so no
|
|
478
|
+
// approval flag is needed. Prompt trails `-x`.
|
|
479
|
+
args: ["-x"],
|
|
480
|
+
// Amp's `--stream-json` emits one JSON object per line; the tolerant generic
|
|
481
|
+
// parser reads it. Opt-in (unverified schema) — see parserUnverified below.
|
|
482
|
+
jsonArgs: ["--stream-json", "-x"],
|
|
483
|
+
parserId: "generic-stream-json",
|
|
484
|
+
parserUnverified: true,
|
|
485
|
+
// `amp threads continue <id> -x "<prompt>"` continues a prior thread by id.
|
|
486
|
+
resume: { template: ["threads", "continue", "{id}", "-x"] },
|
|
487
|
+
// No model flag: Amp manages model selection itself (agent "mode"), so we don't
|
|
488
|
+
// advertise a picker it can't drive.
|
|
489
|
+
promptMode: "argv",
|
|
490
|
+
install: { kind: "npm", pkg: "@sourcegraph/amp" },
|
|
491
|
+
},
|
|
492
|
+
auggie: {
|
|
493
|
+
displayName: "Auggie",
|
|
494
|
+
command: "auggie",
|
|
495
|
+
packageName: "@augmentcode/auggie",
|
|
496
|
+
supportTier: "beta",
|
|
497
|
+
blurb: "Augment Code's terminal agent backed by its codebase context engine (Auggie).",
|
|
498
|
+
// `auggie --quiet --print "<prompt>"` runs one non-interactive turn and prints
|
|
499
|
+
// the final reply (`--print`); `--quiet` drops the UI chatter. Prompt trails.
|
|
500
|
+
args: ["--quiet", "--print"],
|
|
501
|
+
// No pinned by-id resume or model flag upstream (Augment manages the model);
|
|
502
|
+
// override via BIVY_AUGGIE_RESUME_TEMPLATE / _MODELS if your version adds them.
|
|
503
|
+
promptMode: "argv",
|
|
504
|
+
install: { kind: "npm", pkg: "@augmentcode/auggie" },
|
|
505
|
+
},
|
|
506
|
+
droid: {
|
|
507
|
+
displayName: "Droid",
|
|
508
|
+
command: "droid",
|
|
509
|
+
packageName: "droid (curl https://app.factory.ai/cli)",
|
|
510
|
+
supportTier: "beta",
|
|
511
|
+
blurb: "Factory AI's autonomous terminal coding agent (Droid).",
|
|
512
|
+
// `droid exec --auto high "<prompt>"` runs one headless task at high autonomy
|
|
513
|
+
// (auto-approves) and streams to stdout. Prompt trails the `exec` subcommand.
|
|
514
|
+
args: ["exec", "--auto", "high"],
|
|
515
|
+
// `droid exec --output-format json` prints a final JSON object; the tolerant
|
|
516
|
+
// generic parser reads it. Opt-in (unverified schema) — see below.
|
|
517
|
+
jsonArgs: ["exec", "--output-format", "json", "--auto", "high"],
|
|
518
|
+
parserId: "generic-json",
|
|
519
|
+
parserUnverified: true,
|
|
520
|
+
// `droid exec --model <id> …` — after the `exec` subcommand (insertAt: 1).
|
|
521
|
+
model: {
|
|
522
|
+
flag: "--model",
|
|
523
|
+
insertAt: 1,
|
|
524
|
+
models: [
|
|
525
|
+
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
526
|
+
{ id: "claude-opus-4.1", name: "Claude Opus 4.1", provider: "anthropic" },
|
|
527
|
+
{ id: "gpt-5-codex", name: "GPT-5 Codex", provider: "openai" },
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
promptMode: "argv",
|
|
531
|
+
// Not on npm — Factory ships a curl installer that drops `droid` on PATH.
|
|
532
|
+
install: { kind: "curl", display: "curl -fsSL https://app.factory.ai/cli | sh", shell: "curl -fsSL https://app.factory.ai/cli | sh" },
|
|
533
|
+
},
|
|
534
|
+
continue: {
|
|
535
|
+
displayName: "Continue",
|
|
536
|
+
command: "cn",
|
|
537
|
+
packageName: "@continuedev/cli",
|
|
538
|
+
supportTier: "beta",
|
|
539
|
+
blurb: "Continue's headless terminal agent (cn) driving configurable assistants.",
|
|
540
|
+
// `cn --auto -p "<prompt>"` runs one headless turn (`-p` = no TUI) and prints
|
|
541
|
+
// the final response; `--auto` allows all tools without prompting. Prompt
|
|
542
|
+
// trails `-p`.
|
|
543
|
+
args: ["--auto", "-p"],
|
|
544
|
+
// `cn -p … --format json` prints a final JSON object; the tolerant generic
|
|
545
|
+
// parser reads it. Opt-in (unverified schema) — see below.
|
|
546
|
+
jsonArgs: ["--auto", "--format", "json", "-p"],
|
|
547
|
+
parserId: "generic-json",
|
|
548
|
+
parserUnverified: true,
|
|
549
|
+
// `cn --model <slug> …` — Continue Hub owner/model slugs (insertAt: 0).
|
|
550
|
+
model: {
|
|
551
|
+
flag: "--model",
|
|
552
|
+
models: [
|
|
553
|
+
{ id: "anthropic/claude-4-sonnet", name: "Claude Sonnet 4", provider: "anthropic" },
|
|
554
|
+
{ id: "openai/gpt-5", name: "GPT-5", provider: "openai" },
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
// No `resume`: `cn --resume` continues only the last session for the current
|
|
558
|
+
// terminal — there's no resume-by-id form to plug into the generic primitive.
|
|
559
|
+
promptMode: "argv",
|
|
560
|
+
install: { kind: "npm", pkg: "@continuedev/cli" },
|
|
561
|
+
},
|
|
562
|
+
kilocode: {
|
|
563
|
+
displayName: "Kilo Code",
|
|
564
|
+
command: "kilo",
|
|
565
|
+
packageName: "@kilocode/cli",
|
|
566
|
+
supportTier: "beta",
|
|
567
|
+
blurb: "Kilo Code's terminal CLI (an OpenCode fork) for pipeline-friendly agentic coding.",
|
|
568
|
+
// `kilo run --auto "<prompt>"` runs one non-interactive turn (`run`) with
|
|
569
|
+
// auto-approved permissions (`--auto`) and streams to stdout. Prompt trails.
|
|
570
|
+
args: ["run", "--auto"],
|
|
571
|
+
// `kilo run --format json` emits raw JSON events; the tolerant generic parser
|
|
572
|
+
// reads them. Opt-in (unverified schema) — see below.
|
|
573
|
+
jsonArgs: ["run", "--format", "json", "--auto"],
|
|
574
|
+
parserId: "generic-stream-json",
|
|
575
|
+
parserUnverified: true,
|
|
576
|
+
// `kilo run -s <id> --auto "<prompt>"` continues a session by id.
|
|
577
|
+
resume: { template: ["run", "-s", "{id}", "--auto"] },
|
|
578
|
+
// Kilo Code exposes a native ACP server (`kilo acp`, per the Kilo CLI docs —
|
|
579
|
+
// mirroring OpenCode's design, its upstream). Driven through the governed
|
|
580
|
+
// ProtocolRuntime instead of the `run --auto` pipe, it gains per-tool approvals +
|
|
581
|
+
// streaming + resume. Opt in with BIVY_KILOCODE_ACP=1 (or global BIVY_PREFER_ACP=1);
|
|
582
|
+
// off by default until validated for your version.
|
|
583
|
+
acp: { args: ["acp"] },
|
|
584
|
+
// `kilo run -m <provider/model> …` — after the `run` subcommand (insertAt: 1).
|
|
585
|
+
model: {
|
|
586
|
+
flag: "-m",
|
|
587
|
+
insertAt: 1,
|
|
588
|
+
models: [
|
|
589
|
+
{ id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
|
|
590
|
+
{ id: "openai/gpt-5", name: "GPT-5", provider: "openai" },
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
promptMode: "argv",
|
|
594
|
+
install: { kind: "npm", pkg: "@kilocode/cli" },
|
|
595
|
+
},
|
|
596
|
+
rovodev: {
|
|
597
|
+
displayName: "Rovo Dev",
|
|
598
|
+
command: "acli",
|
|
599
|
+
packageName: "atlassian acli (rovodev)",
|
|
600
|
+
supportTier: "beta",
|
|
601
|
+
blurb: "Atlassian's Rovo Dev terminal coding agent, run through the acli CLI.",
|
|
602
|
+
// `acli rovodev run --yolo "<prompt>"` runs one instruction headlessly; --yolo
|
|
603
|
+
// skips tool-approval prompts. Prompt trails the `rovodev run` subcommand.
|
|
604
|
+
args: ["rovodev", "run", "--yolo"],
|
|
605
|
+
// `acli rovodev run --yolo --restore <id> "<prompt>"` restores a prior session.
|
|
606
|
+
resume: { template: ["rovodev", "run", "--yolo", "--restore", "{id}"] },
|
|
607
|
+
// No `--model` CLI flag (Atlassian-managed; models switch via the in-session
|
|
608
|
+
// /models command), so we don't advertise a picker it can't drive.
|
|
609
|
+
promptMode: "argv",
|
|
610
|
+
// Ships as part of the Atlassian CLI, not npm — installed out of band.
|
|
611
|
+
},
|
|
612
|
+
codebuff: {
|
|
613
|
+
displayName: "Codebuff",
|
|
614
|
+
command: "codebuff",
|
|
615
|
+
packageName: "codebuff",
|
|
616
|
+
// Hidden from the picker (see PICKER_RUNTIME_IDS). The `codebuff` binary has no
|
|
617
|
+
// verified non-TTY headless / print-and-exit mode upstream — its trailing-arg
|
|
618
|
+
// `codebuff "<prompt>"` seeds the interactive TUI, and true automation is meant
|
|
619
|
+
// to go through @codebuff/sdk. We keep the spec so it's runnable via
|
|
620
|
+
// BIVY_RUNTIME=codebuff and promotable to the picker (data-only) the moment a
|
|
621
|
+
// headless flag ships; until then it stays out of the picker to keep it honest.
|
|
622
|
+
supportTier: "experimental",
|
|
623
|
+
blurb: "Open-source multi-agent terminal coding assistant (Codebuff). Headless automation is via @codebuff/sdk today.",
|
|
624
|
+
hidden: true,
|
|
625
|
+
args: [],
|
|
626
|
+
// `codebuff --continue <id> "<prompt>"` continues a prior conversation by id.
|
|
627
|
+
resume: { template: ["--continue", "{id}"] },
|
|
628
|
+
promptMode: "argv",
|
|
629
|
+
install: { kind: "npm", pkg: "codebuff" },
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
export function isCliAgentId(id) {
|
|
633
|
+
return Object.prototype.hasOwnProperty.call(CLI_AGENT_SPECS, id);
|
|
634
|
+
}
|
|
635
|
+
/** Ordered CLI agent ids (spec insertion order) — the manifest's canonical order. */
|
|
636
|
+
export const CLI_AGENT_IDS = Object.keys(CLI_AGENT_SPECS);
|
|
637
|
+
/**
|
|
638
|
+
* The install command for a CLI agent, derived from its structured `install`
|
|
639
|
+
* descriptor — the SINGLE source of truth shared by the catalog "Install" button,
|
|
640
|
+
* the server auto-install endpoint, and the terminal CLI manifest. Returns the
|
|
641
|
+
* executable form (`command`/`args`) plus the human `display` string, or undefined
|
|
642
|
+
* when the agent installs out of band (no `install`).
|
|
643
|
+
*
|
|
644
|
+
* `prefix` is the node's npm/bin prefix (BIVY_NPM_GLOBAL_PREFIX, default ~/.local).
|
|
645
|
+
* `{bin}` in a curl `shell` expands to `<prefix>/bin`.
|
|
646
|
+
*/
|
|
647
|
+
export function cliInstallSpec(id, prefix) {
|
|
648
|
+
const install = CLI_AGENT_SPECS[id].install;
|
|
649
|
+
if (!install)
|
|
650
|
+
return undefined;
|
|
651
|
+
if (install.kind === "npm") {
|
|
652
|
+
return {
|
|
653
|
+
command: "npm",
|
|
654
|
+
args: ["install", "--global", "--prefix", prefix, install.pkg],
|
|
655
|
+
display: `npm install --global --prefix ${prefix} ${install.pkg}`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (install.kind === "pip") {
|
|
659
|
+
// Some node images ship a python3 without pip; bootstrap it via ensurepip
|
|
660
|
+
// (best-effort) before installing, but show users the plain pip line.
|
|
661
|
+
return {
|
|
662
|
+
command: "sh",
|
|
663
|
+
args: ["-c", `python3 -m ensurepip --user >/dev/null 2>&1 || true; python3 -m pip install --user ${install.pkg}`],
|
|
664
|
+
display: `python3 -m pip install --user ${install.pkg}`,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
// curl / script: `{bin}` → the node's <prefix>/bin so binaries land on PATH.
|
|
668
|
+
const shell = install.shell.replace(/\{bin\}/g, `${prefix}/bin`);
|
|
669
|
+
return { command: "sh", args: ["-c", shell], display: install.display };
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Serializable agent manifest — the identity/install/visibility subset of
|
|
673
|
+
* CLI_AGENT_SPECS with no functions, so it can be written to
|
|
674
|
+
* `bin/agent-manifest.json` and consumed by the plain-JS terminal CLI
|
|
675
|
+
* (`bin/bivy.mjs`) that can't import this TypeScript module. `scripts/
|
|
676
|
+
* generate-agent-manifest.mjs` regenerates the JSON; a unit test asserts the file
|
|
677
|
+
* is in sync so the two never drift.
|
|
678
|
+
*/
|
|
679
|
+
export function cliAgentManifest() {
|
|
680
|
+
return CLI_AGENT_IDS.map((id) => {
|
|
681
|
+
const spec = CLI_AGENT_SPECS[id];
|
|
682
|
+
// The tokens that mean "one-shot / headless" for `bivy run <agent> …` — the
|
|
683
|
+
// spec's own launch args plus its resume subcommand, deduped. This lets the
|
|
684
|
+
// terminal detect a human running a one-shot without a hand-maintained list.
|
|
685
|
+
const headless = new Set();
|
|
686
|
+
for (const a of spec.args ?? [])
|
|
687
|
+
if (a.startsWith("-") || /^[a-z]/.test(a))
|
|
688
|
+
headless.add(a);
|
|
689
|
+
if (spec.resume)
|
|
690
|
+
for (const a of spec.resume.template)
|
|
691
|
+
if (a.startsWith("-") || /^[a-z]/.test(a))
|
|
692
|
+
headless.add(a);
|
|
693
|
+
return {
|
|
694
|
+
id,
|
|
695
|
+
label: spec.displayName,
|
|
696
|
+
command: spec.command,
|
|
697
|
+
hidden: Boolean(spec.hidden),
|
|
698
|
+
headlessFlags: [...headless].filter((a) => !a.includes("{")),
|
|
699
|
+
install: spec.install ?? null,
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Per-agent launch-arg override, e.g. `BIVY_CLINE_ARGS='["task","--json"]'`. Lets
|
|
705
|
+
* an operator correct a CLI's flags for a version we haven't pinned without a code
|
|
706
|
+
* change (the beta CLI agents ship best-effort defaults). Malformed = ignored.
|
|
707
|
+
*/
|
|
708
|
+
function cliArgsOverride(id) {
|
|
709
|
+
const raw = process.env[`BIVY_${id.toUpperCase()}_ARGS`]?.trim();
|
|
710
|
+
if (!raw)
|
|
711
|
+
return undefined;
|
|
712
|
+
try {
|
|
713
|
+
const parsed = JSON.parse(raw);
|
|
714
|
+
if (Array.isArray(parsed))
|
|
715
|
+
return parsed.map(String);
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
// fall through — ignore a malformed override
|
|
719
|
+
}
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Resolve a CLI agent's resume template: an operator override
|
|
724
|
+
* (`BIVY_<ID>_RESUME_TEMPLATE`, a JSON arg array with `{id}`/`{tier}`) wins, else
|
|
725
|
+
* the spec's built-in template. Returns undefined when the agent has no known
|
|
726
|
+
* resume form — which keeps the catalog honest (resume reported off).
|
|
727
|
+
*/
|
|
728
|
+
function cliResumeTemplate(id) {
|
|
729
|
+
const raw = process.env[`BIVY_${id.toUpperCase()}_RESUME_TEMPLATE`]?.trim();
|
|
730
|
+
if (raw) {
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(raw);
|
|
733
|
+
if (Array.isArray(parsed))
|
|
734
|
+
return parsed.map(String);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// fall through to the spec default
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return CLI_AGENT_SPECS[id].resume?.template;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Resolve a CLI agent's selectable model list: `BIVY_<ID>_MODELS` (a JSON array of
|
|
744
|
+
* `{id,name?,provider?}`) overrides the spec's curated defaults. Each entry is
|
|
745
|
+
* normalized to a full ModelInfo (the id is the CLI's own model name). Returns an
|
|
746
|
+
* empty list when the agent has no model config and no override.
|
|
747
|
+
*/
|
|
748
|
+
function cliModelList(id) {
|
|
749
|
+
const spec = CLI_AGENT_SPECS[id];
|
|
750
|
+
let entries = spec.model?.models;
|
|
751
|
+
const raw = process.env[`BIVY_${id.toUpperCase()}_MODELS`]?.trim();
|
|
752
|
+
if (raw) {
|
|
753
|
+
try {
|
|
754
|
+
const parsed = JSON.parse(raw);
|
|
755
|
+
if (Array.isArray(parsed)) {
|
|
756
|
+
const out = [];
|
|
757
|
+
for (const item of parsed) {
|
|
758
|
+
const e = (item && typeof item === "object" ? item : { id: item });
|
|
759
|
+
const modelId = typeof e.id === "string" ? e.id.trim() : "";
|
|
760
|
+
if (!modelId)
|
|
761
|
+
continue;
|
|
762
|
+
out.push({
|
|
763
|
+
id: modelId,
|
|
764
|
+
name: typeof e.name === "string" ? e.name : undefined,
|
|
765
|
+
provider: typeof e.provider === "string" ? e.provider : undefined,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
entries = out;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// fall through to the spec defaults
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return (entries ?? []).map((e) => ({ provider: e.provider ?? id, id: e.id, name: e.name ?? e.id }));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Build the ProcessRuntime model config for a CLI agent, or undefined when the
|
|
779
|
+
* agent has no model flag or an empty list (so the runtime honestly reports
|
|
780
|
+
* modelSelection off). The chosen model id is passed as the value of `spec.model.flag`.
|
|
781
|
+
*/
|
|
782
|
+
function cliModelConfig(id) {
|
|
783
|
+
const spec = CLI_AGENT_SPECS[id];
|
|
784
|
+
if (!spec.model)
|
|
785
|
+
return undefined;
|
|
786
|
+
const models = cliModelList(id);
|
|
787
|
+
if (!models.length)
|
|
788
|
+
return undefined;
|
|
789
|
+
return {
|
|
790
|
+
models,
|
|
791
|
+
modelArgs: (modelId) => [spec.model.flag, modelId],
|
|
792
|
+
insertAt: spec.model.insertAt,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
// Structured parsers that extract token usage from the agent's output (so the
|
|
796
|
+
// runtime can honestly advertise usageReporting — see cli-parsers.extractTokenUsage).
|
|
797
|
+
const USAGE_PARSERS = new Set(["codex-json", "gemini-json", "goose-stream-json"]);
|
|
798
|
+
/** Whether a CLI agent runs a usage-emitting structured parser this launch. */
|
|
799
|
+
function cliUsageReporting(id) {
|
|
800
|
+
const parserId = process.env.BIVY_AGENT_PARSER || CLI_AGENT_SPECS[id].parserId;
|
|
801
|
+
return Boolean(parserId) && USAGE_PARSERS.has(parserId) && process.env.BIVY_AGENT_STRUCTURED !== "0";
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Build the ProcessRuntime thinking config for a CLI agent, or undefined when it
|
|
805
|
+
* has no reasoning-effort flag. `BIVY_<ID>_THINKING` (JSON
|
|
806
|
+
* `{levels,template,insertAt?,default?}`) overrides/enables it for any agent.
|
|
807
|
+
*/
|
|
808
|
+
function cliThinkingConfig(id) {
|
|
809
|
+
let cfg = CLI_AGENT_SPECS[id].thinking;
|
|
810
|
+
const raw = process.env[`BIVY_${id.toUpperCase()}_THINKING`]?.trim();
|
|
811
|
+
if (raw) {
|
|
812
|
+
try {
|
|
813
|
+
const parsed = JSON.parse(raw);
|
|
814
|
+
if (parsed && Array.isArray(parsed.levels) && Array.isArray(parsed.template)) {
|
|
815
|
+
cfg = { levels: parsed.levels.map(String), template: parsed.template.map(String), insertAt: typeof parsed.insertAt === "number" ? parsed.insertAt : undefined, default: parsed.default ? String(parsed.default) : undefined };
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
// fall through to the spec default
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (!cfg || !cfg.levels.length)
|
|
823
|
+
return undefined;
|
|
824
|
+
const template = cfg.template;
|
|
825
|
+
return {
|
|
826
|
+
levels: cfg.levels,
|
|
827
|
+
default: cfg.default,
|
|
828
|
+
thinkingArgs: (level) => template.map((a) => a.replace(/\{level\}/g, level)),
|
|
829
|
+
insertAt: cfg.insertAt,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
// --- #4: opt-in capability probing (self-healing honesty) -------------------
|
|
833
|
+
// Our advertised resume/model capabilities are pinned against each CLI's docs at a
|
|
834
|
+
// point in time, so a version that renamed or dropped a flag would keep rendering a
|
|
835
|
+
// control that silently no-ops. `BIVY_AGENT_PROBE=1` turns on a preflight that runs
|
|
836
|
+
// `<cli> --help` once (cached) and DOWNGRADES any capability whose flag the
|
|
837
|
+
// installed binary doesn't actually mention. It never UPGRADES — adding a
|
|
838
|
+
// capability needs the exact arg template, which help text can't safely supply — so
|
|
839
|
+
// probing can only make the catalog MORE honest, never invent a no-op control.
|
|
840
|
+
const HELP_PROBE_CACHE = new Map();
|
|
841
|
+
function probeHelpText(command) {
|
|
842
|
+
if (HELP_PROBE_CACHE.has(command))
|
|
843
|
+
return HELP_PROBE_CACHE.get(command) ?? null;
|
|
844
|
+
let text = null;
|
|
845
|
+
try {
|
|
846
|
+
const res = spawnSync(command, ["--help"], { encoding: "utf8", timeout: 4000 });
|
|
847
|
+
const out = `${res.stdout ?? ""}\n${res.stderr ?? ""}`.trim();
|
|
848
|
+
text = out.length > 20 ? out.toLowerCase() : null; // too-short output = not real help
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
text = null;
|
|
852
|
+
}
|
|
853
|
+
HELP_PROBE_CACHE.set(command, text);
|
|
854
|
+
return text;
|
|
855
|
+
}
|
|
856
|
+
// A resume template mixes launch flags (`-p`, `--force`) with the resume-specific
|
|
857
|
+
// token(s) (`--resume`, `threads continue`, `-s`, `--restore`, …). Only the latter
|
|
858
|
+
// evidence resume support, so we match on those — otherwise a shared launch flag
|
|
859
|
+
// appearing in help would mask a genuinely-missing resume flag.
|
|
860
|
+
const RESUME_HINT = /resume|continue|restore|session|thread|^-s$|^-r$|^-c$|^--id$/i;
|
|
861
|
+
/** The resume-indicative flag/subcommand tokens of a resume template. */
|
|
862
|
+
function resumeTokensFor(id) {
|
|
863
|
+
const tmpl = cliResumeTemplate(id) ?? [];
|
|
864
|
+
return tmpl
|
|
865
|
+
.map((t) => t.replace(/=\{[a-z]+\}/g, "").replace(/\{[a-z]+\}/g, "").trim())
|
|
866
|
+
.filter((t) => t && !t.startsWith("{") && RESUME_HINT.test(t));
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Pure refinement: given an installed CLI's `--help` text, drop any capability the
|
|
870
|
+
* binary doesn't evidence. Exported for direct unit testing. `resumeTokens` are the
|
|
871
|
+
* resume form's flag/subcommand words (e.g. `["--resume"]`, `["threads","continue"]`);
|
|
872
|
+
* if NONE appear in help, resume is downgraded. Likewise the model flag.
|
|
873
|
+
*/
|
|
874
|
+
export function refineCapabilitiesFromHelp(help, current, spec) {
|
|
875
|
+
const h = help.toLowerCase();
|
|
876
|
+
let { resume, modelSelection } = current;
|
|
877
|
+
if (resume && spec.resumeTokens.length && !spec.resumeTokens.some((t) => h.includes(t.toLowerCase()))) {
|
|
878
|
+
resume = false;
|
|
879
|
+
}
|
|
880
|
+
if (modelSelection && spec.modelFlag && !h.includes(spec.modelFlag.toLowerCase())) {
|
|
881
|
+
modelSelection = false;
|
|
882
|
+
}
|
|
883
|
+
return { resume, modelSelection };
|
|
884
|
+
}
|
|
885
|
+
function cliAgentInfo(id) {
|
|
886
|
+
const spec = CLI_AGENT_SPECS[id];
|
|
887
|
+
const installed = commandAvailable(spec.command);
|
|
888
|
+
const npmPrefix = process.env.BIVY_NPM_GLOBAL_PREFIX || "~/.local";
|
|
889
|
+
const installCommand = cliInstallSpec(id, npmPrefix);
|
|
890
|
+
// Honesty invariant (see docs/agents-not-fully-supported.md): capabilities must
|
|
891
|
+
// reflect what the ProcessRuntime path actually delivers, or the PWA renders a
|
|
892
|
+
// picker that silently no-ops. These CLI adapters stream stdout (structured via
|
|
893
|
+
// a CliParser when the agent has a validated JSON mode, else raw) and are
|
|
894
|
+
// governed at the effect level (sandbox tier / FS-MCP-network channels), so
|
|
895
|
+
// toolInterception + modelSelection stay false. resume is on only when the
|
|
896
|
+
// agent has a known resume form (spec.resume or a BIVY_<ID>_RESUME_TEMPLATE
|
|
897
|
+
// override) — Codex is the built-in example; the rest are fresh-process-per-
|
|
898
|
+
// prompt until a resume template is wired.
|
|
899
|
+
let resume = id === "codex" || Boolean(cliResumeTemplate(id));
|
|
900
|
+
let modelSelection = Boolean(cliModelConfig(id));
|
|
901
|
+
const usageReporting = cliUsageReporting(id);
|
|
902
|
+
// When the agent is promoted to ACP (spec.acp + BIVY_<ID>_ACP / BIVY_PREFER_ACP),
|
|
903
|
+
// it runs through the governed ProtocolRuntime — so it honestly gains per-tool
|
|
904
|
+
// approvals and resume. Reflect that in the catalog the picker reads.
|
|
905
|
+
const acpActive = prefersAcp(id);
|
|
906
|
+
if (acpActive)
|
|
907
|
+
resume = true;
|
|
908
|
+
// Opt-in self-healing: if the installed binary's --help doesn't evidence a
|
|
909
|
+
// resume/model flag we advertise, downgrade it (never upgrade). Codex keeps its
|
|
910
|
+
// native, separately-verified resume path, so it's exempt.
|
|
911
|
+
if (process.env.BIVY_AGENT_PROBE === "1" && installed && id !== "codex") {
|
|
912
|
+
const help = probeHelpText(spec.command);
|
|
913
|
+
if (help) {
|
|
914
|
+
const refined = refineCapabilitiesFromHelp(help, { resume, modelSelection }, { resumeTokens: resumeTokensFor(id), modelFlag: spec.model?.flag });
|
|
915
|
+
resume = refined.resume;
|
|
916
|
+
modelSelection = refined.modelSelection;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
id,
|
|
921
|
+
displayName: spec.displayName,
|
|
922
|
+
description: spec.blurb ?? `Run the local ${spec.displayName} CLI underneath Bivy in the session workspace.`,
|
|
923
|
+
status: installed ? "available" : "external",
|
|
924
|
+
packageName: spec.packageName,
|
|
925
|
+
language: "Process",
|
|
926
|
+
// MCP tool calls are gated by real approvals when the proxy shim is enabled
|
|
927
|
+
// (BIVY_MCP_PROXY) — an honest, narrower capability than full toolInterception
|
|
928
|
+
// (it governs MCP tools, not the agent's built-in shell/edits). See
|
|
929
|
+
// src/harness/mcp-inject.ts + governMcpCall in src/server.ts.
|
|
930
|
+
capabilities: { toolInterception: acpActive, mcpToolApprovals: acpActive || Boolean(process.env.BIVY_MCP_PROXY), modelSelection, resume, packages: false, fork: false, usageReporting, sessionDiscovery: id === "codex" },
|
|
931
|
+
supportTier: spec.supportTier ?? (id === "codex" ? "supported" : "experimental"),
|
|
932
|
+
authOwner: spec.authOwner ?? "agent",
|
|
933
|
+
notes: installed
|
|
934
|
+
? `Available on PATH. This process adapter ${spec.parserId && !spec.parserUnverified ? "parses its native JSON stream into a structured transcript" : spec.parserId ? "streams stdout/stderr (a structured JSON parser is available; opt in with BIVY_AGENT_STRUCTURED=1 once validated for your version)" : "streams stdout/stderr"}; Bivy governs its filesystem/exec/MCP effects at the sandbox tier rather than intercepting each tool call. Override its launch flags with BIVY_${id.toUpperCase()}_ARGS if your CLI version differs.`
|
|
935
|
+
: `${spec.command} was not found on PATH. Install it on this node, then select this agent again.`,
|
|
936
|
+
install: installed || !installCommand ? undefined : {
|
|
937
|
+
label: `Install ${spec.displayName}`,
|
|
938
|
+
description: `Install ${spec.displayName} on this node now (${installCommand.display}).`,
|
|
939
|
+
command: installCommand.display,
|
|
940
|
+
},
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// "Codex" — the app-server shim runtime (id `codex-approvals`). This is the single
|
|
944
|
+
// Codex we surface: same binary as the plain exec runtime, but driven through the
|
|
945
|
+
// app-server shim so each shell command / file change gets a pre-execution
|
|
946
|
+
// Approve/Deny card via guardianInterceptor, AND it resumes a prior thread by its
|
|
947
|
+
// rollout id (thread/resume). Governed + resumable in one runtime supersedes the
|
|
948
|
+
// exec path, which stays runnable via `BIVY_RUNTIME=codex` for a no-approval flow.
|
|
949
|
+
function codexApprovalsInfo() {
|
|
950
|
+
const installed = commandAvailable("codex");
|
|
951
|
+
return {
|
|
952
|
+
id: "codex-approvals",
|
|
953
|
+
displayName: "Codex",
|
|
954
|
+
description: "Codex driven through its app-server: every shell command or file change it proposes is gated through Bivy's Approve/Deny before it runs (not just the exec jail), and sessions resume with full history.",
|
|
955
|
+
status: installed ? "available" : "external",
|
|
956
|
+
packageName: "codex",
|
|
957
|
+
language: "Process",
|
|
958
|
+
capabilities: {
|
|
959
|
+
toolInterception: true,
|
|
960
|
+
modelSelection: true,
|
|
961
|
+
resume: true,
|
|
962
|
+
packages: false,
|
|
963
|
+
fork: false,
|
|
964
|
+
sessionDiscovery: true,
|
|
965
|
+
// The governed/resumable Codex variant is the one that owns native
|
|
966
|
+
// discovery+adoption (issue #156) — not the plain exec runtime below —
|
|
967
|
+
// so an adopted session gets per-tool approvals from the moment it's
|
|
968
|
+
// imported rather than the ungoverned exec jail.
|
|
969
|
+
nativeSessionDiscovery: true,
|
|
970
|
+
nativeSessionAdoption: true,
|
|
971
|
+
},
|
|
972
|
+
supportTier: "beta",
|
|
973
|
+
authOwner: "agent",
|
|
974
|
+
notes: installed
|
|
975
|
+
? "Drives Codex's experimental app-server so tool calls surface as in-chat approval cards, and resumes a prior thread by its rollout id (thread/resume). Governance AND resume in one runtime."
|
|
976
|
+
: "codex was not found on PATH. Install it on this node, then select this agent again.",
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
async function suggestCodexSessionName(firstPrompt, context) {
|
|
980
|
+
const prompt = firstPrompt.trim();
|
|
981
|
+
if (!prompt)
|
|
982
|
+
return undefined;
|
|
983
|
+
const instruction = [
|
|
984
|
+
"Name this coding-agent session from the user request below.",
|
|
985
|
+
"Return only a concise title of 2-6 words, with no quotes, punctuation, prefix, or explanation.",
|
|
986
|
+
"",
|
|
987
|
+
prompt.slice(0, 4000),
|
|
988
|
+
].join("\n");
|
|
989
|
+
return new Promise((resolve) => {
|
|
990
|
+
const args = ["exec", "--ephemeral", "--json", "--sandbox", "read-only", "--skip-git-repo-check"];
|
|
991
|
+
if (context.model)
|
|
992
|
+
args.push("--model", context.model);
|
|
993
|
+
args.push(instruction);
|
|
994
|
+
const child = spawn(process.env.BIVY_CODEX_BIN || "codex", args, { cwd: context.cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
995
|
+
let stdout = "";
|
|
996
|
+
const timer = setTimeout(() => { child.kill("SIGTERM"); resolve(undefined); }, 60_000);
|
|
997
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
998
|
+
child.on("error", () => { clearTimeout(timer); resolve(undefined); });
|
|
999
|
+
child.on("close", (code) => {
|
|
1000
|
+
clearTimeout(timer);
|
|
1001
|
+
if (code !== 0) {
|
|
1002
|
+
resolve(undefined);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
let text = "";
|
|
1006
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1007
|
+
try {
|
|
1008
|
+
const event = JSON.parse(line);
|
|
1009
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message" && event.item.text)
|
|
1010
|
+
text = event.item.text;
|
|
1011
|
+
}
|
|
1012
|
+
catch { /* ignore non-JSON output */ }
|
|
1013
|
+
}
|
|
1014
|
+
const clean = text.replace(/[\r\n'"`]/g, " ").replace(/\p{Control}/gu, "").replace(/\s+/g, " ").trim().replace(/[.?!,:;–—-]+$/g, "").slice(0, 60).trim();
|
|
1015
|
+
resolve(clean || undefined);
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
// Build the Tier-2 Codex runtime: a ProtocolRuntime driving the app-server shim,
|
|
1020
|
+
// with the concrete agent id so takeover/discovery/UI treat it as its own
|
|
1021
|
+
// selectable Codex variant. Capabilities are seeded (toolInterception up front)
|
|
1022
|
+
// because the daemon decides whether to attach guardianInterceptor from
|
|
1023
|
+
// runtime.capabilities before the shim's hello handshake lands.
|
|
1024
|
+
/**
|
|
1025
|
+
* Catalog-capable runtimes for the unified model catalog — Pi included as one
|
|
1026
|
+
* contributor among equals, not a privileged base. Each runtime's `listCatalog()`
|
|
1027
|
+
* contributes its providers + models, deduped and stamped with the shared vault's
|
|
1028
|
+
* auth status by `aggregateModelCatalog`. Construction is cheap (no session
|
|
1029
|
+
* spawned); listCatalog() is static. Codex is always listed; Claude Code when its
|
|
1030
|
+
* SDK is installed.
|
|
1031
|
+
*/
|
|
1032
|
+
export function catalogRuntimes(credsDir, piDir, sessionsDir) {
|
|
1033
|
+
const runtimes = [
|
|
1034
|
+
new PiRuntime({ credsDir, piDir, sessionsDir }),
|
|
1035
|
+
codexAppServerRuntime(credsDir),
|
|
1036
|
+
];
|
|
1037
|
+
if (claudeSdkInstalled())
|
|
1038
|
+
runtimes.push(new ClaudeCodeRuntime(claudeRuntimeFromEnv()));
|
|
1039
|
+
return runtimes;
|
|
1040
|
+
}
|
|
1041
|
+
// `tier` threads the session's chosen sandbox into the app-server shim as env it
|
|
1042
|
+
// reads at launch (BIVY_CODEX_SANDBOX / BIVY_CODEX_APPROVAL_POLICY), so the
|
|
1043
|
+
// governed Codex runtime is contained at the selected tier — including "full
|
|
1044
|
+
// access" actually disabling the sandbox — instead of the shim's hardcoded
|
|
1045
|
+
// workspace-write default. Absent (the session-less catalog build) leaves the
|
|
1046
|
+
// shim on its own defaults.
|
|
1047
|
+
function codexAppServerRuntime(credsDir, tier) {
|
|
1048
|
+
const shim = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "bin", "codex-app-server-shim.mjs");
|
|
1049
|
+
const policy = tier ? codexSandboxPolicy(tier) : undefined;
|
|
1050
|
+
return new ProtocolRuntime({
|
|
1051
|
+
id: "codex-approvals",
|
|
1052
|
+
displayName: "Codex",
|
|
1053
|
+
command: process.execPath,
|
|
1054
|
+
args: [shim],
|
|
1055
|
+
...(policy ? { env: { BIVY_CODEX_SANDBOX: policy.sandbox, BIVY_CODEX_APPROVAL_POLICY: policy.approvalPolicy } } : {}),
|
|
1056
|
+
credentials: createCredentialStore(credsDir),
|
|
1057
|
+
// Session-less catalog contribution: Codex runs OpenAI models under a ChatGPT
|
|
1058
|
+
// subscription (provider id "openai-codex"). The authoritative per-session
|
|
1059
|
+
// list comes from the app-server; this is the picker preview.
|
|
1060
|
+
catalog: [
|
|
1061
|
+
{
|
|
1062
|
+
id: "openai-codex",
|
|
1063
|
+
name: "OpenAI Codex (ChatGPT)",
|
|
1064
|
+
oauth: true,
|
|
1065
|
+
models: [
|
|
1066
|
+
{ provider: "openai-codex", id: "gpt-5-codex", name: "GPT-5 Codex", reasoning: true },
|
|
1067
|
+
{ provider: "openai-codex", id: "gpt-5", name: "GPT-5", reasoning: true },
|
|
1068
|
+
],
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: true, nativeSessionDiscovery: true, nativeSessionAdoption: true },
|
|
1072
|
+
// Resume: the shim reconnects a prior thread via thread/resume by its rollout
|
|
1073
|
+
// id, and history preloads from the same on-disk rollout the exec path reads —
|
|
1074
|
+
// so takeover/reopen continues a governed session. (Validated on codex-cli
|
|
1075
|
+
// 0.144.1; the app-server threadId == the rollout/session id.)
|
|
1076
|
+
resumable: true,
|
|
1077
|
+
loadHistory: (sessionId) => loadCodexTranscript(sessionId),
|
|
1078
|
+
deleteHistory: (sessionId) => void deleteCodexSession(sessionId),
|
|
1079
|
+
suggestName: suggestCodexSessionName,
|
|
1080
|
+
// Native discovery (issue #156): enumerate Codex rollouts on this node that
|
|
1081
|
+
// Bivy didn't start, so a pre-existing `codex` session can be adopted here
|
|
1082
|
+
// (the governed variant), never the plain exec runtime below.
|
|
1083
|
+
discoverNativeSessions: () => discoverNativeCodexSessions(),
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
// --- #2: the GENERAL ACP adapter (Agent Client Protocol) --------------------
|
|
1087
|
+
// Generalizes the app-server shim pattern to the open ACP standard: any ACP agent
|
|
1088
|
+
// (e.g. `gemini --experimental-acp`) is driven through bin/acp-shim.mjs → the same
|
|
1089
|
+
// ProtocolRuntime that backs Codex approvals, so it gets per-tool Approve/Deny,
|
|
1090
|
+
// streaming, and resume with ZERO per-agent code. Configured as data via
|
|
1091
|
+
// BIVY_ACP_COMMAND / BIVY_ACP_ARGS (mirrors generic-cli / bivy-agent-protocol);
|
|
1092
|
+
// hidden from the picker until validated against a given agent, then promotable
|
|
1093
|
+
// with one catalog edit. Returns null when BIVY_ACP_COMMAND isn't set.
|
|
1094
|
+
/** Absolute path to the ACP bridge shim. */
|
|
1095
|
+
function acpShimPath() {
|
|
1096
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "bin", "acp-shim.mjs");
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Build ProtocolRuntime options that drive an ACP agent (`command` + `agentArgs`)
|
|
1100
|
+
* through bin/acp-shim.mjs. Shared by the generic `acp` runtime and the per-agent
|
|
1101
|
+
* ACP promotion path so both wrap agents identically.
|
|
1102
|
+
*/
|
|
1103
|
+
function acpRuntimeOptions(opts) {
|
|
1104
|
+
return {
|
|
1105
|
+
id: opts.id,
|
|
1106
|
+
displayName: opts.displayName,
|
|
1107
|
+
command: process.execPath,
|
|
1108
|
+
args: [acpShimPath(), "--agent", opts.command, "--", ...opts.agentArgs],
|
|
1109
|
+
// Seed governed+resumable up front so the daemon attaches guardianInterceptor to
|
|
1110
|
+
// the FIRST session (before the shim's hello lands); the hello confirms them.
|
|
1111
|
+
capabilities: { toolInterception: true, resume: true },
|
|
1112
|
+
resumable: true,
|
|
1113
|
+
...(opts.credsDir ? { credentials: createCredentialStore(opts.credsDir) } : {}),
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function acpRuntimeFromEnv(credsDir) {
|
|
1117
|
+
const command = process.env.BIVY_ACP_COMMAND?.trim();
|
|
1118
|
+
if (!command)
|
|
1119
|
+
return null;
|
|
1120
|
+
let agentArgs = [];
|
|
1121
|
+
const rawArgs = process.env.BIVY_ACP_ARGS?.trim();
|
|
1122
|
+
if (rawArgs) {
|
|
1123
|
+
try {
|
|
1124
|
+
const p = JSON.parse(rawArgs);
|
|
1125
|
+
if (Array.isArray(p))
|
|
1126
|
+
agentArgs = p.map(String);
|
|
1127
|
+
}
|
|
1128
|
+
catch { /* ignore malformed */ }
|
|
1129
|
+
}
|
|
1130
|
+
return acpRuntimeOptions({ id: "acp", displayName: process.env.BIVY_ACP_NAME?.trim() || "ACP Agent", command, agentArgs, credsDir });
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Whether a CLI agent should be driven through ACP rather than the one-shot pipe:
|
|
1134
|
+
* it declares an `acp` mode AND ACP is preferred for it (per-agent `BIVY_<ID>_ACP=1`
|
|
1135
|
+
* or global `BIVY_PREFER_ACP=1`). This is the data-driven "promote an agent to the
|
|
1136
|
+
* high-capability path" switch — no per-agent code, just a spec field + a flag.
|
|
1137
|
+
*/
|
|
1138
|
+
function prefersAcp(id) {
|
|
1139
|
+
if (!CLI_AGENT_SPECS[id].acp)
|
|
1140
|
+
return false;
|
|
1141
|
+
return process.env.BIVY_PREFER_ACP === "1" || process.env[`BIVY_${id.toUpperCase()}_ACP`] === "1";
|
|
1142
|
+
}
|
|
1143
|
+
function acpInfo() {
|
|
1144
|
+
const configured = Boolean(process.env.BIVY_ACP_COMMAND?.trim());
|
|
1145
|
+
return {
|
|
1146
|
+
id: "acp",
|
|
1147
|
+
displayName: process.env.BIVY_ACP_NAME?.trim() || "ACP Agent",
|
|
1148
|
+
description: "Any Agent Client Protocol (ACP) agent, driven through Bivy's shim for per-tool approvals, streaming, and resume.",
|
|
1149
|
+
status: configured ? "available" : "planned",
|
|
1150
|
+
packageName: process.env.BIVY_ACP_COMMAND?.trim() || "Set BIVY_ACP_COMMAND",
|
|
1151
|
+
language: "Process",
|
|
1152
|
+
capabilities: { toolInterception: true, modelSelection: false, resume: true, packages: false, fork: false },
|
|
1153
|
+
supportTier: "experimental",
|
|
1154
|
+
authOwner: "agent",
|
|
1155
|
+
notes: configured
|
|
1156
|
+
? "Drives an ACP agent via bin/acp-shim.mjs → ProtocolRuntime: per-tool Approve/Deny, streaming transcript, and session/load resume — no per-agent code. Validate against your agent, then promote it into the picker as data."
|
|
1157
|
+
: "Set BIVY_ACP_COMMAND (and optional BIVY_ACP_ARGS, a JSON array) to the ACP agent's launch command, e.g. BIVY_ACP_COMMAND=gemini BIVY_ACP_ARGS='[\"--experimental-acp\"]'.",
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function splitEnvArgs(value, fallback) {
|
|
1161
|
+
if (!value?.trim())
|
|
1162
|
+
return fallback;
|
|
1163
|
+
try {
|
|
1164
|
+
const parsed = JSON.parse(value);
|
|
1165
|
+
if (Array.isArray(parsed))
|
|
1166
|
+
return parsed.map(String);
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
// fall through to a small shell-like splitter
|
|
1170
|
+
}
|
|
1171
|
+
const out = [];
|
|
1172
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
1173
|
+
let match;
|
|
1174
|
+
while ((match = re.exec(value)))
|
|
1175
|
+
out.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
1176
|
+
return out;
|
|
1177
|
+
}
|
|
1178
|
+
function openClawProcessOptions() {
|
|
1179
|
+
const command = process.env.BIVY_OPENCLAW_COMMAND?.trim() || "openclaw";
|
|
1180
|
+
const args = splitEnvArgs(process.env.BIVY_OPENCLAW_ARGS, ["agent", "--message"]);
|
|
1181
|
+
const agent = process.env.BIVY_OPENCLAW_AGENT?.trim();
|
|
1182
|
+
const agentFlagIndex = args.indexOf("--message");
|
|
1183
|
+
const argsWithAgent = !agent ? args : agentFlagIndex >= 0
|
|
1184
|
+
? [...args.slice(0, agentFlagIndex), "--agent", agent, ...args.slice(agentFlagIndex)]
|
|
1185
|
+
: [...args, "--agent", agent];
|
|
1186
|
+
return {
|
|
1187
|
+
id: "openclaw",
|
|
1188
|
+
displayName: agent ? `OpenClaw (${agent})` : "OpenClaw",
|
|
1189
|
+
command,
|
|
1190
|
+
args: argsWithAgent,
|
|
1191
|
+
promptMode: "argv",
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function openClawInfo() {
|
|
1195
|
+
const options = openClawProcessOptions();
|
|
1196
|
+
const installed = commandAvailable(options.command);
|
|
1197
|
+
const npmPrefix = process.env.BIVY_NPM_GLOBAL_PREFIX || "~/.local";
|
|
1198
|
+
const installCommand = `npm install --global --prefix ${npmPrefix} openclaw`;
|
|
1199
|
+
return {
|
|
1200
|
+
id: "openclaw",
|
|
1201
|
+
displayName: options.displayName,
|
|
1202
|
+
description: "Run the local OpenClaw CLI underneath Bivy in the session workspace.",
|
|
1203
|
+
status: installed ? "available" : "external",
|
|
1204
|
+
packageName: "openclaw",
|
|
1205
|
+
language: "Process",
|
|
1206
|
+
capabilities: { toolInterception: false, modelSelection: false, resume: false, packages: false, fork: false },
|
|
1207
|
+
supportTier: "experimental",
|
|
1208
|
+
authOwner: "agent",
|
|
1209
|
+
notes: installed
|
|
1210
|
+
? "Available on PATH. This phase-1 CLI adapter streams stdout/stderr only; Gateway RPC and structured tool approvals require a future OpenClaw protocol bridge. Configure with BIVY_OPENCLAW_COMMAND, BIVY_OPENCLAW_ARGS, and BIVY_OPENCLAW_AGENT."
|
|
1211
|
+
: `${options.command} was not found on PATH. Install OpenClaw on this node, use the PWA install button, or set BIVY_OPENCLAW_COMMAND to its CLI path.`,
|
|
1212
|
+
install: installed ? undefined : {
|
|
1213
|
+
label: "Install OpenClaw",
|
|
1214
|
+
description: `Install the OpenClaw CLI on this node now (${installCommand}).`,
|
|
1215
|
+
command: installCommand,
|
|
1216
|
+
},
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function protocolInfo() {
|
|
1220
|
+
const configured = Boolean(protocolRuntimeFromEnv());
|
|
1221
|
+
// Surface any BIVY_PROTOCOL_COMMANDS-seeded slash commands in the catalog so
|
|
1222
|
+
// the composer can offer them in autocomplete before the first session's hello
|
|
1223
|
+
// (discovery reads this RuntimeInfo, not the live runtime's refined caps).
|
|
1224
|
+
const commands = protocolCommandsFromEnv();
|
|
1225
|
+
return {
|
|
1226
|
+
id: "bivy-agent-protocol",
|
|
1227
|
+
displayName: process.env.BIVY_PROTOCOL_NAME?.trim() || "Bivy Protocol",
|
|
1228
|
+
description: "JSON-lines process protocol for any agent to expose structured events, tool calls, approvals, models, and sessions without a bespoke Bivy adapter.",
|
|
1229
|
+
status: configured ? "available" : "planned",
|
|
1230
|
+
packageName: process.env.BIVY_PROTOCOL_COMMAND?.trim() || "stdio/jsonl",
|
|
1231
|
+
language: "Any",
|
|
1232
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: true, packages: false, fork: false, ...(commands ? { commands } : {}) },
|
|
1233
|
+
supportTier: "experimental",
|
|
1234
|
+
authOwner: "mixed",
|
|
1235
|
+
notes: configured
|
|
1236
|
+
? "Configured through BIVY_PROTOCOL_COMMAND / BIVY_PROTOCOL_ARGS. Advertise agent-native slash commands with BIVY_PROTOCOL_COMMANDS (JSON [{name,description}]); other capability flags are finalized by the agent handshake."
|
|
1237
|
+
: "Set BIVY_PROTOCOL_COMMAND to enable a JSONL Bivy Agent Protocol runtime.",
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
export const RUNTIME_CATALOG = [
|
|
1241
|
+
{
|
|
1242
|
+
id: "pi",
|
|
1243
|
+
displayName: "Pi",
|
|
1244
|
+
description: "Native Bivy/Pi coding agent runtime with packages, approvals, and model picker.",
|
|
1245
|
+
status: "available",
|
|
1246
|
+
packageName: "@earendil-works/pi-coding-agent",
|
|
1247
|
+
language: "TypeScript",
|
|
1248
|
+
capabilities: PI_CAPABILITIES,
|
|
1249
|
+
supportTier: "supported",
|
|
1250
|
+
authOwner: "bivy",
|
|
1251
|
+
},
|
|
1252
|
+
genericCliInfo(),
|
|
1253
|
+
// `codex` sits before the governed shim it feeds; the rest of the CLI agents are
|
|
1254
|
+
// derived straight from CLI_AGENT_SPECS (adding a spec = one data edit, no list
|
|
1255
|
+
// to keep in sync here).
|
|
1256
|
+
cliAgentInfo("codex"),
|
|
1257
|
+
codexApprovalsInfo(),
|
|
1258
|
+
...CLI_AGENT_IDS.filter((id) => id !== "codex").map(cliAgentInfo),
|
|
1259
|
+
openClawInfo(),
|
|
1260
|
+
claudeCodeInfo(),
|
|
1261
|
+
protocolInfo(),
|
|
1262
|
+
acpInfo(),
|
|
1263
|
+
{
|
|
1264
|
+
id: "openhands",
|
|
1265
|
+
displayName: "OpenHands",
|
|
1266
|
+
description: "Open-source autonomous software engineering agent, usually run as an app/server with sandboxed execution.",
|
|
1267
|
+
status: "planned",
|
|
1268
|
+
packageName: "openhands-ai/openhands",
|
|
1269
|
+
language: "Python",
|
|
1270
|
+
capabilities: { toolInterception: false, modelSelection: true, resume: true, packages: false, fork: false },
|
|
1271
|
+
supportTier: "planned",
|
|
1272
|
+
authOwner: "agent",
|
|
1273
|
+
notes: "Likely needs a server/protocol adapter rather than the generic CLI path so Bivy can map tasks, logs, files, and approvals cleanly.",
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
id: "swe-agent",
|
|
1277
|
+
displayName: "SWE-agent",
|
|
1278
|
+
description: "Batch/task-oriented open-source software engineering agent for issue-to-patch workflows.",
|
|
1279
|
+
status: "planned",
|
|
1280
|
+
packageName: "swe-agent",
|
|
1281
|
+
language: "Python",
|
|
1282
|
+
capabilities: { toolInterception: false, modelSelection: true, resume: false, packages: false, fork: false },
|
|
1283
|
+
supportTier: "planned",
|
|
1284
|
+
authOwner: "agent",
|
|
1285
|
+
notes: "Strong fit for GitHub issue queue runs; likely exposed as a task runner/protocol adapter rather than conversational chat.",
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
id: "openai-agents-sdk",
|
|
1289
|
+
displayName: "OpenAI Agents SDK",
|
|
1290
|
+
description: "OpenAI's agent framework with tools, handoffs, guardrails, and tracing.",
|
|
1291
|
+
status: "planned",
|
|
1292
|
+
packageName: "@openai/agents",
|
|
1293
|
+
language: "TypeScript/Python",
|
|
1294
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: false, packages: false, fork: false },
|
|
1295
|
+
supportTier: "planned",
|
|
1296
|
+
authOwner: "mixed",
|
|
1297
|
+
notes: "Good candidate for non-coding workflows; needs a coding-tool bundle to match Pi/Claude Code behavior.",
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
id: "langgraph",
|
|
1301
|
+
displayName: "LangGraph",
|
|
1302
|
+
description: "Stateful agent graph runtime from LangChain for durable multi-step workflows.",
|
|
1303
|
+
status: "planned",
|
|
1304
|
+
packageName: "@langchain/langgraph",
|
|
1305
|
+
language: "TypeScript/Python",
|
|
1306
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: true, packages: false, fork: true },
|
|
1307
|
+
supportTier: "planned",
|
|
1308
|
+
authOwner: "mixed",
|
|
1309
|
+
notes: "Strong for custom orchestrations; coding-agent semantics would be defined by our graph and tools.",
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
id: "google-adk",
|
|
1313
|
+
displayName: "Google ADK",
|
|
1314
|
+
description: "Google Agent Development Kit for Gemini-oriented agents.",
|
|
1315
|
+
status: "planned",
|
|
1316
|
+
packageName: "google-adk",
|
|
1317
|
+
language: "Python",
|
|
1318
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: false, packages: false, fork: false },
|
|
1319
|
+
supportTier: "planned",
|
|
1320
|
+
authOwner: "mixed",
|
|
1321
|
+
notes: "Likely via a sidecar process/RPC adapter because the primary SDK is Python."
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
id: "autogen",
|
|
1325
|
+
displayName: "AutoGen",
|
|
1326
|
+
description: "Microsoft's multi-agent conversation framework.",
|
|
1327
|
+
status: "planned",
|
|
1328
|
+
packageName: "autogen-agentchat",
|
|
1329
|
+
language: "Python/.NET",
|
|
1330
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: false, packages: false, fork: false },
|
|
1331
|
+
supportTier: "planned",
|
|
1332
|
+
authOwner: "mixed",
|
|
1333
|
+
notes: "Best suited for multi-agent workflows; use a sidecar adapter for Bivy.",
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
id: "crew-ai",
|
|
1337
|
+
displayName: "CrewAI",
|
|
1338
|
+
description: "Python framework for role-based multi-agent task crews.",
|
|
1339
|
+
status: "planned",
|
|
1340
|
+
packageName: "crewai",
|
|
1341
|
+
language: "Python",
|
|
1342
|
+
capabilities: { toolInterception: true, modelSelection: true, resume: false, packages: false, fork: false },
|
|
1343
|
+
supportTier: "planned",
|
|
1344
|
+
authOwner: "mixed",
|
|
1345
|
+
notes: "Use for workflow/crew tasks rather than low-latency coding sessions; sidecar adapter recommended.",
|
|
1346
|
+
},
|
|
1347
|
+
];
|
|
1348
|
+
// Agents Bivy fully integrates today and therefore shows in the agent picker —
|
|
1349
|
+
// the most-used coding agents, all driven through Bivy's general paths (the
|
|
1350
|
+
// native Pi/Claude runtimes, the Codex app-server shim, and the data-driven CLI
|
|
1351
|
+
// ProcessRuntime + CliParser path) rather than bespoke per-agent code:
|
|
1352
|
+
//
|
|
1353
|
+
// pi, claude-code-sdk — native runtimes (approvals, models, resume)
|
|
1354
|
+
// codex-approvals — Codex via the app-server shim (approvals + resume)
|
|
1355
|
+
// opencode, gemini, qwen, — CLI agents on the shared ProcessRuntime path:
|
|
1356
|
+
// goose, aider, cline, crush, structured streaming (JSON parser where the CLI
|
|
1357
|
+
// cursor, copilot, grok, amp, has one), effect-level governance (sandbox tier /
|
|
1358
|
+
// auggie, droid, continue, FS-MCP-network channels), honest capabilities
|
|
1359
|
+
// kilocode, rovodev (resume/model advertised only where the CLI
|
|
1360
|
+
// actually drives it).
|
|
1361
|
+
//
|
|
1362
|
+
// "Codex" here is the app-server *shim* runtime (`codex-approvals`): governed
|
|
1363
|
+
// (per-tool Approve/Deny) AND resumable (thread/resume by rollout id), which
|
|
1364
|
+
// strictly supersedes the plain `codex` exec runtime; that exec path stays
|
|
1365
|
+
// runnable via `BIVY_RUNTIME=codex` for the fast, no-approval flow.
|
|
1366
|
+
//
|
|
1367
|
+
// Everything else in RUNTIME_CATALOG is an extension hook that only works once
|
|
1368
|
+
// configured via env (generic-cli, bivy-agent-protocol), a niche/phase-1 adapter
|
|
1369
|
+
// (hermes, openclaw), or an aspirational "planned" placeholder with no adapter.
|
|
1370
|
+
// They are hidden from the picker to keep it honest, but remain fully runnable via
|
|
1371
|
+
// `BIVY_RUNTIME=<id>`. To promote a CLI agent into the picker, give it honest
|
|
1372
|
+
// capabilities (no silently-no-op pickers) and drop its `hidden: true` flag in
|
|
1373
|
+
// CLI_AGENT_SPECS — the picker set below derives from that one field.
|
|
1374
|
+
// See docs/agents-not-fully-supported.md for the rationale and the promotion path.
|
|
1375
|
+
//
|
|
1376
|
+
// The picker = the native/shim runtimes that aren't CLI-agent specs, PLUS every
|
|
1377
|
+
// non-hidden CLI agent. Visibility lives on the spec (`hidden`), so promoting or
|
|
1378
|
+
// demoting an agent is a single data edit with no id list to drift.
|
|
1379
|
+
const NON_CLI_PICKER_IDS = ["pi", "claude-code-sdk", "codex-approvals"];
|
|
1380
|
+
const PICKER_RUNTIME_IDS = new Set([
|
|
1381
|
+
...NON_CLI_PICKER_IDS,
|
|
1382
|
+
...CLI_AGENT_IDS.filter((id) => !CLI_AGENT_SPECS[id].hidden),
|
|
1383
|
+
]);
|
|
1384
|
+
export function listRuntimes(currentId) {
|
|
1385
|
+
return RUNTIME_CATALOG
|
|
1386
|
+
// Keep the current runtime visible even if hidden, so a session pinned to a
|
|
1387
|
+
// hidden agent (e.g. someone running BIVY_RUNTIME=goose) still renders its
|
|
1388
|
+
// selection instead of showing an empty picker.
|
|
1389
|
+
.filter((runtime) => PICKER_RUNTIME_IDS.has(runtime.id) || runtime.id === currentId)
|
|
1390
|
+
.map((runtime) => {
|
|
1391
|
+
if (runtime.id === "generic-cli")
|
|
1392
|
+
return genericCliInfo();
|
|
1393
|
+
if (isCliAgentId(runtime.id))
|
|
1394
|
+
return cliAgentInfo(runtime.id);
|
|
1395
|
+
if (runtime.id === "codex-approvals")
|
|
1396
|
+
return codexApprovalsInfo();
|
|
1397
|
+
if (runtime.id === "openclaw")
|
|
1398
|
+
return openClawInfo();
|
|
1399
|
+
if (runtime.id === "claude-code-sdk")
|
|
1400
|
+
return claudeCodeInfo();
|
|
1401
|
+
if (runtime.id === "bivy-agent-protocol")
|
|
1402
|
+
return protocolInfo();
|
|
1403
|
+
if (runtime.id === "acp")
|
|
1404
|
+
return acpInfo();
|
|
1405
|
+
return runtime;
|
|
1406
|
+
}).map((runtime) => ({ ...runtime, current: runtime.id === currentId }));
|
|
1407
|
+
}
|
|
1408
|
+
export function makeRuntime(options) {
|
|
1409
|
+
const id = (options.runtime ?? process.env.BIVY_RUNTIME ?? "pi").toLowerCase();
|
|
1410
|
+
switch (id) {
|
|
1411
|
+
case "pi":
|
|
1412
|
+
return new PiRuntime(options);
|
|
1413
|
+
case "generic-cli": {
|
|
1414
|
+
const processOptions = processRuntimeFromEnv();
|
|
1415
|
+
if (!processOptions)
|
|
1416
|
+
throw new Error("generic-cli requires BIVY_AGENT_COMMAND to be set.");
|
|
1417
|
+
// Share the node's provider logins (the shared vault) so the CLI agent finds
|
|
1418
|
+
// whatever model key it needs without a separate per-agent sign-in.
|
|
1419
|
+
// BIVY_AGENT_PARSER opts this agent into Phase 4 structured mode (fidelity).
|
|
1420
|
+
return new ProcessRuntime({ ...processOptions, credentials: createCredentialStore(options.credsDir), parserFactory: parserFactoryFor(process.env.BIVY_AGENT_PARSER) });
|
|
1421
|
+
}
|
|
1422
|
+
case "codex-approvals": {
|
|
1423
|
+
// Tier 2, per-session: the user picked governed Codex from the agent picker.
|
|
1424
|
+
// Drives Codex's app-server through the bivy-agent-protocol shim so each
|
|
1425
|
+
// shell/patch the model proposes becomes a pre-execution Approve/Deny card
|
|
1426
|
+
// via guardianInterceptor — not just the effect-level exec jail.
|
|
1427
|
+
if (!commandAvailable("codex"))
|
|
1428
|
+
throw new Error("Codex command not found on PATH: codex");
|
|
1429
|
+
return codexAppServerRuntime(options.credsDir, sandboxTier(options.sandbox));
|
|
1430
|
+
}
|
|
1431
|
+
case "openclaw": {
|
|
1432
|
+
const openClawOptions = openClawProcessOptions();
|
|
1433
|
+
if (!commandAvailable(openClawOptions.command))
|
|
1434
|
+
throw new Error(`OpenClaw command not found on PATH: ${openClawOptions.command}`);
|
|
1435
|
+
// OpenClaw owns its own auth profiles by default; Bivy only supervises the
|
|
1436
|
+
// local CLI process in this phase-1 adapter.
|
|
1437
|
+
return new ProcessRuntime(openClawOptions);
|
|
1438
|
+
}
|
|
1439
|
+
case "bivy-agent-protocol": {
|
|
1440
|
+
const protocolOptions = protocolRuntimeFromEnv();
|
|
1441
|
+
if (!protocolOptions)
|
|
1442
|
+
throw new Error("bivy-agent-protocol requires BIVY_PROTOCOL_COMMAND to be set.");
|
|
1443
|
+
return new ProtocolRuntime({ ...protocolOptions, credentials: createCredentialStore(options.credsDir) });
|
|
1444
|
+
}
|
|
1445
|
+
case "acp": {
|
|
1446
|
+
const acpOptions = acpRuntimeFromEnv(options.credsDir);
|
|
1447
|
+
if (!acpOptions)
|
|
1448
|
+
throw new Error("acp requires BIVY_ACP_COMMAND to be set (the ACP agent's launch command, e.g. gemini).");
|
|
1449
|
+
return new ProtocolRuntime(acpOptions);
|
|
1450
|
+
}
|
|
1451
|
+
case "claude":
|
|
1452
|
+
case "claude-code":
|
|
1453
|
+
case "claude-code-sdk":
|
|
1454
|
+
// Share the node's provider logins (the shared vault) so the user doesn't
|
|
1455
|
+
// re-auth Anthropic for this agent.
|
|
1456
|
+
return new ClaudeCodeRuntime({ ...claudeRuntimeFromEnv(), credentials: createCredentialStore(options.credsDir), sandbox: options.sandbox });
|
|
1457
|
+
default:
|
|
1458
|
+
// Every CLI agent in CLI_AGENT_SPECS is dispatched here as data — no per-id
|
|
1459
|
+
// case to maintain. Anything that isn't a known CLI agent throws below.
|
|
1460
|
+
if (isCliAgentId(id))
|
|
1461
|
+
return makeCliRuntime(id, options);
|
|
1462
|
+
throw new Error(`Unknown or unavailable BIVY_RUNTIME "${id}". Available runtimes: pi, openclaw/codex/opencode/aider/hermes/goose/gemini/qwen/cline/crush/cursor/copilot/grok/amp/auggie/droid/continue/kilocode/rovodev/codebuff (when their CLI is installed), generic-cli (when BIVY_AGENT_COMMAND is set), claude-code-sdk (when @anthropic-ai/claude-agent-sdk is installed).`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Build a ProcessRuntime for any CLI agent from its CLI_AGENT_SPECS entry — the
|
|
1467
|
+
* single data-driven launch path (structured JSON mode where a parser exists,
|
|
1468
|
+
* effect-level governance, generic resume). Extracted from the makeRuntime switch
|
|
1469
|
+
* so adding an agent stays a pure-data change.
|
|
1470
|
+
*/
|
|
1471
|
+
function makeCliRuntime(id, options) {
|
|
1472
|
+
const spec = CLI_AGENT_SPECS[id];
|
|
1473
|
+
if (!commandAvailable(spec.command))
|
|
1474
|
+
throw new Error(`${spec.displayName} command not found on PATH: ${spec.command}`);
|
|
1475
|
+
// ACP promotion: when the agent declares an `acp` mode and it's preferred
|
|
1476
|
+
// (BIVY_<ID>_ACP=1 / BIVY_PREFER_ACP=1), drive it through the governed
|
|
1477
|
+
// ProtocolRuntime (per-tool approvals + streaming + resume) instead of the
|
|
1478
|
+
// one-shot pipe below — the high-capability path, selected as data.
|
|
1479
|
+
if (spec.acp && prefersAcp(id)) {
|
|
1480
|
+
return new ProtocolRuntime(acpRuntimeOptions({ id, displayName: spec.displayName, command: spec.command, agentArgs: spec.acp.args, credsDir: options.credsDir }));
|
|
1481
|
+
}
|
|
1482
|
+
// Phase 4 — structured mode ON by default when the agent has a VALIDATED JSON
|
|
1483
|
+
// parser: launch with its native JSON flags and parse stdout into normalized
|
|
1484
|
+
// events. BIVY_AGENT_STRUCTURED=0 forces the dumb-pipe fallback everywhere;
|
|
1485
|
+
// BIVY_AGENT_STRUCTURED=1 opts INTO structured mode for agents whose parser
|
|
1486
|
+
// is still unverified (spec.parserUnverified — safe default is dumb pipe so a
|
|
1487
|
+
// wrong flag can't regress a working agent). BIVY_AGENT_PARSER overrides the
|
|
1488
|
+
// parser id (e.g. to "bivy-protocol").
|
|
1489
|
+
const structuredPref = process.env.BIVY_AGENT_STRUCTURED;
|
|
1490
|
+
const parserReady = Boolean(spec.parserId) && (!spec.parserUnverified || structuredPref === "1");
|
|
1491
|
+
const structured = parserReady && structuredPref !== "0";
|
|
1492
|
+
const parserId = process.env.BIVY_AGENT_PARSER || (structured ? spec.parserId : undefined);
|
|
1493
|
+
const tier = sandboxTier(options.sandbox);
|
|
1494
|
+
// BIVY_<ID>_ARGS overrides the launch flags for a CLI version we haven't
|
|
1495
|
+
// pinned; else composeArgs (native sandbox) wins; else structured jsonArgs;
|
|
1496
|
+
// else the plain args.
|
|
1497
|
+
const runArgs = cliArgsOverride(id)
|
|
1498
|
+
?? (spec.composeArgs
|
|
1499
|
+
? spec.composeArgs({ structured, tier })
|
|
1500
|
+
: structured && spec.jsonArgs
|
|
1501
|
+
? spec.jsonArgs
|
|
1502
|
+
: spec.args);
|
|
1503
|
+
// Codex reads OPENAI_API_KEY or its own `$CODEX_HOME/auth.json`. When the
|
|
1504
|
+
// user connected a ChatGPT/Codex subscription in Bivy (but hasn't run
|
|
1505
|
+
// `codex login`), `prepare` mints that auth file from the shared vault so
|
|
1506
|
+
// the run just works; the preflight still catches the genuinely
|
|
1507
|
+
// uncredentialed case with an actionable message instead of an opaque 401.
|
|
1508
|
+
const preflight = id === "codex"
|
|
1509
|
+
? (env) => codexCredentialPreflight(env)
|
|
1510
|
+
: id === "opencode"
|
|
1511
|
+
? (env, ctx) => opencodeCredentialPreflight(env, ctx)
|
|
1512
|
+
: undefined;
|
|
1513
|
+
const prepare = id === "codex"
|
|
1514
|
+
? async () => {
|
|
1515
|
+
const home = await ensureCodexAuth(options.credsDir);
|
|
1516
|
+
return home ? { CODEX_HOME: home } : {};
|
|
1517
|
+
}
|
|
1518
|
+
: undefined;
|
|
1519
|
+
// Resume, the generic way. Codex keeps its verified path (rollout history +
|
|
1520
|
+
// tier-aware `codex exec resume <id> --json`). Every other CLI agent becomes
|
|
1521
|
+
// resumable purely as data: a spec.resume template (or a BIVY_<ID>_RESUME_
|
|
1522
|
+
// TEMPLATE override) whose {id}/{tier}/{sandbox} placeholders are filled per
|
|
1523
|
+
// prompt — no per-agent code. Absent = fresh process per prompt (resume
|
|
1524
|
+
// stays off; see the per-agent comments in CLI_AGENT_SPECS for why some
|
|
1525
|
+
// genuinely have no native "continue session <id>" form).
|
|
1526
|
+
const resumeTemplate = id === "codex" ? undefined : cliResumeTemplate(id);
|
|
1527
|
+
const resumeOpts = id === "codex"
|
|
1528
|
+
? {
|
|
1529
|
+
resumable: true,
|
|
1530
|
+
loadHistory: (sessionId) => loadCodexTranscript(sessionId),
|
|
1531
|
+
deleteHistory: (sessionId) => void deleteCodexSession(sessionId),
|
|
1532
|
+
resumeArgs: (sessionId) => codexResumeArgs(sessionId, tier),
|
|
1533
|
+
}
|
|
1534
|
+
: resumeTemplate
|
|
1535
|
+
? {
|
|
1536
|
+
resumable: true,
|
|
1537
|
+
loadHistory: spec.resume?.loadHistory,
|
|
1538
|
+
// `{sandbox}` expands to that agent's native containment flags for
|
|
1539
|
+
// the tier (e.g. Gemini/Qwen's `--approval-mode <mode>`) — a whole
|
|
1540
|
+
// token, not a string substitution, since it can be multiple argv
|
|
1541
|
+
// words; `{id}`/`{tier}` stay plain per-token string replacement.
|
|
1542
|
+
resumeArgs: (sessionId) => resumeTemplate.flatMap((a) => a === "{sandbox}"
|
|
1543
|
+
? sandboxArgsFor(id, tier)
|
|
1544
|
+
: [a.replace(/\{id\}/g, sessionId).replace(/\{tier\}/g, tier)]),
|
|
1545
|
+
}
|
|
1546
|
+
: {};
|
|
1547
|
+
return new ProcessRuntime({ id, displayName: spec.displayName, command: spec.command, args: runArgs, promptMode: spec.promptMode, credentials: createCredentialStore(options.credsDir), parserFactory: parserFactoryFor(parserId), preflight, prepare, model: cliModelConfig(id), thinking: cliThinkingConfig(id), usageReporting: cliUsageReporting(id), ...resumeOpts });
|
|
1548
|
+
}
|