codeam-cli 2.71.1 → 2.72.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 +10 -0
  2. package/dist/index.js +165 -19
  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.71.1] — 2026-08-30
8
+
9
+ ### Added
10
+
11
+ - **cli:** Reportar en qué fase va cada turno — la mitad del embudo que el servidor no ve (#670)
12
+
13
+ ### Tests
14
+
15
+ - **cli:** Dar plazo realista a los dos tests que conducen start() de verdad (#669)
16
+
7
17
  ## [2.71.0] — 2026-08-29
8
18
 
9
19
  ### Added
package/dist/index.js CHANGED
@@ -2909,6 +2909,17 @@ var USER_EVENTS = {
2909
2909
  SELF_HOSTED_HOST_STATUS: "self_hosted_host_status",
2910
2910
  SELF_HOSTED_HOST_REMOVED: "self_hosted_host_removed",
2911
2911
  SELF_HOSTED_HOST_TELEMETRY: "self_hosted_host_telemetry",
2912
+ /**
2913
+ * Una orden que NINGUN CLI recogio y que expiro de la cola.
2914
+ *
2915
+ * La cola `commands:<pluginId>` caduca a los 600 s y Redis no avisa a nadie:
2916
+ * la tarea deja de existir en silencio. Medido el 2026-08-30 en un usuario
2917
+ * real, SIETE de sus 17 `start_task` acabaron asi — no recibio nada y no
2918
+ * habia forma de saber que se habian descartado. Lo publica el barrido
2919
+ * `command-expiry-sweep` del backend; nadie puede notificar su propio
2920
+ * silencio.
2921
+ */
2922
+ COMMAND_EXPIRED: "command_expired",
2912
2923
  SELF_HOSTED_HOST_METRICS: "self_hosted_host_metrics",
2913
2924
  SELF_HOSTED_HOST_SESSIONS: "self_hosted_host_sessions",
2914
2925
  SELF_HOSTED_DEPLOY_PROGRESS: "self_hosted_deploy_progress",
@@ -8081,7 +8092,7 @@ function readAnonId() {
8081
8092
  }
8082
8093
  function superProperties() {
8083
8094
  return {
8084
- cliVersion: true ? "2.71.1" : "0.0.0-dev",
8095
+ cliVersion: true ? "2.72.0" : "0.0.0-dev",
8085
8096
  nodeVersion: process.version,
8086
8097
  platform: process.platform,
8087
8098
  arch: process.arch,
@@ -8323,7 +8334,7 @@ var http = __toESM(require("http"));
8323
8334
  // package.json
8324
8335
  var package_default = {
8325
8336
  name: "codeam-cli",
8326
- version: "2.71.1",
8337
+ version: "2.72.0",
8327
8338
  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.",
8328
8339
  type: "commonjs",
8329
8340
  main: "dist/index.js",
@@ -9489,9 +9500,14 @@ var CommandRelayService = class _CommandRelayService {
9489
9500
  // evicted (a command that old is long gone from the server queue anyway).
9490
9501
  processedIds = /* @__PURE__ */ new Set();
9491
9502
  static PROCESSED_ID_CAP = 1e3;
9503
+ /** After the first, re-warn every Nth consecutive ack failure — loud enough
9504
+ * to notice, quiet enough not to drown the log during a network outage. */
9505
+ static ACK_WARN_EVERY = 20;
9492
9506
  /** Polling backoff state (only used on the fallback). */
9493
9507
  pollTimer = null;
9494
9508
  pollFailures = 0;
9509
+ /** Consecutive ack POST failures — drives the visibility escalation below. */
9510
+ ackFailures = 0;
9495
9511
  // Successive polls that returned no commands. Drives an idle
9496
9512
  // backoff so a CLI sitting on the polling fallback (SSE was
9497
9513
  // unavailable) doesn't keep hammering the API every 2 s when
@@ -9856,13 +9872,49 @@ var CommandRelayService = class _CommandRelayService {
9856
9872
  }
9857
9873
  }
9858
9874
  /** Confirm receipt of command ids so the backend removes them from the queue
9859
- * (the at-least-once delivery guarantee). Best-effort — never throws. */
9875
+ * (the at-least-once delivery guarantee). Best-effort — never throws.
9876
+ *
9877
+ * ⚠️ A ONE-OFF failure here is harmless (the command redelivers and we dedupe),
9878
+ * but a PERSISTENT one is not: the queue never drains, so every batch is
9879
+ * redelivered forever and the host looks perfectly healthy from outside —
9880
+ * heartbeats land, commands still arrive, `status` stays `online`. That is
9881
+ * exactly how the fleet host ran from 2026-07-15 to 2026-08-31 with NO poll
9882
+ * secret enrolled, 401-ing every single ack, with nobody the wiser: the
9883
+ * failure lived in `log.trace`, below the level anyone reads.
9884
+ *
9885
+ * So: keep it best-effort, but make a SUSTAINED failure impossible to miss,
9886
+ * and name `PLUGIN_SECRET_REQUIRED` explicitly — it is not a network blip,
9887
+ * it is a host that must be re-enrolled and will never recover on its own.
9888
+ */
9860
9889
  ackCommands(ids) {
9861
9890
  const commandIds = ids.filter((id) => typeof id === "string" && id.length > 0);
9862
9891
  if (commandIds.length === 0) return;
9863
9892
  void _postJson(`${API_BASE2}/api/commands/ack`, { pluginId: this.pluginId, commandIds }, {
9864
9893
  ...this.pollSecretHeader()
9865
- }).catch((err) => log.trace("relay", "ack post failed (will redeliver+dedupe)", err));
9894
+ }).then(() => {
9895
+ if (this.ackFailures > 0) {
9896
+ log.warn("relay", `ack recovered after ${this.ackFailures} consecutive failure(s)`);
9897
+ this.ackFailures = 0;
9898
+ }
9899
+ }).catch((err) => {
9900
+ this.ackFailures += 1;
9901
+ const reason = err instanceof Error ? err.message : String(err);
9902
+ if (/PLUGIN_SECRET_REQUIRED|INVALID_PLUGIN_SECRET/.test(reason)) {
9903
+ log.warn(
9904
+ "relay",
9905
+ `ack REJECTED (${reason}) \u2014 this host must be re-paired; retrying will never fix it. Delivery still works while this client drains on receive, but the ack rail is dead and ack-mode would stall here.`
9906
+ );
9907
+ return;
9908
+ }
9909
+ if (this.ackFailures === 1 || this.ackFailures % _CommandRelayService.ACK_WARN_EVERY === 0) {
9910
+ log.warn(
9911
+ "relay",
9912
+ `ack failed ${this.ackFailures}x in a row \u2014 the backend queue is not draining: ${reason}`
9913
+ );
9914
+ return;
9915
+ }
9916
+ log.trace("relay", "ack post failed (will redeliver+dedupe)", err);
9917
+ });
9866
9918
  }
9867
9919
  // ─── Heartbeat + agents ──────────────────────────────────────────
9868
9920
  async sendHeartbeat(online) {
@@ -9875,7 +9927,7 @@ var CommandRelayService = class _CommandRelayService {
9875
9927
  // fresh + clear the "CLI update available" banner after a self-update
9876
9928
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9877
9929
  // pair/reconnect). Older backends ignore the extra field.
9878
- ..."2.71.1" ? { ideVersion: "2.71.1" } : {}
9930
+ ..."2.72.0" ? { ideVersion: "2.72.0" } : {}
9879
9931
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9880
9932
  }
9881
9933
  /**
@@ -11475,6 +11527,13 @@ var startCommandSchema = import_zod.z.object({
11475
11527
  value: import_zod.z.string().max(32768)
11476
11528
  })
11477
11529
  ).max(512).optional(),
11530
+ // `env_parse` — how a pasted `.env` blob meets the project's current one.
11531
+ // `merge` (the default when absent) keeps variables the paste does not
11532
+ // mention, because a PARTIAL paste is the common case: people copy the block
11533
+ // they care about, not the whole file. `replace` is opt-in and destructive
11534
+ // in intent, so it must be asked for explicitly. The blob itself rides the
11535
+ // existing `content` field.
11536
+ mode: import_zod.z.enum(["merge", "replace"]).optional(),
11478
11537
  // `skills_configure` — the curated `SkillId` to add/remove. `list` (and a
11479
11538
  // malformed/unknown id on add/remove) sends no `skillId` or an invalid one;
11480
11539
  // `configureSkill` itself validates against the shared registry.
@@ -22134,7 +22193,7 @@ async function autoUpgradeBeforeCriticalCommand() {
22134
22193
  if (process.env.NODE_ENV === "test") return;
22135
22194
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22136
22195
  if (process.env.CI) return;
22137
- const current2 = true ? "2.71.1" : null;
22196
+ const current2 = true ? "2.72.0" : null;
22138
22197
  if (!current2) return;
22139
22198
  const cache = readCache();
22140
22199
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22151,7 +22210,7 @@ function checkForUpdates() {
22151
22210
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22152
22211
  if (process.env.CI) return;
22153
22212
  if (!process.stdout.isTTY) return;
22154
- const current2 = true ? "2.71.1" : null;
22213
+ const current2 = true ? "2.72.0" : null;
22155
22214
  if (!current2) return;
22156
22215
  const cache = readCache();
22157
22216
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22174,7 +22233,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
22174
22233
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
22175
22234
  var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
22176
22235
  function currentCliVersion() {
22177
- return true ? "2.71.1" : null;
22236
+ return true ? "2.72.0" : null;
22178
22237
  }
22179
22238
  async function installedVersion(deps) {
22180
22239
  const ls = await deps.run(
@@ -24725,6 +24784,14 @@ var REQUIRED_FIELDS2 = [
24725
24784
  ];
24726
24785
  function safeParseDetection(raw) {
24727
24786
  if (!raw) return null;
24787
+ const parsed = parseDetectionObject(raw);
24788
+ if (!parsed) return null;
24789
+ for (const field of REQUIRED_FIELDS2) {
24790
+ if (!(field in parsed)) return null;
24791
+ }
24792
+ return parsed;
24793
+ }
24794
+ function parseDetectionObject(raw) {
24728
24795
  let parsed = tryParseObject(raw.trim());
24729
24796
  if (!parsed) {
24730
24797
  const stripped = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
@@ -24736,12 +24803,7 @@ function safeParseDetection(raw) {
24736
24803
  const candidate = extractFirstJsonObject(raw);
24737
24804
  if (candidate) parsed = tryParseObject(candidate);
24738
24805
  }
24739
- if (!parsed) return null;
24740
- const obj = parsed;
24741
- for (const field of REQUIRED_FIELDS2) {
24742
- if (!(field in obj)) return null;
24743
- }
24744
- return obj;
24806
+ return parsed ?? null;
24745
24807
  }
24746
24808
  function tryParseObject(s) {
24747
24809
  try {
@@ -24793,6 +24855,38 @@ function parseExpoUrl(stdout) {
24793
24855
  const match = stdout.match(EXPO_URL_RE);
24794
24856
  return match ? match[0] : null;
24795
24857
  }
24858
+ var RAW_EXCERPT_LIMIT = 1e3;
24859
+ function describeDetectionFailure(raw) {
24860
+ const text = raw?.trim() ?? "";
24861
+ const rawExcerpt = text.slice(0, RAW_EXCERPT_LIMIT);
24862
+ if (!text) {
24863
+ return {
24864
+ reason: "no_output",
24865
+ message: "The agent didn't return anything for this project. Try again, or add a .codeam/preview.json override.",
24866
+ rawExcerpt
24867
+ };
24868
+ }
24869
+ const parsed = parseDetectionObject(text);
24870
+ if (!parsed) {
24871
+ return {
24872
+ reason: "no_json",
24873
+ message: "The agent replied without a JSON block. Try again, or add a .codeam/preview.json override.",
24874
+ rawExcerpt
24875
+ };
24876
+ }
24877
+ const missing = REQUIRED_FIELDS2.filter((f) => !(f in parsed)).map(String);
24878
+ if (missing.length > 0) {
24879
+ return {
24880
+ reason: "missing_fields",
24881
+ // Decir CUALES: es lo unico con lo que alguien puede escribir el
24882
+ // override a mano y desbloquearse sin esperar a nadie.
24883
+ message: `The agent's answer is missing ${missing.join(", ")}. Try again, or add a .codeam/preview.json override.`,
24884
+ missing,
24885
+ rawExcerpt
24886
+ };
24887
+ }
24888
+ return null;
24889
+ }
24796
24890
 
24797
24891
  // src/services/preview/port-ready.ts
24798
24892
  var net2 = __toESM(require("net"));
@@ -28126,6 +28220,44 @@ var envReadH = async (ctx, cmd) => {
28126
28220
  await ctx.relay.sendResult(cmd.id, "failed", { error: err.message });
28127
28221
  }
28128
28222
  };
28223
+ var envParseH = async (ctx, cmd, parsed) => {
28224
+ const content = typeof parsed.content === "string" ? parsed.content : null;
28225
+ if (content === null) {
28226
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing content" });
28227
+ return;
28228
+ }
28229
+ const replace = parsed.mode === "replace";
28230
+ const incoming = /* @__PURE__ */ new Map();
28231
+ for (const v of parseDotenv(content)) incoming.set(v.key, v.value);
28232
+ const distinctAssignments = new Set(
28233
+ content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#") && l.includes("=")).map((l) => l.startsWith("export ") ? l.slice("export ".length).trim() : l).map((l) => l.slice(0, l.indexOf("=")).trim())
28234
+ );
28235
+ const unrecognized = Math.max(0, distinctAssignments.size - incoming.size);
28236
+ const existing = /* @__PURE__ */ new Map();
28237
+ if (!replace) {
28238
+ try {
28239
+ const raw = await fs65.promises.readFile(path71.join(process.cwd(), ".env"), "utf8");
28240
+ for (const v of parseDotenv(raw)) existing.set(v.key, v.value);
28241
+ } catch {
28242
+ }
28243
+ }
28244
+ let added = 0;
28245
+ let updated = 0;
28246
+ for (const [key, value] of incoming) {
28247
+ if (existing.has(key)) {
28248
+ if (existing.get(key) !== value) updated += 1;
28249
+ } else {
28250
+ added += 1;
28251
+ }
28252
+ existing.set(key, value);
28253
+ }
28254
+ await ctx.relay.sendResult(cmd.id, "completed", {
28255
+ vars: [...existing].map(([key, value]) => ({ key, value })),
28256
+ added,
28257
+ updated,
28258
+ unrecognized
28259
+ });
28260
+ };
28129
28261
  var envWriteH = async (ctx, cmd, parsed) => {
28130
28262
  const vars = parsed.vars;
28131
28263
  if (!Array.isArray(vars)) {
@@ -29139,7 +29271,13 @@ var requestPreviewDetectH = (ctx) => {
29139
29271
  const tookMs = Date.now() - startedAt;
29140
29272
  const detection = safeParseDetection(raw);
29141
29273
  if (!detection) {
29142
- log.info("preview", `detect: invalid agent output after ${tookMs}ms`);
29274
+ const failure = describeDetectionFailure(raw);
29275
+ log.info(
29276
+ "preview",
29277
+ `detect: failed after ${tookMs}ms reason=${failure?.reason ?? "unknown"}` + (failure?.missing ? ` missing=${failure.missing.join(",")}` : "") + `
29278
+ --- salida cruda del agente (acotada) ---
29279
+ ${failure?.rawExcerpt ?? ""}`
29280
+ );
29143
29281
  emitPreviewEvent({
29144
29282
  sessionId: ctx.sessionId,
29145
29283
  pluginId: ctx.pluginId,
@@ -29147,7 +29285,9 @@ var requestPreviewDetectH = (ctx) => {
29147
29285
  type: USER_EVENTS.PREVIEW_ERROR,
29148
29286
  payload: {
29149
29287
  stage: "detection",
29150
- message: "Agent returned invalid JSON. Try again, or add a .codeam/preview.json override."
29288
+ // El mensaje sale del diagnostico: decir "JSON invalido" cuando el
29289
+ // agente no contesto manda al usuario a mirar un JSON que no existe.
29290
+ message: failure?.message ?? "Agent returned invalid JSON. Try again, or add a .codeam/preview.json override."
29151
29291
  }
29152
29292
  });
29153
29293
  return;
@@ -29215,7 +29355,12 @@ function prewarmPreviewDetection(runtime) {
29215
29355
  if (await readPreviewConfig(cwd)) return;
29216
29356
  const raw = await runtime.generateOneShot(PREVIEW_DETECT_PROMPT).catch(() => null);
29217
29357
  const detection = safeParseDetection(raw);
29218
- if (!detection || detection.framework === "unsupported") return;
29358
+ if (!detection) {
29359
+ const failure = describeDetectionFailure(raw);
29360
+ log.info("preview", `prewarm: detection failed reason=${failure?.reason ?? "unknown"}`);
29361
+ return;
29362
+ }
29363
+ if (detection.framework === "unsupported") return;
29219
29364
  await writePreviewConfig(cwd, detection);
29220
29365
  log.info(
29221
29366
  "preview",
@@ -29390,6 +29535,7 @@ var handlers = {
29390
29535
  save_preview_config: savePreviewConfigH,
29391
29536
  env_read: envReadH,
29392
29537
  env_write: envWriteH,
29538
+ env_parse: envParseH,
29393
29539
  skills_configure: skillsConfigureH,
29394
29540
  take_control: takeControlH,
29395
29541
  handback: handbackH,
@@ -46759,7 +46905,7 @@ function checkChokidar() {
46759
46905
  }
46760
46906
  async function doctor(args2 = []) {
46761
46907
  const json = args2.includes("--json");
46762
- const cliVersion = true ? "2.71.1" : "0.0.0-dev";
46908
+ const cliVersion = true ? "2.72.0" : "0.0.0-dev";
46763
46909
  const apiBase2 = resolveApiBaseUrl();
46764
46910
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
46765
46911
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -47150,7 +47296,7 @@ async function mcpRun(args2) {
47150
47296
  // src/commands/version.ts
47151
47297
  var import_picocolors15 = __toESM(require("picocolors"));
47152
47298
  function version2() {
47153
- const v = true ? "2.71.1" : "unknown";
47299
+ const v = true ? "2.72.0" : "unknown";
47154
47300
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
47155
47301
  }
47156
47302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.71.1",
3
+ "version": "2.72.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",