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,197 @@
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 { DebugSession } from "./session.ts";
5
+
6
+ export async function setVariable(
7
+ session: DebugSession,
8
+ varName: string,
9
+ value: string,
10
+ options: { frame?: string } = {},
11
+ ): Promise<{ name: string; oldValue?: string; newValue: string; type: string }> {
12
+ if (!session.cdp) {
13
+ throw new Error("No active debug session");
14
+ }
15
+ if (!session.isPaused()) {
16
+ throw new Error("Cannot set variable: process is not paused");
17
+ }
18
+
19
+ // Determine which frame to evaluate in
20
+ let callFrameId: string;
21
+ if (options.frame) {
22
+ const entry = session.refs.resolve(options.frame);
23
+ if (entry?.remoteId) {
24
+ callFrameId = entry.remoteId;
25
+ } else {
26
+ throw new Error(`Unknown frame ref: ${options.frame}`);
27
+ }
28
+ } else {
29
+ const topFrame = session.pausedCallFrames[0];
30
+ if (!topFrame) {
31
+ throw new Error("No call frame available");
32
+ }
33
+ callFrameId = topFrame.callFrameId;
34
+ }
35
+
36
+ // Try to get old value first (best-effort)
37
+ let oldValue: string | undefined;
38
+ try {
39
+ const oldResult = await session.cdp.send("Debugger.evaluateOnCallFrame", {
40
+ callFrameId,
41
+ expression: varName,
42
+ returnByValue: false,
43
+ generatePreview: true,
44
+ });
45
+ const oldRemote = oldResult.result as RemoteObject | undefined;
46
+ if (oldRemote) {
47
+ oldValue = formatValue(oldRemote);
48
+ }
49
+ } catch {
50
+ // Old value not available
51
+ }
52
+
53
+ // Set the new value
54
+ const expression = `${varName} = ${value}`;
55
+ const setResult = await session.cdp.send("Debugger.evaluateOnCallFrame", {
56
+ callFrameId,
57
+ expression,
58
+ returnByValue: false,
59
+ generatePreview: true,
60
+ });
61
+
62
+ const evalResult = setResult.result as RemoteObject | undefined;
63
+ const exceptionDetails = setResult.exceptionDetails;
64
+
65
+ if (exceptionDetails) {
66
+ const exception = exceptionDetails.exception as RemoteObject | undefined;
67
+ const errorText = exception
68
+ ? formatValue(exception)
69
+ : (exceptionDetails.text ?? "Assignment error");
70
+ throw new Error(errorText);
71
+ }
72
+
73
+ if (!evalResult) {
74
+ throw new Error("No result from assignment");
75
+ }
76
+
77
+ const result: { name: string; oldValue?: string; newValue: string; type: string } = {
78
+ name: varName,
79
+ newValue: formatValue(evalResult),
80
+ type: evalResult.type,
81
+ };
82
+ if (oldValue !== undefined) {
83
+ result.oldValue = oldValue;
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ export async function setReturnValue(
90
+ session: DebugSession,
91
+ value: string,
92
+ ): Promise<{ value: string; type: string }> {
93
+ if (!session.cdp) {
94
+ throw new Error("No active debug session");
95
+ }
96
+ if (!session.isPaused()) {
97
+ throw new Error("Cannot set return value: process is not paused");
98
+ }
99
+
100
+ const topFrame = session.pausedCallFrames[0];
101
+ if (!topFrame) {
102
+ throw new Error("No call frame available");
103
+ }
104
+
105
+ const callFrameId = topFrame.callFrameId;
106
+
107
+ // Evaluate the value expression to get a RemoteObject
108
+ const evalResult = await session.cdp.send("Debugger.evaluateOnCallFrame", {
109
+ callFrameId,
110
+ expression: value,
111
+ returnByValue: false,
112
+ generatePreview: true,
113
+ });
114
+
115
+ const evalRemote = evalResult.result as RemoteObject | undefined;
116
+ const exceptionDetails = evalResult.exceptionDetails;
117
+
118
+ if (exceptionDetails) {
119
+ const exception = exceptionDetails.exception as RemoteObject | undefined;
120
+ const errorText = exception
121
+ ? formatValue(exception)
122
+ : (exceptionDetails.text ?? "Evaluation error");
123
+ throw new Error(errorText);
124
+ }
125
+
126
+ if (!evalRemote) {
127
+ throw new Error("No result from evaluation");
128
+ }
129
+
130
+ // Set the return value using the evaluated RemoteObject
131
+ // Cast to CallArgument — RemoteObject has the objectId/value/unserializableValue fields that CallArgument needs
132
+ const newValue: Protocol.Runtime.CallArgument = {};
133
+ if (evalRemote.objectId) {
134
+ newValue.objectId = evalRemote.objectId;
135
+ } else if (evalRemote.unserializableValue) {
136
+ newValue.unserializableValue = evalRemote.unserializableValue;
137
+ } else {
138
+ newValue.value = evalRemote.value;
139
+ }
140
+ await session.cdp.send("Debugger.setReturnValue", { newValue });
141
+
142
+ return {
143
+ value: formatValue(evalRemote),
144
+ type: evalRemote.type,
145
+ };
146
+ }
147
+
148
+ export async function hotpatch(
149
+ session: DebugSession,
150
+ file: string,
151
+ newSource: string,
152
+ options: { dryRun?: boolean } = {},
153
+ ): Promise<{ status: string; callFrames?: unknown[]; exceptionDetails?: unknown }> {
154
+ if (!session.cdp) {
155
+ throw new Error("No active debug session");
156
+ }
157
+
158
+ // Find the script URL and then look up the scriptId
159
+ const scriptUrl = session.findScriptUrl(file);
160
+ if (!scriptUrl) {
161
+ throw new Error(`No loaded script matches "${file}"`);
162
+ }
163
+
164
+ let scriptId: string | undefined;
165
+ for (const [sid, info] of session.scripts) {
166
+ if (info.url === scriptUrl) {
167
+ scriptId = sid;
168
+ break;
169
+ }
170
+ }
171
+
172
+ if (!scriptId) {
173
+ throw new Error(`Could not find script ID for "${file}"`);
174
+ }
175
+
176
+ const setSourceParams: Protocol.Debugger.SetScriptSourceRequest = {
177
+ scriptId,
178
+ scriptSource: newSource,
179
+ };
180
+ if (options.dryRun) {
181
+ setSourceParams.dryRun = true;
182
+ }
183
+
184
+ const r = await session.cdp.send("Debugger.setScriptSource", setSourceParams);
185
+
186
+ const response: { status: string; callFrames?: unknown[]; exceptionDetails?: unknown } = {
187
+ status: r.status ?? "Ok",
188
+ };
189
+ if (r.callFrames) {
190
+ response.callFrames = r.callFrames;
191
+ }
192
+ if (r.exceptionDetails) {
193
+ response.exceptionDetails = r.exceptionDetails;
194
+ }
195
+
196
+ return response;
197
+ }
@@ -0,0 +1,258 @@
1
+ import type { RemoteObject } from "../formatter/values.ts";
2
+ import { formatValue } from "../formatter/values.ts";
3
+ import type { DebugSession, StateOptions, StateSnapshot } from "./session.ts";
4
+
5
+ export async function buildState(
6
+ session: DebugSession,
7
+ options: StateOptions = {},
8
+ ): Promise<StateSnapshot> {
9
+ if (session.sessionState !== "paused" || !session.cdp || !session.pauseInfo) {
10
+ return { status: session.sessionState };
11
+ }
12
+
13
+ // Clear volatile refs at the START of building state
14
+ session.refs.clearVolatile();
15
+
16
+ const showAll = !options.vars && !options.stack && !options.breakpoints && !options.code;
17
+ const linesContext = options.lines ?? 3;
18
+ const snapshot: StateSnapshot = {
19
+ status: "paused",
20
+ reason: session.pauseInfo.reason,
21
+ };
22
+
23
+ // Determine which frame to inspect
24
+ let frameIndex = 0;
25
+ if (options.frame) {
26
+ const entry = session.refs.resolve(options.frame);
27
+ if (entry?.meta?.frameIndex !== undefined) {
28
+ frameIndex = entry.meta.frameIndex as number;
29
+ }
30
+ }
31
+
32
+ const callFrames = session.pausedCallFrames;
33
+ const targetFrame = callFrames[frameIndex];
34
+
35
+ if (!targetFrame) {
36
+ return snapshot;
37
+ }
38
+
39
+ const frameLocation = targetFrame.location;
40
+ const frameScriptId = frameLocation?.scriptId;
41
+ const frameLine = frameLocation?.lineNumber ?? 0;
42
+ const frameColumn = frameLocation?.columnNumber;
43
+ let frameUrl = frameScriptId ? (session.scripts.get(frameScriptId)?.url ?? "") : "";
44
+ let displayLine = frameLine + 1; // CDP is 0-based, display 1-based
45
+ let displayColumn = frameColumn !== undefined ? frameColumn + 1 : undefined;
46
+
47
+ // Try source map translation for pause location (unless --generated)
48
+ if (frameScriptId && !options.generated) {
49
+ const resolved = session.resolveOriginalLocation(
50
+ frameScriptId,
51
+ frameLine + 1,
52
+ frameColumn ?? 0,
53
+ );
54
+ if (resolved) {
55
+ frameUrl = resolved.url;
56
+ displayLine = resolved.line;
57
+ displayColumn = resolved.column;
58
+ }
59
+ }
60
+
61
+ snapshot.location = {
62
+ url: frameUrl,
63
+ line: displayLine,
64
+ };
65
+ if (displayColumn !== undefined) {
66
+ snapshot.location.column = displayColumn;
67
+ }
68
+
69
+ // Source code
70
+ if (showAll || options.code) {
71
+ try {
72
+ if (frameScriptId) {
73
+ let scriptSource: string | null = null;
74
+ let useOriginalLines = false;
75
+
76
+ if (!options.generated) {
77
+ // Try to get original source from source map
78
+ const smOriginal = session.sourceMapResolver.toOriginal(
79
+ frameScriptId,
80
+ frameLine + 1,
81
+ frameColumn ?? 0,
82
+ );
83
+ if (smOriginal) {
84
+ scriptSource = session.sourceMapResolver.getOriginalSource(
85
+ frameScriptId,
86
+ smOriginal.source,
87
+ );
88
+ useOriginalLines = scriptSource !== null;
89
+ }
90
+ // Fallback: script has source map but line is unmapped — still show original source
91
+ if (!scriptSource) {
92
+ const primarySource = session.sourceMapResolver.getScriptOriginalUrl(frameScriptId);
93
+ if (primarySource) {
94
+ scriptSource = session.sourceMapResolver.getOriginalSource(
95
+ frameScriptId,
96
+ primarySource,
97
+ );
98
+ useOriginalLines = scriptSource !== null;
99
+ }
100
+ }
101
+ }
102
+
103
+ if (!scriptSource) {
104
+ const sourceResult = await session.cdp.send("Debugger.getScriptSource", {
105
+ scriptId: frameScriptId,
106
+ });
107
+ scriptSource = sourceResult.scriptSource;
108
+ }
109
+
110
+ const sourceLines = scriptSource.split("\n");
111
+ // Use original line for windowing if we have source-mapped content
112
+ const currentLine0 = useOriginalLines ? displayLine - 1 : frameLine;
113
+ const startLine = Math.max(0, currentLine0 - linesContext);
114
+ const endLine = Math.min(sourceLines.length - 1, currentLine0 + linesContext);
115
+
116
+ const lines: Array<{ line: number; text: string; current?: boolean }> = [];
117
+ for (let i = startLine; i <= endLine; i++) {
118
+ const entry: { line: number; text: string; current?: boolean } = {
119
+ line: i + 1, // 1-based
120
+ text: sourceLines[i] ?? "",
121
+ };
122
+ if (i === currentLine0) {
123
+ entry.current = true;
124
+ }
125
+ lines.push(entry);
126
+ }
127
+ snapshot.source = { lines };
128
+ }
129
+ } catch {
130
+ // Source not available
131
+ }
132
+ }
133
+
134
+ // Stack frames
135
+ if (showAll || options.stack) {
136
+ const stackFrames: Array<{
137
+ ref: string;
138
+ functionName: string;
139
+ file: string;
140
+ line: number;
141
+ column?: number;
142
+ isAsync?: boolean;
143
+ }> = [];
144
+
145
+ for (let i = 0; i < callFrames.length; i++) {
146
+ const frame = callFrames[i];
147
+ if (!frame) continue;
148
+ const callFrameId = frame.callFrameId;
149
+ const funcName = frame.functionName || "(anonymous)";
150
+ const loc = frame.location;
151
+ const sid = loc.scriptId;
152
+ const lineNum = loc.lineNumber + 1; // 1-based
153
+ const colNum = loc.columnNumber;
154
+ let url = session.scripts.get(sid)?.url ?? "";
155
+ let stackLine = lineNum;
156
+ let stackCol = colNum !== undefined ? colNum + 1 : undefined;
157
+ let resolvedName: string | null = null;
158
+
159
+ if (!options.generated) {
160
+ const resolved = session.resolveOriginalLocation(sid, lineNum, colNum ?? 0);
161
+ if (resolved) {
162
+ url = resolved.url;
163
+ stackLine = resolved.line;
164
+ stackCol = resolved.column;
165
+ }
166
+ // Get original function name from exact mapping
167
+ const smOriginal = session.sourceMapResolver.toOriginal(sid, lineNum, colNum ?? 0);
168
+ resolvedName = smOriginal?.name ?? null;
169
+ }
170
+
171
+ const ref = session.refs.addFrame(callFrameId, funcName, { frameIndex: i });
172
+
173
+ const stackEntry: {
174
+ ref: string;
175
+ functionName: string;
176
+ file: string;
177
+ line: number;
178
+ column?: number;
179
+ isAsync?: boolean;
180
+ } = {
181
+ ref,
182
+ functionName: resolvedName ?? funcName,
183
+ file: url,
184
+ line: stackLine,
185
+ };
186
+ if (stackCol !== undefined) {
187
+ stackEntry.column = stackCol;
188
+ }
189
+
190
+ stackFrames.push(stackEntry);
191
+ }
192
+
193
+ snapshot.stack = stackFrames;
194
+ }
195
+
196
+ // Local variables
197
+ if (showAll || options.vars) {
198
+ try {
199
+ const scopeChain = targetFrame.scopeChain;
200
+ if (scopeChain) {
201
+ const locals: Array<{ ref: string; name: string; value: string }> = [];
202
+
203
+ for (const scope of scopeChain) {
204
+ const scopeType = scope.type;
205
+
206
+ // By default only show "local" scope; with --all-scopes include "closure" too
207
+ if (scopeType === "local" || (options.allScopes && scopeType === "closure")) {
208
+ const scopeObj = scope.object;
209
+ const objectId = scopeObj.objectId;
210
+ if (!objectId) continue;
211
+
212
+ const propsResult = await session.cdp.send("Runtime.getProperties", {
213
+ objectId,
214
+ ownProperties: true,
215
+ generatePreview: true,
216
+ });
217
+
218
+ const properties = propsResult.result;
219
+
220
+ for (const prop of properties) {
221
+ const propName = prop.name;
222
+ const propValue = prop.value as RemoteObject | undefined;
223
+
224
+ if (!propValue) continue;
225
+
226
+ // Skip internal properties
227
+ if (propName.startsWith("__")) continue;
228
+
229
+ const remoteId = propValue.objectId ?? `primitive:${propName}`;
230
+ const ref = session.refs.addVar(remoteId as string, propName);
231
+
232
+ locals.push({
233
+ ref,
234
+ name: propName,
235
+ value: formatValue(propValue),
236
+ });
237
+ }
238
+ }
239
+
240
+ // Skip "global" scope unless explicitly requested
241
+ if (scopeType === "global") continue;
242
+ }
243
+
244
+ snapshot.locals = locals;
245
+ }
246
+ } catch {
247
+ // Variables not available
248
+ }
249
+ }
250
+
251
+ // Breakpoint count
252
+ if (showAll || options.breakpoints) {
253
+ const bpEntries = session.refs.list("BP");
254
+ snapshot.breakpointCount = bpEntries.length;
255
+ }
256
+
257
+ return snapshot;
258
+ }