@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 +3 -0
- package/dist/agents/claude-code/runtime.js +2 -16
- package/dist/agents/codex/integration.js +3 -16
- package/dist/agents/pi/integration.js +2 -16
- package/dist/executable.js +35 -0
- package/dist/runtime/host.js +3 -2
- package/dist/runtime/index.js +8 -59
- package/dist/server.js +1 -1
- package/dist/terminal.js +2 -35
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/runtime/host.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/runtime/index.js
CHANGED
|
@@ -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
|
|
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,
|
|
12
|
-
import {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
*
|