@bivy/bivy 0.19.2-staging.2 → 0.19.2-staging.4

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/acp-shim.mjs CHANGED
@@ -149,11 +149,11 @@ const permissionRequests = new Map();
149
149
  // carded — a second time for the same file.
150
150
  const approvedCalls = new Set();
151
151
 
152
- /** The approved, still-running call whose inputs name `file`, if any. */
153
- function approvedCallWriting(file) {
154
- for (const id of approvedCalls) {
152
+ /** The still-running call among `ids` whose inputs name `file`, if any. */
153
+ function callWriting(file, ids) {
154
+ for (const id of ids) {
155
155
  const state = toolCallState.get(id);
156
- if (!state) { approvedCalls.delete(id); continue; }
156
+ if (!state) { if (ids === approvedCalls) approvedCalls.delete(id); continue; }
157
157
  const input = toolCallInput(state);
158
158
  const paths = [input.path, input.file_path, input.filePath, ...(state.locations || []).map((l) => l?.path)].filter(Boolean);
159
159
  const root = fs.realpathSync(cwd);
@@ -510,15 +510,20 @@ async function onAgentRequest(id, method, params) {
510
510
  try {
511
511
  if (sandboxTier === "read-only") throw new Error("writes are disabled by the read-only sandbox");
512
512
  const file = workspacePath(params?.path, { write: true });
513
- if (approvedCallWriting(file)) {
513
+ if (callWriting(file, approvedCalls)) {
514
514
  fs.writeFileSync(file, String(params?.content ?? ""));
515
515
  agentReply(id, {});
516
516
  return;
517
517
  }
518
- const toolCallId = `acp-fs-write-${id}`;
519
- permissionRequests.set(toolCallId, { kind: "fs-write", requestId: id, file, content: String(params?.content ?? "") });
518
+ // An auto-run edit (no permission asked) performs its change through this
519
+ // write. Gate the write under that call's own id, so the transcript keeps
520
+ // one card for the edit instead of a second "Created" card for its effect.
521
+ const owner = callWriting(file, toolCallState.keys());
522
+ const ownerState = owner ? toolCallState.get(owner) : undefined;
523
+ const toolCallId = owner ?? `acp-fs-write-${id}`;
524
+ permissionRequests.set(toolCallId, { kind: "fs-write", requestId: id, file, content: String(params?.content ?? ""), ownCard: !owner });
520
525
  const parentCall = childRoute(params?.sessionId);
521
- bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file }, ...(parentCall ? { parentToolCallId: parentCall } : {}) });
526
+ bivy({ type: "tool.call", toolCallId, name: ownerState ? toolCallName(ownerState) : "write", input: ownerState ? toolCallInput(ownerState) : { path: file }, ...(parentCall ? { parentToolCallId: parentCall } : {}) });
522
527
  } catch (e) {
523
528
  agentReplyError(id, -32000, `write failed: ${e.message}`);
524
529
  }
@@ -674,7 +679,10 @@ async function onBivyCommand(msg) {
674
679
  }
675
680
  if (error) agentReplyError(entry.requestId, allow ? -32000 : -32001, error);
676
681
  else agentReply(entry.requestId, {});
677
- bivy({ type: "tool.result", toolCallId: msg.toolCallId, name: "write", result: error || `Wrote ${entry.file}`, isError: Boolean(error) });
682
+ // A write gated under the agent's own call leaves that card to the
683
+ // agent's completion update.
684
+ if (!entry.ownCard && allow) approvedCalls.add(msg.toolCallId);
685
+ if (entry.ownCard) bivy({ type: "tool.result", toolCallId: msg.toolCallId, name: "write", result: error || `Wrote ${entry.file}`, isError: Boolean(error) });
678
686
  return;
679
687
  }
680
688
  // Pick an ACP option matching the human's choice by its `kind`
@@ -4,7 +4,7 @@ import { defineAgentIntegration } from "../definition.js";
4
4
  import { withExactCapabilitySurface } from "../../runtime/types.js";
5
5
  import { createCredentialStore } from "../../runtime/credentials.js";
6
6
  import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled, } from "./runtime.js";
7
- export const CLAUDE_TESTED_VERSION = "0.3.284";
7
+ export const CLAUDE_TESTED_VERSION = "0.3.285";
8
8
  const CLAUDE_CAPABILITIES = withExactCapabilitySurface({
9
9
  toolInterception: true,
10
10
  modelSelection: true,
@@ -10,7 +10,7 @@ import { codexCredentialPreflight } from "../../runtime/codex-preflight.js";
10
10
  import { deleteCodexSession, discoverNativeCodexSessions, loadCodexTranscript, writeCodexRollout, exportCodexRollout, importCodexRollout, } from "../../runtime/codex-sessions.js";
11
11
  import { ProtocolRuntime } from "../../runtime/protocol.js";
12
12
  import { codexSlashCommands } from "../../runtime/slash-commands.js";
13
- export const CODEX_TESTED_VERSION = "0.159.0";
13
+ export const CODEX_TESTED_VERSION = "0.159.1";
14
14
  function codexCommand() {
15
15
  return process.env.BIVY_CODEX_BIN?.trim() || "codex";
16
16
  }
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import { defineAgentIntegration } from "../definition.js";
7
7
  import { PI_CAPABILITIES } from "./capabilities.js";
8
8
  import { bridgeInstalled, enableBridgeResolution } from "../../agent-bridges.mjs";
9
- export const PI_TESTED_VERSION = "0.87.1";
9
+ export const PI_TESTED_VERSION = "0.99.1";
10
10
  export function piAgentDir() {
11
11
  return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
12
12
  }
@@ -25,6 +25,15 @@ iframe { width:100%; height:100%; border:0; background:var(--bg); }
25
25
  nav, #draw-bar { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:var(--space-1); max-width:100%; padding:var(--space-1); background:var(--surface); border:thin solid var(--line); border-radius:var(--radius-xl); box-shadow:var(--shadow-lg); }
26
26
  nav .btn, #draw-bar .btn { flex-shrink:0; border-radius:var(--radius-full); }
27
27
  nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accent); }
28
+ /* One row, however narrow: a bar that wraps doubles what it covers. Past the
29
+ edge it scrolls sideways instead. */
30
+ nav { flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none; }
31
+ nav::-webkit-scrollbar { display:none; }
32
+ /* The grip hides the bar on a tap and moves it to the other edge on a drag. */
33
+ #hide { color:var(--muted); cursor:grab; touch-action:none; }
34
+ #dock.dragging, #dock.dragging #hide { cursor:grabbing; }
35
+ #dock[data-edge="top"] { top:calc(var(--space-2) + env(safe-area-inset-top)); bottom:auto; flex-direction:column-reverse; }
36
+ #show[data-edge="top"] { top:calc(var(--space-3) + env(safe-area-inset-top)); bottom:auto; }
28
37
  #name { display:flex; flex-direction:column; min-width:0; padding:0 var(--space-2); }
29
38
  #title { font-size:var(--text-sm); font-weight:var(--weight-semibold); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:14em; }
30
39
  #stamp { font-size:var(--text-xs); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
@@ -80,11 +89,11 @@ nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accen
80
89
  #compare[data-drawing] #compare-stage img { max-height:min(60vh, 560px); }
81
90
  #compare[data-drawing] #compare-slider, #compare[data-drawing] #compare-hint, #compare[data-drawing] #compare-draw { display:none; }
82
91
  .label-narrow { display:none; }
83
- @media (max-width: 699px) { #lens { display:none; } #name { display:none; } .label-wide { display:none; } .label-narrow { display:inline; } }
92
+ @media (max-width: 699px) { #lens { display:none; } #name { display:none; } .label-wide { display:none; } .label-narrow { display:inline; } nav { gap:0; } nav .btn { padding-inline:var(--space-2); } }
84
93
  /* "Made with Bivy": a quiet strip under a shared app, never over it. */
85
94
  #made-with { flex-shrink:0; display:flex; align-items:center; justify-content:center; gap:var(--space-1); min-height:var(--space-6); padding:0 var(--space-3) env(safe-area-inset-bottom); box-sizing:border-box; background:var(--surface); border-top:thin solid var(--line); color:var(--muted); font-size:var(--text-xs); text-decoration:none; }
86
95
  #made-with:hover, #made-with:focus-visible { color:var(--ink); }
87
- body.badged #dock { bottom:calc(var(--space-2) + var(--space-6) + env(safe-area-inset-bottom)); }
96
+ body.badged #dock:not([data-edge="top"]) { bottom:calc(var(--space-2) + var(--space-6) + env(safe-area-inset-bottom)); }
88
97
  [hidden] { display:none !important; }
89
98
  </style></head><body>
90
99
  <div class="banner" data-tone="warn" id="down" hidden><span class="banner-text" id="down-text" role="status"></span><span class="banner-actions"><button class="btn sm" id="ask" hidden>Ask agent to fix</button></span></div>
@@ -127,9 +136,9 @@ body.badged #dock { bottom:calc(var(--space-2) + var(--space-6) + env(safe-area-
127
136
  <button class="btn sm primary" id="draw-done" type="button" disabled>Done</button>
128
137
  </div>
129
138
  <nav aria-label="Bivy preview controls">
139
+ <button class="btn sm ghost" id="hide" aria-label="Hide Bivy controls" title="Tap to hide · Drag to move" aria-keyshortcuts="ArrowUp ArrowDown"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg></button>
130
140
  <button class="btn sm ghost" id="back" aria-label="Back to chat">‹ <span class="label-wide">Back to chat</span><span class="label-narrow">Chat</span></button>
131
141
  <span id="name"><span id="title">App preview</span><span id="stamp" class="muted" role="status"></span></span>
132
- <button class="btn sm ghost" id="hide" aria-label="Hide Bivy controls" title="Hide controls">⌄</button>
133
142
  <button class="btn sm ghost" id="point" aria-pressed="false" disabled>Point</button>
134
143
  <button class="btn sm ghost" id="draw" type="button" hidden>Draw</button>
135
144
  <button class="btn sm ghost" id="compare-btn" aria-pressed="false" hidden>Compare</button>
@@ -468,8 +477,28 @@ compareBtn.onclick=async()=>{
468
477
  };
469
478
  const split=()=>{$('compare-before').style.clipPath='inset(0 '+(100-Number($('compare-slider').value))+'% 0 0)';};
470
479
  $('compare-slider').oninput=split;
471
- // The pill can cover an app's own bottom bar; collapse it to a corner button.
472
- $('hide').onclick=()=>{if(point.getAttribute('aria-pressed')==='true')pointing(false);$('dock').hidden=true;$('show').hidden=false;$('show').focus();};
480
+ // The pill can cover an app's own bars: drag its grip to the other edge (kept
481
+ // for next time), or tap it to collapse the pill to a corner button.
482
+ const dock=$('dock'),grip=$('hide'),edgeKey='bivy-preview-edge';
483
+ function edge(to,keep){
484
+ dock.dataset.edge=to;$('show').dataset.edge=to;
485
+ if(keep)try{localStorage.setItem(edgeKey,to);}catch{}
486
+ }
487
+ try{edge(localStorage.getItem(edgeKey)==='top'?'top':'bottom');}catch{edge('bottom');}
488
+ let drag=null,dragged=false;
489
+ grip.onpointerdown=e=>{drag={y:e.clientY,moved:false};try{grip.setPointerCapture(e.pointerId);}catch{}};
490
+ grip.onpointermove=e=>{
491
+ if(!drag)return;const dy=e.clientY-drag.y;
492
+ if(!drag.moved&&Math.abs(dy)<8)return;
493
+ drag.moved=true;dock.classList.add('dragging');dock.style.transform='translateY('+dy+'px)';
494
+ };
495
+ grip.onpointerup=grip.onpointercancel=e=>{
496
+ if(!drag)return;const {moved}=drag;drag=null;
497
+ if(moved){const r=grip.getBoundingClientRect();edge(r.top+r.height/2<innerHeight/2?'top':'bottom',true);dragged=true;setTimeout(()=>{dragged=false;});}
498
+ dock.classList.remove('dragging');dock.style.transform='';
499
+ };
500
+ grip.onkeydown=e=>{if(e.key==='ArrowUp'||e.key==='ArrowDown'){e.preventDefault();edge(e.key==='ArrowUp'?'top':'bottom',true);}};
501
+ grip.onclick=()=>{if(dragged){dragged=false;return;}if(point.getAttribute('aria-pressed')==='true')pointing(false);dock.hidden=true;$('show').hidden=false;$('show').focus();};
473
502
  $('show').onclick=()=>{$('dock').hidden=false;$('show').hidden=true;$('hide').focus();};
474
503
  // Device lens (desktop widths)
475
504
  for(const b of $('lens').querySelectorAll('button'))b.onclick=()=>{
@@ -669,6 +669,7 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
669
669
  .tool-detail-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
670
670
  .tool-detail-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 10px; max-width: 100%; overflow-x: auto; white-space: pre; }
671
671
  .tool-detail-value.output { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; max-height: 320px; overflow-y: auto; }
672
+ .handoff-seed-text { max-height: none; }
672
673
  .tool-detail-value.output pre,
673
674
  .tool-detail-value.output code { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
674
675
 
@@ -3,9 +3,9 @@
3
3
  export const CERTIFICATION_MATRIX = {
4
4
  schemaVersion: 1,
5
5
  agents: [
6
- { id: "claude-code-sdk", status: "active", executionMode: "protocol", pinnedVersion: "0.3.284", capabilities: ["toolInterception", "modelSelection", "resume"] },
7
- { id: "codex-approvals", status: "active", executionMode: "protocol", pinnedVersion: "0.159.0", capabilities: ["toolInterception", "modelSelection", "resume"] },
8
- { id: "pi", status: "active", executionMode: "protocol", pinnedVersion: "0.87.1", capabilities: ["toolInterception", "modelSelection", "resume"] },
6
+ { id: "claude-code-sdk", status: "active", executionMode: "protocol", pinnedVersion: "0.3.285", capabilities: ["toolInterception", "modelSelection", "resume"] },
7
+ { id: "codex-approvals", status: "active", executionMode: "protocol", pinnedVersion: "0.159.1", capabilities: ["toolInterception", "modelSelection", "resume"] },
8
+ { id: "pi", status: "active", executionMode: "protocol", pinnedVersion: "0.99.1", capabilities: ["toolInterception", "modelSelection", "resume"] },
9
9
  { id: "opencode", status: "active", executionMode: "protocol", pinnedVersion: "1.18.33", capabilities: ["toolInterception", "modelSelection", "resume"] },
10
10
  { id: "grok", status: "active", executionMode: "protocol", pinnedVersion: "1.0.44", capabilities: ["toolInterception", "modelSelection", "resume"] },
11
11
  ]
@@ -50,6 +50,18 @@ export function parseResetsAt(raw) {
50
50
  const iso = /(20\d\d-\d\d-\d\dT[\d:.]+(?:Z|[+-]\d\d:?\d\d))/.exec(raw);
51
51
  return iso?.[1];
52
52
  }
53
+ /**
54
+ * Parse a calendar reset date — Codex's ChatGPT limits read "try again at Oct
55
+ * 4th, 2026 5:18 AM". The CLI formats it in the machine's local time, and it
56
+ * runs on the node, so the local-time parse is the intended one.
57
+ */
58
+ export function parseResetDate(raw) {
59
+ const m = /(?:try again|resets?)\s+(?:at|on|after)?\s*([A-Z][a-z]{2,8}\.?\s+\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4}),?\s+(\d{1,2}:\d{2}\s*(?:[AP]M)?)/i.exec(raw);
60
+ if (!m)
61
+ return undefined;
62
+ const ms = Date.parse(`${m[1].replace(".", "")} ${m[2]} ${m[3].replace(/\s*([AP]M)$/i, " $1")}`);
63
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
64
+ }
53
65
  /**
54
66
  * Parse a bare wall-clock reset time — the shape Claude's subscription limits
55
67
  * surface, e.g. `resets 12am (UTC)`, `resets at 3pm UTC`, `resets 09:00 UTC` —
@@ -146,7 +158,7 @@ export function classifyFailure(error, opts = {}) {
146
158
  // supplied (the provider's own usage snapshot), then an ISO stamp in the
147
159
  // text, then a bare wall-clock ("resets 12am (UTC)") resolved to its next
148
160
  // occurrence.
149
- const resetsAt = opts.resetsAtHint ?? parseResetsAt(raw) ?? parseResetClock(raw, opts.now ?? Date.now());
161
+ const resetsAt = opts.resetsAtHint ?? parseResetsAt(raw) ?? parseResetDate(raw) ?? parseResetClock(raw, opts.now ?? Date.now());
150
162
  if (resetsAt !== undefined)
151
163
  out.resetsAt = resetsAt;
152
164
  }
@@ -10,6 +10,7 @@ import { mergeAgentCommands } from "./slash-commands.js";
10
10
  import { withExactCapabilitySurface } from "./types.js";
11
11
  import { extractTokenUsage } from "./cli-parsers.js";
12
12
  import { mapToolCall, mapToolResult } from "./tool-call-map.js";
13
+ import { stripAnsi } from "./ansi.js";
13
14
  /** A tool message's sub-agent parent (`parentToolCallId`), spread into the
14
15
  * runtime event and persisted block so delegated work nests under its call. */
15
16
  function parentOf(msg) {
@@ -182,6 +183,8 @@ class ProtocolSession {
182
183
  * instead of welding `"first." + "Second"` into `"first.Second"`. */
183
184
  assistantItemBoundary = false;
184
185
  reasoningText = "";
186
+ /** The turnContent thinking block the current reasoning item grows. */
187
+ reasoningBlock;
185
188
  stderrOutput = "";
186
189
  lastUsage;
187
190
  // Accumulate the current turn's content blocks (text + tool calls, in the order
@@ -399,7 +402,7 @@ class ProtocolSession {
399
402
  child.stdout.on("data", (chunk) => this.onData(chunk.toString("utf8")));
400
403
  child.stderr.on("data", (chunk) => {
401
404
  this.stderrOutput += chunk.toString("utf8");
402
- this.emit({ type: "tool_execution_update", toolName: "agent_output", toolCallId: "agent-output", input: { stream: "stderr", output: this.stderrOutput.slice(-4000) } });
405
+ this.emit({ type: "tool_execution_update", toolName: "agent_output", toolCallId: "agent-output", input: { stream: "stderr", output: stripAnsi(this.stderrOutput.slice(-4000)) } });
403
406
  });
404
407
  // The agent's stdin pipe can break (EPIPE) when the shim exits mid-turn — for
405
408
  // example a dispose()/abort() racing an in-flight write (a tool.decision reply,
@@ -567,6 +570,21 @@ class ProtocolSession {
567
570
  if (pending)
568
571
  this.turnContent.push({ type: "text", text: pending });
569
572
  }
573
+ /** Keep reasoning in the turn's ordered blocks where it streamed. Without it a
574
+ * reopened transcript only had the time-placed sidecar copy, which lands ahead
575
+ * of the whole (single-message) turn, so every thought stacked at the top.
576
+ * Consecutive chunks grow one block; prose or a tool in between starts a new one. */
577
+ recordReasoning(chunk) {
578
+ const last = this.turnContent[this.turnContent.length - 1];
579
+ const textSince = this.assistantText.length > this.turnTextFlushed.length;
580
+ if (last && last === this.reasoningBlock && !textSince) {
581
+ last.thinking = `${String(last.thinking ?? "")}${chunk}`;
582
+ return;
583
+ }
584
+ this.flushPendingTurnText();
585
+ this.reasoningBlock = { type: "thinking", thinking: chunk };
586
+ this.turnContent.push(this.reasoningBlock);
587
+ }
570
588
  /** Publish a stable transcript entry at item boundaries, while keeping the
571
589
  * live text cumulative. Replace this entry as the turn grows so reconnects
572
590
  * retain commentary without duplicating it at completion. */
@@ -724,6 +742,8 @@ class ProtocolSession {
724
742
  const text = String(msg.text ?? msg.delta ?? "");
725
743
  if (text) {
726
744
  this.reasoningText += text;
745
+ if (this.streaming)
746
+ this.recordReasoning(text);
727
747
  this.emit({ type: "message_update", message: { role: "assistant", content: [{ type: "thinking", thinking: this.reasoningText }] } });
728
748
  }
729
749
  return;
@@ -746,7 +766,7 @@ class ProtocolSession {
746
766
  // text, so a tool-free turn (turnContent still empty at this point) keeps
747
767
  // the plain-text message shape it always had instead of gaining a
748
768
  // pointless single-text-block wrapper.
749
- const hadTools = this.turnContent.length > 0 || this.turnToolResults.length > 0;
769
+ const hadTools = this.turnContent.some((b) => b.type !== "thinking") || this.turnToolResults.length > 0;
750
770
  const message = { role: "assistant", content: this.assistantText };
751
771
  // Persist the assistant turn. When the turn used tools, store the ordered
752
772
  // content blocks (text/tool_use interleaved exactly as they streamed — see
@@ -783,7 +803,7 @@ class ProtocolSession {
783
803
  // reply and its tool calls/results — persisted exactly as session.done
784
804
  // persists a clean turn. The old handler wiped turnContent here, so a mid-
785
805
  // turn failure dropped all of that work from the reopened transcript.
786
- const hadTools = this.turnContent.length > 0 || this.turnToolResults.length > 0;
806
+ const hadTools = this.turnContent.some((b) => b.type !== "thinking") || this.turnToolResults.length > 0;
787
807
  if (hadTools) {
788
808
  this.flushPendingTurnText();
789
809
  if (this.turnContent.length)
@@ -813,7 +833,10 @@ class ProtocolSession {
813
833
  // not), so the preserved content + error marker are durably persisted.
814
834
  this.emit({ type: "message_end", message: errored });
815
835
  this.emit({ type: "session.error", error: errorText });
816
- this.emit({ type: "agent_end" });
836
+ // Carry the failure on agent_end too (Claude Code's shape), so the daemon's
837
+ // turn-end recovery sees it: usage-limit offers (fork / retry at reset),
838
+ // reroute, the failed state, and the error notification.
839
+ this.emit({ type: "agent_end", error: errorText });
817
840
  return;
818
841
  }
819
842
  if (type === "tool.call" || type === "tool.observe") {
@@ -986,7 +1009,7 @@ class ProtocolSession {
986
1009
  this.emit({ type: "session.error", error: preflightError });
987
1010
  this.emit({ type: "message_end", message });
988
1011
  this.emit({ type: "turn_end" });
989
- this.emit({ type: "agent_end", code: 1, signal: null });
1012
+ this.emit({ type: "agent_end", code: 1, signal: null, error: preflightError });
990
1013
  return;
991
1014
  }
992
1015
  if (!wasStarted)
@@ -163,6 +163,11 @@ export function mapToolCall(toolName, input, context = {}) {
163
163
  return undefined;
164
164
  const oldText = str(o, "old_string", "oldString", "old", "before", "search");
165
165
  const newText = str(o, "new_string", "newString", "new", "after", "replace", "replacement");
166
+ // An edit tool handed the whole file (OpenCode's and Grok's `edit` when
167
+ // creating one) is a write, not an edit with nothing to diff.
168
+ if (!oldText && !newText && !o.changes && str(o, "content", "contents", "file_text")) {
169
+ return decorate({ kind: "write", path }, toolName, input, context);
170
+ }
166
171
  return decorate({ kind: "edit", path, ...(oldText ? { oldText } : {}), ...(newText ? { newText } : {}) }, toolName, input, context);
167
172
  }
168
173
  if (inToolSet(WRITE, toolName, key)) {
package/dist/server.js CHANGED
@@ -9146,6 +9146,14 @@ const forkStandUp = createForkStandUp({
9146
9146
  applyDirtyPatch,
9147
9147
  gitRepoRoot,
9148
9148
  materializeFork,
9149
+ saveForkTranscript: (markdown) => {
9150
+ // Outside the worktree, so the hand-off never shows up as a repo change.
9151
+ const dir = path.join(appDir, "fork-transcripts");
9152
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
9153
+ const file = path.join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}.md`);
9154
+ fs.writeFileSync(file, markdown, { mode: 0o600 });
9155
+ return file;
9156
+ },
9149
9157
  getRuntime,
9150
9158
  listRuntimes,
9151
9159
  reposRoot,
@@ -9849,7 +9857,7 @@ app.get("/api/models", async (req, res, next) => {
9849
9857
  const current = session.getCurrentModel();
9850
9858
  const models = await publicModelsList(session, current);
9851
9859
  const thinking = publicThinkingInfo(session);
9852
- res.json({ sessionId: record.id, modelSelection: getRuntime(record.runtimeId).capabilities.modelSelection !== false, current: current ? publicModel(current, current) : null, models, thinking });
9860
+ res.json({ sessionId: record.id, runtimeId: record.runtimeId, modelSelection: getRuntime(record.runtimeId).capabilities.modelSelection !== false, current: current ? publicModel(current, current) : null, models, thinking });
9853
9861
  }
9854
9862
  catch (error) {
9855
9863
  next(error);
@@ -213,7 +213,7 @@ export function createForkStandUp(deps) {
213
213
  // cross-agent fork must let the destination choose its own default unless the
214
214
  // caller explicitly supplied a model valid for that target.
215
215
  const targetModel = opts.model ?? (targetRuntimeId === bundle.record.runtimeId ? bundle.record.modelRef : undefined);
216
- const plan = await deps.materializeFork({ bundle, targetRuntime, ctx: { workspace, cwd, model: targetModel }, seed: { transcriptUrl: opts.transcriptUrl } });
216
+ const plan = await deps.materializeFork({ bundle, targetRuntime, ctx: { workspace, cwd, model: targetModel }, seed: { transcriptUrl: opts.transcriptUrl }, saveTranscript: deps.saveForkTranscript });
217
217
  const record = plan.kind === "resume"
218
218
  ? await deps.createSession(cwd, plan.sessionFile, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false, newSession: true })
219
219
  : await deps.createSession(cwd, undefined, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false, newSession: true });
@@ -1,4 +1,4 @@
1
- import { normalizeMessages, buildSeedPrompt, buildForkHistory, } from "./transcript-normal.js";
1
+ import { normalizeMessages, buildSeedPrompt, buildForkHistory, renderForkTranscript, } from "./transcript-normal.js";
2
2
  /** Cap on the display transcript a bundle carries; larger sessions fall back to
3
3
  * showing the destination agent's own (text) replay. */
4
4
  export const FORK_DISPLAY_MAX_BYTES = 2 * 1024 * 1024;
@@ -124,9 +124,19 @@ export async function materializeFork(opts) {
124
124
  // Fall through to the agent-independent seeded continuation below.
125
125
  }
126
126
  }
127
+ // The seed only inlines the recent tail. Give the agent the rest as a file
128
+ // its tools can read — an app URL is a web page it can't fetch.
129
+ let transcriptFile;
130
+ try {
131
+ transcriptFile = opts.saveTranscript?.(renderForkTranscript(bundle.normalized));
132
+ }
133
+ catch {
134
+ transcriptFile = undefined;
135
+ }
127
136
  const seedPrompt = buildSeedPrompt(bundle.normalized, {
128
137
  targetAgent: targetRuntime.displayName,
129
138
  context: { repoSlug: bundle.record.repoSlug, branch: bundle.record.branch, prUrl: bundle.record.prUrl },
139
+ ...(transcriptFile ? { transcriptFile } : {}),
130
140
  ...opts.seed,
131
141
  });
132
142
  return { kind: "seed", fidelity: "seeded", seedPrompt };
@@ -136,7 +136,9 @@ export function mergeTranscript(base, extras) {
136
136
  const text = normalizedIntermediateText(thinkingTextFromContent(entry.content));
137
137
  // Skip a sidecar reasoning copy when the adjacent persisted message already
138
138
  // carries the same reasoning, and dedupe repeated stream-final copies.
139
- const alreadyInTranscript = text && [base[index - 1], base[index]].some((message) => normalizedIntermediateText(thinkingTextFromContent(message?.content)) === text);
139
+ // A protocol turn persists each reasoning item as its own block inside one
140
+ // message, so match containment, not only the message's whole reasoning.
141
+ const alreadyInTranscript = text && [base[index - 1], base[index]].some((message) => normalizedIntermediateText(thinkingTextFromContent(message?.content)).includes(text));
140
142
  const dedupeKey = `${index}:${text}`;
141
143
  if (alreadyInTranscript || seenIntermediate.has(dedupeKey))
142
144
  continue;
@@ -168,23 +168,29 @@ export function buildSeedPrompt(transcript, opts = {}) {
168
168
  picked.reverse();
169
169
  const omitted = formatted.length - picked.length;
170
170
  const recent = picked.length ? picked.join("\n") : "- (no prior turns were available)";
171
+ const file = opts.transcriptFile;
172
+ const where = file ? "the conversation file above" : opts.transcriptUrl ? "the full transcript linked above" : "the full transcript";
171
173
  const lines = [
172
174
  `I am continuing an existing Bivy session (forked from ${transcript.header.sourceRuntimeId} to ${targetAgent}).`,
173
175
  `Session: ${title}`,
174
- opts.transcriptUrl ? `Full original transcript: ${opts.transcriptUrl}` : null,
176
+ file
177
+ ? `Full earlier conversation, as a local file (read it with your file tools): ${file}`
178
+ : opts.transcriptUrl ? `Full original transcript: ${opts.transcriptUrl}` : null,
175
179
  transcript.header.model ? `Model before fork: ${transcript.header.model}` : null,
176
180
  opts.context?.repoSlug ? `Repository: ${opts.context.repoSlug}` : null,
177
181
  opts.context?.branch ? `Branch: ${opts.context.branch}` : null,
178
182
  opts.context?.prUrl ? `PR: ${opts.context.prUrl}` : null,
179
183
  "",
180
184
  omitted > 0
181
- ? `Recent conversation (most recent last; ${omitted} earlier turn${omitted === 1 ? "" : "s"} omitted — see the full transcript${opts.transcriptUrl ? " linked above" : ""}):`
185
+ ? `Recent conversation (most recent last; ${omitted} earlier turn${omitted === 1 ? "" : "s"} omitted — see ${where}):`
182
186
  : "Recent conversation (most recent last):",
183
187
  recent,
184
188
  "",
185
- opts.transcriptUrl
186
- ? "Open the full transcript link above if this summary is missing anything, then continue from here."
187
- : "Continue from here.",
189
+ file
190
+ ? "Read the conversation file above only if this summary is missing something you need; otherwise continue from here."
191
+ : opts.transcriptUrl
192
+ ? "Open the full transcript link above if this summary is missing anything, then continue from here."
193
+ : "Continue from here.",
188
194
  ];
189
195
  return lines.filter((line) => line != null).join("\n");
190
196
  }
@@ -232,3 +238,18 @@ export function buildForkHistory(transcript) {
232
238
  }
233
239
  return history;
234
240
  }
241
+ /**
242
+ * The whole conversation as a readable Markdown document — what a seeded fork's
243
+ * agent reads when the inlined tail isn't enough (`SeedPromptOptions.transcriptFile`).
244
+ * Built from the same portable turns a replayed fork writes, so tool activity is
245
+ * inlined as text.
246
+ */
247
+ export function renderForkTranscript(transcript) {
248
+ const header = [
249
+ `# ${transcript.header.title || "Untitled session"}`,
250
+ "",
251
+ `Earlier conversation from a Bivy session on ${transcript.header.sourceRuntimeId}${transcript.header.model ? ` (${transcript.header.model})` : ""}.`,
252
+ ];
253
+ const turns = buildForkHistory(transcript).map((turn) => `## ${turn.role === "user" ? "User" : "Assistant"}\n\n${turn.text}`);
254
+ return [...header, "", ...turns].join("\n\n").replace(/\n{3,}/g, "\n\n") + "\n";
255
+ }
@@ -159,8 +159,11 @@ export function createTranscriptPersistence(deps) {
159
159
  // delegation that spawned them (toolEntriesFromContent → toolParentId),
160
160
  // exactly like the live stream does. Without this the parent id lived only
161
161
  // on the in-flight event and reopening a session flattened sub-agent work.
162
- // Generic: any runtime whose tool_call event carries parentToolUseId nests.
163
- const parentToolUseId = typeof event.parentToolUseId === "string" && event.parentToolUseId ? event.parentToolUseId : undefined;
162
+ // Generic: any runtime whose tool_call event names its parent nests —
163
+ // `parentToolUseId` (Claude, protocol agents) or `parentToolCallId` (Pi's
164
+ // nested calls from codemode / ctx.executeTool).
165
+ const rawParent = event.parentToolUseId ?? event.parentToolCallId;
166
+ const parentToolUseId = typeof rawParent === "string" && rawParent ? rawParent : undefined;
164
167
  eventLog.append(record.id, { ...base, id: `bivy-tool-call-${callId}`, content: [{ type: "tool_use", id: callId, name, input, ...(parentToolUseId ? { parentToolUseId } : {}), ...(event.detail ? { detail: event.detail } : {}) }] });
165
168
  }
166
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.19.2-staging.2",
3
+ "version": "0.19.2-staging.4",
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.",
@@ -68,8 +68,8 @@
68
68
  "readme": "# Bivy\n\n[![npm](https://img.shields.io/npm/v/@bivy/bivy?color=2b6cb0&label=%40bivy%2Fbivy)](https://www.npmjs.com/package/@bivy/bivy)\n[![license: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-2b6cb0)](LICENSE)\n[![node](https://img.shields.io/badge/node-%E2%89%A520-2b6cb0)](https://nodejs.org)\n\n**Build, try, and review apps from your phone. Your coding agent and dev\nenvironment stay on your own machines.**\n\nBivy is an open-source remote development environment built around coding\nagents. Ask for a change, try the running app, and point to or draw on what\nneeds fixing. Send that feedback to the agent and try again—all in the same\nsession, without going back to your computer.\n\nLive previews bring your app to your phone or browser without deploying it to\nRender, building a Docker image, or setting up a separate preview server.\nShare a preview link so someone else can try it and leave notes, too.\n\nKeep Claude Code, Codex, Pi, OpenCode, or another supported agent. Keep your\nrepos, tools, databases, and development environment. Bivy connects them to\nchat, a terminal, app previews, and code review wherever you are. Start work\nyourself, or let an issue, CI failure, message, schedule, or webhook start it.\n\n**[Start free on Bivy Cloud](https://app.bivy.sh)** ·\n**[Quickstart](docs/quickstart.md)** ·\n**[Documentation](docs/README.md)** ·\n**[Self-host](docs/deploy-images.md)** ·\n**[Website](https://bivy.sh)**\n\n**Recommended:** sign in at [app.bivy.sh](https://app.bivy.sh), then copy your\npersonalized **Connect a Machine** command into a terminal on your Mac or Linux\ncomputer. It installs and enrolls the machine without another Bivy login. The\nbrowser connects automatically; choose a repository and send your first task\nright there.\n\nPrefer starting from the terminal?\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # or codex, pi, opencode\nbivy open # continue in the web app (needs remote setup)\n```\n\nBivy Cloud hosts the app, control plane, and relay—not the machines running your\nagents. Connect a Mac, Linux computer, or existing server and bring your own\nagent subscription, model API key, or local model. You can also self-host the\nentire remote-access stack.\n\n> **Bivy is 0.x software.** Claude Code, Codex, Pi, and OpenCode are the\n> release-tested paths. Credential sync, resume, handoffs, approvals, and\n> sandboxing depend on the runtime. See the\n> [runtime support matrix](docs/runtime-support-matrix.md).\n\n## Build → preview → mark up → iterate\n\nRemote chat lets you ask an agent for a change. Bivy also lets you **use what\nit built and show it what to fix**, without a separate deployment step.\n\n1. **Ask for a change.** The agent works in your repo with your existing tools\n and local services.\n2. **Try the app.** Open a live preview from the session. Use the running app,\n not just a screenshot of what the agent says it finished. Bivy can discover\n dev servers running in the session workspace; tap **Preview** to open one.\n3. **Mark up what needs work.** Point at an element or circle and draw on the\n preview. Add a note, then send the draft to the agent with the element and\n page context. Marked-up screenshots and before/after comparison are available\n when agent screenshots are enabled.\n4. **Iterate and review.** Try the next version, inspect the diff, review checks,\n and decide when the work is ready—not just when the agent stops.\n5. **Bring someone else into the review.** Copy a public preview link so they\n can use the app without a Bivy account and leave notes on specific elements.\n Bring their notes into the session when you're ready to act on them.\n\nOn Bivy Cloud, preview delivery is built in: no per-app domains, certificates,\npublic ports, or tunnels to configure. Self-hosters configure preview delivery\nonce for their Bivy deployment, not for every app. The app still needs its normal\nbuild or dev-server setup, and **the machine serving it must stay awake and\nonline**. These are development previews, not production hosting.\n\n**Share deliberately:** anyone with a preview link can use that app, including\nits live backend. Links expire after 24 hours and can be revoked sooner.\nReviewer notes aren't sent to the agent automatically; you send them or explicitly\nallow agent access. Preview traffic uses HTTPS through the preview relay, not\nsession end-to-end encryption; the relay operator can see it.\n\n[App previews, visual feedback, and sharing →](docs/apps.md)\n\n## Your agents and machines, one workspace\n\n| Capability | What it means for you |\n|---|---|\n| **One workspace, multiple agents** | Use different agents and models for different tasks without maintaining a separate workflow for each. |\n| **Your machines and environment** | Work beside your existing repos, dev servers, databases, private networks, toolchains, and GPUs. |\n| **Automations and triggers** | Let issues, failed CI, messages, schedules, and webhooks start work instead of copying requests into a chat. |\n| **Encrypted key and OAuth sync** | Reuse Bivy-managed provider credentials across enrolled machines and compatible runtimes, with less repeated setup. |\n| **Live sessions from anywhere** | Build and review from your phone, browser, or terminal; answer questions and approve supported actions in the same session. |\n| **Live app previews** | Try the running app without deploying it to a separate hosting service. |\n| **Visual feedback and sharing** | Point, draw, and send specific feedback to the agent; share a preview link for outside review. |\n| **Reviewable results** | Inspect the app, changes, declared checks, artifacts, and pull requests—not just an agent's claim that it finished. |\n| **Hosted convenience or self-hosting** | Use Bivy Cloud for managed remote access, or run the same open-source core yourself. |\n\n## One workflow, from trigger to review\n\n```text\nPrompt · GitHub issue · CI failure · Linear · Slack · Schedule · Webhook\n │\n ▼\n Choose machine + agent + model\n + supported credentials\n │\n ▼\n Live agent session\n Join · steer · approve · stop\n │\n ▼\n Try app · inspect changes · checks\n │\n ▼\n Mark up · send feedback · iterate\n │\n ▼\n Share preview · review PR\n```\n\nA **Machine** is a computer or server you connect. A **Session** is live agent\nwork on that machine. A **Run** is delegated background work that creates a\nsession and tracks its outcome. An **Automation** is a reusable definition that\ncreates runs when an event matches.\n\nManual and automated work use the same kind of live session. You can join a run\nwhen it needs help rather than wait for a black-box job to finish.\n\n### Work in the environment you already have\n\nA clean cloud sandbox isn't always enough. Your agent may need the database\nrunning on localhost, an uncommitted change, an internal API behind your VPN,\nor a model running on your GPU. Bivy runs the agent where those things already\nexist, subject to that machine's permissions and the runtime's protection.\n\nConnect several machines to the same account: a laptop for interactive work,\na Linux server for background jobs, or a GPU box for local inference. Choose\nthe machine for each session or pin it in an automation. Repository runs can\nuse isolated Git worktrees without rebuilding the whole development environment.\n\n**The execution machine must stay awake and online.** To close your laptop and\nleave work running, run the agent on a different, always-on machine.\n\n[Environment and multi-machine recipes →](docs/capability-recipes.md)\n\n### Use multiple agents, not multiple disconnected workflows\n\nRun Claude Code for one task, Codex for another, and Pi or OpenCode where they\nfit. Bivy supplies the shared session, remote-access, automation, and review\nsurfaces; your chosen agent still does the coding and uses your model provider.\n\n- Choose an agent and, where supported, a model for each session or run.\n- Import existing Claude Code and Codex sessions.\n- Fork or move work to another agent or machine when a different setup fits\n better. Continuation fidelity varies: some paths preserve native history,\n while others replay portable turns or seed the destination with context.\n- Use agent-native logins, Bivy-managed credentials, or local inference.\n Bivy's custom OpenAI-compatible endpoint registry currently feeds Pi;\n other agents may need their own provider configuration.\n- Register your own ACP or headless process agent with `bivy agent add`.\n\nBivy does not replace your agent, provide model inference, or make every agent's\nfeatures identical. Consult the [support matrix](docs/runtime-support-matrix.md)\nand [handoff recipes](docs/capability-recipes.md#fork-or-move-a-session).\n\n### Less signing in. Less copying secrets.\n\nBivy syncs **Bivy-managed API keys and supported OAuth credentials** across\nenrolled machines for compatible runtimes. Connect supported credentials once\nand reuse them where you run work, rather than manually distributing keys to\neach machine.\n\nFor ordinary account sync, credentials are encrypted on the node before upload.\nThe control plane stores ciphertext and wrapped-key metadata; enrolled nodes\nshare access by wrapping the vault key to one another. Bivy Cloud does not\nreceive plaintext credentials through this sync path.\n\nYou can also keep credentials local, use labeled keys and project presets, or\nreference environment variables and 1Password instead of embedding secrets in\nconfiguration:\n\n```bash\nbivy provider login\nbivy credentials add anthropic work\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\n```\n\n**Not every CLI login syncs.** Native agent logins may still be per-machine;\nGitHub App private-key sync is separately opt-in. If you lose every node and\ndevice able to unwrap a vault, you must sign in to providers again. Explicit\nhosted-provisioning custody grants are separate from ordinary encrypted sync.\n\n[Credential sync and runtime coverage →](docs/credential-sync.md) ·\n[Credentials guide →](docs/credentials-guide.md) ·\n[Key storage →](docs/key-management.md)\n\n### Let events start the work\n\nAutomations turn recurring or incoming work into sessions you can join,\nsupervise, and review. Choose the repository, machine, agent, model, approval\nmode, sandbox setting, and maximum attempts.\n\n| Trigger | Example workflow |\n|---|---|\n| **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |\n| **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |\n| **Linear** | Label an issue to start work without copying its description into an agent. |\n| **Slack** | Send a request from the conversation where the work came up. |\n| **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |\n| **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |\n\nConfigure automations in the app or version them with your repository in\n`.bivy/automations.yaml`:\n\n```bash\nbivy automation init\n# Edit the generated definition for your repository and workflow.\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture\nbivy automation apply\n```\n\nOr delegate a one-off job without creating an automation:\n\n```bash\nbivy runs start \"Review outdated dependencies and propose a small, tested update.\"\nbivy runs wait <id>\n```\n\nRuns keep routing and lifecycle evidence, check results, and output references\nin a reviewable Receipt. For unattended issue work, Bivy runs declared repository\nchecks after the agent's turn; failed required checks fail the run even if the\nagent reports success. A completed process alone is not proof that the task\nsucceeded.\n\n[Automation recipes →](docs/capability-recipes.md#let-events-start-runs) ·\n[Automations as code →](docs/automations-as-code.md) ·\n[Run outcomes and reliability limits →](docs/automation-runs.md)\n\n### Start at your desk. Continue anywhere.\n\nOpen the same session in the browser, phone PWA, or terminal. Watch work live,\nanswer questions, approve supported tool calls, or stop the agent.\n\n- Send screenshots, images, logs, and other files from your phone.\n- Download reports and artifacts the agent creates.\n- Open [session apps](docs/apps.md) from chat or the **Apps** menu: live web\n previews, desktop app views, and CLI/TUI tools. Agents can publish views with\n `bivy app publish bivy.app.json` and create share links with `bivy app share`.\n- Use voice input and read-aloud where supported; provider-backed voice may\n send audio or text to the selected provider.\n- Keep a native terminal workflow or use structured chat, depending on the agent.\n\n```bash\nbivy run claude --no-follow # start without attaching\nbivy open # open the web app\nbivy resume # return to the session in your terminal\nbivy link # pair a device directly via QR\n```\n\nNo phone app installation is required. Open [app.bivy.sh](https://app.bivy.sh)\nin your browser; adding it to your home screen is optional.\n\n[Remote access →](docs/remote-access.md) ·\n[Voice, files, and terminal recipes →](docs/capability-recipes.md)\n\n## Get started\n\n### Install\n\nBivy supports **macOS and Linux with Node.js 20+**. The installer installs the\n`@bivy/bivy` package, runs guided setup, and starts a launchd or systemd service:\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nSetup helps you choose an agent and configure remote access. Existing agents\nkeep their command, login, and configuration. The installer may use `sudo` to\ninstall a missing Node.js, but never for `npm install`. To inspect it first,\ndownload it with `curl -fsSL https://bivy.sh/install.sh -o install.sh`.\n\nAlready have Node.js and want to avoid sudo?\n\n```bash\nnpm install -g @bivy/bivy\nbivy setup\n```\n\nThen try one small task:\n\n```bash\ncd your-repo\nbivy run claude\n# Ask: \"Explain this repo and make one small, safe improvement. Run the relevant checks.\"\nbivy open\n```\n\nOpen that same session on your phone while it runs. For a web app, ask the agent\nto start its dev server, then open **Apps → Running in this workspace → Preview**.\nTry a page, point to something you'd change, and send the feedback back to the\nagent. No separate preview deployment needed.\n\nOnce that works, share a preview for review, connect another machine, or add\nyour first automation.\n\n**Local-only works too.** `bivy run`, `bivy resume`, and `bivy sessions` need no\naccount or server. Choose **local only for now** during setup; use `bivy login`\nlater. Browser and phone access need a hosted or self-hosted control plane;\nthe node itself does not serve a web UI.\n\n[Full quickstart →](docs/quickstart.md) ·\n[Installer options, service management, and uninstall →](docs/install.md)\n\n### Choose hosted or self-hosted\n\n| Option | What you get |\n|---|---|\n| **Free Cloud — $0** | Every launch feature, including automations; 10 new remote sessions per rolling seven days. No credit card required. |\n| **Cloud — $15/month** | The same features with unlimited remote sessions. |\n| **Self-hosted Core** | Operate the app, control plane, and relay yourself, with no Bivy usage limits. |\n\nManual and automated sessions share the Cloud allowance. Resuming existing\nsessions and viewing history do not consume it. Agent subscriptions and model\nprovider charges are separate. See [current pricing](https://bivy.sh#pricing).\n\n**Self-host anywhere:** deploy the public control-plane (including the web app)\nand relay images with Postgres and [a small set of environment variables](docs/deploy-images.md).\nYour server or container platform handles HTTPS. Set up owner access in the\nbrowser—no SSH, external authentication provider, or Bivy Cloud account required.\nFor a bare VPS, the [Compose installer](docs/self-host-quickstart.md) automates the\nsame stack. These onboarding features require a release containing them.\n\nStart on Cloud and self-host later if you prefer. Deploy the stack, reconnect\nmachines with `bivy relay:setup`, and pair devices to your server. This is not a\none-click migration of your Cloud account; your local repos and agent\nconfiguration stay in place.\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nSelf-hosting is community-supported: you own TLS, backups, upgrades, and\nhardening. Public multi-architecture images are available as\n`ghcr.io/bivysh/bivy-control-plane` and `ghcr.io/bivysh/bivy-relay`; pin a release\nversion or full commit SHA.\n\n[Deploy the images anywhere →](docs/deploy-images.md) ·\n[Optional VPS installer →](docs/self-host-quickstart.md) ·\n[Operations reference →](docs/self-host.md)\n\n## Architecture\n\nYour environment, with clear security boundaries:\n\n```text\nYour machine Hosted or self-hosted\n┌──────────────────────┐ ┌──────────────────────┐\n│ Node daemon │──outbound──▶│ Relay │\n│ Agents, repos, tools │ │ Encrypted frames │\n│ Local credentials │ └──────────┬───────────┘\n└──────────────────────┘ │\n Browser / phone\n + control plane\n (app, accounts, metadata)\n```\n\n- **Execution stays on your machine.** Bivy Cloud does not run your agents.\n Your model provider still sees whatever the agent sends it.\n- **Interactive session traffic is end-to-end encrypted** between the node and\n paired devices. The relay forwards opaque session frames; your node dials out,\n so no inbound public port is required.\n- **App previews have a separate security boundary.** Preview traffic uses HTTPS\n and an outbound tunnel, not session E2E encryption. The preview ingress/relay\n operator can see it. Public preview links grant anyone holding them access to\n that view and its live backend until expiry or revocation. See\n [preview security and sharing](docs/apps.md#runtime-and-security-boundaries).\n- **Ordinary credential sync uploads ciphertext, not plaintext keys.**\n Supported credentials and recovery limits are documented separately.\n- **Encryption is not universal across integrations.** Slack commands and\n generic webhook instructions reach the control plane in plaintext. Do not\n put secrets in them. Routing and bounded run metadata are also visible there.\n- **Device authorization matters.** QR pairing authorizes a device directly\n through the node. Hosted account pairing trusts the control plane to authorize\n devices and serve the web app that holds client keys.\n- **Bivy is not an OS-level sandbox.** The default approval mode is\n `autonomous`; protection depends on the runtime. Some agents enforce sandbox\n tiers, while process agents may run with your full user permissions.\n Heuristic tool checks help prevent accidents but are not isolation.\n\nReview the runtime's Protection label and configure approval/sandbox settings\nfor the task, especially before enabling unattended work.\n\n[Security model and known limitations →](docs/security-model.md) ·\n[Runtime protection matrix →](docs/runtime-support-matrix.md) ·\n[Configuration →](docs/configuration.md)\n\n## Agents and everyday commands\n\n**Claude Code, Codex, Pi, and OpenCode are release-tested.** Additional adapters\ninclude Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub Copilot,\nGrok, Amp, Auggie, Droid, Continue, Kilo Code, and Rovo Dev. Installation,\nresume, model selection, and tool protection vary—see the\n[support matrix](docs/runtime-support-matrix.md) and [agent guides](docs/agents/README.md).\n\nRun an arbitrary command with `bivy run -- ./your-agent --flags`, register a\nreusable entry with `bivy agent add`, or package a declarative integration with\nexperimental [plugins](docs/plugins.md).\n\n```bash\nbivy run claude # launch a durable session; also codex, pi, opencode\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires remote setup)\nbivy nodes # list connected account machines\nbivy runs list # inspect delegated work\nbivy automation init # scaffold repo-owned automations\nbivy provider login # connect supported model credentials\nbivy agent add # register an ACP or process agent\nbivy doctor # check installation and connectivity\nbivy logs -f # follow node logs\nbivy update # update and restart the service\n```\n\n`bivy update` uses your original installation method and waits for an active\nturn to finish before restarting. Use `--force` to skip that wait.\n\n[CLI reference →](docs/cli-reference.md) ·\n[Node and project configuration →](docs/config-as-code.md) ·\n[GitHub setup →](docs/github-setup.md) ·\n[Linear setup →](docs/linear-work-queue.md)\n\n## Development and contributions\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server\n```\n\n| Directory | Contents |\n|---|---|\n| `src/`, `bin/` | Node daemon, CLI, runtime adapters, sessions, approvals, secrets |\n| `packages/core/` | Shared protocol, pairing, and wire format |\n| `packages/web/`, `packages/ui/` | React PWA and shared design system |\n| `services/relay/` | Self-hostable encrypted relay |\n| `services/control-plane/` | Self-hostable control plane |\n| `deploy/` | Deployment examples |\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow. Releases\nare published from CI with provenance attestations; see\n[release verification](docs/releasing.md).\n\n**Found a security issue?** Use\n[GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new),\nnot a public issue. See [SECURITY.md](SECURITY.md).\n\n### In development—not available at launch\n\nAutomatically provisioned, short-lived **ephemeral machines** are in development\nfor hosted and bring-your-own-cloud deployments. Neither path is ready or\nsupported for this launch. Use an existing computer or server you operate.\nExperimental provisioning has different credential-custody and encryption\nboundaries; see the [provisioning trust model](docs/hosted-provisioning-trust-model.md).\n\n## License\n\nEverything in this repository—node, CLI, web/PWA, relay, and control plane—is\nfree and open-source **AGPL-3.0-only Core**, with no Bivy usage limits. You may\nuse, modify, and self-host it under that license. If users interact with your\nmodified version over a network, section 13 requires you to offer its\ncorresponding source. See [LICENSE](LICENSE).\n\nBivy Cloud is the hosted operation of that stack plus billing and plans, in a\nseparate private repository. Contributions use the\n[DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
69
69
  "readmeFilename": "README.md",
70
70
  "agentBridges": {
71
- "@anthropic-ai/claude-agent-sdk": "0.3.284",
72
- "@earendil-works/pi-ai": "0.87.1",
73
- "@earendil-works/pi-coding-agent": "0.87.1"
71
+ "@anthropic-ai/claude-agent-sdk": "0.3.285",
72
+ "@earendil-works/pi-ai": "0.99.1",
73
+ "@earendil-works/pi-coding-agent": "0.99.1"
74
74
  }
75
75
  }