codeam-cli 2.60.69 → 2.61.0

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 +29 -0
  2. package/dist/index.js +255 -11
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ 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.60.70] — 2026-07-15
8
+
9
+ ### Added
10
+
11
+ - **cli:** Fleet control plane — DockerRunner + fleet_* box handlers
12
+ - **box:** Production codeam-box rescue-fleet runtime image
13
+
14
+ ### Chore
15
+
16
+ - **meta:** Register box/test commitlint scopes for the fleet work
17
+
18
+ ### Documentation
19
+
20
+ - **fleet:** CLAUDE.md fleet CLI section
21
+
22
+ ### Fixed
23
+
24
+ - **cli:** Deliver fleet box enroll token via env passthrough, not docker argv
25
+
26
+ ### Tests
27
+
28
+ - **test:** Real-Docker integration gate for the fleet control plane
29
+
30
+ ## [2.60.69] — 2026-07-15
31
+
32
+ ### Fixed
33
+
34
+ - **cli:** Provision uvx via the standalone uv installer and resolve per-user bin paths for MCP launchers
35
+
7
36
  ## [2.60.68] — 2026-07-15
8
37
 
9
38
  ### Added
package/dist/index.js CHANGED
@@ -5970,7 +5970,7 @@ function readAnonId() {
5970
5970
  }
5971
5971
  function superProperties() {
5972
5972
  return {
5973
- cliVersion: true ? "2.60.69" : "0.0.0-dev",
5973
+ cliVersion: true ? "2.61.0" : "0.0.0-dev",
5974
5974
  nodeVersion: process.version,
5975
5975
  platform: process.platform,
5976
5976
  arch: process.arch,
@@ -6151,7 +6151,7 @@ var os4 = __toESM(require("os"));
6151
6151
  // package.json
6152
6152
  var package_default = {
6153
6153
  name: "codeam-cli",
6154
- version: "2.60.69",
6154
+ version: "2.61.0",
6155
6155
  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.",
6156
6156
  type: "commonjs",
6157
6157
  main: "dist/index.js",
@@ -7294,7 +7294,7 @@ var CommandRelayService = class _CommandRelayService {
7294
7294
  // fresh + clear the "CLI update available" banner after a self-update
7295
7295
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7296
7296
  // pair/reconnect). Older backends ignore the extra field.
7297
- ..."2.60.69" ? { ideVersion: "2.60.69" } : {}
7297
+ ..."2.61.0" ? { ideVersion: "2.61.0" } : {}
7298
7298
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7299
7299
  }
7300
7300
  /**
@@ -16494,10 +16494,16 @@ async function postJson(pathname, body) {
16494
16494
  }
16495
16495
  return json.data;
16496
16496
  }
16497
+ function resolveHostLabel(label) {
16498
+ const explicit = label?.trim();
16499
+ const envLabel = process.env.CODEAM_HOST_LABEL?.trim();
16500
+ const resolved = explicit || envLabel || os30.hostname();
16501
+ return resolved.slice(0, 80);
16502
+ }
16497
16503
  async function redeemEnrollToken(token, label) {
16498
16504
  const data = await postJson("/api/self-hosted/redeem", {
16499
16505
  token,
16500
- ...label ? { label } : {},
16506
+ label: resolveHostLabel(label),
16501
16507
  osInfo: collectOsInfo()
16502
16508
  });
16503
16509
  return {
@@ -17871,7 +17877,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17871
17877
  if (process.env.NODE_ENV === "test") return;
17872
17878
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17873
17879
  if (process.env.CI) return;
17874
- const current = true ? "2.60.69" : null;
17880
+ const current = true ? "2.61.0" : null;
17875
17881
  if (!current) return;
17876
17882
  const cache = readCache();
17877
17883
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17888,7 +17894,7 @@ function checkForUpdates() {
17888
17894
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17889
17895
  if (process.env.CI) return;
17890
17896
  if (!process.stdout.isTTY) return;
17891
- const current = true ? "2.60.69" : null;
17897
+ const current = true ? "2.61.0" : null;
17892
17898
  if (!current) return;
17893
17899
  const cache = readCache();
17894
17900
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17908,7 +17914,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
17908
17914
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
17909
17915
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
17910
17916
  function currentCliVersion() {
17911
- return true ? "2.60.69" : null;
17917
+ return true ? "2.61.0" : null;
17912
17918
  }
17913
17919
  function runCmd(cmd, args2, timeoutMs) {
17914
17920
  return new Promise((resolve7) => {
@@ -18136,6 +18142,78 @@ function isDeployPayload(p2) {
18136
18142
  function isStopPayload(p2) {
18137
18143
  return typeof p2.sessionId === "string";
18138
18144
  }
18145
+ var FLEET_CONTAINER_NAME_RE = /^codeam-box-[a-z0-9]+$/;
18146
+ function isFleetContainerName(v) {
18147
+ return typeof v === "string" && FLEET_CONTAINER_NAME_RE.test(v);
18148
+ }
18149
+ function isFiniteNumber(v) {
18150
+ return typeof v === "number" && Number.isFinite(v);
18151
+ }
18152
+ function isFleetLimits(v) {
18153
+ if (typeof v !== "object" || v === null) return false;
18154
+ const l = v;
18155
+ return isFiniteNumber(l.memoryMb) && isFiniteNumber(l.cpus) && isFiniteNumber(l.pidsLimit) && isFiniteNumber(l.diskGb);
18156
+ }
18157
+ function isFleetCreateBoxPayload(p2) {
18158
+ return typeof p2.boxId === "string" && isFleetContainerName(p2.containerName) && typeof p2.enrollToken === "string" && p2.enrollToken.length > 0 && typeof p2.apiOrigin === "string" && p2.apiOrigin.length > 0 && isFleetLimits(p2.limits);
18159
+ }
18160
+ function isFleetBoxRefPayload(p2) {
18161
+ if (typeof p2.boxId !== "string") return false;
18162
+ if (!isFleetContainerName(p2.containerName)) return false;
18163
+ if (p2.removeVolume !== void 0 && typeof p2.removeVolume !== "boolean") return false;
18164
+ return true;
18165
+ }
18166
+ function fleetUserIdFromContainerName(containerName) {
18167
+ return containerName.slice("codeam-box-".length);
18168
+ }
18169
+ function isMissingContainerError(stderr) {
18170
+ return /no such container/i.test(stderr);
18171
+ }
18172
+ function isMissingVolumeError(stderr) {
18173
+ return /no such volume/i.test(stderr);
18174
+ }
18175
+ function resolveFleetBoxImage() {
18176
+ return process.env.CODEAM_FLEET_BOX_IMAGE || "ghcr.io/edgar-durand/codeam-box:latest";
18177
+ }
18178
+ var DOCKER_RUN_TIMEOUT_MS = 12e4;
18179
+ var defaultDockerRunner = {
18180
+ run(args2, opts = {}) {
18181
+ return new Promise((resolve7) => {
18182
+ const child = (0, import_node_child_process23.spawn)("docker", args2, {
18183
+ stdio: ["ignore", "pipe", "pipe"],
18184
+ env: { ...process.env, ...opts.env }
18185
+ });
18186
+ let stdoutBuf = "";
18187
+ let stderrBuf = "";
18188
+ let settled = false;
18189
+ const timer = setTimeout(
18190
+ () => {
18191
+ if (settled) return;
18192
+ killQuiet(child);
18193
+ },
18194
+ opts.timeoutMs ?? DOCKER_RUN_TIMEOUT_MS
18195
+ );
18196
+ const done = (code) => {
18197
+ if (settled) return;
18198
+ settled = true;
18199
+ clearTimeout(timer);
18200
+ resolve7({ code, stderr: stderrBuf, stdout: stdoutBuf });
18201
+ };
18202
+ child.stdout?.on("data", (b) => {
18203
+ stdoutBuf += b.toString();
18204
+ });
18205
+ child.stderr?.on("data", (b) => {
18206
+ stderrBuf += b.toString();
18207
+ });
18208
+ child.once("error", (err) => {
18209
+ stderrBuf += stderrBuf ? `
18210
+ ${err.message}` : err.message;
18211
+ done(null);
18212
+ });
18213
+ child.once("exit", (code) => done(code));
18214
+ });
18215
+ }
18216
+ };
18139
18217
  var CONTROL_AGENT_META = {
18140
18218
  id: "claude",
18141
18219
  displayName: "CodeAgent Host Agent",
@@ -18177,6 +18255,7 @@ var HostAgentSupervisor = class {
18177
18255
  this.teardownHeadroom = deps.teardownHeadroom ?? defaultTeardownHeadroom;
18178
18256
  this.selfUpdate = deps.selfUpdate ?? runSelfUpdate;
18179
18257
  this.onUpdated = deps.onUpdated ?? defaultOnUpdated;
18258
+ this.docker = deps.docker ?? defaultDockerRunner;
18180
18259
  }
18181
18260
  identity;
18182
18261
  deps;
@@ -18213,6 +18292,8 @@ var HostAgentSupervisor = class {
18213
18292
  /** Best-effort systemd de-provision used by `self_hosted_wipe`. */
18214
18293
  disableService;
18215
18294
  teardownHeadroom;
18295
+ /** Docker runner for the fleet `fleet_*` control-plane handlers. */
18296
+ docker;
18216
18297
  /** Guards against firing the self-heal more than once. */
18217
18298
  healing = false;
18218
18299
  /**
@@ -18380,6 +18461,38 @@ var HostAgentSupervisor = class {
18380
18461
  * arbitrary command surface).
18381
18462
  */
18382
18463
  async handleCommand(cmd) {
18464
+ if (cmd.type === "fleet_create_box") {
18465
+ if (!isFleetCreateBoxPayload(cmd.payload)) {
18466
+ log.warn("host-agent", `ignoring malformed fleet_create_box id=${cmd.id}`);
18467
+ return;
18468
+ }
18469
+ await this.fleetCreateBox(cmd.payload);
18470
+ return;
18471
+ }
18472
+ if (cmd.type === "fleet_start_box") {
18473
+ if (!isFleetBoxRefPayload(cmd.payload)) {
18474
+ log.warn("host-agent", `ignoring malformed fleet_start_box id=${cmd.id}`);
18475
+ return;
18476
+ }
18477
+ await this.fleetStartBox(cmd.payload);
18478
+ return;
18479
+ }
18480
+ if (cmd.type === "fleet_stop_box") {
18481
+ if (!isFleetBoxRefPayload(cmd.payload)) {
18482
+ log.warn("host-agent", `ignoring malformed fleet_stop_box id=${cmd.id}`);
18483
+ return;
18484
+ }
18485
+ await this.fleetStopBox(cmd.payload);
18486
+ return;
18487
+ }
18488
+ if (cmd.type === "fleet_delete_box") {
18489
+ if (!isFleetBoxRefPayload(cmd.payload)) {
18490
+ log.warn("host-agent", `ignoring malformed fleet_delete_box id=${cmd.id}`);
18491
+ return;
18492
+ }
18493
+ await this.fleetDeleteBox(cmd.payload);
18494
+ return;
18495
+ }
18383
18496
  if (cmd.type === "self_hosted_deploy") {
18384
18497
  if (!isDeployPayload(cmd.payload)) {
18385
18498
  log.warn("host-agent", `ignoring malformed self_hosted_deploy id=${cmd.id}`);
@@ -18417,6 +18530,137 @@ var HostAgentSupervisor = class {
18417
18530
  }
18418
18531
  log.trace("host-agent", `ignoring unsupported command type=${cmd.type}`);
18419
18532
  }
18533
+ /**
18534
+ * `fleet_create_box` — `docker run` a per-user rescue box. Builds the FULL
18535
+ * argv from a fixed template (Global Constraints — nothing from the wire
18536
+ * is passed through as a raw docker argument beyond the already-validated
18537
+ * `containerName` and the numeric resource limits): hard isolation
18538
+ * (`--cap-drop ALL`, `--security-opt no-new-privileges`, resource caps,
18539
+ * the isolated `fleet-net` network, a SINGLE named volume mounted at
18540
+ * `/home/box` — named identically to the container, per the Global
18541
+ * Constraints — and the ops labels). NEVER `--privileged`, NEVER a
18542
+ * `docker.sock` mount, NEVER a host bind mount. The api origin (not a
18543
+ * secret) is delivered via a normal `-e KEY=value`. The enroll token IS a
18544
+ * secret — spec invariant #1 is "token via env, NEVER argv" — so it is
18545
+ * delivered as a BARE `-e CODEAM_ENROLL_TOKEN` (no `=value`) with the
18546
+ * actual value passed only through {@link DockerRunner.run}'s `env` map,
18547
+ * which the runner merges into the `docker` CLI process's OWN env; docker
18548
+ * then reads a bare `-e NAME` from its own env and forwards it into the
18549
+ * container. The value never appears in the `docker run` argv, so it's
18550
+ * never visible via `ps` on the shared fleet host; never logged either.
18551
+ */
18552
+ async fleetCreateBox(payload) {
18553
+ const { containerName, limits } = payload;
18554
+ const userId = fleetUserIdFromContainerName(containerName);
18555
+ log.info(
18556
+ "host-agent",
18557
+ `fleet_create_box id=${payload.boxId} name=${containerName} mem=${limits.memoryMb}m cpus=${limits.cpus} pids=${limits.pidsLimit}`
18558
+ );
18559
+ const args2 = [
18560
+ "run",
18561
+ "-d",
18562
+ "--name",
18563
+ containerName,
18564
+ "--cap-drop",
18565
+ "ALL",
18566
+ "--security-opt",
18567
+ "no-new-privileges",
18568
+ "--memory",
18569
+ `${limits.memoryMb}m`,
18570
+ "--cpus",
18571
+ String(limits.cpus),
18572
+ "--pids-limit",
18573
+ String(limits.pidsLimit),
18574
+ "--network",
18575
+ "fleet-net",
18576
+ "--label",
18577
+ `com.codeagent.user-id=${userId}`,
18578
+ "--label",
18579
+ `com.codeagent.box-id=${payload.boxId}`,
18580
+ "--label",
18581
+ "com.codeagent.created-by=fleet",
18582
+ "-v",
18583
+ `${containerName}:/home/box`,
18584
+ // Bare `-e NAME` (no `=value`) — docker reads the value from ITS OWN
18585
+ // process env (supplied below via `opts.env`), never from this argv.
18586
+ "-e",
18587
+ "CODEAM_ENROLL_TOKEN",
18588
+ "-e",
18589
+ `CODEAM_API_URL=${payload.apiOrigin}`,
18590
+ // User-facing label for the rescued box's "My Servers" card. Not a
18591
+ // secret → plain `-e KEY=value`. The box's host-agent reads it at
18592
+ // redeem (resolveHostLabel) so a rescued user sees "CodeAgent Box",
18593
+ // not the generic "my-server" or the ugly container cuid.
18594
+ "-e",
18595
+ "CODEAM_HOST_LABEL=CodeAgent Box",
18596
+ resolveFleetBoxImage()
18597
+ ];
18598
+ const res = await this.docker.run(args2, {
18599
+ timeoutMs: DOCKER_RUN_TIMEOUT_MS,
18600
+ env: { CODEAM_ENROLL_TOKEN: payload.enrollToken }
18601
+ });
18602
+ if (res.code === 0) {
18603
+ log.info(
18604
+ "host-agent",
18605
+ `fleet box ${containerName} created (${res.stdout.trim().slice(0, 12)})`
18606
+ );
18607
+ } else {
18608
+ log.warn(
18609
+ "host-agent",
18610
+ `fleet_create_box ${containerName} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
18611
+ );
18612
+ }
18613
+ }
18614
+ /** `fleet_start_box` — wake a sleeping box (`docker start`). Idempotent: a
18615
+ * container the host already removed is treated as success. */
18616
+ async fleetStartBox(payload) {
18617
+ log.info("host-agent", `fleet_start_box id=${payload.boxId} name=${payload.containerName}`);
18618
+ const res = await this.docker.run(["start", payload.containerName]);
18619
+ if (res.code !== 0 && !isMissingContainerError(res.stderr)) {
18620
+ log.warn(
18621
+ "host-agent",
18622
+ `fleet_start_box ${payload.containerName} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
18623
+ );
18624
+ }
18625
+ }
18626
+ /** `fleet_stop_box` — sleep an idle box (`docker stop`). MUST be
18627
+ * idempotent — the backend's reap sweeps may re-send a stop for a box
18628
+ * the host already stopped/removed; "No such container" is success. */
18629
+ async fleetStopBox(payload) {
18630
+ log.info("host-agent", `fleet_stop_box id=${payload.boxId} name=${payload.containerName}`);
18631
+ const res = await this.docker.run(["stop", payload.containerName]);
18632
+ if (res.code !== 0 && !isMissingContainerError(res.stderr)) {
18633
+ log.warn(
18634
+ "host-agent",
18635
+ `fleet_stop_box ${payload.containerName} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
18636
+ );
18637
+ }
18638
+ }
18639
+ /** `fleet_delete_box` — `docker rm -f`, plus `docker volume rm` when the
18640
+ * backend asks for a reap (`removeVolume`). MUST be idempotent — deleting
18641
+ * an already-gone container/volume is success, never a failure. */
18642
+ async fleetDeleteBox(payload) {
18643
+ log.info(
18644
+ "host-agent",
18645
+ `fleet_delete_box id=${payload.boxId} name=${payload.containerName} removeVolume=${Boolean(payload.removeVolume)}`
18646
+ );
18647
+ const res = await this.docker.run(["rm", "-f", payload.containerName]);
18648
+ if (res.code !== 0 && !isMissingContainerError(res.stderr)) {
18649
+ log.warn(
18650
+ "host-agent",
18651
+ `fleet_delete_box ${payload.containerName} failed (code=${res.code}): ${res.stderr.trim().slice(-300)}`
18652
+ );
18653
+ }
18654
+ if (payload.removeVolume) {
18655
+ const volRes = await this.docker.run(["volume", "rm", payload.containerName]);
18656
+ if (volRes.code !== 0 && !isMissingVolumeError(volRes.stderr)) {
18657
+ log.warn(
18658
+ "host-agent",
18659
+ `fleet_delete_box volume rm ${payload.containerName} failed (code=${volRes.code}): ${volRes.stderr.trim().slice(-300)}`
18660
+ );
18661
+ }
18662
+ }
18663
+ }
18420
18664
  /**
18421
18665
  * Prepare the workspace, write the agent credential (same as codespace
18422
18666
  * provisioning), and spawn a supervised `codeam pair-auto` child.
@@ -19604,8 +19848,8 @@ function isJsInstallCommand(cmd, args2) {
19604
19848
 
19605
19849
  // src/services/preview/tunnel-bringup.ts
19606
19850
  var TUNNEL_REGISTERED_RE = /Registered tunnel connection/i;
19607
- async function awaitTunnelRegistered(candidate, hostname3, deadlineMs) {
19608
- let url = hostname3 ? `https://${hostname3}` : null;
19851
+ async function awaitTunnelRegistered(candidate, hostname4, deadlineMs) {
19852
+ let url = hostname4 ? `https://${hostname4}` : null;
19609
19853
  let registered = false;
19610
19854
  if (candidate.exitCode != null) {
19611
19855
  return { kind: "exited", sawUrl: url != null, code: candidate.exitCode };
@@ -35337,7 +35581,7 @@ function checkChokidar() {
35337
35581
  }
35338
35582
  async function doctor(args2 = []) {
35339
35583
  const json = args2.includes("--json");
35340
- const cliVersion = true ? "2.60.69" : "0.0.0-dev";
35584
+ const cliVersion = true ? "2.61.0" : "0.0.0-dev";
35341
35585
  const apiBase2 = resolveApiBaseUrl();
35342
35586
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
35343
35587
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -35848,7 +36092,7 @@ async function mcpRun(args2) {
35848
36092
  // src/commands/version.ts
35849
36093
  var import_picocolors15 = __toESM(require("picocolors"));
35850
36094
  function version2() {
35851
- const v = true ? "2.60.69" : "unknown";
36095
+ const v = true ? "2.61.0" : "unknown";
35852
36096
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
35853
36097
  }
35854
36098
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.60.69",
3
+ "version": "2.61.0",
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",