chrome-debugger-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,719 @@
1
+ import CDP from "chrome-remote-interface";
2
+ import { spawn } from "child_process";
3
+ import * as http from "http";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ const MAX_PROPERTY_DEPTH = 3;
7
+ const SKIP_SCOPE_TYPES = new Set(["global"]);
8
+ const RELOAD_STATE_TIMEOUT_MS = 15_000;
9
+ const STEP_TIMEOUT_MS = 30_000;
10
+ function quotePosixArg(arg) {
11
+ if (arg.length === 0) {
12
+ return "''";
13
+ }
14
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
15
+ }
16
+ function quoteWindowsArg(arg) {
17
+ if (!/[\s"]/u.test(arg)) {
18
+ return arg;
19
+ }
20
+ let escaped = '"';
21
+ let backslashCount = 0;
22
+ for (const char of arg) {
23
+ if (char === "\\") {
24
+ backslashCount += 1;
25
+ continue;
26
+ }
27
+ if (char === '"') {
28
+ escaped += "\\".repeat(backslashCount * 2 + 1);
29
+ escaped += '"';
30
+ backslashCount = 0;
31
+ continue;
32
+ }
33
+ escaped += "\\".repeat(backslashCount);
34
+ escaped += char;
35
+ backslashCount = 0;
36
+ }
37
+ escaped += "\\".repeat(backslashCount * 2);
38
+ escaped += '"';
39
+ return escaped;
40
+ }
41
+ function quoteShellArg(arg) {
42
+ return process.platform === "win32"
43
+ ? quoteWindowsArg(arg)
44
+ : quotePosixArg(arg);
45
+ }
46
+ export class ChromeDebuggerManager {
47
+ client = null;
48
+ pauseInfo = null;
49
+ pauseWaiters = [];
50
+ pauseListeners = [];
51
+ connected = false;
52
+ connectedTargetUrl = "";
53
+ /** Whether a reloadPage() is in progress – avoids treating that load as "resumed" unexpectedly */
54
+ reloading = false;
55
+ reloadResetTimer = null;
56
+ get isConnected() {
57
+ return this.connected;
58
+ }
59
+ get isPaused() {
60
+ return this.pauseInfo !== null;
61
+ }
62
+ get currentPauseInfo() {
63
+ return this.pauseInfo;
64
+ }
65
+ onPause(listener) {
66
+ this.pauseListeners.push(listener);
67
+ }
68
+ // ─── Chrome launch helpers ──────────────────────────────
69
+ /** Probe whether Chrome's CDP HTTP endpoint is responding on the given port. */
70
+ async isDebugPortAlive(port = 9222) {
71
+ return new Promise((resolve) => {
72
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
73
+ resolve(res.statusCode === 200);
74
+ });
75
+ req.on("error", () => resolve(false));
76
+ req.setTimeout(1500, () => {
77
+ req.destroy();
78
+ resolve(false);
79
+ });
80
+ });
81
+ }
82
+ /** Return the Chrome executable path for the current OS. */
83
+ getChromePath(explicitPath) {
84
+ if (explicitPath?.trim()) {
85
+ return explicitPath.trim();
86
+ }
87
+ const envChromePath = process.env.CHROME_PATH
88
+ ?? process.env.CHROME_EXECUTABLE
89
+ ?? process.env.GOOGLE_CHROME_BIN;
90
+ if (envChromePath?.trim()) {
91
+ return envChromePath.trim();
92
+ }
93
+ switch (process.platform) {
94
+ case "darwin":
95
+ return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
96
+ case "linux":
97
+ return "google-chrome";
98
+ case "win32":
99
+ return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
100
+ default:
101
+ throw new Error(`Unsupported platform: ${process.platform}`);
102
+ }
103
+ }
104
+ /**
105
+ * Launch Chrome with remote debugging enabled (dual-instance via --user-data-dir).
106
+ * If Chrome is already listening on the port, skips launch.
107
+ * dryRun=true only returns the command without executing.
108
+ * openDevTools=true adds --auto-open-devtools-for-tabs so DevTools opens automatically.
109
+ */
110
+ async launchChrome(options = {}) {
111
+ const port = options.port ?? 9222;
112
+ const userDataDir = options.userDataDir ?? path.join(os.homedir(), ".chrome-debug-profile");
113
+ const chromePath = this.getChromePath(options.chromePath);
114
+ const args = [
115
+ `--remote-debugging-port=${port}`,
116
+ `--user-data-dir=${userDataDir}`,
117
+ "--no-first-run",
118
+ "--no-default-browser-check",
119
+ ];
120
+ // 自动为每个新标签页打开 DevTools 控制台
121
+ if (options.openDevTools)
122
+ args.push("--auto-open-devtools-for-tabs");
123
+ if (options.url)
124
+ args.push(options.url);
125
+ const command = [chromePath, ...args].map(quoteShellArg).join(" ");
126
+ // Already running?
127
+ const alreadyRunning = await this.isDebugPortAlive(port);
128
+ if (alreadyRunning) {
129
+ return {
130
+ command,
131
+ alreadyRunning: true,
132
+ launched: false,
133
+ message: `Chrome debug port ${port} is already active. No new instance started.`,
134
+ };
135
+ }
136
+ if (options.dryRun) {
137
+ return {
138
+ command,
139
+ alreadyRunning: false,
140
+ launched: false,
141
+ message: "Dry run – Chrome was NOT launched. Confirm and re-run without dryRun to execute.",
142
+ };
143
+ }
144
+ try {
145
+ const proc = spawn(chromePath, args, { detached: true, stdio: "ignore" });
146
+ proc.unref();
147
+ // Give Chrome ~2 s to bind the port
148
+ await new Promise((r) => setTimeout(r, 2000));
149
+ const alive = await this.isDebugPortAlive(port);
150
+ return {
151
+ command,
152
+ alreadyRunning: false,
153
+ launched: alive,
154
+ message: alive
155
+ ? `Chrome launched. Debug port: ${port}. Profile dir: ${userDataDir}`
156
+ : `Chrome did not expose debug port ${port} within 2 seconds. Run the command manually if Chrome did not open.`,
157
+ requiresManualLaunch: !alive,
158
+ };
159
+ }
160
+ catch (e) {
161
+ return {
162
+ command,
163
+ alreadyRunning: false,
164
+ launched: false,
165
+ message: `Automatic launch failed: ${e.message}`,
166
+ requiresManualLaunch: true,
167
+ };
168
+ }
169
+ }
170
+ /** List all available Chrome tabs/targets on the given port. */
171
+ async listTargets(port = 9222) {
172
+ return CDP.List({ port });
173
+ }
174
+ /**
175
+ * Connect to Chrome.
176
+ * @param port Remote debugging port (default 9222).
177
+ * @param targetUrlFilter A substring of the tab URL/title to connect to (e.g. "localhost:8080").
178
+ * Required when multiple page tabs are available.
179
+ */
180
+ async connect(port = 9222, targetUrlFilter) {
181
+ if (this.client) {
182
+ await this.disconnect();
183
+ }
184
+ const targets = await CDP.List({ port });
185
+ if (targets.length === 0) {
186
+ throw new Error(`No Chrome targets found on port ${port}. Is Chrome running with --remote-debugging-port=${port}?`);
187
+ }
188
+ const pageTargets = targets.filter((t) => t.type === "page" &&
189
+ !t.url.startsWith("devtools://") &&
190
+ !t.url.startsWith("chrome-extension://"));
191
+ if (pageTargets.length === 0) {
192
+ throw new Error(`No page targets found on port ${port}. Open your target page first.`);
193
+ }
194
+ let target;
195
+ const filter = targetUrlFilter?.trim();
196
+ // targetUrl 必须由用户明确确认后再传入,单 tab 也不例外
197
+ if (!filter) {
198
+ const urls = pageTargets.map((t, i) => ` [${i}] ${t.url}`).join("\n");
199
+ throw new Error(`targetUrl is required. Call listTargets first, show the list to the user, wait for their confirmation, then pass the confirmed URL substring.\nAvailable page targets:\n${urls}`);
200
+ }
201
+ const matches = pageTargets.filter((t) => t.url.includes(filter) || (t.title ?? "").includes(filter));
202
+ if (matches.length === 0) {
203
+ const urls = pageTargets.map((t) => ` [${t.type}] ${t.url}`).join("\n");
204
+ throw new Error(`No page target matching "${filter}" found.\nAvailable page targets:\n${urls}`);
205
+ }
206
+ if (matches.length > 1) {
207
+ const urls = matches.map((t) => ` [${t.type}] ${t.url}`).join("\n");
208
+ throw new Error(`targetUrl "${filter}" matches multiple pages. Use a more specific substring.\nMatched targets:\n${urls}`);
209
+ }
210
+ target = matches[0];
211
+ this.client = await CDP({ port, target: target.id });
212
+ this.connected = true;
213
+ this.connectedTargetUrl = target.url;
214
+ await this.client.Page.enable();
215
+ await this.client.Debugger.enable();
216
+ await this.client.Runtime.enable();
217
+ this.client.Debugger.paused((params) => {
218
+ this.pauseInfo = {
219
+ reason: params.reason,
220
+ callFrames: params.callFrames,
221
+ hitBreakpoints: params.hitBreakpoints,
222
+ };
223
+ this.finishReloadCycle();
224
+ const waiters = this.pauseWaiters.splice(0);
225
+ for (const waiter of waiters) {
226
+ clearTimeout(waiter.timer);
227
+ waiter.resolve(this.pauseInfo);
228
+ }
229
+ for (const listener of this.pauseListeners) {
230
+ try {
231
+ listener(this.pauseInfo);
232
+ }
233
+ catch {
234
+ // ignore listener errors
235
+ }
236
+ }
237
+ });
238
+ this.client.Debugger.resumed(() => {
239
+ this.pauseInfo = null;
240
+ });
241
+ this.client.Page.loadEventFired(() => {
242
+ this.finishReloadCycle();
243
+ });
244
+ this.client.on("disconnect", () => {
245
+ this.resetConnectionState(new Error("Chrome debugging session disconnected."));
246
+ });
247
+ this.client.on("Inspector.detached", () => {
248
+ this.resetConnectionState(new Error("Chrome debugging session detached from the target."));
249
+ });
250
+ // Re-enable Debugger domain after page navigation so pending breakpoints survive
251
+ this.client.on("Runtime.executionContextsCleared", async () => {
252
+ if (this.reloading)
253
+ return;
254
+ try {
255
+ await this.client.Debugger.enable();
256
+ await this.client.Runtime.enable();
257
+ }
258
+ catch {
259
+ // target may have gone away
260
+ }
261
+ });
262
+ return `Connected to: ${target.title || target.url} (${target.url})`;
263
+ }
264
+ async disconnect() {
265
+ if (this.client) {
266
+ const client = this.client;
267
+ try {
268
+ await client.Debugger.disable();
269
+ }
270
+ catch {
271
+ // ignore
272
+ }
273
+ try {
274
+ await client.close();
275
+ }
276
+ catch {
277
+ // ignore
278
+ }
279
+ this.resetConnectionState(new Error("Disconnected from Chrome."));
280
+ }
281
+ }
282
+ /** Reload the page via CDP (more reliable than manual browser refresh). */
283
+ async reloadPage(ignoreCache = false) {
284
+ this.ensureConnected();
285
+ this.beginReloadCycle();
286
+ try {
287
+ await this.client.Page.reload({ ignoreCache });
288
+ return "Page reload requested";
289
+ }
290
+ catch (error) {
291
+ this.finishReloadCycle();
292
+ throw error;
293
+ }
294
+ }
295
+ async setBreakpoint(url, lineNumber, columnNumber, condition) {
296
+ this.ensureConnected();
297
+ // Full URL (with protocol) → exact match; partial name/keyword → urlRegex
298
+ const isFullUrl = /^(https?|file|chrome):\/\//i.test(url);
299
+ const params = isFullUrl
300
+ ? { lineNumber, url, columnNumber, condition }
301
+ : {
302
+ lineNumber,
303
+ urlRegex: url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
304
+ columnNumber,
305
+ condition,
306
+ };
307
+ const result = await this.client.Debugger.setBreakpointByUrl(params);
308
+ const pending = result.locations.length === 0;
309
+ return {
310
+ breakpointId: result.breakpointId,
311
+ locations: result.locations,
312
+ pending,
313
+ ...(pending && {
314
+ hint: "Pending breakpoint – call reloadPage() to reload via CDP, which guarantees the breakpoint resolves when the script loads.",
315
+ }),
316
+ };
317
+ }
318
+ async removeBreakpoint(breakpointId) {
319
+ this.ensureConnected();
320
+ await this.client.Debugger.removeBreakpoint({ breakpointId });
321
+ }
322
+ /** Non-blocking status check – returns immediately with current state. */
323
+ getStatus() {
324
+ if (!this.connected) {
325
+ return { connected: false, paused: false };
326
+ }
327
+ if (!this.pauseInfo) {
328
+ return {
329
+ connected: true,
330
+ paused: false,
331
+ targetUrl: this.connectedTargetUrl,
332
+ };
333
+ }
334
+ return {
335
+ connected: true,
336
+ paused: true,
337
+ targetUrl: this.connectedTargetUrl,
338
+ pauseReason: this.pauseInfo.reason,
339
+ hitBreakpoints: this.pauseInfo.hitBreakpoints,
340
+ callStack: this.getCallStackSummary(),
341
+ };
342
+ }
343
+ /**
344
+ * Wait up to `timeoutMs` for the debugger to pause.
345
+ *
346
+ * NOTE: MCP Inspector has a built-in request timeout (default ~10 s).
347
+ * If you see a -32001 timeout from Inspector, either:
348
+ * 1. Increase Inspector timeout in its UI to ≥ 60000
349
+ * 2. Use a shorter timeout here and retry with polling via getStatus()
350
+ */
351
+ async waitForPause(timeoutMs = 30000) {
352
+ this.ensureConnected();
353
+ if (this.pauseInfo) {
354
+ return this.pauseInfo;
355
+ }
356
+ return this.createPauseWait(timeoutMs, `Timeout: debugger did not pause within ${timeoutMs}ms. Try calling getStatus() to poll, or increase the timeout.`).promise;
357
+ }
358
+ /**
359
+ * Wait for the NEXT debugger pause, then check whether it matches the target location.
360
+ *
361
+ * ⚠️ No auto-resume: execution stays paused after this returns, regardless of whether
362
+ * the pause matched. The caller (AI) decides what to do next:
363
+ * - matched=true → call getScopeVariables() / evaluate() to read variables
364
+ * - matched=false → call resume() explicitly, then waitForSpecificPause() again if needed
365
+ *
366
+ * Matching uses two tiers:
367
+ * Tier 1 (exact): URL contains urlFragment AND |compiledLine - expectedLine| <= lineTolerance
368
+ * Tier 2 (semantic): URL contains urlFragment AND reason === "debugger-statement"
369
+ * CDP reports compiled line numbers; for transpiled/bundled code the line may differ
370
+ * from source, but reason="debugger-statement" is a reliable signal that it IS the
371
+ * debugger; statement we inserted.
372
+ *
373
+ * @param urlFragment Substring of the script URL where debugger; was added
374
+ * @param expectedLine 0-based line number (editor line N → pass N-1)
375
+ * @param timeoutMs How long to wait for any pause (default 90 s)
376
+ * @param lineTolerance ±line tolerance for Tier 1 (default 10)
377
+ */
378
+ async waitForSpecificPause(urlFragment, expectedLine, timeoutMs = 90000, lineTolerance = 10) {
379
+ this.ensureConnected();
380
+ // 等待下一次暂停(不自动 resume,不循环)
381
+ const info = await this.waitForPause(timeoutMs);
382
+ const top = info.callFrames[0];
383
+ // 第1层:URL + 行号容差匹配
384
+ const exactFrame = info.callFrames.find((f) => f.url.includes(urlFragment) &&
385
+ Math.abs(f.location.lineNumber - expectedLine) <= lineTolerance);
386
+ // 第2层:URL + reason 语义匹配(debugger; 触发时 reason 必为 "debugger-statement")
387
+ const semanticFrame = !exactFrame && info.reason === "debugger-statement"
388
+ ? info.callFrames.find((f) => f.url.includes(urlFragment))
389
+ : undefined;
390
+ const matchedFrame = exactFrame ?? semanticFrame;
391
+ const matched = !!matchedFrame;
392
+ // 构建返回值,matched=false 时提示 AI 下一步操作
393
+ let note;
394
+ if (matched && exactFrame) {
395
+ note = `Matched at "${matchedFrame.url}" line ${matchedFrame.location.lineNumber}. Call getScopeVariables() to read variables.`;
396
+ }
397
+ else if (matched && semanticFrame) {
398
+ note = `Matched by debugger-statement reason at "${matchedFrame.url}" line ${matchedFrame.location.lineNumber} (compiled). Source line was ${expectedLine}; offset is normal for transpiled code. Call getScopeVariables() to read variables.`;
399
+ }
400
+ else {
401
+ note = `Paused at "${top?.url ?? ""}" line ${top?.location.lineNumber ?? -1} (reason: ${info.reason}), which does NOT match urlFragment="${urlFragment}" near line ${expectedLine}. Execution is still paused. Options: (1) call getScopeVariables() to inspect here anyway, (2) call resume() to continue, then waitForSpecificPause() again to wait for the next pause.`;
402
+ }
403
+ return {
404
+ matched,
405
+ reason: info.reason,
406
+ pausedUrl: matchedFrame?.url ?? top?.url ?? "",
407
+ pausedLine: matchedFrame?.location.lineNumber ?? top?.location.lineNumber ?? -1,
408
+ functionName: (matchedFrame ?? top)?.functionName || "(anonymous)",
409
+ callStack: this.getCallStackSummary(),
410
+ lineMatchedByTolerance: !!exactFrame,
411
+ note,
412
+ };
413
+ }
414
+ async getScopeVariables(frameIndex = 0) {
415
+ this.ensurePaused();
416
+ const frames = this.pauseInfo.callFrames;
417
+ if (frameIndex < 0 || frameIndex >= frames.length) {
418
+ throw new Error(`Frame index ${frameIndex} out of range (0-${frames.length - 1})`);
419
+ }
420
+ const frame = frames[frameIndex];
421
+ const scopeGroups = [];
422
+ for (const scope of frame.scopeChain) {
423
+ if (SKIP_SCOPE_TYPES.has(scope.type))
424
+ continue;
425
+ if (!scope.object.objectId)
426
+ continue;
427
+ const variables = await this.getObjectProperties(scope.object.objectId, 0);
428
+ scopeGroups.push({
429
+ type: scope.type,
430
+ name: scope.name,
431
+ variables,
432
+ });
433
+ }
434
+ return scopeGroups;
435
+ }
436
+ async evaluate(expression, frameIndex = 0) {
437
+ this.ensurePaused();
438
+ const frames = this.pauseInfo.callFrames;
439
+ if (frameIndex < 0 || frameIndex >= frames.length) {
440
+ throw new Error(`Frame index ${frameIndex} out of range (0-${frames.length - 1})`);
441
+ }
442
+ const frame = frames[frameIndex];
443
+ const response = await this.client.Debugger.evaluateOnCallFrame({
444
+ callFrameId: frame.callFrameId,
445
+ expression,
446
+ generatePreview: true,
447
+ returnByValue: false,
448
+ });
449
+ return {
450
+ result: this.formatRemoteObject(response.result),
451
+ exceptionDetails: response.exceptionDetails
452
+ ? {
453
+ text: response.exceptionDetails.text,
454
+ line: response.exceptionDetails.lineNumber,
455
+ column: response.exceptionDetails.columnNumber,
456
+ exception: response.exceptionDetails.exception
457
+ ? this.formatRemoteObject(response.exceptionDetails.exception)
458
+ : undefined,
459
+ }
460
+ : undefined,
461
+ };
462
+ }
463
+ async resume() {
464
+ this.ensurePaused();
465
+ await this.client.Debugger.resume();
466
+ }
467
+ async stepInto(timeoutMs = STEP_TIMEOUT_MS) {
468
+ this.ensurePaused();
469
+ return this.stepAndWait(() => this.client.Debugger.stepInto(), "step into", timeoutMs);
470
+ }
471
+ async stepOver(timeoutMs = STEP_TIMEOUT_MS) {
472
+ this.ensurePaused();
473
+ return this.stepAndWait(() => this.client.Debugger.stepOver(), "step over", timeoutMs);
474
+ }
475
+ async stepOut(timeoutMs = STEP_TIMEOUT_MS) {
476
+ this.ensurePaused();
477
+ return this.stepAndWait(() => this.client.Debugger.stepOut(), "step out", timeoutMs);
478
+ }
479
+ async pause() {
480
+ this.ensureConnected();
481
+ await this.client.Debugger.pause();
482
+ }
483
+ getCallStackSummary() {
484
+ if (!this.pauseInfo)
485
+ return [];
486
+ return this.pauseInfo.callFrames.map((frame, index) => ({
487
+ index,
488
+ functionName: frame.functionName || "(anonymous)",
489
+ url: frame.url,
490
+ lineNumber: frame.location.lineNumber,
491
+ columnNumber: frame.location.columnNumber,
492
+ }));
493
+ }
494
+ // ─── internal helpers ──────────────────────────────────────
495
+ async getObjectProperties(objectId, depth) {
496
+ if (depth >= MAX_PROPERTY_DEPTH) {
497
+ return [{ name: "...", value: "(max depth reached)", type: "info" }];
498
+ }
499
+ const { result: properties } = await this.client.Runtime.getProperties({
500
+ objectId,
501
+ ownProperties: true,
502
+ generatePreview: true,
503
+ });
504
+ const variables = [];
505
+ for (const prop of properties) {
506
+ if (!prop.value)
507
+ continue;
508
+ variables.push(await this.formatProperty(prop, depth));
509
+ }
510
+ return variables;
511
+ }
512
+ async formatProperty(prop, depth) {
513
+ const obj = prop.value;
514
+ const info = {
515
+ name: prop.name,
516
+ type: obj.type,
517
+ value: this.primitiveValue(obj),
518
+ };
519
+ if (obj.subtype)
520
+ info.subtype = obj.subtype;
521
+ info.structuredValue = await this.toStructuredValue(obj, depth);
522
+ if (obj.type === "object" &&
523
+ obj.objectId &&
524
+ obj.subtype !== "null" &&
525
+ depth < MAX_PROPERTY_DEPTH - 1) {
526
+ info.value = this.describeStructuredValue(obj, info.structuredValue);
527
+ }
528
+ return info;
529
+ }
530
+ primitiveValue(obj) {
531
+ if (obj.type === "undefined")
532
+ return "undefined";
533
+ if (obj.subtype === "null")
534
+ return "null";
535
+ if (obj.type === "string")
536
+ return JSON.stringify(obj.value);
537
+ if (obj.type === "number" ||
538
+ obj.type === "boolean" ||
539
+ obj.type === "bigint") {
540
+ return String(obj.unserializableValue ?? obj.value);
541
+ }
542
+ if (obj.type === "symbol")
543
+ return obj.description ?? "Symbol()";
544
+ if (obj.type === "function")
545
+ return obj.description ?? "function(){}";
546
+ return obj.description ?? obj.className ?? `[${obj.type}]`;
547
+ }
548
+ formatRemoteObject(obj) {
549
+ if (obj.type === "undefined" ||
550
+ obj.type === "string" ||
551
+ obj.type === "number" ||
552
+ obj.type === "boolean") {
553
+ return { type: obj.type, value: obj.value };
554
+ }
555
+ if (obj.subtype === "null") {
556
+ return { type: "object", subtype: "null", value: null };
557
+ }
558
+ return {
559
+ type: obj.type,
560
+ subtype: obj.subtype,
561
+ className: obj.className,
562
+ description: obj.description,
563
+ preview: obj.preview
564
+ ? obj.preview.properties.map((p) => ({
565
+ name: p.name,
566
+ type: p.type,
567
+ value: p.value,
568
+ }))
569
+ : undefined,
570
+ };
571
+ }
572
+ async toStructuredValue(obj, depth) {
573
+ if (obj.type === "undefined")
574
+ return "undefined";
575
+ if (obj.subtype === "null")
576
+ return null;
577
+ if (obj.type === "string")
578
+ return obj.value ?? "";
579
+ if (obj.type === "number") {
580
+ return obj.unserializableValue ?? obj.value ?? "NaN";
581
+ }
582
+ if (obj.type === "boolean")
583
+ return obj.value ?? false;
584
+ if (obj.type === "bigint") {
585
+ return obj.unserializableValue ?? String(obj.value ?? "");
586
+ }
587
+ if (obj.type === "symbol")
588
+ return obj.description ?? "Symbol()";
589
+ if (obj.type === "function")
590
+ return obj.description ?? "function(){}";
591
+ if (!obj.objectId || depth >= MAX_PROPERTY_DEPTH - 1) {
592
+ return obj.description ?? obj.className ?? `[${obj.type}]`;
593
+ }
594
+ const children = await this.getObjectProperties(obj.objectId, depth + 1);
595
+ if (obj.subtype === "array") {
596
+ return this.childrenToArray(children);
597
+ }
598
+ return Object.fromEntries(children.map((child) => [
599
+ child.name,
600
+ child.structuredValue ?? child.value,
601
+ ]));
602
+ }
603
+ childrenToArray(children) {
604
+ const numericEntries = children
605
+ .filter((child) => /^\d+$/u.test(child.name))
606
+ .map((child) => [
607
+ Number(child.name),
608
+ child.structuredValue ?? child.value,
609
+ ])
610
+ .sort((left, right) => left[0] - right[0]);
611
+ const result = [];
612
+ for (const [index, value] of numericEntries) {
613
+ result[index] = value;
614
+ }
615
+ return result;
616
+ }
617
+ describeStructuredValue(obj, structuredValue) {
618
+ if (obj.subtype === "array" && Array.isArray(structuredValue)) {
619
+ return `Array(${structuredValue.length})`;
620
+ }
621
+ if (structuredValue &&
622
+ typeof structuredValue === "object" &&
623
+ !Array.isArray(structuredValue)) {
624
+ const size = Object.keys(structuredValue).length;
625
+ const label = obj.className ?? obj.description ?? "Object";
626
+ return `${label}(${size})`;
627
+ }
628
+ return obj.description ?? obj.className ?? `[${obj.type}]`;
629
+ }
630
+ beginReloadCycle() {
631
+ this.reloading = true;
632
+ this.pauseInfo = null;
633
+ if (this.reloadResetTimer) {
634
+ clearTimeout(this.reloadResetTimer);
635
+ }
636
+ this.reloadResetTimer = setTimeout(() => {
637
+ this.finishReloadCycle();
638
+ }, RELOAD_STATE_TIMEOUT_MS);
639
+ }
640
+ finishReloadCycle() {
641
+ this.reloading = false;
642
+ if (this.reloadResetTimer) {
643
+ clearTimeout(this.reloadResetTimer);
644
+ this.reloadResetTimer = null;
645
+ }
646
+ }
647
+ createPauseWait(timeoutMs, timeoutMessage) {
648
+ let waiter;
649
+ const promise = new Promise((resolve, reject) => {
650
+ waiter = {
651
+ resolve: (info) => {
652
+ this.removePauseWaiter(waiter);
653
+ resolve(info);
654
+ },
655
+ reject: (error) => {
656
+ this.removePauseWaiter(waiter);
657
+ reject(error);
658
+ },
659
+ timer: setTimeout(() => {
660
+ waiter.reject(new Error(timeoutMessage));
661
+ }, timeoutMs),
662
+ };
663
+ this.pauseWaiters.push(waiter);
664
+ });
665
+ return {
666
+ promise,
667
+ cancel: (error) => {
668
+ waiter.reject(error);
669
+ },
670
+ };
671
+ }
672
+ async stepAndWait(command, label, timeoutMs) {
673
+ const nextPauseWait = this.createPauseWait(timeoutMs, `Timeout: debugger did not pause again after ${label} within ${timeoutMs}ms.`);
674
+ try {
675
+ await command();
676
+ return await nextPauseWait.promise;
677
+ }
678
+ catch (error) {
679
+ nextPauseWait.cancel(error instanceof Error
680
+ ? error
681
+ : new Error(`Failed to ${label}.`));
682
+ throw error;
683
+ }
684
+ }
685
+ removePauseWaiter(waiter) {
686
+ clearTimeout(waiter.timer);
687
+ const index = this.pauseWaiters.indexOf(waiter);
688
+ if (index !== -1) {
689
+ this.pauseWaiters.splice(index, 1);
690
+ }
691
+ }
692
+ rejectPauseWaiters(error) {
693
+ const waiters = this.pauseWaiters.splice(0);
694
+ for (const waiter of waiters) {
695
+ clearTimeout(waiter.timer);
696
+ waiter.reject(error);
697
+ }
698
+ }
699
+ resetConnectionState(waiterError) {
700
+ this.client = null;
701
+ this.connected = false;
702
+ this.connectedTargetUrl = "";
703
+ this.pauseInfo = null;
704
+ this.finishReloadCycle();
705
+ this.rejectPauseWaiters(waiterError);
706
+ }
707
+ ensureConnected() {
708
+ if (!this.client || !this.connected) {
709
+ throw new Error("Not connected to Chrome. Call connect() first.");
710
+ }
711
+ }
712
+ ensurePaused() {
713
+ this.ensureConnected();
714
+ if (!this.pauseInfo) {
715
+ throw new Error("Debugger is not paused. Use waitForPause() or set a breakpoint first.");
716
+ }
717
+ }
718
+ }
719
+ //# sourceMappingURL=chrome-manager.js.map