codeam-cli 2.76.11 → 2.76.12

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 (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/index.js +182 -15
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ 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.76.11] — 2026-10-01
8
+
9
+ ### Added
10
+
11
+ - **cli:** Host-agent startup self-update with versioned side-by-side installs (#895)
12
+
13
+ ### CI
14
+
15
+ - **release:** Refresh and verify the wrapper Codespaces prebuild on every release (#894)
16
+
7
17
  ## [2.76.10] — 2026-10-01
8
18
 
9
19
  ### Fixed
package/dist/index.js CHANGED
@@ -10579,7 +10579,7 @@ function readAnonId() {
10579
10579
  }
10580
10580
  function superProperties() {
10581
10581
  return {
10582
- cliVersion: true ? "2.76.11" : "0.0.0-dev",
10582
+ cliVersion: true ? "2.76.12" : "0.0.0-dev",
10583
10583
  nodeVersion: process.version,
10584
10584
  platform: process.platform,
10585
10585
  arch: process.arch,
@@ -10824,7 +10824,7 @@ var http = __toESM(require("http"));
10824
10824
  // package.json
10825
10825
  var package_default = {
10826
10826
  name: "codeam-cli",
10827
- version: "2.76.11",
10827
+ version: "2.76.12",
10828
10828
  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.",
10829
10829
  type: "commonjs",
10830
10830
  main: "dist/index.js",
@@ -12160,7 +12160,7 @@ var CommandRelayService = class _CommandRelayService {
12160
12160
  // fresh + clear the "CLI update available" banner after a self-update
12161
12161
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
12162
12162
  // pair/reconnect). Older backends ignore the extra field.
12163
- ..."2.76.11" ? { ideVersion: "2.76.11" } : {}
12163
+ ..."2.76.12" ? { ideVersion: "2.76.12" } : {}
12164
12164
  };
12165
12165
  try {
12166
12166
  const res = await _postJson(`${API_BASE2}/api/plugin/heartbeat`, body);
@@ -50172,7 +50172,7 @@ async function autoUpgradeBeforeCriticalCommand() {
50172
50172
  if (process.env.NODE_ENV === "test") return;
50173
50173
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
50174
50174
  if (process.env.CI) return;
50175
- const current2 = true ? "2.76.11" : null;
50175
+ const current2 = true ? "2.76.12" : null;
50176
50176
  if (!current2) return;
50177
50177
  const cache = readCache();
50178
50178
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -50189,7 +50189,7 @@ function checkForUpdates() {
50189
50189
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
50190
50190
  if (process.env.CI) return;
50191
50191
  if (!process.stdout.isTTY) return;
50192
- const current2 = true ? "2.76.11" : null;
50192
+ const current2 = true ? "2.76.12" : null;
50193
50193
  if (!current2) return;
50194
50194
  const cache = readCache();
50195
50195
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -50460,7 +50460,7 @@ var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
50460
50460
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
50461
50461
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
50462
50462
  function currentCliVersion() {
50463
- return true ? "2.76.11" : null;
50463
+ return true ? "2.76.12" : null;
50464
50464
  }
50465
50465
  async function installedVersion(deps) {
50466
50466
  const ls = await deps.run(
@@ -50655,7 +50655,7 @@ function signalHostReady(send = process.send?.bind(process)) {
50655
50655
  }
50656
50656
  function defaultDeps() {
50657
50657
  return {
50658
- ownVersion: true ? "2.76.11" : null,
50658
+ ownVersion: true ? "2.76.12" : null,
50659
50659
  root: cliVersionsRoot(),
50660
50660
  args: process.argv.slice(3),
50661
50661
  execPath: process.execPath,
@@ -50832,6 +50832,55 @@ var defaultDisableService = () => {
50832
50832
  }
50833
50833
  };
50834
50834
 
50835
+ // src/commands/host/fleet-image-gc.ts
50836
+ var FLEET_ROLLBACK_TAG = "rollback";
50837
+ var FLEET_IMAGE_INSPECT_FORMAT = '{{.Id}}|{{.Created}}|{{join .RepoTags ","}}|{{join .RootFS.Layers ","}}';
50838
+ function repoOfImageRef(ref) {
50839
+ const colon = ref.lastIndexOf(":");
50840
+ return colon > ref.lastIndexOf("/") ? ref.slice(0, colon) : ref;
50841
+ }
50842
+ function parseFleetImageInspect(stdout) {
50843
+ const out2 = [];
50844
+ for (const line of stdout.split("\n")) {
50845
+ const parts = line.trim().split("|");
50846
+ if (parts.length !== 4 || !parts[0].startsWith("sha256:")) continue;
50847
+ const [id, created, tags, layers] = parts;
50848
+ out2.push({
50849
+ id,
50850
+ created,
50851
+ repoTags: tags ? tags.split(",").filter((t2) => t2 && t2 !== "<none>:<none>") : [],
50852
+ layers: layers ? layers.split(",") : []
50853
+ });
50854
+ }
50855
+ return out2;
50856
+ }
50857
+ function planFleetBoxImageGc(input) {
50858
+ const { images, referenced, currentId, currentRef } = input;
50859
+ const current2 = images.find((i) => i.id === currentId);
50860
+ if (!current2) return { removeTags: [] };
50861
+ const currentRepo = repoOfImageRef(currentRef);
50862
+ const boxRepos = /* @__PURE__ */ new Set([currentRepo, "codeam-box"]);
50863
+ const boxTagsOf = (img) => img.repoTags.filter((t2) => boxRepos.has(repoOfImageRef(t2)));
50864
+ const baseLayer = current2.layers[0];
50865
+ const isBox = (img) => boxTagsOf(img).length > 0 || img.repoTags.length === 0 && !!baseLayer && img.layers[0] === baseLayer;
50866
+ const previous = images.filter((i) => i.id !== currentId && isBox(i)).sort((a, b) => Date.parse(b.created) - Date.parse(a.created));
50867
+ const rollback = previous[0];
50868
+ const rollbackTag = `${currentRepo}:${FLEET_ROLLBACK_TAG}`;
50869
+ const tagAs = rollback && boxTagsOf(rollback).length === 0 ? rollbackTag : void 0;
50870
+ const removeTags = [];
50871
+ for (const img of previous.slice(1)) {
50872
+ if (referenced.has(img.id)) continue;
50873
+ for (const tag of boxTagsOf(img)) {
50874
+ if (tagAs && tag === rollbackTag) continue;
50875
+ removeTags.push(tag);
50876
+ }
50877
+ }
50878
+ return {
50879
+ rollback: rollback ? { id: rollback.id, ...tagAs ? { tagAs } : {} } : void 0,
50880
+ removeTags
50881
+ };
50882
+ }
50883
+
50835
50884
  // src/commands/host-agent.ts
50836
50885
  var HEARTBEAT_INTERVAL_MS = 2e4;
50837
50886
  var RESUME_RETRY_BACKOFF_MS = [15e3, 3e4, 6e4, 12e4];
@@ -50996,6 +51045,7 @@ function buildFleetBoxRunArgs(p2) {
50996
51045
  }
50997
51046
  var DOCKER_RUN_TIMEOUT_MS = 12e4;
50998
51047
  var DOCKER_RUN_WITH_PULL_TIMEOUT_MS = 6e5;
51048
+ var DOCKER_PRUNE_TIMEOUT_MS = 6e5;
50999
51049
  var defaultDockerRunner = {
51000
51050
  run(args2, opts = {}) {
51001
51051
  return new Promise((resolve14) => {
@@ -51767,22 +51817,42 @@ var HostAgentSupervisor = class {
51767
51817
  * `fleet_prune_host` — reclaim Docker residue. Best-effort and idempotent: a
51768
51818
  * prune with nothing to collect exits 0 with "Total reclaimed space: 0B".
51769
51819
  *
51770
- * ⚠️ Only ever `image prune` (dangling) and `builder prune`. NEVER
51771
- * `container prune` (a stopped fleet container is a SLEEPING BOX) and NEVER
51772
- * `volume prune` (a box's volume is the user's workspace and outlives its
51773
- * container by design). See `FleetPruneHostPayload`.
51820
+ * ⚠️ Only superseded Box images, `image prune` (dangling) and `builder prune`.
51821
+ * NEVER `container prune` (a stopped fleet container is a SLEEPING BOX) and
51822
+ * NEVER `volume prune` (a box's volume is the user's workspace and outlives
51823
+ * its container by design). See `FleetPruneHostPayload`.
51824
+ *
51825
+ * ⚠️ Every step runs on the LONG docker bound. Under the 120 s one the dangling
51826
+ * prune never finished on fleet-1: the prod and dev host-agents share one
51827
+ * daemon and both get this command at 15:45 UTC; dev's prune was SIGTERM'd at
51828
+ * 120 s (code=143) and prod's was refused with "a prune operation is already
51829
+ * running" — every day, so 17 GB of dangling Box images stayed (2026-10-02).
51830
+ * That refusal now means the other agent is doing the work, not a failure.
51774
51831
  */
51775
51832
  async fleetPruneHost(payload) {
51776
51833
  log.info(
51777
51834
  "host-agent",
51778
51835
  `fleet_prune_host images=${payload.images} buildCache=${payload.buildCache}`
51779
51836
  );
51837
+ if (payload.images) await this.fleetCollectSupersededBoxImages();
51780
51838
  const steps = [];
51781
51839
  if (payload.images) steps.push({ label: "image", args: ["image", "prune", "-f"] });
51782
- if (payload.buildCache) steps.push({ label: "builder", args: ["builder", "prune", "-f"] });
51840
+ if (payload.buildCache) {
51841
+ steps.push({
51842
+ label: "builder",
51843
+ args: ["builder", "prune", "-f", "--all", "--filter", "until=72h"]
51844
+ });
51845
+ }
51783
51846
  for (const step of steps) {
51784
- const res = await this.docker.run(step.args);
51847
+ const res = await this.docker.run(step.args, { timeoutMs: DOCKER_PRUNE_TIMEOUT_MS });
51785
51848
  if (res.code !== 0) {
51849
+ if (/prune operation is already running/i.test(res.stderr)) {
51850
+ log.info(
51851
+ "host-agent",
51852
+ `fleet_prune_host ${step.label} skipped: another prune is running on this daemon`
51853
+ );
51854
+ continue;
51855
+ }
51786
51856
  log.warn(
51787
51857
  "host-agent",
51788
51858
  `fleet_prune_host ${step.label} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
@@ -51796,6 +51866,103 @@ var HostAgentSupervisor = class {
51796
51866
  );
51797
51867
  }
51798
51868
  }
51869
+ /**
51870
+ * Remove superseded Box images by tag: everything but the current `:latest`,
51871
+ * one previous release (kept, tagged `:rollback`) and any image a container
51872
+ * uses. Fail-SAFE: if the current image, the image list or the container
51873
+ * references cannot be read, it removes nothing.
51874
+ */
51875
+ async fleetCollectSupersededBoxImages() {
51876
+ if (process.env.CODEAM_FLEET_BOX_IMAGE) return;
51877
+ const currentRef = resolveFleetBoxImage();
51878
+ const opts = { timeoutMs: DOCKER_PRUNE_TIMEOUT_MS };
51879
+ const lines = (out2) => [...new Set(out2.split("\n").map((l) => l.trim()).filter(Boolean))];
51880
+ const cur = await this.docker.run(
51881
+ ["image", "inspect", "--format", "{{.Id}}", currentRef],
51882
+ opts
51883
+ );
51884
+ const currentId = cur.stdout.trim();
51885
+ if (cur.code !== 0 || !currentId) {
51886
+ log.warn("host-agent", `fleet_prune_host box-images skipped: ${currentRef} not present`);
51887
+ return;
51888
+ }
51889
+ const ids = await this.docker.run(["image", "ls", "-aq", "--no-trunc"], opts);
51890
+ if (ids.code !== 0) {
51891
+ log.warn(
51892
+ "host-agent",
51893
+ `fleet_prune_host box-images skipped: image ls failed (code=${ids.code})`
51894
+ );
51895
+ return;
51896
+ }
51897
+ const imageIds = lines(ids.stdout);
51898
+ if (imageIds.length === 0) return;
51899
+ const inspect = await this.docker.run(
51900
+ ["image", "inspect", "--format", FLEET_IMAGE_INSPECT_FORMAT, ...imageIds],
51901
+ opts
51902
+ );
51903
+ if (inspect.code !== 0) {
51904
+ log.warn(
51905
+ "host-agent",
51906
+ `fleet_prune_host box-images skipped: image inspect failed (code=${inspect.code})`
51907
+ );
51908
+ return;
51909
+ }
51910
+ const containers = await this.docker.run(["ps", "-aq", "--no-trunc"], opts);
51911
+ if (containers.code !== 0) {
51912
+ log.warn(
51913
+ "host-agent",
51914
+ `fleet_prune_host box-images skipped: ps failed (code=${containers.code})`
51915
+ );
51916
+ return;
51917
+ }
51918
+ const containerIds = lines(containers.stdout);
51919
+ const referenced = /* @__PURE__ */ new Set();
51920
+ if (containerIds.length > 0) {
51921
+ const refs = await this.docker.run(
51922
+ ["inspect", "--format", "{{.Image}}", ...containerIds],
51923
+ opts
51924
+ );
51925
+ if (refs.code !== 0) {
51926
+ log.warn(
51927
+ "host-agent",
51928
+ `fleet_prune_host box-images skipped: container inspect failed (code=${refs.code})`
51929
+ );
51930
+ return;
51931
+ }
51932
+ for (const id of lines(refs.stdout)) referenced.add(id);
51933
+ }
51934
+ const plan = planFleetBoxImageGc({
51935
+ images: parseFleetImageInspect(inspect.stdout),
51936
+ referenced,
51937
+ currentId,
51938
+ currentRef
51939
+ });
51940
+ if (plan.rollback?.tagAs) {
51941
+ const tag = await this.docker.run(["tag", plan.rollback.id, plan.rollback.tagAs], opts);
51942
+ if (tag.code !== 0) {
51943
+ log.warn(
51944
+ "host-agent",
51945
+ `fleet_prune_host box-images: tagging rollback failed (code=${tag.code}), removing nothing`
51946
+ );
51947
+ return;
51948
+ }
51949
+ }
51950
+ for (const ref of plan.removeTags) {
51951
+ const res = await this.docker.run(["rmi", ref], opts);
51952
+ if (res.code === 0 || /no such image/i.test(res.stderr)) {
51953
+ log.info("host-agent", `fleet_prune_host box-images removed ${ref}`);
51954
+ } else {
51955
+ log.warn(
51956
+ "host-agent",
51957
+ `fleet_prune_host box-images rmi ${ref} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
51958
+ );
51959
+ }
51960
+ }
51961
+ log.info(
51962
+ "host-agent",
51963
+ `fleet_prune_host box-images current=${currentId.slice(7, 19)} rollback=${plan.rollback ? plan.rollback.id.slice(7, 19) : "none"} removed=${plan.removeTags.length}`
51964
+ );
51965
+ }
51799
51966
  async fleetStopBox(payload) {
51800
51967
  log.info("host-agent", `fleet_stop_box id=${payload.boxId} name=${payload.containerName}`);
51801
51968
  const res = await this.docker.run(["stop", payload.containerName]);
@@ -52806,7 +52973,7 @@ function checkChokidar() {
52806
52973
  }
52807
52974
  async function doctor(args2 = []) {
52808
52975
  const json = args2.includes("--json");
52809
- const cliVersion = true ? "2.76.11" : "0.0.0-dev";
52976
+ const cliVersion = true ? "2.76.12" : "0.0.0-dev";
52810
52977
  const apiBase2 = resolveApiBaseUrl();
52811
52978
  const diagnosticId = (0, import_node_crypto17.randomUUID)();
52812
52979
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -53330,7 +53497,7 @@ async function mcpWarm(argv = [], opts = {}) {
53330
53497
  // src/commands/version.ts
53331
53498
  var import_picocolors15 = __toESM(require("picocolors"));
53332
53499
  function version2() {
53333
- const v = true ? "2.76.11" : "unknown";
53500
+ const v = true ? "2.76.12" : "unknown";
53334
53501
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
53335
53502
  }
53336
53503
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.76.11",
3
+ "version": "2.76.12",
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",