@bivy/bivy 0.18.9-staging.3 → 0.18.9-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/README.md +1 -1
- package/bin/bivy.mjs +43 -39
- package/bin/codex-app-server-shim.mjs +3 -0
- package/dist/agent-bridges.d.mts +26 -0
- package/dist/agent-bridges.mjs +271 -0
- package/dist/agent-instructions.js +103 -0
- package/dist/agents/claude-code/integration.js +1 -1
- package/dist/agents/claude-code/runtime.js +9 -12
- package/dist/agents/pi/integration.js +4 -7
- package/dist/agents/pi/runtime.js +3 -0
- package/dist/agents/profiles.js +3 -0
- package/dist/harness/mcp-config.js +4 -0
- package/dist/harness/mcp-inject.js +1 -1
- package/dist/harness/mcp-serve-cli.js +14 -1
- package/dist/runtime/agent-service.js +2 -2
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/model-namer.js +2 -0
- package/dist/runtime/pi-oauth.js +2 -0
- package/dist/runtime/process.js +3 -3
- package/dist/runtime/protocol.js +14 -6
- package/dist/runtime/remote.js +2 -2
- package/dist/runtime/session-env.js +18 -2
- package/dist/server.js +72 -6
- package/dist/terminal.js +3 -60
- package/package.json +10 -11
- package/bin/patch-pi-dependencies.mjs +0 -56
- package/bin/repair-pty-permissions.mjs +0 -35
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ curl -fsSL https://bivy.sh/install.sh | bash
|
|
|
282
282
|
|
|
283
283
|
Setup helps you choose an agent and configure remote access. Existing agents
|
|
284
284
|
keep their command, login, and configuration. The installer may use `sudo` to
|
|
285
|
-
install missing Node.js
|
|
285
|
+
install a missing Node.js, but never for `npm install`. To inspect it first,
|
|
286
286
|
download it with `curl -fsSL https://bivy.sh/install.sh -o install.sh`.
|
|
287
287
|
|
|
288
288
|
Already have Node.js and want to avoid sudo?
|
package/bin/bivy.mjs
CHANGED
|
@@ -160,6 +160,7 @@ const relaySetupEntry = path.join(repoRoot, packaged ? "dist/relay-setup.js" : "
|
|
|
160
160
|
// artifact (src/ is not packaged), so resolve it the same packaged-aware way as
|
|
161
161
|
// the runtime entries and import it dynamically at call time.
|
|
162
162
|
const hostedEndpointsEntry = path.join(repoRoot, packaged ? "dist/hosted-endpoints.mjs" : "src/hosted-endpoints.mjs");
|
|
163
|
+
const agentBridgesEntry = path.join(repoRoot, packaged ? "dist/agent-bridges.mjs" : "src/agent-bridges.mjs");
|
|
163
164
|
const githubConnectEntry = path.join(repoRoot, packaged ? "dist/github-connect-repo.js" : "src/github-connect-repo.ts");
|
|
164
165
|
const githubAppConnectEntry = path.join(repoRoot, packaged ? "dist/github-app-connect.js" : "src/github-app-connect.ts");
|
|
165
166
|
const githubAppSyncEntry = path.join(repoRoot, packaged ? "dist/github-app-sync-cli.js" : "src/github-app-sync-cli.ts");
|
|
@@ -185,6 +186,27 @@ async function getHostedEndpoints() {
|
|
|
185
186
|
return _hostedEndpoints();
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// Agent SDK bridges (Claude, Pi) install on demand into the data dir, not with
|
|
190
|
+
// Bivy — the daemon and this CLI share src/agent-bridges.mjs for it.
|
|
191
|
+
function loadAgentBridges() {
|
|
192
|
+
return import(pathToFileURL(agentBridgesEntry).href);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Install any bridge packages the given agents need. True when all are present. */
|
|
196
|
+
async function ensureAgentBridges(agentIds) {
|
|
197
|
+
const bridges = await loadAgentBridges();
|
|
198
|
+
const missing = [...new Set(agentIds.flatMap((id) => bridges.missingBridges(id)))];
|
|
199
|
+
if (!missing.length) return true;
|
|
200
|
+
console.log(c.dim(`Installing agent bridge ${missing.join(", ")} into ${bridges.bridgesDir()}…`));
|
|
201
|
+
try {
|
|
202
|
+
await bridges.installBridges(missing, { stdio: "inherit" });
|
|
203
|
+
return true;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(c.red(error instanceof Error ? error.message : String(error)));
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
const SERVICE_LABEL = "dev.bivy";
|
|
189
211
|
const SERVICE_UNIT = "bivy.service";
|
|
190
212
|
|
|
@@ -587,14 +609,10 @@ async function ensureDeps() {
|
|
|
587
609
|
));
|
|
588
610
|
return false;
|
|
589
611
|
}
|
|
590
|
-
if (process.platform === "linux" && (!commandExists("make") || !commandExists("g++") || !commandExists("python3"))) {
|
|
591
|
-
console.error(c.yellow("Build tools are missing. On Ubuntu/Debian run: sudo apt-get update && sudo apt-get install -y build-essential python3"));
|
|
592
|
-
console.error(c.dim("The node-pty terminal dependency needs these tools if no prebuilt binary matches this machine."));
|
|
593
|
-
}
|
|
594
612
|
console.log(c.dim(`Installing dependencies (${cmd} ${args.join(" ")})…`));
|
|
595
613
|
const code = await run(cmd, args, { cwd: repoRoot });
|
|
596
614
|
if (code !== 0 || !fs.existsSync(dependencyMarker)) {
|
|
597
|
-
console.error(c.red(`${cmd} install failed. Install Node.js 20
|
|
615
|
+
console.error(c.red(`${cmd} install failed. Install Node.js 20+, then try again.`));
|
|
598
616
|
return false;
|
|
599
617
|
}
|
|
600
618
|
return true;
|
|
@@ -628,28 +646,6 @@ function nodePackageLoadable(packageName) {
|
|
|
628
646
|
return runQuiet(nodeBin, ["-e", "require(require.resolve(process.argv[1], { paths: [process.argv[2]] }))", packageName, repoRoot]).code === 0;
|
|
629
647
|
}
|
|
630
648
|
|
|
631
|
-
async function ensureNodePackage(packageName) {
|
|
632
|
-
if (nodePackageInstalled(packageName)) {
|
|
633
|
-
console.log(c.green(` ✓ Found Bivy bridge package ${packageName}`));
|
|
634
|
-
return true;
|
|
635
|
-
}
|
|
636
|
-
// Add with the package manager that owns this tree. Running `npm install` in a
|
|
637
|
-
// pnpm workspace would write a competing package-lock.json and a hoisted
|
|
638
|
-
// node_modules over pnpm's symlink layout, leaving the checkout in a state
|
|
639
|
-
// neither tool resolves correctly.
|
|
640
|
-
const usesPnpm = fs.existsSync(path.join(repoRoot, "pnpm-lock.yaml"));
|
|
641
|
-
const [cmd, baseArgs] = usesPnpm
|
|
642
|
-
? [commandExists("pnpm") ? "pnpm" : "corepack", commandExists("pnpm") ? ["add"] : ["pnpm", "add"]]
|
|
643
|
-
: ["npm", ["install", "--no-audit", "--no-fund"]];
|
|
644
|
-
if (!commandExists(cmd)) {
|
|
645
|
-
console.error(c.red(`${cmd} is required to install ${packageName}.`));
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
console.log(c.dim(`Installing Bivy bridge package ${packageName}…`));
|
|
649
|
-
const code = await run(cmd, [...baseArgs, packageName], { cwd: repoRoot });
|
|
650
|
-
return code === 0 && nodePackageInstalled(packageName);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
649
|
const userLocalPrefix = process.env.BIVY_NPM_GLOBAL_PREFIX || path.join(os.homedir(), ".local");
|
|
654
650
|
|
|
655
651
|
async function ensureNpmCommand(command, packageName, label) {
|
|
@@ -689,10 +685,13 @@ const KNOWN_AGENT_INSTALLS = [
|
|
|
689
685
|
{ command: "gemini", npmPackage: "@google/gemini-cli", label: "Gemini CLI" },
|
|
690
686
|
];
|
|
691
687
|
|
|
692
|
-
async function ensureKnownAgents() {
|
|
688
|
+
async function ensureKnownAgents({ bridgesOnly = false } = {}) {
|
|
693
689
|
if (process.env.BIVY_SKIP_AGENT_PREINSTALL === "1") return true;
|
|
690
|
+
const { AGENT_BRIDGES } = await loadAgentBridges();
|
|
691
|
+
const bridges = await ensureAgentBridges(Object.keys(AGENT_BRIDGES));
|
|
692
|
+
if (bridgesOnly) return bridges;
|
|
694
693
|
console.log(c.dim("Ensuring known agent integrations are installed…"));
|
|
695
|
-
const results = [
|
|
694
|
+
const results = [bridges];
|
|
696
695
|
for (const agent of KNOWN_AGENT_INSTALLS) {
|
|
697
696
|
results.push(
|
|
698
697
|
agent.pythonPackage
|
|
@@ -707,11 +706,15 @@ async function ensureKnownAgents() {
|
|
|
707
706
|
|
|
708
707
|
async function ensureSetupAgent(choice) {
|
|
709
708
|
if (!choice) return true;
|
|
710
|
-
if (choice.runtimeId === "pi")
|
|
709
|
+
if (choice.runtimeId === "pi") {
|
|
710
|
+
const bridge = await ensureAgentBridges(["pi"]);
|
|
711
|
+
const cli = await ensureNpmCommand("pi", "@earendil-works/pi-coding-agent", "Pi");
|
|
712
|
+
return bridge && cli;
|
|
713
|
+
}
|
|
711
714
|
if (choice.runtimeId === "claude-code-sdk") {
|
|
712
|
-
const
|
|
715
|
+
const bridge = await ensureAgentBridges(["claude-code-sdk"]);
|
|
713
716
|
const cli = await ensureNpmCommand("claude", "@anthropic-ai/claude-code", "Claude Code");
|
|
714
|
-
return
|
|
717
|
+
return bridge && cli;
|
|
715
718
|
}
|
|
716
719
|
if (choice.runtimeId === "codex-approvals") return ensureNpmCommand("codex", "@openai/codex", "Codex");
|
|
717
720
|
if (choice.runtimeId === "opencode") return ensureNpmCommand("opencode", "opencode-ai/opencode", "OpenCode");
|
|
@@ -4462,12 +4465,13 @@ async function cmdDoctor(args = []) {
|
|
|
4462
4465
|
|
|
4463
4466
|
console.log(c.bold("\n Bivy doctor\n"));
|
|
4464
4467
|
console.log(` ${mark(hasSupportedNode())} Node ${process.version}${hasSupportedNode() ? "" : c.dim(" (needs >= 20.0.0)")}`);
|
|
4465
|
-
const ptyInstalled = nodePackageInstalled("node-pty");
|
|
4466
|
-
const ptyUsable = ptyInstalled && nodePackageLoadable("node-pty");
|
|
4467
|
-
console.log(` ${mark(ptyUsable, true)} terminal PTY ${ptyUsable ? c.green("available") : ptyInstalled ? c.yellow(
|
|
4468
|
-
const
|
|
4468
|
+
const ptyInstalled = nodePackageInstalled("@lydell/node-pty");
|
|
4469
|
+
const ptyUsable = ptyInstalled && nodePackageLoadable("@lydell/node-pty");
|
|
4470
|
+
console.log(` ${mark(ptyUsable, true)} terminal PTY ${ptyUsable ? c.green("available") : ptyInstalled ? c.yellow(`installed but not loadable — no prebuilt binary for ${process.platform}-${process.arch}, or npm ran with --omit=optional`) : c.yellow("dependency missing — reinstall Bivy; interactive terminals unavailable")}`);
|
|
4471
|
+
const { bridgeInstalled } = await loadAgentBridges();
|
|
4472
|
+
const claudeBridge = bridgeInstalled("@anthropic-ai/claude-agent-sdk");
|
|
4469
4473
|
console.log(` ${mark(claudeBridge, true)} Claude bridge ${claudeBridge ? c.green("installed") : c.dim("optional — installed on first Claude setup/use")}`);
|
|
4470
|
-
const piBridge =
|
|
4474
|
+
const piBridge = bridgeInstalled("@earendil-works/pi-coding-agent");
|
|
4471
4475
|
const piNodeOk = nodeAtLeast(22, 19);
|
|
4472
4476
|
console.log(` ${mark(piBridge && piNodeOk, true)} Pi bridge ${piBridge ? (piNodeOk ? c.green("installed") : c.yellow("installed but needs Node >=22.19")) : c.dim("optional — installed only if you choose Pi")}`);
|
|
4473
4477
|
console.log(` ${mark(commandExists("git"), true)} git${commandExists("git") ? "" : c.dim(" (recommended for repo-backed sessions)")}`);
|
|
@@ -5549,11 +5553,11 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
|
|
|
5549
5553
|
case "agents:install":
|
|
5550
5554
|
case "runtimes:install":
|
|
5551
5555
|
if (args.includes("-h") || args.includes("--help")) {
|
|
5552
|
-
console.log(`Usage: bivy agents:install\n\nInstall known agent integrations (${KNOWN_AGENT_INSTALLS.map((a) => a.label).join(", ")})
|
|
5556
|
+
console.log(`Usage: bivy agents:install [--bridges]\n\nInstall known agent integrations (${KNOWN_AGENT_INSTALLS.map((a) => a.label).join(", ")}).\n\n --bridges only install the SDK bridges Bivy loads for Claude Code and Pi\n (they otherwise install on first use)`);
|
|
5553
5557
|
break;
|
|
5554
5558
|
}
|
|
5555
5559
|
if (!(await ensureDeps())) process.exit(1);
|
|
5556
|
-
await ensureKnownAgents();
|
|
5560
|
+
if (!(await ensureKnownAgents({ bridgesOnly: args.includes("--bridges") })) && args.includes("--bridges")) process.exit(1);
|
|
5557
5561
|
break;
|
|
5558
5562
|
case "open": {
|
|
5559
5563
|
if (args.includes("-h") || args.includes("--help")) {
|
|
@@ -478,6 +478,9 @@ function threadParams(msg, extra = {}) {
|
|
|
478
478
|
approvalPolicy: APPROVAL_POLICY,
|
|
479
479
|
sandbox: SANDBOX_MODE,
|
|
480
480
|
...(selectedModel ? { model: selectedModel } : {}),
|
|
481
|
+
// The user's account-wide Bivy instructions ride as developer instructions,
|
|
482
|
+
// alongside (not instead of) the AGENTS.md files Codex loads itself.
|
|
483
|
+
...(typeof msg.instructions === "string" && msg.instructions ? { developerInstructions: msg.instructions } : {}),
|
|
481
484
|
...extra,
|
|
482
485
|
};
|
|
483
486
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
export interface AgentBridge {
|
|
4
|
+
command: string;
|
|
5
|
+
packages: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export declare const AGENT_BRIDGES: Record<string, AgentBridge>;
|
|
9
|
+
|
|
10
|
+
export interface ResolvedBridge {
|
|
11
|
+
dir: string;
|
|
12
|
+
version: string;
|
|
13
|
+
source: "bivy" | "bridges";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export declare function bridgeVersions(): Record<string, string>;
|
|
17
|
+
export declare function bridgesDir(env?: Record<string, string | undefined>): string;
|
|
18
|
+
export declare function resolveBridge(name: string, dir?: string): ResolvedBridge | undefined;
|
|
19
|
+
export declare function bridgeInstalled(name: string, dir?: string): boolean;
|
|
20
|
+
export declare function missingBridges(agentId: string, dir?: string): string[];
|
|
21
|
+
export declare function bridgesToSync(options?: { dir?: string; commandExists?: (command: string) => boolean }): string[];
|
|
22
|
+
export declare function installBridges(
|
|
23
|
+
names: string[],
|
|
24
|
+
options?: { dir?: string; stdio?: "inherit" | "pipe" },
|
|
25
|
+
): Promise<{ installed: string[]; output: string }>;
|
|
26
|
+
export declare function enableBridgeResolution(dir?: string): void;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// Agent bridges: the SDK packages Bivy loads in-process to drive an agent
|
|
4
|
+
// (Claude's Agent SDK, Pi's coding-agent library).
|
|
5
|
+
//
|
|
6
|
+
// They are large (hundreds of MB together) and a node usually needs one, so
|
|
7
|
+
// they are not installed with Bivy. They install on first use into a directory
|
|
8
|
+
// next to the node's state, which survives `bivy update` replacing the package
|
|
9
|
+
// directory, and resolve from there whenever Bivy's own node_modules lacks
|
|
10
|
+
// them. In a dev checkout the workspace installs them as optionalDependencies,
|
|
11
|
+
// so normal resolution finds them first and the bridge directory is unused.
|
|
12
|
+
//
|
|
13
|
+
// Plain JS (with a .d.mts) so the CLI in bin/ and the daemon share one copy.
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import module from "node:module";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
// src/ in a checkout, dist/ in a release: either way the parent is the package root.
|
|
21
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Which bridge packages each agent loads, and the CLI whose presence means the
|
|
25
|
+
* agent is in use on this node. Adding a bridge is a row here plus a pinned
|
|
26
|
+
* entry in package.json's optionalDependencies (published as `agentBridges`).
|
|
27
|
+
*/
|
|
28
|
+
export const AGENT_BRIDGES = {
|
|
29
|
+
"claude-code-sdk": { command: "claude", packages: ["@anthropic-ai/claude-agent-sdk"] },
|
|
30
|
+
pi: { command: "pi", packages: ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai"] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function readJson(file) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bivyManifest() {
|
|
42
|
+
return readJson(path.join(packageRoot, "package.json")) ?? {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pinned bridge versions: `agentBridges` in a release, optionalDependencies in a checkout. */
|
|
46
|
+
export function bridgeVersions() {
|
|
47
|
+
const pkg = bivyManifest();
|
|
48
|
+
return { ...(pkg.agentBridges ?? pkg.optionalDependencies ?? {}) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Where bridges install: $BIVY_BRIDGES_DIR, else `bridges/` in the node's data dir. */
|
|
52
|
+
export function bridgesDir(env = process.env) {
|
|
53
|
+
if (env.BIVY_BRIDGES_DIR) return path.resolve(env.BIVY_BRIDGES_DIR);
|
|
54
|
+
const dataDir = env.BIVY_DATA_DIR ? path.resolve(env.BIVY_DATA_DIR) : path.join(packageRoot, ".bivy");
|
|
55
|
+
return path.join(dataDir, "bridges");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** "@scope/name/sub" -> "@scope/name"; "name/sub" -> "name". */
|
|
59
|
+
function packageNameOf(specifier) {
|
|
60
|
+
const parts = specifier.split("/");
|
|
61
|
+
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function packageAt(dir) {
|
|
65
|
+
const manifest = readJson(path.join(dir, "package.json"));
|
|
66
|
+
return manifest ? { dir, version: manifest.version } : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Find `name` the way Node's resolver walks node_modules upward from `from`. */
|
|
70
|
+
function findUpward(name, from) {
|
|
71
|
+
for (let dir = from; ; dir = path.dirname(dir)) {
|
|
72
|
+
const found = packageAt(path.join(dir, "node_modules", name));
|
|
73
|
+
if (found) return found;
|
|
74
|
+
if (path.dirname(dir) === dir) return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* npm writes node_modules/.package-lock.json when an install finishes, and we
|
|
80
|
+
* write package.json just before starting one. An interrupted install (the
|
|
81
|
+
* daemon stopped mid-way) leaves half-extracted packages, so trust the
|
|
82
|
+
* directory only when that lockfile is newer than the manifest.
|
|
83
|
+
*/
|
|
84
|
+
function installComplete(dir) {
|
|
85
|
+
try {
|
|
86
|
+
return fs.statSync(path.join(dir, "node_modules", ".package-lock.json")).mtimeMs >= fs.statSync(path.join(dir, "package.json")).mtimeMs;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Where a bridge package resolves from, if anywhere: Bivy's own dependency
|
|
94
|
+
* tree first (a checkout), then the bridge directory.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveBridge(name, dir = bridgesDir()) {
|
|
97
|
+
const own = findUpward(name, packageRoot);
|
|
98
|
+
if (own) return { ...own, source: "bivy" };
|
|
99
|
+
if (!installComplete(dir)) return undefined;
|
|
100
|
+
const bridged = packageAt(path.join(dir, "node_modules", name));
|
|
101
|
+
return bridged ? { ...bridged, source: "bridges" } : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function bridgeInstalled(name, dir) {
|
|
105
|
+
return resolveBridge(name, dir) !== undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Bridge packages `agentId` needs that are not resolvable yet (none for agents without a bridge). */
|
|
109
|
+
export function missingBridges(agentId, dir) {
|
|
110
|
+
return (AGENT_BRIDGES[agentId]?.packages ?? []).filter((name) => !bridgeInstalled(name, dir));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onPath(command, env = process.env) {
|
|
114
|
+
const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
|
|
115
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
116
|
+
for (const ext of exts) {
|
|
117
|
+
try {
|
|
118
|
+
fs.accessSync(path.join(dir, command + ext), fs.constants.X_OK);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Bridge packages that should be (re)installed now: every package already in
|
|
128
|
+
* the bridge directory or belonging to an agent whose CLI is on PATH, that is
|
|
129
|
+
* either missing or not at Bivy's pinned version. Empty means nothing to do,
|
|
130
|
+
* and computing it touches no network.
|
|
131
|
+
*/
|
|
132
|
+
export function bridgesToSync({ dir = bridgesDir(), commandExists = onPath } = {}) {
|
|
133
|
+
const versions = bridgeVersions();
|
|
134
|
+
const wanted = new Set(Object.keys(readJson(path.join(dir, "package.json"))?.dependencies ?? {}));
|
|
135
|
+
for (const bridge of Object.values(AGENT_BRIDGES)) {
|
|
136
|
+
if (commandExists(bridge.command)) bridge.packages.forEach((name) => wanted.add(name));
|
|
137
|
+
}
|
|
138
|
+
return [...wanted].filter((name) => {
|
|
139
|
+
if (!versions[name]) return false;
|
|
140
|
+
const found = resolveBridge(name, dir);
|
|
141
|
+
return !found || (found.source === "bridges" && found.version !== versions[name]);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Replace nested copies that a bridge's own npm-shrinkwrap pins, which npm
|
|
147
|
+
* `overrides` cannot reach. Driven by the nested overrides in package.json
|
|
148
|
+
* (`"parent": { "child": "version" }`), so a new pin is data, not code.
|
|
149
|
+
*/
|
|
150
|
+
function applyNestedOverrides(dir) {
|
|
151
|
+
for (const [parent, pins] of Object.entries(bivyManifest().overrides ?? {})) {
|
|
152
|
+
if (!pins || typeof pins !== "object") continue;
|
|
153
|
+
const parentDir = path.join(dir, "node_modules", parent);
|
|
154
|
+
for (const [child, version] of Object.entries(pins)) {
|
|
155
|
+
const target = path.join(parentDir, "node_modules", child);
|
|
156
|
+
const nested = packageAt(target);
|
|
157
|
+
if (!nested || nested.version === version) continue;
|
|
158
|
+
const source = [packageAt(path.join(dir, "node_modules", child)), findUpward(child, packageRoot)]
|
|
159
|
+
.find((candidate) => candidate?.version === version);
|
|
160
|
+
if (!source) throw new Error(`Security patch source ${child}@${version} is missing`);
|
|
161
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
162
|
+
fs.cpSync(source.dir, target, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Install `names` (at Bivy's pinned versions) into the bridge directory. The
|
|
169
|
+
* directory is an ordinary npm project that keeps earlier bridges and carries
|
|
170
|
+
* Bivy's security overrides. Optional dependencies are omitted: bridges run as
|
|
171
|
+
* libraries against the operator's own agent CLI, never their bundled binaries.
|
|
172
|
+
*
|
|
173
|
+
* `stdio: "inherit"` streams npm's output (CLI); otherwise it is captured.
|
|
174
|
+
* Installs in one process run one at a time, since they share a directory.
|
|
175
|
+
*/
|
|
176
|
+
let installQueue = Promise.resolve();
|
|
177
|
+
export function installBridges(names, options = {}) {
|
|
178
|
+
const run = installQueue.then(() => installNow(names, options));
|
|
179
|
+
installQueue = run.catch(() => {});
|
|
180
|
+
return run;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function installNow(names, { dir = bridgesDir(), stdio = "pipe" } = {}) {
|
|
184
|
+
const versions = bridgeVersions();
|
|
185
|
+
const unknown = names.filter((name) => !versions[name]);
|
|
186
|
+
if (unknown.length) return Promise.reject(new Error(`Not an agent bridge: ${unknown.join(", ")}`));
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
189
|
+
// npm does not repair half-extracted packages, so an interrupted earlier
|
|
190
|
+
// install is discarded rather than installed over.
|
|
191
|
+
if (!installComplete(dir)) fs.rmSync(path.join(dir, "node_modules"), { recursive: true, force: true });
|
|
192
|
+
const manifestPath = path.join(dir, "package.json");
|
|
193
|
+
const previous = readJson(manifestPath)?.dependencies ?? {};
|
|
194
|
+
const dependencies = { ...previous };
|
|
195
|
+
for (const name of names) dependencies[name] = versions[name];
|
|
196
|
+
const manifest = { name: "bivy-agent-bridges", private: true, dependencies, overrides: bivyManifest().overrides ?? {} };
|
|
197
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
198
|
+
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const child = spawn("npm", ["install", "--omit=optional", "--no-audit", "--no-fund"], {
|
|
201
|
+
cwd: dir,
|
|
202
|
+
stdio: stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
203
|
+
});
|
|
204
|
+
let output = "";
|
|
205
|
+
const append = (chunk) => { output = (output + chunk.toString()).slice(-12000); };
|
|
206
|
+
child.stdout?.on("data", append);
|
|
207
|
+
child.stderr?.on("data", append);
|
|
208
|
+
child.on("error", reject);
|
|
209
|
+
child.on("close", (code) => {
|
|
210
|
+
if (code !== 0) {
|
|
211
|
+
reject(new Error(`npm could not install ${names.join(", ")} (exit ${code ?? "unknown"})${output ? `\n${output}` : ""}`));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
applyNestedOverrides(dir);
|
|
216
|
+
resolve({ installed: names, output });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
reject(error);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const NOT_FOUND = new Set(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]);
|
|
225
|
+
|
|
226
|
+
// Same fallback as the in-thread hook below, for Node versions that only have
|
|
227
|
+
// module.register (off-thread, async hooks loaded from this data: URL).
|
|
228
|
+
const ASYNC_HOOKS = `
|
|
229
|
+
let parentURL, names;
|
|
230
|
+
export function initialize(data) { parentURL = data.parentURL; names = new Set(data.names); }
|
|
231
|
+
const nameOf = ${packageNameOf.toString()};
|
|
232
|
+
export async function resolve(specifier, context, next) {
|
|
233
|
+
try { return await next(specifier, context); } catch (error) {
|
|
234
|
+
if (!${JSON.stringify([...NOT_FOUND])}.includes(error?.code) || !names.has(nameOf(specifier))) throw error;
|
|
235
|
+
try { return await next(specifier, { ...context, parentURL }); } catch { throw error; }
|
|
236
|
+
}
|
|
237
|
+
}`;
|
|
238
|
+
|
|
239
|
+
let resolutionEnabled = false;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Let `import`/`require` of a bridge package fall back to the bridge directory
|
|
243
|
+
* when Bivy's own node_modules lacks it. Idempotent; call before loading a
|
|
244
|
+
* bridge. Only bridge package names are redirected, and only after normal
|
|
245
|
+
* resolution fails, so a checkout's workspace copies always win.
|
|
246
|
+
*/
|
|
247
|
+
export function enableBridgeResolution(dir = bridgesDir()) {
|
|
248
|
+
if (resolutionEnabled) return;
|
|
249
|
+
resolutionEnabled = true;
|
|
250
|
+
const parentURL = pathToFileURL(path.join(dir, "package.json")).href;
|
|
251
|
+
const names = Object.keys(bridgeVersions());
|
|
252
|
+
if (typeof module.registerHooks === "function") {
|
|
253
|
+
const bridges = new Set(names);
|
|
254
|
+
module.registerHooks({
|
|
255
|
+
resolve(specifier, context, next) {
|
|
256
|
+
try {
|
|
257
|
+
return next(specifier, context);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!NOT_FOUND.has(error?.code) || !bridges.has(packageNameOf(specifier))) throw error;
|
|
260
|
+
try {
|
|
261
|
+
return next(specifier, { ...context, parentURL });
|
|
262
|
+
} catch {
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
} else if (typeof module.register === "function") {
|
|
269
|
+
module.register(`data:text/javascript,${encodeURIComponent(ASYNC_HOOKS)}`, { data: { parentURL, names } });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Account-wide agent instructions: one Markdown file of the user's global
|
|
7
|
+
* preferences, handed to every agent session IN ADDITION to the repo's own
|
|
8
|
+
* AGENTS.md/CLAUDE.md (which the agent keeps loading itself).
|
|
9
|
+
*
|
|
10
|
+
* The source of truth on a machine is `<appDir>/AGENTS.md` — plain Markdown the
|
|
11
|
+
* user can also edit by hand. Its mtime is the last-writer-wins clock: the
|
|
12
|
+
* account-wide copy rides in the E2E model-auth vault envelope (see server.ts),
|
|
13
|
+
* and a synced import stamps the file with the writer's timestamp so every
|
|
14
|
+
* machine compares like with like. Clearing is an empty file, not a delete, so
|
|
15
|
+
* the empty state carries a timestamp and propagates too.
|
|
16
|
+
*
|
|
17
|
+
* Delivery is the caller's business; this module only produces the composed
|
|
18
|
+
* text (and a file holding it, for agents that take a path).
|
|
19
|
+
*/
|
|
20
|
+
/** Cap on the editable text. It rides on every turn of every session. */
|
|
21
|
+
export const MAX_AGENT_INSTRUCTIONS_BYTES = 16 * 1024;
|
|
22
|
+
const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
|
|
23
|
+
"instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
|
|
24
|
+
export function agentInstructionsPath(appDir) {
|
|
25
|
+
return path.join(appDir, "AGENTS.md");
|
|
26
|
+
}
|
|
27
|
+
function composedPath(appDir) {
|
|
28
|
+
return path.join(appDir, "agent-instructions.composed.md");
|
|
29
|
+
}
|
|
30
|
+
export function readAgentInstructions(appDir) {
|
|
31
|
+
try {
|
|
32
|
+
const file = agentInstructionsPath(appDir);
|
|
33
|
+
return { text: fs.readFileSync(file, "utf8"), updatedAt: Math.trunc(fs.statSync(file).mtimeMs) };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { text: "", updatedAt: 0 };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function writeAgentInstructions(appDir, text, updatedAt = Date.now()) {
|
|
40
|
+
if (Buffer.byteLength(text, "utf8") > MAX_AGENT_INSTRUCTIONS_BYTES) {
|
|
41
|
+
throw new Error(`Agent instructions are limited to ${MAX_AGENT_INSTRUCTIONS_BYTES / 1024} KB.`);
|
|
42
|
+
}
|
|
43
|
+
const file = agentInstructionsPath(appDir);
|
|
44
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(file, text);
|
|
46
|
+
fs.utimesSync(file, new Date(updatedAt), new Date(updatedAt));
|
|
47
|
+
return readAgentInstructions(appDir);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reconcile a synced copy with the local file (last writer wins).
|
|
51
|
+
* - "imported": the synced copy was newer and is now the local file.
|
|
52
|
+
* - "local-newer": this machine holds a newer copy the account should get.
|
|
53
|
+
* - "unchanged": nothing to do.
|
|
54
|
+
*/
|
|
55
|
+
export function mergeSyncedAgentInstructions(appDir, incoming) {
|
|
56
|
+
const local = readAgentInstructions(appDir);
|
|
57
|
+
const synced = incoming && typeof incoming === "object" ? incoming : undefined;
|
|
58
|
+
if (typeof synced?.text !== "string" || typeof synced.updatedAt !== "number") {
|
|
59
|
+
return local.updatedAt > 0 ? "local-newer" : "unchanged";
|
|
60
|
+
}
|
|
61
|
+
if (synced.text === local.text)
|
|
62
|
+
return "unchanged";
|
|
63
|
+
if (synced.updatedAt > local.updatedAt) {
|
|
64
|
+
writeAgentInstructions(appDir, synced.text, synced.updatedAt);
|
|
65
|
+
return "imported";
|
|
66
|
+
}
|
|
67
|
+
return "local-newer";
|
|
68
|
+
}
|
|
69
|
+
/** Wrap the user's text in the precedence preamble; "" when there is nothing to send. */
|
|
70
|
+
export function composeAgentInstructions(text) {
|
|
71
|
+
const body = text.trim();
|
|
72
|
+
return body ? `${PREAMBLE}\n\n${body}\n` : "";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The instructions for a session starting now, or undefined when the user has
|
|
76
|
+
* none. Keeps a composed copy on disk (rewritten only when it changes) for
|
|
77
|
+
* agents that take a file path rather than text.
|
|
78
|
+
*/
|
|
79
|
+
export function sessionInstructions(appDir) {
|
|
80
|
+
const text = composeAgentInstructions(readAgentInstructions(appDir).text);
|
|
81
|
+
const file = composedPath(appDir);
|
|
82
|
+
try {
|
|
83
|
+
if (!text) {
|
|
84
|
+
// An MCP server spec injected earlier may still point here; don't let it
|
|
85
|
+
// serve instructions the user has since cleared.
|
|
86
|
+
fs.rmSync(file, { force: true });
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
let current;
|
|
90
|
+
try {
|
|
91
|
+
current = fs.readFileSync(file, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch { /* not written yet */ }
|
|
94
|
+
if (current !== text)
|
|
95
|
+
fs.writeFileSync(file, text);
|
|
96
|
+
return { text, file };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Never block a session on this; it just starts without them.
|
|
100
|
+
console.warn("[agent-instructions] could not prepare instructions:", error.message);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -55,7 +55,7 @@ export function claudeCodeIntegration(origin) {
|
|
|
55
55
|
notes: installed
|
|
56
56
|
? "Uses the Claude Code executable already on this node, including its native auth, configuration, and sessions. Set BIVY_CLAUDE_MODEL to pick a default model."
|
|
57
57
|
: agentInstalled
|
|
58
|
-
? "The Claude Code agent is installed
|
|
58
|
+
? "The Claude Code agent is installed; Bivy installs its SDK bridge when you start the first Claude session (or run 'bivy agents:install --bridges')."
|
|
59
59
|
: "Install and sign in to Claude Code on this node; Bivy will connect to that existing agent.",
|
|
60
60
|
install: installed || agentInstalled ? undefined : {
|
|
61
61
|
label: "Install Claude Code",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// * The SDK is loaded with a dynamic import so it stays an *optional*
|
|
14
14
|
// dependency: a Bivy install only needs it when this runtime is selected.
|
|
15
15
|
import { withSessionCredentials, credentialEnvFallback } from "../../credentials/session.js";
|
|
16
|
-
import { createRequire } from "node:module";
|
|
17
16
|
import { randomUUID } from "node:crypto";
|
|
18
17
|
import { EventEmitter } from "node:events";
|
|
19
18
|
import { withExactCapabilitySurface } from "../../runtime/types.js";
|
|
@@ -30,6 +29,7 @@ import { anthropicCredentialPreflight, describeAnthropicError, isAnthropicAuthEr
|
|
|
30
29
|
import { toModelInfo as sharedToModelInfo } from "../../runtime/normalize.js";
|
|
31
30
|
import { hasLiveProcessForCwd } from "../../runtime/native-process-scan.js";
|
|
32
31
|
import { bivySessionEnv } from "../../runtime/session-env.js";
|
|
32
|
+
import { bridgeInstalled, enableBridgeResolution } from "../../agent-bridges.mjs";
|
|
33
33
|
/** Binary names a live Claude Code process could be running under (see
|
|
34
34
|
* native-process-scan.ts's best-effort cwd match). */
|
|
35
35
|
const CLAUDE_BIN_NAMES = ["claude"];
|
|
@@ -190,20 +190,15 @@ export function claudeRuntimeFromEnv() {
|
|
|
190
190
|
}
|
|
191
191
|
/** True when `@anthropic-ai/claude-agent-sdk` is resolvable in this install. */
|
|
192
192
|
export function claudeSdkInstalled() {
|
|
193
|
-
|
|
194
|
-
createRequire(import.meta.url).resolve(SDK_PACKAGE);
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
193
|
+
return bridgeInstalled(SDK_PACKAGE);
|
|
200
194
|
}
|
|
201
195
|
async function loadSdk() {
|
|
196
|
+
enableBridgeResolution();
|
|
202
197
|
try {
|
|
203
198
|
return await import(SDK_PACKAGE);
|
|
204
199
|
}
|
|
205
200
|
catch {
|
|
206
|
-
throw new Error(`The claude-code-sdk runtime requires the "${SDK_PACKAGE}"
|
|
201
|
+
throw new Error(`The claude-code-sdk runtime requires the "${SDK_PACKAGE}" bridge. Install it with: bivy agents:install --bridges`);
|
|
207
202
|
}
|
|
208
203
|
}
|
|
209
204
|
/**
|
|
@@ -885,7 +880,9 @@ class ClaudeSession {
|
|
|
885
880
|
// Kept even when the native tool below is also registered: it's a cheap,
|
|
886
881
|
// harmless fallback for a shell/subprocess the agent spawns that can't
|
|
887
882
|
// reach the in-process MCP tool directly.
|
|
888
|
-
|
|
883
|
+
// The user's account-wide instructions follow it; the repo's CLAUDE.md
|
|
884
|
+
// still loads through the preset as usual.
|
|
885
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: [BIVY_ATTACH_SYSTEM_PROMPT, this.runtimeOptions.instructions].filter(Boolean).join("\n\n") },
|
|
889
886
|
...(this.runtimeOptions.executablePath ? { pathToClaudeCodeExecutable: this.runtimeOptions.executablePath } : {}),
|
|
890
887
|
};
|
|
891
888
|
if (resumeId)
|
|
@@ -1516,12 +1513,12 @@ export class ClaudeCodeRuntime {
|
|
|
1516
1513
|
return [{ id: "anthropic", name: "Anthropic", oauth: true, models: FALLBACK_MODELS }];
|
|
1517
1514
|
}
|
|
1518
1515
|
async createSession(options) {
|
|
1519
|
-
const session = new ClaudeSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.toolInterceptor, options.toolProvider);
|
|
1516
|
+
const session = new ClaudeSession({ ...(await withSessionCredentials(this.options, options.credentialLabels)), instructions: options.instructions?.text }, options.workspace, options.toolInterceptor, options.toolProvider);
|
|
1520
1517
|
this.sessions.push(session);
|
|
1521
1518
|
return { session };
|
|
1522
1519
|
}
|
|
1523
1520
|
async openSession(options) {
|
|
1524
|
-
const session = new ClaudeSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
|
|
1521
|
+
const session = new ClaudeSession({ ...(await withSessionCredentials(this.options, options.credentialLabels)), instructions: options.instructions?.text }, options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
|
|
1525
1522
|
this.sessions.push(session);
|
|
1526
1523
|
return {
|
|
1527
1524
|
session,
|