reflexive 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.
- package/CLAUDE.md +77 -0
- package/FAILURES.md +245 -0
- package/README.md +264 -0
- package/Screenshot 2026-01-22 at 6.31.27/342/200/257AM.png +0 -0
- package/dashboard.html +620 -0
- package/demo-ai-features.js +571 -0
- package/demo-app.js +210 -0
- package/demo-inject.js +212 -0
- package/demo-instrumented.js +272 -0
- package/docs/BREAKPOINT-AUDIT.md +293 -0
- package/docs/GENESIS.md +110 -0
- package/docs/HN-LAUNCH-PLAN-V2.md +631 -0
- package/docs/HN-LAUNCH-PLAN.md +492 -0
- package/docs/TODO.md +69 -0
- package/docs/V8-INSPECTOR-RESEARCH.md +1231 -0
- package/logo-carbon.png +0 -0
- package/logo0.jpg +0 -0
- package/logo1.jpg +0 -0
- package/logo2.jpg +0 -0
- package/new-ui-template.html +435 -0
- package/one-shot.js +1109 -0
- package/package.json +47 -0
- package/play-story.sh +10 -0
- package/src/demo-inject.js +3 -0
- package/src/inject.cjs +474 -0
- package/src/reflexive.js +6214 -0
- package/story-game-reflexive.js +1246 -0
- package/story-game-web.js +1030 -0
- package/story-mystery-1769171430377.js +162 -0
|
@@ -0,0 +1,1231 @@
|
|
|
1
|
+
# V8 Inspector Protocol Research
|
|
2
|
+
|
|
3
|
+
Comprehensive research for implementing real debugger breakpoints in Reflexive.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [V8 Inspector Protocol Basics](#1-v8-inspector-protocol-basics)
|
|
8
|
+
2. [Setting Breakpoints Programmatically](#2-setting-breakpoints-programmatically)
|
|
9
|
+
3. [Node.js Inspector APIs](#3-nodejs-inspector-apis)
|
|
10
|
+
4. [Practical Implementation for Reflexive](#4-practical-implementation-for-reflexive)
|
|
11
|
+
5. [NPM Packages and Alternatives](#5-npm-packages-and-alternatives)
|
|
12
|
+
6. [Security Considerations](#6-security-considerations)
|
|
13
|
+
7. [Complete Code Examples](#7-complete-code-examples)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. V8 Inspector Protocol Basics
|
|
18
|
+
|
|
19
|
+
### What is the V8 Inspector?
|
|
20
|
+
|
|
21
|
+
The V8 Inspector is a debugging protocol built into V8 (and thus Node.js) that allows external tools to:
|
|
22
|
+
- Set breakpoints and step through code
|
|
23
|
+
- Inspect variables and call stacks
|
|
24
|
+
- Profile CPU and memory usage
|
|
25
|
+
- Execute arbitrary JavaScript in the runtime context
|
|
26
|
+
|
|
27
|
+
It uses the **Chrome DevTools Protocol (CDP)** - the same protocol Chrome uses for its DevTools.
|
|
28
|
+
|
|
29
|
+
### How Chrome DevTools Connects to Node.js
|
|
30
|
+
|
|
31
|
+
When you start Node.js with the `--inspect` flag:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node --inspect app.js
|
|
35
|
+
# Output: Debugger listening on ws://127.0.0.1:9229/uuid-here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Node.js:
|
|
39
|
+
1. Opens a WebSocket server on port 9229 (default)
|
|
40
|
+
2. Exposes HTTP endpoints for discovery
|
|
41
|
+
3. Accepts CDP commands over the WebSocket connection
|
|
42
|
+
|
|
43
|
+
Chrome DevTools (or any CDP client) can then:
|
|
44
|
+
1. Discover targets via `http://localhost:9229/json/list`
|
|
45
|
+
2. Connect to the WebSocket URL
|
|
46
|
+
3. Send JSON-RPC commands and receive events
|
|
47
|
+
|
|
48
|
+
### The `--inspect` Flag Variants
|
|
49
|
+
|
|
50
|
+
| Flag | Behavior |
|
|
51
|
+
|------|----------|
|
|
52
|
+
| `--inspect` | Enable inspector, continue execution immediately |
|
|
53
|
+
| `--inspect-brk` | Enable inspector, pause on first line (wait for debugger) |
|
|
54
|
+
| `--inspect=host:port` | Specify custom host/port |
|
|
55
|
+
| `--inspect-brk=0` | Use random available port |
|
|
56
|
+
|
|
57
|
+
### Discovery Endpoints
|
|
58
|
+
|
|
59
|
+
When inspector is active, these HTTP endpoints are available:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# List all debuggable targets
|
|
63
|
+
curl http://localhost:9229/json/list
|
|
64
|
+
|
|
65
|
+
# Get version and WebSocket URL
|
|
66
|
+
curl http://localhost:9229/json/version
|
|
67
|
+
|
|
68
|
+
# Protocol schema
|
|
69
|
+
curl http://localhost:9229/json/protocol
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Example response from `/json/list`:
|
|
73
|
+
```json
|
|
74
|
+
[{
|
|
75
|
+
"description": "node.js instance",
|
|
76
|
+
"devtoolsFrontendUrl": "devtools://devtools/bundled/...",
|
|
77
|
+
"id": "uuid-here",
|
|
78
|
+
"title": "app.js",
|
|
79
|
+
"type": "node",
|
|
80
|
+
"url": "file:///path/to/app.js",
|
|
81
|
+
"webSocketDebuggerUrl": "ws://127.0.0.1:9229/uuid-here"
|
|
82
|
+
}]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. Setting Breakpoints Programmatically
|
|
88
|
+
|
|
89
|
+
### CDP Debugger Domain
|
|
90
|
+
|
|
91
|
+
The Chrome DevTools Protocol's `Debugger` domain provides breakpoint functionality.
|
|
92
|
+
|
|
93
|
+
### Key Methods for Breakpoints
|
|
94
|
+
|
|
95
|
+
#### `Debugger.enable`
|
|
96
|
+
Must be called first to enable debugging capabilities.
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
session.post('Debugger.enable');
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### `Debugger.setBreakpointByUrl`
|
|
103
|
+
Set breakpoint by file URL (preferred for most cases).
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
session.post('Debugger.setBreakpointByUrl', {
|
|
107
|
+
lineNumber: 10, // 0-based line number
|
|
108
|
+
url: 'file:///path/to/app.js', // or use urlRegex
|
|
109
|
+
columnNumber: 0, // optional, 0-based
|
|
110
|
+
condition: 'x > 5' // optional conditional expression
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Advantages:**
|
|
115
|
+
- Can set breakpoints BEFORE scripts load
|
|
116
|
+
- Survives page reloads
|
|
117
|
+
- Uses file paths (more intuitive)
|
|
118
|
+
|
|
119
|
+
#### `Debugger.setBreakpoint`
|
|
120
|
+
Set breakpoint by script ID (for already-loaded scripts).
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
session.post('Debugger.setBreakpoint', {
|
|
124
|
+
location: {
|
|
125
|
+
scriptId: '104', // from Debugger.scriptParsed event
|
|
126
|
+
lineNumber: 10,
|
|
127
|
+
columnNumber: 0
|
|
128
|
+
},
|
|
129
|
+
condition: 'x > 5'
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Use when:**
|
|
134
|
+
- You already have the scriptId from `Debugger.scriptParsed`
|
|
135
|
+
- Targeting a specific instance of a script
|
|
136
|
+
|
|
137
|
+
#### `Debugger.setBreakpointsActive`
|
|
138
|
+
Enable/disable all breakpoints globally.
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
session.post('Debugger.setBreakpointsActive', { active: false });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### `Debugger.removeBreakpoint`
|
|
145
|
+
Remove a breakpoint by ID.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
session.post('Debugger.removeBreakpoint', {
|
|
149
|
+
breakpointId: 'file:///path/to/app.js:10:0'
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Execution Control
|
|
154
|
+
|
|
155
|
+
#### `Debugger.pause`
|
|
156
|
+
Pause execution immediately.
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
session.post('Debugger.pause');
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `Debugger.resume`
|
|
163
|
+
Resume execution after pause.
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
session.post('Debugger.resume');
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `Debugger.stepOver` / `Debugger.stepInto` / `Debugger.stepOut`
|
|
170
|
+
Step through code.
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
session.post('Debugger.stepOver');
|
|
174
|
+
session.post('Debugger.stepInto');
|
|
175
|
+
session.post('Debugger.stepOut');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Key Events
|
|
179
|
+
|
|
180
|
+
#### `Debugger.paused`
|
|
181
|
+
Fired when execution pauses (breakpoint hit, exception, etc.).
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
session.on('Debugger.paused', (message) => {
|
|
185
|
+
const { callFrames, reason, hitBreakpoints } = message.params;
|
|
186
|
+
console.log('Paused:', reason);
|
|
187
|
+
console.log('At breakpoints:', hitBreakpoints);
|
|
188
|
+
console.log('Call stack:', callFrames);
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The `reason` field indicates why we paused:
|
|
193
|
+
- `breakpoint` - Hit a breakpoint
|
|
194
|
+
- `exception` - Exception thrown
|
|
195
|
+
- `step` - Step operation completed
|
|
196
|
+
- `debugCommand` - `debugger` statement
|
|
197
|
+
- `other` - Other reasons
|
|
198
|
+
|
|
199
|
+
#### `Debugger.scriptParsed`
|
|
200
|
+
Fired when a new script is loaded.
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
session.on('Debugger.scriptParsed', (message) => {
|
|
204
|
+
const { scriptId, url, startLine, endLine } = message.params;
|
|
205
|
+
// Can now use scriptId for Debugger.setBreakpoint
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `Debugger.breakpointResolved`
|
|
210
|
+
Fired when a breakpoint is resolved to an actual location.
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
session.on('Debugger.breakpointResolved', (message) => {
|
|
214
|
+
const { breakpointId, location } = message.params;
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Exception Handling
|
|
219
|
+
|
|
220
|
+
#### `Debugger.setPauseOnExceptions`
|
|
221
|
+
Control exception behavior.
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
session.post('Debugger.setPauseOnExceptions', {
|
|
225
|
+
state: 'all' // 'none', 'uncaught', or 'all'
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 3. Node.js Inspector APIs
|
|
232
|
+
|
|
233
|
+
Node.js provides the `node:inspector` module for programmatic access.
|
|
234
|
+
|
|
235
|
+
### Basic Usage
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
import * as inspector from 'node:inspector';
|
|
239
|
+
// Or with promises:
|
|
240
|
+
import { Session } from 'node:inspector/promises';
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Opening the Inspector
|
|
244
|
+
|
|
245
|
+
#### `inspector.open([port], [host], [wait])`
|
|
246
|
+
Programmatically enable the inspector.
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
import * as inspector from 'node:inspector';
|
|
250
|
+
|
|
251
|
+
// Open on default port (9229)
|
|
252
|
+
inspector.open();
|
|
253
|
+
|
|
254
|
+
// Open on custom port
|
|
255
|
+
inspector.open(9230);
|
|
256
|
+
|
|
257
|
+
// Open and wait for debugger to attach
|
|
258
|
+
inspector.open(9229, '127.0.0.1', true);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### `inspector.url()`
|
|
262
|
+
Get the WebSocket URL for the inspector.
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const wsUrl = inspector.url();
|
|
266
|
+
// Returns: ws://127.0.0.1:9229/uuid-here
|
|
267
|
+
// Or undefined if inspector not active
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### `inspector.waitForDebugger()`
|
|
271
|
+
Block until a debugger connects.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
inspector.open();
|
|
275
|
+
console.log('Debugger URL:', inspector.url());
|
|
276
|
+
inspector.waitForDebugger(); // Blocks here
|
|
277
|
+
console.log('Debugger connected!');
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `inspector.close()`
|
|
281
|
+
Deactivate the inspector.
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
inspector.close();
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Inspector Session
|
|
288
|
+
|
|
289
|
+
The `Session` class is the primary interface for CDP commands.
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
import { Session } from 'node:inspector/promises';
|
|
293
|
+
|
|
294
|
+
const session = new Session();
|
|
295
|
+
session.connect(); // Connect to current process
|
|
296
|
+
|
|
297
|
+
// Send CDP commands
|
|
298
|
+
await session.post('Debugger.enable');
|
|
299
|
+
|
|
300
|
+
// Clean up
|
|
301
|
+
session.disconnect();
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Session Connection Modes
|
|
305
|
+
|
|
306
|
+
#### `session.connect()`
|
|
307
|
+
Connect to the **current process** (same thread).
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
const session = new Session();
|
|
311
|
+
session.connect();
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Warning:** Setting breakpoints with `session.connect()` will pause the debugger itself since it's running in the same thread.
|
|
315
|
+
|
|
316
|
+
#### `session.connectToMainThread()`
|
|
317
|
+
From a worker thread, connect to the **main thread**.
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
// Inside a Worker
|
|
321
|
+
import { Session } from 'node:inspector/promises';
|
|
322
|
+
import { isMainThread } from 'node:worker_threads';
|
|
323
|
+
|
|
324
|
+
if (!isMainThread) {
|
|
325
|
+
const session = new Session();
|
|
326
|
+
session.connectToMainThread();
|
|
327
|
+
// Now can debug main thread from worker
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Warning:** Can cause deadlocks if the worker suspends itself.
|
|
332
|
+
|
|
333
|
+
### Callback vs Promise API
|
|
334
|
+
|
|
335
|
+
Node.js offers both callback and promise-based APIs:
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
// Callback API
|
|
339
|
+
import inspector from 'node:inspector';
|
|
340
|
+
const session = new inspector.Session();
|
|
341
|
+
session.connect();
|
|
342
|
+
session.post('Runtime.evaluate', { expression: '2 + 2' }, (err, result) => {
|
|
343
|
+
console.log(result); // { result: { type: 'number', value: 4 } }
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Promise API (recommended)
|
|
347
|
+
import { Session } from 'node:inspector/promises';
|
|
348
|
+
const session = new Session();
|
|
349
|
+
session.connect();
|
|
350
|
+
const result = await session.post('Runtime.evaluate', { expression: '2 + 2' });
|
|
351
|
+
console.log(result);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 4. Practical Implementation for Reflexive
|
|
357
|
+
|
|
358
|
+
### Architecture Options
|
|
359
|
+
|
|
360
|
+
#### Option A: Library Mode (Embedded Agent)
|
|
361
|
+
|
|
362
|
+
When Reflexive runs inside the target app, use the inspector module directly:
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
// reflexive.js - Library mode debugger support
|
|
366
|
+
import * as inspector from 'node:inspector';
|
|
367
|
+
import { Session } from 'node:inspector/promises';
|
|
368
|
+
|
|
369
|
+
class ReflexiveDebugger {
|
|
370
|
+
constructor() {
|
|
371
|
+
this.session = null;
|
|
372
|
+
this.breakpoints = new Map();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async enable() {
|
|
376
|
+
// Open inspector if not already open
|
|
377
|
+
if (!inspector.url()) {
|
|
378
|
+
inspector.open(0); // Use random port
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.session = new Session();
|
|
382
|
+
this.session.connect();
|
|
383
|
+
|
|
384
|
+
await this.session.post('Debugger.enable');
|
|
385
|
+
|
|
386
|
+
// Listen for pause events
|
|
387
|
+
this.session.on('Debugger.paused', (msg) => {
|
|
388
|
+
this.handlePause(msg.params);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return inspector.url();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async setBreakpoint(file, line, condition) {
|
|
395
|
+
const result = await this.session.post('Debugger.setBreakpointByUrl', {
|
|
396
|
+
lineNumber: line - 1, // Convert to 0-based
|
|
397
|
+
url: `file://${file}`,
|
|
398
|
+
condition: condition || ''
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
this.breakpoints.set(result.breakpointId, { file, line, condition });
|
|
402
|
+
return result.breakpointId;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async removeBreakpoint(breakpointId) {
|
|
406
|
+
await this.session.post('Debugger.removeBreakpoint', { breakpointId });
|
|
407
|
+
this.breakpoints.delete(breakpointId);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
handlePause(params) {
|
|
411
|
+
const { callFrames, reason, hitBreakpoints } = params;
|
|
412
|
+
// Emit event or call callback with pause info
|
|
413
|
+
console.log(`Paused: ${reason} at ${hitBreakpoints?.join(', ')}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async resume() {
|
|
417
|
+
await this.session.post('Debugger.resume');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async stepOver() {
|
|
421
|
+
await this.session.post('Debugger.stepOver');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Limitation:** In library mode, the debugger runs in the same thread as the target code. When a breakpoint hits, the entire process pauses - including Reflexive's chat interface.
|
|
427
|
+
|
|
428
|
+
#### Option B: CLI Mode (External Process)
|
|
429
|
+
|
|
430
|
+
When Reflexive spawns the target process, connect via WebSocket:
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
// reflexive.js - CLI mode debugger support
|
|
434
|
+
import WebSocket from 'ws';
|
|
435
|
+
|
|
436
|
+
class RemoteDebugger {
|
|
437
|
+
constructor() {
|
|
438
|
+
this.ws = null;
|
|
439
|
+
this.messageId = 0;
|
|
440
|
+
this.pending = new Map();
|
|
441
|
+
this.eventHandlers = new Map();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async connect(wsUrl) {
|
|
445
|
+
return new Promise((resolve, reject) => {
|
|
446
|
+
this.ws = new WebSocket(wsUrl);
|
|
447
|
+
|
|
448
|
+
this.ws.on('open', () => {
|
|
449
|
+
resolve();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
this.ws.on('message', (data) => {
|
|
453
|
+
const msg = JSON.parse(data);
|
|
454
|
+
|
|
455
|
+
if (msg.id !== undefined) {
|
|
456
|
+
// Response to a command
|
|
457
|
+
const { resolve, reject } = this.pending.get(msg.id);
|
|
458
|
+
this.pending.delete(msg.id);
|
|
459
|
+
|
|
460
|
+
if (msg.error) {
|
|
461
|
+
reject(new Error(msg.error.message));
|
|
462
|
+
} else {
|
|
463
|
+
resolve(msg.result);
|
|
464
|
+
}
|
|
465
|
+
} else if (msg.method) {
|
|
466
|
+
// Event notification
|
|
467
|
+
const handler = this.eventHandlers.get(msg.method);
|
|
468
|
+
if (handler) {
|
|
469
|
+
handler(msg.params);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
this.ws.on('error', reject);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
send(method, params = {}) {
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
const id = ++this.messageId;
|
|
481
|
+
this.pending.set(id, { resolve, reject });
|
|
482
|
+
|
|
483
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
on(event, handler) {
|
|
488
|
+
this.eventHandlers.set(event, handler);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async enable() {
|
|
492
|
+
await this.send('Debugger.enable');
|
|
493
|
+
await this.send('Runtime.enable');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async setBreakpoint(file, line, condition) {
|
|
497
|
+
return await this.send('Debugger.setBreakpointByUrl', {
|
|
498
|
+
lineNumber: line - 1,
|
|
499
|
+
url: `file://${file}`,
|
|
500
|
+
condition: condition || ''
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Attaching to a Running Process
|
|
507
|
+
|
|
508
|
+
#### Method 1: Start with `--inspect`
|
|
509
|
+
|
|
510
|
+
The simplest approach - start the target with inspector enabled:
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
// In ProcessManager class
|
|
514
|
+
spawnChild(script, args) {
|
|
515
|
+
const child = spawn('node', [
|
|
516
|
+
'--inspect=0', // Random port
|
|
517
|
+
script,
|
|
518
|
+
...args
|
|
519
|
+
]);
|
|
520
|
+
|
|
521
|
+
// Parse inspector URL from stderr
|
|
522
|
+
child.stderr.on('data', (data) => {
|
|
523
|
+
const match = data.toString().match(/ws:\/\/[\d.]+:\d+\/[\w-]+/);
|
|
524
|
+
if (match) {
|
|
525
|
+
this.inspectorUrl = match[0];
|
|
526
|
+
this.emit('inspector-ready', this.inspectorUrl);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Method 2: Enable Inspector on Running Process (Unix)
|
|
533
|
+
|
|
534
|
+
Send SIGUSR1 to enable inspector on a process started without `--inspect`:
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
import { spawn } from 'child_process';
|
|
538
|
+
|
|
539
|
+
// Enable inspector on running process (Unix only)
|
|
540
|
+
function enableInspector(pid) {
|
|
541
|
+
process.kill(pid, 'SIGUSR1');
|
|
542
|
+
// Process will start listening on default port 9229
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### Method 3: Enable Inspector on Running Process (Windows)
|
|
547
|
+
|
|
548
|
+
Use `process._debugProcess(pid)` from another Node.js process:
|
|
549
|
+
|
|
550
|
+
```javascript
|
|
551
|
+
// Windows only - run in a separate process
|
|
552
|
+
process._debugProcess(targetPid);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
#### Method 4: Programmatic Enable (if you control the target)
|
|
556
|
+
|
|
557
|
+
If the target app uses Reflexive library mode:
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
import * as inspector from 'node:inspector';
|
|
561
|
+
|
|
562
|
+
// Can be called at any time
|
|
563
|
+
export function enableDebugging() {
|
|
564
|
+
if (!inspector.url()) {
|
|
565
|
+
inspector.open(0, '127.0.0.1');
|
|
566
|
+
return inspector.url();
|
|
567
|
+
}
|
|
568
|
+
return inspector.url();
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Handling the Pause Problem
|
|
573
|
+
|
|
574
|
+
When a breakpoint hits in library mode, the event loop stops. Solutions:
|
|
575
|
+
|
|
576
|
+
#### Solution 1: Worker Thread Debugger
|
|
577
|
+
|
|
578
|
+
Run the debugger logic in a worker thread:
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
// main.js
|
|
582
|
+
import { Worker } from 'worker_threads';
|
|
583
|
+
|
|
584
|
+
const debuggerWorker = new Worker('./debugger-worker.js');
|
|
585
|
+
|
|
586
|
+
debuggerWorker.on('message', (msg) => {
|
|
587
|
+
if (msg.type === 'paused') {
|
|
588
|
+
// Breakpoint hit - but we can still handle this message
|
|
589
|
+
// because the worker is in a different thread
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// debugger-worker.js
|
|
594
|
+
import { parentPort } from 'worker_threads';
|
|
595
|
+
import { Session } from 'node:inspector/promises';
|
|
596
|
+
|
|
597
|
+
const session = new Session();
|
|
598
|
+
session.connectToMainThread();
|
|
599
|
+
|
|
600
|
+
await session.post('Debugger.enable');
|
|
601
|
+
|
|
602
|
+
session.on('Debugger.paused', (msg) => {
|
|
603
|
+
parentPort.postMessage({ type: 'paused', data: msg.params });
|
|
604
|
+
});
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### Solution 2: Separate Debugger Process
|
|
608
|
+
|
|
609
|
+
Run the debugger UI in a separate process that connects via WebSocket:
|
|
610
|
+
|
|
611
|
+
```
|
|
612
|
+
[Target Process] <--WebSocket--> [Reflexive Debugger Process]
|
|
613
|
+
(paused) (still running)
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### Solution 3: Use `--inspect-brk` + Lazy Connection
|
|
617
|
+
|
|
618
|
+
Start paused, connect, set breakpoints, then resume:
|
|
619
|
+
|
|
620
|
+
```javascript
|
|
621
|
+
const child = spawn('node', ['--inspect-brk=0', 'app.js']);
|
|
622
|
+
|
|
623
|
+
// Wait for inspector URL
|
|
624
|
+
child.stderr.once('data', async (data) => {
|
|
625
|
+
const url = parseInspectorUrl(data);
|
|
626
|
+
|
|
627
|
+
const debugger = new RemoteDebugger();
|
|
628
|
+
await debugger.connect(url);
|
|
629
|
+
await debugger.enable();
|
|
630
|
+
|
|
631
|
+
// Set initial breakpoints
|
|
632
|
+
await debugger.setBreakpoint('/path/to/app.js', 15);
|
|
633
|
+
|
|
634
|
+
// Now resume from initial pause
|
|
635
|
+
await debugger.send('Debugger.resume');
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## 5. NPM Packages and Alternatives
|
|
642
|
+
|
|
643
|
+
### chrome-remote-interface
|
|
644
|
+
|
|
645
|
+
The most popular CDP client for Node.js.
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
npm install chrome-remote-interface
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
```javascript
|
|
652
|
+
import CDP from 'chrome-remote-interface';
|
|
653
|
+
|
|
654
|
+
// Connect to Node.js inspector
|
|
655
|
+
const client = await CDP({ port: 9229 });
|
|
656
|
+
|
|
657
|
+
const { Debugger, Runtime } = client;
|
|
658
|
+
|
|
659
|
+
await Debugger.enable();
|
|
660
|
+
|
|
661
|
+
Debugger.paused((params) => {
|
|
662
|
+
console.log('Paused:', params);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
await Debugger.setBreakpointByUrl({
|
|
666
|
+
lineNumber: 10,
|
|
667
|
+
url: 'file:///path/to/app.js'
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await Runtime.runIfWaitingForDebugger();
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
**Pros:**
|
|
674
|
+
- Full CDP support
|
|
675
|
+
- Well-maintained
|
|
676
|
+
- TypeScript definitions available
|
|
677
|
+
|
|
678
|
+
**Cons:**
|
|
679
|
+
- Additional dependency (~50KB)
|
|
680
|
+
|
|
681
|
+
### ws (WebSocket)
|
|
682
|
+
|
|
683
|
+
If you want minimal dependencies, use `ws` directly:
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
npm install ws
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
Then implement the CDP protocol yourself (see Section 4).
|
|
690
|
+
|
|
691
|
+
### ndb
|
|
692
|
+
|
|
693
|
+
Google's enhanced Node.js debugger using Chrome DevTools.
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
npm install -g ndb
|
|
697
|
+
ndb node app.js
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**How it works:**
|
|
701
|
+
- Bundles Puppeteer (which includes Chromium)
|
|
702
|
+
- Uses CDP to communicate with Node.js
|
|
703
|
+
- Provides Chrome DevTools UI
|
|
704
|
+
|
|
705
|
+
**Relevance to Reflexive:**
|
|
706
|
+
- Shows that CDP is the right approach
|
|
707
|
+
- Demonstrates worker thread debugging
|
|
708
|
+
- Shows file editing via DevTools is possible
|
|
709
|
+
|
|
710
|
+
### Built-in Node.js Debugger
|
|
711
|
+
|
|
712
|
+
Node.js includes a simple CLI debugger:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
node inspect app.js
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Commands:
|
|
719
|
+
- `cont`, `c` - Continue
|
|
720
|
+
- `next`, `n` - Step over
|
|
721
|
+
- `step`, `s` - Step into
|
|
722
|
+
- `out`, `o` - Step out
|
|
723
|
+
- `setBreakpoint('file.js', line)`, `sb()` - Set breakpoint
|
|
724
|
+
- `clearBreakpoint()`, `cb()` - Clear breakpoint
|
|
725
|
+
- `repl` - Enter REPL
|
|
726
|
+
|
|
727
|
+
**Limitation:** CLI only, not suitable for programmatic use.
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## 6. Security Considerations
|
|
732
|
+
|
|
733
|
+
### The Inspector is Dangerous
|
|
734
|
+
|
|
735
|
+
The V8 inspector provides **full access** to the Node.js execution environment:
|
|
736
|
+
|
|
737
|
+
- Execute arbitrary JavaScript
|
|
738
|
+
- Read/write any variable
|
|
739
|
+
- Access file system (if the app can)
|
|
740
|
+
- Make network requests
|
|
741
|
+
- Access environment variables and secrets
|
|
742
|
+
|
|
743
|
+
### Security Best Practices
|
|
744
|
+
|
|
745
|
+
#### 1. Bind to Localhost Only
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
node --inspect=127.0.0.1:9229 app.js # Good
|
|
749
|
+
node --inspect=0.0.0.0:9229 app.js # DANGEROUS
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
#### 2. Use Random Ports
|
|
753
|
+
|
|
754
|
+
```bash
|
|
755
|
+
node --inspect=0 app.js # Uses random available port
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
#### 3. Require Authentication (Future)
|
|
759
|
+
|
|
760
|
+
Node.js is working on inspector authentication, but it's not yet available.
|
|
761
|
+
|
|
762
|
+
#### 4. Use Firewall Rules
|
|
763
|
+
|
|
764
|
+
Block external access to inspector ports.
|
|
765
|
+
|
|
766
|
+
#### 5. Don't Enable in Production
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
770
|
+
inspector.open();
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Implications for Reflexive
|
|
775
|
+
|
|
776
|
+
Reflexive should:
|
|
777
|
+
- Only bind inspector to localhost
|
|
778
|
+
- Use random ports when possible
|
|
779
|
+
- Warn users about security implications
|
|
780
|
+
- Consider authentication tokens for WebSocket connections
|
|
781
|
+
- Never expose inspector port in production by default
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## 7. Complete Code Examples
|
|
786
|
+
|
|
787
|
+
### Example 1: Simple Breakpoint Setting
|
|
788
|
+
|
|
789
|
+
```javascript
|
|
790
|
+
// simple-breakpoint.js
|
|
791
|
+
import { Session } from 'node:inspector/promises';
|
|
792
|
+
import * as inspector from 'node:inspector';
|
|
793
|
+
import { fileURLToPath } from 'url';
|
|
794
|
+
import path from 'path';
|
|
795
|
+
|
|
796
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
797
|
+
|
|
798
|
+
// Open inspector
|
|
799
|
+
inspector.open(0);
|
|
800
|
+
console.log('Inspector URL:', inspector.url());
|
|
801
|
+
|
|
802
|
+
// Create session
|
|
803
|
+
const session = new Session();
|
|
804
|
+
session.connect();
|
|
805
|
+
|
|
806
|
+
// Enable debugger
|
|
807
|
+
await session.post('Debugger.enable');
|
|
808
|
+
|
|
809
|
+
// Listen for pause events
|
|
810
|
+
session.on('Debugger.paused', (message) => {
|
|
811
|
+
console.log('\n=== PAUSED ===');
|
|
812
|
+
console.log('Reason:', message.params.reason);
|
|
813
|
+
console.log('Hit breakpoints:', message.params.hitBreakpoints);
|
|
814
|
+
|
|
815
|
+
// Examine the call stack
|
|
816
|
+
for (const frame of message.params.callFrames) {
|
|
817
|
+
console.log(` at ${frame.functionName || '(anonymous)'} (${frame.url}:${frame.location.lineNumber + 1})`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Resume after inspection
|
|
821
|
+
setTimeout(() => {
|
|
822
|
+
session.post('Debugger.resume');
|
|
823
|
+
}, 1000);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Set a breakpoint on line 50 of this file
|
|
827
|
+
const result = await session.post('Debugger.setBreakpointByUrl', {
|
|
828
|
+
lineNumber: 49, // 0-based, so this is line 50
|
|
829
|
+
url: `file://${__filename}`
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
console.log('Breakpoint set:', result.breakpointId);
|
|
833
|
+
|
|
834
|
+
// This is line 50 - breakpoint will hit here
|
|
835
|
+
console.log('This line has a breakpoint');
|
|
836
|
+
|
|
837
|
+
console.log('Script completed');
|
|
838
|
+
session.disconnect();
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Example 2: Remote Debugger Client
|
|
842
|
+
|
|
843
|
+
```javascript
|
|
844
|
+
// remote-debugger.js
|
|
845
|
+
import WebSocket from 'ws';
|
|
846
|
+
import { EventEmitter } from 'events';
|
|
847
|
+
|
|
848
|
+
export class RemoteDebugger extends EventEmitter {
|
|
849
|
+
constructor() {
|
|
850
|
+
super();
|
|
851
|
+
this.ws = null;
|
|
852
|
+
this.messageId = 0;
|
|
853
|
+
this.pending = new Map();
|
|
854
|
+
this.scripts = new Map();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async connect(wsUrl) {
|
|
858
|
+
return new Promise((resolve, reject) => {
|
|
859
|
+
this.ws = new WebSocket(wsUrl);
|
|
860
|
+
|
|
861
|
+
this.ws.on('open', resolve);
|
|
862
|
+
this.ws.on('error', reject);
|
|
863
|
+
|
|
864
|
+
this.ws.on('message', (data) => {
|
|
865
|
+
this._handleMessage(JSON.parse(data));
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
this.ws.on('close', () => {
|
|
869
|
+
this.emit('disconnected');
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
_handleMessage(msg) {
|
|
875
|
+
if (msg.id !== undefined) {
|
|
876
|
+
const pending = this.pending.get(msg.id);
|
|
877
|
+
if (pending) {
|
|
878
|
+
this.pending.delete(msg.id);
|
|
879
|
+
if (msg.error) {
|
|
880
|
+
pending.reject(new Error(msg.error.message));
|
|
881
|
+
} else {
|
|
882
|
+
pending.resolve(msg.result);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} else if (msg.method) {
|
|
886
|
+
this.emit(msg.method, msg.params);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async send(method, params = {}) {
|
|
891
|
+
return new Promise((resolve, reject) => {
|
|
892
|
+
const id = ++this.messageId;
|
|
893
|
+
this.pending.set(id, { resolve, reject });
|
|
894
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async enable() {
|
|
899
|
+
// Enable required domains
|
|
900
|
+
await this.send('Debugger.enable');
|
|
901
|
+
await this.send('Runtime.enable');
|
|
902
|
+
|
|
903
|
+
// Track loaded scripts
|
|
904
|
+
this.on('Debugger.scriptParsed', (params) => {
|
|
905
|
+
this.scripts.set(params.scriptId, params);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async setBreakpoint(file, line, condition) {
|
|
910
|
+
return await this.send('Debugger.setBreakpointByUrl', {
|
|
911
|
+
lineNumber: line - 1, // Convert to 0-based
|
|
912
|
+
url: file.startsWith('file://') ? file : `file://${file}`,
|
|
913
|
+
condition: condition || ''
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async removeBreakpoint(breakpointId) {
|
|
918
|
+
return await this.send('Debugger.removeBreakpoint', { breakpointId });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async resume() {
|
|
922
|
+
return await this.send('Debugger.resume');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async stepOver() {
|
|
926
|
+
return await this.send('Debugger.stepOver');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async stepInto() {
|
|
930
|
+
return await this.send('Debugger.stepInto');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async stepOut() {
|
|
934
|
+
return await this.send('Debugger.stepOut');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async pause() {
|
|
938
|
+
return await this.send('Debugger.pause');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async evaluate(expression, callFrameId) {
|
|
942
|
+
if (callFrameId) {
|
|
943
|
+
return await this.send('Debugger.evaluateOnCallFrame', {
|
|
944
|
+
callFrameId,
|
|
945
|
+
expression
|
|
946
|
+
});
|
|
947
|
+
} else {
|
|
948
|
+
return await this.send('Runtime.evaluate', { expression });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async getScopes(callFrameId) {
|
|
953
|
+
// Scopes are included in the callFrame from Debugger.paused
|
|
954
|
+
return null; // Use data from paused event instead
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
disconnect() {
|
|
958
|
+
if (this.ws) {
|
|
959
|
+
this.ws.close();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Usage example
|
|
965
|
+
async function main() {
|
|
966
|
+
const debugger_ = new RemoteDebugger();
|
|
967
|
+
|
|
968
|
+
// Connect to a Node.js process running with --inspect
|
|
969
|
+
await debugger_.connect('ws://127.0.0.1:9229/uuid-here');
|
|
970
|
+
await debugger_.enable();
|
|
971
|
+
|
|
972
|
+
// Handle pause events
|
|
973
|
+
debugger_.on('Debugger.paused', async (params) => {
|
|
974
|
+
console.log('Paused:', params.reason);
|
|
975
|
+
|
|
976
|
+
// Get local variables from first scope
|
|
977
|
+
const frame = params.callFrames[0];
|
|
978
|
+
for (const scope of frame.scopeChain) {
|
|
979
|
+
if (scope.type === 'local') {
|
|
980
|
+
const properties = await debugger_.send('Runtime.getProperties', {
|
|
981
|
+
objectId: scope.object.objectId
|
|
982
|
+
});
|
|
983
|
+
console.log('Local variables:', properties.result);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Resume after 2 seconds
|
|
988
|
+
setTimeout(() => debugger_.resume(), 2000);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// Set a breakpoint
|
|
992
|
+
const bp = await debugger_.setBreakpoint('/path/to/app.js', 10);
|
|
993
|
+
console.log('Breakpoint set:', bp.breakpointId);
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### Example 3: Process Launcher with Debug Support
|
|
998
|
+
|
|
999
|
+
```javascript
|
|
1000
|
+
// debug-launcher.js
|
|
1001
|
+
import { spawn } from 'child_process';
|
|
1002
|
+
import { RemoteDebugger } from './remote-debugger.js';
|
|
1003
|
+
|
|
1004
|
+
export class DebugLauncher {
|
|
1005
|
+
constructor() {
|
|
1006
|
+
this.child = null;
|
|
1007
|
+
this.debugger = null;
|
|
1008
|
+
this.inspectorUrl = null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async launch(script, args = [], options = {}) {
|
|
1012
|
+
const {
|
|
1013
|
+
pauseOnStart = false,
|
|
1014
|
+
port = 0 // Random port
|
|
1015
|
+
} = options;
|
|
1016
|
+
|
|
1017
|
+
const inspectFlag = pauseOnStart
|
|
1018
|
+
? `--inspect-brk=${port}`
|
|
1019
|
+
: `--inspect=${port}`;
|
|
1020
|
+
|
|
1021
|
+
this.child = spawn('node', [inspectFlag, script, ...args], {
|
|
1022
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Capture inspector URL from stderr
|
|
1026
|
+
this.inspectorUrl = await new Promise((resolve, reject) => {
|
|
1027
|
+
const timeout = setTimeout(() => {
|
|
1028
|
+
reject(new Error('Timeout waiting for inspector URL'));
|
|
1029
|
+
}, 5000);
|
|
1030
|
+
|
|
1031
|
+
this.child.stderr.on('data', (data) => {
|
|
1032
|
+
const output = data.toString();
|
|
1033
|
+
const match = output.match(/ws:\/\/[\d.]+:\d+\/[\w-]+/);
|
|
1034
|
+
if (match) {
|
|
1035
|
+
clearTimeout(timeout);
|
|
1036
|
+
resolve(match[0]);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
this.child.on('error', (err) => {
|
|
1041
|
+
clearTimeout(timeout);
|
|
1042
|
+
reject(err);
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// Connect debugger
|
|
1047
|
+
this.debugger = new RemoteDebugger();
|
|
1048
|
+
await this.debugger.connect(this.inspectorUrl);
|
|
1049
|
+
await this.debugger.enable();
|
|
1050
|
+
|
|
1051
|
+
return {
|
|
1052
|
+
inspectorUrl: this.inspectorUrl,
|
|
1053
|
+
debugger: this.debugger,
|
|
1054
|
+
child: this.child
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async stop() {
|
|
1059
|
+
if (this.debugger) {
|
|
1060
|
+
this.debugger.disconnect();
|
|
1061
|
+
}
|
|
1062
|
+
if (this.child) {
|
|
1063
|
+
this.child.kill();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Usage
|
|
1069
|
+
async function main() {
|
|
1070
|
+
const launcher = new DebugLauncher();
|
|
1071
|
+
|
|
1072
|
+
const { debugger: dbg } = await launcher.launch('./app.js', [], {
|
|
1073
|
+
pauseOnStart: true
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Set breakpoints before any code runs
|
|
1077
|
+
await dbg.setBreakpoint('./app.js', 15);
|
|
1078
|
+
await dbg.setBreakpoint('./app.js', 20, 'count > 5');
|
|
1079
|
+
|
|
1080
|
+
// Handle pause events
|
|
1081
|
+
dbg.on('Debugger.paused', (params) => {
|
|
1082
|
+
console.log('Hit breakpoint at line',
|
|
1083
|
+
params.callFrames[0].location.lineNumber + 1);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Resume from initial pause
|
|
1087
|
+
await dbg.resume();
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
### Example 4: MCP Tool for Reflexive
|
|
1092
|
+
|
|
1093
|
+
```javascript
|
|
1094
|
+
// breakpoint-tools.js
|
|
1095
|
+
import { z } from 'zod';
|
|
1096
|
+
|
|
1097
|
+
export function createBreakpointTools(debugger_) {
|
|
1098
|
+
return {
|
|
1099
|
+
set_breakpoint: {
|
|
1100
|
+
description: 'Set a debugger breakpoint at a specific file and line',
|
|
1101
|
+
inputSchema: z.object({
|
|
1102
|
+
file: z.string().describe('Absolute path to the file'),
|
|
1103
|
+
line: z.number().describe('Line number (1-based)'),
|
|
1104
|
+
condition: z.string().optional().describe('Optional condition expression')
|
|
1105
|
+
}),
|
|
1106
|
+
handler: async ({ file, line, condition }) => {
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await debugger_.setBreakpoint(file, line, condition);
|
|
1109
|
+
return {
|
|
1110
|
+
success: true,
|
|
1111
|
+
breakpointId: result.breakpointId,
|
|
1112
|
+
locations: result.locations
|
|
1113
|
+
};
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
return { success: false, error: err.message };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
remove_breakpoint: {
|
|
1121
|
+
description: 'Remove a debugger breakpoint',
|
|
1122
|
+
inputSchema: z.object({
|
|
1123
|
+
breakpointId: z.string().describe('The breakpoint ID to remove')
|
|
1124
|
+
}),
|
|
1125
|
+
handler: async ({ breakpointId }) => {
|
|
1126
|
+
try {
|
|
1127
|
+
await debugger_.removeBreakpoint(breakpointId);
|
|
1128
|
+
return { success: true };
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
return { success: false, error: err.message };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
list_breakpoints: {
|
|
1136
|
+
description: 'List all active breakpoints',
|
|
1137
|
+
inputSchema: z.object({}),
|
|
1138
|
+
handler: async () => {
|
|
1139
|
+
// Note: CDP doesn't have a list breakpoints command
|
|
1140
|
+
// We need to track them ourselves
|
|
1141
|
+
return {
|
|
1142
|
+
breakpoints: Array.from(debugger_.breakpoints.entries())
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
|
|
1147
|
+
debug_resume: {
|
|
1148
|
+
description: 'Resume execution after hitting a breakpoint',
|
|
1149
|
+
inputSchema: z.object({}),
|
|
1150
|
+
handler: async () => {
|
|
1151
|
+
try {
|
|
1152
|
+
await debugger_.resume();
|
|
1153
|
+
return { success: true };
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
return { success: false, error: err.message };
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
|
|
1160
|
+
debug_step_over: {
|
|
1161
|
+
description: 'Step over the current line',
|
|
1162
|
+
inputSchema: z.object({}),
|
|
1163
|
+
handler: async () => {
|
|
1164
|
+
try {
|
|
1165
|
+
await debugger_.stepOver();
|
|
1166
|
+
return { success: true };
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
return { success: false, error: err.message };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
debug_evaluate: {
|
|
1174
|
+
description: 'Evaluate an expression in the current debug context',
|
|
1175
|
+
inputSchema: z.object({
|
|
1176
|
+
expression: z.string().describe('JavaScript expression to evaluate')
|
|
1177
|
+
}),
|
|
1178
|
+
handler: async ({ expression }) => {
|
|
1179
|
+
try {
|
|
1180
|
+
const result = await debugger_.evaluate(expression);
|
|
1181
|
+
return {
|
|
1182
|
+
success: true,
|
|
1183
|
+
result: result.result
|
|
1184
|
+
};
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
return { success: false, error: err.message };
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
---
|
|
1195
|
+
|
|
1196
|
+
## Summary: Implementation Recommendations for Reflexive
|
|
1197
|
+
|
|
1198
|
+
### For CLI Mode (Recommended Approach)
|
|
1199
|
+
|
|
1200
|
+
1. **Start target with `--inspect-brk=0`** - Pauses on first line, uses random port
|
|
1201
|
+
2. **Parse WebSocket URL from stderr** - Extract `ws://...` URL
|
|
1202
|
+
3. **Connect via WebSocket** - Use `ws` package or `chrome-remote-interface`
|
|
1203
|
+
4. **Set breakpoints before resuming** - Use `Debugger.setBreakpointByUrl`
|
|
1204
|
+
5. **Handle pause events** - Capture call stack, scope variables
|
|
1205
|
+
6. **Expose as MCP tools** - `set_breakpoint`, `remove_breakpoint`, `resume`, `step_over`, etc.
|
|
1206
|
+
|
|
1207
|
+
### For Library Mode (Limited)
|
|
1208
|
+
|
|
1209
|
+
1. **Use separate worker thread** - Debugger worker connects to main thread
|
|
1210
|
+
2. **Accept that pause freezes the app** - Including the chat interface
|
|
1211
|
+
3. **Consider hybrid approach** - Use library mode for observation, CLI mode for debugging
|
|
1212
|
+
|
|
1213
|
+
### Key Takeaways
|
|
1214
|
+
|
|
1215
|
+
1. **CDP is the right protocol** - Same as Chrome DevTools uses
|
|
1216
|
+
2. **`Debugger.setBreakpointByUrl` is the key method** - Works before scripts load
|
|
1217
|
+
3. **Remote debugging is more practical** - Separate processes avoid pause issues
|
|
1218
|
+
4. **Security is critical** - Never expose inspector to network
|
|
1219
|
+
5. **No build-in breakpoint listing** - Must track breakpoints manually
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## References
|
|
1224
|
+
|
|
1225
|
+
- [Node.js Inspector Documentation](https://nodejs.org/api/inspector.html)
|
|
1226
|
+
- [Node.js Debugger Documentation](https://nodejs.org/api/debugger.html)
|
|
1227
|
+
- [Chrome DevTools Protocol - Debugger Domain](https://chromedevtools.github.io/devtools-protocol/tot/Debugger/)
|
|
1228
|
+
- [Chrome DevTools Protocol - V8 Version](https://chromedevtools.github.io/devtools-protocol/v8/)
|
|
1229
|
+
- [chrome-remote-interface GitHub](https://github.com/cyrus-and/chrome-remote-interface)
|
|
1230
|
+
- [ndb GitHub (GoogleChromeLabs)](https://github.com/GoogleChromeLabs/ndb)
|
|
1231
|
+
- [Node.js Debugging Guide](https://nodejs.org/en/learn/getting-started/debugging)
|