whipdesk 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.cjs CHANGED
@@ -27641,7 +27641,7 @@ var require_websocket_server = __commonJS({
27641
27641
 
27642
27642
  // src/index.ts
27643
27643
  var import_node_readline2 = require("node:readline");
27644
- var import_node_os8 = require("node:os");
27644
+ var import_node_os9 = require("node:os");
27645
27645
 
27646
27646
  // src/config.ts
27647
27647
  var import_node_crypto = require("node:crypto");
@@ -27693,7 +27693,7 @@ function isPackaged() {
27693
27693
  }
27694
27694
 
27695
27695
  // src/version.ts
27696
- var AGENT_VERSION = "0.1.2";
27696
+ var AGENT_VERSION = "0.1.3";
27697
27697
 
27698
27698
  // src/config.ts
27699
27699
  var here = (0, import_node_path2.dirname)((0, import_node_url2.fileURLToPath)(__whipdesk_meta_url));
@@ -28274,12 +28274,20 @@ async function readNsScreens() {
28274
28274
  return null;
28275
28275
  }
28276
28276
  }
28277
+ function friendlyDisplayName(raw, index) {
28278
+ const s = String(raw ?? "").trim();
28279
+ if (!s) return `Display ${index + 1}`;
28280
+ const m = /DISPLAY(\d+)/i.exec(s);
28281
+ if (m) return `Display ${m[1]}`;
28282
+ if (/^\\\\/.test(s)) return `Display ${index + 1}`;
28283
+ return s;
28284
+ }
28277
28285
  async function listBaseDisplays() {
28278
28286
  try {
28279
28287
  const displays = await import_screenshot_desktop.default.listDisplays();
28280
28288
  return displays.map((d, index) => ({
28281
28289
  id: typeof d.id === "number" ? d.id : index,
28282
- name: String(d.name ?? `Display ${index + 1}`),
28290
+ name: friendlyDisplayName(d.name, index),
28283
28291
  primary: Boolean(d.primary) || index === 0
28284
28292
  }));
28285
28293
  } catch (error) {
@@ -28446,7 +28454,7 @@ function buildWelcome(ctx) {
28446
28454
  return {
28447
28455
  type: "welcome",
28448
28456
  protocol: PROTOCOL_VERSION,
28449
- agent: { version: AGENT_VERSION, platform: (0, import_node_os2.platform)(), hostname: (0, import_node_os2.hostname)() },
28457
+ agent: { version: AGENT_VERSION, platform: (0, import_node_os2.platform)(), hostname: (0, import_node_os2.hostname)(), hdr: ctx.hdrActive || void 0 },
28450
28458
  screen: ctx.screen,
28451
28459
  capture: { fps: ctx.config.fps, quality: ctx.config.quality, maxWidth: ctx.config.maxWidth },
28452
28460
  capabilities: {
@@ -28541,11 +28549,96 @@ async function dispatch(ctx, msg, controller) {
28541
28549
  }
28542
28550
 
28543
28551
  // src/capture/encoder.ts
28544
- var import_node_child_process2 = require("node:child_process");
28552
+ var import_node_child_process3 = require("node:child_process");
28545
28553
  var import_node_dgram = require("node:dgram");
28546
28554
  var import_node_util2 = require("node:util");
28547
28555
  var import_ffmpeg_static = __toESM(require("ffmpeg-static"), 1);
28548
- var exec2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
28556
+
28557
+ // src/capture/hdr-win.ts
28558
+ var import_node_child_process2 = require("node:child_process");
28559
+ var cached = null;
28560
+ var inflight = null;
28561
+ function invalidateHdrCache() {
28562
+ cached = null;
28563
+ }
28564
+ async function windowsHdrState(maxAgeMs = 15e3) {
28565
+ if (process.platform !== "win32") return null;
28566
+ if (cached && Date.now() - cached.at < maxAgeMs) return cached.state;
28567
+ if (inflight) return inflight;
28568
+ inflight = probe().then((state) => {
28569
+ if (state) cached = { state, at: Date.now() };
28570
+ return state;
28571
+ }).finally(() => {
28572
+ inflight = null;
28573
+ });
28574
+ return inflight;
28575
+ }
28576
+ async function probe() {
28577
+ return new Promise((resolve) => {
28578
+ (0, import_node_child_process2.execFile)(
28579
+ "powershell",
28580
+ ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", PROBE_PS],
28581
+ { timeout: 8e3, windowsHide: true },
28582
+ (err, stdout) => {
28583
+ if (err) {
28584
+ log.debug("hdr probe failed:", err.message);
28585
+ return resolve(null);
28586
+ }
28587
+ let supported = false;
28588
+ let active = false;
28589
+ let sawAny = false;
28590
+ for (const line of String(stdout).split(/\r?\n/)) {
28591
+ const m = /^([01]) ([01])$/.exec(line.trim());
28592
+ if (!m) continue;
28593
+ sawAny = true;
28594
+ if (m[1] === "1") supported = true;
28595
+ if (m[2] === "1") active = true;
28596
+ }
28597
+ resolve(sawAny ? { supported, active } : null);
28598
+ }
28599
+ );
28600
+ });
28601
+ }
28602
+ var PROBE_PS = `
28603
+ $src = @'
28604
+ using System;
28605
+ using System.Runtime.InteropServices;
28606
+ public static class WdHdr {
28607
+ [StructLayout(LayoutKind.Sequential)] public struct LUID { public uint Low; public int High; }
28608
+ [StructLayout(LayoutKind.Sequential)] public struct PATH_SOURCE { public LUID adapterId; public uint id; public uint modeInfoIdx; public uint statusFlags; }
28609
+ [StructLayout(LayoutKind.Sequential)] public struct RATIONAL { public uint num; public uint den; }
28610
+ [StructLayout(LayoutKind.Sequential)] public struct PATH_TARGET { public LUID adapterId; public uint id; public uint modeInfoIdx; public uint outputTechnology; public uint rotation; public uint scaling; public RATIONAL refreshRate; public uint scanLineOrdering; public int targetAvailable; public uint statusFlags; }
28611
+ [StructLayout(LayoutKind.Sequential)] public struct PATH_INFO { public PATH_SOURCE sourceInfo; public PATH_TARGET targetInfo; public uint flags; }
28612
+ [StructLayout(LayoutKind.Sequential, Size = 64)] public struct MODE_INFO { public uint infoType; public uint id; public LUID adapterId; }
28613
+ [StructLayout(LayoutKind.Sequential)] public struct DEVICE_INFO_HEADER { public uint type; public uint size; public LUID adapterId; public uint id; }
28614
+ [StructLayout(LayoutKind.Sequential)] public struct GET_ADVANCED_COLOR_INFO { public DEVICE_INFO_HEADER header; public uint value; public uint colorEncoding; public uint bitsPerColorChannel; }
28615
+ [DllImport("user32.dll")] public static extern int GetDisplayConfigBufferSizes(uint flags, out uint numPaths, out uint numModes);
28616
+ [DllImport("user32.dll")] public static extern int QueryDisplayConfig(uint flags, ref uint numPaths, [Out] PATH_INFO[] paths, ref uint numModes, [Out] MODE_INFO[] modes, IntPtr topologyId);
28617
+ [DllImport("user32.dll")] public static extern int DisplayConfigGetDeviceInfo(ref GET_ADVANCED_COLOR_INFO info);
28618
+ public static void Probe() {
28619
+ uint np, nm;
28620
+ if (GetDisplayConfigBufferSizes(2, out np, out nm) != 0) return;
28621
+ var paths = new PATH_INFO[np];
28622
+ var modes = new MODE_INFO[nm];
28623
+ if (QueryDisplayConfig(2, ref np, paths, ref nm, modes, IntPtr.Zero) != 0) return;
28624
+ for (int i = 0; i < np; i++) {
28625
+ var q = new GET_ADVANCED_COLOR_INFO();
28626
+ q.header.type = 9;
28627
+ q.header.size = (uint)Marshal.SizeOf(typeof(GET_ADVANCED_COLOR_INFO));
28628
+ q.header.adapterId = paths[i].targetInfo.adapterId;
28629
+ q.header.id = paths[i].targetInfo.id;
28630
+ if (DisplayConfigGetDeviceInfo(ref q) == 0)
28631
+ Console.WriteLine(((q.value & 1) != 0 ? "1" : "0") + " " + ((q.value & 2) != 0 ? "1" : "0"));
28632
+ }
28633
+ }
28634
+ }
28635
+ '@
28636
+ Add-Type -TypeDefinition $src -Language CSharp
28637
+ [WdHdr]::Probe()
28638
+ `.trim();
28639
+
28640
+ // src/capture/encoder.ts
28641
+ var exec2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
28549
28642
  var VIDEO_PAYLOAD_TYPE = 96;
28550
28643
  var VIDEO_CLOCK_RATE = 9e4;
28551
28644
  var encoderName;
@@ -28597,29 +28690,55 @@ async function avScreenIndex(displayId) {
28597
28690
  const map = await avScreenMapPromise;
28598
28691
  return map.get(displayId) ?? map.get(0) ?? null;
28599
28692
  }
28600
- async function captureDeviceFor(displayIndex, fps) {
28693
+ var HDR_TONEMAP = ",tonemap=tonemap=hable:desat=0:peak=10,zscale=tin=linear:pin=bt709:t=bt709:p=bt709:m=bt709:r=tv,format=yuv420p";
28694
+ async function captureDeviceFor(displayIndex, fps, winFallback) {
28601
28695
  const r = String(Math.max(1, Math.min(60, Math.round(fps))));
28696
+ const ts2 = ["-fflags", "+genpts", "-use_wallclock_as_timestamps", "1"];
28602
28697
  if (process.platform === "darwin") {
28603
28698
  const idx = await avScreenIndex(displayIndex);
28604
28699
  if (idx == null) return null;
28605
- return ["-f", "avfoundation", "-capture_cursor", "1", "-pixel_format", "nv12", "-framerate", r, "-i", `${idx}:none`];
28700
+ return {
28701
+ inputArgs: [...ts2, "-f", "avfoundation", "-capture_cursor", "1", "-pixel_format", "nv12", "-framerate", r, "-i", `${idx}:none`],
28702
+ hwPrefix: "",
28703
+ postFilter: ""
28704
+ };
28606
28705
  }
28607
28706
  if (process.platform === "win32") {
28608
- return ["-f", "gdigrab", "-framerate", r, "-i", "desktop"];
28707
+ if (winFallback) {
28708
+ return { inputArgs: [...ts2, "-f", "gdigrab", "-framerate", r, "-i", "desktop"], hwPrefix: "", postFilter: "" };
28709
+ }
28710
+ const idx = Math.max(0, Math.round(displayIndex));
28711
+ const hdr = (await windowsHdrState())?.active === true;
28712
+ if (hdr) {
28713
+ return {
28714
+ inputArgs: ["-f", "lavfi", "-i", `ddagrab=output_idx=${idx}:framerate=${r}:draw_mouse=1:output_fmt=rgbaf16`],
28715
+ // gbrpf32le BEFORE the split/crop/scale: swscale handles planar-float scaling everywhere,
28716
+ // while direct rgbaf16 scaling is spottier across builds. Tone-mapping itself stays
28717
+ // per-branch AFTER the scale (postFilter) so the heavy math runs at ≤maxWidth, not 4K.
28718
+ hwPrefix: "hwdownload,format=rgbaf16,format=gbrpf32le,",
28719
+ postFilter: HDR_TONEMAP,
28720
+ winHdr: true
28721
+ };
28722
+ }
28723
+ return {
28724
+ inputArgs: ["-f", "lavfi", "-i", `ddagrab=output_idx=${idx}:framerate=${r}:draw_mouse=1`],
28725
+ hwPrefix: "hwdownload,format=bgra,",
28726
+ postFilter: ""
28727
+ };
28609
28728
  }
28610
28729
  if (process.platform === "linux") {
28611
- return ["-f", "x11grab", "-framerate", r, "-i", process.env.DISPLAY || ":0.0"];
28730
+ return { inputArgs: [...ts2, "-f", "x11grab", "-framerate", r, "-i", process.env.DISPLAY || ":0.0"], hwPrefix: "", postFilter: "" };
28612
28731
  }
28613
28732
  return null;
28614
28733
  }
28615
28734
  function videoFilter(crop, maxWidth) {
28616
- const scale = `scale=min(${Math.round(maxWidth)}\\,iw):-2`;
28735
+ const scale = `scale=trunc(min(${Math.round(maxWidth)}\\,iw)/2)*2:-2`;
28617
28736
  if (isFull(crop)) return scale;
28618
28737
  const c = crop;
28619
28738
  return `crop=iw*${c.w.toFixed(4)}:ih*${c.h.toFixed(4)}:iw*${c.x.toFixed(4)}:ih*${c.y.toFixed(4)},${scale}`;
28620
28739
  }
28621
28740
  function overviewFilter(ov) {
28622
- return `scale=min(${Math.round(ov.width)}\\,iw):-2,fps=${Math.max(1, Math.round(ov.fps))}`;
28741
+ return `scale=trunc(min(${Math.round(ov.width)}\\,iw)/2)*2:-2,fps=${Math.max(1, Math.round(ov.fps))}`;
28623
28742
  }
28624
28743
  function sameCrop(a, b) {
28625
28744
  if (isFull(a) && isFull(b)) return true;
@@ -28663,6 +28782,8 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28663
28782
  restarting = false;
28664
28783
  /** Config the running ffmpeg was built from; lets a restart detect that `cfg` moved during it. */
28665
28784
  appliedCfg = null;
28785
+ /** Windows only: set once ddagrab fails to produce a frame, switching capture to gdigrab. */
28786
+ winCaptureFallback = false;
28666
28787
  /** Don't declare a not-yet-producing capture dead until it's had this long to start up. */
28667
28788
  static FIRST_FRAME_GRACE_MS = 6e3;
28668
28789
  /** Once it HAS produced frames, restart if it goes silent this long (display sleep, etc.). */
@@ -28702,7 +28823,7 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28702
28823
  if (this.stopped) return false;
28703
28824
  const cfg = this.cfg;
28704
28825
  const enc = await pickH264Encoder();
28705
- const capture = await captureDeviceFor(cfg.displayIndex, cfg.fps);
28826
+ const capture = await captureDeviceFor(cfg.displayIndex, cfg.fps, this.winCaptureFallback);
28706
28827
  if (!enc || !import_ffmpeg_static.default || !capture) {
28707
28828
  if (!capture) log.warn("no direct screen-capture device on this platform \u2014 screen sharing unavailable");
28708
28829
  this.fail();
@@ -28739,7 +28860,7 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28739
28860
  const port = sock.address().port;
28740
28861
  const portOv = sockOv ? sockOv.address().port : 0;
28741
28862
  const preset = enc === "libx264" ? ["-preset", "ultrafast", "-tune", "zerolatency"] : ["-realtime", "1"];
28742
- const input = ["-hide_banner", "-loglevel", "error", "-fflags", "+genpts", "-use_wallclock_as_timestamps", "1", ...capture, "-an"];
28863
+ const input = ["-hide_banner", "-loglevel", "error", ...capture.inputArgs, "-an"];
28743
28864
  const h264Out = (kbps, gop, ssrc, outPort) => [
28744
28865
  "-fps_mode",
28745
28866
  "vfr",
@@ -28766,26 +28887,30 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28766
28887
  String(VIDEO_PAYLOAD_TYPE),
28767
28888
  "-ssrc",
28768
28889
  ssrc,
28890
+ // buffer_size = SO_SNDBUF: keyframe bursts must not overflow the send side either (the
28891
+ // receive side is widened in bindUdp — see the black-bottom-of-keyframe note there).
28769
28892
  "-f",
28770
28893
  "rtp",
28771
- `rtp://127.0.0.1:${outPort}?pkt_size=1200`
28894
+ `rtp://127.0.0.1:${outPort}?pkt_size=1200&buffer_size=1048576`
28772
28895
  ];
28773
28896
  const mainGop = Math.max(10, Math.round(cfg.fps));
28774
28897
  const args = ov ? [
28775
28898
  ...input,
28776
28899
  // The full captured frame fans into two encodes: [m] the sharp crop, [o] the small overview.
28900
+ // hwPrefix (ddagrab) downloads once, before the split, so both branches get RAM frames.
28901
+ // postFilter (HDR tone map) runs per-branch AFTER the scale — cheap at output size.
28777
28902
  "-filter_complex",
28778
- `[0:v]split=2[m][o];[m]${videoFilter(cfg.crop, cfg.maxWidth)}[mainout];[o]${overviewFilter(ov)}[ovout]`,
28903
+ `[0:v]${capture.hwPrefix}split=2[m][o];[m]${videoFilter(cfg.crop, cfg.maxWidth)}${capture.postFilter}[mainout];[o]${overviewFilter(ov)}${capture.postFilter}[ovout]`,
28779
28904
  "-map",
28780
28905
  "[mainout]",
28781
28906
  ...h264Out(cfg.kbps, mainGop, "1", port),
28782
28907
  "-map",
28783
28908
  "[ovout]",
28784
28909
  ...h264Out(ov.kbps, Math.max(1, Math.round(ov.fps)), "2", portOv)
28785
- ] : [...input, "-vf", videoFilter(cfg.crop, cfg.maxWidth), ...h264Out(cfg.kbps, mainGop, "1", port)];
28910
+ ] : [...input, "-vf", `${capture.hwPrefix}${videoFilter(cfg.crop, cfg.maxWidth)}${capture.postFilter}`, ...h264Out(cfg.kbps, mainGop, "1", port)];
28786
28911
  let proc;
28787
28912
  try {
28788
- proc = (0, import_node_child_process2.spawn)(import_ffmpeg_static.default, args, { stdio: ["ignore", "ignore", "pipe"] });
28913
+ proc = (0, import_node_child_process3.spawn)(import_ffmpeg_static.default, args, { stdio: ["ignore", "ignore", "pipe"] });
28789
28914
  } catch (error) {
28790
28915
  log.warn("video encoder failed to start:", error.message);
28791
28916
  closeQuietly(sock);
@@ -28826,17 +28951,26 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28826
28951
  for (const line of d.toString().split("\n")) {
28827
28952
  const t = line.trim();
28828
28953
  if (!t || /^objc\[|NSKVONotifying|not linked into application/.test(t)) continue;
28954
+ if (process.platform === "win32" && /AcquireNextFrame failed|Output parameters changed|Requested output format unavailable/i.test(t)) {
28955
+ invalidateHdrCache();
28956
+ }
28829
28957
  log.debug("ffmpeg:", t.slice(0, 200));
28830
28958
  }
28831
28959
  });
28832
28960
  proc.on("error", (e) => log.warn("video encoder process error:", e.message));
28833
28961
  proc.on("exit", (code, sig) => {
28834
- if (this.proc === proc && !this.stopped) log.debug(`ffmpeg exited unexpectedly (${code ?? sig ?? "?"})`);
28962
+ if (this.proc !== proc || this.stopped) return;
28963
+ log.debug(`ffmpeg exited unexpectedly (${code ?? sig ?? "?"})`);
28964
+ const delay2 = Date.now() - this.spawnAt < 2e3 ? 2e3 : 400;
28965
+ const t = setTimeout(() => {
28966
+ if (this.proc === proc && !this.stopped) void this.restart();
28967
+ }, delay2);
28968
+ t.unref?.();
28835
28969
  });
28836
28970
  this.startHealthMonitor();
28837
28971
  const cropDesc = isFull(cfg.crop) ? "full desktop" : `zoomed ${cfg.crop.x.toFixed(2)},${cfg.crop.y.toFixed(2)} ${cfg.crop.w.toFixed(2)}x${cfg.crop.h.toFixed(2)}`;
28838
28972
  log.debug(
28839
- `video: H.264 capture started (${enc}, ${kbpsArg(cfg.kbps)}@${cfg.fps}fps, ${cropDesc}${ov ? " + overview" : ""})`
28973
+ `video: H.264 capture started (${enc}, ${kbpsArg(cfg.kbps)}@${cfg.fps}fps, ${cropDesc}${ov ? " + overview" : ""}${capture.winHdr ? ", HDR desktop tone-mapped to SDR" : ""})`
28840
28974
  );
28841
28975
  return true;
28842
28976
  }
@@ -28861,6 +28995,12 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28861
28995
  return;
28862
28996
  }
28863
28997
  if (now - this.spawnAt < _ScreenCaptureSession.FIRST_FRAME_GRACE_MS) return;
28998
+ if (process.platform === "win32" && !this.winCaptureFallback) {
28999
+ this.winCaptureFallback = true;
29000
+ log.debug("screen capture: ddagrab produced no frame \u2014 falling back to gdigrab");
29001
+ void this.restart();
29002
+ return;
29003
+ }
28864
29004
  if (++this.deadRestarts > _ScreenCaptureSession.MAX_DEAD_RESTARTS) {
28865
29005
  this.kill();
28866
29006
  this.fail();
@@ -28973,6 +29113,10 @@ function bindUdp() {
28973
29113
  sock.once("error", reject);
28974
29114
  sock.bind(0, "127.0.0.1", () => {
28975
29115
  sock.removeListener("error", reject);
29116
+ try {
29117
+ sock.setRecvBufferSize(4 * 1024 * 1024);
29118
+ } catch {
29119
+ }
28976
29120
  resolve(sock);
28977
29121
  });
28978
29122
  });
@@ -29251,9 +29395,9 @@ async function answerWebRtcOffer(ctx, offerSdp, onClosed, opts = {}) {
29251
29395
  }
29252
29396
  };
29253
29397
  (isOverview ? overviewSinks : sinks).push(sink);
29254
- const run2 = () => void (isOverview ? ctx.video.attachOverview(sink) : ctx.video.attach(sink));
29255
- if (authenticated) run2();
29256
- else pendingAttach.push(run2);
29398
+ const run3 = () => void (isOverview ? ctx.video.attachOverview(sink) : ctx.video.attach(sink));
29399
+ if (authenticated) run3();
29400
+ else pendingAttach.push(run3);
29257
29401
  } catch (error) {
29258
29402
  log.warn(`video track ${i} setup failed:`, error.message);
29259
29403
  }
@@ -29445,6 +29589,9 @@ function printSetupReminder() {
29445
29589
  console.log(" Setup / permissions:");
29446
29590
  for (const line of lines) console.log(line);
29447
29591
  console.log("");
29592
+ console.log(" Troubleshooting: relaunch with --verbose (e.g. `whipdesk --verbose`) for detailed");
29593
+ console.log(" capture/network logs \u2014 attach them when reporting an issue.");
29594
+ console.log("");
29448
29595
  }
29449
29596
 
29450
29597
  // src/server.ts
@@ -29509,12 +29656,12 @@ var ScreenCapturer = class {
29509
29656
  };
29510
29657
 
29511
29658
  // src/input/applescript.ts
29512
- var import_node_child_process3 = require("node:child_process");
29659
+ var import_node_child_process4 = require("node:child_process");
29513
29660
  var import_node_util3 = require("node:util");
29514
- var exec3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
29661
+ var exec3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
29515
29662
  async function createAppleScriptBackend() {
29516
29663
  if (process.platform !== "darwin") return null;
29517
- const run2 = (script) => exec3("osascript", ["-e", script]).then(() => void 0);
29664
+ const run3 = (script) => exec3("osascript", ["-e", script]).then(() => void 0);
29518
29665
  return {
29519
29666
  name: "applescript (keyboard-only)",
29520
29667
  canMouse: false,
@@ -29535,17 +29682,17 @@ async function createAppleScriptBackend() {
29535
29682
  async scroll() {
29536
29683
  },
29537
29684
  async typeText(text, submit) {
29538
- if (text) await run2(`tell application "System Events" to keystroke ${asAppleString(text)}`);
29539
- if (submit) await run2(`tell application "System Events" to key code 36`);
29685
+ if (text) await run3(`tell application "System Events" to keystroke ${asAppleString(text)}`);
29686
+ if (submit) await run3(`tell application "System Events" to key code 36`);
29540
29687
  },
29541
29688
  async keyTap(name, modifiers = []) {
29542
29689
  const using = modifiers.map(modifierClause).filter((m) => m !== null).join(", ");
29543
29690
  const usingClause = using ? ` using {${using}}` : "";
29544
29691
  const code = KEY_CODES[name.toLowerCase()];
29545
29692
  if (code !== void 0) {
29546
- await run2(`tell application "System Events" to key code ${code}${usingClause}`);
29693
+ await run3(`tell application "System Events" to key code ${code}${usingClause}`);
29547
29694
  } else if (name.length === 1) {
29548
- await run2(`tell application "System Events" to keystroke ${asAppleString(name)}${usingClause}`);
29695
+ await run3(`tell application "System Events" to keystroke ${asAppleString(name)}${usingClause}`);
29549
29696
  }
29550
29697
  }
29551
29698
  };
@@ -29909,7 +30056,7 @@ function meanDiff(a, b) {
29909
30056
  }
29910
30057
 
29911
30058
  // src/presence.ts
29912
- var import_node_child_process4 = require("node:child_process");
30059
+ var import_node_child_process5 = require("node:child_process");
29913
30060
  var Presence = class {
29914
30061
  watchers = 0;
29915
30062
  start() {
@@ -29919,7 +30066,7 @@ var Presence = class {
29919
30066
  if (process.platform !== "darwin") return;
29920
30067
  const escape2 = (s) => s.replace(/[\\"]/g, "\\$&");
29921
30068
  const script = `display notification "${escape2(body)}" with title "${escape2(title)}"`;
29922
- (0, import_node_child_process4.execFile)("osascript", ["-e", script], () => void 0);
30069
+ (0, import_node_child_process5.execFile)("osascript", ["-e", script], () => void 0);
29923
30070
  }
29924
30071
  setTitle(text) {
29925
30072
  if (process.stdout.isTTY) process.stdout.write(`\x1B]2;${text}\x07`);
@@ -29951,7 +30098,7 @@ var Presence = class {
29951
30098
  };
29952
30099
 
29953
30100
  // src/power/keep-awake.ts
29954
- var import_node_child_process5 = require("node:child_process");
30101
+ var import_node_child_process6 = require("node:child_process");
29955
30102
  var import_node_os4 = require("node:os");
29956
30103
  var KeepAwake = class {
29957
30104
  child = null;
@@ -29994,15 +30141,15 @@ var KeepAwake = class {
29994
30141
  spawnBlocker() {
29995
30142
  switch ((0, import_node_os4.platform)()) {
29996
30143
  case "darwin":
29997
- return (0, import_node_child_process5.spawn)("caffeinate", ["-i", "-w", String(process.pid)], { stdio: "ignore" });
30144
+ return (0, import_node_child_process6.spawn)("caffeinate", ["-i", "-w", String(process.pid)], { stdio: "ignore" });
29998
30145
  case "win32":
29999
- return (0, import_node_child_process5.spawn)(
30146
+ return (0, import_node_child_process6.spawn)(
30000
30147
  "powershell",
30001
30148
  ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", WINDOWS_KEEP_AWAKE],
30002
30149
  { stdio: "ignore", windowsHide: true }
30003
30150
  );
30004
30151
  case "linux":
30005
- return (0, import_node_child_process5.spawn)(
30152
+ return (0, import_node_child_process6.spawn)(
30006
30153
  "systemd-inhibit",
30007
30154
  [
30008
30155
  "--what=sleep:idle",
@@ -30027,7 +30174,7 @@ var WINDOWS_KEEP_AWAKE = [
30027
30174
  ].join(" ");
30028
30175
 
30029
30176
  // src/power/wake-display.ts
30030
- var import_node_child_process6 = require("node:child_process");
30177
+ var import_node_child_process7 = require("node:child_process");
30031
30178
  var import_node_os5 = require("node:os");
30032
30179
  var DisplayWake = class {
30033
30180
  hold = null;
@@ -30048,21 +30195,22 @@ var DisplayWake = class {
30048
30195
  this.stopHold();
30049
30196
  }
30050
30197
  }
30051
- /** Turn the display on right now (best-effort, never throws). */
30052
- wake() {
30198
+ /** Turn the display on right now (best-effort, never throws). `holdSeconds` keeps it on that
30199
+ * long on macOS (scheduled actions need the panel up for the whole wake→click→type sequence). */
30200
+ wake(holdSeconds = 5) {
30053
30201
  try {
30054
30202
  switch ((0, import_node_os5.platform)()) {
30055
30203
  case "darwin":
30056
- (0, import_node_child_process6.spawn)("caffeinate", ["-u", "-t", "5"], { stdio: "ignore" }).unref();
30204
+ (0, import_node_child_process7.spawn)("caffeinate", ["-u", "-t", String(holdSeconds)], { stdio: "ignore" }).unref();
30057
30205
  break;
30058
30206
  case "win32":
30059
- (0, import_node_child_process6.spawn)("powershell", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", WINDOWS_WAKE], {
30207
+ (0, import_node_child_process7.spawn)("powershell", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", WINDOWS_WAKE], {
30060
30208
  stdio: "ignore",
30061
30209
  windowsHide: true
30062
30210
  }).unref();
30063
30211
  break;
30064
30212
  case "linux":
30065
- (0, import_node_child_process6.spawn)("sh", ["-c", "xset s reset; xset dpms force on"], { stdio: "ignore" }).unref();
30213
+ (0, import_node_child_process7.spawn)("sh", ["-c", "xset s reset; xset dpms force on"], { stdio: "ignore" }).unref();
30066
30214
  break;
30067
30215
  }
30068
30216
  } catch (e) {
@@ -30073,10 +30221,10 @@ var DisplayWake = class {
30073
30221
  try {
30074
30222
  switch ((0, import_node_os5.platform)()) {
30075
30223
  case "darwin":
30076
- this.hold = (0, import_node_child_process6.spawn)("caffeinate", ["-d", "-w", String(process.pid)], { stdio: "ignore" });
30224
+ this.hold = (0, import_node_child_process7.spawn)("caffeinate", ["-d", "-w", String(process.pid)], { stdio: "ignore" });
30077
30225
  break;
30078
30226
  case "win32":
30079
- this.hold = (0, import_node_child_process6.spawn)(
30227
+ this.hold = (0, import_node_child_process7.spawn)(
30080
30228
  "powershell",
30081
30229
  ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", WINDOWS_HOLD],
30082
30230
  { stdio: "ignore", windowsHide: true }
@@ -30128,6 +30276,37 @@ var WINDOWS_HOLD = [
30128
30276
  "while ($true) { Start-Sleep -Seconds 3600 }"
30129
30277
  ].join(" ");
30130
30278
 
30279
+ // src/power/session-lock.ts
30280
+ var import_node_child_process8 = require("node:child_process");
30281
+ var import_node_os6 = require("node:os");
30282
+ async function isSessionLocked() {
30283
+ try {
30284
+ switch ((0, import_node_os6.platform)()) {
30285
+ case "darwin": {
30286
+ const out = await run("ioreg", ["-n", "Root", "-d1"]);
30287
+ const m = /"IOConsoleLocked"\s*=\s*(Yes|No)/.exec(out);
30288
+ return m ? m[1] === "Yes" : null;
30289
+ }
30290
+ case "win32": {
30291
+ const out = await run("tasklist", ["/FI", "IMAGENAME eq LogonUI.exe", "/NH"]);
30292
+ return /LogonUI\.exe/i.test(out);
30293
+ }
30294
+ default:
30295
+ return null;
30296
+ }
30297
+ } catch {
30298
+ return null;
30299
+ }
30300
+ }
30301
+ function run(cmd, args) {
30302
+ return new Promise((resolve, reject) => {
30303
+ (0, import_node_child_process8.execFile)(cmd, args, { timeout: 4e3, windowsHide: true }, (err, stdout) => {
30304
+ if (err) reject(err);
30305
+ else resolve(String(stdout));
30306
+ });
30307
+ });
30308
+ }
30309
+
30131
30310
  // src/security/setup.ts
30132
30311
  var import_node_readline = require("node:readline");
30133
30312
 
@@ -30486,9 +30665,9 @@ var import_node_path9 = require("node:path");
30486
30665
 
30487
30666
  // src/monitor/agents.ts
30488
30667
  var import_node_fs7 = require("node:fs");
30489
- var import_node_os6 = require("node:os");
30668
+ var import_node_os7 = require("node:os");
30490
30669
  var import_node_path7 = require("node:path");
30491
- var home = (0, import_node_os6.homedir)();
30670
+ var home = (0, import_node_os7.homedir)();
30492
30671
  var has = (tokens, name) => tokens.includes(name);
30493
30672
  function claudeProjectDir(cwd) {
30494
30673
  return (0, import_node_path7.join)(home, ".claude", "projects", cwd.replace(/[/\\.]/g, "-"));
@@ -30549,8 +30728,8 @@ function readLastJsonlLine(path) {
30549
30728
  }
30550
30729
  function vsCodeUserDirs() {
30551
30730
  const variants = ["Code", "Code - Insiders", "VSCodium"];
30552
- if ((0, import_node_os6.platform)() === "darwin") return variants.map((v) => (0, import_node_path7.join)(home, "Library", "Application Support", v, "User"));
30553
- if ((0, import_node_os6.platform)() === "win32") {
30731
+ if ((0, import_node_os7.platform)() === "darwin") return variants.map((v) => (0, import_node_path7.join)(home, "Library", "Application Support", v, "User"));
30732
+ if ((0, import_node_os7.platform)() === "win32") {
30554
30733
  const appData = process.env.APPDATA || (0, import_node_path7.join)(home, "AppData", "Roaming");
30555
30734
  return variants.map((v) => (0, import_node_path7.join)(appData, v, "User"));
30556
30735
  }
@@ -30592,11 +30771,12 @@ function vsCodeCopilotInstalled() {
30592
30771
  } catch {
30593
30772
  }
30594
30773
  }
30774
+ if (!value) value = vsCodeChatSessionDirs().length > 0;
30595
30775
  copilotInstalledCache = { value, at: now };
30596
30776
  return value;
30597
30777
  }
30598
30778
  function isVsCodeExtensionHost(cmd) {
30599
- return cmd.includes("--type=extensionhost") || cmd.includes("extensionhostprocess");
30779
+ return cmd.includes("--type=extensionhost") || cmd.includes("extensionhostprocess") || cmd.includes("node.mojom.nodeservice") && cmd.includes("--inspect-port");
30600
30780
  }
30601
30781
  var AGENTS = [
30602
30782
  {
@@ -30679,11 +30859,11 @@ function agentLabel(kind) {
30679
30859
  }
30680
30860
 
30681
30861
  // src/monitor/processes.ts
30682
- var import_node_child_process7 = require("node:child_process");
30683
- var import_node_os7 = require("node:os");
30684
- function run(cmd, args, timeoutMs = 5e3) {
30862
+ var import_node_child_process9 = require("node:child_process");
30863
+ var import_node_os8 = require("node:os");
30864
+ function run2(cmd, args, timeoutMs = 5e3) {
30685
30865
  return new Promise((resolve) => {
30686
- (0, import_node_child_process7.execFile)(cmd, args, { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (err, stdout) => {
30866
+ (0, import_node_child_process9.execFile)(cmd, args, { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (err, stdout) => {
30687
30867
  resolve(err && !stdout ? "" : stdout.toString());
30688
30868
  });
30689
30869
  });
@@ -30698,7 +30878,7 @@ function tokenize(command) {
30698
30878
  return out;
30699
30879
  }
30700
30880
  async function listUnix() {
30701
- const out = await run("ps", ["-axww", "-o", "pid=,ppid=,pcpu=,tty=,command="]);
30881
+ const out = await run2("ps", ["-axww", "-o", "pid=,ppid=,pcpu=,tty=,command="]);
30702
30882
  const procs = [];
30703
30883
  for (const line of out.split("\n")) {
30704
30884
  const m = line.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\S+)\s+(.*)$/);
@@ -30718,7 +30898,7 @@ async function listUnix() {
30718
30898
  }
30719
30899
  async function listWindows() {
30720
30900
  const script = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,Name | ConvertTo-Csv -NoTypeInformation";
30721
- const out = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], 9e3);
30901
+ const out = await run2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], 9e3);
30722
30902
  const procs = [];
30723
30903
  for (const line of out.split(/\r?\n/)) {
30724
30904
  const m = line.match(/^"(\d+)","(\d*)",(.*)$/);
@@ -30742,16 +30922,16 @@ async function listWindows() {
30742
30922
  return procs;
30743
30923
  }
30744
30924
  function listProcesses() {
30745
- return (0, import_node_os7.platform)() === "win32" ? listWindows() : listUnix();
30925
+ return (0, import_node_os8.platform)() === "win32" ? listWindows() : listUnix();
30746
30926
  }
30747
30927
  async function processCwd(pid) {
30748
- const os = (0, import_node_os7.platform)();
30928
+ const os = (0, import_node_os8.platform)();
30749
30929
  if (os === "win32") return "";
30750
30930
  if (os === "linux") {
30751
- const out2 = await run("readlink", [`/proc/${pid}/cwd`], 2e3);
30931
+ const out2 = await run2("readlink", [`/proc/${pid}/cwd`], 2e3);
30752
30932
  return out2.trim();
30753
30933
  }
30754
- const out = await run("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], 3e3);
30934
+ const out = await run2("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], 3e3);
30755
30935
  for (const line of out.split("\n")) if (line.startsWith("n")) return line.slice(1).trim();
30756
30936
  return "";
30757
30937
  }
@@ -30928,8 +31108,8 @@ var SessionMonitor = class {
30928
31108
  this.timer.unref?.();
30929
31109
  }
30930
31110
  async cwdFor(pid) {
30931
- const cached = this.cwdCache.get(pid);
30932
- if (cached !== void 0) return cached;
31111
+ const cached2 = this.cwdCache.get(pid);
31112
+ if (cached2 !== void 0) return cached2;
30933
31113
  const cwd = await processCwd(pid).catch(() => "");
30934
31114
  this.cwdCache.set(pid, cwd);
30935
31115
  return cwd;
@@ -31325,6 +31505,14 @@ async function startAgent() {
31325
31505
  if (a) {
31326
31506
  try {
31327
31507
  log.info(`timer "${t.label || id}" firing${a.kind ? ` (${a.kind})` : ""}`);
31508
+ displayWake.wake(30);
31509
+ await delay(3e3);
31510
+ const locked = await isSessionLocked();
31511
+ if (locked === true) {
31512
+ throw new Error(
31513
+ "the screen is locked, so the input would go to the lock screen instead of your app. For unattended scheduled work, set the machine to not require a password immediately after display sleep."
31514
+ );
31515
+ }
31328
31516
  if (typeof a.x === "number" && typeof a.y === "number") {
31329
31517
  const pinnedId = t.displayId;
31330
31518
  let restoreInput = null;
@@ -31359,8 +31547,8 @@ async function startAgent() {
31359
31547
  const message = error.message ?? String(error);
31360
31548
  log.warn("timer action failed:", message);
31361
31549
  hub.emit({
31362
- title: t.label || "WhipDesk timer",
31363
- body: `Timer reached zero, but its scheduled action failed: ${message.slice(0, 120)}`,
31550
+ title: t.label || "Scheduled work",
31551
+ body: `Time's up, but the scheduled action failed: ${message.slice(0, 120)}`,
31364
31552
  level: "error",
31365
31553
  source: "timer"
31366
31554
  });
@@ -31368,8 +31556,8 @@ async function startAgent() {
31368
31556
  }
31369
31557
  }
31370
31558
  hub.emit({
31371
- title: t.label || "WhipDesk timer",
31372
- body: a ? "Timer reached zero \u2014 scheduled action sent." : "Timer reached zero.",
31559
+ title: t.label || "Scheduled work",
31560
+ body: a ? "Time's up \u2014 scheduled action done." : "Time's up.",
31373
31561
  level: "success",
31374
31562
  source: "timer"
31375
31563
  });
@@ -31377,8 +31565,8 @@ async function startAgent() {
31377
31565
  for (const t of loadTimers(config.stateDir)) {
31378
31566
  if (t.fireAtMs <= Date.now()) {
31379
31567
  hub.emit({
31380
- title: t.label || "WhipDesk timer",
31381
- body: "This timer came due while WhipDesk was not running \u2014 its action was NOT performed.",
31568
+ title: t.label || "Scheduled work",
31569
+ body: "This came due while WhipDesk was not running \u2014 its action was NOT performed.",
31382
31570
  level: "error",
31383
31571
  source: "timer"
31384
31572
  });
@@ -31450,6 +31638,7 @@ async function startAgent() {
31450
31638
  displays,
31451
31639
  videoAvailable,
31452
31640
  video,
31641
+ hdrActive: false,
31453
31642
  get activeDisplay() {
31454
31643
  return activeDisplay;
31455
31644
  },
@@ -31457,6 +31646,7 @@ async function startAgent() {
31457
31646
  addController(controller) {
31458
31647
  controllers.add(controller);
31459
31648
  log.info(`controller connected (${controllers.size} active)`);
31649
+ void windowsHdrState().then((s) => ctx.hdrActive = s?.active === true);
31460
31650
  displayWake.setActive(true);
31461
31651
  for (const c of controllers) c.send({ type: "presence", watchers: controllers.size });
31462
31652
  controller.send({ type: "watchers", regions: regionWatcher.list() });
@@ -31672,6 +31862,14 @@ async function startAgent() {
31672
31862
  }
31673
31863
  log.info(`listening on :${config.port} (capture: ${capturer.backend}, input: ${input.name})`);
31674
31864
  log.info(pin.isSet ? "connection PIN: required" : "connection PIN: NONE (set one in a terminal)");
31865
+ void windowsHdrState().then((s) => {
31866
+ ctx.hdrActive = s?.active === true;
31867
+ if (s?.active) {
31868
+ log.warn(
31869
+ "HDR is ON on this desktop \u2014 the remote stream is tone-mapped to SDR and colors may look washed out. Turn HDR off if it bothers you."
31870
+ );
31871
+ }
31872
+ });
31675
31873
  return { server, config, presence, keepAwake, ctx };
31676
31874
  }
31677
31875
  function isLevel(value) {
@@ -31735,8 +31933,8 @@ async function main() {
31735
31933
  registry = await startDeviceRegistry({
31736
31934
  rtdb,
31737
31935
  identity,
31738
- name: (0, import_node_os8.hostname)(),
31739
- platform: (0, import_node_os8.platform)(),
31936
+ name: (0, import_node_os9.hostname)(),
31937
+ platform: (0, import_node_os9.platform)(),
31740
31938
  version: AGENT_VERSION,
31741
31939
  getLan: () => ({ ip: getLanIp(), port: config.port, token: config.token })
31742
31940
  });