@bivy/bivy 0.19.4-staging.4 → 0.19.4-staging.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bivy.mjs +146 -0
- package/bin/cli-commands.mjs +4 -0
- package/bin/guides/automate.md +2 -1
- package/bin/guides/more-hands.md +6 -0
- package/bin/guides/show-the-user.md +6 -1
- package/dist/protocol/client-command-routes.js +1 -0
- package/dist/protocol/client-command-schemas.js +1 -0
- package/package.json +1 -1
package/bin/bivy.mjs
CHANGED
|
@@ -2684,6 +2684,140 @@ answer later and 'bivy ask status <id>' checks without waiting.
|
|
|
2684
2684
|
finish(await waitFor(started.id, timeout + 5));
|
|
2685
2685
|
}
|
|
2686
2686
|
|
|
2687
|
+
// One JSON request to the local node for the commands below: starts the node if
|
|
2688
|
+
// needed and returns { status, body }, or fails in the shared error shape.
|
|
2689
|
+
async function nodeRequest(method, pathname, body, { json = false } = {}) {
|
|
2690
|
+
const config = loadConfig();
|
|
2691
|
+
if (!(await ensureNodeRunning(config))) return cliError({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable }, { json, paint: c.red });
|
|
2692
|
+
let token;
|
|
2693
|
+
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2694
|
+
const res = await fetch(`${url(config)}${pathname}`, {
|
|
2695
|
+
method,
|
|
2696
|
+
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
2697
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
2698
|
+
}).catch((error) => error);
|
|
2699
|
+
if (res instanceof Error) return cliError({ code: "node_unreachable", message: `Could not reach the Bivy node: ${res.message}`, next: "bivy status", exit: EXIT.unavailable }, { json, paint: c.red });
|
|
2700
|
+
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// `bivy fork [session-id]` — the app's Fork on this machine: a new session with
|
|
2704
|
+
// the same conversation, agent and uncommitted work, on its own branch. Give it
|
|
2705
|
+
// its next task with `bivy send <new-id> "…"`.
|
|
2706
|
+
async function cmdFork(args = []) {
|
|
2707
|
+
const usage = "Usage: bivy fork [session-id] [--model <provider/id>] [--json]";
|
|
2708
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2709
|
+
console.log(`${usage}\n\nCopy a session (default: this one, $BIVY_SESSION_ID) into a new session on this machine: the same conversation and agent, with uncommitted work carried into a fresh worktree on its own branch. --model switches the fork's model. Prints the new session's id; send it work with 'bivy send <id> "…"'. --json prints {"sessionId","fidelity"}.`);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const json = wantsJson(args);
|
|
2713
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2714
|
+
const parsed = parseSessionArgs(args, ["--model", "--session"]);
|
|
2715
|
+
if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
|
|
2716
|
+
const sessionId = parsed.positional[0] ?? resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
|
|
2717
|
+
if (!sessionId) return fail({ code: "no_session", message: "No session to fork.", hint: "Pass a session id, or run inside an agent session ($BIVY_SESSION_ID).", next: "bivy sessions --json", exit: EXIT.usage });
|
|
2718
|
+
// "provider/id" or a bare model id, as the fork command takes it.
|
|
2719
|
+
const modelArg = parsed.one("--model");
|
|
2720
|
+
const slash = modelArg?.indexOf("/") ?? -1;
|
|
2721
|
+
const model = modelArg ? (slash > 0 ? { provider: modelArg.slice(0, slash), id: modelArg.slice(slash + 1) } : { provider: "", id: modelArg }) : undefined;
|
|
2722
|
+
const { status, body } = await nodeRequest("POST", "/api/session/fork", { sessionId, ...(model ? { model } : {}) }, { json });
|
|
2723
|
+
if (status === 404) return fail({ code: "node_outdated", message: "This node is older than 'bivy fork'.", next: "bivy update", exit: EXIT.unavailable });
|
|
2724
|
+
if (status >= 400 || body.error) return fail({ code: /not found/i.test(body.error ?? "") ? "session_not_found" : "failed", message: `Fork failed: ${body.error || `HTTP ${status}`}`, exit: /not found/i.test(body.error ?? "") ? EXIT.notFound : EXIT.failed });
|
|
2725
|
+
if (json) { console.log(JSON.stringify({ sessionId: body.sessionId, fidelity: body.fidelity, ...(body.missing?.length ? { missing: body.missing } : {}) })); return; }
|
|
2726
|
+
console.log(c.green(`Forked into ${body.sessionId}.`) + ` Give it work with: bivy send ${body.sessionId} "…"`);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
// `bivy approvals` — tool calls waiting for a person, and answering them. For a
|
|
2730
|
+
// supervising agent or script; the app shows the same requests as cards.
|
|
2731
|
+
async function cmdApprovals(args = []) {
|
|
2732
|
+
const usage = "Usage: bivy approvals [list] [--json]\n bivy approvals approve <id> [--json]\n bivy approvals reject <id> [--json]";
|
|
2733
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2734
|
+
console.log(`${usage}\n\nList the tool calls on this node waiting for approval, or answer one. The app shows the same requests as cards; answering here resolves them there too.`);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const json = wantsJson(args);
|
|
2738
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2739
|
+
const [action = "list", id] = args.filter((a) => !a.startsWith("-"));
|
|
2740
|
+
if (action === "list") {
|
|
2741
|
+
const { body } = await nodeRequest("GET", "/api/approvals", undefined, { json });
|
|
2742
|
+
const pending = (Array.isArray(body) ? body : []).filter((a) => a.status === "pending");
|
|
2743
|
+
if (json) { console.log(JSON.stringify(pending, null, 2)); return; }
|
|
2744
|
+
if (!pending.length) { console.log("No approvals waiting."); return; }
|
|
2745
|
+
for (const a of pending) console.log(`${c.cyan(a.id)} ${a.toolName}${a.risk ? c.dim(` (${a.risk})`) : ""} session ${a.sessionId}\n ${a.reason}`);
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
if ((action === "approve" || action === "reject") && id) {
|
|
2749
|
+
const { status } = await nodeRequest("POST", `/api/approvals/${encodeURIComponent(id)}/${action}`, {}, { json });
|
|
2750
|
+
if (status === 404) return fail({ code: "not_found", message: `No pending approval ${id}.`, next: "bivy approvals --json", exit: EXIT.notFound });
|
|
2751
|
+
if (json) { console.log(JSON.stringify({ ok: true, id, approved: action === "approve" })); return; }
|
|
2752
|
+
console.log(c.green(action === "approve" ? `Approved ${id}.` : `Rejected ${id}.`));
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// `bivy issues` — the node's GitHub issue pickup: open issues labelled for an
|
|
2759
|
+
// agent, and starting a session on one (it claims the issue with a label).
|
|
2760
|
+
async function cmdIssues(args = []) {
|
|
2761
|
+
const usage = "Usage: bivy issues [list] [--json]\n bivy issues pickup <number> [--json]";
|
|
2762
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2763
|
+
console.log(`${usage}\n\nOpen GitHub issues labelled for an agent in this node's repository, and picking one up: pickup claims the issue with its claim label and starts a session on it. Needs GitHub issue pickup configured (see docs/github-work-queue.md).`);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
const json = wantsJson(args);
|
|
2767
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2768
|
+
const [action = "list", number] = args.filter((a) => !a.startsWith("-"));
|
|
2769
|
+
if (action === "list") {
|
|
2770
|
+
const { status, body } = await nodeRequest("GET", "/api/github/issues", undefined, { json });
|
|
2771
|
+
if (status >= 400) return fail({ code: "not_configured", message: body.error || `HTTP ${status}`, exit: EXIT.failed });
|
|
2772
|
+
if (json) { console.log(JSON.stringify(body, null, 2)); return; }
|
|
2773
|
+
if (!body.issues?.length) { console.log(`No open issues labelled ${body.label} in ${body.repo}.`); return; }
|
|
2774
|
+
for (const issue of body.issues) console.log(`${c.cyan(`#${issue.number}`)} ${issue.title}`);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
if (action === "pickup" && number) {
|
|
2778
|
+
const { status, body } = await nodeRequest("POST", `/api/github/issues/${encodeURIComponent(number)}/pickup`, {}, { json });
|
|
2779
|
+
if (status >= 400) return fail({ code: status === 404 ? "not_found" : status === 409 ? "conflict" : "failed", message: body.error || `HTTP ${status}`, exit: status === 404 ? EXIT.notFound : status === 409 ? EXIT.conflict : status === 400 ? EXIT.usage : EXIT.failed });
|
|
2780
|
+
if (json) { console.log(JSON.stringify(body)); return; }
|
|
2781
|
+
console.log(c.green(`Picked up #${body.issue?.number} in ${body.repo}; a session is starting on it.`));
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// `bivy instructions` — the account-wide instructions every agent session gets
|
|
2788
|
+
// (Settings → Agent instructions). `set` refuses to overwrite a newer edit
|
|
2789
|
+
// made elsewhere since the text was read.
|
|
2790
|
+
async function cmdInstructions(args = []) {
|
|
2791
|
+
const usage = "Usage: bivy instructions [show] [--json]\n bivy instructions set <file|-> [--json]\n bivy instructions path";
|
|
2792
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
2793
|
+
console.log(`${usage}\n\nThe account-wide instructions every agent session receives (the same text as Settings → Agent instructions). 'set' replaces them from a file, or from stdin with '-'; it applies to sessions started afterwards. 'path' prints the file on this machine.`);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const json = wantsJson(args);
|
|
2797
|
+
const fail = (error) => cliError(error, { json, paint: c.red });
|
|
2798
|
+
const [action = "show", source] = args.filter((a) => a === "-" || !a.startsWith("-"));
|
|
2799
|
+
if (action === "path") { console.log(path.join(appDir, "AGENTS.md")); return; }
|
|
2800
|
+
const { status, body } = await nodeRequest("GET", "/api/node/settings", undefined, { json });
|
|
2801
|
+
const current = body.agentInstructions;
|
|
2802
|
+
if (status >= 400 || !current) return fail({ code: "node_outdated", message: "This node does not report agent instructions.", next: "bivy update", exit: EXIT.unavailable });
|
|
2803
|
+
if (action === "show") {
|
|
2804
|
+
if (json) { console.log(JSON.stringify({ text: current.text, updatedAt: current.updatedAt, bivyNote: current.bivyNote })); return; }
|
|
2805
|
+
console.log(current.text || c.dim("(none yet)"));
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (action === "set" && source) {
|
|
2809
|
+
let text;
|
|
2810
|
+
try { text = source === "-" ? fs.readFileSync(0, "utf8") : fs.readFileSync(path.resolve(source), "utf8"); }
|
|
2811
|
+
catch (error) { return fail({ code: "file_not_found", message: `Could not read ${source}: ${error?.message || error}`, exit: EXIT.notFound }); }
|
|
2812
|
+
const saved = await nodeRequest("POST", "/api/node/settings", { agentInstructions: text, agentInstructionsBaseUpdatedAt: current.updatedAt }, { json });
|
|
2813
|
+
if (saved.status >= 400) return fail({ code: /another device/.test(saved.body.error ?? "") ? "conflict" : "invalid", message: saved.body.error || `HTTP ${saved.status}`, exit: /another device/.test(saved.body.error ?? "") ? EXIT.conflict : EXIT.usage });
|
|
2814
|
+
if (json) { console.log(JSON.stringify({ ok: true, bytes: Buffer.byteLength(text) })); return; }
|
|
2815
|
+
console.log(c.green("Saved. Sessions started from now on get these instructions."));
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
fail({ code: "usage", message: usage, exit: EXIT.usage });
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2687
2821
|
// Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
|
|
2688
2822
|
// self-contained task from this session to another agent, optionally on another
|
|
2689
2823
|
// account machine, and get its answer back. Agent-agnostic — any agent with a
|
|
@@ -5861,6 +5995,18 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
5861
5995
|
case "notify":
|
|
5862
5996
|
await cmdNotify(args);
|
|
5863
5997
|
break;
|
|
5998
|
+
case "fork":
|
|
5999
|
+
await cmdFork(args);
|
|
6000
|
+
break;
|
|
6001
|
+
case "approvals":
|
|
6002
|
+
await cmdApprovals(args);
|
|
6003
|
+
break;
|
|
6004
|
+
case "issues":
|
|
6005
|
+
await cmdIssues(args);
|
|
6006
|
+
break;
|
|
6007
|
+
case "instructions":
|
|
6008
|
+
await cmdInstructions(args);
|
|
6009
|
+
break;
|
|
5864
6010
|
case "ask":
|
|
5865
6011
|
await cmdAsk(args);
|
|
5866
6012
|
break;
|
package/bin/cli-commands.mjs
CHANGED
|
@@ -56,6 +56,7 @@ export const COMMANDS = [
|
|
|
56
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
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
58
|
] },
|
|
59
|
+
{ name: "fork", group: "session", scope: "session", json: true, usage: "fork [session-id] [--model <model>]", summary: "Copy this session (conversation and uncommitted work) into a new one on its own branch" },
|
|
59
60
|
{ 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
|
|
|
61
62
|
// Agents and sessions
|
|
@@ -68,6 +69,8 @@ export const COMMANDS = [
|
|
|
68
69
|
{ 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
70
|
{ name: "promote", group: "sessions", scope: "node", usage: "promote <session-id>", summary: "Continue a warm-replicated session on this node" },
|
|
70
71
|
{ 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" },
|
|
72
|
+
{ name: "approvals", group: "sessions", scope: "node", json: true, usage: "approvals [list|approve <id>|reject <id>]", summary: "List pending tool approvals on this node, or answer one", subcommands: ["list", "approve", "reject"] },
|
|
73
|
+
{ name: "issues", group: "sessions", scope: "node", json: true, usage: "issues [list|pickup <number>]", summary: "GitHub issues waiting for an agent; pick one up to start a session on it", subcommands: ["list", "pickup"] },
|
|
71
74
|
{ name: "agents", group: "sessions", scope: "node", json: true, usage: "agents [--json]", summary: "List supported agents and which are installed" },
|
|
72
75
|
{ 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
76
|
{ name: "agents:install", aliases: ["runtimes:install"], group: "sessions", scope: "node", usage: "agents:install [--bridges]", summary: "Install known upstream agents" },
|
|
@@ -111,6 +114,7 @@ export const COMMANDS = [
|
|
|
111
114
|
{ 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
115
|
|
|
113
116
|
// Configuration and extensions
|
|
117
|
+
{ name: "instructions", group: "config", scope: "account", json: true, usage: "instructions [show|set <file|->|path]", summary: "The account-wide instructions every agent session receives", subcommands: ["show", "set", "path"] },
|
|
114
118
|
{ 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
119
|
{ 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
120
|
{ name: "voice", aliases: ["stt"], group: "config", scope: "node", usage: "voice <provider|key|remove|status>", summary: "Configure speech-to-text", subcommands: ["provider", "key", "remove", "status"] },
|
package/bin/guides/automate.md
CHANGED
|
@@ -13,6 +13,7 @@ infrastructure code:
|
|
|
13
13
|
bivy automation list --json
|
|
14
14
|
bivy automation trigger <id-or-key> # run one now
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
To pause one, set `enabled: false` on it and apply. Always `validate` and
|
|
17
|
+
`plan` before `apply`, and show the user the plan when it
|
|
17
18
|
changes something they didn't ask for. `apply --prune` also removes automations
|
|
18
19
|
the file no longer has. `bivy automation --help` has every flag.
|
package/bin/guides/more-hands.md
CHANGED
|
@@ -15,4 +15,10 @@ Write the task so it stands on its own: the other agent does not see this chat.
|
|
|
15
15
|
get a run id: `bivy delegate status <id>`, `bivy delegate wait <id>`.
|
|
16
16
|
Code comes back by git: fetch the branch or review the PR.
|
|
17
17
|
|
|
18
|
+
To try another approach from the same point, fork this session and give the
|
|
19
|
+
copy its task (it gets its own branch):
|
|
20
|
+
|
|
21
|
+
id=$(bivy fork --json | jq -r .sessionId)
|
|
22
|
+
bivy send "$id" "Try the same fix with a streaming parser instead."
|
|
23
|
+
|
|
18
24
|
For unattended work with checks and a receipt, see `bivy guide long-work`.
|
|
@@ -19,9 +19,14 @@ not render; a remote `https://` image does.
|
|
|
19
19
|
|
|
20
20
|
Publish it once; the user opens it from the chat:
|
|
21
21
|
|
|
22
|
-
bivy app run --
|
|
22
|
+
bivy app run -- cargo run # a desktop app, no manifest
|
|
23
23
|
bivy app publish app.json # web server, static build, terminal
|
|
24
24
|
|
|
25
|
+
A `terminal` view is a command the user can watch and type into from the app,
|
|
26
|
+
such as a REPL or a log tail:
|
|
27
|
+
|
|
28
|
+
{"kind":"terminal","name":"Console","command":"bin/rails","args":["console"]}
|
|
29
|
+
|
|
25
30
|
A manifest with a server Bivy starts on first open:
|
|
26
31
|
|
|
27
32
|
{"version":1,"name":"Site","views":[{"kind":"web","name":"Web",
|
|
@@ -8,6 +8,7 @@ export const CLIENT_COMMAND_ROUTES = [
|
|
|
8
8
|
{ method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
|
|
9
9
|
{ method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
|
|
10
10
|
{ method: "post", path: "/api/session/pause", kind: "session.pause" },
|
|
11
|
+
{ method: "post", path: "/api/session/fork", kind: "session.fork.local" },
|
|
11
12
|
...["get", "draft"].map((action) => ({ method: "post", path: `/api/session/presence/${action}`, kind: `session.presence.${action}` })),
|
|
12
13
|
{ method: "post", path: "/api/session/resume", kind: "session.resume" },
|
|
13
14
|
{ method: "post", path: "/api/session/question/answer", kind: "session.question.answer" },
|
|
@@ -11,6 +11,7 @@ export const CLIENT_COMMAND_SCHEMAS = {
|
|
|
11
11
|
ping: Type.Object(request),
|
|
12
12
|
"apps.list": Type.Object({ ...request, ...optionalSession }),
|
|
13
13
|
"artifacts.list": Type.Object(request),
|
|
14
|
+
"session.fork.local": Type.Object({ ...request, ...session, model: Type.Optional(Type.Object({ provider: Type.Optional(Type.String({ maxLength: 200 })), id: Type.String({ maxLength: 200 }) })) }),
|
|
14
15
|
"apps.publish": Type.Object({ ...request, ...session, manifest: Type.Unknown() }),
|
|
15
16
|
"apps.offers": Type.Object({ ...request, ...session }),
|
|
16
17
|
"apps.adopt": Type.Object({ ...request, ...session, port: Type.Integer({ minimum: 1024, maximum: 65535 }) }),
|
package/package.json
CHANGED