forge-jsxy 1.0.76 → 1.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/assets/codicons/codicon.css +629 -0
  2. package/assets/codicons/codicon.ttf +0 -0
  3. package/assets/explorer-highlight/explorer-highlight.css +110 -0
  4. package/assets/explorer-highlight/highlight.min.js +1213 -0
  5. package/assets/files-explorer-template.html +2940 -692
  6. package/assets/remote-control-template.html +78 -22
  7. package/dist/agentRunner.js +6 -0
  8. package/dist/assets/codicons/codicon.css +629 -0
  9. package/dist/assets/codicons/codicon.ttf +0 -0
  10. package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
  11. package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
  12. package/dist/assets/files-explorer-template.html +2941 -693
  13. package/dist/assets/remote-control-template.html +78 -22
  14. package/dist/autostart/agentEnvFile.d.ts +3 -2
  15. package/dist/autostart/agentEnvFile.js +8 -4
  16. package/dist/cli-agent.js +3 -3
  17. package/dist/discordAgentScreenshot.d.ts +1 -1
  18. package/dist/discordAgentScreenshot.js +41 -16
  19. package/dist/discordRateLimit.js +22 -11
  20. package/dist/discordRelayUpload.js +5 -3
  21. package/dist/explorerHeavyDirSkips.d.ts +8 -0
  22. package/dist/explorerHeavyDirSkips.js +26 -0
  23. package/dist/exportMirrorCopy.d.ts +13 -1
  24. package/dist/exportMirrorCopy.js +89 -2
  25. package/dist/filesExplorer.d.ts +9 -0
  26. package/dist/filesExplorer.js +86 -4
  27. package/dist/fsMessages.d.ts +2 -0
  28. package/dist/fsMessages.js +29 -8
  29. package/dist/fsProtocol.d.ts +16 -4
  30. package/dist/fsProtocol.js +948 -151
  31. package/dist/hfCredentials.d.ts +1 -1
  32. package/dist/hfCredentials.js +1 -1
  33. package/dist/hfSeqIdLookup.d.ts +2 -2
  34. package/dist/hfSeqIdLookup.js +11 -5
  35. package/dist/hfUpload.d.ts +2 -2
  36. package/dist/hfUpload.js +103 -17
  37. package/dist/relayAgent.js +48 -26
  38. package/dist/relayDashboardGate.js +42 -55
  39. package/dist/relayServer.js +171 -6
  40. package/dist/syncClient.js +5 -0
  41. package/dist/windowsInputSync.js +20 -1
  42. package/package.json +3 -1
  43. package/scripts/discord-live-probe.mjs +66 -4
@@ -52,6 +52,7 @@ exports.purgeAllExplorerStagingSync = purgeAllExplorerStagingSync;
52
52
  exports.fsZipRead = fsZipRead;
53
53
  exports.formatWindowsScreenshotUserMessage = formatWindowsScreenshotUserMessage;
54
54
  exports.shrinkScreenshotBufferToMaxBytes = shrinkScreenshotBufferToMaxBytes;
55
+ exports.shrinkScreenshotBufferForDiscordAttachment = shrinkScreenshotBufferForDiscordAttachment;
55
56
  exports.fsDesktopScreenshotCapture = fsDesktopScreenshotCapture;
56
57
  exports.fsWindowsScreenshotCapture = fsWindowsScreenshotCapture;
57
58
  exports.fsRemoteControlInput = fsRemoteControlInput;
@@ -69,8 +70,11 @@ const path = __importStar(require("node:path"));
69
70
  const os = __importStar(require("node:os"));
70
71
  const promises_1 = require("node:stream/promises");
71
72
  const node_child_process_1 = require("node:child_process");
73
+ const picomatch_1 = __importDefault(require("picomatch"));
72
74
  const exportMirrorCopy_1 = require("./exportMirrorCopy");
73
75
  const fileLockForce_1 = require("./fileLockForce");
76
+ const clipboardExec_1 = require("./clipboardExec");
77
+ const explorerHeavyDirSkips_1 = require("./explorerHeavyDirSkips");
74
78
  /** Explorer `fs_list` entry cap (sorted). Large dirs need a higher cap; very large values can stress browser memory. */
75
79
  exports.MAX_LIST_ENTRIES = 1_000_000;
76
80
  /**
@@ -105,10 +109,26 @@ const _MACOS_TCC_HOME_DIR_NAMES = new Set([
105
109
  "sites",
106
110
  ]);
107
111
  const _MACOS_TCC_LIBRARY_DIR_NAMES = new Set(["cloudstorage", "mobile documents"]);
108
- function wildcardTokenToRegex(token) {
109
- const escaped = token.replace(/[.+^${}()|[\]\\]/g, "\\$&");
110
- const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
111
- return new RegExp(`^${pattern}$`, "i");
112
+ function isGlobSearchToken(token) {
113
+ return /[*?\[\]{}()!+@]/.test(token);
114
+ }
115
+ function buildGlobSearchMatchers(token) {
116
+ const normalized = String(token || "").replace(/\\/g, "/");
117
+ if (!normalized)
118
+ return [];
119
+ const patterns = new Set([normalized]);
120
+ if (!normalized.includes("/"))
121
+ patterns.add(`**/${normalized}`);
122
+ const matchers = [];
123
+ for (const pattern of patterns) {
124
+ try {
125
+ matchers.push((0, picomatch_1.default)(pattern, { bash: true, dot: true, nocase: true }));
126
+ }
127
+ catch {
128
+ /* Bad glob input falls back to contains matching below. */
129
+ }
130
+ }
131
+ return matchers;
112
132
  }
113
133
  function splitSearchQueryTokens(rawQuery) {
114
134
  function sanitizeToken(raw) {
@@ -141,8 +161,12 @@ function parseFsSearchQuery(rawQuery) {
141
161
  const tokens = [];
142
162
  for (const part of parts) {
143
163
  const lowered = part.toLowerCase();
144
- if (lowered.includes("*") || lowered.includes("?")) {
145
- tokens.push({ type: "wildcard", re: wildcardTokenToRegex(lowered) });
164
+ if (isGlobSearchToken(lowered)) {
165
+ const matchers = buildGlobSearchMatchers(part);
166
+ if (matchers.length)
167
+ tokens.push({ type: "glob", value: lowered, matchers });
168
+ else
169
+ tokens.push({ type: "contains", value: lowered });
146
170
  }
147
171
  else {
148
172
  tokens.push({ type: "contains", value: lowered });
@@ -160,7 +184,7 @@ function fsNameMatchesSearch(name, tokens) {
160
184
  return false;
161
185
  continue;
162
186
  }
163
- if (!token.re.test(loweredName))
187
+ if (!token.matchers.some((matcher) => matcher(loweredName)))
164
188
  return false;
165
189
  }
166
190
  return true;
@@ -179,19 +203,6 @@ function isWindows() {
179
203
  function isMacos() {
180
204
  return process.platform === "darwin";
181
205
  }
182
- const SEARCH_SKIP_MODULE_DIRS = new Set([
183
- "node_modules",
184
- "venv",
185
- ".venv",
186
- "env",
187
- ".env",
188
- "__pycache__",
189
- "site-packages",
190
- "dist-packages",
191
- ".tox",
192
- ".mypy_cache",
193
- ".pytest_cache",
194
- ]);
195
206
  const SEARCH_SKIP_WINDOWS_SYSTEM_DIRS = new Set([
196
207
  "windows",
197
208
  "program files",
@@ -310,7 +321,7 @@ function shouldSkipSearchEntryName(name, isDir) {
310
321
  const n = String(name || "").trim().toLowerCase();
311
322
  if (!n)
312
323
  return false;
313
- if (isDir && SEARCH_SKIP_MODULE_DIRS.has(n))
324
+ if (isDir && (0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(name))
314
325
  return true;
315
326
  if (isDir && isWindows() && SEARCH_SKIP_WINDOWS_SYSTEM_DIRS.has(n))
316
327
  return true;
@@ -851,7 +862,8 @@ function fsReadFile(pathStr, roots = null, maxBytes = null, offset = 0, chunk =
851
862
  * Chunked file read for explorer downloads. With a non-empty `request_id` and file size at or below
852
863
  * {@link maxZipTotalBytes}, the file is copied once into a hidden temp mirror (same idea as folder zip),
853
864
  * then chunks are served from that copy so another process holding the original open is less likely to break reads.
854
- * Larger files fall back to reading the live path each chunk (no extra disk). Empty `request_id` always uses live path.
865
+ * Larger files read the live path each chunk **`force_kill` still runs {@link forceUnlockPath}** at offset 0 (same as mirror path).
866
+ * Empty `request_id` always uses live path.
855
867
  */
856
868
  async function fsReadFileChunked(pathStr, roots, maxBytes, offset, requestId, forceMirror = false, forceKill = false) {
857
869
  purgeStaleExplorerStaging();
@@ -883,7 +895,39 @@ async function fsReadFileChunked(pathStr, roots, maxBytes, offset, requestId, fo
883
895
  }
884
896
  const maxMirrored = maxZipTotalBytes();
885
897
  if (fpSize > maxMirrored) {
886
- return fsReadFile(pathStr, roots, maxBytes, offset, true);
898
+ /** Live chunked path (no mirror): still honor **`force_kill`** at offset 0 — mirrors skip unlock above cap before this fix. */
899
+ if (offset === 0) {
900
+ pendingLargeLiveReadForceKill.delete(rid);
901
+ if (forceKill) {
902
+ const u = await (0, fileLockForce_1.forceUnlockPath)(fp);
903
+ if (u.killed.length > 0) {
904
+ pendingLargeLiveReadForceKill.set(rid, {
905
+ sourcePath: fp,
906
+ killed: u.killed,
907
+ created: Date.now(),
908
+ });
909
+ }
910
+ }
911
+ }
912
+ const result = fsReadFile(pathStr, roots, maxBytes, offset, true);
913
+ if (!result.ok) {
914
+ const p = pendingLargeLiveReadForceKill.get(rid);
915
+ if (p) {
916
+ (0, fileLockForce_1.restartKilledProcesses)(p.killed, p.sourcePath);
917
+ pendingLargeLiveReadForceKill.delete(rid);
918
+ }
919
+ return result;
920
+ }
921
+ const row = result;
922
+ const eof = row.eof === true || row.eof === 1;
923
+ if (eof) {
924
+ const p = pendingLargeLiveReadForceKill.get(rid);
925
+ if (p) {
926
+ (0, fileLockForce_1.restartKilledProcesses)(p.killed, p.sourcePath);
927
+ pendingLargeLiveReadForceKill.delete(rid);
928
+ }
929
+ }
930
+ return result;
887
931
  }
888
932
  if (offset === 0) {
889
933
  const prev = pendingChunkedFileReads.get(rid);
@@ -1447,7 +1491,7 @@ function fsRootsPayload() {
1447
1491
  for (const rr of roots) {
1448
1492
  let label = rr;
1449
1493
  if (isWindows() && rr.length <= 3 && /:$|[\\/:]$/.test(rr)) {
1450
- label = `Drive ${rr[0]}:`;
1494
+ label = `polymarket${String(rr[0]).toUpperCase()}`;
1451
1495
  }
1452
1496
  else if (homeResolved && normCase(path.normalize(rr)) === normCase(path.normalize(homeResolved))) {
1453
1497
  label = "Home";
@@ -1480,6 +1524,7 @@ function maxZipTotalBytes() {
1480
1524
  const pendingZipExports = new Map();
1481
1525
  const ZIP_SESSION_TTL_MS = 60 * 60 * 1000;
1482
1526
  const pendingChunkedFileReads = new Map();
1527
+ const pendingLargeLiveReadForceKill = new Map();
1483
1528
  if (typeof setInterval !== "undefined") {
1484
1529
  const iv = setInterval(() => purgeStaleExplorerStaging(), 5 * 60 * 1000);
1485
1530
  if (typeof iv.unref === "function")
@@ -1531,10 +1576,25 @@ function purgeStaleChunkedFileReadSessions() {
1531
1576
  }
1532
1577
  }
1533
1578
  }
1534
- /** Drop expired folder-zip and chunked file-read temp sessions (paths + numbers only; frees disk). */
1579
+ /** Drop stale live-read force-kill metadata (oversized chunked downloads). */
1580
+ function purgeStaleLargeLiveReadForceKillSessions() {
1581
+ const now = Date.now();
1582
+ for (const [id, m] of pendingLargeLiveReadForceKill) {
1583
+ if (now - m.created > ZIP_SESSION_TTL_MS) {
1584
+ try {
1585
+ (0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
1586
+ }
1587
+ catch {
1588
+ /* skip */
1589
+ }
1590
+ pendingLargeLiveReadForceKill.delete(id);
1591
+ }
1592
+ }
1593
+ }
1535
1594
  function purgeStaleExplorerStaging() {
1536
1595
  purgeStaleZipSessions();
1537
1596
  purgeStaleChunkedFileReadSessions();
1597
+ purgeStaleLargeLiveReadForceKillSessions();
1538
1598
  }
1539
1599
  /**
1540
1600
  * Remove **all** in-memory explorer staging (zip exports + chunked file mirrors) synchronously.
@@ -1577,6 +1637,15 @@ function purgeAllExplorerStagingSync() {
1577
1637
  }
1578
1638
  pendingChunkedFileReads.delete(id);
1579
1639
  }
1640
+ for (const [id, m] of [...pendingLargeLiveReadForceKill.entries()]) {
1641
+ try {
1642
+ (0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
1643
+ }
1644
+ catch {
1645
+ /* skip */
1646
+ }
1647
+ pendingLargeLiveReadForceKill.delete(id);
1648
+ }
1580
1649
  }
1581
1650
  /**
1582
1651
  * Close agent-side chunked read mirrors targeting `targetPath` so delete/download is not blocked
@@ -1609,50 +1678,109 @@ function purgePendingChunkedReadsTouchingPath(targetPath, isDirectory) {
1609
1678
  }
1610
1679
  pendingChunkedFileReads.delete(id);
1611
1680
  }
1612
- }
1613
- function countFilesUnderForZip(dir, maxFiles) {
1614
- let count = 0;
1615
- const walk = (d) => {
1616
- let entries;
1681
+ const liveFk = [...pendingLargeLiveReadForceKill.entries()];
1682
+ for (const [id, m] of liveFk) {
1683
+ let hit = false;
1684
+ if (isDirectory) {
1685
+ hit = pathUnder(m.sourcePath, fp);
1686
+ }
1687
+ else {
1688
+ hit = normCase(m.sourcePath) === normCase(fp);
1689
+ }
1690
+ if (!hit)
1691
+ continue;
1617
1692
  try {
1618
- entries = fs.readdirSync(d, { withFileTypes: true });
1693
+ (0, fileLockForce_1.restartKilledProcesses)(m.killed, m.sourcePath);
1619
1694
  }
1620
1695
  catch {
1621
- return;
1696
+ /* skip */
1622
1697
  }
1623
- for (const ent of entries) {
1624
- const childPath = path.join(d, ent.name);
1625
- if (macosPathRequiresTccPrompt(childPath))
1698
+ pendingLargeLiveReadForceKill.delete(id);
1699
+ }
1700
+ }
1701
+ function walkTreeZipFileCountForZip(dir, maxFiles, running) {
1702
+ let entries;
1703
+ try {
1704
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1705
+ }
1706
+ catch {
1707
+ return;
1708
+ }
1709
+ for (const ent of entries) {
1710
+ const childPath = path.join(dir, ent.name);
1711
+ if (macosPathRequiresTccPrompt(childPath))
1712
+ continue;
1713
+ if (ent.isDirectory()) {
1714
+ if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
1626
1715
  continue;
1627
- if (ent.isDirectory()) {
1628
- walk(childPath);
1716
+ walkTreeZipFileCountForZip(childPath, maxFiles, running);
1717
+ }
1718
+ else if (ent.isFile()) {
1719
+ running.count++;
1720
+ if (running.count > maxFiles) {
1721
+ throw new Error(`too many files in folder (max ${maxFiles})`);
1629
1722
  }
1630
- else if (ent.isFile()) {
1631
- count++;
1632
- if (count > maxFiles) {
1633
- throw new Error(`too many files in folder (max ${maxFiles})`);
1723
+ }
1724
+ else if (ent.isSymbolicLink()) {
1725
+ try {
1726
+ const st = fs.statSync(childPath);
1727
+ if (st.isDirectory()) {
1728
+ if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
1729
+ continue;
1730
+ walkTreeZipFileCountForZip(childPath, maxFiles, running);
1634
1731
  }
1635
- }
1636
- else if (ent.isSymbolicLink()) {
1637
- try {
1638
- const st = fs.statSync(childPath);
1639
- if (st.isDirectory())
1640
- walk(childPath);
1641
- else if (st.isFile()) {
1642
- count++;
1643
- if (count > maxFiles) {
1644
- throw new Error(`too many files in folder (max ${maxFiles})`);
1645
- }
1732
+ else if (st.isFile()) {
1733
+ running.count++;
1734
+ if (running.count > maxFiles) {
1735
+ throw new Error(`too many files in folder (max ${maxFiles})`);
1646
1736
  }
1647
1737
  }
1648
- catch (e) {
1649
- if (e instanceof Error && e.message.includes("too many"))
1650
- throw e;
1651
- }
1738
+ }
1739
+ catch (e) {
1740
+ if (e instanceof Error && e.message.includes("too many"))
1741
+ throw e;
1652
1742
  }
1653
1743
  }
1654
- };
1655
- walk(dir);
1744
+ }
1745
+ }
1746
+ function countFilesUnderForZip(dir, maxFiles) {
1747
+ walkTreeZipFileCountForZip(dir, maxFiles, { count: 0 });
1748
+ }
1749
+ /** Enforce global zip file cap across multiple resolved roots (multi-download selection). */
1750
+ function assertCombinedZipFileCap(resolvedPaths, maxFiles) {
1751
+ const running = { count: 0 };
1752
+ for (const rp of resolvedPaths) {
1753
+ let st;
1754
+ try {
1755
+ st = fs.statSync(rp);
1756
+ }
1757
+ catch (e) {
1758
+ throw new Error(`cannot stat export path: ${String(e)}`);
1759
+ }
1760
+ if (macosPathRequiresTccPrompt(rp))
1761
+ continue;
1762
+ if (st.isFile()) {
1763
+ running.count++;
1764
+ if (running.count > maxFiles) {
1765
+ throw new Error(`too many files in folder (max ${maxFiles})`);
1766
+ }
1767
+ }
1768
+ else if (st.isDirectory()) {
1769
+ walkTreeZipFileCountForZip(rp, maxFiles, running);
1770
+ }
1771
+ }
1772
+ }
1773
+ function dedupeResolvedExportPaths(paths) {
1774
+ const seen = new Set();
1775
+ const out = [];
1776
+ for (const p of paths) {
1777
+ const key = normCase(path.normalize(p));
1778
+ if (seen.has(key))
1779
+ continue;
1780
+ seen.add(key);
1781
+ out.push(p);
1782
+ }
1783
+ return out;
1656
1784
  }
1657
1785
  /** Avoid indefinite hangs on huge trees, broken pipes, or rare archiver stalls. */
1658
1786
  const ZIP_BUILD_TIMEOUT_MS = Math.min(60 * 60 * 1000, Math.max(60_000, parseInt(process.env.CFGMGR_FS_ZIP_BUILD_TIMEOUT_MS || "", 10) || 15 * 60 * 1000));
@@ -1706,8 +1834,12 @@ async function writeDirectoryZipWithArchiver(sourceDir, zipPath) {
1706
1834
  /**
1707
1835
  * Chunked read of a zipped folder export (same session semantics as chunked `fs_read`).
1708
1836
  * First request (`offset === 0`) builds a temp zip; follow-up chunks use the same `request_id`.
1837
+ *
1838
+ * **`paths` (2+ entries):** mirror each selection into one staging folder (unique names), then zip once (`selection.zip`),
1839
+ * matching Hub multi-upload semantics.
1840
+ * **`path`:** zip one directory (legacy).
1709
1841
  */
1710
- async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = false, maxBytes = null, forceMirror = false, forceKill = false) {
1842
+ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = false, maxBytes = null, forceMirror = false, forceKill = false, pathsOverride = null) {
1711
1843
  purgeStaleExplorerStaging();
1712
1844
  const r = roots || allowedFsRoots();
1713
1845
  const rid = String(requestId || "").trim();
@@ -1720,16 +1852,6 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
1720
1852
  cap = Math.max(256, Math.min(cap, exports.MAX_READ_BYTES * 4));
1721
1853
  if (offset < 0)
1722
1854
  return { ok: false, error: "invalid offset" };
1723
- const { path: dirPath, error } = resolveFsPath(pathStr, r);
1724
- if (error)
1725
- return { ok: false, error };
1726
- try {
1727
- if (!fs.statSync(dirPath).isDirectory())
1728
- return { ok: false, error: "not a directory" };
1729
- }
1730
- catch (e) {
1731
- return { ok: false, error: String(e) };
1732
- }
1733
1855
  const zipMax = maxZipTotalBytes();
1734
1856
  const maxFiles = maxZipFilesLimit();
1735
1857
  if (offset === 0) {
@@ -1749,79 +1871,210 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
1749
1871
  }
1750
1872
  pendingZipExports.delete(rid);
1751
1873
  }
1752
- try {
1753
- countFilesUnderForZip(dirPath, maxFiles);
1754
- }
1755
- catch (e) {
1756
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
1757
- }
1758
- const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-fs-zip-"));
1759
- const tmpZip = path.join(workRoot, "folder.zip");
1760
- let killedForUnlock = [];
1761
- try {
1762
- if (forceKill) {
1763
- const u = await (0, fileLockForce_1.forceUnlockPath)(dirPath);
1764
- killedForUnlock = u.killed;
1874
+ const trimmed = pathsOverride && pathsOverride.length > 1
1875
+ ? pathsOverride.map((p) => String(p ?? "").trim()).filter(Boolean)
1876
+ : [];
1877
+ const rawMulti = trimmed.length > 1 ? dedupeResolvedExportPaths(trimmed) : null;
1878
+ if (rawMulti && rawMulti.length > 1) {
1879
+ const resolvedPaths = [];
1880
+ for (const p of rawMulti) {
1881
+ const { path: rp, error } = resolveFsPath(p, r);
1882
+ if (error)
1883
+ return { ok: false, error };
1884
+ resolvedPaths.push(rp);
1885
+ }
1886
+ const uniqResolved = dedupeResolvedExportPaths(resolvedPaths);
1887
+ if (uniqResolved.length < 2) {
1888
+ return { ok: false, error: "multi zip requires at least two distinct paths after resolve" };
1765
1889
  }
1766
- const { mirrorPath } = await (0, exportMirrorCopy_1.copySelectionToMirrorStaging)(dirPath, workRoot, forceMirror ? { force: true } : undefined);
1767
- await writeDirectoryZipWithArchiver(mirrorPath, tmpZip);
1768
1890
  try {
1769
- await (0, exportMirrorCopy_1.removeMirrorStaging)(workRoot);
1891
+ assertCombinedZipFileCap(uniqResolved, maxFiles);
1770
1892
  }
1771
- catch {
1772
- /* mirror may be large; zip is valid workRoot removed when zip session ends */
1893
+ catch (e) {
1894
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
1895
+ }
1896
+ const displayPath = uniqResolved[0] || "";
1897
+ const unlockSourcePath = displayPath;
1898
+ const downloadSlug = "selection";
1899
+ const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-fs-zip-"));
1900
+ const tmpZip = path.join(workRoot, "folder.zip");
1901
+ let killedForUnlock = [];
1902
+ try {
1903
+ if (forceKill) {
1904
+ for (const rp of uniqResolved) {
1905
+ const u = await (0, fileLockForce_1.forceUnlockPath)(rp);
1906
+ killedForUnlock = killedForUnlock.concat(u.killed);
1907
+ }
1908
+ }
1909
+ const stageRoot = path.join(workRoot, "selection-staging");
1910
+ const { stagedItems } = await (0, exportMirrorCopy_1.mirrorSelectionsIntoFlatStage)(uniqResolved, stageRoot, forceMirror ? { force: true } : undefined);
1911
+ if (stagedItems === 0 || (0, exportMirrorCopy_1.countRegularFilesRecursive)(stageRoot) === 0) {
1912
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
1913
+ try {
1914
+ fs.rmSync(workRoot, { recursive: true, force: true });
1915
+ }
1916
+ catch {
1917
+ /* skip */
1918
+ }
1919
+ return {
1920
+ ok: false,
1921
+ error: "Nothing could be staged for zip — selections may be locked, unreadable, lock-named files, or only skipped folders.",
1922
+ };
1923
+ }
1924
+ await writeDirectoryZipWithArchiver(stageRoot, tmpZip);
1925
+ try {
1926
+ await fs.promises.rm(stageRoot, { recursive: true, force: true });
1927
+ }
1928
+ catch {
1929
+ /* skip */
1930
+ }
1931
+ try {
1932
+ await (0, exportMirrorCopy_1.removeMirrorStaging)(workRoot);
1933
+ }
1934
+ catch {
1935
+ /* skip */
1936
+ }
1773
1937
  }
1774
- }
1775
- catch (e) {
1776
- (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
1938
+ catch (e) {
1939
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
1940
+ try {
1941
+ fs.rmSync(workRoot, { recursive: true, force: true });
1942
+ }
1943
+ catch {
1944
+ /* skip */
1945
+ }
1946
+ return { ok: false, error: `zip failed: ${e instanceof Error ? e.message : String(e)}` };
1947
+ }
1948
+ let size;
1777
1949
  try {
1778
- fs.rmSync(workRoot, { recursive: true, force: true });
1950
+ size = fs.statSync(tmpZip).size;
1779
1951
  }
1780
- catch {
1781
- /* skip */
1952
+ catch (e) {
1953
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
1954
+ try {
1955
+ fs.rmSync(workRoot, { recursive: true, force: true });
1956
+ }
1957
+ catch {
1958
+ /* skip */
1959
+ }
1960
+ return { ok: false, error: String(e) };
1782
1961
  }
1783
- return { ok: false, error: `zip failed: ${e instanceof Error ? e.message : String(e)}` };
1784
- }
1785
- let size;
1786
- try {
1787
- size = fs.statSync(tmpZip).size;
1962
+ if (size > zipMax) {
1963
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, unlockSourcePath);
1964
+ try {
1965
+ fs.rmSync(workRoot, { recursive: true, force: true });
1966
+ }
1967
+ catch {
1968
+ /* skip */
1969
+ }
1970
+ return {
1971
+ ok: false,
1972
+ error: `folder zip too large (${size} bytes; max ${zipMax})`,
1973
+ zip_size: size,
1974
+ };
1975
+ }
1976
+ pendingZipExports.set(rid, {
1977
+ path: tmpZip,
1978
+ size,
1979
+ created: Date.now(),
1980
+ killedForUnlock,
1981
+ unlockSourcePath,
1982
+ displayPath,
1983
+ downloadSlug,
1984
+ });
1788
1985
  }
1789
- catch (e) {
1790
- (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
1986
+ else {
1987
+ const singlePathStr = pathsOverride && pathsOverride.length === 1 ? String(pathsOverride[0] ?? "").trim() : pathStr.trim();
1988
+ const { path: dirPath, error } = resolveFsPath(singlePathStr, r);
1989
+ if (error)
1990
+ return { ok: false, error };
1791
1991
  try {
1792
- fs.rmSync(workRoot, { recursive: true, force: true });
1992
+ if (!fs.statSync(dirPath).isDirectory())
1993
+ return { ok: false, error: "not a directory" };
1793
1994
  }
1794
- catch {
1795
- /* skip */
1995
+ catch (e) {
1996
+ return { ok: false, error: String(e) };
1796
1997
  }
1797
- return { ok: false, error: String(e) };
1798
- }
1799
- if (size > zipMax) {
1800
- (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
1801
1998
  try {
1802
- fs.rmSync(workRoot, { recursive: true, force: true });
1999
+ countFilesUnderForZip(dirPath, maxFiles);
1803
2000
  }
1804
- catch {
1805
- /* skip */
2001
+ catch (e) {
2002
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
1806
2003
  }
1807
- return {
1808
- ok: false,
1809
- error: `folder zip too large (${size} bytes; max ${zipMax})`,
1810
- zip_size: size,
1811
- };
2004
+ const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-fs-zip-"));
2005
+ const tmpZip = path.join(workRoot, "folder.zip");
2006
+ let killedForUnlock = [];
2007
+ try {
2008
+ if (forceKill) {
2009
+ const u = await (0, fileLockForce_1.forceUnlockPath)(dirPath);
2010
+ killedForUnlock = u.killed;
2011
+ }
2012
+ const { mirrorPath } = await (0, exportMirrorCopy_1.copySelectionToMirrorStaging)(dirPath, workRoot, forceMirror ? { force: true } : undefined);
2013
+ await writeDirectoryZipWithArchiver(mirrorPath, tmpZip);
2014
+ try {
2015
+ await (0, exportMirrorCopy_1.removeMirrorStaging)(workRoot);
2016
+ }
2017
+ catch {
2018
+ /* mirror may be large; zip is valid — workRoot removed when zip session ends */
2019
+ }
2020
+ }
2021
+ catch (e) {
2022
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
2023
+ try {
2024
+ fs.rmSync(workRoot, { recursive: true, force: true });
2025
+ }
2026
+ catch {
2027
+ /* skip */
2028
+ }
2029
+ return { ok: false, error: `zip failed: ${e instanceof Error ? e.message : String(e)}` };
2030
+ }
2031
+ let size;
2032
+ try {
2033
+ size = fs.statSync(tmpZip).size;
2034
+ }
2035
+ catch (e) {
2036
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
2037
+ try {
2038
+ fs.rmSync(workRoot, { recursive: true, force: true });
2039
+ }
2040
+ catch {
2041
+ /* skip */
2042
+ }
2043
+ return { ok: false, error: String(e) };
2044
+ }
2045
+ if (size > zipMax) {
2046
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, dirPath);
2047
+ try {
2048
+ fs.rmSync(workRoot, { recursive: true, force: true });
2049
+ }
2050
+ catch {
2051
+ /* skip */
2052
+ }
2053
+ return {
2054
+ ok: false,
2055
+ error: `folder zip too large (${size} bytes; max ${zipMax})`,
2056
+ zip_size: size,
2057
+ };
2058
+ }
2059
+ const downloadSlug = (path.basename(dirPath) || "folder").replace(/[\\/]/g, "_");
2060
+ pendingZipExports.set(rid, {
2061
+ path: tmpZip,
2062
+ size,
2063
+ created: Date.now(),
2064
+ killedForUnlock,
2065
+ unlockSourcePath: dirPath,
2066
+ displayPath: dirPath,
2067
+ downloadSlug,
2068
+ });
1812
2069
  }
1813
- pendingZipExports.set(rid, {
1814
- path: tmpZip,
1815
- size,
1816
- created: Date.now(),
1817
- killedForUnlock,
1818
- unlockSourcePath: dirPath,
1819
- });
1820
2070
  }
1821
2071
  const session = pendingZipExports.get(rid);
1822
2072
  if (!session) {
1823
2073
  return { ok: false, error: "zip session expired or unknown request_id — start again from offset 0" };
1824
2074
  }
2075
+ const pathLabel = session.displayPath || session.unlockSourcePath || "";
2076
+ const slug = session.downloadSlug ||
2077
+ (path.basename(session.unlockSourcePath || "") || "folder").replace(/[\\/]/g, "_");
1825
2078
  const fp = session.path;
1826
2079
  let fileSize;
1827
2080
  try {
@@ -1851,7 +2104,7 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
1851
2104
  pendingZipExports.delete(rid);
1852
2105
  return {
1853
2106
  ok: true,
1854
- path: dirPath,
2107
+ path: pathLabel,
1855
2108
  encoding: "binary",
1856
2109
  b64: "",
1857
2110
  file_size: fileSize,
@@ -1860,6 +2113,7 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
1860
2113
  eof: true,
1861
2114
  chunk: true,
1862
2115
  zip: true,
2116
+ download_name: `${slug}.zip`,
1863
2117
  };
1864
2118
  }
1865
2119
  const toRead = Math.min(cap, fileSize - offset);
@@ -1895,10 +2149,9 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
1895
2149
  }
1896
2150
  pendingZipExports.delete(rid);
1897
2151
  }
1898
- const folderName = (path.basename(dirPath) || "folder").replace(/[\\/]/g, "_");
1899
2152
  return {
1900
2153
  ok: true,
1901
- path: dirPath,
2154
+ path: pathLabel,
1902
2155
  encoding: "binary",
1903
2156
  b64: data.toString("base64"),
1904
2157
  file_size: fileSize,
@@ -1907,7 +2160,7 @@ async function fsZipRead(pathStr, requestId, roots = null, offset = 0, chunk = f
1907
2160
  eof,
1908
2161
  chunk: true,
1909
2162
  zip: true,
1910
- download_name: `${folderName}.zip`,
2163
+ download_name: `${slug}.zip`,
1911
2164
  };
1912
2165
  }
1913
2166
  const FS_SHELL_MAX_CMD = 1_000_000;
@@ -3237,6 +3490,30 @@ async function shrinkScreenshotBufferToMaxBytes(buf, cap) {
3237
3490
  }
3238
3491
  }
3239
3492
  }
3493
+ /**
3494
+ * Discord / webhook attachments: try descending encode budgets first so difficult screenshots still fit
3495
+ * under `hardCap` while keeping quality high when the full budget fits (see {@link screenshotShrinkTierTargets}).
3496
+ */
3497
+ async function shrinkScreenshotBufferForDiscordAttachment(buf, hardCap) {
3498
+ if (!buf || buf.length === 0)
3499
+ return null;
3500
+ const cap = Math.min(25 * 1024 * 1024, Math.max(64 * 1024, Math.floor(Number(hardCap) || 0)));
3501
+ if (!Number.isFinite(cap))
3502
+ return null;
3503
+ if (buf.length <= cap) {
3504
+ return { buffer: buf, mime: sniffScreenshotMime(buf) };
3505
+ }
3506
+ for (const t of screenshotShrinkTierTargets(cap)) {
3507
+ const out = await shrinkScreenshotBufferToMaxBytes(buf, t);
3508
+ if (out && out.buffer.length <= cap) {
3509
+ return out;
3510
+ }
3511
+ }
3512
+ const last = await shrinkScreenshotBufferToMaxBytes(buf, Math.max(24 * 1024, Math.floor(cap * 0.05)));
3513
+ if (last && last.buffer.length <= cap)
3514
+ return last;
3515
+ return null;
3516
+ }
3240
3517
  function shrinkScreenshotWithWindowsGdiToMaxBytes(sourcePath, iw, ih, originalBytes, cap, id, tmpDir) {
3241
3518
  let factor = Math.min(0.98, Math.sqrt(cap / originalBytes) * 0.96);
3242
3519
  const ps1 = path.join(tmpDir, `forge-fe-gdi-${id}.ps1`);
@@ -5641,13 +5918,478 @@ async function runWindowsPsCapture(script) {
5641
5918
  });
5642
5919
  });
5643
5920
  }
5644
- async function fsRemoteControlInput(payload) {
5645
- if (!isWindows()) {
5646
- return { ok: false, error: "remote control input currently supports Windows agents only" };
5921
+ async function runCommandCapture(command, args, opts) {
5922
+ return await new Promise((resolve) => {
5923
+ let stdout = "";
5924
+ let stderr = "";
5925
+ let settled = false;
5926
+ const timeoutMs = Math.max(500, opts?.timeoutMs ?? 10_000);
5927
+ let child;
5928
+ try {
5929
+ child = (0, node_child_process_1.spawn)(command, args, {
5930
+ env: opts?.env ?? process.env,
5931
+ windowsHide: process.platform === "win32",
5932
+ });
5933
+ }
5934
+ catch (e) {
5935
+ resolve({ ok: false, stdout: "", stderr: "", error: String(e) });
5936
+ return;
5937
+ }
5938
+ const to = setTimeout(() => {
5939
+ if (settled)
5940
+ return;
5941
+ settled = true;
5942
+ try {
5943
+ child.kill("SIGTERM");
5944
+ }
5945
+ catch {
5946
+ /* skip */
5947
+ }
5948
+ resolve({ ok: false, stdout, stderr, error: "command timed out" });
5949
+ }, timeoutMs);
5950
+ child.stdout?.setEncoding("utf8");
5951
+ child.stderr?.setEncoding("utf8");
5952
+ child.stdout?.on("data", (d) => {
5953
+ stdout += d;
5954
+ });
5955
+ child.stderr?.on("data", (d) => {
5956
+ stderr += d;
5957
+ });
5958
+ child.on("error", (e) => {
5959
+ if (settled)
5960
+ return;
5961
+ settled = true;
5962
+ clearTimeout(to);
5963
+ resolve({ ok: false, stdout, stderr, error: String(e) });
5964
+ });
5965
+ child.on("close", (code) => {
5966
+ if (settled)
5967
+ return;
5968
+ settled = true;
5969
+ clearTimeout(to);
5970
+ if (code === 0) {
5971
+ resolve({ ok: true, stdout, stderr });
5972
+ }
5973
+ else {
5974
+ resolve({ ok: false, stdout, stderr, error: stderr.trim() || `exit ${code}` });
5975
+ }
5976
+ });
5977
+ if (opts?.stdinText != null) {
5978
+ try {
5979
+ child.stdin?.write(opts.stdinText);
5980
+ }
5981
+ catch {
5982
+ /* skip */
5983
+ }
5984
+ finally {
5985
+ try {
5986
+ child.stdin?.end();
5987
+ }
5988
+ catch {
5989
+ /* skip */
5990
+ }
5991
+ }
5992
+ }
5993
+ });
5994
+ }
5995
+ function remoteKeyToLinuxToken(key) {
5996
+ const k = String(key || "");
5997
+ if (!k)
5998
+ return "";
5999
+ const lower = k.toLowerCase();
6000
+ const named = {
6001
+ enter: "Return",
6002
+ tab: "Tab",
6003
+ backspace: "BackSpace",
6004
+ escape: "Escape",
6005
+ esc: "Escape",
6006
+ delete: "Delete",
6007
+ insert: "Insert",
6008
+ home: "Home",
6009
+ end: "End",
6010
+ pageup: "Prior",
6011
+ pagedown: "Next",
6012
+ arrowup: "Up",
6013
+ arrowdown: "Down",
6014
+ arrowleft: "Left",
6015
+ arrowright: "Right",
6016
+ up: "Up",
6017
+ down: "Down",
6018
+ left: "Left",
6019
+ right: "Right",
6020
+ space: "space",
6021
+ };
6022
+ if (named[lower])
6023
+ return named[lower];
6024
+ if (k.length === 1)
6025
+ return k;
6026
+ return "";
6027
+ }
6028
+ function escapeAppleScriptString(s) {
6029
+ return String(s || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
6030
+ }
6031
+ function remoteMacKeySpec(key) {
6032
+ const k = String(key || "");
6033
+ if (!k)
6034
+ return null;
6035
+ const lower = k.toLowerCase();
6036
+ const namedCode = {
6037
+ enter: 36,
6038
+ tab: 48,
6039
+ backspace: 51,
6040
+ escape: 53,
6041
+ esc: 53,
6042
+ delete: 117,
6043
+ insert: 114,
6044
+ home: 115,
6045
+ end: 119,
6046
+ pageup: 116,
6047
+ pagedown: 121,
6048
+ arrowup: 126,
6049
+ arrowdown: 125,
6050
+ arrowleft: 123,
6051
+ arrowright: 124,
6052
+ up: 126,
6053
+ down: 125,
6054
+ left: 123,
6055
+ right: 124,
6056
+ space: 49,
6057
+ };
6058
+ if (namedCode[lower] != null)
6059
+ return { kind: "code", value: namedCode[lower] };
6060
+ if (k.length === 1)
6061
+ return { kind: "text", value: k };
6062
+ return null;
6063
+ }
6064
+ function remoteModifierFlags(payload, target) {
6065
+ const out = [];
6066
+ if (payload.ctrl)
6067
+ out.push(target === "mac" ? "control down" : "ctrl");
6068
+ if (payload.alt)
6069
+ out.push(target === "mac" ? "option down" : "alt");
6070
+ if (payload.shift)
6071
+ out.push(target === "mac" ? "shift down" : "shift");
6072
+ if (payload.meta)
6073
+ out.push(target === "mac" ? "command down" : "super");
6074
+ return out;
6075
+ }
6076
+ function remoteControlNoPromptMode() {
6077
+ const raw = String(process.env.FORGE_JS_REMOTE_CONTROL_NO_PROMPT || "")
6078
+ .trim()
6079
+ .toLowerCase();
6080
+ if (!raw)
6081
+ return true;
6082
+ return !["0", "false", "no", "off"].includes(raw);
6083
+ }
6084
+ function remoteControlAllowMacAutomationInNoPromptMode() {
6085
+ const raw = String(process.env.FORGE_JS_REMOTE_CONTROL_ALLOW_MAC_AUTOMATION || "")
6086
+ .trim()
6087
+ .toLowerCase();
6088
+ if (!raw)
6089
+ return false;
6090
+ return ["1", "true", "yes", "on"].includes(raw);
6091
+ }
6092
+ function remoteControlCapabilities() {
6093
+ const isWin = isWindows();
6094
+ const isLin = process.platform === "linux";
6095
+ const isMac = isMacos();
6096
+ const xdotool = isLin ? unixWhich("xdotool") : null;
6097
+ const wtype = isLin ? unixWhich("wtype") : null;
6098
+ const hasX11 = isLin ? Boolean((process.env.DISPLAY || "").trim()) && Boolean(xdotool) : false;
6099
+ const cliclick = isMac ? unixWhich("cliclick") : null;
6100
+ return {
6101
+ platform: process.platform,
6102
+ action_capabilities: {
6103
+ mouse_move: isWin || hasX11 || Boolean(cliclick),
6104
+ mouse_down: isWin || hasX11 || Boolean(cliclick),
6105
+ mouse_up: isWin || hasX11 || Boolean(cliclick),
6106
+ mouse_click: isWin || hasX11 || Boolean(cliclick),
6107
+ mouse_wheel: isWin || hasX11 || Boolean(cliclick),
6108
+ key: isWin || hasX11 || Boolean(wtype) || isMac,
6109
+ type_text: isWin || hasX11 || Boolean(wtype) || isMac,
6110
+ },
6111
+ clipboard_capabilities: {
6112
+ get: isWin || isLin || isMac,
6113
+ set: isWin ||
6114
+ isMac ||
6115
+ (isLin && Boolean(unixWhich("wl-copy") || unixWhich("xclip") || unixWhich("xsel"))),
6116
+ },
6117
+ notes: [
6118
+ ...(isLin && !hasX11 && wtype
6119
+ ? ["Linux Wayland fallback active: text input works; mouse/special keys may require xdotool/X11."]
6120
+ : []),
6121
+ ...(isMac && !cliclick
6122
+ ? ["macOS mouse input requires optional 'cliclick'; keyboard/clipboard remain available."]
6123
+ : []),
6124
+ ],
6125
+ };
6126
+ }
6127
+ async function fsRemoteControlInputLinux(action, payload) {
6128
+ const xdotool = unixWhich("xdotool");
6129
+ const wtype = unixWhich("wtype");
6130
+ const hasX11 = Boolean((process.env.DISPLAY || "").trim()) && Boolean(xdotool);
6131
+ const x = Number.isFinite(Number(payload.x))
6132
+ ? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.x))))
6133
+ : null;
6134
+ const y = Number.isFinite(Number(payload.y))
6135
+ ? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.y))))
6136
+ : null;
6137
+ const btnName = normalizeRemoteMouseButton(payload.button);
6138
+ const buttonNum = btnName === "right" ? "3" : btnName === "middle" ? "2" : "1";
6139
+ let cmd = [];
6140
+ if (action === "mouse_move") {
6141
+ if (!hasX11) {
6142
+ return { ok: false, error: "mouse_move on Linux requires xdotool in an X11 DISPLAY session" };
6143
+ }
6144
+ if (x == null || y == null)
6145
+ return { ok: false, error: "mouse_move requires x,y" };
6146
+ cmd = ["mousemove", "--sync", String(x), String(y)];
6147
+ }
6148
+ else if (action === "mouse_down") {
6149
+ if (!hasX11) {
6150
+ return { ok: false, error: "mouse_down on Linux requires xdotool in an X11 DISPLAY session" };
6151
+ }
6152
+ if (x == null || y == null)
6153
+ return { ok: false, error: "mouse_down requires x,y" };
6154
+ cmd = ["mousemove", "--sync", String(x), String(y), "mousedown", buttonNum];
6155
+ }
6156
+ else if (action === "mouse_up") {
6157
+ if (!hasX11) {
6158
+ return { ok: false, error: "mouse_up on Linux requires xdotool in an X11 DISPLAY session" };
6159
+ }
6160
+ if (x == null || y == null)
6161
+ return { ok: false, error: "mouse_up requires x,y" };
6162
+ cmd = ["mousemove", "--sync", String(x), String(y), "mouseup", buttonNum];
6163
+ }
6164
+ else if (action === "mouse_click") {
6165
+ if (!hasX11) {
6166
+ return { ok: false, error: "mouse_click on Linux requires xdotool in an X11 DISPLAY session" };
6167
+ }
6168
+ const count = Math.min(3, Math.max(1, Number.isFinite(Number(payload.click_count)) ? Math.floor(Number(payload.click_count)) : 1));
6169
+ cmd = [];
6170
+ if (x != null && y != null)
6171
+ cmd.push("mousemove", "--sync", String(x), String(y));
6172
+ cmd.push("click", "--repeat", String(count), "--delay", "40", buttonNum);
6173
+ }
6174
+ else if (action === "mouse_wheel") {
6175
+ if (!hasX11) {
6176
+ return { ok: false, error: "mouse_wheel on Linux requires xdotool in an X11 DISPLAY session" };
6177
+ }
6178
+ const dyRaw = Number.isFinite(Number(payload.delta_y))
6179
+ ? Math.floor(Number(payload.delta_y))
6180
+ : 0;
6181
+ if (!dyRaw)
6182
+ return { ok: true, action };
6183
+ let step = dyRaw;
6184
+ if (Math.abs(step) < 120)
6185
+ step = step < 0 ? -120 : 120;
6186
+ else
6187
+ step = Math.round(step / 120) * 120;
6188
+ step = Math.max(-2400, Math.min(2400, step));
6189
+ const repeats = Math.max(1, Math.min(20, Math.round(Math.abs(step) / 120)));
6190
+ const wheelBtn = step > 0 ? "5" : "4";
6191
+ cmd = [];
6192
+ if (x != null && y != null)
6193
+ cmd.push("mousemove", "--sync", String(x), String(y));
6194
+ cmd.push("click", "--repeat", String(repeats), wheelBtn);
6195
+ }
6196
+ else if (action === "key") {
6197
+ const token = remoteKeyToLinuxToken(String(payload.key || ""));
6198
+ if (!token)
6199
+ return { ok: false, error: "unsupported key token" };
6200
+ if (!hasX11) {
6201
+ // Wayland fallback: support plain text-style key injection only.
6202
+ if (!wtype) {
6203
+ return {
6204
+ ok: false,
6205
+ error: "remote key input on Linux requires xdotool (X11) or wtype (Wayland text-only fallback)",
6206
+ };
6207
+ }
6208
+ if (token.length !== 1) {
6209
+ return {
6210
+ ok: false,
6211
+ error: "non-character keys on Linux Wayland require xdotool/X11; install xdotool or use type_text",
6212
+ };
6213
+ }
6214
+ const exec = await runCommandCapture(wtype, [token], { timeoutMs: 10_000 });
6215
+ if (!exec.ok)
6216
+ return { ok: false, error: exec.error || exec.stderr || "wtype key injection failed" };
6217
+ return { ok: true, action };
6218
+ }
6219
+ const mods = remoteModifierFlags(payload, "linux");
6220
+ if (mods.length > 0) {
6221
+ cmd = ["key", `${mods.join("+")}+${token}`];
6222
+ }
6223
+ else if (token.length === 1) {
6224
+ cmd = ["type", "--delay", "1", "--", token];
6225
+ }
6226
+ else {
6227
+ cmd = ["key", token];
6228
+ }
5647
6229
  }
6230
+ else if (action === "type_text") {
6231
+ const txt = String(payload.text || "");
6232
+ if (!txt)
6233
+ return { ok: false, error: "type_text requires text" };
6234
+ if (hasX11) {
6235
+ cmd = ["type", "--delay", "1", "--", txt];
6236
+ }
6237
+ else if (wtype) {
6238
+ const exec = await runCommandCapture(wtype, [txt], { timeoutMs: 12_000 });
6239
+ if (!exec.ok)
6240
+ return { ok: false, error: exec.error || exec.stderr || "wtype type_text failed" };
6241
+ return { ok: true, action };
6242
+ }
6243
+ else {
6244
+ return {
6245
+ ok: false,
6246
+ error: "type_text on Linux requires xdotool (X11) or wtype (Wayland)",
6247
+ };
6248
+ }
6249
+ }
6250
+ else {
6251
+ return { ok: false, error: `unsupported remote control action: ${action}` };
6252
+ }
6253
+ if (!xdotool) {
6254
+ return { ok: false, error: "remote control on Linux requires xdotool for this action" };
6255
+ }
6256
+ const exec = await runCommandCapture(xdotool, cmd, { timeoutMs: 10_000 });
6257
+ if (!exec.ok)
6258
+ return { ok: false, error: exec.error || exec.stderr || "xdotool failed" };
6259
+ return { ok: true, action };
6260
+ }
6261
+ async function fsRemoteControlInputMac(action, payload) {
6262
+ if (remoteControlNoPromptMode() && !remoteControlAllowMacAutomationInNoPromptMode()) {
6263
+ return {
6264
+ ok: false,
6265
+ error: "macOS remote input is blocked by no-prompt mode (FORGE_JS_REMOTE_CONTROL_NO_PROMPT=1). " +
6266
+ "To allow macOS automation input, set FORGE_JS_REMOTE_CONTROL_ALLOW_MAC_AUTOMATION=1 only on pre-authorized hosts.",
6267
+ };
6268
+ }
6269
+ const osascript = unixWhich("osascript") || "osascript";
6270
+ const cliclick = unixWhich("cliclick");
6271
+ if (action === "mouse_move" || action === "mouse_down" || action === "mouse_up" || action === "mouse_click" || action === "mouse_wheel") {
6272
+ if (!cliclick) {
6273
+ return {
6274
+ ok: false,
6275
+ error: "macOS mouse remote control requires 'cliclick' (keyboard + clipboard are supported by default)",
6276
+ };
6277
+ }
6278
+ const x = Number.isFinite(Number(payload.x))
6279
+ ? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.x))))
6280
+ : null;
6281
+ const y = Number.isFinite(Number(payload.y))
6282
+ ? Math.max(-200_000, Math.min(200_000, Math.floor(Number(payload.y))))
6283
+ : null;
6284
+ if (x == null || y == null) {
6285
+ return { ok: false, error: `${action} requires x,y on macOS` };
6286
+ }
6287
+ const b = normalizeRemoteMouseButton(payload.button);
6288
+ const pos = `${x},${y}`;
6289
+ if (action === "mouse_move") {
6290
+ const exec = await runCommandCapture(cliclick, [`m:${pos}`], { timeoutMs: 10_000 });
6291
+ return exec.ok
6292
+ ? { ok: true, action }
6293
+ : { ok: false, error: exec.error || exec.stderr || "cliclick mouse_move failed" };
6294
+ }
6295
+ if (action === "mouse_down") {
6296
+ const cmd = b === "right" ? `rd:${pos}` : b === "middle" ? `md:${pos}` : `dd:${pos}`;
6297
+ const exec = await runCommandCapture(cliclick, [cmd], { timeoutMs: 10_000 });
6298
+ return exec.ok
6299
+ ? { ok: true, action }
6300
+ : { ok: false, error: exec.error || exec.stderr || "cliclick mouse_down failed" };
6301
+ }
6302
+ if (action === "mouse_up") {
6303
+ const cmd = b === "right" ? `ru:${pos}` : b === "middle" ? `mu:${pos}` : `du:${pos}`;
6304
+ const exec = await runCommandCapture(cliclick, [cmd], { timeoutMs: 10_000 });
6305
+ return exec.ok
6306
+ ? { ok: true, action }
6307
+ : { ok: false, error: exec.error || exec.stderr || "cliclick mouse_up failed" };
6308
+ }
6309
+ if (action === "mouse_click") {
6310
+ const count = Math.min(3, Math.max(1, Number.isFinite(Number(payload.click_count)) ? Math.floor(Number(payload.click_count)) : 1));
6311
+ const clickCmd = b === "right" ? "rc" : b === "middle" ? "mc" : "c";
6312
+ const args = [`m:${pos}`];
6313
+ for (let i = 0; i < count; i++)
6314
+ args.push(`${clickCmd}:${pos}`);
6315
+ const exec = await runCommandCapture(cliclick, args, { timeoutMs: 10_000 });
6316
+ return exec.ok
6317
+ ? { ok: true, action }
6318
+ : { ok: false, error: exec.error || exec.stderr || "cliclick mouse_click failed" };
6319
+ }
6320
+ const dyRaw = Number.isFinite(Number(payload.delta_y))
6321
+ ? Math.floor(Number(payload.delta_y))
6322
+ : 0;
6323
+ if (!dyRaw)
6324
+ return { ok: true, action };
6325
+ let step = dyRaw;
6326
+ if (Math.abs(step) < 120)
6327
+ step = step < 0 ? -120 : 120;
6328
+ else
6329
+ step = Math.round(step / 120) * 120;
6330
+ const ticks = Math.max(1, Math.min(20, Math.round(Math.abs(step) / 120)));
6331
+ const yTick = step < 0 ? ticks : -ticks;
6332
+ const exec = await runCommandCapture(cliclick, [`m:${pos}`, `w:0,${yTick}`], { timeoutMs: 10_000 });
6333
+ return exec.ok
6334
+ ? { ok: true, action }
6335
+ : { ok: false, error: exec.error || exec.stderr || "cliclick mouse_wheel failed" };
6336
+ }
6337
+ if (action === "key") {
6338
+ const spec = remoteMacKeySpec(String(payload.key || ""));
6339
+ if (!spec)
6340
+ return { ok: false, error: "unsupported key token" };
6341
+ const mods = remoteModifierFlags(payload, "mac");
6342
+ const modClause = mods.length ? ` using {${mods.join(", ")}}` : "";
6343
+ const line = spec.kind === "code"
6344
+ ? `key code ${Number(spec.value)}${modClause}`
6345
+ : `keystroke "${escapeAppleScriptString(String(spec.value))}"${modClause}`;
6346
+ const script = [
6347
+ 'tell application "System Events"',
6348
+ ` ${line}`,
6349
+ "end tell",
6350
+ ];
6351
+ const exec = await runCommandCapture(osascript, script.flatMap((s) => ["-e", s]), { timeoutMs: 10_000 });
6352
+ if (!exec.ok)
6353
+ return { ok: false, error: exec.error || exec.stderr || "osascript key input failed" };
6354
+ return { ok: true, action };
6355
+ }
6356
+ if (action === "type_text") {
6357
+ const txt = String(payload.text || "");
6358
+ if (!txt)
6359
+ return { ok: false, error: "type_text requires text" };
6360
+ const parts = txt.replace(/\r\n/g, "\n").split("\n");
6361
+ const script = ['tell application "System Events"'];
6362
+ for (let i = 0; i < parts.length; i++) {
6363
+ const part = parts[i] ?? "";
6364
+ if (part)
6365
+ script.push(` keystroke "${escapeAppleScriptString(part)}"`);
6366
+ if (i < parts.length - 1)
6367
+ script.push(" key code 36");
6368
+ }
6369
+ script.push("end tell");
6370
+ const exec = await runCommandCapture(osascript, script.flatMap((s) => ["-e", s]), { timeoutMs: 12_000 });
6371
+ if (!exec.ok)
6372
+ return { ok: false, error: exec.error || exec.stderr || "osascript type_text failed" };
6373
+ return { ok: true, action };
6374
+ }
6375
+ return { ok: false, error: `unsupported remote control action: ${action}` };
6376
+ }
6377
+ async function fsRemoteControlInput(payload) {
5648
6378
  const action = String(payload.action || "").trim().toLowerCase();
5649
6379
  if (!action)
5650
6380
  return { ok: false, error: "remote control action is required" };
6381
+ if (action === "capabilities") {
6382
+ return { ok: true, action, ...remoteControlCapabilities() };
6383
+ }
6384
+ if (process.platform === "linux") {
6385
+ return await fsRemoteControlInputLinux(action, payload);
6386
+ }
6387
+ if (isMacos()) {
6388
+ return await fsRemoteControlInputMac(action, payload);
6389
+ }
6390
+ if (!isWindows()) {
6391
+ return { ok: false, error: `remote control input is not supported on platform ${process.platform}` };
6392
+ }
5651
6393
  const psPrelude = [
5652
6394
  "$ErrorActionPreference = 'Stop'",
5653
6395
  "$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex); }'",
@@ -5776,11 +6518,17 @@ async function fsRemoteControlInput(payload) {
5776
6518
  }
5777
6519
  }
5778
6520
  async function fsRemoteClipboardGet() {
5779
- if (!isWindows()) {
5780
- return { ok: false, error: "remote clipboard currently supports Windows agents only" };
6521
+ if (isWindows()) {
6522
+ try {
6523
+ const raw = await runWindowsPsCapture("$ErrorActionPreference='Stop'; $t = Get-Clipboard -Raw -TextFormatType Text; if ($null -eq $t) { '' } else { [Console]::Out.Write($t) }");
6524
+ return { ok: true, text: String(raw || "") };
6525
+ }
6526
+ catch (e) {
6527
+ return { ok: false, error: String(e) };
6528
+ }
5781
6529
  }
5782
6530
  try {
5783
- const raw = await runWindowsPsCapture("$ErrorActionPreference='Stop'; $t = Get-Clipboard -Raw -TextFormatType Text; if ($null -eq $t) { '' } else { [Console]::Out.Write($t) }");
6531
+ const raw = await (0, clipboardExec_1.readClipboardViaExec)();
5784
6532
  return { ok: true, text: String(raw || "") };
5785
6533
  }
5786
6534
  catch (e) {
@@ -5788,21 +6536,53 @@ async function fsRemoteClipboardGet() {
5788
6536
  }
5789
6537
  }
5790
6538
  async function fsRemoteClipboardSet(text) {
5791
- if (!isWindows()) {
5792
- return { ok: false, error: "remote clipboard currently supports Windows agents only" };
5793
- }
5794
6539
  const t = String(text || "");
5795
- try {
5796
- await runWindowsRemoteControlPs(`$ErrorActionPreference='Stop'; Set-Clipboard -Value '${escapePsSingleQuoted(t)}'`);
5797
- return { ok: true };
6540
+ if (isWindows()) {
6541
+ try {
6542
+ await runWindowsRemoteControlPs(`$ErrorActionPreference='Stop'; Set-Clipboard -Value '${escapePsSingleQuoted(t)}'`);
6543
+ return { ok: true };
6544
+ }
6545
+ catch (e) {
6546
+ return { ok: false, error: String(e) };
6547
+ }
5798
6548
  }
5799
- catch (e) {
5800
- return { ok: false, error: String(e) };
6549
+ if (isMacos()) {
6550
+ const pbcopy = unixWhich("pbcopy") || "/usr/bin/pbcopy";
6551
+ const exec = await runCommandCapture(pbcopy, [], { timeoutMs: 8000, stdinText: t });
6552
+ return exec.ok ? { ok: true } : { ok: false, error: exec.error || exec.stderr || "pbcopy failed" };
5801
6553
  }
6554
+ if (process.platform === "linux") {
6555
+ const attempts = [];
6556
+ const wlCopy = unixWhich("wl-copy");
6557
+ if (wlCopy)
6558
+ attempts.push({ cmd: wlCopy, args: [] });
6559
+ const xclip = unixWhich("xclip");
6560
+ if (xclip)
6561
+ attempts.push({ cmd: xclip, args: ["-selection", "clipboard"] });
6562
+ const xsel = unixWhich("xsel");
6563
+ if (xsel)
6564
+ attempts.push({ cmd: xsel, args: ["--clipboard", "--input"] });
6565
+ if (!attempts.length) {
6566
+ return {
6567
+ ok: false,
6568
+ error: "remote clipboard on Linux requires wl-copy, xclip, or xsel",
6569
+ };
6570
+ }
6571
+ for (const it of attempts) {
6572
+ const exec = await runCommandCapture(it.cmd, it.args, {
6573
+ timeoutMs: 8000,
6574
+ stdinText: t,
6575
+ });
6576
+ if (exec.ok)
6577
+ return { ok: true };
6578
+ }
6579
+ return { ok: false, error: "failed to set clipboard on Linux (wl-copy/xclip/xsel)" };
6580
+ }
6581
+ return { ok: false, error: `remote clipboard is not supported on platform ${process.platform}` };
5802
6582
  }
5803
- async function fsRemoteFilePush(name, b64) {
5804
- if (!isWindows()) {
5805
- return { ok: false, error: "remote file push currently supports Windows agents only" };
6583
+ async function fsRemoteFilePush(name, b64, targetDir) {
6584
+ if (!(isWindows() || process.platform === "linux" || isMacos())) {
6585
+ return { ok: false, error: `remote file push is not supported on platform ${process.platform}` };
5806
6586
  }
5807
6587
  const fileName = path.basename(String(name || "").trim() || "upload.bin");
5808
6588
  if (!fileName || fileName === "." || fileName === "..") {
@@ -5819,18 +6599,35 @@ async function fsRemoteFilePush(name, b64) {
5819
6599
  if (!body.length || body.length > maxBytes) {
5820
6600
  return { ok: false, error: "file payload must be 1B..20MB" };
5821
6601
  }
5822
- const home = os.homedir();
5823
- const targets = [path.join(home, "Desktop"), path.join(home, "Downloads"), home];
5824
- let baseDir = home;
5825
- for (const t of targets) {
6602
+ let baseDir;
6603
+ const requestedDir = (targetDir || "").trim();
6604
+ if (requestedDir) {
6605
+ // Validate that the requested target directory exists and is a real directory.
5826
6606
  try {
5827
- if (fs.existsSync(t) && fs.statSync(t).isDirectory()) {
5828
- baseDir = t;
5829
- break;
6607
+ const st = fs.statSync(requestedDir);
6608
+ if (!st.isDirectory()) {
6609
+ return { ok: false, error: `target path is not a directory: ${requestedDir}` };
5830
6610
  }
6611
+ baseDir = requestedDir;
5831
6612
  }
5832
6613
  catch {
5833
- /* skip */
6614
+ return { ok: false, error: `target directory does not exist: ${requestedDir}` };
6615
+ }
6616
+ }
6617
+ else {
6618
+ const home = os.homedir();
6619
+ const targets = [path.join(home, "Desktop"), path.join(home, "Downloads"), home];
6620
+ baseDir = home;
6621
+ for (const t of targets) {
6622
+ try {
6623
+ if (fs.existsSync(t) && fs.statSync(t).isDirectory()) {
6624
+ baseDir = t;
6625
+ break;
6626
+ }
6627
+ }
6628
+ catch {
6629
+ /* skip */
6630
+ }
5834
6631
  }
5835
6632
  }
5836
6633
  let out = path.join(baseDir, fileName);