@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

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