@bivy/bivy 0.21.1 → 0.21.2-staging.10
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/archive-install.mjs +150 -0
- package/bin/bivy.mjs +66 -7
- package/bin/cli-commands.mjs +1 -1
- package/bin/guides/add-an-agent.md +39 -0
- package/bin/prune-sessions.mjs +46 -14
- package/dist/agent-cli.js +91 -63
- package/dist/agents/claude-code/runtime.js +19 -10
- package/dist/apps/styles.css +1 -50
- package/dist/automation-reseal.js +2 -0
- package/dist/node-config.js +18 -0
- package/dist/node-update.js +26 -1
- package/dist/plugin-cli.js +3 -4
- package/dist/plugin-sdk/manifest.js +19 -11
- package/dist/runtime/cli-parsers.js +18 -1
- package/dist/runtime/index.js +71 -41
- package/dist/server.js +38 -34
- package/dist/worktree.js +17 -0
- package/package.json +1 -1
- package/web/assets/AutomationsView-DRwMTpsz.js +51 -0
- package/web/assets/{ChangesCard-CxQKGf6b.js → ChangesCard-C5rvJ8Cq.js} +3 -3
- package/web/assets/{LibraryView-D3uu06xU.js → LibraryView-CIJ-Pfv-.js} +1 -1
- package/web/assets/ProviderConnect-DiFSOT6L.js +1 -0
- package/web/assets/SessionList-CxRibKT5.js +1 -0
- package/web/assets/{Settings-DrK5DKuy.js → Settings-BukJBTwA.js} +8 -8
- package/web/assets/{Sheet-C8gx0OoI.js → Sheet-CfbcQeWy.js} +1 -1
- package/web/assets/{Terminal-DrqMIIhr.js → Terminal-7Q0Os9n9.js} +5 -5
- package/web/assets/{VoiceSettings-CBBuyKN7.js → VoiceSettings-DwIi92pR.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-CgIjZZoL.js → abnfDiagram-VCTEODGH-Czjfvfve.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-DD5qRKOE.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-BMOlU5hT.js → architectureDiagram-5GKGNRK7-DYay_IWI.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-DkwymH1p.js → blockDiagram-I7D4REHJ-CPlmINAT.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-B0UDgtWS.js → c4Diagram-7LVT6UL2-DYcLdJNN.js} +1 -1
- package/web/assets/channel-BUg-Y_4E.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-1y76NzcS.js → chunk-4HAMMTFA-CdyB2GrZ.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-CPFkG_FO.js → chunk-75Z2AOVW-D7h9tbn8.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-Dy4LQ-iK.js → chunk-DU6HZSFF-BISdfBlT.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-cjpOzWOK.js → chunk-F27PBJKO-2JkB2zU9.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-CpYC8bxK.js → chunk-GMAD6QVW-BfxS2p5H.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-DUT271z8.js → chunk-GVQU2GXP-vBPS78dh.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-C-G3w5yT.js → chunk-IMKFNOWR-C1PvbEPt.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-C5N0po6u.js → chunk-L3NEJ4N5-wug0ZtiI.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-7_vZDCyQ.js → chunk-OSK3NFVY-CSx22WTp.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-BhXxw6WP.js → chunk-P2QGCYS3-BEJVL1Uw.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-iw5O0upC.js → chunk-POPQ4Y6H-DuEsyMhL.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-C8u0Giuo.js → chunk-PWAF6VOD-DVLOKu0r.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-BM_QeCu-.js → chunk-SHT3W25Y-Opayr0C_.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-cLDkReFI.js → chunk-SVP7TREG-DgHJUbyu.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-C8fbKbO_.js → chunk-TICWLB2K-DOOCLkvY.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-DPpdYhAI.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-DPpdYhAI.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-6kzYNsQ0.js → cynefin-OW5HDTMX-1MBj3btC.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-B3tl5cyI.js → cynefinDiagram-5FMLGOSQ-Cc6rrG7K.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-mHX4cPQQ.js → dagre-GXQ25YYZ-DFG3Ndtk.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-DhOtHdTh.js → diagram-S7CK7UJ4-SiM6_pqS.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-B6BD-P5q.js → diagram-UQ7AKVKN-BUI0Ju7Z.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-DWXWifeJ.js → diagram-VSXAHHWV-CXMYZkbm.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DcU0vblN.js → diagram-VX7I27RA-CNCbmY76.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-Tc32VaTG.js → diagram-Z3DM3KII-Cf3mrdyV.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-fbSC7Ytl.js → ebnfDiagram-PWID7BFC-Ban7TN8N.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-VCd4vHPL.js → erDiagram-RLTQ6QDP-yDvXNUaN.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-BktxSOvh.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-CuT_k3cJ.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-qlPJPHC9.js → ganttDiagram-EL5Y4UJY-CRtk9R_T.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-Frn6_UlZ.js → gitGraph-4MIJSDKK-DVCE-pa5.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-CJv1U3iy.js → gitGraphDiagram-WWUBYQGX-BGNEoPtQ.js} +1 -1
- package/web/assets/{index-DoVsvSyc.js → index-Bkcd452U.js} +2 -2
- package/web/assets/index-CkoStrAe.css +1 -0
- package/web/assets/{info-A6RAGUB7-x2ehGecF.js → info-A6RAGUB7-CmlSnIUY.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-CzHIphTX.js → infoDiagram-27XIBGKW-CV6e39nj.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-1aIpAo5f.js → ishikawaDiagram-5VMMS53U-BZ317iz5.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-DZFs2eed.js → journeyDiagram-3NMN7TZE-aRfKE67B.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-CFPF7vk4.js → kanban-definition-UXKFOSKX-DRAkPvek.js} +1 -1
- package/web/assets/{mermaid-parser.core-eET97OnM.js → mermaid-parser.core-BzKTg0uA.js} +3 -3
- package/web/assets/{mermaid.core-D-l2iyMD.js → mermaid.core-cF9385Jc.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-CzO4WiUd.js → mindmap-definition-YA3MSWOX-CLiDn2-4.js} +1 -1
- package/web/assets/mount-DdwNnK_L.js +36 -0
- package/web/assets/{packet-AYTQ26CC-DiT50-0V.js → packet-AYTQ26CC-C_A2hl_f.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-DG3Ad8mS.js → pegDiagram-XKGWAZYB-DzWlf-iX.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-BGJZoyxN.js → pie-WAS4IAKB-C8_wbVGt.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-C_IdKjBF.js → pieDiagram-E7YTZNPT-CqlAZb6C.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-CVvfK1li.js → quadrantDiagram-AXDQQJYC-BmL5gqum.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-C14GlZJM.js → radar-RG4KPBEZ-wXteP-ND.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-Cg3OXIZS.js → railroad-74A4TZTK-CiAiQ98V.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-7KYSR92O.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-Dt8zrJqM.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-NioqZSpU.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-_oTlqB8E.js → railroadDiagram-O6MQD6OU-m83Fr2E6.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-6_cpGq7p.js → requirementDiagram-BXWQKSXE-DdiUlbNn.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-CIDdTiIM.js → sankeyDiagram-P5KCCOFB-DgRmQoC3.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-OmglouV4.js → sequenceDiagram-WJ2MYXX4-CLvzUry_.js} +1 -1
- package/web/assets/{settingsRoute-Y4aQ907P.js → settingsRoute-CY886TNp.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-lmR5zsjq.js → stateDiagram-D77RDMKH-CXEsod-X.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-sOazOE8m.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-BRP1y0vF.js → swimlanes-42K2YHIH-BIXrBhfr.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-CEQROFH_.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-74uE748S.js → timeline-definition-24CTP7MA-CHNGDuRJ.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-C7rSzN0q.js → treeView-Q6P3EWNA-B5PrKFUx.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-B-Eqh7wJ.js → treemap-WGGIJYW6-CyfiLDp3.js} +1 -1
- package/web/assets/useStore-BirjskHz.js +16 -0
- package/web/assets/{vennDiagram-4TSXK5OY-9HXkWxEw.js → vennDiagram-4TSXK5OY-DsQXSlTg.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-Bvwh4mZv.js → wardley-WFR3VGLG-BzGhnDYD.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-D_t1chE2.js → wardleyDiagram-VM6X3IG4-BIxMxWJH.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-BwaxhGtz.js → xychartDiagram-S5SC5T6Z-B6m5ol80.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/web/assets/AutomationsView-Cw3-fec-.js +0 -51
- package/web/assets/ProviderConnect-BVnL5_ML.js +0 -1
- package/web/assets/SessionList-qypmhsdJ.js +0 -1
- package/web/assets/architecture-7GRP2DOG-DjnOFWJ6.js +0 -1
- package/web/assets/channel-B7AEPkF6.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BEtpnW4V.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BEtpnW4V.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-CYiUVM8V.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-kSHsw5bd.js +0 -1
- package/web/assets/index-CtJDDeFG.css +0 -1
- package/web/assets/mount-Cnz2WLxO.js +0 -36
- package/web/assets/railroad-abnf-HS5TGJTU-CzpKO8Mq.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-BBhUgg4O.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-C2WJ96PH.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-DJ79XS5i.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-D08eaEQz.js +0 -8
- package/web/assets/useStore-TM8ZVr6X.js +0 -16
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
/**
|
|
4
|
+
* Install and update from the self-contained GitHub release archives
|
|
5
|
+
* (scripts/release-archives.mjs). Layout under the install root
|
|
6
|
+
* (default ~/.local/share/bivy):
|
|
7
|
+
*
|
|
8
|
+
* versions/<version>-<target>/ one extracted archive each
|
|
9
|
+
* current -> versions/… the active one, swapped atomically
|
|
10
|
+
*
|
|
11
|
+
* The launcher (current/bin/bivy) passes its unresolved `current` path as
|
|
12
|
+
* BIVY_ARCHIVE_ROOT, so a service unit or shim written through it keeps
|
|
13
|
+
* working after `current` moves to a new version.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import crypto from "node:crypto";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
export const RELEASES_URL = "https://github.com/bivysh/bivy/releases";
|
|
21
|
+
export const ARCHIVE_INFO = "bivy-archive.json";
|
|
22
|
+
/** Versions kept on disk: the active one and the one before it, for rollback. */
|
|
23
|
+
const KEEP = 2;
|
|
24
|
+
|
|
25
|
+
/** "linux-x64" etc.: the archive target for this machine, or undefined. */
|
|
26
|
+
export function archiveTarget(platform = process.platform, arch = process.arch) {
|
|
27
|
+
return ["linux", "darwin"].includes(platform) && ["x64", "arm64"].includes(arch) ? `${platform}-${arch}` : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Where a channel's manifest and archives live. `latest` is the newest stable release. */
|
|
31
|
+
export function channelUrls(channel, base = process.env.BIVY_RELEASES_URL || RELEASES_URL) {
|
|
32
|
+
const dir = channel === "latest" ? `${base}/latest/download` : `${base}/download/${encodeURIComponent(channel)}`;
|
|
33
|
+
return {
|
|
34
|
+
manifest: `${dir}/manifest.json`,
|
|
35
|
+
// Stable archives are fetched from their own tag, so a release published
|
|
36
|
+
// mid-update can't swap the file under a manifest read a moment earlier.
|
|
37
|
+
archive: (manifest, file) => (channel === "latest" ? `${base}/download/v${manifest.version}/${file}` : `${dir}/${file}`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The archive install this CLI runs from, or null. `archiveRoot` is the
|
|
43
|
+
* launcher's BIVY_ARCHIVE_ROOT; it only counts when it really is the install
|
|
44
|
+
* holding `appRoot`, so a value inherited into some other checkout is ignored.
|
|
45
|
+
*/
|
|
46
|
+
export function archiveInstall(archiveRoot, appRoot) {
|
|
47
|
+
if (!archiveRoot) return null;
|
|
48
|
+
try {
|
|
49
|
+
if (fs.realpathSync(path.join(archiveRoot, "app")) !== fs.realpathSync(appRoot)) return null;
|
|
50
|
+
const info = JSON.parse(fs.readFileSync(path.join(archiveRoot, ARCHIVE_INFO), "utf8"));
|
|
51
|
+
// Run as current/bin/bivy or as versions/<v>/bin/bivy, the install root is
|
|
52
|
+
// the same, and `current` is the path worth writing down.
|
|
53
|
+
const parent = path.dirname(archiveRoot);
|
|
54
|
+
const installRoot = path.basename(archiveRoot) === "current" ? parent : path.basename(parent) === "versions" ? path.dirname(parent) : undefined;
|
|
55
|
+
// An archive unpacked anywhere else runs, but has no install to update.
|
|
56
|
+
const current = path.join(installRoot ?? "", "current");
|
|
57
|
+
if (!installRoot || !fs.existsSync(current)) return null;
|
|
58
|
+
// Run through `current`, write `current`; a specific version (say, a new one
|
|
59
|
+
// being checked before the swap) stays itself.
|
|
60
|
+
const root = fs.realpathSync(archiveRoot) === fs.realpathSync(current) ? current : archiveRoot;
|
|
61
|
+
return { root, installRoot, version: info.version, target: info.target };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchManifest(url, fetchImpl = fetch) {
|
|
68
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
|
|
69
|
+
if (!res.ok) throw new Error(`Could not read the release manifest (${res.status} from ${url})`);
|
|
70
|
+
const manifest = await res.json();
|
|
71
|
+
if (typeof manifest?.version !== "string" || !manifest.archives) throw new Error(`${url} is not a Bivy release manifest`);
|
|
72
|
+
return manifest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch a large file with curl. Node's fetch can abort the process on an
|
|
77
|
+
* assertion when a server closes the connection while the body is paused
|
|
78
|
+
* for backpressure; curl is on every machine install.sh runs on.
|
|
79
|
+
*/
|
|
80
|
+
export function curlDownload(url, dest) {
|
|
81
|
+
const res = spawnSync("curl", ["-fsSL", "--retry", "2", "-o", dest, url], { encoding: "utf8" });
|
|
82
|
+
if (res.status !== 0) throw new Error(`Could not download ${url}: ${(res.stderr || "").trim() || `curl exit ${res.status}`}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Download, verify and extract one archive into versions/. Returns its
|
|
87
|
+
* directory. Nothing outside versions/ changes, so a failure here leaves the
|
|
88
|
+
* running install untouched.
|
|
89
|
+
*/
|
|
90
|
+
export async function stageArchive({ installRoot, manifest, target, archiveUrl, download = curlDownload }) {
|
|
91
|
+
const entry = manifest.archives[target];
|
|
92
|
+
if (!entry) throw new Error(`Bivy ${manifest.version} has no archive for ${target}`);
|
|
93
|
+
const versions = path.join(installRoot, "versions");
|
|
94
|
+
fs.mkdirSync(versions, { recursive: true });
|
|
95
|
+
const work = fs.mkdtempSync(path.join(versions, ".stage-"));
|
|
96
|
+
try {
|
|
97
|
+
const tarball = path.join(work, entry.file);
|
|
98
|
+
await download(archiveUrl(manifest, entry.file), tarball);
|
|
99
|
+
const sha256 = crypto.createHash("sha256").update(fs.readFileSync(tarball)).digest("hex");
|
|
100
|
+
if (sha256 !== entry.sha256) throw new Error(`${entry.file} failed verification: sha256 ${sha256}, the manifest says ${entry.sha256}`);
|
|
101
|
+
|
|
102
|
+
const tar = spawnSync("tar", ["-xzf", tarball, "-C", work], { encoding: "utf8" });
|
|
103
|
+
if (tar.status !== 0) throw new Error(`Could not extract ${entry.file}: ${tar.stderr}`);
|
|
104
|
+
const extracted = path.join(work, entry.file.replace(/\.tar\.gz$/, ""));
|
|
105
|
+
const info = JSON.parse(fs.readFileSync(path.join(extracted, ARCHIVE_INFO), "utf8"));
|
|
106
|
+
if (info.version !== manifest.version || info.target !== target) throw new Error(`${entry.file} holds ${info.version} ${info.target}, not ${manifest.version} ${target}`);
|
|
107
|
+
|
|
108
|
+
const dest = path.join(versions, `${manifest.version}-${target}`);
|
|
109
|
+
// Re-staging a version (a reinstall) must not replace the directory `current`
|
|
110
|
+
// points at while it runs; move it aside first and drop it afterwards.
|
|
111
|
+
const aside = fs.existsSync(dest) ? `${dest}.old-${process.pid}` : undefined;
|
|
112
|
+
if (aside) fs.renameSync(dest, aside);
|
|
113
|
+
fs.renameSync(extracted, dest);
|
|
114
|
+
if (aside) fs.rmSync(aside, { recursive: true, force: true });
|
|
115
|
+
return dest;
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The version directory `current` points at, or undefined. */
|
|
122
|
+
export function activeVersion(installRoot) {
|
|
123
|
+
try {
|
|
124
|
+
return path.resolve(installRoot, fs.readlinkSync(path.join(installRoot, "current")));
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Point `current` at a version directory atomically. Returns what it pointed at before. */
|
|
131
|
+
export function activate(installRoot, versionDir) {
|
|
132
|
+
const previous = activeVersion(installRoot);
|
|
133
|
+
const link = path.join(installRoot, "current");
|
|
134
|
+
const next = `${link}.next-${process.pid}`;
|
|
135
|
+
fs.rmSync(next, { force: true });
|
|
136
|
+
fs.symlinkSync(path.relative(installRoot, versionDir), next);
|
|
137
|
+
fs.renameSync(next, link);
|
|
138
|
+
return previous;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Remove version directories other than the newest KEEP, never the active one. */
|
|
142
|
+
export function prune(installRoot, keep = []) {
|
|
143
|
+
const versions = path.join(installRoot, "versions");
|
|
144
|
+
const active = activeVersion(installRoot);
|
|
145
|
+
const kept = new Set([...new Set([active, ...keep].filter(Boolean).map((p) => path.resolve(p)))].slice(0, KEEP));
|
|
146
|
+
for (const name of fs.readdirSync(versions)) {
|
|
147
|
+
const dir = path.join(versions, name);
|
|
148
|
+
if (!kept.has(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
package/bin/bivy.mjs
CHANGED
|
@@ -45,11 +45,16 @@ import { parseTailscaleStatus, serveConflict, serveFailureHint, serveOffArgs, se
|
|
|
45
45
|
import { resolveAttachSessionId } from "./attach-session-id.mjs";
|
|
46
46
|
import { detectInstallKind as classifyInstallKind, managedInstall, npmGlobalPrefix } from "./install-kind.mjs";
|
|
47
47
|
import { hasConfiguredService as configuredServiceExists } from "./service-state.mjs";
|
|
48
|
+
import { activate, activeVersion, archiveInstall, archiveTarget, channelUrls, fetchManifest, prune, stageArchive } from "./archive-install.mjs";
|
|
48
49
|
import { COMMANDS, EXIT, cliError, completionWords, describeCli, describeCommand, describeTools, renderHelp, resolveCommand, subcommandTable, suggestCommands, toolArgv, wantsJson } from "./cli-commands.mjs";
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
// A GitHub release archive install runs through its launcher, which names the
|
|
53
|
+
// install's `current` path. Paths written to disk (service unit, shims) use it,
|
|
54
|
+
// so they stay valid when an update points `current` at a new version.
|
|
55
|
+
const archive = archiveInstall(process.env.BIVY_ARCHIVE_ROOT, path.resolve(__dirname, ".."));
|
|
56
|
+
const repoRoot = archive ? path.join(archive.root, "app") : path.resolve(__dirname, "..");
|
|
57
|
+
const selfScript = path.join(repoRoot, "bin", "bivy.mjs");
|
|
53
58
|
// Mutable node state (config, relay E2E keys, sessions, logs) must live in a
|
|
54
59
|
// stable, user-owned directory that survives reinstalls. Pick it by install type:
|
|
55
60
|
// - BIVY_DATA_DIR set -> honor it (explicit override)
|
|
@@ -77,10 +82,11 @@ process.env.BIVY_DATA_DIR = appDir;
|
|
|
77
82
|
// - "git" dev checkout (has .git)
|
|
78
83
|
// - "npx" ephemeral `npx bivy` run (repoRoot under an npm _npx cache)
|
|
79
84
|
// - "npm-global" `npm i -g @bivy/bivy` (repoRoot is below node_modules/@bivy)
|
|
85
|
+
// - "archive" a GitHub release archive under ~/.local/share/bivy (install.sh)
|
|
80
86
|
// - "packaged" install.sh tarball tree (user-owned, self-preserving)
|
|
81
87
|
// - "managed" a system package (the AUR's `bivy`), updated by its manager
|
|
82
88
|
function detectInstallKind() {
|
|
83
|
-
return classifyInstallKind(repoRoot);
|
|
89
|
+
return archive ? "archive" : classifyInstallKind(repoRoot);
|
|
84
90
|
}
|
|
85
91
|
const cliConfigPath = path.join(appDir, "cli.json");
|
|
86
92
|
const canonicalConfigPath = path.join(appDir, "config.yaml");
|
|
@@ -185,7 +191,7 @@ const mcpProxyEntry = path.join(repoRoot, packaged ? "dist/harness/mcp-proxy-cli
|
|
|
185
191
|
const mcpServeEntry = path.join(repoRoot, packaged ? "dist/harness/mcp-serve-cli.js" : "src/harness/mcp-serve-cli.ts");
|
|
186
192
|
const qrEntry = path.join(repoRoot, "public", "qr.js");
|
|
187
193
|
const tsxCli = packaged ? "" : path.join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
188
|
-
const nodeBin = process.execPath;
|
|
194
|
+
const nodeBin = archive ? path.join(archive.root, "runtime", "bin", "node") : process.execPath;
|
|
189
195
|
const nodeScriptArgs = (entry) => (tsxCli ? [tsxCli, entry] : [entry]);
|
|
190
196
|
|
|
191
197
|
// Resolve the baked-in hosted endpoints (app/relay/client URLs). Imported lazily
|
|
@@ -230,7 +236,7 @@ function commandPath(extraPath = "") {
|
|
|
230
236
|
process.env.PATH || "",
|
|
231
237
|
// npm global executables normally live beside the Node binary (including
|
|
232
238
|
// nvm/fnm installs). Services need this even without shell initialization.
|
|
233
|
-
path.dirname(
|
|
239
|
+
path.dirname(nodeBin),
|
|
234
240
|
].filter(Boolean);
|
|
235
241
|
return [...new Set(parts.flatMap((part) => String(part).split(path.delimiter)).filter(Boolean))].join(path.delimiter);
|
|
236
242
|
}
|
|
@@ -4065,6 +4071,7 @@ function plistContent(config) {
|
|
|
4065
4071
|
PORT: String(config.port),
|
|
4066
4072
|
BIVY_WORKSPACE: config.workspace,
|
|
4067
4073
|
BIVY_DATA_DIR: appDir,
|
|
4074
|
+
...(archive ? { BIVY_ARCHIVE_ROOT: archive.root } : {}),
|
|
4068
4075
|
...config.env,
|
|
4069
4076
|
PATH: commandPath(config.env?.PATH),
|
|
4070
4077
|
})
|
|
@@ -4109,6 +4116,7 @@ function systemdContent(config) {
|
|
|
4109
4116
|
PORT: String(config.port),
|
|
4110
4117
|
BIVY_WORKSPACE: config.workspace,
|
|
4111
4118
|
BIVY_DATA_DIR: appDir,
|
|
4119
|
+
...(archive ? { BIVY_ARCHIVE_ROOT: archive.root } : {}),
|
|
4112
4120
|
...config.env,
|
|
4113
4121
|
PATH: commandPath(config.env?.PATH),
|
|
4114
4122
|
})
|
|
@@ -4301,7 +4309,9 @@ async function restartServiceReconciled(config) {
|
|
|
4301
4309
|
// A typed config edit may change workspace, port, or environment without
|
|
4302
4310
|
// touching the old unit. Reinstall on content drift so `bivy restart` really
|
|
4303
4311
|
// applies the canonical file instead of reviving stale baked-in values.
|
|
4304
|
-
|
|
4312
|
+
// installService only *starts* the unit (`enable --now`, `launchctl load`),
|
|
4313
|
+
// which leaves an already-running node on its old code; restart it as well.
|
|
4314
|
+
if (portChanged || configChanged) return (await installService(config)) && restartService();
|
|
4305
4315
|
return restartService();
|
|
4306
4316
|
}
|
|
4307
4317
|
|
|
@@ -5515,6 +5525,49 @@ async function cmdUpdate(args = []) {
|
|
|
5515
5525
|
await runUpdate(args);
|
|
5516
5526
|
}
|
|
5517
5527
|
|
|
5528
|
+
/**
|
|
5529
|
+
* Update a GitHub release archive install: stage the channel's archive beside
|
|
5530
|
+
* the running one, point `current` at it, restart, and point `current` back if
|
|
5531
|
+
* the node doesn't come up. The version before stays on disk for that.
|
|
5532
|
+
*/
|
|
5533
|
+
async function updateArchiveInstall(channel, { skipWait, force }) {
|
|
5534
|
+
const urls = channelUrls(channel);
|
|
5535
|
+
const target = archiveTarget();
|
|
5536
|
+
console.log(c.dim(`Checking ${urls.manifest} (channel: ${channel})…`));
|
|
5537
|
+
const manifest = await fetchManifest(urls.manifest);
|
|
5538
|
+
if (manifest.version === archive.version && !force) {
|
|
5539
|
+
console.log(c.green(`Bivy ${archive.version} is the newest on ${channel}.`));
|
|
5540
|
+
return;
|
|
5541
|
+
}
|
|
5542
|
+
console.log(c.dim(`Downloading Bivy ${manifest.version} for ${target}…`));
|
|
5543
|
+
const staged = await stageArchive({ installRoot: archive.installRoot, manifest, target, archiveUrl: urls.archive });
|
|
5544
|
+
const probe = runQuiet(path.join(staged, "bin", "bivy"), ["--version"]);
|
|
5545
|
+
if (probe.code !== 0 || probe.stdout.trim() !== manifest.version) {
|
|
5546
|
+
throw new Error(`The downloaded Bivy ${manifest.version} does not run here (${(probe.stderr || probe.stdout).trim() || `exit ${probe.code}`}); kept ${archive.version}.`);
|
|
5547
|
+
}
|
|
5548
|
+
const config = loadConfig();
|
|
5549
|
+
await waitForIdleSessions(config, { skip: skipWait });
|
|
5550
|
+
const previous = activate(archive.installRoot, staged);
|
|
5551
|
+
if (!hasConfiguredService(config)) {
|
|
5552
|
+
prune(archive.installRoot, [staged, previous]);
|
|
5553
|
+
console.log(c.green(`Updated to ${manifest.version}. Run 'bivy start' to use it.`));
|
|
5554
|
+
return;
|
|
5555
|
+
}
|
|
5556
|
+
if ((await restartServiceReconciled(config)) && (await verifyNodeCameUp(config))) {
|
|
5557
|
+
prune(archive.installRoot, [staged, previous]);
|
|
5558
|
+
console.log(c.green(`Updated ${archive.version} → ${manifest.version} and restarted the background service.`));
|
|
5559
|
+
return;
|
|
5560
|
+
}
|
|
5561
|
+
if (previous && previous !== activeVersion(archive.installRoot)) {
|
|
5562
|
+
activate(archive.installRoot, previous);
|
|
5563
|
+
await restartServiceReconciled(config);
|
|
5564
|
+
console.log(c.red(`⚠ Bivy ${manifest.version} did not come up, so ${archive.version} is back in place.`));
|
|
5565
|
+
} else {
|
|
5566
|
+
reportNodeDidNotStart();
|
|
5567
|
+
}
|
|
5568
|
+
process.exitCode = 1;
|
|
5569
|
+
}
|
|
5570
|
+
|
|
5518
5571
|
async function runUpdate(args = []) {
|
|
5519
5572
|
const skipWait = args.includes("--force") || args.includes("--no-wait");
|
|
5520
5573
|
const kind = detectInstallKind();
|
|
@@ -5564,6 +5617,12 @@ async function runUpdate(args = []) {
|
|
|
5564
5617
|
return;
|
|
5565
5618
|
}
|
|
5566
5619
|
|
|
5620
|
+
if (kind === "archive") {
|
|
5621
|
+
await updateArchiveInstall(channel, { skipWait, force: args.includes("--force") });
|
|
5622
|
+
console.log(c.dim(`=== bivy update finished ${new Date().toISOString()} ===`));
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5567
5626
|
if (kind === "packaged") {
|
|
5568
5627
|
console.log(c.dim(`Updating packaged Bivy install (channel: ${channel})…`));
|
|
5569
5628
|
// The actual restart happens inside install.sh, which shells out to
|
package/bin/cli-commands.mjs
CHANGED
|
@@ -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
|
|
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/bin/prune-sessions.mjs
CHANGED
|
@@ -68,23 +68,55 @@ export function linkSessionWorktrees({ sessions, staleSessions, staleWorktrees,
|
|
|
68
68
|
return { sessions: sorted, worktrees: [...removeWorktrees] };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// Delegated child sessions (sub-agents, `delegatedFrom`) belong to the session
|
|
72
|
+
// that started them, as the sidebar nests them. Groups sessions into families
|
|
73
|
+
// keyed by the top-most listed ancestor; a child whose parent isn't listed is
|
|
74
|
+
// its own root.
|
|
75
|
+
function sessionFamilies(sessions) {
|
|
76
|
+
const byId = new Map(sessions.map((s) => [s.id, s]));
|
|
77
|
+
const rootOf = (s) => {
|
|
78
|
+
const seen = new Set([s.id]);
|
|
79
|
+
let cur = s;
|
|
80
|
+
for (;;) {
|
|
81
|
+
const parent = byId.get(cur.delegatedFrom?.sessionId);
|
|
82
|
+
if (!parent || seen.has(parent.id)) return cur.id;
|
|
83
|
+
seen.add(parent.id);
|
|
84
|
+
cur = parent;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const families = new Map();
|
|
88
|
+
for (const s of sessions) {
|
|
89
|
+
const root = rootOf(s);
|
|
90
|
+
families.set(root, [...(families.get(root) ?? []), s]);
|
|
91
|
+
}
|
|
92
|
+
return [...families.values()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Removal set for sessions. A parent and its sub-agent sessions are judged as one
|
|
96
|
+
// unit: the family's activity is its newest member's, it is live if any member is
|
|
97
|
+
// working, and it is empty only if every member is. So sub-agents never take
|
|
98
|
+
// keep slots from real sessions, and a recent child keeps its parent (and vice
|
|
99
|
+
// versa). Live families are never touched. The newest `keep` NON-EMPTY families
|
|
100
|
+
// survive; empty ones are always beyond the keep window. A family is removed only
|
|
101
|
+
// when it is BOTH beyond the keep window AND older than `ageMs` — the same safe
|
|
102
|
+
// intersection as the file-based selectStale. `keep`/`ageMs` of null disable that
|
|
103
|
+
// half of the test.
|
|
77
104
|
export function selectStaleSessions(sessions, keep, ageMs, now) {
|
|
78
|
-
const eligible = sessions
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
const eligible = sessionFamilies(sessions)
|
|
106
|
+
.filter((members) => !members.some((s) => s.status === "working"))
|
|
107
|
+
.map((members) => ({
|
|
108
|
+
members,
|
|
109
|
+
activity: Math.max(...members.map(sessionActivityMs)),
|
|
110
|
+
empty: members.every(isEmptySession),
|
|
111
|
+
}));
|
|
112
|
+
const nonEmptyDesc = eligible.filter((f) => !f.empty).sort((a, b) => b.activity - a.activity);
|
|
113
|
+
const rank = new Map(nonEmptyDesc.map((f, i) => [f, i]));
|
|
83
114
|
return eligible
|
|
84
|
-
.filter((
|
|
85
|
-
const beyondKeep = keep === null ? true : (
|
|
86
|
-
const olderThan = ageMs === null ? true : now -
|
|
115
|
+
.filter((f) => {
|
|
116
|
+
const beyondKeep = keep === null ? true : (f.empty ? true : rank.get(f) >= keep);
|
|
117
|
+
const olderThan = ageMs === null ? true : now - f.activity >= ageMs;
|
|
87
118
|
return beyondKeep && olderThan;
|
|
88
119
|
})
|
|
120
|
+
.flatMap((f) => f.members)
|
|
89
121
|
.sort((a, b) => sessionActivityMs(b) - sessionActivityMs(a));
|
|
90
122
|
}
|
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
|
|
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 {
|
|
12
|
-
import {
|
|
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
|
|
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
|
|
45
|
+
throw new Error("--args and --resume-args must be JSON string arrays");
|
|
49
46
|
}
|
|
50
47
|
return parsed;
|
|
51
48
|
}
|
|
52
|
-
function
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
88
|
+
const parser = option(args, "--parser");
|
|
89
|
+
const resumeArgs = option(args, "--resume-args");
|
|
68
90
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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> [--
|
|
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 <
|
|
114
|
+
bivy agent remove <id>
|
|
100
115
|
|
|
101
|
-
|
|
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
|
|
107
|
-
const
|
|
108
|
-
|
|
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(
|
|
130
|
+
const retained = runtimeModule.canonicalAgentId(id);
|
|
111
131
|
if (retained)
|
|
112
|
-
throw new Error(`Agent 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 <
|
|
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})
|
|
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);
|