codeam-cli 2.23.2 → 2.23.4
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 +12 -0
- package/dist/index.js +114 -18
- package/package.json +1 -1
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.23.3] — 2026-05-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **cli:** Reap in-flight spawnAndCapture children on sigint/exit (#191)
|
|
12
|
+
|
|
13
|
+
## [2.23.2] — 2026-05-25
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **cli:** Fire-and-forget background handlers so they don't block start_task (#190)
|
|
18
|
+
|
|
7
19
|
## [2.23.1] — 2026-05-25
|
|
8
20
|
|
|
9
21
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -441,7 +441,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
|
441
441
|
// package.json
|
|
442
442
|
var package_default = {
|
|
443
443
|
name: "codeam-cli",
|
|
444
|
-
version: "2.23.
|
|
444
|
+
version: "2.23.4",
|
|
445
445
|
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.",
|
|
446
446
|
type: "commonjs",
|
|
447
447
|
main: "dist/index.js",
|
|
@@ -5768,7 +5768,7 @@ function readAnonId() {
|
|
|
5768
5768
|
}
|
|
5769
5769
|
function superProperties() {
|
|
5770
5770
|
return {
|
|
5771
|
-
cliVersion: true ? "2.23.
|
|
5771
|
+
cliVersion: true ? "2.23.4" : "0.0.0-dev",
|
|
5772
5772
|
nodeVersion: process.version,
|
|
5773
5773
|
platform: process.platform,
|
|
5774
5774
|
arch: process.arch,
|
|
@@ -5781,7 +5781,7 @@ function initTelemetry() {
|
|
|
5781
5781
|
log.trace("telemetry", "opted out (CODEAM_TELEMETRY / CI / NODE_ENV=test)");
|
|
5782
5782
|
return false;
|
|
5783
5783
|
}
|
|
5784
|
-
const apiKey = true ? "" : "";
|
|
5784
|
+
const apiKey = true ? "phc_Otx6LUf0KeabkbTnPuLPLg6d6r8319k8bKDYqoXyeb4" : "";
|
|
5785
5785
|
if (!apiKey) {
|
|
5786
5786
|
log.trace("telemetry", "no PostHog API key baked into build \u2014 disabled");
|
|
5787
5787
|
return false;
|
|
@@ -5860,6 +5860,8 @@ function maybePrintFirstRunBanner() {
|
|
|
5860
5860
|
|
|
5861
5861
|
// src/services/command-relay.service.ts
|
|
5862
5862
|
var API_BASE2 = resolveApiBaseUrl();
|
|
5863
|
+
var SSE_LIVENESS_TIMEOUT_MS = 45e3;
|
|
5864
|
+
var SSE_WATCHDOG_INTERVAL_MS = 1e4;
|
|
5863
5865
|
var CommandRelayService = class {
|
|
5864
5866
|
constructor(pluginId, onCommand, agentMeta) {
|
|
5865
5867
|
this.pluginId = pluginId;
|
|
@@ -5889,10 +5891,25 @@ var CommandRelayService = class {
|
|
|
5889
5891
|
/** Reconnect backoff state for the SSE stream. */
|
|
5890
5892
|
sseFailures = 0;
|
|
5891
5893
|
sseReconnectTimer = null;
|
|
5894
|
+
/**
|
|
5895
|
+
* Liveness watchdog for the SSE socket. The server pings every 30 s
|
|
5896
|
+
* (`pending-stream.controller.ts` interval(30_000)). If we go more
|
|
5897
|
+
* than `SSE_LIVENESS_TIMEOUT_MS` without ANY bytes, we assume the
|
|
5898
|
+
* TCP peer died half-open (Cloud Run scaled / redeployed mid-stream,
|
|
5899
|
+
* NAT dropped the conntrack entry, laptop slept) and force a
|
|
5900
|
+
* reconnect. Without this the kernel can keep the socket in a
|
|
5901
|
+
* zombie ESTABLISHED state for HOURS before the default TCP
|
|
5902
|
+
* keepalive triggers — observed live in #190 where a 17:18 redeploy
|
|
5903
|
+
* left CLIs subscribed for >30 minutes without ever receiving a
|
|
5904
|
+
* pushed command.
|
|
5905
|
+
*/
|
|
5906
|
+
sseWatchdog = null;
|
|
5907
|
+
sseLastByteAt = 0;
|
|
5892
5908
|
start() {
|
|
5893
5909
|
this.cleanup();
|
|
5894
5910
|
this._running = true;
|
|
5895
5911
|
this.agentsRegistered = false;
|
|
5912
|
+
log.info("relay", `start pluginId=${this.pluginId.slice(0, 8)} agent=${this.agentMeta.id}`);
|
|
5896
5913
|
this.sendHeartbeat(true);
|
|
5897
5914
|
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(true), 2e4);
|
|
5898
5915
|
this.agentsTimer = setInterval(() => {
|
|
@@ -5900,6 +5917,7 @@ var CommandRelayService = class {
|
|
|
5900
5917
|
}, 5e3);
|
|
5901
5918
|
this.reportAgents();
|
|
5902
5919
|
if (process.env.NODE_ENV === "test" || process.env.CODEAM_DISABLE_SSE_PULL === "1") {
|
|
5920
|
+
log.info("relay", "SSE disabled \u2014 using polling fallback");
|
|
5903
5921
|
this.startPollingFallback();
|
|
5904
5922
|
} else {
|
|
5905
5923
|
this.connectSSE();
|
|
@@ -5921,7 +5939,7 @@ var CommandRelayService = class {
|
|
|
5921
5939
|
const url = new URL(`${API_BASE2}/api/commands/pending/stream`);
|
|
5922
5940
|
url.searchParams.set("pluginId", this.pluginId);
|
|
5923
5941
|
const transport = url.protocol === "https:" ? https2 : http2;
|
|
5924
|
-
log.
|
|
5942
|
+
log.info("relay", `sse connect pluginId=${this.pluginId.slice(0, 8)}`);
|
|
5925
5943
|
const req = transport.request(
|
|
5926
5944
|
{
|
|
5927
5945
|
hostname: url.hostname,
|
|
@@ -5933,11 +5951,11 @@ var CommandRelayService = class {
|
|
|
5933
5951
|
},
|
|
5934
5952
|
(res) => {
|
|
5935
5953
|
if (res.statusCode !== 200) {
|
|
5936
|
-
log.
|
|
5954
|
+
log.info("relay", `sse status=${res.statusCode} \u2014 backing off`);
|
|
5937
5955
|
res.resume();
|
|
5938
5956
|
this.sseFailures += 1;
|
|
5939
5957
|
if (this.sseFailures >= 2) {
|
|
5940
|
-
log.
|
|
5958
|
+
log.info("relay", "sse unavailable \u2014 falling back to polling");
|
|
5941
5959
|
capture("sse_fallback_to_poll", {
|
|
5942
5960
|
pluginId: this.pluginId,
|
|
5943
5961
|
agentId: this.agentMeta.id,
|
|
@@ -5950,10 +5968,13 @@ var CommandRelayService = class {
|
|
|
5950
5968
|
this.scheduleSseReconnect();
|
|
5951
5969
|
return;
|
|
5952
5970
|
}
|
|
5971
|
+
log.info("relay", "sse connected");
|
|
5953
5972
|
this.sseFailures = 0;
|
|
5973
|
+
this.armSseWatchdog();
|
|
5954
5974
|
let buffer = "";
|
|
5955
5975
|
res.setEncoding("utf8");
|
|
5956
5976
|
res.on("data", (chunk) => {
|
|
5977
|
+
this.sseLastByteAt = Date.now();
|
|
5957
5978
|
buffer += chunk;
|
|
5958
5979
|
let frameEnd;
|
|
5959
5980
|
while ((frameEnd = buffer.indexOf("\n\n")) !== -1) {
|
|
@@ -5963,15 +5984,26 @@ var CommandRelayService = class {
|
|
|
5963
5984
|
}
|
|
5964
5985
|
});
|
|
5965
5986
|
res.on("end", () => {
|
|
5987
|
+
log.info("relay", "sse end \u2014 reconnecting");
|
|
5988
|
+
this.disarmSseWatchdog();
|
|
5966
5989
|
if (this._running) this.scheduleSseReconnect();
|
|
5967
5990
|
});
|
|
5968
|
-
res.on("error", () => {
|
|
5991
|
+
res.on("error", (err) => {
|
|
5992
|
+
log.info("relay", `sse res error \u2014 reconnecting (${err.message})`);
|
|
5993
|
+
this.disarmSseWatchdog();
|
|
5969
5994
|
if (this._running) this.scheduleSseReconnect();
|
|
5970
5995
|
});
|
|
5971
5996
|
}
|
|
5972
5997
|
);
|
|
5998
|
+
req.on("socket", (socket) => {
|
|
5999
|
+
try {
|
|
6000
|
+
socket.setKeepAlive(true, 3e4);
|
|
6001
|
+
} catch {
|
|
6002
|
+
}
|
|
6003
|
+
});
|
|
5973
6004
|
req.on("error", (err) => {
|
|
5974
|
-
log.
|
|
6005
|
+
log.info("relay", `sse req error \u2014 ${err.message}`);
|
|
6006
|
+
this.disarmSseWatchdog();
|
|
5975
6007
|
this.sseFailures += 1;
|
|
5976
6008
|
if (this.sseFailures >= 2) {
|
|
5977
6009
|
capture("sse_fallback_to_poll", {
|
|
@@ -5986,11 +6018,43 @@ var CommandRelayService = class {
|
|
|
5986
6018
|
this.scheduleSseReconnect();
|
|
5987
6019
|
});
|
|
5988
6020
|
req.on("timeout", () => {
|
|
6021
|
+
log.info("relay", "sse req timeout \u2014 destroying + reconnecting");
|
|
5989
6022
|
req.destroy();
|
|
5990
6023
|
});
|
|
5991
6024
|
req.end();
|
|
5992
6025
|
this.sseRequest = req;
|
|
5993
6026
|
}
|
|
6027
|
+
// ─── SSE liveness watchdog ───────────────────────────────────────
|
|
6028
|
+
//
|
|
6029
|
+
// The server pings every 30 s; if more than SSE_LIVENESS_TIMEOUT_MS
|
|
6030
|
+
// pass without a byte, we assume the TCP peer is dead-zombie and
|
|
6031
|
+
// force the request to destroy → triggers `error`/`end` → standard
|
|
6032
|
+
// reconnect path. Without this, observed in the field: half-open
|
|
6033
|
+
// socket sits ESTABLISHED for 30 + minutes (until macOS default
|
|
6034
|
+
// 2 h TCP keepalive kicks in) and the CLI silently misses every
|
|
6035
|
+
// pushed command.
|
|
6036
|
+
armSseWatchdog() {
|
|
6037
|
+
this.disarmSseWatchdog();
|
|
6038
|
+
this.sseLastByteAt = Date.now();
|
|
6039
|
+
this.sseWatchdog = setInterval(() => {
|
|
6040
|
+
const idle = Date.now() - this.sseLastByteAt;
|
|
6041
|
+
if (idle > SSE_LIVENESS_TIMEOUT_MS) {
|
|
6042
|
+
log.info("relay", `sse watchdog \u2014 ${idle}ms since last byte, forcing reconnect`);
|
|
6043
|
+
try {
|
|
6044
|
+
this.sseRequest?.destroy();
|
|
6045
|
+
} catch {
|
|
6046
|
+
}
|
|
6047
|
+
this.disarmSseWatchdog();
|
|
6048
|
+
}
|
|
6049
|
+
}, SSE_WATCHDOG_INTERVAL_MS);
|
|
6050
|
+
this.sseWatchdog.unref?.();
|
|
6051
|
+
}
|
|
6052
|
+
disarmSseWatchdog() {
|
|
6053
|
+
if (this.sseWatchdog) {
|
|
6054
|
+
clearInterval(this.sseWatchdog);
|
|
6055
|
+
this.sseWatchdog = null;
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
5994
6058
|
handleSseFrame(frame) {
|
|
5995
6059
|
let event = "message";
|
|
5996
6060
|
let data = "";
|
|
@@ -6003,10 +6067,13 @@ var CommandRelayService = class {
|
|
|
6003
6067
|
const parsed = JSON.parse(data);
|
|
6004
6068
|
const commands = parsed.commands ?? [];
|
|
6005
6069
|
if (commands.length === 0) return;
|
|
6006
|
-
log.
|
|
6070
|
+
log.info(
|
|
6071
|
+
"relay",
|
|
6072
|
+
`sse received ${commands.length} command(s) types=[${commands.map((c2) => c2.type).join(",")}]`
|
|
6073
|
+
);
|
|
6007
6074
|
void this.dispatchCommands(commands);
|
|
6008
6075
|
} catch (err) {
|
|
6009
|
-
log.
|
|
6076
|
+
log.info("relay", "sse parse error", err);
|
|
6010
6077
|
}
|
|
6011
6078
|
}
|
|
6012
6079
|
scheduleSseReconnect() {
|
|
@@ -6101,6 +6168,7 @@ var CommandRelayService = class {
|
|
|
6101
6168
|
clearTimeout(this.sseReconnectTimer);
|
|
6102
6169
|
this.sseReconnectTimer = null;
|
|
6103
6170
|
}
|
|
6171
|
+
this.disarmSseWatchdog();
|
|
6104
6172
|
if (this.sseRequest) {
|
|
6105
6173
|
try {
|
|
6106
6174
|
this.sseRequest.destroy();
|
|
@@ -9129,6 +9197,23 @@ async function fetchClaudeQuota() {
|
|
|
9129
9197
|
|
|
9130
9198
|
// src/services/spawn-and-capture.ts
|
|
9131
9199
|
var import_child_process6 = require("child_process");
|
|
9200
|
+
var activeChildren = /* @__PURE__ */ new Set();
|
|
9201
|
+
function killActiveSpawnAndCaptureChildren() {
|
|
9202
|
+
for (const child of activeChildren) {
|
|
9203
|
+
try {
|
|
9204
|
+
child.kill("SIGTERM");
|
|
9205
|
+
} catch {
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
setTimeout(() => {
|
|
9209
|
+
for (const child of activeChildren) {
|
|
9210
|
+
try {
|
|
9211
|
+
child.kill("SIGKILL");
|
|
9212
|
+
} catch {
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
}, 250).unref?.();
|
|
9216
|
+
}
|
|
9132
9217
|
async function spawnAndCapture(cmd, args2, opts = {}) {
|
|
9133
9218
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
9134
9219
|
return new Promise((resolve5) => {
|
|
@@ -9149,6 +9234,7 @@ async function spawnAndCapture(cmd, args2, opts = {}) {
|
|
|
9149
9234
|
settle(null);
|
|
9150
9235
|
return;
|
|
9151
9236
|
}
|
|
9237
|
+
activeChildren.add(child);
|
|
9152
9238
|
let stdout = "";
|
|
9153
9239
|
child.stdout?.on("data", (chunk) => {
|
|
9154
9240
|
stdout += chunk.toString("utf8");
|
|
@@ -9166,10 +9252,12 @@ async function spawnAndCapture(cmd, args2, opts = {}) {
|
|
|
9166
9252
|
timer.unref();
|
|
9167
9253
|
child.on("error", () => {
|
|
9168
9254
|
clearTimeout(timer);
|
|
9255
|
+
activeChildren.delete(child);
|
|
9169
9256
|
settle(null);
|
|
9170
9257
|
});
|
|
9171
9258
|
child.on("exit", (code) => {
|
|
9172
9259
|
clearTimeout(timer);
|
|
9260
|
+
activeChildren.delete(child);
|
|
9173
9261
|
if (code !== 0) {
|
|
9174
9262
|
settle(null);
|
|
9175
9263
|
return;
|
|
@@ -12581,11 +12669,17 @@ var FileWatcherService = class {
|
|
|
12581
12669
|
} : {},
|
|
12582
12670
|
awaitWriteFinish: {
|
|
12583
12671
|
// Coalesces rapid sequential writes (npm install spam, build
|
|
12584
|
-
// tools emitting bursts).
|
|
12585
|
-
//
|
|
12586
|
-
//
|
|
12587
|
-
|
|
12588
|
-
|
|
12672
|
+
// tools emitting bursts). Tuned for monorepo workloads after
|
|
12673
|
+
// observing a CLI process spike to 126% CPU on the codeagent
|
|
12674
|
+
// monorepo: pollInterval=50ms caused thousands of fs.stat
|
|
12675
|
+
// calls per second when many files were being edited
|
|
12676
|
+
// concurrently (agent edits + AI summary subprocess writes +
|
|
12677
|
+
// git status outputs all touching files in the watched tree).
|
|
12678
|
+
// 200ms is still well below the typical user perception
|
|
12679
|
+
// threshold for "the file appeared in the Files screen" while
|
|
12680
|
+
// keeping the polling cost bounded.
|
|
12681
|
+
stabilityThreshold: 300,
|
|
12682
|
+
pollInterval: 200
|
|
12589
12683
|
}
|
|
12590
12684
|
});
|
|
12591
12685
|
watcher.on("add", (filePath) => this.schedule(filePath, "add"));
|
|
@@ -15715,6 +15809,7 @@ async function start(requestedAgent) {
|
|
|
15715
15809
|
void streamingEmitter?.stop();
|
|
15716
15810
|
closeAllTerminals();
|
|
15717
15811
|
cleanupAttachmentTempFiles();
|
|
15812
|
+
killActiveSpawnAndCaptureChildren();
|
|
15718
15813
|
process.exit(code);
|
|
15719
15814
|
}
|
|
15720
15815
|
}
|
|
@@ -15760,6 +15855,7 @@ async function start(requestedAgent) {
|
|
|
15760
15855
|
void streamingEmitter?.stop();
|
|
15761
15856
|
closeAllTerminals();
|
|
15762
15857
|
cleanupAttachmentTempFiles();
|
|
15858
|
+
killActiveSpawnAndCaptureChildren();
|
|
15763
15859
|
void shutdownTelemetry();
|
|
15764
15860
|
process.exit(0);
|
|
15765
15861
|
}
|
|
@@ -18304,7 +18400,7 @@ function checkChokidar() {
|
|
|
18304
18400
|
}
|
|
18305
18401
|
async function doctor(args2 = []) {
|
|
18306
18402
|
const json = args2.includes("--json");
|
|
18307
|
-
const cliVersion = true ? "2.23.
|
|
18403
|
+
const cliVersion = true ? "2.23.4" : "0.0.0-dev";
|
|
18308
18404
|
const apiBase = resolveApiBaseUrl();
|
|
18309
18405
|
const diagnosticId = (0, import_node_crypto5.randomUUID)();
|
|
18310
18406
|
log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
|
|
@@ -18503,7 +18599,7 @@ async function completion(args2) {
|
|
|
18503
18599
|
// src/commands/version.ts
|
|
18504
18600
|
var import_picocolors13 = __toESM(require("picocolors"));
|
|
18505
18601
|
function version2() {
|
|
18506
|
-
const v = true ? "2.23.
|
|
18602
|
+
const v = true ? "2.23.4" : "unknown";
|
|
18507
18603
|
console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
|
|
18508
18604
|
}
|
|
18509
18605
|
|
|
@@ -18731,7 +18827,7 @@ function checkForUpdates() {
|
|
|
18731
18827
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
18732
18828
|
if (process.env.CI) return;
|
|
18733
18829
|
if (!process.stdout.isTTY) return;
|
|
18734
|
-
const current = true ? "2.23.
|
|
18830
|
+
const current = true ? "2.23.4" : null;
|
|
18735
18831
|
if (!current) return;
|
|
18736
18832
|
const cache = readCache();
|
|
18737
18833
|
const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeam-cli",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.4",
|
|
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",
|