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.
@@ -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)