@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 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 or Debian/Ubuntu build tools, but never for `npm install`. To inspect it first,
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+ and, if native dependencies failed, build tools (make/g++/python3), then try again.`));
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 = [await ensureNodePackage("@anthropic-ai/claude-agent-sdk")];
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") return ensureNpmCommand("pi", "@earendil-works/pi-coding-agent", "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 sdk = await ensureNodePackage("@anthropic-ai/claude-agent-sdk");
715
+ const bridge = await ensureAgentBridges(["claude-code-sdk"]);
713
716
  const cli = await ensureNpmCommand("claude", "@anthropic-ai/claude-code", "Claude Code");
714
- return sdk && cli;
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("installed but not loadable — reinstall after installing build tools") : c.yellow("dependency missing — reinstall Bivy; interactive terminals unavailable")}`);
4468
- const claudeBridge = nodePackageInstalled("@anthropic-ai/claude-agent-sdk");
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 = nodePackageInstalled("@earendil-works/pi-coding-agent");
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, but this Bivy distribution is missing its SDK bridge dependency."
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
- try {
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}" package. Install it with: npm install ${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
- systemPrompt: { type: "preset", preset: "claude_code", append: BIVY_ATTACH_SYSTEM_PROMPT },
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,