| name | dap-cli |
| description | Use when: an agent needs to debug, inspect, or control a local program from shell commands with dap-cli; trigger for DAP debugging, breakpoints, stack/variables inspection, launch.json debugging, js-debug, debugpy, CodeLLDB, Rust, Chrome debugging, Playwright interop, or poll-then-inspect debug loops. Assumes dap-cli is already on PATH. |
Debug Adapter Control with dap-cli
dap-cli is an agent-facing CLI for the Debug Adapter Protocol. It's language-agnostic — any DAP adapter (js-debug, debugpy, delve, codelldb, custom) works through the same control loop.
If the dap-cli command is not on PATH, run it as npx @roblourens/dap-cli.
The model is intentionally simple:
- Polling, not streaming. Drive the target with commands; poll
status for state.
- JSON envelope by default. Every reply is
{ ok, data, error?, meta }. JSON is emitted automatically when stdout is not a TTY.
- Refs are scoped to the current stop. After
continue / next / step-in / step-out, reacquire threads / stack / scopes / variables before reusing IDs.
Quick start
dap-cli start
dap-cli launch --program app.js --stop-on-entry
dap-cli breakpoints set --source app.js --line 12
dap-cli continue
dap-cli status
dap-cli threads
dap-cli stack
dap-cli scopes --frame-id 10
dap-cli variables --variables-reference 100
dap-cli evaluate --expression "user.email"
dap-cli close
dap-cli stop-controller
Standard loop
dap-cli start if you get controller_unavailable.
- Prefer
--config over raw --adapter flags when the project has a .vscode/launch.json — the config carries adapter-specific fields (outFiles, webRoot, runtimeArgs) you'd otherwise have to reconstruct.
- Poll
status for stop detection.
- When stopped:
threads → stack → scopes → variables. --thread-id and --frame-id are auto-resolved when there is exactly one candidate; --variables-reference and --source-reference always need a value from a fresh call.
- Drive with
evaluate / continue / next / step-in / step-out. Poll again after every resume.
- Clean up with
close / cleanup / stop-controller.
Commands
Lifecycle
dap-cli start
dap-cli stop-controller
dap-cli sessions
dap-cli sessions --show-children
dap-cli close
dap-cli cleanup
dap-cli cleanup --purge
Launch / attach
dap-cli launch --program app.js --stop-on-entry
dap-cli launch --program main.py
dap-cli launch --list-configs
dap-cli attach --config "Attach Worker"
dap-cli attach --workspace /elsewhere --config "Attach Worker"
dap-cli launch --config "Attach to App" \
--json-overrides '{"sourceMaps":true,"resolveSourceMapLocations":["**","!**/node_modules/**"]}'
dap-cli launch --config "Attach to App" \
--resolve-source-maps '**' '!**/node_modules/**'
dap-cli launch --config "Attach to App" \
--out-files 'dist/**/*.js'
dap-cli launch --adapter js-debug --type pwa-node --json '{"program":"app.js","stopOnEntry":true}'
launch and attach map directly to the DAP request field — there is no --request flag. With --config, a verb mismatch auto-routes (warns + sets autoRouted on the response). Raw --json payloads do not auto-route.
Inspection
dap-cli status
dap-cli events --after-cursor 0 --limit 20
dap-cli events --include stopped --include output
dap-cli threads
dap-cli stack
dap-cli scopes --frame-id 10
dap-cli variables --variables-reference 100
dap-cli evaluate --expression "value + 1"
dap-cli evaluate --expression "value > 10" --frame-id 10 --context repl
dap-cli capabilities
Stepping
dap-cli continue
dap-cli next
dap-cli step-in
dap-cli step-out
dap-cli pause
Breakpoints
dap-cli breakpoints set --source app.js --line 12
dap-cli breakpoints set --source app.js --line 22 --condition "count === 1"
dap-cli breakpoints set --source app.js --line 22 --hit-condition 2
dap-cli breakpoints set --source app.js --line 22 --log-message "count={count}"
dap-cli breakpoints list
dap-cli breakpoints list --source app.js
dap-cli breakpoints clear --source app.js
dap-cli breakpoints clear
Raw DAP
For DAP requests not exposed as first-class commands:
dap-cli request stackTrace --json '{"threadId":1}'
dap-cli dap loaded-sources
dap-cli dap set-variable --json '{"variablesReference":100,"name":"x","value":"42"}'
dap-cli dap set-expression --json '{"frameId":10,"expression":"x","value":"42"}'
Output mode
dap-cli sessions --human
dap-cli status --no-human
DAP_CLI_HUMAN=1 dap-cli status
Non-TTY stdout always gets JSON. The command-level --json <json> is request payload input, not an output-format switch.
Multiple sessions
--name <session> lets you target one of several sessions explicitly. When omitted, commands act on the active session (the most recent one, or whatever was set with dap-cli use <name>).
dap-cli launch --program app.js --name api
dap-cli launch --program worker.js --name worker
dap-cli status --name api
dap-cli close api
dap-cli use api
dap-cli status
Always pass --name in scripts and agent playbooks; rely on the active session only for one-off interactive use.
JSON envelope
{ "ok": true, "data": { }, "meta": { "command": "status", "timestamp": "…" } }
{ "ok": false, "error": { "code": "adapter_not_found", "category": "usage", "exitCode": 2, "diagnostics": [] }, "meta": { } }
Read error.code, error.category, error.diagnostics, stderrTail, and logPath before retrying. If a request fails because execution resumed, restart at status and reacquire IDs on the next stop.
Breakpoint verification is asynchronous
The verified flag in the immediate breakpoints set response is a snapshot, not a verdict. Adapters often return verified: false initially and upgrade once the runtime loads the source.
- Set the bp. If
verified: true, you're done.
- If
verified: false, don't conclude failure. Continue and poll status. If a stop arrives with reason: "breakpoint", your bp resolved.
- Only after the program has plausibly run past the line without stopping should you read
verificationDiagnostic and treat it as a binding failure.
The success payload carries verificationDiagnostic when any bp is unverified:
{
"verificationDiagnostic": {
"unverifiedCount": 1,
"loadedSourcesCount": 0,
"matchingLoadedSources": [],
"hint": "1 of 1 breakpoints unverified; debuggee has loaded 0 sources — likely attached to the wrong process. Run: dap-cli dap loaded-sources",
"recipe": "dap-cli dap loaded-sources"
}
}
recipe is the literal next command to run. Hint cases:
- 0 sources loaded → likely attached to the wrong process.
- Loaded but no path/basename match → check source maps / outFiles.
- Matching loaded source → check breakpoint line numbers.
- Adapter doesn't advertise
supportsLoadedSourcesRequest → fall back to event inspection.
An empty loaded-sources result with childSessionCount > 0 is normal for multi-process js-debug (children carry the sources, not the parent).
Example: launch a Node target, break, inspect, continue
dap-cli start
dap-cli launch --program app.js --stop-on-entry
dap-cli breakpoints set --source app.js --line 12
dap-cli continue
dap-cli status
dap-cli stack
dap-cli scopes --frame-id 1000
dap-cli variables --variables-reference 1001
dap-cli evaluate --expression "user.email"
dap-cli continue
dap-cli close
Example: attach to a workspace launch.json config
dap-cli launch --list-configs
dap-cli attach --config "Attach Worker"
dap-cli evaluate --expression "process.pid"
lsof -i :9229 | grep LISTEN
If the resolved config has request: "attach", use dap-cli attach (or let --config auto-route from launch). Picking the wrong verb is the highest-impact footgun: an adapter helper process spawns, every breakpoints set returns verified: false, and dap loaded-sources returns [] — looks like a source-map bug; isn't.
Example: js-debug pwa-chrome with multi-renderer events
dap-cli launch --name web-demo --adapter js-debug --type pwa-chrome \
--url "file://$PWD/index.html" \
--json '{"webRoot":"'"$PWD"'","runtimeArgs":["--remote-debugging-port=9222"]}'
dap-cli sessions --show-children
dap-cli events --name web-demo --include output --after-cursor 0
dap-cli events --name web-demo --after-cursor 0 \
| jq '.data.events[] | select(.body.child_session_id == "<child-id>")'
Set bps on the parent. For multi-process js-debug attaches (pwa-node workers, Electron sub-Node helpers including the extension host, pwa-chrome page children), dap-cli rolls per-child paused state up into the parent: status --name <parent> reports paused: true whenever any child is stopped, and routes thread-bearing requests to the paused child. --thread-id auto-resolves. Targeting a child directly returns child_session_not_targetable with error.data.parentSessionId.
Example: Playwright interop (drive UI while dap-cli polls)
Playwright drives the browser UI. dap-cli controls and inspects debugger state. Same Chromium instance, separate responsibilities, coordinate by polling.
dap-cli start
dap-cli launch --name web-demo --adapter js-debug --type pwa-chrome \
--url "file://$PWD/page.html" \
--json '{"webRoot":"'"$PWD"'","runtimeArgs":["--remote-debugging-port=9222"]}'
dap-cli breakpoints set --name web-demo --source "$PWD/src/app.ts" --line 22
playwright-cli attach --cdp=http://localhost:9222
playwright-cli click e5
dap-cli status --name web-demo
dap-cli stack --name web-demo
dap-cli evaluate --expression "count" --name web-demo
dap-cli continue --name web-demo
Common gotchas
- Array-shaped
data fields. sessions, sessions --show-children, and launch --list-configs all return the standard {ok, data, meta} envelope, but data is an array (of session objects, or of config-name strings for --list-configs) rather than an object with named fields. Don't assume data.sessions or data.configs — iterate data directly.
- Don't set
DAP_CLI_HOME to a fresh per-task dir without staging adapters. Built-in adapter caches (js-debug, debugpy, delve, codelldb) live there; prewarm or share the intended cache before real debugging.
- Compounds. Member session names are derived as
<compound>/<member>; use dap-cli sessions to discover exact names before targeting a member. Closing one member closes the group unless stopAll: false.
--json-overrides cannot bypass --config auto-route. A '{"request":"launch"}' override is silently overwritten by the auto-routed request field.
- Stale state. When sessions or adapters look wrong:
dap-cli sessions → dap-cli cleanup → dap-cli cleanup --purge → dap-cli stop-controller.
Going deeper