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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // The `bivy` command table: one row per top-level command. bin/bivy.mjs
4
+ // dispatches only commands found here, and `bivy help`, `bivy help --json`,
5
+ // shell completions, "did you mean" and `pnpm run check:cli` (which keeps
6
+ // docs/cli-reference.md in step) all read these rows. Adding a command means
7
+ // adding a row; a command without one cannot be dispatched.
8
+ //
9
+ // Fields:
10
+ // name canonical name (what the dispatcher switches on)
11
+ // aliases other spellings that dispatch to it
12
+ // group help section (GROUPS below, in display order)
13
+ // usage one-line synopsis after `bivy`
14
+ // summary what it does, one line
15
+ // scope "session" (acts on $BIVY_SESSION_ID), "node" (this machine) or "account"
16
+ // json true when it accepts --json (or honors BIVY_OUTPUT=json)
17
+ // subcommands first-argument verbs, for completion and discovery
18
+ // hidden plumbing launched by Bivy itself; left out of help and completions
19
+ // tools MCP tools `bivy mcp-serve` offers for this command. Each maps
20
+ // its input to CLI words (`argv`, below) and runs `bivy … --json`,
21
+ // so a tool behaves exactly like the command. Only session
22
+ // commands that are safe to call routinely get one; delegation
23
+ // and Runs stay shell-only on purpose (docs/agent-delegation.md).
24
+ //
25
+ // A tool's `argv` is data: a literal word, {arg} (a positional value),
26
+ // {flag, from} (a flag with a value, repeated for arrays, joined with `join`),
27
+ // or {when, flag} (a bare switch when the input is true).
28
+
29
+ /** Exit codes shared by the commands agents call. 0 and 1 hold everywhere. */
30
+ export const EXIT = Object.freeze({ ok: 0, failed: 1, usage: 2, notFound: 3, denied: 4, timeout: 5, conflict: 6, unavailable: 75 });
31
+
32
+ export const GROUPS = [
33
+ ["session", "Inside an agent session (defaults to $BIVY_SESSION_ID)"],
34
+ ["sessions", "Agents and sessions"],
35
+ ["runs", "Runs and automations"],
36
+ ["node", "This node"],
37
+ ["accounts", "Accounts, providers and credentials"],
38
+ ["config", "Configuration and extensions"],
39
+ ["help", "Help and discovery"],
40
+ ];
41
+
42
+ export const COMMANDS = [
43
+ // Inside an agent session
44
+ { name: "context", group: "session", scope: "session", json: true, usage: "context [--json]", summary: "Where this agent is running: session, workspace, machine, apps, and what to run next",
45
+ tools: [{ name: "bivy_context", description: "Where you are running inside Bivy: your session, agent, workspace and git branch, the machine, apps you have published, whether the user has the app open, and the commands that reach them.", input: {}, argv: ["context"] }] },
46
+ { name: "attach", group: "session", scope: "session", json: true, usage: 'attach <file> [--caption "…"] [--artifact]', summary: "Show a local file or image to the user in the chat" },
47
+ { name: "notify", group: "session", scope: "session", json: true, usage: 'notify "<message>" [--urgent]', summary: "Message the user: a card in the chat, and a push when they're away",
48
+ tools: [{ name: "notify_user", description: "Send the user a message: a card in the chat, and a push to their phone when nobody has the app open (the push names the session, not the text). Use it when long work finishes, you are blocked, or something needs their attention. At most one push a minute per session.", input: { message: { type: "string", description: "What to tell the user.", required: true }, urgent: { type: "boolean", description: "Push even while the user has the app open." } }, argv: ["notify", { arg: "message" }, { when: "urgent", flag: "--urgent" }] }] },
49
+ { name: "ask", group: "session", scope: "session", json: true, usage: 'ask "<question>" [--option A --option B] [--async]', summary: "Ask the user a question and wait for the answer", subcommands: ["status", "wait"],
50
+ tools: [{ name: "ask_user", description: "Ask the user a question in the chat and wait for their answer (their phone gets a push). Offer 2-8 options, or none for a free-text answer; they can always write their own. Returns {status, answer}: status is answered, dismissed (decide yourself) or expired.", input: { question: { type: "string", description: "The question.", required: true }, options: { type: "array", items: { type: "string" }, description: "Choices to pick from (2-8)." }, multiple: { type: "boolean", description: "Allow several choices." }, header: { type: "string", description: "A short label for the card." }, timeout_seconds: { type: "number", description: "How long to wait (default 600)." } }, argv: ["ask", { arg: "question" }, { flag: "--option", from: "options" }, { when: "multiple", flag: "--multi" }, { flag: "--header", from: "header" }, { flag: "--timeout", from: "timeout_seconds" }] }] },
51
+ { name: "suggest", group: "session", scope: "session", json: true, usage: 'suggest "<task>" [--title "label"]', summary: "Propose a task the user can start in one tap",
52
+ tools: [{ name: "suggest_task", description: "Propose a next step the user can start in one tap, in this session or in a parallel one with its own copy of the project. Write the task as a complete instruction with paths relative to the project root. Post one per idea instead of listing them.", input: { task: { type: "string", description: "The complete instruction.", required: true }, title: { type: "string", description: "A short label for the card." } }, argv: ["suggest", { arg: "task" }, { flag: "--title", from: "title" }] }] },
53
+ { name: "app", group: "session", scope: "session", json: true, usage: "app <publish|shot|present|share|run|…>", summary: "Live previews: publish a web/terminal/desktop app, screenshot it, present it", subcommands: ["publish", "list", "remove", "shot", "present", "share", "notes", "run", "click", "type", "key", "scroll", "drag", "move", "menu"],
54
+ tools: [
55
+ { name: "app_publish", description: "Give the user a live preview of something with a UI, from an app manifest file (web server port, static build, terminal, or desktop app). Run `bivy app --help` for the manifest format.", input: { manifest_path: { type: "string", description: "Path to the manifest JSON.", required: true } }, argv: ["app", "publish", { arg: "manifest_path" }] },
56
+ { name: "app_screenshot", description: "Screenshot this session's web views (or one app) and return the PNG paths, to check your work before saying a UI change is done.", input: { app: { type: "string", description: "App ID or name (default: all web views)." }, widths: { type: "array", items: { type: "number" }, description: "Viewport widths, e.g. [390, 1280]." }, path: { type: "string", description: "Page path, e.g. /settings." } }, argv: ["app", "shot", { arg: "app" }, { flag: "--widths", from: "widths", join: "," }, { flag: "--path", from: "path" }] },
57
+ { name: "app_present", description: "Tell the user a visible change is ready: a card in the chat with the app at phone width (before and after), and the preview opens.", input: { view: { type: "string", description: "App or view name or ID (default: the one opened last)." }, note: { type: "string", description: "What changed." }, path: { type: "string", description: "Page path to show." } }, argv: ["app", "present", { arg: "view" }, { flag: "--note", from: "note" }, { flag: "--path", from: "path" }] },
58
+ ] },
59
+ { name: "delegate", group: "session", scope: "session", json: true, usage: 'delegate "<task>" [--agent <id>] [--machine <name>] [--wait]', summary: "Hand a task to another agent or machine and get its answer back", subcommands: ["machines", "status", "wait"] },
60
+
61
+ // Agents and sessions
62
+ { name: "run", group: "sessions", scope: "node", usage: "run <agent> [--chat] [--name …] [--model …] [--node …] [--workspace …]", summary: "Run an agent (native CLI/TUI) as a session the app can see" },
63
+ { name: "exec", group: "sessions", scope: "node", json: true, usage: 'exec "<prompt>" [--agent …] [--session …]', summary: "One-shot headless session: prints the answer to stdout" },
64
+ { name: "send", group: "sessions", scope: "node", json: true, usage: 'send <id> "<message>"', summary: "Send a prompt to an existing session and stream the reply" },
65
+ { name: "sessions", aliases: ["ls"], group: "sessions", scope: "node", json: true, usage: "sessions [--json]", summary: "List recent sessions (live and saved) and resume one" },
66
+ { name: "resume", group: "sessions", scope: "node", usage: "resume [n|id]", summary: "Resume a session directly (default: most recent)" },
67
+ { name: "kill", group: "sessions", scope: "node", usage: "kill <id> [--delete]", summary: "Stop a session or terminal" },
68
+ { name: "takeover", group: "sessions", scope: "node", usage: "takeover <id>", summary: "Stop a run-terminal's native TUI and continue it as a governed chat" },
69
+ { name: "promote", group: "sessions", scope: "node", usage: "promote <session-id>", summary: "Continue a warm-replicated session on this node" },
70
+ { name: "prune", aliases: ["clean"], group: "sessions", scope: "node", json: true, usage: "prune [--keep N] [--older-than 7d] [--dry-run]", summary: "Delete old sessions, workspaces and worktrees" },
71
+ { name: "agents", group: "sessions", scope: "node", json: true, usage: "agents [--json]", summary: "List supported agents and which are installed" },
72
+ { name: "agent", group: "sessions", scope: "node", json: true, usage: "agent <add|list|remove>", summary: "Connect, list or remove a user-owned agent", subcommands: ["add", "list", "remove"] },
73
+ { name: "agents:install", aliases: ["runtimes:install"], group: "sessions", scope: "node", usage: "agents:install [--bridges]", summary: "Install known upstream agents" },
74
+ { name: "shim", aliases: ["listen"], group: "sessions", scope: "node", usage: "shim [install|uninstall|status] <agent>", summary: "Make typing '<agent>' open its TUI in a session the app can see", subcommands: ["install", "uninstall", "status"] },
75
+
76
+ // Runs and automations
77
+ { name: "runs", group: "runs", scope: "account", json: true, usage: 'runs <start|list|status|wait>', summary: "Unattended Runs with checks, evidence and a Receipt", subcommands: ["start", "list", "status", "wait"] },
78
+ { name: "automation", aliases: ["automations"], group: "runs", scope: "account", json: true, usage: "automation <list|trigger|init|validate|plan|test|apply>", summary: "Automations as code: validate, plan, apply, trigger", subcommands: ["list", "trigger", "init", "validate", "plan", "test", "test-filter", "apply"] },
79
+
80
+ // This node
81
+ { name: "setup", aliases: ["init"], group: "node", scope: "node", usage: "setup", summary: "First-run wizard: agent, model login, remote sign-in, background service" },
82
+ { name: "start", aliases: ["dev"], group: "node", scope: "node", usage: "start", summary: "Run the daemon in the foreground" },
83
+ { name: "stop", group: "node", scope: "node", usage: "stop", summary: "Stop the background service" },
84
+ { name: "restart", group: "node", scope: "node", usage: "restart [--force]", summary: "Restart the background service after active turns finish" },
85
+ { name: "status", group: "node", scope: "node", json: true, usage: "status [--json]", summary: "Show config and whether the node is reachable" },
86
+ { name: "doctor", group: "node", scope: "node", usage: "doctor", summary: "Health check: deps, node, model, remote, agents" },
87
+ { name: "diagnostics", group: "node", scope: "node", usage: "diagnostics [--out <file>]", summary: "Print a redacted, shareable diagnostics bundle" },
88
+ { name: "capabilities", group: "node", scope: "node", json: true, usage: "capabilities [--json]", summary: "What this machine unlocks: OS, agents, providers, Docker/GPU, plugins" },
89
+ { name: "logs", group: "node", scope: "node", usage: "logs [-f]", summary: "Tail the node logs" },
90
+ { name: "audit", group: "node", scope: "node", json: true, usage: "audit [--session <id>] [--verify] [--json]", summary: "Show and verify the node's governance audit trail" },
91
+ { name: "update", group: "node", scope: "node", usage: "update [--force]", summary: "Update Bivy, install deps and restart the service" },
92
+ { name: "update:log", group: "node", scope: "node", usage: "update:log", summary: "Show output of the last (or in-progress) update" },
93
+ { name: "service", group: "node", scope: "node", usage: "service <install|uninstall|status>", summary: "Manage the background service", subcommands: ["install", "uninstall", "status"] },
94
+ { name: "rename", aliases: ["node:rename"], group: "node", scope: "node", usage: "rename <name>", summary: "Rename this node (no restart)" },
95
+ { name: "nodes", group: "node", scope: "node", usage: "nodes [list|add|remove]", summary: "List, add or remove other nodes", subcommands: ["list", "add", "remove"] },
96
+ { name: "token", group: "node", scope: "node", usage: "token", summary: "Print a device token for this node" },
97
+ { name: "open", group: "node", scope: "node", usage: "open", summary: "Open the remote web app" },
98
+ { name: "link", group: "node", scope: "node", usage: "link", summary: "Show a web app link as a QR code in the terminal" },
99
+ { name: "relay:setup", group: "node", scope: "node", usage: "relay:setup", summary: "Enable secure remote web app access" },
100
+ { name: "uninstall", group: "node", scope: "node", usage: "uninstall [--keep-sessions] [--dry-run]", summary: "Remove Bivy and all its data" },
101
+
102
+ // Accounts, providers and credentials
103
+ { name: "login", group: "accounts", scope: "account", usage: "login", summary: "Sign this machine into a Bivy account" },
104
+ { name: "logout", aliases: ["signout"], group: "accounts", scope: "account", usage: "logout", summary: "Sign this machine out" },
105
+ { name: "provider", aliases: ["model"], group: "accounts", scope: "node", usage: "provider login [provider]", summary: "Sign into a model provider", subcommands: ["login"] },
106
+ { name: "auth", aliases: ["credentials", "creds"], group: "accounts", scope: "node", usage: "auth <list|add|remove|import|sync|…>", summary: "Model credentials: import local logins, add, sync", subcommands: ["list", "add", "remove", "sync", "preset", "ingest", "config", "import"] },
107
+ { name: "secrets", aliases: ["secret"], group: "accounts", scope: "node", usage: "secrets <list|set|ref|delete|doctor|resolve>", summary: "Store tokens in the node vault", subcommands: ["list", "set", "ref", "delete", "doctor", "resolve"] },
108
+ { name: "github:connect", aliases: ["connect-repo"], group: "accounts", scope: "node", usage: "github:connect [owner/repo]", summary: "Connect GitHub" },
109
+ { name: "github:app-create", group: "accounts", scope: "node", usage: "github:app-create [--org <org>]", summary: "One-click: create and connect a GitHub App" },
110
+ { name: "github:app-connect", group: "accounts", scope: "node", usage: "github:app-connect --app-id <id> --key <path.pem>", summary: "Connect an existing GitHub App" },
111
+ { name: "github:app-sync", group: "accounts", scope: "account", usage: "github:app-sync [on|off]", summary: "Sync GitHub App keys to this account's other nodes" },
112
+
113
+ // Configuration and extensions
114
+ { name: "config", group: "config", scope: "node", json: true, usage: "config <show|get|set|unset|explain|validate|init|path>", summary: "Typed node configuration", subcommands: ["init", "validate", "show", "get", "set", "unset", "explain", "path"] },
115
+ { name: "plugin", aliases: ["plugins"], group: "config", scope: "node", json: true, usage: "plugin <init|validate|doctor|test|install|list|remove>", summary: "Build and install plugins", subcommands: ["init", "validate", "doctor", "test", "install", "list", "remove"] },
116
+ { name: "voice", aliases: ["stt"], group: "config", scope: "node", usage: "voice <provider|key|remove|status>", summary: "Configure speech-to-text", subcommands: ["provider", "key", "remove", "status"] },
117
+ { name: "completions", aliases: ["completion"], group: "config", scope: "node", usage: "completions <bash|zsh|fish>", summary: "Print a shell completion script", subcommands: ["bash", "zsh", "fish"] },
118
+
119
+ // Help and discovery
120
+ { name: "guide", group: "help", scope: "node", json: true, usage: "guide [topic] [--json]", summary: "Short playbooks for agents: show the user, talk to them, long work, more hands, automate",
121
+ tools: [{ name: "bivy_guide", description: "Short playbooks for working in Bivy. Without a topic, lists them.", input: { topic: { type: "string", description: "show-the-user, talk-to-the-user, long-work, more-hands or automate." } }, argv: ["guide", { arg: "topic" }] }] },
122
+ { name: "help", aliases: ["-h", "--help"], group: "help", scope: "node", json: true, usage: "help [command] [--json]", summary: "Show this help, one command's row, or every command as JSON" },
123
+ { name: "version", aliases: ["--version", "-v"], group: "help", scope: "node", usage: "version", summary: "Print the installed Bivy version" },
124
+
125
+ // Plumbing
126
+ { name: "tool", group: "help", scope: "session", hidden: true, json: true, usage: "tool <name> '<json input>'", summary: "Run one MCP tool (from `bivy help --json`) as its bivy command" },
127
+ { name: "mcp-serve", group: "help", scope: "session", hidden: true, usage: "mcp-serve", summary: "Bivy's MCP server for agents (stdio; injected into agent MCP config)" },
128
+ { name: "mcp-proxy", group: "help", scope: "session", hidden: true, usage: "mcp-proxy -- <server command>", summary: "MCP proxy in front of an agent's own MCP servers (stdio)" },
129
+ ];
130
+
131
+ const byWord = new Map();
132
+ for (const command of COMMANDS) for (const word of [command.name, ...(command.aliases ?? [])]) byWord.set(word, command);
133
+
134
+ /** The row for a typed command word (name or alias), or undefined. */
135
+ export function resolveCommand(word) {
136
+ return typeof word === "string" ? byWord.get(word) : undefined;
137
+ }
138
+
139
+ // Edit distance where swapping two neighbors counts once ("hepl" → "help").
140
+ function distance(a, b) {
141
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
142
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
143
+ for (let i = 1; i <= a.length; i++) {
144
+ for (let j = 1; j <= b.length; j++) {
145
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
146
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
147
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
148
+ }
149
+ }
150
+ return d[a.length][b.length];
151
+ }
152
+
153
+ /** Closest visible command names to a mistyped word, best first (at most 3). */
154
+ export function suggestCommands(word) {
155
+ const typed = String(word ?? "").toLowerCase();
156
+ if (!typed) return [];
157
+ const scored = [];
158
+ for (const command of COMMANDS) {
159
+ if (command.hidden) continue;
160
+ const words = [command.name, ...(command.aliases ?? [])].filter((w) => !w.startsWith("-"));
161
+ const best = Math.min(...words.map((w) => (w.startsWith(typed) || typed.startsWith(w) ? 0 : distance(typed, w))));
162
+ if (best <= Math.max(1, Math.floor(typed.length / 3))) scored.push([best, command.name]);
163
+ }
164
+ return scored.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1])).slice(0, 3).map(([, name]) => name);
165
+ }
166
+
167
+ /** The words shells should complete after `bivy`. */
168
+ export function completionWords() {
169
+ return COMMANDS.filter((command) => !command.hidden).flatMap((command) => [command.name, ...(command.aliases ?? []).filter((a) => !a.startsWith("-"))]);
170
+ }
171
+
172
+ /** Commands with first-argument verbs, for completing `bivy <cmd> <verb>`. */
173
+ export function subcommandTable() {
174
+ return COMMANDS.filter((command) => !command.hidden && command.subcommands?.length).map((command) => ({
175
+ words: [command.name, ...(command.aliases ?? [])],
176
+ subcommands: command.subcommands,
177
+ }));
178
+ }
179
+
180
+ /** A tool's input as JSON Schema. */
181
+ function toolSchema(input) {
182
+ const properties = {};
183
+ const required = [];
184
+ for (const [key, { required: isRequired, ...schema }] of Object.entries(input)) {
185
+ properties[key] = schema;
186
+ if (isRequired) required.push(key);
187
+ }
188
+ return { type: "object", properties, ...(required.length ? { required } : {}), additionalProperties: false };
189
+ }
190
+
191
+ /** Every MCP tool, with its JSON Schema and argv template: what `bivy mcp-serve` offers. */
192
+ export function describeTools() {
193
+ return COMMANDS.flatMap((command) => (command.tools ?? []).map((tool) => ({
194
+ name: tool.name, description: tool.description, inputSchema: toolSchema(tool.input), argv: tool.argv, command: command.name,
195
+ })));
196
+ }
197
+
198
+ /** The `bivy` words for a tool call: argv template + the caller's input. */
199
+ export function toolArgv(argv, input = {}) {
200
+ const words = [];
201
+ for (const part of argv) {
202
+ if (typeof part === "string") words.push(part);
203
+ else if (part.arg) { if (input[part.arg] !== undefined && input[part.arg] !== "") words.push(String(input[part.arg])); }
204
+ else if (part.when) { if (input[part.when] === true) words.push(part.flag); }
205
+ else if (part.flag) {
206
+ const value = input[part.from];
207
+ if (value === undefined || value === null || value === "") continue;
208
+ if (Array.isArray(value) && part.join) words.push(part.flag, value.join(part.join));
209
+ else for (const item of Array.isArray(value) ? value : [value]) words.push(part.flag, String(item));
210
+ }
211
+ }
212
+ return words;
213
+ }
214
+
215
+ /** One command as plain data: what `bivy help --json` and `bivy help <cmd> --json` print. */
216
+ export function describeCommand(command) {
217
+ const { name, aliases = [], group, usage, summary, scope, json = false, subcommands = [] } = command;
218
+ return { name, aliases, group, usage: `bivy ${usage}`, summary, scope, json, subcommands, help: `bivy ${name} --help` };
219
+ }
220
+
221
+ export function describeCli() {
222
+ return {
223
+ exitCodes: EXIT,
224
+ output: "Commands marked json accept --json; BIVY_OUTPUT=json turns it on for every such command.",
225
+ errors: "With --json, failures print {\"error\":{\"code\",\"message\",\"hint\"?,\"next\"?}} to stderr.",
226
+ commands: COMMANDS.filter((command) => !command.hidden).map(describeCommand),
227
+ tools: describeTools(),
228
+ };
229
+ }
230
+
231
+ /** `bivy help` text. `paint` colors a command synopsis (the CLI passes its cyan);
232
+ * `details` adds a line under a command, for lists only known at runtime. */
233
+ export function renderHelp({ paint = (s) => s, bold = (s) => s, details = {} } = {}) {
234
+ const visible = COMMANDS.filter((command) => !command.hidden);
235
+ const width = Math.min(44, Math.max(...visible.map((command) => command.usage.length)) + 5);
236
+ const lines = [`${bold("bivy")} — Bivy node CLI`, ""];
237
+ for (const [group, title] of GROUPS) {
238
+ const rows = visible.filter((command) => command.group === group);
239
+ if (!rows.length) continue;
240
+ lines.push(bold(title));
241
+ for (const command of rows) {
242
+ const synopsis = `bivy ${command.usage}`;
243
+ const pad = synopsis.length < width ? " ".repeat(width - synopsis.length) : "\n" + " ".repeat(width + 2);
244
+ lines.push(` ${paint(synopsis)}${pad}${command.summary}`);
245
+ if (details[command.name]) lines.push(`${" ".repeat(width + 2)}${details[command.name]}`);
246
+ }
247
+ lines.push("");
248
+ }
249
+ lines.push("Every command takes --help. 'bivy help --json' lists all commands, flags that take --json, and exit codes.");
250
+ lines.push("Agents: start with 'bivy context'.");
251
+ return lines.join("\n");
252
+ }
253
+
254
+ /** `--json` was passed, or BIVY_OUTPUT=json asks for JSON everywhere. */
255
+ export function wantsJson(args = [], env = process.env) {
256
+ return args.includes("--json") || String(env?.BIVY_OUTPUT ?? "").toLowerCase() === "json";
257
+ }
258
+
259
+ /**
260
+ * The one error shape for commands agents call: JSON on stderr under --json, a
261
+ * readable line otherwise, then exit with a code from EXIT. `next` names the
262
+ * command that gets the caller unstuck.
263
+ */
264
+ export function cliError({ code, message, hint, next, exit = EXIT.failed }, { json = false, paint = (s) => s } = {}) {
265
+ if (json) {
266
+ process.stderr.write(`${JSON.stringify({ error: { code, message, ...(hint ? { hint } : {}), ...(next ? { next } : {}) } })}\n`);
267
+ } else {
268
+ process.stderr.write(`${paint(message)}\n`);
269
+ if (hint) process.stderr.write(`${hint}\n`);
270
+ if (next) process.stderr.write(`Try: ${next}\n`);
271
+ }
272
+ process.exit(exit);
273
+ }
@@ -0,0 +1,18 @@
1
+ # Automate
2
+
3
+ Summary: Automations as code: write, validate, preview, test, apply and trigger.
4
+
5
+ Automations live in the repository, in `.bivy/automations.yaml`, and apply like
6
+ infrastructure code:
7
+
8
+ bivy automation init # write a safe starter file
9
+ bivy automation validate # check it
10
+ bivy automation plan --json # triggers, routing, effective safety
11
+ bivy automation test --event event.json # which automation an event would start
12
+ bivy automation apply # reconcile the account with the file
13
+ bivy automation list --json
14
+ bivy automation trigger <id-or-key> # run one now
15
+
16
+ Always `validate` and `plan` before `apply`, and show the user the plan when it
17
+ changes something they didn't ask for. `apply --prune` also removes automations
18
+ the file no longer has. `bivy automation --help` has every flag.
@@ -0,0 +1,23 @@
1
+ # Long and unattended work
2
+
3
+ Summary: One-shot answers, governed Runs with checks and receipts, and following them.
4
+
5
+ ## One question, one answer
6
+
7
+ bivy exec "Summarize the open TODOs in src/" --json
8
+
9
+ Runs a headless session and prints the answer (`{"sessionId","answer"}` with
10
+ `--json`).
11
+
12
+ ## A governed Run
13
+
14
+ bivy runs start "Upgrade the test runner and fix what breaks"
15
+ bivy runs list --json
16
+ bivy runs wait <id> --timeout 3600 --json
17
+
18
+ A Run is unattended work with checks, evidence and a Receipt. `runs wait` exits
19
+ 0 when it succeeded, 1 when it failed or was cancelled, 2 on timeout.
20
+
21
+ ## While you wait
22
+
23
+ Tell the user when it matters: `bivy notify "…"` (see `bivy guide talk-to-the-user`).
@@ -0,0 +1,18 @@
1
+ # More hands: other agents and machines
2
+
3
+ Summary: Delegate a task to another agent or machine, compare several, and follow the result.
4
+
5
+ Use your own sub-agents for ordinary work. Delegate only when the user asks for
6
+ another agent or another of their machines.
7
+
8
+ bivy delegate machines # machines and their agents
9
+ bivy delegate "Review this branch for bugs" --agent codex --wait
10
+ bivy delegate "Run the Linux-only build" --machine build-box --agent claude --wait
11
+ bivy delegate "Propose a fix for issue 42" --to codex,claude@laptop --wait
12
+
13
+ Write the task so it stands on its own: the other agent does not see this chat.
14
+ `--wait` prints the child's answer and any branch or PR it pushed. Without it you
15
+ get a run id: `bivy delegate status <id>`, `bivy delegate wait <id>`.
16
+ Code comes back by git: fetch the branch or review the PR.
17
+
18
+ For unattended work with checks and a receipt, see `bivy guide long-work`.
@@ -0,0 +1,44 @@
1
+ # Show the user something
2
+
3
+ Summary: Attach files, publish live previews, check them with screenshots, and present a finished change.
4
+
5
+ The user reads your session in a chat app, often on a phone. They cannot see your
6
+ terminal, your files, or anything you only describe. Put things in front of them.
7
+
8
+ ## A file or image
9
+
10
+ bivy attach out/chart.png --caption "Revenue by month"
11
+ bivy attach report.pdf --artifact
12
+
13
+ Images render inline; other files download. The path must be inside the session
14
+ workspace. `--artifact` also lists it in the session's Artifacts, for durable
15
+ outputs (reports, coverage, builds). Markdown image syntax with a local path does
16
+ not render; a remote `https://` image does.
17
+
18
+ ## Something with a UI
19
+
20
+ Publish it once; the user opens it from the chat:
21
+
22
+ bivy app run -- pnpm dev # a desktop app, no manifest
23
+ bivy app publish app.json # web server, static build, terminal
24
+
25
+ A manifest with a server Bivy starts on first open:
26
+
27
+ {"version":1,"name":"Site","views":[{"kind":"web","name":"Web",
28
+ "source":{"kind":"service","port":5173,"start":{"command":"pnpm","args":["dev"]}}}]}
29
+
30
+ ## Check your work before you say it's done
31
+
32
+ bivy app shot --widths 390,1280 --themes light,dark
33
+
34
+ prints PNG paths. Look at them. Screenshots are off until the user turns them
35
+ on (`bivy config set sessions.appScreenshots true`).
36
+
37
+ ## Hand it over
38
+
39
+ bivy app present --note "The header now wraps on phones"
40
+
41
+ posts a card with the app at phone width (before and after) and opens the
42
+ preview. Use it when a visible change is ready, not after every edit.
43
+
44
+ More: `bivy app --help`, `bivy attach --help`.
@@ -0,0 +1,38 @@
1
+ # Talk to the user
2
+
3
+ Summary: When to answer in chat, when to notify, when to ask and wait, and how to offer next steps.
4
+
5
+ Your chat reply is the default. Reach for these when the chat alone won't do.
6
+
7
+ ## They may be away: notify
8
+
9
+ bivy notify "Migration finished: 3 tables rewritten, all tests pass."
10
+ bivy notify --urgent "Production is serving the old build."
11
+
12
+ Posts a card in the chat. When nobody has the app open, their devices get a push
13
+ naming this session (the text stays in the chat). `--urgent` pushes even while
14
+ they are looking. One push a minute per session; don't send one per step.
15
+ Good moments: long work finished, you are blocked, something needs a look.
16
+
17
+ ## You need a decision: ask
18
+
19
+ bivy ask "Which database?" --option Postgres --option SQLite
20
+ bivy ask "What should the release be called?"
21
+
22
+ Blocks until they answer and prints the answer (exit 0). Exit 1 means they
23
+ dismissed it (use your judgment), exit 5 that `--timeout` (default 10m) passed.
24
+ For long waits use `--async`, keep working, and later `bivy ask wait <id>`.
25
+ Ask only when you can't reasonably decide yourself.
26
+
27
+ ## Offer next steps: suggest
28
+
29
+ bivy suggest "Add a GET /version endpoint that returns the package version." --title "Add /version"
30
+
31
+ Each suggestion is a card the user can start in one tap, here or in a parallel
32
+ session with its own copy of the project. Write it as a complete instruction with
33
+ paths relative to the project root. Post one per idea instead of a bulleted list.
34
+
35
+ ## Is anyone there?
36
+
37
+ `bivy context --json` reports `userConnected` (some device has the app open) and
38
+ `lastDriver` (the device that last typed into this session).
@@ -42,10 +42,15 @@ export const BIVY_AGENT_NOTE = [
42
42
  '`bivy suggest "<complete instruction>" [--title "short label"]` instead of only listing them. The user can ' +
43
43
  "start each in one tap, in this session or in a parallel one that works in its own copy of the project, so " +
44
44
  "write it to stand on its own, with paths relative to the project root.",
45
+ "- When you finish long work, get blocked, or need the user to look at something: " +
46
+ '`bivy notify "<message>"` (a chat card, plus a push to their phone when they are away). To ask and wait for ' +
47
+ 'an answer: `bivy ask "<question>" [--option A --option B]` prints their answer (or use your own ask-the-user tool).',
45
48
  "- Only when the user asks for another agent or another of their machines to take part: " +
46
49
  '`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
47
50
  "its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
48
51
  "lists machines and their agents. Use your own sub-agents for everything else.",
52
+ "- `bivy context` shows your session, workspace and published apps; `bivy guide` has short playbooks; " +
53
+ "`bivy help` lists every command (`--json` for all).",
49
54
  ].join("\n");
50
55
  const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
51
56
  "instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
@@ -3119,17 +3119,23 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3119
3119
  /* Suggested task (SuggestionCard.tsx): a task the agent proposed, one tap to
3120
3120
  start in its own session. Shell from .card; layout only here. */
3121
3121
  .suggestion-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
3122
- .suggestion-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3122
+ /* The small label above a chat card's content, shared by every agent-posted card. */
3123
+ .suggestion-eyebrow, .delegation-eyebrow, .notice-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3123
3124
  .suggestion-title { margin: 0; font-size: var(--text-base); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
3124
3125
  .suggestion-text { margin: 0; color: var(--muted); font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
3125
3126
  .suggestion-actions, .suggestion-all { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
3126
3127
  .suggestion-all { padding-top: var(--space-2); border-top: 1px solid var(--line); }
3127
3128
  .suggestion-status { margin: var(--space-2) 0 0; display: flex; align-items: center; gap: var(--space-2); color: var(--ok); font-size: var(--text-sm); font-weight: var(--weight-medium); }
3129
+ /* Notice (NoticeCard.tsx): a message the agent sent with `bivy notify`.
3130
+ Shell and tone from .card, the urgent marker from .badge. */
3131
+ .notice-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
3132
+ .notice-eyebrow { display: flex; align-items: center; gap: var(--space-2); }
3133
+ .notice-text { margin: 0; font-size: var(--text-base); white-space: pre-wrap; overflow-wrap: anywhere; }
3134
+
3128
3135
  /* Delegation (DelegationCard.tsx): a task handed to another agent/machine.
3129
3136
  Shell from .card, status from .badge; layout only here. A fan-out renders as
3130
3137
  one group whose members sit side by side where there is room. */
3131
3138
  .delegation-card { max-inline-size: 440px; display: flex; flex-direction: column; gap: var(--space-1); }
3132
- .delegation-eyebrow { margin: 0; color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3133
3139
  .delegation-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
3134
3140
  .delegation-head { align-items: flex-start; }
3135
3141
  .delegation-target { display: flex; flex-direction: column; font-weight: var(--weight-semibold); overflow-wrap: anywhere; min-inline-size: 0; }
@@ -7,7 +7,11 @@
7
7
  // start (see mcp-inject.ts's injectBivyToolsForSession), it exposes Bivy's chat
8
8
  // affordances as first-class tools so ANY agent — codex, gemini, aider, opencode,
9
9
  // … — discovers them in its tool list instead of having to be told about a shell
10
- // command (issue #290). Today it serves one tool, `attach_to_chat`.
10
+ // command (issue #290). It serves `attach_to_chat`, plus every tool listed in
11
+ // the `bivy` command table (bin/cli-commands.mjs, read through `bivy help
12
+ // --json`): each runs as `bivy tool <name> <json>`, i.e. the command itself with
13
+ // --json, so tools and commands share one implementation and error contract.
14
+ // The agent guides (`bivy guide`) are served as resources.
11
15
  //
12
16
  // Claude and Pi already get `attach_to_chat` natively (in-process SDK MCP server /
13
17
  // integration ToolProvider); this covers everyone else. The tool just POSTs to the
@@ -20,12 +24,13 @@
20
24
  // with an injected fetch; the process/transport wiring is thin. Deliberately do
21
25
  // not expose governed Runs here: agents should use their native sub-agent tools,
22
26
  // which stay inside the parent Session instead of cluttering the Session list.
27
+ import { spawn } from "node:child_process";
23
28
  import fs from "node:fs";
24
29
  import path from "node:path";
25
30
  import { fileURLToPath } from "node:url";
26
31
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
27
32
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
28
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
33
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
29
34
  const DEFAULT_ENDPOINT = "http://127.0.0.1:4317";
30
35
  /** The tools this server advertises, in MCP `tools/list` shape. */
31
36
  export const BIVY_MCP_TOOLS = [
@@ -89,6 +94,30 @@ export async function runAttachToChat(endpoint, sessionId, args, fetchImpl, toke
89
94
  const name = body?.name || filePath;
90
95
  return { isError: false, text: `Attached ${name} to the chat as ${body?.kind === "image" ? "an inline image" : "a downloadable file"}. The user can see it now.` };
91
96
  }
97
+ /** Run this install's `bivy` (bin/bivy.mjs, two levels up from src/ or dist/). */
98
+ function defaultRunBivy(sessionId) {
99
+ const cli = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../bin/bivy.mjs");
100
+ return (args) => new Promise((resolve) => {
101
+ const child = spawn(process.execPath, [cli, ...args], { env: { ...process.env, BIVY_SESSION_ID: sessionId, NO_COLOR: "1" }, stdio: ["ignore", "pipe", "pipe"] });
102
+ let stdout = "";
103
+ let stderr = "";
104
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
105
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
106
+ child.on("error", (error) => resolve({ code: 1, stdout, stderr: stderr || error.message }));
107
+ child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
108
+ });
109
+ }
110
+ /** The tools the command table offers, or none when the CLI can't be asked. */
111
+ async function cliTools(runBivy) {
112
+ const result = await runBivy(["help", "--json"]);
113
+ try {
114
+ const tools = JSON.parse(result.stdout).tools ?? [];
115
+ return tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ }
92
121
  /** The user's instructions to advertise at initialize, or undefined when there are none. */
93
122
  function readInstructions(file) {
94
123
  if (!file)
@@ -107,14 +136,47 @@ export function createBivyMcpServer(deps = {}) {
107
136
  const token = deps.token ?? process.env.BIVY_MCP_TOKEN ?? undefined;
108
137
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
109
138
  const instructions = readInstructions(deps.instructionsFile ?? process.env.BIVY_AGENT_INSTRUCTIONS_FILE);
110
- const server = new Server({ name: "bivy", version: "1.0.0" }, { capabilities: { tools: {} }, ...(instructions ? { instructions } : {}) });
111
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: BIVY_MCP_TOOLS }));
139
+ const runBivy = deps.runBivy ?? defaultRunBivy(sessionId);
140
+ let tools;
141
+ const generated = () => (tools ??= cliTools(runBivy));
142
+ const server = new Server({ name: "bivy", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} }, ...(instructions ? { instructions } : {}) });
143
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...BIVY_MCP_TOOLS, ...await generated()] }));
112
144
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
113
145
  const args = (req.params.arguments ?? {});
114
- const result = req.params.name === "attach_to_chat"
115
- ? await runAttachToChat(endpoint, sessionId, args, fetchImpl, token)
116
- : { isError: true, text: `Unknown tool: ${req.params.name}` };
117
- return { isError: result.isError, content: [{ type: "text", text: result.text }] };
146
+ if (req.params.name === "attach_to_chat") {
147
+ const result = await runAttachToChat(endpoint, sessionId, args, fetchImpl, token);
148
+ return { isError: result.isError, content: [{ type: "text", text: result.text }] };
149
+ }
150
+ if (!(await generated()).some((tool) => tool.name === req.params.name)) {
151
+ return { isError: true, content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }] };
152
+ }
153
+ const result = await runBivy(["tool", req.params.name, JSON.stringify(args)]);
154
+ const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n") || (result.code === 0 ? "Done." : `bivy exited ${result.code}.`);
155
+ return { isError: result.code !== 0, content: [{ type: "text", text }] };
156
+ });
157
+ // The agent guides, one resource per topic.
158
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
159
+ const result = await runBivy(["guide", "--json"]);
160
+ const guides = (() => { try {
161
+ return JSON.parse(result.stdout).guides;
162
+ }
163
+ catch {
164
+ return [];
165
+ } })();
166
+ return { resources: guides.map((g) => ({ uri: `bivy://guide/${g.topic}`, name: g.title, description: g.summary, mimeType: "text/markdown" })) };
167
+ });
168
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
169
+ const topic = /^bivy:\/\/guide\/([a-z0-9-]+)$/.exec(req.params.uri)?.[1];
170
+ const result = topic ? await runBivy(["guide", topic, "--json"]) : undefined;
171
+ const guide = (() => { try {
172
+ return result && result.code === 0 ? JSON.parse(result.stdout) : undefined;
173
+ }
174
+ catch {
175
+ return undefined;
176
+ } })();
177
+ if (!guide)
178
+ throw new Error(`No Bivy resource at ${req.params.uri}`);
179
+ return { contents: [{ uri: req.params.uri, mimeType: "text/markdown", text: guide.markdown }] };
118
180
  });
119
181
  return server;
120
182
  }
package/dist/question.js CHANGED
@@ -48,7 +48,7 @@ export class QuestionManager {
48
48
  */
49
49
  request(input) {
50
50
  const request = {
51
- id: randomUUID(),
51
+ id: input.id ?? randomUUID(),
52
52
  sessionId: input.sessionId,
53
53
  questions: input.questions,
54
54
  createdAt: Date.now(),