screenhand 0.1.1 → 0.2.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.
- package/README.md +458 -93
- package/dist/.audit-log.jsonl +55 -0
- package/dist/.screenhand/memory/.lock +1 -0
- package/dist/.screenhand/memory/actions.jsonl +85 -0
- package/dist/.screenhand/memory/errors.jsonl +5 -0
- package/dist/.screenhand/memory/errors.jsonl.bak +4 -0
- package/dist/.screenhand/memory/state.json +35 -0
- package/dist/.screenhand/memory/state.json.bak +35 -0
- package/dist/.screenhand/memory/strategies.jsonl +12 -0
- package/dist/agent/cli.js +73 -0
- package/dist/agent/loop.js +258 -0
- package/dist/config.js +9 -0
- package/dist/index.js +56 -0
- package/dist/logging/timeline-logger.js +29 -0
- package/dist/mcp/mcp-stdio-server.js +448 -0
- package/dist/mcp/server.js +347 -0
- package/dist/mcp-desktop.js +2731 -0
- package/dist/mcp-entry.js +59 -0
- package/dist/memory/recall.js +160 -0
- package/dist/memory/research.js +98 -0
- package/dist/memory/seeds.js +89 -0
- package/dist/memory/session.js +161 -0
- package/dist/memory/store.js +391 -0
- package/dist/memory/types.js +4 -0
- package/dist/monitor/codex-monitor.js +377 -0
- package/dist/monitor/task-queue.js +84 -0
- package/dist/monitor/types.js +49 -0
- package/dist/native/bridge-client.js +174 -0
- package/dist/native/macos-bridge-client.js +5 -0
- package/dist/npm-publish-helper.js +117 -0
- package/dist/npm-token-cdp.js +113 -0
- package/dist/npm-token-create.js +135 -0
- package/dist/npm-token-finish.js +126 -0
- package/dist/playbook/engine.js +193 -0
- package/dist/playbook/index.js +4 -0
- package/dist/playbook/recorder.js +519 -0
- package/dist/playbook/runner.js +392 -0
- package/dist/playbook/store.js +166 -0
- package/dist/playbook/types.js +4 -0
- package/dist/runtime/accessibility-adapter.js +377 -0
- package/dist/runtime/app-adapter.js +48 -0
- package/dist/runtime/applescript-adapter.js +283 -0
- package/dist/runtime/ax-role-map.js +80 -0
- package/dist/runtime/browser-adapter.js +36 -0
- package/dist/runtime/cdp-chrome-adapter.js +505 -0
- package/dist/runtime/composite-adapter.js +205 -0
- package/dist/runtime/executor.js +250 -0
- package/dist/runtime/locator-cache.js +12 -0
- package/dist/runtime/planning-loop.js +47 -0
- package/dist/runtime/service.js +372 -0
- package/dist/runtime/session-manager.js +28 -0
- package/dist/runtime/state-observer.js +105 -0
- package/dist/runtime/vision-adapter.js +208 -0
- package/dist/scripts/codex-monitor-daemon.js +335 -0
- package/dist/scripts/supervisor-daemon.js +272 -0
- package/dist/scripts/worker-daemon.js +228 -0
- package/dist/src/agent/cli.js +82 -0
- package/dist/src/agent/loop.js +274 -0
- package/{src/config.ts → dist/src/config.js} +5 -10
- package/{src/index.ts → dist/src/index.js} +32 -52
- package/dist/src/jobs/manager.js +237 -0
- package/dist/src/jobs/runner.js +683 -0
- package/dist/src/jobs/store.js +102 -0
- package/dist/src/jobs/types.js +30 -0
- package/dist/src/jobs/worker.js +97 -0
- package/dist/src/logging/timeline-logger.js +45 -0
- package/dist/src/mcp/mcp-stdio-server.js +464 -0
- package/dist/src/mcp/server.js +363 -0
- package/dist/src/mcp-entry.js +60 -0
- package/dist/src/memory/recall.js +170 -0
- package/dist/src/memory/research.js +104 -0
- package/dist/src/memory/seeds.js +101 -0
- package/dist/src/memory/service.js +421 -0
- package/dist/src/memory/session.js +169 -0
- package/dist/src/memory/store.js +422 -0
- package/dist/src/memory/types.js +17 -0
- package/dist/src/monitor/codex-monitor.js +382 -0
- package/dist/src/monitor/task-queue.js +97 -0
- package/dist/src/monitor/types.js +62 -0
- package/dist/src/native/bridge-client.js +190 -0
- package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
- package/dist/src/playbook/engine.js +201 -0
- package/dist/src/playbook/index.js +20 -0
- package/dist/src/playbook/recorder.js +535 -0
- package/dist/src/playbook/runner.js +408 -0
- package/dist/src/playbook/store.js +183 -0
- package/dist/src/playbook/types.js +17 -0
- package/dist/src/runtime/accessibility-adapter.js +393 -0
- package/dist/src/runtime/app-adapter.js +64 -0
- package/dist/src/runtime/applescript-adapter.js +299 -0
- package/dist/src/runtime/ax-role-map.js +96 -0
- package/dist/src/runtime/browser-adapter.js +52 -0
- package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
- package/dist/src/runtime/composite-adapter.js +221 -0
- package/dist/src/runtime/execution-contract.js +159 -0
- package/dist/src/runtime/executor.js +266 -0
- package/{src/runtime/locator-cache.ts → dist/src/runtime/locator-cache.js} +10 -15
- package/dist/src/runtime/planning-loop.js +63 -0
- package/dist/src/runtime/service.js +388 -0
- package/dist/src/runtime/session-manager.js +60 -0
- package/dist/src/runtime/state-observer.js +121 -0
- package/dist/src/runtime/vision-adapter.js +224 -0
- package/dist/src/supervisor/locks.js +186 -0
- package/dist/src/supervisor/supervisor.js +403 -0
- package/dist/src/supervisor/types.js +30 -0
- package/dist/src/test-mcp-protocol.js +154 -0
- package/dist/src/types.js +17 -0
- package/dist/src/util/atomic-write.js +118 -0
- package/dist/test-mcp-protocol.js +138 -0
- package/dist/types.js +1 -0
- package/package.json +18 -4
- package/.claude/commands/automate.md +0 -28
- package/.claude/commands/debug-ui.md +0 -19
- package/.claude/commands/screenshot.md +0 -15
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.mcp.json +0 -8
- package/DESKTOP_MCP_GUIDE.md +0 -92
- package/SECURITY.md +0 -44
- package/docs/architecture.md +0 -47
- package/install-skills.sh +0 -19
- package/mcp-bridge.ts +0 -271
- package/mcp-desktop.ts +0 -1221
- package/native/macos-bridge/Package.swift +0 -21
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +0 -261
- package/native/macos-bridge/Sources/AppManagement.swift +0 -129
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +0 -242
- package/native/macos-bridge/Sources/ObserverBridge.swift +0 -120
- package/native/macos-bridge/Sources/VisionBridge.swift +0 -80
- package/native/macos-bridge/Sources/main.swift +0 -345
- package/native/windows-bridge/AppManagement.cs +0 -234
- package/native/windows-bridge/InputBridge.cs +0 -436
- package/native/windows-bridge/Program.cs +0 -265
- package/native/windows-bridge/ScreenCapture.cs +0 -329
- package/native/windows-bridge/UIAutomationBridge.cs +0 -571
- package/native/windows-bridge/WindowsBridge.csproj +0 -17
- package/playbooks/devpost.json +0 -186
- package/playbooks/instagram.json +0 -41
- package/playbooks/instagram_v2.json +0 -201
- package/playbooks/x_v1.json +0 -211
- package/scripts/devpost-live-loop.mjs +0 -421
- package/src/logging/timeline-logger.ts +0 -55
- package/src/mcp/server.ts +0 -449
- package/src/memory/recall.ts +0 -191
- package/src/memory/research.ts +0 -146
- package/src/memory/seeds.ts +0 -123
- package/src/memory/session.ts +0 -201
- package/src/memory/store.ts +0 -434
- package/src/memory/types.ts +0 -69
- package/src/native/bridge-client.ts +0 -239
- package/src/runtime/accessibility-adapter.ts +0 -487
- package/src/runtime/app-adapter.ts +0 -169
- package/src/runtime/applescript-adapter.ts +0 -376
- package/src/runtime/ax-role-map.ts +0 -102
- package/src/runtime/browser-adapter.ts +0 -129
- package/src/runtime/cdp-chrome-adapter.ts +0 -676
- package/src/runtime/composite-adapter.ts +0 -274
- package/src/runtime/executor.ts +0 -396
- package/src/runtime/planning-loop.ts +0 -81
- package/src/runtime/service.ts +0 -448
- package/src/runtime/session-manager.ts +0 -50
- package/src/runtime/state-observer.ts +0 -136
- package/src/runtime/vision-adapter.ts +0 -297
- package/src/types.ts +0 -297
- package/tests/bridge-client.test.ts +0 -176
- package/tests/browser-stealth.test.ts +0 -210
- package/tests/composite-adapter.test.ts +0 -64
- package/tests/mcp-server.test.ts +0 -151
- package/tests/memory-recall.test.ts +0 -339
- package/tests/memory-research.test.ts +0 -159
- package/tests/memory-seeds.test.ts +0 -120
- package/tests/memory-store.test.ts +0 -392
- package/tests/types.test.ts +0 -92
- package/tsconfig.check.json +0 -17
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -8
|
@@ -1,676 +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
|
-
import { randomUUID } from "node:crypto";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
20
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import { getChromePath, launch } from "chrome-launcher";
|
|
23
|
-
import type { LaunchedChrome } from "chrome-launcher";
|
|
24
|
-
import CDP from "chrome-remote-interface";
|
|
25
|
-
import type {
|
|
26
|
-
AppContext,
|
|
27
|
-
ExtractFormat,
|
|
28
|
-
LocatedElement,
|
|
29
|
-
PageMeta,
|
|
30
|
-
SessionInfo,
|
|
31
|
-
Target,
|
|
32
|
-
WaitCondition,
|
|
33
|
-
} from "../types.js";
|
|
34
|
-
import type { AppAdapter } from "./app-adapter.js";
|
|
35
|
-
|
|
36
|
-
const HANDLE_ATTR = "data-automator-handle";
|
|
37
|
-
const POLL_INTERVAL_MS = 100;
|
|
38
|
-
|
|
39
|
-
type CdpClient = Awaited<ReturnType<typeof CDP>>;
|
|
40
|
-
|
|
41
|
-
interface SessionState {
|
|
42
|
-
info: SessionInfo;
|
|
43
|
-
profileDir: string;
|
|
44
|
-
chrome: LaunchedChrome;
|
|
45
|
-
client: CdpClient;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface CdpChromeAdapterOptions {
|
|
49
|
-
profileRootDir?: string;
|
|
50
|
-
screenshotDir?: string;
|
|
51
|
-
chromePath?: string;
|
|
52
|
-
headless?: boolean;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export class CdpChromeAdapter implements AppAdapter {
|
|
56
|
-
private readonly sessions = new Map<string, SessionState>();
|
|
57
|
-
private readonly sessionsByProfile = new Map<string, SessionState>();
|
|
58
|
-
|
|
59
|
-
constructor(private readonly options: CdpChromeAdapterOptions = {}) {}
|
|
60
|
-
|
|
61
|
-
async attach(profile: string): Promise<SessionInfo> {
|
|
62
|
-
const existing = this.sessionsByProfile.get(profile);
|
|
63
|
-
if (existing) {
|
|
64
|
-
return existing.info;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const profileDir = path.resolve(
|
|
68
|
-
this.options.profileRootDir ?? path.join(process.cwd(), ".profiles"),
|
|
69
|
-
profile,
|
|
70
|
-
);
|
|
71
|
-
await mkdir(profileDir, { recursive: true });
|
|
72
|
-
|
|
73
|
-
const chromePath = this.options.chromePath ?? resolveChromePath();
|
|
74
|
-
const chrome = await launch({
|
|
75
|
-
chromePath,
|
|
76
|
-
startingUrl: "about:blank",
|
|
77
|
-
userDataDir: profileDir,
|
|
78
|
-
chromeFlags: this.buildChromeFlags(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const targetId = await this.resolveTargetId(chrome.port);
|
|
82
|
-
const client = await CDP({ port: chrome.port, target: targetId });
|
|
83
|
-
await Promise.all([client.Page.enable(), client.Runtime.enable()]);
|
|
84
|
-
|
|
85
|
-
const info: SessionInfo = {
|
|
86
|
-
sessionId: `session_${profile}_${Date.now()}`,
|
|
87
|
-
profile,
|
|
88
|
-
createdAt: new Date().toISOString(),
|
|
89
|
-
adapterType: "cdp",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const state: SessionState = { info, profileDir, chrome, client };
|
|
93
|
-
this.sessions.set(info.sessionId, state);
|
|
94
|
-
this.sessionsByProfile.set(profile, state);
|
|
95
|
-
return info;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Backward-compatible alias. */
|
|
99
|
-
async connect(profile: string): Promise<SessionInfo> {
|
|
100
|
-
return this.attach(profile);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async getAppContext(sessionId: string): Promise<AppContext> {
|
|
104
|
-
const page = await this.getPageMeta(sessionId);
|
|
105
|
-
return {
|
|
106
|
-
bundleId: "com.google.Chrome",
|
|
107
|
-
appName: "Google Chrome",
|
|
108
|
-
pid: this.requireSession(sessionId).chrome.pid,
|
|
109
|
-
windowTitle: page.title,
|
|
110
|
-
url: page.url,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async getPageMeta(sessionId: string): Promise<PageMeta> {
|
|
115
|
-
const state = this.requireSession(sessionId);
|
|
116
|
-
const page = await this.evaluateJson<PageMeta>(
|
|
117
|
-
state,
|
|
118
|
-
"(() => ({ url: String(window.location.href), title: String(document.title || '') }))()",
|
|
119
|
-
);
|
|
120
|
-
return page;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async navigate(
|
|
124
|
-
sessionId: string,
|
|
125
|
-
url: string,
|
|
126
|
-
timeoutMs: number,
|
|
127
|
-
): Promise<PageMeta> {
|
|
128
|
-
const state = this.requireSession(sessionId);
|
|
129
|
-
await state.client.Page.navigate({ url });
|
|
130
|
-
|
|
131
|
-
const ready = await this.waitUntil(timeoutMs, async () => {
|
|
132
|
-
const readyState = await this.evaluateJson<string>(
|
|
133
|
-
state,
|
|
134
|
-
"(() => String(document.readyState))()",
|
|
135
|
-
);
|
|
136
|
-
return readyState === "interactive" || readyState === "complete";
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (!ready) {
|
|
140
|
-
throw new Error(`Navigation timeout after ${timeoutMs}ms for ${url}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return this.getPageMeta(sessionId);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async locate(
|
|
147
|
-
sessionId: string,
|
|
148
|
-
target: Target,
|
|
149
|
-
timeoutMs: number,
|
|
150
|
-
): Promise<LocatedElement | null> {
|
|
151
|
-
const state = this.requireSession(sessionId);
|
|
152
|
-
const deadline = Date.now() + timeoutMs;
|
|
153
|
-
while (Date.now() < deadline) {
|
|
154
|
-
const result = await this.evaluateJson<LocatedElement | null>(
|
|
155
|
-
state,
|
|
156
|
-
buildLocateExpression(target),
|
|
157
|
-
);
|
|
158
|
-
if (result) {
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
await sleep(POLL_INTERVAL_MS);
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async click(sessionId: string, element: LocatedElement): Promise<void> {
|
|
167
|
-
const state = this.requireSession(sessionId);
|
|
168
|
-
const response = await this.evaluateJson<{ ok: boolean; reason?: string }>(
|
|
169
|
-
state,
|
|
170
|
-
buildClickExpression(element.handleId),
|
|
171
|
-
);
|
|
172
|
-
if (!response.ok) {
|
|
173
|
-
throw new Error(response.reason ?? "Click failed");
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async setValue(
|
|
178
|
-
sessionId: string,
|
|
179
|
-
element: LocatedElement,
|
|
180
|
-
text: string,
|
|
181
|
-
clear: boolean,
|
|
182
|
-
): Promise<void> {
|
|
183
|
-
const state = this.requireSession(sessionId);
|
|
184
|
-
const response = await this.evaluateJson<{ ok: boolean; reason?: string }>(
|
|
185
|
-
state,
|
|
186
|
-
buildSetValueExpression(element.handleId, text, clear),
|
|
187
|
-
);
|
|
188
|
-
if (!response.ok) {
|
|
189
|
-
throw new Error(response.reason ?? "setValue failed");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async getValue(sessionId: string, element: LocatedElement): Promise<string> {
|
|
194
|
-
const state = this.requireSession(sessionId);
|
|
195
|
-
return this.evaluateJson<string>(state, buildGetValueExpression(element.handleId));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async waitFor(
|
|
199
|
-
sessionId: string,
|
|
200
|
-
condition: WaitCondition,
|
|
201
|
-
timeoutMs: number,
|
|
202
|
-
): Promise<boolean> {
|
|
203
|
-
const state = this.requireSession(sessionId);
|
|
204
|
-
return this.waitUntil(timeoutMs, async () =>
|
|
205
|
-
this.checkCondition(state, condition),
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async extract(
|
|
210
|
-
sessionId: string,
|
|
211
|
-
target: Target,
|
|
212
|
-
format: ExtractFormat,
|
|
213
|
-
): Promise<unknown> {
|
|
214
|
-
const state = this.requireSession(sessionId);
|
|
215
|
-
const element = await this.locate(sessionId, target, 1500);
|
|
216
|
-
if (!element) {
|
|
217
|
-
throw new Error("Extract target not found");
|
|
218
|
-
}
|
|
219
|
-
return this.evaluateJson<unknown>(
|
|
220
|
-
state,
|
|
221
|
-
buildExtractExpression(element.handleId, format),
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async screenshot(
|
|
226
|
-
sessionId: string,
|
|
227
|
-
region?: { x: number; y: number; width: number; height: number },
|
|
228
|
-
): Promise<string> {
|
|
229
|
-
const state = this.requireSession(sessionId);
|
|
230
|
-
const screenshotDir = path.resolve(
|
|
231
|
-
this.options.screenshotDir ?? path.join(process.cwd(), ".artifacts", "screenshots"),
|
|
232
|
-
);
|
|
233
|
-
await mkdir(screenshotDir, { recursive: true });
|
|
234
|
-
|
|
235
|
-
const captureParams: {
|
|
236
|
-
format: "png";
|
|
237
|
-
captureBeyondViewport: boolean;
|
|
238
|
-
clip?: { x: number; y: number; width: number; height: number; scale: number };
|
|
239
|
-
} = {
|
|
240
|
-
format: "png",
|
|
241
|
-
captureBeyondViewport: true,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
if (region) {
|
|
245
|
-
captureParams.clip = {
|
|
246
|
-
x: region.x,
|
|
247
|
-
y: region.y,
|
|
248
|
-
width: region.width,
|
|
249
|
-
height: region.height,
|
|
250
|
-
scale: 1,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const shot = await state.client.Page.captureScreenshot(captureParams);
|
|
255
|
-
if (!shot.data) {
|
|
256
|
-
throw new Error("Screenshot capture returned empty data");
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const filePath = path.join(
|
|
260
|
-
screenshotDir,
|
|
261
|
-
`shot_${new Date().toISOString().replaceAll(":", "-")}_${randomUUID()}.png`,
|
|
262
|
-
);
|
|
263
|
-
await writeFile(filePath, Buffer.from(shot.data, "base64"));
|
|
264
|
-
return filePath;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private async checkCondition(
|
|
268
|
-
state: SessionState,
|
|
269
|
-
condition: WaitCondition,
|
|
270
|
-
): Promise<boolean> {
|
|
271
|
-
switch (condition.type) {
|
|
272
|
-
case "selector_visible":
|
|
273
|
-
return this.evaluateJson<boolean>(
|
|
274
|
-
state,
|
|
275
|
-
buildSelectorVisibilityExpression(condition.selector, true),
|
|
276
|
-
);
|
|
277
|
-
case "selector_hidden":
|
|
278
|
-
case "spinner_disappears":
|
|
279
|
-
return this.evaluateJson<boolean>(
|
|
280
|
-
state,
|
|
281
|
-
buildSelectorVisibilityExpression(condition.selector, false),
|
|
282
|
-
);
|
|
283
|
-
case "text_appears":
|
|
284
|
-
return this.evaluateJson<boolean>(
|
|
285
|
-
state,
|
|
286
|
-
buildTextAppearsExpression(condition.text),
|
|
287
|
-
);
|
|
288
|
-
case "url_matches": {
|
|
289
|
-
const page = await this.getPageMeta(state.info.sessionId);
|
|
290
|
-
let regex: RegExp;
|
|
291
|
-
try {
|
|
292
|
-
regex = new RegExp(condition.regex);
|
|
293
|
-
} catch (error) {
|
|
294
|
-
throw new Error(
|
|
295
|
-
`Invalid regex "${condition.regex}": ${
|
|
296
|
-
error instanceof Error ? error.message : "unknown error"
|
|
297
|
-
}`,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
return regex.test(page.url);
|
|
301
|
-
}
|
|
302
|
-
case "element_exists":
|
|
303
|
-
case "element_gone":
|
|
304
|
-
case "window_title_matches":
|
|
305
|
-
case "app_idle":
|
|
306
|
-
// Desktop-specific conditions not supported by CDP adapter
|
|
307
|
-
throw new Error(`Condition type "${condition.type}" not supported by CDP adapter`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private async waitUntil(
|
|
312
|
-
timeoutMs: number,
|
|
313
|
-
predicate: () => Promise<boolean>,
|
|
314
|
-
): Promise<boolean> {
|
|
315
|
-
const deadline = Date.now() + timeoutMs;
|
|
316
|
-
while (Date.now() < deadline) {
|
|
317
|
-
if (await predicate()) {
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
await sleep(POLL_INTERVAL_MS);
|
|
321
|
-
}
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private async evaluateJson<T>(state: SessionState, expression: string): Promise<T> {
|
|
326
|
-
const result = await state.client.Runtime.evaluate({
|
|
327
|
-
expression,
|
|
328
|
-
awaitPromise: true,
|
|
329
|
-
returnByValue: true,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
if (result.exceptionDetails) {
|
|
333
|
-
const description = result.exceptionDetails.exception?.description;
|
|
334
|
-
throw new Error(description ?? "Runtime.evaluate exception");
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return result.result.value as T;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private requireSession(sessionId: string): SessionState {
|
|
341
|
-
const session = this.sessions.get(sessionId);
|
|
342
|
-
if (!session) {
|
|
343
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
344
|
-
}
|
|
345
|
-
return session;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private buildChromeFlags(): string[] {
|
|
349
|
-
const flags = [
|
|
350
|
-
"--remote-allow-origins=*",
|
|
351
|
-
"--no-first-run",
|
|
352
|
-
"--no-default-browser-check",
|
|
353
|
-
"--disable-background-networking",
|
|
354
|
-
"--disable-background-timer-throttling",
|
|
355
|
-
"--disable-backgrounding-occluded-windows",
|
|
356
|
-
"--disable-renderer-backgrounding",
|
|
357
|
-
];
|
|
358
|
-
if (this.options.headless) {
|
|
359
|
-
flags.push("--headless=new");
|
|
360
|
-
}
|
|
361
|
-
return flags;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
private async resolveTargetId(port: number): Promise<string> {
|
|
365
|
-
const targets = await CDP.List({ port });
|
|
366
|
-
const pageTarget = targets.find((target) => target.type === "page");
|
|
367
|
-
if (pageTarget?.id) {
|
|
368
|
-
return pageTarget.id;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const created = await CDP.New({ port });
|
|
372
|
-
if (typeof created === "string") {
|
|
373
|
-
return created;
|
|
374
|
-
}
|
|
375
|
-
if (created && typeof created.id === "string") {
|
|
376
|
-
return created.id;
|
|
377
|
-
}
|
|
378
|
-
throw new Error("Could not create a page target for CDP");
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function resolveChromePath(): string {
|
|
383
|
-
const envPath = process.env.CHROME_PATH;
|
|
384
|
-
if (envPath && existsSync(envPath)) {
|
|
385
|
-
return envPath;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
const discovered = getChromePath();
|
|
390
|
-
if (discovered && existsSync(discovered)) {
|
|
391
|
-
return discovered;
|
|
392
|
-
}
|
|
393
|
-
} catch {
|
|
394
|
-
// Fall through to fixed-path probes below.
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const candidates = [
|
|
398
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
399
|
-
"/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
400
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
401
|
-
];
|
|
402
|
-
|
|
403
|
-
for (const candidate of candidates) {
|
|
404
|
-
if (existsSync(candidate)) {
|
|
405
|
-
return candidate;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
throw new Error(
|
|
410
|
-
"Chrome executable not found. Set CHROME_PATH or install Google Chrome.",
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function escapeForAttribute(value: string): string {
|
|
415
|
-
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function buildLocateExpression(target: Target): string {
|
|
419
|
-
const encodedTarget = JSON.stringify(target);
|
|
420
|
-
return `
|
|
421
|
-
(() => {
|
|
422
|
-
const HANDLE_ATTR = "${HANDLE_ATTR}";
|
|
423
|
-
const target = ${encodedTarget};
|
|
424
|
-
const normalize = (value) => String(value ?? "").replace(/\\s+/g, " ").trim();
|
|
425
|
-
const lower = (value) => normalize(value).toLowerCase();
|
|
426
|
-
const isVisible = (element) => {
|
|
427
|
-
if (!(element instanceof Element)) return false;
|
|
428
|
-
const style = window.getComputedStyle(element);
|
|
429
|
-
if (style.visibility === "hidden" || style.display === "none" || style.opacity === "0") {
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
const rect = element.getBoundingClientRect();
|
|
433
|
-
return rect.width > 0 && rect.height > 0;
|
|
434
|
-
};
|
|
435
|
-
const implicitRole = (element) => {
|
|
436
|
-
if (!(element instanceof Element)) return "";
|
|
437
|
-
const explicit = element.getAttribute("role");
|
|
438
|
-
if (explicit) return explicit;
|
|
439
|
-
const tag = element.tagName.toLowerCase();
|
|
440
|
-
if (tag === "button") return "button";
|
|
441
|
-
if (tag === "a" && element.hasAttribute("href")) return "link";
|
|
442
|
-
if (tag === "input") {
|
|
443
|
-
const type = (element.getAttribute("type") || "text").toLowerCase();
|
|
444
|
-
if (type === "submit" || type === "button") return "button";
|
|
445
|
-
if (type === "checkbox") return "checkbox";
|
|
446
|
-
if (type === "radio") return "radio";
|
|
447
|
-
return "textbox";
|
|
448
|
-
}
|
|
449
|
-
if (tag === "textarea") return "textbox";
|
|
450
|
-
if (tag === "select") return "combobox";
|
|
451
|
-
return "";
|
|
452
|
-
};
|
|
453
|
-
const nameFor = (element) => {
|
|
454
|
-
if (!(element instanceof Element)) return "";
|
|
455
|
-
const aria = element.getAttribute("aria-label");
|
|
456
|
-
if (aria) return aria;
|
|
457
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
458
|
-
return element.value || element.placeholder || "";
|
|
459
|
-
}
|
|
460
|
-
return element.textContent || "";
|
|
461
|
-
};
|
|
462
|
-
const assignHandle = (element, locatorUsed) => {
|
|
463
|
-
let handle = element.getAttribute(HANDLE_ATTR);
|
|
464
|
-
if (!handle) {
|
|
465
|
-
handle = "ah_" + Math.random().toString(36).slice(2, 10);
|
|
466
|
-
element.setAttribute(HANDLE_ATTR, handle);
|
|
467
|
-
}
|
|
468
|
-
return { handleId: handle, locatorUsed };
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
if (target.type === "selector") {
|
|
472
|
-
const element = document.querySelector(target.value);
|
|
473
|
-
if (element && isVisible(element)) return assignHandle(element, target.value);
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const pool = Array.from(
|
|
478
|
-
document.querySelectorAll("button,a,input,textarea,select,[role],label,[aria-label],span,div")
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
if (target.type === "text") {
|
|
482
|
-
const wanted = lower(target.value);
|
|
483
|
-
for (const element of pool) {
|
|
484
|
-
if (!isVisible(element)) continue;
|
|
485
|
-
const text = lower(nameFor(element));
|
|
486
|
-
if (!text) continue;
|
|
487
|
-
const matched = target.exact ? text === wanted : text.includes(wanted);
|
|
488
|
-
if (matched) return assignHandle(element, "text:" + target.value);
|
|
489
|
-
}
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const wantedRole = lower(target.role);
|
|
494
|
-
const wantedName = lower(target.name);
|
|
495
|
-
for (const element of pool) {
|
|
496
|
-
if (!isVisible(element)) continue;
|
|
497
|
-
if (lower(implicitRole(element)) !== wantedRole) continue;
|
|
498
|
-
const elementName = lower(nameFor(element));
|
|
499
|
-
if (!elementName) continue;
|
|
500
|
-
const matched = target.exact ? elementName === wantedName : elementName.includes(wantedName);
|
|
501
|
-
if (matched) return assignHandle(element, "role:" + target.role + "|name:" + target.name);
|
|
502
|
-
}
|
|
503
|
-
return null;
|
|
504
|
-
})()
|
|
505
|
-
`.trim();
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function buildClickExpression(handleId: string): string {
|
|
509
|
-
const safeHandle = escapeForAttribute(handleId);
|
|
510
|
-
return `
|
|
511
|
-
(() => {
|
|
512
|
-
const selector = '[${HANDLE_ATTR}="${safeHandle}"]';
|
|
513
|
-
const element = document.querySelector(selector);
|
|
514
|
-
if (!(element instanceof Element)) {
|
|
515
|
-
return { ok: false, reason: "ELEMENT_NOT_FOUND" };
|
|
516
|
-
}
|
|
517
|
-
element.scrollIntoView({ block: "center", inline: "center" });
|
|
518
|
-
if (element instanceof HTMLElement) {
|
|
519
|
-
element.click();
|
|
520
|
-
} else {
|
|
521
|
-
element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
522
|
-
}
|
|
523
|
-
return { ok: true };
|
|
524
|
-
})()
|
|
525
|
-
`.trim();
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function buildSetValueExpression(handleId: string, text: string, clear: boolean): string {
|
|
529
|
-
const safeHandle = escapeForAttribute(handleId);
|
|
530
|
-
const safeText = JSON.stringify(text);
|
|
531
|
-
return `
|
|
532
|
-
(() => {
|
|
533
|
-
const selector = '[${HANDLE_ATTR}="${safeHandle}"]';
|
|
534
|
-
const element = document.querySelector(selector);
|
|
535
|
-
if (!(element instanceof Element)) {
|
|
536
|
-
return { ok: false, reason: "ELEMENT_NOT_FOUND" };
|
|
537
|
-
}
|
|
538
|
-
const text = ${safeText};
|
|
539
|
-
const clear = ${clear ? "true" : "false"};
|
|
540
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
541
|
-
element.focus();
|
|
542
|
-
if (clear) element.value = "";
|
|
543
|
-
element.value = text;
|
|
544
|
-
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
545
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
546
|
-
return { ok: true };
|
|
547
|
-
}
|
|
548
|
-
if (element instanceof HTMLElement && element.isContentEditable) {
|
|
549
|
-
element.focus();
|
|
550
|
-
if (clear) element.textContent = "";
|
|
551
|
-
element.textContent = text;
|
|
552
|
-
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
553
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
554
|
-
return { ok: true };
|
|
555
|
-
}
|
|
556
|
-
return { ok: false, reason: "UNSUPPORTED_ELEMENT_FOR_SET_VALUE" };
|
|
557
|
-
})()
|
|
558
|
-
`.trim();
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function buildGetValueExpression(handleId: string): string {
|
|
562
|
-
const safeHandle = escapeForAttribute(handleId);
|
|
563
|
-
return `
|
|
564
|
-
(() => {
|
|
565
|
-
const selector = '[${HANDLE_ATTR}="${safeHandle}"]';
|
|
566
|
-
const element = document.querySelector(selector);
|
|
567
|
-
if (!(element instanceof Element)) {
|
|
568
|
-
throw new Error("ELEMENT_NOT_FOUND");
|
|
569
|
-
}
|
|
570
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
571
|
-
return String(element.value ?? "");
|
|
572
|
-
}
|
|
573
|
-
return String(element.textContent ?? "");
|
|
574
|
-
})()
|
|
575
|
-
`.trim();
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function buildSelectorVisibilityExpression(
|
|
579
|
-
selector: string,
|
|
580
|
-
shouldBeVisible: boolean,
|
|
581
|
-
): string {
|
|
582
|
-
const safeSelector = JSON.stringify(selector);
|
|
583
|
-
return `
|
|
584
|
-
(() => {
|
|
585
|
-
const selector = ${safeSelector};
|
|
586
|
-
const element = document.querySelector(selector);
|
|
587
|
-
const isVisible = (node) => {
|
|
588
|
-
if (!(node instanceof Element)) return false;
|
|
589
|
-
const style = window.getComputedStyle(node);
|
|
590
|
-
if (style.visibility === "hidden" || style.display === "none" || style.opacity === "0") {
|
|
591
|
-
return false;
|
|
592
|
-
}
|
|
593
|
-
const rect = node.getBoundingClientRect();
|
|
594
|
-
return rect.width > 0 && rect.height > 0;
|
|
595
|
-
};
|
|
596
|
-
const visible = isVisible(element);
|
|
597
|
-
return ${shouldBeVisible ? "visible" : "!visible || !element"};
|
|
598
|
-
})()
|
|
599
|
-
`.trim();
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function buildTextAppearsExpression(text: string): string {
|
|
603
|
-
const safeText = JSON.stringify(text);
|
|
604
|
-
return `
|
|
605
|
-
(() => {
|
|
606
|
-
const wanted = String(${safeText}).toLowerCase();
|
|
607
|
-
const pageText = String(document.body?.innerText || "").toLowerCase();
|
|
608
|
-
return pageText.includes(wanted);
|
|
609
|
-
})()
|
|
610
|
-
`.trim();
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function buildExtractExpression(handleId: string, format: ExtractFormat): string {
|
|
614
|
-
const safeHandle = escapeForAttribute(handleId);
|
|
615
|
-
if (format === "text") {
|
|
616
|
-
return `
|
|
617
|
-
(() => {
|
|
618
|
-
const selector = '[${HANDLE_ATTR}="${safeHandle}"]';
|
|
619
|
-
const element = document.querySelector(selector);
|
|
620
|
-
if (!(element instanceof Element)) {
|
|
621
|
-
throw new Error("ELEMENT_NOT_FOUND");
|
|
622
|
-
}
|
|
623
|
-
return String(element.textContent || "").trim();
|
|
624
|
-
})()
|
|
625
|
-
`.trim();
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (format === "table") {
|
|
629
|
-
return `
|
|
630
|
-
(() => {
|
|
631
|
-
const selector = '[${HANDLE_ATTR}="${safeHandle}"]';
|
|
632
|
-
const element = document.querySelector(selector);
|
|
633
|
-
if (!(element instanceof Element)) {
|
|
634
|
-
throw new Error("ELEMENT_NOT_FOUND");
|
|
635
|
-
}
|
|
636
|
-
const table = element.tagName.toLowerCase() === "table" ? element : element.closest("table");
|
|
637
|
-
if (!(table instanceof HTMLTableElement)) {
|
|
638
|
-
throw new Error("TARGET_IS_NOT_A_TABLE");
|
|
639
|
-
}
|
|
640
|
-
const headers = Array.from(table.querySelectorAll("thead th")).map((th) =>
|
|
641
|
-
String(th.textContent || "").trim()
|
|
642
|
-
);
|
|
643
|
-
const rows = Array.from(table.querySelectorAll("tbody tr"))
|
|
644
|
-
.map((row) =>
|
|
645
|
-
Array.from(row.querySelectorAll("th,td")).map((cell) =>
|
|
646
|
-
String(cell.textContent || "").trim()
|
|
647
|
-
)
|
|
648
|
-
)
|
|
649
|
-
.filter((row) => row.length > 0);
|
|
650
|
-
return { headers, rows };
|
|
651
|
-
})()
|
|
652
|
-
`.trim();
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return `
|
|
656
|
-
(() => {
|
|
657
|
-
const selector = '[${HANDLE_ATTR}="${safeHandle}"]';
|
|
658
|
-
const element = document.querySelector(selector);
|
|
659
|
-
if (!(element instanceof Element)) {
|
|
660
|
-
throw new Error("ELEMENT_NOT_FOUND");
|
|
661
|
-
}
|
|
662
|
-
const raw = String(element.textContent || "").trim();
|
|
663
|
-
try {
|
|
664
|
-
return JSON.parse(raw);
|
|
665
|
-
} catch {
|
|
666
|
-
return { raw };
|
|
667
|
-
}
|
|
668
|
-
})()
|
|
669
|
-
`.trim();
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function sleep(ms: number): Promise<void> {
|
|
673
|
-
return new Promise((resolve) => {
|
|
674
|
-
setTimeout(resolve, ms);
|
|
675
|
-
});
|
|
676
|
-
}
|