skydive-cli 0.5.0-beta.30 → 0.5.0-beta.31

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/CHANGELOG.md CHANGED
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  - `/search` (alias `/find`) in the chat TUI — opens the conversation picker with its search field focused, so "find a conversation" has a discoverable command instead of being a feature you had to know the bare picker already had. Pass a query inline (`/search billing deep dive`) and the picker opens already narrowed to the matches; bare `/search` opens it ready for you to type. The search runs server-side across conversation titles, message bodies, and participant names, the same search the picker exposes.
13
13
  - First run in a workspace with no agents now sets you up automatically: instead of dropping you on an empty agent picker, `skydive` creates your first agent for you (with a suggested name and avatar, mirroring the web onboarding flow) and then offers to have it learn how you already work — reading the other coding agents set up on your machine (Claude Code, Cursor, Codex, and more), the same import that `skydive import` runs, with a plan shown first and credentials never copied. Decline and you go straight into the new agent's chat. You can replay this flow anytime with `skydive --onboarding` (or `SKYDIVE_FORCE_ONBOARDING=1`), even in a workspace that already has agents.
14
- - `/computer` (alias `/stats`) in the chat TUI — a snapshot of the agent's sandbox drawn as colored bar graphs: CPU load (scaled to core count), memory, swap (when present), and disk usage, plus uptime and process count. Bars turn amber past 75%% and red past 90%% so a glance tells you whether the box is under pressure. The card keeps itself current quietly (a slow background refresh, no flicker or "updating" chrome); only the most recent card refreshes, and it silently stops while you're scrolled up (no wasted polls on a card you're not looking at) and resumes at the bottom. Stats are read over the same relay `skydive sandbox` uses, so it's gated on EDIT access to the agent like the other sandbox commands.
14
+ - `/computer` (alias `/stats`) in the chat TUI — a snapshot of the agent's sandbox drawn as colored bar graphs: CPU, memory, and disk usage. Bars turn amber past 75%% and red past 90%% so a glance tells you whether the box is under pressure. The numbers come from the Skydive API (the same live resource sample the web computer view reads), not by connecting to the sandbox — so the card shows instantly, never waking or hanging on a box that isn't running: when the agent has no live sandbox it simply reads "no sandbox running". The card keeps itself current quietly (a slow background refresh, no flicker or "updating" chrome); only the most recent card refreshes, and it silently stops while you're scrolled up (no wasted polls on a card you're not looking at) and resumes at the bottom.
15
15
  - `skydive fs` — a namespace for working with the files on an agent's computer (its sandbox) from your own machine. Its first subcommand, `skydive fs edit <path>`, reads a remote file down over the same relay `skydive sandbox` uses, opens it in your local editor (`$VISUAL`/`$EDITOR`, override with `--editor`, defaults to `vi`), and writes your changes back atomically when you save and close — so you can edit a remote file as if it were local without opening the chat TUI. A path that doesn't exist yet is created on save; no changes means nothing is written. Access is gated on EDIT permission for the agent, exactly like `skydive sandbox`. (A first-class `fs` relay channel and `skydive fs mount` land in follow-ups.)
16
16
 
17
17
  ## [0.4.0] - 2026-08-07
package/dist/js/bin.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-BoO7ldO0.mjs";
3
- import { A as getShareMachineDefault, C as getConfigPath, D as getPromptHistoryPath, E as getPreference, F as resolveAppUrl, H as saveSession, I as resolveChatAuth, L as resolveConfig, M as getStoredApiKeyWorkspaceName, N as getUpdateCheckDisabled, R as resolveManagementAuth, S as deleteConfig, T as getLastSeenVersion, V as saveConfig, W as setLastSeenVersion, _ as API_KEY_FAMILY_PREFIX, g as API_KEYS_URL, i as resolveAgent$1, j as getStoredApiKeyId, v as API_KEY_PREFIX, w as getDefaultAgent, x as PREFERENCES, y as DEFAULT_API_URL, z as resolveSession } from "./print-DYiQC5IW.mjs";
2
+ import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-BVbYs_Yg.mjs";
3
+ import { A as getShareMachineDefault, C as getConfigPath, D as getPromptHistoryPath, E as getPreference, F as resolveAppUrl, H as saveSession, I as resolveChatAuth, L as resolveConfig, M as getStoredApiKeyWorkspaceName, N as getUpdateCheckDisabled, R as resolveManagementAuth, S as deleteConfig, T as getLastSeenVersion, V as saveConfig, W as setLastSeenVersion, _ as API_KEY_FAMILY_PREFIX, g as API_KEYS_URL, i as resolveAgent$1, j as getStoredApiKeyId, v as API_KEY_PREFIX, w as getDefaultAgent, x as PREFERENCES, y as DEFAULT_API_URL, z as resolveSession } from "./print-C_TnQSPg.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
6
- import { t as createRestClient } from "./rest-xXfqT4SX.mjs";
6
+ import { t as createRestClient } from "./rest-CgfbKXst.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
8
  import { r as resolveMachineIdentity } from "./client-DbqRBquD.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-DuxaUuek.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-BTyg329O.mjs";
10
10
  import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
11
11
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
12
12
  import { hideBin } from "yargs/helpers";
@@ -1584,7 +1584,7 @@ const importCommand = {
1584
1584
  process.exit(1);
1585
1585
  }
1586
1586
  }
1587
- const { runChat } = await import("./boot-0-dpPM1_.mjs");
1587
+ const { runChat } = await import("./boot-iaVSnd7h.mjs");
1588
1588
  await runChat({
1589
1589
  appUrl,
1590
1590
  sessionToken: session.value.sessionToken,
@@ -1622,7 +1622,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1622
1622
  });
1623
1623
  const extra = (argv.print ?? "").trim();
1624
1624
  const prompt = buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform)) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1625
- const { runPrint } = await import("./print-CQkXZ02a.mjs");
1625
+ const { runPrint } = await import("./print-DACzRUwQ.mjs");
1626
1626
  try {
1627
1627
  const result = await runPrint({
1628
1628
  appUrl,
@@ -2029,7 +2029,7 @@ const chatCommand = {
2029
2029
  sessionToken: auth.value.token,
2030
2030
  agentSelector: argv.agent ?? null
2031
2031
  });
2032
- const { runChat } = await import("./boot-0-dpPM1_.mjs");
2032
+ const { runChat } = await import("./boot-iaVSnd7h.mjs");
2033
2033
  await runChat({
2034
2034
  appUrl: auth.value.appUrl,
2035
2035
  sessionToken: auth.value.token,
@@ -2113,7 +2113,7 @@ async function runPrintMode({ argv, appUrl }) {
2113
2113
  printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2114
2114
  process.exit(1);
2115
2115
  }
2116
- const { runPrint, readStdin } = await import("./print-CQkXZ02a.mjs");
2116
+ const { runPrint, readStdin } = await import("./print-DACzRUwQ.mjs");
2117
2117
  const workspaceId = await resolveWorkspaceFlag({
2118
2118
  appUrl,
2119
2119
  sessionToken: auth.value.token,
@@ -2208,7 +2208,7 @@ const getCommand$1 = {
2208
2208
  printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2209
2209
  process.exit(1);
2210
2210
  }
2211
- const { messageGet } = await import("./print-CQkXZ02a.mjs");
2211
+ const { messageGet } = await import("./print-DACzRUwQ.mjs");
2212
2212
  try {
2213
2213
  const result = await messageGet({
2214
2214
  appUrl: auth.value.appUrl,
@@ -2411,7 +2411,7 @@ const switchCommand = {
2411
2411
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
2412
2412
  process.exit(1);
2413
2413
  }
2414
- const { runWorkspacePicker } = await import("./boot-0-dpPM1_.mjs");
2414
+ const { runWorkspacePicker } = await import("./boot-iaVSnd7h.mjs");
2415
2415
  await runWorkspacePicker(session);
2416
2416
  return;
2417
2417
  }
@@ -2490,7 +2490,7 @@ const openCommand = {
2490
2490
  const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
2491
2491
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2492
2492
  const { machineName } = await resolveMachineIdentity(null);
2493
- const { PortalDaemonClient } = await import("./daemon-client-LJdnM_nE.mjs");
2493
+ const { PortalDaemonClient } = await import("./daemon-client-C7emKKlj.mjs");
2494
2494
  let lastLine = "";
2495
2495
  let signalConnected;
2496
2496
  const connected = new Promise((resolve) => {
@@ -2800,8 +2800,8 @@ const sandboxCommand = {
2800
2800
  }).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
2801
2801
  handler: async (argv) => {
2802
2802
  const session = requireSession(argv);
2803
- const { createRestClient } = await import("./rest-DSpGS5XU.mjs");
2804
- const { resolveAgent } = await import("./print-CQkXZ02a.mjs");
2803
+ const { createRestClient } = await import("./rest-VV5nc-Mn.mjs");
2804
+ const { resolveAgent } = await import("./print-DACzRUwQ.mjs");
2805
2805
  const client = createRestClient({
2806
2806
  appUrl: session.appUrl,
2807
2807
  sessionToken: session.sessionToken
@@ -2958,8 +2958,8 @@ const fsEditCommand = {
2958
2958
  handler: async (argv) => {
2959
2959
  const session = requireSession(argv);
2960
2960
  const remotePath = argv.path;
2961
- const { createRestClient } = await import("./rest-DSpGS5XU.mjs");
2962
- const { resolveAgent } = await import("./print-CQkXZ02a.mjs");
2961
+ const { createRestClient } = await import("./rest-VV5nc-Mn.mjs");
2962
+ const { resolveAgent } = await import("./print-DACzRUwQ.mjs");
2963
2963
  const client = createRestClient({
2964
2964
  appUrl: session.appUrl,
2965
2965
  sessionToken: session.sessionToken
@@ -4193,7 +4193,7 @@ function setupUpdateCheck(argv) {
4193
4193
 
4194
4194
  //#endregion
4195
4195
  //#region src/changelog.generated.ts
4196
- const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n\n- `/search` (alias `/find`) in the chat TUI — opens the conversation picker with its search field focused, so \"find a conversation\" has a discoverable command instead of being a feature you had to know the bare picker already had. Pass a query inline (`/search billing deep dive`) and the picker opens already narrowed to the matches; bare `/search` opens it ready for you to type. The search runs server-side across conversation titles, message bodies, and participant names, the same search the picker exposes.\n- First run in a workspace with no agents now sets you up automatically: instead of dropping you on an empty agent picker, `skydive` creates your first agent for you (with a suggested name and avatar, mirroring the web onboarding flow) and then offers to have it learn how you already work — reading the other coding agents set up on your machine (Claude Code, Cursor, Codex, and more), the same import that `skydive import` runs, with a plan shown first and credentials never copied. Decline and you go straight into the new agent's chat. You can replay this flow anytime with `skydive --onboarding` (or `SKYDIVE_FORCE_ONBOARDING=1`), even in a workspace that already has agents.\n- `/computer` (alias `/stats`) in the chat TUI — a snapshot of the agent's sandbox drawn as colored bar graphs: CPU load (scaled to core count), memory, swap (when present), and disk usage, plus uptime and process count. Bars turn amber past 75%% and red past 90%% so a glance tells you whether the box is under pressure. The card keeps itself current quietly (a slow background refresh, no flicker or \"updating\" chrome); only the most recent card refreshes, and it silently stops while you're scrolled up (no wasted polls on a card you're not looking at) and resumes at the bottom. Stats are read over the same relay `skydive sandbox` uses, so it's gated on EDIT access to the agent like the other sandbox commands.\n- `skydive fs` — a namespace for working with the files on an agent's computer (its sandbox) from your own machine. Its first subcommand, `skydive fs edit <path>`, reads a remote file down over the same relay `skydive sandbox` uses, opens it in your local editor (`$VISUAL`/`$EDITOR`, override with `--editor`, defaults to `vi`), and writes your changes back atomically when you save and close — so you can edit a remote file as if it were local without opening the chat TUI. A path that doesn't exist yet is created on save; no changes means nothing is written. Access is gated on EDIT permission for the agent, exactly like `skydive sandbox`. (A first-class `fs` relay channel and `skydive fs mount` land in follow-ups.)\n\n## [0.4.0] - 2026-08-07\n\n### Added\n\n- `skydive chat` works with API key auth: an API key alone now opens the chat TUI (and headless `chat -p`) without a browser login, using the key's pinned workspace.\n- `skydive config` command to view and set the safe, hand-editable CLI preferences (`shareMachineDefault`, `updateCheck`) without opening `config.json`. `config`/`config list` shows each key, its effective value, and whether it comes from the file or the built-in default; `config get`/`set`/`unset` read and change one key; `config path` prints the config file location. It only ever touches those preference keys, never credentials or flow-managed state.\n- Billing-blocked sends and runs now surface the server-authored recovery\n guidance from the typed billing outcome. The chat TUI renders it as a\n warning-toned system-notice row — live when a send is refused or a run is\n stopped at the billing boundary, and again when reopening a conversation\n that holds a persisted billing notice — instead of a generic error.\n Headless `chat -p` and `messages get` print the guidance (state, recovery\n step, and manage-billing URL) on stderr, keep any partial output already\n streamed on stdout, and exit with code `5` (distinct from the generic `1`)\n so scripted callers can tell a billing pause from a failure.\n- Syntax highlighting for 14 more languages in the chat transcript and file/review panes (python, bash, go, rust, c, cpp, java, ruby, css, html, json, yaml, and more) — previously only JavaScript/TypeScript rendered colored.\n\n### Changed\n\n- `skydive agent list` lists the full agent roster by default instead of a truncated page.\n\n### Fixed\n\n- Long multi-line bash tool-call commands (e.g. heredocs) are clamped with a `+N more · click to expand` tail instead of dumping their whole body into the scrollback.\n- Tool error output is clipped the same way as ok output, so a failing command no longer floods the transcript.\n- The TUI toast overlay has an opaque background instead of letting the transcript bleed through.\n\n## [0.3.0] - 2026-08-04\n\n### Added\n\n**Chat TUI**\n\n- `/fork` slash command to fork a conversation at any point and continue in a new thread.\n- `/conversation` switches threads from inside a live session.\n- `/compact` triggers manual conversation compaction.\n- `/portal` and `/copy` slash commands.\n- Archive the current conversation from the chat TUI.\n- `--new` flag on `skydive chat` to start a fresh conversation.\n- Debounced server-side conversation search in the picker, with title matches ranked first.\n- File review pane orientation toggle, and review comments in the file pane.\n- Pinned plan card controls and a jump-to-latest hint.\n- Terminal title (OSC) follows the live conversation title on every screen; inside cmux the workspace is renamed too, and linked PRs are pushed to the cmux sidebar.\n- Every participating agent shows in the conversation list; connect cards each have their own identity, and ctrl+r asks which connect card to act on.\n- Colored Skydive splash art in `skydive --help` and the TUI pickers.\n- New-agent name prefilled from the server's suggestion.\n\n**CLI & platform**\n\n- `skydive update` self-updater, with an update notice when a new version is available.\n- Shell completions for bash, zsh, and fish via `completion install`.\n- Portal daemon: one connection per host with per-conversation working directories.\n- Agent-led coding-agent import (`skydive import`).\n- Per-workspace scoping on the REST client, and cross-org `--agent <uuid>` auto-switches workspace.\n- `skydive-beta` wrapper for the canary channel.\n\n### Fixed\n\n- `skydive update` no longer claims an update is available on every npm install.\n- Chat memory is bounded (payload clamps + windowed rows) with a memory watchdog and OOM diagnostic report; transcript rows are memoized so streaming stops re-rendering the whole chat.\n- Chat-send failures map to real error messages.\n- Notification and sidebar previews use the last text block of the reply and strip inline markdown.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n- Sending to non-web channel conversations is blocked in the TUI.\n- GitHub bot connect opens on the web front door, not api.skydive.com.\n- TUI activity counter times from the run, not the screen.\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
4196
+ const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n\n- `/search` (alias `/find`) in the chat TUI — opens the conversation picker with its search field focused, so \"find a conversation\" has a discoverable command instead of being a feature you had to know the bare picker already had. Pass a query inline (`/search billing deep dive`) and the picker opens already narrowed to the matches; bare `/search` opens it ready for you to type. The search runs server-side across conversation titles, message bodies, and participant names, the same search the picker exposes.\n- First run in a workspace with no agents now sets you up automatically: instead of dropping you on an empty agent picker, `skydive` creates your first agent for you (with a suggested name and avatar, mirroring the web onboarding flow) and then offers to have it learn how you already work — reading the other coding agents set up on your machine (Claude Code, Cursor, Codex, and more), the same import that `skydive import` runs, with a plan shown first and credentials never copied. Decline and you go straight into the new agent's chat. You can replay this flow anytime with `skydive --onboarding` (or `SKYDIVE_FORCE_ONBOARDING=1`), even in a workspace that already has agents.\n- `/computer` (alias `/stats`) in the chat TUI — a snapshot of the agent's sandbox drawn as colored bar graphs: CPU, memory, and disk usage. Bars turn amber past 75%% and red past 90%% so a glance tells you whether the box is under pressure. The numbers come from the Skydive API (the same live resource sample the web computer view reads), not by connecting to the sandbox — so the card shows instantly, never waking or hanging on a box that isn't running: when the agent has no live sandbox it simply reads \"no sandbox running\". The card keeps itself current quietly (a slow background refresh, no flicker or \"updating\" chrome); only the most recent card refreshes, and it silently stops while you're scrolled up (no wasted polls on a card you're not looking at) and resumes at the bottom.\n- `skydive fs` — a namespace for working with the files on an agent's computer (its sandbox) from your own machine. Its first subcommand, `skydive fs edit <path>`, reads a remote file down over the same relay `skydive sandbox` uses, opens it in your local editor (`$VISUAL`/`$EDITOR`, override with `--editor`, defaults to `vi`), and writes your changes back atomically when you save and close — so you can edit a remote file as if it were local without opening the chat TUI. A path that doesn't exist yet is created on save; no changes means nothing is written. Access is gated on EDIT permission for the agent, exactly like `skydive sandbox`. (A first-class `fs` relay channel and `skydive fs mount` land in follow-ups.)\n\n## [0.4.0] - 2026-08-07\n\n### Added\n\n- `skydive chat` works with API key auth: an API key alone now opens the chat TUI (and headless `chat -p`) without a browser login, using the key's pinned workspace.\n- `skydive config` command to view and set the safe, hand-editable CLI preferences (`shareMachineDefault`, `updateCheck`) without opening `config.json`. `config`/`config list` shows each key, its effective value, and whether it comes from the file or the built-in default; `config get`/`set`/`unset` read and change one key; `config path` prints the config file location. It only ever touches those preference keys, never credentials or flow-managed state.\n- Billing-blocked sends and runs now surface the server-authored recovery\n guidance from the typed billing outcome. The chat TUI renders it as a\n warning-toned system-notice row — live when a send is refused or a run is\n stopped at the billing boundary, and again when reopening a conversation\n that holds a persisted billing notice — instead of a generic error.\n Headless `chat -p` and `messages get` print the guidance (state, recovery\n step, and manage-billing URL) on stderr, keep any partial output already\n streamed on stdout, and exit with code `5` (distinct from the generic `1`)\n so scripted callers can tell a billing pause from a failure.\n- Syntax highlighting for 14 more languages in the chat transcript and file/review panes (python, bash, go, rust, c, cpp, java, ruby, css, html, json, yaml, and more) — previously only JavaScript/TypeScript rendered colored.\n\n### Changed\n\n- `skydive agent list` lists the full agent roster by default instead of a truncated page.\n\n### Fixed\n\n- Long multi-line bash tool-call commands (e.g. heredocs) are clamped with a `+N more · click to expand` tail instead of dumping their whole body into the scrollback.\n- Tool error output is clipped the same way as ok output, so a failing command no longer floods the transcript.\n- The TUI toast overlay has an opaque background instead of letting the transcript bleed through.\n\n## [0.3.0] - 2026-08-04\n\n### Added\n\n**Chat TUI**\n\n- `/fork` slash command to fork a conversation at any point and continue in a new thread.\n- `/conversation` switches threads from inside a live session.\n- `/compact` triggers manual conversation compaction.\n- `/portal` and `/copy` slash commands.\n- Archive the current conversation from the chat TUI.\n- `--new` flag on `skydive chat` to start a fresh conversation.\n- Debounced server-side conversation search in the picker, with title matches ranked first.\n- File review pane orientation toggle, and review comments in the file pane.\n- Pinned plan card controls and a jump-to-latest hint.\n- Terminal title (OSC) follows the live conversation title on every screen; inside cmux the workspace is renamed too, and linked PRs are pushed to the cmux sidebar.\n- Every participating agent shows in the conversation list; connect cards each have their own identity, and ctrl+r asks which connect card to act on.\n- Colored Skydive splash art in `skydive --help` and the TUI pickers.\n- New-agent name prefilled from the server's suggestion.\n\n**CLI & platform**\n\n- `skydive update` self-updater, with an update notice when a new version is available.\n- Shell completions for bash, zsh, and fish via `completion install`.\n- Portal daemon: one connection per host with per-conversation working directories.\n- Agent-led coding-agent import (`skydive import`).\n- Per-workspace scoping on the REST client, and cross-org `--agent <uuid>` auto-switches workspace.\n- `skydive-beta` wrapper for the canary channel.\n\n### Fixed\n\n- `skydive update` no longer claims an update is available on every npm install.\n- Chat memory is bounded (payload clamps + windowed rows) with a memory watchdog and OOM diagnostic report; transcript rows are memoized so streaming stops re-rendering the whole chat.\n- Chat-send failures map to real error messages.\n- Notification and sidebar previews use the last text block of the reply and strip inline markdown.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n- Sending to non-web channel conversations is blocked in the TUI.\n- GitHub bot connect opens on the web front door, not api.skydive.com.\n- TUI activity counter times from the run, not the screen.\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
4197
4197
 
4198
4198
  //#endregion
4199
4199
  //#region src/whats-new.ts
@@ -4336,7 +4336,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4336
4336
  process.exit(0);
4337
4337
  }
4338
4338
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4339
- const { runPortalDaemon } = await import("./daemon-Ce8WLdGI.mjs");
4339
+ const { runPortalDaemon } = await import("./daemon-SPQgtsbW.mjs");
4340
4340
  runPortalDaemon(process.argv);
4341
4341
  } else runCli();
4342
4342
  function runCli() {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-BoO7ldO0.mjs";
3
- import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-DYiQC5IW.mjs";
2
+ import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-BVbYs_Yg.mjs";
3
+ import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-C_TnQSPg.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
- import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-xXfqT4SX.mjs";
5
+ import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-CgfbKXst.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
7
  import { t as PortalClient } from "./client-DbqRBquD.mjs";
8
- import "./daemon-DuxaUuek.mjs";
8
+ import "./daemon-BTyg329O.mjs";
9
9
  import "./api-DG5W6iwx.mjs";
10
10
  import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
- import { t as PortalDaemonClient } from "./daemon-client-C4heMWTq.mjs";
11
+ import { t as PortalDaemonClient } from "./daemon-client--A_yMKq6.mjs";
12
12
  import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
13
13
  import * as os$1 from "node:os";
14
14
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -4805,28 +4805,38 @@ function useConversationContext() {
4805
4805
  /**
4806
4806
  * `/computer`: a live snapshot of the agent's sandbox as a set of bar graphs.
4807
4807
  *
4808
- * The CLI runs on the USER's machine, so it can't read the sandbox's `/proc`
4809
- * directly it runs one small, portable shell command over the sandbox-exec
4810
- * stream (`SandboxStream`, mode `exec`) and parses the output here. Everything
4811
- * in this module is pure so the parse + layout are unit-testable without a
4812
- * sandbox or a renderer.
4808
+ * The numbers come from the api, NOT from the CLI reaching into the sandbox.
4809
+ * The worker's fleet metrics sweep already samples every live box's CPU /
4810
+ * memory / disk from the provider and stores the latest per-sandbox snapshot;
4811
+ * the CLI reads that back over one plain REST call (`collectSandboxStats`). So
4812
+ * there is no exec into the box and no cold-start wake — when the agent has no
4813
+ * running sandbox the api answers "no snapshot" immediately instead of the card
4814
+ * hanging on a stream that never replies.
4813
4815
  *
4814
- * The command reads `/proc` and `df` only (no tools that might be missing from
4815
- * a minimal image), emitting a tagged, tab-separated block. The leading
4816
- * `SKYDIVE_STATS_V1` sentinel lets the parser find the block even when a shell
4817
- * rc file prints a banner first, and versions the wire shape.
4816
+ * Everything in this module is pure so the layout math is unit-testable without
4817
+ * an api or a renderer.
4818
4818
  */
4819
- const SANDBOX_STATS_SENTINEL = "SKYDIVE_STATS_V1";
4820
4819
  /**
4821
4820
  * How often the live `/computer` card refreshes its snapshot, in ms.
4822
4821
  * Deliberately slow: the card is a background health readout, not a `top`, so
4823
- * it refreshes quietly every 30s rather than flickering every second.
4822
+ * it refreshes quietly every 30s rather than flickering every second. Also
4823
+ * comfortably slower than the worker's ~60s sweep cadence, so we never poll for
4824
+ * a sample that can't have changed.
4824
4825
  */
4825
4826
  const COMPUTER_POLL_INTERVAL_MS = 3e4;
4826
4827
  /**
4828
+ * A snapshot older than this is stale enough that we no longer present it as
4829
+ * "current". The api sample carries the provider's measurement time; the sweep
4830
+ * that writes it runs ~every 60s and the Redis key TTLs at 5 min, so a sample
4831
+ * this old means the box likely stopped reporting (paused / gone). The card
4832
+ * treats it as "no current snapshot" rather than drawing minutes-old bars as if
4833
+ * they were live.
4834
+ */
4835
+ const COMPUTER_STATS_STALE_MS = 5 * 6e4;
4836
+ /**
4827
4837
  * Whether a `/computer` card should refresh right now. Pure so the gating is
4828
4838
  * unit-testable. A card refreshes only when it is the most recent `/computer`
4829
- * in the transcript (`isLatest`), the session can reach the sandbox
4839
+ * in the transcript (`isLatest`), the session can reach the api
4830
4840
  * (`canCollect`), and the transcript is at the bottom (`!scrolledUp`) — while
4831
4841
  * you're scrolled up reading something else it quietly stops (no wasted polls),
4832
4842
  * and resumes when you scroll back down. Older cards freeze regardless, so a
@@ -4836,180 +4846,6 @@ const COMPUTER_POLL_INTERVAL_MS = 3e4;
4836
4846
  function shouldPollComputer(opts) {
4837
4847
  return opts.isLatest && opts.canCollect && !opts.scrolledUp;
4838
4848
  }
4839
- /**
4840
- * The command run in the sandbox to collect the snapshot. `sh`-portable, reads
4841
- * only `/proc` and `df`. Each metric is one `key<TAB>value` line so the parser
4842
- * never has to positionally align anything.
4843
- *
4844
- * The `port` lines enumerate LISTEN sockets the sandbox user's own processes
4845
- * hold: one awk pass maps socket inodes from /proc/net/tcp{,6} to pids via
4846
- * /proc/<pid>/fd, then a shell loop attaches each pid's cmdline. Platform
4847
- * daemons run as root, so their /proc/<pid>/fd is unreadable here and they
4848
- * never emit a line — the "user's servers only" filter falls out of
4849
- * permissions rather than a curated list. Hex address:port is left for the
4850
- * parser to decode (strtonum is gawk-only).
4851
- */
4852
- const SANDBOX_STATS_COMMAND = [
4853
- `printf '${SANDBOX_STATS_SENTINEL}\\n'`,
4854
- `printf 'nproc\\t%s\\n' "$(nproc 2>/dev/null || echo 1)"`,
4855
- `printf 'loadavg\\t%s\\n' "$(cut -d' ' -f1-3 /proc/loadavg)"`,
4856
- `printf 'uptime\\t%s\\n' "$(cut -d' ' -f1 /proc/uptime)"`,
4857
- `awk '/^MemTotal:/{t=$2}/^MemAvailable:/{a=$2}END{printf "mem_total_kb\\t%d\\nmem_avail_kb\\t%d\\n",t,a}' /proc/meminfo`,
4858
- `awk '/^SwapTotal:/{t=$2}/^SwapFree:/{f=$2}END{printf "swap_total_kb\\t%d\\nswap_free_kb\\t%d\\n",t,f}' /proc/meminfo`,
4859
- `df -Pk / | awk 'NR==2{printf "disk_total_kb\\t%d\\ndisk_used_kb\\t%d\\n",$2,$3}'`,
4860
- `printf 'procs\\t%s\\n' "$(ls -1 /proc 2>/dev/null | grep -c '^[0-9]\\+$')"`,
4861
- `ls -l /proc/[0-9]*/fd 2>/dev/null | awk 'BEGIN { while ((getline l < "/proc/net/tcp") > 0) { split(l,f," "); if (f[4]=="0A") want[f[10]]=f[2] } close("/proc/net/tcp"); while ((getline l < "/proc/net/tcp6") > 0) { split(l,f," "); if (f[4]=="0A") want[f[10]]=f[2] } close("/proc/net/tcp6") } $0 ~ "^/proc/[0-9]+/fd:$" { pid=substr($0,7,length($0)-10) } match($0, /socket:\\[[0-9]+\\]/) { ino=substr($0,RSTART+8,RLENGTH-9); if (ino in want && !(ino in done)) { done[ino]=1; print want[ino] "\\t" pid } }' | while read addr pid; do cl=$(head -c 200 "/proc/$pid/cmdline" 2>/dev/null | tr "\\0" " "); printf 'port\\t%s\\t%s\\t%s\\n' "$addr" "$pid" "$cl"; done`
4862
- ].join(" ; ");
4863
- const KB = 1024;
4864
- const HIDDEN_PORTS = new Set([
4865
- 38994,
4866
- 49983,
4867
- 38996
4868
- ]);
4869
- const V6_LOOPBACK_HEX = "00000000000000000000000001000000";
4870
- /**
4871
- * Whether a /proc/net/tcp{,6} local-address hex field is a loopback bind.
4872
- * v4 words are little-endian, so 127.0.0.1 is `0100007F` — the first octet
4873
- * (127) is the LAST byte pair. Covers v4-mapped v6 (`::ffff:127.x.y.z`) via
4874
- * the same tail check.
4875
- */
4876
- function isLoopbackHex(addrHex) {
4877
- const hex = addrHex.toUpperCase();
4878
- if (hex.length === 8) return hex.slice(6) === "7F";
4879
- if (hex.length === 32) {
4880
- if (hex === V6_LOOPBACK_HEX) return true;
4881
- return hex.startsWith("0000000000000000FFFF0000") && hex.slice(30) === "7F";
4882
- }
4883
- return false;
4884
- }
4885
- const RUNTIME_NAMES = new Set([
4886
- "node",
4887
- "bun",
4888
- "deno",
4889
- "python",
4890
- "python3",
4891
- "sh",
4892
- "bash"
4893
- ]);
4894
- const FLAGS_WITH_VALUE = new Set([
4895
- "--require",
4896
- "-r",
4897
- "--import",
4898
- "--loader",
4899
- "--experimental-loader"
4900
- ]);
4901
- /**
4902
- * A short display name from a raw cmdline: basename of argv[0], plus — when
4903
- * argv[0] is a bare runtime — the basename of the first non-flag argument with
4904
- * its extension dropped (`node …/vite/bin/vite.js` → `node vite`).
4905
- */
4906
- function processDisplayName(cmdline) {
4907
- const argv = cmdline.trim().split(/\s+/).filter(Boolean);
4908
- const first = argv[0];
4909
- if (first === void 0) return null;
4910
- const base = (p) => p.split("/").pop() ?? p;
4911
- const name = base(first);
4912
- if (!RUNTIME_NAMES.has(name)) return name;
4913
- let script;
4914
- for (let i = 1; i < argv.length; i += 1) {
4915
- const arg = argv[i];
4916
- if (arg === void 0) break;
4917
- if (FLAGS_WITH_VALUE.has(arg)) {
4918
- i += 1;
4919
- continue;
4920
- }
4921
- if (arg.startsWith("-")) continue;
4922
- script = arg;
4923
- break;
4924
- }
4925
- if (!script) return name;
4926
- return `${name} ${base(script).replace(/\.(?:mjs|cjs|js|ts|tsx|py)$/, "")}`;
4927
- }
4928
- /**
4929
- * Fold the collector's `port` lines (one per listen socket) into one entry
4930
- * per port: v4/v6 binds of the same server dedupe, `loopbackOnly` only when
4931
- * every bind is loopback, and the hidden platform ports drop out.
4932
- */
4933
- function parsePortLines(lines) {
4934
- const byPort = /* @__PURE__ */ new Map();
4935
- for (const line of lines) {
4936
- const port = Number.parseInt(line.portHex, 16);
4937
- if (!Number.isFinite(port) || port <= 0 || HIDDEN_PORTS.has(port)) continue;
4938
- const entry = byPort.get(port);
4939
- if (entry) entry.loopback.push(isLoopbackHex(line.addrHex));
4940
- else byPort.set(port, {
4941
- loopback: [isLoopbackHex(line.addrHex)],
4942
- cmdline: line.cmdline
4943
- });
4944
- }
4945
- return [...byPort.entries()].map(([port, entry]) => ({
4946
- port,
4947
- loopbackOnly: entry.loopback.every(Boolean),
4948
- process: entry.cmdline ? processDisplayName(entry.cmdline) : null
4949
- })).sort((a, b) => a.port - b.port);
4950
- }
4951
- /**
4952
- * Parse the collector's output into `SandboxStats`, or return null when the
4953
- * block is absent or malformed (so the caller can show the raw output as an
4954
- * error instead of a broken graph). Tolerant of extra lines before the
4955
- * sentinel (shell banners) and of a missing swap section (swap totals default
4956
- * to 0, which the renderer treats as "no swap").
4957
- */
4958
- function parseSandboxStats(raw) {
4959
- const start = raw.indexOf(SANDBOX_STATS_SENTINEL);
4960
- if (start === -1) return null;
4961
- const fields = /* @__PURE__ */ new Map();
4962
- const portLines = [];
4963
- for (const line of raw.slice(start).split("\n")) {
4964
- const tab = line.indexOf(" ");
4965
- if (tab === -1) continue;
4966
- const key = line.slice(0, tab).trim();
4967
- if (key === "port") {
4968
- const parts = line.split(" ");
4969
- const [addrHex = "", portHex = ""] = (parts[1] ?? "").split(":");
4970
- if (addrHex && portHex) portLines.push({
4971
- addrHex,
4972
- portHex,
4973
- cmdline: parts.slice(3).join(" ").trim()
4974
- });
4975
- continue;
4976
- }
4977
- fields.set(key, line.slice(tab + 1).trim());
4978
- }
4979
- const num = (key) => {
4980
- const v = fields.get(key);
4981
- if (v === void 0) return null;
4982
- const n = Number(v);
4983
- return Number.isFinite(n) ? n : null;
4984
- };
4985
- const load = (fields.get("loadavg") ?? "").split(/\s+/).map(Number).filter((n) => Number.isFinite(n));
4986
- const nproc = num("nproc");
4987
- const uptime = num("uptime");
4988
- const memTotal = num("mem_total_kb");
4989
- const memAvail = num("mem_avail_kb");
4990
- const diskTotal = num("disk_total_kb");
4991
- const diskUsed = num("disk_used_kb");
4992
- const procs = num("procs");
4993
- const [load1, load5, load15] = load;
4994
- if (nproc === null || uptime === null || memTotal === null || memAvail === null || diskTotal === null || diskUsed === null || procs === null || load1 === void 0 || load5 === void 0 || load15 === void 0) return null;
4995
- return {
4996
- nproc: Math.max(1, Math.round(nproc)),
4997
- loadavg: [
4998
- load1,
4999
- load5,
5000
- load15
5001
- ],
5002
- uptimeSeconds: uptime,
5003
- memTotalBytes: memTotal * KB,
5004
- memAvailBytes: memAvail * KB,
5005
- swapTotalBytes: (num("swap_total_kb") ?? 0) * KB,
5006
- swapFreeBytes: (num("swap_free_kb") ?? 0) * KB,
5007
- diskTotalBytes: diskTotal * KB,
5008
- diskUsedBytes: diskUsed * KB,
5009
- procs: Math.round(procs),
5010
- ports: parsePortLines(portLines)
5011
- };
5012
- }
5013
4849
  function severityFor(fraction) {
5014
4850
  if (fraction >= .9) return "crit";
5015
4851
  if (fraction >= .75) return "warn";
@@ -5019,52 +4855,29 @@ const GiB = 1024 * 1024 * 1024;
5019
4855
  function gb(bytes) {
5020
4856
  return (bytes / GiB).toFixed(1);
5021
4857
  }
5022
- /** `120050` seconds -> `1d 9h 20m`. Always at least `0m`. */
5023
- function formatUptime(seconds) {
5024
- const total = Math.max(0, Math.floor(seconds));
5025
- const d = Math.floor(total / 86400);
5026
- const h = Math.floor(total % 86400 / 3600);
5027
- const m = Math.floor(total % 3600 / 60);
5028
- const parts = [];
5029
- if (d > 0) parts.push(`${d}d`);
5030
- if (h > 0 || d > 0) parts.push(`${h}h`);
5031
- parts.push(`${m}m`);
5032
- return parts.join(" ");
5033
- }
5034
4858
  /**
5035
4859
  * Reduce a snapshot to the ordered list of gauges the `/computer` card draws:
5036
- * CPU load (normalized to core count), memory, swap (only when the box has
5037
- * any), and disk. Load can exceed 1.0 (oversubscribed CPU) — the bar clamps
5038
- * but the caption shows the true number so that's still legible.
4860
+ * CPU utilization, memory, and disk the three signals the api's live-stats
4861
+ * read carries. CPU is a provider-measured utilization percent (already
4862
+ * normalized across cores), so it maps straight to a 0..1 bar; the caption
4863
+ * shows the vCPU count for context.
5039
4864
  */
5040
4865
  function toGauges(stats) {
5041
4866
  const gauges = [];
5042
- const load1 = stats.loadavg[0];
5043
- const loadFraction = load1 / stats.nproc;
4867
+ const cpuFraction = stats.cpuUsedPct / 100;
5044
4868
  gauges.push({
5045
- label: "cpu load",
5046
- fraction: loadFraction,
5047
- detail: `${load1.toFixed(2)} / ${stats.nproc} cores`,
5048
- severity: severityFor(loadFraction)
4869
+ label: "cpu",
4870
+ fraction: cpuFraction,
4871
+ detail: `${stats.cpuUsedPct.toFixed(0)}% · ${stats.cpuCount} vCPU`,
4872
+ severity: severityFor(cpuFraction)
5049
4873
  });
5050
- const memUsed = Math.max(0, stats.memTotalBytes - stats.memAvailBytes);
5051
- const memFraction = stats.memTotalBytes > 0 ? memUsed / stats.memTotalBytes : 0;
4874
+ const memFraction = stats.memTotalBytes > 0 ? stats.memUsedBytes / stats.memTotalBytes : 0;
5052
4875
  gauges.push({
5053
4876
  label: "memory",
5054
4877
  fraction: memFraction,
5055
- detail: `${gb(memUsed)} / ${gb(stats.memTotalBytes)} GB`,
4878
+ detail: `${gb(stats.memUsedBytes)} / ${gb(stats.memTotalBytes)} GB`,
5056
4879
  severity: severityFor(memFraction)
5057
4880
  });
5058
- if (stats.swapTotalBytes > 0) {
5059
- const swapUsed = Math.max(0, stats.swapTotalBytes - stats.swapFreeBytes);
5060
- const swapFraction = swapUsed / stats.swapTotalBytes;
5061
- gauges.push({
5062
- label: "swap",
5063
- fraction: swapFraction,
5064
- detail: `${gb(swapUsed)} / ${gb(stats.swapTotalBytes)} GB`,
5065
- severity: severityFor(swapFraction)
5066
- });
5067
- }
5068
4881
  const diskFraction = stats.diskTotalBytes > 0 ? stats.diskUsedBytes / stats.diskTotalBytes : 0;
5069
4882
  gauges.push({
5070
4883
  label: "disk",
@@ -5100,29 +4913,18 @@ function renderBar(fraction, width) {
5100
4913
  const filled = "█".repeat(Math.min(full, cells)) + partial;
5101
4914
  return (filled + "░".repeat(Math.max(0, cells - filled.length))).slice(0, cells);
5102
4915
  }
5103
- /** How many port rows the card draws before folding the rest into `+N more`. */
5104
- const MAX_PORT_ROWS = 8;
5105
- const WELL_KNOWN_PORTS = new Map([[3e3, "agent harness"], [3001, "agent webserver"]]);
5106
- /**
5107
- * Reduce the snapshot's ports to drawable rows. The well-known note wins over
5108
- * `local only` — a loopback-bound 3001 being unreachable is a distinct problem
5109
- * the gauges' error path already surfaces, and one annotation per row keeps
5110
- * the table scannable.
5111
- */
5112
- function toPortRows(ports) {
5113
- return ports.map((p) => ({
5114
- port: p.port,
5115
- process: p.process ?? "?",
5116
- note: WELL_KNOWN_PORTS.get(p.port) ?? (p.loopbackOnly ? "local only" : null)
5117
- }));
5118
- }
5119
4916
 
5120
4917
  //#endregion
5121
4918
  //#region src/chat/tui/chunks/computer.tsx
5122
4919
  /**
5123
4920
  * Renders the `/computer` card: a snapshot of the agent's sandbox as a stack of
5124
- * colored bar graphs (cpu load, memory, swap, disk) plus a footer of scalar
5125
- * facts (uptime, process count).
4921
+ * colored bar graphs (cpu, memory, disk).
4922
+ *
4923
+ * The numbers come from the api, not from reaching into the box: the card polls
4924
+ * `collectSandboxStats` (injected via context), which reads the live resource
4925
+ * sample the worker's fleet sweep already stores. So there's no exec and no
4926
+ * cold-start wake — when the agent has no running sandbox the read returns
4927
+ * "no snapshot" at once and the card shows an idle line instead of hanging.
5126
4928
  *
5127
4929
  * The card keeps itself current in place rather than making the user re-run the
5128
4930
  * command — but quietly: it refreshes on a slow interval with no "updating"
@@ -5130,9 +4932,8 @@ function toPortRows(ports) {
5130
4932
  * recent `/computer` card refreshes (older ones freeze at their last snapshot),
5131
4933
  * and the latest one silently stops while the transcript is scrolled up (no
5132
4934
  * wasted polls on a card you're not looking at) and resumes at the bottom — the
5133
- * pause is never rendered. The refresh uses the same sandbox-exec relay
5134
- * `/sandbox` uses, injected via context; the parse/layout math lives in
5135
- * `sandbox-stats.ts` and this component only draws.
4935
+ * pause is never rendered. The parse/layout math lives in `sandbox-stats.ts` and
4936
+ * this component only draws.
5136
4937
  */
5137
4938
  function ComputerItem({ item }) {
5138
4939
  const { collectSandboxStats, latestComputerId, scrolledUp } = useConversationContext();
@@ -5143,7 +4944,8 @@ function ComputerItem({ item }) {
5143
4944
  });
5144
4945
  const [stats, setStats] = useState(item.stats);
5145
4946
  const [error, setError] = useState(item.error);
5146
- const hasStats = useRef(item.stats !== null);
4947
+ const hasResolved = useRef(item.stats !== null || item.state !== "loading");
4948
+ const [idle, setIdle] = useState(item.state !== "loading" && item.stats === null && item.error === null);
5147
4949
  useEffect(() => {
5148
4950
  if (!live || !collectSandboxStats) return;
5149
4951
  let cancelled = false;
@@ -5156,11 +4958,12 @@ function ComputerItem({ item }) {
5156
4958
  inFlight = null;
5157
4959
  if (cancelled) return;
5158
4960
  if (result.ok) {
5159
- hasStats.current = true;
4961
+ hasResolved.current = true;
5160
4962
  setStats(result.stats);
5161
4963
  setError(null);
4964
+ setIdle(result.stats === null);
5162
4965
  } else if (!("aborted" in result)) {
5163
- if (!hasStats.current) setError(result.error);
4966
+ if (!hasResolved.current) setError(result.error);
5164
4967
  }
5165
4968
  if (!cancelled) timer = setTimeout(tick, COMPUTER_POLL_INTERVAL_MS);
5166
4969
  };
@@ -5171,43 +4974,26 @@ function ComputerItem({ item }) {
5171
4974
  inFlight?.abort();
5172
4975
  };
5173
4976
  }, [live, collectSandboxStats]);
5174
- if (stats === null && error === null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsx("text", {
4977
+ if (stats === null && error === null && !idle) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsx("text", {
5175
4978
  fg: theme.dim,
5176
4979
  children: "reading sandbox stats…"
5177
4980
  }) });
5178
- if (stats === null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsxs("text", {
4981
+ if (stats === null && error !== null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsxs("text", {
5179
4982
  fg: theme.error,
5180
- children: ["couldn't read sandbox stats", error ? `: ${error}` : ""]
4983
+ children: ["couldn't read sandbox stats: ", error]
4984
+ }) });
4985
+ if (stats === null) return /* @__PURE__ */ jsx(Shell, { children: /* @__PURE__ */ jsx("text", {
4986
+ fg: theme.dim,
4987
+ children: "no sandbox running"
5181
4988
  }) });
5182
4989
  const gauges = toGauges(stats);
5183
- const portRows = toPortRows(stats.ports);
5184
- const labelWidth = Math.max(5, ...gauges.map((g) => g.label.length));
4990
+ const labelWidth = Math.max(...gauges.map((g) => g.label.length));
5185
4991
  const barWidth = 24;
5186
- return /* @__PURE__ */ jsxs(Shell, { children: [
5187
- gauges.map((g) => /* @__PURE__ */ jsx(GaugeRow, {
5188
- gauge: g,
5189
- labelWidth,
5190
- barWidth
5191
- }, g.label)),
5192
- /* @__PURE__ */ jsx(PortsSection, {
5193
- rows: portRows,
5194
- labelWidth
5195
- }),
5196
- /* @__PURE__ */ jsxs("text", {
5197
- fg: theme.dim,
5198
- children: [
5199
- `${"uptime".padEnd(labelWidth)} `,
5200
- /* @__PURE__ */ jsx("span", {
5201
- fg: theme.muted,
5202
- children: formatUptime(stats.uptimeSeconds)
5203
- }),
5204
- /* @__PURE__ */ jsx("span", {
5205
- fg: theme.dim,
5206
- children: ` · ${stats.procs} processes`
5207
- })
5208
- ]
5209
- })
5210
- ] });
4992
+ return /* @__PURE__ */ jsx(Shell, { children: gauges.map((g) => /* @__PURE__ */ jsx(GaugeRow, {
4993
+ gauge: g,
4994
+ labelWidth,
4995
+ barWidth
4996
+ }, g.label)) });
5211
4997
  }
5212
4998
  /** The card frame: a bold `computer` title over the graph rows. */
5213
4999
  function Shell({ children }) {
@@ -5255,42 +5041,6 @@ function GaugeRow({ gauge, labelWidth, barWidth }) {
5255
5041
  })
5256
5042
  ] });
5257
5043
  }
5258
- /**
5259
- * The listening-ports table: one row per port the sandbox user's processes
5260
- * hold, first row carrying the `ports` gutter label to line up with the
5261
- * gauges. Folds past MAX_PORT_ROWS into a `+N more` line so a busy box can't
5262
- * flood the transcript.
5263
- */
5264
- function PortsSection({ rows, labelWidth }) {
5265
- if (rows.length === 0) return null;
5266
- const shown = rows.slice(0, MAX_PORT_ROWS);
5267
- const folded = rows.length - shown.length;
5268
- const gutter = " ".repeat(labelWidth);
5269
- return /* @__PURE__ */ jsxs(Fragment, { children: [shown.map((row, i) => /* @__PURE__ */ jsxs("text", { children: [
5270
- /* @__PURE__ */ jsx("span", {
5271
- fg: theme.muted,
5272
- children: `${i === 0 ? "ports".padEnd(labelWidth) : gutter} `
5273
- }),
5274
- /* @__PURE__ */ jsx("span", {
5275
- fg: theme.fg,
5276
- children: String(row.port).padStart(5)
5277
- }),
5278
- /* @__PURE__ */ jsx("span", {
5279
- fg: theme.muted,
5280
- children: ` ${row.process}`
5281
- }),
5282
- row.note ? /* @__PURE__ */ jsx("span", {
5283
- fg: theme.dim,
5284
- children: ` · ${row.note}`
5285
- }) : null
5286
- ] }, row.port)), folded > 0 ? /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
5287
- fg: theme.muted,
5288
- children: `${gutter} `
5289
- }), /* @__PURE__ */ jsx("span", {
5290
- fg: theme.dim,
5291
- children: `+${folded} more`
5292
- })] }) : null] });
5293
- }
5294
5044
 
5295
5045
  //#endregion
5296
5046
  //#region src/chat/tui/chunks/index.tsx
@@ -7042,55 +6792,45 @@ function formatShare(share) {
7042
6792
 
7043
6793
  //#endregion
7044
6794
  //#region src/chat/sandbox/collect-stats.ts
6795
+ /**
6796
+ * Drop a snapshot the api hasn't refreshed recently: the sweep that writes it
6797
+ * runs ~every 60s, so a sample older than {@link COMPUTER_STATS_STALE_MS} means
6798
+ * the box stopped reporting (paused / gone) and its numbers are no longer live.
6799
+ * Presenting it as current would be a lie, so treat it as "no snapshot".
6800
+ */
6801
+ function freshOrNull(stats, now) {
6802
+ if (!stats) return null;
6803
+ return now - stats.sampledAt <= COMPUTER_STATS_STALE_MS ? stats : null;
6804
+ }
7045
6805
  function collectSandboxStats(cfg) {
7046
- const open = cfg.open ?? SandboxStream.open;
7047
- const dec = new TextDecoder();
7048
- let buffer = "";
7049
- let settled = false;
7050
- let stream = null;
6806
+ let aborted = false;
7051
6807
  let resolveRef = null;
6808
+ let settled = false;
6809
+ const settle = (r) => {
6810
+ if (settled) return;
6811
+ settled = true;
6812
+ resolveRef?.(r);
6813
+ };
7052
6814
  return {
7053
6815
  result: new Promise((resolve) => {
7054
6816
  resolveRef = resolve;
7055
- const settle = (r) => {
7056
- if (settled) return;
7057
- settled = true;
7058
- resolve(r);
7059
- };
7060
- stream = open({
7061
- mode: "exec",
7062
- appUrl: cfg.appUrl,
7063
- sessionToken: cfg.sessionToken,
7064
- agentId: cfg.agentId,
7065
- command: SANDBOX_STATS_COMMAND,
7066
- onEvent: (e) => {
7067
- if (e.type === "data") buffer += dec.decode(e.bytes);
7068
- else if (e.type === "error") settle({
7069
- ok: false,
7070
- error: e.message
7071
- });
7072
- else if (e.type === "exit") {
7073
- const stats = parseSandboxStats(buffer);
7074
- if (stats) settle({
7075
- ok: true,
7076
- stats
7077
- });
7078
- else settle({
7079
- ok: false,
7080
- error: e.code === 0 ? "unexpected output from the sandbox" : `sandbox command exited ${e.code}`
7081
- });
7082
- } else if (e.type === "close") settle({
7083
- ok: false,
7084
- error: e.failure ?? "connection closed before the snapshot finished"
7085
- });
7086
- }
6817
+ cfg.read().then((stats) => {
6818
+ if (aborted) return;
6819
+ settle({
6820
+ ok: true,
6821
+ stats: freshOrNull(stats, cfg.now())
6822
+ });
6823
+ }).catch((err) => {
6824
+ if (aborted) return;
6825
+ settle({
6826
+ ok: false,
6827
+ error: err instanceof Error ? err.message : String(err)
6828
+ });
7087
6829
  });
7088
6830
  }),
7089
6831
  abort: () => {
7090
- if (settled) return;
7091
- settled = true;
7092
- stream?.close();
7093
- resolveRef?.({
6832
+ aborted = true;
6833
+ settle({
7094
6834
  ok: false,
7095
6835
  aborted: true,
7096
6836
  error: "aborted"
@@ -13123,17 +12863,12 @@ function ChatScreen({ agent, conversation }) {
13123
12863
  return null;
13124
12864
  }, [items]);
13125
12865
  const boundCollectSandboxStats = useMemo(() => {
13126
- if (!appUrl || !sessionToken) return null;
12866
+ if (!rest) return null;
13127
12867
  return () => collectSandboxStats({
13128
- appUrl,
13129
- sessionToken,
13130
- agentId: agent.id
12868
+ read: () => rest.computeLiveStats(),
12869
+ now: () => Date.now()
13131
12870
  });
13132
- }, [
13133
- appUrl,
13134
- sessionToken,
13135
- agent.id
13136
- ]);
12871
+ }, [rest]);
13137
12872
  const conversationContext = useMemo(() => ({
13138
12873
  rest,
13139
12874
  conversationId,
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
20
20
  * the commit time of the built tree is one monotonic clock they share.
21
21
  */
22
22
  function portalDaemonBuild() {
23
- return "1786755327";
23
+ return "1786761220";
24
24
  }
25
25
  /**
26
26
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./client-DbqRBquD.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-DuxaUuek.mjs";
3
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-BTyg329O.mjs";
4
4
  import "./api-DG5W6iwx.mjs";
5
5
 
6
6
  export { runPortalDaemon };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-DuxaUuek.mjs";
2
+ import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-BTyg329O.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DbqRBquD.mjs";
3
+ import "./daemon-BTyg329O.mjs";
4
+ import "./api-DG5W6iwx.mjs";
5
+ import { t as PortalDaemonClient } from "./daemon-client--A_yMKq6.mjs";
6
+
7
+ export { PortalDaemonClient };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
- import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-xXfqT4SX.mjs";
3
+ import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-CgfbKXst.mjs";
4
4
  import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-2wju4gC_.mjs";
5
5
  import path from "node:path";
6
6
  import Conf from "conf";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-DYiQC5IW.mjs";
3
- import "./rest-xXfqT4SX.mjs";
2
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-C_TnQSPg.mjs";
3
+ import "./rest-CgfbKXst.mjs";
4
4
  import "./billing-blocked-2wju4gC_.mjs";
5
5
 
6
6
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as getConfigPath } from "./print-DYiQC5IW.mjs";
2
+ import { C as getConfigPath } from "./print-C_TnQSPg.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { err, ok } from "neverthrow";
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.5.0-beta.30";
11
+ var version$1 = "0.5.0-beta.31";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
@@ -299,6 +299,10 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
299
299
  const { result } = await get(`/api/v1/trpc/v2.subagentTasks.list?input=${encodeURIComponent(JSON.stringify({ conversationId }))}`, subagentTasksResponseSchema);
300
300
  return new Map(result.data.subagentTasks.map((task) => [task.id, task]));
301
301
  },
302
+ computeLiveStats: async () => {
303
+ const { stats } = await get("/api/v1/compute/live-stats", liveStatsResponseSchema);
304
+ return stats;
305
+ },
302
306
  uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
303
307
  const size = data.byteLength;
304
308
  const presign = await post("/api/v1/attachments/presign", {
@@ -549,6 +553,15 @@ const subagentTaskSnapshotSchema = z.object({
549
553
  completedAt: z.string().nullable()
550
554
  });
551
555
  const subagentTasksResponseSchema = z.object({ result: z.object({ data: z.object({ subagentTasks: z.array(subagentTaskSnapshotSchema) }) }) });
556
+ const liveStatsResponseSchema = z.object({ stats: z.object({
557
+ sampledAt: z.number(),
558
+ cpuUsedPct: z.number(),
559
+ cpuCount: z.number(),
560
+ memUsedBytes: z.number(),
561
+ memTotalBytes: z.number(),
562
+ diskUsedBytes: z.number(),
563
+ diskTotalBytes: z.number()
564
+ }).nullable() });
552
565
  const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
553
566
  const workspaceFileSchema = z.object({
554
567
  id: z.string(),
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
- import { n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-xXfqT4SX.mjs";
3
+ import { n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-CgfbKXst.mjs";
4
4
  import "./billing-blocked-2wju4gC_.mjs";
5
5
 
6
6
  export { createRestClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.30",
3
+ "version": "0.5.0-beta.31",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-DbqRBquD.mjs";
3
- import "./daemon-DuxaUuek.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
- import { t as PortalDaemonClient } from "./daemon-client-C4heMWTq.mjs";
6
-
7
- export { PortalDaemonClient };