replicas-cli 0.2.462 → 0.2.464

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 (2) hide show
  1. package/dist/index.mjs +163 -3061
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -7335,8 +7335,9 @@ var require_dist = __commonJS({
7335
7335
 
7336
7336
  // src/index.ts
7337
7337
  import "dotenv/config";
7338
+ import { spawnSync } from "child_process";
7338
7339
  import { Command, InvalidArgumentError } from "commander";
7339
- import chalk24 from "chalk";
7340
+ import chalk23 from "chalk";
7340
7341
 
7341
7342
  // src/commands/login.ts
7342
7343
  import http from "http";
@@ -8397,6 +8398,7 @@ Then embed the printed \`![\u2026](\u2026)\` line in your chat reply. See \`MEDI
8397
8398
 
8398
8399
  ## Failure modes
8399
8400
 
8401
+ - **"Computer-use daemon is unavailable"**: the package-managed systemd service did not start. Check \`systemctl status replicas-computer.service\` and its journal.
8400
8402
  - **"Desktop services script missing"**: workspace image is older than this skill. Tell the user - nothing you can do from the CLI side.
8401
8403
  - **\`xdotool ... failed: Can't open display\`**: Xvfb didn't come up. \`replicas computer status\` will show which service is dead and auto-repair the desktop bridge.
8402
8404
  - **Browser doesn't appear after \`launch chrome\`**: run \`replicas computer observe /tmp/state.png\`. Chrome cold-start on the virtual display takes ~500ms but bigger pages take longer.
@@ -8408,7 +8410,8 @@ Then embed the printed \`![\u2026](\u2026)\` line in your chat reply. See \`MEDI
8408
8410
  | Component | Where |
8409
8411
  |---|---|
8410
8412
  | \`xvfb\`, \`openbox\`, \`tint2\`, \`x11vnc\`, \`websockify\`, \`xdotool\`, \`scrot\`, \`ffmpeg\`, \`google-chrome\` | Baked into the workspace image |
8411
- | Xvfb / openbox / tint2 / x11vnc / websockify processes | Started at workspace boot (\`replicas-start-desktop-services\`) |
8413
+ | \`replicas-computer\`, desktop assets, and service unit | Installed by \`replicas-computer setup\` during the image build |
8414
+ | Xvfb / openbox / tint2 / x11vnc / websockify processes | Managed by \`replicas-computer.service\` |
8412
8415
  | noVNC preview URL (port 6080) | Registered by the engine on startup (authenticated) |
8413
8416
  | Dashboard \`Desktop\` tab | Always available once the engine has registered the preview |
8414
8417
 
@@ -10078,7 +10081,7 @@ function formatTurnElapsed(ms) {
10078
10081
  }
10079
10082
 
10080
10083
  // ../shared/src/cli-version.ts
10081
- var CLI_VERSION = "0.2.462";
10084
+ var CLI_VERSION = "0.2.464";
10082
10085
 
10083
10086
  // ../shared/src/version.ts
10084
10087
  function compareVersions(v1, v2) {
@@ -10093,16 +10096,6 @@ function compareVersions(v1, v2) {
10093
10096
  return 0;
10094
10097
  }
10095
10098
 
10096
- // ../shared/src/engine/environment.ts
10097
- var DESKTOP_NOVNC_PORT = 6080;
10098
- function getDesktopViewerUrl(publicUrl) {
10099
- const url2 = new URL(`${publicUrl.replace(/\/$/, "")}/`);
10100
- url2.searchParams.set("v", "13");
10101
- return url2.toString();
10102
- }
10103
- var DESKTOP_VIEWER_WIDTH = 1920;
10104
- var DESKTOP_VIEWER_HEIGHT = 1080;
10105
-
10106
10099
  // ../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
10107
10100
  var external_exports = {};
10108
10101
  __export(external_exports, {
@@ -27703,7 +27696,7 @@ function generateState() {
27703
27696
  }
27704
27697
  async function loginCommand() {
27705
27698
  const state = generateState();
27706
- return new Promise((resolve3, reject) => {
27699
+ return new Promise((resolve2, reject) => {
27707
27700
  let authTimeout;
27708
27701
  let hasHandledCallback = false;
27709
27702
  let lastRedirectUrl = null;
@@ -27850,7 +27843,7 @@ async function loginCommand() {
27850
27843
  setImmediate(() => {
27851
27844
  server.closeAllConnections?.();
27852
27845
  server.close();
27853
- resolve3();
27846
+ resolve2();
27854
27847
  });
27855
27848
  } catch (error52) {
27856
27849
  const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Failed to verify authentication.")}`;
@@ -27956,13 +27949,13 @@ import chalk6 from "chalk";
27956
27949
  import { spawn } from "child_process";
27957
27950
  var SSH_OPTIONS = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
27958
27951
  async function connectSSH(token, host, proxyCommand) {
27959
- return new Promise((resolve3, reject) => {
27952
+ return new Promise((resolve2, reject) => {
27960
27953
  const sshArgs = proxyCommand ? [...SSH_OPTIONS, "-o", `ProxyCommand=${proxyCommand}`, `${token}@${host}`] : [...SSH_OPTIONS, `${token}@${host}`];
27961
27954
  const ssh = spawn("ssh", sshArgs, {
27962
27955
  stdio: "inherit"
27963
27956
  });
27964
27957
  ssh.on("close", () => {
27965
- resolve3();
27958
+ resolve2();
27966
27959
  });
27967
27960
  ssh.on("error", reject);
27968
27961
  });
@@ -28471,7 +28464,7 @@ async function exchangeCodeForTokens(code, codeVerifier) {
28471
28464
  };
28472
28465
  }
28473
28466
  function startCallbackServer(expectedState, codeVerifier) {
28474
- return new Promise((resolve3, reject) => {
28467
+ return new Promise((resolve2, reject) => {
28475
28468
  const server = http2.createServer(async (req, res) => {
28476
28469
  try {
28477
28470
  if (!req.url) {
@@ -28514,7 +28507,7 @@ function startCallbackServer(expectedState, codeVerifier) {
28514
28507
  res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/success` });
28515
28508
  res.end();
28516
28509
  server.close();
28517
- resolve3(tokens);
28510
+ resolve2(tokens);
28518
28511
  } catch (tokenError) {
28519
28512
  const errorMessage = encodeURIComponent(tokenError instanceof Error ? tokenError.message : "Unknown error");
28520
28513
  res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
@@ -28548,7 +28541,7 @@ async function runCodexOAuthFlow(totalSteps) {
28548
28541
  const state = generateState2();
28549
28542
  const authUrl = buildAuthorizationUrl(pkce.codeChallenge, state);
28550
28543
  const tokensPromise = startCallbackServer(state, pkce.codeVerifier);
28551
- await new Promise((resolve3) => setTimeout(resolve3, 500));
28544
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
28552
28545
  renderAuthFlowStep(1, totalSteps, "Approve access in your browser");
28553
28546
  await openForApproval("auth.openai.com", authUrl);
28554
28547
  renderAuthFlowDetail("waiting for sign-in\u2026");
@@ -28689,10 +28682,10 @@ async function promptForAuthorizationCode() {
28689
28682
  process.stdin.setRawMode(false);
28690
28683
  process.stdin.resume();
28691
28684
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
28692
- return new Promise((resolve3) => {
28693
- rl.on("close", () => resolve3(""));
28685
+ return new Promise((resolve2) => {
28686
+ rl.on("close", () => resolve2(""));
28694
28687
  rl.question(chalk11.gray(" Paste code here: "), (answer) => {
28695
- resolve3(answer.trim());
28688
+ resolve2(answer.trim());
28696
28689
  rl.close();
28697
28690
  });
28698
28691
  });
@@ -29297,7 +29290,7 @@ async function replicaReadCommand(id, options) {
29297
29290
  try {
29298
29291
  const params = new URLSearchParams();
29299
29292
  if (options.limit) params.set("limit", options.limit);
29300
- if (options.offset) params.set("offset", options.offset);
29293
+ if (options.beforeEvent) params.set("beforeEvent", options.beforeEvent);
29301
29294
  const query = params.toString();
29302
29295
  const response = await orgAuthenticatedFetch(
29303
29296
  `/v1/replica/${id}/read${query ? "?" + query : ""}`
@@ -29318,7 +29311,7 @@ Conversation History
29318
29311
  console.log(chalk14.gray(` Total Events: ${response.total}`));
29319
29312
  console.log(chalk14.gray(` Showing: ${response.events.length} events`));
29320
29313
  if (response.has_more) {
29321
- console.log(chalk14.gray(` Has More: yes (use --offset to paginate)`));
29314
+ console.log(chalk14.gray(` Has More: yes (--before-event ${response.eventsStartIndex} for the previous page)`));
29322
29315
  }
29323
29316
  console.log();
29324
29317
  if (response.events.length === 0) {
@@ -30911,2871 +30904,8 @@ async function mothershipRelayCommand(options) {
30911
30904
  console.log("Relayed to the thread's dedicated workspace.");
30912
30905
  }
30913
30906
 
30914
- // src/commands/computer/index.ts
30915
- import { spawn as spawn5, spawnSync as spawnSync3 } from "child_process";
30916
- import { createHash as createHash3 } from "crypto";
30917
- import { closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync5, readSync, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
30918
- import { dirname as dirname3 } from "path";
30919
- import chalk21 from "chalk";
30920
-
30921
- // src/commands/computer/desktop.ts
30922
- import { spawnSync } from "child_process";
30923
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
30924
- import { dirname, isAbsolute, resolve as resolve2 } from "path";
30925
- var STATE_DIR = process.env.REPLICAS_DESKTOP_STATE_DIR || "/tmp/replicas-computer";
30926
- var DEFAULT_DISPLAY = process.env.REPLICAS_DESKTOP_DISPLAY || ":99";
30927
- var NOVNC_PORT = process.env.REPLICAS_DESKTOP_NOVNC_PORT ? parseInt(process.env.REPLICAS_DESKTOP_NOVNC_PORT, 10) : DESKTOP_NOVNC_PORT;
30928
- var SERVICES_SCRIPT = "/usr/local/bin/replicas-start-desktop-services";
30929
- var INPUT_LOCK_FILE = process.env.REPLICAS_DESKTOP_INPUT_LOCK_FILE || `${STATE_DIR}/input.lock`;
30930
- var CHROME_WRAPPER = "/usr/local/bin/replicas-chrome";
30931
- var INFO_WAIT_TIMEOUT_MS = 1e4;
30932
- var INFO_WAIT_INTERVAL_MS = 500;
30933
- var lastDesktopHealthAt = 0;
30934
- var CHROME_DEBUG_PORT = (() => {
30935
- const value = process.env.REPLICAS_DESKTOP_CHROME_DEBUG_PORT;
30936
- if (!value) return 9222;
30937
- const port = parseInt(value, 10);
30938
- if (!Number.isInteger(port) || port <= 0 || port > 65535) {
30939
- fail(`REPLICAS_DESKTOP_CHROME_DEBUG_PORT must be a valid TCP port, got ${JSON.stringify(value)}`);
30940
- }
30941
- return port;
30942
- })();
30943
- function fail(msg) {
30944
- throw new Error(msg);
30945
- }
30946
- function getDesktopBridgeStatus() {
30947
- const r = spawnSync("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
30948
- if (r.status !== 0) return null;
30949
- try {
30950
- return JSON.parse(r.stdout.toString());
30951
- } catch {
30952
- return null;
30953
- }
30954
- }
30955
- function desktopStackHealthy() {
30956
- const pids = {};
30957
- for (const name of ["openbox", "tint2", "x11vnc", "novnc"]) {
30958
- try {
30959
- const pid = Number.parseInt(readFileSync3(`${STATE_DIR}/${name}.pid`, "utf8").trim(), 10);
30960
- if (!Number.isFinite(pid)) return false;
30961
- process.kill(pid, 0);
30962
- pids[name] = pid;
30963
- } catch {
30964
- return false;
30965
- }
30966
- }
30967
- if (spawnSync("xdpyinfo", [], { env: withDisplay(), stdio: "ignore" }).status !== 0) return false;
30968
- const wm = spawnSync("xprop", ["-root", "_NET_SUPPORTING_WM_CHECK"], { env: withDisplay(), stdio: "pipe" });
30969
- if (wm.status !== 0 || !wm.stdout?.toString().includes("window id")) return false;
30970
- const bridge = getDesktopBridgeStatus();
30971
- return !!bridge?.x11vnc.listenerPids?.includes(pids.x11vnc) && !!bridge.websockify.listenerPids?.includes(pids.novnc);
30972
- }
30973
- function ensureServicesRunning() {
30974
- if (!existsSync(SERVICES_SCRIPT)) {
30975
- fail(
30976
- `Desktop services script missing at ${SERVICES_SCRIPT}. The workspace image is out of date \u2014 Xvfb / openbox / x11vnc / websockify must be installed and \`replicas-start-desktop-services\` baked in.`
30977
- );
30978
- }
30979
- if (Date.now() - lastDesktopHealthAt < 1e3) return;
30980
- if (desktopStackHealthy()) {
30981
- lastDesktopHealthAt = Date.now();
30982
- return;
30983
- }
30984
- const r = spawnSync("bash", [SERVICES_SCRIPT], { stdio: "pipe" });
30985
- if (r.status !== 0) {
30986
- fail(`Failed to start desktop services: ${r.stderr?.toString() || "unknown error"}`);
30987
- }
30988
- lastDesktopHealthAt = Date.now();
30989
- }
30990
- function withDisplay(env = process.env) {
30991
- return { ...env, DISPLAY: DEFAULT_DISPLAY };
30992
- }
30993
- function runDisplayCmd(bin, args) {
30994
- ensureServicesRunning();
30995
- const r = spawnSync(bin, args, { env: withDisplay(), stdio: "pipe" });
30996
- if (r.status !== 0) {
30997
- fail(`${bin} ${args.join(" ")} failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
30998
- }
30999
- return r.stdout?.toString() ?? "";
31000
- }
31001
- function runDesktopInputCmd(args) {
31002
- mkdirSync3(dirname(INPUT_LOCK_FILE), { recursive: true });
31003
- return runDisplayCmd("flock", [
31004
- "--exclusive",
31005
- "--wait",
31006
- process.env.REPLICAS_DESKTOP_INPUT_LOCK_WAIT_SECONDS || "60",
31007
- INPUT_LOCK_FILE,
31008
- "xdotool",
31009
- ...args
31010
- ]);
31011
- }
31012
- function tryDisplayCmd(bin, args) {
31013
- ensureServicesRunning();
31014
- const r = spawnSync(bin, args, { env: withDisplay(), stdio: "pipe" });
31015
- if (r.status !== 0) return null;
31016
- const out = r.stdout?.toString().trim();
31017
- return out || null;
31018
- }
31019
- function parseCoord(value, label) {
31020
- if (!/^-?\d+$/.test(value)) fail(`${label} must be an integer (got "${value}")`);
31021
- const n = Number.parseInt(value, 10);
31022
- if (!Number.isFinite(n)) fail(`${label} must be an integer (got "${value}")`);
31023
- return n;
31024
- }
31025
- function getDisplayDimensions() {
31026
- const out = runDisplayCmd("xdpyinfo", []);
31027
- const match = out.match(/dimensions:\s+(\d+)x(\d+)\s+pixels/);
31028
- if (!match) fail("could not read display dimensions from xdpyinfo");
31029
- return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
31030
- }
31031
- function getMouseLocation() {
31032
- const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
31033
- if (!out) return null;
31034
- const values = Object.fromEntries(
31035
- out.split("\n").map((line) => {
31036
- const [key, value] = line.split("=");
31037
- return [key.toLowerCase(), Number.parseInt(value, 10)];
31038
- })
31039
- );
31040
- if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
31041
- return {
31042
- x: values.x,
31043
- y: values.y,
31044
- screen: Number.isFinite(values.screen) ? values.screen : 0,
31045
- window: Number.isFinite(values.window) ? values.window : 0
31046
- };
31047
- }
31048
- function parseScreenCoord(value, label, size) {
31049
- if (value.endsWith("%")) {
31050
- const rawPct = value.slice(0, -1);
31051
- if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(rawPct)) fail(`${label} percent must be a number between 0% and 100% (got "${value}")`);
31052
- const pct = Number.parseFloat(rawPct);
31053
- if (!Number.isFinite(pct) || pct < 0 || pct > 100) fail(`${label} percent must be between 0% and 100% (got "${value}")`);
31054
- return Math.round(pct / 100 * (size - 1));
31055
- }
31056
- const n = parseCoord(value, label);
31057
- if (n < 0 || n >= size) fail(`${label} must be between 0 and ${size - 1} pixels, or 0%-100% (got "${value}")`);
31058
- return n;
31059
- }
31060
- function resolvePath(p) {
31061
- return isAbsolute(p) ? p : resolve2(process.cwd(), p);
31062
- }
31063
- function configuredDesktopDimensions() {
31064
- const width = parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
31065
- const height = parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
31066
- if (!Number.isFinite(width) || width <= 0) fail(`REPLICAS_DESKTOP_WIDTH must be a positive integer`);
31067
- if (!Number.isFinite(height) || height <= 0) fail(`REPLICAS_DESKTOP_HEIGHT must be a positive integer`);
31068
- return { width, height };
31069
- }
31070
- function clamp(n, min, max) {
31071
- return Math.min(max, Math.max(min, n));
31072
- }
31073
- var sleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
31074
-
31075
- // src/commands/computer/recording.ts
31076
- import { spawn as spawn4 } from "child_process";
31077
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
31078
- import { dirname as dirname2 } from "path";
31079
-
31080
- // src/commands/computer/recording/render.ts
31081
- import { spawnSync as spawnSync2 } from "child_process";
31082
- import { copyFileSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
31083
-
31084
- // src/commands/computer/recording/config.ts
31085
- var cameraMotion = {
31086
- clickLeadSeconds: 1.2,
31087
- clickTailSeconds: 0.5,
31088
- actionLeadSeconds: 0.9,
31089
- clickZoom: 1.24,
31090
- closeClickOutputGapSeconds: 6,
31091
- clickMoveSeconds: 1.2,
31092
- returnSeconds: 1,
31093
- minSegmentSeconds: 0.05
31094
- };
31095
- var cursorMotion = {
31096
- minMoveSeconds: 0.38,
31097
- maxMoveSeconds: 1.7,
31098
- speedPxPerSecond: 700,
31099
- defaultLeadSeconds: 0.16,
31100
- clickShrinkSeconds: 0.08,
31101
- clickGrowSeconds: 0.14,
31102
- clickDwellGapStartSeconds: 0.75,
31103
- clickDwellFullGapSeconds: 3.2,
31104
- clickMinDwellSeconds: 0.12,
31105
- clickMaxDwellSeconds: 1.15,
31106
- maxDwellGapShare: 0.45,
31107
- dragLeadSeconds: 0.55,
31108
- maxKeyframes: 120
31109
- };
31110
- var cursorStyle = {
31111
- size: 30,
31112
- clickSize: 24
31113
- };
31114
-
31115
- // src/commands/computer/recording/ffmpeg-expressions.ts
31116
- function expressionNumber(n) {
31117
- return Number.isInteger(n) ? String(n) : n.toFixed(3);
31118
- }
31119
- function mixNumber(from, to, progress) {
31120
- return from + (to - from) * progress;
31121
- }
31122
- function easeInOutSmootherNumber(progress) {
31123
- const p = clamp(progress, 0, 1);
31124
- return p * p * p * (p * (p * 6 - 15) + 10);
31125
- }
31126
- function easeInOutSmootherExpression(progress) {
31127
- const p = `(${progress})`;
31128
- return `${p}*${p}*${p}*(${p}*(${p}*6-15)+10)`;
31129
- }
31130
- function progressOverTime(timeExpression, seconds) {
31131
- return easeInOutSmootherExpression(`min(max((${timeExpression})/${seconds.toFixed(3)}\\,0)\\,1)`);
31132
- }
31133
- function progressBetween(start, duration3) {
31134
- return easeInOutSmootherExpression(`min(max((t-${start.toFixed(3)})/${duration3.toFixed(3)}\\,0)\\,1)`);
31135
- }
31136
- function mixExpression(from, to, progress) {
31137
- return `${expressionNumber(from)}+(${expressionNumber(to - from)})*${progress}`;
31138
- }
31139
- function cropOrigin(point, zoom, size) {
31140
- return clamp(point * zoom - size / 2, 0, size * zoom - size);
31141
- }
31142
- function easedCropOrigin(fromPoint, toPoint, fromZoom, toZoom, size, progress) {
31143
- return mixExpression(
31144
- cropOrigin(fromPoint, fromZoom, size),
31145
- cropOrigin(toPoint, toZoom, size),
31146
- progress
31147
- );
31148
- }
31149
- function betweenExpression(start, end) {
31150
- return `between(t\\,${start.toFixed(3)}\\,${end.toFixed(3)})`;
31151
- }
31152
-
31153
- // src/commands/computer/recording/timeline.ts
31154
- function centerOf(size) {
31155
- return { x: size.width / 2, y: size.height / 2 };
31156
- }
31157
- function actionPoint(action, size) {
31158
- const x = action.toX ?? action.x ?? size.width / 2;
31159
- const y = action.toY ?? action.y ?? size.height / 2;
31160
- return { x: clamp(x, 0, size.width), y: clamp(y, 0, size.height) };
31161
- }
31162
- function idleSpeed(duration3) {
31163
- if (duration3 < 1.2) return 1;
31164
- return clamp(1 + Math.log1p(duration3 - 0.8) * 1.8, 1.5, 5);
31165
- }
31166
- function renderedGapSeconds(duration3) {
31167
- return duration3 / idleSpeed(duration3);
31168
- }
31169
- function timelineActions(actions) {
31170
- return actions.filter((action) => action.type !== "key" && action.type !== "move");
31171
- }
31172
- function actionWindow(action, duration3) {
31173
- const shouldZoom = action.type === "click";
31174
- const at = clamp(action.atMs / 1e3, 0, duration3);
31175
- const start = clamp(at - (shouldZoom ? cameraMotion.clickLeadSeconds : cameraMotion.actionLeadSeconds), 0, duration3);
31176
- const tail = shouldZoom ? cameraMotion.clickTailSeconds : action.type === "type" ? 1.5 : 0.9;
31177
- return { start, end: clamp(at + tail, start, duration3), shouldZoom };
31178
- }
31179
- function nextClickStart(actions, index, duration3) {
31180
- const nextClick = actions.slice(index + 1).find((candidate) => candidate.type === "click");
31181
- return nextClick ? clamp(nextClick.atMs / 1e3 - cameraMotion.clickLeadSeconds, 0, duration3) : null;
31182
- }
31183
- function shouldPreserveZoom(nextStart, cursor, lastZoom) {
31184
- return lastZoom > 1 && nextStart !== null && nextStart > cursor && renderedGapSeconds(nextStart - cursor) <= cameraMotion.closeClickOutputGapSeconds;
31185
- }
31186
- function fullFrameSegment(duration3, size) {
31187
- const center = centerOf(size);
31188
- return {
31189
- start: 0,
31190
- end: duration3,
31191
- speed: 1,
31192
- motionSeconds: cameraMotion.returnSeconds,
31193
- fromZoom: 1,
31194
- zoom: 1,
31195
- fromX: center.x,
31196
- fromY: center.y,
31197
- ...center
31198
- };
31199
- }
31200
- function buildRecordingSegments(actions, duration3, size) {
31201
- const actionsForTimeline = timelineActions(actions);
31202
- if (actionsForTimeline.length === 0) return [fullFrameSegment(duration3, size)];
31203
- const center = centerOf(size);
31204
- const segments = [];
31205
- let cursor = 0;
31206
- let lastPoint = center;
31207
- let lastZoom = 1;
31208
- for (let i = 0; i < actionsForTimeline.length; i++) {
31209
- const action = actionsForTimeline[i];
31210
- const { start, end, shouldZoom } = actionWindow(action, duration3);
31211
- const preserveZoom = !shouldZoom && shouldPreserveZoom(nextClickStart(actionsForTimeline, i, duration3), cursor, lastZoom);
31212
- const point = shouldZoom ? actionPoint(action, size) : preserveZoom ? lastPoint : center;
31213
- const zoom = shouldZoom ? cameraMotion.clickZoom : preserveZoom ? lastZoom : 1;
31214
- if (start > cursor + cameraMotion.minSegmentSeconds) {
31215
- const gap = start - cursor;
31216
- const speed = idleSpeed(gap);
31217
- const bridgeZoom = shouldZoom && lastZoom > 1 && renderedGapSeconds(gap) <= cameraMotion.closeClickOutputGapSeconds;
31218
- segments.push({
31219
- start: cursor,
31220
- end: start,
31221
- speed,
31222
- motionSeconds: bridgeZoom ? Math.min(cameraMotion.clickMoveSeconds, Math.max(0.18, gap / speed)) : cameraMotion.returnSeconds,
31223
- fromZoom: lastZoom,
31224
- zoom: bridgeZoom ? cameraMotion.clickZoom : zoom,
31225
- fromX: lastPoint.x,
31226
- fromY: lastPoint.y,
31227
- ...point
31228
- });
31229
- lastZoom = bridgeZoom ? cameraMotion.clickZoom : zoom;
31230
- lastPoint = point;
31231
- }
31232
- const actionStart = Math.max(cursor, start);
31233
- if (end > actionStart + cameraMotion.minSegmentSeconds) {
31234
- segments.push({
31235
- start: actionStart,
31236
- end,
31237
- speed: 1,
31238
- motionSeconds: shouldZoom ? cameraMotion.clickMoveSeconds : cameraMotion.returnSeconds,
31239
- fromZoom: lastZoom,
31240
- zoom,
31241
- fromX: lastPoint.x,
31242
- fromY: lastPoint.y,
31243
- ...point
31244
- });
31245
- cursor = end;
31246
- lastZoom = zoom;
31247
- }
31248
- lastPoint = point;
31249
- }
31250
- if (duration3 > cursor + cameraMotion.minSegmentSeconds) {
31251
- segments.push({
31252
- start: cursor,
31253
- end: duration3,
31254
- speed: idleSpeed(duration3 - cursor),
31255
- motionSeconds: cameraMotion.returnSeconds,
31256
- fromZoom: lastZoom,
31257
- zoom: 1,
31258
- fromX: lastPoint.x,
31259
- fromY: lastPoint.y,
31260
- ...center
31261
- });
31262
- }
31263
- return segments.filter((segment) => segment.end > segment.start + cameraMotion.minSegmentSeconds);
31264
- }
31265
-
31266
- // src/commands/computer/recording/cursor.ts
31267
- var cursorHotspotRatio = { x: 8 / 32, y: 4 / 32 };
31268
- function cursorAssets(stamp) {
31269
- return {
31270
- path: `/tmp/replicas-cursor-${stamp}.svg`,
31271
- size: cursorStyle.size
31272
- };
31273
- }
31274
- function clickDwellSeconds(gapSeconds) {
31275
- if (gapSeconds <= cursorMotion.clickDwellGapStartSeconds) return cursorMotion.defaultLeadSeconds;
31276
- const progress = (gapSeconds - cursorMotion.clickDwellGapStartSeconds) / (cursorMotion.clickDwellFullGapSeconds - cursorMotion.clickDwellGapStartSeconds);
31277
- const dwell = mixNumber(
31278
- cursorMotion.clickMinDwellSeconds,
31279
- cursorMotion.clickMaxDwellSeconds,
31280
- easeInOutSmootherNumber(progress)
31281
- );
31282
- return Math.min(dwell, gapSeconds * cursorMotion.maxDwellGapShare);
31283
- }
31284
- function arrivalLeadSeconds(action, gapSeconds) {
31285
- return action.type === "click" ? clickDwellSeconds(gapSeconds) : cursorMotion.defaultLeadSeconds;
31286
- }
31287
- function cursorKeyframe(action, at, previousAt, size) {
31288
- if (typeof action.x !== "number" || typeof action.y !== "number") return null;
31289
- const point = actionPoint(action, size);
31290
- const lead = arrivalLeadSeconds(action, at - previousAt);
31291
- return { ...point, at, arriveAt: clamp(at - lead, previousAt, at) };
31292
- }
31293
- function dedupeKeyframes(points) {
31294
- return points.sort((a, b) => a.at - b.at).filter(
31295
- (point, index, all) => index === 0 || point.at - all[index - 1].at > 0.08 || Math.hypot(point.x - all[index - 1].x, point.y - all[index - 1].y) > 24
31296
- );
31297
- }
31298
- function downsampleKeyframes(points, actions, size) {
31299
- if (points.length <= cursorMotion.maxKeyframes) return points;
31300
- const anchors = actions.filter((action) => action.type !== "move").map((action) => actionPoint(action, size));
31301
- const result = points.filter(
31302
- (point, index) => index === 0 || index === points.length - 1 || anchors.some((anchor) => Math.hypot(anchor.x - point.x, anchor.y - point.y) < 1)
31303
- );
31304
- const step = points.length / Math.max(1, cursorMotion.maxKeyframes - result.length);
31305
- for (let i = 0; result.length < cursorMotion.maxKeyframes && i < points.length; i += step) {
31306
- const point = points[Math.floor(i)];
31307
- if (!result.includes(point)) result.push(point);
31308
- }
31309
- return result.sort((a, b) => a.at - b.at);
31310
- }
31311
- function cursorKeyframes(actions, duration3, size) {
31312
- const center = { x: size.width / 2, y: size.height / 2 };
31313
- const points = [{ at: 0, arriveAt: 0, ...center }];
31314
- for (const action of actions) {
31315
- const at = clamp(action.atMs / 1e3, 0, duration3);
31316
- const previousAt = points[points.length - 1].at;
31317
- if (action.type === "drag" && typeof action.x === "number" && typeof action.y === "number") {
31318
- const startAt = clamp(at - cursorMotion.dragLeadSeconds, 0, duration3);
31319
- points.push({
31320
- at: startAt,
31321
- arriveAt: startAt,
31322
- x: clamp(action.x, 0, size.width),
31323
- y: clamp(action.y, 0, size.height)
31324
- });
31325
- if (typeof action.toX === "number" && typeof action.toY === "number") {
31326
- points.push({
31327
- at,
31328
- arriveAt: at,
31329
- x: clamp(action.toX, 0, size.width),
31330
- y: clamp(action.toY, 0, size.height)
31331
- });
31332
- }
31333
- continue;
31334
- }
31335
- const point = cursorKeyframe(action, at, previousAt, size);
31336
- if (point) points.push(point);
31337
- }
31338
- return downsampleKeyframes(dedupeKeyframes(points), actions, size);
31339
- }
31340
- function cursorAxisExpression(points, axis) {
31341
- if (points.length === 0) return "0";
31342
- let expression = expressionNumber(points[points.length - 1][axis]);
31343
- for (let i = points.length - 2; i >= 0; i--) {
31344
- const from = points[i];
31345
- const to = points[i + 1];
31346
- const arriveAt = clamp(to.arriveAt, from.at, to.at);
31347
- const moveWindow = Math.max(1e-3, arriveAt - from.at);
31348
- const distance = Math.hypot(to.x - from.x, to.y - from.y);
31349
- const moveSeconds = Math.min(
31350
- moveWindow,
31351
- Math.max(cursorMotion.minMoveSeconds, Math.min(cursorMotion.maxMoveSeconds, distance / cursorMotion.speedPxPerSecond))
31352
- );
31353
- const moveStart = Math.max(from.at, arriveAt - moveSeconds);
31354
- const duration3 = Math.max(1e-3, arriveAt - moveStart);
31355
- const interpolated = mixExpression(from[axis], to[axis], progressBetween(moveStart, duration3));
31356
- expression = `if(lte(t\\,${moveStart.toFixed(3)})\\,${expressionNumber(from[axis])}\\,if(lte(t\\,${to.at.toFixed(3)})\\,${interpolated}\\,${expression}))`;
31357
- }
31358
- return expression;
31359
- }
31360
- function cursorSizeExpression(actions, normalSize) {
31361
- let expression = expressionNumber(normalSize);
31362
- const clicks = actions.filter((action) => action.type === "click").sort((a, b) => b.atMs - a.atMs);
31363
- for (const action of clicks) {
31364
- const at = action.atMs / 1e3;
31365
- const shrinkStart = Math.max(0, at - cursorMotion.clickShrinkSeconds);
31366
- const shrink = mixExpression(
31367
- normalSize,
31368
- cursorStyle.clickSize,
31369
- progressBetween(shrinkStart, Math.max(1e-3, at - shrinkStart))
31370
- );
31371
- const grow = mixExpression(cursorStyle.clickSize, normalSize, progressBetween(at, cursorMotion.clickGrowSeconds));
31372
- expression = `if(${betweenExpression(shrinkStart, at)}\\,${shrink}\\,if(${betweenExpression(at, at + cursorMotion.clickGrowSeconds)}\\,${grow}\\,${expression}))`;
31373
- }
31374
- return expression;
31375
- }
31376
- function cursorOverlayPositionExpression(cursor, size, hotspotRatio, overlaySize) {
31377
- return `max(0\\,min(${expressionNumber(size)}-${overlaySize}\\,(${cursor})-${hotspotRatio.toFixed(6)}*${overlaySize}))`;
31378
- }
31379
- function cursorSvg(size) {
31380
- return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7.7 3.4c-1.45 0-2.62 1.17-2.62 2.62v19.7c0 2.46 3.02 3.65 4.7 1.85l5.06-5.44a5.42 5.42 0 0 1 3.96-1.73h5.82c2.48 0 3.63-3.08 1.76-4.7L9.42 4.04A2.62 2.62 0 0 0 7.7 3.4Z" fill="white" fill-opacity="0.98" stroke="black" stroke-opacity="0.88" stroke-width="2.9" stroke-linejoin="round"/></svg>`;
31381
- }
31382
- function buildCursorOverlayFilter(actions, duration3, size, assets, projectCursor = null, inputLabel = "[screen]", outputLabel = "[v]") {
31383
- const points = cursorKeyframes(actions, duration3, size);
31384
- const rawCursorX = cursorAxisExpression(points, "x");
31385
- const rawCursorY = cursorAxisExpression(points, "y");
31386
- const cursorX = projectCursor ? projectCursor.x(rawCursorX) : rawCursorX;
31387
- const cursorY = projectCursor ? projectCursor.y(rawCursorY) : rawCursorY;
31388
- const cursorSize = cursorSizeExpression(actions, assets.size);
31389
- const cursorOverlayX = cursorOverlayPositionExpression(cursorX, size.width, cursorHotspotRatio.x, "overlay_w");
31390
- const cursorOverlayY = cursorOverlayPositionExpression(cursorY, size.height, cursorHotspotRatio.y, "overlay_h");
31391
- return `[1:v]scale=w='${cursorSize}':h='${cursorSize}':eval=frame[cursor];${inputLabel}[cursor]overlay=x='${cursorOverlayX}':y='${cursorOverlayY}':eof_action=repeat:shortest=1:eval=frame${outputLabel}`;
31392
- }
31393
-
31394
- // src/commands/computer/recording/render.ts
31395
- function videoDuration(path6) {
31396
- const r = spawnSync2("ffprobe", [
31397
- "-v",
31398
- "error",
31399
- "-show_entries",
31400
- "format=duration",
31401
- "-of",
31402
- "default=noprint_wrappers=1:nokey=1",
31403
- path6
31404
- ], { stdio: "pipe" });
31405
- if (r.status !== 0) fail(`ffprobe failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
31406
- const duration3 = Number.parseFloat(r.stdout?.toString().trim() ?? "");
31407
- if (!Number.isFinite(duration3) || duration3 <= 0) fail(`could not read recording duration for ${path6}`);
31408
- return duration3;
31409
- }
31410
- function renderedSegmentSpans(segments) {
31411
- let outputStart = 0;
31412
- return segments.map((segment) => {
31413
- const outputEnd = outputStart + (segment.end - segment.start) / segment.speed;
31414
- const span = {
31415
- rawStart: segment.start,
31416
- rawEnd: segment.end,
31417
- outputStart,
31418
- outputEnd,
31419
- speed: segment.speed
31420
- };
31421
- outputStart = outputEnd;
31422
- return span;
31423
- });
31424
- }
31425
- function outputTimeForRawTime(spans, rawTime) {
31426
- const span = spans.find((candidate) => rawTime >= candidate.rawStart && rawTime <= candidate.rawEnd);
31427
- if (span) return span.outputStart + (rawTime - span.rawStart) / span.speed;
31428
- if (spans.length === 0) return rawTime;
31429
- if (rawTime <= spans[0].rawStart) return spans[0].outputStart;
31430
- const lastSpan = spans[spans.length - 1];
31431
- if (rawTime >= lastSpan.rawEnd) return lastSpan.outputEnd;
31432
- return spans.find((candidate) => rawTime < candidate.rawStart)?.outputStart ?? lastSpan.outputEnd;
31433
- }
31434
- function actionsOnRenderedTimeline(actions, spans) {
31435
- return actions.map((action) => ({
31436
- ...action,
31437
- atMs: outputTimeForRawTime(spans, action.atMs / 1e3) * 1e3
31438
- }));
31439
- }
31440
- function segmentMotionSeconds(segment) {
31441
- return Math.min(segment.motionSeconds, Math.max(0.12, (segment.end - segment.start) / segment.speed));
31442
- }
31443
- function scaledFrameClampExpression(value, zoom, outputSize) {
31444
- return `max(0\\,min(${outputSize}*(${zoom})-${outputSize}\\,${value}))`;
31445
- }
31446
- function cameraTransformExpressions(segment, size, timeExpression) {
31447
- const progress = progressOverTime(timeExpression, segmentMotionSeconds(segment));
31448
- const zoom = mixExpression(segment.fromZoom, segment.zoom, progress);
31449
- return {
31450
- zoom,
31451
- cropX: scaledFrameClampExpression(
31452
- easedCropOrigin(segment.fromX, segment.x, segment.fromZoom, segment.zoom, size.width, progress),
31453
- zoom,
31454
- size.width
31455
- ),
31456
- cropY: scaledFrameClampExpression(
31457
- easedCropOrigin(segment.fromY, segment.y, segment.fromZoom, segment.zoom, size.height, progress),
31458
- zoom,
31459
- size.height
31460
- )
31461
- };
31462
- }
31463
- function projectedCursorExpression(rawCursor, axis, segments, spans, size) {
31464
- let expression = rawCursor;
31465
- for (let i = segments.length - 1; i >= 0; i--) {
31466
- const span = spans[i];
31467
- const camera = cameraTransformExpressions(segments[i], size, `t-${span.outputStart.toFixed(3)}`);
31468
- const crop = axis === "x" ? camera.cropX : camera.cropY;
31469
- const projected = `(${rawCursor})*(${camera.zoom})-(${crop})`;
31470
- expression = `if(${betweenExpression(span.outputStart, span.outputEnd)}\\,${projected}\\,${expression})`;
31471
- }
31472
- return expression;
31473
- }
31474
- function renderRecording(rawPath, target, actions, fps, size) {
31475
- if (actions.length === 0) {
31476
- copyFileSync(rawPath, target);
31477
- return;
31478
- }
31479
- const duration3 = videoDuration(rawPath);
31480
- const segments = buildRecordingSegments(actions, duration3, size);
31481
- if (segments.length === 0) {
31482
- copyFileSync(rawPath, target);
31483
- return;
31484
- }
31485
- const stamp = `${process.pid}-${Date.now()}`;
31486
- const cursor = cursorAssets(stamp);
31487
- writeFileSync3(cursor.path, cursorSvg(cursor.size));
31488
- const spans = renderedSegmentSpans(segments);
31489
- const renderedDuration = spans.length ? spans[spans.length - 1].outputEnd : duration3;
31490
- const renderedActions = actionsOnRenderedTimeline(actions, spans);
31491
- const cursorFilter = buildCursorOverlayFilter(
31492
- renderedActions,
31493
- renderedDuration,
31494
- size,
31495
- cursor,
31496
- {
31497
- x: (cursorX) => projectedCursorExpression(cursorX, "x", segments, spans, size),
31498
- y: (cursorY) => projectedCursorExpression(cursorY, "y", segments, spans, size)
31499
- }
31500
- );
31501
- const screenLabels = segments.map((_, index) => `[screen${index}]`).join("");
31502
- const screenSplitFilter = `[0:v]split=${segments.length}${screenLabels}`;
31503
- const filters = segments.map((segment, index) => {
31504
- const { zoom, cropX, cropY } = cameraTransformExpressions(segment, size, "t");
31505
- return `[screen${index}]trim=start=${segment.start.toFixed(3)}:end=${segment.end.toFixed(3)},setpts=(PTS-STARTPTS)/${segment.speed.toFixed(3)},scale=w=${size.width}*(${zoom}):h=${size.height}*(${zoom}):eval=frame,crop=${size.width}:${size.height}:x=${cropX}:y=${cropY},setsar=1[v${index}]`;
31506
- });
31507
- const concatInputs = segments.map((_, index) => `[v${index}]`).join("");
31508
- const filter = `${screenSplitFilter};${filters.join(";")};${concatInputs}concat=n=${segments.length}:v=1:a=0[screen];${cursorFilter}`;
31509
- const filterPath = `/tmp/replicas-recording-filter-${stamp}.ffgraph`;
31510
- writeFileSync3(filterPath, filter);
31511
- try {
31512
- const r = spawnSync2("ffmpeg", [
31513
- "-y",
31514
- "-hide_banner",
31515
- "-loglevel",
31516
- "warning",
31517
- "-i",
31518
- rawPath,
31519
- "-loop",
31520
- "1",
31521
- "-i",
31522
- cursor.path,
31523
- "-filter_complex_script",
31524
- filterPath,
31525
- "-map",
31526
- "[v]",
31527
- "-r",
31528
- String(fps),
31529
- "-c:v",
31530
- "libx264",
31531
- "-preset",
31532
- "veryfast",
31533
- "-crf",
31534
- "23",
31535
- "-pix_fmt",
31536
- "yuv420p",
31537
- "-movflags",
31538
- "+faststart",
31539
- target
31540
- ], { stdio: "pipe", maxBuffer: 20 * 1024 * 1024 });
31541
- if (r.status !== 0) {
31542
- fail(`recording post-processing failed: ${r.error?.message || r.stderr?.toString().trim() || `exit ${r.status}`}`);
31543
- }
31544
- } finally {
31545
- rmSync2(filterPath, { force: true });
31546
- rmSync2(cursor.path, { force: true });
31547
- }
31548
- }
31549
-
31550
- // src/commands/computer/recording.ts
31551
- var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
31552
- var RECORD_PATH_FILE = `${STATE_DIR}/recording-path.txt`;
31553
- var RECORD_RAW_PATH_FILE = `${STATE_DIR}/recording-raw-path.txt`;
31554
- var RECORD_STARTED_AT_FILE = `${STATE_DIR}/recording-started-at.txt`;
31555
- var RECORD_FPS_FILE = `${STATE_DIR}/recording-fps.txt`;
31556
- var RECORD_DIMENSIONS_FILE = `${STATE_DIR}/recording-dimensions.json`;
31557
- var RECORD_ACTIONS_FILE = `${STATE_DIR}/recording-actions.jsonl`;
31558
- var RECORD_STATE_FILES = [
31559
- RECORD_PID_FILE,
31560
- RECORD_PATH_FILE,
31561
- RECORD_RAW_PATH_FILE,
31562
- RECORD_STARTED_AT_FILE,
31563
- RECORD_FPS_FILE,
31564
- RECORD_DIMENSIONS_FILE,
31565
- RECORD_ACTIONS_FILE
31566
- ];
31567
- function clearRecordingState() {
31568
- for (const file2 of RECORD_STATE_FILES) rmSync3(file2, { force: true });
31569
- }
31570
- function recordingStartedAt() {
31571
- if (!existsSync2(RECORD_STARTED_AT_FILE)) return null;
31572
- const startedAt = Number.parseInt(readFileSync4(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
31573
- return Number.isFinite(startedAt) ? startedAt : null;
31574
- }
31575
- function logRecordingAction(action) {
31576
- const startedAt = recordingStartedAt();
31577
- if (!startedAt) return;
31578
- const atMs = Date.now() - startedAt;
31579
- appendFileSync(RECORD_ACTIONS_FILE, `${JSON.stringify({ ...action, atMs })}
31580
- `);
31581
- }
31582
- function readRecordingDimensions() {
31583
- if (!existsSync2(RECORD_DIMENSIONS_FILE)) return configuredDesktopDimensions();
31584
- try {
31585
- const dimensions = JSON.parse(readFileSync4(RECORD_DIMENSIONS_FILE, "utf8"));
31586
- const width = dimensions?.width;
31587
- const height = dimensions?.height;
31588
- if (typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0) {
31589
- return { width, height };
31590
- }
31591
- } catch {
31592
- }
31593
- fail("invalid recording dimensions state");
31594
- }
31595
- function isRecordingActionType(value) {
31596
- return value === "click" || value === "drag" || value === "key" || value === "move" || value === "scroll" || value === "type";
31597
- }
31598
- function isOptionalNumber(value) {
31599
- return value === void 0 || typeof value === "number";
31600
- }
31601
- function readRecordingActions() {
31602
- if (!existsSync2(RECORD_ACTIONS_FILE)) return [];
31603
- return readFileSync4(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
31604
- try {
31605
- const value = JSON.parse(line);
31606
- if (typeof value !== "object" || value === null) return [];
31607
- const type = "type" in value ? value.type : void 0;
31608
- const atMs = "atMs" in value ? value.atMs : void 0;
31609
- const x = "x" in value ? value.x : void 0;
31610
- const y = "y" in value ? value.y : void 0;
31611
- const toX = "toX" in value ? value.toX : void 0;
31612
- const toY = "toY" in value ? value.toY : void 0;
31613
- if (!isRecordingActionType(type) || typeof atMs !== "number" || !Number.isFinite(atMs) || !isOptionalNumber(x) || !isOptionalNumber(y) || !isOptionalNumber(toX) || !isOptionalNumber(toY)) {
31614
- return [];
31615
- }
31616
- return [{ type, atMs, x, y, toX, toY }];
31617
- } catch {
31618
- return [];
31619
- }
31620
- }).sort((a, b) => a.atMs - b.atMs);
31621
- }
31622
- async function computerRecordStartCommand(path6, options) {
31623
- ensureServicesRunning();
31624
- if (existsSync2(RECORD_PID_FILE)) {
31625
- const pid = parseInt(readFileSync4(RECORD_PID_FILE, "utf8").trim(), 10);
31626
- if (Number.isFinite(pid)) {
31627
- let alive2 = false;
31628
- try {
31629
- process.kill(pid, 0);
31630
- alive2 = true;
31631
- } catch {
31632
- }
31633
- if (alive2) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
31634
- }
31635
- }
31636
- const target = resolvePath(path6);
31637
- mkdirSync4(dirname2(target), { recursive: true });
31638
- const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
31639
- const { width, height } = configuredDesktopDimensions();
31640
- mkdirSync4(STATE_DIR, { recursive: true });
31641
- const rawTarget = `${target}.raw-${Date.now()}.mp4`;
31642
- rmSync3(RECORD_ACTIONS_FILE, { force: true });
31643
- const child = spawn4("ffmpeg", [
31644
- "-y",
31645
- "-hide_banner",
31646
- "-loglevel",
31647
- "warning",
31648
- "-f",
31649
- "x11grab",
31650
- "-framerate",
31651
- String(fps),
31652
- "-video_size",
31653
- `${width}x${height}`,
31654
- "-draw_mouse",
31655
- "0",
31656
- "-i",
31657
- DEFAULT_DISPLAY,
31658
- "-c:v",
31659
- "libx264",
31660
- "-preset",
31661
- "ultrafast",
31662
- "-tune",
31663
- "zerolatency",
31664
- "-crf",
31665
- "23",
31666
- "-pix_fmt",
31667
- "yuv420p",
31668
- "-movflags",
31669
- "+faststart+frag_keyframe+empty_moov",
31670
- rawTarget
31671
- ], { detached: true, stdio: "ignore" });
31672
- child.unref();
31673
- if (!child.pid) fail("failed to launch ffmpeg");
31674
- writeFileSync4(RECORD_PID_FILE, String(child.pid));
31675
- writeFileSync4(RECORD_PATH_FILE, target);
31676
- writeFileSync4(RECORD_RAW_PATH_FILE, rawTarget);
31677
- writeFileSync4(RECORD_STARTED_AT_FILE, String(Date.now()));
31678
- writeFileSync4(RECORD_FPS_FILE, String(fps));
31679
- writeFileSync4(RECORD_DIMENSIONS_FILE, JSON.stringify({ width, height }));
31680
- const startedAt = Date.now();
31681
- while (Date.now() - startedAt < 5e3) {
31682
- try {
31683
- process.kill(child.pid, 0);
31684
- if (existsSync2(rawTarget) && statSync2(rawTarget).size > 0) {
31685
- console.log(`${target} (recording ready in ${Date.now() - startedAt}ms)`);
31686
- return;
31687
- }
31688
- } catch {
31689
- break;
31690
- }
31691
- await sleep2(100);
31692
- }
31693
- let alive = false;
31694
- try {
31695
- process.kill(child.pid, 0);
31696
- alive = true;
31697
- } catch {
31698
- }
31699
- if (alive) fail("ffmpeg is running but did not produce recording output within 5 seconds; run `replicas computer record stop` to finalize or retry");
31700
- clearRecordingState();
31701
- rmSync3(rawTarget, { force: true });
31702
- fail("ffmpeg exited before screen recording became ready");
31703
- }
31704
- async function computerRecordStopCommand() {
31705
- if (!existsSync2(RECORD_PID_FILE) && !existsSync2(RECORD_PATH_FILE)) fail("no recording in progress");
31706
- if (existsSync2(RECORD_PID_FILE)) {
31707
- const pid = parseInt(readFileSync4(RECORD_PID_FILE, "utf8").trim(), 10);
31708
- if (!Number.isFinite(pid)) fail("invalid recording pidfile");
31709
- try {
31710
- process.kill(pid, "SIGINT");
31711
- } catch {
31712
- }
31713
- let alive = true;
31714
- for (let i = 0; i < 150; i++) {
31715
- try {
31716
- process.kill(pid, 0);
31717
- } catch {
31718
- alive = false;
31719
- break;
31720
- }
31721
- await sleep2(200);
31722
- }
31723
- if (alive) fail(`ffmpeg did not finalize recording within 30 seconds (pid ${pid})`);
31724
- }
31725
- if (existsSync2(RECORD_PATH_FILE)) {
31726
- const target = readFileSync4(RECORD_PATH_FILE, "utf8").trim();
31727
- const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync4(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
31728
- const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync4(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
31729
- const size = readRecordingDimensions();
31730
- const actions = readRecordingActions();
31731
- if (rawPath !== target) {
31732
- renderRecording(rawPath, target, actions, Number.isFinite(fps) ? fps : 60, size);
31733
- rmSync3(rawPath, { force: true });
31734
- }
31735
- console.log(target);
31736
- }
31737
- clearRecordingState();
31738
- }
31739
-
31740
- // src/commands/computer/index.ts
31741
- function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
31742
- const pids = rootsOnly ? details.rootPids ?? [] : details.listenerPids ?? [];
31743
- const listenerCount = rootsOnly ? details.rootListeners ?? pids.length : details.listeners ?? pids.length;
31744
- const listenerLabel = rootsOnly ? "root listener" : "listener";
31745
- const parts = [`${listenerCount} ${listenerLabel}${listenerCount === 1 ? "" : "s"} on ${details.port}`];
31746
- if (includeBacklog) parts.push(`backlog ${details.backlog ?? 0}`);
31747
- if (pids.length > 0) parts.push(`pid${pids.length === 1 ? "" : "s"} ${pids.join(",")}`);
31748
- return ` (${parts.join(", ")})`;
31749
- }
31750
- async function lookupDesktopViewerUrl() {
31751
- try {
31752
- const list = await listAgentPreviews();
31753
- const preview2 = list.previews.find((p) => p.port === NOVNC_PORT);
31754
- return preview2 ? getDesktopViewerUrl(preview2.publicUrl) : null;
31755
- } catch {
31756
- return null;
31757
- }
31758
- }
31759
- async function waitForDesktopViewerUrl(timeoutMs) {
31760
- const deadline = Date.now() + timeoutMs;
31761
- while (Date.now() < deadline) {
31762
- const url2 = await lookupDesktopViewerUrl();
31763
- if (url2) return url2;
31764
- await new Promise((resolve3) => setTimeout(resolve3, INFO_WAIT_INTERVAL_MS));
31765
- }
31766
- return await lookupDesktopViewerUrl();
31767
- }
31768
- async function computerInfoCommand() {
31769
- const viewerUrl = await waitForDesktopViewerUrl(INFO_WAIT_TIMEOUT_MS);
31770
- if (!viewerUrl) {
31771
- fail(
31772
- `Desktop preview for port ${NOVNC_PORT} is not registered. The engine registers it at startup \u2014 check /tmp/replicas-desktop-stack.log and engine logs for errors.`
31773
- );
31774
- }
31775
- console.log(viewerUrl);
31776
- console.error(chalk21.dim(`Share this URL with the user to let them watch the desktop live.`));
31777
- }
31778
- async function computerStatusCommand() {
31779
- ensureServicesRunning();
31780
- const bridge = getDesktopBridgeStatus();
31781
- const procs = ["Xvfb", "openbox", "tint2", "x11vnc", "websockify"];
31782
- for (const p of procs) {
31783
- const r = spawnSync3("pgrep", ["-af", p], { stdio: "pipe" });
31784
- const running = r.status === 0 && !!r.stdout?.toString().trim();
31785
- const suffix = p === "x11vnc" && bridge ? bridgeStatus(bridge.x11vnc, true) : p === "websockify" && bridge ? bridgeStatus(bridge.websockify, false, true) : "";
31786
- console.log(` ${running ? chalk21.green("\u25CF") : chalk21.red("\u25CB")} ${p}${suffix}`);
31787
- }
31788
- const viewerUrl = await lookupDesktopViewerUrl();
31789
- if (viewerUrl) {
31790
- console.log(` ${chalk21.cyan("preview")}: ${viewerUrl}`);
31791
- } else {
31792
- console.log(` ${chalk21.dim("preview: not yet registered (engine registers it at startup)")}`);
31793
- }
31794
- }
31795
- var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
31796
- function readPngDimensions(filePath) {
31797
- const fd = openSync2(filePath, "r");
31798
- try {
31799
- const buf = Buffer.alloc(24);
31800
- const bytesRead = readSync(fd, buf, 0, 24, 0);
31801
- if (bytesRead < 24 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
31802
- fail(`${filePath} is not a valid PNG (read ${bytesRead}/24 bytes)`);
31803
- }
31804
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
31805
- } finally {
31806
- closeSync2(fd);
31807
- }
31808
- }
31809
- function brandSvgPath() {
31810
- const dir = process.env.REPLICAS_DESKTOP_TEMPLATES || "/usr/local/share/replicas/desktop";
31811
- return `${dir}/brand-wallpaper.svg`;
31812
- }
31813
- function loadBrandSvg(canvasW, canvasH) {
31814
- const path6 = brandSvgPath();
31815
- if (!existsSync3(path6)) {
31816
- fail(
31817
- `Brand wallpaper SVG missing at ${path6}. The workspace image is out of date \u2014 \`desktop/brand-wallpaper.svg\` must be installed at $REPLICAS_DESKTOP_TEMPLATES.`
31818
- );
31819
- }
31820
- return readFileSync5(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
31821
- }
31822
- var BRAND_PAD_FRACTION = 0.06;
31823
- var SCREENSHOT_CORNER_FRACTION = 0.022;
31824
- var SHADOW_SIGMA_FRACTION = 0.022;
31825
- var SHADOW_OFFSET_Y_FRACTION = 0.013;
31826
- var SHADOW_ALPHA = 0.6;
31827
- var SCREENSHOT_MASK_TEMPLATE = `<svg width="__W__" height="__H__" xmlns="http://www.w3.org/2000/svg"><rect width="__W__" height="__H__" rx="__R__" ry="__R__" fill="white"/></svg>`;
31828
- var SHADOW_MASK_TEMPLATE = `<svg width="__SW__" height="__SH__" xmlns="http://www.w3.org/2000/svg"><rect x="__M__" y="__M__" width="__W__" height="__H__" rx="__R__" ry="__R__" fill="white"/></svg>`;
31829
- function parseGridSize(value) {
31830
- if (value === void 0 || value === false) return null;
31831
- const size = value === true ? 100 : parseCoord(value, "--grid");
31832
- if (size < 25 || size > 500) fail("--grid must be between 25 and 500 pixels");
31833
- return size;
31834
- }
31835
- function buildGridSvg(width, height, size) {
31836
- const lines = [];
31837
- const labels = [];
31838
- for (let x = 0; x <= width; x += size) {
31839
- const strokeWidth = x % (size * 5) === 0 ? 2 : 1;
31840
- lines.push(`<line x1="${x}" y1="0" x2="${x}" y2="${height}" stroke="#00e5ff" stroke-opacity="0.42" stroke-width="${strokeWidth}"/>`);
31841
- if (x > 0 && x < width) labels.push(`<text x="${x + 4}" y="18">${x}</text>`);
31842
- }
31843
- for (let y = 0; y <= height; y += size) {
31844
- const strokeWidth = y % (size * 5) === 0 ? 2 : 1;
31845
- lines.push(`<line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="#00e5ff" stroke-opacity="0.42" stroke-width="${strokeWidth}"/>`);
31846
- if (y > 0 && y < height) labels.push(`<text x="4" y="${y - 4}">${y}</text>`);
31847
- }
31848
- return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
31849
- <style>text{font:16px monospace;fill:white;paint-order:stroke;stroke:black;stroke-width:4px;stroke-linejoin:round}</style>
31850
- ${lines.join("\n")}
31851
- ${labels.join("\n")}
31852
- </svg>`;
31853
- }
31854
- function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
31855
- writeFileSync5(gridPath, buildGridSvg(width, height, gridSize));
31856
- const r = spawnSync3(
31857
- "ffmpeg",
31858
- [
31859
- "-y",
31860
- "-hide_banner",
31861
- "-loglevel",
31862
- "error",
31863
- "-i",
31864
- rawPath,
31865
- "-i",
31866
- gridPath,
31867
- "-filter_complex",
31868
- "[0:v][1:v]overlay=0:0:format=auto",
31869
- "-frames:v",
31870
- "1",
31871
- "-update",
31872
- "1",
31873
- target
31874
- ],
31875
- { stdio: "pipe" }
31876
- );
31877
- if (r.status !== 0) {
31878
- fail(`ffmpeg grid overlay failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
31879
- }
31880
- }
31881
- async function computerScreenshotCommand(path6, options = {}) {
31882
- const target = resolvePath(path6);
31883
- mkdirSync5(dirname3(target), { recursive: true });
31884
- const stamp = `${process.pid}-${Date.now()}`;
31885
- const rawPath = `/tmp/replicas-screenshot-${stamp}.raw.png`;
31886
- const svgPath = `/tmp/replicas-screenshot-${stamp}.brand.svg`;
31887
- const maskPath = `/tmp/replicas-screenshot-${stamp}.mask.svg`;
31888
- const shadowPath = `/tmp/replicas-screenshot-${stamp}.shadow.svg`;
31889
- const gridPath = `/tmp/replicas-screenshot-${stamp}.grid.svg`;
31890
- try {
31891
- runDisplayCmd("scrot", ["-o", rawPath]);
31892
- const { width, height } = readPngDimensions(rawPath);
31893
- const gridSize = parseGridSize(options.grid);
31894
- if (options.raw && gridSize === null) {
31895
- copyFileSync2(rawPath, target);
31896
- console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels)`);
31897
- return;
31898
- }
31899
- if (gridSize !== null) {
31900
- overlayGrid(rawPath, target, width, height, gridSize, gridPath);
31901
- console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels, ${gridSize}px grid)`);
31902
- return;
31903
- }
31904
- const padX = Math.round(width * BRAND_PAD_FRACTION);
31905
- const padY = Math.round(height * BRAND_PAD_FRACTION);
31906
- const canvasW = width + padX * 2;
31907
- const canvasH = height + padY * 2;
31908
- const minDim = Math.min(width, height);
31909
- const cornerR = Math.round(minDim * SCREENSHOT_CORNER_FRACTION);
31910
- const shadowSigma = Math.max(12, Math.round(minDim * SHADOW_SIGMA_FRACTION));
31911
- const shadowOffsetY = Math.round(minDim * SHADOW_OFFSET_Y_FRACTION);
31912
- const shadowMargin = shadowSigma * 3;
31913
- const shadowW = width + shadowMargin * 2;
31914
- const shadowH = height + shadowMargin * 2;
31915
- writeFileSync5(svgPath, loadBrandSvg(canvasW, canvasH));
31916
- writeFileSync5(
31917
- maskPath,
31918
- SCREENSHOT_MASK_TEMPLATE.replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
31919
- );
31920
- writeFileSync5(
31921
- shadowPath,
31922
- SHADOW_MASK_TEMPLATE.replace(/__SW__/g, String(shadowW)).replace(/__SH__/g, String(shadowH)).replace(/__M__/g, String(shadowMargin)).replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
31923
- );
31924
- const r = spawnSync3(
31925
- "ffmpeg",
31926
- [
31927
- "-y",
31928
- "-hide_banner",
31929
- "-loglevel",
31930
- "error",
31931
- "-i",
31932
- svgPath,
31933
- "-i",
31934
- rawPath,
31935
- "-i",
31936
- maskPath,
31937
- "-i",
31938
- shadowPath,
31939
- "-filter_complex",
31940
- `[2:v]format=rgba,alphaextract[mask];[3:v]format=rgba,colorchannelmixer=rr=0:gg=0:bb=0:aa=${SHADOW_ALPHA},gblur=sigma=${shadowSigma}[shadow];[1:v]format=rgba[scr];[scr][mask]alphamerge[rounded];[0:v]format=rgba[bg];[bg][shadow]overlay=${padX - shadowMargin}:${padY - shadowMargin + shadowOffsetY}:format=auto[bg_shadow];[bg_shadow][rounded]overlay=${padX}:${padY}:format=auto`,
31941
- "-frames:v",
31942
- "1",
31943
- "-update",
31944
- "1",
31945
- target
31946
- ],
31947
- { stdio: "pipe" }
31948
- );
31949
- if (r.status !== 0) {
31950
- fail(`ffmpeg branding failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
31951
- }
31952
- } finally {
31953
- rmSync4(rawPath, { force: true });
31954
- rmSync4(svgPath, { force: true });
31955
- rmSync4(maskPath, { force: true });
31956
- rmSync4(shadowPath, { force: true });
31957
- rmSync4(gridPath, { force: true });
31958
- }
31959
- console.log(target);
31960
- }
31961
- function hashFile(path6) {
31962
- return createHash3("sha256").update(readFileSync5(path6)).digest("hex");
31963
- }
31964
- async function captureStableRawScreenshot(target, options) {
31965
- const start = Date.now();
31966
- const framePath = `${target}.frame.png`;
31967
- let lastHash = null;
31968
- let lastChangeAt = start;
31969
- let frames = 0;
31970
- let changes = 0;
31971
- let width = 0;
31972
- let height = 0;
31973
- try {
31974
- while (Date.now() - start <= options.timeoutMs) {
31975
- runDisplayCmd("scrot", ["-o", framePath]);
31976
- frames++;
31977
- const dimensions = readPngDimensions(framePath);
31978
- width = dimensions.width;
31979
- height = dimensions.height;
31980
- const hash2 = hashFile(framePath);
31981
- const now = Date.now();
31982
- if (lastHash === null) {
31983
- lastChangeAt = now;
31984
- } else if (hash2 !== lastHash) {
31985
- changes++;
31986
- lastChangeAt = now;
31987
- }
31988
- lastHash = hash2;
31989
- copyFileSync2(framePath, target);
31990
- if (frames > 1 && now - lastChangeAt >= options.stableMs) {
31991
- return { width, height, stable: true, elapsedMs: now - start, frames, changes };
31992
- }
31993
- await sleep2(options.pollMs);
31994
- }
31995
- return { width, height, stable: false, elapsedMs: Date.now() - start, frames, changes };
31996
- } finally {
31997
- rmSync4(framePath, { force: true });
31998
- }
31999
- }
32000
- function getActiveWindowTitle() {
32001
- return tryDisplayCmd("xdotool", ["getactivewindow", "getwindowname"]);
32002
- }
32003
- function getVisibleWindowTitles() {
32004
- const out = tryDisplayCmd("xdotool", ["search", "--onlyvisible", "--name", ".", "getwindowname", "%@"]);
32005
- return out ? out.split("\n").filter(Boolean).slice(0, 20) : [];
32006
- }
32007
- function recordingMousePosition() {
32008
- const mouse = getMouseLocation();
32009
- return mouse ? { x: mouse.x, y: mouse.y } : null;
32010
- }
32011
- async function computerObserveCommand(path6, options = {}) {
32012
- const target = resolvePath(path6);
32013
- mkdirSync5(dirname3(target), { recursive: true });
32014
- const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 3e3;
32015
- const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 600;
32016
- const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 200;
32017
- if (timeoutMs < 0) fail("--timeout must be >= 0");
32018
- if (stableMs < 0) fail("--stable-ms must be >= 0");
32019
- if (pollMs < 50 || pollMs > 2e3) fail("--poll-ms must be between 50 and 2000");
32020
- const stamp = `${process.pid}-${Date.now()}`;
32021
- const rawPath = `/tmp/replicas-observe-${stamp}.raw.png`;
32022
- const gridPath = `/tmp/replicas-observe-${stamp}.grid.svg`;
32023
- try {
32024
- ensureServicesRunning();
32025
- const capture = await captureStableRawScreenshot(rawPath, { timeoutMs, stableMs, pollMs });
32026
- const gridSize = options.raw ? null : parseGridSize(options.grid ?? true);
32027
- if (gridSize === null) {
32028
- copyFileSync2(rawPath, target);
32029
- } else {
32030
- overlayGrid(rawPath, target, capture.width, capture.height, gridSize, gridPath);
32031
- }
32032
- console.log(JSON.stringify({
32033
- screenshot: target,
32034
- width: capture.width,
32035
- height: capture.height,
32036
- stable: capture.stable,
32037
- elapsedMs: capture.elapsedMs,
32038
- frames: capture.frames,
32039
- changes: capture.changes,
32040
- mouse: getMouseLocation(),
32041
- activeWindow: getActiveWindowTitle(),
32042
- visibleWindows: getVisibleWindowTitles(),
32043
- mode: gridSize === null ? "raw" : "grid",
32044
- gridSize
32045
- }, null, 2));
32046
- } finally {
32047
- rmSync4(rawPath, { force: true });
32048
- rmSync4(gridPath, { force: true });
32049
- }
32050
- }
32051
- async function fetchChromeJson(path6) {
32052
- let res;
32053
- try {
32054
- res = await fetch(`http://127.0.0.1:${CHROME_DEBUG_PORT}${path6}`);
32055
- } catch {
32056
- fail(
32057
- `Chrome DevTools is not available on localhost:${CHROME_DEBUG_PORT}. Launch Chrome with \`replicas computer launch chrome <url>\` and try again.`
32058
- );
32059
- }
32060
- if (!res.ok) fail(`Chrome DevTools returned HTTP ${res.status}`);
32061
- try {
32062
- return await res.json();
32063
- } catch (error51) {
32064
- const reason = error51 instanceof Error ? error51.message : String(error51);
32065
- fail(`Chrome DevTools returned invalid JSON from ${path6}: ${reason}`);
32066
- }
32067
- }
32068
- async function getChromePages() {
32069
- const targets = await fetchChromeJson("/json/list");
32070
- if (!Array.isArray(targets)) fail("Chrome DevTools returned an unexpected target list");
32071
- return targets.filter((target) => typeof target === "object" && target !== null).filter((target) => target.type === "page");
32072
- }
32073
- async function selectChromePage(options = {}) {
32074
- const pages = await getChromePages();
32075
- let matches = pages;
32076
- if (options.targetId) {
32077
- matches = matches.filter((page2) => page2.id === options.targetId);
32078
- }
32079
- if (options.title) {
32080
- const needle = options.title.toLowerCase();
32081
- matches = matches.filter((page2) => (page2.title ?? "").toLowerCase().includes(needle));
32082
- }
32083
- if (options.url) {
32084
- const needle = options.url.toLowerCase();
32085
- matches = matches.filter((page2) => (page2.url ?? "").toLowerCase().includes(needle));
32086
- }
32087
- const index = options.page ? parseCoord(options.page, "--page") : 0;
32088
- if (index < 0) fail("--page must be >= 0");
32089
- const page = matches[index];
32090
- const webSocketDebuggerUrl = page?.webSocketDebuggerUrl;
32091
- if (!webSocketDebuggerUrl) {
32092
- fail(
32093
- `No matching debuggable Chrome page found (${matches.length} match${matches.length === 1 ? "" : "es"}, index ${index}). Use \`replicas computer browser\` to list pages.`
32094
- );
32095
- }
32096
- await sendChromeCommand(webSocketDebuggerUrl, "Page.bringToFront", {});
32097
- return { ...page, webSocketDebuggerUrl };
32098
- }
32099
- var VISIBLE_JS = `
32100
- const visible = (el) => {
32101
- const rect = el.getBoundingClientRect();
32102
- const style = getComputedStyle(el);
32103
- return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
32104
- };`;
32105
- async function sendChromeSessionCommand(session, method, params) {
32106
- return chromeResult(await session.send({ method, params }), method);
32107
- }
32108
- async function withChromeSession(webSocketDebuggerUrl, callback) {
32109
- return await new Promise((resolve3, reject) => {
32110
- const ws = new WebSocket(webSocketDebuggerUrl);
32111
- const pending = /* @__PURE__ */ new Map();
32112
- let nextId = 1;
32113
- let settled = false;
32114
- const failSession = (error51) => {
32115
- if (settled) return;
32116
- settled = true;
32117
- for (const request of pending.values()) {
32118
- clearTimeout(request.timeout);
32119
- request.reject(error51);
32120
- }
32121
- pending.clear();
32122
- ws.close();
32123
- reject(error51);
32124
- };
32125
- const finishSession = (value) => {
32126
- if (settled) return;
32127
- settled = true;
32128
- for (const request of pending.values()) clearTimeout(request.timeout);
32129
- pending.clear();
32130
- ws.close();
32131
- resolve3(value);
32132
- };
32133
- const session = {
32134
- send: ({ method, params }) => new Promise((resolveCommand, rejectCommand) => {
32135
- const id = nextId++;
32136
- const timeout = setTimeout(() => {
32137
- pending.delete(id);
32138
- rejectCommand(new Error(`Chrome DevTools ${method} timed out`));
32139
- }, 1e4);
32140
- pending.set(id, { method, resolve: resolveCommand, reject: rejectCommand, timeout });
32141
- ws.send(JSON.stringify({ id, method, params }));
32142
- })
32143
- };
32144
- ws.addEventListener("open", async () => {
32145
- try {
32146
- finishSession(await callback(session));
32147
- } catch (error51) {
32148
- failSession(error51 instanceof Error ? error51 : new Error(String(error51)));
32149
- }
32150
- });
32151
- ws.addEventListener("message", (event) => {
32152
- const data = typeof event.data === "string" ? event.data : Buffer.isBuffer(event.data) ? event.data.toString("utf8") : Buffer.from(event.data).toString("utf8");
32153
- let message;
32154
- try {
32155
- message = JSON.parse(data);
32156
- } catch (error51) {
32157
- failSession(new Error(`Chrome DevTools returned invalid WebSocket JSON: ${error51 instanceof Error ? error51.message : String(error51)}`));
32158
- return;
32159
- }
32160
- if (!isRecord(message) || typeof message.id !== "number") return;
32161
- const request = pending.get(message.id);
32162
- if (!request) return;
32163
- clearTimeout(request.timeout);
32164
- pending.delete(message.id);
32165
- const detail = isRecord(message.error) && typeof message.error.message === "string" ? message.error.message : void 0;
32166
- request.resolve(message.error ? { error: { message: detail } } : { result: isRecord(message.result) ? message.result : {} });
32167
- });
32168
- ws.addEventListener("error", () => failSession(new Error("Chrome DevTools WebSocket failed")));
32169
- ws.addEventListener("close", () => {
32170
- if (!settled) failSession(new Error("Chrome DevTools WebSocket closed unexpectedly"));
32171
- });
32172
- });
32173
- }
32174
- async function sendChromeCommands(webSocketDebuggerUrl, commands) {
32175
- return await withChromeSession(webSocketDebuggerUrl, async (session) => await Promise.all(commands.map((command) => session.send(command))));
32176
- }
32177
- async function sendChromeCommand(webSocketDebuggerUrl, method, params) {
32178
- const response = (await sendChromeCommands(webSocketDebuggerUrl, [{ method, params }]))[0];
32179
- if (response.error) throw new Error(response.error.message || `Chrome DevTools ${method} failed`);
32180
- return response.result ?? {};
32181
- }
32182
- async function callFunctionOnChromeNodeInSession(session, backendNodeId, functionDeclaration, args = []) {
32183
- const resolved = await sendChromeSessionCommand(session, "DOM.resolveNode", { backendNodeId });
32184
- const objectId = isRecord(resolved.object) ? resolved.object.objectId : void 0;
32185
- if (typeof objectId !== "string") throw new Error(`Could not resolve browser element ref ${backendNodeId}`);
32186
- return await sendChromeSessionCommand(session, "Runtime.callFunctionOn", {
32187
- objectId,
32188
- functionDeclaration,
32189
- arguments: args.map((value) => ({ value })),
32190
- returnByValue: true
32191
- });
32192
- }
32193
- function chromeCallValue(response) {
32194
- return isRecord(response.result) ? response.result.value : void 0;
32195
- }
32196
- async function evaluateChromeTarget(webSocketDebuggerUrl, expression) {
32197
- return await withChromeSession(webSocketDebuggerUrl, async (session) => await evaluateChromeSession(session, expression));
32198
- }
32199
- async function evaluateChromeSession(session, expression) {
32200
- const response = await sendChromeSessionCommand(session, "Runtime.evaluate", {
32201
- expression,
32202
- awaitPromise: true,
32203
- returnByValue: true
32204
- });
32205
- const exception = response.exceptionDetails;
32206
- if (exception) {
32207
- const text = typeof exception === "object" && exception !== null && "text" in exception && typeof exception.text === "string" ? exception.text : "";
32208
- throw new Error(text || "Chrome DevTools evaluation threw");
32209
- }
32210
- const evaluated = response.result;
32211
- return typeof evaluated === "object" && evaluated !== null && "value" in evaluated ? evaluated.value ?? null : null;
32212
- }
32213
- function isRawAXNode(value) {
32214
- return isRecord(value);
32215
- }
32216
- function isDOMSnapshotDocument(value) {
32217
- return isRecord(value);
32218
- }
32219
- function chromeVisualViewport(layout) {
32220
- const viewport = isRecord(layout.cssVisualViewport) ? layout.cssVisualViewport : {};
32221
- return {
32222
- width: typeof viewport.clientWidth === "number" ? viewport.clientWidth : 0,
32223
- height: typeof viewport.clientHeight === "number" ? viewport.clientHeight : 0
32224
- };
32225
- }
32226
- function roundBrowserRect(rect) {
32227
- return {
32228
- x: Math.round(rect.x),
32229
- y: Math.round(rect.y),
32230
- width: Math.round(rect.width),
32231
- height: Math.round(rect.height),
32232
- centerX: Math.round(rect.centerX),
32233
- centerY: Math.round(rect.centerY)
32234
- };
32235
- }
32236
- function browserStateProperties(node) {
32237
- const properties = [];
32238
- for (const { name, value } of node.properties ?? []) {
32239
- const state = value?.value;
32240
- if (!name || !BROWSER_STATE_PROPERTIES.has(name) || typeof state !== "string" && typeof state !== "number" && typeof state !== "boolean") continue;
32241
- properties.push([name, name === "url" ? clippedText(state, 300) : state]);
32242
- }
32243
- return Object.fromEntries(properties);
32244
- }
32245
- function readBrowserStateCache(path6) {
32246
- let value;
32247
- try {
32248
- value = JSON.parse(readFileSync5(path6, "utf8"));
32249
- } catch {
32250
- return null;
32251
- }
32252
- if (!isRecord(value) || !Array.isArray(value.entries)) return null;
32253
- const entries = [];
32254
- for (const entry of value.entries) {
32255
- if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.semantic !== "string" || typeof entry.line !== "string") return null;
32256
- entries.push({ key: entry.key, semantic: entry.semantic, line: entry.line });
32257
- }
32258
- return {
32259
- url: typeof value.url === "string" ? value.url : void 0,
32260
- documentId: typeof value.documentId === "string" || value.documentId === null ? value.documentId : void 0,
32261
- entries
32262
- };
32263
- }
32264
- function chromeResult(response, method) {
32265
- if (response.error) throw new Error(response.error.message || `Chrome DevTools ${method} failed`);
32266
- return response.result ?? {};
32267
- }
32268
- function collectFrameIds(frameTree) {
32269
- const ids = [];
32270
- const visit = (value) => {
32271
- if (!isRecord(value)) return;
32272
- const frame = isRecord(value.frame) ? value.frame : {};
32273
- if (typeof frame.id === "string") ids.push(frame.id);
32274
- if (Array.isArray(value.childFrames)) value.childFrames.forEach(visit);
32275
- };
32276
- visit(frameTree);
32277
- return ids;
32278
- }
32279
- function browserDocumentId(frameTree) {
32280
- if (!isRecord(frameTree) || !isRecord(frameTree.frame)) return null;
32281
- const loaderId = frameTree.frame.loaderId;
32282
- return typeof loaderId === "string" ? loaderId : null;
32283
- }
32284
- function intersectRects(a, b) {
32285
- const x = Math.max(a.x, b.x);
32286
- const y = Math.max(a.y, b.y);
32287
- const right = Math.min(a.x + a.width, b.x + b.width);
32288
- const bottom = Math.min(a.y + a.height, b.y + b.height);
32289
- if (right <= x || bottom <= y) return null;
32290
- return {
32291
- x,
32292
- y,
32293
- width: right - x,
32294
- height: bottom - y,
32295
- centerX: x + (right - x) / 2,
32296
- centerY: y + (bottom - y) / 2
32297
- };
32298
- }
32299
- function browserContentQuadRects(quads, viewportRect) {
32300
- if (!Array.isArray(quads)) return [];
32301
- return quads.filter((quad) => Array.isArray(quad) && quad.length >= 8 && quad.every(Number.isFinite)).map((quad) => {
32302
- const xs = [quad[0], quad[2], quad[4], quad[6]];
32303
- const ys = [quad[1], quad[3], quad[5], quad[7]];
32304
- const x = Math.min(...xs);
32305
- const y = Math.min(...ys);
32306
- const width = Math.max(...xs) - x;
32307
- const height = Math.max(...ys) - y;
32308
- return { x, y, width, height, centerX: x + width / 2, centerY: y + height / 2 };
32309
- }).map((rect) => intersectRects(rect, viewportRect)).filter((rect) => !!rect).sort((a, b) => b.width * b.height - a.width * a.height);
32310
- }
32311
- function buildBrowserBounds(result, viewport, targetId) {
32312
- const strings = Array.isArray(result.strings) ? result.strings : [];
32313
- const documents = Array.isArray(result.documents) ? result.documents.filter(isDOMSnapshotDocument) : [];
32314
- const localRects = documents.map((document) => {
32315
- const rects = /* @__PURE__ */ new Map();
32316
- const nodeIndexes = document.layout?.nodeIndex ?? [];
32317
- const bounds = document.layout?.bounds ?? [];
32318
- const scrollX = document.scrollOffsetX ?? 0;
32319
- const scrollY = document.scrollOffsetY ?? 0;
32320
- nodeIndexes.forEach((nodeIndex, index) => {
32321
- const bound = bounds[index];
32322
- if (!Array.isArray(bound) || bound.length < 4 || !bound.every(Number.isFinite)) return;
32323
- const [rawX, rawY, width, height] = bound;
32324
- rects.set(nodeIndex, {
32325
- x: rawX - scrollX,
32326
- y: rawY - scrollY,
32327
- width,
32328
- height,
32329
- centerX: rawX - scrollX + width / 2,
32330
- centerY: rawY - scrollY + height / 2
32331
- });
32332
- });
32333
- return rects;
32334
- });
32335
- const parents = /* @__PURE__ */ new Map();
32336
- documents.forEach((document, documentIndex) => {
32337
- const sparse = document.nodes?.contentDocumentIndex;
32338
- sparse?.index?.forEach((nodeIndex, index) => {
32339
- const childDocumentIndex = sparse.value?.[index];
32340
- if (typeof childDocumentIndex === "number") parents.set(childDocumentIndex, { documentIndex, nodeIndex });
32341
- });
32342
- });
32343
- const targetDocumentIndex = documents.findIndex(
32344
- (document) => typeof document.frameId === "number" && strings[document.frameId] === targetId
32345
- );
32346
- const rootIndex = targetDocumentIndex >= 0 ? targetDocumentIndex : documents.findIndex((_, index) => !parents.has(index));
32347
- const viewportRect = {
32348
- x: 0,
32349
- y: 0,
32350
- width: viewport.width,
32351
- height: viewport.height,
32352
- centerX: viewport.width / 2,
32353
- centerY: viewport.height / 2
32354
- };
32355
- const origins = /* @__PURE__ */ new Map();
32356
- origins.set(rootIndex < 0 ? 0 : rootIndex, { x: 0, y: 0, clip: viewportRect });
32357
- for (let pass = 0; pass < documents.length; pass++) {
32358
- let changed = false;
32359
- for (const [childIndex, parent] of parents) {
32360
- if (origins.has(childIndex)) continue;
32361
- const parentOrigin = origins.get(parent.documentIndex);
32362
- const frameRect = localRects[parent.documentIndex]?.get(parent.nodeIndex);
32363
- if (!parentOrigin || !frameRect) continue;
32364
- const globalFrameRect = {
32365
- ...frameRect,
32366
- x: parentOrigin.x + frameRect.x,
32367
- y: parentOrigin.y + frameRect.y,
32368
- centerX: parentOrigin.x + frameRect.centerX,
32369
- centerY: parentOrigin.y + frameRect.centerY
32370
- };
32371
- const childDocument = documents[childIndex];
32372
- const insetX = Math.max(0, (globalFrameRect.width - (childDocument?.contentWidth ?? globalFrameRect.width)) / 2);
32373
- const insetY = Math.max(0, (globalFrameRect.height - (childDocument?.contentHeight ?? globalFrameRect.height)) / 2);
32374
- const contentRect = {
32375
- x: globalFrameRect.x + insetX,
32376
- y: globalFrameRect.y + insetY,
32377
- width: Math.max(0, globalFrameRect.width - insetX * 2),
32378
- height: Math.max(0, globalFrameRect.height - insetY * 2),
32379
- centerX: globalFrameRect.centerX,
32380
- centerY: globalFrameRect.centerY
32381
- };
32382
- const clip = intersectRects(parentOrigin.clip, contentRect);
32383
- if (!clip) continue;
32384
- origins.set(childIndex, { x: contentRect.x, y: contentRect.y, clip });
32385
- changed = true;
32386
- }
32387
- if (!changed) break;
32388
- }
32389
- const byBackendNodeId = /* @__PURE__ */ new Map();
32390
- documents.forEach((document, documentIndex) => {
32391
- const origin = origins.get(documentIndex);
32392
- if (!origin) return;
32393
- const backendNodeIds = document.nodes?.backendNodeId ?? [];
32394
- for (const [nodeIndex, local] of localRects[documentIndex] ?? []) {
32395
- const backendNodeId = backendNodeIds[nodeIndex];
32396
- if (!Number.isInteger(backendNodeId)) continue;
32397
- const rect = {
32398
- ...local,
32399
- x: origin.x + local.x,
32400
- y: origin.y + local.y,
32401
- centerX: origin.x + local.centerX,
32402
- centerY: origin.y + local.centerY
32403
- };
32404
- if (intersectRects(rect, origin.clip)) byBackendNodeId.set(backendNodeId, { rect, clip: origin.clip });
32405
- }
32406
- });
32407
- return byBackendNodeId;
32408
- }
32409
- var BROWSER_INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
32410
- "button",
32411
- "checkbox",
32412
- "combobox",
32413
- "link",
32414
- "listbox",
32415
- "menuitem",
32416
- "menuitemcheckbox",
32417
- "menuitemradio",
32418
- "option",
32419
- "radio",
32420
- "scrollbar",
32421
- "searchbox",
32422
- "slider",
32423
- "spinbutton",
32424
- "switch",
32425
- "tab",
32426
- "textbox",
32427
- "treeitem"
32428
- ]);
32429
- var BROWSER_STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
32430
- "alert",
32431
- "cell",
32432
- "columnheader",
32433
- "dialog",
32434
- "figure",
32435
- "gridcell",
32436
- "heading",
32437
- "image",
32438
- "listitem",
32439
- "main",
32440
- "paragraph",
32441
- "region",
32442
- "row",
32443
- "rowheader",
32444
- "status",
32445
- "StaticText"
32446
- ]);
32447
- var BROWSER_STATE_PROPERTIES = /* @__PURE__ */ new Set([
32448
- "autocomplete",
32449
- "checked",
32450
- "disabled",
32451
- "editable",
32452
- "expanded",
32453
- "focusable",
32454
- "focused",
32455
- "haspopup",
32456
- "invalid",
32457
- "level",
32458
- "multiselectable",
32459
- "orientation",
32460
- "pressed",
32461
- "readonly",
32462
- "protected",
32463
- "required",
32464
- "selected",
32465
- "url"
32466
- ]);
32467
- function clippedText(value, limit = 240) {
32468
- return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, limit);
32469
- }
32470
- function browserTargetOutput(page) {
32471
- return {
32472
- targetId: page.id ?? null,
32473
- title: (page.title ?? "").slice(0, 1e3),
32474
- url: (page.url ?? "").slice(0, 2e3)
32475
- };
32476
- }
32477
- function browserNodeActionable(role, states) {
32478
- return BROWSER_INTERACTIVE_ROLES.has(role) || states.editable === true || states.focusable === true && role !== "RootWebArea";
32479
- }
32480
- async function captureBrowserSnapshot(page, options) {
32481
- const initialCommands = [
32482
- { method: "Page.getFrameTree", params: {} },
32483
- { method: "Page.getLayoutMetrics", params: {} },
32484
- { method: "DOMSnapshot.captureSnapshot", params: { computedStyles: [], includeDOMRects: true, includePaintOrder: true } }
32485
- ];
32486
- const initial = await sendChromeCommands(page.webSocketDebuggerUrl, initialCommands);
32487
- const frameTree = chromeResult(initial[0], initialCommands[0].method).frameTree;
32488
- const layout = chromeResult(initial[1], initialCommands[1].method);
32489
- const domSnapshot = chromeResult(initial[2], initialCommands[2].method);
32490
- const viewport = chromeVisualViewport(layout);
32491
- const frameIds = collectFrameIds(frameTree);
32492
- if (frameIds.length === 0 && page.id) frameIds.push(page.id);
32493
- const axCommands = frameIds.map((frameId) => ({
32494
- method: "Accessibility.getFullAXTree",
32495
- params: { frameId }
32496
- }));
32497
- const axResults = await sendChromeCommands(page.webSocketDebuggerUrl, axCommands);
32498
- const nodes = [];
32499
- axResults.forEach((response, index) => {
32500
- if (response.error) return;
32501
- const responseNodes = response.result?.nodes;
32502
- if (!Array.isArray(responseNodes)) return;
32503
- for (const node of responseNodes) {
32504
- if (!isRawAXNode(node)) continue;
32505
- nodes.push({ ...node, sourceFrameId: frameIds[index] });
32506
- }
32507
- });
32508
- const bounds = buildBrowserBounds(domSnapshot, viewport, page.id);
32509
- const nodesByKey = new Map(nodes.map((node) => [`${node.sourceFrameId}:${node.nodeId}`, node]));
32510
- const propertyMap = browserStateProperties;
32511
- const roleOf = (node) => clippedText(node.role?.value, 80);
32512
- const nameOf = (node) => clippedText(node.name?.value);
32513
- const valueOf = (node) => {
32514
- if (propertyMap(node).protected === true) return null;
32515
- const value = node.value?.value;
32516
- if (typeof value === "number" || typeof value === "boolean") return value;
32517
- const text = clippedText(value);
32518
- if (/^[•●*]+$/.test(text)) return "[protected]";
32519
- return text || null;
32520
- };
32521
- const meaningful = nodes.filter((node) => !node.ignored && Number.isInteger(node.backendDOMNodeId)).map((node) => ({
32522
- node,
32523
- role: roleOf(node),
32524
- bound: typeof node.backendDOMNodeId === "number" ? bounds.get(node.backendDOMNodeId) : void 0
32525
- })).filter(({ node, role, bound }) => {
32526
- if (!bound || bound.rect.width <= 0 || bound.rect.height <= 0) return false;
32527
- if (role === "InlineTextBox" || role === "none" || role === "generic") return false;
32528
- if (BROWSER_INTERACTIVE_ROLES.has(role) || BROWSER_STRUCTURAL_ROLES.has(role)) return true;
32529
- return !!nameOf(node) || valueOf(node) !== null || propertyMap(node).focusable === true;
32530
- }).filter(({ node, role }) => {
32531
- if (role !== "StaticText") return true;
32532
- const parent = node.parentId ? nodesByKey.get(`${node.sourceFrameId}:${node.parentId}`) : void 0;
32533
- return !parent || nameOf(parent) !== nameOf(node);
32534
- }).sort((a, b) => a.bound.rect.y - b.bound.rect.y || a.bound.rect.x - b.bound.rect.x);
32535
- const elements = [];
32536
- const entries = [];
32537
- const meaningfulKeys = new Set(meaningful.map(({ node }) => `${node.sourceFrameId}:${node.nodeId}`));
32538
- const visibleText = [];
32539
- let visibleTextLength = 0;
32540
- let treeLength = 0;
32541
- const treeLimit = Math.min(5e4, Math.max(4e3, options.textLimit * 2));
32542
- for (const { node, role, bound } of meaningful) {
32543
- const name = nameOf(node);
32544
- const description = clippedText(node.description?.value) || null;
32545
- const value = valueOf(node);
32546
- const states = propertyMap(node);
32547
- const actionable = browserNodeActionable(role, states);
32548
- const ref = actionable && elements.length < options.elementLimit ? String(node.backendDOMNodeId) : null;
32549
- const visibleRect = role === "RootWebArea" && node.sourceFrameId === page.id ? { x: 0, y: 0, width: viewport.width, height: viewport.height, centerX: viewport.width / 2, centerY: viewport.height / 2 } : intersectRects(bound.rect, bound.clip) ?? bound.rect;
32550
- const rect = roundBrowserRect(visibleRect);
32551
- const actions = [
32552
- ...actionable ? ["click"] : [],
32553
- ...["textbox", "searchbox", "combobox", "spinbutton", "slider"].includes(role) || states.editable === true ? ["set_value"] : [],
32554
- "scroll"
32555
- ];
32556
- if (ref) {
32557
- elements.push({
32558
- ref,
32559
- role,
32560
- name: name || null,
32561
- description,
32562
- value,
32563
- states,
32564
- actions,
32565
- rect,
32566
- frameId: node.sourceFrameId
32567
- });
32568
- }
32569
- const stateText = Object.entries(states).filter(([key]) => key !== "url" && key !== "focusable").map(([key, state]) => `${key}=${JSON.stringify(state)}`);
32570
- const url2 = typeof states.url === "string" ? clippedText(states.url, 300) : "";
32571
- const semantic = [role, name && JSON.stringify(name), value !== null && `value=${JSON.stringify(value)}`, description && `description=${JSON.stringify(description)}`, url2 && `url=${JSON.stringify(url2)}`, ...stateText].filter(Boolean).join(" ");
32572
- let depth = 0;
32573
- let parentId = node.parentId;
32574
- const visited = /* @__PURE__ */ new Set();
32575
- while (parentId && depth < 8) {
32576
- const parentKey = `${node.sourceFrameId}:${parentId}`;
32577
- if (visited.has(parentKey)) break;
32578
- visited.add(parentKey);
32579
- if (meaningfulKeys.has(parentKey)) depth++;
32580
- parentId = nodesByKey.get(parentKey)?.parentId;
32581
- }
32582
- const line = `${" ".repeat(depth)}${ref ? `[ref=${ref}]` : "-"} ${semantic} (${rect.x},${rect.y} ${rect.width}x${rect.height})`;
32583
- if (treeLength + line.length + 1 <= treeLimit) {
32584
- const key = Number.isInteger(node.backendDOMNodeId) ? `dom:${node.sourceFrameId}:${node.backendDOMNodeId}` : `ax:${node.sourceFrameId}:${node.nodeId}`;
32585
- entries.push({ key, semantic, line });
32586
- treeLength += line.length + 1;
32587
- }
32588
- if (name && visibleTextLength < options.textLimit && (role === "StaticText" || role === "heading" || BROWSER_INTERACTIVE_ROLES.has(role))) {
32589
- const remaining = options.textLimit - visibleTextLength;
32590
- visibleText.push(name.slice(0, remaining));
32591
- visibleTextLength += Math.min(name.length, remaining) + 1;
32592
- }
32593
- }
32594
- const revision = createHash3("sha256").update(JSON.stringify([page.url, entries.map(({ key, semantic }) => [key, semantic])])).digest("hex").slice(0, 16);
32595
- const snapshot = {
32596
- title: (page.title ?? "").slice(0, 1e3),
32597
- url: (page.url ?? "").slice(0, 2e3),
32598
- documentId: browserDocumentId(frameTree),
32599
- revision,
32600
- viewport,
32601
- text: visibleText.join(" ").slice(0, options.textLimit),
32602
- tree: entries.map(({ line }) => line).join("\n"),
32603
- elements,
32604
- controls: elements,
32605
- nodeCount: entries.length,
32606
- elementCount: elements.length,
32607
- truncated: entries.length < meaningful.length || elements.length < meaningful.filter(({ node, role }) => browserNodeActionable(role, propertyMap(node))).length
32608
- };
32609
- return { snapshot, entries };
32610
- }
32611
- async function computerBrowserCommand(options = {}) {
32612
- const textLimit = options.limit ? parseCoord(options.limit, "--limit") : 4e3;
32613
- const elementLimit = options.elementLimit ? parseCoord(options.elementLimit, "--element-limit") : 80;
32614
- if (textLimit < 0 || textLimit > 5e4) fail("--limit must be between 0 and 50000");
32615
- if (elementLimit < 0 || elementLimit > 500) fail("--element-limit must be between 0 and 500");
32616
- const targets = options.targetId || options.page || options.title || options.url ? [await selectChromePage(options)] : await getChromePages();
32617
- const pages = targets.map((target) => ({
32618
- target,
32619
- page: {
32620
- id: target.id ?? null,
32621
- title: target.title ?? "",
32622
- url: target.url ?? "",
32623
- attached: !!target.attached
32624
- }
32625
- }));
32626
- const pageResults = [];
32627
- for (const { target, page } of pages) {
32628
- if (options.snapshot && target.webSocketDebuggerUrl) {
32629
- try {
32630
- const { snapshot } = await captureBrowserSnapshot(
32631
- { ...target, webSocketDebuggerUrl: target.webSocketDebuggerUrl },
32632
- { textLimit, elementLimit }
32633
- );
32634
- pageResults.push({ ...page, snapshot });
32635
- } catch (error51) {
32636
- pageResults.push({ ...page, snapshotError: error51 instanceof Error ? error51.message : "snapshot failed" });
32637
- }
32638
- } else {
32639
- pageResults.push(page);
32640
- }
32641
- }
32642
- console.log(JSON.stringify({
32643
- port: CHROME_DEBUG_PORT,
32644
- pageCount: pageResults.length,
32645
- pages: pageResults
32646
- }, null, 2));
32647
- }
32648
- function buildBrowserStabilityExpression() {
32649
- return `(() => {
32650
- const key = '__replicasComputerUseObserver';
32651
- if (!globalThis[key]) {
32652
- const state = { revision: 0 };
32653
- const observer = new MutationObserver(() => state.revision++);
32654
- globalThis[key] = { state, observer, observing: false };
32655
- }
32656
- if (document.documentElement && !globalThis[key].observing) {
32657
- globalThis[key].observer.observe(document.documentElement, {
32658
- subtree: true,
32659
- childList: true,
32660
- attributes: true,
32661
- characterData: true,
32662
- });
32663
- globalThis[key].observing = true;
32664
- }
32665
- const controls = Array.from(document.querySelectorAll('input,textarea,select,[contenteditable="true"],[role="checkbox"],[role="combobox"],[role="slider"],[role="switch"],[role="textbox"]')).slice(0, 200);
32666
- const active = document.activeElement;
32667
- const controlState = controls.map((element, index) => {
32668
- const value = 'value' in element
32669
- ? element instanceof HTMLInputElement && element.type === 'password'
32670
- ? String(element.value).length
32671
- : String(element.value)
32672
- : element.textContent;
32673
- return [
32674
- index,
32675
- element === active,
32676
- value,
32677
- 'checked' in element ? element.checked : null,
32678
- 'selectedIndex' in element ? element.selectedIndex : null,
32679
- 'disabled' in element ? element.disabled : null,
32680
- element.getAttribute('aria-checked'),
32681
- element.getAttribute('aria-expanded'),
32682
- element.getAttribute('aria-selected'),
32683
- ].join('|');
32684
- }).join('\\u001f');
32685
- let controlRevision = 2166136261;
32686
- for (let index = 0; index < controlState.length; index++) {
32687
- controlRevision ^= controlState.charCodeAt(index);
32688
- controlRevision = Math.imul(controlRevision, 16777619);
32689
- }
32690
- return {
32691
- revision: globalThis[key].state.revision,
32692
- controlRevision: (controlRevision >>> 0).toString(16),
32693
- readyState: document.readyState,
32694
- busy: !!document.querySelector('[aria-busy="true"]'),
32695
- url: location.href.slice(0, 2000),
32696
- scrollX: Math.round(scrollX),
32697
- scrollY: Math.round(scrollY),
32698
- innerWidth,
32699
- innerHeight,
32700
- devicePixelRatio,
32701
- };
32702
- })()`;
32703
- }
32704
- async function waitForBrowserStability(page, options) {
32705
- const startedAt = Date.now();
32706
- let lastSignature = null;
32707
- let lastChangeAt = startedAt;
32708
- let samples = 0;
32709
- let changes = 0;
32710
- let state = null;
32711
- while (Date.now() - startedAt <= options.timeoutMs) {
32712
- try {
32713
- state = await evaluateChromeTarget(page.webSocketDebuggerUrl, buildBrowserStabilityExpression());
32714
- samples++;
32715
- const signature = JSON.stringify(state);
32716
- const now = Date.now();
32717
- if (lastSignature === null || signature !== lastSignature) {
32718
- if (lastSignature !== null) changes++;
32719
- lastSignature = signature;
32720
- lastChangeAt = now;
32721
- }
32722
- const ready = typeof state === "object" && state !== null && "readyState" in state && state.readyState !== "loading" && "busy" in state && state.busy !== true;
32723
- if (samples > 1 && ready && now - lastChangeAt >= options.stableMs) {
32724
- return { stable: true, elapsedMs: now - startedAt, samples, changes, state };
32725
- }
32726
- } catch {
32727
- }
32728
- await sleep2(options.pollMs);
32729
- }
32730
- return { stable: false, elapsedMs: Date.now() - startedAt, samples, changes, state };
32731
- }
32732
- function browserStateCachePath(targetId) {
32733
- return `${STATE_DIR}/browser-state-${targetId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
32734
- }
32735
- function browserStateDiff(previous, current) {
32736
- const before = new Map(previous.map((entry) => [entry.key, entry]));
32737
- const after = new Map(current.map((entry) => [entry.key, entry]));
32738
- const lines = [];
32739
- let added = 0;
32740
- let changed = 0;
32741
- let removed = 0;
32742
- for (const entry of current) {
32743
- const old = before.get(entry.key);
32744
- if (!old) {
32745
- added++;
32746
- lines.push(`+ ${entry.line}`);
32747
- } else if (old.semantic !== entry.semantic) {
32748
- changed++;
32749
- lines.push(`~ ${old.line}
32750
- ${entry.line}`);
32751
- }
32752
- }
32753
- for (const entry of previous) {
32754
- if (after.has(entry.key)) continue;
32755
- removed++;
32756
- lines.push(`- ${entry.line}`);
32757
- }
32758
- return { tree: lines.join("\n") || "No accessibility changes.", added, changed, removed };
32759
- }
32760
- async function computerBrowserStateCommand(path6, options = {}) {
32761
- const textLimit = options.limit ? parseCoord(options.limit, "--limit") : 8e3;
32762
- const elementLimit = options.elementLimit ? parseCoord(options.elementLimit, "--element-limit") : 120;
32763
- const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 5e3;
32764
- const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 500;
32765
- const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 100;
32766
- if (textLimit < 0 || textLimit > 5e4) fail("--limit must be between 0 and 50000");
32767
- if (elementLimit < 0 || elementLimit > 500) fail("--element-limit must be between 0 and 500");
32768
- if (timeoutMs < 0) fail("--timeout must be >= 0");
32769
- if (stableMs < 0) fail("--stable-ms must be >= 0");
32770
- if (pollMs < 50 || pollMs > 2e3) fail("--poll-ms must be between 50 and 2000");
32771
- const page = await selectChromePage(options);
32772
- const stability = await waitForBrowserStability(page, { timeoutMs, stableMs, pollMs });
32773
- const target = resolvePath(path6);
32774
- mkdirSync5(dirname3(target), { recursive: true });
32775
- const [{ snapshot, entries }, screenshotResult] = await Promise.all([
32776
- captureBrowserSnapshot(page, { textLimit, elementLimit }),
32777
- sendChromeCommand(page.webSocketDebuggerUrl, "Page.captureScreenshot", {
32778
- format: "png",
32779
- fromSurface: true,
32780
- captureBeyondViewport: false
32781
- })
32782
- ]);
32783
- const data = screenshotResult.data;
32784
- if (typeof data !== "string") fail("Chrome did not return screenshot data");
32785
- writeFileSync5(target, Buffer.from(data, "base64"));
32786
- const screenshot = readPngDimensions(target);
32787
- const targetId = page.id ?? "unknown";
32788
- const cachePath = browserStateCachePath(targetId);
32789
- let mode = "full";
32790
- let tree = snapshot.tree;
32791
- let changes = { added: snapshot.nodeCount, changed: 0, removed: 0 };
32792
- if (!options.full && existsSync3(cachePath)) {
32793
- const cached2 = readBrowserStateCache(cachePath);
32794
- if (cached2?.url === snapshot.url && cached2.documentId === snapshot.documentId) {
32795
- mode = "diff";
32796
- const diff = browserStateDiff(cached2.entries, entries);
32797
- tree = diff.tree;
32798
- changes = { added: diff.added, changed: diff.changed, removed: diff.removed };
32799
- }
32800
- }
32801
- mkdirSync5(STATE_DIR, { recursive: true });
32802
- writeFileSync5(cachePath, JSON.stringify({ url: snapshot.url, documentId: snapshot.documentId, entries }));
32803
- const stableState = isRecord(stability.state) ? stability.state : {};
32804
- const state = {
32805
- title: snapshot.title,
32806
- url: snapshot.url,
32807
- documentId: snapshot.documentId,
32808
- revision: snapshot.revision,
32809
- viewport: snapshot.viewport,
32810
- text: snapshot.text,
32811
- tree,
32812
- nodeCount: snapshot.nodeCount,
32813
- elementCount: snapshot.elementCount,
32814
- truncated: snapshot.truncated,
32815
- mode,
32816
- changes
32817
- };
32818
- console.log(JSON.stringify({
32819
- targetId,
32820
- screenshot: {
32821
- path: target,
32822
- width: screenshot.width,
32823
- height: screenshot.height,
32824
- devicePixelRatio: typeof stableState.devicePixelRatio === "number" ? stableState.devicePixelRatio : 1
32825
- },
32826
- stability,
32827
- state
32828
- }, null, 2));
32829
- }
32830
- var DESKTOP_POINT_JS = `
32831
- const desktopPoint = (rect) => {
32832
- const borderX = Math.max(0, (window.outerWidth - window.innerWidth) / 2);
32833
- const topChrome = Math.max(0, window.outerHeight - window.innerHeight - borderX);
32834
- return {
32835
- x: Math.round(window.screenX + borderX + rect.x + rect.width / 2),
32836
- y: Math.round(window.screenY + topChrome + rect.y + rect.height / 2),
32837
- };
32838
- };`;
32839
- function parseBrowserRef(value) {
32840
- const ref = parseCoord(value.replace(/^ref=/, ""), "--ref");
32841
- if (ref <= 0) fail("--ref must be a positive backend DOM node ID from the latest browser state");
32842
- return ref;
32843
- }
32844
- function readBrowserRefCache(page) {
32845
- const targetId = page.id;
32846
- if (!targetId) fail("Chrome target has no stable ID. Capture fresh browser state and retry.");
32847
- const cachePath = browserStateCachePath(targetId);
32848
- if (!existsSync3(cachePath)) fail(`No browser state is cached for target ${targetId}. Run replicas computer browser-state <path> --target-id ${targetId} first.`);
32849
- const cached2 = readBrowserStateCache(cachePath);
32850
- if (!cached2) fail(`Browser state for target ${targetId} is invalid. Capture fresh browser state and retry.`);
32851
- if (cached2.url !== (page.url ?? "").slice(0, 2e3)) fail("Cached browser state belongs to a different URL. Capture fresh browser state and retry.");
32852
- return { targetId, cached: cached2 };
32853
- }
32854
- function browserViewportRect(layout) {
32855
- const viewport = chromeVisualViewport(layout);
32856
- return {
32857
- x: 0,
32858
- y: 0,
32859
- width: viewport.width,
32860
- height: viewport.height,
32861
- centerX: viewport.width / 2,
32862
- centerY: viewport.height / 2
32863
- };
32864
- }
32865
- async function prepareBrowserElement(page, refValue) {
32866
- const backendNodeId = parseBrowserRef(refValue);
32867
- const { cached: cached2 } = readBrowserRefCache(page);
32868
- return await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
32869
- const layout = await sendChromeSessionCommand(session, "Page.getLayoutMetrics", {});
32870
- const rect = await prepareBrowserRefInSession(session, backendNodeId, cached2, browserViewportRect(layout));
32871
- const axNodes = (await sendChromeSessionCommand(session, "Accessibility.getPartialAXTree", {
32872
- backendNodeId,
32873
- fetchRelatives: false
32874
- })).nodes;
32875
- const rawNode = Array.isArray(axNodes) && isRawAXNode(axNodes[0]) ? axNodes[0] : null;
32876
- const states = rawNode ? browserStateProperties(rawNode) : {};
32877
- const role = clippedText(rawNode?.role?.value, 80);
32878
- const element = rawNode ? {
32879
- ref: String(backendNodeId),
32880
- role,
32881
- name: clippedText(rawNode.name?.value) || null,
32882
- description: clippedText(rawNode.description?.value) || null,
32883
- value: typeof rawNode.value?.value === "string" ? clippedText(rawNode.value.value) : typeof rawNode.value?.value === "number" || typeof rawNode.value?.value === "boolean" ? rawNode.value.value : null,
32884
- states,
32885
- actions: [
32886
- "click",
32887
- ...["textbox", "searchbox", "combobox", "spinbutton", "slider"].includes(role) || states.editable === true ? ["set_value"] : [],
32888
- "scroll"
32889
- ]
32890
- } : null;
32891
- const desktopPoint = await evaluateChromeSession(session, `(() => {
32892
- ${DESKTOP_POINT_JS}
32893
- return desktopPoint({ x: ${rect.centerX}, y: ${rect.centerY}, width: 0, height: 0 });
32894
- })()`);
32895
- return {
32896
- ref: String(backendNodeId),
32897
- rect,
32898
- element,
32899
- desktopPoint: typeof desktopPoint === "object" && desktopPoint !== null && "x" in desktopPoint && typeof desktopPoint.x === "number" && "y" in desktopPoint && typeof desktopPoint.y === "number" ? { x: desktopPoint.x, y: desktopPoint.y } : null
32900
- };
32901
- });
32902
- }
32903
- async function prepareBrowserRefInSession(session, backendNodeId, cached2, viewportRect) {
32904
- if (!cached2.entries.some(({ line }) => line.trimStart().startsWith(`[ref=${backendNodeId}]`))) {
32905
- fail(`Browser element ref ${backendNodeId} is not in the latest state. Capture fresh browser state and retry.`);
32906
- }
32907
- const frameTree = (await sendChromeSessionCommand(session, "Page.getFrameTree", {})).frameTree;
32908
- if (cached2.documentId !== browserDocumentId(frameTree)) {
32909
- fail(`Browser element ref ${backendNodeId} belongs to a previous document. Capture fresh browser state and retry.`);
32910
- }
32911
- try {
32912
- await sendChromeSessionCommand(session, "DOM.scrollIntoViewIfNeeded", { backendNodeId });
32913
- } catch (error51) {
32914
- fail(`Browser element ref ${backendNodeId} is stale or cannot be scrolled into view: ${error51 instanceof Error ? error51.message : String(error51)}`);
32915
- }
32916
- const quads = (await sendChromeSessionCommand(session, "DOM.getContentQuads", { backendNodeId })).quads;
32917
- const candidates = browserContentQuadRects(quads, viewportRect);
32918
- if (!candidates[0]) fail(`Browser element ref ${backendNodeId} has no visible content quad. Fetch fresh browser state and retry.`);
32919
- return roundBrowserRect(candidates[0]);
32920
- }
32921
- function buildBrowserClickExpression(query, options) {
32922
- return `(() => {
32923
- const query = ${JSON.stringify(query)};
32924
- const exact = ${JSON.stringify(options.exact)};
32925
- const index = ${options.index};
32926
- const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
32927
- ${VISIBLE_JS}
32928
- const label = (el) => normalize(el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || el.href || el.id || el.name || el.tagName);
32929
- ${DESKTOP_POINT_JS}
32930
- const matches = (text) => {
32931
- const haystack = text.toLowerCase();
32932
- const needle = query.toLowerCase();
32933
- return exact ? haystack === needle : haystack.includes(needle);
32934
- };
32935
- const selector = 'a,button,input,textarea,select,[role="button"],[role="link"],[role="textbox"],[contenteditable="true"]';
32936
- const candidates = Array.from(document.querySelectorAll(selector))
32937
- .filter(visible)
32938
- .map((el) => ({ el, text: label(el) }))
32939
- .filter((candidate) => candidate.text && matches(candidate.text));
32940
- const candidate = candidates[index];
32941
- if (!candidate) {
32942
- return {
32943
- clicked: false,
32944
- query,
32945
- matchCount: candidates.length,
32946
- reason: candidates.length ? 'index out of range' : 'no visible matching control',
32947
- };
32948
- }
32949
- const { el, text } = candidate;
32950
- el.scrollIntoView({ block: 'center', inline: 'center' });
32951
- const rect = el.getBoundingClientRect();
32952
- if (typeof el.focus === 'function') el.focus({ preventScroll: true });
32953
- return {
32954
- clicked: true,
32955
- query,
32956
- matchCount: candidates.length,
32957
- index,
32958
- tag: el.tagName.toLowerCase(),
32959
- role: el.getAttribute('role') || null,
32960
- type: el.getAttribute('type') || null,
32961
- text,
32962
- href: el.href || null,
32963
- desktopPoint: desktopPoint(rect),
32964
- rect: {
32965
- x: Math.round(rect.x),
32966
- y: Math.round(rect.y),
32967
- width: Math.round(rect.width),
32968
- height: Math.round(rect.height),
32969
- centerX: Math.round(rect.x + rect.width / 2),
32970
- centerY: Math.round(rect.y + rect.height / 2),
32971
- },
32972
- };
32973
- })()`;
32974
- }
32975
- function browserActionRectCenter(result) {
32976
- if (typeof result !== "object" || result === null || !("rect" in result)) return null;
32977
- const rect = result.rect;
32978
- if (typeof rect !== "object" || rect === null) return null;
32979
- const x = "centerX" in rect ? rect.centerX : void 0;
32980
- const y = "centerY" in rect ? rect.centerY : void 0;
32981
- if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
32982
- return { x, y };
32983
- }
32984
- function browserActionPoint(result) {
32985
- if (typeof result !== "object" || result === null) return null;
32986
- const action = "result" in result ? result.result : result;
32987
- if (typeof action !== "object" || action === null || !("desktopPoint" in action)) return null;
32988
- const point = action.desktopPoint;
32989
- if (typeof point !== "object" || point === null) return null;
32990
- const x = "x" in point ? point.x : void 0;
32991
- const y = "y" in point ? point.y : void 0;
32992
- if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
32993
- const dimensions = getDisplayDimensions();
32994
- return {
32995
- x: clamp(Math.round(x), 0, dimensions.width),
32996
- y: clamp(Math.round(y), 0, dimensions.height)
32997
- };
32998
- }
32999
- async function dispatchChromeClick(page, x, y, button, clicks) {
33000
- await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
33001
- await dispatchChromeClickInSession(session, x, y, button, clicks);
33002
- });
33003
- }
33004
- async function dispatchChromeClickInSession(session, x, y, button, clicks) {
33005
- for (let clickCount = 1; clickCount <= clicks; clickCount++) {
33006
- await sendChromeSessionCommand(session, "Input.dispatchMouseEvent", {
33007
- type: "mousePressed",
33008
- x,
33009
- y,
33010
- button,
33011
- clickCount
33012
- });
33013
- await sendChromeSessionCommand(session, "Input.dispatchMouseEvent", {
33014
- type: "mouseReleased",
33015
- x,
33016
- y,
33017
- button,
33018
- clickCount
33019
- });
33020
- }
33021
- }
33022
- async function replaceChromeText(page, text) {
33023
- await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
33024
- await replaceChromeTextInSession(session, text);
33025
- });
33026
- }
33027
- async function replaceChromeTextInSession(session, text) {
33028
- await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
33029
- type: "rawKeyDown",
33030
- key: "a",
33031
- code: "KeyA",
33032
- windowsVirtualKeyCode: 65,
33033
- nativeVirtualKeyCode: 65,
33034
- modifiers: 2
33035
- });
33036
- await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
33037
- type: "keyUp",
33038
- key: "a",
33039
- code: "KeyA",
33040
- windowsVirtualKeyCode: 65,
33041
- nativeVirtualKeyCode: 65,
33042
- modifiers: 2
33043
- });
33044
- await sendChromeSessionCommand(session, "Input.insertText", { text });
33045
- }
33046
- var BROWSER_DIRECT_VALUE_INPUT_TYPES = /* @__PURE__ */ new Set(["number", "range", "date", "datetime-local", "month", "week", "time", "color"]);
33047
- function browserFillInvalid(actual, expected, fieldType, semanticSelection = false, selectionMatched = false) {
33048
- if (semanticSelection) return !selectionMatched;
33049
- if (["number", "range"].includes(fieldType)) {
33050
- const actualNumber = typeof actual === "number" ? actual : Number(actual);
33051
- const expectedNumber = Number(expected);
33052
- return !Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber) || actualNumber !== expectedNumber;
33053
- }
33054
- if (fieldType === "color") return String(actual).toLowerCase() !== expected.toLowerCase();
33055
- return actual !== expected;
33056
- }
33057
- async function fillChromeNodeInSession(session, backendNodeId, value) {
33058
- await sendChromeSessionCommand(session, "DOM.focus", { backendNodeId });
33059
- const directResult = await callFunctionOnChromeNodeInSession(
33060
- session,
33061
- backendNodeId,
33062
- `function(value) {
33063
- const emit = () => {
33064
- this.dispatchEvent(new Event('input', { bubbles: true }));
33065
- this.dispatchEvent(new Event('change', { bubbles: true }));
33066
- };
33067
- if (this instanceof HTMLSelectElement) {
33068
- const option = Array.from(this.options).find((item) => item.value === value || item.text.trim() === value);
33069
- if (option) this.value = option.value;
33070
- else this.value = value;
33071
- emit();
33072
- return { handled: true, semanticSelection: true, matched: !!option, value: this.value };
33073
- }
33074
- if (this instanceof HTMLInputElement && ${JSON.stringify([...BROWSER_DIRECT_VALUE_INPUT_TYPES])}.includes(this.type.toLowerCase())) {
33075
- const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
33076
- if (!setter) return { handled: false };
33077
- setter.call(this, value);
33078
- emit();
33079
- return { handled: true, semanticSelection: false, matched: true, value: this.value };
33080
- }
33081
- return { handled: false };
33082
- }`,
33083
- [value]
33084
- );
33085
- const directValue = chromeCallValue(directResult);
33086
- const directHandled = isRecord(directValue) && directValue.handled === true;
33087
- if (!directHandled) await replaceChromeTextInSession(session, value);
33088
- const verification = await callFunctionOnChromeNodeInSession(
33089
- session,
33090
- backendNodeId,
33091
- `function() {
33092
- const value = this instanceof HTMLSelectElement || 'value' in this ? this.value : this.textContent;
33093
- const type = this instanceof HTMLSelectElement
33094
- ? 'select'
33095
- : this instanceof HTMLInputElement
33096
- ? this.type.toLowerCase()
33097
- : this.isContentEditable
33098
- ? 'contenteditable'
33099
- : 'text';
33100
- return { value, valueLength: String(value ?? '').length, type };
33101
- }`
33102
- );
33103
- const actual = chromeCallValue(verification);
33104
- if (!isRecord(actual)) fail(`Chrome did not return a value for browser element ref ${backendNodeId}.`);
33105
- const fieldType = typeof actual.type === "string" ? actual.type : "";
33106
- const semanticSelection = isRecord(directValue) && directValue.semanticSelection === true;
33107
- const selectionMatched = isRecord(directValue) && directValue.matched === true;
33108
- if (browserFillInvalid(actual.value, value, fieldType, semanticSelection, selectionMatched)) {
33109
- fail(`Chrome reported that ref ${backendNodeId} contains ${JSON.stringify(actual.value)} after filling, expected ${JSON.stringify(value)}.`);
33110
- }
33111
- return { value: actual.value, valueLength: actual.valueLength, type: fieldType };
33112
- }
33113
- async function computerBrowserClickCommand(query, options = {}) {
33114
- const page = await selectChromePage(options);
33115
- if (options.ref) {
33116
- const target = await prepareBrowserElement(page, options.ref);
33117
- const buttonNames = { "1": "left", "2": "middle", "3": "right", left: "left", middle: "middle", right: "right" };
33118
- const button = buttonNames[(options.button ?? "left").toLowerCase()];
33119
- if (!button) fail("--button must be one of left|middle|right|1|2|3");
33120
- const clicks = options.double ? 2 : 1;
33121
- await dispatchChromeClick(page, target.rect.centerX, target.rect.centerY, button, clicks);
33122
- if (target.desktopPoint) logRecordingAction({ type: "click", ...target.desktopPoint });
33123
- console.log(JSON.stringify({
33124
- ...browserTargetOutput(page),
33125
- result: { clicked: true, ref: target.ref, button, clickCount: clicks, rect: target.rect, element: target.element }
33126
- }, null, 2));
33127
- return;
33128
- }
33129
- if (!query) fail("Provide control text or --ref <id> from the latest browser state");
33130
- const index = options.index ? parseCoord(options.index, "--index") : 0;
33131
- if (index < 0) fail("--index must be >= 0");
33132
- const result = await evaluateChromeTarget(
33133
- page.webSocketDebuggerUrl,
33134
- buildBrowserClickExpression(query, { exact: !!options.exact, index })
33135
- );
33136
- const center = browserActionRectCenter(result);
33137
- if (center) await dispatchChromeClick(page, center.x, center.y, "left", 1);
33138
- const point = browserActionPoint(result);
33139
- if (point) logRecordingAction({ type: "click", ...point });
33140
- console.log(JSON.stringify({
33141
- ...browserTargetOutput(page),
33142
- result
33143
- }, null, 2));
33144
- }
33145
- function buildBrowserFillExpression(query, value, options) {
33146
- return `(() => {
33147
- const query = ${JSON.stringify(query)};
33148
- const value = ${JSON.stringify(value)};
33149
- const exact = ${JSON.stringify(options.exact)};
33150
- const index = ${options.index};
33151
- const normalize = (text) => String(text || '').replace(/\\s+/g, ' ').trim();
33152
- const escape = (text) => globalThis.CSS && CSS.escape ? CSS.escape(text) : String(text).replace(/["\\\\]/g, '\\\\$&');
33153
- ${VISIBLE_JS}
33154
- const matches = (text) => {
33155
- const haystack = text.toLowerCase();
33156
- const needle = query.toLowerCase();
33157
- return exact ? haystack === needle : haystack.includes(needle);
33158
- };
33159
- const labelParts = (el) => {
33160
- const id = el.getAttribute('id');
33161
- const labels = [];
33162
- if (id) labels.push(...Array.from(document.querySelectorAll('label[for="' + escape(id) + '"]')).map((label) => label.innerText));
33163
- const wrappingLabel = el.closest('label');
33164
- if (wrappingLabel) labels.push(wrappingLabel.innerText);
33165
- labels.push(
33166
- el.getAttribute('aria-label'),
33167
- el.getAttribute('title'),
33168
- el.getAttribute('placeholder'),
33169
- el.getAttribute('name'),
33170
- el.getAttribute('id'),
33171
- el.value,
33172
- el.innerText,
33173
- );
33174
- return labels.map(normalize).filter(Boolean);
33175
- };
33176
- const labelText = (el) => labelParts(el).join(' ');
33177
- ${DESKTOP_POINT_JS}
33178
- const fieldMatches = (el) => {
33179
- const parts = labelParts(el);
33180
- return exact ? parts.some(matches) : matches(parts.join(' '));
33181
- };
33182
- const selector = 'input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]):not([type="radio"]),textarea,select,[role="textbox"],[contenteditable="true"]';
33183
- const candidates = Array.from(document.querySelectorAll(selector))
33184
- .filter(visible)
33185
- .map((el) => ({ el, text: labelText(el) }))
33186
- .filter((candidate) => candidate.text && fieldMatches(candidate.el));
33187
- const candidate = candidates[index];
33188
- if (!candidate) {
33189
- return {
33190
- filled: false,
33191
- query,
33192
- matchCount: candidates.length,
33193
- reason: candidates.length ? 'index out of range' : 'no visible matching field',
33194
- };
33195
- }
33196
- const { el, text } = candidate;
33197
- el.scrollIntoView({ block: 'center', inline: 'center' });
33198
- if (typeof el.focus === 'function') el.focus({ preventScroll: true });
33199
- const fieldType = el.tagName.toLowerCase() === 'select' ? 'select' : String(el.getAttribute('type') || 'text').toLowerCase();
33200
- let nativeInputRequired = false;
33201
- let semanticSelection = false;
33202
- let selectionMatched = false;
33203
- if (el.tagName.toLowerCase() === 'select') {
33204
- const option = Array.from(el.options).find((item) => item.value === value || normalize(item.text) === value);
33205
- if (option) el.value = option.value;
33206
- else el.value = value;
33207
- semanticSelection = true;
33208
- selectionMatched = !!option;
33209
- el.dispatchEvent(new Event('input', { bubbles: true }));
33210
- el.dispatchEvent(new Event('change', { bubbles: true }));
33211
- } else if (el instanceof HTMLInputElement && ${JSON.stringify([...BROWSER_DIRECT_VALUE_INPUT_TYPES])}.includes(fieldType)) {
33212
- const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
33213
- if (setter) {
33214
- setter.call(el, value);
33215
- el.dispatchEvent(new Event('input', { bubbles: true }));
33216
- el.dispatchEvent(new Event('change', { bubbles: true }));
33217
- } else {
33218
- nativeInputRequired = true;
33219
- }
33220
- } else if (el.isContentEditable) {
33221
- const selection = getSelection();
33222
- const range = document.createRange();
33223
- range.selectNodeContents(el);
33224
- selection.removeAllRanges();
33225
- selection.addRange(range);
33226
- nativeInputRequired = true;
33227
- } else if (typeof el.select === 'function') {
33228
- el.select();
33229
- nativeInputRequired = true;
33230
- } else {
33231
- el.setSelectionRange(0, String(el.value || '').length);
33232
- nativeInputRequired = true;
33233
- }
33234
- const rect = el.getBoundingClientRect();
33235
- return {
33236
- filled: true,
33237
- query,
33238
- matchCount: candidates.length,
33239
- index,
33240
- tag: el.tagName.toLowerCase(),
33241
- role: el.getAttribute('role') || null,
33242
- type: fieldType,
33243
- text,
33244
- actualValue: el.isContentEditable ? el.textContent : 'value' in el ? el.value : null,
33245
- valueLength: String(el.isContentEditable ? el.textContent : 'value' in el ? el.value : '').length,
33246
- nativeInputRequired,
33247
- semanticSelection,
33248
- selectionMatched,
33249
- desktopPoint: desktopPoint(rect),
33250
- rect: {
33251
- x: Math.round(rect.x),
33252
- y: Math.round(rect.y),
33253
- width: Math.round(rect.width),
33254
- height: Math.round(rect.height),
33255
- centerX: Math.round(rect.x + rect.width / 2),
33256
- centerY: Math.round(rect.y + rect.height / 2),
33257
- },
33258
- };
33259
- })()`;
33260
- }
33261
- async function computerBrowserFillCommand(query, value, options = {}) {
33262
- const page = await selectChromePage(options);
33263
- if (options.ref) {
33264
- const target = await prepareBrowserElement(page, options.ref);
33265
- const backendNodeId = parseBrowserRef(options.ref);
33266
- const actual = await withChromeSession(page.webSocketDebuggerUrl, async (session) => await fillChromeNodeInSession(session, backendNodeId, value));
33267
- if (target.desktopPoint) logRecordingAction({ type: "type", ...target.desktopPoint });
33268
- console.log(JSON.stringify({
33269
- ...browserTargetOutput(page),
33270
- result: { filled: true, ref: target.ref, valueLength: actual.valueLength, rect: target.rect, element: target.element }
33271
- }, null, 2));
33272
- return;
33273
- }
33274
- if (!query) fail("Provide field text or --ref <id> from the latest browser state");
33275
- const index = options.index ? parseCoord(options.index, "--index") : 0;
33276
- if (index < 0) fail("--index must be >= 0");
33277
- const result = await evaluateChromeTarget(
33278
- page.webSocketDebuggerUrl,
33279
- buildBrowserFillExpression(query, value, { exact: !!options.exact, index })
33280
- );
33281
- const point = browserActionPoint(result);
33282
- if (typeof result === "object" && result !== null && "nativeInputRequired" in result && result.nativeInputRequired === true) {
33283
- await replaceChromeText(page, value);
33284
- const actualValue = await evaluateChromeTarget(page.webSocketDebuggerUrl, `(() => {
33285
- const el = document.activeElement;
33286
- return el && el.isContentEditable ? el.textContent : el && 'value' in el ? el.value : null;
33287
- })()`);
33288
- const fieldType = "type" in result && typeof result.type === "string" ? result.type.toLowerCase() : "";
33289
- if (browserFillInvalid(actualValue, value, fieldType)) {
33290
- fail(`Chrome reported that ${JSON.stringify(query)} contains ${JSON.stringify(actualValue)} after filling, expected ${JSON.stringify(value)}.`);
33291
- }
33292
- if (isRecord(result)) {
33293
- Object.assign(result, { actualValue, valueLength: String(actualValue ?? "").length });
33294
- }
33295
- } else if (isRecord(result) && result.filled === true) {
33296
- const fieldType = typeof result.type === "string" ? result.type : "";
33297
- if (browserFillInvalid(result.actualValue, value, fieldType, result.semanticSelection === true, result.selectionMatched === true)) {
33298
- fail(`Chrome reported that ${JSON.stringify(query)} contains ${JSON.stringify(result.actualValue)} after filling, expected ${JSON.stringify(value)}.`);
33299
- }
33300
- }
33301
- if (point) logRecordingAction({ type: "type", ...point });
33302
- console.log(JSON.stringify({
33303
- ...browserTargetOutput(page),
33304
- result
33305
- }, null, 2));
33306
- }
33307
- function buildBrowserWaitExpression(query, options) {
33308
- return `(() => {
33309
- const query = ${JSON.stringify(query)};
33310
- const mode = ${JSON.stringify(options.mode)};
33311
- const exact = ${JSON.stringify(options.exact)};
33312
- const normalize = (text) => String(text || '').replace(/\\s+/g, ' ').trim();
33313
- ${VISIBLE_JS}
33314
- const matches = (text) => {
33315
- const haystack = normalize(text).toLowerCase();
33316
- const needle = query.toLowerCase();
33317
- return exact ? haystack === needle : haystack.includes(needle);
33318
- };
33319
- const controlText = (el) => normalize(el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || el.href || el.id || el.name || el.tagName);
33320
- const controls = () => Array.from(document.querySelectorAll('a,button,input,textarea,select,[role="button"],[role="link"],[role="textbox"],[contenteditable="true"]'))
33321
- .filter(visible)
33322
- .map(controlText)
33323
- .filter(Boolean);
33324
- const values = {
33325
- title: [document.title],
33326
- url: [location.href],
33327
- text: [document.body ? document.body.innerText : ''],
33328
- control: controls(),
33329
- any: [document.title, location.href, document.body ? document.body.innerText : '', ...controls()],
33330
- }[mode];
33331
- if (!values) return { matched: false, query, mode, reason: 'invalid mode' };
33332
- const match = values.find(matches) || null;
33333
- return {
33334
- matched: !!match,
33335
- query,
33336
- mode,
33337
- exact,
33338
- match: match ? normalize(match).slice(0, 500) : null,
33339
- title: document.title,
33340
- url: location.href,
33341
- };
33342
- })()`;
33343
- }
33344
- async function computerBrowserWaitCommand(query, options = {}) {
33345
- const mode = options.mode ?? "any";
33346
- if (!["any", "text", "title", "url", "control"].includes(mode)) fail("--mode must be one of any|text|title|url|control");
33347
- const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 1e4;
33348
- const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 250;
33349
- if (timeoutMs < 0) fail("--timeout must be >= 0");
33350
- if (pollMs < 50 || pollMs > 2e3) fail("--poll-ms must be between 50 and 2000");
33351
- const page = await selectChromePage(options);
33352
- const start = Date.now();
33353
- const expression = buildBrowserWaitExpression(query, { mode, exact: !!options.exact });
33354
- let attempts = 0;
33355
- let lastResult = null;
33356
- while (Date.now() - start <= timeoutMs) {
33357
- attempts++;
33358
- lastResult = await evaluateChromeTarget(page.webSocketDebuggerUrl, expression);
33359
- if (typeof lastResult === "object" && lastResult !== null && "matched" in lastResult && lastResult.matched) {
33360
- console.log(JSON.stringify({ ok: true, elapsedMs: Date.now() - start, attempts, result: lastResult }, null, 2));
33361
- return;
33362
- }
33363
- await sleep2(pollMs);
33364
- }
33365
- console.log(JSON.stringify({ ok: false, elapsedMs: Date.now() - start, attempts, result: lastResult }, null, 2));
33366
- process.exitCode = 1;
33367
- }
33368
- async function computerBrowserScrollCommand(direction, options = {}) {
33369
- const page = await selectChromePage(options);
33370
- const normalized = direction.toLowerCase();
33371
- if (!["up", "down", "left", "right"].includes(normalized)) fail("direction must be one of up|down|left|right");
33372
- const amount = options.amount ? parseCoord(options.amount, "--amount") : 600;
33373
- if (amount <= 0 || amount > 1e4) fail("--amount must be between 1 and 10000 CSS pixels");
33374
- const layout = await sendChromeCommand(page.webSocketDebuggerUrl, "Page.getLayoutMetrics", {});
33375
- const viewport = chromeVisualViewport(layout);
33376
- const target = options.ref ? await prepareBrowserElement(page, options.ref) : null;
33377
- const x = target?.rect.centerX ?? viewport.width / 2;
33378
- const y = target?.rect.centerY ?? viewport.height / 2;
33379
- const deltaX = normalized === "left" ? -amount : normalized === "right" ? amount : 0;
33380
- const deltaY = normalized === "up" ? -amount : normalized === "down" ? amount : 0;
33381
- await sendChromeCommand(page.webSocketDebuggerUrl, "Input.dispatchMouseEvent", {
33382
- type: "mouseWheel",
33383
- x,
33384
- y,
33385
- deltaX,
33386
- deltaY
33387
- });
33388
- logRecordingAction({ type: "scroll", ...target?.desktopPoint ?? {} });
33389
- console.log(JSON.stringify({
33390
- ...browserTargetOutput(page),
33391
- result: { scrolled: true, direction: normalized, amount, ref: target?.ref ?? null, x: Math.round(x), y: Math.round(y) }
33392
- }, null, 2));
33393
- }
33394
- function browserKeyDetails(combo) {
33395
- const tokens = combo.split("+").map((token) => token.trim()).filter(Boolean);
33396
- if (tokens.length === 0) fail("key combination cannot be empty");
33397
- let modifiers = 0;
33398
- for (const token of tokens.slice(0, -1)) {
33399
- const normalized2 = token.toLowerCase();
33400
- if (["alt", "option"].includes(normalized2)) modifiers |= 1;
33401
- else if (["ctrl", "control"].includes(normalized2)) modifiers |= 2;
33402
- else if (["meta", "cmd", "command", "super"].includes(normalized2)) modifiers |= 4;
33403
- else if (normalized2 === "shift") modifiers |= 8;
33404
- else fail(`Unsupported browser key modifier ${JSON.stringify(token)}`);
33405
- }
33406
- const requested = tokens[tokens.length - 1];
33407
- const aliases = {
33408
- enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
33409
- return: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
33410
- tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
33411
- escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
33412
- esc: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
33413
- backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
33414
- delete: { key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 },
33415
- space: { key: " ", code: "Space", windowsVirtualKeyCode: 32 },
33416
- up: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
33417
- arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
33418
- down: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
33419
- arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
33420
- left: { key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 },
33421
- arrowleft: { key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 },
33422
- right: { key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 },
33423
- arrowright: { key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 },
33424
- home: { key: "Home", code: "Home", windowsVirtualKeyCode: 36 },
33425
- end: { key: "End", code: "End", windowsVirtualKeyCode: 35 },
33426
- pageup: { key: "PageUp", code: "PageUp", windowsVirtualKeyCode: 33 },
33427
- pagedown: { key: "PageDown", code: "PageDown", windowsVirtualKeyCode: 34 }
33428
- };
33429
- const normalized = requested.toLowerCase();
33430
- const alias = aliases[normalized];
33431
- if (alias) return { ...alias, modifiers, ...alias.key === " " && modifiers === 0 ? { text: " " } : {} };
33432
- if (/^[a-z]$/i.test(requested)) {
33433
- const upper = requested.toUpperCase();
33434
- const key = modifiers & 8 ? upper : requested.toLowerCase();
33435
- return { key, code: `Key${upper}`, windowsVirtualKeyCode: upper.charCodeAt(0), modifiers, ...modifiers === 0 ? { text: key } : {} };
33436
- }
33437
- if (/^[0-9]$/.test(requested)) {
33438
- return { key: requested, code: `Digit${requested}`, windowsVirtualKeyCode: requested.charCodeAt(0), modifiers, ...modifiers === 0 ? { text: requested } : {} };
33439
- }
33440
- fail(`Unsupported browser key ${JSON.stringify(requested)}`);
33441
- }
33442
- async function computerBrowserKeyCommand(combo, options = {}) {
33443
- const page = await selectChromePage(options);
33444
- const key = browserKeyDetails(combo);
33445
- await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
33446
- await dispatchChromeKeyInSession(session, key);
33447
- });
33448
- logRecordingAction({ type: "key" });
33449
- console.log(JSON.stringify({ targetId: page.id ?? null, result: { pressed: combo } }, null, 2));
33450
- }
33451
- async function dispatchChromeKeyInSession(session, key) {
33452
- await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
33453
- type: "keyDown",
33454
- ...key,
33455
- nativeVirtualKeyCode: key.windowsVirtualKeyCode
33456
- });
33457
- await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
33458
- type: "keyUp",
33459
- key: key.key,
33460
- code: key.code,
33461
- modifiers: key.modifiers,
33462
- windowsVirtualKeyCode: key.windowsVirtualKeyCode,
33463
- nativeVirtualKeyCode: key.windowsVirtualKeyCode
33464
- });
33465
- }
33466
- async function computerBrowserTypeCommand(text, options = {}) {
33467
- const page = await selectChromePage(options);
33468
- await sendChromeCommand(page.webSocketDebuggerUrl, "Input.insertText", { text });
33469
- logRecordingAction({ type: "type" });
33470
- console.log(JSON.stringify({ targetId: page.id ?? null, result: { typed: true, length: text.length } }, null, 2));
33471
- }
33472
- function parseBrowserBatchActions(value) {
33473
- let input;
33474
- try {
33475
- input = JSON.parse(value);
33476
- } catch (error51) {
33477
- fail(`Batch actions must be valid JSON: ${error51 instanceof Error ? error51.message : String(error51)}`);
33478
- }
33479
- if (!Array.isArray(input) || input.length === 0 || input.length > 100) {
33480
- fail("Batch actions must be a JSON array containing 1 to 100 actions");
33481
- }
33482
- return input.map((item, index) => {
33483
- if (!isRecord(item) || typeof item.action !== "string") fail(`Batch action ${index} must be an object with an action`);
33484
- if (item.action === "click") {
33485
- if (typeof item.ref !== "string" && typeof item.ref !== "number") fail(`Batch click ${index} requires ref`);
33486
- const buttons = { "1": "left", "2": "middle", "3": "right", left: "left", middle: "middle", right: "right" };
33487
- const requestedButton = item.button === void 0 ? "left" : String(item.button).toLowerCase();
33488
- const button = buttons[requestedButton];
33489
- if (!button) fail(`Batch click ${index} button must be left, middle, or right`);
33490
- return { action: "click", ref: String(item.ref), button, clicks: item.double === true ? 2 : 1 };
33491
- }
33492
- if (item.action === "fill") {
33493
- if (typeof item.ref !== "string" && typeof item.ref !== "number" || typeof item.value !== "string") {
33494
- fail(`Batch fill ${index} requires ref and string value`);
33495
- }
33496
- return { action: "fill", ref: String(item.ref), value: item.value };
33497
- }
33498
- if (item.action === "key") {
33499
- if (typeof item.combo !== "string") fail(`Batch key ${index} requires combo`);
33500
- return { action: "key", combo: item.combo };
33501
- }
33502
- if (item.action === "type") {
33503
- if (typeof item.text !== "string") fail(`Batch type ${index} requires text`);
33504
- return { action: "type", text: item.text };
33505
- }
33506
- if (item.action === "scroll") {
33507
- const directions = { up: "up", down: "down", left: "left", right: "right" };
33508
- const direction = typeof item.direction === "string" ? directions[item.direction] : void 0;
33509
- if (!direction) {
33510
- fail(`Batch scroll ${index} direction must be up, down, left, or right`);
33511
- }
33512
- const amount = item.amount === void 0 ? 600 : Number(item.amount);
33513
- if (!Number.isFinite(amount) || amount <= 0 || amount > 1e4) fail(`Batch scroll ${index} amount must be between 1 and 10000`);
33514
- if (item.ref !== void 0 && typeof item.ref !== "string" && typeof item.ref !== "number") fail(`Batch scroll ${index} ref is invalid`);
33515
- return {
33516
- action: "scroll",
33517
- direction,
33518
- amount,
33519
- ...item.ref === void 0 ? {} : { ref: String(item.ref) }
33520
- };
33521
- }
33522
- if (item.action === "wait") {
33523
- if (typeof item.text !== "string") fail(`Batch wait ${index} requires text`);
33524
- const mode = item.mode === void 0 ? "any" : String(item.mode);
33525
- if (!["any", "text", "title", "url", "control"].includes(mode)) fail(`Batch wait ${index} mode is invalid`);
33526
- const timeoutMs = item.timeoutMs === void 0 ? 1e4 : Number(item.timeoutMs);
33527
- const pollMs = item.pollMs === void 0 ? 250 : Number(item.pollMs);
33528
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0) fail(`Batch wait ${index} timeoutMs must be >= 0`);
33529
- if (!Number.isFinite(pollMs) || pollMs < 50 || pollMs > 2e3) fail(`Batch wait ${index} pollMs must be between 50 and 2000`);
33530
- return { action: "wait", text: item.text, mode, exact: item.exact === true, timeoutMs, pollMs };
33531
- }
33532
- fail(`Batch action ${index} has unsupported action ${JSON.stringify(item.action)}`);
33533
- });
33534
- }
33535
- async function computerBrowserBatchCommand(actionsJson, options = {}) {
33536
- const actions = parseBrowserBatchActions(actionsJson);
33537
- const page = await selectChromePage(options);
33538
- const { targetId, cached: cached2 } = readBrowserRefCache(page);
33539
- const startedAt = Date.now();
33540
- const completed = [];
33541
- try {
33542
- await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
33543
- const layout = await sendChromeSessionCommand(session, "Page.getLayoutMetrics", {});
33544
- const viewportRect = browserViewportRect(layout);
33545
- for (const [index, action] of actions.entries()) {
33546
- const actionStartedAt = Date.now();
33547
- if (action.action === "click") {
33548
- const ref = parseBrowserRef(action.ref);
33549
- const rect = await prepareBrowserRefInSession(session, ref, cached2, viewportRect);
33550
- await dispatchChromeClickInSession(session, rect.centerX, rect.centerY, action.button, action.clicks);
33551
- logRecordingAction({ type: "click" });
33552
- completed.push({ index, action: action.action, ref: String(ref), elapsedMs: Date.now() - actionStartedAt });
33553
- } else if (action.action === "fill") {
33554
- const ref = parseBrowserRef(action.ref);
33555
- await prepareBrowserRefInSession(session, ref, cached2, viewportRect);
33556
- const result = await fillChromeNodeInSession(session, ref, action.value);
33557
- logRecordingAction({ type: "type" });
33558
- completed.push({ index, action: action.action, ref: String(ref), valueLength: result.valueLength, elapsedMs: Date.now() - actionStartedAt });
33559
- } else if (action.action === "key") {
33560
- await dispatchChromeKeyInSession(session, browserKeyDetails(action.combo));
33561
- logRecordingAction({ type: "key" });
33562
- completed.push({ index, action: action.action, elapsedMs: Date.now() - actionStartedAt });
33563
- } else if (action.action === "type") {
33564
- await sendChromeSessionCommand(session, "Input.insertText", { text: action.text });
33565
- logRecordingAction({ type: "type" });
33566
- completed.push({ index, action: action.action, length: action.text.length, elapsedMs: Date.now() - actionStartedAt });
33567
- } else if (action.action === "scroll") {
33568
- const ref = action.ref ? parseBrowserRef(action.ref) : null;
33569
- const rect = ref ? await prepareBrowserRefInSession(session, ref, cached2, viewportRect) : viewportRect;
33570
- const deltaX = action.direction === "left" ? -action.amount : action.direction === "right" ? action.amount : 0;
33571
- const deltaY = action.direction === "up" ? -action.amount : action.direction === "down" ? action.amount : 0;
33572
- await sendChromeSessionCommand(session, "Input.dispatchMouseEvent", {
33573
- type: "mouseWheel",
33574
- x: rect.centerX,
33575
- y: rect.centerY,
33576
- deltaX,
33577
- deltaY
33578
- });
33579
- logRecordingAction({ type: "scroll" });
33580
- completed.push({ index, action: action.action, direction: action.direction, amount: action.amount, elapsedMs: Date.now() - actionStartedAt });
33581
- } else {
33582
- const expression = buildBrowserWaitExpression(action.text, { mode: action.mode, exact: action.exact });
33583
- let attempts = 0;
33584
- let result = null;
33585
- while (Date.now() - actionStartedAt <= action.timeoutMs) {
33586
- attempts++;
33587
- result = await evaluateChromeSession(session, expression);
33588
- if (isRecord(result) && result.matched === true) break;
33589
- await sleep2(action.pollMs);
33590
- }
33591
- if (!isRecord(result) || result.matched !== true) fail(`Batch wait ${index} timed out after ${Date.now() - actionStartedAt}ms`);
33592
- completed.push({ index, action: action.action, attempts, elapsedMs: Date.now() - actionStartedAt });
33593
- }
33594
- }
33595
- });
33596
- } catch (error51) {
33597
- const message = error51 instanceof Error ? error51.message : String(error51);
33598
- console.log(JSON.stringify({ ok: false, targetId, elapsedMs: Date.now() - startedAt, completed, failedIndex: completed.length, error: message }, null, 2));
33599
- throw error51;
33600
- }
33601
- console.log(JSON.stringify({ ok: true, targetId, elapsedMs: Date.now() - startedAt, completed }, null, 2));
33602
- }
33603
- async function computerClickCommand(xStr, yStr, options) {
33604
- const dimensions = getDisplayDimensions();
33605
- const x = parseScreenCoord(xStr, "x", dimensions.width);
33606
- const y = parseScreenCoord(yStr, "y", dimensions.height);
33607
- const button = options.button ?? "1";
33608
- const args = ["mousemove", "--sync", String(x), String(y)];
33609
- if (options.modifiers) {
33610
- for (const mod of options.modifiers.split("+")) {
33611
- args.push("keydown", mod);
33612
- }
33613
- }
33614
- if (options.double) {
33615
- args.push("click", "--repeat", "2", "--delay", "50", button);
33616
- } else {
33617
- args.push("click", button);
33618
- }
33619
- if (options.modifiers) {
33620
- for (const mod of options.modifiers.split("+").reverse()) {
33621
- args.push("keyup", mod);
33622
- }
33623
- }
33624
- runDesktopInputCmd(args);
33625
- logRecordingAction({ type: "click", x, y });
33626
- console.log(`clicked ${button === "1" ? "left" : button === "2" ? "middle" : button === "3" ? "right" : `button ${button}`} at (${x},${y})${options.double ? " x2" : ""}`);
33627
- }
33628
- async function computerMoveCommand(xStr, yStr) {
33629
- const dimensions = getDisplayDimensions();
33630
- const x = parseScreenCoord(xStr, "x", dimensions.width);
33631
- const y = parseScreenCoord(yStr, "y", dimensions.height);
33632
- runDesktopInputCmd(["mousemove", "--sync", String(x), String(y)]);
33633
- logRecordingAction({ type: "move", x, y });
33634
- console.log(`moved to (${x},${y})`);
33635
- }
33636
- async function computerTypeCommand(text, options) {
33637
- const delay = options.delay ? parseCoord(options.delay, "--delay") : 12;
33638
- runDesktopInputCmd(["type", "--delay", String(delay), "--", text]);
33639
- const position = recordingMousePosition();
33640
- logRecordingAction({ type: "type", ...position ?? {} });
33641
- console.log(`typed ${text.length} char${text.length === 1 ? "" : "s"}`);
33642
- }
33643
- async function computerKeyCommand(combo) {
33644
- runDesktopInputCmd(["key", "--", combo]);
33645
- const position = recordingMousePosition();
33646
- logRecordingAction({ type: "key", ...position ?? {} });
33647
- console.log(`pressed ${combo}`);
33648
- }
33649
- async function computerScrollCommand(direction, options) {
33650
- const dir = direction.toLowerCase();
33651
- const buttonMap = { up: "4", down: "5", left: "6", right: "7" };
33652
- const button = buttonMap[dir];
33653
- if (!button) fail(`direction must be one of up|down|left|right (got "${direction}")`);
33654
- const amount = options.amount ? parseCoord(options.amount, "--amount") : 3;
33655
- const args = [];
33656
- let hoverPosition = null;
33657
- if (options.x && options.y) {
33658
- const dimensions = getDisplayDimensions();
33659
- hoverPosition = {
33660
- x: parseScreenCoord(options.x, "--x", dimensions.width),
33661
- y: parseScreenCoord(options.y, "--y", dimensions.height)
33662
- };
33663
- args.push(
33664
- "mousemove",
33665
- "--sync",
33666
- String(hoverPosition.x),
33667
- String(hoverPosition.y)
33668
- );
33669
- }
33670
- args.push("click", "--repeat", String(amount), "--delay", "30", button);
33671
- runDesktopInputCmd(args);
33672
- const position = hoverPosition ?? recordingMousePosition();
33673
- logRecordingAction({ type: "scroll", ...position ?? {} });
33674
- console.log(`scrolled ${dir} x${amount}`);
33675
- }
33676
- async function computerDragCommand(fx, fy, tx, ty) {
33677
- const dimensions = getDisplayDimensions();
33678
- const fromX = parseScreenCoord(fx, "fromX", dimensions.width);
33679
- const fromY = parseScreenCoord(fy, "fromY", dimensions.height);
33680
- const toX = parseScreenCoord(tx, "toX", dimensions.width);
33681
- const toY = parseScreenCoord(ty, "toY", dimensions.height);
33682
- const distance = Math.hypot(toX - fromX, toY - fromY);
33683
- const steps = clamp(Math.ceil(distance / 70), 8, 24);
33684
- const args = [
33685
- "mousemove",
33686
- "--sync",
33687
- String(fromX),
33688
- String(fromY),
33689
- "mousedown",
33690
- "1"
33691
- ];
33692
- for (let i = 1; i <= steps; i++) {
33693
- const x = Math.round(fromX + (toX - fromX) * i / steps);
33694
- const y = Math.round(fromY + (toY - fromY) * i / steps);
33695
- args.push("mousemove", "--sync", String(x), String(y), "sleep", "0.018");
33696
- }
33697
- args.push("mouseup", "1");
33698
- runDesktopInputCmd(args);
33699
- logRecordingAction({ type: "drag", x: fromX, y: fromY, toX, toY });
33700
- console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
33701
- }
33702
- var APP_ALIASES = {
33703
- chrome: [CHROME_WRAPPER],
33704
- chromium: ["chromium", "--no-first-run", "--no-default-browser-check"],
33705
- firefox: ["firefox"],
33706
- terminal: ["xfce4-terminal"],
33707
- xterm: ["xterm"],
33708
- notepad: ["mousepad"],
33709
- editor: ["mousepad"],
33710
- files: ["thunar"],
33711
- filemanager: ["thunar"]
33712
- };
33713
- var LEGACY_CHROME_ALIAS = [
33714
- "google-chrome",
33715
- "--no-first-run",
33716
- "--no-default-browser-check",
33717
- "--start-maximized",
33718
- "--user-data-dir=/tmp/replicas-computer/chrome-profile"
33719
- ];
33720
- async function computerLaunchCommand(app, args) {
33721
- ensureServicesRunning();
33722
- const baseArgs = app === "chrome" && !existsSync3(CHROME_WRAPPER) ? LEGACY_CHROME_ALIAS : APP_ALIASES[app] ?? [app];
33723
- const bin = baseArgs[0];
33724
- const fullArgs = [...baseArgs.slice(1), ...args];
33725
- const expectsNewChromePage = args.some((arg) => !arg.startsWith("-"));
33726
- const existingChromePageIds = /* @__PURE__ */ new Set();
33727
- if (app === "chrome") {
33728
- try {
33729
- for (const page of await getChromePages()) {
33730
- if (page.id) existingChromePageIds.add(page.id);
33731
- }
33732
- } catch {
33733
- }
33734
- }
33735
- const child = spawn5(bin, fullArgs, {
33736
- env: withDisplay(),
33737
- detached: true,
33738
- stdio: "ignore"
33739
- });
33740
- child.unref();
33741
- if (app === "chrome") {
33742
- const startedAt = Date.now();
33743
- let pageCount = 0;
33744
- let readyPage = null;
33745
- while (Date.now() - startedAt < 15e3) {
33746
- let pages = [];
33747
- try {
33748
- pages = await getChromePages();
33749
- } catch {
33750
- }
33751
- pageCount = pages.length;
33752
- const candidates = expectsNewChromePage && existingChromePageIds.size > 0 ? pages.filter((page) => page.id && !existingChromePageIds.has(page.id)) : pages;
33753
- for (const page of candidates) {
33754
- if (!page.webSocketDebuggerUrl || !page.url || page.url === "about:blank") continue;
33755
- const readyState = await evaluateChromeTarget(page.webSocketDebuggerUrl, "document.readyState");
33756
- if (readyState === "interactive" || readyState === "complete") {
33757
- readyPage = page;
33758
- break;
33759
- }
33760
- }
33761
- if (readyPage) break;
33762
- try {
33763
- if (child.pid) process.kill(child.pid, 0);
33764
- } catch {
33765
- fail(`Chrome exited before its desktop control channel became ready.`);
33766
- }
33767
- await sleep2(100);
33768
- }
33769
- if (!readyPage?.webSocketDebuggerUrl) fail(`Chrome launched but its requested page was not controllable after 15 seconds.`);
33770
- await sendChromeCommand(readyPage.webSocketDebuggerUrl, "Page.bringToFront", {});
33771
- console.log(`launched ${bin} (pid ${child.pid}, ready in ${Date.now() - startedAt}ms, page ${readyPage.id}, ${pageCount} open)`);
33772
- return;
33773
- }
33774
- console.log(`launched ${bin} (pid ${child.pid})`);
33775
- }
33776
-
33777
30907
  // src/commands/interactive.ts
33778
- import chalk22 from "chalk";
30908
+ import chalk21 from "chalk";
33779
30909
 
33780
30910
  // src/interactive/index.tsx
33781
30911
  import { createCliRenderer } from "@opentui/core";
@@ -33849,7 +30979,7 @@ function useReconnectingSseStream(options) {
33849
30979
  while (!cancelled) {
33850
30980
  const stop = await connect();
33851
30981
  if (cancelled || stop) break;
33852
- await new Promise((resolve3) => setTimeout(resolve3, reconnectDelayMs));
30982
+ await new Promise((resolve2) => setTimeout(resolve2, reconnectDelayMs));
33853
30983
  }
33854
30984
  };
33855
30985
  run().catch(() => setConnected(false));
@@ -37084,13 +34214,13 @@ async function interactiveCommand() {
37084
34214
  'No organization selected. Please run "replicas org switch" to select an organization.'
37085
34215
  );
37086
34216
  }
37087
- console.log(chalk22.gray("Starting interactive mode..."));
34217
+ console.log(chalk21.gray("Starting interactive mode..."));
37088
34218
  await launchInteractive();
37089
34219
  }
37090
34220
 
37091
34221
  // src/commands/environment.ts
37092
34222
  import fs5 from "fs";
37093
- import chalk23 from "chalk";
34223
+ import chalk22 from "chalk";
37094
34224
  import prompts6 from "prompts";
37095
34225
  var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
37096
34226
  function maskValue(value) {
@@ -37103,38 +34233,38 @@ async function resolveEnvironmentId(input) {
37103
34233
  const response = await orgAuthenticatedFetch("/v1/environments");
37104
34234
  const resolved = resolveByNameOrId(input, response.environments);
37105
34235
  if (!resolved) {
37106
- console.log(chalk23.red(`Environment not found: ${input}`));
34236
+ console.log(chalk22.red(`Environment not found: ${input}`));
37107
34237
  const available = response.environments.map((e) => e.name).join(", ");
37108
- console.log(chalk23.gray(`Available: ${available || "(none)"}`));
34238
+ console.log(chalk22.gray(`Available: ${available || "(none)"}`));
37109
34239
  process.exit(1);
37110
34240
  }
37111
34241
  return resolved.id;
37112
34242
  }
37113
34243
  function printEnvironment(env) {
37114
- console.log(chalk23.white(` ${env.name}${env.is_global ? chalk23.gray(" (global)") : ""}`));
37115
- console.log(chalk23.gray(` ID: ${env.id}`));
34244
+ console.log(chalk22.white(` ${env.name}${env.is_global ? chalk22.gray(" (global)") : ""}`));
34245
+ console.log(chalk22.gray(` ID: ${env.id}`));
37116
34246
  if (env.description) {
37117
- console.log(chalk23.gray(` Description: ${env.description}`));
34247
+ console.log(chalk22.gray(` Description: ${env.description}`));
37118
34248
  }
37119
34249
  if (env.repository_id) {
37120
- console.log(chalk23.gray(` Repository: ${env.repository_id}`));
34250
+ console.log(chalk22.gray(` Repository: ${env.repository_id}`));
37121
34251
  } else if (env.repository_set_id) {
37122
- console.log(chalk23.gray(` Repository Set: ${env.repository_set_id}`));
34252
+ console.log(chalk22.gray(` Repository Set: ${env.repository_set_id}`));
37123
34253
  }
37124
34254
  if (env.variable_count !== void 0) {
37125
- console.log(chalk23.gray(` Variables: ${env.variable_count}, Files: ${env.file_count ?? 0}, Skills: ${env.skill_count ?? 0}, MCPs: ${env.mcp_count ?? 0}`));
34255
+ console.log(chalk22.gray(` Variables: ${env.variable_count}, Files: ${env.file_count ?? 0}, Skills: ${env.skill_count ?? 0}, MCPs: ${env.mcp_count ?? 0}`));
37126
34256
  }
37127
- console.log(chalk23.gray(` Updated: ${formatDate2(env.updated_at)}`));
34257
+ console.log(chalk22.gray(` Updated: ${formatDate2(env.updated_at)}`));
37128
34258
  console.log();
37129
34259
  }
37130
34260
  async function environmentListCommand() {
37131
34261
  ensureOrgApiAuthenticated();
37132
34262
  const response = await orgAuthenticatedFetch("/v1/environments");
37133
34263
  if (response.environments.length === 0) {
37134
- console.log(chalk23.yellow("\nNo environments found.\n"));
34264
+ console.log(chalk22.yellow("\nNo environments found.\n"));
37135
34265
  return;
37136
34266
  }
37137
- console.log(chalk23.green(`
34267
+ console.log(chalk22.green(`
37138
34268
  Environments (${response.environments.length}):
37139
34269
  `));
37140
34270
  for (const env of response.environments) {
@@ -37145,7 +34275,7 @@ async function environmentGetCommand(idOrName) {
37145
34275
  ensureOrgApiAuthenticated();
37146
34276
  const id = await resolveEnvironmentId(idOrName);
37147
34277
  const response = await orgAuthenticatedFetch(`/v1/environments/${id}`);
37148
- console.log(chalk23.green(`
34278
+ console.log(chalk22.green(`
37149
34279
  Environment: ${response.environment.name}
37150
34280
  `));
37151
34281
  printEnvironment(response.environment);
@@ -37161,7 +34291,7 @@ async function environmentCreateCommand(name, options) {
37161
34291
  validate: (v) => v.trim() ? true : "Name is required"
37162
34292
  });
37163
34293
  if (!r.name) {
37164
- console.log(chalk23.yellow("\nCancelled."));
34294
+ console.log(chalk22.yellow("\nCancelled."));
37165
34295
  return;
37166
34296
  }
37167
34297
  envName = r.name;
@@ -37174,8 +34304,8 @@ async function environmentCreateCommand(name, options) {
37174
34304
  const repos2 = await orgAuthenticatedFetch("/v1/repositories");
37175
34305
  const repo = repos2.repositories.find((r) => r.name === options.repository);
37176
34306
  if (!repo) {
37177
- console.log(chalk23.red(`Repository not found: ${options.repository}`));
37178
- console.log(chalk23.gray(`Available: ${repos2.repositories.map((r) => r.name).join(", ")}`));
34307
+ console.log(chalk22.red(`Repository not found: ${options.repository}`));
34308
+ console.log(chalk22.gray(`Available: ${repos2.repositories.map((r) => r.name).join(", ")}`));
37179
34309
  process.exit(1);
37180
34310
  }
37181
34311
  repositoryId = repo.id;
@@ -37205,9 +34335,9 @@ async function environmentCreateCommand(name, options) {
37205
34335
  method: "POST",
37206
34336
  body
37207
34337
  });
37208
- console.log(chalk23.green(`
34338
+ console.log(chalk22.green(`
37209
34339
  Created environment: ${response.environment.name}`));
37210
- console.log(chalk23.gray(` ID: ${response.environment.id}
34340
+ console.log(chalk22.gray(` ID: ${response.environment.id}
37211
34341
  `));
37212
34342
  }
37213
34343
  async function environmentEditCommand(idOrName, options) {
@@ -37226,21 +34356,21 @@ async function environmentEditCommand(idOrName, options) {
37226
34356
  const repos2 = await orgAuthenticatedFetch("/v1/repositories");
37227
34357
  const repo = repos2.repositories.find((r) => r.name === options.repository);
37228
34358
  if (!repo) {
37229
- console.log(chalk23.red(`Repository not found: ${options.repository}`));
34359
+ console.log(chalk22.red(`Repository not found: ${options.repository}`));
37230
34360
  process.exit(1);
37231
34361
  }
37232
34362
  body.repository_id = repo.id;
37233
34363
  }
37234
34364
  }
37235
34365
  if (Object.keys(body).length === 0) {
37236
- console.log(chalk23.yellow("\nNo changes specified. Pass --name, --description, --repository, or --system-prompt."));
34366
+ console.log(chalk22.yellow("\nNo changes specified. Pass --name, --description, --repository, or --system-prompt."));
37237
34367
  return;
37238
34368
  }
37239
34369
  const response = await orgAuthenticatedFetch(`/v1/environments/${id}`, {
37240
34370
  method: "PATCH",
37241
34371
  body
37242
34372
  });
37243
- console.log(chalk23.green(`
34373
+ console.log(chalk22.green(`
37244
34374
  Updated environment: ${response.environment.name}
37245
34375
  `));
37246
34376
  }
@@ -37255,20 +34385,20 @@ async function environmentDeleteCommand(idOrName, options) {
37255
34385
  initial: false
37256
34386
  });
37257
34387
  if (!r.confirm) {
37258
- console.log(chalk23.yellow("\nCancelled."));
34388
+ console.log(chalk22.yellow("\nCancelled."));
37259
34389
  return;
37260
34390
  }
37261
34391
  }
37262
34392
  await orgAuthenticatedFetch(`/v1/environments/${id}`, { method: "DELETE" });
37263
- console.log(chalk23.green(`
34393
+ console.log(chalk22.green(`
37264
34394
  Deleted environment ${idOrName}.
37265
34395
  `));
37266
34396
  }
37267
34397
  function printVariable(v, reveal) {
37268
- console.log(chalk23.white(` ${v.key}`));
37269
- console.log(chalk23.gray(` ID: ${v.id}`));
37270
- console.log(chalk23.gray(` Value: ${reveal ? v.value : maskValue(v.value)}`));
37271
- console.log(chalk23.gray(` Updated: ${formatDate2(v.updated_at)}`));
34398
+ console.log(chalk22.white(` ${v.key}`));
34399
+ console.log(chalk22.gray(` ID: ${v.id}`));
34400
+ console.log(chalk22.gray(` Value: ${reveal ? v.value : maskValue(v.value)}`));
34401
+ console.log(chalk22.gray(` Updated: ${formatDate2(v.updated_at)}`));
37272
34402
  console.log();
37273
34403
  }
37274
34404
  async function envVarsListCommand(envIdOrName, options) {
@@ -37278,14 +34408,14 @@ async function envVarsListCommand(envIdOrName, options) {
37278
34408
  `/v1/environments/${id}/variables`
37279
34409
  );
37280
34410
  if (response.environment_variables.length === 0) {
37281
- console.log(chalk23.yellow("\nNo variables.\n"));
34411
+ console.log(chalk22.yellow("\nNo variables.\n"));
37282
34412
  return;
37283
34413
  }
37284
- console.log(chalk23.green(`
34414
+ console.log(chalk22.green(`
37285
34415
  Variables (${response.environment_variables.length}):
37286
34416
  `));
37287
34417
  if (!options.reveal) {
37288
- console.log(chalk23.gray(" Values are masked. Pass --reveal to show full values.\n"));
34418
+ console.log(chalk22.gray(" Values are masked. Pass --reveal to show full values.\n"));
37289
34419
  }
37290
34420
  for (const v of response.environment_variables) printVariable(v, !!options.reveal);
37291
34421
  }
@@ -37302,7 +34432,7 @@ async function envVarsSetCommand(envIdOrName, key, value) {
37302
34432
  `/v1/environments/${id}/variables/${match.id}`,
37303
34433
  { method: "PATCH", body: body2 }
37304
34434
  );
37305
- console.log(chalk23.green(`
34435
+ console.log(chalk22.green(`
37306
34436
  Updated variable ${response2.environment_variable.key}.
37307
34437
  `));
37308
34438
  return;
@@ -37316,7 +34446,7 @@ Updated variable ${response2.environment_variable.key}.
37316
34446
  `/v1/environments/${id}/variables`,
37317
34447
  { method: "POST", body }
37318
34448
  );
37319
- console.log(chalk23.green(`
34449
+ console.log(chalk22.green(`
37320
34450
  Created variable ${response.environment_variable.key}.
37321
34451
  `));
37322
34452
  }
@@ -37330,7 +34460,7 @@ async function envVarsDeleteCommand(envIdOrName, keyOrId, options) {
37330
34460
  );
37331
34461
  const match = existing.environment_variables.find((v) => v.key === keyOrId);
37332
34462
  if (!match) {
37333
- console.log(chalk23.red(`Variable not found: ${keyOrId}`));
34463
+ console.log(chalk22.red(`Variable not found: ${keyOrId}`));
37334
34464
  process.exit(1);
37335
34465
  }
37336
34466
  variableId = match.id;
@@ -37343,23 +34473,23 @@ async function envVarsDeleteCommand(envIdOrName, keyOrId, options) {
37343
34473
  initial: false
37344
34474
  });
37345
34475
  if (!r.confirm) {
37346
- console.log(chalk23.yellow("\nCancelled."));
34476
+ console.log(chalk22.yellow("\nCancelled."));
37347
34477
  return;
37348
34478
  }
37349
34479
  }
37350
34480
  await orgAuthenticatedFetch(`/v1/environments/${id}/variables/${variableId}`, {
37351
34481
  method: "DELETE"
37352
34482
  });
37353
- console.log(chalk23.green(`
34483
+ console.log(chalk22.green(`
37354
34484
  Deleted variable ${keyOrId}.
37355
34485
  `));
37356
34486
  }
37357
34487
  function printFile(f) {
37358
- console.log(chalk23.white(` ${f.path}`));
37359
- console.log(chalk23.gray(` ID: ${f.id}`));
37360
- console.log(chalk23.gray(` Name: ${f.name}`));
37361
- console.log(chalk23.gray(` Size: ${f.content.length} bytes`));
37362
- console.log(chalk23.gray(` Updated: ${formatDate2(f.updated_at)}`));
34488
+ console.log(chalk22.white(` ${f.path}`));
34489
+ console.log(chalk22.gray(` ID: ${f.id}`));
34490
+ console.log(chalk22.gray(` Name: ${f.name}`));
34491
+ console.log(chalk22.gray(` Size: ${f.content.length} bytes`));
34492
+ console.log(chalk22.gray(` Updated: ${formatDate2(f.updated_at)}`));
37363
34493
  console.log();
37364
34494
  }
37365
34495
  async function envFilesListCommand(envIdOrName) {
@@ -37369,10 +34499,10 @@ async function envFilesListCommand(envIdOrName) {
37369
34499
  `/v1/environments/${id}/files`
37370
34500
  );
37371
34501
  if (response.environment_files.length === 0) {
37372
- console.log(chalk23.yellow("\nNo files.\n"));
34502
+ console.log(chalk22.yellow("\nNo files.\n"));
37373
34503
  return;
37374
34504
  }
37375
- console.log(chalk23.green(`
34505
+ console.log(chalk22.green(`
37376
34506
  Files (${response.environment_files.length}):
37377
34507
  `));
37378
34508
  for (const f of response.environment_files) printFile(f);
@@ -37403,7 +34533,7 @@ async function envFilesSetCommand(envIdOrName, destinationPath, options) {
37403
34533
  `/v1/environments/${id}/files/${match.id}`,
37404
34534
  { method: "PATCH", body: body2 }
37405
34535
  );
37406
- console.log(chalk23.green(`
34536
+ console.log(chalk22.green(`
37407
34537
  Updated file ${response2.environment_file.path}.
37408
34538
  `));
37409
34539
  return;
@@ -37418,7 +34548,7 @@ Updated file ${response2.environment_file.path}.
37418
34548
  `/v1/environments/${id}/files`,
37419
34549
  { method: "POST", body }
37420
34550
  );
37421
- console.log(chalk23.green(`
34551
+ console.log(chalk22.green(`
37422
34552
  Created file ${response.environment_file.path}.
37423
34553
  `));
37424
34554
  }
@@ -37432,7 +34562,7 @@ async function envFilesDeleteCommand(envIdOrName, pathOrId, options) {
37432
34562
  );
37433
34563
  const match = existing.environment_files.find((f) => f.path === pathOrId);
37434
34564
  if (!match) {
37435
- console.log(chalk23.red(`File not found: ${pathOrId}`));
34565
+ console.log(chalk22.red(`File not found: ${pathOrId}`));
37436
34566
  process.exit(1);
37437
34567
  }
37438
34568
  fileId = match.id;
@@ -37445,14 +34575,14 @@ async function envFilesDeleteCommand(envIdOrName, pathOrId, options) {
37445
34575
  initial: false
37446
34576
  });
37447
34577
  if (!r.confirm) {
37448
- console.log(chalk23.yellow("\nCancelled."));
34578
+ console.log(chalk22.yellow("\nCancelled."));
37449
34579
  return;
37450
34580
  }
37451
34581
  }
37452
34582
  await orgAuthenticatedFetch(`/v1/environments/${id}/files/${fileId}`, {
37453
34583
  method: "DELETE"
37454
34584
  });
37455
- console.log(chalk23.green(`
34585
+ console.log(chalk22.green(`
37456
34586
  Deleted file ${pathOrId}.
37457
34587
  `));
37458
34588
  }
@@ -37463,15 +34593,15 @@ async function envStartHookGetCommand(envIdOrName) {
37463
34593
  `/v1/environments/${id}/start-hooks`
37464
34594
  );
37465
34595
  if (!response.start_hook) {
37466
- console.log(chalk23.yellow("\nNo start hook configured.\n"));
34596
+ console.log(chalk22.yellow("\nNo start hook configured.\n"));
37467
34597
  return;
37468
34598
  }
37469
34599
  const hook = response.start_hook;
37470
- console.log(chalk23.green(`
34600
+ console.log(chalk22.green(`
37471
34601
  Start hook (v${hook.version}, ${hook.is_active ? "active" : "inactive"}):
37472
34602
  `));
37473
- console.log(chalk23.gray(` ID: ${hook.id}`));
37474
- console.log(chalk23.gray(` Created: ${formatDate2(hook.created_at)}
34603
+ console.log(chalk22.gray(` ID: ${hook.id}`));
34604
+ console.log(chalk22.gray(` Created: ${formatDate2(hook.created_at)}
37475
34605
  `));
37476
34606
  console.log(hook.content);
37477
34607
  console.log();
@@ -37486,10 +34616,10 @@ async function envStartHookSaveCommand(envIdOrName, options) {
37486
34616
  { method: "POST", body }
37487
34617
  );
37488
34618
  if (!response.start_hook) {
37489
- console.log(chalk23.green("\nCleared start hook.\n"));
34619
+ console.log(chalk22.green("\nCleared start hook.\n"));
37490
34620
  return;
37491
34621
  }
37492
- console.log(chalk23.green(`
34622
+ console.log(chalk22.green(`
37493
34623
  Saved start hook v${response.start_hook.version}.
37494
34624
  `));
37495
34625
  }
@@ -37506,7 +34636,7 @@ async function envStartHookTestCommand(envIdOrName, options) {
37506
34636
  body: { content },
37507
34637
  onEvent: (event) => {
37508
34638
  if (event.type === "progress" && event.message) {
37509
- console.log(chalk23.gray(event.message));
34639
+ console.log(chalk22.gray(event.message));
37510
34640
  } else if (event.type === "output" && event.output) {
37511
34641
  process.stdout.write(event.output);
37512
34642
  } else if (event.type === "complete") {
@@ -37519,21 +34649,21 @@ async function envStartHookTestCommand(envIdOrName, options) {
37519
34649
  }
37520
34650
  );
37521
34651
  if (errorMessage) {
37522
- console.log(chalk23.red(`
34652
+ console.log(chalk22.red(`
37523
34653
  ${errorMessage}
37524
34654
  `));
37525
34655
  process.exit(1);
37526
34656
  }
37527
34657
  if (timedOut) {
37528
- console.log(chalk23.yellow("\nStart hook timed out.\n"));
34658
+ console.log(chalk22.yellow("\nStart hook timed out.\n"));
37529
34659
  process.exit(1);
37530
34660
  }
37531
34661
  if (exitCode === 0) {
37532
- console.log(chalk23.green(`
34662
+ console.log(chalk22.green(`
37533
34663
  Start hook passed (exit code ${exitCode}).
37534
34664
  `));
37535
34665
  } else {
37536
- console.log(chalk23.red(`
34666
+ console.log(chalk22.red(`
37537
34667
  Start hook failed (exit code ${exitCode ?? "unknown"}).
37538
34668
  `));
37539
34669
  process.exit(1);
@@ -37546,24 +34676,24 @@ async function envStartHookRepositoryHooksCommand(envIdOrName) {
37546
34676
  `/v1/environments/${id}/start-hooks/repository-hooks`
37547
34677
  );
37548
34678
  if (response.repositories.length === 0) {
37549
- console.log(chalk23.yellow("\nNo repositories bound to this environment.\n"));
34679
+ console.log(chalk22.yellow("\nNo repositories bound to this environment.\n"));
37550
34680
  return;
37551
34681
  }
37552
- console.log(chalk23.green(`
34682
+ console.log(chalk22.green(`
37553
34683
  Repository start hooks (${response.repositories.length}):
37554
34684
  `));
37555
34685
  for (const repo of response.repositories) {
37556
- console.log(chalk23.white(` ${repo.repository_name} @${repo.default_branch}`));
34686
+ console.log(chalk22.white(` ${repo.repository_name} @${repo.default_branch}`));
37557
34687
  if (repo.error) {
37558
- console.log(chalk23.red(` Error: ${repo.error}`));
34688
+ console.log(chalk22.red(` Error: ${repo.error}`));
37559
34689
  } else if (repo.start_hook) {
37560
- console.log(chalk23.gray(` Source: ${repo.filename ?? "(unknown)"}`));
37561
- console.log(chalk23.gray(` Commands (${repo.start_hook.commands.length}):`));
34690
+ console.log(chalk22.gray(` Source: ${repo.filename ?? "(unknown)"}`));
34691
+ console.log(chalk22.gray(` Commands (${repo.start_hook.commands.length}):`));
37562
34692
  for (const cmd of repo.start_hook.commands) {
37563
- console.log(chalk23.gray(` ${cmd}`));
34693
+ console.log(chalk22.gray(` ${cmd}`));
37564
34694
  }
37565
34695
  } else {
37566
- console.log(chalk23.gray(` No startHook defined.`));
34696
+ console.log(chalk22.gray(` No startHook defined.`));
37567
34697
  }
37568
34698
  console.log();
37569
34699
  }
@@ -37585,7 +34715,7 @@ function registerSlackCommands(parent) {
37585
34715
  await slackThreadAttachCommand(options);
37586
34716
  } catch (error51) {
37587
34717
  if (error51 instanceof Error) {
37588
- console.error(chalk24.red(`
34718
+ console.error(chalk23.red(`
37589
34719
  \u2717 ${error51.message}
37590
34720
  `));
37591
34721
  }
@@ -37597,7 +34727,7 @@ function registerSlackCommands(parent) {
37597
34727
  await slackThreadSwitchCommand(workspace, options);
37598
34728
  } catch (error51) {
37599
34729
  if (error51 instanceof Error) {
37600
- console.error(chalk24.red(`
34730
+ console.error(chalk23.red(`
37601
34731
  \u2717 ${error51.message}
37602
34732
  `));
37603
34733
  }
@@ -37611,7 +34741,7 @@ program.command("login").description("Authenticate with your Replicas account").
37611
34741
  await loginCommand();
37612
34742
  } catch (error51) {
37613
34743
  if (error51 instanceof Error) {
37614
- console.error(chalk24.red(`
34744
+ console.error(chalk23.red(`
37615
34745
  \u2717 ${error51.message}
37616
34746
  `));
37617
34747
  }
@@ -37623,7 +34753,7 @@ program.command("init").description("Create a replicas.json or replicas.yaml con
37623
34753
  initCommand(options);
37624
34754
  } catch (error51) {
37625
34755
  if (error51 instanceof Error) {
37626
- console.error(chalk24.red(`
34756
+ console.error(chalk23.red(`
37627
34757
  \u2717 ${error51.message}
37628
34758
  `));
37629
34759
  }
@@ -37635,7 +34765,7 @@ program.command("logout").description("Clear stored credentials").action(() => {
37635
34765
  logoutCommand();
37636
34766
  } catch (error51) {
37637
34767
  if (error51 instanceof Error) {
37638
- console.error(chalk24.red(`
34768
+ console.error(chalk23.red(`
37639
34769
  \u2717 ${error51.message}
37640
34770
  `));
37641
34771
  }
@@ -37647,7 +34777,7 @@ program.command("whoami").description("Display current authenticated user").acti
37647
34777
  await whoamiCommand();
37648
34778
  } catch (error51) {
37649
34779
  if (error51 instanceof Error) {
37650
- console.error(chalk24.red(`
34780
+ console.error(chalk23.red(`
37651
34781
  \u2717 ${error51.message}
37652
34782
  `));
37653
34783
  }
@@ -37659,7 +34789,7 @@ program.command("codex-auth").description("Connect your Codex credentials to Rep
37659
34789
  await codexAuthCommand(options);
37660
34790
  } catch (error51) {
37661
34791
  if (error51 instanceof Error) {
37662
- console.error(chalk24.red(`
34792
+ console.error(chalk23.red(`
37663
34793
  \u2717 ${error51.message}
37664
34794
  `));
37665
34795
  }
@@ -37671,7 +34801,7 @@ program.command("claude-auth").description("Connect your Claude Code credentials
37671
34801
  await claudeAuthCommand(options);
37672
34802
  } catch (error51) {
37673
34803
  if (error51 instanceof Error) {
37674
- console.error(chalk24.red(`
34804
+ console.error(chalk23.red(`
37675
34805
  \u2717 ${error51.message}
37676
34806
  `));
37677
34807
  }
@@ -37684,7 +34814,7 @@ org.command("switch").description("Switch to a different organization").action(a
37684
34814
  await orgSwitchCommand();
37685
34815
  } catch (error51) {
37686
34816
  if (error51 instanceof Error) {
37687
- console.error(chalk24.red(`
34817
+ console.error(chalk23.red(`
37688
34818
  \u2717 ${error51.message}
37689
34819
  `));
37690
34820
  }
@@ -37696,7 +34826,7 @@ org.action(async () => {
37696
34826
  await orgCommand();
37697
34827
  } catch (error51) {
37698
34828
  if (error51 instanceof Error) {
37699
- console.error(chalk24.red(`
34829
+ console.error(chalk23.red(`
37700
34830
  \u2717 ${error51.message}
37701
34831
  `));
37702
34832
  }
@@ -37708,7 +34838,7 @@ program.command("connect <workspace-name>").description("Connect to a workspace
37708
34838
  await connectCommand(workspaceName);
37709
34839
  } catch (error51) {
37710
34840
  if (error51 instanceof Error) {
37711
- console.error(chalk24.red(`
34841
+ console.error(chalk23.red(`
37712
34842
  \u2717 ${error51.message}
37713
34843
  `));
37714
34844
  }
@@ -37720,7 +34850,7 @@ program.command("code <workspace-name>").description("Open a workspace in VSCode
37720
34850
  await codeCommand(workspaceName);
37721
34851
  } catch (error51) {
37722
34852
  if (error51 instanceof Error) {
37723
- console.error(chalk24.red(`
34853
+ console.error(chalk23.red(`
37724
34854
  \u2717 ${error51.message}
37725
34855
  `));
37726
34856
  }
@@ -37733,7 +34863,7 @@ config2.command("get <key>").description("Get a configuration value").action(asy
37733
34863
  await configGetCommand(key);
37734
34864
  } catch (error51) {
37735
34865
  if (error51 instanceof Error) {
37736
- console.error(chalk24.red(`
34866
+ console.error(chalk23.red(`
37737
34867
  \u2717 ${error51.message}
37738
34868
  `));
37739
34869
  }
@@ -37745,7 +34875,7 @@ config2.command("set <key> <value>").description("Set a configuration value").ac
37745
34875
  await configSetCommand(key, value);
37746
34876
  } catch (error51) {
37747
34877
  if (error51 instanceof Error) {
37748
- console.error(chalk24.red(`
34878
+ console.error(chalk23.red(`
37749
34879
  \u2717 ${error51.message}
37750
34880
  `));
37751
34881
  }
@@ -37757,7 +34887,7 @@ config2.command("list").description("List all configuration values").action(asyn
37757
34887
  await configListCommand();
37758
34888
  } catch (error51) {
37759
34889
  if (error51 instanceof Error) {
37760
- console.error(chalk24.red(`
34890
+ console.error(chalk23.red(`
37761
34891
  \u2717 ${error51.message}
37762
34892
  `));
37763
34893
  }
@@ -37769,7 +34899,7 @@ program.command("list").description("List all replicas").option("-p, --page <pag
37769
34899
  await replicaListCommand(options);
37770
34900
  } catch (error51) {
37771
34901
  if (error51 instanceof Error) {
37772
- console.error(chalk24.red(`
34902
+ console.error(chalk23.red(`
37773
34903
  \u2717 ${error51.message}
37774
34904
  `));
37775
34905
  }
@@ -37781,7 +34911,7 @@ program.command("get <id>").description("Get replica details by ID").action(asyn
37781
34911
  await replicaGetCommand(id);
37782
34912
  } catch (error51) {
37783
34913
  if (error51 instanceof Error) {
37784
- console.error(chalk24.red(`
34914
+ console.error(chalk23.red(`
37785
34915
  \u2717 ${error51.message}
37786
34916
  `));
37787
34917
  }
@@ -37793,7 +34923,7 @@ program.command("create [name]").description("Create a new replica").option("-m,
37793
34923
  await replicaCreateCommand(name, options);
37794
34924
  } catch (error51) {
37795
34925
  if (error51 instanceof Error) {
37796
- console.error(chalk24.red(`
34926
+ console.error(chalk23.red(`
37797
34927
  \u2717 ${error51.message}
37798
34928
  `));
37799
34929
  }
@@ -37805,7 +34935,7 @@ program.command("send <id>").description("Send a message to a replica").option("
37805
34935
  await replicaSendCommand(id, options);
37806
34936
  } catch (error51) {
37807
34937
  if (error51 instanceof Error) {
37808
- console.error(chalk24.red(`
34938
+ console.error(chalk23.red(`
37809
34939
  \u2717 ${error51.message}
37810
34940
  `));
37811
34941
  }
@@ -37817,19 +34947,19 @@ program.command("delete <id>").description("Delete a replica").option("-f, --for
37817
34947
  await replicaDeleteCommand(id, options);
37818
34948
  } catch (error51) {
37819
34949
  if (error51 instanceof Error) {
37820
- console.error(chalk24.red(`
34950
+ console.error(chalk23.red(`
37821
34951
  \u2717 ${error51.message}
37822
34952
  `));
37823
34953
  }
37824
34954
  process.exit(1);
37825
34955
  }
37826
34956
  });
37827
- program.command("read <id>").description("Read conversation history of a replica").option("-l, --limit <limit>", "Maximum number of events to return").option("-o, --offset <offset>", "Number of events to skip from the end").action(async (id, options) => {
34957
+ program.command("read <id>").description("Read conversation history of a replica").option("-l, --limit <limit>", "Maximum number of events to return, from the end").option("-b, --before-event <index>", "Read the page before this event index (see Has More output)").action(async (id, options) => {
37828
34958
  try {
37829
34959
  await replicaReadCommand(id, options);
37830
34960
  } catch (error51) {
37831
34961
  if (error51 instanceof Error) {
37832
- console.error(chalk24.red(`
34962
+ console.error(chalk23.red(`
37833
34963
  \u2717 ${error51.message}
37834
34964
  `));
37835
34965
  }
@@ -37842,7 +34972,7 @@ automation.command("list").description("List all automations").option("-p, --pag
37842
34972
  await automationListCommand(options);
37843
34973
  } catch (error51) {
37844
34974
  if (error51 instanceof Error) {
37845
- console.error(chalk24.red(`
34975
+ console.error(chalk23.red(`
37846
34976
  \u2717 ${error51.message}
37847
34977
  `));
37848
34978
  }
@@ -37854,7 +34984,7 @@ automation.command("get <id>").description("Get automation details by ID").actio
37854
34984
  await automationGetCommand(id);
37855
34985
  } catch (error51) {
37856
34986
  if (error51 instanceof Error) {
37857
- console.error(chalk24.red(`
34987
+ console.error(chalk23.red(`
37858
34988
  \u2717 ${error51.message}
37859
34989
  `));
37860
34990
  }
@@ -37869,7 +34999,7 @@ automation.command("create [name]").description("Create a new automation").optio
37869
34999
  });
37870
35000
  } catch (error51) {
37871
35001
  if (error51 instanceof Error) {
37872
- console.error(chalk24.red(`
35002
+ console.error(chalk23.red(`
37873
35003
  \u2717 ${error51.message}
37874
35004
  `));
37875
35005
  }
@@ -37881,7 +35011,7 @@ automation.command("edit <id>").description("Edit an existing automation").optio
37881
35011
  await automationEditCommand(id, options);
37882
35012
  } catch (error51) {
37883
35013
  if (error51 instanceof Error) {
37884
- console.error(chalk24.red(`
35014
+ console.error(chalk23.red(`
37885
35015
  \u2717 ${error51.message}
37886
35016
  `));
37887
35017
  }
@@ -37893,7 +35023,7 @@ automation.command("run <id>").description("Manually trigger an automation (cron
37893
35023
  await automationRunCommand(id);
37894
35024
  } catch (error51) {
37895
35025
  if (error51 instanceof Error) {
37896
- console.error(chalk24.red(`
35026
+ console.error(chalk23.red(`
37897
35027
  \u2717 ${error51.message}
37898
35028
  `));
37899
35029
  }
@@ -37905,7 +35035,7 @@ automation.command("delete <id>").description("Delete an automation").option("-f
37905
35035
  await automationDeleteCommand(id, options);
37906
35036
  } catch (error51) {
37907
35037
  if (error51 instanceof Error) {
37908
- console.error(chalk24.red(`
35038
+ console.error(chalk23.red(`
37909
35039
  \u2717 ${error51.message}
37910
35040
  `));
37911
35041
  }
@@ -37917,7 +35047,7 @@ automation.command("check <checkRunId>").description("Report this automation run
37917
35047
  await automationCheckCommand(checkRunId, options);
37918
35048
  } catch (error51) {
37919
35049
  if (error51 instanceof Error) {
37920
- console.error(chalk24.red(`
35050
+ console.error(chalk23.red(`
37921
35051
  \u2717 ${error51.message}
37922
35052
  `));
37923
35053
  }
@@ -37929,7 +35059,7 @@ automation.action(async () => {
37929
35059
  await automationListCommand({});
37930
35060
  } catch (error51) {
37931
35061
  if (error51 instanceof Error) {
37932
- console.error(chalk24.red(`
35062
+ console.error(chalk23.red(`
37933
35063
  \u2717 ${error51.message}
37934
35064
  `));
37935
35065
  }
@@ -37942,7 +35072,7 @@ repos.command("list").description("List all repositories").action(async () => {
37942
35072
  await repositoriesListCommand();
37943
35073
  } catch (error51) {
37944
35074
  if (error51 instanceof Error) {
37945
- console.error(chalk24.red(`
35075
+ console.error(chalk23.red(`
37946
35076
  \u2717 ${error51.message}
37947
35077
  `));
37948
35078
  }
@@ -37954,7 +35084,7 @@ repos.action(async () => {
37954
35084
  await repositoriesListCommand();
37955
35085
  } catch (error51) {
37956
35086
  if (error51 instanceof Error) {
37957
- console.error(chalk24.red(`
35087
+ console.error(chalk23.red(`
37958
35088
  \u2717 ${error51.message}
37959
35089
  `));
37960
35090
  }
@@ -37967,7 +35097,7 @@ environment.command("list").description("List all environments").action(async ()
37967
35097
  await environmentListCommand();
37968
35098
  } catch (error51) {
37969
35099
  if (error51 instanceof Error) {
37970
- console.error(chalk24.red(`
35100
+ console.error(chalk23.red(`
37971
35101
  \u2717 ${error51.message}
37972
35102
  `));
37973
35103
  }
@@ -37979,7 +35109,7 @@ environment.command("get <id-or-name>").description('Get an environment by ID or
37979
35109
  await environmentGetCommand(idOrName);
37980
35110
  } catch (error51) {
37981
35111
  if (error51 instanceof Error) {
37982
- console.error(chalk24.red(`
35112
+ console.error(chalk23.red(`
37983
35113
  \u2717 ${error51.message}
37984
35114
  `));
37985
35115
  }
@@ -37991,7 +35121,7 @@ environment.command("create [name]").description("Create a new environment").opt
37991
35121
  await environmentCreateCommand(name, options);
37992
35122
  } catch (error51) {
37993
35123
  if (error51 instanceof Error) {
37994
- console.error(chalk24.red(`
35124
+ console.error(chalk23.red(`
37995
35125
  \u2717 ${error51.message}
37996
35126
  `));
37997
35127
  }
@@ -38003,7 +35133,7 @@ environment.command("edit <id-or-name>").description("Edit an environment").opti
38003
35133
  await environmentEditCommand(idOrName, options);
38004
35134
  } catch (error51) {
38005
35135
  if (error51 instanceof Error) {
38006
- console.error(chalk24.red(`
35136
+ console.error(chalk23.red(`
38007
35137
  \u2717 ${error51.message}
38008
35138
  `));
38009
35139
  }
@@ -38015,7 +35145,7 @@ environment.command("delete <id-or-name>").description("Delete an environment").
38015
35145
  await environmentDeleteCommand(idOrName, options);
38016
35146
  } catch (error51) {
38017
35147
  if (error51 instanceof Error) {
38018
- console.error(chalk24.red(`
35148
+ console.error(chalk23.red(`
38019
35149
  \u2717 ${error51.message}
38020
35150
  `));
38021
35151
  }
@@ -38028,7 +35158,7 @@ envVars.command("list <env>").description("List variables in an environment (val
38028
35158
  await envVarsListCommand(env, options);
38029
35159
  } catch (error51) {
38030
35160
  if (error51 instanceof Error) {
38031
- console.error(chalk24.red(`
35161
+ console.error(chalk23.red(`
38032
35162
  \u2717 ${error51.message}
38033
35163
  `));
38034
35164
  }
@@ -38040,7 +35170,7 @@ envVars.command("set <env> <key> <value>").description("Create or update a varia
38040
35170
  await envVarsSetCommand(env, key, value);
38041
35171
  } catch (error51) {
38042
35172
  if (error51 instanceof Error) {
38043
- console.error(chalk24.red(`
35173
+ console.error(chalk23.red(`
38044
35174
  \u2717 ${error51.message}
38045
35175
  `));
38046
35176
  }
@@ -38052,7 +35182,7 @@ envVars.command("delete <env> <key-or-id>").description("Delete a variable by ke
38052
35182
  await envVarsDeleteCommand(env, keyOrId, options);
38053
35183
  } catch (error51) {
38054
35184
  if (error51 instanceof Error) {
38055
- console.error(chalk24.red(`
35185
+ console.error(chalk23.red(`
38056
35186
  \u2717 ${error51.message}
38057
35187
  `));
38058
35188
  }
@@ -38065,7 +35195,7 @@ envFiles.command("list <env>").description("List files in an environment").actio
38065
35195
  await envFilesListCommand(env);
38066
35196
  } catch (error51) {
38067
35197
  if (error51 instanceof Error) {
38068
- console.error(chalk24.red(`
35198
+ console.error(chalk23.red(`
38069
35199
  \u2717 ${error51.message}
38070
35200
  `));
38071
35201
  }
@@ -38077,7 +35207,7 @@ envFiles.command("set <env> <destination-path>").description("Create or update a
38077
35207
  await envFilesSetCommand(env, destinationPath, options);
38078
35208
  } catch (error51) {
38079
35209
  if (error51 instanceof Error) {
38080
- console.error(chalk24.red(`
35210
+ console.error(chalk23.red(`
38081
35211
  \u2717 ${error51.message}
38082
35212
  `));
38083
35213
  }
@@ -38089,7 +35219,7 @@ envFiles.command("delete <env> <path-or-id>").description("Delete a file by dest
38089
35219
  await envFilesDeleteCommand(env, pathOrId, options);
38090
35220
  } catch (error51) {
38091
35221
  if (error51 instanceof Error) {
38092
- console.error(chalk24.red(`
35222
+ console.error(chalk23.red(`
38093
35223
  \u2717 ${error51.message}
38094
35224
  `));
38095
35225
  }
@@ -38102,7 +35232,7 @@ envStartHooks.command("get <env>").description("Show the active start hook for a
38102
35232
  await envStartHookGetCommand(env);
38103
35233
  } catch (error51) {
38104
35234
  if (error51 instanceof Error) {
38105
- console.error(chalk24.red(`
35235
+ console.error(chalk23.red(`
38106
35236
  \u2717 ${error51.message}
38107
35237
  `));
38108
35238
  }
@@ -38114,7 +35244,7 @@ envStartHooks.command("save <env>").description("Save and activate a start hook
38114
35244
  await envStartHookSaveCommand(env, options);
38115
35245
  } catch (error51) {
38116
35246
  if (error51 instanceof Error) {
38117
- console.error(chalk24.red(`
35247
+ console.error(chalk23.red(`
38118
35248
  \u2717 ${error51.message}
38119
35249
  `));
38120
35250
  }
@@ -38126,7 +35256,7 @@ envStartHooks.command("test <env>").description("Run a start hook in an isolated
38126
35256
  await envStartHookTestCommand(env, options);
38127
35257
  } catch (error51) {
38128
35258
  if (error51 instanceof Error) {
38129
- console.error(chalk24.red(`
35259
+ console.error(chalk23.red(`
38130
35260
  \u2717 ${error51.message}
38131
35261
  `));
38132
35262
  }
@@ -38138,7 +35268,7 @@ envStartHooks.command("repository-hooks <env>").description("List per-repo start
38138
35268
  await envStartHookRepositoryHooksCommand(env);
38139
35269
  } catch (error51) {
38140
35270
  if (error51 instanceof Error) {
38141
- console.error(chalk24.red(`
35271
+ console.error(chalk23.red(`
38142
35272
  \u2717 ${error51.message}
38143
35273
  `));
38144
35274
  }
@@ -38150,7 +35280,7 @@ environment.action(async () => {
38150
35280
  await environmentListCommand();
38151
35281
  } catch (error51) {
38152
35282
  if (error51 instanceof Error) {
38153
- console.error(chalk24.red(`
35283
+ console.error(chalk23.red(`
38154
35284
  \u2717 ${error51.message}
38155
35285
  `));
38156
35286
  }
@@ -38162,7 +35292,7 @@ program.command("interact").alias("i").description("Launch the interactive termi
38162
35292
  await interactiveCommand();
38163
35293
  } catch (error51) {
38164
35294
  if (error51 instanceof Error) {
38165
- console.error(chalk24.red(`
35295
+ console.error(chalk23.red(`
38166
35296
  \u2717 ${error51.message}
38167
35297
  `));
38168
35298
  }
@@ -38213,7 +35343,7 @@ if (isAgentMode()) {
38213
35343
  await previewAddCommand(workspaceId, options);
38214
35344
  } catch (error51) {
38215
35345
  if (error51 instanceof Error) {
38216
- console.error(chalk24.red(`
35346
+ console.error(chalk23.red(`
38217
35347
  \u2717 ${error51.message}
38218
35348
  `));
38219
35349
  }
@@ -38225,7 +35355,7 @@ if (isAgentMode()) {
38225
35355
  await previewListCommand(workspaceId);
38226
35356
  } catch (error51) {
38227
35357
  if (error51 instanceof Error) {
38228
- console.error(chalk24.red(`
35358
+ console.error(chalk23.red(`
38229
35359
  \u2717 ${error51.message}
38230
35360
  `));
38231
35361
  }
@@ -38237,7 +35367,7 @@ if (isAgentMode()) {
38237
35367
  await previewRemoveCommand(workspaceId, options);
38238
35368
  } catch (error51) {
38239
35369
  if (error51 instanceof Error) {
38240
- console.error(chalk24.red(`
35370
+ console.error(chalk23.red(`
38241
35371
  \u2717 ${error51.message}
38242
35372
  `));
38243
35373
  }
@@ -38298,7 +35428,7 @@ if (isAgentMode()) {
38298
35428
  await mediaUploadCommand(files, options);
38299
35429
  } catch (error51) {
38300
35430
  if (error51 instanceof Error) {
38301
- console.error(chalk24.red(`
35431
+ console.error(chalk23.red(`
38302
35432
  \u2717 ${error51.message}
38303
35433
  `));
38304
35434
  }
@@ -38309,7 +35439,7 @@ if (isAgentMode()) {
38309
35439
  try {
38310
35440
  await mediaShareCommand(mediaId);
38311
35441
  } catch (error51) {
38312
- if (error51 instanceof Error) console.error(chalk24.red(`
35442
+ if (error51 instanceof Error) console.error(chalk23.red(`
38313
35443
  \u2717 ${error51.message}
38314
35444
  `));
38315
35445
  process.exit(1);
@@ -38319,7 +35449,7 @@ if (isAgentMode()) {
38319
35449
  try {
38320
35450
  await mediaRevokeCommand(mediaId);
38321
35451
  } catch (error51) {
38322
- if (error51 instanceof Error) console.error(chalk24.red(`
35452
+ if (error51 instanceof Error) console.error(chalk23.red(`
38323
35453
  \u2717 ${error51.message}
38324
35454
  `));
38325
35455
  process.exit(1);
@@ -38330,7 +35460,7 @@ if (isAgentMode()) {
38330
35460
  await mediaListCommand(options);
38331
35461
  } catch (error51) {
38332
35462
  if (error51 instanceof Error) {
38333
- console.error(chalk24.red(`
35463
+ console.error(chalk23.red(`
38334
35464
  \u2717 ${error51.message}
38335
35465
  `));
38336
35466
  }
@@ -38343,7 +35473,7 @@ if (isAgentMode()) {
38343
35473
  await learningsReadCommand(options);
38344
35474
  } catch (error51) {
38345
35475
  if (error51 instanceof Error) {
38346
- console.error(chalk24.red(`
35476
+ console.error(chalk23.red(`
38347
35477
  \u2717 ${error51.message}
38348
35478
  `));
38349
35479
  }
@@ -38355,7 +35485,7 @@ if (isAgentMode()) {
38355
35485
  await learningsAddCommand(options);
38356
35486
  } catch (error51) {
38357
35487
  if (error51 instanceof Error) {
38358
- console.error(chalk24.red(`
35488
+ console.error(chalk23.red(`
38359
35489
  \u2717 ${error51.message}
38360
35490
  `));
38361
35491
  }
@@ -38367,7 +35497,7 @@ if (isAgentMode()) {
38367
35497
  await learningsUpdateCommand(id, options);
38368
35498
  } catch (error51) {
38369
35499
  if (error51 instanceof Error) {
38370
- console.error(chalk24.red(`
35500
+ console.error(chalk23.red(`
38371
35501
  \u2717 ${error51.message}
38372
35502
  `));
38373
35503
  }
@@ -38379,7 +35509,7 @@ if (isAgentMode()) {
38379
35509
  await learningsDeleteCommand(id);
38380
35510
  } catch (error51) {
38381
35511
  if (error51 instanceof Error) {
38382
- console.error(chalk24.red(`
35512
+ console.error(chalk23.red(`
38383
35513
  \u2717 ${error51.message}
38384
35514
  `));
38385
35515
  }
@@ -38391,7 +35521,7 @@ if (isAgentMode()) {
38391
35521
  await learningsWithdrawCommand(id);
38392
35522
  } catch (error51) {
38393
35523
  if (error51 instanceof Error) {
38394
- console.error(chalk24.red(`
35524
+ console.error(chalk23.red(`
38395
35525
  \u2717 ${error51.message}
38396
35526
  `));
38397
35527
  }
@@ -38404,7 +35534,7 @@ if (isAgentMode()) {
38404
35534
  await mothershipSpawnCommand(options);
38405
35535
  } catch (error51) {
38406
35536
  if (error51 instanceof Error) {
38407
- console.error(chalk24.red(`
35537
+ console.error(chalk23.red(`
38408
35538
  \u2717 ${error51.message}
38409
35539
  `));
38410
35540
  }
@@ -38416,54 +35546,14 @@ if (isAgentMode()) {
38416
35546
  await mothershipRelayCommand(options);
38417
35547
  } catch (error51) {
38418
35548
  if (error51 instanceof Error) {
38419
- console.error(chalk24.red(`
35549
+ console.error(chalk23.red(`
38420
35550
  \u2717 ${error51.message}
38421
35551
  `));
38422
35552
  }
38423
35553
  process.exit(1);
38424
35554
  }
38425
35555
  });
38426
- const computer = program.command("computer").description("Drive the workspace Linux desktop (mouse, keyboard, screenshots, screen recording, live preview)");
38427
- const wrap = (fn) => async (...args) => {
38428
- try {
38429
- await fn(...args);
38430
- } catch (error51) {
38431
- if (error51 instanceof Error) {
38432
- console.error(chalk24.red(`
38433
- \u2717 ${error51.message}
38434
- `));
38435
- }
38436
- process.exit(1);
38437
- }
38438
- };
38439
- computer.command("info").description("Print the live noVNC viewer URL for the workspace desktop. The preview is registered automatically at engine startup, so this just looks it up \u2014 share the URL with the user to let them watch the desktop.").action(wrap(() => computerInfoCommand()));
38440
- computer.command("status").description("Show which desktop services are running and the active preview URL").action(wrap(() => computerStatusCommand()));
38441
- computer.command("screenshot <path>").description("Capture the current desktop to a PNG file. Use --raw or --grid for agent click planning; omit both for a branded shareable image.").option("--raw", "Save a 1:1 desktop capture with no branding, padding, or rounded corners").option("--grid [px]", "Overlay a coordinate grid on a 1:1 desktop capture. Default 100px.").action(wrap((path6, options) => computerScreenshotCommand(path6, options)));
38442
- computer.command("observe <path>").description("Wait briefly for the desktop to settle, save a 1:1 screenshot, and print JSON screen context for agents.").option("--raw", "Save a 1:1 desktop capture with no coordinate grid").option("--grid [px]", "Overlay a coordinate grid on the 1:1 desktop capture. Default 100px.").option("--timeout <ms>", "Maximum time to wait for visual stability. Default 3000.").option("--stable-ms <ms>", "Required unchanged time before the screen is considered stable. Default 600.").option("--poll-ms <ms>", "Screenshot polling interval while waiting. Default 200.").action(wrap((path6, options) => computerObserveCommand(path6, options)));
38443
- computer.command("browser").description("Print JSON for Chrome tabs launched through Replicas, including page titles and URLs.").option("--snapshot", "Include visible page text and interactive controls with bounding boxes").option("--limit <chars>", "Maximum body text characters per page snapshot. Default 4000.").option("--element-limit <n>", "Maximum controls per page snapshot. Default 80.").option("--target-id <id>", "Only include the Chrome page with this target id").option("--page <n>", "When multiple pages match, include the nth page. Default 0.").option("--title <text>", "Only include pages whose title contains text").option("--url <text>", "Only include pages whose URL contains text").action(wrap((options) => computerBrowserCommand(options)));
38444
- computer.command("browser-state <path>").description("Wait for a Chrome page to settle, capture its viewport, and print a bounded accessibility state with actionable refs.").option("--full", "Return the full accessibility tree instead of a diff from the previous state").option("--limit <chars>", "Maximum visible text characters. Default 8000.").option("--element-limit <n>", "Maximum actionable elements. Default 120.").option("--timeout <ms>", "Maximum time to wait for state stability. Default 5000.").option("--stable-ms <ms>", "Required unchanged time before capture. Default 500.").option("--poll-ms <ms>", "State polling interval. Default 100.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((path6, options) => computerBrowserStateCommand(path6, options)));
38445
- computer.command("browser-batch <actions>").description("Run an ordered, fail-fast JSON array of browser actions through one Chrome session.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((actions, options) => computerBrowserBatchCommand(actions, options)));
38446
- computer.command("browser-click [text]").description("Click a Chrome element by ref from browser-state, or fall back to visible control text.").option("--ref <id>", "Click the backend DOM node ref from the latest browser state").option("--exact", "Require an exact text match instead of substring matching").option("--index <n>", "When multiple controls match, click the nth match. Default 0.").option("--button <button>", "Mouse button for ref clicks: left, middle, or right. Default left.").option("--double", "Double-click a ref target").option("--target-id <id>", "Only target the Chrome page with this target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((text, options) => computerBrowserClickCommand(text, options)));
38447
- computer.command("browser-fill [field] [value]").description("Fill a Chrome field by ref from browser-state, or fall back to visible field text.").option("--ref <id>", "Fill the backend DOM node ref from the latest browser state").option("--exact", "Require an exact field match instead of substring matching").option("--index <n>", "When multiple fields match, fill the nth match. Default 0.").option("--target-id <id>", "Only target the Chrome page with this target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((field2, value, options) => {
38448
- const resolvedValue = value ?? (options.ref ? field2 : void 0);
38449
- const resolvedField = value === void 0 && options.ref ? void 0 : field2;
38450
- if (resolvedValue === void 0) throw new Error("Provide <field> <value>, or --ref <id> <value>");
38451
- return computerBrowserFillCommand(resolvedField, resolvedValue, options);
38452
- }));
38453
- computer.command("browser-scroll <direction>").description("Send trusted wheel input to a Chrome element ref or the center of the target page.").option("--ref <id>", "Scroll from the backend DOM node ref from the latest browser state").option("--amount <px>", "Scroll distance in CSS pixels. Default 600.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((direction, options) => computerBrowserScrollCommand(direction, options)));
38454
- computer.command("browser-key <combo>").description("Press a key or modifier chord in the target Chrome page with trusted browser input.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((combo, options) => computerBrowserKeyCommand(combo, options)));
38455
- computer.command("browser-type <text>").description("Type literal text into the focused field in the target Chrome page with trusted browser input.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((text, options) => computerBrowserTypeCommand(text, options)));
38456
- computer.command("browser-wait <text>").description("Wait until Chrome page title, URL, body text, or controls match <text>.").option("--mode <mode>", "Where to match: any, text, title, url, or control. Default any.").option("--exact", "Require an exact match instead of substring matching").option("--timeout <ms>", "Maximum time to wait. Default 10000.").option("--poll-ms <ms>", "Polling interval. Default 250.").option("--target-id <id>", "Only target the Chrome page with this target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((text, options) => computerBrowserWaitCommand(text, options)));
38457
- computer.command("click <x> <y>").description("Move to (x, y) and click. Coordinates are pixels or percentages like 50% 50%.").option("-b, --button <n>", "Mouse button (1=left, 2=middle, 3=right). Default 1.").option("--double", "Double-click instead of single-click").option("--modifiers <mods>", "Hold modifier keys during the click, e.g. ctrl or ctrl+shift").action(wrap((x, y, options) => computerClickCommand(x, y, options)));
38458
- computer.command("move <x> <y>").description("Move the mouse to (x, y) without clicking. Coordinates are pixels or percentages like 50% 50%.").action(wrap((x, y) => computerMoveCommand(x, y)));
38459
- computer.command("type <text>").description("Type a literal string into the focused field. Use `key` for key combos like ctrl+l.").option("--delay <ms>", "Per-character delay in ms (default 12 \u2248 80 wpm)").action(wrap((text, options) => computerTypeCommand(text, options)));
38460
- computer.command("key <combo>").description("Press a key combo, e.g. Return, Escape, ctrl+l, ctrl+shift+t. Same syntax as xdotool key.").action(wrap((combo) => computerKeyCommand(combo)));
38461
- computer.command("scroll <direction>").description("Scroll up | down | left | right. Optionally provide --x / --y to hover before scrolling.").option("--amount <n>", "Wheel ticks (default 3)").option("--x <x>", "Hover x before scrolling. Pixels or percentage.").option("--y <y>", "Hover y before scrolling. Pixels or percentage.").action(wrap((direction, options) => computerScrollCommand(direction, options)));
38462
- computer.command("drag <fromX> <fromY> <toX> <toY>").description("Press the left mouse button at (fromX, fromY), drag to (toX, toY), release. Coordinates are pixels or percentages.").action(wrap((fx, fy, tx, ty) => computerDragCommand(fx, fy, tx, ty)));
38463
- computer.command("launch <app> [args...]").description("Launch an app on the workspace display. Aliases: chrome, chromium, firefox, terminal.").action(wrap((app, args) => computerLaunchCommand(app, args)));
38464
- const record2 = computer.command("record").description("Screen-record the workspace display to an MP4 (60fps libx264).");
38465
- record2.command("start <path>").description("Start recording to <path>. Output is fragmented MP4 (safe if the workspace dies mid-record).").option("--fps <n>", "Frame rate (default 60)").action(wrap((path6, options) => computerRecordStartCommand(path6, options)));
38466
- record2.command("stop").description("Stop the active recording and finalize the MP4. Prints the output path.").action(wrap(() => computerRecordStopCommand()));
35556
+ program.command("computer").description("Drive the workspace Linux desktop through replicas-computer");
38467
35557
  const allowed = /* @__PURE__ */ new Set([
38468
35558
  "init",
38469
35559
  "whoami",
@@ -38486,6 +35576,18 @@ if (isAgentMode()) {
38486
35576
  cmds.push(...kept);
38487
35577
  }
38488
35578
  async function main() {
35579
+ if (process.argv[2] === "computer" && isAgentMode()) {
35580
+ const result = spawnSync("replicas-computer", process.argv.slice(3), { stdio: "inherit" });
35581
+ if (result.error) {
35582
+ console.error(chalk23.red(`
35583
+ \u2717 replicas-computer is unavailable in this workspace image
35584
+ `));
35585
+ process.exitCode = 1;
35586
+ return;
35587
+ }
35588
+ process.exitCode = result.status ?? 1;
35589
+ return;
35590
+ }
38489
35591
  startUpdateCheck(CLI_VERSION);
38490
35592
  program.parse();
38491
35593
  }