codeam-cli 2.41.1 → 2.42.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 +110 -61
  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.41.0] — 2026-06-24
8
+
9
+ ### Added
10
+
11
+ - **cli:** Surface friendly provider-outage message + status-page link instead of silent hang
12
+
13
+ ### Merge
14
+
15
+ - Friendly provider-outage message + status-page link (no silent hang on provider 529/5xx)
16
+
7
17
  ## [2.40.2] — 2026-06-24
8
18
 
9
19
  ### Fixed
package/dist/index.js CHANGED
@@ -5390,7 +5390,7 @@ function readAnonId() {
5390
5390
  }
5391
5391
  function superProperties() {
5392
5392
  return {
5393
- cliVersion: true ? "2.41.1" : "0.0.0-dev",
5393
+ cliVersion: true ? "2.42.0" : "0.0.0-dev",
5394
5394
  nodeVersion: process.version,
5395
5395
  platform: process.platform,
5396
5396
  arch: process.arch,
@@ -5536,6 +5536,28 @@ var _execSeam = {
5536
5536
  return typeof out2 === "string" ? out2 : out2.toString("utf8");
5537
5537
  }
5538
5538
  };
5539
+ var _execSeamAsync = {
5540
+ exec: (file, args2, opts) => new Promise((resolve7, reject) => {
5541
+ (0, import_child_process.execFile)(file, args2, opts, (err, stdout) => {
5542
+ if (err) reject(err);
5543
+ else resolve7(typeof stdout === "string" ? stdout : stdout.toString("utf8"));
5544
+ });
5545
+ })
5546
+ };
5547
+ async function detectCurrentBranchAsync(cwd = process.cwd()) {
5548
+ try {
5549
+ const raw = await _execSeamAsync.exec("git", ["branch", "--show-current"], {
5550
+ cwd,
5551
+ timeout: 1e3,
5552
+ encoding: "utf8",
5553
+ windowsHide: true
5554
+ });
5555
+ const trimmed = raw.trim();
5556
+ return trimmed.length > 0 ? trimmed : null;
5557
+ } catch {
5558
+ return null;
5559
+ }
5560
+ }
5539
5561
 
5540
5562
  // src/services/command-relay.service.ts
5541
5563
  var https2 = __toESM(require("https"));
@@ -5549,7 +5571,7 @@ var os4 = __toESM(require("os"));
5549
5571
  // package.json
5550
5572
  var package_default = {
5551
5573
  name: "codeam-cli",
5552
- version: "2.41.1",
5574
+ version: "2.42.0",
5553
5575
  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.",
5554
5576
  type: "commonjs",
5555
5577
  main: "dist/index.js",
@@ -6044,13 +6066,31 @@ var CommandRelayService = class {
6044
6066
  */
6045
6067
  sseWatchdog = null;
6046
6068
  sseLastByteAt = 0;
6069
+ /**
6070
+ * Last-known git branch, shipped on every heartbeat. Seeded with a
6071
+ * single SYNCHRONOUS read at `start()` (before any turn is running)
6072
+ * for an accurate first beat, then refreshed ASYNCHRONOUSLY off the
6073
+ * recurring heartbeat tick. The heartbeat POST reads this cached
6074
+ * value and never spawns `git` synchronously — so the 20 s beat
6075
+ * stays punctual even while a long agent tool call is hammering the
6076
+ * event loop. Trade-off: because the async refresh fires alongside
6077
+ * the POST (not before it), a `git checkout` propagates on the NEXT
6078
+ * beat — up to ~40 s worst case vs the old at-POST-time read. The
6079
+ * backend dedupes a stable branch, so this lag costs nothing but
6080
+ * freshness, and a punctual heartbeat matters far more.
6081
+ */
6082
+ cachedBranch = null;
6047
6083
  start() {
6048
6084
  this.cleanup();
6049
6085
  this._running = true;
6050
6086
  this.agentsRegistered = false;
6051
6087
  log.info("relay", `start pluginId=${this.pluginId.slice(0, 8)} agent=${this.agentMeta.id}`);
6088
+ this.cachedBranch = detectCurrentBranch();
6052
6089
  this.sendHeartbeat(true);
6053
- this.heartbeatTimer = setInterval(() => this.sendHeartbeat(true), 2e4);
6090
+ this.heartbeatTimer = setInterval(() => {
6091
+ void this.refreshBranch();
6092
+ this.sendHeartbeat(true);
6093
+ }, 2e4);
6054
6094
  this.agentsTimer = setInterval(() => {
6055
6095
  if (this._running && !this.agentsRegistered) this.reportAgents();
6056
6096
  }, 5e3);
@@ -6294,9 +6334,20 @@ var CommandRelayService = class {
6294
6334
  pluginId: this.pluginId,
6295
6335
  online,
6296
6336
  agentId: this.agentMeta.id,
6297
- branch: detectCurrentBranch()
6337
+ branch: this.cachedBranch
6298
6338
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
6299
6339
  }
6340
+ /**
6341
+ * Refresh {@link cachedBranch} without blocking the event loop.
6342
+ * Fire-and-forget from the heartbeat tick; failures leave the last
6343
+ * known value in place (the backend dedupes a stable branch).
6344
+ */
6345
+ async refreshBranch() {
6346
+ try {
6347
+ this.cachedBranch = await detectCurrentBranchAsync();
6348
+ } catch {
6349
+ }
6350
+ }
6300
6351
  reportAgents() {
6301
6352
  const agents = this.agentsOverride ? [...this.agentsOverride] : [
6302
6353
  {
@@ -17753,7 +17804,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17753
17804
  if (process.env.NODE_ENV === "test") return;
17754
17805
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17755
17806
  if (process.env.CI) return;
17756
- const current = true ? "2.41.1" : null;
17807
+ const current = true ? "2.42.0" : null;
17757
17808
  if (!current) return;
17758
17809
  const cache = readCache();
17759
17810
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17770,7 +17821,7 @@ function checkForUpdates() {
17770
17821
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17771
17822
  if (process.env.CI) return;
17772
17823
  if (!process.stdout.isTTY) return;
17773
- const current = true ? "2.41.1" : null;
17824
+ const current = true ? "2.42.0" : null;
17774
17825
  if (!current) return;
17775
17826
  const cache = readCache();
17776
17827
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -18208,7 +18259,7 @@ var defaultSpawner = (env, cwd, args2 = []) => (0, import_node_child_process14.s
18208
18259
  detached: false
18209
18260
  });
18210
18261
  function currentCliVersion() {
18211
- return true ? "2.41.1" : null;
18262
+ return true ? "2.42.0" : null;
18212
18263
  }
18213
18264
  function runCmd(cmd, args2, timeoutMs) {
18214
18265
  return new Promise((resolve7) => {
@@ -24450,6 +24501,22 @@ var StreamingState = class {
24450
24501
  this.flushStreamingChunks()
24451
24502
  ]);
24452
24503
  }
24504
+ /**
24505
+ * Close the turn but REPLACE the streamed reply with `bubble` as the
24506
+ * terminal chat frame. The chat pipe treats each `text` chunk's content as
24507
+ * the full (replacing) bubble body, so a final `done:true` carrying the
24508
+ * bubble overwrites whatever raw text streamed. Used when the agent's
24509
+ * completed-turn reply was itself an auth-failure notice
24510
+ * ({@link replyIsAuthFailure}) → swap the raw "Please run /login" for the
24511
+ * actionable re-auth bubble.
24512
+ */
24513
+ async closeWithBubble(bubble) {
24514
+ this.text = "";
24515
+ await Promise.all([
24516
+ this.publisher.publishOutput({ type: "text", content: bubble, done: true }),
24517
+ this.flushStreamingChunks()
24518
+ ]);
24519
+ }
24453
24520
  /**
24454
24521
  * Re-emit every open streaming-chunk buffer with `isFinal: true`
24455
24522
  * so SessionDetailScreen's bubbles flip out of "still streaming"
@@ -24617,6 +24684,10 @@ var AUTH_FAILURE_RE = /invalid authentication credentials|authentication[_ ]erro
24617
24684
  function looksLikeAuthFailure(text) {
24618
24685
  return AUTH_FAILURE_RE.test(text);
24619
24686
  }
24687
+ function replyIsAuthFailure(finalText) {
24688
+ const t2 = finalText.trim();
24689
+ return t2.length > 0 && t2.length <= 200 && looksLikeAuthFailure(t2);
24690
+ }
24620
24691
  var AUTH_FAILURE_MESSAGE = "\u{1F512} **Authentication failed \u2014 your agent credentials are invalid or expired (API 401).**\n\nTap [Re-authenticate this agent](codeam://reauth) to renew your credentials in Profile \u203A Agents, then send your message again.";
24621
24692
  var TURN_FAILURE_MESSAGE = "\u26A0\uFE0F **The agent hit an error and couldn\u2019t finish this turn.** Please send your message again.";
24622
24693
  var PROVIDER_OUTAGE_RE = /overloaded_error|\boverloaded\b|service[ _]unavailable|temporarily[ _]unavailable|(?:api error|http|status)[:\s]+(?:529|503|502|504)\b|\b(?:529|503|502|504)\b[^\n]{0,40}(?:overload|unavailable|gateway|upstream|server error)|bad gateway|gateway time-?out|upstream (?:error|connect|timeout)/i;
@@ -24626,46 +24697,17 @@ function looksLikeProviderOutage(text) {
24626
24697
  function agentStatusPage(agent) {
24627
24698
  const a = (agent ?? "").toLowerCase();
24628
24699
  if (a.includes("claude") || a.includes("anthropic"))
24629
- return {
24630
- vendor: "Anthropic",
24631
- url: "https://status.anthropic.com",
24632
- statusApi: "https://status.anthropic.com/api/v2/status.json"
24633
- };
24700
+ return { vendor: "Anthropic", url: "https://status.anthropic.com" };
24634
24701
  if (a.includes("codex") || a.includes("openai"))
24635
- return {
24636
- vendor: "OpenAI",
24637
- url: "https://status.openai.com",
24638
- statusApi: "https://status.openai.com/api/v2/status.json"
24639
- };
24702
+ return { vendor: "OpenAI", url: "https://status.openai.com" };
24640
24703
  if (a.includes("gemini") || a.includes("google"))
24641
24704
  return { vendor: "Google", url: "https://status.cloud.google.com" };
24642
24705
  if (a.includes("copilot"))
24643
- return {
24644
- vendor: "GitHub",
24645
- url: "https://www.githubstatus.com",
24646
- statusApi: "https://www.githubstatus.com/api/v2/status.json"
24647
- };
24706
+ return { vendor: "GitHub", url: "https://www.githubstatus.com" };
24648
24707
  if (a.includes("cursor"))
24649
- return {
24650
- vendor: "Cursor",
24651
- url: "https://status.cursor.com",
24652
- statusApi: "https://status.cursor.com/api/v2/status.json"
24653
- };
24708
+ return { vendor: "Cursor", url: "https://status.cursor.com" };
24654
24709
  return null;
24655
24710
  }
24656
- async function checkProviderStatus(agent, fetchImpl = fetch) {
24657
- const info = agentStatusPage(agent);
24658
- if (!info?.statusApi) return false;
24659
- try {
24660
- const res = await withTimeout(fetchImpl(info.statusApi), 4e3);
24661
- if (!res || !res.ok) return false;
24662
- const body = await res.json();
24663
- const indicator = body?.status?.indicator;
24664
- return typeof indicator === "string" && indicator.toLowerCase() !== "none";
24665
- } catch {
24666
- return false;
24667
- }
24668
- }
24669
24711
  function providerOutageMessage(agent) {
24670
24712
  const info = agentStatusPage(agent);
24671
24713
  const who = info ? info.vendor : "The agent provider";
@@ -24977,34 +25019,41 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
24977
25019
  try {
24978
25020
  const reply = await client2.prompt(blocks);
24979
25021
  const finalText = streaming.getCurrentText();
24980
- await streaming.closeTurnWithInteractiveDetection();
24981
- const replyLine = formatAgentReplyLine(finalText);
24982
- if (replyLine.length > 0) {
24983
- showInfo(replyLine);
25022
+ if (replyIsAuthFailure(finalText)) {
25023
+ await streaming.closeWithBubble(AUTH_FAILURE_MESSAGE);
25024
+ history.appendAgentReply(AUTH_FAILURE_MESSAGE);
25025
+ void history.flush();
25026
+ turnFiles.flushTurn().catch((err) => {
25027
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
25028
+ });
25029
+ void reportCredentialInvalid(opts);
25030
+ log.info("acpRunner", `start_task \u2190 auth-failure-in-reply id=${cmd.id.slice(0, 8)}`);
25031
+ await relay.sendResult(cmd.id, "failed", { error: "agent reply reported auth failure" });
25032
+ } else {
25033
+ await streaming.closeTurnWithInteractiveDetection();
25034
+ const replyLine = formatAgentReplyLine(finalText);
25035
+ if (replyLine.length > 0) {
25036
+ showInfo(replyLine);
25037
+ }
25038
+ history.appendAgentReply(finalText);
25039
+ void history.flush();
25040
+ turnFiles.flushTurn().catch((err) => {
25041
+ log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
25042
+ });
25043
+ log.info("acpRunner", `start_task \u2190 done stopReason=${reply.stopReason ?? "?"} id=${cmd.id.slice(0, 8)}`);
25044
+ await relay.sendResult(cmd.id, "completed", { stopReason: reply.stopReason });
24984
25045
  }
24985
- history.appendAgentReply(finalText);
24986
- void history.flush();
24987
- turnFiles.flushTurn().catch((err) => {
24988
- log.warn("acpRunner", `turnFiles.flushTurn failed: ${describeError(err)}`);
24989
- });
24990
- log.info("acpRunner", `start_task \u2190 done stopReason=${reply.stopReason ?? "?"} id=${cmd.id.slice(0, 8)}`);
24991
- await relay.sendResult(cmd.id, "completed", { stopReason: reply.stopReason });
24992
25046
  } catch (err) {
24993
25047
  const hadText = streaming.getCurrentText().trim().length > 0;
24994
25048
  await recoverFromFailedTurn(client2, streaming);
24995
25049
  const detail = describeError(err);
24996
25050
  log.warn("acpRunner", `prompt failed: ${detail}`);
24997
- let bubble = failureBubble({
25051
+ const bubble = failureBubble({
24998
25052
  detail,
24999
25053
  recentStderr: recentStderr.join("\n"),
25000
25054
  hadText,
25001
25055
  agent: opts.agent
25002
25056
  });
25003
- if (bubble !== AUTH_FAILURE_MESSAGE && bubble !== providerOutageMessage(opts.agent)) {
25004
- if (await checkProviderStatus(opts.agent)) {
25005
- bubble = providerOutageMessage(opts.agent);
25006
- }
25007
- }
25008
25057
  if (bubble) {
25009
25058
  await publisher.publishOutput({ type: "text", content: bubble, done: true });
25010
25059
  history.appendAgentReply(bubble);
@@ -28936,9 +28985,9 @@ async function probeCodeamPair(provider, workspace) {
28936
28985
  }
28937
28986
  async function stopWorkspaceFromLocal(target) {
28938
28987
  if (target.provider.id === "github-codespaces") {
28939
- const { execFile: execFile12 } = await import("child_process");
28988
+ const { execFile: execFile13 } = await import("child_process");
28940
28989
  const { promisify: promisify11 } = await import("util");
28941
- const execFileP10 = promisify11(execFile12);
28990
+ const execFileP10 = promisify11(execFile13);
28942
28991
  await execFileP10("gh", ["codespace", "stop", "-c", target.id], { maxBuffer: 8 * 1024 * 1024 });
28943
28992
  return;
28944
28993
  }
@@ -29162,7 +29211,7 @@ function checkChokidar() {
29162
29211
  }
29163
29212
  async function doctor(args2 = []) {
29164
29213
  const json = args2.includes("--json");
29165
- const cliVersion = true ? "2.41.1" : "0.0.0-dev";
29214
+ const cliVersion = true ? "2.42.0" : "0.0.0-dev";
29166
29215
  const apiBase2 = resolveApiBaseUrl();
29167
29216
  const diagnosticId = (0, import_node_crypto8.randomUUID)();
29168
29217
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -29361,7 +29410,7 @@ async function completion(args2) {
29361
29410
  // src/commands/version.ts
29362
29411
  var import_picocolors14 = __toESM(require("picocolors"));
29363
29412
  function version2() {
29364
- const v = true ? "2.41.1" : "unknown";
29413
+ const v = true ? "2.42.0" : "unknown";
29365
29414
  console.log(`${import_picocolors14.default.bold("codeam-cli")} ${import_picocolors14.default.cyan(v)}`);
29366
29415
  }
29367
29416
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.41.1",
3
+ "version": "2.42.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",