@bivy/bivy 0.4.0-staging.52 → 0.4.0-staging.54

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.
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Pure session-id resolution for `bivy attach` (bin/bivy.mjs's cmdAttach),
4
+ // extracted so it's unit-testable without executing the CLI.
5
+ //
6
+ // Every runtime adapter injects BIVY_SESSION_ID into its agent's subprocess env
7
+ // (see src/runtime/session-env.ts) so `bivy attach <path>`, run from the agent's
8
+ // own shell, can resolve its session without being told the id. Pi
9
+ // (src/runtime/pi.ts) is the one exception: its agent loop runs in-process
10
+ // rather than under a subprocess Bivy controls, so it has no hook to inject
11
+ // BIVY_SESSION_ID into its own bash tool's env the way the other adapters do.
12
+ // The pi-coding-agent SDK's bash tool already exposes PI_SESSION_ID to every
13
+ // command it runs by default — and that id IS the Bivy session id for a pi
14
+ // session (PiSession.id reads the exact same SessionManager the SDK reads it
15
+ // from) — so it's accepted here as an equivalent fallback.
16
+ export function resolveAttachSessionId({ sessionFlag, env } = {}) {
17
+ const flag = typeof sessionFlag === "string" ? sessionFlag.trim() : "";
18
+ if (flag) return flag;
19
+ const bivy = env?.BIVY_SESSION_ID?.trim?.();
20
+ if (bivy) return bivy;
21
+ const pi = env?.PI_SESSION_ID?.trim?.();
22
+ if (pi) return pi;
23
+ return undefined;
24
+ }
package/bin/bivy.mjs CHANGED
@@ -38,6 +38,7 @@ import { resolveSessionsLimit, truncateSavedSessions } from "./sessions-list.mjs
38
38
  import { renderManagedBlock, upsertManagedBlock, removeManagedBlock, rcFileForShell } from "./shim-path.mjs";
39
39
  import { removeExcept } from "./uninstall-paths.mjs";
40
40
  import { findAvailablePort, reconcilePort } from "./port-picker.mjs";
41
+ import { resolveAttachSessionId } from "./attach-session-id.mjs";
41
42
 
42
43
  const selfScript = fileURLToPath(import.meta.url);
43
44
  const __dirname = path.dirname(selfScript);
@@ -1977,16 +1978,19 @@ async function cmdSend(args = []) {
1977
1978
  // `bivy attach <file> [--caption "…"] [--session <id>]` — surface a file the
1978
1979
  // agent produced into the chat as an image/file attachment (the reverse of the
1979
1980
  // composer paperclip). The universal path: any agent that can run a shell command
1980
- // can call this. The session id defaults to $BIVY_SESSION_ID, which the daemon
1981
- // injects into the agent's subprocess env. The file is resolved to an absolute
1982
- // path here (the CLI's cwd is the agent's workdir) and confined to the session
1983
- // workspace server-side.
1981
+ // can call this. The session id defaults to $BIVY_SESSION_ID, which every
1982
+ // runtime adapter injects into the agent's subprocess env (see
1983
+ // src/runtime/session-env.ts) except pi, whose SDK exposes its own
1984
+ // $PI_SESSION_ID instead (same id, different var name; see
1985
+ // resolveAttachSessionId). The file is resolved to an absolute path here (the
1986
+ // CLI's cwd is the agent's workdir) and confined to the session workspace
1987
+ // server-side.
1984
1988
  async function cmdAttach(args = []) {
1985
1989
  const flag = (name) => {
1986
1990
  const i = args.indexOf(name);
1987
1991
  return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
1988
1992
  };
1989
- const sessionId = flag("--session") || process.env.BIVY_SESSION_ID;
1993
+ const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
1990
1994
  const caption = flag("--caption");
1991
1995
  const name = flag("--name");
1992
1996
  const mimeType = flag("--mime") || flag("--mimeType");
@@ -26,6 +26,7 @@ import { sandboxTier, claudePermissionModeFor } from "../harness/sandbox.js";
26
26
  import { anthropicCredentialPreflight, describeAnthropicError, isAnthropicAuthError } from "./anthropic-preflight.js";
27
27
  import { toModelInfo as sharedToModelInfo } from "./normalize.js";
28
28
  import { hasLiveProcessForCwd } from "./native-process-scan.js";
29
+ import { bivySessionEnv } from "./session-env.js";
29
30
  /** Binary names a live Claude Code process could be running under (see
30
31
  * native-process-scan.ts's best-effort cwd match). */
31
32
  const CLAUDE_BIN_NAMES = ["claude"];
@@ -56,6 +57,21 @@ const FALLBACK_MODELS = [
56
57
  { provider: "anthropic", id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true },
57
58
  { provider: "anthropic", id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", reasoning: true },
58
59
  ];
60
+ /**
61
+ * Appended to the Claude Code system prompt so the agent DISCOVERS the outbound
62
+ * attachment capability. `bivy attach` is just a shell command — without this the
63
+ * agent has no way to know it exists and, when asked to "send a file", concludes
64
+ * it can't (it looks for a tool, finds none). BIVY_SESSION_ID is injected into the
65
+ * subprocess env (see spawnQuery), so the bare command resolves the session. Keep
66
+ * this short: it rides on every turn's system prompt.
67
+ */
68
+ export const BIVY_ATTACH_SYSTEM_PROMPT = "Sending files and images to the user: the person you're talking to is in a chat UI. They cannot see files you only " +
69
+ "write to disk, and the chat cannot load remote image URLs or workspace file paths. " +
70
+ "To show them a file or image — a report, screenshot, chart, or a file they asked for — run " +
71
+ '`bivy attach <path> [--caption "short note"]` in your shell. ' +
72
+ "An image renders inline in the chat; any other file shows as a downloadable chip. The path must be inside the session " +
73
+ "workspace. Do NOT use markdown image syntax like ![](path) to show a local file or a URL — it will not render; always " +
74
+ "use `bivy attach`. Prefer this over pasting large file contents or describing where a file lives on disk.";
59
75
  export function claudeRuntimeFromEnv() {
60
76
  return {
61
77
  defaultModel: process.env.BIVY_CLAUDE_MODEL?.trim() || undefined,
@@ -673,9 +689,9 @@ class ClaudeSession {
673
689
  Object.assign(env, credEnv);
674
690
  // Let the agent's own shell surface a file into the chat via `bivy attach`
675
691
  // (POST /api/session/:id/attach). The session id is otherwise invisible to
676
- // the subprocess. Other runtimes should set this the same way to enable the
677
- // universal attach path for their agents.
678
- env.BIVY_SESSION_ID = this.id;
692
+ // the subprocess. Shared with process.ts and protocol.ts via bivySessionEnv
693
+ // (see session-env.ts) so every CLI-spawning adapter injects it the same way.
694
+ Object.assign(env, bivySessionEnv(this.id));
679
695
  this.spawnedToken = authTokenFromEnv(credEnv);
680
696
  const options = {
681
697
  cwd: this.cwd,
@@ -688,6 +704,10 @@ class ClaudeSession {
688
704
  permissionMode,
689
705
  canUseTool,
690
706
  env,
707
+ // Keep the default Claude Code prompt, appending the note that teaches the
708
+ // agent how to send a file to the user (`bivy attach`) — otherwise the
709
+ // capability is undiscoverable and "send me X as an attachment" fails.
710
+ systemPrompt: { type: "preset", preset: "claude_code", append: BIVY_ATTACH_SYSTEM_PROMPT },
691
711
  };
692
712
  if (resumeId)
693
713
  options.resume = resumeId;
@@ -17,6 +17,7 @@ import { createPiModelRuntime } from "./pi-oauth.js";
17
17
  import { toModelInfo as sharedToModelInfo } from "./normalize.js";
18
18
  import { provisionPiAuthJson } from "./credential-provisioning.js";
19
19
  import { isNativeOAuthProvider } from "./oauth/model-oauth-providers.js";
20
+ import { bivySessionEnv } from "./session-env.js";
20
21
  /**
21
22
  * Extract Pi's own slash commands from a live AgentSession: extension commands
22
23
  * (`pi.registerCommand`, exposed via `extensionRunner.getRegisteredCommands()`),
@@ -175,6 +176,17 @@ class PiSession {
175
176
  * daemon's own agent dir and session store (same files the SDK reads) and
176
177
  * resumes by session file, so the TUI shows the live conversation. Returns
177
178
  * null for an unsaved session (nothing to resume yet).
179
+ *
180
+ * This is also the one place PiSession spawns a subprocess Bivy itself
181
+ * configures, so it's where BIVY_SESSION_ID (see session-env.ts) is injected
182
+ * for this adapter. It does NOT cover the live-chat case (an agent turn
183
+ * running pi's own bash tool): pi's SDK runs its agent loop in-process and
184
+ * builds that tool's subprocess env internally, with no hook for a host to
185
+ * inject its own vars. That gap is closed differently — the SDK's bash tool
186
+ * already exposes PI_SESSION_ID to every command it runs, and that id IS the
187
+ * Bivy session id for a pi session (this.id reads the exact same
188
+ * SessionManager the SDK reads it from) — so `bivy attach` accepts
189
+ * PI_SESSION_ID as an equivalent fallback (see bin/attach-session-id.mjs).
178
190
  */
179
191
  async interactiveTuiCommand() {
180
192
  const file = this.sessionFile;
@@ -187,7 +199,7 @@ class PiSession {
187
199
  return {
188
200
  command: process.execPath,
189
201
  args: [this.tui.piCli, "--session", file, "--session-dir", this.tui.sessionsDir],
190
- env: { PI_CODING_AGENT_DIR: this.tui.piDir },
202
+ env: { PI_CODING_AGENT_DIR: this.tui.piDir, ...bivySessionEnv(this.id) },
191
203
  };
192
204
  }
193
205
  prompt(text, options) {
@@ -7,6 +7,7 @@ import { stripAnsi } from "./ansi.js";
7
7
  import { buildAgentCredentialEnv } from "./credentials.js";
8
8
  import { egressEnv } from "../harness/egress.js";
9
9
  import { depCacheEnv } from "../harness/dep-cache.js";
10
+ import { bivySessionEnv } from "./session-env.js";
10
11
  /**
11
12
  * Send `signal` to `child`'s whole process group when possible, so a forking CLI
12
13
  * agent's grandchildren (it shells out to git/npm/build tools, or forks its own
@@ -279,7 +280,10 @@ class ProcessSession {
279
280
  cwd: this.cwd,
280
281
  // egressEnv() routes this agent's outbound traffic through the harness
281
282
  // network broker when BIVY_EGRESS_PROXY is enabled (else it's {}).
282
- env: { ...process.env, ...depCacheEnv(), ...this.runtimeOptions.env, ...credentialEnv, ...prepareEnv, ...egressEnv() },
283
+ // bivySessionEnv() lets the agent's own shell resolve its session for
284
+ // `bivy attach <path>` (see session-env.ts); spread last so it can never
285
+ // be shadowed by an operator-configured env var of the same name.
286
+ env: { ...process.env, ...depCacheEnv(), ...this.runtimeOptions.env, ...credentialEnv, ...prepareEnv, ...egressEnv(), ...bivySessionEnv(this.id) },
283
287
  stdio: "pipe",
284
288
  // Detached so the child becomes the leader of its own process group
285
289
  // (POSIX) — see killProcessGroup() / abort() below, which kill that whole
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
6
  import { buildAgentCredentialEnv } from "./credentials.js";
7
+ import { bivySessionEnv } from "./session-env.js";
7
8
  import { extractTokenUsage } from "./cli-parsers.js";
8
9
  /** A protocol `usage` message → UsageSnapshot (reuses the CLI token-key scan). */
9
10
  function parseProtocolUsage(raw) {
@@ -258,7 +259,10 @@ class ProtocolSession {
258
259
  : {};
259
260
  const child = spawn(this.runtimeOptions.command, this.runtimeOptions.args ?? [], {
260
261
  cwd: this.cwd,
261
- env: { ...process.env, ...this.runtimeOptions.env, ...credentialEnv },
262
+ // bivySessionEnv() lets the agent's own shell resolve its session for
263
+ // `bivy attach <path>` (see session-env.ts); spread last so it can never
264
+ // be shadowed by an operator-configured env var of the same name.
265
+ env: { ...process.env, ...this.runtimeOptions.env, ...credentialEnv, ...bivySessionEnv(this.id) },
262
266
  stdio: "pipe",
263
267
  });
264
268
  this.child = child;
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ //
4
+ // Shared across every runtime adapter that spawns (or configures the spawn of) a
5
+ // subprocess for its agent: fold this into that subprocess's env so the agent's
6
+ // own shell can resolve its chat session without being told the id.
7
+ // `bivy attach <path>` (bin/bivy.mjs's cmdAttach) reads $BIVY_SESSION_ID to know
8
+ // which session to post an outbound attachment to — see
9
+ // claude-code.ts's BIVY_ATTACH_SYSTEM_PROMPT for the discoverability half of this
10
+ // feature (issue #288 shipped attach; issue #290 is making it universal). One
11
+ // helper, one env-var name, so a new adapter can't independently invent — or
12
+ // simply forget — its own convention.
13
+ //
14
+ // Used by claude-code.ts (spawnQuery), process.ts (ProcessSession.prompt), and
15
+ // protocol.ts (ProtocolSession.start). pi.ts is the one exception: Pi runs its
16
+ // agent loop in-process rather than spawning a subprocess Bivy controls, so it
17
+ // has no hook to inject this into its bash tool's env the same way — see the
18
+ // comment on PiSession.interactiveTuiCommand and bin/attach-session-id.mjs for
19
+ // how that gap is closed instead.
20
+ export function bivySessionEnv(sessionId) {
21
+ return { BIVY_SESSION_ID: sessionId };
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.4.0-staging.52",
3
+ "version": "0.4.0-staging.54",
4
4
  "type": "module",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",