codeam-cli 2.39.65 → 2.39.68

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,18 @@ 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.39.66] — 2026-06-20
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Kill preview dev-server process group to stop port leaks (EADDRINUSE)
12
+
13
+ ## [2.39.65] — 2026-06-20
14
+
15
+ ### Added
16
+
17
+ - **self-hosted:** Run sessions in AUTO mode (auto-approve permissions)
18
+
7
19
  ## [2.39.64] — 2026-06-20
8
20
 
9
21
  ### Added
package/dist/index.js CHANGED
@@ -5388,7 +5388,7 @@ function readAnonId() {
5388
5388
  }
5389
5389
  function superProperties() {
5390
5390
  return {
5391
- cliVersion: true ? "2.39.65" : "0.0.0-dev",
5391
+ cliVersion: true ? "2.39.68" : "0.0.0-dev",
5392
5392
  nodeVersion: process.version,
5393
5393
  platform: process.platform,
5394
5394
  arch: process.arch,
@@ -5547,7 +5547,7 @@ var os4 = __toESM(require("os"));
5547
5547
  // package.json
5548
5548
  var package_default = {
5549
5549
  name: "codeam-cli",
5550
- version: "2.39.65",
5550
+ version: "2.39.68",
5551
5551
  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.",
5552
5552
  type: "commonjs",
5553
5553
  main: "dist/index.js",
@@ -14143,29 +14143,32 @@ var activePreviews = /* @__PURE__ */ new Map();
14143
14143
  function registerPreview(sessionId, preview) {
14144
14144
  activePreviews.set(sessionId, preview);
14145
14145
  }
14146
- async function killPreview(sessionId) {
14147
- const preview = activePreviews.get(sessionId);
14148
- if (!preview) return;
14149
- if (preview.tunnel) {
14146
+ function killProcessTree(child, signal = "SIGTERM") {
14147
+ const pid = child.pid;
14148
+ if (pid == null) return;
14149
+ if (process.platform !== "win32") {
14150
14150
  try {
14151
- preview.tunnel.kill("SIGTERM");
14151
+ process.kill(-pid, signal);
14152
+ return;
14152
14153
  } catch {
14153
14154
  }
14154
14155
  }
14155
- await new Promise((r) => setTimeout(r, 100));
14156
14156
  try {
14157
- preview.devServer.kill("SIGTERM");
14157
+ child.kill(signal);
14158
14158
  } catch {
14159
14159
  }
14160
+ }
14161
+ async function killPreview(sessionId) {
14162
+ const preview = activePreviews.get(sessionId);
14163
+ if (!preview) return;
14164
+ if (preview.tunnel) {
14165
+ killProcessTree(preview.tunnel, "SIGTERM");
14166
+ }
14167
+ await new Promise((r) => setTimeout(r, 100));
14168
+ killProcessTree(preview.devServer, "SIGTERM");
14160
14169
  const sigkillTimer = setTimeout(() => {
14161
- try {
14162
- preview.devServer.kill("SIGKILL");
14163
- } catch {
14164
- }
14165
- try {
14166
- preview.tunnel?.kill("SIGKILL");
14167
- } catch {
14168
- }
14170
+ killProcessTree(preview.devServer, "SIGKILL");
14171
+ if (preview.tunnel) killProcessTree(preview.tunnel, "SIGKILL");
14169
14172
  }, 250);
14170
14173
  sigkillTimer.unref?.();
14171
14174
  activePreviews.delete(sessionId);
@@ -16225,17 +16228,42 @@ var previewStartH = (ctx, _cmd, parsed) => {
16225
16228
  });
16226
16229
  return;
16227
16230
  }
16228
- void postPreviewEvent({
16229
- sessionId: ctx.sessionId,
16230
- pluginId: ctx.pluginId,
16231
- pluginAuthToken,
16232
- type: "preview_error",
16233
- payload: {
16234
- stage: "spawn",
16235
- message: `Port ${detection.port} is already in use by another process, so the dev server can't start there. Stop whatever is listening on port ${detection.port} and try the preview again.`
16231
+ if (raceExisting) {
16232
+ log.info(
16233
+ "preview",
16234
+ `reclaiming stale preview holding port ${detection.port} for session=${ctx.sessionId} (exit=${raceExisting.devServer.exitCode})`
16235
+ );
16236
+ await killPreview(ctx.sessionId);
16237
+ const freeDeadline = Date.now() + 4e3;
16238
+ while (await isPortListening(detection.port) && Date.now() < freeDeadline) {
16239
+ await new Promise((r) => setTimeout(r, 200));
16236
16240
  }
16237
- });
16238
- return;
16241
+ if (await isPortListening(detection.port)) {
16242
+ void postPreviewEvent({
16243
+ sessionId: ctx.sessionId,
16244
+ pluginId: ctx.pluginId,
16245
+ pluginAuthToken,
16246
+ type: "preview_error",
16247
+ payload: {
16248
+ stage: "spawn",
16249
+ message: `Port ${detection.port} is still in use after stopping the previous preview. Wait a moment and try again.`
16250
+ }
16251
+ });
16252
+ return;
16253
+ }
16254
+ } else {
16255
+ void postPreviewEvent({
16256
+ sessionId: ctx.sessionId,
16257
+ pluginId: ctx.pluginId,
16258
+ pluginAuthToken,
16259
+ type: "preview_error",
16260
+ payload: {
16261
+ stage: "spawn",
16262
+ message: `Port ${detection.port} is already in use by another process, so the dev server can't start there. Stop whatever is listening on port ${detection.port} and try the preview again.`
16263
+ }
16264
+ });
16265
+ return;
16266
+ }
16239
16267
  }
16240
16268
  const spawnable = normalizeDetectionForSpawn(detection, process.cwd());
16241
16269
  emitProgress(
@@ -16245,7 +16273,14 @@ var previewStartH = (ctx, _cmd, parsed) => {
16245
16273
  const devServer = (0, import_child_process20.spawn)(spawnable.command, spawnable.args, {
16246
16274
  cwd: process.cwd(),
16247
16275
  env: { ...process.env, ...spawnable.env ?? {} },
16248
- stdio: ["ignore", "pipe", "pipe"]
16276
+ stdio: ["ignore", "pipe", "pipe"],
16277
+ // POSIX: lead a new process group so teardown can SIGTERM the whole
16278
+ // tree. Dev servers fork worker children that bind the port; killing
16279
+ // only the direct child orphans them and leaks the port, so the next
16280
+ // preview_start hits EADDRINUSE on a port we already hold. Group-kill
16281
+ // (killProcessTree) reaps the workers too. Windows has no process
16282
+ // groups — leave detached off there (direct kill is the only option).
16283
+ detached: process.platform !== "win32"
16249
16284
  });
16250
16285
  emitProgress("BIND_PORT", String(detection.port));
16251
16286
  emitProgress("WAITING_FOR_READY", detection.ready_pattern);
@@ -16264,6 +16299,7 @@ var previewStartH = (ctx, _cmd, parsed) => {
16264
16299
  portProbe: isNextJs ? () => waitForPortListening(detection.port, { timeoutMs: 1e3, intervalMs: 250 }) : void 0
16265
16300
  });
16266
16301
  if (outcome.kind === "exited") {
16302
+ killProcessTree(devServer, "SIGTERM");
16267
16303
  void postPreviewEvent({
16268
16304
  sessionId: ctx.sessionId,
16269
16305
  pluginId: ctx.pluginId,
@@ -16278,10 +16314,7 @@ var previewStartH = (ctx, _cmd, parsed) => {
16278
16314
  return;
16279
16315
  }
16280
16316
  if (outcome.kind === "timeout") {
16281
- try {
16282
- devServer.kill("SIGTERM");
16283
- } catch {
16284
- }
16317
+ killProcessTree(devServer, "SIGTERM");
16285
16318
  void postPreviewEvent({
16286
16319
  sessionId: ctx.sessionId,
16287
16320
  pluginId: ctx.pluginId,
@@ -16310,10 +16343,7 @@ var previewStartH = (ctx, _cmd, parsed) => {
16310
16343
  }
16311
16344
  }
16312
16345
  if (!expoUrl) {
16313
- try {
16314
- devServer.kill("SIGTERM");
16315
- } catch {
16316
- }
16346
+ killProcessTree(devServer, "SIGTERM");
16317
16347
  void postPreviewEvent({
16318
16348
  sessionId: ctx.sessionId,
16319
16349
  pluginId: ctx.pluginId,
@@ -16329,10 +16359,7 @@ var previewStartH = (ctx, _cmd, parsed) => {
16329
16359
  try {
16330
16360
  bin = await resolveCloudflared();
16331
16361
  } catch (e) {
16332
- try {
16333
- devServer.kill("SIGTERM");
16334
- } catch {
16335
- }
16362
+ killProcessTree(devServer, "SIGTERM");
16336
16363
  void postPreviewEvent({
16337
16364
  sessionId: ctx.sessionId,
16338
16365
  pluginId: ctx.pluginId,
@@ -16426,10 +16453,7 @@ var previewStartH = (ctx, _cmd, parsed) => {
16426
16453
  }
16427
16454
  }
16428
16455
  if (!parsedUrl) {
16429
- try {
16430
- devServer.kill("SIGTERM");
16431
- } catch {
16432
- }
16456
+ killProcessTree(devServer, "SIGTERM");
16433
16457
  void postPreviewEvent({
16434
16458
  sessionId: ctx.sessionId,
16435
16459
  pluginId: ctx.pluginId,
@@ -17367,7 +17391,7 @@ function checkForUpdates() {
17367
17391
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17368
17392
  if (process.env.CI) return;
17369
17393
  if (!process.stdout.isTTY) return;
17370
- const current = true ? "2.39.65" : null;
17394
+ const current = true ? "2.39.68" : null;
17371
17395
  if (!current) return;
17372
17396
  const cache = readCache();
17373
17397
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17772,7 +17796,7 @@ var defaultSpawner = (env, cwd, args2 = []) => (0, import_node_child_process13.s
17772
17796
  detached: false
17773
17797
  });
17774
17798
  function currentCliVersion() {
17775
- return true ? "2.39.65" : null;
17799
+ return true ? "2.39.68" : null;
17776
17800
  }
17777
17801
  function runCmd(cmd, args2, timeoutMs) {
17778
17802
  return new Promise((resolve7) => {
@@ -17843,6 +17867,20 @@ var defaultDisableService = () => {
17843
17867
  } catch {
17844
17868
  }
17845
17869
  };
17870
+ var defaultTeardownHeadroom = () => {
17871
+ try {
17872
+ const kind = JSON.parse(fs39.readFileSync(headroomConfigPath(), "utf8")).agent;
17873
+ if (kind) {
17874
+ (0, import_node_child_process13.execFileSync)("headroom", ["unwrap", kind], { stdio: "ignore", timeout: 15e3 });
17875
+ }
17876
+ } catch {
17877
+ }
17878
+ try {
17879
+ (0, import_node_child_process13.execFileSync)("pkill", ["-TERM", "-f", "headroom.*proxy"], { stdio: "ignore" });
17880
+ } catch {
17881
+ }
17882
+ persistHeadroomConfig({ enabled: false });
17883
+ };
17846
17884
  var HostAgentSupervisor = class {
17847
17885
  constructor(identity, deps = {}) {
17848
17886
  this.identity = identity;
@@ -17853,6 +17891,7 @@ var HostAgentSupervisor = class {
17853
17891
  this.metrics = deps.metricsCollector ?? new MetricsCollector();
17854
17892
  this.onIdentityRejected = deps.onIdentityRejected ?? defaultOnIdentityRejected;
17855
17893
  this.disableService = deps.disableService ?? defaultDisableService;
17894
+ this.teardownHeadroom = deps.teardownHeadroom ?? defaultTeardownHeadroom;
17856
17895
  this.selfUpdate = deps.selfUpdate ?? runSelfUpdate;
17857
17896
  this.onUpdated = deps.onUpdated ?? defaultOnUpdated;
17858
17897
  }
@@ -17886,6 +17925,7 @@ var HostAgentSupervisor = class {
17886
17925
  onIdentityRejected;
17887
17926
  /** Best-effort systemd de-provision used by `self_hosted_wipe`. */
17888
17927
  disableService;
17928
+ teardownHeadroom;
17889
17929
  /** Guards against firing the self-heal more than once. */
17890
17930
  healing = false;
17891
17931
  /**
@@ -18083,6 +18123,7 @@ var HostAgentSupervisor = class {
18083
18123
  if (cmd.type === "self_hosted_wipe") {
18084
18124
  log.warn("host-agent", `self_hosted_wipe received id=${cmd.id} \u2014 de-provisioning`);
18085
18125
  this.stop();
18126
+ this.teardownHeadroom();
18086
18127
  this.disableService();
18087
18128
  if (!this.healing) {
18088
18129
  this.healing = true;
@@ -21771,6 +21812,13 @@ var AcpClient = class {
21771
21812
  if (!this.connection) {
21772
21813
  throw new Error("AcpClient.loadSession called before start()");
21773
21814
  }
21815
+ if (sessionId === this.sessionId) {
21816
+ log.info(
21817
+ "acpClient",
21818
+ `loadSession skipped \u2014 already the active session (${sessionId.slice(0, 8)})`
21819
+ );
21820
+ return;
21821
+ }
21774
21822
  log.info("acpClient", `loadSession \u2192 sessionId=${sessionId.slice(0, 8)}`);
21775
21823
  await this.connection.loadSession({
21776
21824
  sessionId,
@@ -28387,7 +28435,7 @@ function checkChokidar() {
28387
28435
  }
28388
28436
  async function doctor(args2 = []) {
28389
28437
  const json = args2.includes("--json");
28390
- const cliVersion = true ? "2.39.65" : "0.0.0-dev";
28438
+ const cliVersion = true ? "2.39.68" : "0.0.0-dev";
28391
28439
  const apiBase2 = resolveApiBaseUrl();
28392
28440
  const diagnosticId = (0, import_node_crypto8.randomUUID)();
28393
28441
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -28586,7 +28634,7 @@ async function completion(args2) {
28586
28634
  // src/commands/version.ts
28587
28635
  var import_picocolors14 = __toESM(require("picocolors"));
28588
28636
  function version2() {
28589
- const v = true ? "2.39.65" : "unknown";
28637
+ const v = true ? "2.39.68" : "unknown";
28590
28638
  console.log(`${import_picocolors14.default.bold("codeam-cli")} ${import_picocolors14.default.cyan(v)}`);
28591
28639
  }
28592
28640
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.39.65",
3
+ "version": "2.39.68",
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",