codeam-cli 2.67.7 → 2.68.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ 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.67.6] — 2026-08-27
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Ordena los POST del ciclo de vida del preview
12
+ - **cli:** Re-afirma el preview en el heartbeat para que su snapshot no caduque
13
+
7
14
  ## [2.67.5] — 2026-08-27
8
15
 
9
16
  ### Fixed
package/dist/index.js CHANGED
@@ -8081,7 +8081,7 @@ function readAnonId() {
8081
8081
  }
8082
8082
  function superProperties() {
8083
8083
  return {
8084
- cliVersion: true ? "2.67.7" : "0.0.0-dev",
8084
+ cliVersion: true ? "2.68.0" : "0.0.0-dev",
8085
8085
  nodeVersion: process.version,
8086
8086
  platform: process.platform,
8087
8087
  arch: process.arch,
@@ -8323,7 +8323,7 @@ var http = __toESM(require("http"));
8323
8323
  // package.json
8324
8324
  var package_default = {
8325
8325
  name: "codeam-cli",
8326
- version: "2.67.7",
8326
+ version: "2.68.0",
8327
8327
  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
8328
  type: "commonjs",
8329
8329
  main: "dist/index.js",
@@ -8345,7 +8345,8 @@ var package_default = {
8345
8345
  "test:coverage": "vitest run --coverage",
8346
8346
  typecheck: "tsc --noEmit",
8347
8347
  postinstall: "node dist/postinstall.js || true",
8348
- prepublishOnly: "npm run build"
8348
+ prepublishOnly: "npm run build",
8349
+ e2e: "playwright test"
8349
8350
  },
8350
8351
  keywords: [
8351
8352
  "claude",
@@ -8413,11 +8414,14 @@ var package_default = {
8413
8414
  },
8414
8415
  devDependencies: {
8415
8416
  "@codeam/shared": "*",
8417
+ "@playwright/test": "^1.62.1",
8418
+ "@types/jsdom": "^30.0.0",
8416
8419
  "@types/node": "^22.0.0",
8417
8420
  "@types/qrcode-terminal": "^0.12.0",
8418
8421
  "@types/which": "^3.0.4",
8419
8422
  "@types/ws": "^8.5.0",
8420
8423
  "@vitest/coverage-v8": "^4.1.7",
8424
+ jsdom: "^29.1.1",
8421
8425
  "node-pty": "^1.1.0",
8422
8426
  tsup: "^8.0.0",
8423
8427
  typescript: "^7.0.2",
@@ -9855,7 +9859,7 @@ var CommandRelayService = class _CommandRelayService {
9855
9859
  // fresh + clear the "CLI update available" banner after a self-update
9856
9860
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9857
9861
  // pair/reconnect). Older backends ignore the extra field.
9858
- ..."2.67.7" ? { ideVersion: "2.67.7" } : {}
9862
+ ..."2.68.0" ? { ideVersion: "2.68.0" } : {}
9859
9863
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9860
9864
  }
9861
9865
  /**
@@ -14058,7 +14062,7 @@ function subscribeToPairCompletion(pluginId, onPaired, onTimeout, pollSecret) {
14058
14062
  onPaired(info);
14059
14063
  };
14060
14064
  dispatchers.add(dispatch);
14061
- const connect2 = () => {
14065
+ const connect3 = () => {
14062
14066
  if (stopped) return;
14063
14067
  const url2 = new URL(`${API_BASE5}/api/commands/pending/stream`);
14064
14068
  url2.searchParams.set("pluginId", pluginId);
@@ -14084,7 +14088,7 @@ function subscribeToPairCompletion(pluginId, onPaired, onTimeout, pollSecret) {
14084
14088
  `sse status=${res.statusCode}; will reconnect in 1s`
14085
14089
  );
14086
14090
  res.resume();
14087
- setTimeout(connect2, 1e3);
14091
+ setTimeout(connect3, 1e3);
14088
14092
  return;
14089
14093
  }
14090
14094
  let buf = "";
@@ -14099,23 +14103,23 @@ function subscribeToPairCompletion(pluginId, onPaired, onTimeout, pollSecret) {
14099
14103
  }
14100
14104
  });
14101
14105
  res.on("end", () => {
14102
- if (!stopped) setTimeout(connect2, 250);
14106
+ if (!stopped) setTimeout(connect3, 250);
14103
14107
  });
14104
14108
  res.on("error", () => {
14105
- if (!stopped) setTimeout(connect2, 1e3);
14109
+ if (!stopped) setTimeout(connect3, 1e3);
14106
14110
  });
14107
14111
  }
14108
14112
  );
14109
14113
  req.on("error", () => {
14110
- if (!stopped) setTimeout(connect2, 1e3);
14114
+ if (!stopped) setTimeout(connect3, 1e3);
14111
14115
  });
14112
14116
  req.on("timeout", () => {
14113
14117
  req?.destroy();
14114
- if (!stopped) setTimeout(connect2, 250);
14118
+ if (!stopped) setTimeout(connect3, 250);
14115
14119
  });
14116
14120
  req.end();
14117
14121
  };
14118
- connect2();
14122
+ connect3();
14119
14123
  timeoutTimer = setTimeout(() => {
14120
14124
  stop();
14121
14125
  onTimeout();
@@ -22114,7 +22118,7 @@ async function autoUpgradeBeforeCriticalCommand() {
22114
22118
  if (process.env.NODE_ENV === "test") return;
22115
22119
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22116
22120
  if (process.env.CI) return;
22117
- const current2 = true ? "2.67.7" : null;
22121
+ const current2 = true ? "2.68.0" : null;
22118
22122
  if (!current2) return;
22119
22123
  const cache = readCache();
22120
22124
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22131,7 +22135,7 @@ function checkForUpdates() {
22131
22135
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22132
22136
  if (process.env.CI) return;
22133
22137
  if (!process.stdout.isTTY) return;
22134
- const current2 = true ? "2.67.7" : null;
22138
+ const current2 = true ? "2.68.0" : null;
22135
22139
  if (!current2) return;
22136
22140
  const cache = readCache();
22137
22141
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22154,7 +22158,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
22154
22158
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
22155
22159
  var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
22156
22160
  function currentCliVersion() {
22157
- return true ? "2.67.7" : null;
22161
+ return true ? "2.68.0" : null;
22158
22162
  }
22159
22163
  async function installedVersion(deps) {
22160
22164
  const ls = await deps.run(
@@ -25103,7 +25107,7 @@ function registerPreview(sessionId, preview) {
25103
25107
  }
25104
25108
  function killProcessTree(child, signal = "SIGTERM") {
25105
25109
  const pid = child.pid;
25106
- if (pid == null) return;
25110
+ if (!pid) return;
25107
25111
  if (process.platform !== "win32") {
25108
25112
  try {
25109
25113
  process.kill(-pid, signal);
@@ -25125,6 +25129,10 @@ async function killPreview(sessionId) {
25125
25129
  killProcessTree(preview.tunnel, "SIGTERM");
25126
25130
  }
25127
25131
  await new Promise((r) => setTimeout(r, 100));
25132
+ try {
25133
+ await preview.inspector?.close();
25134
+ } catch {
25135
+ }
25128
25136
  killProcessTree(preview.devServer, "SIGTERM");
25129
25137
  const sigkillTimer = setTimeout(() => {
25130
25138
  killProcessTree(preview.devServer, "SIGKILL");
@@ -25309,6 +25317,336 @@ async function resolveNamedTunnel(ctx, fetchTunnel) {
25309
25317
  }
25310
25318
  }
25311
25319
 
25320
+ // src/services/preview/inspector-proxy.ts
25321
+ var http6 = __toESM(require("http"));
25322
+ var net3 = __toESM(require("net"));
25323
+ var INJECTION_SCAN_BUDGET_BYTES = 64 * 1024;
25324
+ var INJECTION_ANCHORS = ["</head>", "<body>", "</body>"];
25325
+ function wantsHtml(accept) {
25326
+ return typeof accept === "string" && accept.includes("text/html");
25327
+ }
25328
+ function isHtmlResponse(contentType) {
25329
+ return typeof contentType === "string" && contentType.toLowerCase().includes("text/html");
25330
+ }
25331
+ function injectAt(html, script) {
25332
+ for (const anchor of INJECTION_ANCHORS) {
25333
+ const at3 = html.indexOf(anchor);
25334
+ if (at3 === -1) continue;
25335
+ const insertAt = anchor.startsWith("</") ? at3 : at3 + anchor.length;
25336
+ return html.slice(0, insertAt) + script + html.slice(insertAt);
25337
+ }
25338
+ return null;
25339
+ }
25340
+ async function startInspectorProxy(opts) {
25341
+ const targetHost = opts.targetHost ?? "127.0.0.1";
25342
+ const upgraded = /* @__PURE__ */ new Set();
25343
+ const server = http6.createServer((req, res) => {
25344
+ const inject = wantsHtml(req.headers.accept);
25345
+ const headers = { ...req.headers };
25346
+ if (inject) delete headers["accept-encoding"];
25347
+ const upstream = http6.request(
25348
+ { host: targetHost, port: opts.targetPort, method: req.method, path: req.url, headers },
25349
+ (originRes) => {
25350
+ const outHeaders = { ...originRes.headers };
25351
+ const willInject = inject && isHtmlResponse(originRes.headers["content-type"]);
25352
+ if (willInject) delete outHeaders["content-length"];
25353
+ res.writeHead(originRes.statusCode ?? 502, outHeaders);
25354
+ if (!willInject) {
25355
+ originRes.pipe(res);
25356
+ return;
25357
+ }
25358
+ let buffered = "";
25359
+ let done = false;
25360
+ originRes.setEncoding("utf8");
25361
+ originRes.on("data", (chunk) => {
25362
+ if (done) {
25363
+ res.write(chunk);
25364
+ return;
25365
+ }
25366
+ buffered += chunk;
25367
+ const injected = injectAt(buffered, opts.script);
25368
+ if (injected !== null) {
25369
+ done = true;
25370
+ res.write(injected);
25371
+ buffered = "";
25372
+ return;
25373
+ }
25374
+ if (buffered.length >= INJECTION_SCAN_BUDGET_BYTES) {
25375
+ done = true;
25376
+ res.write(buffered);
25377
+ buffered = "";
25378
+ }
25379
+ });
25380
+ originRes.on("end", () => {
25381
+ if (!done && buffered.length > 0) res.write(buffered);
25382
+ res.end();
25383
+ });
25384
+ }
25385
+ );
25386
+ upstream.on("error", () => {
25387
+ if (!res.headersSent) res.writeHead(502, { "content-type": "text/plain" });
25388
+ res.end("preview origin unreachable");
25389
+ });
25390
+ req.pipe(upstream);
25391
+ });
25392
+ server.on("upgrade", (req, clientSocket, head) => {
25393
+ upgraded.add(clientSocket);
25394
+ const upstream = net3.connect(opts.targetPort, targetHost, () => {
25395
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
25396
+ for (const [k2, v] of Object.entries(req.headers)) {
25397
+ for (const one of Array.isArray(v) ? v : [v]) {
25398
+ if (one !== void 0) lines.push(`${k2}: ${one}`);
25399
+ }
25400
+ }
25401
+ upstream.write(`${lines.join("\r\n")}\r
25402
+ \r
25403
+ `);
25404
+ if (head?.length) upstream.write(head);
25405
+ upstream.pipe(clientSocket);
25406
+ clientSocket.pipe(upstream);
25407
+ });
25408
+ const drop = () => {
25409
+ upgraded.delete(clientSocket);
25410
+ upstream.destroy();
25411
+ clientSocket.destroy();
25412
+ };
25413
+ upstream.on("error", drop);
25414
+ upstream.on("close", drop);
25415
+ clientSocket.on("error", drop);
25416
+ clientSocket.on("close", drop);
25417
+ });
25418
+ await new Promise((resolve10, reject) => {
25419
+ server.once("error", reject);
25420
+ server.listen(0, "127.0.0.1", () => resolve10());
25421
+ });
25422
+ const port = server.address().port;
25423
+ return {
25424
+ port,
25425
+ close: () => new Promise((resolve10) => {
25426
+ for (const socket of upgraded) socket.destroy();
25427
+ upgraded.clear();
25428
+ server.closeAllConnections?.();
25429
+ server.close(() => resolve10());
25430
+ })
25431
+ };
25432
+ }
25433
+
25434
+ // src/services/preview/inspector-client.ts
25435
+ var INSPECTOR_CHANNEL = "codeam-inspector";
25436
+ function inspectorClientSource(opts) {
25437
+ const origins = JSON.stringify(opts.allowedOrigins);
25438
+ const channel = JSON.stringify(INSPECTOR_CHANNEL);
25439
+ return `<script id="codeam-inspector" data-codeam="inspector">
25440
+ (function () {
25441
+ 'use strict';
25442
+ try {
25443
+ var CHANNEL = ${channel};
25444
+ var ALLOWED = ${origins};
25445
+
25446
+ var enabled = false;
25447
+ var driver = null; // el origen que nos encendio
25448
+ var overlay = null;
25449
+ var label = null;
25450
+ var current = null;
25451
+
25452
+ function allowed(origin) {
25453
+ return ALLOWED.indexOf(origin) !== -1;
25454
+ }
25455
+
25456
+ function send(type, payload) {
25457
+ if (!driver || window.parent === window) return;
25458
+ var msg = { source: CHANNEL, type: type };
25459
+ for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) msg[k] = payload[k];
25460
+ window.parent.postMessage(msg, driver);
25461
+ }
25462
+
25463
+ // El resaltado. Un div fijo por encima de todo que NUNCA captura el
25464
+ // puntero: si lo capturara, el elemento de debajo dejaria de recibir el
25465
+ // mousemove y el resaltado se quedaria pegado al primero que tocara.
25466
+ function ensureOverlay() {
25467
+ if (overlay) return;
25468
+ overlay = document.createElement('div');
25469
+ overlay.setAttribute('data-codeam', 'inspector-overlay');
25470
+ overlay.style.cssText = [
25471
+ 'position:fixed', 'pointer-events:none', 'z-index:2147483647',
25472
+ 'border:1px solid #a855f7', 'background:rgba(168,85,247,0.14)',
25473
+ 'border-radius:2px', 'transition:none', 'display:none'
25474
+ ].join(';');
25475
+ label = document.createElement('div');
25476
+ label.setAttribute('data-codeam', 'inspector-label');
25477
+ label.style.cssText = [
25478
+ 'position:fixed', 'pointer-events:none', 'z-index:2147483647',
25479
+ 'background:#a855f7', 'color:#000', 'font:600 11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace',
25480
+ 'padding:2px 6px', 'border-radius:3px', 'white-space:nowrap', 'display:none'
25481
+ ].join(';');
25482
+ document.body.appendChild(overlay);
25483
+ document.body.appendChild(label);
25484
+ }
25485
+
25486
+ function removeOverlay() {
25487
+ if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
25488
+ if (label && label.parentNode) label.parentNode.removeChild(label);
25489
+ overlay = null;
25490
+ label = null;
25491
+ }
25492
+
25493
+ // Fichero y linea del componente de React, cuando el dev server los
25494
+ // expone. Es lo que hace que el agente reciba
25495
+ // "components/whatsapp-button.tsx:42" en vez de "x:0.62 y:0.81".
25496
+ // Solo existe en builds de desarrollo; en produccion devuelve null y se
25497
+ // cae al selector CSS.
25498
+ function reactSource(el) {
25499
+ try {
25500
+ for (var key in el) {
25501
+ if (key.indexOf('__reactFiber$') !== 0) continue;
25502
+ var fiber = el[key];
25503
+ while (fiber) {
25504
+ var src = fiber._debugSource;
25505
+ if (src && src.fileName) {
25506
+ return { file: String(src.fileName), line: Number(src.lineNumber) || null };
25507
+ }
25508
+ fiber = fiber.return;
25509
+ }
25510
+ }
25511
+ } catch (e) { /* nunca romper la pagina por esto */ }
25512
+ return null;
25513
+ }
25514
+
25515
+ // Un selector corto y legible, el respaldo cuando no hay React.
25516
+ function selectorFor(el) {
25517
+ if (el.id) return '#' + el.id;
25518
+ var name = el.tagName ? el.tagName.toLowerCase() : 'node';
25519
+ var cls = (el.className && typeof el.className === 'string')
25520
+ ? el.className.trim().split(/\\s+/).slice(0, 3).join('.')
25521
+ : '';
25522
+ return cls ? name + '.' + cls : name;
25523
+ }
25524
+
25525
+ function describe(el) {
25526
+ var r = el.getBoundingClientRect();
25527
+ var text = (el.textContent || '').replace(/\\s+/g, ' ').trim();
25528
+ return {
25529
+ tag: el.tagName ? el.tagName.toLowerCase() : 'node',
25530
+ id: el.id || null,
25531
+ classes: (el.className && typeof el.className === 'string') ? el.className.trim() : '',
25532
+ selector: selectorFor(el),
25533
+ // Un recorte, no el contenido: describir no es extraer.
25534
+ text: text.length > 120 ? text.slice(0, 120) + '\u2026' : text,
25535
+ rect: { x: r.left, y: r.top, w: r.width, h: r.height },
25536
+ viewport: { w: window.innerWidth, h: window.innerHeight },
25537
+ source: reactSource(el)
25538
+ };
25539
+ }
25540
+
25541
+ function paint(el) {
25542
+ ensureOverlay();
25543
+ var r = el.getBoundingClientRect();
25544
+ overlay.style.display = 'block';
25545
+ overlay.style.left = r.left + 'px';
25546
+ overlay.style.top = r.top + 'px';
25547
+ overlay.style.width = r.width + 'px';
25548
+ overlay.style.height = r.height + 'px';
25549
+ label.style.display = 'block';
25550
+ label.textContent = selectorFor(el) + ' ' + Math.round(r.width) + '\xD7' + Math.round(r.height);
25551
+ // Encima si cabe; si no, dentro. Una etiqueta cortada por el borde
25552
+ // superior no se lee.
25553
+ var above = r.top >= 20;
25554
+ label.style.left = r.left + 'px';
25555
+ label.style.top = (above ? r.top - 20 : r.top + 2) + 'px';
25556
+ }
25557
+
25558
+ function onMove(e) {
25559
+ if (!enabled) return;
25560
+ var el = e.target;
25561
+ if (!el || el === current || !el.getBoundingClientRect) return;
25562
+ if (el.getAttribute && el.getAttribute('data-codeam')) return;
25563
+ current = el;
25564
+ paint(el);
25565
+ send('hover', { element: describe(el) });
25566
+ }
25567
+
25568
+ // Captura, y se traga el evento: mientras el modo esta activo un clic
25569
+ // SELECCIONA, no navega. Sin esto, pulsar un enlace se llevaria el
25570
+ // preview a otra pagina en mitad del gesto.
25571
+ function onClick(e) {
25572
+ if (!enabled) return;
25573
+ var el = e.target;
25574
+ if (el && el.getAttribute && el.getAttribute('data-codeam')) return;
25575
+ e.preventDefault();
25576
+ e.stopPropagation();
25577
+ if (el) send('pick', { element: describe(el) });
25578
+ }
25579
+
25580
+ function enable(origin) {
25581
+ if (enabled) return;
25582
+ enabled = true;
25583
+ driver = origin;
25584
+ document.addEventListener('mousemove', onMove, true);
25585
+ document.addEventListener('click', onClick, true);
25586
+ send('ready', {});
25587
+ }
25588
+
25589
+ function disable() {
25590
+ if (!enabled) return;
25591
+ enabled = false;
25592
+ current = null;
25593
+ document.removeEventListener('mousemove', onMove, true);
25594
+ document.removeEventListener('click', onClick, true);
25595
+ removeOverlay();
25596
+ driver = null;
25597
+ }
25598
+
25599
+ window.addEventListener('message', function (e) {
25600
+ if (!allowed(e.origin)) return;
25601
+ var data = e.data;
25602
+ if (!data || data.source !== CHANNEL) return;
25603
+ if (data.type === 'enable') enable(e.origin);
25604
+ else if (data.type === 'disable') disable();
25605
+ });
25606
+ } catch (e) {
25607
+ // Pase lo que pase, la app del usuario sigue funcionando.
25608
+ }
25609
+ })();
25610
+ </script>`;
25611
+ }
25612
+
25613
+ // src/services/preview/inspector-bringup.ts
25614
+ var DEFAULT_INSPECTOR_ORIGINS = [
25615
+ "https://www.codeagent-mobile.com",
25616
+ "https://codeagent-mobile.com",
25617
+ "https://dev.codeagent-mobile.com"
25618
+ ];
25619
+ function inspectorEnabled(env) {
25620
+ const raw = env.CODEAM_PREVIEW_INSPECTOR;
25621
+ return raw !== "0" && raw !== "false";
25622
+ }
25623
+ function resolveInspectorOrigins(env) {
25624
+ const raw = env.CODEAM_INSPECTOR_ORIGINS;
25625
+ if (!raw) return DEFAULT_INSPECTOR_ORIGINS;
25626
+ const extra = raw.split(",").map((s) => s.trim()).filter(Boolean);
25627
+ return [...DEFAULT_INSPECTOR_ORIGINS, ...extra];
25628
+ }
25629
+ async function bringUpInspector(targetPort, deps = {}) {
25630
+ const env = deps.env ?? process.env;
25631
+ const log2 = deps.log ?? (() => void 0);
25632
+ if (!inspectorEnabled(env)) {
25633
+ log2("preview inspector disabled by CODEAM_PREVIEW_INSPECTOR");
25634
+ return { port: targetPort, proxy: null };
25635
+ }
25636
+ try {
25637
+ const start2 = deps.start ?? startInspectorProxy;
25638
+ const proxy = await start2({
25639
+ targetPort,
25640
+ script: inspectorClientSource({ allowedOrigins: resolveInspectorOrigins(env) })
25641
+ });
25642
+ log2(`preview inspector proxy on :${proxy.port} \u2192 :${targetPort}`);
25643
+ return { port: proxy.port, proxy };
25644
+ } catch (e) {
25645
+ log2(`preview inspector unavailable (${e.message}) \u2014 going direct`);
25646
+ return { port: targetPort, proxy: null };
25647
+ }
25648
+ }
25649
+
25312
25650
  // src/services/preview/start-orchestrator.ts
25313
25651
  var INSTALL_TIMEOUT_MS2 = 5 * 6e4;
25314
25652
  var SETUP_TIMEOUT_MS = 2 * 6e4;
@@ -25420,6 +25758,7 @@ async function runPreviewStart(args2) {
25420
25758
  sessionId,
25421
25759
  devServer: dev.devServer,
25422
25760
  tunnel: tun.tunnel,
25761
+ inspector: tun.inspector,
25423
25762
  url: tun.url,
25424
25763
  framework: detection.framework,
25425
25764
  detection,
@@ -25665,8 +26004,12 @@ async function establishTunnel(ctx, dev) {
25665
26004
  });
25666
26005
  return null;
25667
26006
  }
25668
- return { url: expoUrl, tunnel: null };
26007
+ return { url: expoUrl, tunnel: null, inspector: null };
25669
26008
  }
26009
+ const inspection = await bringUpInspector(detection.port, {
26010
+ log: (m) => log.info("preview", m)
26011
+ });
26012
+ const tunnelPort = inspection.port;
25670
26013
  let bin;
25671
26014
  try {
25672
26015
  bin = await resolveCloudflared();
@@ -25694,7 +26037,7 @@ async function establishTunnel(ctx, dev) {
25694
26037
  if (namedToken && namedHostname) {
25695
26038
  try {
25696
26039
  emitProgress("TUNNEL_STARTING", `named tunnel ${namedHostname}`);
25697
- const candidate = await spawnNamedTunnel(bin, namedToken, detection.port);
26040
+ const candidate = await spawnNamedTunnel(bin, namedToken, tunnelPort);
25698
26041
  const outcome = await awaitTunnelRegistered(
25699
26042
  candidate,
25700
26043
  namedHostname,
@@ -25727,7 +26070,7 @@ async function establishTunnel(ctx, dev) {
25727
26070
  }
25728
26071
  const candidate = (0, import_child_process22.spawn)(
25729
26072
  bin,
25730
- ["tunnel", "--url", `http://localhost:${detection.port}`],
26073
+ ["tunnel", "--url", `http://localhost:${tunnelPort}`],
25731
26074
  { stdio: ["ignore", "pipe", "pipe"] }
25732
26075
  );
25733
26076
  const outcome = await awaitTunnelRegistered(
@@ -25752,7 +26095,7 @@ async function establishTunnel(ctx, dev) {
25752
26095
  });
25753
26096
  return null;
25754
26097
  }
25755
- return { url: parsedUrl, tunnel };
26098
+ return { url: parsedUrl, tunnel, inspector: inspection.proxy };
25756
26099
  }
25757
26100
 
25758
26101
  // src/beads/bd-adapter.ts
@@ -29842,7 +30185,7 @@ var fs67 = __toESM(require("fs"));
29842
30185
  var path74 = __toESM(require("path"));
29843
30186
  var os56 = __toESM(require("os"));
29844
30187
  var https7 = __toESM(require("https"));
29845
- var http6 = __toESM(require("http"));
30188
+ var http7 = __toESM(require("http"));
29846
30189
  var import_zod2 = require("zod");
29847
30190
  var historyRecordSchema = import_zod2.z.object({
29848
30191
  type: import_zod2.z.string().optional(),
@@ -29925,7 +30268,7 @@ function post(endpoint, body, pluginAuthToken) {
29925
30268
  return new Promise((resolve10) => {
29926
30269
  const payload = JSON.stringify(body);
29927
30270
  const u2 = new URL(`${API_BASE8}${endpoint}`);
29928
- const transport = u2.protocol === "https:" ? https7 : http6;
30271
+ const transport = u2.protocol === "https:" ? https7 : http7;
29929
30272
  const req = transport.request(
29930
30273
  {
29931
30274
  hostname: u2.hostname,
@@ -32822,8 +33165,8 @@ var ConnectionBuilder = class {
32822
33165
  if (message.kind !== "request" || message.method !== method) {
32823
33166
  return Handled.no(message);
32824
33167
  }
32825
- const request = parse(message.params);
32826
- return await handler(request, message.responder, cx) ?? Handled.yes();
33168
+ const request2 = parse(message.params);
33169
+ return await handler(request2, message.responder, cx) ?? Handled.yes();
32827
33170
  },
32828
33171
  describe: () => `${this.connectionName ?? "request"}:${method}`
32829
33172
  });
@@ -33384,23 +33727,23 @@ var AsyncQueue = class {
33384
33727
  });
33385
33728
  }
33386
33729
  };
33387
- function cloneNewSessionRequest(request) {
33730
+ function cloneNewSessionRequest(request2) {
33388
33731
  return {
33389
- ...request,
33390
- additionalDirectories: request.additionalDirectories ? [...request.additionalDirectories] : void 0,
33391
- mcpServers: [...request.mcpServers]
33732
+ ...request2,
33733
+ additionalDirectories: request2.additionalDirectories ? [...request2.additionalDirectories] : void 0,
33734
+ mcpServers: [...request2.mcpServers]
33392
33735
  };
33393
33736
  }
33394
33737
  var SessionBuilder = class _SessionBuilder {
33395
33738
  cx;
33396
33739
  request;
33397
- constructor(cx, request) {
33740
+ constructor(cx, request2) {
33398
33741
  this.cx = cx;
33399
- this.request = cloneNewSessionRequest(request);
33742
+ this.request = cloneNewSessionRequest(request2);
33400
33743
  }
33401
33744
  /** @internal */
33402
- static create(cx, request) {
33403
- return new _SessionBuilder(cx, request);
33745
+ static create(cx, request2) {
33746
+ return new _SessionBuilder(cx, request2);
33404
33747
  }
33405
33748
  /**
33406
33749
  * Returns the `session/new` request that will be sent.
@@ -34528,9 +34871,9 @@ function toolCallReferencesInternal(call, homeDir2) {
34528
34871
  }
34529
34872
  return false;
34530
34873
  }
34531
- function internalPathPermissionOutcome(request, homeDir2) {
34532
- if (!toolCallReferencesInternal(request.toolCall, homeDir2)) return null;
34533
- const reject = request.options.find((o) => o.kind === "reject_once") ?? request.options.find((o) => o.kind === "reject_always");
34874
+ function internalPathPermissionOutcome(request2, homeDir2) {
34875
+ if (!toolCallReferencesInternal(request2.toolCall, homeDir2)) return null;
34876
+ const reject = request2.options.find((o) => o.kind === "reject_once") ?? request2.options.find((o) => o.kind === "reject_always");
34534
34877
  if (reject) return { outcome: { outcome: "selected", optionId: reject.optionId } };
34535
34878
  return { outcome: { outcome: "cancelled" } };
34536
34879
  }
@@ -34589,9 +34932,9 @@ function toolPathIsSecret(p2) {
34589
34932
  return !!p2 && SECRET_RE.test(p2);
34590
34933
  }
34591
34934
  var GUARDRAIL_SECRET_READ_BLOCK_REASON = "Reading this secret file is blocked by this session's guardrails (Reading secrets = Deny). Adjust it in the session Guardrails settings if intended.";
34592
- function guardrailDecision(request, policy) {
34593
- if (isProseOnlyToolKind(request.toolCall.kind)) return null;
34594
- const hay = haystack(request.toolCall);
34935
+ function guardrailDecision(request2, policy) {
34936
+ if (isProseOnlyToolKind(request2.toolCall.kind)) return null;
34937
+ const hay = haystack(request2.toolCall);
34595
34938
  if (!hay) return null;
34596
34939
  const matched = matchedCategories(hay);
34597
34940
  if (matched.size === 0) return null;
@@ -34611,7 +34954,7 @@ function guardrailDecision(request, policy) {
34611
34954
  kind: "deny",
34612
34955
  category: denyCat,
34613
34956
  reason: GUARDRAIL_CATEGORY_META[denyCat].description,
34614
- outcome: rejectOutcome(request.options)
34957
+ outcome: rejectOutcome(request2.options)
34615
34958
  };
34616
34959
  }
34617
34960
  if (confirmCat) {
@@ -35876,7 +36219,7 @@ async function performAgentSwitch(deps, rawAgentId, fastPath = {}) {
35876
36219
  }
35877
36220
 
35878
36221
  // src/services/streaming/transport.ts
35879
- var http7 = __toESM(require("http"));
36222
+ var http8 = __toESM(require("http"));
35880
36223
  var https8 = __toESM(require("https"));
35881
36224
  var _transport4 = {
35882
36225
  post: _post3,
@@ -35886,7 +36229,7 @@ function _post3(url2, headers, payload) {
35886
36229
  return new Promise((resolve10, reject) => {
35887
36230
  let settled = false;
35888
36231
  const u2 = new URL(url2);
35889
- const lib = u2.protocol === "https:" ? https8 : http7;
36232
+ const lib = u2.protocol === "https:" ? https8 : http8;
35890
36233
  const req = lib.request(
35891
36234
  {
35892
36235
  hostname: u2.hostname,
@@ -35928,7 +36271,7 @@ function _get(url2, headers) {
35928
36271
  return new Promise((resolve10, reject) => {
35929
36272
  let settled = false;
35930
36273
  const u2 = new URL(url2);
35931
- const lib = u2.protocol === "https:" ? https8 : http7;
36274
+ const lib = u2.protocol === "https:" ? https8 : http8;
35932
36275
  const req = lib.request(
35933
36276
  {
35934
36277
  hostname: u2.hostname,
@@ -36733,12 +37076,12 @@ function mapSessionUpdate(notification) {
36733
37076
  return [];
36734
37077
  }
36735
37078
  }
36736
- function mapPermissionRequest(request) {
36737
- const prompt = describeToolCall(request.toolCall) ?? "The agent requested permission to continue.";
37079
+ function mapPermissionRequest(request2) {
37080
+ const prompt = describeToolCall(request2.toolCall) ?? "The agent requested permission to continue.";
36738
37081
  const optionIdByLabel = {};
36739
37082
  const kindByLabel = {};
36740
37083
  const labels = [];
36741
- for (const opt of request.options) {
37084
+ for (const opt of request2.options) {
36742
37085
  const label = opt.name?.trim() || humanizeKind(opt.kind);
36743
37086
  if (label in optionIdByLabel) continue;
36744
37087
  optionIdByLabel[label] = opt.optionId;
@@ -36834,10 +37177,10 @@ function guardrailBlockNotice(decision, toolCall) {
36834
37177
  return `\u26D4 Guardrail auto-blocked ${describeToolCallForNotice(toolCall)} \u2014 ${decision.reason} This session's "${decision.category}" guardrail is set to Deny (adjust it in the session's Guardrails settings if intended). This was an automatic block, not a rejection by you.`;
36835
37178
  }
36836
37179
  function createOnRequestPermission(deps) {
36837
- return async (request) => {
37180
+ return async (request2) => {
36838
37181
  let guardrailConfirm = false;
36839
37182
  if (!deps.isLocal()) {
36840
- const denied = internalPathPermissionOutcome(request);
37183
+ const denied = internalPathPermissionOutcome(request2);
36841
37184
  if (denied) {
36842
37185
  log.warn(
36843
37186
  "acpRunner",
@@ -36845,17 +37188,17 @@ function createOnRequestPermission(deps) {
36845
37188
  );
36846
37189
  void deps.publisher.publishOutput({
36847
37190
  type: "text",
36848
- content: internalBlockNotice(request.toolCall),
37191
+ content: internalBlockNotice(request2.toolCall),
36849
37192
  done: true
36850
37193
  });
36851
37194
  return denied;
36852
37195
  }
36853
- const g = guardrailDecision(request, deps.getPolicy());
37196
+ const g = guardrailDecision(request2, deps.getPolicy());
36854
37197
  if (g?.kind === "deny") {
36855
37198
  log.warn("acpRunner", `guardrail [${g.category}] \u2014 denying tool call`);
36856
37199
  void deps.publisher.publishOutput({
36857
37200
  type: "text",
36858
- content: guardrailBlockNotice(g, request.toolCall),
37201
+ content: guardrailBlockNotice(g, request2.toolCall),
36859
37202
  done: true
36860
37203
  });
36861
37204
  return g.outcome;
@@ -36866,7 +37209,7 @@ function createOnRequestPermission(deps) {
36866
37209
  }
36867
37210
  }
36868
37211
  if (deps.autoApprovePermissions && !guardrailConfirm) {
36869
- const allow = pickAllowOption(request.options);
37212
+ const allow = pickAllowOption(request2.options);
36870
37213
  if (allow) {
36871
37214
  log.info(
36872
37215
  "acpRunner",
@@ -36876,7 +37219,7 @@ function createOnRequestPermission(deps) {
36876
37219
  }
36877
37220
  log.warn("acpRunner", "AUTO mode \u2014 no allow option offered; falling back to interactive");
36878
37221
  }
36879
- const { event, optionIdByLabel } = mapPermissionRequest(request);
37222
+ const { event, optionIdByLabel } = mapPermissionRequest(request2);
36880
37223
  await deps.publisher.publishAwaitingAnswer(event);
36881
37224
  return deps.registerPermission({
36882
37225
  questionId: event.questionId,
@@ -42787,8 +43130,8 @@ async function runBatonSession(opts) {
42787
43130
  // instance the baton's own AcpDriver.start bracketing uses.
42788
43131
  beginLoadReplay: () => streaming.beginLoadReplay(),
42789
43132
  endLoadReplay: () => streaming.endLoadReplay(),
42790
- onRequestPermission: async (request) => {
42791
- const { event, optionIdByLabel } = mapPermissionRequest(request);
43133
+ onRequestPermission: async (request2) => {
43134
+ const { event, optionIdByLabel } = mapPermissionRequest(request2);
42792
43135
  await publisher.publishAwaitingAnswer(event);
42793
43136
  return streaming.registerPermission({
42794
43137
  questionId: event.questionId,
@@ -45990,7 +46333,7 @@ function checkChokidar() {
45990
46333
  }
45991
46334
  async function doctor(args2 = []) {
45992
46335
  const json = args2.includes("--json");
45993
- const cliVersion = true ? "2.67.7" : "0.0.0-dev";
46336
+ const cliVersion = true ? "2.68.0" : "0.0.0-dev";
45994
46337
  const apiBase2 = resolveApiBaseUrl();
45995
46338
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
45996
46339
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -46381,7 +46724,7 @@ async function mcpRun(args2) {
46381
46724
  // src/commands/version.ts
46382
46725
  var import_picocolors15 = __toESM(require("picocolors"));
46383
46726
  function version2() {
46384
- const v = true ? "2.67.7" : "unknown";
46727
+ const v = true ? "2.68.0" : "unknown";
46385
46728
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
46386
46729
  }
46387
46730
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.67.7",
3
+ "version": "2.68.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",
@@ -22,7 +22,8 @@
22
22
  "test:coverage": "vitest run --coverage",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "postinstall": "node dist/postinstall.js || true",
25
- "prepublishOnly": "npm run build"
25
+ "prepublishOnly": "npm run build",
26
+ "e2e": "playwright test"
26
27
  },
27
28
  "keywords": [
28
29
  "claude",
@@ -90,11 +91,14 @@
90
91
  },
91
92
  "devDependencies": {
92
93
  "@codeam/shared": "*",
94
+ "@playwright/test": "^1.62.1",
95
+ "@types/jsdom": "^30.0.0",
93
96
  "@types/node": "^22.0.0",
94
97
  "@types/qrcode-terminal": "^0.12.0",
95
98
  "@types/which": "^3.0.4",
96
99
  "@types/ws": "^8.5.0",
97
100
  "@vitest/coverage-v8": "^4.1.7",
101
+ "jsdom": "^29.1.1",
98
102
  "node-pty": "^1.1.0",
99
103
  "tsup": "^8.0.0",
100
104
  "typescript": "^7.0.2",