codeam-cli 2.39.29 → 2.39.31
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 +153 -20
- 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.39.30] — 2026-06-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **cli:** Report real system metrics in host-agent heartbeat
|
|
12
|
+
|
|
13
|
+
## [2.39.29] — 2026-06-18
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **cli:** Report host-agent enrollment progress to backend
|
|
18
|
+
|
|
7
19
|
## [2.39.28] — 2026-06-18
|
|
8
20
|
|
|
9
21
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -498,7 +498,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
|
498
498
|
// package.json
|
|
499
499
|
var package_default = {
|
|
500
500
|
name: "codeam-cli",
|
|
501
|
-
version: "2.39.
|
|
501
|
+
version: "2.39.31",
|
|
502
502
|
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.",
|
|
503
503
|
type: "commonjs",
|
|
504
504
|
main: "dist/index.js",
|
|
@@ -5908,7 +5908,7 @@ function readAnonId() {
|
|
|
5908
5908
|
}
|
|
5909
5909
|
function superProperties() {
|
|
5910
5910
|
return {
|
|
5911
|
-
cliVersion: true ? "2.39.
|
|
5911
|
+
cliVersion: true ? "2.39.31" : "0.0.0-dev",
|
|
5912
5912
|
nodeVersion: process.version,
|
|
5913
5913
|
platform: process.platform,
|
|
5914
5914
|
arch: process.arch,
|
|
@@ -26205,6 +26205,48 @@ async function stopWorkspaceFromLocal(target) {
|
|
|
26205
26205
|
var fs41 = __toESM(require("fs"));
|
|
26206
26206
|
var os32 = __toESM(require("os"));
|
|
26207
26207
|
var path53 = __toESM(require("path"));
|
|
26208
|
+
function sampleCpuTimes() {
|
|
26209
|
+
let idle = 0;
|
|
26210
|
+
let total = 0;
|
|
26211
|
+
for (const cpu of os32.cpus()) {
|
|
26212
|
+
const t2 = cpu.times;
|
|
26213
|
+
idle += t2.idle;
|
|
26214
|
+
total += t2.user + t2.nice + t2.sys + t2.idle + t2.irq;
|
|
26215
|
+
}
|
|
26216
|
+
return { idle, total };
|
|
26217
|
+
}
|
|
26218
|
+
var MetricsCollector = class {
|
|
26219
|
+
prevCpu = null;
|
|
26220
|
+
lastLatencyMs = 0;
|
|
26221
|
+
/** Record the measured round-trip of the heartbeat just sent. */
|
|
26222
|
+
recordLatency(latencyMs) {
|
|
26223
|
+
this.lastLatencyMs = Math.max(0, Math.round(latencyMs));
|
|
26224
|
+
}
|
|
26225
|
+
cpuPct() {
|
|
26226
|
+
const current = sampleCpuTimes();
|
|
26227
|
+
const prev = this.prevCpu;
|
|
26228
|
+
this.prevCpu = current;
|
|
26229
|
+
if (!prev) {
|
|
26230
|
+
const cores = os32.cpus().length || 1;
|
|
26231
|
+
const proxy = os32.loadavg()[0] / cores * 100;
|
|
26232
|
+
return Math.min(100, Math.max(0, Math.round(proxy)));
|
|
26233
|
+
}
|
|
26234
|
+
const idleDelta = current.idle - prev.idle;
|
|
26235
|
+
const totalDelta = current.total - prev.total;
|
|
26236
|
+
if (totalDelta <= 0) return 0;
|
|
26237
|
+
const busy = (1 - idleDelta / totalDelta) * 100;
|
|
26238
|
+
return Math.min(100, Math.max(0, Math.round(busy)));
|
|
26239
|
+
}
|
|
26240
|
+
/** Collect the current metrics snapshot for this beat. */
|
|
26241
|
+
collect() {
|
|
26242
|
+
return {
|
|
26243
|
+
cpuPct: this.cpuPct(),
|
|
26244
|
+
ramUsedMb: Math.round((os32.totalmem() - os32.freemem()) / 1048576),
|
|
26245
|
+
ramTotalMb: Math.round(os32.totalmem() / 1048576),
|
|
26246
|
+
latencyMs: this.lastLatencyMs
|
|
26247
|
+
};
|
|
26248
|
+
}
|
|
26249
|
+
};
|
|
26208
26250
|
function apiBase() {
|
|
26209
26251
|
return process.env.CODEAM_API_URL ?? resolveApiBaseUrl();
|
|
26210
26252
|
}
|
|
@@ -26241,6 +26283,31 @@ function saveHostIdentity(identity) {
|
|
|
26241
26283
|
});
|
|
26242
26284
|
fs41.chmodSync(file, 384);
|
|
26243
26285
|
}
|
|
26286
|
+
var HostHttpError = class extends Error {
|
|
26287
|
+
constructor(message, status2) {
|
|
26288
|
+
super(message);
|
|
26289
|
+
this.status = status2;
|
|
26290
|
+
this.name = "HostHttpError";
|
|
26291
|
+
}
|
|
26292
|
+
status;
|
|
26293
|
+
/**
|
|
26294
|
+
* True iff this is a genuine auth-rejection of the host identity:
|
|
26295
|
+
* 401 (token invalid), 403 (forbidden), 404 (host not found / deleted).
|
|
26296
|
+
* Transient failures (5xx, timeouts, network errors) are NOT rejections.
|
|
26297
|
+
*/
|
|
26298
|
+
get isAuthRejection() {
|
|
26299
|
+
return this.status === 401 || this.status === 403 || this.status === 404;
|
|
26300
|
+
}
|
|
26301
|
+
};
|
|
26302
|
+
function isHostAuthRejection(err) {
|
|
26303
|
+
return err instanceof HostHttpError && err.isAuthRejection;
|
|
26304
|
+
}
|
|
26305
|
+
function deleteHostIdentity() {
|
|
26306
|
+
try {
|
|
26307
|
+
fs41.rmSync(hostIdentityPath(), { force: true });
|
|
26308
|
+
} catch {
|
|
26309
|
+
}
|
|
26310
|
+
}
|
|
26244
26311
|
async function postJson(pathname, body) {
|
|
26245
26312
|
const res = await fetch(`${apiBase()}${pathname}`, {
|
|
26246
26313
|
method: "POST",
|
|
@@ -26250,8 +26317,9 @@ async function postJson(pathname, body) {
|
|
|
26250
26317
|
const json = await res.json();
|
|
26251
26318
|
if (!res.ok || !json.success) {
|
|
26252
26319
|
const err = !json.success ? json.error : void 0;
|
|
26253
|
-
throw new
|
|
26254
|
-
`${pathname} failed (${err?.code ?? `HTTP_${res.status}`}): ${err?.message ?? res.statusText}
|
|
26320
|
+
throw new HostHttpError(
|
|
26321
|
+
`${pathname} failed (${err?.code ?? `HTTP_${res.status}`}): ${err?.message ?? res.statusText}`,
|
|
26322
|
+
res.status
|
|
26255
26323
|
);
|
|
26256
26324
|
}
|
|
26257
26325
|
return json.data;
|
|
@@ -26268,11 +26336,15 @@ async function redeemEnrollToken(token, label) {
|
|
|
26268
26336
|
controlPluginId: data.controlPluginId
|
|
26269
26337
|
};
|
|
26270
26338
|
}
|
|
26271
|
-
async function sendHostHeartbeat(identity) {
|
|
26339
|
+
async function sendHostHeartbeat(identity, metrics) {
|
|
26340
|
+
const start2 = process.hrtime.bigint();
|
|
26272
26341
|
await postJson("/api/self-hosted/heartbeat", {
|
|
26273
26342
|
hostId: identity.hostId,
|
|
26274
|
-
hostToken: identity.hostToken
|
|
26343
|
+
hostToken: identity.hostToken,
|
|
26344
|
+
...metrics ? { metrics } : {}
|
|
26275
26345
|
});
|
|
26346
|
+
const elapsedNs = process.hrtime.bigint() - start2;
|
|
26347
|
+
return Math.round(Number(elapsedNs) / 1e6);
|
|
26276
26348
|
}
|
|
26277
26349
|
function isSealedEnvelope(v) {
|
|
26278
26350
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -26509,12 +26581,26 @@ var defaultSpawner = (env, cwd, args2 = []) => (0, import_node_child_process13.s
|
|
|
26509
26581
|
stdio: "ignore",
|
|
26510
26582
|
detached: false
|
|
26511
26583
|
});
|
|
26584
|
+
var defaultOnIdentityRejected = () => {
|
|
26585
|
+
deleteHostIdentity();
|
|
26586
|
+
log.warn("host-agent", "host identity rejected by backend \u2014 wiped sealed identity, exiting");
|
|
26587
|
+
process.exit(1);
|
|
26588
|
+
};
|
|
26589
|
+
var defaultDisableService = () => {
|
|
26590
|
+
try {
|
|
26591
|
+
(0, import_node_child_process13.execFileSync)("systemctl", ["disable", "--now", "codeam-host-agent"], { stdio: "ignore" });
|
|
26592
|
+
} catch {
|
|
26593
|
+
}
|
|
26594
|
+
};
|
|
26512
26595
|
var HostAgentSupervisor = class {
|
|
26513
26596
|
constructor(identity, deps = {}) {
|
|
26514
26597
|
this.identity = identity;
|
|
26515
26598
|
this.deps = deps;
|
|
26516
26599
|
this.spawnChild = deps.spawnChild ?? defaultSpawner;
|
|
26517
26600
|
this.resolveAgentAuth = deps.resolveAgentAuth ?? unsealAgentAuth;
|
|
26601
|
+
this.metrics = deps.metricsCollector ?? new MetricsCollector();
|
|
26602
|
+
this.onIdentityRejected = deps.onIdentityRejected ?? defaultOnIdentityRejected;
|
|
26603
|
+
this.disableService = deps.disableService ?? defaultDisableService;
|
|
26518
26604
|
}
|
|
26519
26605
|
identity;
|
|
26520
26606
|
deps;
|
|
@@ -26525,6 +26611,14 @@ var HostAgentSupervisor = class {
|
|
|
26525
26611
|
heartbeatTimer = null;
|
|
26526
26612
|
/** Guards the one-shot 'connected' telemetry on the first heartbeat. */
|
|
26527
26613
|
reportedConnected = false;
|
|
26614
|
+
/** Live-metrics collector — stateful across beats (CPU delta + latency). */
|
|
26615
|
+
metrics;
|
|
26616
|
+
/** Self-heal action when the backend rejects this identity. */
|
|
26617
|
+
onIdentityRejected;
|
|
26618
|
+
/** Best-effort systemd de-provision used by `self_hosted_wipe`. */
|
|
26619
|
+
disableService;
|
|
26620
|
+
/** Guards against firing the self-heal more than once. */
|
|
26621
|
+
healing = false;
|
|
26528
26622
|
/** Open the control channel (reusing the relay) + start heartbeats. */
|
|
26529
26623
|
start() {
|
|
26530
26624
|
const make = this.deps.makeRelay ?? ((pluginId, onCommand, meta) => new CommandRelayService(pluginId, onCommand, meta));
|
|
@@ -26556,7 +26650,14 @@ var HostAgentSupervisor = class {
|
|
|
26556
26650
|
}
|
|
26557
26651
|
async beat() {
|
|
26558
26652
|
try {
|
|
26559
|
-
|
|
26653
|
+
let metrics;
|
|
26654
|
+
try {
|
|
26655
|
+
metrics = this.metrics.collect();
|
|
26656
|
+
} catch (err) {
|
|
26657
|
+
log.trace("host-agent", "metrics collection failed", err);
|
|
26658
|
+
}
|
|
26659
|
+
const latencyMs = await sendHostHeartbeat(this.identity, metrics);
|
|
26660
|
+
this.metrics.recordLatency(latencyMs);
|
|
26560
26661
|
if (!this.reportedConnected) {
|
|
26561
26662
|
this.reportedConnected = true;
|
|
26562
26663
|
void reportProgress(
|
|
@@ -26566,6 +26667,14 @@ var HostAgentSupervisor = class {
|
|
|
26566
26667
|
);
|
|
26567
26668
|
}
|
|
26568
26669
|
} catch (err) {
|
|
26670
|
+
if (isHostAuthRejection(err)) {
|
|
26671
|
+
if (!this.healing) {
|
|
26672
|
+
this.healing = true;
|
|
26673
|
+
log.warn("host-agent", "heartbeat rejected \u2014 host deleted/revoked, self-healing", err);
|
|
26674
|
+
this.onIdentityRejected();
|
|
26675
|
+
}
|
|
26676
|
+
return;
|
|
26677
|
+
}
|
|
26569
26678
|
log.trace("host-agent", "heartbeat failed", err);
|
|
26570
26679
|
}
|
|
26571
26680
|
}
|
|
@@ -26595,6 +26704,16 @@ var HostAgentSupervisor = class {
|
|
|
26595
26704
|
this.stopChild(cmd.payload.sessionId);
|
|
26596
26705
|
return;
|
|
26597
26706
|
}
|
|
26707
|
+
if (cmd.type === "self_hosted_wipe") {
|
|
26708
|
+
log.warn("host-agent", `self_hosted_wipe received id=${cmd.id} \u2014 de-provisioning`);
|
|
26709
|
+
this.stop();
|
|
26710
|
+
this.disableService();
|
|
26711
|
+
if (!this.healing) {
|
|
26712
|
+
this.healing = true;
|
|
26713
|
+
this.onIdentityRejected();
|
|
26714
|
+
}
|
|
26715
|
+
return;
|
|
26716
|
+
}
|
|
26598
26717
|
log.trace("host-agent", `ignoring unsupported command type=${cmd.type}`);
|
|
26599
26718
|
}
|
|
26600
26719
|
/**
|
|
@@ -26669,17 +26788,31 @@ var HostAgentSupervisor = class {
|
|
|
26669
26788
|
};
|
|
26670
26789
|
async function resolveHostIdentity(enrollToken) {
|
|
26671
26790
|
const existing = loadHostIdentity();
|
|
26791
|
+
if (enrollToken) {
|
|
26792
|
+
await reportProgress({ enrollToken }, "redeeming", "redeeming enrollment token\u2026");
|
|
26793
|
+
try {
|
|
26794
|
+
const identity = await redeemEnrollToken(enrollToken);
|
|
26795
|
+
saveHostIdentity(identity);
|
|
26796
|
+
await reportProgress(
|
|
26797
|
+
{ hostId: identity.hostId, hostToken: identity.hostToken },
|
|
26798
|
+
"enrolled",
|
|
26799
|
+
"host enrolled"
|
|
26800
|
+
);
|
|
26801
|
+
return identity;
|
|
26802
|
+
} catch (err) {
|
|
26803
|
+
if (existing) {
|
|
26804
|
+
log.trace(
|
|
26805
|
+
"host-agent",
|
|
26806
|
+
"enroll-token redeem failed; reusing sealed identity (likely a restart)",
|
|
26807
|
+
err
|
|
26808
|
+
);
|
|
26809
|
+
return existing;
|
|
26810
|
+
}
|
|
26811
|
+
throw err;
|
|
26812
|
+
}
|
|
26813
|
+
}
|
|
26672
26814
|
if (existing) return existing;
|
|
26673
|
-
|
|
26674
|
-
await reportProgress({ enrollToken }, "redeeming", "redeeming enrollment token\u2026");
|
|
26675
|
-
const identity = await redeemEnrollToken(enrollToken);
|
|
26676
|
-
saveHostIdentity(identity);
|
|
26677
|
-
await reportProgress(
|
|
26678
|
-
{ hostId: identity.hostId, hostToken: identity.hostToken },
|
|
26679
|
-
"enrolled",
|
|
26680
|
-
"host enrolled"
|
|
26681
|
-
);
|
|
26682
|
-
return identity;
|
|
26815
|
+
return null;
|
|
26683
26816
|
}
|
|
26684
26817
|
async function hostAgent(args2 = []) {
|
|
26685
26818
|
const tokenArg = args2.find((a) => a.startsWith("--token="));
|
|
@@ -26875,7 +27008,7 @@ function checkChokidar() {
|
|
|
26875
27008
|
}
|
|
26876
27009
|
async function doctor(args2 = []) {
|
|
26877
27010
|
const json = args2.includes("--json");
|
|
26878
|
-
const cliVersion = true ? "2.39.
|
|
27011
|
+
const cliVersion = true ? "2.39.31" : "0.0.0-dev";
|
|
26879
27012
|
const apiBase2 = resolveApiBaseUrl();
|
|
26880
27013
|
const diagnosticId = (0, import_node_crypto8.randomUUID)();
|
|
26881
27014
|
log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
|
|
@@ -27074,7 +27207,7 @@ async function completion(args2) {
|
|
|
27074
27207
|
// src/commands/version.ts
|
|
27075
27208
|
var import_picocolors13 = __toESM(require("picocolors"));
|
|
27076
27209
|
function version2() {
|
|
27077
|
-
const v = true ? "2.39.
|
|
27210
|
+
const v = true ? "2.39.31" : "unknown";
|
|
27078
27211
|
console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
|
|
27079
27212
|
}
|
|
27080
27213
|
|
|
@@ -27360,7 +27493,7 @@ function checkForUpdates() {
|
|
|
27360
27493
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
27361
27494
|
if (process.env.CI) return;
|
|
27362
27495
|
if (!process.stdout.isTTY) return;
|
|
27363
|
-
const current = true ? "2.39.
|
|
27496
|
+
const current = true ? "2.39.31" : null;
|
|
27364
27497
|
if (!current) return;
|
|
27365
27498
|
const cache = readCache();
|
|
27366
27499
|
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.39.
|
|
3
|
+
"version": "2.39.31",
|
|
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",
|