@bivy/bivy 0.6.0-staging.82 → 0.6.0-staging.83

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.
Files changed (2) hide show
  1. package/dist/server.js +72 -21
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -535,7 +535,9 @@ if (sessionRunPolicy) {
535
535
  console.log(`[policy] in-session model reroute enabled: ${process.env.BIVY_SESSION_MODEL_FALLBACK}`);
536
536
  }
537
537
  let lastUpdateCheckAt = 0;
538
- let updateNoticeSentFor = "";
538
+ // The most recent "this node is behind" finding, so a client that connects after
539
+ // the check already ran still gets the banner (replayed on connect below).
540
+ let pendingBivyUpdate = null;
539
541
  function runtimeSummary(rt) {
540
542
  return runtimeHost.summary(rt);
541
543
  }
@@ -625,11 +627,11 @@ function readJsonFile(file) {
625
627
  return undefined;
626
628
  }
627
629
  }
628
- async function maybeNotifyBivyUpdate(record) {
629
- // The daemon creates an initial session during startup before any UI is
630
- // connected. Don't consume the once-per-version notice until someone can see it.
631
- if (clients.size === 0 && !relay)
632
- return;
630
+ // Poll npm for a newer release (throttled to every 6h). On finding one, remember
631
+ // it and push a dedicated `node.update` event so every connected app can show a
632
+ // banner with a one-tap "Update this node" button (see runBivyUpdate). Safe to
633
+ // call from anywhere never throws, never interrupts a session.
634
+ async function checkBivyUpdate() {
633
635
  const now = Date.now();
634
636
  if (now - lastUpdateCheckAt < 6 * 60 * 60 * 1000)
635
637
  return;
@@ -641,25 +643,48 @@ async function maybeNotifyBivyUpdate(record) {
641
643
  const res = await fetch(updateRegistryUrl, { signal: AbortSignal.timeout(5000) });
642
644
  if (!res.ok)
643
645
  return;
644
- const latestVersion = (await res.json()).version;
645
- if (!latestVersion || !isNewerVersion(latestVersion, current))
646
+ const latest = (await res.json()).version;
647
+ if (!latest || !isNewerVersion(latest, current))
646
648
  return;
647
- if (updateNoticeSentFor === latestVersion)
648
- return;
649
- updateNoticeSentFor = latestVersion;
650
- const label = ` ${latestVersion}`;
651
- broadcast({
652
- type: "session.notice",
653
- sessionId: record.id,
654
- level: "info",
655
- message: `A newer Bivy version${label} is available. Run \`bivy update\` in your terminal to update.`,
656
- action: "bivy update",
657
- });
649
+ pendingBivyUpdate = { current, latest };
650
+ broadcast({ type: "node.update", current, latest });
658
651
  }
659
652
  catch {
660
653
  // Best-effort update checks should never interrupt a session.
661
654
  }
662
655
  }
656
+ async function maybeNotifyBivyUpdate() {
657
+ // The daemon creates an initial session during startup before any UI is
658
+ // connected. Don't spend a check until someone can see the banner.
659
+ if (clients.size === 0 && !relay)
660
+ return;
661
+ await checkBivyUpdate();
662
+ }
663
+ // Run `bivy update` on this node, the same command a user would type. The CLI
664
+ // re-spawns itself detached, waits for any in-flight turn, updates, and restarts
665
+ // the service (logging to update.log), so we just fire-and-forget it here. The
666
+ // bin ships next to this server bundle in both the git checkout (src/server.ts)
667
+ // and the published package (dist/server.js), so repoRoot/bin/bivy.mjs resolves
668
+ // in both. Returns a friendly error instead of throwing when it can't be found
669
+ // (e.g. an unusual layout), so the banner can fall back to the manual command.
670
+ function runBivyUpdate() {
671
+ const script = path.join(repoRoot, "bin", "bivy.mjs");
672
+ if (!fs.existsSync(script)) {
673
+ return { ok: false, error: "Could not locate the bivy CLI on this node — run `bivy update` in a terminal." };
674
+ }
675
+ try {
676
+ const child = spawn(process.execPath, [script, "update"], {
677
+ detached: true,
678
+ stdio: "ignore",
679
+ env: process.env,
680
+ });
681
+ child.unref();
682
+ return { ok: true };
683
+ }
684
+ catch (error) {
685
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
686
+ }
687
+ }
663
688
  function runtimeInstallSpec(requested) {
664
689
  let id = String(requested ?? "").trim().toLowerCase();
665
690
  // Normalize a few historical aliases to their canonical runtime id.
@@ -2597,6 +2622,14 @@ const RELAY_COMMANDS = {
2597
2622
  ping(msg, ctx) {
2598
2623
  ctx.reply({ type: "pong", requestId: typeof msg.requestId === "string" ? msg.requestId : undefined });
2599
2624
  },
2625
+ // Kick off `bivy update` on this node from the app's version-mismatch banner
2626
+ // (see runBivyUpdate). The node restarts itself when the update lands, so the
2627
+ // client just sees the socket reconnect on the new build; a failure to even
2628
+ // start reports back so the banner can show the manual command.
2629
+ "node.update"(_msg, ctx) {
2630
+ const result = runBivyUpdate();
2631
+ ctx.reply({ type: "node.update.result", ok: result.ok, error: result.error });
2632
+ },
2600
2633
  // Fetch a stored attachment's bytes by content hash. The relay client (a phone
2601
2634
  // not on the LAN) can't reach the GET /api/attachment endpoint, so it fetches
2602
2635
  // over the encrypted tunnel instead; the relay framing chunks the base64 payload
@@ -7640,7 +7673,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
7640
7673
  if (makeActive)
7641
7674
  active = existing;
7642
7675
  broadcast({ type: "session.created", sessionId: existing.id, name: existing.session.getName(), workspace: existing.workspace, sessionFile: existing.sessionFile, source: existing.source, branch: existing.worktree?.branch, prUrl: existing.prUrl, runtimeId: existing.runtimeId, agentName: getRuntime(existing.runtimeId).displayName, bivySession: bivySessionEnvelope(existing), capabilities: capabilitiesWithCommands(existing.runtimeId, existing.session) });
7643
- void maybeNotifyBivyUpdate(existing);
7676
+ void maybeNotifyBivyUpdate();
7644
7677
  return existing;
7645
7678
  }
7646
7679
  // Pick the agent for this session (fixed for its life). Resuming a tagged
@@ -7829,7 +7862,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
7829
7862
  if (makeActive)
7830
7863
  active = record;
7831
7864
  broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
7832
- void maybeNotifyBivyUpdate(record);
7865
+ void maybeNotifyBivyUpdate();
7833
7866
  scheduleAdvertise();
7834
7867
  return record;
7835
7868
  }
@@ -8544,6 +8577,16 @@ app.get("/api/node/info", (_req, res) => {
8544
8577
  sandbox: sandboxInfo(),
8545
8578
  });
8546
8579
  });
8580
+ // One-tap "Update this node" from the app's version-mismatch banner, for
8581
+ // direct/LAN clients (the relay path uses the RELAY_COMMANDS "node.update"
8582
+ // handler). Both call the same runBivyUpdate.
8583
+ app.post("/api/node/update", (_req, res) => {
8584
+ const result = runBivyUpdate();
8585
+ if (result.ok)
8586
+ res.json({ ok: true });
8587
+ else
8588
+ res.status(500).json({ ok: false, error: result.error });
8589
+ });
8547
8590
  // Build collectNodeStats() options, resolving the optional session so the panel
8548
8591
  // can attribute a session-scoped tier (its live agent process + workspace size).
8549
8592
  function nodeStatsOptsFor(sessionId) {
@@ -10444,6 +10487,14 @@ wss.on("connection", (socket, req) => {
10444
10487
  // sharing a PTY size it to their min (see TerminalManager.setClientSize).
10445
10488
  const clientTerminalId = `sock-${randomUUID()}`;
10446
10489
  socket.send(JSON.stringify({ type: "hello", activeSessionId: active?.id, activeSession: active ? { id: active.id, isStreaming: sessionBusy(active), lastActivity: active.lastActivity, workingStartedAt: active.workingStartedAt } : null }));
10490
+ // Authoritative version status on every connect: `latest` set means this node
10491
+ // is behind (banner shows); absent means up to date (banner + any "Updating…"
10492
+ // state clear — this is how the banner disappears after an update lands and
10493
+ // the socket reconnects on the new build). Then (re)run the throttled check so
10494
+ // a freshly-opened app surfaces a newly-available update without waiting for a
10495
+ // session turn.
10496
+ socket.send(JSON.stringify({ type: "node.update", current: currentVersion() ?? "", latest: pendingBivyUpdate?.latest }));
10497
+ void checkBivyUpdate();
10447
10498
  socket.on("message", (raw) => {
10448
10499
  let msg;
10449
10500
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.6.0-staging.82",
3
+ "version": "0.6.0-staging.83",
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.",