codeam-cli 2.39.31 → 2.39.33

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 +12 -0
  2. package/dist/index.js +194 -55
  3. 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.32] — 2026-06-18
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Host-agent deploy clones private repos + reports progress, no silent hang
12
+
13
+ ## [2.39.31] — 2026-06-18
14
+
15
+ ### Fixed
16
+
17
+ - **cli:** Host-agent re-enrollment + self-heal on deleted host
18
+
7
19
  ## [2.39.30] — 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.31",
501
+ version: "2.39.33",
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.31" : "0.0.0-dev",
5911
+ cliVersion: true ? "2.39.33" : "0.0.0-dev",
5912
5912
  nodeVersion: process.version,
5913
5913
  platform: process.platform,
5914
5914
  arch: process.arch,
@@ -14705,6 +14705,14 @@ function buildAcpPromptBlocks(payload) {
14705
14705
  return blocks;
14706
14706
  }
14707
14707
 
14708
+ // src/agents/acp/reconcileDelta.ts
14709
+ function reconcileCumulative(existing, incoming) {
14710
+ if (incoming.length === 0) return existing;
14711
+ if (incoming.startsWith(existing)) return incoming;
14712
+ if (existing.startsWith(incoming)) return existing;
14713
+ return existing + incoming;
14714
+ }
14715
+
14708
14716
  // src/agents/acp/onboarding.ts
14709
14717
  var fs22 = __toESM(require("fs"));
14710
14718
  var os23 = __toESM(require("os"));
@@ -20639,6 +20647,18 @@ var StreamingState = class {
20639
20647
  this.publisher = publisher;
20640
20648
  }
20641
20649
  publisher;
20650
+ /**
20651
+ * Cumulative agent reply for the in-progress turn — the body of the
20652
+ * chat bubble (`/api/commands/output` text events). DERIVED from the
20653
+ * per-chunkId text buffers in {@link streamingChunks} via
20654
+ * {@link recomputeText} on every `append`, NOT accumulated with a
20655
+ * blind `+=`. That derivation is what makes the chat pipe correct for
20656
+ * adapters that send cumulative snapshots (a self-hosted MiniMax proxy
20657
+ * behind claude-agent-acp) as well as true-delta adapters (Anthropic
20658
+ * Claude): `reconcileCumulative` collapses a re-sent snapshot instead
20659
+ * of concatenating the reply with itself (the "…hoy?¡Hola!…hoy?"
20660
+ * intra-reply duplication bug).
20661
+ */
20642
20662
  text = "";
20643
20663
  pending = null;
20644
20664
  /**
@@ -20754,19 +20774,19 @@ var StreamingState = class {
20754
20774
  await this.publisher.publishOutput({ type: "new_turn", done: false });
20755
20775
  }
20756
20776
  append(delta) {
20757
- if (delta.kind === "text") {
20758
- this.text += delta.delta;
20759
- void this.publisher.publishOutput({ type: "text", content: this.text, done: false });
20760
- }
20761
20777
  const existing = this.streamingChunks.get(delta.chunkId);
20762
- const cumulativeContent = (existing?.content ?? "") + delta.delta;
20763
20778
  if (existing && existing.kind !== delta.kind) {
20764
20779
  log.warn(
20765
20780
  "acpRunner",
20766
20781
  `streaming-chunk kind flip chunkId=${delta.chunkId.slice(0, 8)} from=${existing.kind} to=${delta.kind}`
20767
20782
  );
20768
20783
  }
20784
+ const cumulativeContent = reconcileCumulative(existing?.content ?? "", delta.delta);
20769
20785
  this.streamingChunks.set(delta.chunkId, { kind: delta.kind, content: cumulativeContent });
20786
+ if (delta.kind === "text") {
20787
+ this.recomputeText();
20788
+ void this.publisher.publishOutput({ type: "text", content: this.text, done: false });
20789
+ }
20770
20790
  void this.publisher.publishStreamingChunk({
20771
20791
  chunkId: delta.chunkId,
20772
20792
  kind: delta.kind,
@@ -20774,6 +20794,20 @@ var StreamingState = class {
20774
20794
  isFinal: false
20775
20795
  });
20776
20796
  }
20797
+ /**
20798
+ * Rebuild the cumulative chat-bubble text from the per-chunkId text
20799
+ * buffers, in arrival order (Map preserves insertion order). Source
20800
+ * of truth for the `text` field — never accumulated incrementally, so
20801
+ * a re-sent snapshot updates its own chunk in place rather than
20802
+ * lengthening the reply.
20803
+ */
20804
+ recomputeText() {
20805
+ let next = "";
20806
+ for (const { kind, content } of this.streamingChunks.values()) {
20807
+ if (kind === "text") next += content;
20808
+ }
20809
+ this.text = next;
20810
+ }
20777
20811
  /**
20778
20812
  * Flip the chat out of "Thinking…" with one final cumulative
20779
20813
  * `done: true`. Idempotent — safe to call from happy + error +
@@ -26393,6 +26427,30 @@ async function reportProgress(auth, step, message) {
26393
26427
  } catch {
26394
26428
  }
26395
26429
  }
26430
+ async function reportDeployProgress(auth, deployId, step, message, sessionId) {
26431
+ try {
26432
+ const controller = new AbortController();
26433
+ const timer = setTimeout(() => controller.abort(), PROGRESS_TIMEOUT_MS);
26434
+ timer.unref?.();
26435
+ try {
26436
+ await fetch(`${apiBase()}/api/self-hosted/deploy-progress`, {
26437
+ method: "POST",
26438
+ headers: { "Content-Type": "application/json", ...vercelBypassHeader() },
26439
+ body: JSON.stringify({
26440
+ ...auth,
26441
+ deployId,
26442
+ step,
26443
+ message,
26444
+ ...sessionId ? { sessionId } : {}
26445
+ }),
26446
+ signal: controller.signal
26447
+ });
26448
+ } finally {
26449
+ clearTimeout(timer);
26450
+ }
26451
+ } catch {
26452
+ }
26453
+ }
26396
26454
 
26397
26455
  // src/commands/host.ts
26398
26456
  function readTokenFlag(args2) {
@@ -26455,12 +26513,46 @@ function isAbsolutePathTarget(target) {
26455
26513
  function selfHostedWorkspaceRoot() {
26456
26514
  return path54.join(os33.homedir(), ".codeam", "self-hosted");
26457
26515
  }
26458
- function repoCloneUrl(repoRef) {
26516
+ function nonInteractiveGitEnv() {
26517
+ return {
26518
+ ...process.env,
26519
+ GIT_TERMINAL_PROMPT: "0",
26520
+ GIT_ASKPASS: "",
26521
+ GCM_INTERACTIVE: "never"
26522
+ };
26523
+ }
26524
+ function githubOwnerRepo(repoRef) {
26459
26525
  const trimmed = repoRef.trim();
26526
+ const shorthand = /^([^/\s]+)\/([^/\s]+?)(?:\.git)?$/.exec(trimmed);
26527
+ if (shorthand && !/^https?:\/\//.test(trimmed) && !trimmed.startsWith("git@")) {
26528
+ return { owner: shorthand[1], repo: shorthand[2] };
26529
+ }
26530
+ const httpsMatch = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(trimmed);
26531
+ if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
26532
+ return null;
26533
+ }
26534
+ function repoCloneUrl(repoRef, cloneToken) {
26535
+ const trimmed = repoRef.trim();
26536
+ if (cloneToken) {
26537
+ const gh = githubOwnerRepo(trimmed);
26538
+ if (gh) {
26539
+ return `https://x-access-token:${cloneToken}@github.com/${gh.owner}/${gh.repo}.git`;
26540
+ }
26541
+ }
26460
26542
  if (/^https?:\/\//.test(trimmed) || trimmed.startsWith("git@")) return trimmed;
26461
26543
  return `https://github.com/${trimmed.replace(/\.git$/, "")}.git`;
26462
26544
  }
26463
- async function prepareWorkspace(repoOrPath, deployId) {
26545
+ function maskCloneUrl(url) {
26546
+ return url.replace(/(https?:\/\/)[^@/]+@/, "$1***@");
26547
+ }
26548
+ function maskToken(text, cloneToken) {
26549
+ const masked = maskCloneUrl(text);
26550
+ if (cloneToken && cloneToken.length > 0) {
26551
+ return masked.split(cloneToken).join("***");
26552
+ }
26553
+ return masked;
26554
+ }
26555
+ async function prepareWorkspace(repoOrPath, deployId, cloneToken) {
26464
26556
  if (isAbsolutePathTarget(repoOrPath)) {
26465
26557
  if (!fs42.existsSync(repoOrPath)) {
26466
26558
  throw new Error(`deploy target path does not exist: ${repoOrPath}`);
@@ -26472,10 +26564,17 @@ async function prepareWorkspace(repoOrPath, deployId) {
26472
26564
  return dest;
26473
26565
  }
26474
26566
  fs42.mkdirSync(selfHostedWorkspaceRoot(), { recursive: true, mode: 448 });
26475
- await execFileP9("git", ["clone", "--depth", "1", repoCloneUrl(repoOrPath), dest], {
26476
- timeout: 12e4,
26477
- maxBuffer: 16 * 1024 * 1024
26478
- });
26567
+ const cloneUrl = repoCloneUrl(repoOrPath, cloneToken);
26568
+ try {
26569
+ await execFileP9("git", ["clone", "--depth", "1", cloneUrl, dest], {
26570
+ timeout: 12e4,
26571
+ maxBuffer: 16 * 1024 * 1024,
26572
+ env: nonInteractiveGitEnv()
26573
+ });
26574
+ } catch (err) {
26575
+ const reason = err instanceof Error ? err.message : String(err);
26576
+ throw new Error(`git clone failed for ${maskCloneUrl(cloneUrl)}: ${maskToken(reason, cloneToken)}`);
26577
+ }
26479
26578
  return dest;
26480
26579
  }
26481
26580
 
@@ -26560,6 +26659,9 @@ function isDeployPayload(p2) {
26560
26659
  if (typeof p2.deployId !== "string" || typeof p2.repoOrPath !== "string" || typeof p2.agentId !== "string" || typeof p2.autoPairToken !== "string") {
26561
26660
  return false;
26562
26661
  }
26662
+ if (p2.cloneToken !== void 0 && typeof p2.cloneToken !== "string") {
26663
+ return false;
26664
+ }
26563
26665
  const hasHouse = isHouseProxy(p2.houseProxy);
26564
26666
  const hasSealed = typeof p2.sealedAgentAuth === "string";
26565
26667
  return hasHouse || hasSealed;
@@ -26578,7 +26680,7 @@ var CONTROL_AGENT_META = {
26578
26680
  var defaultSpawner = (env, cwd, args2 = []) => (0, import_node_child_process13.spawn)(process.execPath, [process.argv[1], "pair-auto", ...args2], {
26579
26681
  cwd,
26580
26682
  env: { ...process.env, ...env },
26581
- stdio: "ignore",
26683
+ stdio: ["ignore", "pipe", "pipe"],
26582
26684
  detached: false
26583
26685
  });
26584
26686
  var defaultOnIdentityRejected = () => {
@@ -26731,43 +26833,86 @@ var HostAgentSupervisor = class {
26731
26833
  "host-agent",
26732
26834
  `deploy id=${payload.deployId.slice(0, 8)} agent=${payload.agentId} target=${payload.repoOrPath}`
26733
26835
  );
26734
- const cwd = await prepareWorkspace(payload.repoOrPath, payload.deployId);
26735
- let childEnv;
26736
- let extraArgs = [];
26737
- if (payload.houseProxy) {
26738
- const { baseUrl, token, agentKind } = payload.houseProxy;
26739
- childEnv = {
26740
- ANTHROPIC_BASE_URL: baseUrl,
26741
- ANTHROPIC_AUTH_TOKEN: token,
26742
- ANTHROPIC_MODEL: "MiniMax-M3",
26743
- ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M3",
26744
- ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M3",
26745
- ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M3",
26746
- CLAUDE_CODE_AUTO_COMPACT_WINDOW: "512000",
26747
- API_TIMEOUT_MS: "3000000",
26748
- CODEAM_AUTO_TOKEN: payload.autoPairToken
26749
- };
26750
- extraArgs = [`--agent=${agentKind || "claude"}`];
26751
- } else {
26752
- const auth = await this.resolveAgentAuth(this.identity, payload.sealedAgentAuth);
26753
- const credEnv = provisionAgentCredentials(payload.agentId, auth, void 0);
26754
- childEnv = {
26755
- ...credEnv,
26756
- CODEAM_AUTO_TOKEN: payload.autoPairToken
26836
+ const report = (step, message) => {
26837
+ void reportDeployProgress(
26838
+ { hostId: this.identity.hostId, hostToken: this.identity.hostToken },
26839
+ payload.deployId,
26840
+ step,
26841
+ message
26842
+ );
26843
+ };
26844
+ try {
26845
+ report("preparing", "preparing workspace");
26846
+ if (!isAbsolutePathTarget(payload.repoOrPath)) {
26847
+ report("cloning", "cloning repository");
26848
+ }
26849
+ const cwd = await prepareWorkspace(payload.repoOrPath, payload.deployId, payload.cloneToken);
26850
+ let childEnv;
26851
+ let extraArgs = [];
26852
+ if (payload.houseProxy) {
26853
+ const { baseUrl, token, agentKind } = payload.houseProxy;
26854
+ childEnv = {
26855
+ ANTHROPIC_BASE_URL: baseUrl,
26856
+ ANTHROPIC_AUTH_TOKEN: token,
26857
+ ANTHROPIC_MODEL: "MiniMax-M3",
26858
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M3",
26859
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M3",
26860
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M3",
26861
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: "512000",
26862
+ API_TIMEOUT_MS: "3000000",
26863
+ CODEAM_AUTO_TOKEN: payload.autoPairToken
26864
+ };
26865
+ extraArgs = [`--agent=${agentKind || "claude"}`];
26866
+ } else {
26867
+ const auth = await this.resolveAgentAuth(this.identity, payload.sealedAgentAuth);
26868
+ const credEnv = provisionAgentCredentials(payload.agentId, auth, void 0);
26869
+ childEnv = {
26870
+ ...credEnv,
26871
+ CODEAM_AUTO_TOKEN: payload.autoPairToken
26872
+ };
26873
+ }
26874
+ report("spawning", "starting agent");
26875
+ const proc = this.spawnChild(childEnv, cwd, extraArgs);
26876
+ const child = { deployId: payload.deployId, proc };
26877
+ this.children.set(payload.deployId, child);
26878
+ let tail = "";
26879
+ const appendTail = (buf) => {
26880
+ tail = (tail + buf.toString("utf8")).slice(-2e3);
26757
26881
  };
26758
- }
26759
- const proc = this.spawnChild(childEnv, cwd, extraArgs);
26760
- const child = { deployId: payload.deployId, sessionId: payload.deployId, proc };
26761
- this.children.set(payload.deployId, child);
26762
- proc.once("exit", () => {
26763
- if (this.children.get(payload.deployId)?.proc === proc) {
26882
+ proc.stdout?.on("data", appendTail);
26883
+ proc.stderr?.on("data", appendTail);
26884
+ report("agent_starting", "agent process started");
26885
+ proc.once("exit", (code) => {
26886
+ const tracked = this.children.get(payload.deployId)?.proc === proc;
26887
+ if (tracked) {
26888
+ this.children.delete(payload.deployId);
26889
+ }
26890
+ if (tracked && typeof code === "number" && code !== 0) {
26891
+ const detail = tail.trim().slice(-500);
26892
+ report("failed", detail ? `agent exited (${code}): ${detail}` : `agent exited (${code})`);
26893
+ }
26894
+ });
26895
+ } catch (err) {
26896
+ const message = err instanceof Error ? err.message : String(err);
26897
+ log.warn("host-agent", `deploy ${payload.deployId.slice(0, 8)} failed: ${message}`);
26898
+ const existing = this.children.get(payload.deployId);
26899
+ if (existing) {
26900
+ try {
26901
+ existing.proc.kill("SIGTERM");
26902
+ } catch {
26903
+ }
26764
26904
  this.children.delete(payload.deployId);
26765
26905
  }
26766
- });
26906
+ report("failed", message);
26907
+ }
26767
26908
  }
26768
- /** Kill the child matching `sessionId` (or its deployId). No-op if absent. */
26909
+ /**
26910
+ * Kill the child for the given id. The backend correlates the session it
26911
+ * sends to this deploy, so the id matches the deployId we keyed on. No-op
26912
+ * if absent.
26913
+ */
26769
26914
  stopChild(sessionId) {
26770
- const child = this.children.get(sessionId) ?? this.findBySessionId(sessionId);
26915
+ const child = this.children.get(sessionId);
26771
26916
  if (!child) {
26772
26917
  log.trace("host-agent", `stop: no child for sessionId=${sessionId}`);
26773
26918
  return;
@@ -26779,12 +26924,6 @@ var HostAgentSupervisor = class {
26779
26924
  }
26780
26925
  this.children.delete(child.deployId);
26781
26926
  }
26782
- findBySessionId(sessionId) {
26783
- for (const child of this.children.values()) {
26784
- if (child.sessionId === sessionId) return child;
26785
- }
26786
- return void 0;
26787
- }
26788
26927
  };
26789
26928
  async function resolveHostIdentity(enrollToken) {
26790
26929
  const existing = loadHostIdentity();
@@ -27008,7 +27147,7 @@ function checkChokidar() {
27008
27147
  }
27009
27148
  async function doctor(args2 = []) {
27010
27149
  const json = args2.includes("--json");
27011
- const cliVersion = true ? "2.39.31" : "0.0.0-dev";
27150
+ const cliVersion = true ? "2.39.33" : "0.0.0-dev";
27012
27151
  const apiBase2 = resolveApiBaseUrl();
27013
27152
  const diagnosticId = (0, import_node_crypto8.randomUUID)();
27014
27153
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -27207,7 +27346,7 @@ async function completion(args2) {
27207
27346
  // src/commands/version.ts
27208
27347
  var import_picocolors13 = __toESM(require("picocolors"));
27209
27348
  function version2() {
27210
- const v = true ? "2.39.31" : "unknown";
27349
+ const v = true ? "2.39.33" : "unknown";
27211
27350
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
27212
27351
  }
27213
27352
 
@@ -27493,7 +27632,7 @@ function checkForUpdates() {
27493
27632
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
27494
27633
  if (process.env.CI) return;
27495
27634
  if (!process.stdout.isTTY) return;
27496
- const current = true ? "2.39.31" : null;
27635
+ const current = true ? "2.39.33" : null;
27497
27636
  if (!current) return;
27498
27637
  const cache = readCache();
27499
27638
  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.31",
3
+ "version": "2.39.33",
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",