@bivy/bivy 0.21.2-staging.1 → 0.21.2-staging.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/bin/cli-commands.mjs +1 -1
  2. package/bin/guides/add-an-agent.md +39 -0
  3. package/dist/agent-cli.js +91 -63
  4. package/dist/agents/claude-code/runtime.js +19 -10
  5. package/dist/node-config.js +18 -0
  6. package/dist/plugin-cli.js +3 -4
  7. package/dist/plugin-sdk/manifest.js +19 -11
  8. package/dist/runtime/cli-parsers.js +18 -1
  9. package/dist/runtime/index.js +71 -41
  10. package/dist/server.js +0 -3
  11. package/package.json +1 -1
  12. package/web/assets/{AutomationsView-chlf1Cpr.js → AutomationsView-BJlyyzZB.js} +1 -1
  13. package/web/assets/{ChangesCard-BQ3lwCYn.js → ChangesCard-DFbnxxCZ.js} +1 -1
  14. package/web/assets/{LibraryView-CqY8xTAh.js → LibraryView-D1K5Ve6r.js} +1 -1
  15. package/web/assets/{ProviderConnect-BC-EWUjN.js → ProviderConnect-p4QZTuEv.js} +1 -1
  16. package/web/assets/{SessionList-BG0cvQfe.js → SessionList-D9XLxV2U.js} +1 -1
  17. package/web/assets/{Settings-Bpd2LYcT.js → Settings-B6zqi76M.js} +3 -3
  18. package/web/assets/{Sheet-B0HjJdGp.js → Sheet-BPVeFpV8.js} +1 -1
  19. package/web/assets/{Terminal-DTbs_kzR.js → Terminal-B3Qb-iB3.js} +1 -1
  20. package/web/assets/{VoiceSettings-5stXm8Rh.js → VoiceSettings-CufMc1h5.js} +1 -1
  21. package/web/assets/{abnfDiagram-VCTEODGH-BzxGthCK.js → abnfDiagram-VCTEODGH-D8vrVv5p.js} +1 -1
  22. package/web/assets/architecture-7GRP2DOG-CsAMjbd6.js +1 -0
  23. package/web/assets/{architectureDiagram-5GKGNRK7-BUKaHpyZ.js → architectureDiagram-5GKGNRK7-CA18nPWs.js} +1 -1
  24. package/web/assets/{blockDiagram-I7D4REHJ-BBCtk8Uh.js → blockDiagram-I7D4REHJ-D_8ZYvG5.js} +1 -1
  25. package/web/assets/{c4Diagram-7LVT6UL2-DY29eJER.js → c4Diagram-7LVT6UL2-CjaYAD8H.js} +1 -1
  26. package/web/assets/channel-D41IngZD.js +1 -0
  27. package/web/assets/{chunk-4HAMMTFA-Dwwu-TeK.js → chunk-4HAMMTFA-CYyeNdgB.js} +1 -1
  28. package/web/assets/{chunk-75Z2AOVW-CIRWwn6Y.js → chunk-75Z2AOVW-9eSzU1hH.js} +1 -1
  29. package/web/assets/{chunk-DU6HZSFF-BjC0SliR.js → chunk-DU6HZSFF-f2mc_gOc.js} +1 -1
  30. package/web/assets/{chunk-F27PBJKO-wa_cUFjb.js → chunk-F27PBJKO-GTHjKaWt.js} +1 -1
  31. package/web/assets/{chunk-GMAD6QVW-ldP8bvnz.js → chunk-GMAD6QVW-B4cWkFg0.js} +1 -1
  32. package/web/assets/{chunk-GVQU2GXP-BiIt8KQk.js → chunk-GVQU2GXP-Doh88dY5.js} +1 -1
  33. package/web/assets/{chunk-IMKFNOWR-Dy7EkDid.js → chunk-IMKFNOWR-evZ-skzX.js} +1 -1
  34. package/web/assets/{chunk-L3NEJ4N5-DrtR_CiV.js → chunk-L3NEJ4N5-dT84YG5m.js} +1 -1
  35. package/web/assets/{chunk-OSK3NFVY-Dnvo5XKB.js → chunk-OSK3NFVY--wC-pTsP.js} +1 -1
  36. package/web/assets/{chunk-P2QGCYS3-CdZdfs9H.js → chunk-P2QGCYS3-DoGDSquD.js} +1 -1
  37. package/web/assets/{chunk-POPQ4Y6H-DUdouJMc.js → chunk-POPQ4Y6H-C9yH5MMt.js} +1 -1
  38. package/web/assets/{chunk-PWAF6VOD-BMGGIPTG.js → chunk-PWAF6VOD-xmjxvk12.js} +1 -1
  39. package/web/assets/{chunk-SHT3W25Y-Mb0aPPgh.js → chunk-SHT3W25Y-DpzxUCCq.js} +1 -1
  40. package/web/assets/{chunk-SVP7TREG-C5eicczL.js → chunk-SVP7TREG-CBIQl1au.js} +1 -1
  41. package/web/assets/{chunk-TICWLB2K-CoKD47HN.js → chunk-TICWLB2K-jXdC4ntQ.js} +1 -1
  42. package/web/assets/classDiagram-ZZMXUADV-Co6_Cdek.js +1 -0
  43. package/web/assets/classDiagram-v2-VYDZK3BY-Co6_Cdek.js +1 -0
  44. package/web/assets/{cynefin-OW5HDTMX-BTzJNWu1.js → cynefin-OW5HDTMX-1yYUWfHS.js} +1 -1
  45. package/web/assets/{cynefinDiagram-5FMLGOSQ-Dd7g6wFx.js → cynefinDiagram-5FMLGOSQ-D5veF8XC.js} +1 -1
  46. package/web/assets/{dagre-GXQ25YYZ-D3zVJkwr.js → dagre-GXQ25YYZ-LCKXulr3.js} +1 -1
  47. package/web/assets/{diagram-S7CK7UJ4-S9vzG5Iu.js → diagram-S7CK7UJ4-BPZhUsIe.js} +1 -1
  48. package/web/assets/{diagram-UQ7AKVKN-Csc1xBzG.js → diagram-UQ7AKVKN-_I_1mnhT.js} +1 -1
  49. package/web/assets/{diagram-VSXAHHWV-Bigb1Thn.js → diagram-VSXAHHWV-Bi-jK_Tw.js} +1 -1
  50. package/web/assets/{diagram-VX7I27RA-D-FXM6TU.js → diagram-VX7I27RA-SdcuETX7.js} +1 -1
  51. package/web/assets/{diagram-Z3DM3KII-D8hJhbd2.js → diagram-Z3DM3KII-CK_8wm9X.js} +1 -1
  52. package/web/assets/{ebnfDiagram-PWID7BFC-DXvgoAAx.js → ebnfDiagram-PWID7BFC-BQYIrtCp.js} +1 -1
  53. package/web/assets/{erDiagram-RLTQ6QDP-ZBHJikuC.js → erDiagram-RLTQ6QDP-DZ4oAu3B.js} +1 -1
  54. package/web/assets/eventmodeling-NTZA5JFV-lCMmC-9N.js +1 -0
  55. package/web/assets/flowDiagram-HODETNUW-BgbYUorQ.js +1 -0
  56. package/web/assets/{ganttDiagram-EL5Y4UJY-CouBpzDS.js → ganttDiagram-EL5Y4UJY-OajJ0HO8.js} +1 -1
  57. package/web/assets/{gitGraph-4MIJSDKK-D_EH-2tK.js → gitGraph-4MIJSDKK-D7ihHY4y.js} +1 -1
  58. package/web/assets/{gitGraphDiagram-WWUBYQGX-CiYi0gV_.js → gitGraphDiagram-WWUBYQGX-tZPS5_ZS.js} +1 -1
  59. package/web/assets/{index-BqTUAMUo.js → index-CVAJisfq.js} +2 -2
  60. package/web/assets/{info-A6RAGUB7-CqxR-vqg.js → info-A6RAGUB7-qgTcQ5q7.js} +1 -1
  61. package/web/assets/{infoDiagram-27XIBGKW-CKjwiYqh.js → infoDiagram-27XIBGKW-CUqC2Hn0.js} +1 -1
  62. package/web/assets/{ishikawaDiagram-5VMMS53U-OKhyRTlP.js → ishikawaDiagram-5VMMS53U-3oPUqLcD.js} +1 -1
  63. package/web/assets/{journeyDiagram-3NMN7TZE-DjpB5OfX.js → journeyDiagram-3NMN7TZE-BNEGkbMD.js} +1 -1
  64. package/web/assets/{kanban-definition-UXKFOSKX-B8Xzxar1.js → kanban-definition-UXKFOSKX-Bp_UeKxn.js} +1 -1
  65. package/web/assets/{mermaid-parser.core-DiDXDDZp.js → mermaid-parser.core-D6tfqL1x.js} +3 -3
  66. package/web/assets/{mermaid.core-BcCoKrIv.js → mermaid.core-_9gpP23c.js} +4 -4
  67. package/web/assets/{mindmap-definition-YA3MSWOX-B37CHXfT.js → mindmap-definition-YA3MSWOX-W3i2E1kK.js} +1 -1
  68. package/web/assets/{mount-CyPeuIDQ.js → mount-wBvtB0Wr.js} +5 -5
  69. package/web/assets/{packet-AYTQ26CC-DADMJ2Yt.js → packet-AYTQ26CC-CHFLOgM5.js} +1 -1
  70. package/web/assets/{pegDiagram-XKGWAZYB-BwXOeSnI.js → pegDiagram-XKGWAZYB-Bk_pKocX.js} +1 -1
  71. package/web/assets/{pie-WAS4IAKB-D9hNG31t.js → pie-WAS4IAKB-CGsCI2xw.js} +1 -1
  72. package/web/assets/{pieDiagram-E7YTZNPT-BjQhU1Mo.js → pieDiagram-E7YTZNPT-CTXXQyut.js} +1 -1
  73. package/web/assets/{quadrantDiagram-AXDQQJYC-BUmmIEMt.js → quadrantDiagram-AXDQQJYC-B68enp1E.js} +1 -1
  74. package/web/assets/{radar-RG4KPBEZ-DW58Z5Mj.js → radar-RG4KPBEZ-2L7avVui.js} +1 -1
  75. package/web/assets/{railroad-74A4TZTK-Bxb0GBL6.js → railroad-74A4TZTK-BbnL91ru.js} +1 -1
  76. package/web/assets/railroad-abnf-HS5TGJTU-BkQBYxO_.js +1 -0
  77. package/web/assets/railroad-ebnf-LZEXJU2U-e2fBEWtf.js +1 -0
  78. package/web/assets/railroad-peg-WCYAUIDC-BxZNtI59.js +1 -0
  79. package/web/assets/{railroadDiagram-O6MQD6OU-CqOaqCxM.js → railroadDiagram-O6MQD6OU-BFzOlAw5.js} +1 -1
  80. package/web/assets/{requirementDiagram-BXWQKSXE-B2Mihs8T.js → requirementDiagram-BXWQKSXE-BYbscRnJ.js} +1 -1
  81. package/web/assets/{sankeyDiagram-P5KCCOFB-DI2cV18A.js → sankeyDiagram-P5KCCOFB-BrqGvX7c.js} +1 -1
  82. package/web/assets/{sequenceDiagram-WJ2MYXX4-Ct6KM5nc.js → sequenceDiagram-WJ2MYXX4-D57SwjIA.js} +1 -1
  83. package/web/assets/{settingsRoute-DiYTWwf-.js → settingsRoute-DR3oWvRe.js} +1 -1
  84. package/web/assets/{stateDiagram-D77RDMKH-0z6WQwlg.js → stateDiagram-D77RDMKH-CbWa8uWb.js} +1 -1
  85. package/web/assets/stateDiagram-v2-MP3YSRHH-rYTuDWOL.js +1 -0
  86. package/web/assets/{swimlanes-42K2YHIH-mLnDFml4.js → swimlanes-42K2YHIH-Db1Z11QB.js} +1 -1
  87. package/web/assets/swimlanesDiagram-VR7AAH4N-CaOU_rKU.js +8 -0
  88. package/web/assets/{timeline-definition-24CTP7MA-CSZZvZUD.js → timeline-definition-24CTP7MA-CFvEEzXN.js} +1 -1
  89. package/web/assets/{treeView-Q6P3EWNA-nkM4Mhz8.js → treeView-Q6P3EWNA-B06daggs.js} +1 -1
  90. package/web/assets/{treemap-WGGIJYW6-D0q8GQM0.js → treemap-WGGIJYW6-QlWbZ0PY.js} +1 -1
  91. package/web/assets/{useStore-0Cz83n1l.js → useStore-ByZarnni.js} +1 -1
  92. package/web/assets/{vennDiagram-4TSXK5OY-DMh3_TDy.js → vennDiagram-4TSXK5OY-R_UCGezy.js} +1 -1
  93. package/web/assets/{wardley-WFR3VGLG-C1n_QCAB.js → wardley-WFR3VGLG-D2xzUpJ1.js} +1 -1
  94. package/web/assets/{wardleyDiagram-VM6X3IG4-C6ap89Zw.js → wardleyDiagram-VM6X3IG4-B30QUmzL.js} +1 -1
  95. package/web/assets/{xychartDiagram-S5SC5T6Z-B27MAKKy.js → xychartDiagram-S5SC5T6Z-BV8t7MaL.js} +1 -1
  96. package/web/index.html +1 -1
  97. package/web/sw.js +1 -1
  98. package/web/assets/architecture-7GRP2DOG-jzSX7bLG.js +0 -1
  99. package/web/assets/channel-C1pvpPyo.js +0 -1
  100. package/web/assets/classDiagram-ZZMXUADV-LoptuGLH.js +0 -1
  101. package/web/assets/classDiagram-v2-VYDZK3BY-LoptuGLH.js +0 -1
  102. package/web/assets/eventmodeling-NTZA5JFV-QziTZg_i.js +0 -1
  103. package/web/assets/flowDiagram-HODETNUW-hMB4P6zf.js +0 -1
  104. package/web/assets/railroad-abnf-HS5TGJTU-D71MxTg_.js +0 -1
  105. package/web/assets/railroad-ebnf-LZEXJU2U-DuU1fM8N.js +0 -1
  106. package/web/assets/railroad-peg-WCYAUIDC-B0-xZffZ.js +0 -1
  107. package/web/assets/stateDiagram-v2-MP3YSRHH-CrCsCpWo.js +0 -1
  108. package/web/assets/swimlanesDiagram-VR7AAH4N-CRl9QwhL.js +0 -8
@@ -129,7 +129,7 @@ export const COMMANDS = [
129
129
 
130
130
  // Help and discovery
131
131
  { 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",
132
- 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" }] }] },
132
+ 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, automate or add-an-agent." } }, argv: ["guide", { arg: "topic" }] }] },
133
133
  { 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" },
134
134
  { name: "version", aliases: ["--version", "-v"], group: "help", scope: "node", usage: "version", summary: "Print the installed Bivy version" },
135
135
 
@@ -0,0 +1,39 @@
1
+ # Add an agent: any ACP agent or CLI
2
+
3
+ Summary: Make another coding agent available in Bivy, writing a small adapter when its CLI needs one.
4
+
5
+ An agent with an ACP mode needs only its command:
6
+
7
+ bivy agent add gemini-acp --command gemini --args '["--experimental-acp"]'
8
+
9
+ Any other CLI that takes a prompt on stdin (or as its last argument with
10
+ `--prompt-mode argv`):
11
+
12
+ bivy agent add review-bot --transport process --command review-bot --args '["run"]'
13
+
14
+ It is in the picker at once. Check it with one real turn, which prints the answer:
15
+
16
+ bivy exec -a gemini-acp "Say hello"
17
+
18
+ When the CLI's output needs translating, write an adapter: a program Bivy runs
19
+ once per turn with the prompt on stdin. It prints one JSON event per line:
20
+
21
+ {"type":"session.started","sessionId":"abc"} # passed back as {id} to resume
22
+ {"type":"message.reasoning","text":"…"}
23
+ {"type":"tool.call","toolCallId":"t1","name":"bash","input":{"command":"ls"}}
24
+ {"type":"tool.result","toolCallId":"t1","result":"…"}
25
+ {"type":"message.delta","text":"The answer…"}
26
+ {"type":"usage","tokens":{"input_tokens":10,"output_tokens":20}}
27
+ {"type":"session.done"} # or {"type":"session.error","error":"…"}
28
+
29
+ Start from examples/agents/jsonl-adapter/adapter.mjs in the Bivy repo, then:
30
+
31
+ bivy agent add my-agent --transport process --parser bivy-protocol \
32
+ --command "$PWD/adapter.mjs" --resume-args '["--resume","{id}"]'
33
+ bivy exec -a my-agent "List the files here"
34
+
35
+ These events report what the agent did; they cannot ask before a tool runs. For
36
+ approvals, the adapter must speak ACP (`--transport acp`).
37
+
38
+ The entry lives under `agents:` in the node's config.yaml (`bivy agent list`,
39
+ `bivy agent remove <id>`). The full format is in docs/add-an-agent.md.
package/dist/agent-cli.js CHANGED
@@ -1,21 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
  // Copyright (c) 2026 Petter André Sjulstad
4
- /** First-class BYO-agent convenience CLI backed by the ordinary plugin manifest. */
4
+ /** First-class bring-your-own-agent CLI: writes `agents:` in the node config.yaml. */
5
5
  import fs from "node:fs";
6
- import os from "node:os";
7
6
  import path from "node:path";
8
- import { stringify as stringifyYaml } from "yaml";
9
- import { currentBivyVersion } from "./app-version.js";
10
7
  import { defaultDataDir } from "./data-dir.js";
11
- import { recommendedBivyRange } from "./plugin-sdk/index.js";
12
- import { installPlugin, listInstalledPlugins, removePlugin } from "./plugins/store.js";
8
+ import { mergeLegacyIntoNodeConfig, nodeConfigPath, readNodeConfig, writeNodeConfig } from "./node-config.js";
9
+ import { listInstalledPlugins, removePlugin } from "./plugins/store.js";
13
10
  function option(args, name) {
14
11
  const index = args.indexOf(name);
15
12
  return index >= 0 ? args[index + 1] : undefined;
16
13
  }
17
14
  function positional(args) {
18
- const valued = new Set(["--command", "--transport", "--adapter", "--args", "--name", "--prompt-mode", "--data-dir"]);
15
+ const valued = new Set(["--command", "--transport", "--adapter", "--args", "--name", "--prompt-mode", "--parser", "--resume-args", "--data-dir"]);
19
16
  const out = [];
20
17
  for (let index = 0; index < args.length; index += 1) {
21
18
  if (valued.has(args[index])) {
@@ -42,17 +39,35 @@ function parseArgs(value) {
42
39
  parsed = JSON.parse(value);
43
40
  }
44
41
  catch {
45
- throw new Error("--args must be a JSON string array");
42
+ throw new Error("--args and --resume-args must be JSON string arrays");
46
43
  }
47
44
  if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
48
- throw new Error("--args must be a JSON string array");
45
+ throw new Error("--args and --resume-args must be JSON string arrays");
49
46
  }
50
47
  return parsed;
51
48
  }
52
- function localManifest(args) {
49
+ function readJson(dataDir, name) {
50
+ try {
51
+ const value = JSON.parse(fs.readFileSync(path.join(dataDir, name), "utf8"));
52
+ return value && typeof value === "object" ? value : {};
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ /** The node's config.yaml, or the one the daemon would create from legacy files
59
+ * — never a bare file that would skip that migration. */
60
+ function nodeConfig(dataDir) {
61
+ return readNodeConfig(dataDir) ?? mergeLegacyIntoNodeConfig(readJson(dataDir, "cli.json"), readJson(dataDir, "settings.json"));
62
+ }
63
+ function agentId(args) {
53
64
  const id = positional(args)[1]?.trim().toLowerCase() ?? "";
54
65
  if (!/^[a-z][a-z0-9-]{1,47}$/.test(id))
55
66
  throw new Error("Agent id must be a lowercase slug of 2-48 characters");
67
+ return id;
68
+ }
69
+ /** `bivy agent add` flags → the `adapter` block config.yaml and plugins share. */
70
+ function agentConfig(args, id) {
56
71
  const command = option(args, "--command")?.trim();
57
72
  if (!command)
58
73
  throw new Error("--command is required");
@@ -61,29 +76,26 @@ function localManifest(args) {
61
76
  throw new Error("--transport must be acp or process");
62
77
  const adapterArgs = parseArgs(option(args, "--args"));
63
78
  const name = option(args, "--name")?.trim() || id.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
64
- const promptModeValue = option(args, "--prompt-mode") ?? "stdin";
65
- if (promptModeValue !== "stdin" && promptModeValue !== "argv")
79
+ if (transport === "acp") {
80
+ for (const flag of ["--prompt-mode", "--parser", "--resume-args"])
81
+ if (option(args, flag))
82
+ throw new Error(`${flag} applies to --transport process only`);
83
+ return { name, adapter: { kind: "acp", command, ...(adapterArgs.length ? { args: adapterArgs } : {}) } };
84
+ }
85
+ const promptMode = option(args, "--prompt-mode") ?? "stdin";
86
+ if (promptMode !== "stdin" && promptMode !== "argv")
66
87
  throw new Error("--prompt-mode must be stdin or argv");
67
- const promptMode = promptModeValue;
88
+ const parser = option(args, "--parser");
89
+ const resumeArgs = option(args, "--resume-args");
68
90
  return {
69
- apiVersion: "bivy.sh/v1alpha1",
70
- kind: "Plugin",
71
- metadata: {
72
- id,
73
- name,
74
- version: "0.1.0",
75
- description: `Local integration for the ${name} agent.`,
76
- },
77
- requires: { bivy: recommendedBivyRange(currentBivyVersion()) },
78
- contributes: {
79
- agents: [{
80
- id,
81
- name,
82
- authOwner: "agent",
83
- adapter: transport === "acp"
84
- ? { kind: "acp", command, ...(adapterArgs.length ? { args: adapterArgs } : {}) }
85
- : { kind: "process", command, ...(adapterArgs.length ? { args: adapterArgs } : {}), promptMode },
86
- }],
91
+ name,
92
+ adapter: {
93
+ kind: "process",
94
+ command,
95
+ ...(adapterArgs.length ? { args: adapterArgs } : {}),
96
+ promptMode,
97
+ ...(parser ? { structured: { args: adapterArgs, parser } } : {}),
98
+ ...(resumeArgs ? { resume: { args: parseArgs(resumeArgs) } } : {}),
87
99
  },
88
100
  };
89
101
  }
@@ -94,64 +106,80 @@ async function main() {
94
106
  const dataDir = path.resolve(option(args, "--data-dir") ?? defaultDataDir());
95
107
  if (!command || command === "help" || args.includes("--help") || args.includes("-h")) {
96
108
  console.log(`Usage:
97
- bivy agent add <id> --command <command> [--transport acp|process] [--args '["..."]']
109
+ bivy agent add <id> --command <command> [--args '["..."]'] [--name <name>] [--force]
110
+ ACP agent (default): --transport acp
111
+ Any other CLI: --transport process [--prompt-mode stdin|argv]
112
+ [--parser <id>] [--resume-args '["...","{id}"]']
98
113
  bivy agent list [--json]
99
- bivy agent remove <integration-id>
114
+ bivy agent remove <id>
100
115
 
101
- Adds an existing user-owned agent through the same declarative package contract used by every external integration.`);
116
+ Agents are saved under agents: in <data-dir>/config.yaml and appear in the
117
+ picker right away. Edit that file directly for every adapter option.`);
102
118
  return;
103
119
  }
104
120
  try {
105
121
  if (command === "add") {
106
- const manifest = localManifest(args);
107
- const existingPackage = listInstalledPlugins(dataDir).some((plugin) => plugin.id === manifest.metadata.id);
108
- if (!existingPackage) {
122
+ const id = agentId(args);
123
+ const spec = agentConfig(args, id);
124
+ const config = nodeConfig(dataDir);
125
+ const replaced = Boolean(config.agents?.[id]);
126
+ if (replaced && !args.includes("--force"))
127
+ throw new Error(`Agent ${id} is already configured; pass --force to replace it`);
128
+ if (!replaced) {
109
129
  const runtimeModule = await import("./runtime/index.js");
110
- const retained = runtimeModule.canonicalAgentId(manifest.contributes.agents[0].id);
130
+ const retained = runtimeModule.canonicalAgentId(id);
111
131
  if (retained)
112
- throw new Error(`Agent id ${manifest.contributes.agents[0].id} conflicts with retained integration ${retained}; choose a unique id`);
113
- }
114
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), "bivy-agent-add-"));
115
- const file = path.join(temp, "bivy.plugin.yaml");
116
- try {
117
- fs.writeFileSync(file, stringifyYaml(manifest), { mode: 0o600 });
118
- const result = installPlugin(file, { dataDir, force: args.includes("--force"), bivyVersion: currentBivyVersion() });
119
- const body = { ok: true, id: manifest.metadata.id, path: result.path, replaced: result.replaced, restartRequired: true };
120
- if (json)
121
- console.log(JSON.stringify(body, null, 2));
122
- else {
123
- console.log(`${result.replaced ? "Updated" : "Added"} agent integration ${manifest.metadata.name} (${manifest.metadata.id}).`);
124
- console.log("Restart Bivy to activate it: bivy restart");
125
- }
126
- }
127
- finally {
128
- fs.rmSync(temp, { recursive: true, force: true });
132
+ throw new Error(`Agent id ${id} conflicts with ${retained}; choose a unique id`);
129
133
  }
134
+ writeNodeConfig(dataDir, { ...config, agents: { ...config.agents, [id]: spec } });
135
+ const body = { ok: true, id, path: nodeConfigPath(dataDir), replaced };
136
+ if (json)
137
+ console.log(JSON.stringify(body, null, 2));
138
+ else
139
+ console.log(`${replaced ? "Updated" : "Added"} ${"name" in spec ? spec.name : id} (${id}). It is in the agent picker now.`);
130
140
  return;
131
141
  }
132
142
  if (command === "list" || command === "ls") {
133
- const rows = listInstalledPlugins(dataDir).flatMap((plugin) => plugin.manifest?.contributes.agents.map((agent) => ({
143
+ const configured = Object.entries(readNodeConfig(dataDir)?.agents ?? {}).map(([id, spec]) => ({
144
+ id,
145
+ name: "adapter" in spec ? spec.name ?? id : spec.label ?? id,
146
+ source: "config",
147
+ transport: "adapter" in spec ? spec.adapter.kind : `extends ${spec.extends}`,
148
+ command: "adapter" in spec ? spec.adapter.command : spec.command ?? spec.extends,
149
+ }));
150
+ const packaged = listInstalledPlugins(dataDir).flatMap((plugin) => plugin.manifest?.contributes.agents.map((agent) => ({
134
151
  id: agent.id,
135
152
  name: agent.name,
136
- package: plugin.id,
137
- version: plugin.manifest.metadata.version,
153
+ source: `${plugin.id}@${plugin.manifest.metadata.version}`,
138
154
  transport: agent.adapter.kind,
139
155
  command: agent.adapter.command,
140
- status: plugin.errors.length ? "invalid" : "installed",
156
+ ...(plugin.errors.length ? { status: "invalid" } : {}),
141
157
  })) ?? []);
158
+ const rows = [...configured, ...packaged];
142
159
  if (json)
143
160
  console.log(JSON.stringify({ ok: true, agents: rows }, null, 2));
144
161
  else if (!rows.length)
145
- console.log("No installed agent integration packages.");
162
+ console.log("No agents added on this machine. Add one with: bivy agent add <id> --command <command>");
146
163
  else
147
164
  for (const row of rows)
148
- console.log(`${row.id}\t${row.transport}\t${row.command}\t${row.package}@${row.version}`);
165
+ console.log(`${row.id}\t${row.transport}\t${row.command}\t${row.source}`);
149
166
  return;
150
167
  }
151
168
  if (command === "remove" || command === "rm") {
152
169
  const id = positional(args)[1];
153
170
  if (!id)
154
- fail("Usage: bivy agent remove <integration-id>", json);
171
+ fail("Usage: bivy agent remove <id>", json);
172
+ const config = readNodeConfig(dataDir);
173
+ if (config?.agents?.[id]) {
174
+ const { [id]: _removed, ...agents } = config.agents;
175
+ writeNodeConfig(dataDir, { ...config, agents });
176
+ if (json)
177
+ console.log(JSON.stringify({ ok: true, id, removed: true }));
178
+ else
179
+ console.log(`Removed ${id}.`);
180
+ return;
181
+ }
182
+ // Agents added before config.yaml held them were single-agent plugin packages.
155
183
  const installed = listInstalledPlugins(dataDir);
156
184
  const direct = installed.find((plugin) => plugin.id === id);
157
185
  const matches = direct ? [direct] : installed.filter((plugin) => plugin.manifest?.contributes.agents.some((agent) => agent.id === id));
@@ -162,7 +190,7 @@ Adds an existing user-owned agent through the same declarative package contract
162
190
  if (json)
163
191
  console.log(JSON.stringify({ ok: true, id, packageId, removed }));
164
192
  else
165
- console.log(removed ? `Removed ${id} (${packageId}). Restart Bivy to deactivate it.` : `Agent integration ${id} is not installed.`);
193
+ console.log(removed ? `Removed ${id} (${packageId}).` : `Agent ${id} is not configured.`);
166
194
  return;
167
195
  }
168
196
  fail(`Unknown agent command: ${command}`, json);
@@ -691,6 +691,8 @@ class ClaudeSession {
691
691
  messages = [];
692
692
  streaming = false;
693
693
  name;
694
+ /** Set by the runtime so a disposed session leaves its registry. */
695
+ onDispose;
694
696
  models = [];
695
697
  currentModel;
696
698
  desiredModel;
@@ -1390,6 +1392,7 @@ class ClaudeSession {
1390
1392
  }
1391
1393
  }
1392
1394
  this.emitter.removeAllListeners();
1395
+ this.onDispose?.();
1393
1396
  }
1394
1397
  async getModels() {
1395
1398
  // Re-query the live agent so the picker reflects the current lineup instead
@@ -1515,6 +1518,16 @@ export class ClaudeCodeRuntime {
1515
1518
  constructor(options = {}) {
1516
1519
  this.options = options;
1517
1520
  }
1521
+ /** Each session holds its whole transcript, so it stays registered only
1522
+ * while open; a closed one would otherwise be kept for the daemon's lifetime. */
1523
+ track(session) {
1524
+ this.sessions.push(session);
1525
+ session.onDispose = () => {
1526
+ const i = this.sessions.indexOf(session);
1527
+ if (i >= 0)
1528
+ this.sessions.splice(i, 1);
1529
+ };
1530
+ }
1518
1531
  /** Claude Code runs Anthropic models; the authoritative list comes from the
1519
1532
  * live query per session, so the session-less catalog is the known lineup. */
1520
1533
  listCatalog() {
@@ -1522,12 +1535,12 @@ export class ClaudeCodeRuntime {
1522
1535
  }
1523
1536
  async createSession(options) {
1524
1537
  const session = new ClaudeSession({ ...(await withSessionCredentials(this.options, options.credentialLabels)), instructions: options.instructions?.text }, options.workspace, options.toolInterceptor, options.toolProvider);
1525
- this.sessions.push(session);
1538
+ this.track(session);
1526
1539
  return { session };
1527
1540
  }
1528
1541
  async openSession(options) {
1529
1542
  const session = new ClaudeSession({ ...(await withSessionCredentials(this.options, options.credentialLabels)), instructions: options.instructions?.text }, options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
1530
- this.sessions.push(session);
1543
+ this.track(session);
1531
1544
  return {
1532
1545
  session,
1533
1546
  warning: "Resumed Claude Code session history is replayed by the agent on the next turn; prior messages are not preloaded.",
@@ -1732,15 +1745,11 @@ export class ClaudeCodeRuntime {
1732
1745
  // Drop (and tear down) any in-memory handle so the fallback branch of
1733
1746
  // listSessions (used when no sessionsDir is set) can't re-advertise it, and a
1734
1747
  // still-live child doesn't outlive the delete.
1735
- for (let i = this.sessions.length - 1; i >= 0; i--) {
1736
- const s = this.sessions[i];
1737
- if (s.id === sessionId || (sessionFile && s.sessionFile === sessionFile)) {
1738
- try {
1739
- s.dispose();
1740
- }
1741
- catch { /* already disposed by the caller's close */ }
1742
- this.sessions.splice(i, 1);
1748
+ for (const s of this.sessions.filter((s) => s.id === sessionId || (sessionFile && s.sessionFile === sessionFile))) {
1749
+ try {
1750
+ s.dispose();
1743
1751
  }
1752
+ catch { /* already disposed by the caller's close */ }
1744
1753
  }
1745
1754
  // Prefer the exact path the SDK reports (that's the file it would re-read on
1746
1755
  // the next list); fall back to locating the transcript by id across Claude's
@@ -4,6 +4,7 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7
+ import { parseAgentAdapter } from "./plugin-sdk/manifest.js";
7
8
  export const NODE_CONFIG_FILE = "config.yaml";
8
9
  export const NODE_CONFIG_VERSION = 1;
9
10
  const ROOT_KEYS = ["version", "node", "defaults", "safety", "sessions", "github", "automation", "agents", "environment"];
@@ -170,6 +171,23 @@ export function validateNodeConfig(value) {
170
171
  errors.push(`${at} must be an object`);
171
172
  continue;
172
173
  }
174
+ if (spec.adapter !== undefined) {
175
+ unknownKeys(spec, ["name", "description", "hidden", "authOwner", "adapter"], at, errors);
176
+ const adapter = parseAgentAdapter(spec.adapter, `${at}.adapter`, errors);
177
+ const authOwner = spec.authOwner;
178
+ if (authOwner !== undefined && authOwner !== "agent" && authOwner !== "bivy" && authOwner !== "mixed")
179
+ errors.push(`${at}.authOwner must be agent, bivy, or mixed`);
180
+ const name = optionalString(spec.name, `${at}.name`, errors, 120);
181
+ const description = optionalString(spec.description, `${at}.description`, errors, 500);
182
+ const hidden = optionalBoolean(spec.hidden, `${at}.hidden`, errors);
183
+ if (adapter)
184
+ agents[id] = { name, description, hidden, authOwner, adapter };
185
+ continue;
186
+ }
187
+ if (spec.extends === undefined) {
188
+ errors.push(`${at} needs an adapter (any ACP or CLI command) or extends (a maintained agent id)`);
189
+ continue;
190
+ }
173
191
  unknownKeys(spec, ["extends", "label", "command", "args", "jsonArgs", "parserId", "promptMode", "hidden"], at, errors);
174
192
  const extend = optionalString(spec.extends, `${at}.extends`, errors, 80);
175
193
  const list = (value, key) => {
@@ -247,13 +247,13 @@ export async function main(argv = process.argv.slice(2)) {
247
247
  if (!input)
248
248
  fail("Usage: bivy plugin install <path> [--force]", json);
249
249
  const result = installPlugin(input, { dataDir, force: args.includes("--force") });
250
- const body = { ok: true, installed: result.manifest.metadata.id, version: result.manifest.metadata.version, path: result.path, replaced: result.replaced, restartRequired: true };
250
+ const body = { ok: true, installed: result.manifest.metadata.id, version: result.manifest.metadata.version, path: result.path, replaced: result.replaced };
251
251
  if (json)
252
252
  outputJson(body);
253
253
  else {
254
254
  console.log(`${result.replaced ? "Updated" : "Installed"} ${result.manifest.metadata.name} (${result.manifest.metadata.id}@${result.manifest.metadata.version}).`);
255
255
  console.log(`Stored at ${result.path}`);
256
- console.log("Restart Bivy to activate it: bivy restart");
256
+ console.log("It is in the agent picker now.");
257
257
  }
258
258
  return;
259
259
  }
@@ -300,12 +300,11 @@ export async function main(argv = process.argv.slice(2)) {
300
300
  const removed = removePlugin(id, dataDir);
301
301
  if (!removed)
302
302
  fail(`Plugin ${id} is not installed`, json);
303
- const body = { ok: true, removed: id, restartRequired: true };
303
+ const body = { ok: true, removed: id };
304
304
  if (json)
305
305
  outputJson(body);
306
306
  else {
307
307
  console.log(`Removed plugin ${id}.`);
308
- console.log("Restart Bivy to unload it: bivy restart");
309
308
  }
310
309
  return;
311
310
  }
@@ -179,6 +179,21 @@ function parseAcpAdapter(raw, at, errors) {
179
179
  const args = stringList(raw.args, `${at}.args`, errors);
180
180
  return command ? { kind: "acp", command, ...(args ? { args } : {}) } : undefined;
181
181
  }
182
+ /** Validate one `adapter` block. Plugin manifests and node `config.yaml` agents
183
+ * share it, so an agent is described the same way wherever it is declared. */
184
+ export function parseAgentAdapter(value, at, errors) {
185
+ const raw = object(value);
186
+ if (!raw) {
187
+ errors.push(`${at} must be an object`);
188
+ return undefined;
189
+ }
190
+ if (raw.kind === "process")
191
+ return parseProcessAdapter(raw, at, errors);
192
+ if (raw.kind === "acp")
193
+ return parseAcpAdapter(raw, at, errors);
194
+ errors.push(`${at}.kind must be process or acp`);
195
+ return undefined;
196
+ }
182
197
  function parseAgent(value, index, errors) {
183
198
  const at = `contributes.agents[${index}]`;
184
199
  const raw = object(value);
@@ -198,16 +213,7 @@ function parseAgent(value, index, errors) {
198
213
  if (authOwner !== undefined && authOwner !== "agent" && authOwner !== "bivy" && authOwner !== "mixed") {
199
214
  errors.push(`${at}.authOwner must be agent, bivy, or mixed`);
200
215
  }
201
- const adapterRaw = object(raw.adapter);
202
- let adapter;
203
- if (!adapterRaw)
204
- errors.push(`${at}.adapter must be an object`);
205
- else if (adapterRaw.kind === "process")
206
- adapter = parseProcessAdapter(adapterRaw, `${at}.adapter`, errors);
207
- else if (adapterRaw.kind === "acp")
208
- adapter = parseAcpAdapter(adapterRaw, `${at}.adapter`, errors);
209
- else
210
- errors.push(`${at}.adapter.kind must be process or acp`);
216
+ const adapter = parseAgentAdapter(raw.adapter, `${at}.adapter`, errors);
211
217
  if (!id || !name || !adapter)
212
218
  return undefined;
213
219
  return {
@@ -351,5 +357,7 @@ export function recommendedBivyRange(currentVersion) {
351
357
  const parsed = parseSemver(currentVersion);
352
358
  if (!parsed)
353
359
  throw new Error(`Cannot generate a compatibility range from invalid Bivy version ${currentVersion}`);
354
- return `>=${parsed.major}.${parsed.minor}.${parsed.patch} <${parsed.major}.${parsed.minor + 1}.0`;
360
+ // The manifest contract is versioned by apiVersion, so a plugin keeps working
361
+ // across Bivy minor releases; only a major release may break it.
362
+ return `>=${parsed.major}.${parsed.minor}.${parsed.patch} <${parsed.major + 1}.0.0`;
355
363
  }
@@ -241,6 +241,7 @@ export function bivyProtocolParser() {
241
241
  // appended verbatim (a shim emits an explicit break when it wants one), so it
242
242
  // opts out of inferred segment boundaries.
243
243
  const acc = new TurnAccumulator({ provider: "bivy-protocol", protocol: "protocol" }, false);
244
+ let sessionRef;
244
245
  return {
245
246
  onLine(line) {
246
247
  const events = [];
@@ -255,9 +256,23 @@ export function bivyProtocolParser() {
255
256
  return events; // ignore non-JSON noise (banners etc.)
256
257
  }
257
258
  const type = String(msg.type ?? "");
258
- if (type === "message.delta") {
259
+ if (type === "session.started") {
260
+ // The adapter's own conversation id; `resume.args` gets it back as {id}.
261
+ if (typeof msg.sessionId === "string" && msg.sessionId.trim())
262
+ sessionRef = msg.sessionId.trim();
263
+ }
264
+ else if (type === "message.delta") {
259
265
  acc.appendText(String(msg.text ?? ""), events);
260
266
  }
267
+ else if (type === "message.reasoning") {
268
+ acc.appendReasoning(String(msg.text ?? ""), events);
269
+ }
270
+ else if (type === "usage") {
271
+ const tokens = extractTokenUsage(msg.tokens ?? msg);
272
+ const costUsd = typeof msg.costUsd === "number" ? { costUsd: msg.costUsd } : {};
273
+ if (tokens || costUsd.costUsd !== undefined)
274
+ acc.addUsage({ ...tokens, ...costUsd });
275
+ }
261
276
  else if (type === "session.status") {
262
277
  const status = String(msg.status ?? "");
263
278
  if (status === "working")
@@ -288,6 +303,8 @@ export function bivyProtocolParser() {
288
303
  return events;
289
304
  },
290
305
  messages: () => acc.history(),
306
+ usage: () => acc.usageSnapshot,
307
+ sessionRef: () => sessionRef,
291
308
  };
292
309
  }
293
310
  /** Parser for Claude Code CLI `--output-format stream-json`. */