screenhand 0.1.1 → 0.3.0

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 (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
package/src/types.ts DELETED
@@ -1,297 +0,0 @@
1
- // Copyright (C) 2025 Clazro Technology Private Limited
2
- // SPDX-License-Identifier: AGPL-3.0-only
3
- //
4
- // This file is part of ScreenHand.
5
- //
6
- // ScreenHand is free software: you can redistribute it and/or modify
7
- // it under the terms of the GNU Affero General Public License as
8
- // published by the Free Software Foundation, version 3.
9
- //
10
- // ScreenHand is distributed in the hope that it will be useful,
11
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- // GNU Affero General Public License for more details.
14
- //
15
- // You should have received a copy of the GNU Affero General Public License
16
- // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
-
18
- export type ToolName =
19
- | "session_start"
20
- | "navigate"
21
- | "press"
22
- | "type_into"
23
- | "wait_for"
24
- | "extract"
25
- | "screenshot"
26
- | "app_launch"
27
- | "app_focus"
28
- | "app_list"
29
- | "window_list"
30
- | "menu_click"
31
- | "key_combo"
32
- | "element_tree"
33
- | "observe_start"
34
- | "observe_stop"
35
- | "drag"
36
- | "scroll";
37
-
38
- export type Target =
39
- | { type: "selector"; value: string }
40
- | { type: "text"; value: string; exact?: boolean }
41
- | { type: "role"; role: string; name: string; exact?: boolean }
42
- | { type: "ax_path"; path: string[] }
43
- | { type: "ax_attribute"; attribute: string; value: string }
44
- | { type: "coordinates"; x: number; y: number }
45
- | { type: "image"; base64: string; confidence?: number };
46
-
47
- export type WaitCondition =
48
- | { type: "selector_visible"; selector: string }
49
- | { type: "selector_hidden"; selector: string }
50
- | { type: "url_matches"; regex: string }
51
- | { type: "text_appears"; text: string }
52
- | { type: "spinner_disappears"; selector: string }
53
- | { type: "element_exists"; target: Target }
54
- | { type: "element_gone"; target: Target }
55
- | { type: "window_title_matches"; regex: string }
56
- | { type: "app_idle"; bundleId: string; timeoutMs?: number };
57
-
58
- export type ExtractFormat = "text" | "table" | "json";
59
-
60
- export type ActionStatus = "success" | "failed";
61
-
62
- export interface ActionBudget {
63
- locateMs: number;
64
- actMs: number;
65
- verifyMs: number;
66
- maxRetries: number;
67
- }
68
-
69
- export interface SessionInfo {
70
- sessionId: string;
71
- profile: string;
72
- createdAt: string;
73
- adapterType?: "cdp" | "accessibility" | "applescript" | "vision" | "composite";
74
- }
75
-
76
- export interface PageMeta {
77
- url: string;
78
- title: string;
79
- }
80
-
81
- export interface AppContext {
82
- bundleId: string;
83
- appName: string;
84
- pid: number;
85
- windowTitle: string;
86
- windowId?: number;
87
- url?: string;
88
- }
89
-
90
- export interface LocatedElement {
91
- handleId: string;
92
- locatorUsed: string;
93
- role?: string;
94
- label?: string;
95
- coordinates?: { x: number; y: number; width: number; height: number };
96
- }
97
-
98
- export interface LocatorAttempt {
99
- strategy: string;
100
- target: string;
101
- timeoutMs: number;
102
- matched: boolean;
103
- reason?: string;
104
- }
105
-
106
- export interface RuntimeError {
107
- code:
108
- | "SESSION_NOT_FOUND"
109
- | "LOCATE_FAILED"
110
- | "ACTION_FAILED"
111
- | "VERIFY_FAILED"
112
- | "TIMEOUT"
113
- | "NOT_IMPLEMENTED"
114
- | "PERMISSION_DENIED"
115
- | "APP_NOT_FOUND"
116
- | "BRIDGE_ERROR";
117
- message: string;
118
- page?: PageMeta;
119
- appContext?: AppContext;
120
- attempts?: LocatorAttempt[];
121
- cause?: string;
122
- }
123
-
124
- export interface ActionTelemetry {
125
- action: string;
126
- sessionId: string;
127
- startedAt: string;
128
- finishedAt?: string;
129
- totalMs?: number;
130
- locateMs: number;
131
- actMs: number;
132
- verifyMs: number;
133
- retries: number;
134
- status?: ActionStatus;
135
- }
136
-
137
- export interface ToolSuccess<T> {
138
- ok: true;
139
- data: T;
140
- telemetry: ActionTelemetry;
141
- }
142
-
143
- export interface ToolFailure {
144
- ok: false;
145
- error: RuntimeError;
146
- telemetry: ActionTelemetry;
147
- }
148
-
149
- export type ToolResult<T> = ToolSuccess<T> | ToolFailure;
150
-
151
- export interface PressInput {
152
- sessionId: string;
153
- target: Target;
154
- verify?: WaitCondition;
155
- budget?: Partial<ActionBudget>;
156
- }
157
-
158
- export interface TypeIntoInput {
159
- sessionId: string;
160
- target: Target;
161
- text: string;
162
- clear?: boolean;
163
- verifyValue?: boolean;
164
- verify?: WaitCondition;
165
- budget?: Partial<ActionBudget>;
166
- }
167
-
168
- export interface NavigateInput {
169
- sessionId: string;
170
- url: string;
171
- timeoutMs?: number;
172
- }
173
-
174
- export interface WaitForInput {
175
- sessionId: string;
176
- condition: WaitCondition;
177
- timeoutMs?: number;
178
- }
179
-
180
- export interface ExtractInput {
181
- sessionId: string;
182
- target: Target;
183
- format: ExtractFormat;
184
- }
185
-
186
- export interface ScreenshotInput {
187
- sessionId: string;
188
- region?: {
189
- x: number;
190
- y: number;
191
- width: number;
192
- height: number;
193
- };
194
- }
195
-
196
- export interface AppLaunchInput {
197
- sessionId: string;
198
- bundleId: string;
199
- waitForReady?: boolean;
200
- }
201
-
202
- export interface AppFocusInput {
203
- sessionId: string;
204
- bundleId: string;
205
- }
206
-
207
- export interface MenuClickInput {
208
- sessionId: string;
209
- menuPath: string[];
210
- }
211
-
212
- export interface KeyComboInput {
213
- sessionId: string;
214
- keys: string[];
215
- }
216
-
217
- export interface ElementTreeInput {
218
- sessionId: string;
219
- maxDepth?: number;
220
- root?: Target;
221
- }
222
-
223
- export interface DragInput {
224
- sessionId: string;
225
- from: Target;
226
- to: Target;
227
- budget?: Partial<ActionBudget>;
228
- }
229
-
230
- export interface ScrollInput {
231
- sessionId: string;
232
- target?: Target;
233
- direction: "up" | "down" | "left" | "right";
234
- amount?: number;
235
- }
236
-
237
- export interface ObserveStartInput {
238
- sessionId: string;
239
- events?: UIEventType[];
240
- }
241
-
242
- export interface ObserveStopInput {
243
- sessionId: string;
244
- }
245
-
246
- export type UIEventType =
247
- | "value_changed"
248
- | "focus_changed"
249
- | "window_created"
250
- | "window_closed"
251
- | "dialog_appeared"
252
- | "menu_opened"
253
- | "title_changed"
254
- | "layout_changed"
255
- | "app_activated"
256
- | "app_deactivated";
257
-
258
- export interface UIEvent {
259
- type: UIEventType;
260
- timestamp: string;
261
- pid: number;
262
- bundleId?: string;
263
- elementRole?: string;
264
- elementLabel?: string;
265
- oldValue?: string;
266
- newValue?: string;
267
- windowTitle?: string;
268
- }
269
-
270
- export interface RunningApp {
271
- bundleId: string;
272
- name: string;
273
- pid: number;
274
- isActive: boolean;
275
- }
276
-
277
- export interface WindowInfo {
278
- windowId: number;
279
- title: string;
280
- bundleId: string;
281
- pid: number;
282
- bounds: { x: number; y: number; width: number; height: number };
283
- isOnScreen: boolean;
284
- }
285
-
286
- export interface AXNode {
287
- role: string;
288
- title?: string;
289
- value?: string;
290
- description?: string;
291
- identifier?: string;
292
- enabled?: boolean;
293
- focused?: boolean;
294
- position?: { x: number; y: number };
295
- size?: { width: number; height: number };
296
- children?: AXNode[];
297
- }
@@ -1,176 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { BridgeClient } from "../src/native/bridge-client.js";
3
- import { EventEmitter } from "node:events";
4
- import { ChildProcess } from "node:child_process";
5
- import path from "node:path";
6
-
7
- // ── Platform detection ──
8
-
9
- describe("BridgeClient platform detection", () => {
10
- it("selects macOS binary path when platform is darwin", () => {
11
- const originalPlatform = process.platform;
12
- Object.defineProperty(process, "platform", { value: "darwin", writable: true });
13
-
14
- const client = new BridgeClient();
15
- // Access the private binaryPath via any cast
16
- const binaryPath = (client as any).binaryPath as string;
17
- expect(binaryPath).toContain("macos-bridge");
18
- expect(binaryPath).not.toContain("windows-bridge");
19
-
20
- Object.defineProperty(process, "platform", { value: originalPlatform, writable: true });
21
- });
22
-
23
- it("selects Windows binary path when platform is win32", () => {
24
- const originalPlatform = process.platform;
25
- Object.defineProperty(process, "platform", { value: "win32", writable: true });
26
-
27
- const client = new BridgeClient();
28
- const binaryPath = (client as any).binaryPath as string;
29
- expect(binaryPath).toContain("windows-bridge");
30
- expect(binaryPath).toContain(".exe");
31
-
32
- Object.defineProperty(process, "platform", { value: originalPlatform, writable: true });
33
- });
34
-
35
- it("accepts a custom binary path", () => {
36
- const customPath = "/custom/path/to/bridge";
37
- const client = new BridgeClient(customPath);
38
- expect((client as any).binaryPath).toBe(customPath);
39
- });
40
- });
41
-
42
- // ── JSON-RPC line parsing ──
43
-
44
- describe("BridgeClient JSON-RPC parsing", () => {
45
- let client: BridgeClient;
46
-
47
- beforeEach(() => {
48
- client = new BridgeClient("/fake/path");
49
- });
50
-
51
- afterEach(async () => {
52
- await client.stop();
53
- });
54
-
55
- it("resolves pending request on valid response", async () => {
56
- // Manually set up a pending request and feed it a line
57
- const pending = (client as any).pending as Map<number, any>;
58
-
59
- const resultPromise = new Promise((resolve, reject) => {
60
- pending.set(1, {
61
- resolve,
62
- reject,
63
- timer: setTimeout(() => reject(new Error("timeout")), 5000),
64
- });
65
- });
66
-
67
- // Simulate receiving a response line
68
- (client as any).handleLine(JSON.stringify({ id: 1, result: { pong: true } }));
69
-
70
- const result = await resultPromise;
71
- expect(result).toEqual({ pong: true });
72
- expect(pending.size).toBe(0);
73
- });
74
-
75
- it("rejects pending request on error response", async () => {
76
- const pending = (client as any).pending as Map<number, any>;
77
-
78
- const resultPromise = new Promise((resolve, reject) => {
79
- pending.set(2, {
80
- resolve,
81
- reject,
82
- timer: setTimeout(() => reject(new Error("timeout")), 5000),
83
- });
84
- });
85
-
86
- (client as any).handleLine(JSON.stringify({
87
- id: 2,
88
- result: null,
89
- error: { code: -1, message: "Not found: element" },
90
- }));
91
-
92
- await expect(resultPromise).rejects.toThrow("Not found: element");
93
- expect(pending.size).toBe(0);
94
- });
95
-
96
- it("emits ax-event for event messages", async () => {
97
- const eventPromise = new Promise<Record<string, unknown>>((resolve) => {
98
- client.on("ax-event", resolve);
99
- });
100
-
101
- (client as any).handleLine(JSON.stringify({
102
- id: 0,
103
- event: { type: "value_changed", pid: 123 },
104
- }));
105
-
106
- const event = await eventPromise;
107
- expect(event).toEqual({ type: "value_changed", pid: 123 });
108
- });
109
-
110
- it("ignores malformed JSON lines", () => {
111
- // Should not throw
112
- (client as any).handleLine("not json at all");
113
- (client as any).handleLine("{incomplete");
114
- (client as any).handleLine("");
115
- });
116
-
117
- it("ignores responses for unknown request IDs", () => {
118
- // Should not throw
119
- (client as any).handleLine(JSON.stringify({ id: 999, result: "orphan" }));
120
- });
121
- });
122
-
123
- // ── Lifecycle ──
124
-
125
- describe("BridgeClient lifecycle", () => {
126
- it("rejects all pending requests on stop", async () => {
127
- const client = new BridgeClient("/fake/path");
128
- const pending = (client as any).pending as Map<number, any>;
129
-
130
- const p1 = new Promise((resolve, reject) => {
131
- pending.set(10, {
132
- resolve,
133
- reject,
134
- timer: setTimeout(() => reject(new Error("timeout")), 5000),
135
- });
136
- });
137
-
138
- const p2 = new Promise((resolve, reject) => {
139
- pending.set(11, {
140
- resolve,
141
- reject,
142
- timer: setTimeout(() => reject(new Error("timeout")), 5000),
143
- });
144
- });
145
-
146
- await client.stop();
147
-
148
- await expect(p1).rejects.toThrow("Bridge stopped");
149
- await expect(p2).rejects.toThrow("Bridge stopped");
150
- expect(pending.size).toBe(0);
151
- });
152
-
153
- it("start is idempotent after first call", async () => {
154
- const client = new BridgeClient("/fake/path");
155
- (client as any).started = true;
156
-
157
- // Second start() should return immediately without spawning
158
- await client.start(); // should not throw or spawn
159
- expect((client as any).process).toBeNull();
160
- });
161
- });
162
-
163
- // ── MacOSBridgeClient backward compat ──
164
-
165
- describe("MacOSBridgeClient backward compatibility", () => {
166
- it("MacOSBridgeClient is the same as BridgeClient", async () => {
167
- const { MacOSBridgeClient } = await import("../src/native/bridge-client.js");
168
- expect(MacOSBridgeClient).toBe(BridgeClient);
169
- });
170
-
171
- it("re-export from macos-bridge-client.ts works", async () => {
172
- const mod = await import("../src/native/macos-bridge-client.js");
173
- expect(mod.MacOSBridgeClient).toBe(BridgeClient);
174
- expect(mod.BridgeClient).toBe(BridgeClient);
175
- });
176
- });
@@ -1,210 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- /**
4
- * Tests for browser stealth / anti-detection features.
5
- * These test the stealth script validity, CDP Input event sequences,
6
- * and human-like timing without requiring a real Chrome instance.
7
- */
8
-
9
- // ── Stealth script validation ──
10
-
11
- // Extract the stealth script source inline (mirrors mcp-desktop.ts STEALTH_SCRIPT)
12
- const STEALTH_SCRIPT = `
13
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
14
- for (const key of Object.keys(window)) {
15
- if (key.match(/^cdc_/)) delete (window)[key];
16
- }
17
- Object.defineProperty(navigator, 'plugins', {
18
- get: () => [
19
- { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
20
- { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
21
- { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
22
- ],
23
- });
24
- Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
25
- `;
26
-
27
- describe("browser_stealth", () => {
28
- it("stealth patches are valid JavaScript (no syntax errors)", () => {
29
- // If this throws, the script has syntax errors
30
- expect(() => new Function(STEALTH_SCRIPT)).not.toThrow();
31
- });
32
-
33
- it("stealth script hides webdriver property", () => {
34
- // Simulate in a minimal environment
35
- const nav: any = { webdriver: true };
36
- const fn = new Function("navigator", `
37
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
38
- `);
39
- fn(nav);
40
- expect(nav.webdriver).toBeUndefined();
41
- });
42
-
43
- it("stealth script sets realistic plugins", () => {
44
- const nav: any = { plugins: [] };
45
- const fn = new Function("navigator", `
46
- Object.defineProperty(navigator, 'plugins', {
47
- get: () => [
48
- { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
49
- { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
50
- { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
51
- ],
52
- });
53
- `);
54
- fn(nav);
55
- expect(nav.plugins).toHaveLength(3);
56
- expect(nav.plugins[0].name).toBe("Chrome PDF Plugin");
57
- });
58
-
59
- it("stealth script sets realistic languages", () => {
60
- const nav: any = { languages: [] };
61
- const fn = new Function("navigator", `
62
- Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
63
- `);
64
- fn(nav);
65
- expect(nav.languages).toEqual(["en-US", "en"]);
66
- });
67
- });
68
-
69
- // ── Mock CDP client for Input domain tests ──
70
-
71
- function createMockCDPClient() {
72
- const calls: { method: string; params: any }[] = [];
73
- return {
74
- calls,
75
- Runtime: {
76
- enable: vi.fn(),
77
- evaluate: vi.fn().mockResolvedValue({
78
- result: { value: { ok: true, x: 100, y: 200, text: "Submit" } },
79
- }),
80
- },
81
- Input: {
82
- dispatchKeyEvent: vi.fn().mockImplementation(async (params: any) => {
83
- calls.push({ method: "Input.dispatchKeyEvent", params });
84
- }),
85
- dispatchMouseEvent: vi.fn().mockImplementation(async (params: any) => {
86
- calls.push({ method: "Input.dispatchMouseEvent", params });
87
- }),
88
- },
89
- Page: {
90
- enable: vi.fn(),
91
- addScriptToEvaluateOnNewDocument: vi.fn(),
92
- },
93
- close: vi.fn(),
94
- };
95
- }
96
-
97
- describe("browser_fill_form (mock CDP)", () => {
98
- it("types character by character using Input.dispatchKeyEvent", async () => {
99
- const client = createMockCDPClient();
100
- const text = "hello";
101
-
102
- // Simulate what browser_fill_form does internally
103
- // Clear: select all + backspace
104
- await client.Input.dispatchKeyEvent({ type: "keyDown", key: "a", code: "KeyA", modifiers: 4 });
105
- await client.Input.dispatchKeyEvent({ type: "keyUp", key: "a", code: "KeyA", modifiers: 4 });
106
- await client.Input.dispatchKeyEvent({ type: "keyDown", key: "Backspace", code: "Backspace" });
107
- await client.Input.dispatchKeyEvent({ type: "keyUp", key: "Backspace", code: "Backspace" });
108
-
109
- // Type each char
110
- for (const char of text) {
111
- await client.Input.dispatchKeyEvent({ type: "keyDown", text: char, key: char, unmodifiedText: char });
112
- await client.Input.dispatchKeyEvent({ type: "keyUp", text: char, key: char, unmodifiedText: char });
113
- }
114
-
115
- // 4 clear events + 2 per char = 4 + 10 = 14
116
- const keyEvents = client.calls.filter(c => c.method === "Input.dispatchKeyEvent");
117
- expect(keyEvents).toHaveLength(14);
118
-
119
- // Verify keyDown/keyUp pairs for typed chars
120
- const typedEvents = keyEvents.slice(4); // skip clear events
121
- for (let i = 0; i < text.length; i++) {
122
- const down = typedEvents[i * 2];
123
- const up = typedEvents[i * 2 + 1];
124
- expect(down.params.type).toBe("keyDown");
125
- expect(down.params.text).toBe(text[i]);
126
- expect(up.params.type).toBe("keyUp");
127
- expect(up.params.text).toBe(text[i]);
128
- }
129
- });
130
-
131
- it("does not use el.value assignment (no Runtime.evaluate with el.value =)", () => {
132
- // The implementation should use Input.dispatchKeyEvent, NOT el.value = text
133
- // This test verifies the approach by checking that the mock client
134
- // receives dispatchKeyEvent calls instead of evaluate calls with el.value
135
- const client = createMockCDPClient();
136
-
137
- // The evaluate call should only be for focusing, not for setting value
138
- // In the real implementation, evaluate is called once for focus only
139
- expect(client.Runtime.evaluate).not.toHaveBeenCalledWith(
140
- expect.objectContaining({
141
- expression: expect.stringContaining("el.value ="),
142
- })
143
- );
144
- });
145
- });
146
-
147
- describe("browser_human_click (mock CDP)", () => {
148
- it("dispatches mouseMoved → mousePressed → mouseReleased sequence", async () => {
149
- const client = createMockCDPClient();
150
- const x = 100;
151
- const y = 200;
152
-
153
- await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
154
- await client.Input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
155
- await client.Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
156
-
157
- const mouseEvents = client.calls.filter(c => c.method === "Input.dispatchMouseEvent");
158
- expect(mouseEvents).toHaveLength(3);
159
- expect(mouseEvents[0].params.type).toBe("mouseMoved");
160
- expect(mouseEvents[1].params.type).toBe("mousePressed");
161
- expect(mouseEvents[1].params.button).toBe("left");
162
- expect(mouseEvents[2].params.type).toBe("mouseReleased");
163
- expect(mouseEvents[2].params.button).toBe("left");
164
- });
165
-
166
- it("uses correct coordinates from element bounding rect", async () => {
167
- const client = createMockCDPClient();
168
- // Simulate element at (50, 100) with size 200x40
169
- const rect = { x: 50, y: 100, width: 200, height: 40 };
170
- const centerX = rect.x + rect.width / 2; // 150
171
- const centerY = rect.y + rect.height / 2; // 120
172
-
173
- await client.Input.dispatchMouseEvent({ type: "mouseMoved", x: centerX, y: centerY });
174
- await client.Input.dispatchMouseEvent({ type: "mousePressed", x: centerX, y: centerY, button: "left", clickCount: 1 });
175
- await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: centerX, y: centerY, button: "left", clickCount: 1 });
176
-
177
- const mouseEvents = client.calls.filter(c => c.method === "Input.dispatchMouseEvent");
178
- for (const evt of mouseEvents) {
179
- expect(evt.params.x).toBe(150);
180
- expect(evt.params.y).toBe(120);
181
- }
182
- });
183
- });
184
-
185
- describe("random delay timing", () => {
186
- it("randomDelay produces values within expected range", async () => {
187
- // Test the delay logic directly
188
- const min = 30;
189
- const max = 80;
190
- const samples: number[] = [];
191
-
192
- for (let i = 0; i < 50; i++) {
193
- const start = Date.now();
194
- await new Promise(r => setTimeout(r, min + Math.random() * (max - min)));
195
- const elapsed = Date.now() - start;
196
- samples.push(elapsed);
197
- }
198
-
199
- // All delays should be roughly in the expected range
200
- // Allow some tolerance for timer imprecision
201
- for (const s of samples) {
202
- expect(s).toBeGreaterThanOrEqual(min - 5);
203
- expect(s).toBeLessThan(max + 50); // generous upper bound for CI
204
- }
205
-
206
- // Should not all be the same (randomness check)
207
- const unique = new Set(samples);
208
- expect(unique.size).toBeGreaterThan(1);
209
- });
210
- });