agent-dbg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.bin/ndbg +0 -0
  2. package/.claude/settings.local.json +21 -0
  3. package/.claude/skills/ndbg-debugger/ndbg-debugger/SKILL.md +116 -0
  4. package/.claude/skills/ndbg-debugger/ndbg-debugger/references/commands.md +173 -0
  5. package/CLAUDE.md +43 -0
  6. package/PROGRESS.md +261 -0
  7. package/README.md +67 -0
  8. package/biome.json +41 -0
  9. package/ndbg-spec.md +958 -0
  10. package/package.json +30 -0
  11. package/src/cdp/client.ts +198 -0
  12. package/src/cdp/types.ts +16 -0
  13. package/src/cli/parser.ts +287 -0
  14. package/src/cli/registry.ts +7 -0
  15. package/src/cli/types.ts +24 -0
  16. package/src/commands/attach.ts +47 -0
  17. package/src/commands/blackbox-ls.ts +38 -0
  18. package/src/commands/blackbox-rm.ts +57 -0
  19. package/src/commands/blackbox.ts +48 -0
  20. package/src/commands/break-ls.ts +57 -0
  21. package/src/commands/break-rm.ts +40 -0
  22. package/src/commands/break-toggle.ts +42 -0
  23. package/src/commands/break.ts +145 -0
  24. package/src/commands/breakable.ts +69 -0
  25. package/src/commands/catch.ts +38 -0
  26. package/src/commands/console.ts +61 -0
  27. package/src/commands/continue.ts +46 -0
  28. package/src/commands/eval.ts +70 -0
  29. package/src/commands/exceptions.ts +61 -0
  30. package/src/commands/hotpatch.ts +67 -0
  31. package/src/commands/launch.ts +69 -0
  32. package/src/commands/logpoint.ts +78 -0
  33. package/src/commands/pause.ts +46 -0
  34. package/src/commands/props.ts +77 -0
  35. package/src/commands/restart-frame.ts +36 -0
  36. package/src/commands/run-to.ts +70 -0
  37. package/src/commands/scripts.ts +57 -0
  38. package/src/commands/search.ts +73 -0
  39. package/src/commands/sessions.ts +71 -0
  40. package/src/commands/set-return.ts +49 -0
  41. package/src/commands/set.ts +61 -0
  42. package/src/commands/source.ts +59 -0
  43. package/src/commands/sourcemap.ts +66 -0
  44. package/src/commands/stack.ts +64 -0
  45. package/src/commands/state.ts +124 -0
  46. package/src/commands/status.ts +57 -0
  47. package/src/commands/step.ts +50 -0
  48. package/src/commands/stop.ts +27 -0
  49. package/src/commands/vars.ts +71 -0
  50. package/src/daemon/client.ts +147 -0
  51. package/src/daemon/entry.ts +242 -0
  52. package/src/daemon/paths.ts +26 -0
  53. package/src/daemon/server.ts +185 -0
  54. package/src/daemon/session-blackbox.ts +41 -0
  55. package/src/daemon/session-breakpoints.ts +492 -0
  56. package/src/daemon/session-execution.ts +121 -0
  57. package/src/daemon/session-inspection.ts +701 -0
  58. package/src/daemon/session-mutation.ts +197 -0
  59. package/src/daemon/session-state.ts +258 -0
  60. package/src/daemon/session.ts +938 -0
  61. package/src/daemon/spawn.ts +53 -0
  62. package/src/formatter/errors.ts +15 -0
  63. package/src/formatter/source.ts +74 -0
  64. package/src/formatter/stack.ts +70 -0
  65. package/src/formatter/values.ts +269 -0
  66. package/src/formatter/variables.ts +20 -0
  67. package/src/main.ts +45 -0
  68. package/src/protocol/messages.ts +316 -0
  69. package/src/refs/ref-table.ts +120 -0
  70. package/src/refs/resolver.ts +24 -0
  71. package/src/sourcemap/resolver.ts +318 -0
  72. package/tests/fixtures/async-app.js +34 -0
  73. package/tests/fixtures/console-app.js +12 -0
  74. package/tests/fixtures/error-app.js +28 -0
  75. package/tests/fixtures/exception-app.js +6 -0
  76. package/tests/fixtures/inspect-app.js +10 -0
  77. package/tests/fixtures/mutation-app.js +9 -0
  78. package/tests/fixtures/simple-app.js +50 -0
  79. package/tests/fixtures/step-app.js +13 -0
  80. package/tests/fixtures/ts-app/src/app.ts +21 -0
  81. package/tests/fixtures/ts-app/tsconfig.json +14 -0
  82. package/tests/integration/blackbox.test.ts +135 -0
  83. package/tests/integration/break-extras.test.ts +241 -0
  84. package/tests/integration/breakpoint.test.ts +217 -0
  85. package/tests/integration/console.test.ts +275 -0
  86. package/tests/integration/execution.test.ts +247 -0
  87. package/tests/integration/inspection.test.ts +311 -0
  88. package/tests/integration/mutation.test.ts +178 -0
  89. package/tests/integration/session.test.ts +223 -0
  90. package/tests/integration/source.test.ts +209 -0
  91. package/tests/integration/sourcemap.test.ts +214 -0
  92. package/tests/integration/state.test.ts +208 -0
  93. package/tests/unit/cdp-client.test.ts +422 -0
  94. package/tests/unit/daemon.test.ts +286 -0
  95. package/tests/unit/formatter.test.ts +716 -0
  96. package/tests/unit/parser.test.ts +105 -0
  97. package/tests/unit/refs.test.ts +383 -0
  98. package/tests/unit/sourcemap.test.ts +236 -0
  99. package/tsconfig.json +32 -0
@@ -0,0 +1,492 @@
1
+ import type Protocol from "devtools-protocol/types/protocol.js";
2
+ import type { DebugSession } from "./session.ts";
3
+
4
+ export async function setBreakpoint(
5
+ session: DebugSession,
6
+ file: string,
7
+ line: number,
8
+ options?: { condition?: string; hitCount?: number; urlRegex?: string },
9
+ ): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
10
+ if (!session.cdp) {
11
+ throw new Error("No active debug session");
12
+ }
13
+
14
+ const condition = session.buildBreakpointCondition(options?.condition, options?.hitCount);
15
+
16
+ // Try source map translation (.ts → .js) before setting breakpoint
17
+ let originalFile: string | null = null;
18
+ let originalLine: number | null = null;
19
+ let actualLine = line;
20
+ let actualFile = file;
21
+
22
+ if (!options?.urlRegex) {
23
+ const generated = session.sourceMapResolver.toGenerated(file, line, 0);
24
+ if (generated) {
25
+ originalFile = file;
26
+ originalLine = line;
27
+ actualLine = generated.line;
28
+ // Find the URL of the generated script
29
+ const scriptInfo = session.scripts.get(generated.scriptId);
30
+ if (scriptInfo) {
31
+ actualFile = scriptInfo.url;
32
+ }
33
+ }
34
+ }
35
+
36
+ const params: Protocol.Debugger.SetBreakpointByUrlRequest = {
37
+ lineNumber: actualLine - 1, // CDP uses 0-based lines
38
+ };
39
+
40
+ let url: string | null = null;
41
+ if (options?.urlRegex) {
42
+ // Use urlRegex directly without resolving file path
43
+ params.urlRegex = options.urlRegex;
44
+ } else {
45
+ url = session.findScriptUrl(actualFile);
46
+ if (url) {
47
+ params.url = url;
48
+ } else {
49
+ // Fall back to urlRegex to match partial paths
50
+ params.urlRegex = `${actualFile.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
51
+ }
52
+ }
53
+ if (condition) {
54
+ params.condition = condition;
55
+ }
56
+
57
+ const r = await session.cdp.send("Debugger.setBreakpointByUrl", params);
58
+
59
+ const loc = r.locations[0];
60
+ const resolvedUrl = originalFile ?? url ?? file;
61
+ const resolvedLine = originalLine ?? (loc ? loc.lineNumber + 1 : line); // Convert back to 1-based
62
+ const resolvedColumn = loc?.columnNumber;
63
+
64
+ const meta: Record<string, unknown> = {
65
+ url: resolvedUrl,
66
+ line: resolvedLine,
67
+ };
68
+ if (originalFile) {
69
+ meta.originalUrl = originalFile;
70
+ meta.originalLine = originalLine;
71
+ meta.generatedUrl = url ?? actualFile;
72
+ meta.generatedLine = loc ? loc.lineNumber + 1 : actualLine;
73
+ }
74
+ if (resolvedColumn !== undefined) {
75
+ meta.column = resolvedColumn;
76
+ }
77
+ if (options?.condition) {
78
+ meta.condition = options.condition;
79
+ }
80
+ if (options?.hitCount) {
81
+ meta.hitCount = options.hitCount;
82
+ }
83
+ if (options?.urlRegex) {
84
+ meta.urlRegex = options.urlRegex;
85
+ }
86
+
87
+ const ref = session.refs.addBreakpoint(r.breakpointId, meta);
88
+
89
+ const location: { url: string; line: number; column?: number } = {
90
+ url: resolvedUrl,
91
+ line: resolvedLine,
92
+ };
93
+ if (resolvedColumn !== undefined) {
94
+ location.column = resolvedColumn;
95
+ }
96
+
97
+ return { ref, location };
98
+ }
99
+
100
+ export async function removeBreakpoint(session: DebugSession, ref: string): Promise<void> {
101
+ if (!session.cdp) {
102
+ throw new Error("No active debug session");
103
+ }
104
+
105
+ const entry = session.refs.resolve(ref);
106
+ if (!entry) {
107
+ throw new Error(`Unknown ref: ${ref}`);
108
+ }
109
+
110
+ if (entry.type !== "BP" && entry.type !== "LP") {
111
+ throw new Error(`Ref ${ref} is not a breakpoint or logpoint`);
112
+ }
113
+
114
+ await session.cdp.send("Debugger.removeBreakpoint", {
115
+ breakpointId: entry.remoteId,
116
+ });
117
+
118
+ session.refs.remove(ref);
119
+ }
120
+
121
+ export async function removeAllBreakpoints(session: DebugSession): Promise<void> {
122
+ if (!session.cdp) {
123
+ throw new Error("No active debug session");
124
+ }
125
+
126
+ const bps = session.refs.list("BP");
127
+ const lps = session.refs.list("LP");
128
+ const all = [...bps, ...lps];
129
+
130
+ for (const entry of all) {
131
+ await session.cdp.send("Debugger.removeBreakpoint", {
132
+ breakpointId: entry.remoteId,
133
+ });
134
+ session.refs.remove(entry.ref);
135
+ }
136
+ }
137
+
138
+ export function listBreakpoints(session: DebugSession): Array<{
139
+ ref: string;
140
+ type: "BP" | "LP";
141
+ url: string;
142
+ line: number;
143
+ column?: number;
144
+ condition?: string;
145
+ hitCount?: number;
146
+ template?: string;
147
+ disabled?: boolean;
148
+ originalUrl?: string;
149
+ originalLine?: number;
150
+ }> {
151
+ const bps = session.refs.list("BP");
152
+ const lps = session.refs.list("LP");
153
+ const all = [...bps, ...lps];
154
+
155
+ const results: Array<{
156
+ ref: string;
157
+ type: "BP" | "LP";
158
+ url: string;
159
+ line: number;
160
+ column?: number;
161
+ condition?: string;
162
+ hitCount?: number;
163
+ template?: string;
164
+ disabled?: boolean;
165
+ originalUrl?: string;
166
+ originalLine?: number;
167
+ }> = all.map((entry) => {
168
+ const meta = entry.meta ?? {};
169
+ const item: {
170
+ ref: string;
171
+ type: "BP" | "LP";
172
+ url: string;
173
+ line: number;
174
+ column?: number;
175
+ condition?: string;
176
+ hitCount?: number;
177
+ template?: string;
178
+ disabled?: boolean;
179
+ originalUrl?: string;
180
+ originalLine?: number;
181
+ } = {
182
+ ref: entry.ref,
183
+ type: entry.type as "BP" | "LP",
184
+ url: meta.url as string,
185
+ line: meta.line as number,
186
+ };
187
+
188
+ if (meta.column !== undefined) {
189
+ item.column = meta.column as number;
190
+ }
191
+ if (meta.condition !== undefined) {
192
+ item.condition = meta.condition as string;
193
+ }
194
+ if (meta.hitCount !== undefined) {
195
+ item.hitCount = meta.hitCount as number;
196
+ }
197
+ if (meta.template !== undefined) {
198
+ item.template = meta.template as string;
199
+ }
200
+ if (meta.originalUrl !== undefined) {
201
+ item.originalUrl = meta.originalUrl as string;
202
+ item.originalLine = meta.originalLine as number;
203
+ }
204
+
205
+ return item;
206
+ });
207
+
208
+ // Include disabled breakpoints
209
+ for (const [ref, entry] of session.disabledBreakpoints) {
210
+ const meta = entry.meta;
211
+ const item: {
212
+ ref: string;
213
+ type: "BP" | "LP";
214
+ url: string;
215
+ line: number;
216
+ column?: number;
217
+ condition?: string;
218
+ hitCount?: number;
219
+ template?: string;
220
+ disabled?: boolean;
221
+ } = {
222
+ ref,
223
+ type: (meta.type as "BP" | "LP") ?? "BP",
224
+ url: meta.url as string,
225
+ line: meta.line as number,
226
+ disabled: true,
227
+ };
228
+
229
+ if (meta.column !== undefined) {
230
+ item.column = meta.column as number;
231
+ }
232
+ if (meta.condition !== undefined) {
233
+ item.condition = meta.condition as string;
234
+ }
235
+ if (meta.hitCount !== undefined) {
236
+ item.hitCount = meta.hitCount as number;
237
+ }
238
+ if (meta.template !== undefined) {
239
+ item.template = meta.template as string;
240
+ }
241
+
242
+ results.push(item);
243
+ }
244
+
245
+ return results;
246
+ }
247
+
248
+ export async function toggleBreakpoint(
249
+ session: DebugSession,
250
+ ref: string,
251
+ ): Promise<{ ref: string; state: "enabled" | "disabled" }> {
252
+ if (!session.cdp) {
253
+ throw new Error("No active debug session");
254
+ }
255
+
256
+ if (ref === "all") {
257
+ // Toggle all: if any are enabled, disable all; otherwise enable all
258
+ const activeBps = session.refs.list("BP");
259
+ const activeLps = session.refs.list("LP");
260
+ const allActive = [...activeBps, ...activeLps];
261
+
262
+ if (allActive.length > 0) {
263
+ // Disable all active breakpoints
264
+ for (const entry of allActive) {
265
+ await session.cdp.send("Debugger.removeBreakpoint", {
266
+ breakpointId: entry.remoteId,
267
+ });
268
+ const meta = { ...(entry.meta ?? {}), type: entry.type };
269
+ session.disabledBreakpoints.set(entry.ref, {
270
+ breakpointId: entry.remoteId,
271
+ meta,
272
+ });
273
+ session.refs.remove(entry.ref);
274
+ }
275
+ return { ref: "all", state: "disabled" };
276
+ }
277
+ // Re-enable all disabled breakpoints
278
+ const disabledRefs = [...session.disabledBreakpoints.keys()];
279
+ for (const dRef of disabledRefs) {
280
+ const entry = session.disabledBreakpoints.get(dRef);
281
+ if (!entry) continue;
282
+ await reEnableBreakpoint(session, dRef, entry);
283
+ }
284
+ return { ref: "all", state: "enabled" };
285
+ }
286
+
287
+ // Single breakpoint toggle
288
+ // Check if it's currently active
289
+ const activeEntry = session.refs.resolve(ref);
290
+ if (activeEntry && (activeEntry.type === "BP" || activeEntry.type === "LP")) {
291
+ // Disable it
292
+ await session.cdp.send("Debugger.removeBreakpoint", {
293
+ breakpointId: activeEntry.remoteId,
294
+ });
295
+ const meta = { ...(activeEntry.meta ?? {}), type: activeEntry.type };
296
+ session.disabledBreakpoints.set(ref, {
297
+ breakpointId: activeEntry.remoteId,
298
+ meta,
299
+ });
300
+ session.refs.remove(ref);
301
+ return { ref, state: "disabled" };
302
+ }
303
+
304
+ // Check if it's disabled
305
+ const disabledEntry = session.disabledBreakpoints.get(ref);
306
+ if (disabledEntry) {
307
+ await reEnableBreakpoint(session, ref, disabledEntry);
308
+ return { ref, state: "enabled" };
309
+ }
310
+
311
+ throw new Error(`Unknown breakpoint ref: ${ref}`);
312
+ }
313
+
314
+ async function reEnableBreakpoint(
315
+ session: DebugSession,
316
+ ref: string,
317
+ entry: { breakpointId: string; meta: Record<string, unknown> },
318
+ ): Promise<void> {
319
+ if (!session.cdp) return;
320
+
321
+ const meta = entry.meta;
322
+ const line = meta.line as number;
323
+ const url = meta.url as string | undefined;
324
+ const condition = meta.condition as string | undefined;
325
+ const hitCount = meta.hitCount as number | undefined;
326
+ const urlRegex = meta.urlRegex as string | undefined;
327
+
328
+ const builtCondition = session.buildBreakpointCondition(condition, hitCount);
329
+
330
+ const bpParams: Protocol.Debugger.SetBreakpointByUrlRequest = {
331
+ lineNumber: line - 1,
332
+ };
333
+
334
+ if (urlRegex) {
335
+ bpParams.urlRegex = urlRegex;
336
+ } else if (url) {
337
+ bpParams.url = url;
338
+ }
339
+
340
+ if (builtCondition) {
341
+ bpParams.condition = builtCondition;
342
+ }
343
+
344
+ const r = await session.cdp.send("Debugger.setBreakpointByUrl", bpParams);
345
+
346
+ // Re-create the ref entry in the ref table
347
+ const type = (meta.type as string) === "LP" ? "LP" : "BP";
348
+ const newMeta = { ...meta };
349
+ delete newMeta.type; // type is stored in the ref entry, not meta
350
+ if (type === "BP") {
351
+ session.refs.addBreakpoint(r.breakpointId, newMeta);
352
+ } else {
353
+ session.refs.addLogpoint(r.breakpointId, newMeta);
354
+ }
355
+
356
+ session.disabledBreakpoints.delete(ref);
357
+ }
358
+
359
+ export async function getBreakableLocations(
360
+ session: DebugSession,
361
+ file: string,
362
+ startLine: number,
363
+ endLine: number,
364
+ ): Promise<Array<{ line: number; column: number }>> {
365
+ if (!session.cdp) {
366
+ throw new Error("No active debug session");
367
+ }
368
+
369
+ const scriptUrl = session.findScriptUrl(file);
370
+ if (!scriptUrl) {
371
+ throw new Error(`No loaded script matches "${file}"`);
372
+ }
373
+
374
+ // Find the scriptId for this URL
375
+ let scriptId: string | undefined;
376
+ for (const [sid, info] of session.scripts) {
377
+ if (info.url === scriptUrl) {
378
+ scriptId = sid;
379
+ break;
380
+ }
381
+ }
382
+
383
+ if (!scriptId) {
384
+ throw new Error(`No scriptId found for "${file}"`);
385
+ }
386
+
387
+ const r = await session.cdp.send("Debugger.getPossibleBreakpoints", {
388
+ start: { scriptId, lineNumber: startLine - 1 },
389
+ end: { scriptId, lineNumber: endLine },
390
+ });
391
+
392
+ return r.locations.map((loc) => ({
393
+ line: loc.lineNumber + 1, // Convert to 1-based
394
+ column: (loc.columnNumber ?? 0) + 1, // Convert to 1-based
395
+ }));
396
+ }
397
+
398
+ export async function setLogpoint(
399
+ session: DebugSession,
400
+ file: string,
401
+ line: number,
402
+ template: string,
403
+ options?: { condition?: string; maxEmissions?: number },
404
+ ): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
405
+ if (!session.cdp) {
406
+ throw new Error("No active debug session");
407
+ }
408
+
409
+ const url = session.findScriptUrl(file);
410
+
411
+ // Build the logpoint condition: evaluate console.log(...), then return false
412
+ // so execution does not pause.
413
+ let logExpr = `console.log(${template})`;
414
+ if (options?.condition) {
415
+ logExpr = `(${options.condition}) ? (${logExpr}, false) : false`;
416
+ } else {
417
+ logExpr = `${logExpr}, false`;
418
+ }
419
+
420
+ const lpParams: Protocol.Debugger.SetBreakpointByUrlRequest = {
421
+ lineNumber: line - 1, // CDP uses 0-based lines
422
+ condition: logExpr,
423
+ };
424
+ if (url) {
425
+ lpParams.url = url;
426
+ } else {
427
+ lpParams.urlRegex = `${file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
428
+ }
429
+
430
+ const r = await session.cdp.send("Debugger.setBreakpointByUrl", lpParams);
431
+
432
+ const loc = r.locations[0];
433
+ const resolvedUrl = url ?? file;
434
+ const resolvedLine = loc ? loc.lineNumber + 1 : line;
435
+ const resolvedColumn = loc?.columnNumber;
436
+
437
+ const meta: Record<string, unknown> = {
438
+ url: resolvedUrl,
439
+ line: resolvedLine,
440
+ template,
441
+ };
442
+ if (resolvedColumn !== undefined) {
443
+ meta.column = resolvedColumn;
444
+ }
445
+ if (options?.condition) {
446
+ meta.condition = options.condition;
447
+ }
448
+ if (options?.maxEmissions) {
449
+ meta.maxEmissions = options.maxEmissions;
450
+ }
451
+
452
+ const ref = session.refs.addLogpoint(r.breakpointId, meta);
453
+
454
+ const location: { url: string; line: number; column?: number } = {
455
+ url: resolvedUrl,
456
+ line: resolvedLine,
457
+ };
458
+ if (resolvedColumn !== undefined) {
459
+ location.column = resolvedColumn;
460
+ }
461
+
462
+ return { ref, location };
463
+ }
464
+
465
+ export async function setExceptionPause(
466
+ session: DebugSession,
467
+ mode: "all" | "uncaught" | "caught" | "none",
468
+ ): Promise<void> {
469
+ if (!session.cdp) {
470
+ throw new Error("No active debug session");
471
+ }
472
+
473
+ // CDP only supports "none", "all", and "uncaught".
474
+ // Map "caught" to "all" since CDP does not have a "caught-only" mode.
475
+ let cdpState: Protocol.Debugger.SetPauseOnExceptionsRequest["state"];
476
+ switch (mode) {
477
+ case "all":
478
+ cdpState = "all";
479
+ break;
480
+ case "uncaught":
481
+ cdpState = "uncaught";
482
+ break;
483
+ case "caught":
484
+ cdpState = "all";
485
+ break;
486
+ case "none":
487
+ cdpState = "none";
488
+ break;
489
+ }
490
+
491
+ await session.cdp.send("Debugger.setPauseOnExceptions", { state: cdpState });
492
+ }
@@ -0,0 +1,121 @@
1
+ import type { DebugSession } from "./session.ts";
2
+
3
+ export async function continueExecution(session: DebugSession): Promise<void> {
4
+ if (!session.isPaused()) {
5
+ throw new Error("Cannot continue: process is not paused");
6
+ }
7
+ if (!session.cdp) {
8
+ throw new Error("Cannot continue: no CDP connection");
9
+ }
10
+ const waiter = session.createPauseWaiter();
11
+ await session.cdp.send("Debugger.resume");
12
+ await waiter;
13
+ }
14
+
15
+ export async function stepExecution(
16
+ session: DebugSession,
17
+ mode: "over" | "into" | "out",
18
+ ): Promise<void> {
19
+ if (!session.isPaused()) {
20
+ throw new Error("Cannot step: process is not paused");
21
+ }
22
+ if (!session.cdp) {
23
+ throw new Error("Cannot step: no CDP connection");
24
+ }
25
+
26
+ const methodMap = {
27
+ over: "Debugger.stepOver",
28
+ into: "Debugger.stepInto",
29
+ out: "Debugger.stepOut",
30
+ } as const;
31
+
32
+ const waiter = session.createPauseWaiter();
33
+ await session.cdp.send(methodMap[mode]);
34
+ await waiter;
35
+ }
36
+
37
+ export async function pauseExecution(session: DebugSession): Promise<void> {
38
+ if (session.sessionState !== "running") {
39
+ throw new Error("Cannot pause: process is not running");
40
+ }
41
+ if (!session.cdp) {
42
+ throw new Error("Cannot pause: no CDP connection");
43
+ }
44
+ const waiter = session.createPauseWaiter();
45
+ await session.cdp.send("Debugger.pause");
46
+ await waiter;
47
+ }
48
+
49
+ export async function runToLocation(
50
+ session: DebugSession,
51
+ file: string,
52
+ line: number,
53
+ ): Promise<void> {
54
+ if (!session.isPaused()) {
55
+ throw new Error("Cannot run-to: process is not paused");
56
+ }
57
+ if (!session.cdp) {
58
+ throw new Error("Cannot run-to: no CDP connection");
59
+ }
60
+
61
+ // Find the script URL matching the given file (by suffix)
62
+ const scriptUrl = session.findScriptUrl(file);
63
+ if (!scriptUrl) {
64
+ throw new Error(`Cannot run-to: no loaded script matches "${file}"`);
65
+ }
66
+
67
+ // Set a temporary breakpoint (CDP lines are 0-based)
68
+ const bpResult = await session.cdp.send("Debugger.setBreakpointByUrl", {
69
+ lineNumber: line - 1,
70
+ urlRegex: scriptUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
71
+ });
72
+
73
+ const breakpointId = bpResult.breakpointId;
74
+
75
+ // Resume execution — set up waiter before sending resume
76
+ const waiter = session.createPauseWaiter();
77
+ await session.cdp.send("Debugger.resume");
78
+ await waiter;
79
+
80
+ // Remove the temporary breakpoint
81
+ if (breakpointId && session.cdp) {
82
+ try {
83
+ await session.cdp.send("Debugger.removeBreakpoint", { breakpointId });
84
+ } catch {
85
+ // Breakpoint may already be gone if process exited
86
+ }
87
+ }
88
+ }
89
+
90
+ export async function restartFrameExecution(
91
+ session: DebugSession,
92
+ frameRef?: string,
93
+ ): Promise<{ status: string }> {
94
+ if (!session.isPaused()) {
95
+ throw new Error("Cannot restart frame: process is not paused");
96
+ }
97
+ if (!session.cdp) {
98
+ throw new Error("Cannot restart frame: no CDP connection");
99
+ }
100
+
101
+ let callFrameId: string;
102
+ if (frameRef) {
103
+ const entry = session.refs.resolve(frameRef);
104
+ if (!entry) {
105
+ throw new Error(`Unknown frame ref: ${frameRef}`);
106
+ }
107
+ callFrameId = entry.remoteId;
108
+ } else {
109
+ const topFrame = session.pausedCallFrames[0];
110
+ if (!topFrame) {
111
+ throw new Error("No call frames available");
112
+ }
113
+ callFrameId = topFrame.callFrameId;
114
+ }
115
+
116
+ const waiter = session.createPauseWaiter();
117
+ await session.cdp.send("Debugger.restartFrame", { callFrameId, mode: "StepInto" });
118
+ await waiter;
119
+
120
+ return { status: "restarted" };
121
+ }