@epsilon-asi/actors 0.0.22 → 0.0.33

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 (194) hide show
  1. package/dist/browser/RuntimeConfig.d.ts +26 -0
  2. package/dist/browser/RuntimeConfig.d.ts.map +1 -1
  3. package/dist/browser/RuntimeConfig.js +29 -1
  4. package/dist/browser/RuntimeConfig.js.map +1 -1
  5. package/dist/core/ActorContext.d.ts +2 -0
  6. package/dist/core/ActorContext.d.ts.map +1 -1
  7. package/dist/core/ActorRunner.d.ts +3 -0
  8. package/dist/core/ActorRunner.d.ts.map +1 -1
  9. package/dist/core/ActorRunner.js +11 -1
  10. package/dist/core/ActorRunner.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/native/CompositeNativeWindowDriver.d.ts +11 -0
  16. package/dist/native/CompositeNativeWindowDriver.d.ts.map +1 -0
  17. package/dist/native/CompositeNativeWindowDriver.js +31 -0
  18. package/dist/native/CompositeNativeWindowDriver.js.map +1 -0
  19. package/dist/native/NativeActionRegistry.d.ts +14 -0
  20. package/dist/native/NativeActionRegistry.d.ts.map +1 -0
  21. package/dist/native/NativeActionRegistry.js +101 -0
  22. package/dist/native/NativeActionRegistry.js.map +1 -0
  23. package/dist/native/NativeAutomation.d.ts +3 -0
  24. package/dist/native/NativeAutomation.d.ts.map +1 -0
  25. package/dist/native/NativeAutomation.js +12 -0
  26. package/dist/native/NativeAutomation.js.map +1 -0
  27. package/dist/native/NativeCoordinateMapper.d.ts +23 -0
  28. package/dist/native/NativeCoordinateMapper.d.ts.map +1 -0
  29. package/dist/native/NativeCoordinateMapper.js +201 -0
  30. package/dist/native/NativeCoordinateMapper.js.map +1 -0
  31. package/dist/native/NativeFileDialogService.d.ts +26 -0
  32. package/dist/native/NativeFileDialogService.d.ts.map +1 -0
  33. package/dist/native/NativeFileDialogService.js +121 -0
  34. package/dist/native/NativeFileDialogService.js.map +1 -0
  35. package/dist/native/NativeImageFinder.d.ts +12 -0
  36. package/dist/native/NativeImageFinder.d.ts.map +1 -0
  37. package/dist/native/NativeImageFinder.js +29 -0
  38. package/dist/native/NativeImageFinder.js.map +1 -0
  39. package/dist/native/NativeKeyboard.d.ts +10 -0
  40. package/dist/native/NativeKeyboard.d.ts.map +1 -0
  41. package/dist/native/NativeKeyboard.js +16 -0
  42. package/dist/native/NativeKeyboard.js.map +1 -0
  43. package/dist/native/NativeMouse.d.ts +38 -0
  44. package/dist/native/NativeMouse.d.ts.map +1 -0
  45. package/dist/native/NativeMouse.js +82 -0
  46. package/dist/native/NativeMouse.js.map +1 -0
  47. package/dist/native/NativeWindowService.d.ts +31 -0
  48. package/dist/native/NativeWindowService.d.ts.map +1 -0
  49. package/dist/native/NativeWindowService.js +183 -0
  50. package/dist/native/NativeWindowService.js.map +1 -0
  51. package/dist/native/UnsupportedNativeAutomation.d.ts +4 -0
  52. package/dist/native/UnsupportedNativeAutomation.d.ts.map +1 -0
  53. package/dist/native/UnsupportedNativeAutomation.js +77 -0
  54. package/dist/native/UnsupportedNativeAutomation.js.map +1 -0
  55. package/dist/native/WindowMatcher.d.ts +4 -0
  56. package/dist/native/WindowMatcher.d.ts.map +1 -0
  57. package/dist/native/WindowMatcher.js +39 -0
  58. package/dist/native/WindowMatcher.js.map +1 -0
  59. package/dist/native/drivers.d.ts +37 -0
  60. package/dist/native/drivers.d.ts.map +1 -0
  61. package/dist/native/drivers.js +2 -0
  62. package/dist/native/drivers.js.map +1 -0
  63. package/dist/native/errors.d.ts +23 -0
  64. package/dist/native/errors.d.ts.map +1 -0
  65. package/dist/native/errors.js +45 -0
  66. package/dist/native/errors.js.map +1 -0
  67. package/dist/native/index.d.ts +13 -0
  68. package/dist/native/index.d.ts.map +1 -0
  69. package/dist/native/index.js +13 -0
  70. package/dist/native/index.js.map +1 -0
  71. package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts +11 -0
  72. package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts.map +1 -0
  73. package/dist/native/macos/MacOSAccessibilityWindowDriver.js +180 -0
  74. package/dist/native/macos/MacOSAccessibilityWindowDriver.js.map +1 -0
  75. package/dist/native/macos/MacOSAppleScriptClient.d.ts +24 -0
  76. package/dist/native/macos/MacOSAppleScriptClient.d.ts.map +1 -0
  77. package/dist/native/macos/MacOSAppleScriptClient.js +163 -0
  78. package/dist/native/macos/MacOSAppleScriptClient.js.map +1 -0
  79. package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts +10 -0
  80. package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts.map +1 -0
  81. package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js +12 -0
  82. package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js.map +1 -0
  83. package/dist/native/macos/MacOSNativeAutomation.d.ts +3 -0
  84. package/dist/native/macos/MacOSNativeAutomation.d.ts.map +1 -0
  85. package/dist/native/macos/MacOSNativeAutomation.js +88 -0
  86. package/dist/native/macos/MacOSNativeAutomation.js.map +1 -0
  87. package/dist/native/nut/NutNativeImageFinder.d.ts +17 -0
  88. package/dist/native/nut/NutNativeImageFinder.d.ts.map +1 -0
  89. package/dist/native/nut/NutNativeImageFinder.js +84 -0
  90. package/dist/native/nut/NutNativeImageFinder.js.map +1 -0
  91. package/dist/native/nut/NutNativeKeyboardDriver.d.ts +8 -0
  92. package/dist/native/nut/NutNativeKeyboardDriver.d.ts.map +1 -0
  93. package/dist/native/nut/NutNativeKeyboardDriver.js +39 -0
  94. package/dist/native/nut/NutNativeKeyboardDriver.js.map +1 -0
  95. package/dist/native/nut/NutNativeMouseDriver.d.ts +8 -0
  96. package/dist/native/nut/NutNativeMouseDriver.d.ts.map +1 -0
  97. package/dist/native/nut/NutNativeMouseDriver.js +24 -0
  98. package/dist/native/nut/NutNativeMouseDriver.js.map +1 -0
  99. package/dist/native/nut/NutNativeScreenDriver.d.ts +6 -0
  100. package/dist/native/nut/NutNativeScreenDriver.d.ts.map +1 -0
  101. package/dist/native/nut/NutNativeScreenDriver.js +12 -0
  102. package/dist/native/nut/NutNativeScreenDriver.js.map +1 -0
  103. package/dist/native/nut/NutNativeWindowDriver.d.ts +6 -0
  104. package/dist/native/nut/NutNativeWindowDriver.d.ts.map +1 -0
  105. package/dist/native/nut/NutNativeWindowDriver.js +53 -0
  106. package/dist/native/nut/NutNativeWindowDriver.js.map +1 -0
  107. package/dist/native/nut/loadNut.d.ts +58 -0
  108. package/dist/native/nut/loadNut.d.ts.map +1 -0
  109. package/dist/native/nut/loadNut.js +25 -0
  110. package/dist/native/nut/loadNut.js.map +1 -0
  111. package/dist/native/types.d.ts +194 -0
  112. package/dist/native/types.d.ts.map +1 -0
  113. package/dist/native/types.js +2 -0
  114. package/dist/native/types.js.map +1 -0
  115. package/dist/native/utils/appleScriptEscape.d.ts +7 -0
  116. package/dist/native/utils/appleScriptEscape.d.ts.map +1 -0
  117. package/dist/native/utils/appleScriptEscape.js +11 -0
  118. package/dist/native/utils/appleScriptEscape.js.map +1 -0
  119. package/dist/native/utils/geometry.d.ts +12 -0
  120. package/dist/native/utils/geometry.d.ts.map +1 -0
  121. package/dist/native/utils/geometry.js +77 -0
  122. package/dist/native/utils/geometry.js.map +1 -0
  123. package/dist/native/utils/redactNative.d.ts +2 -0
  124. package/dist/native/utils/redactNative.d.ts.map +1 -0
  125. package/dist/native/utils/redactNative.js +7 -0
  126. package/dist/native/utils/redactNative.js.map +1 -0
  127. package/dist/native/utils/waitFor.d.ts +7 -0
  128. package/dist/native/utils/waitFor.d.ts.map +1 -0
  129. package/dist/native/utils/waitFor.js +17 -0
  130. package/dist/native/utils/waitFor.js.map +1 -0
  131. package/dist/sites/upwork-com/upwork-com.actor.d.ts +4 -1
  132. package/dist/sites/upwork-com/upwork-com.actor.d.ts.map +1 -1
  133. package/dist/sites/upwork-com/upwork-com.actor.js +31 -11
  134. package/dist/sites/upwork-com/upwork-com.actor.js.map +1 -1
  135. package/dist/sites/upwork-com/upwork-com.types.d.ts +3 -1
  136. package/dist/sites/upwork-com/upwork-com.types.d.ts.map +1 -1
  137. package/dist/sites/upwork-com/upwork-com.types.js.map +1 -1
  138. package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts +70 -0
  139. package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts.map +1 -0
  140. package/dist/sites/upwork-com/util/parseJobApplicationDetails.js +334 -0
  141. package/dist/sites/upwork-com/util/parseJobApplicationDetails.js.map +1 -0
  142. package/npmrc +5 -0
  143. package/package.json +5 -1
  144. package/src/browser/RuntimeConfig.ts +57 -1
  145. package/src/core/ActorContext.ts +2 -0
  146. package/src/core/ActorRunner.ts +13 -1
  147. package/src/index.ts +2 -0
  148. package/src/native/CompositeNativeWindowDriver.ts +30 -0
  149. package/src/native/NativeActionRegistry.ts +114 -0
  150. package/src/native/NativeAutomation.ts +15 -0
  151. package/src/native/NativeCoordinateMapper.ts +258 -0
  152. package/src/native/NativeFileDialogService.ts +138 -0
  153. package/src/native/NativeImageFinder.ts +33 -0
  154. package/src/native/NativeKeyboard.ts +18 -0
  155. package/src/native/NativeMouse.ts +116 -0
  156. package/src/native/NativeWindowService.ts +229 -0
  157. package/src/native/UnsupportedNativeAutomation.ts +92 -0
  158. package/src/native/WindowMatcher.ts +31 -0
  159. package/src/native/drivers.ts +38 -0
  160. package/src/native/errors.ts +51 -0
  161. package/src/native/index.ts +12 -0
  162. package/src/native/macos/MacOSAccessibilityWindowDriver.ts +183 -0
  163. package/src/native/macos/MacOSAppleScriptClient.ts +182 -0
  164. package/src/native/macos/MacOSFileDialogAccessibilityStrategy.ts +11 -0
  165. package/src/native/macos/MacOSNativeAutomation.ts +86 -0
  166. package/src/native/nut/NutNativeImageFinder.ts +98 -0
  167. package/src/native/nut/NutNativeKeyboardDriver.ts +38 -0
  168. package/src/native/nut/NutNativeMouseDriver.ts +27 -0
  169. package/src/native/nut/NutNativeScreenDriver.ts +14 -0
  170. package/src/native/nut/NutNativeWindowDriver.ts +61 -0
  171. package/src/native/nut/loadNut.ts +86 -0
  172. package/src/native/types.ts +224 -0
  173. package/src/native/utils/appleScriptEscape.ts +11 -0
  174. package/src/native/utils/geometry.ts +88 -0
  175. package/src/native/utils/redactNative.ts +6 -0
  176. package/src/native/utils/waitFor.ts +25 -0
  177. package/src/sites/upwork-com/upwork-com.actor.ts +47 -14
  178. package/src/sites/upwork-com/upwork-com.types.ts +4 -1
  179. package/src/sites/upwork-com/util/parseJobApplicationDetails.ts +622 -0
  180. package/tests/fixtures/makeContext.ts +7 -2
  181. package/tests/fixtures/native/FakeNativeAutomation.ts +138 -0
  182. package/tests/unit/browser/RuntimeConfig.native.test.ts +63 -0
  183. package/tests/unit/core/ActorRunner.native.test.ts +69 -0
  184. package/tests/unit/native/MacOSAppleScriptClient.test.ts +35 -0
  185. package/tests/unit/native/NativeActionRegistry.test.ts +34 -0
  186. package/tests/unit/native/NativeCoordinateMapper.test.ts +92 -0
  187. package/tests/unit/native/NativeFileDialogService.test.ts +91 -0
  188. package/tests/unit/native/NativeMouse.test.ts +91 -0
  189. package/tests/unit/native/NativeWindowService.test.ts +87 -0
  190. package/tests/unit/native/WindowMatcher.test.ts +32 -0
  191. package/tests/unit/native/appleScriptEscape.test.ts +9 -0
  192. package/tests/unit/sites/myvistage-com.login.test.ts +1 -1
  193. package/tests/unit/sites/myvistage-com.postComment.test.ts +0 -1
  194. package/tests/unit/sites/upwork-com.login.test.ts +1 -1
@@ -0,0 +1,38 @@
1
+ import type { NativeMouseButton, NativePoint, NativeRegion, NativeSize, NativeWindowInfo } from './types.js';
2
+
3
+ export interface NativeMouseDriver {
4
+ getPosition(): Promise<NativePoint>;
5
+ move(point: NativePoint): Promise<void>;
6
+ click(button: NativeMouseButton): Promise<void>;
7
+ }
8
+
9
+ export interface NativeKeyboardDriver {
10
+ type(text: string, options?: { delayMs?: number }): Promise<void>;
11
+ pressKey(key: string): Promise<void>;
12
+ pressShortcut(keys: readonly string[]): Promise<void>;
13
+ }
14
+
15
+ export interface NativeScreenDriver {
16
+ width(): Promise<number>;
17
+ height(): Promise<number>;
18
+ }
19
+
20
+ export interface NativeWindowHandle {
21
+ info(): Promise<NativeWindowInfo>;
22
+ focus(): Promise<void>;
23
+ minimize(): Promise<void>;
24
+ restore(): Promise<void>;
25
+ move(point: NativePoint): Promise<void>;
26
+ resize(size: NativeSize): Promise<void>;
27
+ setFullscreen?(fullscreen: boolean): Promise<void>;
28
+ toggleFullscreen?(): Promise<void>;
29
+ }
30
+
31
+ export interface NativeWindowDriver {
32
+ list(): Promise<NativeWindowHandle[]>;
33
+ active?(): Promise<NativeWindowHandle | null>;
34
+ }
35
+
36
+ export interface AppleScriptExecutor {
37
+ execute(script: string, options?: { timeoutMs?: number }): Promise<string>;
38
+ }
@@ -0,0 +1,51 @@
1
+ export class NativeAutomationError extends Error {
2
+ constructor(message: string, readonly meta: Record<string, unknown> = {}) {
3
+ super(message);
4
+ this.name = 'NativeAutomationError';
5
+ }
6
+ }
7
+
8
+ export class UnsupportedPlatformError extends NativeAutomationError {
9
+ constructor(platform: NodeJS.Platform) {
10
+ super(`Native automation is not supported on platform: ${platform}.`, { platform });
11
+ this.name = 'UnsupportedPlatformError';
12
+ }
13
+ }
14
+
15
+ export class NativeDependencyError extends NativeAutomationError {
16
+ constructor(packageName: string, cause?: unknown) {
17
+ super(
18
+ `Native automation dependency ${packageName} is not available. Install the optional native automation peer dependencies before using this feature.`,
19
+ cause === undefined ? { packageName } : { packageName, cause }
20
+ );
21
+ this.name = 'NativeDependencyError';
22
+ }
23
+ }
24
+
25
+ export class NativeWindowNotFoundError extends NativeAutomationError {
26
+ constructor(meta: Record<string, unknown>) {
27
+ super('Native window was not found.', meta);
28
+ this.name = 'NativeWindowNotFoundError';
29
+ }
30
+ }
31
+
32
+ export class NativeImageNotFoundError extends NativeAutomationError {
33
+ constructor(meta: Record<string, unknown>) {
34
+ super('Native image/template was not found on screen.', meta);
35
+ this.name = 'NativeImageNotFoundError';
36
+ }
37
+ }
38
+
39
+ export class NativeFileDialogError extends NativeAutomationError {
40
+ constructor(message: string, meta: Record<string, unknown> = {}) {
41
+ super(message, meta);
42
+ this.name = 'NativeFileDialogError';
43
+ }
44
+ }
45
+
46
+ export class CoordinateCalibrationError extends NativeAutomationError {
47
+ constructor(message: string, meta: Record<string, unknown> = {}) {
48
+ super(message, meta);
49
+ this.name = 'CoordinateCalibrationError';
50
+ }
51
+ }
@@ -0,0 +1,12 @@
1
+ export * from './types.js';
2
+ export * from './errors.js';
3
+ export * from './drivers.js';
4
+ export * from './NativeActionRegistry.js';
5
+ export * from './NativeAutomation.js';
6
+ export * from './NativeCoordinateMapper.js';
7
+ export * from './NativeFileDialogService.js';
8
+ export * from './NativeImageFinder.js';
9
+ export * from './NativeKeyboard.js';
10
+ export * from './NativeMouse.js';
11
+ export * from './NativeWindowService.js';
12
+ export * from './WindowMatcher.js';
@@ -0,0 +1,183 @@
1
+ import type { Logger } from '../../logging/Logger.js';
2
+ import type { NativeWindowDriver, NativeWindowHandle } from '../drivers.js';
3
+ import type { NativePoint, NativeRegion, NativeSize, NativeWindowInfo } from '../types.js';
4
+ import { appleScriptString } from '../utils/appleScriptEscape.js';
5
+ import { MacOSAppleScriptClient } from './MacOSAppleScriptClient.js';
6
+
7
+ const FIELD_SEPARATOR = '\u001f';
8
+ const ROW_SEPARATOR = '\u001e';
9
+
10
+ function parseBoolean(value: string): boolean | undefined {
11
+ if (value === 'true') return true;
12
+ if (value === 'false') return false;
13
+ return undefined;
14
+ }
15
+
16
+ function parseNumber(value: string): number {
17
+ const parsed = Number(value);
18
+ return Number.isFinite(parsed) ? parsed : 0;
19
+ }
20
+
21
+ class MacOSAccessibilityWindowHandle implements NativeWindowHandle {
22
+ constructor(private readonly client: MacOSAppleScriptClient, private currentInfo: NativeWindowInfo) {}
23
+
24
+ async info(): Promise<NativeWindowInfo> {
25
+ return this.currentInfo;
26
+ }
27
+
28
+ async focus(): Promise<void> {
29
+ await this.client.focusWindow(this.matcher(), this.currentInfo.appName ?? 'Google Chrome');
30
+ this.currentInfo = { ...this.currentInfo, isFocused: true };
31
+ }
32
+
33
+ async minimize(): Promise<void> {
34
+ await this.client.minimize(this.matcher(), this.currentInfo.appName ?? 'Google Chrome');
35
+ this.currentInfo = { ...this.currentInfo, isMinimized: true };
36
+ }
37
+
38
+ async restore(): Promise<void> {
39
+ await this.executeOnWindow(`set value of attribute "AXMinimized" of candidateWindow to false`);
40
+ this.currentInfo = { ...this.currentInfo, isMinimized: false };
41
+ }
42
+
43
+ async move(point: NativePoint): Promise<void> {
44
+ await this.executeOnWindow(`set position of candidateWindow to {${Math.round(point.x)}, ${Math.round(point.y)}}`);
45
+ this.currentInfo = {
46
+ ...this.currentInfo,
47
+ region: { ...this.currentInfo.region, x: Math.round(point.x), y: Math.round(point.y) }
48
+ };
49
+ }
50
+
51
+ async resize(size: NativeSize): Promise<void> {
52
+ await this.executeOnWindow(`set size of candidateWindow to {${Math.round(size.width)}, ${Math.round(size.height)}}`);
53
+ this.currentInfo = {
54
+ ...this.currentInfo,
55
+ region: { ...this.currentInfo.region, width: Math.round(size.width), height: Math.round(size.height) }
56
+ };
57
+ }
58
+
59
+ async setFullscreen(fullscreen: boolean): Promise<void> {
60
+ await this.client.setFullscreen(this.matcher(), fullscreen, this.currentInfo.appName ?? 'Google Chrome');
61
+ this.currentInfo = { ...this.currentInfo, isFullscreen: fullscreen };
62
+ }
63
+
64
+ async toggleFullscreen(): Promise<void> {
65
+ await this.client.toggleFullscreen(this.matcher(), this.currentInfo.appName ?? 'Google Chrome');
66
+ if (this.currentInfo.isFullscreen === undefined) {
67
+ this.currentInfo = { ...this.currentInfo };
68
+ } else {
69
+ this.currentInfo = { ...this.currentInfo, isFullscreen: !this.currentInfo.isFullscreen };
70
+ }
71
+ }
72
+
73
+ private matcher() {
74
+ const matcher: { appName?: string; title?: string } = {};
75
+ if (this.currentInfo.appName !== undefined) matcher.appName = this.currentInfo.appName;
76
+ matcher.title = this.currentInfo.title;
77
+ return matcher;
78
+ }
79
+
80
+ private async executeOnWindow(action: string): Promise<void> {
81
+ const appName = this.currentInfo.appName ?? 'Google Chrome';
82
+ const title = this.currentInfo.title;
83
+ const script = `
84
+ tell application "System Events"
85
+ tell process ${appleScriptString(appName)}
86
+ repeat with candidateWindow in windows
87
+ if name of candidateWindow is ${appleScriptString(title)} then
88
+ ${action}
89
+ return
90
+ end if
91
+ end repeat
92
+ end tell
93
+ end tell
94
+ `;
95
+ await this.client.execute(script);
96
+ }
97
+ }
98
+
99
+ export class MacOSAccessibilityWindowDriver implements NativeWindowDriver {
100
+ constructor(private readonly client: MacOSAppleScriptClient, private readonly logger?: Logger) {}
101
+
102
+ async list(): Promise<NativeWindowHandle[]> {
103
+ const script = `
104
+ set fieldSeparator to ASCII character 31
105
+ set rowSeparator to ASCII character 30
106
+ set rows to {}
107
+ tell application "System Events"
108
+ repeat with appProcess in application processes
109
+ set appName to name of appProcess
110
+ repeat with candidateWindow in windows of appProcess
111
+ set windowTitle to ""
112
+ set px to 0
113
+ set py to 0
114
+ set windowWidth to 0
115
+ set windowHeight to 0
116
+ set minimizedValue to ""
117
+ set fullscreenValue to ""
118
+ try
119
+ set windowTitle to name of candidateWindow
120
+ end try
121
+ try
122
+ set windowPosition to position of candidateWindow
123
+ set px to item 1 of windowPosition
124
+ set py to item 2 of windowPosition
125
+ end try
126
+ try
127
+ set windowSize to size of candidateWindow
128
+ set windowWidth to item 1 of windowSize
129
+ set windowHeight to item 2 of windowSize
130
+ end try
131
+ try
132
+ set minimizedValue to (value of attribute "AXMinimized" of candidateWindow) as text
133
+ end try
134
+ try
135
+ set fullscreenValue to (value of attribute "AXFullScreen" of candidateWindow) as text
136
+ end try
137
+ copy (appName & fieldSeparator & windowTitle & fieldSeparator & px & fieldSeparator & py & fieldSeparator & windowWidth & fieldSeparator & windowHeight & fieldSeparator & minimizedValue & fieldSeparator & fullscreenValue) to end of rows
138
+ end repeat
139
+ end repeat
140
+ end tell
141
+ set AppleScript's text item delimiters to rowSeparator
142
+ return rows as text
143
+ `;
144
+
145
+ try {
146
+ const output = await this.client.execute(script);
147
+ if (output.length === 0) return [];
148
+ return output.split(ROW_SEPARATOR)
149
+ .map(row => this.parseRow(row))
150
+ .filter((info): info is NativeWindowInfo => info !== null)
151
+ .map(info => new MacOSAccessibilityWindowHandle(this.client, info));
152
+ } catch (error) {
153
+ this.logger?.debug('Failed to list windows through macOS Accessibility.', { error });
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ private parseRow(row: string): NativeWindowInfo | null {
159
+ const fields = row.split(FIELD_SEPARATOR);
160
+ if (fields.length < 8) return null;
161
+ const [appName, title, xRaw, yRaw, widthRaw, heightRaw, minimizedRaw, fullscreenRaw] = fields;
162
+ if (appName === undefined || title === undefined || xRaw === undefined || yRaw === undefined || widthRaw === undefined || heightRaw === undefined) {
163
+ return null;
164
+ }
165
+
166
+ const region: NativeRegion = {
167
+ x: parseNumber(xRaw),
168
+ y: parseNumber(yRaw),
169
+ width: parseNumber(widthRaw),
170
+ height: parseNumber(heightRaw)
171
+ };
172
+ const info: NativeWindowInfo = {
173
+ appName,
174
+ title,
175
+ region
176
+ };
177
+ const isMinimized = parseBoolean(minimizedRaw ?? '');
178
+ const isFullscreen = parseBoolean(fullscreenRaw ?? '');
179
+ if (isMinimized !== undefined) info.isMinimized = isMinimized;
180
+ if (isFullscreen !== undefined) info.isFullscreen = isFullscreen;
181
+ return info;
182
+ }
183
+ }
@@ -0,0 +1,182 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import type { AppleScriptExecutor } from '../drivers.js';
4
+ import type { WindowMatcher } from '../types.js';
5
+ import { appleScriptString } from '../utils/appleScriptEscape.js';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export class OsaScriptExecutor implements AppleScriptExecutor {
10
+ async execute(script: string, options: { timeoutMs?: number } = {}): Promise<string> {
11
+ const { stdout } = await execFileAsync('osascript', ['-l', 'AppleScript', '-e', script], {
12
+ timeout: options.timeoutMs ?? 15_000,
13
+ maxBuffer: 1024 * 1024
14
+ });
15
+ return stdout.trim();
16
+ }
17
+ }
18
+
19
+ export interface MacOSAppleScriptClientOptions {
20
+ executor?: AppleScriptExecutor;
21
+ timeoutMs?: number;
22
+ }
23
+
24
+ function matcherCondition(matcher: WindowMatcher): string {
25
+ const conditions: string[] = [];
26
+ if (matcher.title !== undefined) {
27
+ conditions.push(`name of candidateWindow is ${appleScriptString(matcher.title)}`);
28
+ }
29
+ if (matcher.titleIncludes !== undefined) {
30
+ conditions.push(`name of candidateWindow contains ${appleScriptString(matcher.titleIncludes)}`);
31
+ }
32
+ if (matcher.role !== undefined) {
33
+ conditions.push(`role of candidateWindow is ${appleScriptString(matcher.role)}`);
34
+ }
35
+ return conditions.length === 0 ? 'true' : conditions.join(' and ');
36
+ }
37
+
38
+ function processTarget(matcher: WindowMatcher, fallbackAppName: string): string {
39
+ return appleScriptString(matcher.appName ?? fallbackAppName);
40
+ }
41
+
42
+ export class MacOSAppleScriptClient {
43
+ private readonly executor: AppleScriptExecutor;
44
+ private readonly timeoutMs: number;
45
+
46
+ constructor(options: MacOSAppleScriptClientOptions = {}) {
47
+ this.executor = options.executor ?? new OsaScriptExecutor();
48
+ this.timeoutMs = options.timeoutMs ?? 15_000;
49
+ }
50
+
51
+ async execute(script: string, timeoutMs?: number): Promise<string> {
52
+ return this.executor.execute(script, { timeoutMs: timeoutMs ?? this.timeoutMs });
53
+ }
54
+
55
+ async isWindowOpen(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<boolean> {
56
+ const script = `
57
+ tell application "System Events"
58
+ if not (exists process ${processTarget(matcher, fallbackAppName)}) then return "false"
59
+ tell process ${processTarget(matcher, fallbackAppName)}
60
+ repeat with candidateWindow in windows
61
+ if ${matcherCondition(matcher)} then return "true"
62
+ end repeat
63
+ end tell
64
+ end tell
65
+ return "false"
66
+ `;
67
+ return (await this.execute(script)) === 'true';
68
+ }
69
+
70
+ async focusWindow(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<void> {
71
+ const script = `
72
+ tell application ${processTarget(matcher, fallbackAppName)} to activate
73
+ tell application "System Events"
74
+ tell process ${processTarget(matcher, fallbackAppName)}
75
+ repeat with candidateWindow in windows
76
+ if ${matcherCondition(matcher)} then
77
+ perform action "AXRaise" of candidateWindow
78
+ return
79
+ end if
80
+ end repeat
81
+ end tell
82
+ end tell
83
+ `;
84
+ await this.execute(script);
85
+ }
86
+
87
+ async setFullscreen(matcher: WindowMatcher, fullscreen: boolean, fallbackAppName = 'Google Chrome'): Promise<void> {
88
+ const desired = fullscreen ? 'true' : 'false';
89
+ const script = `
90
+ tell application ${processTarget(matcher, fallbackAppName)} to activate
91
+ tell application "System Events"
92
+ tell process ${processTarget(matcher, fallbackAppName)}
93
+ repeat with candidateWindow in windows
94
+ if ${matcherCondition(matcher)} then
95
+ set currentFullscreen to false
96
+ try
97
+ set currentFullscreen to value of attribute "AXFullScreen" of candidateWindow
98
+ end try
99
+ if currentFullscreen is not ${desired} then
100
+ try
101
+ perform action "AXPress" of (first button of candidateWindow whose subrole is "AXFullScreenButton")
102
+ on error
103
+ click (first button of candidateWindow whose subrole is "AXFullScreenButton")
104
+ end try
105
+ end if
106
+ return
107
+ end if
108
+ end repeat
109
+ end tell
110
+ end tell
111
+ `;
112
+ await this.execute(script);
113
+ }
114
+
115
+ async toggleFullscreen(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<void> {
116
+ const script = `
117
+ tell application ${processTarget(matcher, fallbackAppName)} to activate
118
+ tell application "System Events"
119
+ tell process ${processTarget(matcher, fallbackAppName)}
120
+ repeat with candidateWindow in windows
121
+ if ${matcherCondition(matcher)} then
122
+ try
123
+ perform action "AXPress" of (first button of candidateWindow whose subrole is "AXFullScreenButton")
124
+ on error
125
+ click (first button of candidateWindow whose subrole is "AXFullScreenButton")
126
+ end try
127
+ return
128
+ end if
129
+ end repeat
130
+ end tell
131
+ end tell
132
+ `;
133
+ await this.execute(script);
134
+ }
135
+
136
+ async minimize(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<void> {
137
+ const script = `
138
+ tell application "System Events"
139
+ tell process ${processTarget(matcher, fallbackAppName)}
140
+ repeat with candidateWindow in windows
141
+ if ${matcherCondition(matcher)} then
142
+ try
143
+ perform action "AXPress" of (first button of candidateWindow whose subrole is "AXMinimizeButton")
144
+ on error
145
+ set value of attribute "AXMinimized" of candidateWindow to true
146
+ end try
147
+ return
148
+ end if
149
+ end repeat
150
+ end tell
151
+ end tell
152
+ `;
153
+ await this.execute(script);
154
+ }
155
+
156
+ async selectFileInOpenDialog(filePath: string, appName = 'Google Chrome', timeoutMs?: number): Promise<void> {
157
+ const script = `
158
+ set targetPath to ${appleScriptString(filePath)}
159
+ tell application ${appleScriptString(appName)} to activate
160
+ tell application "System Events"
161
+ tell process ${appleScriptString(appName)}
162
+ set frontmost to true
163
+ set waited to 0
164
+ repeat while waited < 100
165
+ if (exists window 1) then exit repeat
166
+ delay 0.1
167
+ set waited to waited + 1
168
+ end repeat
169
+ keystroke "g" using {command down, shift down}
170
+ delay 0.2
171
+ set the clipboard to targetPath
172
+ keystroke "v" using {command down}
173
+ delay 0.1
174
+ key code 36
175
+ delay 0.2
176
+ key code 36
177
+ end tell
178
+ end tell
179
+ `;
180
+ await this.execute(script, timeoutMs);
181
+ }
182
+ }
@@ -0,0 +1,11 @@
1
+ import type { SelectFileOptions } from '../types.js';
2
+ import type { FileDialogAccessibilityStrategy } from '../NativeFileDialogService.js';
3
+ import { MacOSAppleScriptClient } from './MacOSAppleScriptClient.js';
4
+
5
+ export class MacOSFileDialogAccessibilityStrategy implements FileDialogAccessibilityStrategy {
6
+ constructor(private readonly client: MacOSAppleScriptClient, private readonly defaultAppName = 'Google Chrome') {}
7
+
8
+ async selectFile(filePath: string, options: SelectFileOptions): Promise<void> {
9
+ await this.client.selectFileInOpenDialog(filePath, options.appName ?? this.defaultAppName, options.timeoutMs);
10
+ }
11
+ }
@@ -0,0 +1,86 @@
1
+ import type { NativeAutomation, NativeAutomationFactoryArgs } from '../types.js';
2
+ import { DefaultNativeActionRegistry, registerDefaultNativeActions } from '../NativeActionRegistry.js';
3
+ import { HumanizedNativeMouse, type HumanizedNativeMouseOptions } from '../NativeMouse.js';
4
+ import { DefaultNativeKeyboard } from '../NativeKeyboard.js';
5
+ import { DefaultNativeWindowService, type NativeWindowServiceDependencies } from '../NativeWindowService.js';
6
+ import { DefaultNativeImageFinder } from '../NativeImageFinder.js';
7
+ import { ImageBasedCoordinateMapper, type ImageBasedCoordinateMapperOptions } from '../NativeCoordinateMapper.js';
8
+ import { DefaultNativeFileDialogService, type NativeFileDialogServiceDependencies } from '../NativeFileDialogService.js';
9
+ import { NutNativeMouseDriver } from '../nut/NutNativeMouseDriver.js';
10
+ import { NutNativeKeyboardDriver } from '../nut/NutNativeKeyboardDriver.js';
11
+ import { NutNativeScreenDriver } from '../nut/NutNativeScreenDriver.js';
12
+ import { NutNativeWindowDriver } from '../nut/NutNativeWindowDriver.js';
13
+ import { FallbackNativeWindowDriver } from '../CompositeNativeWindowDriver.js';
14
+ import { MacOSAccessibilityWindowDriver } from './MacOSAccessibilityWindowDriver.js';
15
+ import { NutNativeImageFinderDriver, type NutNativeImageFinderOptions } from '../nut/NutNativeImageFinder.js';
16
+ import { MacOSAppleScriptClient, type MacOSAppleScriptClientOptions } from './MacOSAppleScriptClient.js';
17
+ import { MacOSFileDialogAccessibilityStrategy } from './MacOSFileDialogAccessibilityStrategy.js';
18
+
19
+ export function createMacOSNativeAutomation(args: NativeAutomationFactoryArgs): NativeAutomation {
20
+ const nativeConfig = args.config.native;
21
+ const mouseDriver = new NutNativeMouseDriver();
22
+ const keyboardDriver = new NutNativeKeyboardDriver();
23
+ const screenDriver = new NutNativeScreenDriver();
24
+ const appleScriptOptions: MacOSAppleScriptClientOptions = {};
25
+ const osascriptTimeoutMs = nativeConfig?.macos?.osascriptTimeoutMs ?? args.config.defaultTimeoutMs;
26
+ if (osascriptTimeoutMs !== undefined) appleScriptOptions.timeoutMs = osascriptTimeoutMs;
27
+ const appleScriptClient = new MacOSAppleScriptClient(appleScriptOptions);
28
+ const windowDriver = new FallbackNativeWindowDriver(
29
+ new MacOSAccessibilityWindowDriver(appleScriptClient, args.logger),
30
+ new NutNativeWindowDriver(),
31
+ args.logger
32
+ );
33
+ const keyboard = new DefaultNativeKeyboard(keyboardDriver);
34
+ const imageDriverOptions: NutNativeImageFinderOptions = { enableNlMatcher: true };
35
+ if (nativeConfig?.templatesDir !== undefined) imageDriverOptions.templatesDir = nativeConfig.templatesDir;
36
+ if (nativeConfig?.imageConfidence !== undefined) imageDriverOptions.confidence = nativeConfig.imageConfidence;
37
+ const imageDriver = new NutNativeImageFinderDriver(imageDriverOptions);
38
+ const images = new DefaultNativeImageFinder(imageDriver, args.config.defaultTimeoutMs);
39
+ const coordinateOptions: ImageBasedCoordinateMapperOptions = {
40
+ images,
41
+ logger: args.logger
42
+ };
43
+ const coordinateTimeoutMs = nativeConfig?.coordinateCalibration?.timeoutMs ?? args.config.defaultTimeoutMs;
44
+ if (coordinateTimeoutMs !== undefined) coordinateOptions.defaultTimeoutMs = coordinateTimeoutMs;
45
+ if (nativeConfig?.coordinateCalibration?.cacheTtlMs !== undefined) {
46
+ coordinateOptions.defaultCacheTtlMs = nativeConfig.coordinateCalibration.cacheTtlMs;
47
+ }
48
+ const coordinates = new ImageBasedCoordinateMapper(coordinateOptions);
49
+ const mouseOptions: HumanizedNativeMouseOptions = { coordinateMapper: coordinates };
50
+ if (nativeConfig?.mouse?.jitterPixels !== undefined) mouseOptions.jitterPixels = nativeConfig.mouse.jitterPixels;
51
+ if (nativeConfig?.mouse?.minJitterPixels !== undefined) mouseOptions.minJitterPixels = nativeConfig.mouse.minJitterPixels;
52
+ if (nativeConfig?.mouse?.moveSpeed !== undefined) mouseOptions.moveSpeed = nativeConfig.mouse.moveSpeed;
53
+ const mouse = new HumanizedNativeMouse(mouseDriver, mouseOptions);
54
+ const windowOptions: NativeWindowServiceDependencies = {
55
+ driver: windowDriver,
56
+ screen: screenDriver,
57
+ mouse,
58
+ images,
59
+ logger: args.logger
60
+ };
61
+ if (args.config.defaultTimeoutMs !== undefined) windowOptions.defaultTimeoutMs = args.config.defaultTimeoutMs;
62
+ const windows = new DefaultNativeWindowService(windowOptions);
63
+ const fileDialogOptions: NativeFileDialogServiceDependencies = {
64
+ keyboard,
65
+ mouse,
66
+ images,
67
+ accessibility: new MacOSFileDialogAccessibilityStrategy(appleScriptClient, nativeConfig?.macos?.targetApplicationName ?? 'Google Chrome'),
68
+ logger: args.logger
69
+ };
70
+ if (args.config.defaultTimeoutMs !== undefined) fileDialogOptions.defaultTimeoutMs = args.config.defaultTimeoutMs;
71
+ const fileDialogs = new DefaultNativeFileDialogService(fileDialogOptions);
72
+
73
+ const automation = {
74
+ windows,
75
+ fileDialogs,
76
+ mouse,
77
+ keyboard,
78
+ coordinates,
79
+ images,
80
+ actions: undefined as unknown as DefaultNativeActionRegistry
81
+ } satisfies Omit<NativeAutomation, 'actions'> & { actions: DefaultNativeActionRegistry };
82
+
83
+ const registry = new DefaultNativeActionRegistry(() => ({ automation, logger: args.logger }));
84
+ automation.actions = registerDefaultNativeActions(registry, automation, args.logger);
85
+ return automation;
86
+ }
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import type { NativeImageFinderDriver } from '../NativeImageFinder.js';
5
+ import type { NativeImageMatch, NativeImageSearchOptions, NativeRegion, NativeTemplateRef } from '../types.js';
6
+ import { loadNlMatcher, loadNut, type NutRegion } from './loadNut.js';
7
+
8
+ export interface NutNativeImageFinderOptions {
9
+ templatesDir?: string;
10
+ confidence?: number;
11
+ enableNlMatcher?: boolean;
12
+ }
13
+
14
+ function toRegion(region: NutRegion): NativeRegion {
15
+ return {
16
+ x: region.left ?? region.x ?? 0,
17
+ y: region.top ?? region.y ?? 0,
18
+ width: region.width,
19
+ height: region.height
20
+ };
21
+ }
22
+
23
+ function templateName(template: string | NativeTemplateRef): string {
24
+ return typeof template === 'string' ? template : template.name;
25
+ }
26
+
27
+ export class NutNativeImageFinderDriver implements NativeImageFinderDriver {
28
+ private initialized = false;
29
+
30
+ constructor(private readonly options: NutNativeImageFinderOptions = {}) {}
31
+
32
+ async findTemplate(template: string | NativeTemplateRef, options: NativeImageSearchOptions = {}): Promise<NativeImageMatch | null> {
33
+ const nut = await this.initialize();
34
+ if (nut.screen.find === undefined || nut.imageResource === undefined) {
35
+ return null;
36
+ }
37
+
38
+ const resourcePath = await this.materializeTemplate(template);
39
+ const searchOptions = this.buildSearchOptions(options);
40
+
41
+ try {
42
+ const region = await nut.screen.find(nut.imageResource(resourcePath), searchOptions);
43
+ const match: NativeImageMatch = { region: toRegion(region) };
44
+ if (options.confidence !== undefined) match.confidence = options.confidence;
45
+ return match;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ private async initialize() {
52
+ const nut = await loadNut();
53
+ if (!this.initialized) {
54
+ if (this.options.enableNlMatcher ?? true) {
55
+ await loadNlMatcher();
56
+ }
57
+ if (this.options.templatesDir !== undefined && nut.screen.config !== undefined) {
58
+ nut.screen.config.resourceDirectory = this.options.templatesDir;
59
+ }
60
+ if (this.options.confidence !== undefined && nut.screen.config !== undefined) {
61
+ nut.screen.config.confidence = this.options.confidence;
62
+ }
63
+ this.initialized = true;
64
+ }
65
+ return nut;
66
+ }
67
+
68
+ private buildSearchOptions(options: NativeImageSearchOptions): Record<string, unknown> | undefined {
69
+ const searchOptions: Record<string, unknown> = {};
70
+ if (options.searchRegion !== undefined) {
71
+ searchOptions.searchRegion = {
72
+ left: options.searchRegion.x,
73
+ top: options.searchRegion.y,
74
+ width: options.searchRegion.width,
75
+ height: options.searchRegion.height
76
+ };
77
+ }
78
+ if (options.confidence !== undefined) {
79
+ searchOptions.confidence = options.confidence;
80
+ }
81
+ return Object.keys(searchOptions).length === 0 ? undefined : searchOptions;
82
+ }
83
+
84
+ private async materializeTemplate(template: string | NativeTemplateRef): Promise<string> {
85
+ if (typeof template === 'string') {
86
+ return this.options.templatesDir === undefined ? template : path.join(this.options.templatesDir, template);
87
+ }
88
+
89
+ if (template.buffer === undefined) {
90
+ return this.options.templatesDir === undefined ? template.name : path.join(this.options.templatesDir, template.name);
91
+ }
92
+
93
+ const fileName = `${template.name.replace(/[^a-z0-9._-]/gi, '_')}`;
94
+ const filePath = path.join(os.tmpdir(), fileName);
95
+ await fs.writeFile(filePath, template.buffer);
96
+ return filePath;
97
+ }
98
+ }