@bivy/bivy 0.18.4 → 0.18.5-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/bivy.mjs CHANGED
@@ -194,6 +194,9 @@ function commandPath(extraPath = "") {
194
194
  path.join(os.homedir(), ".local", "bin"),
195
195
  extraPath,
196
196
  process.env.PATH || "",
197
+ // npm global executables normally live beside the Node binary (including
198
+ // nvm/fnm installs). Services need this even without shell initialization.
199
+ path.dirname(process.execPath),
197
200
  ].filter(Boolean);
198
201
  return [...new Set(parts.flatMap((part) => String(part).split(path.delimiter)).filter(Boolean))].join(path.delimiter);
199
202
  }
@@ -18,7 +18,7 @@ import { randomUUID } from "node:crypto";
18
18
  import { EventEmitter } from "node:events";
19
19
  import { withExactCapabilitySurface } from "../../runtime/types.js";
20
20
  import { mapToolCall, mapToolResult } from "../../runtime/tool-call-map.js";
21
- import { spawnSync } from "node:child_process";
21
+ import { resolveExecutable } from "../../executable.js";
22
22
  import fs from "node:fs";
23
23
  import { depCacheEnv } from "../../harness/dep-cache.js";
24
24
  import os from "node:os";
@@ -33,23 +33,9 @@ import { bivySessionEnv } from "../../runtime/session-env.js";
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"];
36
- const CLAUDE_CLI_PATH_CACHE = new Map();
37
36
  /** Resolve the operator-installed Claude Code CLI; never use the SDK's bundled fallback. */
38
37
  export function claudeCliPath() {
39
- const command = process.env.BIVY_CLAUDE_COMMAND?.trim() || "claude";
40
- const key = `${command}\0${process.env.PATH ?? ""}`;
41
- if (CLAUDE_CLI_PATH_CACHE.has(key))
42
- return CLAUDE_CLI_PATH_CACHE.get(key);
43
- const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
44
- shell: process.platform !== "win32",
45
- encoding: "utf8",
46
- });
47
- const resolved = result.status === 0 ? result.stdout.split(/\r?\n/)[0]?.trim() || undefined : undefined;
48
- CLAUDE_CLI_PATH_CACHE.set(key, resolved);
49
- return resolved;
50
- }
51
- export function invalidateClaudeCliProbe() {
52
- CLAUDE_CLI_PATH_CACHE.clear();
38
+ return resolveExecutable(process.env.BIVY_CLAUDE_COMMAND?.trim() || "claude") ?? undefined;
53
39
  }
54
40
  export function claudeCliAvailable() {
55
41
  return Boolean(claudeCliPath());
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
- import { spawn, spawnSync } from "node:child_process";
3
+ import { spawn } from "node:child_process";
4
+ import { resolveExecutable } from "../../executable.js";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { defineAgentIntegration } from "../definition.js";
@@ -10,25 +11,11 @@ import { deleteCodexSession, discoverNativeCodexSessions, loadCodexTranscript, w
10
11
  import { ProtocolRuntime } from "../../runtime/protocol.js";
11
12
  import { codexSlashCommands } from "../../runtime/slash-commands.js";
12
13
  export const CODEX_TESTED_VERSION = "0.156.1";
13
- const CODEX_AVAILABLE_CACHE = new Map();
14
14
  function codexCommand() {
15
15
  return process.env.BIVY_CODEX_BIN?.trim() || "codex";
16
16
  }
17
17
  export function codexCommandAvailable() {
18
- const command = codexCommand();
19
- const cached = CODEX_AVAILABLE_CACHE.get(command);
20
- if (cached !== undefined)
21
- return cached;
22
- const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
23
- shell: process.platform !== "win32",
24
- stdio: "ignore",
25
- });
26
- const available = result.status === 0;
27
- CODEX_AVAILABLE_CACHE.set(command, available);
28
- return available;
29
- }
30
- export function invalidateCodexCommandProbe() {
31
- CODEX_AVAILABLE_CACHE.clear();
18
+ return resolveExecutable(codexCommand()) !== null;
32
19
  }
33
20
  async function suggestCodexSessionName(firstPrompt, context) {
34
21
  const prompt = firstPrompt.trim();
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
- import { spawnSync } from "node:child_process";
3
+ import { resolveExecutable } from "../../executable.js";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { defineAgentIntegration } from "../definition.js";
@@ -36,22 +36,8 @@ export async function listNativePiSessions() {
36
36
  firstMessage: session.firstMessage,
37
37
  }));
38
38
  }
39
- const PI_COMMAND_CACHE = new Map();
40
39
  export function piCommandAvailable() {
41
- const command = piCommand();
42
- const cached = PI_COMMAND_CACHE.get(command);
43
- if (cached !== undefined)
44
- return cached;
45
- const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
46
- shell: process.platform !== "win32",
47
- stdio: "ignore",
48
- });
49
- const available = result.status === 0;
50
- PI_COMMAND_CACHE.set(command, available);
51
- return available;
52
- }
53
- export function invalidatePiCommandProbe() {
54
- PI_COMMAND_CACHE.clear();
40
+ return resolveExecutable(piCommand()) !== null;
55
41
  }
56
42
  export function piBridgeInstalled() {
57
43
  try {
@@ -0,0 +1,35 @@
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
+ /** Resolve without a shell or a lifetime cache: installs, removals and PATH
6
+ * changes must be visible to a running daemon as well as its terminals. */
7
+ export function resolveExecutable(command, env = process.env, cwd = process.cwd()) {
8
+ if (!command.trim())
9
+ return null;
10
+ const win = process.platform === "win32";
11
+ const isExecutable = (file) => {
12
+ try {
13
+ if (!fs.statSync(file).isFile())
14
+ return false;
15
+ fs.accessSync(file, win ? fs.constants.F_OK : fs.constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
22
+ const exts = win ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
23
+ const withExts = (file) => exts.map((ext) => (ext && !file.toLowerCase().endsWith(ext.toLowerCase()) ? file + ext : file));
24
+ const explicit = command.includes("/") || (win && command.includes("\\"));
25
+ const bases = explicit
26
+ ? [path.resolve(cwd, command)]
27
+ : (env.PATH || "").split(path.delimiter).filter(Boolean).map((dir) => path.resolve(cwd, dir, command));
28
+ for (const base of bases) {
29
+ for (const candidate of withExts(base)) {
30
+ if (isExecutable(candidate))
31
+ return candidate;
32
+ }
33
+ }
34
+ return null;
35
+ }
@@ -66,8 +66,9 @@ export class RuntimeHost {
66
66
  const info = this.list(canonical).find((candidate) => candidate.id === canonical);
67
67
  if (!info)
68
68
  throw new Error(`Unknown agent: ${wantId}`);
69
- if (info.status !== "available")
70
- throw new Error(`${info.displayName} is not available on this node yet.`);
69
+ if (info.status !== "available") {
70
+ throw new Error(`${info.displayName} is not available on this node. ${info.notes || "Install this agent on the node before starting a session."}`);
71
+ }
71
72
  return canonical;
72
73
  }
73
74
  get(requested, fallback, sandbox) {
@@ -4,12 +4,13 @@
4
4
  // This is the seam where additional runtimes (Claude Agent SDK, generic RPC,
5
5
  // …) are registered without touching the daemon.
6
6
  import { spawnSync } from "node:child_process";
7
+ import { resolveExecutable as resolveCommandPath } from "../executable.js";
7
8
  import path from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
- import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled, invalidateClaudeCliProbe } from "../agents/claude-code/runtime.js";
10
+ import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled } from "../agents/claude-code/runtime.js";
10
11
  import { claudeCodeIntegration } from "../agents/claude-code/integration.js";
11
- import { codexAppServerRuntime, codexIntegration, invalidateCodexCommandProbe, } from "../agents/codex/integration.js";
12
- import { invalidatePiCommandProbe, piAgentDir, piCommandAvailable, piIntegration, LazyPiRuntime } from "../agents/pi/integration.js";
12
+ import { codexAppServerRuntime, codexIntegration, } from "../agents/codex/integration.js";
13
+ import { piAgentDir, piCommandAvailable, piIntegration, LazyPiRuntime } from "../agents/pi/integration.js";
13
14
  import { deleteCodexSession, loadCodexTranscript } from "./codex-sessions.js";
14
15
  import { deleteOpenCodeSession, exportOpenCodeSession, importOpenCodeSession, loadOpenCodeTranscript, writeOpenCodeHistory } from "./opencode-sessions.js";
15
16
  import { discoverNativeGrokSessions, listGrokSessions, loadGrokTranscript } from "./grok-sessions.js";
@@ -70,27 +71,9 @@ function cliSlashCommands(spec) {
70
71
  }
71
72
  export * from "./types.js";
72
73
  export { NodeCredentialResolver, createCredentialStore } from "./credentials.js";
73
- // Memoized CLI probes. `commandAvailable`/`resolveCommandPath`/`probeHelpText`
74
- // each shell out with a BLOCKING spawnSync, and the runtime catalog that calls
75
- // them (cliAgentInfo → prefersAcp/acpSupportedByBinary) is rebuilt often — on
76
- // every advertise and every runtimes.list send. Re-probing per build stalled the
77
- // event loop for seconds at a time (a synchronous spawn storm). A CLI's presence
78
- // is effectively constant for the daemon's run, so cache per command for the
79
- // process lifetime and clear on install (invalidateCliProbeCache).
80
- const COMMAND_AVAILABLE_CACHE = new Map();
74
+ // Filesystem lookup stays fresh without spawning a shell on every catalog build.
81
75
  function commandAvailable(command) {
82
- if (!command.trim())
83
- return false;
84
- const cached = COMMAND_AVAILABLE_CACHE.get(command);
85
- if (cached !== undefined)
86
- return cached;
87
- const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
88
- shell: process.platform !== "win32",
89
- stdio: "ignore",
90
- });
91
- const available = result.status === 0;
92
- COMMAND_AVAILABLE_CACHE.set(command, available);
93
- return available;
76
+ return resolveCommandPath(command) !== null;
94
77
  }
95
78
  function genericCliInfo() {
96
79
  const options = processRuntimeFromEnv();
@@ -421,29 +404,6 @@ function cliThinkingConfig(id, spec) {
421
404
  // installed binary doesn't actually mention. It never UPGRADES — adding a
422
405
  // capability needs the exact arg template, which help text can't safely supply — so
423
406
  // probing can only make the catalog MORE honest, never invent a no-op control.
424
- /**
425
- * Absolute path of a command on the current PATH, or null when it isn't there.
426
- * Used to key the help-probe cache: caching by the bare NAME would keep serving a
427
- * stale answer after the binary behind that name changed (a CLI upgraded or
428
- * installed while the daemon is running, or a different PATH entry winning).
429
- * Memoized per command (see COMMAND_AVAILABLE_CACHE) — it spawnSyncs, and is hit
430
- * on every catalog build.
431
- */
432
- const COMMAND_PATH_CACHE = new Map();
433
- function resolveCommandPath(command) {
434
- if (!command.trim())
435
- return null;
436
- const cached = COMMAND_PATH_CACHE.get(command);
437
- if (cached !== undefined)
438
- return cached;
439
- const res = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
440
- shell: process.platform !== "win32",
441
- encoding: "utf8",
442
- });
443
- const resolved = res.status !== 0 ? null : ((res.stdout ?? "").split(/\r?\n/)[0]?.trim() || null);
444
- COMMAND_PATH_CACHE.set(command, resolved);
445
- return resolved;
446
- }
447
407
  const HELP_PROBE_CACHE = new Map();
448
408
  function probeHelpText(command) {
449
409
  const key = resolveCommandPath(command) ?? command;
@@ -461,21 +421,10 @@ function probeHelpText(command) {
461
421
  HELP_PROBE_CACHE.set(key, text);
462
422
  return text;
463
423
  }
464
- /**
465
- * Drop every memoized CLI probe (availability, resolved path, --help text). These
466
- * probes shell out with a blocking spawnSync and are cached for the process
467
- * lifetime to keep the frequently-rebuilt runtime catalog off the event loop, so a
468
- * CLI installed/updated mid-run wouldn't otherwise be noticed until a restart.
469
- * Call this right after Bivy installs a runtime so the next catalog build re-probes
470
- * and reflects the new binary immediately.
471
- */
424
+ /** Refresh expensive help probes after Bivy installs or updates an agent.
425
+ * Executable availability itself is never cached. */
472
426
  export function invalidateCliProbeCache() {
473
- COMMAND_AVAILABLE_CACHE.clear();
474
- COMMAND_PATH_CACHE.clear();
475
427
  HELP_PROBE_CACHE.clear();
476
- invalidatePiCommandProbe();
477
- invalidateClaudeCliProbe();
478
- invalidateCodexCommandProbe();
479
428
  }
480
429
  // A resume template mixes launch flags (`-p`, `--force`) with the resume-specific
481
430
  // token(s) (`--resume`, `threads continue`, `-s`, `--restore`, …). Only the latter
package/dist/server.js CHANGED
@@ -239,7 +239,7 @@ process.chdir(appDir);
239
239
  // dirs here makes commands work from agent shells without requiring a user shell
240
240
  // profile.
241
241
  const userLocalPrefix = process.env.BIVY_NPM_GLOBAL_PREFIX || path.join(process.env.HOME ?? os.homedir(), ".local");
242
- process.env.PATH = [path.join(repoRoot, "bin"), path.join(userLocalPrefix, "bin"), process.env.PATH || ""].filter(Boolean).join(path.delimiter);
242
+ process.env.PATH = [path.join(repoRoot, "bin"), path.join(userLocalPrefix, "bin"), process.env.PATH || "", path.dirname(process.execPath)].filter(Boolean).join(path.delimiter);
243
243
  const piDir = path.join(appDir, "pi");
244
244
  // The node's shared, agent-neutral credential vault (auth.enc/auth.key). NOT
245
245
  // inside any one agent's dir — every runtime (Pi, Codex, Claude, …) reads the
package/dist/terminal.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
3
  import { randomUUID } from "node:crypto";
4
4
  import fs from "node:fs";
5
+ import { resolveExecutable } from "./executable.js";
5
6
  import { createRequire } from "node:module";
6
7
  import path from "node:path";
7
8
  import { depCacheEnv } from "./harness/dep-cache.js";
@@ -70,41 +71,7 @@ function defaultShell() {
70
71
  * Resolving up front lets open() throw a clear, catchable error — and hands the
71
72
  * PTY an unambiguous absolute path.
72
73
  */
73
- export function resolveExecutable(command, env, cwd) {
74
- const win = process.platform === "win32";
75
- const isExecutable = (file) => {
76
- try {
77
- if (!fs.statSync(file).isFile())
78
- return false;
79
- if (win)
80
- return true;
81
- fs.accessSync(file, fs.constants.X_OK);
82
- return true;
83
- }
84
- catch {
85
- return false;
86
- }
87
- };
88
- // On Windows a bare name may need a PATHEXT suffix (claude → claude.cmd).
89
- const exts = win ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
90
- const withExts = (file) => exts.map((ext) => (ext && !file.toLowerCase().endsWith(ext.toLowerCase()) ? file + ext : file));
91
- // A command with an explicit path is resolved directly (against the workspace
92
- // for a relative one), never via PATH — same as exec.
93
- if (command.includes("/") || (win && command.includes("\\"))) {
94
- for (const candidate of withExts(path.resolve(cwd, command))) {
95
- if (isExecutable(candidate))
96
- return candidate;
97
- }
98
- return null;
99
- }
100
- for (const dir of (env.PATH || "").split(path.delimiter).filter(Boolean)) {
101
- for (const candidate of withExts(path.join(dir, command))) {
102
- if (isExecutable(candidate))
103
- return candidate;
104
- }
105
- }
106
- return null;
107
- }
74
+ export { resolveExecutable } from "./executable.js";
108
75
  /**
109
76
  * Restore the execute bit on node-pty's bundled `spawn-helper` binary.
110
77
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.18.4",
3
+ "version": "0.18.5-staging.1",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",