@midscene/computer 1.9.2-beta-20260608084200.0 → 1.9.2

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.
Binary file
package/dist/es/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { getDebug } from "@midscene/shared/logger";
4
4
  import { BaseMidsceneTools } from "@midscene/shared/mcp/base-tools";
5
5
  import { Agent } from "@midscene/core/agent";
6
6
  import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
7
- import { existsSync } from "node:fs";
7
+ import { chmodSync, existsSync, statSync } from "node:fs";
8
8
  import { createRequire } from "node:module";
9
9
  import { dirname, resolve as external_node_path_resolve } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
@@ -350,9 +350,13 @@ async function getLibnut() {
350
350
  }
351
351
  }
352
352
  const debugDevice = getDebug('computer:device');
353
+ const warnDevice = getDebug('computer:device', {
354
+ console: true
355
+ });
353
356
  const debugComputerInput = getDebug('computer:input', {
354
357
  console: true
355
358
  });
359
+ const WINDOWS_UIPI_DOC_URL = 'https://midscenejs.com/computer-getting-started#windows-clicks-have-no-effect-on-some-apps';
356
360
  function resolvePackageRoot(helperName) {
357
361
  const require = createRequire(import.meta.url);
358
362
  let pkgRoot = null;
@@ -392,6 +396,15 @@ function getPhasedScrollBinary() {
392
396
  phasedScrollBinaryPath = null;
393
397
  return null;
394
398
  }
399
+ try {
400
+ const st = statSync(binPath);
401
+ if ((73 & st.mode) === 0) {
402
+ chmodSync(binPath, 493);
403
+ debugDevice('phased-scroll: restored executable bit on', binPath);
404
+ }
405
+ } catch (err) {
406
+ debugDevice('phased-scroll: chmod self-heal failed', err);
407
+ }
395
408
  phasedScrollBinaryPath = binPath;
396
409
  return binPath;
397
410
  }
@@ -519,9 +532,10 @@ function runPhasedScroll(direction, pixels, steps) {
519
532
  if (0 === res.status) return true;
520
533
  if (!phasedScrollExecWarned) {
521
534
  phasedScrollExecWarned = true;
522
- console.warn(`[@midscene/computer] phased-scroll helper exited with status ${res.status}; falling back to keyboard/libnut. This usually means Accessibility permission has not been granted to the host process.`);
535
+ const hint = null === res.status ? `signal ${res.signal ?? 'unknown'}; the binary may not be executable (npm tarball extraction can drop the +x bit) or may be blocked by quarantine. Try: chmod +x "${bin}"` : 'this usually means Accessibility permission has not been granted to the host process (System Settings → Privacy & Security → Accessibility)';
536
+ console.warn(`[@midscene/computer] phased-scroll helper failed (exit=${res.status}, signal=${res.signal ?? 'none'}); falling back to keyboard/libnut. ${hint}`);
523
537
  }
524
- debugDevice('phased-scroll exited non-zero', res.status, res.error);
538
+ debugDevice('phased-scroll exited non-zero', res.status, res.signal, res.error);
525
539
  return false;
526
540
  } catch (err) {
527
541
  if (!phasedScrollExecWarned) {
@@ -642,7 +656,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
642
656
  }
643
657
  async healthCheck() {
644
658
  console.log('[HealthCheck] Starting health check...');
645
- console.log("[HealthCheck] @midscene/computer v1.9.2-beta-20260608084200.0");
659
+ console.log("[HealthCheck] @midscene/computer v1.9.2");
646
660
  console.log('[HealthCheck] Taking screenshot...');
647
661
  const screenshotTimeout = 15000;
648
662
  let timeoutId;
@@ -670,13 +684,15 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
670
684
  const deltaY = Math.abs(movedPos.y - targetY);
671
685
  if (deltaX > 5 || deltaY > 5) {
672
686
  const msg = `[HealthCheck] WARNING: Mouse control may not be working. Expected (${targetX}, ${targetY}), got (${movedPos.x}, ${movedPos.y}), delta=(${deltaX}, ${deltaY})`;
673
- console.warn(msg);
674
- debugDevice(msg);
675
- if ('win32' === process.platform && !this.isRunningAsAdmin()) {
676
- const hint = "Midscene is NOT running as Administrator. Windows blocks mouse/keyboard input to elevated (admin) applications from non-admin processes (UIPI). Please run your terminal or Node.js as Administrator and try again.";
677
- console.error(`\n[HealthCheck] ${hint}\n`);
678
- debugDevice(hint);
679
- }
687
+ warnDevice(msg);
688
+ }
689
+ if ('win32' === process.platform && !this.isRunningAsAdmin()) {
690
+ const hint = [
691
+ 'Heads-up: Midscene is not running as Administrator.',
692
+ 'If clicks or key presses have no effect while the cursor still moves to the right position,',
693
+ `see the Windows permission troubleshooting guide: ${WINDOWS_UIPI_DOC_URL}`
694
+ ].join(' ');
695
+ warnDevice(`[HealthCheck] ${hint}`);
680
696
  }
681
697
  this.inputDriver.moveMouse(startPos.x, startPos.y);
682
698
  console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
@@ -693,14 +709,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
693
709
  }
694
710
  isRunningAsAdmin() {
695
711
  if ('win32' !== process.platform) return false;
712
+ if (void 0 !== this.adminCheckCache) return this.adminCheckCache;
696
713
  try {
697
714
  execSync('net session', {
698
715
  stdio: 'pipe'
699
716
  });
700
- return true;
717
+ this.adminCheckCache = true;
701
718
  } catch {
702
- return false;
719
+ this.adminCheckCache = false;
703
720
  }
721
+ return this.adminCheckCache;
704
722
  }
705
723
  async screenshotBase64() {
706
724
  if (this.destroyed) throw new Error('ComputerDevice has been destroyed');
@@ -945,6 +963,7 @@ Original error: ${lastRawMessage}`);
945
963
  debug: (message)=>debugDevice(message)
946
964
  }));
947
965
  device_define_property(this, "useAppleScript", void 0);
966
+ device_define_property(this, "adminCheckCache", void 0);
948
967
  device_define_property(this, "uri", void 0);
949
968
  device_define_property(this, "inputPrimitives", {
950
969
  pointer: {
@@ -1095,24 +1114,6 @@ function createPlatformActions() {
1095
1114
  })
1096
1115
  };
1097
1116
  }
1098
- function normalizeRdpHost(host) {
1099
- const trimmed = host.trim();
1100
- if (trimmed.length >= 2 && trimmed.startsWith('[') && trimmed.endsWith(']') && trimmed.includes(':')) return trimmed.slice(1, -1);
1101
- return trimmed;
1102
- }
1103
- function formatRdpHost(host) {
1104
- const normalizedHost = normalizeRdpHost(host);
1105
- return normalizedHost.includes(':') ? `[${normalizedHost}]` : normalizedHost;
1106
- }
1107
- function formatRdpServerAddress(host, port) {
1108
- return `${formatRdpHost(host)}:${port}`;
1109
- }
1110
- function normalizeRdpConnectionConfig(config) {
1111
- return {
1112
- ...config,
1113
- host: normalizeRdpHost(config.host)
1114
- };
1115
- }
1116
1117
  const platformBinaryMap = {
1117
1118
  darwin: {
1118
1119
  directory: 'darwin',
@@ -1167,7 +1168,7 @@ class HelperProcessRDPBackendClient {
1167
1168
  await this.ensureHelperStarted();
1168
1169
  const response = await this.send({
1169
1170
  type: 'connect',
1170
- config: normalizeRdpConnectionConfig(config)
1171
+ config
1171
1172
  });
1172
1173
  if ('connected' !== response.type) throw new Error(`Expected connected response, got ${response.type}`);
1173
1174
  this.connected = true;
@@ -1441,10 +1442,9 @@ const DEFAULT_SCROLL_STEP_AMOUNT = 120;
1441
1442
  class RDPDevice {
1442
1443
  describe() {
1443
1444
  const port = this.options.port || 3389;
1444
- const server = formatRdpServerAddress(this.options.host, port);
1445
1445
  const username = this.options.username ? ` as ${this.options.username}` : '';
1446
1446
  const session = this.connectionInfo?.sessionId ? ` [session ${this.connectionInfo.sessionId}]` : '';
1447
- return `RDP Device ${server}${username}${session}`;
1447
+ return `RDP Device ${this.options.host}:${port}${username}${session}`;
1448
1448
  }
1449
1449
  async connect() {
1450
1450
  this.throwIfDestroyed();
@@ -1484,11 +1484,10 @@ class RDPDevice {
1484
1484
  call: async ()=>{
1485
1485
  this.assertConnected();
1486
1486
  const size = await this.size();
1487
- const server = this.connectionInfo?.server || formatRdpServerAddress(this.options.host, this.options.port || 3389);
1488
1487
  return [
1489
1488
  {
1490
1489
  id: this.connectionInfo?.sessionId || this.options.host,
1491
- name: `RDP ${server} (${size.width}x${size.height})`,
1490
+ name: `RDP ${this.connectionInfo?.server || this.options.host} (${size.width}x${size.height})`,
1492
1491
  primary: true
1493
1492
  }
1494
1493
  ];
@@ -1686,12 +1685,11 @@ class RDPDevice {
1686
1685
  }
1687
1686
  }
1688
1687
  });
1689
- const normalizedOptions = normalizeRdpConnectionConfig(options);
1690
1688
  this.options = {
1691
1689
  port: 3389,
1692
1690
  securityProtocol: 'auto',
1693
1691
  ignoreCertificate: false,
1694
- ...normalizedOptions
1692
+ ...options
1695
1693
  };
1696
1694
  this.backend = options.backend || createDefaultRDPBackendClient();
1697
1695
  }
@@ -1769,11 +1767,10 @@ function adaptComputerInitArgs(extracted) {
1769
1767
  if (!extracted || 0 === Object.keys(extracted).length) return;
1770
1768
  if (extracted.host) {
1771
1769
  const { displayId: _d, headless: _h, ...rdpFields } = extracted;
1772
- const host = normalizeRdpHost(extracted.host);
1773
1770
  return {
1774
1771
  mode: 'rdp',
1775
1772
  ...rdpFields,
1776
- host
1773
+ host: extracted.host
1777
1774
  };
1778
1775
  }
1779
1776
  return {
@@ -1789,15 +1786,15 @@ function shouldRetargetAgent(opts) {
1789
1786
  }
1790
1787
  function describeConnectTarget(opts) {
1791
1788
  if (opts?.mode === 'rdp') {
1792
- const target = opts.port ? formatRdpServerAddress(opts.host, opts.port) : formatRdpHost(opts.host);
1789
+ const portSuffix = opts.port ? `:${opts.port}` : '';
1793
1790
  const userSuffix = opts.username ? ` as ${opts.username}` : '';
1794
- return ` via RDP (${target}${userSuffix})`;
1791
+ return ` via RDP (${opts.host}${portSuffix}${userSuffix})`;
1795
1792
  }
1796
1793
  if (opts?.mode === 'local' && opts.displayId) return ` (Display: ${opts.displayId})`;
1797
1794
  return ' (Primary display)';
1798
1795
  }
1799
1796
  function getCliReportSessionTarget(opts) {
1800
- if (opts?.mode === 'rdp') return `rdp:${formatRdpHost(opts.host)}`;
1797
+ if (opts?.mode === 'rdp') return `rdp:${opts.host}`;
1801
1798
  if (opts?.mode === 'local' && opts.displayId) return opts.displayId;
1802
1799
  return 'primary';
1803
1800
  }
@@ -1915,7 +1912,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
1915
1912
  const tools = new ComputerMidsceneTools();
1916
1913
  runToolsCLI(tools, 'midscene-computer', {
1917
1914
  stripPrefix: 'computer_',
1918
- version: "1.9.2-beta-20260608084200.0",
1915
+ version: "1.9.2",
1919
1916
  extraCommands: createReportCliCommands()
1920
1917
  }).catch((e)=>{
1921
1918
  process.exit(reportCLIError(e));
package/dist/es/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { chmodSync, existsSync, statSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, resolve as external_node_path_resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -350,9 +350,13 @@ async function getLibnut() {
350
350
  }
351
351
  }
352
352
  const debugDevice = getDebug('computer:device');
353
+ const warnDevice = getDebug('computer:device', {
354
+ console: true
355
+ });
353
356
  const debugComputerInput = getDebug('computer:input', {
354
357
  console: true
355
358
  });
359
+ const WINDOWS_UIPI_DOC_URL = 'https://midscenejs.com/computer-getting-started#windows-clicks-have-no-effect-on-some-apps';
356
360
  function resolvePackageRoot(helperName) {
357
361
  const require = createRequire(import.meta.url);
358
362
  let pkgRoot = null;
@@ -392,6 +396,15 @@ function getPhasedScrollBinary() {
392
396
  phasedScrollBinaryPath = null;
393
397
  return null;
394
398
  }
399
+ try {
400
+ const st = statSync(binPath);
401
+ if ((73 & st.mode) === 0) {
402
+ chmodSync(binPath, 493);
403
+ debugDevice('phased-scroll: restored executable bit on', binPath);
404
+ }
405
+ } catch (err) {
406
+ debugDevice('phased-scroll: chmod self-heal failed', err);
407
+ }
395
408
  phasedScrollBinaryPath = binPath;
396
409
  return binPath;
397
410
  }
@@ -519,9 +532,10 @@ function runPhasedScroll(direction, pixels, steps) {
519
532
  if (0 === res.status) return true;
520
533
  if (!phasedScrollExecWarned) {
521
534
  phasedScrollExecWarned = true;
522
- console.warn(`[@midscene/computer] phased-scroll helper exited with status ${res.status}; falling back to keyboard/libnut. This usually means Accessibility permission has not been granted to the host process.`);
535
+ const hint = null === res.status ? `signal ${res.signal ?? 'unknown'}; the binary may not be executable (npm tarball extraction can drop the +x bit) or may be blocked by quarantine. Try: chmod +x "${bin}"` : 'this usually means Accessibility permission has not been granted to the host process (System Settings → Privacy & Security → Accessibility)';
536
+ console.warn(`[@midscene/computer] phased-scroll helper failed (exit=${res.status}, signal=${res.signal ?? 'none'}); falling back to keyboard/libnut. ${hint}`);
523
537
  }
524
- debugDevice('phased-scroll exited non-zero', res.status, res.error);
538
+ debugDevice('phased-scroll exited non-zero', res.status, res.signal, res.error);
525
539
  return false;
526
540
  } catch (err) {
527
541
  if (!phasedScrollExecWarned) {
@@ -642,7 +656,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
642
656
  }
643
657
  async healthCheck() {
644
658
  console.log('[HealthCheck] Starting health check...');
645
- console.log("[HealthCheck] @midscene/computer v1.9.2-beta-20260608084200.0");
659
+ console.log("[HealthCheck] @midscene/computer v1.9.2");
646
660
  console.log('[HealthCheck] Taking screenshot...');
647
661
  const screenshotTimeout = 15000;
648
662
  let timeoutId;
@@ -670,13 +684,15 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
670
684
  const deltaY = Math.abs(movedPos.y - targetY);
671
685
  if (deltaX > 5 || deltaY > 5) {
672
686
  const msg = `[HealthCheck] WARNING: Mouse control may not be working. Expected (${targetX}, ${targetY}), got (${movedPos.x}, ${movedPos.y}), delta=(${deltaX}, ${deltaY})`;
673
- console.warn(msg);
674
- debugDevice(msg);
675
- if ('win32' === process.platform && !this.isRunningAsAdmin()) {
676
- const hint = "Midscene is NOT running as Administrator. Windows blocks mouse/keyboard input to elevated (admin) applications from non-admin processes (UIPI). Please run your terminal or Node.js as Administrator and try again.";
677
- console.error(`\n[HealthCheck] ${hint}\n`);
678
- debugDevice(hint);
679
- }
687
+ warnDevice(msg);
688
+ }
689
+ if ('win32' === process.platform && !this.isRunningAsAdmin()) {
690
+ const hint = [
691
+ 'Heads-up: Midscene is not running as Administrator.',
692
+ 'If clicks or key presses have no effect while the cursor still moves to the right position,',
693
+ `see the Windows permission troubleshooting guide: ${WINDOWS_UIPI_DOC_URL}`
694
+ ].join(' ');
695
+ warnDevice(`[HealthCheck] ${hint}`);
680
696
  }
681
697
  this.inputDriver.moveMouse(startPos.x, startPos.y);
682
698
  console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
@@ -693,14 +709,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
693
709
  }
694
710
  isRunningAsAdmin() {
695
711
  if ('win32' !== process.platform) return false;
712
+ if (void 0 !== this.adminCheckCache) return this.adminCheckCache;
696
713
  try {
697
714
  execSync('net session', {
698
715
  stdio: 'pipe'
699
716
  });
700
- return true;
717
+ this.adminCheckCache = true;
701
718
  } catch {
702
- return false;
719
+ this.adminCheckCache = false;
703
720
  }
721
+ return this.adminCheckCache;
704
722
  }
705
723
  async screenshotBase64() {
706
724
  if (this.destroyed) throw new Error('ComputerDevice has been destroyed');
@@ -945,6 +963,7 @@ Original error: ${lastRawMessage}`);
945
963
  debug: (message)=>debugDevice(message)
946
964
  }));
947
965
  device_define_property(this, "useAppleScript", void 0);
966
+ device_define_property(this, "adminCheckCache", void 0);
948
967
  device_define_property(this, "uri", void 0);
949
968
  device_define_property(this, "inputPrimitives", {
950
969
  pointer: {
@@ -1095,24 +1114,6 @@ function createPlatformActions() {
1095
1114
  })
1096
1115
  };
1097
1116
  }
1098
- function normalizeRdpHost(host) {
1099
- const trimmed = host.trim();
1100
- if (trimmed.length >= 2 && trimmed.startsWith('[') && trimmed.endsWith(']') && trimmed.includes(':')) return trimmed.slice(1, -1);
1101
- return trimmed;
1102
- }
1103
- function formatRdpHost(host) {
1104
- const normalizedHost = normalizeRdpHost(host);
1105
- return normalizedHost.includes(':') ? `[${normalizedHost}]` : normalizedHost;
1106
- }
1107
- function formatRdpServerAddress(host, port) {
1108
- return `${formatRdpHost(host)}:${port}`;
1109
- }
1110
- function normalizeRdpConnectionConfig(config) {
1111
- return {
1112
- ...config,
1113
- host: normalizeRdpHost(config.host)
1114
- };
1115
- }
1116
1117
  const platformBinaryMap = {
1117
1118
  darwin: {
1118
1119
  directory: 'darwin',
@@ -1210,7 +1211,7 @@ class HelperProcessRDPBackendClient {
1210
1211
  await this.ensureHelperStarted();
1211
1212
  const response = await this.send({
1212
1213
  type: 'connect',
1213
- config: normalizeRdpConnectionConfig(config)
1214
+ config
1214
1215
  });
1215
1216
  if ('connected' !== response.type) throw new Error(`Expected connected response, got ${response.type}`);
1216
1217
  this.connected = true;
@@ -1484,10 +1485,9 @@ const DEFAULT_SCROLL_STEP_AMOUNT = 120;
1484
1485
  class RDPDevice {
1485
1486
  describe() {
1486
1487
  const port = this.options.port || 3389;
1487
- const server = formatRdpServerAddress(this.options.host, port);
1488
1488
  const username = this.options.username ? ` as ${this.options.username}` : '';
1489
1489
  const session = this.connectionInfo?.sessionId ? ` [session ${this.connectionInfo.sessionId}]` : '';
1490
- return `RDP Device ${server}${username}${session}`;
1490
+ return `RDP Device ${this.options.host}:${port}${username}${session}`;
1491
1491
  }
1492
1492
  async connect() {
1493
1493
  this.throwIfDestroyed();
@@ -1527,11 +1527,10 @@ class RDPDevice {
1527
1527
  call: async ()=>{
1528
1528
  this.assertConnected();
1529
1529
  const size = await this.size();
1530
- const server = this.connectionInfo?.server || formatRdpServerAddress(this.options.host, this.options.port || 3389);
1531
1530
  return [
1532
1531
  {
1533
1532
  id: this.connectionInfo?.sessionId || this.options.host,
1534
- name: `RDP ${server} (${size.width}x${size.height})`,
1533
+ name: `RDP ${this.connectionInfo?.server || this.options.host} (${size.width}x${size.height})`,
1535
1534
  primary: true
1536
1535
  }
1537
1536
  ];
@@ -1729,12 +1728,11 @@ class RDPDevice {
1729
1728
  }
1730
1729
  }
1731
1730
  });
1732
- const normalizedOptions = normalizeRdpConnectionConfig(options);
1733
1731
  this.options = {
1734
1732
  port: 3389,
1735
1733
  securityProtocol: 'auto',
1736
1734
  ignoreCertificate: false,
1737
- ...normalizedOptions
1735
+ ...options
1738
1736
  };
1739
1737
  this.backend = options.backend || createDefaultRDPBackendClient();
1740
1738
  }
@@ -1812,11 +1810,10 @@ function adaptComputerInitArgs(extracted) {
1812
1810
  if (!extracted || 0 === Object.keys(extracted).length) return;
1813
1811
  if (extracted.host) {
1814
1812
  const { displayId: _d, headless: _h, ...rdpFields } = extracted;
1815
- const host = normalizeRdpHost(extracted.host);
1816
1813
  return {
1817
1814
  mode: 'rdp',
1818
1815
  ...rdpFields,
1819
- host
1816
+ host: extracted.host
1820
1817
  };
1821
1818
  }
1822
1819
  return {
@@ -1832,15 +1829,15 @@ function shouldRetargetAgent(opts) {
1832
1829
  }
1833
1830
  function describeConnectTarget(opts) {
1834
1831
  if (opts?.mode === 'rdp') {
1835
- const target = opts.port ? formatRdpServerAddress(opts.host, opts.port) : formatRdpHost(opts.host);
1832
+ const portSuffix = opts.port ? `:${opts.port}` : '';
1836
1833
  const userSuffix = opts.username ? ` as ${opts.username}` : '';
1837
- return ` via RDP (${target}${userSuffix})`;
1834
+ return ` via RDP (${opts.host}${portSuffix}${userSuffix})`;
1838
1835
  }
1839
1836
  if (opts?.mode === 'local' && opts.displayId) return ` (Display: ${opts.displayId})`;
1840
1837
  return ' (Primary display)';
1841
1838
  }
1842
1839
  function getCliReportSessionTarget(opts) {
1843
- if (opts?.mode === 'rdp') return `rdp:${formatRdpHost(opts.host)}`;
1840
+ if (opts?.mode === 'rdp') return `rdp:${opts.host}`;
1844
1841
  if (opts?.mode === 'local' && opts.displayId) return opts.displayId;
1845
1842
  return 'primary';
1846
1843
  }
@@ -1956,7 +1953,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
1956
1953
  }
1957
1954
  }
1958
1955
  function version() {
1959
- const currentVersion = "1.9.2-beta-20260608084200.0";
1956
+ const currentVersion = "1.9.2";
1960
1957
  console.log(`@midscene/computer v${currentVersion}`);
1961
1958
  return currentVersion;
1962
1959
  }
@@ -1,7 +1,7 @@
1
1
  import { BaseMCPServer, createMCPServerLauncher } from "@midscene/shared/mcp";
2
2
  import { Agent } from "@midscene/core/agent";
3
3
  import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
4
- import { existsSync } from "node:fs";
4
+ import { chmodSync, existsSync, statSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { dirname, resolve as external_node_path_resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -350,9 +350,13 @@ async function getLibnut() {
350
350
  }
351
351
  }
352
352
  const debugDevice = getDebug('computer:device');
353
+ const warnDevice = getDebug('computer:device', {
354
+ console: true
355
+ });
353
356
  const debugComputerInput = getDebug('computer:input', {
354
357
  console: true
355
358
  });
359
+ const WINDOWS_UIPI_DOC_URL = 'https://midscenejs.com/computer-getting-started#windows-clicks-have-no-effect-on-some-apps';
356
360
  function resolvePackageRoot(helperName) {
357
361
  const require = createRequire(import.meta.url);
358
362
  let pkgRoot = null;
@@ -392,6 +396,15 @@ function getPhasedScrollBinary() {
392
396
  phasedScrollBinaryPath = null;
393
397
  return null;
394
398
  }
399
+ try {
400
+ const st = statSync(binPath);
401
+ if ((73 & st.mode) === 0) {
402
+ chmodSync(binPath, 493);
403
+ debugDevice('phased-scroll: restored executable bit on', binPath);
404
+ }
405
+ } catch (err) {
406
+ debugDevice('phased-scroll: chmod self-heal failed', err);
407
+ }
395
408
  phasedScrollBinaryPath = binPath;
396
409
  return binPath;
397
410
  }
@@ -519,9 +532,10 @@ function runPhasedScroll(direction, pixels, steps) {
519
532
  if (0 === res.status) return true;
520
533
  if (!phasedScrollExecWarned) {
521
534
  phasedScrollExecWarned = true;
522
- console.warn(`[@midscene/computer] phased-scroll helper exited with status ${res.status}; falling back to keyboard/libnut. This usually means Accessibility permission has not been granted to the host process.`);
535
+ const hint = null === res.status ? `signal ${res.signal ?? 'unknown'}; the binary may not be executable (npm tarball extraction can drop the +x bit) or may be blocked by quarantine. Try: chmod +x "${bin}"` : 'this usually means Accessibility permission has not been granted to the host process (System Settings → Privacy & Security → Accessibility)';
536
+ console.warn(`[@midscene/computer] phased-scroll helper failed (exit=${res.status}, signal=${res.signal ?? 'none'}); falling back to keyboard/libnut. ${hint}`);
523
537
  }
524
- debugDevice('phased-scroll exited non-zero', res.status, res.error);
538
+ debugDevice('phased-scroll exited non-zero', res.status, res.signal, res.error);
525
539
  return false;
526
540
  } catch (err) {
527
541
  if (!phasedScrollExecWarned) {
@@ -642,7 +656,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
642
656
  }
643
657
  async healthCheck() {
644
658
  console.log('[HealthCheck] Starting health check...');
645
- console.log("[HealthCheck] @midscene/computer v1.9.2-beta-20260608084200.0");
659
+ console.log("[HealthCheck] @midscene/computer v1.9.2");
646
660
  console.log('[HealthCheck] Taking screenshot...');
647
661
  const screenshotTimeout = 15000;
648
662
  let timeoutId;
@@ -670,13 +684,15 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
670
684
  const deltaY = Math.abs(movedPos.y - targetY);
671
685
  if (deltaX > 5 || deltaY > 5) {
672
686
  const msg = `[HealthCheck] WARNING: Mouse control may not be working. Expected (${targetX}, ${targetY}), got (${movedPos.x}, ${movedPos.y}), delta=(${deltaX}, ${deltaY})`;
673
- console.warn(msg);
674
- debugDevice(msg);
675
- if ('win32' === process.platform && !this.isRunningAsAdmin()) {
676
- const hint = "Midscene is NOT running as Administrator. Windows blocks mouse/keyboard input to elevated (admin) applications from non-admin processes (UIPI). Please run your terminal or Node.js as Administrator and try again.";
677
- console.error(`\n[HealthCheck] ${hint}\n`);
678
- debugDevice(hint);
679
- }
687
+ warnDevice(msg);
688
+ }
689
+ if ('win32' === process.platform && !this.isRunningAsAdmin()) {
690
+ const hint = [
691
+ 'Heads-up: Midscene is not running as Administrator.',
692
+ 'If clicks or key presses have no effect while the cursor still moves to the right position,',
693
+ `see the Windows permission troubleshooting guide: ${WINDOWS_UIPI_DOC_URL}`
694
+ ].join(' ');
695
+ warnDevice(`[HealthCheck] ${hint}`);
680
696
  }
681
697
  this.inputDriver.moveMouse(startPos.x, startPos.y);
682
698
  console.log(`[HealthCheck] Mouse restored to (${startPos.x}, ${startPos.y})`);
@@ -693,14 +709,16 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
693
709
  }
694
710
  isRunningAsAdmin() {
695
711
  if ('win32' !== process.platform) return false;
712
+ if (void 0 !== this.adminCheckCache) return this.adminCheckCache;
696
713
  try {
697
714
  execSync('net session', {
698
715
  stdio: 'pipe'
699
716
  });
700
- return true;
717
+ this.adminCheckCache = true;
701
718
  } catch {
702
- return false;
719
+ this.adminCheckCache = false;
703
720
  }
721
+ return this.adminCheckCache;
704
722
  }
705
723
  async screenshotBase64() {
706
724
  if (this.destroyed) throw new Error('ComputerDevice has been destroyed');
@@ -945,6 +963,7 @@ Original error: ${lastRawMessage}`);
945
963
  debug: (message)=>debugDevice(message)
946
964
  }));
947
965
  device_define_property(this, "useAppleScript", void 0);
966
+ device_define_property(this, "adminCheckCache", void 0);
948
967
  device_define_property(this, "uri", void 0);
949
968
  device_define_property(this, "inputPrimitives", {
950
969
  pointer: {
@@ -1095,24 +1114,6 @@ function createPlatformActions() {
1095
1114
  })
1096
1115
  };
1097
1116
  }
1098
- function normalizeRdpHost(host) {
1099
- const trimmed = host.trim();
1100
- if (trimmed.length >= 2 && trimmed.startsWith('[') && trimmed.endsWith(']') && trimmed.includes(':')) return trimmed.slice(1, -1);
1101
- return trimmed;
1102
- }
1103
- function formatRdpHost(host) {
1104
- const normalizedHost = normalizeRdpHost(host);
1105
- return normalizedHost.includes(':') ? `[${normalizedHost}]` : normalizedHost;
1106
- }
1107
- function formatRdpServerAddress(host, port) {
1108
- return `${formatRdpHost(host)}:${port}`;
1109
- }
1110
- function normalizeRdpConnectionConfig(config) {
1111
- return {
1112
- ...config,
1113
- host: normalizeRdpHost(config.host)
1114
- };
1115
- }
1116
1117
  const platformBinaryMap = {
1117
1118
  darwin: {
1118
1119
  directory: 'darwin',
@@ -1167,7 +1168,7 @@ class HelperProcessRDPBackendClient {
1167
1168
  await this.ensureHelperStarted();
1168
1169
  const response = await this.send({
1169
1170
  type: 'connect',
1170
- config: normalizeRdpConnectionConfig(config)
1171
+ config
1171
1172
  });
1172
1173
  if ('connected' !== response.type) throw new Error(`Expected connected response, got ${response.type}`);
1173
1174
  this.connected = true;
@@ -1441,10 +1442,9 @@ const DEFAULT_SCROLL_STEP_AMOUNT = 120;
1441
1442
  class RDPDevice {
1442
1443
  describe() {
1443
1444
  const port = this.options.port || 3389;
1444
- const server = formatRdpServerAddress(this.options.host, port);
1445
1445
  const username = this.options.username ? ` as ${this.options.username}` : '';
1446
1446
  const session = this.connectionInfo?.sessionId ? ` [session ${this.connectionInfo.sessionId}]` : '';
1447
- return `RDP Device ${server}${username}${session}`;
1447
+ return `RDP Device ${this.options.host}:${port}${username}${session}`;
1448
1448
  }
1449
1449
  async connect() {
1450
1450
  this.throwIfDestroyed();
@@ -1484,11 +1484,10 @@ class RDPDevice {
1484
1484
  call: async ()=>{
1485
1485
  this.assertConnected();
1486
1486
  const size = await this.size();
1487
- const server = this.connectionInfo?.server || formatRdpServerAddress(this.options.host, this.options.port || 3389);
1488
1487
  return [
1489
1488
  {
1490
1489
  id: this.connectionInfo?.sessionId || this.options.host,
1491
- name: `RDP ${server} (${size.width}x${size.height})`,
1490
+ name: `RDP ${this.connectionInfo?.server || this.options.host} (${size.width}x${size.height})`,
1492
1491
  primary: true
1493
1492
  }
1494
1493
  ];
@@ -1686,12 +1685,11 @@ class RDPDevice {
1686
1685
  }
1687
1686
  }
1688
1687
  });
1689
- const normalizedOptions = normalizeRdpConnectionConfig(options);
1690
1688
  this.options = {
1691
1689
  port: 3389,
1692
1690
  securityProtocol: 'auto',
1693
1691
  ignoreCertificate: false,
1694
- ...normalizedOptions
1692
+ ...options
1695
1693
  };
1696
1694
  this.backend = options.backend || createDefaultRDPBackendClient();
1697
1695
  }
@@ -1769,11 +1767,10 @@ function adaptComputerInitArgs(extracted) {
1769
1767
  if (!extracted || 0 === Object.keys(extracted).length) return;
1770
1768
  if (extracted.host) {
1771
1769
  const { displayId: _d, headless: _h, ...rdpFields } = extracted;
1772
- const host = normalizeRdpHost(extracted.host);
1773
1770
  return {
1774
1771
  mode: 'rdp',
1775
1772
  ...rdpFields,
1776
- host
1773
+ host: extracted.host
1777
1774
  };
1778
1775
  }
1779
1776
  return {
@@ -1789,15 +1786,15 @@ function shouldRetargetAgent(opts) {
1789
1786
  }
1790
1787
  function describeConnectTarget(opts) {
1791
1788
  if (opts?.mode === 'rdp') {
1792
- const target = opts.port ? formatRdpServerAddress(opts.host, opts.port) : formatRdpHost(opts.host);
1789
+ const portSuffix = opts.port ? `:${opts.port}` : '';
1793
1790
  const userSuffix = opts.username ? ` as ${opts.username}` : '';
1794
- return ` via RDP (${target}${userSuffix})`;
1791
+ return ` via RDP (${opts.host}${portSuffix}${userSuffix})`;
1795
1792
  }
1796
1793
  if (opts?.mode === 'local' && opts.displayId) return ` (Display: ${opts.displayId})`;
1797
1794
  return ' (Primary display)';
1798
1795
  }
1799
1796
  function getCliReportSessionTarget(opts) {
1800
- if (opts?.mode === 'rdp') return `rdp:${formatRdpHost(opts.host)}`;
1797
+ if (opts?.mode === 'rdp') return `rdp:${opts.host}`;
1801
1798
  if (opts?.mode === 'local' && opts.displayId) return opts.displayId;
1802
1799
  return 'primary';
1803
1800
  }
@@ -1919,7 +1916,7 @@ class ComputerMCPServer extends BaseMCPServer {
1919
1916
  constructor(toolsManager){
1920
1917
  super({
1921
1918
  name: '@midscene/computer-mcp',
1922
- version: "1.9.2-beta-20260608084200.0",
1919
+ version: "1.9.2",
1923
1920
  description: 'Control the computer desktop using natural language commands'
1924
1921
  }, toolsManager);
1925
1922
  }