Node Inspect Debugger
SkillMonitoring & opsSee runtime state a console.log cannot reach — set real breakpoints, step, and dump the scope chain of a paused Node/TypeScript process. Use on "set a breakpoint", "inspect runtime state", "console.log isn''t enough", "step through this", "what''s in this closure at runtime", "attach a debugger". Not a logging or observability-setup workflow.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Node Inspect Debugger skill
What this skill tells your AI
The instructions your AI receives, as published by blackbelttechnology/pi-agent-dashboard in packages/eng-disciplines/.pi/skills/node-inspect-debugger/SKILL.md and read by ahel’s review.
Overview
console.log is a guess with a print statement attached — you have to already suspect where to look and what to print. A breakpoint inverts that: you stop the world at a line and read every local and closure variable in the frame, walk the call stack, and evaluate expressions in the paused context. When the bug lives in state a log can't cheaply reach — a closure captured three calls ago, a paused async frame, the Electron main process, the internals of a long-lived WebSocket server — the inspector is the tool.
This skill is the TOOL half of a pair; the METHOD is systematic-debugging (its Phase 1 "gather evidence" and Phase 3 "test one variable" are exactly when you reach for a breakpoint).
When to Use
Reach for the inspector when the state you need is expensive or impossible to log:
- The jiti server (
packages/server, TypeScript run directly via jiti) — request handlers whose failure depends on accumulated in-memory state. - The restart orchestrator / PTY workers (
restart-helper.ts) — a detached process whose closure state you can'tconsole.logfrom the parent. - Dual WebSocket server closure state — connection maps and buffers held in closures across the bridge and browser servers.
- The Electron main process — lifecycle/bootstrap state that never reaches a browser console.
- The bridge extension — code running inside every pi session, where adding logs means reloading every session.
If a single well-placed console.log would answer the question, use the log. The inspector earns its setup cost when the state is deep, closure-bound, or in a process you can't easily instrument.
Two tiers
| Tier | Use when | Interface |
|---|---|---|
REPL (node inspect) | interactive, one-off, you're driving | the node inspect command REPL |
| Programmatic CDP | scripted, repeatable, or attaching from another process | scripts/cdp-inspect.ts (this skill) |
Tier 1 — the node inspect REPL cheat-sheet
node inspect opens a REPL against the inspector. Core commands:
| Command | Does |
|---|---|
sb('file.ts', N) | set breakpoint at line N of file.ts (see jiti note below — .ts works directly) |
c | continue to next breakpoint |
n / s / o | step next / step-in / step-out |
bt | backtrace (call stack of the paused frame) |
list(5) | show 5 source lines around the pause point |
repl | drop into a REPL evaluated in the paused frame's scope — inspect any local |
watch('expr') | re-evaluate expr at every pause |
exec expr | evaluate one expression in the paused frame |
restart / kill | restart / kill the inspected process |
In repl mode you can read any in-scope variable by name — this is the fastest way to answer "what is x right now?".
Tier 2 — pi-dashboard jiti launch (spike-verified)
This repo runs TypeScript directly through jiti (no dist/*.js build). Launch the target with the inspector and jiti's register hook:
node --inspect-brk=<port> --enable-source-maps --import <jiti-register-hook-url> cli.ts
--inspect-brk=<port>halts at the very first line so you can set breakpoints before anything runs.--enable-source-mapsis not required for line-level breakpoints (jiti is line-preserving, see pitfall below) but keep it: it gives exact column precision and correctError.stackline numbers.
Locating the jiti register hook (do not hard-code the path)
The register hook path differs across local / standalone / global installs. Resolve it the same way this repo's launcher (packages/server/bin/pi-dashboard.mjs) does — via createRequire, trying each supported jiti package:
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
const req = createRequire(import.meta.url); // or createRequire(process.argv[1])
const JITI_PACKAGES = ["jiti", "@mariozechner/jiti"];
let hook;
for (const pkg of JITI_PACKAGES) {
try {
const pkgJson = req.resolve(`${pkg}/package.json`);
hook = pathToFileURL(join(dirname(pkgJson), "lib", "jiti-register.mjs")).href;
break;
} catch { /* try next */ }
}
// pass `hook` to `node --import <hook> cli.ts`
Attaching to the live server (not a fresh launch)
To debug the already-running server instead of a cold --inspect-brk launch, start it with the inspector enabled and discover the port:
NODE_OPTIONS="--inspect=0" pi-dashboard start # 0 = pick a free port
curl -s http://127.0.0.1:<port>/json/list # find the webSocketDebuggerUrl / port
--inspect=0 avoids a port collision when something already holds the default 9229.
Pitfalls
The upstream "emitted JS" pitfall does NOT apply to jiti — corrected
A common guide warns: "breakpoints hit the emitted JS, not your .ts, and node inspect doesn't follow sourcemaps." This is false for this repo. jiti transpiles line-preserving and registers the compiled JS under the .ts URL (1:1 line alignment). There is no separate dist/*.js file and no sourcemap indirection to fight.
Consequences you can rely on:
sb('cli.ts', 42)in the plainnode inspectREPL binds directly to line 42 of the.tssource and hits.Debugger.setBreakpointByUrl('cli.ts', line)over CDP binds to the.tsline.- CDP may report
sourceMapURL: ""(jiti attaches none) — this does not mean the breakpoint won't bind. It binds and hits anyway.
Do not port the emitted-JS workaround; it's solving a problem jiti doesn't create.
Pending breakpoints return empty locations but still hit
A breakpoint set before the target script has parsed (e.g. right after the --inspect-brk halt) returns locations: [] at set-time. This is not a failure — the breakpoint is deferred and resolves and hits once the script parses. Setting a breakpoint after the script has parsed returns populated locations. Do not treat an empty locations array at set-time as "the breakpoint didn't take."
Programmatic CDP — scripts/cdp-inspect.ts
For scripted or repeatable inspection, this skill ships a dependency-free TypeScript helper. It uses Node 24's global WebSocket — no chrome-remote-interface dependency. It attaches to a paused target, sets a .ts breakpoint, resumes past the entry halt, and on the hit prints the paused frame plus every local and closure variable:
# 1. launch the target halted (see jiti recipe above), noting <port>
# 2. attach + break at a line inside a function:
npx tsx packages/eng-disciplines/.pi/skills/node-inspect-debugger/scripts/cdp-inspect.ts <port> <ts-url> <line>
# prints: PAUSED at <file>:<line> fn=<name>
# local cfg = Object
# local doubled = 42
# closure label = "HELLO"
Use it as the ready-made "dump the frame" step whenever the REPL's interactivity isn't worth it.
Verification
- The launch recipe used jiti's register hook resolved via
createRequire, not a hard-coded path -
.tsbreakpoints were set directly (no emitted-JS workaround) - An empty
locationsat set-time was treated as deferred, not failed - The paused frame's locals were read (via
repl,exec, orcdp-inspect.ts) — the actual state, not a guess
Signals
- GitHub stars
- 283
- Forks
- 41
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
node-inspect-debugger- Source
- github.com/blackbelttechnology/pi-agent-dashboard