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,701 @@
1
+ import type Protocol from "devtools-protocol/types/protocol.js";
2
+ import type { RemoteObject } from "../formatter/values.ts";
3
+ import { formatValue } from "../formatter/values.ts";
4
+ import type { ConsoleMessage, DebugSession, ExceptionEntry } from "./session.ts";
5
+
6
+ export async function evalExpression(
7
+ session: DebugSession,
8
+ expression: string,
9
+ options: {
10
+ frame?: string;
11
+ awaitPromise?: boolean;
12
+ throwOnSideEffect?: boolean;
13
+ timeout?: number;
14
+ } = {},
15
+ ): Promise<{
16
+ ref: string;
17
+ type: string;
18
+ value: string;
19
+ objectId?: string;
20
+ }> {
21
+ if (!session.cdp) {
22
+ throw new Error("No active debug session");
23
+ }
24
+ if (session.sessionState !== "paused") {
25
+ throw new Error("Cannot eval: process is not paused");
26
+ }
27
+
28
+ // Determine which frame to evaluate in
29
+ let frameIndex = 0;
30
+ if (options.frame) {
31
+ const entry = session.refs.resolve(options.frame);
32
+ if (entry?.meta?.frameIndex !== undefined) {
33
+ frameIndex = entry.meta.frameIndex as number;
34
+ }
35
+ }
36
+
37
+ const targetFrame = session.pausedCallFrames[frameIndex];
38
+ if (!targetFrame) {
39
+ throw new Error("No call frame available");
40
+ }
41
+
42
+ const callFrameId = targetFrame.callFrameId;
43
+
44
+ // Resolve @ref patterns in the expression
45
+ let resolvedExpression = expression;
46
+ const refPattern = /@[vof]\d+/g;
47
+ const refMatches = expression.match(refPattern);
48
+ if (refMatches) {
49
+ const refEntries: Array<{
50
+ ref: string;
51
+ name: string;
52
+ objectId: string;
53
+ }> = [];
54
+ for (const ref of refMatches) {
55
+ const remoteId = session.refs.resolveId(ref);
56
+ if (remoteId) {
57
+ const argName = `__ndbg_ref_${ref.slice(1)}`;
58
+ resolvedExpression = resolvedExpression.replace(ref, argName);
59
+ refEntries.push({
60
+ ref,
61
+ name: argName,
62
+ objectId: remoteId,
63
+ });
64
+ }
65
+ }
66
+
67
+ // If we have ref entries, use callFunctionOn to bind them
68
+ if (refEntries.length > 0) {
69
+ const argNames = refEntries.map((e) => e.name);
70
+ const funcBody = `return (function(${argNames.join(", ")}) { return ${resolvedExpression}; })(...arguments)`;
71
+ const firstObjectId = refEntries[0]?.objectId;
72
+ if (!firstObjectId) {
73
+ throw new Error("No object ID for ref resolution");
74
+ }
75
+
76
+ const callFnParams: Protocol.Runtime.CallFunctionOnRequest = {
77
+ functionDeclaration: `function() { ${funcBody} }`,
78
+ arguments: refEntries.map((e) => ({
79
+ objectId: e.objectId,
80
+ })),
81
+ objectId: firstObjectId,
82
+ returnByValue: false,
83
+ generatePreview: true,
84
+ };
85
+
86
+ if (options.awaitPromise) {
87
+ callFnParams.awaitPromise = true;
88
+ }
89
+
90
+ const evalPromise = session.cdp.send("Runtime.callFunctionOn", callFnParams);
91
+
92
+ let evalResponse: Protocol.Runtime.CallFunctionOnResponse;
93
+ if (options.timeout) {
94
+ const timeoutPromise = Bun.sleep(options.timeout).then(() => {
95
+ throw new Error(`Evaluation timed out after ${options.timeout}ms`);
96
+ });
97
+ evalResponse = (await Promise.race([
98
+ evalPromise,
99
+ timeoutPromise,
100
+ ])) as Protocol.Runtime.CallFunctionOnResponse;
101
+ } else {
102
+ evalResponse = await evalPromise;
103
+ }
104
+
105
+ return session.processEvalResult(evalResponse, expression);
106
+ }
107
+ }
108
+
109
+ // Standard evaluation on call frame
110
+ const frameEvalParams: Protocol.Debugger.EvaluateOnCallFrameRequest = {
111
+ callFrameId,
112
+ expression: resolvedExpression,
113
+ returnByValue: false,
114
+ generatePreview: true,
115
+ };
116
+
117
+ if (options.throwOnSideEffect) {
118
+ frameEvalParams.throwOnSideEffect = true;
119
+ }
120
+
121
+ const evalPromise = session.cdp.send("Debugger.evaluateOnCallFrame", frameEvalParams);
122
+
123
+ let evalResponse: Protocol.Debugger.EvaluateOnCallFrameResponse;
124
+ if (options.timeout) {
125
+ const timeoutPromise = Bun.sleep(options.timeout).then(() => {
126
+ throw new Error(`Evaluation timed out after ${options.timeout}ms`);
127
+ });
128
+ evalResponse = (await Promise.race([
129
+ evalPromise,
130
+ timeoutPromise,
131
+ ])) as Protocol.Debugger.EvaluateOnCallFrameResponse;
132
+ } else {
133
+ evalResponse = await evalPromise;
134
+ }
135
+
136
+ return session.processEvalResult(evalResponse, expression);
137
+ }
138
+
139
+ export async function getVars(
140
+ session: DebugSession,
141
+ options: { frame?: string; names?: string[]; allScopes?: boolean } = {},
142
+ ): Promise<Array<{ ref: string; name: string; type: string; value: string }>> {
143
+ if (!session.cdp) {
144
+ throw new Error("No active debug session");
145
+ }
146
+ if (session.sessionState !== "paused") {
147
+ throw new Error("Cannot get vars: process is not paused");
148
+ }
149
+
150
+ // Clear volatile refs at the start
151
+ session.refs.clearVolatile();
152
+
153
+ // Determine which frame to inspect
154
+ let frameIndex = 0;
155
+ if (options.frame) {
156
+ const entry = session.refs.resolve(options.frame);
157
+ if (entry?.meta?.frameIndex !== undefined) {
158
+ frameIndex = entry.meta.frameIndex as number;
159
+ }
160
+ }
161
+
162
+ const targetFrame = session.pausedCallFrames[frameIndex];
163
+ if (!targetFrame) {
164
+ return [];
165
+ }
166
+
167
+ const scopeChain = targetFrame.scopeChain;
168
+ if (!scopeChain) {
169
+ return [];
170
+ }
171
+
172
+ const variables: Array<{
173
+ ref: string;
174
+ name: string;
175
+ type: string;
176
+ value: string;
177
+ }> = [];
178
+
179
+ for (const scope of scopeChain) {
180
+ const scopeType = scope.type;
181
+
182
+ // Include local, module, block, and script scopes by default.
183
+ // Include closure scope only with allScopes. Always skip global.
184
+ const includeScope =
185
+ scopeType === "local" ||
186
+ scopeType === "module" ||
187
+ scopeType === "block" ||
188
+ scopeType === "script" ||
189
+ (options.allScopes && scopeType === "closure");
190
+
191
+ if (includeScope) {
192
+ const scopeObj = scope.object;
193
+ const objectId = scopeObj.objectId;
194
+ if (!objectId) continue;
195
+
196
+ const propsResult = await session.cdp.send("Runtime.getProperties", {
197
+ objectId,
198
+ ownProperties: true,
199
+ generatePreview: true,
200
+ });
201
+
202
+ const properties = propsResult.result;
203
+
204
+ for (const prop of properties) {
205
+ const propName = prop.name;
206
+ const propValue = prop.value as RemoteObject | undefined;
207
+
208
+ if (!propValue) continue;
209
+
210
+ // Skip internal properties
211
+ if (propName.startsWith("__")) continue;
212
+
213
+ // Apply name filter if provided
214
+ if (options.names && options.names.length > 0) {
215
+ if (!options.names.includes(propName)) continue;
216
+ }
217
+
218
+ const remoteId = (propValue.objectId as string) ?? `primitive:${propName}`;
219
+ const ref = session.refs.addVar(remoteId, propName);
220
+
221
+ variables.push({
222
+ ref,
223
+ name: propName,
224
+ type: propValue.type,
225
+ value: formatValue(propValue),
226
+ });
227
+ }
228
+ }
229
+
230
+ // Skip "global" scope
231
+ if (scopeType === "global") continue;
232
+ }
233
+
234
+ return variables;
235
+ }
236
+
237
+ export async function getProps(
238
+ session: DebugSession,
239
+ ref: string,
240
+ options: {
241
+ own?: boolean;
242
+ internal?: boolean;
243
+ depth?: number;
244
+ } = {},
245
+ ): Promise<
246
+ Array<{
247
+ ref?: string;
248
+ name: string;
249
+ type: string;
250
+ value: string;
251
+ isOwn?: boolean;
252
+ isAccessor?: boolean;
253
+ }>
254
+ > {
255
+ if (!session.cdp) {
256
+ throw new Error("No active debug session");
257
+ }
258
+
259
+ const entry = session.refs.resolve(ref);
260
+ if (!entry) {
261
+ throw new Error(`Unknown ref: ${ref}`);
262
+ }
263
+
264
+ const objectId = entry.remoteId;
265
+
266
+ // Verify it's a valid object ID (not a primitive placeholder)
267
+ if (objectId.startsWith("primitive:") || objectId.startsWith("eval:")) {
268
+ throw new Error(`Ref ${ref} is a primitive and has no properties`);
269
+ }
270
+
271
+ const propsParams: Protocol.Runtime.GetPropertiesRequest = {
272
+ objectId,
273
+ ownProperties: options.own ?? true,
274
+ generatePreview: true,
275
+ };
276
+
277
+ if (options.internal) {
278
+ propsParams.accessorPropertiesOnly = false;
279
+ }
280
+
281
+ const propsResult = await session.cdp.send("Runtime.getProperties", propsParams);
282
+ const properties = propsResult.result ?? [];
283
+ const internalProps = options.internal ? (propsResult.internalProperties ?? []) : [];
284
+
285
+ const result: Array<{
286
+ ref?: string;
287
+ name: string;
288
+ type: string;
289
+ value: string;
290
+ isOwn?: boolean;
291
+ isAccessor?: boolean;
292
+ }> = [];
293
+
294
+ for (const prop of properties) {
295
+ const propName = prop.name;
296
+ const propValue = prop.value as RemoteObject | undefined;
297
+ const isOwn = prop.isOwn;
298
+ const getDesc = prop.get as RemoteObject | undefined;
299
+ const setDesc = prop.set as RemoteObject | undefined;
300
+ const isAccessor =
301
+ !!(getDesc?.type && getDesc.type !== "undefined") ||
302
+ !!(setDesc?.type && setDesc.type !== "undefined");
303
+
304
+ if (!propValue && !isAccessor) continue;
305
+
306
+ const displayValue = propValue
307
+ ? propValue
308
+ : ({
309
+ type: "function",
310
+ description: "getter/setter",
311
+ } as RemoteObject);
312
+
313
+ let propRef: string | undefined;
314
+ if (propValue?.objectId) {
315
+ propRef = session.refs.addObject(propValue.objectId, propName);
316
+ }
317
+
318
+ const item: {
319
+ ref?: string;
320
+ name: string;
321
+ type: string;
322
+ value: string;
323
+ isOwn?: boolean;
324
+ isAccessor?: boolean;
325
+ } = {
326
+ name: propName,
327
+ type: displayValue.type,
328
+ value: formatValue(displayValue),
329
+ };
330
+
331
+ if (propRef) {
332
+ item.ref = propRef;
333
+ }
334
+ if (isOwn !== undefined) {
335
+ item.isOwn = isOwn;
336
+ }
337
+ if (isAccessor) {
338
+ item.isAccessor = true;
339
+ }
340
+
341
+ result.push(item);
342
+ }
343
+
344
+ // Add internal properties
345
+ for (const prop of internalProps) {
346
+ const propName = prop.name;
347
+ const propValue = prop.value as RemoteObject | undefined;
348
+
349
+ if (!propValue) continue;
350
+
351
+ let propRef: string | undefined;
352
+ if (propValue.objectId) {
353
+ propRef = session.refs.addObject(propValue.objectId, propName);
354
+ }
355
+
356
+ const item: {
357
+ ref?: string;
358
+ name: string;
359
+ type: string;
360
+ value: string;
361
+ isOwn?: boolean;
362
+ isAccessor?: boolean;
363
+ } = {
364
+ name: `[[${propName}]]`,
365
+ type: propValue.type,
366
+ value: formatValue(propValue),
367
+ };
368
+
369
+ if (propRef) {
370
+ item.ref = propRef;
371
+ }
372
+
373
+ result.push(item);
374
+ }
375
+
376
+ return result;
377
+ }
378
+
379
+ export async function getSource(
380
+ session: DebugSession,
381
+ options: { file?: string; lines?: number; all?: boolean; generated?: boolean } = {},
382
+ ): Promise<{
383
+ url: string;
384
+ lines: Array<{ line: number; text: string; current?: boolean }>;
385
+ }> {
386
+ if (!session.cdp) {
387
+ throw new Error("No active debug session");
388
+ }
389
+
390
+ let scriptId: string | undefined;
391
+ let url = "";
392
+ let currentLine: number | undefined;
393
+
394
+ if (options.file) {
395
+ // Find the script by file name
396
+ const scriptUrl = session.findScriptUrl(options.file);
397
+ if (!scriptUrl) {
398
+ throw new Error(`No loaded script matches "${options.file}"`);
399
+ }
400
+ url = scriptUrl;
401
+ // Find the scriptId for this URL
402
+ for (const [sid, info] of session.scripts) {
403
+ if (info.url === scriptUrl) {
404
+ scriptId = sid;
405
+ break;
406
+ }
407
+ }
408
+ // If we are paused in this file, mark the current line
409
+ if (
410
+ session.sessionState === "paused" &&
411
+ session.pauseInfo &&
412
+ session.pauseInfo.scriptId === scriptId
413
+ ) {
414
+ currentLine = session.pauseInfo.line;
415
+ }
416
+ } else {
417
+ // Use current pause location
418
+ if (session.sessionState !== "paused" || !session.pauseInfo?.scriptId) {
419
+ throw new Error("Not paused; specify --file to view source");
420
+ }
421
+ scriptId = session.pauseInfo.scriptId;
422
+ url = session.scripts.get(scriptId)?.url ?? "";
423
+ currentLine = session.pauseInfo.line;
424
+ }
425
+
426
+ if (!scriptId) {
427
+ throw new Error("Could not determine script to show");
428
+ }
429
+
430
+ // Try to get original source from source map (unless --generated)
431
+ let scriptSource: string | null = null;
432
+ let useOriginalSource = false;
433
+ let originalCurrentLine: number | undefined;
434
+
435
+ if (!options.generated) {
436
+ // Check if this file is being requested by original source path
437
+ const smMatch = session.sourceMapResolver.findScriptForSource(options.file ?? "");
438
+ if (smMatch) {
439
+ scriptId = smMatch.scriptId;
440
+ const origSource = session.sourceMapResolver.getOriginalSource(scriptId, options.file ?? "");
441
+ if (origSource) {
442
+ scriptSource = origSource;
443
+ useOriginalSource = true;
444
+ url = options.file ?? url;
445
+ }
446
+ }
447
+
448
+ // Also try source map for the current scriptId (when paused at a .js file that has a .ts source)
449
+ if (!useOriginalSource) {
450
+ const smInfo = session.sourceMapResolver.getInfo(scriptId);
451
+ if (smInfo && smInfo.sources.length > 0) {
452
+ const primarySource = smInfo.sources[0];
453
+ if (primarySource) {
454
+ const origSource = session.sourceMapResolver.getOriginalSource(scriptId, primarySource);
455
+ if (origSource) {
456
+ scriptSource = origSource;
457
+ useOriginalSource = true;
458
+ url = primarySource;
459
+ // Translate current line to original
460
+ if (currentLine !== undefined) {
461
+ const original = session.sourceMapResolver.toOriginal(scriptId, currentLine + 1, 0);
462
+ if (original) {
463
+ originalCurrentLine = original.line - 1; // 0-based
464
+ }
465
+ }
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ if (!scriptSource) {
473
+ const sourceResult = await session.cdp.send("Debugger.getScriptSource", {
474
+ scriptId,
475
+ });
476
+ scriptSource = sourceResult.scriptSource;
477
+ }
478
+
479
+ const sourceLines = scriptSource.split("\n");
480
+ const effectiveCurrentLine =
481
+ useOriginalSource && originalCurrentLine !== undefined ? originalCurrentLine : currentLine;
482
+
483
+ const linesContext = options.lines ?? 5;
484
+ let startLine: number;
485
+ let endLine: number;
486
+
487
+ if (options.all) {
488
+ startLine = 0;
489
+ endLine = sourceLines.length - 1;
490
+ } else if (effectiveCurrentLine !== undefined) {
491
+ startLine = Math.max(0, effectiveCurrentLine - linesContext);
492
+ endLine = Math.min(sourceLines.length - 1, effectiveCurrentLine + linesContext);
493
+ } else {
494
+ // No current line (viewing a different file while paused), show from the top
495
+ startLine = 0;
496
+ endLine = Math.min(sourceLines.length - 1, linesContext * 2);
497
+ }
498
+
499
+ const lines: Array<{ line: number; text: string; current?: boolean }> = [];
500
+ for (let i = startLine; i <= endLine; i++) {
501
+ const entry: { line: number; text: string; current?: boolean } = {
502
+ line: i + 1, // 1-based
503
+ text: sourceLines[i] ?? "",
504
+ };
505
+ if (effectiveCurrentLine !== undefined && i === effectiveCurrentLine) {
506
+ entry.current = true;
507
+ }
508
+ lines.push(entry);
509
+ }
510
+
511
+ return { url, lines };
512
+ }
513
+
514
+ export function getScripts(
515
+ session: DebugSession,
516
+ filter?: string,
517
+ ): Array<{ scriptId: string; url: string; sourceMapURL?: string }> {
518
+ const result: Array<{ scriptId: string; url: string; sourceMapURL?: string }> = [];
519
+ for (const info of session.scripts.values()) {
520
+ // Filter out empty-URL scripts
521
+ if (!info.url) continue;
522
+ // Apply filter if provided
523
+ if (filter && !info.url.includes(filter)) continue;
524
+
525
+ const entry: { scriptId: string; url: string; sourceMapURL?: string } = {
526
+ scriptId: info.scriptId,
527
+ url: info.url,
528
+ };
529
+ if (info.sourceMapURL) {
530
+ entry.sourceMapURL = info.sourceMapURL;
531
+ }
532
+ result.push(entry);
533
+ }
534
+ return result;
535
+ }
536
+
537
+ export function getStack(
538
+ session: DebugSession,
539
+ options: { asyncDepth?: number; generated?: boolean } = {},
540
+ ): Array<{
541
+ ref: string;
542
+ functionName: string;
543
+ file: string;
544
+ line: number;
545
+ column?: number;
546
+ isAsync?: boolean;
547
+ }> {
548
+ if (session.sessionState !== "paused" || !session.cdp) {
549
+ throw new Error("Not paused");
550
+ }
551
+
552
+ // Clear volatile refs so frame refs are fresh
553
+ session.refs.clearVolatile();
554
+
555
+ const callFrames = session.pausedCallFrames;
556
+ const stackFrames: Array<{
557
+ ref: string;
558
+ functionName: string;
559
+ file: string;
560
+ line: number;
561
+ column?: number;
562
+ isAsync?: boolean;
563
+ }> = [];
564
+
565
+ for (let i = 0; i < callFrames.length; i++) {
566
+ const frame = callFrames[i];
567
+ if (!frame) continue;
568
+ const callFrameId = frame.callFrameId;
569
+ const funcName = frame.functionName || "(anonymous)";
570
+ const loc = frame.location;
571
+ const sid = loc.scriptId;
572
+ const lineNum = loc.lineNumber + 1; // 1-based
573
+ const colNum = loc.columnNumber;
574
+ let url = session.scripts.get(sid)?.url ?? "";
575
+ let displayLine = lineNum;
576
+ let displayCol = colNum !== undefined ? colNum + 1 : undefined;
577
+ let resolvedName: string | null = null;
578
+
579
+ if (!options.generated) {
580
+ const resolved = session.resolveOriginalLocation(sid, lineNum, colNum ?? 0);
581
+ if (resolved) {
582
+ url = resolved.url;
583
+ displayLine = resolved.line;
584
+ displayCol = resolved.column;
585
+ }
586
+ const smOriginal = session.sourceMapResolver.toOriginal(sid, lineNum, colNum ?? 0);
587
+ resolvedName = smOriginal?.name ?? null;
588
+ }
589
+
590
+ const ref = session.refs.addFrame(callFrameId, funcName, { frameIndex: i });
591
+
592
+ const stackEntry: {
593
+ ref: string;
594
+ functionName: string;
595
+ file: string;
596
+ line: number;
597
+ column?: number;
598
+ isAsync?: boolean;
599
+ } = {
600
+ ref,
601
+ functionName: resolvedName ?? funcName,
602
+ file: url,
603
+ line: displayLine,
604
+ };
605
+ if (displayCol !== undefined) {
606
+ stackEntry.column = displayCol;
607
+ }
608
+
609
+ stackFrames.push(stackEntry);
610
+ }
611
+
612
+ return stackFrames;
613
+ }
614
+
615
+ export async function searchInScripts(
616
+ session: DebugSession,
617
+ query: string,
618
+ options: {
619
+ scriptId?: string;
620
+ isRegex?: boolean;
621
+ caseSensitive?: boolean;
622
+ } = {},
623
+ ): Promise<Array<{ url: string; line: number; column: number; content: string }>> {
624
+ if (!session.cdp) {
625
+ throw new Error("No active debug session");
626
+ }
627
+
628
+ const results: Array<{ url: string; line: number; column: number; content: string }> = [];
629
+
630
+ const scriptsToSearch: Array<{ scriptId: string; url: string }> = [];
631
+
632
+ if (options.scriptId) {
633
+ const info = session.scripts.get(options.scriptId);
634
+ if (info) {
635
+ scriptsToSearch.push({ scriptId: options.scriptId, url: info.url });
636
+ }
637
+ } else {
638
+ for (const [sid, info] of session.scripts) {
639
+ if (!info.url) continue;
640
+ scriptsToSearch.push({ scriptId: sid, url: info.url });
641
+ }
642
+ }
643
+
644
+ for (const script of scriptsToSearch) {
645
+ try {
646
+ const searchResult = await session.cdp.send("Debugger.searchInContent", {
647
+ scriptId: script.scriptId,
648
+ query,
649
+ isRegex: options.isRegex ?? false,
650
+ caseSensitive: options.caseSensitive ?? false,
651
+ });
652
+ const matches = searchResult.result;
653
+ if (matches) {
654
+ for (const match of matches) {
655
+ results.push({
656
+ url: script.url,
657
+ line: (match.lineNumber ?? 0) + 1, // 1-based
658
+ column: 1, // SearchMatch doesn't provide column
659
+ content: match.lineContent ?? "",
660
+ });
661
+ }
662
+ }
663
+ } catch {
664
+ // Script may have been garbage collected, skip
665
+ }
666
+ }
667
+
668
+ return results;
669
+ }
670
+
671
+ export function getConsoleMessages(
672
+ session: DebugSession,
673
+ options: { level?: string; since?: number; clear?: boolean } = {},
674
+ ): ConsoleMessage[] {
675
+ let messages = [...session.consoleMessages];
676
+ if (options.level) {
677
+ messages = messages.filter((m) => m.level === options.level);
678
+ }
679
+ if (options.since !== undefined && options.since > 0) {
680
+ messages = messages.slice(-options.since);
681
+ }
682
+ if (options.clear) {
683
+ session.consoleMessages = [];
684
+ }
685
+ return messages;
686
+ }
687
+
688
+ export function getExceptions(
689
+ session: DebugSession,
690
+ options: { since?: number } = {},
691
+ ): ExceptionEntry[] {
692
+ let entries = [...session.exceptionEntries];
693
+ if (options.since !== undefined && options.since > 0) {
694
+ entries = entries.slice(-options.since);
695
+ }
696
+ return entries;
697
+ }
698
+
699
+ export function clearConsole(session: DebugSession): void {
700
+ session.consoleMessages = [];
701
+ }