replicas-cli 0.2.327 → 0.2.329

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 +738 -146
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -8086,7 +8086,7 @@ Anything else gets \`exec\`'d verbatim, so \`replicas computer launch xeyes\` wo
8086
8086
  When opening a known page, prefer passing the URL directly: \`replicas computer launch chrome http://localhost:3000/\`.
8087
8087
 
8088
8088
  ### \`replicas computer record start <path> [--fps N]\`
8089
- Starts an ffmpeg screen recorder. Output is a fragmented MP4 (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
8089
+ Starts an ffmpeg screen recorder. Output is post-processed when stopped: action windows stay at normal speed, idle gaps accelerate, click moments get eased camera zoom, and a synthetic cursor animates between logged mouse positions. The raw capture is fragmented MP4 while active (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
8090
8090
 
8091
8091
  Only one recording at a time. Re-running \`start\` while one is active fails - call \`stop\` first.
8092
8092
 
@@ -9307,7 +9307,7 @@ replicas automation create "Review my MRs" \\
9307
9307
  replicas automation create ... --disabled
9308
9308
 
9309
9309
  # Workspace lifecycle
9310
- replicas automation create ... --lifecycle delete_when_done
9310
+ replicas automation create ... --lifecycle archive_when_done
9311
9311
  replicas automation create ... --lifecycle sleep_when_done
9312
9312
  replicas automation create ... --lifecycle delete_after_inactivity --auto-stop-minutes 30
9313
9313
  \`\`\`
@@ -9537,7 +9537,7 @@ var HOOK_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
9537
9537
  var REPLICAS_CONFIG_FILENAMES = ["replicas.json", "replicas.yaml", "replicas.yml"];
9538
9538
 
9539
9539
  // ../shared/src/cli-version.ts
9540
- var CLI_VERSION = "0.2.327";
9540
+ var CLI_VERSION = "0.2.329";
9541
9541
 
9542
9542
  // ../shared/src/engine/environment.ts
9543
9543
  var DESKTOP_NOVNC_PORT = 6080;
@@ -9562,7 +9562,7 @@ var WORKSPACE_STATUSES = ["active", "sleeping", "archived", "preparing", "error"
9562
9562
  function isWorkspaceSuspendedStatus(status) {
9563
9563
  return status === "sleeping" || status === "archived";
9564
9564
  }
9565
- var VALID_LIFECYCLE_POLICIES = ["default", "delete_when_done", "sleep_when_done", "delete_after_inactivity"];
9565
+ var VALID_LIFECYCLE_POLICIES = ["default", "archive_when_done", "sleep_when_done", "delete_after_inactivity"];
9566
9566
  function workspaceConfigWithCapabilities(config2, capabilities = {}) {
9567
9567
  const mergedCapabilities = {
9568
9568
  ...config2?.capabilities,
@@ -13658,8 +13658,7 @@ function printAutomation(automation2) {
13658
13658
  console.log(chalk18.gray(` Next Run: ${formatDate2(automation2.cron_next_fire_at)}`));
13659
13659
  }
13660
13660
  if (automation2.workspace_lifecycle_policy && automation2.workspace_lifecycle_policy !== "default") {
13661
- const lifecycle = automation2.workspace_lifecycle_policy === "delete_after_inactivity" ? `delete_after_inactivity (${automation2.workspace_auto_stop_minutes ?? 30}m)` : automation2.workspace_lifecycle_policy;
13662
- console.log(chalk18.gray(` Lifecycle: ${lifecycle}`));
13661
+ console.log(chalk18.gray(` Lifecycle: ${automation2.workspace_lifecycle_policy}`));
13663
13662
  }
13664
13663
  if (automation2.agent_provider || automation2.model || automation2.thinking_level) {
13665
13664
  console.log(chalk18.gray(` Agent: ${formatAgentSummary(automation2)}`));
@@ -14482,12 +14481,17 @@ async function mediaListCommand(options) {
14482
14481
  }
14483
14482
  }
14484
14483
 
14485
- // src/commands/computer.ts
14486
- import { spawn as spawn3, spawnSync } from "child_process";
14484
+ // src/commands/computer/index.ts
14485
+ import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
14487
14486
  import { createHash as createHash2 } from "crypto";
14488
- import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "fs";
14489
- import { dirname, isAbsolute, resolve } from "path";
14487
+ import { closeSync, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
14488
+ import { dirname as dirname2 } from "path";
14490
14489
  import chalk20 from "chalk";
14490
+
14491
+ // src/commands/computer/desktop.ts
14492
+ import { spawnSync } from "child_process";
14493
+ import { existsSync } from "fs";
14494
+ import { isAbsolute, resolve } from "path";
14491
14495
  var STATE_DIR = process.env.REPLICAS_DESKTOP_STATE_DIR || "/tmp/replicas-computer";
14492
14496
  var DEFAULT_DISPLAY = process.env.REPLICAS_DESKTOP_DISPLAY || ":99";
14493
14497
  var NOVNC_PORT = process.env.REPLICAS_DESKTOP_NOVNC_PORT ? parseInt(process.env.REPLICAS_DESKTOP_NOVNC_PORT, 10) : DESKTOP_NOVNC_PORT;
@@ -14548,6 +14552,23 @@ function getDisplayDimensions() {
14548
14552
  if (!match) fail("could not read display dimensions from xdpyinfo");
14549
14553
  return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
14550
14554
  }
14555
+ function getMouseLocation() {
14556
+ const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
14557
+ if (!out) return null;
14558
+ const values = Object.fromEntries(
14559
+ out.split("\n").map((line) => {
14560
+ const [key, value] = line.split("=");
14561
+ return [key.toLowerCase(), Number.parseInt(value, 10)];
14562
+ })
14563
+ );
14564
+ if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
14565
+ return {
14566
+ x: values.x,
14567
+ y: values.y,
14568
+ screen: Number.isFinite(values.screen) ? values.screen : 0,
14569
+ window: Number.isFinite(values.window) ? values.window : 0
14570
+ };
14571
+ }
14551
14572
  function parseScreenCoord(value, label, size) {
14552
14573
  if (value.endsWith("%")) {
14553
14574
  const rawPct = value.slice(0, -1);
@@ -14563,8 +14584,655 @@ function parseScreenCoord(value, label, size) {
14563
14584
  function resolvePath(p) {
14564
14585
  return isAbsolute(p) ? p : resolve(process.cwd(), p);
14565
14586
  }
14587
+ function configuredDesktopDimensions() {
14588
+ const width = parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
14589
+ const height = parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
14590
+ if (!Number.isFinite(width) || width <= 0) fail(`REPLICAS_DESKTOP_WIDTH must be a positive integer`);
14591
+ if (!Number.isFinite(height) || height <= 0) fail(`REPLICAS_DESKTOP_HEIGHT must be a positive integer`);
14592
+ return { width, height };
14593
+ }
14594
+ function clamp(n, min, max) {
14595
+ return Math.min(max, Math.max(min, n));
14596
+ }
14597
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
14598
+
14599
+ // src/commands/computer/recording.ts
14600
+ import { spawn as spawn3 } from "child_process";
14601
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
14602
+ import { dirname } from "path";
14603
+
14604
+ // src/commands/computer/recording/render.ts
14605
+ import { spawnSync as spawnSync2 } from "child_process";
14606
+ import { copyFileSync, rmSync, writeFileSync } from "fs";
14607
+
14608
+ // src/commands/computer/recording/config.ts
14609
+ var cameraMotion = {
14610
+ clickLeadSeconds: 1.2,
14611
+ clickTailSeconds: 0.5,
14612
+ actionLeadSeconds: 0.9,
14613
+ clickZoom: 1.24,
14614
+ closeClickOutputGapSeconds: 6,
14615
+ clickMoveSeconds: 1.2,
14616
+ returnSeconds: 1,
14617
+ minSegmentSeconds: 0.05
14618
+ };
14619
+ var cursorMotion = {
14620
+ minMoveSeconds: 0.38,
14621
+ maxMoveSeconds: 1.7,
14622
+ speedPxPerSecond: 700,
14623
+ defaultLeadSeconds: 0.16,
14624
+ clickShrinkSeconds: 0.08,
14625
+ clickGrowSeconds: 0.14,
14626
+ clickDwellGapStartSeconds: 0.75,
14627
+ clickDwellFullGapSeconds: 3.2,
14628
+ clickMinDwellSeconds: 0.12,
14629
+ clickMaxDwellSeconds: 1.15,
14630
+ maxDwellGapShare: 0.45,
14631
+ dragLeadSeconds: 0.55,
14632
+ maxKeyframes: 120
14633
+ };
14634
+ var cursorStyle = {
14635
+ size: 30,
14636
+ clickSize: 24
14637
+ };
14638
+
14639
+ // src/commands/computer/recording/ffmpeg-expressions.ts
14640
+ function expressionNumber(n) {
14641
+ return Number.isInteger(n) ? String(n) : n.toFixed(3);
14642
+ }
14643
+ function mixNumber(from, to, progress) {
14644
+ return from + (to - from) * progress;
14645
+ }
14646
+ function easeInOutSmootherNumber(progress) {
14647
+ const p = clamp(progress, 0, 1);
14648
+ return p * p * p * (p * (p * 6 - 15) + 10);
14649
+ }
14650
+ function easeInOutSmootherExpression(progress) {
14651
+ const p = `(${progress})`;
14652
+ return `${p}*${p}*${p}*(${p}*(${p}*6-15)+10)`;
14653
+ }
14654
+ function progressOverTime(timeExpression, seconds) {
14655
+ return easeInOutSmootherExpression(`min(max((${timeExpression})/${seconds.toFixed(3)}\\,0)\\,1)`);
14656
+ }
14657
+ function progressBetween(start, duration) {
14658
+ return easeInOutSmootherExpression(`min(max((t-${start.toFixed(3)})/${duration.toFixed(3)}\\,0)\\,1)`);
14659
+ }
14660
+ function mixExpression(from, to, progress) {
14661
+ return `${expressionNumber(from)}+(${expressionNumber(to - from)})*${progress}`;
14662
+ }
14663
+ function cropOrigin(point, zoom, size) {
14664
+ return clamp(point * zoom - size / 2, 0, size * zoom - size);
14665
+ }
14666
+ function easedCropOrigin(fromPoint, toPoint, fromZoom, toZoom, size, progress) {
14667
+ return mixExpression(
14668
+ cropOrigin(fromPoint, fromZoom, size),
14669
+ cropOrigin(toPoint, toZoom, size),
14670
+ progress
14671
+ );
14672
+ }
14673
+ function betweenExpression(start, end) {
14674
+ return `between(t\\,${start.toFixed(3)}\\,${end.toFixed(3)})`;
14675
+ }
14676
+
14677
+ // src/commands/computer/recording/timeline.ts
14678
+ function centerOf(size) {
14679
+ return { x: size.width / 2, y: size.height / 2 };
14680
+ }
14681
+ function actionPoint(action, size) {
14682
+ const x = action.toX ?? action.x ?? size.width / 2;
14683
+ const y = action.toY ?? action.y ?? size.height / 2;
14684
+ return { x: clamp(x, 0, size.width), y: clamp(y, 0, size.height) };
14685
+ }
14686
+ function idleSpeed(duration) {
14687
+ if (duration < 1.2) return 1;
14688
+ return clamp(1 + Math.log1p(duration - 0.8) * 1.8, 1.5, 5);
14689
+ }
14690
+ function renderedGapSeconds(duration) {
14691
+ return duration / idleSpeed(duration);
14692
+ }
14693
+ function timelineActions(actions) {
14694
+ return actions.filter((action) => action.type !== "key" && action.type !== "move");
14695
+ }
14696
+ function actionWindow(action, duration) {
14697
+ const shouldZoom = action.type === "click";
14698
+ const at = clamp(action.atMs / 1e3, 0, duration);
14699
+ const start = clamp(at - (shouldZoom ? cameraMotion.clickLeadSeconds : cameraMotion.actionLeadSeconds), 0, duration);
14700
+ const tail = shouldZoom ? cameraMotion.clickTailSeconds : action.type === "type" ? 1.5 : 0.9;
14701
+ return { start, end: clamp(at + tail, start, duration), shouldZoom };
14702
+ }
14703
+ function nextClickStart(actions, index, duration) {
14704
+ const nextClick = actions.slice(index + 1).find((candidate) => candidate.type === "click");
14705
+ return nextClick ? clamp(nextClick.atMs / 1e3 - cameraMotion.clickLeadSeconds, 0, duration) : null;
14706
+ }
14707
+ function shouldPreserveZoom(nextStart, cursor, lastZoom) {
14708
+ return lastZoom > 1 && nextStart !== null && nextStart > cursor && renderedGapSeconds(nextStart - cursor) <= cameraMotion.closeClickOutputGapSeconds;
14709
+ }
14710
+ function fullFrameSegment(duration, size) {
14711
+ const center = centerOf(size);
14712
+ return {
14713
+ start: 0,
14714
+ end: duration,
14715
+ speed: 1,
14716
+ motionSeconds: cameraMotion.returnSeconds,
14717
+ fromZoom: 1,
14718
+ zoom: 1,
14719
+ fromX: center.x,
14720
+ fromY: center.y,
14721
+ ...center
14722
+ };
14723
+ }
14724
+ function buildRecordingSegments(actions, duration, size) {
14725
+ const actionsForTimeline = timelineActions(actions);
14726
+ if (actionsForTimeline.length === 0) return [fullFrameSegment(duration, size)];
14727
+ const center = centerOf(size);
14728
+ const segments = [];
14729
+ let cursor = 0;
14730
+ let lastPoint = center;
14731
+ let lastZoom = 1;
14732
+ for (let i = 0; i < actionsForTimeline.length; i++) {
14733
+ const action = actionsForTimeline[i];
14734
+ const { start, end, shouldZoom } = actionWindow(action, duration);
14735
+ const preserveZoom = !shouldZoom && shouldPreserveZoom(nextClickStart(actionsForTimeline, i, duration), cursor, lastZoom);
14736
+ const point = shouldZoom ? actionPoint(action, size) : preserveZoom ? lastPoint : center;
14737
+ const zoom = shouldZoom ? cameraMotion.clickZoom : preserveZoom ? lastZoom : 1;
14738
+ if (start > cursor + cameraMotion.minSegmentSeconds) {
14739
+ const gap = start - cursor;
14740
+ const speed = idleSpeed(gap);
14741
+ const bridgeZoom = shouldZoom && lastZoom > 1 && renderedGapSeconds(gap) <= cameraMotion.closeClickOutputGapSeconds;
14742
+ segments.push({
14743
+ start: cursor,
14744
+ end: start,
14745
+ speed,
14746
+ motionSeconds: bridgeZoom ? Math.min(cameraMotion.clickMoveSeconds, Math.max(0.18, gap / speed)) : cameraMotion.returnSeconds,
14747
+ fromZoom: lastZoom,
14748
+ zoom: bridgeZoom ? cameraMotion.clickZoom : zoom,
14749
+ fromX: lastPoint.x,
14750
+ fromY: lastPoint.y,
14751
+ ...point
14752
+ });
14753
+ lastZoom = bridgeZoom ? cameraMotion.clickZoom : zoom;
14754
+ lastPoint = point;
14755
+ }
14756
+ const actionStart = Math.max(cursor, start);
14757
+ if (end > actionStart + cameraMotion.minSegmentSeconds) {
14758
+ segments.push({
14759
+ start: actionStart,
14760
+ end,
14761
+ speed: 1,
14762
+ motionSeconds: shouldZoom ? cameraMotion.clickMoveSeconds : cameraMotion.returnSeconds,
14763
+ fromZoom: lastZoom,
14764
+ zoom,
14765
+ fromX: lastPoint.x,
14766
+ fromY: lastPoint.y,
14767
+ ...point
14768
+ });
14769
+ cursor = end;
14770
+ lastZoom = zoom;
14771
+ }
14772
+ lastPoint = point;
14773
+ }
14774
+ if (duration > cursor + cameraMotion.minSegmentSeconds) {
14775
+ segments.push({
14776
+ start: cursor,
14777
+ end: duration,
14778
+ speed: idleSpeed(duration - cursor),
14779
+ motionSeconds: cameraMotion.returnSeconds,
14780
+ fromZoom: lastZoom,
14781
+ zoom: 1,
14782
+ fromX: lastPoint.x,
14783
+ fromY: lastPoint.y,
14784
+ ...center
14785
+ });
14786
+ }
14787
+ return segments.filter((segment) => segment.end > segment.start + cameraMotion.minSegmentSeconds);
14788
+ }
14789
+
14790
+ // src/commands/computer/recording/cursor.ts
14791
+ var cursorHotspotRatio = { x: 8 / 32, y: 4 / 32 };
14792
+ function cursorAssets(stamp) {
14793
+ return {
14794
+ path: `/tmp/replicas-cursor-${stamp}.svg`,
14795
+ size: cursorStyle.size
14796
+ };
14797
+ }
14798
+ function clickDwellSeconds(gapSeconds) {
14799
+ if (gapSeconds <= cursorMotion.clickDwellGapStartSeconds) return cursorMotion.defaultLeadSeconds;
14800
+ const progress = (gapSeconds - cursorMotion.clickDwellGapStartSeconds) / (cursorMotion.clickDwellFullGapSeconds - cursorMotion.clickDwellGapStartSeconds);
14801
+ const dwell = mixNumber(
14802
+ cursorMotion.clickMinDwellSeconds,
14803
+ cursorMotion.clickMaxDwellSeconds,
14804
+ easeInOutSmootherNumber(progress)
14805
+ );
14806
+ return Math.min(dwell, gapSeconds * cursorMotion.maxDwellGapShare);
14807
+ }
14808
+ function arrivalLeadSeconds(action, gapSeconds) {
14809
+ return action.type === "click" ? clickDwellSeconds(gapSeconds) : cursorMotion.defaultLeadSeconds;
14810
+ }
14811
+ function cursorKeyframe(action, at, previousAt, size) {
14812
+ if (typeof action.x !== "number" || typeof action.y !== "number") return null;
14813
+ const point = actionPoint(action, size);
14814
+ const lead = arrivalLeadSeconds(action, at - previousAt);
14815
+ return { ...point, at, arriveAt: clamp(at - lead, previousAt, at) };
14816
+ }
14817
+ function dedupeKeyframes(points) {
14818
+ return points.sort((a, b) => a.at - b.at).filter(
14819
+ (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
14820
+ );
14821
+ }
14822
+ function downsampleKeyframes(points, actions, size) {
14823
+ if (points.length <= cursorMotion.maxKeyframes) return points;
14824
+ const anchors = actions.filter((action) => action.type !== "move").map((action) => actionPoint(action, size));
14825
+ const result = points.filter(
14826
+ (point, index) => index === 0 || index === points.length - 1 || anchors.some((anchor) => Math.hypot(anchor.x - point.x, anchor.y - point.y) < 1)
14827
+ );
14828
+ const step = points.length / Math.max(1, cursorMotion.maxKeyframes - result.length);
14829
+ for (let i = 0; result.length < cursorMotion.maxKeyframes && i < points.length; i += step) {
14830
+ const point = points[Math.floor(i)];
14831
+ if (!result.includes(point)) result.push(point);
14832
+ }
14833
+ return result.sort((a, b) => a.at - b.at);
14834
+ }
14835
+ function cursorKeyframes(actions, duration, size) {
14836
+ const center = { x: size.width / 2, y: size.height / 2 };
14837
+ const points = [{ at: 0, arriveAt: 0, ...center }];
14838
+ for (const action of actions) {
14839
+ const at = clamp(action.atMs / 1e3, 0, duration);
14840
+ const previousAt = points[points.length - 1].at;
14841
+ if (action.type === "drag" && typeof action.x === "number" && typeof action.y === "number") {
14842
+ const startAt = clamp(at - cursorMotion.dragLeadSeconds, 0, duration);
14843
+ points.push({
14844
+ at: startAt,
14845
+ arriveAt: startAt,
14846
+ x: clamp(action.x, 0, size.width),
14847
+ y: clamp(action.y, 0, size.height)
14848
+ });
14849
+ if (typeof action.toX === "number" && typeof action.toY === "number") {
14850
+ points.push({
14851
+ at,
14852
+ arriveAt: at,
14853
+ x: clamp(action.toX, 0, size.width),
14854
+ y: clamp(action.toY, 0, size.height)
14855
+ });
14856
+ }
14857
+ continue;
14858
+ }
14859
+ const point = cursorKeyframe(action, at, previousAt, size);
14860
+ if (point) points.push(point);
14861
+ }
14862
+ return downsampleKeyframes(dedupeKeyframes(points), actions, size);
14863
+ }
14864
+ function cursorAxisExpression(points, axis) {
14865
+ if (points.length === 0) return "0";
14866
+ let expression = expressionNumber(points[points.length - 1][axis]);
14867
+ for (let i = points.length - 2; i >= 0; i--) {
14868
+ const from = points[i];
14869
+ const to = points[i + 1];
14870
+ const arriveAt = clamp(to.arriveAt, from.at, to.at);
14871
+ const moveWindow = Math.max(1e-3, arriveAt - from.at);
14872
+ const distance = Math.hypot(to.x - from.x, to.y - from.y);
14873
+ const moveSeconds = Math.min(
14874
+ moveWindow,
14875
+ Math.max(cursorMotion.minMoveSeconds, Math.min(cursorMotion.maxMoveSeconds, distance / cursorMotion.speedPxPerSecond))
14876
+ );
14877
+ const moveStart = Math.max(from.at, arriveAt - moveSeconds);
14878
+ const duration = Math.max(1e-3, arriveAt - moveStart);
14879
+ const interpolated = mixExpression(from[axis], to[axis], progressBetween(moveStart, duration));
14880
+ expression = `if(lte(t\\,${moveStart.toFixed(3)})\\,${expressionNumber(from[axis])}\\,if(lte(t\\,${to.at.toFixed(3)})\\,${interpolated}\\,${expression}))`;
14881
+ }
14882
+ return expression;
14883
+ }
14884
+ function cursorSizeExpression(actions, normalSize) {
14885
+ let expression = expressionNumber(normalSize);
14886
+ const clicks = actions.filter((action) => action.type === "click").sort((a, b) => b.atMs - a.atMs);
14887
+ for (const action of clicks) {
14888
+ const at = action.atMs / 1e3;
14889
+ const shrinkStart = Math.max(0, at - cursorMotion.clickShrinkSeconds);
14890
+ const shrink = mixExpression(
14891
+ normalSize,
14892
+ cursorStyle.clickSize,
14893
+ progressBetween(shrinkStart, Math.max(1e-3, at - shrinkStart))
14894
+ );
14895
+ const grow = mixExpression(cursorStyle.clickSize, normalSize, progressBetween(at, cursorMotion.clickGrowSeconds));
14896
+ expression = `if(${betweenExpression(shrinkStart, at)}\\,${shrink}\\,if(${betweenExpression(at, at + cursorMotion.clickGrowSeconds)}\\,${grow}\\,${expression}))`;
14897
+ }
14898
+ return expression;
14899
+ }
14900
+ function cursorOverlayPositionExpression(cursor, size, hotspotRatio, overlaySize) {
14901
+ return `max(0\\,min(${expressionNumber(size)}-${overlaySize}\\,(${cursor})-${hotspotRatio.toFixed(6)}*${overlaySize}))`;
14902
+ }
14903
+ function cursorSvg(size) {
14904
+ 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>`;
14905
+ }
14906
+ function buildCursorOverlayFilter(actions, duration, size, assets, projectCursor = null, inputLabel = "[screen]", outputLabel = "[v]") {
14907
+ const points = cursorKeyframes(actions, duration, size);
14908
+ const rawCursorX = cursorAxisExpression(points, "x");
14909
+ const rawCursorY = cursorAxisExpression(points, "y");
14910
+ const cursorX = projectCursor ? projectCursor.x(rawCursorX) : rawCursorX;
14911
+ const cursorY = projectCursor ? projectCursor.y(rawCursorY) : rawCursorY;
14912
+ const cursorSize = cursorSizeExpression(actions, assets.size);
14913
+ const cursorOverlayX = cursorOverlayPositionExpression(cursorX, size.width, cursorHotspotRatio.x, "overlay_w");
14914
+ const cursorOverlayY = cursorOverlayPositionExpression(cursorY, size.height, cursorHotspotRatio.y, "overlay_h");
14915
+ 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}`;
14916
+ }
14917
+
14918
+ // src/commands/computer/recording/render.ts
14919
+ function videoDuration(path6) {
14920
+ const r = spawnSync2("ffprobe", [
14921
+ "-v",
14922
+ "error",
14923
+ "-show_entries",
14924
+ "format=duration",
14925
+ "-of",
14926
+ "default=noprint_wrappers=1:nokey=1",
14927
+ path6
14928
+ ], { stdio: "pipe" });
14929
+ if (r.status !== 0) fail(`ffprobe failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
14930
+ const duration = Number.parseFloat(r.stdout?.toString().trim() ?? "");
14931
+ if (!Number.isFinite(duration) || duration <= 0) fail(`could not read recording duration for ${path6}`);
14932
+ return duration;
14933
+ }
14934
+ function renderedSegmentSpans(segments) {
14935
+ let outputStart = 0;
14936
+ return segments.map((segment) => {
14937
+ const outputEnd = outputStart + (segment.end - segment.start) / segment.speed;
14938
+ const span = {
14939
+ rawStart: segment.start,
14940
+ rawEnd: segment.end,
14941
+ outputStart,
14942
+ outputEnd,
14943
+ speed: segment.speed
14944
+ };
14945
+ outputStart = outputEnd;
14946
+ return span;
14947
+ });
14948
+ }
14949
+ function outputTimeForRawTime(spans, rawTime) {
14950
+ const span = spans.find((candidate) => rawTime >= candidate.rawStart && rawTime <= candidate.rawEnd);
14951
+ if (span) return span.outputStart + (rawTime - span.rawStart) / span.speed;
14952
+ if (spans.length === 0) return rawTime;
14953
+ if (rawTime <= spans[0].rawStart) return spans[0].outputStart;
14954
+ const lastSpan = spans[spans.length - 1];
14955
+ if (rawTime >= lastSpan.rawEnd) return lastSpan.outputEnd;
14956
+ return spans.find((candidate) => rawTime < candidate.rawStart)?.outputStart ?? lastSpan.outputEnd;
14957
+ }
14958
+ function actionsOnRenderedTimeline(actions, spans) {
14959
+ return actions.map((action) => ({
14960
+ ...action,
14961
+ atMs: outputTimeForRawTime(spans, action.atMs / 1e3) * 1e3
14962
+ }));
14963
+ }
14964
+ function segmentMotionSeconds(segment) {
14965
+ return Math.min(segment.motionSeconds, Math.max(0.12, (segment.end - segment.start) / segment.speed));
14966
+ }
14967
+ function scaledFrameClampExpression(value, zoom, outputSize) {
14968
+ return `max(0\\,min(${outputSize}*(${zoom})-${outputSize}\\,${value}))`;
14969
+ }
14970
+ function cameraTransformExpressions(segment, size, timeExpression) {
14971
+ const progress = progressOverTime(timeExpression, segmentMotionSeconds(segment));
14972
+ const zoom = mixExpression(segment.fromZoom, segment.zoom, progress);
14973
+ return {
14974
+ zoom,
14975
+ cropX: scaledFrameClampExpression(
14976
+ easedCropOrigin(segment.fromX, segment.x, segment.fromZoom, segment.zoom, size.width, progress),
14977
+ zoom,
14978
+ size.width
14979
+ ),
14980
+ cropY: scaledFrameClampExpression(
14981
+ easedCropOrigin(segment.fromY, segment.y, segment.fromZoom, segment.zoom, size.height, progress),
14982
+ zoom,
14983
+ size.height
14984
+ )
14985
+ };
14986
+ }
14987
+ function projectedCursorExpression(rawCursor, axis, segments, spans, size) {
14988
+ let expression = rawCursor;
14989
+ for (let i = segments.length - 1; i >= 0; i--) {
14990
+ const span = spans[i];
14991
+ const camera = cameraTransformExpressions(segments[i], size, `t-${span.outputStart.toFixed(3)}`);
14992
+ const crop = axis === "x" ? camera.cropX : camera.cropY;
14993
+ const projected = `(${rawCursor})*(${camera.zoom})-(${crop})`;
14994
+ expression = `if(${betweenExpression(span.outputStart, span.outputEnd)}\\,${projected}\\,${expression})`;
14995
+ }
14996
+ return expression;
14997
+ }
14998
+ function renderRecording(rawPath, target, actions, fps, size) {
14999
+ if (actions.length === 0) {
15000
+ copyFileSync(rawPath, target);
15001
+ return;
15002
+ }
15003
+ const duration = videoDuration(rawPath);
15004
+ const segments = buildRecordingSegments(actions, duration, size);
15005
+ if (segments.length === 0) {
15006
+ copyFileSync(rawPath, target);
15007
+ return;
15008
+ }
15009
+ const stamp = `${process.pid}-${Date.now()}`;
15010
+ const cursor = cursorAssets(stamp);
15011
+ writeFileSync(cursor.path, cursorSvg(cursor.size));
15012
+ const spans = renderedSegmentSpans(segments);
15013
+ const renderedDuration = spans.length ? spans[spans.length - 1].outputEnd : duration;
15014
+ const renderedActions = actionsOnRenderedTimeline(actions, spans);
15015
+ const cursorFilter = buildCursorOverlayFilter(
15016
+ renderedActions,
15017
+ renderedDuration,
15018
+ size,
15019
+ cursor,
15020
+ {
15021
+ x: (cursorX) => projectedCursorExpression(cursorX, "x", segments, spans, size),
15022
+ y: (cursorY) => projectedCursorExpression(cursorY, "y", segments, spans, size)
15023
+ }
15024
+ );
15025
+ const screenLabels = segments.map((_, index) => `[screen${index}]`).join("");
15026
+ const screenSplitFilter = `[0:v]split=${segments.length}${screenLabels}`;
15027
+ const filters = segments.map((segment, index) => {
15028
+ const { zoom, cropX, cropY } = cameraTransformExpressions(segment, size, "t");
15029
+ 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}]`;
15030
+ });
15031
+ const concatInputs = segments.map((_, index) => `[v${index}]`).join("");
15032
+ const filter = `${screenSplitFilter};${filters.join(";")};${concatInputs}concat=n=${segments.length}:v=1:a=0[screen];${cursorFilter}`;
15033
+ const filterPath = `/tmp/replicas-recording-filter-${stamp}.ffgraph`;
15034
+ writeFileSync(filterPath, filter);
15035
+ try {
15036
+ const r = spawnSync2("ffmpeg", [
15037
+ "-y",
15038
+ "-hide_banner",
15039
+ "-loglevel",
15040
+ "warning",
15041
+ "-i",
15042
+ rawPath,
15043
+ "-loop",
15044
+ "1",
15045
+ "-i",
15046
+ cursor.path,
15047
+ "-filter_complex_script",
15048
+ filterPath,
15049
+ "-map",
15050
+ "[v]",
15051
+ "-r",
15052
+ String(fps),
15053
+ "-c:v",
15054
+ "libx264",
15055
+ "-preset",
15056
+ "veryfast",
15057
+ "-crf",
15058
+ "23",
15059
+ "-pix_fmt",
15060
+ "yuv420p",
15061
+ "-movflags",
15062
+ "+faststart",
15063
+ target
15064
+ ], { stdio: "pipe", maxBuffer: 20 * 1024 * 1024 });
15065
+ if (r.status !== 0) {
15066
+ fail(`recording post-processing failed: ${r.error?.message || r.stderr?.toString().trim() || `exit ${r.status}`}`);
15067
+ }
15068
+ } finally {
15069
+ rmSync(filterPath, { force: true });
15070
+ rmSync(cursor.path, { force: true });
15071
+ }
15072
+ }
15073
+
15074
+ // src/commands/computer/recording.ts
15075
+ var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
15076
+ var RECORD_PATH_FILE = `${STATE_DIR}/recording-path.txt`;
15077
+ var RECORD_RAW_PATH_FILE = `${STATE_DIR}/recording-raw-path.txt`;
15078
+ var RECORD_STARTED_AT_FILE = `${STATE_DIR}/recording-started-at.txt`;
15079
+ var RECORD_FPS_FILE = `${STATE_DIR}/recording-fps.txt`;
15080
+ var RECORD_DIMENSIONS_FILE = `${STATE_DIR}/recording-dimensions.json`;
15081
+ var RECORD_ACTIONS_FILE = `${STATE_DIR}/recording-actions.jsonl`;
15082
+ function recordingStartedAt() {
15083
+ if (!existsSync2(RECORD_STARTED_AT_FILE)) return null;
15084
+ const startedAt = Number.parseInt(readFileSync(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
15085
+ return Number.isFinite(startedAt) ? startedAt : null;
15086
+ }
15087
+ function logRecordingAction(action) {
15088
+ const startedAt = recordingStartedAt();
15089
+ if (!startedAt) return;
15090
+ const atMs = Date.now() - startedAt;
15091
+ appendFileSync(RECORD_ACTIONS_FILE, `${JSON.stringify({ ...action, atMs })}
15092
+ `);
15093
+ }
15094
+ function readRecordingDimensions() {
15095
+ if (!existsSync2(RECORD_DIMENSIONS_FILE)) return configuredDesktopDimensions();
15096
+ try {
15097
+ const dimensions = JSON.parse(readFileSync(RECORD_DIMENSIONS_FILE, "utf8"));
15098
+ const width = dimensions?.width;
15099
+ const height = dimensions?.height;
15100
+ if (typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0) {
15101
+ return { width, height };
15102
+ }
15103
+ } catch {
15104
+ }
15105
+ fail("invalid recording dimensions state");
15106
+ }
15107
+ function isRecordingActionType(value) {
15108
+ return value === "click" || value === "drag" || value === "key" || value === "move" || value === "scroll" || value === "type";
15109
+ }
15110
+ function isOptionalNumber(value) {
15111
+ return value === void 0 || typeof value === "number";
15112
+ }
15113
+ function readRecordingActions() {
15114
+ if (!existsSync2(RECORD_ACTIONS_FILE)) return [];
15115
+ return readFileSync(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
15116
+ try {
15117
+ const value = JSON.parse(line);
15118
+ if (typeof value !== "object" || value === null) return [];
15119
+ const type = "type" in value ? value.type : void 0;
15120
+ const atMs = "atMs" in value ? value.atMs : void 0;
15121
+ const x = "x" in value ? value.x : void 0;
15122
+ const y = "y" in value ? value.y : void 0;
15123
+ const toX = "toX" in value ? value.toX : void 0;
15124
+ const toY = "toY" in value ? value.toY : void 0;
15125
+ if (!isRecordingActionType(type) || typeof atMs !== "number" || !Number.isFinite(atMs) || !isOptionalNumber(x) || !isOptionalNumber(y) || !isOptionalNumber(toX) || !isOptionalNumber(toY)) {
15126
+ return [];
15127
+ }
15128
+ return [{ type, atMs, x, y, toX, toY }];
15129
+ } catch {
15130
+ return [];
15131
+ }
15132
+ }).sort((a, b) => a.atMs - b.atMs);
15133
+ }
15134
+ async function computerRecordStartCommand(path6, options) {
15135
+ ensureServicesRunning();
15136
+ if (existsSync2(RECORD_PID_FILE)) {
15137
+ const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
15138
+ if (Number.isFinite(pid)) {
15139
+ let alive = false;
15140
+ try {
15141
+ process.kill(pid, 0);
15142
+ alive = true;
15143
+ } catch {
15144
+ }
15145
+ if (alive) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
15146
+ }
15147
+ }
15148
+ const target = resolvePath(path6);
15149
+ mkdirSync(dirname(target), { recursive: true });
15150
+ const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
15151
+ const { width, height } = configuredDesktopDimensions();
15152
+ mkdirSync(STATE_DIR, { recursive: true });
15153
+ const rawTarget = `${target}.raw-${Date.now()}.mp4`;
15154
+ rmSync2(RECORD_ACTIONS_FILE, { force: true });
15155
+ const child = spawn3("ffmpeg", [
15156
+ "-y",
15157
+ "-hide_banner",
15158
+ "-loglevel",
15159
+ "warning",
15160
+ "-f",
15161
+ "x11grab",
15162
+ "-framerate",
15163
+ String(fps),
15164
+ "-video_size",
15165
+ `${width}x${height}`,
15166
+ "-draw_mouse",
15167
+ "0",
15168
+ "-i",
15169
+ DEFAULT_DISPLAY,
15170
+ "-c:v",
15171
+ "libx264",
15172
+ "-preset",
15173
+ "ultrafast",
15174
+ "-tune",
15175
+ "zerolatency",
15176
+ "-crf",
15177
+ "23",
15178
+ "-pix_fmt",
15179
+ "yuv420p",
15180
+ "-movflags",
15181
+ "+faststart+frag_keyframe+empty_moov",
15182
+ rawTarget
15183
+ ], { detached: true, stdio: "ignore" });
15184
+ child.unref();
15185
+ if (!child.pid) fail("failed to launch ffmpeg");
15186
+ writeFileSync2(RECORD_PID_FILE, String(child.pid));
15187
+ writeFileSync2(RECORD_PATH_FILE, target);
15188
+ writeFileSync2(RECORD_RAW_PATH_FILE, rawTarget);
15189
+ writeFileSync2(RECORD_STARTED_AT_FILE, String(Date.now()));
15190
+ writeFileSync2(RECORD_FPS_FILE, String(fps));
15191
+ writeFileSync2(RECORD_DIMENSIONS_FILE, JSON.stringify({ width, height }));
15192
+ console.log(target);
15193
+ }
15194
+ async function computerRecordStopCommand() {
15195
+ if (!existsSync2(RECORD_PID_FILE) && !existsSync2(RECORD_PATH_FILE)) fail("no recording in progress");
15196
+ if (existsSync2(RECORD_PID_FILE)) {
15197
+ const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
15198
+ if (!Number.isFinite(pid)) fail("invalid recording pidfile");
15199
+ try {
15200
+ process.kill(pid, "SIGINT");
15201
+ } catch {
15202
+ }
15203
+ for (let i = 0; i < 30; i++) {
15204
+ try {
15205
+ process.kill(pid, 0);
15206
+ } catch {
15207
+ break;
15208
+ }
15209
+ await new Promise((r) => setTimeout(r, 200));
15210
+ }
15211
+ rmSync2(RECORD_PID_FILE, { force: true });
15212
+ }
15213
+ if (existsSync2(RECORD_PATH_FILE)) {
15214
+ const target = readFileSync(RECORD_PATH_FILE, "utf8").trim();
15215
+ const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
15216
+ const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
15217
+ const size = readRecordingDimensions();
15218
+ const actions = readRecordingActions();
15219
+ if (rawPath !== target) {
15220
+ renderRecording(rawPath, target, actions, Number.isFinite(fps) ? fps : 60, size);
15221
+ rmSync2(rawPath, { force: true });
15222
+ }
15223
+ console.log(target);
15224
+ rmSync2(RECORD_PATH_FILE, { force: true });
15225
+ rmSync2(RECORD_RAW_PATH_FILE, { force: true });
15226
+ rmSync2(RECORD_STARTED_AT_FILE, { force: true });
15227
+ rmSync2(RECORD_FPS_FILE, { force: true });
15228
+ rmSync2(RECORD_DIMENSIONS_FILE, { force: true });
15229
+ rmSync2(RECORD_ACTIONS_FILE, { force: true });
15230
+ }
15231
+ }
15232
+
15233
+ // src/commands/computer/index.ts
14566
15234
  function desktopBridgeStatus() {
14567
- const r = spawnSync("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
15235
+ const r = spawnSync3("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
14568
15236
  if (r.status !== 0) return null;
14569
15237
  try {
14570
15238
  return JSON.parse(r.stdout.toString());
@@ -14581,7 +15249,6 @@ function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
14581
15249
  if (pids.length > 0) parts.push(`pid${pids.length === 1 ? "" : "s"} ${pids.join(",")}`);
14582
15250
  return ` (${parts.join(", ")})`;
14583
15251
  }
14584
- var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
14585
15252
  async function lookupDesktopViewerUrl() {
14586
15253
  try {
14587
15254
  const list = await listAgentPreviews();
@@ -14615,7 +15282,7 @@ async function computerStatusCommand() {
14615
15282
  const bridge = desktopBridgeStatus();
14616
15283
  const procs = ["Xvfb", "openbox", "tint2", "x11vnc", "websockify"];
14617
15284
  for (const p of procs) {
14618
- const r = spawnSync("pgrep", ["-af", p], { stdio: "pipe" });
15285
+ const r = spawnSync3("pgrep", ["-af", p], { stdio: "pipe" });
14619
15286
  const running = r.status === 0 && !!r.stdout?.toString().trim();
14620
15287
  const suffix = p === "x11vnc" && bridge ? bridgeStatus(bridge.x11vnc, true) : p === "websockify" && bridge ? bridgeStatus(bridge.websockify, false, true) : "";
14621
15288
  console.log(` ${running ? chalk20.green("\u25CF") : chalk20.red("\u25CB")} ${p}${suffix}`);
@@ -14647,12 +15314,12 @@ function brandSvgPath() {
14647
15314
  }
14648
15315
  function loadBrandSvg(canvasW, canvasH) {
14649
15316
  const path6 = brandSvgPath();
14650
- if (!existsSync(path6)) {
15317
+ if (!existsSync3(path6)) {
14651
15318
  fail(
14652
15319
  `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.`
14653
15320
  );
14654
15321
  }
14655
- return readFileSync(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
15322
+ return readFileSync2(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
14656
15323
  }
14657
15324
  var BRAND_PAD_FRACTION = 0.06;
14658
15325
  var SCREENSHOT_CORNER_FRACTION = 0.022;
@@ -14687,8 +15354,8 @@ ${labels.join("\n")}
14687
15354
  </svg>`;
14688
15355
  }
14689
15356
  function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
14690
- writeFileSync(gridPath, buildGridSvg(width, height, gridSize));
14691
- const r = spawnSync(
15357
+ writeFileSync3(gridPath, buildGridSvg(width, height, gridSize));
15358
+ const r = spawnSync3(
14692
15359
  "ffmpeg",
14693
15360
  [
14694
15361
  "-y",
@@ -14715,7 +15382,7 @@ function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
14715
15382
  }
14716
15383
  async function computerScreenshotCommand(path6, options = {}) {
14717
15384
  const target = resolvePath(path6);
14718
- mkdirSync(dirname(target), { recursive: true });
15385
+ mkdirSync2(dirname2(target), { recursive: true });
14719
15386
  const stamp = `${process.pid}-${Date.now()}`;
14720
15387
  const rawPath = `/tmp/replicas-screenshot-${stamp}.raw.png`;
14721
15388
  const svgPath = `/tmp/replicas-screenshot-${stamp}.brand.svg`;
@@ -14727,7 +15394,7 @@ async function computerScreenshotCommand(path6, options = {}) {
14727
15394
  const { width, height } = readPngDimensions(rawPath);
14728
15395
  const gridSize = parseGridSize(options.grid);
14729
15396
  if (options.raw && gridSize === null) {
14730
- copyFileSync(rawPath, target);
15397
+ copyFileSync2(rawPath, target);
14731
15398
  console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels)`);
14732
15399
  return;
14733
15400
  }
@@ -14747,16 +15414,16 @@ async function computerScreenshotCommand(path6, options = {}) {
14747
15414
  const shadowMargin = shadowSigma * 3;
14748
15415
  const shadowW = width + shadowMargin * 2;
14749
15416
  const shadowH = height + shadowMargin * 2;
14750
- writeFileSync(svgPath, loadBrandSvg(canvasW, canvasH));
14751
- writeFileSync(
15417
+ writeFileSync3(svgPath, loadBrandSvg(canvasW, canvasH));
15418
+ writeFileSync3(
14752
15419
  maskPath,
14753
15420
  SCREENSHOT_MASK_TEMPLATE.replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
14754
15421
  );
14755
- writeFileSync(
15422
+ writeFileSync3(
14756
15423
  shadowPath,
14757
15424
  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))
14758
15425
  );
14759
- const r = spawnSync(
15426
+ const r = spawnSync3(
14760
15427
  "ffmpeg",
14761
15428
  [
14762
15429
  "-y",
@@ -14785,16 +15452,16 @@ async function computerScreenshotCommand(path6, options = {}) {
14785
15452
  fail(`ffmpeg branding failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
14786
15453
  }
14787
15454
  } finally {
14788
- rmSync(rawPath, { force: true });
14789
- rmSync(svgPath, { force: true });
14790
- rmSync(maskPath, { force: true });
14791
- rmSync(shadowPath, { force: true });
14792
- rmSync(gridPath, { force: true });
15455
+ rmSync3(rawPath, { force: true });
15456
+ rmSync3(svgPath, { force: true });
15457
+ rmSync3(maskPath, { force: true });
15458
+ rmSync3(shadowPath, { force: true });
15459
+ rmSync3(gridPath, { force: true });
14793
15460
  }
14794
15461
  console.log(target);
14795
15462
  }
14796
15463
  function hashFile(path6) {
14797
- return createHash2("sha256").update(readFileSync(path6)).digest("hex");
15464
+ return createHash2("sha256").update(readFileSync2(path6)).digest("hex");
14798
15465
  }
14799
15466
  async function captureStableRawScreenshot(target, options) {
14800
15467
  const start = Date.now();
@@ -14821,7 +15488,7 @@ async function captureStableRawScreenshot(target, options) {
14821
15488
  lastChangeAt = now;
14822
15489
  }
14823
15490
  lastHash = hash;
14824
- copyFileSync(framePath, target);
15491
+ copyFileSync2(framePath, target);
14825
15492
  if (frames > 1 && now - lastChangeAt >= options.stableMs) {
14826
15493
  return { width, height, stable: true, elapsedMs: now - start, frames, changes };
14827
15494
  }
@@ -14829,26 +15496,9 @@ async function captureStableRawScreenshot(target, options) {
14829
15496
  }
14830
15497
  return { width, height, stable: false, elapsedMs: Date.now() - start, frames, changes };
14831
15498
  } finally {
14832
- rmSync(framePath, { force: true });
15499
+ rmSync3(framePath, { force: true });
14833
15500
  }
14834
15501
  }
14835
- function getMouseLocation() {
14836
- const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
14837
- if (!out) return null;
14838
- const values = Object.fromEntries(
14839
- out.split("\n").map((line) => {
14840
- const [key, value] = line.split("=");
14841
- return [key.toLowerCase(), Number.parseInt(value, 10)];
14842
- })
14843
- );
14844
- if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
14845
- return {
14846
- x: values.x,
14847
- y: values.y,
14848
- screen: Number.isFinite(values.screen) ? values.screen : 0,
14849
- window: Number.isFinite(values.window) ? values.window : 0
14850
- };
14851
- }
14852
15502
  function getActiveWindowTitle() {
14853
15503
  return tryDisplayCmd("xdotool", ["getactivewindow", "getwindowname"]);
14854
15504
  }
@@ -14856,9 +15506,13 @@ function getVisibleWindowTitles() {
14856
15506
  const out = tryDisplayCmd("xdotool", ["search", "--onlyvisible", "--name", ".", "getwindowname", "%@"]);
14857
15507
  return out ? out.split("\n").filter(Boolean).slice(0, 20) : [];
14858
15508
  }
15509
+ function recordingMousePosition() {
15510
+ const mouse = getMouseLocation();
15511
+ return mouse ? { x: mouse.x, y: mouse.y } : null;
15512
+ }
14859
15513
  async function computerObserveCommand(path6, options = {}) {
14860
15514
  const target = resolvePath(path6);
14861
- mkdirSync(dirname(target), { recursive: true });
15515
+ mkdirSync2(dirname2(target), { recursive: true });
14862
15516
  const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 3e3;
14863
15517
  const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 600;
14864
15518
  const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 200;
@@ -14872,7 +15526,7 @@ async function computerObserveCommand(path6, options = {}) {
14872
15526
  const capture = await captureStableRawScreenshot(rawPath, { timeoutMs, stableMs, pollMs });
14873
15527
  const gridSize = options.raw ? null : parseGridSize(options.grid ?? true);
14874
15528
  if (gridSize === null) {
14875
- copyFileSync(rawPath, target);
15529
+ copyFileSync2(rawPath, target);
14876
15530
  } else {
14877
15531
  overlayGrid(rawPath, target, capture.width, capture.height, gridSize, gridPath);
14878
15532
  }
@@ -14891,8 +15545,8 @@ async function computerObserveCommand(path6, options = {}) {
14891
15545
  gridSize
14892
15546
  }, null, 2));
14893
15547
  } finally {
14894
- rmSync(rawPath, { force: true });
14895
- rmSync(gridPath, { force: true });
15548
+ rmSync3(rawPath, { force: true });
15549
+ rmSync3(gridPath, { force: true });
14896
15550
  }
14897
15551
  }
14898
15552
  async function fetchChromeJson(path6) {
@@ -15018,7 +15672,7 @@ async function evaluateChromeTarget(webSocketDebuggerUrl, expression) {
15018
15672
  }));
15019
15673
  });
15020
15674
  ws.addEventListener("message", (event) => {
15021
- const data = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf8");
15675
+ const data = typeof event.data === "string" ? event.data : Buffer.isBuffer(event.data) ? event.data.toString("utf8") : Buffer.from(event.data).toString("utf8");
15022
15676
  let parsed;
15023
15677
  try {
15024
15678
  parsed = JSON.parse(data);
@@ -15343,6 +15997,7 @@ async function computerClickCommand(xStr, yStr, options) {
15343
15997
  }
15344
15998
  }
15345
15999
  runDisplayCmd("xdotool", args);
16000
+ logRecordingAction({ type: "click", x, y });
15346
16001
  console.log(`clicked ${button === "1" ? "left" : button === "2" ? "middle" : button === "3" ? "right" : `button ${button}`} at (${x},${y})${options.double ? " x2" : ""}`);
15347
16002
  }
15348
16003
  async function computerMoveCommand(xStr, yStr) {
@@ -15350,15 +16005,20 @@ async function computerMoveCommand(xStr, yStr) {
15350
16005
  const x = parseScreenCoord(xStr, "x", dimensions.width);
15351
16006
  const y = parseScreenCoord(yStr, "y", dimensions.height);
15352
16007
  runDisplayCmd("xdotool", ["mousemove", "--sync", String(x), String(y)]);
16008
+ logRecordingAction({ type: "move", x, y });
15353
16009
  console.log(`moved to (${x},${y})`);
15354
16010
  }
15355
16011
  async function computerTypeCommand(text, options) {
15356
16012
  const delay = options.delay ? parseCoord(options.delay, "--delay") : 12;
15357
16013
  runDisplayCmd("xdotool", ["type", "--delay", String(delay), "--", text]);
16014
+ const position = recordingMousePosition();
16015
+ logRecordingAction({ type: "type", ...position ?? {} });
15358
16016
  console.log(`typed ${text.length} char${text.length === 1 ? "" : "s"}`);
15359
16017
  }
15360
16018
  async function computerKeyCommand(combo) {
15361
16019
  runDisplayCmd("xdotool", ["key", "--", combo]);
16020
+ const position = recordingMousePosition();
16021
+ logRecordingAction({ type: "key", ...position ?? {} });
15362
16022
  console.log(`pressed ${combo}`);
15363
16023
  }
15364
16024
  async function computerScrollCommand(direction, options) {
@@ -15368,17 +16028,24 @@ async function computerScrollCommand(direction, options) {
15368
16028
  if (!button) fail(`direction must be one of up|down|left|right (got "${direction}")`);
15369
16029
  const amount = options.amount ? parseCoord(options.amount, "--amount") : 3;
15370
16030
  const args = [];
16031
+ let hoverPosition = null;
15371
16032
  if (options.x && options.y) {
15372
16033
  const dimensions = getDisplayDimensions();
16034
+ hoverPosition = {
16035
+ x: parseScreenCoord(options.x, "--x", dimensions.width),
16036
+ y: parseScreenCoord(options.y, "--y", dimensions.height)
16037
+ };
15373
16038
  args.push(
15374
16039
  "mousemove",
15375
16040
  "--sync",
15376
- String(parseScreenCoord(options.x, "--x", dimensions.width)),
15377
- String(parseScreenCoord(options.y, "--y", dimensions.height))
16041
+ String(hoverPosition.x),
16042
+ String(hoverPosition.y)
15378
16043
  );
15379
16044
  }
15380
16045
  args.push("click", "--repeat", String(amount), "--delay", "30", button);
15381
16046
  runDisplayCmd("xdotool", args);
16047
+ const position = hoverPosition ?? recordingMousePosition();
16048
+ logRecordingAction({ type: "scroll", ...position ?? {} });
15382
16049
  console.log(`scrolled ${dir} x${amount}`);
15383
16050
  }
15384
16051
  async function computerDragCommand(fx, fy, tx, ty) {
@@ -15387,20 +16054,24 @@ async function computerDragCommand(fx, fy, tx, ty) {
15387
16054
  const fromY = parseScreenCoord(fy, "fromY", dimensions.height);
15388
16055
  const toX = parseScreenCoord(tx, "toX", dimensions.width);
15389
16056
  const toY = parseScreenCoord(ty, "toY", dimensions.height);
15390
- runDisplayCmd("xdotool", [
16057
+ const distance = Math.hypot(toX - fromX, toY - fromY);
16058
+ const steps = clamp(Math.ceil(distance / 70), 8, 24);
16059
+ const args = [
15391
16060
  "mousemove",
15392
16061
  "--sync",
15393
16062
  String(fromX),
15394
16063
  String(fromY),
15395
16064
  "mousedown",
15396
- "1",
15397
- "mousemove",
15398
- "--sync",
15399
- String(toX),
15400
- String(toY),
15401
- "mouseup",
15402
16065
  "1"
15403
- ]);
16066
+ ];
16067
+ for (let i = 1; i <= steps; i++) {
16068
+ const x = Math.round(fromX + (toX - fromX) * i / steps);
16069
+ const y = Math.round(fromY + (toY - fromY) * i / steps);
16070
+ args.push("mousemove", "--sync", String(x), String(y), "sleep", "0.018");
16071
+ }
16072
+ args.push("mouseup", "1");
16073
+ runDisplayCmd("xdotool", args);
16074
+ logRecordingAction({ type: "drag", x: fromX, y: fromY, toX, toY });
15404
16075
  console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
15405
16076
  }
15406
16077
  var APP_ALIASES = {
@@ -15423,10 +16094,10 @@ var LEGACY_CHROME_ALIAS = [
15423
16094
  ];
15424
16095
  async function computerLaunchCommand(app, args) {
15425
16096
  ensureServicesRunning();
15426
- const baseArgs = app === "chrome" && !existsSync(CHROME_WRAPPER) ? LEGACY_CHROME_ALIAS : APP_ALIASES[app] ?? [app];
16097
+ const baseArgs = app === "chrome" && !existsSync3(CHROME_WRAPPER) ? LEGACY_CHROME_ALIAS : APP_ALIASES[app] ?? [app];
15427
16098
  const bin = baseArgs[0];
15428
16099
  const fullArgs = [...baseArgs.slice(1), ...args];
15429
- const child = spawn3(bin, fullArgs, {
16100
+ const child = spawn4(bin, fullArgs, {
15430
16101
  env: withDisplay(),
15431
16102
  detached: true,
15432
16103
  stdio: "ignore"
@@ -15434,85 +16105,6 @@ async function computerLaunchCommand(app, args) {
15434
16105
  child.unref();
15435
16106
  console.log(`launched ${bin} (pid ${child.pid})`);
15436
16107
  }
15437
- var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
15438
- async function computerRecordStartCommand(path6, options) {
15439
- ensureServicesRunning();
15440
- if (existsSync(RECORD_PID_FILE)) {
15441
- const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
15442
- if (Number.isFinite(pid)) {
15443
- let alive = false;
15444
- try {
15445
- process.kill(pid, 0);
15446
- alive = true;
15447
- } catch {
15448
- }
15449
- if (alive) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
15450
- }
15451
- }
15452
- const target = resolvePath(path6);
15453
- mkdirSync(dirname(target), { recursive: true });
15454
- const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
15455
- const width = process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH);
15456
- const height = process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT);
15457
- const child = spawn3("ffmpeg", [
15458
- "-y",
15459
- "-hide_banner",
15460
- "-loglevel",
15461
- "warning",
15462
- "-f",
15463
- "x11grab",
15464
- "-framerate",
15465
- String(fps),
15466
- "-video_size",
15467
- `${width}x${height}`,
15468
- "-draw_mouse",
15469
- "1",
15470
- "-i",
15471
- DEFAULT_DISPLAY,
15472
- "-c:v",
15473
- "libx264",
15474
- "-preset",
15475
- "ultrafast",
15476
- "-tune",
15477
- "zerolatency",
15478
- "-crf",
15479
- "23",
15480
- "-pix_fmt",
15481
- "yuv420p",
15482
- "-movflags",
15483
- "+faststart+frag_keyframe+empty_moov",
15484
- target
15485
- ], { detached: true, stdio: "ignore" });
15486
- child.unref();
15487
- if (!child.pid) fail("failed to launch ffmpeg");
15488
- mkdirSync(STATE_DIR, { recursive: true });
15489
- writeFileSync(RECORD_PID_FILE, String(child.pid));
15490
- writeFileSync(`${STATE_DIR}/recording-path.txt`, target);
15491
- console.log(target);
15492
- }
15493
- async function computerRecordStopCommand() {
15494
- if (!existsSync(RECORD_PID_FILE)) fail("no recording in progress");
15495
- const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
15496
- if (!Number.isFinite(pid)) fail("invalid recording pidfile");
15497
- try {
15498
- process.kill(pid, "SIGINT");
15499
- } catch {
15500
- }
15501
- for (let i = 0; i < 30; i++) {
15502
- try {
15503
- process.kill(pid, 0);
15504
- } catch {
15505
- break;
15506
- }
15507
- await new Promise((r) => setTimeout(r, 200));
15508
- }
15509
- rmSync(RECORD_PID_FILE, { force: true });
15510
- const pathFile = `${STATE_DIR}/recording-path.txt`;
15511
- if (existsSync(pathFile)) {
15512
- console.log(readFileSync(pathFile, "utf8").trim());
15513
- rmSync(pathFile, { force: true });
15514
- }
15515
- }
15516
16108
 
15517
16109
  // src/commands/interactive.ts
15518
16110
  import chalk21 from "chalk";
@@ -19404,7 +19996,7 @@ automation.command("get <id>").description("Get automation details by ID").actio
19404
19996
  process.exit(1);
19405
19997
  }
19406
19998
  });
19407
- automation.command("create [name]").description("Create a new automation").option("-p, --prompt <prompt>", "Prompt for the automation").option("-e, --environment <environment>", "Environment name or ID").option("--trigger-cron <schedule>", 'Cron schedule expression (e.g. "0 9 * * 1-5")').option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger (default: UTC)").option("--trigger-github <event>", 'GitHub event (e.g. "pull_request.opened")').option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", 'GitLab event (e.g. "merge_request.opened")').option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--lifecycle <policy>", "Workspace lifecycle: delete_when_done, sleep_when_done, delete_after_inactivity, default").option("--auto-stop-minutes <minutes>", "Inactivity timeout in minutes (3-1440, requires --lifecycle default or delete_after_inactivity)").option("--pr-followups", "Allow follow-up actions on matching PRs").option("--agent-provider <provider>", 'Coding agent to use: claude, codex, cursor, relay (or "none" to inherit org default)').option("--model <model>", 'Model identifier (must be valid for --agent-provider; pass "none" to clear)').option("--thinking-level <level>", 'Thinking/reasoning level: low, medium, high, max (or "none" to clear)').option("--plan-mode", "Run automation messages in plan mode").option("--goal-mode", "Set automation messages as Codex goals").option("--fast-mode", "Run automation messages in fast mode").option("--personal", "Create a personal automation owned by the authenticated user").option("--disabled", "Create in disabled state").action(async (name, options) => {
19999
+ automation.command("create [name]").description("Create a new automation").option("-p, --prompt <prompt>", "Prompt for the automation").option("-e, --environment <environment>", "Environment name or ID").option("--trigger-cron <schedule>", 'Cron schedule expression (e.g. "0 9 * * 1-5")').option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger (default: UTC)").option("--trigger-github <event>", 'GitHub event (e.g. "pull_request.opened")').option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", 'GitLab event (e.g. "merge_request.opened")').option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--lifecycle <policy>", "Workspace lifecycle: archive_when_done, sleep_when_done, delete_after_inactivity, default").option("--auto-stop-minutes <minutes>", "Inactivity timeout in minutes (3-1440, requires --lifecycle default or delete_after_inactivity)").option("--pr-followups", "Allow follow-up actions on matching PRs").option("--agent-provider <provider>", 'Coding agent to use: claude, codex, cursor, relay (or "none" to inherit org default)').option("--model <model>", 'Model identifier (must be valid for --agent-provider; pass "none" to clear)').option("--thinking-level <level>", 'Thinking/reasoning level: low, medium, high, max (or "none" to clear)').option("--plan-mode", "Run automation messages in plan mode").option("--goal-mode", "Set automation messages as Codex goals").option("--fast-mode", "Run automation messages in fast mode").option("--personal", "Create a personal automation owned by the authenticated user").option("--disabled", "Create in disabled state").action(async (name, options) => {
19408
20000
  try {
19409
20001
  await automationCreateCommand(name, {
19410
20002
  ...options,
@@ -19419,7 +20011,7 @@ automation.command("create [name]").description("Create a new automation").optio
19419
20011
  process.exit(1);
19420
20012
  }
19421
20013
  });
19422
- automation.command("edit <id>").description("Edit an existing automation").option("-n, --name <name>", "New name").option("-p, --prompt <prompt>", "New prompt").option("-e, --enabled <enabled>", "Enable or disable (true/false)").option("--trigger-cron <schedule>", "Set cron schedule (replaces existing triggers)").option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger").option("--trigger-github <event>", "Set GitHub event (replaces existing triggers)").option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", "Set GitLab event (replaces existing triggers)").option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--environment <environment>", "Environment name or ID").option("--lifecycle <policy>", "Workspace lifecycle: delete_when_done, sleep_when_done, delete_after_inactivity, default").option("--auto-stop-minutes <minutes>", "Inactivity timeout in minutes (3-1440, requires --lifecycle default or delete_after_inactivity)").option("--pr-followups <enabled>", "Allow follow-up actions on matching PRs (true/false)", parseBooleanOption).option("--agent-provider <provider>", 'Coding agent to use: claude, codex, cursor, relay (or "none" to inherit org default)').option("--model <model>", 'Model identifier (must be valid for --agent-provider; pass "none" to clear)').option("--thinking-level <level>", 'Thinking/reasoning level: low, medium, high, max (or "none" to clear)').option("--plan-mode <enabled>", "Run automation messages in plan mode (true/false)", parseBooleanOption).option("--goal-mode <enabled>", "Set automation messages as Codex goals (true/false)", parseBooleanOption).option("--fast-mode <enabled>", "Run automation messages in fast mode (true/false)", parseBooleanOption).action(async (id, options) => {
20014
+ automation.command("edit <id>").description("Edit an existing automation").option("-n, --name <name>", "New name").option("-p, --prompt <prompt>", "New prompt").option("-e, --enabled <enabled>", "Enable or disable (true/false)").option("--trigger-cron <schedule>", "Set cron schedule (replaces existing triggers)").option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger").option("--trigger-github <event>", "Set GitHub event (replaces existing triggers)").option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", "Set GitLab event (replaces existing triggers)").option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--environment <environment>", "Environment name or ID").option("--lifecycle <policy>", "Workspace lifecycle: archive_when_done, sleep_when_done, delete_after_inactivity, default").option("--auto-stop-minutes <minutes>", "Inactivity timeout in minutes (3-1440, requires --lifecycle default or delete_after_inactivity)").option("--pr-followups <enabled>", "Allow follow-up actions on matching PRs (true/false)", parseBooleanOption).option("--agent-provider <provider>", 'Coding agent to use: claude, codex, cursor, relay (or "none" to inherit org default)').option("--model <model>", 'Model identifier (must be valid for --agent-provider; pass "none" to clear)').option("--thinking-level <level>", 'Thinking/reasoning level: low, medium, high, max (or "none" to clear)').option("--plan-mode <enabled>", "Run automation messages in plan mode (true/false)", parseBooleanOption).option("--goal-mode <enabled>", "Set automation messages as Codex goals (true/false)", parseBooleanOption).option("--fast-mode <enabled>", "Run automation messages in fast mode (true/false)", parseBooleanOption).action(async (id, options) => {
19423
20015
  try {
19424
20016
  await automationEditCommand(id, options);
19425
20017
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-cli",
3
- "version": "0.2.327",
3
+ "version": "0.2.329",
4
4
  "description": "CLI for managing Replicas workspaces - SSH into cloud dev environments with automatic port forwarding",
5
5
  "main": "dist/index.mjs",
6
6
  "bin": {