@reconcrap/boss-recommend-mcp 2.1.20 → 2.1.22

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 (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2372
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +529 -499
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,2374 +1,2421 @@
1
- import { execFile, spawn } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import CDP from "chrome-remote-interface";
6
-
7
- export const DEFAULT_CHROME_HOST = "127.0.0.1";
8
- export const DEFAULT_CHROME_PORT = 9222;
9
- export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
- export const LID_CLOSED_SAFE_CHROME_ARGS = [
11
- "--disable-backgrounding-occluded-windows",
12
- "--disable-background-timer-throttling",
13
- "--disable-renderer-backgrounding",
14
- "--disable-features=CalculateNativeWinOcclusion"
15
- ];
16
- export const DEFAULT_REQUIRED_CHROME_FLAGS = LID_CLOSED_SAFE_CHROME_ARGS;
17
-
18
- export const ALLOWED_CDP_DOMAINS = new Set([
19
- "Accessibility",
20
- "Browser",
21
- "DOM",
22
- "Input",
23
- "Network",
24
- "Page",
25
- "Target"
26
- ]);
27
-
1
+ import { execFile, spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import CDP from "chrome-remote-interface";
6
+
7
+ export const DEFAULT_CHROME_HOST = "127.0.0.1";
8
+ export const DEFAULT_CHROME_PORT = 9222;
9
+ export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
+ export const LID_CLOSED_SAFE_CHROME_ARGS = [
11
+ "--disable-backgrounding-occluded-windows",
12
+ "--disable-background-timer-throttling",
13
+ "--disable-renderer-backgrounding",
14
+ "--disable-features=CalculateNativeWinOcclusion"
15
+ ];
16
+ export const DEFAULT_REQUIRED_CHROME_FLAGS = LID_CLOSED_SAFE_CHROME_ARGS;
17
+
18
+ export const ALLOWED_CDP_DOMAINS = new Set([
19
+ "Accessibility",
20
+ "Browser",
21
+ "DOM",
22
+ "Input",
23
+ "Network",
24
+ "Page",
25
+ "Target"
26
+ ]);
27
+
28
28
  export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
29
-
30
- const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
31
- const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
32
- const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
33
- const CDP_CLOSED_TRANSPORT_PATTERN = /WebSocket is not open|readyState\s+\d+\s+\(CLOSED\)|ECONNRESET|socket hang up|Target closed|Session closed|Connection closed/i;
34
- const BOSS_LOGIN_DOM_SELECTORS = [
35
- ".login-box",
36
- ".login-form",
37
- ".login-dialog",
38
- ".sign-form",
39
- ".qrcode-box",
40
- ".user-login",
41
- "input[name='phone']",
42
- "input[placeholder*='手机号']",
43
- "input[placeholder*='验证码']"
44
- ];
45
- const HUMAN_INTERACTION_CONFIG = new WeakMap();
46
- const DEFAULT_HUMAN_BEHAVIOR_PROFILE = "paced_with_rests";
47
- export const DETERMINISTIC_CLICK_OPTIONS = Object.freeze({
48
- humanRestEnabled: false
49
- });
50
- const HUMAN_BEHAVIOR_PROFILES = Object.freeze({
51
- baseline: Object.freeze({
52
- enabled: false,
53
- clickMovement: false,
54
- textEntry: false,
55
- listScrollJitter: false,
56
- shortRest: false,
57
- batchRest: false,
58
- actionCooldown: false
59
- }),
60
- paced: Object.freeze({
61
- enabled: true,
62
- clickMovement: true,
63
- textEntry: true,
64
- listScrollJitter: true,
65
- shortRest: false,
66
- batchRest: false,
67
- actionCooldown: true
68
- }),
69
- paced_with_rests: Object.freeze({
70
- enabled: true,
71
- clickMovement: true,
72
- textEntry: true,
73
- listScrollJitter: true,
74
- shortRest: true,
75
- batchRest: true,
76
- actionCooldown: true
77
- })
78
- });
79
- const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
80
- off: "baseline",
81
- disabled: "baseline",
82
- deterministic: "baseline",
83
- safe: "paced",
84
- safe_pacing: "paced",
85
- paced_with_rest: "paced_with_rests",
86
- rests: "paced_with_rests",
87
- rest: "paced_with_rests"
88
- });
89
- const DEFAULT_HUMAN_REST_LEVEL = "low";
90
- const HUMAN_REST_LEVEL_ALIASES = Object.freeze({
91
- default: "low",
92
- light: "low",
93
- normal: "medium",
94
- med: "medium",
95
- heavy: "high"
96
- });
97
- const HUMAN_REST_LEVEL_PROFILES = Object.freeze({
98
- medium: Object.freeze({
99
- targetRestMs: 30 * 60 * 1000,
100
- targetCandidateCount: 700,
101
- targetWindowMs: 5 * 60 * 60 * 1000,
102
- intervalMin: 4,
103
- intervalMax: 16,
104
- longRestProbability: 0.22,
105
- shortRestMinMs: 8000,
106
- shortRestMaxMs: 45000,
107
- longRestMinMs: 60000,
108
- longRestMaxMs: 180000,
109
- minDebtToRestMs: 8000,
110
- forceDebtMs: 90000,
111
- maxOverspendMs: 15000
112
- }),
113
- high: Object.freeze({
114
- targetRestMs: 60 * 60 * 1000,
115
- targetCandidateCount: 700,
116
- targetWindowMs: 5 * 60 * 60 * 1000,
117
- intervalMin: 3,
118
- intervalMax: 12,
119
- longRestProbability: 0.28,
120
- shortRestMinMs: 12000,
121
- shortRestMaxMs: 75000,
122
- longRestMinMs: 90000,
123
- longRestMaxMs: 300000,
124
- minDebtToRestMs: 12000,
125
- forceDebtMs: 150000,
126
- maxOverspendMs: 25000
127
- })
128
- });
129
-
130
- function clampNumber(value, min, max) {
131
- const number = Number(value);
132
- if (!Number.isFinite(number)) return min;
133
- return Math.min(max, Math.max(min, number));
134
- }
135
-
136
- function randomBetween(random, min, max) {
137
- const lower = Number(min) || 0;
138
- const upper = Number(max) || lower;
139
- if (upper <= lower) return lower;
140
- return lower + random() * (upper - lower);
141
- }
142
-
143
- function randomIntegerBetween(random, min, max) {
144
- return Math.floor(randomBetween(random, min, max + 1));
145
- }
146
-
147
- function normalizePoint(point) {
148
- const x = Number(point?.x);
149
- const y = Number(point?.y);
150
- if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
151
- return { x, y };
152
- }
153
-
154
- function normalizeRandom(random) {
155
- return typeof random === "function" ? random : Math.random;
156
- }
157
-
158
- function getHumanInteractionConfig(client) {
159
- return HUMAN_INTERACTION_CONFIG.get(client) || null;
160
- }
161
-
162
- function normalizeBooleanOption(raw, fallback = null) {
163
- if (typeof raw === "boolean") return raw;
164
- if (typeof raw === "number" && Number.isFinite(raw)) return raw !== 0;
165
- const normalized = String(raw ?? "").trim().toLowerCase();
166
- if (!normalized) return fallback;
167
- if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
168
- if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
169
- return fallback;
170
- }
171
-
172
- function readFirstOption(source, keys = []) {
173
- if (!source || typeof source !== "object") return undefined;
174
- for (const key of keys) {
175
- if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
176
- }
177
- return undefined;
178
- }
179
-
180
- function normalizeFeatureBoolean(raw, fallback) {
181
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
182
- return normalizeBooleanOption(readFirstOption(raw, ["enabled", "enable"]), fallback);
183
- }
184
- return normalizeBooleanOption(raw, fallback);
185
- }
186
-
187
- export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
188
- const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
189
- const profile = HUMAN_BEHAVIOR_PROFILE_ALIASES[normalized] || normalized;
190
- return Object.prototype.hasOwnProperty.call(HUMAN_BEHAVIOR_PROFILES, profile)
191
- ? profile
192
- : fallback;
193
- }
194
-
195
- export function normalizeHumanRestLevel(raw, fallback = DEFAULT_HUMAN_REST_LEVEL) {
196
- const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
197
- const level = HUMAN_REST_LEVEL_ALIASES[normalized] || normalized;
198
- return level === "low" || level === "medium" || level === "high"
199
- ? level
200
- : fallback;
201
- }
202
-
203
- export function normalizeHumanBehaviorOptions(raw = null, {
204
- legacyEnabled = false,
205
- safePacing = null,
206
- batchRestEnabled = null
207
- } = {}) {
208
- const safePacingFlag = normalizeBooleanOption(safePacing, null);
209
- const batchRestFlag = normalizeBooleanOption(batchRestEnabled, null);
210
- let source = "default";
211
- let rawObject = {};
212
- if (typeof raw === "boolean") {
213
- rawObject = { enabled: raw };
214
- source = "boolean";
215
- } else if (typeof raw === "string") {
216
- rawObject = { profile: raw };
217
- source = "profile";
218
- } else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
219
- rawObject = raw;
220
- source = "object";
221
- }
222
-
223
- const explicitProfile = readFirstOption(rawObject, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
224
- const enabledRaw = readFirstOption(rawObject, ["enabled", "enable", "human_behavior_enabled"]);
225
- const explicitEnabled = normalizeBooleanOption(enabledRaw, null);
226
- const inferredProfile = (raw === true || explicitEnabled === true) && legacyEnabled !== true && batchRestFlag !== true
227
- ? "paced"
228
- : legacyEnabled === true || batchRestFlag === true
229
- ? "paced_with_rests"
230
- : safePacingFlag === true
231
- ? "paced"
232
- : DEFAULT_HUMAN_BEHAVIOR_PROFILE;
233
- const profile = normalizeHumanBehaviorProfile(explicitProfile, inferredProfile);
234
- const profileDefaults = {
235
- ...HUMAN_BEHAVIOR_PROFILES[profile]
236
- };
237
- if (legacyEnabled === true && !explicitProfile) {
238
- Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
239
- } else if (safePacingFlag === true && !explicitProfile) {
240
- Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced);
241
- }
242
- if (batchRestFlag === true && !explicitProfile) {
243
- Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
244
- }
245
-
246
- const hasExplicitEnabled = enabledRaw !== undefined;
247
- if (hasExplicitEnabled) {
248
- profileDefaults.enabled = normalizeBooleanOption(enabledRaw, profileDefaults.enabled);
249
- }
250
- if (!hasExplicitEnabled && (safePacingFlag === false || batchRestFlag === false) && !explicitProfile && legacyEnabled !== true) {
251
- profileDefaults.enabled = false;
252
- }
253
- if (!hasExplicitEnabled && (safePacingFlag === true || batchRestFlag === true || legacyEnabled === true)) {
254
- profileDefaults.enabled = true;
255
- }
256
-
257
- const enabled = profileDefaults.enabled === true;
258
- const clickMovement = normalizeFeatureBoolean(
259
- readFirstOption(rawObject, ["clickMovement", "click_movement", "click_movement_enabled"]),
260
- profileDefaults.clickMovement
261
- );
262
- const textEntry = normalizeFeatureBoolean(
263
- readFirstOption(rawObject, ["textEntry", "text_entry", "text_entry_enabled"]),
264
- profileDefaults.textEntry
265
- );
266
- const listScrollJitter = normalizeFeatureBoolean(
267
- readFirstOption(rawObject, ["listScrollJitter", "list_scroll_jitter", "scrollJitter", "scroll_jitter"]),
268
- profileDefaults.listScrollJitter
269
- );
270
- const actionCooldown = normalizeFeatureBoolean(
271
- readFirstOption(rawObject, ["actionCooldown", "action_cooldown", "readPause", "read_pause"]),
272
- profileDefaults.actionCooldown
273
- );
274
- let shortRest = normalizeFeatureBoolean(
275
- readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]),
276
- profileDefaults.shortRest
277
- );
278
- let batchRest = normalizeFeatureBoolean(
279
- readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
280
- profileDefaults.batchRest
281
- );
282
- const restLevel = normalizeHumanRestLevel(
283
- readFirstOption(rawObject, ["restLevel", "rest_level"]),
284
- DEFAULT_HUMAN_REST_LEVEL
285
- );
286
- if (batchRestFlag !== null) {
287
- batchRest = batchRestFlag;
288
- if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
289
- shortRest = true;
290
- }
291
- }
292
-
293
- return {
294
- enabled,
295
- profile,
296
- source,
297
- clickMovement: enabled && clickMovement === true,
298
- textEntry: enabled && textEntry === true,
299
- listScrollJitter: enabled && listScrollJitter === true,
300
- shortRest: enabled && shortRest === true,
301
- batchRest: enabled && batchRest === true,
302
- actionCooldown: enabled && actionCooldown === true,
303
- restLevel,
304
- restEnabled: enabled && (shortRest === true || batchRest === true)
305
- };
306
- }
307
-
308
- function nowIso() {
309
- return new Date().toISOString();
310
- }
311
-
312
- function normalizeTargetMatcher({ targetUrlIncludes, targetPredicate } = {}) {
313
- if (typeof targetPredicate === "function") return targetPredicate;
314
- if (targetUrlIncludes) {
315
- return (target) => String(target?.url || "").includes(targetUrlIncludes);
316
- }
317
- return (target) => target?.type === "page";
318
- }
319
-
29
+ export const FORBIDDEN_CDP_METHODS = new Set([
30
+ "Page.addScriptToEvaluateOnNewDocument"
31
+ ]);
32
+
33
+ const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
34
+ const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
35
+ const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
36
+ const CDP_CLOSED_TRANSPORT_PATTERN = /WebSocket is not open|readyState\s+\d+\s+\(CLOSED\)|ECONNRESET|socket hang up|Target closed|Session closed|Connection closed/i;
37
+ const BOSS_LOGIN_DOM_SELECTORS = [
38
+ ".login-box",
39
+ ".login-form",
40
+ ".login-dialog",
41
+ ".sign-form",
42
+ ".qrcode-box",
43
+ ".user-login",
44
+ "input[name='phone']",
45
+ "input[placeholder*='手机号']",
46
+ "input[placeholder*='验证码']"
47
+ ];
48
+ const HUMAN_INTERACTION_CONFIG = new WeakMap();
49
+ const DEFAULT_HUMAN_BEHAVIOR_PROFILE = "paced_with_rests";
50
+ export const DETERMINISTIC_CLICK_OPTIONS = Object.freeze({
51
+ humanRestEnabled: false
52
+ });
53
+ const HUMAN_BEHAVIOR_PROFILES = Object.freeze({
54
+ baseline: Object.freeze({
55
+ enabled: false,
56
+ clickMovement: false,
57
+ textEntry: false,
58
+ listScrollJitter: false,
59
+ shortRest: false,
60
+ batchRest: false,
61
+ actionCooldown: false
62
+ }),
63
+ paced: Object.freeze({
64
+ enabled: true,
65
+ clickMovement: true,
66
+ textEntry: true,
67
+ listScrollJitter: true,
68
+ shortRest: false,
69
+ batchRest: false,
70
+ actionCooldown: true
71
+ }),
72
+ paced_with_rests: Object.freeze({
73
+ enabled: true,
74
+ clickMovement: true,
75
+ textEntry: true,
76
+ listScrollJitter: true,
77
+ shortRest: true,
78
+ batchRest: true,
79
+ actionCooldown: true
80
+ })
81
+ });
82
+ const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
83
+ off: "baseline",
84
+ disabled: "baseline",
85
+ deterministic: "baseline",
86
+ safe: "paced",
87
+ safe_pacing: "paced",
88
+ paced_with_rest: "paced_with_rests",
89
+ rests: "paced_with_rests",
90
+ rest: "paced_with_rests"
91
+ });
92
+ const DEFAULT_HUMAN_REST_LEVEL = "low";
93
+ const HUMAN_REST_LEVEL_ALIASES = Object.freeze({
94
+ default: "low",
95
+ light: "low",
96
+ normal: "medium",
97
+ med: "medium",
98
+ heavy: "high"
99
+ });
100
+ const HUMAN_REST_LEVEL_PROFILES = Object.freeze({
101
+ medium: Object.freeze({
102
+ targetRestMs: 30 * 60 * 1000,
103
+ targetCandidateCount: 700,
104
+ targetWindowMs: 5 * 60 * 60 * 1000,
105
+ intervalMin: 4,
106
+ intervalMax: 16,
107
+ longRestProbability: 0.22,
108
+ shortRestMinMs: 8000,
109
+ shortRestMaxMs: 45000,
110
+ longRestMinMs: 60000,
111
+ longRestMaxMs: 180000,
112
+ minDebtToRestMs: 8000,
113
+ forceDebtMs: 90000,
114
+ maxOverspendMs: 15000
115
+ }),
116
+ high: Object.freeze({
117
+ targetRestMs: 60 * 60 * 1000,
118
+ targetCandidateCount: 700,
119
+ targetWindowMs: 5 * 60 * 60 * 1000,
120
+ intervalMin: 3,
121
+ intervalMax: 12,
122
+ longRestProbability: 0.28,
123
+ shortRestMinMs: 12000,
124
+ shortRestMaxMs: 75000,
125
+ longRestMinMs: 90000,
126
+ longRestMaxMs: 300000,
127
+ minDebtToRestMs: 12000,
128
+ forceDebtMs: 150000,
129
+ maxOverspendMs: 25000
130
+ })
131
+ });
132
+
133
+ function clampNumber(value, min, max) {
134
+ const number = Number(value);
135
+ if (!Number.isFinite(number)) return min;
136
+ return Math.min(max, Math.max(min, number));
137
+ }
138
+
139
+ function randomBetween(random, min, max) {
140
+ const lower = Number(min) || 0;
141
+ const upper = Number(max) || lower;
142
+ if (upper <= lower) return lower;
143
+ return lower + random() * (upper - lower);
144
+ }
145
+
146
+ function randomIntegerBetween(random, min, max) {
147
+ return Math.floor(randomBetween(random, min, max + 1));
148
+ }
149
+
150
+ function normalizePoint(point) {
151
+ const x = Number(point?.x);
152
+ const y = Number(point?.y);
153
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
154
+ return { x, y };
155
+ }
156
+
157
+ function normalizeRandom(random) {
158
+ return typeof random === "function" ? random : Math.random;
159
+ }
160
+
161
+ function getHumanInteractionConfig(client) {
162
+ return HUMAN_INTERACTION_CONFIG.get(client) || null;
163
+ }
164
+
165
+ function normalizeBooleanOption(raw, fallback = null) {
166
+ if (typeof raw === "boolean") return raw;
167
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw !== 0;
168
+ const normalized = String(raw ?? "").trim().toLowerCase();
169
+ if (!normalized) return fallback;
170
+ if (["true", "1", "yes", "y", "on", "enabled"].includes(normalized)) return true;
171
+ if (["false", "0", "no", "n", "off", "disabled"].includes(normalized)) return false;
172
+ return fallback;
173
+ }
174
+
175
+ function readFirstOption(source, keys = []) {
176
+ if (!source || typeof source !== "object") return undefined;
177
+ for (const key of keys) {
178
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
179
+ }
180
+ return undefined;
181
+ }
182
+
183
+ function normalizeFeatureBoolean(raw, fallback) {
184
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
185
+ return normalizeBooleanOption(readFirstOption(raw, ["enabled", "enable"]), fallback);
186
+ }
187
+ return normalizeBooleanOption(raw, fallback);
188
+ }
189
+
190
+ export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
191
+ const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
192
+ const profile = HUMAN_BEHAVIOR_PROFILE_ALIASES[normalized] || normalized;
193
+ return Object.prototype.hasOwnProperty.call(HUMAN_BEHAVIOR_PROFILES, profile)
194
+ ? profile
195
+ : fallback;
196
+ }
197
+
198
+ export function normalizeHumanRestLevel(raw, fallback = DEFAULT_HUMAN_REST_LEVEL) {
199
+ const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
200
+ const level = HUMAN_REST_LEVEL_ALIASES[normalized] || normalized;
201
+ return level === "low" || level === "medium" || level === "high"
202
+ ? level
203
+ : fallback;
204
+ }
205
+
206
+ export function normalizeHumanBehaviorOptions(raw = null, {
207
+ legacyEnabled = false,
208
+ safePacing = null,
209
+ batchRestEnabled = null
210
+ } = {}) {
211
+ const safePacingFlag = normalizeBooleanOption(safePacing, null);
212
+ const batchRestFlag = normalizeBooleanOption(batchRestEnabled, null);
213
+ let source = "default";
214
+ let rawObject = {};
215
+ if (typeof raw === "boolean") {
216
+ rawObject = { enabled: raw };
217
+ source = "boolean";
218
+ } else if (typeof raw === "string") {
219
+ rawObject = { profile: raw };
220
+ source = "profile";
221
+ } else if (raw && typeof raw === "object" && !Array.isArray(raw)) {
222
+ rawObject = raw;
223
+ source = "object";
224
+ }
225
+
226
+ const explicitProfile = readFirstOption(rawObject, ["profile", "mode", "behaviorProfile", "behavior_profile"]);
227
+ const enabledRaw = readFirstOption(rawObject, ["enabled", "enable", "human_behavior_enabled"]);
228
+ const explicitEnabled = normalizeBooleanOption(enabledRaw, null);
229
+ const inferredProfile = (raw === true || explicitEnabled === true) && legacyEnabled !== true && batchRestFlag !== true
230
+ ? "paced"
231
+ : legacyEnabled === true || batchRestFlag === true
232
+ ? "paced_with_rests"
233
+ : safePacingFlag === true
234
+ ? "paced"
235
+ : DEFAULT_HUMAN_BEHAVIOR_PROFILE;
236
+ const profile = normalizeHumanBehaviorProfile(explicitProfile, inferredProfile);
237
+ const profileDefaults = {
238
+ ...HUMAN_BEHAVIOR_PROFILES[profile]
239
+ };
240
+ if (legacyEnabled === true && !explicitProfile) {
241
+ Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
242
+ } else if (safePacingFlag === true && !explicitProfile) {
243
+ Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced);
244
+ }
245
+ if (batchRestFlag === true && !explicitProfile) {
246
+ Object.assign(profileDefaults, HUMAN_BEHAVIOR_PROFILES.paced_with_rests);
247
+ }
248
+
249
+ const hasExplicitEnabled = enabledRaw !== undefined;
250
+ if (hasExplicitEnabled) {
251
+ profileDefaults.enabled = normalizeBooleanOption(enabledRaw, profileDefaults.enabled);
252
+ }
253
+ if (!hasExplicitEnabled && (safePacingFlag === false || batchRestFlag === false) && !explicitProfile && legacyEnabled !== true) {
254
+ profileDefaults.enabled = false;
255
+ }
256
+ if (!hasExplicitEnabled && (safePacingFlag === true || batchRestFlag === true || legacyEnabled === true)) {
257
+ profileDefaults.enabled = true;
258
+ }
259
+
260
+ const enabled = profileDefaults.enabled === true;
261
+ const clickMovement = normalizeFeatureBoolean(
262
+ readFirstOption(rawObject, ["clickMovement", "click_movement", "click_movement_enabled"]),
263
+ profileDefaults.clickMovement
264
+ );
265
+ const textEntry = normalizeFeatureBoolean(
266
+ readFirstOption(rawObject, ["textEntry", "text_entry", "text_entry_enabled"]),
267
+ profileDefaults.textEntry
268
+ );
269
+ const listScrollJitter = normalizeFeatureBoolean(
270
+ readFirstOption(rawObject, ["listScrollJitter", "list_scroll_jitter", "scrollJitter", "scroll_jitter"]),
271
+ profileDefaults.listScrollJitter
272
+ );
273
+ const actionCooldown = normalizeFeatureBoolean(
274
+ readFirstOption(rawObject, ["actionCooldown", "action_cooldown", "readPause", "read_pause"]),
275
+ profileDefaults.actionCooldown
276
+ );
277
+ let shortRest = normalizeFeatureBoolean(
278
+ readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]),
279
+ profileDefaults.shortRest
280
+ );
281
+ let batchRest = normalizeFeatureBoolean(
282
+ readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
283
+ profileDefaults.batchRest
284
+ );
285
+ const restLevel = normalizeHumanRestLevel(
286
+ readFirstOption(rawObject, ["restLevel", "rest_level"]),
287
+ DEFAULT_HUMAN_REST_LEVEL
288
+ );
289
+ if (batchRestFlag !== null) {
290
+ batchRest = batchRestFlag;
291
+ if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
292
+ shortRest = true;
293
+ }
294
+ }
295
+
296
+ return {
297
+ enabled,
298
+ profile,
299
+ source,
300
+ clickMovement: enabled && clickMovement === true,
301
+ textEntry: enabled && textEntry === true,
302
+ listScrollJitter: enabled && listScrollJitter === true,
303
+ shortRest: enabled && shortRest === true,
304
+ batchRest: enabled && batchRest === true,
305
+ actionCooldown: enabled && actionCooldown === true,
306
+ restLevel,
307
+ restEnabled: enabled && (shortRest === true || batchRest === true)
308
+ };
309
+ }
310
+
311
+ function nowIso() {
312
+ return new Date().toISOString();
313
+ }
314
+
315
+ function normalizeTargetMatcher({ targetUrlIncludes, targetPredicate } = {}) {
316
+ if (typeof targetPredicate === "function") return targetPredicate;
317
+ if (targetUrlIncludes) {
318
+ return (target) => String(target?.url || "").includes(targetUrlIncludes);
319
+ }
320
+ return (target) => target?.type === "page";
321
+ }
322
+
320
323
  function isForbiddenMethod(methodName) {
321
- const [domain] = String(methodName || "").split(".");
322
- return FORBIDDEN_CDP_DOMAINS.has(domain);
323
- }
324
-
325
- function methodName(domain, method) {
326
- return `${String(domain)}.${String(method)}`;
327
- }
328
-
329
- function recordMethod(methodLog, method) {
330
- if (Array.isArray(methodLog)) {
331
- methodLog.push({ method, at: nowIso() });
332
- }
333
- }
334
-
335
- export function assertNoForbiddenCdpCalls(methodLog = []) {
336
- const forbidden = methodLog.filter((entry) => isForbiddenMethod(entry?.method));
337
- if (forbidden.length > 0) {
338
- const methods = forbidden.map((entry) => entry.method).join(", ");
339
- throw new Error(`Forbidden CDP methods were used: ${methods}`);
340
- }
341
- }
342
-
343
- export function humanDelay(baseMs, varianceMs, {
344
- minMs = 100,
345
- maxMs = 60000,
346
- random = Math.random
347
- } = {}) {
348
- const nextRandom = normalizeRandom(random);
349
- const base = Math.max(0, Number(baseMs) || 0);
350
- const variance = Math.max(0, Number(varianceMs) || 0);
351
- const lower = Math.max(0, Number(minMs) || 0);
352
- const upper = Math.max(lower, Number(maxMs) || lower);
353
- if (variance <= 0) return Math.round(clampNumber(base, lower, upper));
354
- const u1 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
355
- const u2 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
356
- const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
357
- return Math.round(clampNumber(base + z * variance, lower, upper));
358
- }
359
-
360
- export function generateBezierPath(start, end, {
361
- steps = 18,
362
- random = Math.random,
363
- controlJitterX = 100,
364
- controlJitterY = 60
365
- } = {}) {
366
- const startPoint = normalizePoint(start);
367
- const endPoint = normalizePoint(end);
368
- if (!startPoint || !endPoint) {
369
- throw new Error("generateBezierPath requires finite start and end points");
370
- }
371
- const nextRandom = normalizeRandom(random);
372
- const safeSteps = Math.max(1, Math.floor(Number(steps) || 18));
373
- const midX = (startPoint.x + endPoint.x) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterX) || 0);
374
- const midY = (startPoint.y + endPoint.y) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterY) || 0);
375
- const path = [];
376
- for (let index = 0; index <= safeSteps; index += 1) {
377
- const t = index / safeSteps;
378
- const inverse = 1 - t;
379
- path.push({
380
- x: inverse * inverse * startPoint.x + 2 * inverse * t * midX + t * t * endPoint.x,
381
- y: inverse * inverse * startPoint.y + 2 * inverse * t * midY + t * t * endPoint.y
382
- });
383
- }
384
- return path;
385
- }
386
-
387
- export function configureHumanInteraction(client, {
388
- enabled = false,
389
- clickMovementEnabled = null,
390
- textEntryEnabled = null,
391
- safeClickPointEnabled = null,
392
- actionCooldownEnabled = null,
393
- random = Math.random,
394
- sleepFn = null,
395
- moveSteps = 18,
396
- moveJitterPx = 3,
397
- hoverJitterPx = 5,
398
- moveDelayMinMs = 5,
399
- moveDelayMaxMs = 23,
400
- hoverDelayMinMs = 10,
401
- hoverDelayMaxMs = 30,
402
- prePressBaseMs = 260,
403
- prePressVarianceMs = 80,
404
- holdVarianceMs = 30,
405
- safeClickMinWidth = 44,
406
- safeClickMinHeight = 28,
407
- safeClickInsetRatio = 0.22,
408
- safeClickMinInsetPx = 4,
409
- safeClickMaxInsetPx = 18,
410
- textChunkMinLength = 1,
411
- textChunkMaxLength = 5,
412
- textChunkDelayBaseMs = 55,
413
- textChunkDelayVarianceMs = 30
414
- } = {}) {
415
- const previous = getHumanInteractionConfig(client);
416
- const normalizedEnabled = enabled === true;
417
- HUMAN_INTERACTION_CONFIG.set(client, {
418
- enabled: normalizedEnabled,
419
- clickMovementEnabled: normalizedEnabled && clickMovementEnabled !== false,
420
- textEntryEnabled: normalizedEnabled && textEntryEnabled !== false,
421
- safeClickPointEnabled: normalizedEnabled && safeClickPointEnabled !== false,
422
- actionCooldownEnabled: normalizedEnabled && actionCooldownEnabled !== false,
423
- random: normalizeRandom(random),
424
- sleepFn: typeof sleepFn === "function" ? sleepFn : sleep,
425
- moveSteps: Math.max(1, Math.floor(Number(moveSteps) || 18)),
426
- moveJitterPx: Math.max(0, Number(moveJitterPx) || 0),
427
- hoverJitterPx: Math.max(0, Number(hoverJitterPx) || 0),
428
- moveDelayMinMs: Math.max(0, Number(moveDelayMinMs) || 0),
429
- moveDelayMaxMs: Math.max(0, Number(moveDelayMaxMs) || 0),
430
- hoverDelayMinMs: Math.max(0, Number(hoverDelayMinMs) || 0),
431
- hoverDelayMaxMs: Math.max(0, Number(hoverDelayMaxMs) || 0),
432
- prePressBaseMs: Math.max(0, Number(prePressBaseMs) || 0),
433
- prePressVarianceMs: Math.max(0, Number(prePressVarianceMs) || 0),
434
- holdVarianceMs: Math.max(0, Number(holdVarianceMs) || 0),
435
- safeClickMinWidth: Math.max(1, Number(safeClickMinWidth) || 44),
436
- safeClickMinHeight: Math.max(1, Number(safeClickMinHeight) || 28),
437
- safeClickInsetRatio: clampNumber(safeClickInsetRatio, 0.05, 0.45),
438
- safeClickMinInsetPx: Math.max(0, Number(safeClickMinInsetPx) || 0),
439
- safeClickMaxInsetPx: Math.max(0, Number(safeClickMaxInsetPx) || 0),
440
- textChunkMinLength: Math.max(1, Math.floor(Number(textChunkMinLength) || 1)),
441
- textChunkMaxLength: Math.max(1, Math.floor(Number(textChunkMaxLength) || 5)),
442
- textChunkDelayBaseMs: Math.max(0, Number(textChunkDelayBaseMs) || 0),
443
- textChunkDelayVarianceMs: Math.max(0, Number(textChunkDelayVarianceMs) || 0),
444
- lastMousePoint: previous?.lastMousePoint || null
445
- });
446
- return () => {
447
- if (previous) {
448
- HUMAN_INTERACTION_CONFIG.set(client, previous);
449
- } else {
450
- HUMAN_INTERACTION_CONFIG.delete(client);
451
- }
452
- };
453
- }
454
-
455
- export function createHumanRestController({
456
- enabled = false,
457
- shortRestEnabled = true,
458
- batchRestEnabled = true,
459
- random = Math.random,
460
- nowFn = Date.now,
461
- restLevel = DEFAULT_HUMAN_REST_LEVEL,
462
- shortRestProbability = 0.08,
463
- shortRestMinMs = 3000,
464
- shortRestMaxMs = 7000,
465
- batchThresholdBase = 25,
466
- batchThresholdJitter = 8,
467
- batchRestMinMs = 15000,
468
- batchRestMaxMs = 30000
469
- } = {}) {
470
- const nextRandom = normalizeRandom(random);
471
- const readNow = typeof nowFn === "function" ? nowFn : Date.now;
472
- const normalizedRestLevel = normalizeHumanRestLevel(restLevel);
473
- const budgetProfile = (shortRestEnabled !== false || batchRestEnabled !== false)
474
- ? HUMAN_REST_LEVEL_PROFILES[normalizedRestLevel] || null
475
- : null;
476
- const nextBudgetRestInterval = () => budgetProfile
477
- ? randomIntegerBetween(nextRandom, budgetProfile.intervalMin, budgetProfile.intervalMax)
478
- : 0;
479
- const state = {
480
- enabled: enabled === true,
481
- rest_level: normalizedRestLevel,
482
- short_rest_enabled: enabled === true && shortRestEnabled !== false,
483
- batch_rest_enabled: enabled === true && batchRestEnabled !== false,
484
- rest_counter: 0,
485
- rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
486
- processed_count: 0,
487
- candidates_since_last_rest: 0,
488
- candidates_until_next_rest: nextBudgetRestInterval(),
489
- active_elapsed_ms: 0,
490
- last_active_at_ms: Number(readNow()) || 0,
491
- rest_count: 0,
492
- total_rest_ms: 0
493
- };
494
-
495
- function resetThreshold() {
496
- state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
497
- }
498
-
499
- function updateActiveElapsed() {
500
- const now = Number(readNow()) || 0;
501
- if (state.last_active_at_ms >= 0 && now >= state.last_active_at_ms) {
502
- state.active_elapsed_ms += now - state.last_active_at_ms;
503
- }
504
- state.last_active_at_ms = now;
505
- return now;
506
- }
507
-
508
- function getBudgetTargetMs() {
509
- if (!budgetProfile) return 0;
510
- const candidateTarget = state.processed_count * (budgetProfile.targetRestMs / budgetProfile.targetCandidateCount);
511
- const elapsedTarget = state.active_elapsed_ms * (budgetProfile.targetRestMs / budgetProfile.targetWindowMs);
512
- return Math.max(candidateTarget, elapsedTarget);
513
- }
514
-
515
- function chooseBudgetRestPause(debtMs) {
516
- const longRest = nextRandom() < budgetProfile.longRestProbability;
517
- const minMs = longRest ? budgetProfile.longRestMinMs : budgetProfile.shortRestMinMs;
518
- const maxMs = longRest ? budgetProfile.longRestMaxMs : budgetProfile.shortRestMaxMs;
519
- const scaleMin = longRest ? 0.75 : 0.38;
520
- const scaleMax = longRest ? 1.1 : 0.78;
521
- const desiredMs = debtMs * randomBetween(nextRandom, scaleMin, scaleMax);
522
- const randomizedMs = randomBetween(nextRandom, minMs, maxMs);
523
- const blendedMs = Math.max(minMs, Math.min(maxMs, (desiredMs + randomizedMs) / 2));
524
- const maxAllowedMs = Math.max(minMs, debtMs + budgetProfile.maxOverspendMs);
525
- return {
526
- pauseMs: Math.round(Math.min(blendedMs, maxAllowedMs)),
527
- restSize: longRest ? "long" : "short"
528
- };
529
- }
530
-
531
- async function takeBudgetBreakIfNeeded(sleeper) {
532
- state.processed_count += 1;
533
- state.candidates_since_last_rest += 1;
534
- state.candidates_until_next_rest -= 1;
535
- const debtMs = getBudgetTargetMs() - state.total_rest_ms;
536
- const intervalDue = state.candidates_until_next_rest <= 0;
537
- const forceDue = debtMs >= budgetProfile.forceDebtMs;
538
- if (!intervalDue && !forceDue) {
539
- return null;
540
- }
541
- if (debtMs < budgetProfile.minDebtToRestMs) {
542
- if (intervalDue) state.candidates_until_next_rest = nextBudgetRestInterval();
543
- return null;
544
- }
545
- const { pauseMs, restSize } = chooseBudgetRestPause(debtMs);
546
- await sleeper(pauseMs);
547
- const event = {
548
- kind: "random_rest",
549
- rest_level: normalizedRestLevel,
550
- rest_size: restSize,
551
- pause_ms: pauseMs,
552
- processed_since_last_rest: state.candidates_since_last_rest,
553
- rest_budget_debt_ms: Math.round(Math.max(0, debtMs))
554
- };
555
- state.candidates_since_last_rest = 0;
556
- state.candidates_until_next_rest = nextBudgetRestInterval();
557
- return event;
558
- }
559
-
560
- async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
561
- if (!state.enabled) {
562
- return {
563
- enabled: false,
564
- rested: false,
565
- rest_counter: state.rest_counter,
566
- rest_threshold: state.rest_threshold,
567
- events: []
568
- };
569
- }
570
- const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
571
- updateActiveElapsed();
572
- if (budgetProfile) {
573
- const budgetEvent = await takeBudgetBreakIfNeeded(sleeper);
574
- const pauseMs = budgetEvent?.pause_ms || 0;
575
- if (pauseMs > 0) {
576
- state.rest_count += 1;
577
- state.total_rest_ms += pauseMs;
578
- state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
579
- }
580
- return {
581
- enabled: true,
582
- rested: Boolean(budgetEvent),
583
- pause_ms: pauseMs,
584
- rest_level: normalizedRestLevel,
585
- rest_counter: state.rest_counter,
586
- rest_threshold: state.rest_threshold,
587
- processed_count: state.processed_count,
588
- candidates_until_next_rest: state.candidates_until_next_rest,
589
- active_elapsed_ms: state.active_elapsed_ms,
590
- rest_count: state.rest_count,
591
- total_rest_ms: state.total_rest_ms,
592
- events: budgetEvent ? [budgetEvent] : []
593
- };
594
- }
595
- state.rest_counter += 1;
596
- state.processed_count += 1;
597
- const events = [];
598
- if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
599
- const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
600
- await sleeper(pauseMs);
601
- events.push({ kind: "random_rest", rest_level: normalizedRestLevel, pause_ms: pauseMs });
602
- }
603
- if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
604
- const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
605
- await sleeper(pauseMs);
606
- events.push({
607
- kind: "batch_rest",
608
- rest_level: normalizedRestLevel,
609
- pause_ms: pauseMs,
610
- processed_since_last_batch_rest: state.rest_counter
611
- });
612
- state.rest_counter = 0;
613
- resetThreshold();
614
- }
615
- const pauseMs = events.reduce((sum, event) => sum + event.pause_ms, 0);
616
- if (pauseMs > 0) {
617
- state.rest_count += events.length;
618
- state.total_rest_ms += pauseMs;
619
- state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
620
- }
621
- return {
622
- enabled: true,
623
- rested: events.length > 0,
624
- pause_ms: pauseMs,
625
- rest_level: normalizedRestLevel,
626
- rest_counter: state.rest_counter,
627
- rest_threshold: state.rest_threshold,
628
- processed_count: state.processed_count,
629
- active_elapsed_ms: state.active_elapsed_ms,
630
- rest_count: state.rest_count,
631
- total_rest_ms: state.total_rest_ms,
632
- events
633
- };
634
- }
635
-
636
- return {
637
- takeBreakIfNeeded,
638
- getState() {
639
- return { ...state };
640
- }
641
- };
642
- }
643
-
644
- export function isBossLoginUrl(url) {
645
- return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
646
- }
647
-
648
- export function createBossLoginRequiredError({
649
- domain = "boss",
650
- currentUrl = "",
651
- targetUrl = "",
652
- loginUrl = BOSS_LOGIN_URL,
653
- loginDetection = null,
654
- chrome = null
655
- } = {}) {
656
- const error = new Error(`Boss login is required before starting the ${domain} run.`);
657
- error.code = "BOSS_LOGIN_REQUIRED";
658
- error.requires_login = true;
659
- error.current_url = currentUrl || null;
660
- error.target_url = targetUrl || null;
661
- error.login_url = loginUrl;
662
- error.login_detection = loginDetection || null;
663
- error.chrome = chrome || null;
664
- error.retryable = true;
665
- return error;
666
- }
667
-
668
- export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
669
- const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
670
- if (isBossLoginUrl(inspectedUrl)) {
671
- return {
672
- requires_login: true,
673
- reason: "url",
674
- current_url: inspectedUrl,
675
- matched_selectors: []
676
- };
677
- }
678
-
679
- let root = null;
680
- try {
681
- root = await getDocumentRoot(client, { depth: 1, pierce: true });
682
- } catch (error) {
683
- return {
684
- requires_login: false,
685
- reason: "dom_unavailable",
686
- current_url: inspectedUrl,
687
- error: error?.message || String(error || "")
688
- };
689
- }
690
-
691
- const matchedSelectors = [];
692
- for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
693
- const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
694
- if (nodeId) matchedSelectors.push(selector);
695
- }
696
-
697
- if (matchedSelectors.length === 0) {
698
- return {
699
- requires_login: false,
700
- reason: "no_login_dom",
701
- current_url: inspectedUrl,
702
- matched_selectors: []
703
- };
704
- }
705
-
706
- const html = await getOuterHTML(client, root.nodeId).catch(() => "");
707
- const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
708
- return {
709
- requires_login: looksLikeLogin,
710
- reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
711
- current_url: inspectedUrl,
712
- matched_selectors: matchedSelectors
713
- };
714
- }
715
-
716
- export function isChromeDebugUnavailableError(error) {
717
- return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
718
- }
719
-
720
- function pathExists(targetPath) {
721
- try {
722
- return Boolean(targetPath) && fs.existsSync(targetPath);
723
- } catch {
724
- return false;
725
- }
726
- }
727
-
728
- function ensureDir(targetPath) {
729
- fs.mkdirSync(targetPath, { recursive: true });
730
- }
731
-
732
- function isLocalChromeHost(host) {
733
- const normalized = String(host || "").trim().toLowerCase();
734
- return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
735
- }
736
-
737
- function getCodexHome() {
738
- return process.env.CODEX_HOME
739
- ? path.resolve(process.env.CODEX_HOME)
740
- : path.join(os.homedir(), ".codex");
741
- }
742
-
743
- function getDefaultChromeExecutableCandidates() {
744
- const candidates = [
745
- process.env.BOSS_MCP_CHROME_PATH,
746
- process.env.BOSS_RECOMMEND_CHROME_PATH
747
- ].filter(Boolean);
748
- if (process.platform === "win32") {
749
- candidates.push(
750
- path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
751
- path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
752
- path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
753
- );
754
- } else if (process.platform === "darwin") {
755
- candidates.push(
756
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
757
- path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
758
- "/Applications/Chromium.app/Contents/MacOS/Chromium"
759
- );
760
- } else {
761
- candidates.push(
762
- "/usr/bin/google-chrome",
763
- "/usr/bin/google-chrome-stable",
764
- "/usr/bin/chromium-browser",
765
- "/usr/bin/chromium",
766
- "/snap/bin/chromium"
767
- );
768
- }
769
- return Array.from(new Set(candidates.filter(Boolean)));
770
- }
771
-
772
- export function getChromeExecutable() {
773
- return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
774
- }
775
-
776
- export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
777
- const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
778
- ensureDir(sharedPath);
779
- return sharedPath;
780
- }
781
-
782
- function parseExtraChromeArgs(value = "") {
783
- return String(value || "")
784
- .split(/\s+/)
785
- .map((item) => item.trim())
786
- .filter(Boolean);
787
- }
788
-
789
- export function parseChromeCommandLineArgs(commandLineOrArgs = []) {
790
- if (Array.isArray(commandLineOrArgs)) {
791
- return commandLineOrArgs
792
- .map((item) => String(item || "").trim())
793
- .filter(Boolean);
794
- }
795
-
796
- const text = String(commandLineOrArgs || "").trim();
797
- if (!text) return [];
798
- const args = [];
799
- let current = "";
800
- let quote = null;
801
- for (const char of text) {
802
- if (quote) {
803
- if (char === quote) {
804
- quote = null;
805
- } else {
806
- current += char;
807
- }
808
- continue;
809
- }
810
- if (char === '"' || char === "'") {
811
- quote = char;
812
- continue;
813
- }
814
- if (/\s/.test(char)) {
815
- if (current) {
816
- args.push(current);
817
- current = "";
818
- }
819
- continue;
820
- }
821
- current += char;
822
- }
823
- if (current) args.push(current);
824
- return args;
825
- }
826
-
827
- function splitChromeFeatureList(value = "") {
828
- return String(value || "")
829
- .split(",")
830
- .map((item) => item.trim())
831
- .filter(Boolean);
832
- }
833
-
834
- function chromeFlagIsPresent(args, requiredFlag) {
835
- if (!requiredFlag) return true;
836
- const disableFeaturesPrefix = "--disable-features=";
837
- if (requiredFlag.startsWith(disableFeaturesPrefix)) {
838
- const requiredFeatures = splitChromeFeatureList(requiredFlag.slice(disableFeaturesPrefix.length));
839
- const disableFeatureArgs = args.filter((arg) => arg.startsWith(disableFeaturesPrefix));
840
- const lastDisableFeatureArg = disableFeatureArgs[disableFeatureArgs.length - 1] || "";
841
- const features = splitChromeFeatureList(lastDisableFeatureArg.slice(disableFeaturesPrefix.length));
842
- return requiredFeatures.every((feature) => features.includes(feature));
843
- }
844
- if (args.includes(requiredFlag)) return true;
845
- return false;
846
- }
847
-
848
- export function getMissingRequiredChromeFlags(
849
- commandLineOrArgs = [],
850
- requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS
851
- ) {
852
- const args = parseChromeCommandLineArgs(commandLineOrArgs);
853
- return requiredFlags.filter((flag) => !chromeFlagIsPresent(args, flag));
854
- }
855
-
856
- function normalizeChromeLaunchArgs(args = []) {
857
- const disableFeaturesPrefix = "--disable-features=";
858
- const result = [];
859
- const seen = new Set();
860
- const disabledFeatures = [];
861
- const disabledFeatureSet = new Set();
862
- let disabledFeatureIndex = -1;
863
-
864
- for (const rawArg of args) {
865
- const arg = String(rawArg || "").trim();
866
- if (!arg) continue;
867
- if (arg.startsWith(disableFeaturesPrefix)) {
868
- if (disabledFeatureIndex < 0) {
869
- disabledFeatureIndex = result.length;
870
- result.push(null);
871
- }
872
- for (const feature of splitChromeFeatureList(arg.slice(disableFeaturesPrefix.length))) {
873
- if (!disabledFeatureSet.has(feature)) {
874
- disabledFeatureSet.add(feature);
875
- disabledFeatures.push(feature);
876
- }
877
- }
878
- continue;
879
- }
880
- if (seen.has(arg)) continue;
881
- seen.add(arg);
882
- result.push(arg);
883
- }
884
-
885
- return result.map((arg) => (
886
- arg === null
887
- ? `${disableFeaturesPrefix}${disabledFeatures.join(",")}`
888
- : arg
889
- ));
890
- }
891
-
892
- export function buildBossChromeLaunchArgs({
893
- port = DEFAULT_CHROME_PORT,
894
- userDataDir = "",
895
- url = "about:blank",
896
- extraArgs = []
897
- } = {}) {
898
- const args = [
899
- `--remote-debugging-port=${port}`,
900
- `--user-data-dir=${userDataDir}`,
901
- "--no-first-run",
902
- "--no-default-browser-check",
903
- ...LID_CLOSED_SAFE_CHROME_ARGS,
904
- ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
905
- ...extraArgs,
906
- "--start-maximized",
907
- "--new-window",
908
- url
909
- ];
910
- return normalizeChromeLaunchArgs(args);
911
- }
912
-
913
- function execFileText(file, args = [], { timeoutMs = 5000, maxBuffer = 1024 * 1024 } = {}) {
914
- return new Promise((resolve) => {
915
- execFile(file, args, {
916
- timeout: timeoutMs,
917
- maxBuffer,
918
- windowsHide: true
919
- }, (error, stdout, stderr) => {
920
- resolve({
921
- ok: !error,
922
- stdout: String(stdout || ""),
923
- stderr: String(stderr || ""),
924
- error: error?.message || ""
925
- });
926
- });
927
- });
928
- }
929
-
930
- async function inspectChromeCommandLineViaCdp({
931
- host = DEFAULT_CHROME_HOST,
932
- port = DEFAULT_CHROME_PORT
933
- } = {}) {
934
- let client = null;
935
- try {
936
- client = await CDP({ host, port });
937
- const result = await client.Browser.getBrowserCommandLine();
938
- const args = parseChromeCommandLineArgs(result?.arguments || result?.commandLine || result?.command_line || []);
939
- if (args.length === 0) {
940
- return {
941
- ok: false,
942
- source: "cdp_browser_command_line",
943
- arguments: [],
944
- error: "Browser.getBrowserCommandLine returned no command-line arguments"
945
- };
946
- }
947
- return {
948
- ok: true,
949
- source: "cdp_browser_command_line",
950
- arguments: args
951
- };
952
- } catch (error) {
953
- return {
954
- ok: false,
955
- source: "cdp_browser_command_line",
956
- arguments: [],
957
- error: error?.message || String(error || "")
958
- };
959
- } finally {
960
- if (client) {
961
- await client.close().catch(() => {});
962
- }
963
- }
964
- }
965
-
966
- function parseWindowsProcessListJson(text = "") {
967
- const trimmed = String(text || "").trim();
968
- if (!trimmed) return [];
969
- const parsed = JSON.parse(trimmed);
970
- const items = Array.isArray(parsed) ? parsed : [parsed];
971
- return items
972
- .map((item) => ({
973
- pid: Number(item?.ProcessId),
974
- command_line: String(item?.CommandLine || "")
975
- }))
976
- .filter((item) => Number.isFinite(item.pid) && item.command_line);
977
- }
978
-
979
- function parsePosixProcessList(text = "", port = DEFAULT_CHROME_PORT) {
980
- const portPattern = new RegExp(`--remote-debugging-port(?:=|\\s+)${port}(?=\\s|$)`);
981
- return String(text || "")
982
- .split(/\r?\n/)
983
- .map((line) => {
984
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
985
- return match
986
- ? { pid: Number(match[1]), command_line: match[2] }
987
- : null;
988
- })
989
- .filter((item) => item && Number.isFinite(item.pid) && portPattern.test(item.command_line));
990
- }
991
-
992
- function summarizeChromeProcesses(processes = []) {
993
- return processes
994
- .map((item) => ({
995
- pid: item.pid,
996
- command_line_length: String(item.command_line || "").length
997
- }))
998
- .filter((item) => Number.isFinite(item.pid));
999
- }
1000
-
1001
- async function inspectChromeCommandLineViaProcessList({
1002
- port = DEFAULT_CHROME_PORT
1003
- } = {}) {
1004
- const portText = String(port);
1005
- let processes = [];
1006
- let raw = null;
1007
-
1008
- if (process.platform === "win32") {
1009
- const portPattern = `--remote-debugging-port(=|\\s+)${portText}(\\s|$)`;
1010
- const script = [
1011
- "$items = Get-CimInstance Win32_Process",
1012
- `| Where-Object { $_.CommandLine -and $_.CommandLine -match '${portPattern}' }`,
1013
- "| Select-Object ProcessId,CommandLine;",
1014
- "$items | ConvertTo-Json -Compress"
1015
- ].join(" ");
1016
- raw = await execFileText("powershell.exe", [
1017
- "-NoProfile",
1018
- "-ExecutionPolicy",
1019
- "Bypass",
1020
- "-Command",
1021
- script
1022
- ], { timeoutMs: 6000 });
1023
- if (!raw.ok) {
1024
- return {
1025
- ok: false,
1026
- source: "process_list",
1027
- arguments: [],
1028
- processes: [],
1029
- error: raw.error || raw.stderr || "Failed to inspect Windows process list"
1030
- };
1031
- }
1032
- try {
1033
- processes = parseWindowsProcessListJson(raw.stdout);
1034
- } catch (error) {
1035
- return {
1036
- ok: false,
1037
- source: "process_list",
1038
- arguments: [],
1039
- processes: [],
1040
- error: `Failed to parse Windows process list: ${error?.message || error}`
1041
- };
1042
- }
1043
- } else {
1044
- const psArgs = process.platform === "darwin"
1045
- ? ["-axo", "pid=,command="]
1046
- : ["-eo", "pid=,args="];
1047
- raw = await execFileText("ps", psArgs, { timeoutMs: 6000 });
1048
- if (!raw.ok) {
1049
- return {
1050
- ok: false,
1051
- source: "process_list",
1052
- arguments: [],
1053
- processes: [],
1054
- error: raw.error || raw.stderr || "Failed to inspect process list"
1055
- };
1056
- }
1057
- processes = parsePosixProcessList(raw.stdout, port);
1058
- }
1059
-
1060
- if (processes.length === 0) {
1061
- return {
1062
- ok: false,
1063
- source: "process_list",
1064
- arguments: [],
1065
- processes: [],
1066
- error: `No local process was found for --remote-debugging-port=${port}`
1067
- };
1068
- }
1069
- const primary = processes[0];
1070
- return {
1071
- ok: true,
1072
- source: "process_list",
1073
- arguments: parseChromeCommandLineArgs(primary.command_line),
1074
- process: {
1075
- pid: primary.pid,
1076
- command_line_length: primary.command_line.length
1077
- },
1078
- processes: summarizeChromeProcesses(processes)
1079
- };
1080
- }
1081
-
1082
- export async function inspectChromeDebugCommandLine({
1083
- host = DEFAULT_CHROME_HOST,
1084
- port = DEFAULT_CHROME_PORT,
1085
- _deps = {}
1086
- } = {}) {
1087
- const inspectViaCdp = _deps.inspectChromeCommandLineViaCdpImpl || inspectChromeCommandLineViaCdp;
1088
- const inspectViaProcess = _deps.inspectChromeCommandLineViaProcessListImpl || inspectChromeCommandLineViaProcessList;
1089
- const cdpResult = await inspectViaCdp({ host, port });
1090
- if (cdpResult?.ok && cdpResult.arguments?.length) {
1091
- return cdpResult;
1092
- }
1093
- if (!isLocalChromeHost(host)) {
1094
- return {
1095
- ok: false,
1096
- source: cdpResult?.source || "unknown",
1097
- arguments: [],
1098
- error: cdpResult?.error || `Cannot inspect process list for non-local Chrome debug host: ${host}`
1099
- };
1100
- }
1101
- const processResult = await inspectViaProcess({ port });
1102
- if (processResult?.ok && processResult.arguments?.length) {
1103
- return {
1104
- ...processResult,
1105
- cdp_error: cdpResult?.error || null
1106
- };
1107
- }
1108
- return {
1109
- ok: false,
1110
- source: processResult?.source || cdpResult?.source || "unknown",
1111
- arguments: [],
1112
- processes: processResult?.processes || [],
1113
- error: processResult?.error || cdpResult?.error || "Chrome command line could not be inspected"
1114
- };
1115
- }
1116
-
1117
- async function waitForChromeDebugPortClosed({
1118
- host = DEFAULT_CHROME_HOST,
1119
- port = DEFAULT_CHROME_PORT,
1120
- timeoutMs = 6000,
1121
- intervalMs = 300,
1122
- listChromeTargetsImpl = listChromeTargets
1123
- } = {}) {
1124
- const started = Date.now();
1125
- let lastError = null;
1126
- let lastTargetCount = 0;
1127
- while (Date.now() - started <= timeoutMs) {
1128
- try {
1129
- const targets = await listChromeTargetsImpl({ host, port });
1130
- lastTargetCount = Array.isArray(targets) ? targets.length : 0;
1131
- } catch (error) {
1132
- if (isChromeDebugUnavailableError(error)) {
1133
- return {
1134
- ok: true,
1135
- elapsed_ms: Date.now() - started
1136
- };
1137
- }
1138
- lastError = error;
1139
- }
1140
- await sleep(intervalMs);
1141
- }
1142
- return {
1143
- ok: false,
1144
- elapsed_ms: Date.now() - started,
1145
- target_count: lastTargetCount,
1146
- error: lastError?.message || `Chrome debug port ${port} is still reachable`
1147
- };
1148
- }
1149
-
1150
- export async function closeChromeDebugInstance({
1151
- host = DEFAULT_CHROME_HOST,
1152
- port = DEFAULT_CHROME_PORT,
1153
- processes = [],
1154
- timeoutMs = 8000,
1155
- intervalMs = 300,
1156
- _deps = {}
1157
- } = {}) {
1158
- if (!isLocalChromeHost(host)) {
1159
- return {
1160
- ok: false,
1161
- method: "none",
1162
- error: `Refusing to close non-local Chrome debug host: ${host}`
1163
- };
1164
- }
1165
-
1166
- const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
1167
- const waitClosed = _deps.waitForChromeDebugPortClosedImpl || waitForChromeDebugPortClosed;
1168
- let browserCloseAttempted = false;
1169
- let browserCloseError = null;
1170
- try {
1171
- let client = null;
1172
- try {
1173
- client = await CDP({ host, port });
1174
- if (typeof client?.Browser?.close !== "function") {
1175
- throw new Error("Browser.close is not available");
1176
- }
1177
- browserCloseAttempted = true;
1178
- await client.Browser.close();
1179
- } finally {
1180
- if (client) await client.close().catch(() => {});
1181
- }
1182
- } catch (error) {
1183
- browserCloseError = error?.message || String(error || "");
1184
- }
1185
-
1186
- let closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
1187
- if (closed.ok) {
1188
- return {
1189
- ok: true,
1190
- method: browserCloseAttempted ? "Browser.close" : "port_already_closed",
1191
- elapsed_ms: closed.elapsed_ms,
1192
- browser_close_error: browserCloseError
1193
- };
1194
- }
1195
-
1196
- const pids = Array.from(new Set((processes || [])
1197
- .map((item) => Number(item?.pid))
1198
- .filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid)));
1199
- const killedPids = [];
1200
- const processErrors = [];
1201
- for (const pid of pids) {
1202
- try {
1203
- process.kill(pid);
1204
- killedPids.push(pid);
1205
- } catch (error) {
1206
- processErrors.push({
1207
- pid,
1208
- error: error?.message || String(error || "")
1209
- });
1210
- }
1211
- }
1212
-
1213
- if (killedPids.length > 0) {
1214
- closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
1215
- if (closed.ok) {
1216
- return {
1217
- ok: true,
1218
- method: browserCloseAttempted ? "Browser.close+process.kill" : "process.kill",
1219
- elapsed_ms: closed.elapsed_ms,
1220
- killed_pids: killedPids,
1221
- browser_close_error: browserCloseError,
1222
- process_errors: processErrors
1223
- };
1224
- }
1225
- }
1226
-
1227
- return {
1228
- ok: false,
1229
- method: browserCloseAttempted && killedPids.length > 0
1230
- ? "Browser.close+process.kill"
1231
- : browserCloseAttempted
1232
- ? "Browser.close"
1233
- : killedPids.length > 0
1234
- ? "process.kill"
1235
- : "none",
1236
- killed_pids: killedPids,
1237
- browser_close_error: browserCloseError,
1238
- process_errors: processErrors,
1239
- wait: closed,
1240
- error: closed.error || browserCloseError || "Failed to close Chrome debug instance"
1241
- };
1242
- }
1243
-
1244
- function summarizeRelaunch(result = {}, reason = "") {
1245
- return {
1246
- reason,
1247
- launched: Boolean(result?.launched),
1248
- chrome_path: result?.chrome_path || null,
1249
- user_data_dir: result?.user_data_dir || null,
1250
- launch_args: Array.isArray(result?.launch_args) ? result.launch_args : [],
1251
- readiness: result?.readiness || null
1252
- };
1253
- }
1254
-
1255
- function createChromeGuardError(message, code, chromeGuard) {
1256
- const error = new Error(message);
1257
- error.code = code;
1258
- error.chrome_guard = chromeGuard;
1259
- return error;
1260
- }
1261
-
1262
- export async function waitForChromeDebugPort({
1263
- host = DEFAULT_CHROME_HOST,
1264
- port = DEFAULT_CHROME_PORT,
1265
- timeoutMs = 8000,
1266
- intervalMs = 300
1267
- } = {}) {
1268
- const started = Date.now();
1269
- let lastError = null;
1270
- while (Date.now() - started <= timeoutMs) {
1271
- try {
1272
- const targets = await listChromeTargets({ host, port });
1273
- return {
1274
- ok: true,
1275
- elapsed_ms: Date.now() - started,
1276
- targets
1277
- };
1278
- } catch (error) {
1279
- lastError = error;
1280
- await sleep(intervalMs);
1281
- }
1282
- }
1283
- return {
1284
- ok: false,
1285
- elapsed_ms: Date.now() - started,
1286
- error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
1287
- };
1288
- }
1289
-
1290
- export async function launchChromeDebugInstance({
1291
- host = DEFAULT_CHROME_HOST,
1292
- port = DEFAULT_CHROME_PORT,
1293
- url = "about:blank",
1294
- slowLive = false,
1295
- userDataDir = ""
1296
- } = {}) {
1297
- if (!isLocalChromeHost(host)) {
1298
- throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
1299
- }
1300
- const chromePath = getChromeExecutable();
1301
- if (!chromePath) {
1302
- throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
1303
- }
1304
- const resolvedUserDataDir = userDataDir || getBossChromeUserDataDir(port);
1305
- ensureDir(resolvedUserDataDir);
1306
- const args = buildBossChromeLaunchArgs({ port, userDataDir: resolvedUserDataDir, url });
1307
- const child = spawn(chromePath, args, {
1308
- detached: true,
1309
- stdio: "ignore",
1310
- windowsHide: false
1311
- });
1312
- child.unref();
1313
- const readiness = await waitForChromeDebugPort({
1314
- host,
1315
- port,
1316
- timeoutMs: slowLive ? 30000 : 12000,
1317
- intervalMs: slowLive ? 700 : 300
1318
- });
1319
- if (!readiness.ok) {
1320
- throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
1321
- }
1322
- return {
1323
- launched: true,
1324
- chrome_path: chromePath,
1325
- user_data_dir: resolvedUserDataDir,
1326
- launch_args: args,
1327
- port,
1328
- url,
1329
- readiness: {
1330
- elapsed_ms: readiness.elapsed_ms,
1331
- target_count: readiness.targets.length
1332
- }
1333
- };
1334
- }
1335
-
1336
- export async function ensureChromeDebugPort({
1337
- host = DEFAULT_CHROME_HOST,
1338
- port = DEFAULT_CHROME_PORT,
1339
- url = "about:blank",
1340
- slowLive = false,
1341
- launchIfMissing = true,
1342
- userDataDir = "",
1343
- enforceRequiredFlags = true,
1344
- requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS,
1345
- _deps = {}
1346
- } = {}) {
1347
- const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
1348
- const inspectCommandLineImpl = _deps.inspectChromeDebugCommandLineImpl || inspectChromeDebugCommandLine;
1349
- const closeChromeDebugInstanceImpl = _deps.closeChromeDebugInstanceImpl || closeChromeDebugInstance;
1350
- const launchChromeDebugInstanceImpl = _deps.launchChromeDebugInstanceImpl || launchChromeDebugInstance;
1351
- const required = Array.from(new Set((requiredFlags || []).filter(Boolean)));
1352
- const baseGuard = {
1353
- guard_checked: Boolean(enforceRequiredFlags),
1354
- required_flags: required,
1355
- missing_flags: [],
1356
- required_flags_ok: !enforceRequiredFlags,
1357
- replaced: false,
1358
- close_method: null,
1359
- relaunch: null,
1360
- host,
1361
- port
1362
- };
1363
-
1364
- try {
1365
- const targets = await listChromeTargetsImpl({ host, port });
1366
- if (!enforceRequiredFlags) {
1367
- return {
1368
- launched: false,
1369
- reused: true,
1370
- port,
1371
- target_count: targets.length,
1372
- ...baseGuard
1373
- };
1374
- }
1375
-
1376
- const commandLine = await inspectCommandLineImpl({ host, port, _deps });
1377
- const missingFlags = commandLine?.ok
1378
- ? getMissingRequiredChromeFlags(commandLine.arguments, required)
1379
- : required.slice();
1380
- const commandLineEvidence = {
1381
- command_line_source: commandLine?.source || "unknown",
1382
- command_line_error: commandLine?.ok ? null : (commandLine?.error || "Chrome command line could not be inspected"),
1383
- command_line_args_count: Array.isArray(commandLine?.arguments) ? commandLine.arguments.length : 0,
1384
- inspected_process: commandLine?.process || null,
1385
- inspected_processes: commandLine?.processes || []
1386
- };
1387
- if (missingFlags.length === 0) {
1388
- return {
1389
- launched: false,
1390
- reused: true,
1391
- port,
1392
- target_count: targets.length,
1393
- ...baseGuard,
1394
- required_flags_ok: true,
1395
- ...commandLineEvidence
1396
- };
1397
- }
1398
-
1399
- const guard = {
1400
- ...baseGuard,
1401
- required_flags_ok: false,
1402
- missing_flags: missingFlags,
1403
- target_count: targets.length,
1404
- ...commandLineEvidence
1405
- };
1406
- if (!isLocalChromeHost(host)) {
1407
- throw createChromeGuardError(
1408
- `Chrome debug host ${host}:${port} is missing required Chrome flags and is not local, so it will not be auto-closed.`,
1409
- "CHROME_REQUIRED_FLAGS_MISSING_NON_LOCAL",
1410
- guard
1411
- );
1412
- }
1413
-
1414
- const closeResult = await closeChromeDebugInstanceImpl({
1415
- host,
1416
- port,
1417
- processes: commandLine?.processes || [],
1418
- _deps
1419
- });
1420
- if (!closeResult?.ok) {
1421
- throw createChromeGuardError(
1422
- `Chrome debug instance on port ${port} is missing required flags and could not be closed: ${closeResult?.error || "unknown close failure"}`,
1423
- "CHROME_REQUIRED_FLAGS_REPLACE_FAILED",
1424
- {
1425
- ...guard,
1426
- close_method: closeResult?.method || null,
1427
- close_result: closeResult || null
1428
- }
1429
- );
1430
- }
1431
-
1432
- try {
1433
- const relaunch = await launchChromeDebugInstanceImpl({
1434
- host,
1435
- port,
1436
- url,
1437
- slowLive,
1438
- userDataDir
1439
- });
1440
- return {
1441
- ...relaunch,
1442
- reused: false,
1443
- ...guard,
1444
- required_flags_ok: true,
1445
- replaced: true,
1446
- close_method: closeResult.method || null,
1447
- close_result: closeResult,
1448
- relaunch: summarizeRelaunch(relaunch, "missing_required_flags")
1449
- };
1450
- } catch (error) {
1451
- throw createChromeGuardError(
1452
- `Chrome debug instance on port ${port} was closed for missing flags, but relaunch failed: ${error?.message || error}`,
1453
- "CHROME_REQUIRED_FLAGS_RELAUNCH_FAILED",
1454
- {
1455
- ...guard,
1456
- close_method: closeResult.method || null,
1457
- close_result: closeResult,
1458
- relaunch: {
1459
- reason: "missing_required_flags",
1460
- launched: false,
1461
- error: error?.message || String(error || "")
1462
- }
1463
- }
1464
- );
1465
- }
1466
- } catch (error) {
1467
- if (error?.chrome_guard) {
1468
- throw error;
1469
- }
1470
- if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
1471
- throw error;
1472
- }
1473
- try {
1474
- const relaunch = await launchChromeDebugInstanceImpl({
1475
- host,
1476
- port,
1477
- url,
1478
- slowLive,
1479
- userDataDir
1480
- });
1481
- return {
1482
- ...baseGuard,
1483
- ...relaunch,
1484
- reused: false,
1485
- required_flags_ok: true,
1486
- relaunch: summarizeRelaunch(relaunch, "port_unreachable")
1487
- };
1488
- } catch (launchError) {
1489
- throw createChromeGuardError(
1490
- `Chrome debug port ${port} was unreachable and Chrome relaunch failed: ${launchError?.message || launchError}`,
1491
- "CHROME_RELAUNCH_FAILED",
1492
- {
1493
- ...baseGuard,
1494
- required_flags_ok: false,
1495
- relaunch: {
1496
- reason: "port_unreachable",
1497
- launched: false,
1498
- error: launchError?.message || String(launchError || "")
1499
- }
1500
- }
1501
- );
1502
- }
1503
- }
1504
- }
1505
-
1506
- export async function openChromeTarget({
1507
- host = DEFAULT_CHROME_HOST,
1508
- port = DEFAULT_CHROME_PORT,
1509
- url
1510
- } = {}) {
1511
- const encodedUrl = encodeURIComponent(url || "about:blank");
1512
- const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
1513
- const methods = ["PUT", "GET"];
1514
- let lastError = null;
1515
- for (const method of methods) {
1516
- try {
1517
- const response = await fetch(endpoint, { method });
1518
- if (response.ok) {
1519
- let payload = null;
1520
- try {
1521
- payload = await response.json();
1522
- } catch {}
1523
- return {
1524
- ok: true,
1525
- method,
1526
- target_id: payload?.id || null,
1527
- url: payload?.url || url || null
1528
- };
1529
- }
1530
- lastError = new Error(`DevTools /json/new returned ${response.status}`);
1531
- } catch (error) {
1532
- lastError = error;
1533
- }
1534
- }
1535
- return {
1536
- ok: false,
1537
- error: lastError?.message || "Failed to open Chrome target"
1538
- };
1539
- }
1540
-
1541
- export async function connectToChromeTargetOrOpen({
1542
- host = DEFAULT_CHROME_HOST,
1543
- port = DEFAULT_CHROME_PORT,
1544
- targetUrlIncludes,
1545
- targetPredicate,
1546
- fallbackTargetPredicate,
1547
- targetUrl,
1548
- allowNavigate = true,
1549
- slowLive = false,
1550
- launchIfMissing = true,
1551
- _deps = {}
1552
- } = {}) {
1553
- const ensureChromeDebugPortImpl = _deps.ensureChromeDebugPortImpl || ensureChromeDebugPort;
1554
- const connectToChromeTargetImpl = _deps.connectToChromeTargetImpl || connectToChromeTarget;
1555
- const openChromeTargetImpl = _deps.openChromeTargetImpl || openChromeTarget;
1556
- let chrome = null;
1557
- if (targetUrl) {
1558
- chrome = await ensureChromeDebugPortImpl({
1559
- host,
1560
- port,
1561
- url: targetUrl,
1562
- slowLive,
1563
- launchIfMissing: allowNavigate && launchIfMissing
1564
- });
1565
- }
1566
-
1567
- try {
1568
- const session = await connectToChromeTargetImpl({
1569
- host,
1570
- port,
1571
- targetUrlIncludes,
1572
- targetPredicate
1573
- });
1574
- return {
1575
- ...session,
1576
- chrome: {
1577
- ...(chrome || { launched: false, reused: true, port }),
1578
- target_created: false
1579
- }
1580
- };
1581
- } catch (primaryError) {
1582
- if (!allowNavigate) throw primaryError;
1583
-
1584
- if (typeof fallbackTargetPredicate === "function") {
1585
- try {
1586
- const session = await connectToChromeTargetImpl({
1587
- host,
1588
- port,
1589
- targetPredicate: fallbackTargetPredicate
1590
- });
1591
- return {
1592
- ...session,
1593
- chrome: {
1594
- ...(chrome || { launched: false, reused: true, port }),
1595
- target_created: false,
1596
- fallback_target: true
1597
- }
1598
- };
1599
- } catch {}
1600
- }
1601
-
1602
- let openAttempt = null;
1603
- if (targetUrl) {
1604
- openAttempt = await openChromeTargetImpl({ host, port, url: targetUrl });
1605
- if (openAttempt.ok) {
1606
- const session = await connectToChromeTargetImpl({
1607
- host,
1608
- port,
1609
- targetPredicate: (target) => (
1610
- (openAttempt.target_id && target?.id === openAttempt.target_id)
1611
- || String(target?.url || "").includes(targetUrlIncludes || targetUrl)
1612
- || (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
1613
- )
1614
- });
1615
- return {
1616
- ...session,
1617
- chrome: {
1618
- ...(chrome || { launched: false, reused: true, port }),
1619
- target_created: true,
1620
- open_attempt: openAttempt
1621
- }
1622
- };
1623
- }
1624
- }
1625
-
1626
- const session = await connectToChromeTargetImpl({
1627
- host,
1628
- port,
1629
- targetPredicate: (target) => target?.type === "page"
1630
- });
1631
- return {
1632
- ...session,
1633
- chrome: {
1634
- ...(chrome || { launched: false, reused: true, port }),
1635
- target_created: false,
1636
- open_attempt: openAttempt,
1637
- fallback_any_page: true
1638
- }
1639
- };
1640
- }
1641
- }
1642
-
1643
- export function isClosedCdpTransportError(error) {
1644
- return CDP_CLOSED_TRANSPORT_PATTERN.test(String(error?.message || error || ""));
1645
- }
1646
-
1647
- function cloneCdpParams(params = {}) {
1648
- if (!params || typeof params !== "object" || typeof params === "function") return params;
1649
- try {
1650
- return JSON.parse(JSON.stringify(params));
1651
- } catch {
1652
- return { ...params };
1653
- }
1654
- }
1655
-
1656
- function shouldReplayCdpSetupCall(domain, method) {
1657
- return method === "enable"
1658
- || (domain === "Network" && method === "setCacheDisabled")
1659
- || (domain === "Page" && method === "bringToFront");
1660
- }
1661
-
1662
- export function createGuardedCdpClient(client, { methodLog = [], reconnect = null } = {}) {
1663
- let currentClient = client;
1664
- let reconnectInFlight = null;
1665
- const setupCalls = [];
1666
- const eventSubscriptions = [];
1667
-
1668
- async function replaySessionSetup(nextClient) {
1669
- for (const call of setupCalls) {
1670
- const fn = nextClient?.[call.domain]?.[call.method];
1671
- if (typeof fn === "function") {
1672
- await fn.call(nextClient[call.domain], cloneCdpParams(call.params));
1673
- }
1674
- }
1675
- for (const subscription of eventSubscriptions) {
1676
- const fn = nextClient?.[subscription.domain]?.[subscription.event];
1677
- if (typeof fn === "function") {
1678
- fn.call(nextClient[subscription.domain], subscription.listener);
1679
- }
1680
- }
1681
- }
1682
-
1683
- async function reconnectClient() {
1684
- if (typeof reconnect !== "function") return null;
1685
- if (!reconnectInFlight) {
1686
- reconnectInFlight = Promise.resolve()
1687
- .then(() => reconnect())
1688
- .then(async (nextClient) => {
1689
- if (!nextClient) throw new Error("CDP reconnect returned no client");
1690
- currentClient = nextClient;
1691
- await replaySessionSetup(nextClient);
1692
- return nextClient;
1693
- })
1694
- .finally(() => {
1695
- reconnectInFlight = null;
1696
- });
1697
- }
1698
- return reconnectInFlight;
1699
- }
1700
-
1701
- async function invokeWithReconnect({
1702
- methodNameForLog,
1703
- invoke,
1704
- retryable = true
1705
- }) {
1706
- recordMethod(methodLog, methodNameForLog);
1707
- try {
1708
- return await invoke(currentClient);
1709
- } catch (error) {
1710
- if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
1711
- throw error;
1712
- }
1713
- await reconnectClient();
1714
- recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
1715
- return invoke(currentClient);
1716
- }
1717
- }
1718
-
1719
- return new Proxy({}, {
1720
- get(_target, property, receiver) {
1721
- if (property === "send") {
1722
- return async (method, params = {}) => {
1723
- if (isForbiddenMethod(method)) {
1724
- throw new Error(`Forbidden CDP method blocked: ${method}`);
1725
- }
1726
- return invokeWithReconnect({
1727
- methodNameForLog: method,
1728
- invoke: (activeClient) => activeClient.send(method, params)
1729
- });
1730
- };
1731
- }
1732
-
1733
- if (property === "close") {
1734
- return async () => currentClient?.close?.();
1735
- }
1736
-
1737
- if (property === "__rawClient") return currentClient;
1738
-
1739
- const value = Reflect.get(currentClient, property, receiver);
1740
- if (!value || typeof value !== "object") return value;
1741
-
1742
- return new Proxy({}, {
1743
- get(_domainTarget, method, domainReceiver) {
1744
- const domainTarget = Reflect.get(currentClient, property, receiver);
1745
- const domainValue = Reflect.get(domainTarget, method, domainReceiver);
1746
- if (typeof domainValue !== "function") return domainValue;
1747
-
1748
- return (params = {}) => {
1749
- const fullMethod = methodName(property, method);
1750
- if (isForbiddenMethod(fullMethod)) {
1751
- throw new Error(`Forbidden CDP method blocked: ${fullMethod}`);
1752
- }
1753
- if (typeof params === "function") {
1754
- eventSubscriptions.push({
1755
- domain: property,
1756
- event: method,
1757
- listener: params
1758
- });
1759
- recordMethod(methodLog, fullMethod);
1760
- return domainValue.call(domainTarget, params);
1761
- }
1762
- if (shouldReplayCdpSetupCall(property, method)) {
1763
- setupCalls.push({
1764
- domain: property,
1765
- method,
1766
- params: cloneCdpParams(params)
1767
- });
1768
- }
1769
- return invokeWithReconnect({
1770
- methodNameForLog: fullMethod,
1771
- invoke: (activeClient) => {
1772
- const activeDomain = activeClient?.[property];
1773
- const activeMethod = activeDomain?.[method];
1774
- if (typeof activeMethod !== "function") {
1775
- throw new Error(`CDP method is unavailable after reconnect: ${fullMethod}`);
1776
- }
1777
- return activeMethod.call(activeDomain, params);
1778
- }
1779
- });
1780
- };
1781
- }
1782
- });
1783
- }
1784
- });
1785
- }
1786
-
1787
- export async function listChromeTargets({
1788
- host = DEFAULT_CHROME_HOST,
1789
- port = DEFAULT_CHROME_PORT
1790
- } = {}) {
1791
- return CDP.List({ host, port });
1792
- }
1793
-
1794
- export async function connectToChromeTarget({
1795
- host = DEFAULT_CHROME_HOST,
1796
- port = DEFAULT_CHROME_PORT,
1797
- targetUrlIncludes,
1798
- targetPredicate
1799
- } = {}) {
1800
- const targets = await listChromeTargets({ host, port });
1801
- const matcher = normalizeTargetMatcher({ targetUrlIncludes, targetPredicate });
1802
- const target = targets.find(matcher);
1803
- if (!target) {
1804
- const urls = targets.map((item) => item.url).filter(Boolean).join("\n");
1805
- throw new Error(`No matching Chrome target found on ${host}:${port}.\nAvailable targets:\n${urls}`);
1806
- }
1807
-
1808
- let rawClient = await CDP({ host, port, target });
1809
- let activeTarget = target;
1810
- const methodLog = [];
1811
- const client = createGuardedCdpClient(rawClient, {
1812
- methodLog,
1813
- reconnect: async () => {
1814
- const latestTargets = await listChromeTargets({ host, port });
1815
- const nextTarget = activeTarget?.id
1816
- ? latestTargets.find((item) => item?.id === activeTarget.id)
1817
- : latestTargets.find(matcher);
1818
- if (!nextTarget) {
1819
- const urls = latestTargets.map((item) => item.url).filter(Boolean).join("\n");
1820
- throw new Error(`No matching Chrome target found while reconnecting to ${host}:${port}.\nAvailable targets:\n${urls}`);
1821
- }
1822
- try {
1823
- await rawClient.close();
1824
- } catch {}
1825
- rawClient = await CDP({ host, port, target: nextTarget });
1826
- activeTarget = nextTarget;
1827
- return rawClient;
1828
- }
1829
- });
1830
-
1831
- return {
1832
- client,
1833
- get rawClient() {
1834
- return rawClient;
1835
- },
1836
- get target() {
1837
- return activeTarget;
1838
- },
1839
- methodLog,
1840
- async close() {
1841
- await rawClient.close();
1842
- }
1843
- };
1844
- }
1845
-
1846
- export async function assertRuntimeEvaluateBlocked(client) {
1847
- try {
1848
- await client.Runtime.evaluate({ expression: "1" });
1849
- } catch (error) {
1850
- if (/Forbidden CDP method blocked: Runtime\.evaluate/.test(String(error?.message || ""))) {
1851
- return { blocked: true, message: error.message };
1852
- }
1853
- throw error;
1854
- }
1855
- throw new Error("Runtime.evaluate was not blocked by the CDP guard");
1856
- }
1857
-
1858
- export async function enableDomains(client, domains = ["Page", "DOM", "Input"]) {
1859
- for (const domain of domains) {
1860
- if (!ALLOWED_CDP_DOMAINS.has(domain)) {
1861
- throw new Error(`CDP domain is not allowed by the CDP-only contract: ${domain}`);
1862
- }
1863
- if (typeof client?.[domain]?.enable === "function") {
1864
- await client[domain].enable();
1865
- }
1866
- }
1867
- }
1868
-
1869
- export async function bringPageToFront(client) {
1870
- if (typeof client?.Page?.bringToFront === "function") {
1871
- await client.Page.bringToFront();
1872
- }
1873
- }
1874
-
1875
- export async function getPageFrameTree(client) {
1876
- const result = await client.Page.getFrameTree();
1877
- return result.frameTree || null;
1878
- }
1879
-
1880
- export async function getMainFrame(client) {
1881
- const frameTree = await getPageFrameTree(client);
1882
- return frameTree?.frame || null;
1883
- }
1884
-
1885
- export async function getMainFrameUrl(client) {
1886
- const frame = await getMainFrame(client);
1887
- return frame?.url || "";
1888
- }
1889
-
1890
- export async function waitForMainFrameUrl(client, predicate, {
1891
- timeoutMs = 10000,
1892
- intervalMs = 250
1893
- } = {}) {
1894
- const started = Date.now();
1895
- let lastUrl = "";
1896
- while (Date.now() - started <= timeoutMs) {
1897
- lastUrl = await getMainFrameUrl(client);
1898
- if (predicate(lastUrl)) {
1899
- return {
1900
- ok: true,
1901
- elapsed_ms: Date.now() - started,
1902
- url: lastUrl
1903
- };
1904
- }
1905
- await sleep(intervalMs);
1906
- }
1907
- return {
1908
- ok: false,
1909
- elapsed_ms: Date.now() - started,
1910
- url: lastUrl
1911
- };
1912
- }
1913
-
1914
- export async function getDocumentRoot(client, { depth = 1, pierce = true } = {}) {
1915
- const result = await client.DOM.getDocument({ depth, pierce });
1916
- return result.root;
1917
- }
1918
-
1919
- export async function querySelector(client, nodeId, selector) {
1920
- const result = await client.DOM.querySelector({ nodeId, selector });
1921
- return result.nodeId || 0;
1922
- }
1923
-
1924
- export async function querySelectorAll(client, nodeId, selector) {
1925
- const result = await client.DOM.querySelectorAll({ nodeId, selector });
1926
- return result.nodeIds || [];
1927
- }
1928
-
1929
- export async function findFirstNode(client, rootNodeId, selectors = []) {
1930
- for (const selector of selectors) {
1931
- const nodeId = await querySelector(client, rootNodeId, selector);
1932
- if (nodeId) return { selector, nodeId };
1933
- }
1934
- return null;
1935
- }
1936
-
1937
- export async function describeNode(client, nodeId, { depth = 1, pierce = true } = {}) {
1938
- const result = await client.DOM.describeNode({ nodeId, depth, pierce });
1939
- return result.node;
1940
- }
1941
-
1942
- export async function getFrameDocumentNodeId(client, iframeNodeId) {
1943
- const node = await describeNode(client, iframeNodeId, { depth: 1, pierce: true });
1944
- const documentNodeId = node?.contentDocument?.nodeId;
1945
- if (!documentNodeId) {
1946
- throw new Error(`Node ${iframeNodeId} does not expose a contentDocument node`);
1947
- }
1948
- return documentNodeId;
1949
- }
1950
-
1951
- export async function findIframeDocument(client, rootNodeId, selectors = []) {
1952
- const iframe = await findFirstNode(client, rootNodeId, selectors);
1953
- if (!iframe) return null;
1954
- const documentNodeId = await getFrameDocumentNodeId(client, iframe.nodeId);
1955
- return { ...iframe, documentNodeId };
1956
- }
1957
-
1958
- export async function getAttributesMap(client, nodeId) {
1959
- const result = await client.DOM.getAttributes({ nodeId });
1960
- const attributes = {};
1961
- const raw = result.attributes || [];
1962
- for (let index = 0; index < raw.length; index += 2) {
1963
- attributes[raw[index]] = raw[index + 1] || "";
1964
- }
1965
- return attributes;
1966
- }
1967
-
1968
- export async function getOuterHTML(client, nodeId) {
1969
- const result = await client.DOM.getOuterHTML({ nodeId });
1970
- return result.outerHTML || "";
1971
- }
1972
-
1973
- export async function getNodeBox(client, nodeId) {
1974
- let result;
1975
- try {
1976
- result = await client.DOM.getBoxModel({ nodeId });
1977
- } catch (error) {
1978
- const wrapped = new Error(error?.message || String(error));
1979
- wrapped.name = error?.name || "Error";
1980
- wrapped.node_id = nodeId;
1981
- wrapped.cdp_method = "DOM.getBoxModel";
1982
- wrapped.original_stack = error?.stack || "";
1983
- wrapped.stack = `${new Error(`getNodeBox failed for nodeId=${nodeId}`).stack || wrapped.stack}\nCaused by: ${error?.stack || error}`;
1984
- throw wrapped;
1985
- }
1986
- const model = result.model;
1987
- const quad = model.border?.length ? model.border : model.content;
1988
- const xs = [quad[0], quad[2], quad[4], quad[6]];
1989
- const ys = [quad[1], quad[3], quad[5], quad[7]];
1990
- const minX = Math.min(...xs);
1991
- const maxX = Math.max(...xs);
1992
- const minY = Math.min(...ys);
1993
- const maxY = Math.max(...ys);
1994
- return {
1995
- model,
1996
- center: {
1997
- x: (minX + maxX) / 2,
1998
- y: (minY + maxY) / 2
1999
- },
2000
- rect: {
2001
- x: minX,
2002
- y: minY,
2003
- width: maxX - minX,
2004
- height: maxY - minY
2005
- }
2006
- };
2007
- }
2008
-
2009
- export async function simulateHumanClick(client, targetX, targetY, {
2010
- button = "left",
2011
- clickCount = 1,
2012
- delayMs = 80,
2013
- random = Math.random,
2014
- sleepFn = sleep,
2015
- moveSteps = 18,
2016
- moveJitterPx = 3,
2017
- hoverJitterPx = 5,
2018
- moveDelayMinMs = 5,
2019
- moveDelayMaxMs = 23,
2020
- hoverDelayMinMs = 10,
2021
- hoverDelayMaxMs = 30,
2022
- prePressBaseMs = 260,
2023
- prePressVarianceMs = 80,
2024
- holdVarianceMs = 30,
2025
- startPoint = null
2026
- } = {}) {
2027
- const target = normalizePoint({ x: targetX, y: targetY });
2028
- if (!target) throw new Error("simulateHumanClick requires finite target coordinates");
2029
- const nextRandom = normalizeRandom(random);
2030
- const interactionConfig = getHumanInteractionConfig(client) || {};
2031
- const start = normalizePoint(startPoint)
2032
- || normalizePoint(interactionConfig.lastMousePoint)
2033
- || {
2034
- x: Math.max(0, target.x + randomBetween(nextRandom, -140, 140)),
2035
- y: Math.max(0, target.y + randomBetween(nextRandom, -90, 90))
2036
- };
2037
- const path = generateBezierPath(start, target, {
2038
- steps: moveSteps,
2039
- random: nextRandom
2040
- });
2041
- const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
2042
- const moveDelayMin = Math.min(moveDelayMinMs, moveDelayMaxMs);
2043
- const moveDelayMax = Math.max(moveDelayMinMs, moveDelayMaxMs);
2044
- const hoverDelayMin = Math.min(hoverDelayMinMs, hoverDelayMaxMs);
2045
- const hoverDelayMax = Math.max(hoverDelayMinMs, hoverDelayMaxMs);
2046
- for (const point of path) {
2047
- await client.Input.dispatchMouseEvent({
2048
- type: "mouseMoved",
2049
- x: Math.round(point.x + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
2050
- y: Math.round(point.y + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
2051
- button: "none"
2052
- });
2053
- const pauseMs = Math.round(randomBetween(nextRandom, moveDelayMin, moveDelayMax));
2054
- if (pauseMs > 0) await sleeper(pauseMs);
2055
- }
2056
- const hoverSteps = randomIntegerBetween(nextRandom, 3, 6);
2057
- for (let index = 0; index < hoverSteps; index += 1) {
2058
- await client.Input.dispatchMouseEvent({
2059
- type: "mouseMoved",
2060
- x: Math.round(target.x + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
2061
- y: Math.round(target.y + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
2062
- button: "none"
2063
- });
2064
- const pauseMs = Math.round(randomBetween(nextRandom, hoverDelayMin, hoverDelayMax));
2065
- if (pauseMs > 0) await sleeper(pauseMs);
2066
- }
2067
- const prePressMs = humanDelay(prePressBaseMs, prePressVarianceMs, {
2068
- minMs: 0,
2069
- maxMs: Math.max(prePressBaseMs + prePressVarianceMs * 4, prePressBaseMs),
2070
- random: nextRandom
2071
- });
2072
- if (prePressMs > 0) await sleeper(prePressMs);
2073
- await client.Input.dispatchMouseEvent({ type: "mousePressed", x: target.x, y: target.y, button, clickCount });
2074
- const holdMs = humanDelay(delayMs, holdVarianceMs, {
2075
- minMs: 0,
2076
- maxMs: Math.max(delayMs + holdVarianceMs * 4, delayMs),
2077
- random: nextRandom
2078
- });
2079
- if (holdMs > 0) await sleeper(holdMs);
2080
- await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: target.x, y: target.y, button, clickCount });
2081
- const latestConfig = getHumanInteractionConfig(client);
2082
- if (latestConfig) latestConfig.lastMousePoint = target;
2083
- return {
2084
- mode: "human",
2085
- path_points: path.length,
2086
- hover_steps: hoverSteps,
2087
- pre_press_ms: prePressMs,
2088
- hold_ms: holdMs
2089
- };
2090
- }
2091
-
2092
- export function resolveHumanClickPointForBox(box, {
2093
- enabled = true,
2094
- safeClickPointEnabled = true,
2095
- random = Math.random,
2096
- safeClickMinWidth = 44,
2097
- safeClickMinHeight = 28,
2098
- safeClickInsetRatio = 0.22,
2099
- safeClickMinInsetPx = 4,
2100
- safeClickMaxInsetPx = 18
2101
- } = {}) {
2102
- const center = normalizePoint(box?.center);
2103
- if (!center) throw new Error("resolveHumanClickPointForBox requires a box center");
2104
- const rect = box?.rect || {};
2105
- const width = Number(rect.width);
2106
- const height = Number(rect.height);
2107
- const originX = Number(rect.x);
2108
- const originY = Number(rect.y);
2109
- if (
2110
- enabled !== true
2111
- || safeClickPointEnabled === false
2112
- || !Number.isFinite(width)
2113
- || !Number.isFinite(height)
2114
- || !Number.isFinite(originX)
2115
- || !Number.isFinite(originY)
2116
- || width < Math.max(1, Number(safeClickMinWidth) || 44)
2117
- || height < Math.max(1, Number(safeClickMinHeight) || 28)
2118
- ) {
2119
- return {
2120
- x: center.x,
2121
- y: center.y,
2122
- mode: "center",
2123
- reason: "small_or_disabled"
2124
- };
2125
- }
2126
-
2127
- const nextRandom = normalizeRandom(random);
2128
- const insetRatio = clampNumber(safeClickInsetRatio, 0.05, 0.45);
2129
- const minInset = Math.max(0, Number(safeClickMinInsetPx) || 0);
2130
- const maxInset = Math.max(minInset, Number(safeClickMaxInsetPx) || minInset);
2131
- const insetX = Math.min(width / 2 - 1, Math.max(minInset, Math.min(maxInset, width * insetRatio)));
2132
- const insetY = Math.min(height / 2 - 1, Math.max(minInset, Math.min(maxInset, height * insetRatio)));
2133
- const usableWidth = Math.max(0, width - insetX * 2);
2134
- const usableHeight = Math.max(0, height - insetY * 2);
2135
- if (usableWidth <= 0 || usableHeight <= 0) {
2136
- return {
2137
- x: center.x,
2138
- y: center.y,
2139
- mode: "center",
2140
- reason: "insufficient_safe_area"
2141
- };
2142
- }
2143
- return {
2144
- x: originX + insetX + nextRandom() * usableWidth,
2145
- y: originY + insetY + nextRandom() * usableHeight,
2146
- mode: "safe_inset",
2147
- inset_x: insetX,
2148
- inset_y: insetY
2149
- };
2150
- }
2151
-
2152
- export async function clickPoint(client, x, y, {
2153
- button = "left",
2154
- clickCount = 1,
2155
- delayMs = 80,
2156
- humanRestEnabled = null,
2157
- humanInteraction = null
2158
- } = {}) {
2159
- const configured = getHumanInteractionConfig(client);
2160
- const mergedHumanInteraction = {
2161
- ...(configured || {}),
2162
- ...(humanInteraction || {})
2163
- };
2164
- const humanEnabled = humanRestEnabled === true
2165
- || humanInteraction?.enabled === true
2166
- || (humanRestEnabled !== false && configured?.enabled === true);
2167
- if (humanEnabled && mergedHumanInteraction.clickMovementEnabled !== false) {
2168
- return simulateHumanClick(client, x, y, {
2169
- ...mergedHumanInteraction,
2170
- button,
2171
- clickCount,
2172
- delayMs
2173
- });
2174
- }
2175
- await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
2176
- await client.Input.dispatchMouseEvent({ type: "mousePressed", x, y, button, clickCount });
2177
- if (delayMs > 0) await sleep(delayMs);
2178
- await client.Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button, clickCount });
2179
- return {
2180
- mode: "direct"
2181
- };
2182
- }
2183
-
2184
- export async function scrollNodeIntoView(client, nodeId) {
2185
- try {
2186
- await client.DOM.scrollIntoViewIfNeeded({ nodeId });
2187
- } catch (error) {
2188
- const wrapped = new Error(error?.message || String(error));
2189
- wrapped.name = error?.name || "Error";
2190
- wrapped.node_id = nodeId;
2191
- wrapped.cdp_method = "DOM.scrollIntoViewIfNeeded";
2192
- wrapped.original_stack = error?.stack || "";
2193
- wrapped.stack = `${new Error(`scrollNodeIntoView failed for nodeId=${nodeId}`).stack || wrapped.stack}\nCaused by: ${error?.stack || error}`;
2194
- throw wrapped;
2195
- }
2196
- }
2197
-
2198
- export async function clickNodeCenter(client, nodeId, {
2199
- scrollIntoView = false,
2200
- ...clickOptions
2201
- } = {}) {
2202
- if (scrollIntoView) {
2203
- await scrollNodeIntoView(client, nodeId);
2204
- await sleep(150);
2205
- }
2206
- const box = await getNodeBox(client, nodeId);
2207
- const configured = getHumanInteractionConfig(client);
2208
- const mergedHumanInteraction = {
2209
- ...(configured || {}),
2210
- ...(clickOptions.humanInteraction || {})
2211
- };
2212
- const humanClickPointEnabled = (
2213
- clickOptions.humanRestEnabled === true
2214
- || clickOptions.humanInteraction?.enabled === true
2215
- || (clickOptions.humanRestEnabled !== false && configured?.enabled === true)
2216
- ) && mergedHumanInteraction.safeClickPointEnabled !== false;
2217
- const clickPointTarget = humanClickPointEnabled
2218
- ? resolveHumanClickPointForBox(box, mergedHumanInteraction)
2219
- : { ...box.center, mode: "center" };
2220
- const clickResult = await clickPoint(client, clickPointTarget.x, clickPointTarget.y, clickOptions);
2221
- return {
2222
- ...box,
2223
- click_target: clickPointTarget,
2224
- click_result: clickResult
2225
- };
2226
- }
2227
-
2228
- export async function pressKey(client, key, {
2229
- code = key,
2230
- windowsVirtualKeyCode,
2231
- nativeVirtualKeyCode = windowsVirtualKeyCode,
2232
- text = "",
2233
- modifiers = 0
2234
- } = {}) {
2235
- await client.Input.dispatchKeyEvent({
2236
- type: "keyDown",
2237
- key,
2238
- code,
2239
- windowsVirtualKeyCode,
2240
- nativeVirtualKeyCode,
2241
- text,
2242
- modifiers
2243
- });
2244
- await client.Input.dispatchKeyEvent({
2245
- type: "keyUp",
2246
- key,
2247
- code,
2248
- windowsVirtualKeyCode,
2249
- nativeVirtualKeyCode,
2250
- modifiers
2251
- });
2252
- }
2253
-
2254
- export function chunkHumanText(text, {
2255
- random = Math.random,
2256
- minLength = 1,
2257
- maxLength = 5
2258
- } = {}) {
2259
- const chars = Array.from(String(text || ""));
2260
- const min = Math.max(1, Math.floor(Number(minLength) || 1));
2261
- const max = Math.max(min, Math.floor(Number(maxLength) || min));
2262
- const nextRandom = normalizeRandom(random);
2263
- const chunks = [];
2264
- let index = 0;
2265
- while (index < chars.length) {
2266
- const remaining = chars.length - index;
2267
- const size = Math.min(remaining, randomIntegerBetween(nextRandom, min, max));
2268
- chunks.push(chars.slice(index, index + size).join(""));
2269
- index += size;
2270
- }
2271
- return chunks;
2272
- }
2273
-
2274
- export async function insertText(client, text, {
2275
- humanTextEntryEnabled = null,
2276
- humanInteraction = null
2277
- } = {}) {
2278
- const value = String(text || "");
2279
- const configured = getHumanInteractionConfig(client);
2280
- const mergedHumanInteraction = {
2281
- ...(configured || {}),
2282
- ...(humanInteraction || {})
2283
- };
2284
- const textEntryEnabled = humanTextEntryEnabled === true
2285
- || humanInteraction?.textEntryEnabled === true
2286
- || (humanTextEntryEnabled !== false
2287
- && configured?.enabled === true
2288
- && configured?.textEntryEnabled !== false);
2289
- if (!textEntryEnabled || value.length <= 1) {
2290
- await client.Input.insertText({ text: value });
2291
- return {
2292
- mode: "direct",
2293
- chunk_count: value ? 1 : 0
2294
- };
2295
- }
2296
- const chunks = chunkHumanText(value, {
2297
- random: mergedHumanInteraction.random,
2298
- minLength: mergedHumanInteraction.textChunkMinLength,
2299
- maxLength: mergedHumanInteraction.textChunkMaxLength
2300
- });
2301
- const sleeper = typeof mergedHumanInteraction.sleepFn === "function"
2302
- ? mergedHumanInteraction.sleepFn
2303
- : sleep;
2304
- for (let index = 0; index < chunks.length; index += 1) {
2305
- await client.Input.insertText({ text: chunks[index] });
2306
- if (index < chunks.length - 1) {
2307
- const pauseMs = humanDelay(
2308
- mergedHumanInteraction.textChunkDelayBaseMs,
2309
- mergedHumanInteraction.textChunkDelayVarianceMs,
2310
- {
2311
- minMs: 0,
2312
- maxMs: Math.max(
2313
- mergedHumanInteraction.textChunkDelayBaseMs + mergedHumanInteraction.textChunkDelayVarianceMs * 4,
2314
- mergedHumanInteraction.textChunkDelayBaseMs
2315
- ),
2316
- random: mergedHumanInteraction.random
2317
- }
2318
- );
2319
- if (pauseMs > 0) await sleeper(pauseMs);
2320
- }
2321
- }
2322
- return {
2323
- mode: "chunked",
2324
- chunk_count: chunks.length,
2325
- chunks
2326
- };
2327
- }
2328
-
2329
- export async function selectAllFocusedText(client) {
2330
- await pressKey(client, "a", {
2331
- code: "KeyA",
2332
- windowsVirtualKeyCode: 65,
2333
- nativeVirtualKeyCode: 65,
2334
- modifiers: 2
2335
- });
2336
- }
2337
-
2338
- export async function clearFocusedInput(client) {
2339
- await selectAllFocusedText(client);
2340
- await pressKey(client, "Backspace", {
2341
- code: "Backspace",
2342
- windowsVirtualKeyCode: 8,
2343
- nativeVirtualKeyCode: 8
2344
- });
2345
- }
2346
-
2347
- export async function waitForSelector(client, nodeId, selector, {
2348
- timeoutMs = 5000,
2349
- intervalMs = 150
2350
- } = {}) {
2351
- const started = Date.now();
2352
- while (Date.now() - started <= timeoutMs) {
2353
- const foundNodeId = await querySelector(client, nodeId, selector);
2354
- if (foundNodeId) return foundNodeId;
2355
- await sleep(intervalMs);
2356
- }
2357
- return 0;
2358
- }
2359
-
2360
- export async function countSelectors(client, nodeId, selectors = {}) {
2361
- const counts = {};
2362
- for (const [name, selector] of Object.entries(selectors)) {
2363
- counts[name] = (await querySelectorAll(client, nodeId, selector)).length;
2364
- }
2365
- return counts;
2366
- }
2367
-
2368
- export async function getAccessibilityTree(client, options = {}) {
2369
- return client.Accessibility.getFullAXTree(options);
2370
- }
2371
-
2372
- export async function sleep(ms) {
2373
- await new Promise((resolve) => setTimeout(resolve, ms));
2374
- }
324
+ const canonicalMethod = String(methodName || "").replace(/:retry_after_reconnect$/, "");
325
+ const [domain] = canonicalMethod.split(".");
326
+ return FORBIDDEN_CDP_DOMAINS.has(domain) || FORBIDDEN_CDP_METHODS.has(canonicalMethod);
327
+ }
328
+
329
+ function methodName(domain, method) {
330
+ return `${String(domain)}.${String(method)}`;
331
+ }
332
+
333
+ function recordMethod(methodLog, method) {
334
+ if (Array.isArray(methodLog)) {
335
+ methodLog.push({ method, at: nowIso() });
336
+ }
337
+ }
338
+
339
+ export function assertNoForbiddenCdpCalls(methodLog = []) {
340
+ const forbidden = methodLog.filter((entry) => isForbiddenMethod(entry?.method));
341
+ if (forbidden.length > 0) {
342
+ const methods = forbidden.map((entry) => entry.method).join(", ");
343
+ throw new Error(`Forbidden CDP methods were used: ${methods}`);
344
+ }
345
+ }
346
+
347
+ export function humanDelay(baseMs, varianceMs, {
348
+ minMs = 100,
349
+ maxMs = 60000,
350
+ random = Math.random
351
+ } = {}) {
352
+ const nextRandom = normalizeRandom(random);
353
+ const base = Math.max(0, Number(baseMs) || 0);
354
+ const variance = Math.max(0, Number(varianceMs) || 0);
355
+ const lower = Math.max(0, Number(minMs) || 0);
356
+ const upper = Math.max(lower, Number(maxMs) || lower);
357
+ if (variance <= 0) return Math.round(clampNumber(base, lower, upper));
358
+ const u1 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
359
+ const u2 = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, nextRandom()));
360
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
361
+ return Math.round(clampNumber(base + z * variance, lower, upper));
362
+ }
363
+
364
+ export function generateBezierPath(start, end, {
365
+ steps = 18,
366
+ random = Math.random,
367
+ controlJitterX = 100,
368
+ controlJitterY = 60
369
+ } = {}) {
370
+ const startPoint = normalizePoint(start);
371
+ const endPoint = normalizePoint(end);
372
+ if (!startPoint || !endPoint) {
373
+ throw new Error("generateBezierPath requires finite start and end points");
374
+ }
375
+ const nextRandom = normalizeRandom(random);
376
+ const safeSteps = Math.max(1, Math.floor(Number(steps) || 18));
377
+ const midX = (startPoint.x + endPoint.x) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterX) || 0);
378
+ const midY = (startPoint.y + endPoint.y) / 2 + (nextRandom() - 0.5) * Math.max(0, Number(controlJitterY) || 0);
379
+ const path = [];
380
+ for (let index = 0; index <= safeSteps; index += 1) {
381
+ const t = index / safeSteps;
382
+ const inverse = 1 - t;
383
+ path.push({
384
+ x: inverse * inverse * startPoint.x + 2 * inverse * t * midX + t * t * endPoint.x,
385
+ y: inverse * inverse * startPoint.y + 2 * inverse * t * midY + t * t * endPoint.y
386
+ });
387
+ }
388
+ return path;
389
+ }
390
+
391
+ export function configureHumanInteraction(client, {
392
+ enabled = false,
393
+ clickMovementEnabled = null,
394
+ textEntryEnabled = null,
395
+ safeClickPointEnabled = null,
396
+ actionCooldownEnabled = null,
397
+ random = Math.random,
398
+ sleepFn = null,
399
+ moveSteps = 18,
400
+ moveJitterPx = 3,
401
+ hoverJitterPx = 5,
402
+ moveDelayMinMs = 5,
403
+ moveDelayMaxMs = 23,
404
+ hoverDelayMinMs = 10,
405
+ hoverDelayMaxMs = 30,
406
+ prePressBaseMs = 260,
407
+ prePressVarianceMs = 80,
408
+ holdVarianceMs = 30,
409
+ safeClickMinWidth = 44,
410
+ safeClickMinHeight = 28,
411
+ safeClickInsetRatio = 0.22,
412
+ safeClickMinInsetPx = 4,
413
+ safeClickMaxInsetPx = 18,
414
+ textChunkMinLength = 1,
415
+ textChunkMaxLength = 5,
416
+ textChunkDelayBaseMs = 55,
417
+ textChunkDelayVarianceMs = 30
418
+ } = {}) {
419
+ const previous = getHumanInteractionConfig(client);
420
+ const normalizedEnabled = enabled === true;
421
+ HUMAN_INTERACTION_CONFIG.set(client, {
422
+ enabled: normalizedEnabled,
423
+ clickMovementEnabled: normalizedEnabled && clickMovementEnabled !== false,
424
+ textEntryEnabled: normalizedEnabled && textEntryEnabled !== false,
425
+ safeClickPointEnabled: normalizedEnabled && safeClickPointEnabled !== false,
426
+ actionCooldownEnabled: normalizedEnabled && actionCooldownEnabled !== false,
427
+ random: normalizeRandom(random),
428
+ sleepFn: typeof sleepFn === "function" ? sleepFn : sleep,
429
+ moveSteps: Math.max(1, Math.floor(Number(moveSteps) || 18)),
430
+ moveJitterPx: Math.max(0, Number(moveJitterPx) || 0),
431
+ hoverJitterPx: Math.max(0, Number(hoverJitterPx) || 0),
432
+ moveDelayMinMs: Math.max(0, Number(moveDelayMinMs) || 0),
433
+ moveDelayMaxMs: Math.max(0, Number(moveDelayMaxMs) || 0),
434
+ hoverDelayMinMs: Math.max(0, Number(hoverDelayMinMs) || 0),
435
+ hoverDelayMaxMs: Math.max(0, Number(hoverDelayMaxMs) || 0),
436
+ prePressBaseMs: Math.max(0, Number(prePressBaseMs) || 0),
437
+ prePressVarianceMs: Math.max(0, Number(prePressVarianceMs) || 0),
438
+ holdVarianceMs: Math.max(0, Number(holdVarianceMs) || 0),
439
+ safeClickMinWidth: Math.max(1, Number(safeClickMinWidth) || 44),
440
+ safeClickMinHeight: Math.max(1, Number(safeClickMinHeight) || 28),
441
+ safeClickInsetRatio: clampNumber(safeClickInsetRatio, 0.05, 0.45),
442
+ safeClickMinInsetPx: Math.max(0, Number(safeClickMinInsetPx) || 0),
443
+ safeClickMaxInsetPx: Math.max(0, Number(safeClickMaxInsetPx) || 0),
444
+ textChunkMinLength: Math.max(1, Math.floor(Number(textChunkMinLength) || 1)),
445
+ textChunkMaxLength: Math.max(1, Math.floor(Number(textChunkMaxLength) || 5)),
446
+ textChunkDelayBaseMs: Math.max(0, Number(textChunkDelayBaseMs) || 0),
447
+ textChunkDelayVarianceMs: Math.max(0, Number(textChunkDelayVarianceMs) || 0),
448
+ lastMousePoint: previous?.lastMousePoint || null
449
+ });
450
+ return () => {
451
+ if (previous) {
452
+ HUMAN_INTERACTION_CONFIG.set(client, previous);
453
+ } else {
454
+ HUMAN_INTERACTION_CONFIG.delete(client);
455
+ }
456
+ };
457
+ }
458
+
459
+ export function createHumanRestController({
460
+ enabled = false,
461
+ shortRestEnabled = true,
462
+ batchRestEnabled = true,
463
+ random = Math.random,
464
+ nowFn = Date.now,
465
+ restLevel = DEFAULT_HUMAN_REST_LEVEL,
466
+ shortRestProbability = 0.08,
467
+ shortRestMinMs = 3000,
468
+ shortRestMaxMs = 7000,
469
+ perCandidateRestEnabled = false,
470
+ perCandidateRestMinMs = 0,
471
+ perCandidateRestMaxMs = 0,
472
+ batchThresholdBase = 25,
473
+ batchThresholdJitter = 8,
474
+ batchRestMinMs = 15000,
475
+ batchRestMaxMs = 30000
476
+ } = {}) {
477
+ const nextRandom = normalizeRandom(random);
478
+ const readNow = typeof nowFn === "function" ? nowFn : Date.now;
479
+ const normalizedRestLevel = normalizeHumanRestLevel(restLevel);
480
+ const perCandidateMinMs = Math.max(0, Number(perCandidateRestMinMs) || 0);
481
+ const perCandidateMaxMs = Math.max(perCandidateMinMs, Number(perCandidateRestMaxMs) || perCandidateMinMs);
482
+ const perCandidateEnabled = enabled === true && perCandidateRestEnabled === true && perCandidateMaxMs > 0;
483
+ const budgetProfile = !perCandidateEnabled && (shortRestEnabled !== false || batchRestEnabled !== false)
484
+ ? HUMAN_REST_LEVEL_PROFILES[normalizedRestLevel] || null
485
+ : null;
486
+ const nextBudgetRestInterval = () => budgetProfile
487
+ ? randomIntegerBetween(nextRandom, budgetProfile.intervalMin, budgetProfile.intervalMax)
488
+ : 0;
489
+ const state = {
490
+ enabled: enabled === true,
491
+ rest_level: normalizedRestLevel,
492
+ per_candidate_rest_enabled: perCandidateEnabled,
493
+ per_candidate_rest_min_ms: perCandidateMinMs,
494
+ per_candidate_rest_max_ms: perCandidateMaxMs,
495
+ short_rest_enabled: enabled === true && shortRestEnabled !== false,
496
+ batch_rest_enabled: enabled === true && batchRestEnabled !== false,
497
+ rest_counter: 0,
498
+ rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
499
+ processed_count: 0,
500
+ candidates_since_last_rest: 0,
501
+ candidates_until_next_rest: nextBudgetRestInterval(),
502
+ active_elapsed_ms: 0,
503
+ last_active_at_ms: Number(readNow()) || 0,
504
+ rest_count: 0,
505
+ total_rest_ms: 0
506
+ };
507
+
508
+ function resetThreshold() {
509
+ state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
510
+ }
511
+
512
+ function updateActiveElapsed() {
513
+ const now = Number(readNow()) || 0;
514
+ if (state.last_active_at_ms >= 0 && now >= state.last_active_at_ms) {
515
+ state.active_elapsed_ms += now - state.last_active_at_ms;
516
+ }
517
+ state.last_active_at_ms = now;
518
+ return now;
519
+ }
520
+
521
+ function getBudgetTargetMs() {
522
+ if (!budgetProfile) return 0;
523
+ const candidateTarget = state.processed_count * (budgetProfile.targetRestMs / budgetProfile.targetCandidateCount);
524
+ const elapsedTarget = state.active_elapsed_ms * (budgetProfile.targetRestMs / budgetProfile.targetWindowMs);
525
+ return Math.max(candidateTarget, elapsedTarget);
526
+ }
527
+
528
+ function chooseBudgetRestPause(debtMs) {
529
+ const longRest = nextRandom() < budgetProfile.longRestProbability;
530
+ const minMs = longRest ? budgetProfile.longRestMinMs : budgetProfile.shortRestMinMs;
531
+ const maxMs = longRest ? budgetProfile.longRestMaxMs : budgetProfile.shortRestMaxMs;
532
+ const scaleMin = longRest ? 0.75 : 0.38;
533
+ const scaleMax = longRest ? 1.1 : 0.78;
534
+ const desiredMs = debtMs * randomBetween(nextRandom, scaleMin, scaleMax);
535
+ const randomizedMs = randomBetween(nextRandom, minMs, maxMs);
536
+ const blendedMs = Math.max(minMs, Math.min(maxMs, (desiredMs + randomizedMs) / 2));
537
+ const maxAllowedMs = Math.max(minMs, debtMs + budgetProfile.maxOverspendMs);
538
+ return {
539
+ pauseMs: Math.round(Math.min(blendedMs, maxAllowedMs)),
540
+ restSize: longRest ? "long" : "short"
541
+ };
542
+ }
543
+
544
+ async function takeBudgetBreakIfNeeded(sleeper) {
545
+ state.processed_count += 1;
546
+ state.candidates_since_last_rest += 1;
547
+ state.candidates_until_next_rest -= 1;
548
+ const debtMs = getBudgetTargetMs() - state.total_rest_ms;
549
+ const intervalDue = state.candidates_until_next_rest <= 0;
550
+ const forceDue = debtMs >= budgetProfile.forceDebtMs;
551
+ if (!intervalDue && !forceDue) {
552
+ return null;
553
+ }
554
+ if (debtMs < budgetProfile.minDebtToRestMs) {
555
+ if (intervalDue) state.candidates_until_next_rest = nextBudgetRestInterval();
556
+ return null;
557
+ }
558
+ const { pauseMs, restSize } = chooseBudgetRestPause(debtMs);
559
+ await sleeper(pauseMs);
560
+ const event = {
561
+ kind: "random_rest",
562
+ rest_level: normalizedRestLevel,
563
+ rest_size: restSize,
564
+ pause_ms: pauseMs,
565
+ processed_since_last_rest: state.candidates_since_last_rest,
566
+ rest_budget_debt_ms: Math.round(Math.max(0, debtMs))
567
+ };
568
+ state.candidates_since_last_rest = 0;
569
+ state.candidates_until_next_rest = nextBudgetRestInterval();
570
+ return event;
571
+ }
572
+
573
+ async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
574
+ if (!state.enabled) {
575
+ return {
576
+ enabled: false,
577
+ rested: false,
578
+ rest_counter: state.rest_counter,
579
+ rest_threshold: state.rest_threshold,
580
+ events: []
581
+ };
582
+ }
583
+ const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
584
+ updateActiveElapsed();
585
+ if (state.per_candidate_rest_enabled) {
586
+ state.rest_counter += 1;
587
+ state.processed_count += 1;
588
+ state.candidates_since_last_rest += 1;
589
+ const pauseMs = Math.round(randomBetween(
590
+ nextRandom,
591
+ state.per_candidate_rest_min_ms,
592
+ state.per_candidate_rest_max_ms
593
+ ));
594
+ await sleeper(pauseMs);
595
+ state.rest_count += 1;
596
+ state.total_rest_ms += pauseMs;
597
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
598
+ const event = {
599
+ kind: "per_candidate_rest",
600
+ rest_level: normalizedRestLevel,
601
+ pause_ms: pauseMs,
602
+ processed_since_last_rest: state.candidates_since_last_rest
603
+ };
604
+ state.candidates_since_last_rest = 0;
605
+ return {
606
+ enabled: true,
607
+ rested: true,
608
+ pause_ms: pauseMs,
609
+ rest_level: normalizedRestLevel,
610
+ rest_counter: state.rest_counter,
611
+ rest_threshold: state.rest_threshold,
612
+ processed_count: state.processed_count,
613
+ active_elapsed_ms: state.active_elapsed_ms,
614
+ rest_count: state.rest_count,
615
+ total_rest_ms: state.total_rest_ms,
616
+ events: [event]
617
+ };
618
+ }
619
+ if (budgetProfile) {
620
+ const budgetEvent = await takeBudgetBreakIfNeeded(sleeper);
621
+ const pauseMs = budgetEvent?.pause_ms || 0;
622
+ if (pauseMs > 0) {
623
+ state.rest_count += 1;
624
+ state.total_rest_ms += pauseMs;
625
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
626
+ }
627
+ return {
628
+ enabled: true,
629
+ rested: Boolean(budgetEvent),
630
+ pause_ms: pauseMs,
631
+ rest_level: normalizedRestLevel,
632
+ rest_counter: state.rest_counter,
633
+ rest_threshold: state.rest_threshold,
634
+ processed_count: state.processed_count,
635
+ candidates_until_next_rest: state.candidates_until_next_rest,
636
+ active_elapsed_ms: state.active_elapsed_ms,
637
+ rest_count: state.rest_count,
638
+ total_rest_ms: state.total_rest_ms,
639
+ events: budgetEvent ? [budgetEvent] : []
640
+ };
641
+ }
642
+ state.rest_counter += 1;
643
+ state.processed_count += 1;
644
+ const events = [];
645
+ if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
646
+ const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
647
+ await sleeper(pauseMs);
648
+ events.push({ kind: "random_rest", rest_level: normalizedRestLevel, pause_ms: pauseMs });
649
+ }
650
+ if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
651
+ const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
652
+ await sleeper(pauseMs);
653
+ events.push({
654
+ kind: "batch_rest",
655
+ rest_level: normalizedRestLevel,
656
+ pause_ms: pauseMs,
657
+ processed_since_last_batch_rest: state.rest_counter
658
+ });
659
+ state.rest_counter = 0;
660
+ resetThreshold();
661
+ }
662
+ const pauseMs = events.reduce((sum, event) => sum + event.pause_ms, 0);
663
+ if (pauseMs > 0) {
664
+ state.rest_count += events.length;
665
+ state.total_rest_ms += pauseMs;
666
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
667
+ }
668
+ return {
669
+ enabled: true,
670
+ rested: events.length > 0,
671
+ pause_ms: pauseMs,
672
+ rest_level: normalizedRestLevel,
673
+ rest_counter: state.rest_counter,
674
+ rest_threshold: state.rest_threshold,
675
+ processed_count: state.processed_count,
676
+ active_elapsed_ms: state.active_elapsed_ms,
677
+ rest_count: state.rest_count,
678
+ total_rest_ms: state.total_rest_ms,
679
+ events
680
+ };
681
+ }
682
+
683
+ return {
684
+ takeBreakIfNeeded,
685
+ getState() {
686
+ return { ...state };
687
+ }
688
+ };
689
+ }
690
+
691
+ export function isBossLoginUrl(url) {
692
+ return BOSS_LOGIN_URL_PATTERN.test(String(url || ""));
693
+ }
694
+
695
+ export function createBossLoginRequiredError({
696
+ domain = "boss",
697
+ currentUrl = "",
698
+ targetUrl = "",
699
+ loginUrl = BOSS_LOGIN_URL,
700
+ loginDetection = null,
701
+ chrome = null
702
+ } = {}) {
703
+ const error = new Error(`Boss login is required before starting the ${domain} run.`);
704
+ error.code = "BOSS_LOGIN_REQUIRED";
705
+ error.requires_login = true;
706
+ error.current_url = currentUrl || null;
707
+ error.target_url = targetUrl || null;
708
+ error.login_url = loginUrl;
709
+ error.login_detection = loginDetection || null;
710
+ error.chrome = chrome || null;
711
+ error.retryable = true;
712
+ return error;
713
+ }
714
+
715
+ export async function detectBossLoginState(client, { currentUrl = "" } = {}) {
716
+ const inspectedUrl = currentUrl || await getMainFrameUrl(client).catch(() => "");
717
+ if (isBossLoginUrl(inspectedUrl)) {
718
+ return {
719
+ requires_login: true,
720
+ reason: "url",
721
+ current_url: inspectedUrl,
722
+ matched_selectors: []
723
+ };
724
+ }
725
+
726
+ let root = null;
727
+ try {
728
+ root = await getDocumentRoot(client, { depth: 1, pierce: true });
729
+ } catch (error) {
730
+ return {
731
+ requires_login: false,
732
+ reason: "dom_unavailable",
733
+ current_url: inspectedUrl,
734
+ error: error?.message || String(error || "")
735
+ };
736
+ }
737
+
738
+ const matchedSelectors = [];
739
+ for (const selector of BOSS_LOGIN_DOM_SELECTORS) {
740
+ const nodeId = await querySelector(client, root.nodeId, selector).catch(() => 0);
741
+ if (nodeId) matchedSelectors.push(selector);
742
+ }
743
+
744
+ if (matchedSelectors.length === 0) {
745
+ return {
746
+ requires_login: false,
747
+ reason: "no_login_dom",
748
+ current_url: inspectedUrl,
749
+ matched_selectors: []
750
+ };
751
+ }
752
+
753
+ const html = await getOuterHTML(client, root.nodeId).catch(() => "");
754
+ const looksLikeLogin = BOSS_LOGIN_TEXT_PATTERN.test(html);
755
+ return {
756
+ requires_login: looksLikeLogin,
757
+ reason: looksLikeLogin ? "dom" : "login_selector_without_login_text",
758
+ current_url: inspectedUrl,
759
+ matched_selectors: matchedSelectors
760
+ };
761
+ }
762
+
763
+ export function isChromeDebugUnavailableError(error) {
764
+ return CHROME_DEBUG_UNAVAILABLE_PATTERN.test(String(error?.message || error || ""));
765
+ }
766
+
767
+ function pathExists(targetPath) {
768
+ try {
769
+ return Boolean(targetPath) && fs.existsSync(targetPath);
770
+ } catch {
771
+ return false;
772
+ }
773
+ }
774
+
775
+ function ensureDir(targetPath) {
776
+ fs.mkdirSync(targetPath, { recursive: true });
777
+ }
778
+
779
+ function isLocalChromeHost(host) {
780
+ const normalized = String(host || "").trim().toLowerCase();
781
+ return !normalized || normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
782
+ }
783
+
784
+ function getCodexHome() {
785
+ return process.env.CODEX_HOME
786
+ ? path.resolve(process.env.CODEX_HOME)
787
+ : path.join(os.homedir(), ".codex");
788
+ }
789
+
790
+ function getDefaultChromeExecutableCandidates() {
791
+ const candidates = [
792
+ process.env.BOSS_MCP_CHROME_PATH,
793
+ process.env.BOSS_RECOMMEND_CHROME_PATH
794
+ ].filter(Boolean);
795
+ if (process.platform === "win32") {
796
+ candidates.push(
797
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
798
+ path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
799
+ path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
800
+ );
801
+ } else if (process.platform === "darwin") {
802
+ candidates.push(
803
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
804
+ path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
805
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
806
+ );
807
+ } else {
808
+ candidates.push(
809
+ "/usr/bin/google-chrome",
810
+ "/usr/bin/google-chrome-stable",
811
+ "/usr/bin/chromium-browser",
812
+ "/usr/bin/chromium",
813
+ "/snap/bin/chromium"
814
+ );
815
+ }
816
+ return Array.from(new Set(candidates.filter(Boolean)));
817
+ }
818
+
819
+ export function getChromeExecutable() {
820
+ return getDefaultChromeExecutableCandidates().find((candidate) => pathExists(candidate)) || null;
821
+ }
822
+
823
+ export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
824
+ const sharedPath = path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
825
+ ensureDir(sharedPath);
826
+ return sharedPath;
827
+ }
828
+
829
+ function parseExtraChromeArgs(value = "") {
830
+ return String(value || "")
831
+ .split(/\s+/)
832
+ .map((item) => item.trim())
833
+ .filter(Boolean);
834
+ }
835
+
836
+ export function parseChromeCommandLineArgs(commandLineOrArgs = []) {
837
+ if (Array.isArray(commandLineOrArgs)) {
838
+ return commandLineOrArgs
839
+ .map((item) => String(item || "").trim())
840
+ .filter(Boolean);
841
+ }
842
+
843
+ const text = String(commandLineOrArgs || "").trim();
844
+ if (!text) return [];
845
+ const args = [];
846
+ let current = "";
847
+ let quote = null;
848
+ for (const char of text) {
849
+ if (quote) {
850
+ if (char === quote) {
851
+ quote = null;
852
+ } else {
853
+ current += char;
854
+ }
855
+ continue;
856
+ }
857
+ if (char === '"' || char === "'") {
858
+ quote = char;
859
+ continue;
860
+ }
861
+ if (/\s/.test(char)) {
862
+ if (current) {
863
+ args.push(current);
864
+ current = "";
865
+ }
866
+ continue;
867
+ }
868
+ current += char;
869
+ }
870
+ if (current) args.push(current);
871
+ return args;
872
+ }
873
+
874
+ function splitChromeFeatureList(value = "") {
875
+ return String(value || "")
876
+ .split(",")
877
+ .map((item) => item.trim())
878
+ .filter(Boolean);
879
+ }
880
+
881
+ function chromeFlagIsPresent(args, requiredFlag) {
882
+ if (!requiredFlag) return true;
883
+ const disableFeaturesPrefix = "--disable-features=";
884
+ if (requiredFlag.startsWith(disableFeaturesPrefix)) {
885
+ const requiredFeatures = splitChromeFeatureList(requiredFlag.slice(disableFeaturesPrefix.length));
886
+ const disableFeatureArgs = args.filter((arg) => arg.startsWith(disableFeaturesPrefix));
887
+ const lastDisableFeatureArg = disableFeatureArgs[disableFeatureArgs.length - 1] || "";
888
+ const features = splitChromeFeatureList(lastDisableFeatureArg.slice(disableFeaturesPrefix.length));
889
+ return requiredFeatures.every((feature) => features.includes(feature));
890
+ }
891
+ if (args.includes(requiredFlag)) return true;
892
+ return false;
893
+ }
894
+
895
+ export function getMissingRequiredChromeFlags(
896
+ commandLineOrArgs = [],
897
+ requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS
898
+ ) {
899
+ const args = parseChromeCommandLineArgs(commandLineOrArgs);
900
+ return requiredFlags.filter((flag) => !chromeFlagIsPresent(args, flag));
901
+ }
902
+
903
+ function normalizeChromeLaunchArgs(args = []) {
904
+ const disableFeaturesPrefix = "--disable-features=";
905
+ const result = [];
906
+ const seen = new Set();
907
+ const disabledFeatures = [];
908
+ const disabledFeatureSet = new Set();
909
+ let disabledFeatureIndex = -1;
910
+
911
+ for (const rawArg of args) {
912
+ const arg = String(rawArg || "").trim();
913
+ if (!arg) continue;
914
+ if (arg.startsWith(disableFeaturesPrefix)) {
915
+ if (disabledFeatureIndex < 0) {
916
+ disabledFeatureIndex = result.length;
917
+ result.push(null);
918
+ }
919
+ for (const feature of splitChromeFeatureList(arg.slice(disableFeaturesPrefix.length))) {
920
+ if (!disabledFeatureSet.has(feature)) {
921
+ disabledFeatureSet.add(feature);
922
+ disabledFeatures.push(feature);
923
+ }
924
+ }
925
+ continue;
926
+ }
927
+ if (seen.has(arg)) continue;
928
+ seen.add(arg);
929
+ result.push(arg);
930
+ }
931
+
932
+ return result.map((arg) => (
933
+ arg === null
934
+ ? `${disableFeaturesPrefix}${disabledFeatures.join(",")}`
935
+ : arg
936
+ ));
937
+ }
938
+
939
+ export function buildBossChromeLaunchArgs({
940
+ port = DEFAULT_CHROME_PORT,
941
+ userDataDir = "",
942
+ url = "about:blank",
943
+ extraArgs = []
944
+ } = {}) {
945
+ const args = [
946
+ `--remote-debugging-port=${port}`,
947
+ `--user-data-dir=${userDataDir}`,
948
+ "--no-first-run",
949
+ "--no-default-browser-check",
950
+ ...LID_CLOSED_SAFE_CHROME_ARGS,
951
+ ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
952
+ ...extraArgs,
953
+ "--start-maximized",
954
+ "--new-window",
955
+ url
956
+ ];
957
+ return normalizeChromeLaunchArgs(args);
958
+ }
959
+
960
+ function execFileText(file, args = [], { timeoutMs = 5000, maxBuffer = 1024 * 1024 } = {}) {
961
+ return new Promise((resolve) => {
962
+ execFile(file, args, {
963
+ timeout: timeoutMs,
964
+ maxBuffer,
965
+ windowsHide: true
966
+ }, (error, stdout, stderr) => {
967
+ resolve({
968
+ ok: !error,
969
+ stdout: String(stdout || ""),
970
+ stderr: String(stderr || ""),
971
+ error: error?.message || ""
972
+ });
973
+ });
974
+ });
975
+ }
976
+
977
+ async function inspectChromeCommandLineViaCdp({
978
+ host = DEFAULT_CHROME_HOST,
979
+ port = DEFAULT_CHROME_PORT
980
+ } = {}) {
981
+ let client = null;
982
+ try {
983
+ client = await CDP({ host, port });
984
+ const result = await client.Browser.getBrowserCommandLine();
985
+ const args = parseChromeCommandLineArgs(result?.arguments || result?.commandLine || result?.command_line || []);
986
+ if (args.length === 0) {
987
+ return {
988
+ ok: false,
989
+ source: "cdp_browser_command_line",
990
+ arguments: [],
991
+ error: "Browser.getBrowserCommandLine returned no command-line arguments"
992
+ };
993
+ }
994
+ return {
995
+ ok: true,
996
+ source: "cdp_browser_command_line",
997
+ arguments: args
998
+ };
999
+ } catch (error) {
1000
+ return {
1001
+ ok: false,
1002
+ source: "cdp_browser_command_line",
1003
+ arguments: [],
1004
+ error: error?.message || String(error || "")
1005
+ };
1006
+ } finally {
1007
+ if (client) {
1008
+ await client.close().catch(() => {});
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ function parseWindowsProcessListJson(text = "") {
1014
+ const trimmed = String(text || "").trim();
1015
+ if (!trimmed) return [];
1016
+ const parsed = JSON.parse(trimmed);
1017
+ const items = Array.isArray(parsed) ? parsed : [parsed];
1018
+ return items
1019
+ .map((item) => ({
1020
+ pid: Number(item?.ProcessId),
1021
+ command_line: String(item?.CommandLine || "")
1022
+ }))
1023
+ .filter((item) => Number.isFinite(item.pid) && item.command_line);
1024
+ }
1025
+
1026
+ function parsePosixProcessList(text = "", port = DEFAULT_CHROME_PORT) {
1027
+ const portPattern = new RegExp(`--remote-debugging-port(?:=|\\s+)${port}(?=\\s|$)`);
1028
+ return String(text || "")
1029
+ .split(/\r?\n/)
1030
+ .map((line) => {
1031
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
1032
+ return match
1033
+ ? { pid: Number(match[1]), command_line: match[2] }
1034
+ : null;
1035
+ })
1036
+ .filter((item) => item && Number.isFinite(item.pid) && portPattern.test(item.command_line));
1037
+ }
1038
+
1039
+ function summarizeChromeProcesses(processes = []) {
1040
+ return processes
1041
+ .map((item) => ({
1042
+ pid: item.pid,
1043
+ command_line_length: String(item.command_line || "").length
1044
+ }))
1045
+ .filter((item) => Number.isFinite(item.pid));
1046
+ }
1047
+
1048
+ async function inspectChromeCommandLineViaProcessList({
1049
+ port = DEFAULT_CHROME_PORT
1050
+ } = {}) {
1051
+ const portText = String(port);
1052
+ let processes = [];
1053
+ let raw = null;
1054
+
1055
+ if (process.platform === "win32") {
1056
+ const portPattern = `--remote-debugging-port(=|\\s+)${portText}(\\s|$)`;
1057
+ const script = [
1058
+ "$items = Get-CimInstance Win32_Process",
1059
+ `| Where-Object { $_.CommandLine -and $_.CommandLine -match '${portPattern}' }`,
1060
+ "| Select-Object ProcessId,CommandLine;",
1061
+ "$items | ConvertTo-Json -Compress"
1062
+ ].join(" ");
1063
+ raw = await execFileText("powershell.exe", [
1064
+ "-NoProfile",
1065
+ "-ExecutionPolicy",
1066
+ "Bypass",
1067
+ "-Command",
1068
+ script
1069
+ ], { timeoutMs: 6000 });
1070
+ if (!raw.ok) {
1071
+ return {
1072
+ ok: false,
1073
+ source: "process_list",
1074
+ arguments: [],
1075
+ processes: [],
1076
+ error: raw.error || raw.stderr || "Failed to inspect Windows process list"
1077
+ };
1078
+ }
1079
+ try {
1080
+ processes = parseWindowsProcessListJson(raw.stdout);
1081
+ } catch (error) {
1082
+ return {
1083
+ ok: false,
1084
+ source: "process_list",
1085
+ arguments: [],
1086
+ processes: [],
1087
+ error: `Failed to parse Windows process list: ${error?.message || error}`
1088
+ };
1089
+ }
1090
+ } else {
1091
+ const psArgs = process.platform === "darwin"
1092
+ ? ["-axo", "pid=,command="]
1093
+ : ["-eo", "pid=,args="];
1094
+ raw = await execFileText("ps", psArgs, { timeoutMs: 6000 });
1095
+ if (!raw.ok) {
1096
+ return {
1097
+ ok: false,
1098
+ source: "process_list",
1099
+ arguments: [],
1100
+ processes: [],
1101
+ error: raw.error || raw.stderr || "Failed to inspect process list"
1102
+ };
1103
+ }
1104
+ processes = parsePosixProcessList(raw.stdout, port);
1105
+ }
1106
+
1107
+ if (processes.length === 0) {
1108
+ return {
1109
+ ok: false,
1110
+ source: "process_list",
1111
+ arguments: [],
1112
+ processes: [],
1113
+ error: `No local process was found for --remote-debugging-port=${port}`
1114
+ };
1115
+ }
1116
+ const primary = processes[0];
1117
+ return {
1118
+ ok: true,
1119
+ source: "process_list",
1120
+ arguments: parseChromeCommandLineArgs(primary.command_line),
1121
+ process: {
1122
+ pid: primary.pid,
1123
+ command_line_length: primary.command_line.length
1124
+ },
1125
+ processes: summarizeChromeProcesses(processes)
1126
+ };
1127
+ }
1128
+
1129
+ export async function inspectChromeDebugCommandLine({
1130
+ host = DEFAULT_CHROME_HOST,
1131
+ port = DEFAULT_CHROME_PORT,
1132
+ _deps = {}
1133
+ } = {}) {
1134
+ const inspectViaCdp = _deps.inspectChromeCommandLineViaCdpImpl || inspectChromeCommandLineViaCdp;
1135
+ const inspectViaProcess = _deps.inspectChromeCommandLineViaProcessListImpl || inspectChromeCommandLineViaProcessList;
1136
+ const cdpResult = await inspectViaCdp({ host, port });
1137
+ if (cdpResult?.ok && cdpResult.arguments?.length) {
1138
+ return cdpResult;
1139
+ }
1140
+ if (!isLocalChromeHost(host)) {
1141
+ return {
1142
+ ok: false,
1143
+ source: cdpResult?.source || "unknown",
1144
+ arguments: [],
1145
+ error: cdpResult?.error || `Cannot inspect process list for non-local Chrome debug host: ${host}`
1146
+ };
1147
+ }
1148
+ const processResult = await inspectViaProcess({ port });
1149
+ if (processResult?.ok && processResult.arguments?.length) {
1150
+ return {
1151
+ ...processResult,
1152
+ cdp_error: cdpResult?.error || null
1153
+ };
1154
+ }
1155
+ return {
1156
+ ok: false,
1157
+ source: processResult?.source || cdpResult?.source || "unknown",
1158
+ arguments: [],
1159
+ processes: processResult?.processes || [],
1160
+ error: processResult?.error || cdpResult?.error || "Chrome command line could not be inspected"
1161
+ };
1162
+ }
1163
+
1164
+ async function waitForChromeDebugPortClosed({
1165
+ host = DEFAULT_CHROME_HOST,
1166
+ port = DEFAULT_CHROME_PORT,
1167
+ timeoutMs = 6000,
1168
+ intervalMs = 300,
1169
+ listChromeTargetsImpl = listChromeTargets
1170
+ } = {}) {
1171
+ const started = Date.now();
1172
+ let lastError = null;
1173
+ let lastTargetCount = 0;
1174
+ while (Date.now() - started <= timeoutMs) {
1175
+ try {
1176
+ const targets = await listChromeTargetsImpl({ host, port });
1177
+ lastTargetCount = Array.isArray(targets) ? targets.length : 0;
1178
+ } catch (error) {
1179
+ if (isChromeDebugUnavailableError(error)) {
1180
+ return {
1181
+ ok: true,
1182
+ elapsed_ms: Date.now() - started
1183
+ };
1184
+ }
1185
+ lastError = error;
1186
+ }
1187
+ await sleep(intervalMs);
1188
+ }
1189
+ return {
1190
+ ok: false,
1191
+ elapsed_ms: Date.now() - started,
1192
+ target_count: lastTargetCount,
1193
+ error: lastError?.message || `Chrome debug port ${port} is still reachable`
1194
+ };
1195
+ }
1196
+
1197
+ export async function closeChromeDebugInstance({
1198
+ host = DEFAULT_CHROME_HOST,
1199
+ port = DEFAULT_CHROME_PORT,
1200
+ processes = [],
1201
+ timeoutMs = 8000,
1202
+ intervalMs = 300,
1203
+ _deps = {}
1204
+ } = {}) {
1205
+ if (!isLocalChromeHost(host)) {
1206
+ return {
1207
+ ok: false,
1208
+ method: "none",
1209
+ error: `Refusing to close non-local Chrome debug host: ${host}`
1210
+ };
1211
+ }
1212
+
1213
+ const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
1214
+ const waitClosed = _deps.waitForChromeDebugPortClosedImpl || waitForChromeDebugPortClosed;
1215
+ let browserCloseAttempted = false;
1216
+ let browserCloseError = null;
1217
+ try {
1218
+ let client = null;
1219
+ try {
1220
+ client = await CDP({ host, port });
1221
+ if (typeof client?.Browser?.close !== "function") {
1222
+ throw new Error("Browser.close is not available");
1223
+ }
1224
+ browserCloseAttempted = true;
1225
+ await client.Browser.close();
1226
+ } finally {
1227
+ if (client) await client.close().catch(() => {});
1228
+ }
1229
+ } catch (error) {
1230
+ browserCloseError = error?.message || String(error || "");
1231
+ }
1232
+
1233
+ let closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
1234
+ if (closed.ok) {
1235
+ return {
1236
+ ok: true,
1237
+ method: browserCloseAttempted ? "Browser.close" : "port_already_closed",
1238
+ elapsed_ms: closed.elapsed_ms,
1239
+ browser_close_error: browserCloseError
1240
+ };
1241
+ }
1242
+
1243
+ const pids = Array.from(new Set((processes || [])
1244
+ .map((item) => Number(item?.pid))
1245
+ .filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid)));
1246
+ const killedPids = [];
1247
+ const processErrors = [];
1248
+ for (const pid of pids) {
1249
+ try {
1250
+ process.kill(pid);
1251
+ killedPids.push(pid);
1252
+ } catch (error) {
1253
+ processErrors.push({
1254
+ pid,
1255
+ error: error?.message || String(error || "")
1256
+ });
1257
+ }
1258
+ }
1259
+
1260
+ if (killedPids.length > 0) {
1261
+ closed = await waitClosed({ host, port, timeoutMs, intervalMs, listChromeTargetsImpl });
1262
+ if (closed.ok) {
1263
+ return {
1264
+ ok: true,
1265
+ method: browserCloseAttempted ? "Browser.close+process.kill" : "process.kill",
1266
+ elapsed_ms: closed.elapsed_ms,
1267
+ killed_pids: killedPids,
1268
+ browser_close_error: browserCloseError,
1269
+ process_errors: processErrors
1270
+ };
1271
+ }
1272
+ }
1273
+
1274
+ return {
1275
+ ok: false,
1276
+ method: browserCloseAttempted && killedPids.length > 0
1277
+ ? "Browser.close+process.kill"
1278
+ : browserCloseAttempted
1279
+ ? "Browser.close"
1280
+ : killedPids.length > 0
1281
+ ? "process.kill"
1282
+ : "none",
1283
+ killed_pids: killedPids,
1284
+ browser_close_error: browserCloseError,
1285
+ process_errors: processErrors,
1286
+ wait: closed,
1287
+ error: closed.error || browserCloseError || "Failed to close Chrome debug instance"
1288
+ };
1289
+ }
1290
+
1291
+ function summarizeRelaunch(result = {}, reason = "") {
1292
+ return {
1293
+ reason,
1294
+ launched: Boolean(result?.launched),
1295
+ chrome_path: result?.chrome_path || null,
1296
+ user_data_dir: result?.user_data_dir || null,
1297
+ launch_args: Array.isArray(result?.launch_args) ? result.launch_args : [],
1298
+ readiness: result?.readiness || null
1299
+ };
1300
+ }
1301
+
1302
+ function createChromeGuardError(message, code, chromeGuard) {
1303
+ const error = new Error(message);
1304
+ error.code = code;
1305
+ error.chrome_guard = chromeGuard;
1306
+ return error;
1307
+ }
1308
+
1309
+ export async function waitForChromeDebugPort({
1310
+ host = DEFAULT_CHROME_HOST,
1311
+ port = DEFAULT_CHROME_PORT,
1312
+ timeoutMs = 8000,
1313
+ intervalMs = 300
1314
+ } = {}) {
1315
+ const started = Date.now();
1316
+ let lastError = null;
1317
+ while (Date.now() - started <= timeoutMs) {
1318
+ try {
1319
+ const targets = await listChromeTargets({ host, port });
1320
+ return {
1321
+ ok: true,
1322
+ elapsed_ms: Date.now() - started,
1323
+ targets
1324
+ };
1325
+ } catch (error) {
1326
+ lastError = error;
1327
+ await sleep(intervalMs);
1328
+ }
1329
+ }
1330
+ return {
1331
+ ok: false,
1332
+ elapsed_ms: Date.now() - started,
1333
+ error: lastError?.message || String(lastError || "Chrome debug port did not become ready")
1334
+ };
1335
+ }
1336
+
1337
+ export async function launchChromeDebugInstance({
1338
+ host = DEFAULT_CHROME_HOST,
1339
+ port = DEFAULT_CHROME_PORT,
1340
+ url = "about:blank",
1341
+ slowLive = false,
1342
+ userDataDir = ""
1343
+ } = {}) {
1344
+ if (!isLocalChromeHost(host)) {
1345
+ throw new Error(`Cannot auto-launch Chrome for non-local debug host: ${host}`);
1346
+ }
1347
+ const chromePath = getChromeExecutable();
1348
+ if (!chromePath) {
1349
+ throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
1350
+ }
1351
+ const resolvedUserDataDir = userDataDir || getBossChromeUserDataDir(port);
1352
+ ensureDir(resolvedUserDataDir);
1353
+ const args = buildBossChromeLaunchArgs({ port, userDataDir: resolvedUserDataDir, url });
1354
+ const child = spawn(chromePath, args, {
1355
+ detached: true,
1356
+ stdio: "ignore",
1357
+ windowsHide: false
1358
+ });
1359
+ child.unref();
1360
+ const readiness = await waitForChromeDebugPort({
1361
+ host,
1362
+ port,
1363
+ timeoutMs: slowLive ? 30000 : 12000,
1364
+ intervalMs: slowLive ? 700 : 300
1365
+ });
1366
+ if (!readiness.ok) {
1367
+ throw new Error(`Chrome launched but DevTools port ${port} did not become reachable: ${readiness.error}`);
1368
+ }
1369
+ return {
1370
+ launched: true,
1371
+ chrome_path: chromePath,
1372
+ user_data_dir: resolvedUserDataDir,
1373
+ launch_args: args,
1374
+ port,
1375
+ url,
1376
+ readiness: {
1377
+ elapsed_ms: readiness.elapsed_ms,
1378
+ target_count: readiness.targets.length
1379
+ }
1380
+ };
1381
+ }
1382
+
1383
+ export async function ensureChromeDebugPort({
1384
+ host = DEFAULT_CHROME_HOST,
1385
+ port = DEFAULT_CHROME_PORT,
1386
+ url = "about:blank",
1387
+ slowLive = false,
1388
+ launchIfMissing = true,
1389
+ userDataDir = "",
1390
+ enforceRequiredFlags = true,
1391
+ requiredFlags = DEFAULT_REQUIRED_CHROME_FLAGS,
1392
+ _deps = {}
1393
+ } = {}) {
1394
+ const listChromeTargetsImpl = _deps.listChromeTargetsImpl || listChromeTargets;
1395
+ const inspectCommandLineImpl = _deps.inspectChromeDebugCommandLineImpl || inspectChromeDebugCommandLine;
1396
+ const closeChromeDebugInstanceImpl = _deps.closeChromeDebugInstanceImpl || closeChromeDebugInstance;
1397
+ const launchChromeDebugInstanceImpl = _deps.launchChromeDebugInstanceImpl || launchChromeDebugInstance;
1398
+ const required = Array.from(new Set((requiredFlags || []).filter(Boolean)));
1399
+ const baseGuard = {
1400
+ guard_checked: Boolean(enforceRequiredFlags),
1401
+ required_flags: required,
1402
+ missing_flags: [],
1403
+ required_flags_ok: !enforceRequiredFlags,
1404
+ replaced: false,
1405
+ close_method: null,
1406
+ relaunch: null,
1407
+ host,
1408
+ port
1409
+ };
1410
+
1411
+ try {
1412
+ const targets = await listChromeTargetsImpl({ host, port });
1413
+ if (!enforceRequiredFlags) {
1414
+ return {
1415
+ launched: false,
1416
+ reused: true,
1417
+ port,
1418
+ target_count: targets.length,
1419
+ ...baseGuard
1420
+ };
1421
+ }
1422
+
1423
+ const commandLine = await inspectCommandLineImpl({ host, port, _deps });
1424
+ const missingFlags = commandLine?.ok
1425
+ ? getMissingRequiredChromeFlags(commandLine.arguments, required)
1426
+ : required.slice();
1427
+ const commandLineEvidence = {
1428
+ command_line_source: commandLine?.source || "unknown",
1429
+ command_line_error: commandLine?.ok ? null : (commandLine?.error || "Chrome command line could not be inspected"),
1430
+ command_line_args_count: Array.isArray(commandLine?.arguments) ? commandLine.arguments.length : 0,
1431
+ inspected_process: commandLine?.process || null,
1432
+ inspected_processes: commandLine?.processes || []
1433
+ };
1434
+ if (missingFlags.length === 0) {
1435
+ return {
1436
+ launched: false,
1437
+ reused: true,
1438
+ port,
1439
+ target_count: targets.length,
1440
+ ...baseGuard,
1441
+ required_flags_ok: true,
1442
+ ...commandLineEvidence
1443
+ };
1444
+ }
1445
+
1446
+ const guard = {
1447
+ ...baseGuard,
1448
+ required_flags_ok: false,
1449
+ missing_flags: missingFlags,
1450
+ target_count: targets.length,
1451
+ ...commandLineEvidence
1452
+ };
1453
+ if (!isLocalChromeHost(host)) {
1454
+ throw createChromeGuardError(
1455
+ `Chrome debug host ${host}:${port} is missing required Chrome flags and is not local, so it will not be auto-closed.`,
1456
+ "CHROME_REQUIRED_FLAGS_MISSING_NON_LOCAL",
1457
+ guard
1458
+ );
1459
+ }
1460
+
1461
+ const closeResult = await closeChromeDebugInstanceImpl({
1462
+ host,
1463
+ port,
1464
+ processes: commandLine?.processes || [],
1465
+ _deps
1466
+ });
1467
+ if (!closeResult?.ok) {
1468
+ throw createChromeGuardError(
1469
+ `Chrome debug instance on port ${port} is missing required flags and could not be closed: ${closeResult?.error || "unknown close failure"}`,
1470
+ "CHROME_REQUIRED_FLAGS_REPLACE_FAILED",
1471
+ {
1472
+ ...guard,
1473
+ close_method: closeResult?.method || null,
1474
+ close_result: closeResult || null
1475
+ }
1476
+ );
1477
+ }
1478
+
1479
+ try {
1480
+ const relaunch = await launchChromeDebugInstanceImpl({
1481
+ host,
1482
+ port,
1483
+ url,
1484
+ slowLive,
1485
+ userDataDir
1486
+ });
1487
+ return {
1488
+ ...relaunch,
1489
+ reused: false,
1490
+ ...guard,
1491
+ required_flags_ok: true,
1492
+ replaced: true,
1493
+ close_method: closeResult.method || null,
1494
+ close_result: closeResult,
1495
+ relaunch: summarizeRelaunch(relaunch, "missing_required_flags")
1496
+ };
1497
+ } catch (error) {
1498
+ throw createChromeGuardError(
1499
+ `Chrome debug instance on port ${port} was closed for missing flags, but relaunch failed: ${error?.message || error}`,
1500
+ "CHROME_REQUIRED_FLAGS_RELAUNCH_FAILED",
1501
+ {
1502
+ ...guard,
1503
+ close_method: closeResult.method || null,
1504
+ close_result: closeResult,
1505
+ relaunch: {
1506
+ reason: "missing_required_flags",
1507
+ launched: false,
1508
+ error: error?.message || String(error || "")
1509
+ }
1510
+ }
1511
+ );
1512
+ }
1513
+ } catch (error) {
1514
+ if (error?.chrome_guard) {
1515
+ throw error;
1516
+ }
1517
+ if (!launchIfMissing || !isChromeDebugUnavailableError(error)) {
1518
+ throw error;
1519
+ }
1520
+ try {
1521
+ const relaunch = await launchChromeDebugInstanceImpl({
1522
+ host,
1523
+ port,
1524
+ url,
1525
+ slowLive,
1526
+ userDataDir
1527
+ });
1528
+ return {
1529
+ ...baseGuard,
1530
+ ...relaunch,
1531
+ reused: false,
1532
+ required_flags_ok: true,
1533
+ relaunch: summarizeRelaunch(relaunch, "port_unreachable")
1534
+ };
1535
+ } catch (launchError) {
1536
+ throw createChromeGuardError(
1537
+ `Chrome debug port ${port} was unreachable and Chrome relaunch failed: ${launchError?.message || launchError}`,
1538
+ "CHROME_RELAUNCH_FAILED",
1539
+ {
1540
+ ...baseGuard,
1541
+ required_flags_ok: false,
1542
+ relaunch: {
1543
+ reason: "port_unreachable",
1544
+ launched: false,
1545
+ error: launchError?.message || String(launchError || "")
1546
+ }
1547
+ }
1548
+ );
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ export async function openChromeTarget({
1554
+ host = DEFAULT_CHROME_HOST,
1555
+ port = DEFAULT_CHROME_PORT,
1556
+ url
1557
+ } = {}) {
1558
+ const encodedUrl = encodeURIComponent(url || "about:blank");
1559
+ const endpoint = `http://${host}:${port}/json/new?${encodedUrl}`;
1560
+ const methods = ["PUT", "GET"];
1561
+ let lastError = null;
1562
+ for (const method of methods) {
1563
+ try {
1564
+ const response = await fetch(endpoint, { method });
1565
+ if (response.ok) {
1566
+ let payload = null;
1567
+ try {
1568
+ payload = await response.json();
1569
+ } catch {}
1570
+ return {
1571
+ ok: true,
1572
+ method,
1573
+ target_id: payload?.id || null,
1574
+ url: payload?.url || url || null
1575
+ };
1576
+ }
1577
+ lastError = new Error(`DevTools /json/new returned ${response.status}`);
1578
+ } catch (error) {
1579
+ lastError = error;
1580
+ }
1581
+ }
1582
+ return {
1583
+ ok: false,
1584
+ error: lastError?.message || "Failed to open Chrome target"
1585
+ };
1586
+ }
1587
+
1588
+ export async function connectToChromeTargetOrOpen({
1589
+ host = DEFAULT_CHROME_HOST,
1590
+ port = DEFAULT_CHROME_PORT,
1591
+ targetUrlIncludes,
1592
+ targetPredicate,
1593
+ fallbackTargetPredicate,
1594
+ targetUrl,
1595
+ allowNavigate = true,
1596
+ slowLive = false,
1597
+ launchIfMissing = true,
1598
+ _deps = {}
1599
+ } = {}) {
1600
+ const ensureChromeDebugPortImpl = _deps.ensureChromeDebugPortImpl || ensureChromeDebugPort;
1601
+ const connectToChromeTargetImpl = _deps.connectToChromeTargetImpl || connectToChromeTarget;
1602
+ const openChromeTargetImpl = _deps.openChromeTargetImpl || openChromeTarget;
1603
+ let chrome = null;
1604
+ if (targetUrl) {
1605
+ chrome = await ensureChromeDebugPortImpl({
1606
+ host,
1607
+ port,
1608
+ url: targetUrl,
1609
+ slowLive,
1610
+ launchIfMissing: allowNavigate && launchIfMissing
1611
+ });
1612
+ }
1613
+
1614
+ try {
1615
+ const session = await connectToChromeTargetImpl({
1616
+ host,
1617
+ port,
1618
+ targetUrlIncludes,
1619
+ targetPredicate
1620
+ });
1621
+ return {
1622
+ ...session,
1623
+ chrome: {
1624
+ ...(chrome || { launched: false, reused: true, port }),
1625
+ target_created: false
1626
+ }
1627
+ };
1628
+ } catch (primaryError) {
1629
+ if (!allowNavigate) throw primaryError;
1630
+
1631
+ if (typeof fallbackTargetPredicate === "function") {
1632
+ try {
1633
+ const session = await connectToChromeTargetImpl({
1634
+ host,
1635
+ port,
1636
+ targetPredicate: fallbackTargetPredicate
1637
+ });
1638
+ return {
1639
+ ...session,
1640
+ chrome: {
1641
+ ...(chrome || { launched: false, reused: true, port }),
1642
+ target_created: false,
1643
+ fallback_target: true
1644
+ }
1645
+ };
1646
+ } catch {}
1647
+ }
1648
+
1649
+ let openAttempt = null;
1650
+ if (targetUrl) {
1651
+ openAttempt = await openChromeTargetImpl({ host, port, url: targetUrl });
1652
+ if (openAttempt.ok) {
1653
+ const session = await connectToChromeTargetImpl({
1654
+ host,
1655
+ port,
1656
+ targetPredicate: (target) => (
1657
+ (openAttempt.target_id && target?.id === openAttempt.target_id)
1658
+ || String(target?.url || "").includes(targetUrlIncludes || targetUrl)
1659
+ || (targetUrl.includes("zhipin.com") && String(target?.url || "").includes("zhipin.com"))
1660
+ )
1661
+ });
1662
+ return {
1663
+ ...session,
1664
+ chrome: {
1665
+ ...(chrome || { launched: false, reused: true, port }),
1666
+ target_created: true,
1667
+ open_attempt: openAttempt
1668
+ }
1669
+ };
1670
+ }
1671
+ }
1672
+
1673
+ const session = await connectToChromeTargetImpl({
1674
+ host,
1675
+ port,
1676
+ targetPredicate: (target) => target?.type === "page"
1677
+ });
1678
+ return {
1679
+ ...session,
1680
+ chrome: {
1681
+ ...(chrome || { launched: false, reused: true, port }),
1682
+ target_created: false,
1683
+ open_attempt: openAttempt,
1684
+ fallback_any_page: true
1685
+ }
1686
+ };
1687
+ }
1688
+ }
1689
+
1690
+ export function isClosedCdpTransportError(error) {
1691
+ return CDP_CLOSED_TRANSPORT_PATTERN.test(String(error?.message || error || ""));
1692
+ }
1693
+
1694
+ function cloneCdpParams(params = {}) {
1695
+ if (!params || typeof params !== "object" || typeof params === "function") return params;
1696
+ try {
1697
+ return JSON.parse(JSON.stringify(params));
1698
+ } catch {
1699
+ return { ...params };
1700
+ }
1701
+ }
1702
+
1703
+ function shouldReplayCdpSetupCall(domain, method) {
1704
+ return method === "enable"
1705
+ || (domain === "Network" && method === "setCacheDisabled")
1706
+ || (domain === "Page" && method === "bringToFront");
1707
+ }
1708
+
1709
+ export function createGuardedCdpClient(client, { methodLog = [], reconnect = null } = {}) {
1710
+ let currentClient = client;
1711
+ let reconnectInFlight = null;
1712
+ const setupCalls = [];
1713
+ const eventSubscriptions = [];
1714
+
1715
+ async function replaySessionSetup(nextClient) {
1716
+ for (const call of setupCalls) {
1717
+ const fn = nextClient?.[call.domain]?.[call.method];
1718
+ if (typeof fn === "function") {
1719
+ await fn.call(nextClient[call.domain], cloneCdpParams(call.params));
1720
+ }
1721
+ }
1722
+ for (const subscription of eventSubscriptions) {
1723
+ const fn = nextClient?.[subscription.domain]?.[subscription.event];
1724
+ if (typeof fn === "function") {
1725
+ fn.call(nextClient[subscription.domain], subscription.listener);
1726
+ }
1727
+ }
1728
+ }
1729
+
1730
+ async function reconnectClient() {
1731
+ if (typeof reconnect !== "function") return null;
1732
+ if (!reconnectInFlight) {
1733
+ reconnectInFlight = Promise.resolve()
1734
+ .then(() => reconnect())
1735
+ .then(async (nextClient) => {
1736
+ if (!nextClient) throw new Error("CDP reconnect returned no client");
1737
+ currentClient = nextClient;
1738
+ await replaySessionSetup(nextClient);
1739
+ return nextClient;
1740
+ })
1741
+ .finally(() => {
1742
+ reconnectInFlight = null;
1743
+ });
1744
+ }
1745
+ return reconnectInFlight;
1746
+ }
1747
+
1748
+ async function invokeWithReconnect({
1749
+ methodNameForLog,
1750
+ invoke,
1751
+ retryable = true
1752
+ }) {
1753
+ recordMethod(methodLog, methodNameForLog);
1754
+ try {
1755
+ return await invoke(currentClient);
1756
+ } catch (error) {
1757
+ if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
1758
+ throw error;
1759
+ }
1760
+ await reconnectClient();
1761
+ recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
1762
+ return invoke(currentClient);
1763
+ }
1764
+ }
1765
+
1766
+ return new Proxy({}, {
1767
+ get(_target, property, receiver) {
1768
+ if (property === "send") {
1769
+ return async (method, params = {}) => {
1770
+ if (isForbiddenMethod(method)) {
1771
+ throw new Error(`Forbidden CDP method blocked: ${method}`);
1772
+ }
1773
+ return invokeWithReconnect({
1774
+ methodNameForLog: method,
1775
+ invoke: (activeClient) => activeClient.send(method, params)
1776
+ });
1777
+ };
1778
+ }
1779
+
1780
+ if (property === "close") {
1781
+ return async () => currentClient?.close?.();
1782
+ }
1783
+
1784
+ if (property === "__rawClient") return currentClient;
1785
+
1786
+ const value = Reflect.get(currentClient, property, receiver);
1787
+ if (!value || typeof value !== "object") return value;
1788
+
1789
+ return new Proxy({}, {
1790
+ get(_domainTarget, method, domainReceiver) {
1791
+ const domainTarget = Reflect.get(currentClient, property, receiver);
1792
+ const domainValue = Reflect.get(domainTarget, method, domainReceiver);
1793
+ if (typeof domainValue !== "function") return domainValue;
1794
+
1795
+ return (params = {}) => {
1796
+ const fullMethod = methodName(property, method);
1797
+ if (isForbiddenMethod(fullMethod)) {
1798
+ throw new Error(`Forbidden CDP method blocked: ${fullMethod}`);
1799
+ }
1800
+ if (typeof params === "function") {
1801
+ eventSubscriptions.push({
1802
+ domain: property,
1803
+ event: method,
1804
+ listener: params
1805
+ });
1806
+ recordMethod(methodLog, fullMethod);
1807
+ return domainValue.call(domainTarget, params);
1808
+ }
1809
+ if (shouldReplayCdpSetupCall(property, method)) {
1810
+ setupCalls.push({
1811
+ domain: property,
1812
+ method,
1813
+ params: cloneCdpParams(params)
1814
+ });
1815
+ }
1816
+ return invokeWithReconnect({
1817
+ methodNameForLog: fullMethod,
1818
+ invoke: (activeClient) => {
1819
+ const activeDomain = activeClient?.[property];
1820
+ const activeMethod = activeDomain?.[method];
1821
+ if (typeof activeMethod !== "function") {
1822
+ throw new Error(`CDP method is unavailable after reconnect: ${fullMethod}`);
1823
+ }
1824
+ return activeMethod.call(activeDomain, params);
1825
+ }
1826
+ });
1827
+ };
1828
+ }
1829
+ });
1830
+ }
1831
+ });
1832
+ }
1833
+
1834
+ export async function listChromeTargets({
1835
+ host = DEFAULT_CHROME_HOST,
1836
+ port = DEFAULT_CHROME_PORT
1837
+ } = {}) {
1838
+ return CDP.List({ host, port });
1839
+ }
1840
+
1841
+ export async function connectToChromeTarget({
1842
+ host = DEFAULT_CHROME_HOST,
1843
+ port = DEFAULT_CHROME_PORT,
1844
+ targetUrlIncludes,
1845
+ targetPredicate
1846
+ } = {}) {
1847
+ const targets = await listChromeTargets({ host, port });
1848
+ const matcher = normalizeTargetMatcher({ targetUrlIncludes, targetPredicate });
1849
+ const target = targets.find(matcher);
1850
+ if (!target) {
1851
+ const urls = targets.map((item) => item.url).filter(Boolean).join("\n");
1852
+ throw new Error(`No matching Chrome target found on ${host}:${port}.\nAvailable targets:\n${urls}`);
1853
+ }
1854
+
1855
+ let rawClient = await CDP({ host, port, target });
1856
+ let activeTarget = target;
1857
+ const methodLog = [];
1858
+ const client = createGuardedCdpClient(rawClient, {
1859
+ methodLog,
1860
+ reconnect: async () => {
1861
+ const latestTargets = await listChromeTargets({ host, port });
1862
+ const nextTarget = activeTarget?.id
1863
+ ? latestTargets.find((item) => item?.id === activeTarget.id)
1864
+ : latestTargets.find(matcher);
1865
+ if (!nextTarget) {
1866
+ const urls = latestTargets.map((item) => item.url).filter(Boolean).join("\n");
1867
+ throw new Error(`No matching Chrome target found while reconnecting to ${host}:${port}.\nAvailable targets:\n${urls}`);
1868
+ }
1869
+ try {
1870
+ await rawClient.close();
1871
+ } catch {}
1872
+ rawClient = await CDP({ host, port, target: nextTarget });
1873
+ activeTarget = nextTarget;
1874
+ return rawClient;
1875
+ }
1876
+ });
1877
+
1878
+ return {
1879
+ client,
1880
+ get rawClient() {
1881
+ return rawClient;
1882
+ },
1883
+ get target() {
1884
+ return activeTarget;
1885
+ },
1886
+ methodLog,
1887
+ async close() {
1888
+ await rawClient.close();
1889
+ }
1890
+ };
1891
+ }
1892
+
1893
+ export async function assertRuntimeEvaluateBlocked(client) {
1894
+ try {
1895
+ await client.Runtime.evaluate({ expression: "1" });
1896
+ } catch (error) {
1897
+ if (/Forbidden CDP method blocked: Runtime\.evaluate/.test(String(error?.message || ""))) {
1898
+ return { blocked: true, message: error.message };
1899
+ }
1900
+ throw error;
1901
+ }
1902
+ throw new Error("Runtime.evaluate was not blocked by the CDP guard");
1903
+ }
1904
+
1905
+ export async function enableDomains(client, domains = ["Page", "DOM", "Input"]) {
1906
+ for (const domain of domains) {
1907
+ if (!ALLOWED_CDP_DOMAINS.has(domain)) {
1908
+ throw new Error(`CDP domain is not allowed by the CDP-only contract: ${domain}`);
1909
+ }
1910
+ if (typeof client?.[domain]?.enable === "function") {
1911
+ await client[domain].enable();
1912
+ }
1913
+ }
1914
+ }
1915
+
1916
+ export async function bringPageToFront(client) {
1917
+ if (typeof client?.Page?.bringToFront === "function") {
1918
+ await client.Page.bringToFront();
1919
+ }
1920
+ }
1921
+
1922
+ export async function getPageFrameTree(client) {
1923
+ const result = await client.Page.getFrameTree();
1924
+ return result.frameTree || null;
1925
+ }
1926
+
1927
+ export async function getMainFrame(client) {
1928
+ const frameTree = await getPageFrameTree(client);
1929
+ return frameTree?.frame || null;
1930
+ }
1931
+
1932
+ export async function getMainFrameUrl(client) {
1933
+ const frame = await getMainFrame(client);
1934
+ return frame?.url || "";
1935
+ }
1936
+
1937
+ export async function waitForMainFrameUrl(client, predicate, {
1938
+ timeoutMs = 10000,
1939
+ intervalMs = 250
1940
+ } = {}) {
1941
+ const started = Date.now();
1942
+ let lastUrl = "";
1943
+ while (Date.now() - started <= timeoutMs) {
1944
+ lastUrl = await getMainFrameUrl(client);
1945
+ if (predicate(lastUrl)) {
1946
+ return {
1947
+ ok: true,
1948
+ elapsed_ms: Date.now() - started,
1949
+ url: lastUrl
1950
+ };
1951
+ }
1952
+ await sleep(intervalMs);
1953
+ }
1954
+ return {
1955
+ ok: false,
1956
+ elapsed_ms: Date.now() - started,
1957
+ url: lastUrl
1958
+ };
1959
+ }
1960
+
1961
+ export async function getDocumentRoot(client, { depth = 1, pierce = true } = {}) {
1962
+ const result = await client.DOM.getDocument({ depth, pierce });
1963
+ return result.root;
1964
+ }
1965
+
1966
+ export async function querySelector(client, nodeId, selector) {
1967
+ const result = await client.DOM.querySelector({ nodeId, selector });
1968
+ return result.nodeId || 0;
1969
+ }
1970
+
1971
+ export async function querySelectorAll(client, nodeId, selector) {
1972
+ const result = await client.DOM.querySelectorAll({ nodeId, selector });
1973
+ return result.nodeIds || [];
1974
+ }
1975
+
1976
+ export async function findFirstNode(client, rootNodeId, selectors = []) {
1977
+ for (const selector of selectors) {
1978
+ const nodeId = await querySelector(client, rootNodeId, selector);
1979
+ if (nodeId) return { selector, nodeId };
1980
+ }
1981
+ return null;
1982
+ }
1983
+
1984
+ export async function describeNode(client, nodeId, { depth = 1, pierce = true } = {}) {
1985
+ const result = await client.DOM.describeNode({ nodeId, depth, pierce });
1986
+ return result.node;
1987
+ }
1988
+
1989
+ export async function getFrameDocumentNodeId(client, iframeNodeId) {
1990
+ const node = await describeNode(client, iframeNodeId, { depth: 1, pierce: true });
1991
+ const documentNodeId = node?.contentDocument?.nodeId;
1992
+ if (!documentNodeId) {
1993
+ throw new Error(`Node ${iframeNodeId} does not expose a contentDocument node`);
1994
+ }
1995
+ return documentNodeId;
1996
+ }
1997
+
1998
+ export async function findIframeDocument(client, rootNodeId, selectors = []) {
1999
+ const iframe = await findFirstNode(client, rootNodeId, selectors);
2000
+ if (!iframe) return null;
2001
+ const documentNodeId = await getFrameDocumentNodeId(client, iframe.nodeId);
2002
+ return { ...iframe, documentNodeId };
2003
+ }
2004
+
2005
+ export async function getAttributesMap(client, nodeId) {
2006
+ const result = await client.DOM.getAttributes({ nodeId });
2007
+ const attributes = {};
2008
+ const raw = result.attributes || [];
2009
+ for (let index = 0; index < raw.length; index += 2) {
2010
+ attributes[raw[index]] = raw[index + 1] || "";
2011
+ }
2012
+ return attributes;
2013
+ }
2014
+
2015
+ export async function getOuterHTML(client, nodeId) {
2016
+ const result = await client.DOM.getOuterHTML({ nodeId });
2017
+ return result.outerHTML || "";
2018
+ }
2019
+
2020
+ export async function getNodeBox(client, nodeId) {
2021
+ let result;
2022
+ try {
2023
+ result = await client.DOM.getBoxModel({ nodeId });
2024
+ } catch (error) {
2025
+ const wrapped = new Error(error?.message || String(error));
2026
+ wrapped.name = error?.name || "Error";
2027
+ wrapped.node_id = nodeId;
2028
+ wrapped.cdp_method = "DOM.getBoxModel";
2029
+ wrapped.original_stack = error?.stack || "";
2030
+ wrapped.stack = `${new Error(`getNodeBox failed for nodeId=${nodeId}`).stack || wrapped.stack}\nCaused by: ${error?.stack || error}`;
2031
+ throw wrapped;
2032
+ }
2033
+ const model = result.model;
2034
+ const quad = model.border?.length ? model.border : model.content;
2035
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
2036
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
2037
+ const minX = Math.min(...xs);
2038
+ const maxX = Math.max(...xs);
2039
+ const minY = Math.min(...ys);
2040
+ const maxY = Math.max(...ys);
2041
+ return {
2042
+ model,
2043
+ center: {
2044
+ x: (minX + maxX) / 2,
2045
+ y: (minY + maxY) / 2
2046
+ },
2047
+ rect: {
2048
+ x: minX,
2049
+ y: minY,
2050
+ width: maxX - minX,
2051
+ height: maxY - minY
2052
+ }
2053
+ };
2054
+ }
2055
+
2056
+ export async function simulateHumanClick(client, targetX, targetY, {
2057
+ button = "left",
2058
+ clickCount = 1,
2059
+ delayMs = 80,
2060
+ random = Math.random,
2061
+ sleepFn = sleep,
2062
+ moveSteps = 18,
2063
+ moveJitterPx = 3,
2064
+ hoverJitterPx = 5,
2065
+ moveDelayMinMs = 5,
2066
+ moveDelayMaxMs = 23,
2067
+ hoverDelayMinMs = 10,
2068
+ hoverDelayMaxMs = 30,
2069
+ prePressBaseMs = 260,
2070
+ prePressVarianceMs = 80,
2071
+ holdVarianceMs = 30,
2072
+ startPoint = null
2073
+ } = {}) {
2074
+ const target = normalizePoint({ x: targetX, y: targetY });
2075
+ if (!target) throw new Error("simulateHumanClick requires finite target coordinates");
2076
+ const nextRandom = normalizeRandom(random);
2077
+ const interactionConfig = getHumanInteractionConfig(client) || {};
2078
+ const start = normalizePoint(startPoint)
2079
+ || normalizePoint(interactionConfig.lastMousePoint)
2080
+ || {
2081
+ x: Math.max(0, target.x + randomBetween(nextRandom, -140, 140)),
2082
+ y: Math.max(0, target.y + randomBetween(nextRandom, -90, 90))
2083
+ };
2084
+ const path = generateBezierPath(start, target, {
2085
+ steps: moveSteps,
2086
+ random: nextRandom
2087
+ });
2088
+ const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
2089
+ const moveDelayMin = Math.min(moveDelayMinMs, moveDelayMaxMs);
2090
+ const moveDelayMax = Math.max(moveDelayMinMs, moveDelayMaxMs);
2091
+ const hoverDelayMin = Math.min(hoverDelayMinMs, hoverDelayMaxMs);
2092
+ const hoverDelayMax = Math.max(hoverDelayMinMs, hoverDelayMaxMs);
2093
+ for (const point of path) {
2094
+ await client.Input.dispatchMouseEvent({
2095
+ type: "mouseMoved",
2096
+ x: Math.round(point.x + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
2097
+ y: Math.round(point.y + randomBetween(nextRandom, -moveJitterPx / 2, moveJitterPx / 2)),
2098
+ button: "none"
2099
+ });
2100
+ const pauseMs = Math.round(randomBetween(nextRandom, moveDelayMin, moveDelayMax));
2101
+ if (pauseMs > 0) await sleeper(pauseMs);
2102
+ }
2103
+ const hoverSteps = randomIntegerBetween(nextRandom, 3, 6);
2104
+ for (let index = 0; index < hoverSteps; index += 1) {
2105
+ await client.Input.dispatchMouseEvent({
2106
+ type: "mouseMoved",
2107
+ x: Math.round(target.x + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
2108
+ y: Math.round(target.y + randomBetween(nextRandom, -hoverJitterPx / 2, hoverJitterPx / 2)),
2109
+ button: "none"
2110
+ });
2111
+ const pauseMs = Math.round(randomBetween(nextRandom, hoverDelayMin, hoverDelayMax));
2112
+ if (pauseMs > 0) await sleeper(pauseMs);
2113
+ }
2114
+ const prePressMs = humanDelay(prePressBaseMs, prePressVarianceMs, {
2115
+ minMs: 0,
2116
+ maxMs: Math.max(prePressBaseMs + prePressVarianceMs * 4, prePressBaseMs),
2117
+ random: nextRandom
2118
+ });
2119
+ if (prePressMs > 0) await sleeper(prePressMs);
2120
+ await client.Input.dispatchMouseEvent({ type: "mousePressed", x: target.x, y: target.y, button, clickCount });
2121
+ const holdMs = humanDelay(delayMs, holdVarianceMs, {
2122
+ minMs: 0,
2123
+ maxMs: Math.max(delayMs + holdVarianceMs * 4, delayMs),
2124
+ random: nextRandom
2125
+ });
2126
+ if (holdMs > 0) await sleeper(holdMs);
2127
+ await client.Input.dispatchMouseEvent({ type: "mouseReleased", x: target.x, y: target.y, button, clickCount });
2128
+ const latestConfig = getHumanInteractionConfig(client);
2129
+ if (latestConfig) latestConfig.lastMousePoint = target;
2130
+ return {
2131
+ mode: "human",
2132
+ path_points: path.length,
2133
+ hover_steps: hoverSteps,
2134
+ pre_press_ms: prePressMs,
2135
+ hold_ms: holdMs
2136
+ };
2137
+ }
2138
+
2139
+ export function resolveHumanClickPointForBox(box, {
2140
+ enabled = true,
2141
+ safeClickPointEnabled = true,
2142
+ random = Math.random,
2143
+ safeClickMinWidth = 44,
2144
+ safeClickMinHeight = 28,
2145
+ safeClickInsetRatio = 0.22,
2146
+ safeClickMinInsetPx = 4,
2147
+ safeClickMaxInsetPx = 18
2148
+ } = {}) {
2149
+ const center = normalizePoint(box?.center);
2150
+ if (!center) throw new Error("resolveHumanClickPointForBox requires a box center");
2151
+ const rect = box?.rect || {};
2152
+ const width = Number(rect.width);
2153
+ const height = Number(rect.height);
2154
+ const originX = Number(rect.x);
2155
+ const originY = Number(rect.y);
2156
+ if (
2157
+ enabled !== true
2158
+ || safeClickPointEnabled === false
2159
+ || !Number.isFinite(width)
2160
+ || !Number.isFinite(height)
2161
+ || !Number.isFinite(originX)
2162
+ || !Number.isFinite(originY)
2163
+ || width < Math.max(1, Number(safeClickMinWidth) || 44)
2164
+ || height < Math.max(1, Number(safeClickMinHeight) || 28)
2165
+ ) {
2166
+ return {
2167
+ x: center.x,
2168
+ y: center.y,
2169
+ mode: "center",
2170
+ reason: "small_or_disabled"
2171
+ };
2172
+ }
2173
+
2174
+ const nextRandom = normalizeRandom(random);
2175
+ const insetRatio = clampNumber(safeClickInsetRatio, 0.05, 0.45);
2176
+ const minInset = Math.max(0, Number(safeClickMinInsetPx) || 0);
2177
+ const maxInset = Math.max(minInset, Number(safeClickMaxInsetPx) || minInset);
2178
+ const insetX = Math.min(width / 2 - 1, Math.max(minInset, Math.min(maxInset, width * insetRatio)));
2179
+ const insetY = Math.min(height / 2 - 1, Math.max(minInset, Math.min(maxInset, height * insetRatio)));
2180
+ const usableWidth = Math.max(0, width - insetX * 2);
2181
+ const usableHeight = Math.max(0, height - insetY * 2);
2182
+ if (usableWidth <= 0 || usableHeight <= 0) {
2183
+ return {
2184
+ x: center.x,
2185
+ y: center.y,
2186
+ mode: "center",
2187
+ reason: "insufficient_safe_area"
2188
+ };
2189
+ }
2190
+ return {
2191
+ x: originX + insetX + nextRandom() * usableWidth,
2192
+ y: originY + insetY + nextRandom() * usableHeight,
2193
+ mode: "safe_inset",
2194
+ inset_x: insetX,
2195
+ inset_y: insetY
2196
+ };
2197
+ }
2198
+
2199
+ export async function clickPoint(client, x, y, {
2200
+ button = "left",
2201
+ clickCount = 1,
2202
+ delayMs = 80,
2203
+ humanRestEnabled = null,
2204
+ humanInteraction = null
2205
+ } = {}) {
2206
+ const configured = getHumanInteractionConfig(client);
2207
+ const mergedHumanInteraction = {
2208
+ ...(configured || {}),
2209
+ ...(humanInteraction || {})
2210
+ };
2211
+ const humanEnabled = humanRestEnabled === true
2212
+ || humanInteraction?.enabled === true
2213
+ || (humanRestEnabled !== false && configured?.enabled === true);
2214
+ if (humanEnabled && mergedHumanInteraction.clickMovementEnabled !== false) {
2215
+ return simulateHumanClick(client, x, y, {
2216
+ ...mergedHumanInteraction,
2217
+ button,
2218
+ clickCount,
2219
+ delayMs
2220
+ });
2221
+ }
2222
+ await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
2223
+ await client.Input.dispatchMouseEvent({ type: "mousePressed", x, y, button, clickCount });
2224
+ if (delayMs > 0) await sleep(delayMs);
2225
+ await client.Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button, clickCount });
2226
+ return {
2227
+ mode: "direct"
2228
+ };
2229
+ }
2230
+
2231
+ export async function scrollNodeIntoView(client, nodeId) {
2232
+ try {
2233
+ await client.DOM.scrollIntoViewIfNeeded({ nodeId });
2234
+ } catch (error) {
2235
+ const wrapped = new Error(error?.message || String(error));
2236
+ wrapped.name = error?.name || "Error";
2237
+ wrapped.node_id = nodeId;
2238
+ wrapped.cdp_method = "DOM.scrollIntoViewIfNeeded";
2239
+ wrapped.original_stack = error?.stack || "";
2240
+ wrapped.stack = `${new Error(`scrollNodeIntoView failed for nodeId=${nodeId}`).stack || wrapped.stack}\nCaused by: ${error?.stack || error}`;
2241
+ throw wrapped;
2242
+ }
2243
+ }
2244
+
2245
+ export async function clickNodeCenter(client, nodeId, {
2246
+ scrollIntoView = false,
2247
+ ...clickOptions
2248
+ } = {}) {
2249
+ if (scrollIntoView) {
2250
+ await scrollNodeIntoView(client, nodeId);
2251
+ await sleep(150);
2252
+ }
2253
+ const box = await getNodeBox(client, nodeId);
2254
+ const configured = getHumanInteractionConfig(client);
2255
+ const mergedHumanInteraction = {
2256
+ ...(configured || {}),
2257
+ ...(clickOptions.humanInteraction || {})
2258
+ };
2259
+ const humanClickPointEnabled = (
2260
+ clickOptions.humanRestEnabled === true
2261
+ || clickOptions.humanInteraction?.enabled === true
2262
+ || (clickOptions.humanRestEnabled !== false && configured?.enabled === true)
2263
+ ) && mergedHumanInteraction.safeClickPointEnabled !== false;
2264
+ const clickPointTarget = humanClickPointEnabled
2265
+ ? resolveHumanClickPointForBox(box, mergedHumanInteraction)
2266
+ : { ...box.center, mode: "center" };
2267
+ const clickResult = await clickPoint(client, clickPointTarget.x, clickPointTarget.y, clickOptions);
2268
+ return {
2269
+ ...box,
2270
+ click_target: clickPointTarget,
2271
+ click_result: clickResult
2272
+ };
2273
+ }
2274
+
2275
+ export async function pressKey(client, key, {
2276
+ code = key,
2277
+ windowsVirtualKeyCode,
2278
+ nativeVirtualKeyCode = windowsVirtualKeyCode,
2279
+ text = "",
2280
+ modifiers = 0
2281
+ } = {}) {
2282
+ await client.Input.dispatchKeyEvent({
2283
+ type: "keyDown",
2284
+ key,
2285
+ code,
2286
+ windowsVirtualKeyCode,
2287
+ nativeVirtualKeyCode,
2288
+ text,
2289
+ modifiers
2290
+ });
2291
+ await client.Input.dispatchKeyEvent({
2292
+ type: "keyUp",
2293
+ key,
2294
+ code,
2295
+ windowsVirtualKeyCode,
2296
+ nativeVirtualKeyCode,
2297
+ modifiers
2298
+ });
2299
+ }
2300
+
2301
+ export function chunkHumanText(text, {
2302
+ random = Math.random,
2303
+ minLength = 1,
2304
+ maxLength = 5
2305
+ } = {}) {
2306
+ const chars = Array.from(String(text || ""));
2307
+ const min = Math.max(1, Math.floor(Number(minLength) || 1));
2308
+ const max = Math.max(min, Math.floor(Number(maxLength) || min));
2309
+ const nextRandom = normalizeRandom(random);
2310
+ const chunks = [];
2311
+ let index = 0;
2312
+ while (index < chars.length) {
2313
+ const remaining = chars.length - index;
2314
+ const size = Math.min(remaining, randomIntegerBetween(nextRandom, min, max));
2315
+ chunks.push(chars.slice(index, index + size).join(""));
2316
+ index += size;
2317
+ }
2318
+ return chunks;
2319
+ }
2320
+
2321
+ export async function insertText(client, text, {
2322
+ humanTextEntryEnabled = null,
2323
+ humanInteraction = null
2324
+ } = {}) {
2325
+ const value = String(text || "");
2326
+ const configured = getHumanInteractionConfig(client);
2327
+ const mergedHumanInteraction = {
2328
+ ...(configured || {}),
2329
+ ...(humanInteraction || {})
2330
+ };
2331
+ const textEntryEnabled = humanTextEntryEnabled === true
2332
+ || humanInteraction?.textEntryEnabled === true
2333
+ || (humanTextEntryEnabled !== false
2334
+ && configured?.enabled === true
2335
+ && configured?.textEntryEnabled !== false);
2336
+ if (!textEntryEnabled || value.length <= 1) {
2337
+ await client.Input.insertText({ text: value });
2338
+ return {
2339
+ mode: "direct",
2340
+ chunk_count: value ? 1 : 0
2341
+ };
2342
+ }
2343
+ const chunks = chunkHumanText(value, {
2344
+ random: mergedHumanInteraction.random,
2345
+ minLength: mergedHumanInteraction.textChunkMinLength,
2346
+ maxLength: mergedHumanInteraction.textChunkMaxLength
2347
+ });
2348
+ const sleeper = typeof mergedHumanInteraction.sleepFn === "function"
2349
+ ? mergedHumanInteraction.sleepFn
2350
+ : sleep;
2351
+ for (let index = 0; index < chunks.length; index += 1) {
2352
+ await client.Input.insertText({ text: chunks[index] });
2353
+ if (index < chunks.length - 1) {
2354
+ const pauseMs = humanDelay(
2355
+ mergedHumanInteraction.textChunkDelayBaseMs,
2356
+ mergedHumanInteraction.textChunkDelayVarianceMs,
2357
+ {
2358
+ minMs: 0,
2359
+ maxMs: Math.max(
2360
+ mergedHumanInteraction.textChunkDelayBaseMs + mergedHumanInteraction.textChunkDelayVarianceMs * 4,
2361
+ mergedHumanInteraction.textChunkDelayBaseMs
2362
+ ),
2363
+ random: mergedHumanInteraction.random
2364
+ }
2365
+ );
2366
+ if (pauseMs > 0) await sleeper(pauseMs);
2367
+ }
2368
+ }
2369
+ return {
2370
+ mode: "chunked",
2371
+ chunk_count: chunks.length,
2372
+ chunks
2373
+ };
2374
+ }
2375
+
2376
+ export async function selectAllFocusedText(client) {
2377
+ await pressKey(client, "a", {
2378
+ code: "KeyA",
2379
+ windowsVirtualKeyCode: 65,
2380
+ nativeVirtualKeyCode: 65,
2381
+ modifiers: 2
2382
+ });
2383
+ }
2384
+
2385
+ export async function clearFocusedInput(client) {
2386
+ await selectAllFocusedText(client);
2387
+ await pressKey(client, "Backspace", {
2388
+ code: "Backspace",
2389
+ windowsVirtualKeyCode: 8,
2390
+ nativeVirtualKeyCode: 8
2391
+ });
2392
+ }
2393
+
2394
+ export async function waitForSelector(client, nodeId, selector, {
2395
+ timeoutMs = 5000,
2396
+ intervalMs = 150
2397
+ } = {}) {
2398
+ const started = Date.now();
2399
+ while (Date.now() - started <= timeoutMs) {
2400
+ const foundNodeId = await querySelector(client, nodeId, selector);
2401
+ if (foundNodeId) return foundNodeId;
2402
+ await sleep(intervalMs);
2403
+ }
2404
+ return 0;
2405
+ }
2406
+
2407
+ export async function countSelectors(client, nodeId, selectors = {}) {
2408
+ const counts = {};
2409
+ for (const [name, selector] of Object.entries(selectors)) {
2410
+ counts[name] = (await querySelectorAll(client, nodeId, selector)).length;
2411
+ }
2412
+ return counts;
2413
+ }
2414
+
2415
+ export async function getAccessibilityTree(client, options = {}) {
2416
+ return client.Accessibility.getFullAXTree(options);
2417
+ }
2418
+
2419
+ export async function sleep(ms) {
2420
+ await new Promise((resolve) => setTimeout(resolve, ms));
2421
+ }