codeam-cli 2.67.7 → 2.69.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 +13 -0
  2. package/dist/index.js +440 -55
  3. package/package.json +6 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ 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.68.0] — 2026-08-29
8
+
9
+ ### Added
10
+
11
+ - **cli:** Inspector de preview — proxy inyector, script y E2E en navegador (#665)
12
+
13
+ ## [2.67.6] — 2026-08-27
14
+
15
+ ### Fixed
16
+
17
+ - **cli:** Ordena los POST del ciclo de vida del preview
18
+ - **cli:** Re-afirma el preview en el heartbeat para que su snapshot no caduque
19
+
7
20
  ## [2.67.5] — 2026-08-27
8
21
 
9
22
  ### 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.69.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.69.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.69.0" ? { ideVersion: "2.69.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.69.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.69.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.69.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,378 @@ 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
+ // \xBFEstamos dentro del WebView de la app movil?
25457
+ //
25458
+ // \u26A0\uFE0F En un WebView el documento es de NIVEL SUPERIOR, asi que
25459
+ // \`window.parent === window\` y la via del iframe no existe: el script
25460
+ // hablaba solo, y en movil el inspector no devolvia nada. React Native
25461
+ // expone su propio canal, que es el unico que sale de ahi.
25462
+ function rnBridge() {
25463
+ return typeof window.ReactNativeWebView !== 'undefined' &&
25464
+ window.ReactNativeWebView &&
25465
+ typeof window.ReactNativeWebView.postMessage === 'function'
25466
+ ? window.ReactNativeWebView
25467
+ : null;
25468
+ }
25469
+
25470
+ function send(type, payload) {
25471
+ if (!driver) return;
25472
+ var msg = { source: CHANNEL, type: type };
25473
+ for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) msg[k] = payload[k];
25474
+
25475
+ var rn = rnBridge();
25476
+ if (rn) {
25477
+ // El canal de RN transporta CADENAS, no objetos: pasarle el objeto
25478
+ // llega al otro lado como "[object Object]".
25479
+ rn.postMessage(JSON.stringify(msg));
25480
+ return;
25481
+ }
25482
+ if (window.parent === window) return;
25483
+ window.parent.postMessage(msg, driver);
25484
+ }
25485
+
25486
+ // El resaltado. Un div fijo por encima de todo que NUNCA captura el
25487
+ // puntero: si lo capturara, el elemento de debajo dejaria de recibir el
25488
+ // mousemove y el resaltado se quedaria pegado al primero que tocara.
25489
+ function ensureOverlay() {
25490
+ if (overlay) return;
25491
+ overlay = document.createElement('div');
25492
+ overlay.setAttribute('data-codeam', 'inspector-overlay');
25493
+ overlay.style.cssText = [
25494
+ 'position:fixed', 'pointer-events:none', 'z-index:2147483647',
25495
+ 'border:1px solid #a855f7', 'background:rgba(168,85,247,0.14)',
25496
+ 'border-radius:2px', 'transition:none', 'display:none'
25497
+ ].join(';');
25498
+ label = document.createElement('div');
25499
+ label.setAttribute('data-codeam', 'inspector-label');
25500
+ label.style.cssText = [
25501
+ 'position:fixed', 'pointer-events:none', 'z-index:2147483647',
25502
+ 'background:#a855f7', 'color:#000', 'font:600 11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace',
25503
+ 'padding:2px 6px', 'border-radius:3px', 'white-space:nowrap', 'display:none'
25504
+ ].join(';');
25505
+ document.body.appendChild(overlay);
25506
+ document.body.appendChild(label);
25507
+ }
25508
+
25509
+ function removeOverlay() {
25510
+ if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
25511
+ if (label && label.parentNode) label.parentNode.removeChild(label);
25512
+ overlay = null;
25513
+ label = null;
25514
+ }
25515
+
25516
+ // Fichero y linea del componente de React, cuando el dev server los
25517
+ // expone. Es lo que hace que el agente reciba
25518
+ // "components/whatsapp-button.tsx:42" en vez de "x:0.62 y:0.81".
25519
+ // Solo existe en builds de desarrollo; en produccion devuelve null y se
25520
+ // cae al selector CSS.
25521
+ function reactSource(el) {
25522
+ try {
25523
+ for (var key in el) {
25524
+ if (key.indexOf('__reactFiber$') !== 0) continue;
25525
+ var fiber = el[key];
25526
+ while (fiber) {
25527
+ var src = fiber._debugSource;
25528
+ if (src && src.fileName) {
25529
+ return { file: String(src.fileName), line: Number(src.lineNumber) || null };
25530
+ }
25531
+ fiber = fiber.return;
25532
+ }
25533
+ }
25534
+ } catch (e) { /* nunca romper la pagina por esto */ }
25535
+ return null;
25536
+ }
25537
+
25538
+ // Un selector corto y legible, el respaldo cuando no hay React.
25539
+ function selectorFor(el) {
25540
+ if (el.id) return '#' + el.id;
25541
+ var name = el.tagName ? el.tagName.toLowerCase() : 'node';
25542
+ var cls = (el.className && typeof el.className === 'string')
25543
+ ? el.className.trim().split(/\\s+/).slice(0, 3).join('.')
25544
+ : '';
25545
+ return cls ? name + '.' + cls : name;
25546
+ }
25547
+
25548
+ function describe(el) {
25549
+ var r = el.getBoundingClientRect();
25550
+ var text = (el.textContent || '').replace(/\\s+/g, ' ').trim();
25551
+ return {
25552
+ tag: el.tagName ? el.tagName.toLowerCase() : 'node',
25553
+ id: el.id || null,
25554
+ classes: (el.className && typeof el.className === 'string') ? el.className.trim() : '',
25555
+ selector: selectorFor(el),
25556
+ // Un recorte, no el contenido: describir no es extraer.
25557
+ text: text.length > 120 ? text.slice(0, 120) + '\u2026' : text,
25558
+ rect: { x: r.left, y: r.top, w: r.width, h: r.height },
25559
+ viewport: { w: window.innerWidth, h: window.innerHeight },
25560
+ source: reactSource(el)
25561
+ };
25562
+ }
25563
+
25564
+ function paint(el) {
25565
+ ensureOverlay();
25566
+ var r = el.getBoundingClientRect();
25567
+ overlay.style.display = 'block';
25568
+ overlay.style.left = r.left + 'px';
25569
+ overlay.style.top = r.top + 'px';
25570
+ overlay.style.width = r.width + 'px';
25571
+ overlay.style.height = r.height + 'px';
25572
+ label.style.display = 'block';
25573
+ label.textContent = selectorFor(el) + ' ' + Math.round(r.width) + '\xD7' + Math.round(r.height);
25574
+ // Encima si cabe; si no, dentro. Una etiqueta cortada por el borde
25575
+ // superior no se lee.
25576
+ var above = r.top >= 20;
25577
+ label.style.left = r.left + 'px';
25578
+ label.style.top = (above ? r.top - 20 : r.top + 2) + 'px';
25579
+ }
25580
+
25581
+ function onMove(e) {
25582
+ if (!enabled) return;
25583
+ var el = e.target;
25584
+ if (!el || el === current || !el.getBoundingClientRect) return;
25585
+ if (el.getAttribute && el.getAttribute('data-codeam')) return;
25586
+ current = el;
25587
+ paint(el);
25588
+ send('hover', { element: describe(el) });
25589
+ }
25590
+
25591
+ // Captura, y se traga el evento: mientras el modo esta activo un clic
25592
+ // SELECCIONA, no navega. Sin esto, pulsar un enlace se llevaria el
25593
+ // preview a otra pagina en mitad del gesto.
25594
+ function onClick(e) {
25595
+ if (!enabled) return;
25596
+ var el = e.target;
25597
+ if (el && el.getAttribute && el.getAttribute('data-codeam')) return;
25598
+ e.preventDefault();
25599
+ e.stopPropagation();
25600
+ if (el) send('pick', { element: describe(el) });
25601
+ }
25602
+
25603
+ function enable(origin) {
25604
+ if (enabled) return;
25605
+ enabled = true;
25606
+ driver = origin;
25607
+ document.addEventListener('mousemove', onMove, true);
25608
+ document.addEventListener('click', onClick, true);
25609
+ send('ready', {});
25610
+ }
25611
+
25612
+ function disable() {
25613
+ if (!enabled) return;
25614
+ enabled = false;
25615
+ current = null;
25616
+ document.removeEventListener('mousemove', onMove, true);
25617
+ document.removeEventListener('click', onClick, true);
25618
+ removeOverlay();
25619
+ driver = null;
25620
+ }
25621
+
25622
+ window.addEventListener('message', function (e) {
25623
+ if (!allowed(e.origin)) return;
25624
+ var data = e.data;
25625
+ if (!data || data.source !== CHANNEL) return;
25626
+ if (data.type === 'enable') enable(e.origin);
25627
+ else if (data.type === 'disable') disable();
25628
+ });
25629
+
25630
+ /**
25631
+ * La puerta para React Native, que no puede usar \`postMessage\`.
25632
+ *
25633
+ * \u26A0\uFE0F Aqui NO hay lista blanca de origenes, y no es un descuido: quien
25634
+ * llama esto es la app anfitriona sobre SU PROPIO WebView, via
25635
+ * \`injectJavaScript\` \u2014 no hay un tercero al que filtrar. La lista blanca
25636
+ * protege el caso del iframe, donde cualquier pagina puede embeber el
25637
+ * preview y mandar mensajes; ese caso sigue exactamente igual de cerrado.
25638
+ *
25639
+ * Solo se expone donde ese puente existe, para no dejar en una pagina
25640
+ * normal una funcion global que encienda el inspector sin permiso.
25641
+ */
25642
+ if (rnBridge()) {
25643
+ window.__codeamInspector = {
25644
+ enable: function () { enable('react-native'); },
25645
+ disable: disable,
25646
+ };
25647
+ }
25648
+ } catch (e) {
25649
+ // Pase lo que pase, la app del usuario sigue funcionando.
25650
+ }
25651
+ })();
25652
+ </script>`;
25653
+ }
25654
+
25655
+ // src/services/preview/inspector-bringup.ts
25656
+ var DEFAULT_INSPECTOR_ORIGINS = [
25657
+ "https://www.codeagent-mobile.com",
25658
+ "https://codeagent-mobile.com",
25659
+ "https://dev.codeagent-mobile.com"
25660
+ ];
25661
+ function inspectorEnabled(env) {
25662
+ const raw = env.CODEAM_PREVIEW_INSPECTOR;
25663
+ return raw !== "0" && raw !== "false";
25664
+ }
25665
+ function resolveInspectorOrigins(env) {
25666
+ const raw = env.CODEAM_INSPECTOR_ORIGINS;
25667
+ if (!raw) return DEFAULT_INSPECTOR_ORIGINS;
25668
+ const extra = raw.split(",").map((s) => s.trim()).filter(Boolean);
25669
+ return [...DEFAULT_INSPECTOR_ORIGINS, ...extra];
25670
+ }
25671
+ async function bringUpInspector(targetPort, deps = {}) {
25672
+ const env = deps.env ?? process.env;
25673
+ const log2 = deps.log ?? (() => void 0);
25674
+ if (!inspectorEnabled(env)) {
25675
+ log2("preview inspector disabled by CODEAM_PREVIEW_INSPECTOR");
25676
+ return { port: targetPort, proxy: null };
25677
+ }
25678
+ try {
25679
+ const start2 = deps.start ?? startInspectorProxy;
25680
+ const proxy = await start2({
25681
+ targetPort,
25682
+ script: inspectorClientSource({ allowedOrigins: resolveInspectorOrigins(env) })
25683
+ });
25684
+ log2(`preview inspector proxy on :${proxy.port} \u2192 :${targetPort}`);
25685
+ return { port: proxy.port, proxy };
25686
+ } catch (e) {
25687
+ log2(`preview inspector unavailable (${e.message}) \u2014 going direct`);
25688
+ return { port: targetPort, proxy: null };
25689
+ }
25690
+ }
25691
+
25312
25692
  // src/services/preview/start-orchestrator.ts
25313
25693
  var INSTALL_TIMEOUT_MS2 = 5 * 6e4;
25314
25694
  var SETUP_TIMEOUT_MS = 2 * 6e4;
@@ -25420,6 +25800,7 @@ async function runPreviewStart(args2) {
25420
25800
  sessionId,
25421
25801
  devServer: dev.devServer,
25422
25802
  tunnel: tun.tunnel,
25803
+ inspector: tun.inspector,
25423
25804
  url: tun.url,
25424
25805
  framework: detection.framework,
25425
25806
  detection,
@@ -25665,8 +26046,12 @@ async function establishTunnel(ctx, dev) {
25665
26046
  });
25666
26047
  return null;
25667
26048
  }
25668
- return { url: expoUrl, tunnel: null };
26049
+ return { url: expoUrl, tunnel: null, inspector: null };
25669
26050
  }
26051
+ const inspection = await bringUpInspector(detection.port, {
26052
+ log: (m) => log.info("preview", m)
26053
+ });
26054
+ const tunnelPort = inspection.port;
25670
26055
  let bin;
25671
26056
  try {
25672
26057
  bin = await resolveCloudflared();
@@ -25694,7 +26079,7 @@ async function establishTunnel(ctx, dev) {
25694
26079
  if (namedToken && namedHostname) {
25695
26080
  try {
25696
26081
  emitProgress("TUNNEL_STARTING", `named tunnel ${namedHostname}`);
25697
- const candidate = await spawnNamedTunnel(bin, namedToken, detection.port);
26082
+ const candidate = await spawnNamedTunnel(bin, namedToken, tunnelPort);
25698
26083
  const outcome = await awaitTunnelRegistered(
25699
26084
  candidate,
25700
26085
  namedHostname,
@@ -25727,7 +26112,7 @@ async function establishTunnel(ctx, dev) {
25727
26112
  }
25728
26113
  const candidate = (0, import_child_process22.spawn)(
25729
26114
  bin,
25730
- ["tunnel", "--url", `http://localhost:${detection.port}`],
26115
+ ["tunnel", "--url", `http://localhost:${tunnelPort}`],
25731
26116
  { stdio: ["ignore", "pipe", "pipe"] }
25732
26117
  );
25733
26118
  const outcome = await awaitTunnelRegistered(
@@ -25752,7 +26137,7 @@ async function establishTunnel(ctx, dev) {
25752
26137
  });
25753
26138
  return null;
25754
26139
  }
25755
- return { url: parsedUrl, tunnel };
26140
+ return { url: parsedUrl, tunnel, inspector: inspection.proxy };
25756
26141
  }
25757
26142
 
25758
26143
  // src/beads/bd-adapter.ts
@@ -29842,7 +30227,7 @@ var fs67 = __toESM(require("fs"));
29842
30227
  var path74 = __toESM(require("path"));
29843
30228
  var os56 = __toESM(require("os"));
29844
30229
  var https7 = __toESM(require("https"));
29845
- var http6 = __toESM(require("http"));
30230
+ var http7 = __toESM(require("http"));
29846
30231
  var import_zod2 = require("zod");
29847
30232
  var historyRecordSchema = import_zod2.z.object({
29848
30233
  type: import_zod2.z.string().optional(),
@@ -29925,7 +30310,7 @@ function post(endpoint, body, pluginAuthToken) {
29925
30310
  return new Promise((resolve10) => {
29926
30311
  const payload = JSON.stringify(body);
29927
30312
  const u2 = new URL(`${API_BASE8}${endpoint}`);
29928
- const transport = u2.protocol === "https:" ? https7 : http6;
30313
+ const transport = u2.protocol === "https:" ? https7 : http7;
29929
30314
  const req = transport.request(
29930
30315
  {
29931
30316
  hostname: u2.hostname,
@@ -32822,8 +33207,8 @@ var ConnectionBuilder = class {
32822
33207
  if (message.kind !== "request" || message.method !== method) {
32823
33208
  return Handled.no(message);
32824
33209
  }
32825
- const request = parse(message.params);
32826
- return await handler(request, message.responder, cx) ?? Handled.yes();
33210
+ const request2 = parse(message.params);
33211
+ return await handler(request2, message.responder, cx) ?? Handled.yes();
32827
33212
  },
32828
33213
  describe: () => `${this.connectionName ?? "request"}:${method}`
32829
33214
  });
@@ -33384,23 +33769,23 @@ var AsyncQueue = class {
33384
33769
  });
33385
33770
  }
33386
33771
  };
33387
- function cloneNewSessionRequest(request) {
33772
+ function cloneNewSessionRequest(request2) {
33388
33773
  return {
33389
- ...request,
33390
- additionalDirectories: request.additionalDirectories ? [...request.additionalDirectories] : void 0,
33391
- mcpServers: [...request.mcpServers]
33774
+ ...request2,
33775
+ additionalDirectories: request2.additionalDirectories ? [...request2.additionalDirectories] : void 0,
33776
+ mcpServers: [...request2.mcpServers]
33392
33777
  };
33393
33778
  }
33394
33779
  var SessionBuilder = class _SessionBuilder {
33395
33780
  cx;
33396
33781
  request;
33397
- constructor(cx, request) {
33782
+ constructor(cx, request2) {
33398
33783
  this.cx = cx;
33399
- this.request = cloneNewSessionRequest(request);
33784
+ this.request = cloneNewSessionRequest(request2);
33400
33785
  }
33401
33786
  /** @internal */
33402
- static create(cx, request) {
33403
- return new _SessionBuilder(cx, request);
33787
+ static create(cx, request2) {
33788
+ return new _SessionBuilder(cx, request2);
33404
33789
  }
33405
33790
  /**
33406
33791
  * Returns the `session/new` request that will be sent.
@@ -34528,9 +34913,9 @@ function toolCallReferencesInternal(call, homeDir2) {
34528
34913
  }
34529
34914
  return false;
34530
34915
  }
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");
34916
+ function internalPathPermissionOutcome(request2, homeDir2) {
34917
+ if (!toolCallReferencesInternal(request2.toolCall, homeDir2)) return null;
34918
+ const reject = request2.options.find((o) => o.kind === "reject_once") ?? request2.options.find((o) => o.kind === "reject_always");
34534
34919
  if (reject) return { outcome: { outcome: "selected", optionId: reject.optionId } };
34535
34920
  return { outcome: { outcome: "cancelled" } };
34536
34921
  }
@@ -34589,9 +34974,9 @@ function toolPathIsSecret(p2) {
34589
34974
  return !!p2 && SECRET_RE.test(p2);
34590
34975
  }
34591
34976
  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);
34977
+ function guardrailDecision(request2, policy) {
34978
+ if (isProseOnlyToolKind(request2.toolCall.kind)) return null;
34979
+ const hay = haystack(request2.toolCall);
34595
34980
  if (!hay) return null;
34596
34981
  const matched = matchedCategories(hay);
34597
34982
  if (matched.size === 0) return null;
@@ -34611,7 +34996,7 @@ function guardrailDecision(request, policy) {
34611
34996
  kind: "deny",
34612
34997
  category: denyCat,
34613
34998
  reason: GUARDRAIL_CATEGORY_META[denyCat].description,
34614
- outcome: rejectOutcome(request.options)
34999
+ outcome: rejectOutcome(request2.options)
34615
35000
  };
34616
35001
  }
34617
35002
  if (confirmCat) {
@@ -35876,7 +36261,7 @@ async function performAgentSwitch(deps, rawAgentId, fastPath = {}) {
35876
36261
  }
35877
36262
 
35878
36263
  // src/services/streaming/transport.ts
35879
- var http7 = __toESM(require("http"));
36264
+ var http8 = __toESM(require("http"));
35880
36265
  var https8 = __toESM(require("https"));
35881
36266
  var _transport4 = {
35882
36267
  post: _post3,
@@ -35886,7 +36271,7 @@ function _post3(url2, headers, payload) {
35886
36271
  return new Promise((resolve10, reject) => {
35887
36272
  let settled = false;
35888
36273
  const u2 = new URL(url2);
35889
- const lib = u2.protocol === "https:" ? https8 : http7;
36274
+ const lib = u2.protocol === "https:" ? https8 : http8;
35890
36275
  const req = lib.request(
35891
36276
  {
35892
36277
  hostname: u2.hostname,
@@ -35928,7 +36313,7 @@ function _get(url2, headers) {
35928
36313
  return new Promise((resolve10, reject) => {
35929
36314
  let settled = false;
35930
36315
  const u2 = new URL(url2);
35931
- const lib = u2.protocol === "https:" ? https8 : http7;
36316
+ const lib = u2.protocol === "https:" ? https8 : http8;
35932
36317
  const req = lib.request(
35933
36318
  {
35934
36319
  hostname: u2.hostname,
@@ -36733,12 +37118,12 @@ function mapSessionUpdate(notification) {
36733
37118
  return [];
36734
37119
  }
36735
37120
  }
36736
- function mapPermissionRequest(request) {
36737
- const prompt = describeToolCall(request.toolCall) ?? "The agent requested permission to continue.";
37121
+ function mapPermissionRequest(request2) {
37122
+ const prompt = describeToolCall(request2.toolCall) ?? "The agent requested permission to continue.";
36738
37123
  const optionIdByLabel = {};
36739
37124
  const kindByLabel = {};
36740
37125
  const labels = [];
36741
- for (const opt of request.options) {
37126
+ for (const opt of request2.options) {
36742
37127
  const label = opt.name?.trim() || humanizeKind(opt.kind);
36743
37128
  if (label in optionIdByLabel) continue;
36744
37129
  optionIdByLabel[label] = opt.optionId;
@@ -36834,10 +37219,10 @@ function guardrailBlockNotice(decision, toolCall) {
36834
37219
  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
37220
  }
36836
37221
  function createOnRequestPermission(deps) {
36837
- return async (request) => {
37222
+ return async (request2) => {
36838
37223
  let guardrailConfirm = false;
36839
37224
  if (!deps.isLocal()) {
36840
- const denied = internalPathPermissionOutcome(request);
37225
+ const denied = internalPathPermissionOutcome(request2);
36841
37226
  if (denied) {
36842
37227
  log.warn(
36843
37228
  "acpRunner",
@@ -36845,17 +37230,17 @@ function createOnRequestPermission(deps) {
36845
37230
  );
36846
37231
  void deps.publisher.publishOutput({
36847
37232
  type: "text",
36848
- content: internalBlockNotice(request.toolCall),
37233
+ content: internalBlockNotice(request2.toolCall),
36849
37234
  done: true
36850
37235
  });
36851
37236
  return denied;
36852
37237
  }
36853
- const g = guardrailDecision(request, deps.getPolicy());
37238
+ const g = guardrailDecision(request2, deps.getPolicy());
36854
37239
  if (g?.kind === "deny") {
36855
37240
  log.warn("acpRunner", `guardrail [${g.category}] \u2014 denying tool call`);
36856
37241
  void deps.publisher.publishOutput({
36857
37242
  type: "text",
36858
- content: guardrailBlockNotice(g, request.toolCall),
37243
+ content: guardrailBlockNotice(g, request2.toolCall),
36859
37244
  done: true
36860
37245
  });
36861
37246
  return g.outcome;
@@ -36866,7 +37251,7 @@ function createOnRequestPermission(deps) {
36866
37251
  }
36867
37252
  }
36868
37253
  if (deps.autoApprovePermissions && !guardrailConfirm) {
36869
- const allow = pickAllowOption(request.options);
37254
+ const allow = pickAllowOption(request2.options);
36870
37255
  if (allow) {
36871
37256
  log.info(
36872
37257
  "acpRunner",
@@ -36876,7 +37261,7 @@ function createOnRequestPermission(deps) {
36876
37261
  }
36877
37262
  log.warn("acpRunner", "AUTO mode \u2014 no allow option offered; falling back to interactive");
36878
37263
  }
36879
- const { event, optionIdByLabel } = mapPermissionRequest(request);
37264
+ const { event, optionIdByLabel } = mapPermissionRequest(request2);
36880
37265
  await deps.publisher.publishAwaitingAnswer(event);
36881
37266
  return deps.registerPermission({
36882
37267
  questionId: event.questionId,
@@ -42787,8 +43172,8 @@ async function runBatonSession(opts) {
42787
43172
  // instance the baton's own AcpDriver.start bracketing uses.
42788
43173
  beginLoadReplay: () => streaming.beginLoadReplay(),
42789
43174
  endLoadReplay: () => streaming.endLoadReplay(),
42790
- onRequestPermission: async (request) => {
42791
- const { event, optionIdByLabel } = mapPermissionRequest(request);
43175
+ onRequestPermission: async (request2) => {
43176
+ const { event, optionIdByLabel } = mapPermissionRequest(request2);
42792
43177
  await publisher.publishAwaitingAnswer(event);
42793
43178
  return streaming.registerPermission({
42794
43179
  questionId: event.questionId,
@@ -45990,7 +46375,7 @@ function checkChokidar() {
45990
46375
  }
45991
46376
  async function doctor(args2 = []) {
45992
46377
  const json = args2.includes("--json");
45993
- const cliVersion = true ? "2.67.7" : "0.0.0-dev";
46378
+ const cliVersion = true ? "2.69.0" : "0.0.0-dev";
45994
46379
  const apiBase2 = resolveApiBaseUrl();
45995
46380
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
45996
46381
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -46381,7 +46766,7 @@ async function mcpRun(args2) {
46381
46766
  // src/commands/version.ts
46382
46767
  var import_picocolors15 = __toESM(require("picocolors"));
46383
46768
  function version2() {
46384
- const v = true ? "2.67.7" : "unknown";
46769
+ const v = true ? "2.69.0" : "unknown";
46385
46770
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
46386
46771
  }
46387
46772
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.67.7",
3
+ "version": "2.69.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",