codeam-cli 2.61.35 → 2.61.37

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
@@ -4,6 +4,12 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.61.35] — 2026-07-24
8
+
9
+ ### Fixed
10
+
11
+ - **box:** Box user must own /home/box — set HOME late so the build doesn't root-own ~/.codeam
12
+
7
13
  ## [2.61.34] — 2026-07-24
8
14
 
9
15
  ### Added
package/dist/index.js CHANGED
@@ -6355,7 +6355,7 @@ function readAnonId() {
6355
6355
  }
6356
6356
  function superProperties() {
6357
6357
  return {
6358
- cliVersion: true ? "2.61.35" : "0.0.0-dev",
6358
+ cliVersion: true ? "2.61.37" : "0.0.0-dev",
6359
6359
  nodeVersion: process.version,
6360
6360
  platform: process.platform,
6361
6361
  arch: process.arch,
@@ -6536,7 +6536,7 @@ var os4 = __toESM(require("os"));
6536
6536
  // package.json
6537
6537
  var package_default = {
6538
6538
  name: "codeam-cli",
6539
- version: "2.61.35",
6539
+ version: "2.61.37",
6540
6540
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
6541
6541
  type: "commonjs",
6542
6542
  main: "dist/index.js",
@@ -7707,7 +7707,7 @@ var CommandRelayService = class _CommandRelayService {
7707
7707
  // fresh + clear the "CLI update available" banner after a self-update
7708
7708
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7709
7709
  // pair/reconnect). Older backends ignore the extra field.
7710
- ..."2.61.35" ? { ideVersion: "2.61.35" } : {}
7710
+ ..."2.61.37" ? { ideVersion: "2.61.37" } : {}
7711
7711
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7712
7712
  }
7713
7713
  /**
@@ -18691,7 +18691,7 @@ async function autoUpgradeBeforeCriticalCommand() {
18691
18691
  if (process.env.NODE_ENV === "test") return;
18692
18692
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
18693
18693
  if (process.env.CI) return;
18694
- const current = true ? "2.61.35" : null;
18694
+ const current = true ? "2.61.37" : null;
18695
18695
  if (!current) return;
18696
18696
  const cache = readCache();
18697
18697
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -18708,7 +18708,7 @@ function checkForUpdates() {
18708
18708
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
18709
18709
  if (process.env.CI) return;
18710
18710
  if (!process.stdout.isTTY) return;
18711
- const current = true ? "2.61.35" : null;
18711
+ const current = true ? "2.61.37" : null;
18712
18712
  if (!current) return;
18713
18713
  const cache = readCache();
18714
18714
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -18728,7 +18728,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
18728
18728
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
18729
18729
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
18730
18730
  function currentCliVersion() {
18731
- return true ? "2.61.35" : null;
18731
+ return true ? "2.61.37" : null;
18732
18732
  }
18733
18733
  function runCmd(cmd, args2, timeoutMs) {
18734
18734
  return new Promise((resolve8) => {
@@ -18978,8 +18978,16 @@ function isFleetBoxRefPayload(p2) {
18978
18978
  if (typeof p2.boxId !== "string") return false;
18979
18979
  if (!isFleetContainerName(p2.containerName)) return false;
18980
18980
  if (p2.removeVolume !== void 0 && typeof p2.removeVolume !== "boolean") return false;
18981
+ if (p2.enrollToken !== void 0 && (typeof p2.enrollToken !== "string" || !p2.enrollToken)) {
18982
+ return false;
18983
+ }
18984
+ if (p2.apiOrigin !== void 0 && (typeof p2.apiOrigin !== "string" || !p2.apiOrigin)) return false;
18985
+ if (p2.limits !== void 0 && !isFleetLimits(p2.limits)) return false;
18981
18986
  return true;
18982
18987
  }
18988
+ function fleetRefCanRecreate(p2) {
18989
+ return typeof p2.enrollToken === "string" && p2.enrollToken.length > 0 && typeof p2.apiOrigin === "string" && p2.apiOrigin.length > 0 && p2.limits !== void 0;
18990
+ }
18983
18991
  function fleetUserIdFromContainerName(containerName) {
18984
18992
  return containerName.slice("codeam-box-".length);
18985
18993
  }
@@ -18992,6 +19000,57 @@ function isMissingVolumeError(stderr) {
18992
19000
  function resolveFleetBoxImage() {
18993
19001
  return process.env.CODEAM_FLEET_BOX_IMAGE || "ghcr.io/edgar-durand/codeam-box:latest";
18994
19002
  }
19003
+ function buildFleetBoxRunArgs(p2) {
19004
+ const userId = fleetUserIdFromContainerName(p2.containerName);
19005
+ const image = resolveFleetBoxImage();
19006
+ return [
19007
+ "run",
19008
+ "-d",
19009
+ // Registry-default image: always pull so the box never silently runs this
19010
+ // host's STALE cached :latest. An explicit CODEAM_FLEET_BOX_IMAGE override
19011
+ // (int test / operator) keeps docker's default policy — a local-only tag
19012
+ // can't be pulled.
19013
+ ...process.env.CODEAM_FLEET_BOX_IMAGE ? [] : ["--pull=always"],
19014
+ "--name",
19015
+ p2.containerName,
19016
+ "--cap-drop",
19017
+ "ALL",
19018
+ "--security-opt",
19019
+ "no-new-privileges",
19020
+ "--memory",
19021
+ `${p2.limits.memoryMb}m`,
19022
+ "--cpus",
19023
+ String(p2.limits.cpus),
19024
+ "--pids-limit",
19025
+ String(p2.limits.pidsLimit),
19026
+ "--network",
19027
+ "fleet-net",
19028
+ "--label",
19029
+ `com.codeagent.user-id=${userId}`,
19030
+ "--label",
19031
+ `com.codeagent.box-id=${p2.boxId}`,
19032
+ "--label",
19033
+ "com.codeagent.created-by=fleet",
19034
+ "-v",
19035
+ `${p2.containerName}:/home/box`,
19036
+ // Bare `-e NAME` — docker reads the value from ITS OWN process env (supplied
19037
+ // via opts.env), never from this argv.
19038
+ "-e",
19039
+ "CODEAM_ENROLL_TOKEN",
19040
+ "-e",
19041
+ `CODEAM_API_URL=${p2.apiOrigin}`,
19042
+ // User-facing "My Servers" label; read at redeem via resolveHostLabel.
19043
+ "-e",
19044
+ "CODEAM_HOST_LABEL=CodeAgent Box",
19045
+ // The enroll token is single-use but lives in the container's fixed env;
19046
+ // resolveHostIdentity resumes from the sealed identity on a terminal
19047
+ // enroll-token rejection (a plain wake/restart replaying the consumed
19048
+ // token). Kept for observability / older-CLI back-compat.
19049
+ "-e",
19050
+ "CODEAM_ENROLL_EPHEMERAL=1",
19051
+ image
19052
+ ];
19053
+ }
18995
19054
  var DOCKER_RUN_TIMEOUT_MS = 12e4;
18996
19055
  var defaultDockerRunner = {
18997
19056
  run(args2, opts = {}) {
@@ -19423,7 +19482,6 @@ var HostAgentSupervisor = class {
19423
19482
  */
19424
19483
  async fleetCreateBox(payload) {
19425
19484
  const { containerName, limits } = payload;
19426
- const userId = fleetUserIdFromContainerName(containerName);
19427
19485
  log.info(
19428
19486
  "host-agent",
19429
19487
  `fleet_create_box id=${payload.boxId} name=${containerName} mem=${limits.memoryMb}m cpus=${limits.cpus} pids=${limits.pidsLimit}`
@@ -19437,63 +19495,7 @@ var HostAgentSupervisor = class {
19437
19495
  `fleet_create_box: pre-create rm of ${containerName} failed (code=${rm.code}): ${rm.stderr.trim().slice(-200)} \u2014 attempting run anyway`
19438
19496
  );
19439
19497
  }
19440
- const image = resolveFleetBoxImage();
19441
- const args2 = [
19442
- "run",
19443
- "-d",
19444
- // Registry-default image: always pull so a new box never silently
19445
- // runs this host's STALE cached :latest (2026-07-16: cache had CLI
19446
- // 2.61.4 while the registry had 2.61.9). An explicit
19447
- // CODEAM_FLEET_BOX_IMAGE override (int test / operator) keeps
19448
- // docker's default policy — a local-only tag can't be pulled.
19449
- ...process.env.CODEAM_FLEET_BOX_IMAGE ? [] : ["--pull=always"],
19450
- "--name",
19451
- containerName,
19452
- "--cap-drop",
19453
- "ALL",
19454
- "--security-opt",
19455
- "no-new-privileges",
19456
- "--memory",
19457
- `${limits.memoryMb}m`,
19458
- "--cpus",
19459
- String(limits.cpus),
19460
- "--pids-limit",
19461
- String(limits.pidsLimit),
19462
- "--network",
19463
- "fleet-net",
19464
- "--label",
19465
- `com.codeagent.user-id=${userId}`,
19466
- "--label",
19467
- `com.codeagent.box-id=${payload.boxId}`,
19468
- "--label",
19469
- "com.codeagent.created-by=fleet",
19470
- "-v",
19471
- `${containerName}:/home/box`,
19472
- // Bare `-e NAME` (no `=value`) — docker reads the value from ITS OWN
19473
- // process env (supplied below via `opts.env`), never from this argv.
19474
- "-e",
19475
- "CODEAM_ENROLL_TOKEN",
19476
- "-e",
19477
- `CODEAM_API_URL=${payload.apiOrigin}`,
19478
- // User-facing label for the rescued box's "My Servers" card. Not a
19479
- // secret → plain `-e KEY=value`. The box's host-agent reads it at
19480
- // redeem (resolveHostLabel) so a rescued user sees "CodeAgent Box",
19481
- // not the generic "my-server" or the ugly container cuid.
19482
- "-e",
19483
- "CODEAM_HOST_LABEL=CodeAgent Box",
19484
- // The enroll token above is a SINGLE-USE bootstrap, but it lives in the
19485
- // container's fixed env — it can't be stripped after first boot like the
19486
- // self-hosted systemd unit does, so on every restart (docker restart /
19487
- // reboot / fleet_start_box wake) the redeem terminally expires.
19488
- // `resolveHostIdentity` now resumes from a sealed identity on ANY
19489
- // terminal enroll-token rejection (not just ephemeral boxes — see its
19490
- // doc comment), so this flag is no longer load-bearing for that
19491
- // decision; kept set for observability/back-compat with older CLI
19492
- // builds that still gate on it. Not a secret → plain KEY=value.
19493
- "-e",
19494
- "CODEAM_ENROLL_EPHEMERAL=1",
19495
- image
19496
- ];
19498
+ const args2 = buildFleetBoxRunArgs(payload);
19497
19499
  const res = await this.docker.run(args2, {
19498
19500
  timeoutMs: DOCKER_RUN_TIMEOUT_MS,
19499
19501
  env: { CODEAM_ENROLL_TOKEN: payload.enrollToken }
@@ -19513,15 +19515,74 @@ var HostAgentSupervisor = class {
19513
19515
  /** `fleet_start_box` — wake a sleeping box (`docker start`). Idempotent: a
19514
19516
  * container the host already removed is treated as success. */
19515
19517
  async fleetStartBox(payload) {
19516
- log.info("host-agent", `fleet_start_box id=${payload.boxId} name=${payload.containerName}`);
19517
- const res = await this.docker.run(["start", payload.containerName]);
19518
+ const { containerName } = payload;
19519
+ if (fleetRefCanRecreate(payload) && await this.fleetBoxImageStale(containerName)) {
19520
+ log.info(
19521
+ "host-agent",
19522
+ `fleet_start_box id=${payload.boxId} name=${containerName} \u2014 image stale \u2192 recreating from :latest`
19523
+ );
19524
+ const rm = await this.docker.run(["rm", "-f", containerName], {
19525
+ timeoutMs: DOCKER_RUN_TIMEOUT_MS
19526
+ });
19527
+ if (rm.code !== 0 && !isMissingContainerError(rm.stderr)) {
19528
+ log.warn(
19529
+ "host-agent",
19530
+ `fleet_start_box: pre-recreate rm of ${containerName} failed (code=${rm.code}): ${rm.stderr.trim().slice(-200)} \u2014 attempting run anyway`
19531
+ );
19532
+ }
19533
+ const args2 = buildFleetBoxRunArgs({
19534
+ boxId: payload.boxId,
19535
+ containerName,
19536
+ apiOrigin: payload.apiOrigin,
19537
+ limits: payload.limits
19538
+ });
19539
+ const res2 = await this.docker.run(args2, {
19540
+ timeoutMs: DOCKER_RUN_TIMEOUT_MS,
19541
+ env: { CODEAM_ENROLL_TOKEN: payload.enrollToken }
19542
+ });
19543
+ if (res2.code === 0) {
19544
+ log.info(
19545
+ "host-agent",
19546
+ `fleet box ${containerName} recreated from :latest (${res2.stdout.trim().slice(0, 12)})`
19547
+ );
19548
+ } else {
19549
+ log.warn(
19550
+ "host-agent",
19551
+ `fleet_start_box recreate ${containerName} failed (code=${res2.code}): ${res2.stderr.trim().slice(-300)}`
19552
+ );
19553
+ }
19554
+ return;
19555
+ }
19556
+ log.info("host-agent", `fleet_start_box id=${payload.boxId} name=${containerName}`);
19557
+ const res = await this.docker.run(["start", containerName]);
19518
19558
  if (res.code !== 0 && !isMissingContainerError(res.stderr)) {
19519
19559
  log.warn(
19520
19560
  "host-agent",
19521
- `fleet_start_box ${payload.containerName} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
19561
+ `fleet_start_box ${containerName} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
19522
19562
  );
19523
19563
  }
19524
19564
  }
19565
+ /**
19566
+ * True when the box's container was created from an image OTHER than the
19567
+ * current registry `:latest` (a stale runtime), or the container is gone.
19568
+ * Pulls `:latest` first so the comparison is against the freshest image.
19569
+ * Best-effort + fail-SAFE: a local-image override (int test / operator) or any
19570
+ * pull/inspect error returns false → the caller falls back to a plain
19571
+ * `docker start`, so a transient docker hiccup can never wedge a wake.
19572
+ */
19573
+ async fleetBoxImageStale(containerName) {
19574
+ if (process.env.CODEAM_FLEET_BOX_IMAGE) return false;
19575
+ const image = resolveFleetBoxImage();
19576
+ const cur = await this.docker.run(["inspect", "--format", "{{.Image}}", containerName]);
19577
+ if (cur.code !== 0) {
19578
+ return isMissingContainerError(cur.stderr);
19579
+ }
19580
+ const pull = await this.docker.run(["pull", image], { timeoutMs: DOCKER_RUN_TIMEOUT_MS });
19581
+ if (pull.code !== 0) return false;
19582
+ const latest = await this.docker.run(["inspect", "--format", "{{.Id}}", image]);
19583
+ if (latest.code !== 0) return false;
19584
+ return cur.stdout.trim() !== "" && cur.stdout.trim() !== latest.stdout.trim();
19585
+ }
19525
19586
  /** `fleet_stop_box` — sleep an idle box (`docker stop`). MUST be
19526
19587
  * idempotent — the backend's reap sweeps may re-send a stop for a box
19527
19588
  * the host already stopped/removed; "No such container" is success. */
@@ -38737,7 +38798,7 @@ function checkChokidar() {
38737
38798
  }
38738
38799
  async function doctor(args2 = []) {
38739
38800
  const json = args2.includes("--json");
38740
- const cliVersion = true ? "2.61.35" : "0.0.0-dev";
38801
+ const cliVersion = true ? "2.61.37" : "0.0.0-dev";
38741
38802
  const apiBase2 = resolveApiBaseUrl();
38742
38803
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
38743
38804
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -39248,7 +39309,7 @@ async function mcpRun(args2) {
39248
39309
  // src/commands/version.ts
39249
39310
  var import_picocolors15 = __toESM(require("picocolors"));
39250
39311
  function version2() {
39251
- const v = true ? "2.61.35" : "unknown";
39312
+ const v = true ? "2.61.37" : "unknown";
39252
39313
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
39253
39314
  }
39254
39315
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.61.35",
3
+ "version": "2.61.37",
4
4
  "description": "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device — async. The terminal companion for CodeAgent Mobile.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",