@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 { NativeKeyboardDriver } from '../drivers.js';
2
+ import { loadNut } from './loadNut.js';
3
+
4
+ function keyName(key: string): string {
5
+ const normalized = key.toLowerCase();
6
+ if (normalized === 'enter' || normalized === 'return') return 'ENTER';
7
+ if (normalized === 'escape' || normalized === 'esc') return 'ESCAPE';
8
+ if (normalized === 'meta' || normalized === 'command' || normalized === 'cmd') return 'LEFT_META';
9
+ if (normalized === 'control' || normalized === 'ctrl') return 'LEFT_CONTROL';
10
+ if (normalized === 'shift') return 'LEFT_SHIFT';
11
+ if (normalized === 'alt' || normalized === 'option') return 'LEFT_ALT';
12
+ return key.toUpperCase();
13
+ }
14
+
15
+ export class NutNativeKeyboardDriver implements NativeKeyboardDriver {
16
+ async type(text: string): Promise<void> {
17
+ const nut = await loadNut();
18
+ await nut.keyboard.type(text);
19
+ }
20
+
21
+ async pressKey(key: string): Promise<void> {
22
+ const nut = await loadNut();
23
+ await nut.keyboard.pressKey(this.resolveKey(nut.Key, key));
24
+ }
25
+
26
+ async pressShortcut(keys: readonly string[]): Promise<void> {
27
+ const nut = await loadNut();
28
+ const resolved = keys.map(key => this.resolveKey(nut.Key, key));
29
+ await nut.keyboard.pressKey(...resolved);
30
+ if (nut.keyboard.releaseKey !== undefined) {
31
+ await nut.keyboard.releaseKey(...resolved.slice().reverse());
32
+ }
33
+ }
34
+
35
+ private resolveKey(keyMap: Record<string, unknown> | undefined, key: string): unknown {
36
+ return keyMap?.[keyName(key)] ?? key;
37
+ }
38
+ }
@@ -0,0 +1,27 @@
1
+ import type { NativeMouseDriver } from '../drivers.js';
2
+ import type { NativeMouseButton, NativePoint } from '../types.js';
3
+ import { loadNut } from './loadNut.js';
4
+
5
+ function buttonName(button: NativeMouseButton): string {
6
+ if (button === 'left') return 'LEFT';
7
+ if (button === 'right') return 'RIGHT';
8
+ return 'MIDDLE';
9
+ }
10
+
11
+ export class NutNativeMouseDriver implements NativeMouseDriver {
12
+ async getPosition(): Promise<NativePoint> {
13
+ const nut = await loadNut();
14
+ return nut.mouse.getPosition();
15
+ }
16
+
17
+ async move(point: NativePoint): Promise<void> {
18
+ const nut = await loadNut();
19
+ await nut.mouse.move(nut.straightTo({ x: point.x, y: point.y }));
20
+ }
21
+
22
+ async click(button: NativeMouseButton): Promise<void> {
23
+ const nut = await loadNut();
24
+ const nutButton = nut.Button?.[buttonName(button)] ?? button;
25
+ await nut.mouse.click(nutButton);
26
+ }
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { NativeScreenDriver } from '../drivers.js';
2
+ import { loadNut } from './loadNut.js';
3
+
4
+ export class NutNativeScreenDriver implements NativeScreenDriver {
5
+ async width(): Promise<number> {
6
+ const nut = await loadNut();
7
+ return nut.screen.width();
8
+ }
9
+
10
+ async height(): Promise<number> {
11
+ const nut = await loadNut();
12
+ return nut.screen.height();
13
+ }
14
+ }
@@ -0,0 +1,61 @@
1
+ import type { NativeWindowDriver, NativeWindowHandle } from '../drivers.js';
2
+ import type { NativePoint, NativeRegion, NativeSize, NativeWindowInfo } from '../types.js';
3
+ import { loadNut, type NutRegion, type NutWindow } from './loadNut.js';
4
+
5
+ function toRegion(region: NutRegion): NativeRegion {
6
+ return {
7
+ x: region.left ?? region.x ?? 0,
8
+ y: region.top ?? region.y ?? 0,
9
+ width: region.width,
10
+ height: region.height
11
+ };
12
+ }
13
+
14
+ class NutNativeWindowHandle implements NativeWindowHandle {
15
+ constructor(private readonly window: NutWindow) {}
16
+
17
+ async info(): Promise<NativeWindowInfo> {
18
+ const [title, region] = await Promise.all([
19
+ Promise.resolve(this.window.title),
20
+ Promise.resolve(this.window.region)
21
+ ]);
22
+ return {
23
+ title,
24
+ region: toRegion(region)
25
+ };
26
+ }
27
+
28
+ async focus(): Promise<void> {
29
+ await this.window.focus();
30
+ }
31
+
32
+ async minimize(): Promise<void> {
33
+ await this.window.minimize();
34
+ }
35
+
36
+ async restore(): Promise<void> {
37
+ await this.window.restore();
38
+ }
39
+
40
+ async move(point: NativePoint): Promise<void> {
41
+ await this.window.move({ x: point.x, y: point.y });
42
+ }
43
+
44
+ async resize(size: NativeSize): Promise<void> {
45
+ await this.window.resize(size);
46
+ }
47
+ }
48
+
49
+ export class NutNativeWindowDriver implements NativeWindowDriver {
50
+ async list(): Promise<NativeWindowHandle[]> {
51
+ const nut = await loadNut();
52
+ const windows = await nut.getWindows();
53
+ return windows.map(window => new NutNativeWindowHandle(window));
54
+ }
55
+
56
+ async active(): Promise<NativeWindowHandle | null> {
57
+ const nut = await loadNut();
58
+ const window = await nut.getActiveWindow();
59
+ return new NutNativeWindowHandle(window);
60
+ }
61
+ }
@@ -0,0 +1,86 @@
1
+ import { NativeDependencyError } from '../errors.js';
2
+
3
+ type DynamicImport = (specifier: string) => Promise<unknown>;
4
+
5
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as DynamicImport;
6
+
7
+ export interface NutPoint {
8
+ x: number;
9
+ y: number;
10
+ }
11
+
12
+ export interface NutRegion {
13
+ left?: number;
14
+ top?: number;
15
+ x?: number;
16
+ y?: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
21
+ export interface NutWindow {
22
+ title: Promise<string> | string;
23
+ region: Promise<NutRegion> | NutRegion;
24
+ focus(): Promise<void>;
25
+ minimize(): Promise<void>;
26
+ restore(): Promise<void>;
27
+ move(point: NutPoint): Promise<void>;
28
+ resize(size: { width: number; height: number }): Promise<void>;
29
+ }
30
+
31
+ export interface NutModule {
32
+ mouse: {
33
+ getPosition(): Promise<NutPoint>;
34
+ move(path: unknown): Promise<void>;
35
+ click(button: unknown): Promise<void>;
36
+ };
37
+ keyboard: {
38
+ type(text: string): Promise<void>;
39
+ pressKey(...keys: unknown[]): Promise<void>;
40
+ releaseKey?(...keys: unknown[]): Promise<void>;
41
+ };
42
+ screen: {
43
+ width(): Promise<number>;
44
+ height(): Promise<number>;
45
+ find?(resource: unknown, options?: unknown): Promise<NutRegion>;
46
+ waitFor?(resource: unknown, timeoutMs?: number, intervalMs?: number): Promise<NutRegion>;
47
+ config?: {
48
+ confidence?: number;
49
+ resourceDirectory?: string;
50
+ };
51
+ };
52
+ getWindows(): Promise<NutWindow[]>;
53
+ getActiveWindow(): Promise<NutWindow>;
54
+ straightTo(point: NutPoint): unknown;
55
+ imageResource?(path: string): unknown;
56
+ Button?: Record<string, unknown>;
57
+ Key?: Record<string, unknown>;
58
+ }
59
+
60
+ export interface NlMatcherModule {
61
+ useNlMatcher(): void;
62
+ }
63
+
64
+ let nutModulePromise: Promise<NutModule> | null = null;
65
+ let nlMatcherLoaded = false;
66
+
67
+ export async function loadNut(): Promise<NutModule> {
68
+ nutModulePromise ??= dynamicImport('@nut-tree/nut-js')
69
+ .then(module => module as NutModule)
70
+ .catch(error => {
71
+ nutModulePromise = null;
72
+ throw new NativeDependencyError('@nut-tree/nut-js', error);
73
+ });
74
+ return nutModulePromise;
75
+ }
76
+
77
+ export async function loadNlMatcher(): Promise<void> {
78
+ if (nlMatcherLoaded) return;
79
+ const module = await dynamicImport('@nut-tree/nl-matcher')
80
+ .then(value => value as NlMatcherModule)
81
+ .catch(error => {
82
+ throw new NativeDependencyError('@nut-tree/nl-matcher', error);
83
+ });
84
+ module.useNlMatcher();
85
+ nlMatcherLoaded = true;
86
+ }
@@ -0,0 +1,224 @@
1
+ import type { BrowserSession } from '../browser/BrowserSession.js';
2
+ import type { PageLike } from '../browser/PuppeteerLike.js';
3
+ import type { RuntimeConfig } from '../browser/RuntimeConfig.js';
4
+ import type { Logger } from '../logging/Logger.js';
5
+
6
+ export type NativePlatform = 'macos';
7
+ export type NativeMouseButton = 'left' | 'right' | 'middle';
8
+ export type NativeFileDialogStrategy = 'auto' | 'applescript' | 'visual' | 'keyboard';
9
+ export type NativeWindowControlStrategy = 'auto' | 'accessibility' | 'visual';
10
+
11
+ export interface NativePoint {
12
+ x: number;
13
+ y: number;
14
+ }
15
+
16
+ export interface ScreenPoint extends NativePoint {}
17
+ export interface BrowserPoint extends NativePoint {}
18
+
19
+ export interface NativeSize {
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface NativeRegion extends NativePoint, NativeSize {}
25
+
26
+ export interface WaitOptions {
27
+ timeoutMs?: number;
28
+ intervalMs?: number;
29
+ }
30
+
31
+ export interface WindowMatcher {
32
+ appName?: string;
33
+ title?: string;
34
+ titleIncludes?: string;
35
+ titleRegex?: RegExp | string;
36
+ bundleId?: string;
37
+ role?: string;
38
+ }
39
+
40
+ export interface NativeWindowInfo {
41
+ id?: string | number;
42
+ title: string;
43
+ appName?: string;
44
+ bundleId?: string;
45
+ role?: string;
46
+ region: NativeRegion;
47
+ isFocused?: boolean;
48
+ isMinimized?: boolean;
49
+ isFullscreen?: boolean;
50
+ }
51
+
52
+ export interface EnsureWindowInViewOptions extends WaitOptions {
53
+ restore?: boolean;
54
+ focus?: boolean;
55
+ moveIntoPrimaryDisplay?: boolean;
56
+ minimumVisibleWidth?: number;
57
+ minimumVisibleHeight?: number;
58
+ }
59
+
60
+ export interface WindowButtonOptions extends WaitOptions {
61
+ strategy?: NativeWindowControlStrategy;
62
+ fullscreenButtonTemplate?: string;
63
+ searchRegionPaddingPx?: number;
64
+ }
65
+
66
+ export interface SelectFileOptions extends WaitOptions {
67
+ appName?: string;
68
+ dialogTitle?: string;
69
+ openButtonTemplate?: string;
70
+ inputFieldTemplate?: string;
71
+ strategy?: NativeFileDialogStrategy;
72
+ clickOpen?: boolean;
73
+ }
74
+
75
+ export interface NativeMouseMoveOptions {
76
+ precise?: boolean;
77
+ jitterPixels?: number;
78
+ minJitterPixels?: number;
79
+ moveSpeed?: number;
80
+ disableHumanPath?: boolean;
81
+ }
82
+
83
+ export interface NativeMouseClickOptions extends NativeMouseMoveOptions {
84
+ button?: NativeMouseButton;
85
+ delayBeforeClickMs?: number;
86
+ delayAfterMoveMs?: number;
87
+ regionPaddingPx?: number;
88
+ }
89
+
90
+ export interface NativeKeyboardTypeOptions {
91
+ delayMs?: number;
92
+ }
93
+
94
+ export interface NativeKeyboard {
95
+ type(text: string, options?: NativeKeyboardTypeOptions): Promise<void>;
96
+ pressKey(key: string): Promise<void>;
97
+ pressShortcut(keys: readonly string[]): Promise<void>;
98
+ }
99
+
100
+ export interface NativeMouse {
101
+ moveTo(point: ScreenPoint, options?: NativeMouseMoveOptions): Promise<void>;
102
+ click(point: ScreenPoint, options?: NativeMouseClickOptions): Promise<void>;
103
+ clickRegion(region: NativeRegion, options?: NativeMouseClickOptions): Promise<void>;
104
+ clickBrowserPoint(page: PageLike, point: BrowserPoint, options?: NativeMouseClickOptions): Promise<void>;
105
+ }
106
+
107
+ export interface NativeTemplateRef {
108
+ name: string;
109
+ buffer?: Uint8Array;
110
+ }
111
+
112
+ export interface NativeImageSearchOptions extends WaitOptions {
113
+ searchRegion?: NativeRegion;
114
+ confidence?: number;
115
+ }
116
+
117
+ export interface NativeImageMatch {
118
+ region: NativeRegion;
119
+ confidence?: number;
120
+ }
121
+
122
+ export interface NativeImageFinder {
123
+ findTemplate(template: string | NativeTemplateRef, options?: NativeImageSearchOptions): Promise<NativeImageMatch | null>;
124
+ waitForTemplate(template: string | NativeTemplateRef, options?: NativeImageSearchOptions): Promise<NativeImageMatch>;
125
+ }
126
+
127
+ export interface NativeWindowService {
128
+ list(): Promise<NativeWindowInfo[]>;
129
+ find(matcher: WindowMatcher, options?: WaitOptions): Promise<NativeWindowInfo | null>;
130
+ waitFor(matcher: WindowMatcher, options?: WaitOptions): Promise<NativeWindowInfo>;
131
+ isOpen(matcher: WindowMatcher): Promise<boolean>;
132
+ focus(matcher: WindowMatcher): Promise<NativeWindowInfo>;
133
+ ensureInView(matcher: WindowMatcher, options?: EnsureWindowInViewOptions): Promise<NativeWindowInfo>;
134
+ setFullscreen(matcher: WindowMatcher, fullscreen: boolean, options?: WindowButtonOptions): Promise<void>;
135
+ toggleFullscreen(matcher: WindowMatcher, options?: WindowButtonOptions): Promise<void>;
136
+ minimize(matcher: WindowMatcher): Promise<void>;
137
+ restore(matcher: WindowMatcher): Promise<void>;
138
+ }
139
+
140
+ export interface NativeFileDialogService {
141
+ selectFile(filePath: string, options?: SelectFileOptions): Promise<void>;
142
+ }
143
+
144
+ export interface BrowserViewportSnapshot {
145
+ screenX: number;
146
+ screenY: number;
147
+ outerWidth: number;
148
+ outerHeight: number;
149
+ innerWidth: number;
150
+ innerHeight: number;
151
+ devicePixelRatio: number;
152
+ }
153
+
154
+ export interface CoordinateTransform {
155
+ offsetX: number;
156
+ offsetY: number;
157
+ scaleX: number;
158
+ scaleY: number;
159
+ calibratedAt: number;
160
+ viewport: Pick<BrowserViewportSnapshot, 'innerWidth' | 'innerHeight' | 'devicePixelRatio'>;
161
+ window?: NativeWindowInfo;
162
+ }
163
+
164
+ export interface CalibrationOptions extends WaitOptions {
165
+ markerSizePx?: number;
166
+ markerA?: BrowserPoint;
167
+ markerB?: BrowserPoint;
168
+ cacheTtlMs?: number;
169
+ windowMatcher?: WindowMatcher;
170
+ }
171
+
172
+ export interface CoordinateMapOptions {
173
+ page?: PageLike;
174
+ useCache?: boolean;
175
+ }
176
+
177
+ export interface NativeCoordinateMapper {
178
+ calibrate(page: PageLike, options?: CalibrationOptions): Promise<CoordinateTransform>;
179
+ browserToScreen(point: BrowserPoint, options?: CoordinateMapOptions): Promise<ScreenPoint>;
180
+ screenToBrowser(point: ScreenPoint, options?: CoordinateMapOptions): Promise<BrowserPoint>;
181
+ invalidate(): void;
182
+ }
183
+
184
+ export interface NativeActionContext {
185
+ automation: NativeAutomation;
186
+ logger: Logger;
187
+ }
188
+
189
+ export interface NativeAction<TInput = unknown, TOutput = unknown> {
190
+ name: string;
191
+ description?: string;
192
+ run(context: NativeActionContext, input: TInput): Promise<TOutput>;
193
+ }
194
+
195
+ export interface NativeActionMetadata {
196
+ name: string;
197
+ description?: string;
198
+ }
199
+
200
+ export interface NativeActionRegistry {
201
+ register<TInput, TOutput>(action: NativeAction<TInput, TOutput>): this;
202
+ get<TInput = unknown, TOutput = unknown>(name: string): NativeAction<TInput, TOutput>;
203
+ run<TInput = unknown, TOutput = unknown>(name: string, input: TInput): Promise<TOutput>;
204
+ list(): NativeActionMetadata[];
205
+ }
206
+
207
+ export interface NativeAutomation {
208
+ windows: NativeWindowService;
209
+ fileDialogs: NativeFileDialogService;
210
+ mouse: NativeMouse;
211
+ keyboard: NativeKeyboard;
212
+ coordinates: NativeCoordinateMapper;
213
+ images: NativeImageFinder;
214
+ actions: NativeActionRegistry;
215
+ }
216
+
217
+ export interface NativeAutomationFactoryArgs {
218
+ session: BrowserSession;
219
+ page: PageLike;
220
+ config: RuntimeConfig;
221
+ logger: Logger;
222
+ }
223
+
224
+ export type NativeAutomationFactory = (args: NativeAutomationFactoryArgs) => NativeAutomation;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Escapes arbitrary text for use as an AppleScript string literal.
3
+ * AppleScript string literals are double-quoted and escape double quotes/backslashes.
4
+ */
5
+ export function appleScriptString(value: string): string {
6
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
7
+ }
8
+
9
+ export function appleScriptRegexLiteral(value: RegExp | string): string {
10
+ return value instanceof RegExp ? value.source : value;
11
+ }
@@ -0,0 +1,88 @@
1
+ import type { NativePoint, NativeRegion } from '../types.js';
2
+
3
+ export function centerOf(region: NativeRegion): NativePoint {
4
+ return {
5
+ x: region.x + region.width / 2,
6
+ y: region.y + region.height / 2
7
+ };
8
+ }
9
+
10
+ export function normalizeRegion(region: NativeRegion): NativeRegion {
11
+ return {
12
+ x: region.x,
13
+ y: region.y,
14
+ width: Math.max(0, region.width),
15
+ height: Math.max(0, region.height)
16
+ };
17
+ }
18
+
19
+ export function isPointInsideRegion(point: NativePoint, region: NativeRegion): boolean {
20
+ return point.x >= region.x
21
+ && point.x <= region.x + region.width
22
+ && point.y >= region.y
23
+ && point.y <= region.y + region.height;
24
+ }
25
+
26
+ export function clampPointToRegion(point: NativePoint, region: NativeRegion): NativePoint {
27
+ return {
28
+ x: Math.min(Math.max(point.x, region.x), region.x + region.width),
29
+ y: Math.min(Math.max(point.y, region.y), region.y + region.height)
30
+ };
31
+ }
32
+
33
+ export function paddedRegion(region: NativeRegion, paddingPx: number): NativeRegion {
34
+ const x = region.x + paddingPx;
35
+ const y = region.y + paddingPx;
36
+ return normalizeRegion({
37
+ x,
38
+ y,
39
+ width: region.width - paddingPx * 2,
40
+ height: region.height - paddingPx * 2
41
+ });
42
+ }
43
+
44
+ export function intersects(a: NativeRegion, b: NativeRegion): boolean {
45
+ return a.x < b.x + b.width
46
+ && a.x + a.width > b.x
47
+ && a.y < b.y + b.height
48
+ && a.y + a.height > b.y;
49
+ }
50
+
51
+ export function intersection(a: NativeRegion, b: NativeRegion): NativeRegion {
52
+ const x = Math.max(a.x, b.x);
53
+ const y = Math.max(a.y, b.y);
54
+ const right = Math.min(a.x + a.width, b.x + b.width);
55
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
56
+
57
+ return normalizeRegion({
58
+ x,
59
+ y,
60
+ width: right - x,
61
+ height: bottom - y
62
+ });
63
+ }
64
+
65
+ export function visibleEnough(region: NativeRegion, viewport: NativeRegion, minimumWidth: number, minimumHeight: number): boolean {
66
+ if (!intersects(region, viewport)) return false;
67
+ const visible = intersection(region, viewport);
68
+ return visible.width >= Math.min(region.width, minimumWidth) && visible.height >= Math.min(region.height, minimumHeight);
69
+ }
70
+
71
+ export function randomPointInRegion(region: NativeRegion, random: () => number, paddingPx = 0): NativePoint {
72
+ const safe = paddedRegion(region, paddingPx);
73
+ if (safe.width <= 0 || safe.height <= 0) {
74
+ return centerOf(region);
75
+ }
76
+
77
+ return {
78
+ x: safe.x + random() * safe.width,
79
+ y: safe.y + random() * safe.height
80
+ };
81
+ }
82
+
83
+ export function roundPoint(point: NativePoint): NativePoint {
84
+ return {
85
+ x: Math.round(point.x),
86
+ y: Math.round(point.y)
87
+ };
88
+ }
@@ -0,0 +1,6 @@
1
+ import path from 'node:path';
2
+
3
+ export function redactPath(filePath: string, reveal = false): string {
4
+ if (reveal) return filePath;
5
+ return path.basename(filePath) === '' ? '<path>' : `<path:${path.basename(filePath)}>`;
6
+ }
@@ -0,0 +1,25 @@
1
+ import { delay } from '../../utils/delay.js';
2
+
3
+ export interface WaitForOptions {
4
+ timeoutMs?: number;
5
+ intervalMs?: number;
6
+ description?: string;
7
+ }
8
+
9
+ export async function waitFor<T>(predicate: () => Promise<T | null | undefined | false>, options: WaitForOptions = {}): Promise<T> {
10
+ const timeoutMs = options.timeoutMs ?? 15_000;
11
+ const intervalMs = options.intervalMs ?? 100;
12
+ const startedAt = Date.now();
13
+ let lastValue: T | null | undefined | false;
14
+
15
+ do {
16
+ lastValue = await predicate();
17
+ if (lastValue !== null && lastValue !== undefined && lastValue !== false) {
18
+ return lastValue;
19
+ }
20
+ await delay(intervalMs);
21
+ } while (Date.now() - startedAt < timeoutMs);
22
+
23
+ const description = options.description ?? 'condition';
24
+ throw new Error(`Timed out after ${timeoutMs}ms waiting for ${description}.`);
25
+ }
@@ -6,6 +6,11 @@ import {parseRate} from "./upwork-com.util.js";
6
6
  import {ScrapeDashboardInput} from "../example/index.js";
7
7
  import {parseUpworkSearchResults, UpworkJobListing} from "./util/scrapeJobListing.js";
8
8
  import {Page} from "puppeteer-core";
9
+ import {
10
+ fillUpworkProposalQuestions,
11
+ parseUpworkProposalQuestions,
12
+ UpworkProposalQuestions
13
+ } from "./util/parseJobApplicationDetails.js";
9
14
 
10
15
  export const upworkComActor = defineActor({
11
16
  id: 'upwork-com',
@@ -57,32 +62,60 @@ export const upworkComActor = defineActor({
57
62
  ]
58
63
  }),
59
64
  tasks: {
65
+ test: async (context, input) => {
66
+ await context.native.fileDialogs.selectFile('~/LocalWorkspace/test.png')
67
+ await context.native.mouse.moveTo({x: 100, y: 200})
68
+ },
60
69
  searchJobs: async (context, input: UpworkJobSearchFields = {}): Promise<UpworkJobListing[]> => {
61
70
  const path = '/nx/s/universal-search/jobs?category2_uid=531770282580668420,531770282580668419,531770282580668418&client_hires=1-9,10-&payment_verified=1&q=%27rancher%27%20or%20%27terraform%27%20or%20%27gitops%27%20or%20%27azure%27%20or%20%27microsoft%20azure%27%20or%20%27cloud%20architect%27%20or%20%27ai%20architect%27%20or%20%27forward%20deployed%20engineer%27%20or%20%27aws%27%20or%20%27aks%27%20or%20%27eks%27%20or%20%27gke%27%20or%20%27cloud%20engineer%27%20or%20devops%20or%20kuberentes%20or%20%27platform%20engineer%27%20or%20%27infrastructure%20engineer%27%20or%20"google%20cloud%20platform"%20or%20"GCP"%20or%20"langsmith"%20or%20"langgraph"%20or%20"gemini%20enterprise"&sort=recency&user_location_match=1';
62
71
 
63
- await context.nav.goto(path, {
64
-
65
- });
72
+ await context.nav.goto(path, {});
66
73
 
67
74
  return await parseUpworkSearchResults(context.session.page as Page);
68
75
  },
69
76
  scrapeJobs: async (context, input: ScrapeDashboardInput = {}): Promise<any> => {
70
77
  await parseUpworkSearchResults(context.session.page as Page)
71
78
  },
72
- applyToJob: async (_context, input: UpworkApplyToJobInput): Promise<UpworkApplyToJobResult> => {
73
- const coverLetter = input.coverLetter.trim();
74
- if (coverLetter.length === 0) {
75
- throw new Error('coverLetter must be a non-empty string.');
76
- }
79
+ applyToJobPart1: async (_context, input: UpworkApplyToJobInput): Promise<UpworkProposalQuestions> => {
77
80
 
78
- const rate = parseRate(input.rate);
79
- if (!Number.isFinite(rate) || rate <= 0) {
80
- throw new Error('rate must be a positive number.');
81
- }
81
+ await _context.cursor.scrollIntoView(`[data-ev-job-uid="${input.jobId}"]`);
82
+
83
+ await _context.cursor.move(`[data-ev-job-uid="${input.jobId}"]`);
84
+ await _context.cursor.click(`[data-ev-job-uid="${input.jobId}"]`);
85
+
86
+ await _context.cursor.move(`[data-test="Apply"]>button`);
87
+ // await _context.cursor.click(`[data-test="Apply"]>button`);
88
+
89
+ await _context.nav.goto(`/nx/proposals/job/~${input.jobId}/apply/`);
90
+
91
+ await _context.cursor.move('.up-fe-agency-member-selector > [data-test="dropdown-toggle"]')
92
+ await _context.cursor.click('.up-fe-agency-member-selector > [data-test="dropdown-toggle"]')
93
+
94
+ await _context.cursor.move('li[role="option"][tabindex="0"]')
95
+ await _context.cursor.click('li[role="option"][tabindex="0"]')
96
+
97
+ await _context.cursor.move('.up-fe-contractor-selector > [data-test="dropdown-toggle"]')
98
+ await _context.cursor.click('.up-fe-contractor-selector > [data-test="dropdown-toggle"]')
99
+
100
+ await _context.cursor.move('li[role="option"][tabindex="0"]')
101
+ await _context.cursor.click('li[role="option"][tabindex="0"]')
102
+
103
+ await _context.cursor.scrollIntoView('[data-test="sri-form-card"] > [data-test="dropdown-toggle"] > [aria-label="How often do you want a rate increase?"]')
104
+
105
+ await _context.cursor.move('[data-test="sri-form-card"] > [data-test="dropdown-toggle"] > [aria-label="How often do you want a rate increase?"]')
106
+ await _context.cursor.click('[data-test="sri-form-card"] > [data-test="dropdown-toggle"] > [aria-label="How often do you want a rate increase?"]')
107
+ await _context.cursor.move('li[role="option"][tabindex="0"]')
108
+ await _context.cursor.click('li[role="option"][tabindex="0"]')
109
+
110
+
111
+ return await parseUpworkProposalQuestions(_context.session.page as Page);
112
+ },
113
+ applyToJobPart2: async (_context, input: UpworkApplyToJobInput): Promise<UpworkApplyToJobResult> => {
114
+ await fillUpworkProposalQuestions(_context.session.page as Page, input.answers)
82
115
 
83
116
  return {
84
- coverLetter,
85
- rate
117
+ coverLetter: input.answers.coverLetter ?? '',
118
+ rate: parseRate(input.rate)
86
119
  };
87
120
  }
88
121
  }