@saptools/cf-inspector
Sets breakpoints, captures variable snapshots and evaluates expressions on a remote Node.jsNode.jsA cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts. process over the Chrome DevTools Protocol from the shell, with no IDE attached.
Built so an AI agent or a CI job can debug a running Node.jsNode.jsA cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts. process from one shell command and read the result as JSON. cf-inspector snapshot --bp src/handler.ts:42 arms the breakpoint, waits for the hit, evaluates the requested expressions, resumes and exits, exception does the same once for a thrown exception, and watch and log stream the same kind of capture as JSON Lines. Captures run under V8's side-effect check by default, so an expression that would change the live process comes back blocked unless --allow-mutation is passed. Given --app, --region, --org and --space it opens the Cloud FoundrySAP BTP, Cloud Foundry RuntimeThe Cloud Foundry runtime of SAP BTP: memory for applications in a Cloud Foundry org, run from SAP's Java, Node.js and Python buildpacks or community buildpacks and managed with the standard Cloud Foundry tools.sap official, btp tunnel through @saptools/cf-debugger@saptools/cf-debuggerOpens a verified Node.js inspector tunnel over cf ssh to an app on SAP BTP Cloud Foundry from one command, with its own local port for each concurrent session.independent, btp and closes it on exit; given --port it attaches to any inspector already listening. Only one local cf-inspector may debug a target at a time, and a debugger running on another machine is not detected.
Facts
Where it sits
Who is behind it
In the atlas: maintains 18, contributes to 18@dongitranmaintainer
What it declares
Every artifact
- Type
- CLI
- Origin
- Independently maintained
- Runs on
- Cloud Foundry
As a CLI
- Distributed through
- npm
Connections
Seeing what running code did between input and result
Step-by-step state recorder for one JavaScript function in Node.js
HTTP trace hook streaming a running CF Node.js app's traffic
Node.js inspector tunnel over cf ssh to Cloud Foundry apps
A cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts.
Download
Install
npm add @saptools/cf-inspectorDerived from the package name. No recipe recorded; the README has the configuration.
Architecture
Download
README
quoted from dongitran/saptools under MIT, read🔍 @saptools/cf-inspector
Set breakpoints, capture variable snapshots, and evaluate expressions on a remote Node.js process — over the Chrome DevTools Protocol, no IDE required.
Built so an AI agent (or a CI job) can drive a debugger from a single shell command. Pairs with @saptools/cf-debugger when the target lives behind a Cloud Foundry SSH tunnel.
Install • Quick Start • CLI • How it works
Debug-session safety: only one local
cf-inspectorprocess may actively debug a given target at a time. Concurrent CDP debugger clients can receive the same pause and raceDebugger.resume, disrupting the target and real application traffic. A second local invocation now fails before connecting withTARGET_ALREADY_DEBUGGED; wait for the named owner process to finish. Locks left by dead processes are reclaimed automatically. Debuggers running on another machine or outsidecf-inspectorcannot be detected by this local guard and must still be coordinated operationally.
✨ Features
- 🎯 One-shot snapshot —
cf-inspector snapshot --bp src/handler.ts:42sets the breakpoint, waits for it to hit, captures requested expressions, auto-resumes, prints JSON, exits - ✅ Conditional breakpoints —
--condition 'req.userId === "abc"'only pauses when the predicate is truthy - 🔢 Hit-count breakpoints —
--hit-count 5skips the first N − 1 hits and pauses on the Nth, on every command (snapshot, log, watch) - 🎭 Multi-breakpoint — repeat
--bpto race several locations; first hit wins - 🪜 Stack capture —
--stack-depth N --stack-captures 'this, args'walks call frames and evaluates expressions per frame - 🔁 Watch streaming —
cf-inspector watch --bp file:line --capture user.id --duration 30re-captures on every hit and emits JSON Lines (the streaming counterpart ofsnapshot) - 💥 Exception breakpoints —
cf-inspector exception --type uncaught --capture err.messagepauses on the next thrown error and materializes the exception value - 📡 Non-pausing logpoints —
cf-inspector log --at file:line --expr 'JSON.stringify({…})'streams JSON Lines as the line executes without pausing the inspectee, with optional--condition,--hit-count, and--max-events - 🛡️ Read-only capture guard — snapshot, watch, and exception captures use V8's side-effect analysis by default;
--allow-mutationis an explicit escape hatch - 🧵 Automatic worker fan-out — snapshot, watch, exception, and log attach to the main isolate plus every current or newly-spawned NodeWorker; explicit selectors remain available for pinning
- 🚦 Machine-readable readiness — opt into a versioned
breakpoint-armedstderr event before triggering external traffic; no prose matching or guessed delay required - 📜 Isolate-aware script listing —
list-scriptsaggregates the main isolate and every current worker, tagging every script with its isolate - 🧠 Agent-friendly — JSON-by-default I/O, deterministic shapes, and explicit
truncated/originalLength/omittedCountmetadata for bounded values - 🧭 Path mapping — local
src/handler.ts:42is matched against the remote URL via aurlRegex, with optional--remote-rootliteral or regex (same DSL ascds-debug) - 🔁 Composes with
cf-debugger— pass--app/--region/--org/--spaceand the tunnel is opened automatically; pass--portto attach to anything CDP-speaking - 🪶 Tiny dependency footprint —
@saptools/cf-debugger+commander+ws, with no heavy CDP framework - 🧩 Typed API — every CLI command has a programmatic equivalent with full TypeScript definitions
📦 Install
npm install -g @saptools/cf-inspector
# or
pnpm add @saptools/cf-inspector
cf-inspector --version
Note
Requires Node.js ≥ 20. Cloud Foundry support uses
@saptools/cf-debugger, which is installed automatically as a normal runtime dependency.
Updates
Every command first checks npm for a newer @saptools/cf-inspector (at most once an hour, one small request
with a 2-second timeout) and, when one exists, installs that exact version with the package manager
that owns the running binary and re-runs the command you typed on the new version. Both steps are
announced on stderr; nothing is printed when the install is already current:
cf-inspector: updating 0.9.0 -> 0.10.0 ...
cf-inspector: updated to 0.10.0; re-running the command
If the install cannot complete, one stderr line gives the manual command and the command runs on the
installed version; that version is not retried for a day. cf-inspector self-update forces the check and
install now; cf-inspector self-update --check only reports.
| Control | Effect |
|---|---|
SAPTOOLS_AUTO_UPDATE=on|notify|off | on (default) installs and re-runs; notify prints the manual command once per version; off never checks. Applies to every @saptools CLI. |
CF_INSPECTOR_AUTO_UPDATE | same values, this CLI only; wins over the global variable |
SAPTOOLS_UPDATE_INTERVAL_MINUTES | minutes between checks (default 60; 0 checks on every run) |
SAPTOOLS_NPM_REGISTRY | registry to check and install from (default: npm's configured registry, then npmjs) |
SAPTOOLS_UPDATE_DEBUG=1 | explain on stderr why nothing happened |
The updater switches itself off in CI (CI set), under NODE_ENV=test or NO_UPDATE_NOTIFIER, when
the binary runs from a source checkout, an npm link or an npx cache, and inside the re-run itself.
It never writes to stdout, never asks for input, never uses sudo, and never moves onto a prerelease.
Its state lives in ~/.saptools/updates/.
🚀 Quick Start
Cloud Foundry app (auto-tunnel)
export SAP_EMAIL=...
export SAP_PASSWORD=...
cf-inspector snapshot \
--region eu10 --org my-org --space dev --app my-srv \
--bp src/handler.ts:42 \
--remote-root 'regex:^/(home/vcap/app|example-root-.*)$'
This command internally calls @saptools/cf-debugger to open the SSH tunnel, runs the snapshot through it, and tears the tunnel down on exit.
Cloud Foundry targeting is deliberately deterministic: --app requires
--region, --org, and --space. The CLI never inherits missing selectors
from ambient cf target state. Use --api-endpoint only when the selected
region needs an explicit endpoint override.
Worker behavior
When no isolate selector is passed, snapshot, watch, exception, log, and
check-breakpoint attach to the main isolate and every NodeWorker under the
selected raw target. Breakpoints are mirrored to workers that attach later, the
first matching pause wins, and JSON/human output identifies the winning
isolate as either {"kind":"main"} or
{"kind":"worker","workerId":"…"}. Losing isolates that also paused are
resumed before the command continues or exits.
Use --worker-id <id> to pin a stable live worker ID from list-targets,
--worker <index> for the legacy positional selector, --target <index> to
pin a raw inspector target, or --main-only to deliberately ignore workers.
Explicit selectors preserve single-isolate behavior.
Automation readiness contract
Commands that arm debugger behavior before waiting (snapshot, watch,
exception, and log) accept --ready-event. The flag writes exactly one
compact JSON object to stderr after every session present during initial
arming has completed its CDP setup and immediately before the command waits or
streams:
{"event":"breakpoint-armed","schemaVersion":1,"command":"snapshot","sessions":3,"resolvedLocations":2,"timeoutMs":30000}
This is the supported synchronization contract for callers that must start the
CLI before firing an HTTP request, queue job, or other external trigger. Parse
the JSON line and require event === "breakpoint-armed" plus
schemaVersion === 1, then confirm command matches the invocation; do not
poll prose such as Waiting up to.
| Field | Contract |
|---|---|
event | Always "breakpoint-armed" |
schemaVersion | Event schema version; currently 1 |
command | snapshot, watch, exception, or log |
sessions | Number of main/worker sessions whose initial arming completed before emission |
resolvedLocations | Total locations V8 had already resolved, or null for exception; 0 can still represent an accepted URL breakpoint that may resolve when a script loads |
timeoutMs | Snapshot/exception wait or watch per-hit timeout; null for log |
Workers attaching after this one-time event are still armed dynamically, but
are not included in its sessions or resolvedLocations totals. If initial
arming fails or the command is cancelled first, no readiness event is emitted.
With log --ready-event, matching events received while the remaining initial
sessions are still arming are neither emitted nor counted, so the log stream
cannot precede its readiness marker.
Without --ready-event, stderr/stdout behavior is unchanged. An explicitly
requested event is still emitted with snapshot --quiet.
🧰 CLI
📸 cf-inspector snapshot
Set one or more breakpoints, wait for any of them to hit, capture frame metadata and requested expressions, auto-resume, exit.
# Conditional snapshot — only pauses for the user we care about
cf-inspector snapshot --port 9229 \
--bp src/handler.ts:42 \
--condition 'req.userId === "abc"' \
--capture 'req.body'
# Multi-breakpoint — first hit wins (useful when you don't know which path is taken)
cf-inspector snapshot --port 9229 \
--bp src/auth.ts:120 \
--bp src/auth.ts:155 \
--bp src/auth.ts:180 \
--capture 'req.url, this.user'
| Flag | Description |
|---|---|
--port <number> | Local port the inspector or tunnel listens on. Required unless --app/--region/--org/--space are all set |
--region/--org/--space/--app | Explicit Cloud Foundry target. All four are required when --port is omitted; ambient cf target is never consulted |
--api-endpoint <url> | Override the API endpoint resolved from --region |
--target <index> | Raw /json/list target index (default: 0) |
--worker <index> | Nested NodeWorker index reported under the selected raw target by list-targets |
--worker-id <id> | Stable live NodeWorker ID reported by list-targets |
--main-only | Attach only to the main isolate and ignore NodeWorkers |
--bp <file:line> | Required. Source location to break at. Pass multiple times to race several locations — the first one to hit wins |
--condition <expr> | Native breakpoint condition. It is compile-checked before arming; mutation-shaped conditions require --allow-mutation because CDP provides no side-effect guard for native conditions |
--hit-count <n> | Skip the first N − 1 hits and only pause on the Nth (combines with --condition via logical AND) |
--capture <expr,…> | Top-level comma-separated expressions evaluated in the paused frame under V8's side-effect guard. Nested commas inside objects, arrays, calls, or strings are preserved. Objects are materialized to JSON strings when serializable |
--setup-eval <expr> | Repeatable, order-preserving global expression evaluated before breakpoint setup. It is mutation-capable and only receives an advisory warning |
--stack-depth <n> | Walk this many call frames per hit (default: 1, top frame only). When > 1, the result includes a stack array |
--stack-captures <expr,…> | Expressions evaluated on each captured call frame under the same side-effect guard as --capture |
--allow-mutation | Disable V8's capture side-effect guard and explicitly allow mutation-shaped native conditions. Heuristic matches are annotated with mutationRisk: true |
--timeout <seconds> | How long to wait for the breakpoint to hit (default: 30) |
--max-value-length <chars> | Maximum characters per captured value before truncation (one-shot default: 131072). Explicit values are honored exactly |
--remote-root <value> | Optional path-mapping anchor: literal path or regex:<pattern> / /pattern/flags |
--include-scopes | Include expanded paused-frame scopes under topFrame.scopes. Omitted by default to keep targeted captures concise |
--no-json | Print a human-readable summary instead of JSON |
--quiet | Suppress snapshot progress messages on stderr |
--ready-event | Emit the versioned breakpoint-armed JSON event on stderr after every current isolate is armed |
--keep-paused | Skip Debugger.resume after capture |
--fail-on-unmatched-pause | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
Snapshot progress is printed to stderr by default, including Cloud Foundry
login/tunnel setup, inspector connection, breakpoint binding, the breakpoint
wait, capture, resume, and cleanup phases. The final JSON document remains the
only content written to stdout, so piping it to jq or another parser stays
safe. Pass --quiet to suppress these progress lines; warnings and errors still
use stderr.
Snapshot JSON includes frame metadata and captures by default. topFrame.scopes
is only present with --include-scopes because scope objects can be large and
drown out targeted captures. Values are raw debugger values, so be careful when
sharing logs.
Capture expressions are read-only by default. cf-inspector sends
throwOnSideEffect: true to V8 for both --capture and --stack-captures.
Assignments, mutating methods such as push, and calls V8 cannot prove pure
are returned as a blocked capture with blocked: true, mutationRisk: true,
and a MUTATION_NOT_ALLOWED error. Pass --allow-mutation only when changing
the live inspectee is intentional. The opt-in disables the V8 guard and adds
mutationRisk: true when the advisory syntax scan recognizes a likely
mutation; arbitrary function calls can still mutate without being recognized.
--condition is different: V8 executes it internally as a native breakpoint
condition, where CDP has no throwOnSideEffect option. Mutation-shaped native
conditions are rejected unless --allow-mutation is present. --setup-eval
and the standalone eval command remain mutation-capable by design and emit
advisory warnings for recognizable mutation syntax.
A blocked capture remains a normal additive CapturedExpression, so one unsafe
expression does not corrupt the rest of the snapshot:
{"expression":"items.push(1)","error":"MUTATION_NOT_ALLOWED: V8 blocked the capture expression ...","mutationRisk":true,"blocked":true}
With --allow-mutation, recognized mutation syntax runs and the result carries
"mutationRisk": true; ordinary reads do not gain the field.
Truncation contract
JSON truncation is always out-of-band. A text value longer than the effective
limit is cut to exactly that many JavaScript characters and gains
"truncated": true plus its full "originalLength"; no ellipsis is appended
to the JSON value. These fields are absent when no cut occurs. Human output may
add a visual ellipsis.
Expanded objects and scopes also report bounded structural capture. The
VariableSnapshot, ScopeSnapshot, or FrameSnapshot whose properties,
variables, or scopes were cut gains truncated: true and the exact direct
omittedCount. A serialized object capture propagates the aggregate known
omission count to its CapturedExpression. ExceptionSnapshot additionally
uses valueOriginalLength and descriptionOriginalLength so consumers can
identify which field was cut; its compatibility originalLength is the larger
reported field length.
One-shot snapshot and exception commands default to 131072 characters.
Repeated watch and log events default to 4096. All four accept
--max-value-length, and an explicit limit is applied exactly.
{"expression":"largeText","value":"exactly-N-characters","type":"string","truncated":true,"originalLength":250000}
pausedDurationMs measures the client-observed time from receiving the matching
pause event until Debugger.resume completes. With --keep-paused, it is null
because resume is intentionally skipped.
If the target pauses somewhere else first, for example another debugger's
breakpoint or a debugger; statement, snapshot does not resume it by default.
It warns once, waits for Debugger.resumed, then continues waiting for its own
breakpoint within the remaining timeout. Use --fail-on-unmatched-pause when a
strict immediate error is preferred.
For Cloud Foundry targets, replace --port with
--region/--org/--space/--app. Cloud Foundry commands and tunnel readiness
allow up to 180 seconds by default. For commands without their own wait
semantics, --timeout <seconds> controls CF tunnel readiness. For breakpoint
and exception commands, --timeout is reserved for the command wait and tunnel
readiness keeps the 180-second default.
📡 cf-inspector log
Set a non-pausing logpoint and stream the evaluated expression each time the line executes. The inspectee does not pause, but the expression and condition still execute against live state and can mutate it.
# Stream user IDs hitting handler.ts:42 for 30 seconds
cf-inspector log \
--port 9229 \
--at src/handler.ts:42 \
--expr 'JSON.stringify({ user: req.user, body: req.body })' \
--duration 30
Output is JSON Lines on stdout (one event per line) plus a summary trailer on stderr:
{"ts":"2026-04-29T...","at":"src/handler.ts:42","value":"{\"user\":\"alice\",\"body\":{}}"}
{"ts":"2026-04-29T...","at":"src/handler.ts:42","value":"{\"user\":\"bob\",\"body\":{}}"}
// stderr:
{"stopped":"duration","emitted":2}
When the user expression throws, the event is emitted with error instead of value so the stream never silently gaps:
{"ts":"…","at":"src/handler.ts:42","error":"Cannot read properties of undefined (reading 'user')"}
| Flag | Description |
|---|---|
--port <number> | Local port the inspector or tunnel listens on. Required unless --app/--region/--org/--space are all set |
--target <index> / --worker <index> / --worker-id <id> | Pin one raw target or worker instead of automatic fan-out |
--main-only | Ignore workers and attach only to the main isolate |
--at <file:line> | Required. Source location to log at |
--expr <expression> | Required. JavaScript expression evaluated at each hit, wrapped in try/catch on the inspectee side. It is mutation-capable; recognizable risks produce a warning |
--duration <seconds> | Stop streaming after N seconds (default: run until SIGINT) |
--max-events <n> | Stop streaming after emitting N log events. The trailer reports stopped: "max-events" |
--hit-count <n> | Start emitting once the line has been hit N or more times |
--condition <expr> | Mutation-capable native condition evaluated on the inspectee. Recognizable risks produce a warning. Composes with --hit-count via logical AND |
--max-value-length <chars> | Maximum characters per log value (streaming default: 4096). Truncated events include truncated and originalLength |
--remote-root <value> | Optional path-mapping anchor (same DSL as snapshot) |
--no-json | Print human-readable lines instead of JSON Lines |
--ready-event | Emit the versioned breakpoint-armed JSON event on stderr after every current isolate is armed |
Native logpoint expressions and conditions have no V8 side-effect gate. The CLI warns when its best-effort syntax scan recognizes assignments or common mutating calls, but that scan cannot prove an arbitrary function is pure. Treat logpoint expressions as executable live code.
🔁 cf-inspector watch
Stream a snapshot per breakpoint hit. The inspectee is paused briefly while
captures are evaluated, then resumed automatically; output is JSON Lines on
stdout with a trailer on stderr (same shape as log).
cf-inspector watch --port 9229 \
--bp src/handler.ts:42 \
--capture 'user.id, payload' \
--condition 'user.id !== "system"' \
--duration 30 \
--max-events 50
Each event is a WatchEvent:
{"ts":"2026-04-29T...","at":"file:///app/src/handler.ts:42","hit":1,"reason":"other","hitBreakpoints":["..."],"captures":[{"expression":"user.id","value":"\"alice\""}]}
{"ts":"2026-04-29T...","at":"file:///app/src/handler.ts:42","hit":2,"reason":"other","hitBreakpoints":["..."],"captures":[{"expression":"user.id","value":"\"bob\""}]}
// stderr trailer:
{"stopped":"max-events","emitted":50}
| Flag | Description |
|---|---|
--port <number> | Local port the inspector or tunnel listens on. Otherwise pass all explicit Cloud Foundry selectors |
--target <index> / --worker <index> / --worker-id <id> | Pin one raw target or worker instead of automatic fan-out |
--main-only | Ignore workers and attach only to the main isolate |
--bp <file:line> | Required. Source location to capture on (repeatable) |
--capture <expr,…> | Top-level comma-separated expressions evaluated per hit under V8's side-effect guard |
--setup-eval <expr> | Repeatable mutation-capable global expression evaluated before breakpoint setup; recognizable risks produce a warning |
--condition <expr> | Native condition; mutation-shaped conditions require --allow-mutation |
--hit-count <n> | Start emitting once the line has been hit N or more times |
--remote-root <value> | Path-mapping anchor (same DSL as snapshot) |
--duration <seconds> | Stop streaming after N seconds (default: until SIGINT) |
--max-events <n> | Stop streaming after emitting N events |
--timeout <seconds> | How long to wait for the next hit before giving up (default: 30) |
--max-value-length <chars> | Maximum characters per captured value (streaming default: 4096) |
--stack-depth <n> | Walk this many call frames per hit (default: 1) |
--stack-captures <expr,…> | Expressions evaluated on each call frame under the capture side-effect guard |
--allow-mutation | Disable the capture side-effect guard and explicitly allow mutation-shaped native conditions |
--include-scopes | Include expanded paused-frame scopes per hit |
--no-json | Print human-readable lines instead of JSON Lines |
--ready-event | Emit the versioned breakpoint-armed JSON event on stderr after every current isolate is armed |
💥 cf-inspector exception
Pause on a thrown exception, capture the exception value plus the paused frame, then resume.
cf-inspector exception --port 9229 \
--type uncaught \
--capture 'this' \
--stack-depth 4 \
--stack-captures 'arguments[0]' \
--timeout 30
Result is a SnapshotResult with an extra exception field:
{
"reason": "exception",
"hitBreakpoints": [],
"capturedAt": "2026-04-29T...",
"pausedDurationMs": 0.5,
"topFrame": {"functionName": "validate", "url": "...", "line": 42, "column": 5},
"exception": {"value": "{\"message\":\"missing field\",\"name\":\"Error\"}", "type": "object", "description": "missing field"},
"captures": [],
"stack": [...]
}
| Flag | Description |
|---|---|
--port or explicit --region/--org/--space/--app | Select the local inspector or deterministic Cloud Foundry target |
--target <index> / --worker <index> / --worker-id <id> | Pin one raw target or worker instead of automatic fan-out |
--main-only | Ignore workers and attach only to the main isolate |
--type <state> | Pause on which exceptions: uncaught (default), caught, or all |
--capture <expr,…> | Top-level expressions evaluated in the paused frame under V8's side-effect guard |
--stack-depth <n> | Walk this many call frames (default: 1) |
--stack-captures <expr,…> | Expressions evaluated on each frame under V8's side-effect guard |
--allow-mutation | Disable the capture side-effect guard; heuristic matches gain mutationRisk: true |
--include-scopes | Include paused-frame scopes |
--remote-root <value> | Path-mapping anchor (only used if you also wire snapshot helpers) |
--timeout <seconds> | How long to wait for an exception (default: 30) |
--max-value-length <chars> | Maximum characters per captured value (one-shot default: 131072) |
--keep-paused | Skip Debugger.resume after capture |
--no-json | Print a human-readable summary instead of JSON |
--ready-event | Emit the versioned breakpoint-armed JSON event on stderr after pause-on-exception is active in every current isolate |
🧮 cf-inspector eval
Evaluate one expression with Runtime.evaluate in the selected isolate's
global scope and print the result. eval is intentionally mutation-capable and
has no side-effect gate; recognizable mutation syntax emits an advisory warning
to stderr. For read-only paused-frame values, use snapshot --capture or call
the programmatic evaluateOnFrame(..., { throwOnSideEffect: true }) API. Plain
evaluateOnFrame(...) remains unrestricted by default for backward
compatibility.
cf-inspector eval --port 9229 --expr 'process.uptime()'
📜 cf-inspector list-scripts
Print every script known by the main isolate and each currently attached
NodeWorker (useful for debugging path-mapping issues). Each JSON entry carries
isolate: {"kind":"main"} or
isolate: {"kind":"worker","workerId":"…"}, so duplicate per-isolate
scriptId values remain unambiguous. Add --filter <pattern> to narrow noisy
script lists with a literal/wildcard pattern; | separates alternatives and
.* / .+ match variable text.
cf-inspector list-scripts --port 9229 --filter 'dist/.+\.js'
With --no-json, rows are
scriptId<TAB>url<TAB>isolate, where isolate is main or
worker:<workerId>. --main-only, --worker-id, --worker, and --target
retain their normal narrowing behavior.
🎯 cf-inspector list-targets
Print raw /json/list inspector targets with stable index values, likely
worker labels, and the total target/worker counts on stderr. For each raw
target, the command also probes Node's NodeWorker CDP domain and lists live
nested workers with their own indexes.
cf-inspector list-targets --port 9229
cf-inspector snapshot --port 9229 --bp dist/worker.js:42
cf-inspector snapshot --port 9229 --worker-id 1 --bp dist/worker.js:42
# If a runtime publishes a worker as another raw /json/list target instead:
cf-inspector snapshot --port 9229 --target 1 --bp dist/worker.js:42
JSON output nests workers beneath their raw target:
[
{
"index": 0,
"description": "node.js instance",
"id": "target-id",
"type": "node",
"title": "app.mjs",
"url": "file:///app/app.mjs",
"webSocketDebuggerUrl": "ws://127.0.0.1:9229/target-id",
"likelyWorker": false,
"workerDiscoverySupported": true,
"workers": [
{"index": 0, "workerId": "1", "type": "worker", "title": "jobs", "url": "file:///app/worker.mjs"}
]
}
]
--target selects a complete raw inspector endpoint. --worker-id selects a
live nested worker by its stable ID, while --worker retains positional-index
selection for compatibility. Modern Node.js 20–25 verification found workers on
the NodeWorker path, including workers already alive before post-hoc
SIGUSR1 inspector activation; the raw-target selector remains supported for
runtimes that publish that shape.
When no selector is passed, breakpoint-oriented commands attach to raw target
0 and automatically fan out across its main isolate and nested workers. If only one raw target
and no workers are visible, list-targets explains that the worker may have
exited, the runtime may not expose NodeWorker discovery, or a separate worker
port may be unreachable through the single Cloud Foundry tunnel. Rerun the
command while the worker is alive before pinning its workerId.
If list-targets, attach, or another command reports ECONNREFUSED, the local inspector or tunnel on that port is usually stale/closed. Restart the local Node inspector or tunnel and retry; for Cloud Foundry targets, pass the complete --region/--org/--space/--app selector so cf-inspector can open a fresh tunnel.
✅ cf-inspector check-breakpoint
Check whether a file:line can accept a breakpoint before arming one. The
command uses the same path mapping as --bp and checks every currently attached
isolate by default.
cf-inspector check-breakpoint --port 9229 --bp dist/handler.js:42
status: "script-not-loaded" means no loaded script matched the file/path
mapping; use list-scripts, adjust --remote-root, or trigger lazy loading.
status: "unbreakable" means the script is loaded but that exact line has no
V8 break location; choose a neighboring executable line. status: "breakable"
includes the concrete script IDs, isolate identities, lines, and columns.
🔗 cf-inspector attach
Connect, fetch the runtime version, print it, disconnect. Useful as a smoke-test that the tunnel is healthy.
cf-inspector attach --port 9229
attach checks the port-level /json/version endpoint, so raw-target and
worker selectors do not apply to this smoke test.
Exact CDP tracing APIs
The library also exports lower-level, validated primitives for tools that need to plan and step an exact loaded function instead of using a URL-regex breakpoint:
const scripts = listScripts(session);
const source = await getScriptSource(session, scripts[0].scriptId);
const locations = await getPossibleBreakpoints(session, {
start: { scriptId: scripts[0].scriptId, lineNumber: 10 },
restrictToFunction: true,
});
const breakpoint = await setBreakpointAtLocation(session, { location: locations[0] });
const pause = await waitForPause(session, {
timeoutMs: 30_000,
breakpointIds: [breakpoint.breakpointId],
signal: abortController.signal,
});
await stepOver(session);
await releaseObject(session, objectId);
ScriptLocation and BreakLocation use CDP-native zero-based line and column numbers. Exact
breakpoint setup returns both the requested and actual location, then fails closed and removes the
breakpoint if V8 resolves a different script, line, or column. An omitted column means CDP column
zero. RemoteObjectInfo.completeness is truncated for logical values stored in internal slots
(including maps, sets, promises, and dates) and unavailable for proxies; it is omitted for
ordinary objects. waitForPause accepts an AbortSignal and cleans its event listener and timer
on success, timeout, abort, or session close. The same layer exports stepInto, stepOver,
stepOut, releaseObject, and releaseObjectGroup for bounded controllers.
Programmatic Cloud Foundry tunnels can select the same process instance and Node PID used for both the remote signal and SSH forwarding:
const tunnel = await openCfTunnel({
region: "eu10",
org: "my-org",
space: "dev",
app: "orders-srv",
process: "worker",
instance: 2,
nodePid: 4312,
});
openCfTunnel retains backward-compatible reuse when cf-debugger reports a healthy local port
for an existing session. Controllers that must prove they own and can dispose the tunnel should use
openOwnedCfTunnel; it propagates SESSION_ALREADY_RUNNING instead of parsing or borrowing a
pre-existing session.
🔭 How it works
┌──────────────────────┐ 1. GET http://127.0.0.1:<port>/json/list
│ cf-inspector │ 2. Open the selected raw WebSocket target
│ snapshot --bp X:Y │ ─►3. Auto-attach to current and future NodeWorker sub-sessions
└──────────────────────┘ 4. Debugger.enable + Runtime.enable
│ 5. Debugger.setBreakpointByUrl({ urlRegex, lineNumber: Y - 1 })
▼ 6. Wait for `Debugger.paused`
JSON snapshot 7. Debugger.evaluateOnCallFrame({ throwOnSideEffect: true, ... })
8. Runtime.getProperties(...) when object/scopes are expanded
9. Debugger.resume (unless --keep-paused)
Path mapping uses CDP's first-class urlRegex:
--remote-root | Resulting urlRegex (line 42 of src/handler.ts) |
|---|---|
| omitted | `(?:^ |
/home/vcap/app (literal) | ^file:///home/vcap/app/src/handler\.(?:ts|js|mts|mjs|cts|cjs)$ |
regex:^/example-root-.*$ | ^file:///example-root-.*/src/handler\.(?:ts|js|mts|mjs|cts|cjs)$ |
regex:^/(home/vcap/app|example-root-.*)$ | ^file:///(home/vcap/app|example-root-.*)/src/handler\.(?:ts|js|mts|mjs|cts|cjs)$ |
.ts ↔ .js is folded into the regex automatically because Node's V8 inspector normally serves both the source-mapped TypeScript URL and the runtime JavaScript URL — matching either is correct.
⚙️ Composing with cf-debugger
If --port is omitted, all of --region/--org/--space/--app are required. The
CLI does not read ambient cf target state. It calls startDebugger(...) from
@saptools/cf-debugger, attaches over the SSH tunnel, and disposes the tunnel
on exit. You get the same one-shot UX whether the target is local or in CF.
The tunnel forwards one inspector port. Nested NodeWorker sessions carried by
that inspector connection are auto-attached by breakpoint-oriented commands and
can be pinned with --worker-id; a worker exposing
only an unrelated separate port is outside that tunnel's reach.
cf-inspector snapshot \
--region eu10 --org my-org --space dev --app my-srv \
--bp src/handler.ts:42 \
--capture 'req.url, this.user'
🌐 Related
- 🐛
@saptools/cf-debugger— opens the SSH inspector tunnel - ☁️
@saptools/cf-sync— snapshot CF topology + DB bindings into JSON - 🗂️ saptools monorepo — the full toolbox
👨💻 Author
dongtran ✨
📄 License
MIT
Made with ❤️ to make your work life easier!
History
Fixes 1
Notes
Fixed
- Suppressed the CodeQL
js/regex-injectionalert oncheck-breakpoint'snew RegExp(urlRegex, "u")with an inlinecodeql[js/regex-injection]comment, backed by a code comment explaining why:--remote-root's raw-regex mode is an intentional, already-hardened (0.9.2) escape hatch, and CodeQL's only recognized sanitizer for this query is escaping the input — which would defeat the feature outright. No behavior change.
Fixes 1
Notes
Fixed
- Hardened
--remote-root regex:...//pattern/flags: patterns are now capped at 200 characters and rejected outright if they contain a nested-quantifier shape (e.g.(x+)+), which can cause catastrophic regex backtracking. This mode intentionally lets an operator supply a raw regex (unlike every other path here, which already escapes user input), so the guard is defense-in-depth against a self-inflicted hang rather than a change in what the flag can express for any reasonable pattern.
- Hardened
Other 1
Notes
- Accept the self-updating releases of
@saptools/cf-debuggeras dependencies (range widened, no behaviour change).
- Accept the self-updating releases of
Other 1
Notes
- Self-updating. Every command now checks npm for a newer
@saptools/cf-inspector(at most once an hour) and, when one exists, installs that exact version and re-runs the command on it, announcing both steps on stderr.SAPTOOLS_AUTO_UPDATE=on|notify|offcontrols it (see the README's Updates section); it is off by itself in CI, in tests, from a source checkout and inside the re-run. Newcf-inspector self-update [--check]forces the check and install now.--versionnow readspackage.jsonat runtime. Both come from the private, build-time-bundled@saptools/corepackage; the published tarball gains no runtime dependency.
- Self-updating. Every command now checks npm for a newer
Other 1
Notes
CDP request reliability
- Increased the default timeout for individual Chrome DevTools Protocol requests from 15 seconds
to 60 seconds for main and worker inspector sessions. Explicit
requestTimeoutMsoverrides and command-level breakpoint/exception wait timeouts remain unchanged.
- Increased the default timeout for individual Chrome DevTools Protocol requests from 15 seconds
to 60 seconds for main and worker inspector sessions. Explicit
Fixes 4, Other 1
Notes
Machine-readable debugger readiness
- Added opt-in
--ready-eventsupport tosnapshot,watch,exception, andlog. Each command writes the same versionedbreakpoint-armedJSON event to stderr after all initially/currently registered isolates finish arming and immediately before waiting or streaming. - Kept default stdout, stderr, progress prose, and final JSON behavior
unchanged when the new flag is absent.
snapshot --quiet --ready-eventsuppresses progress lines while retaining the explicitly requested event. - Gate
log --ready-eventoutput until aggregate arming completes, so no matching log event is emitted or counted ahead of the readiness marker. - Added a local externally-triggered inspector fixture proving all four commands can synchronize without guessed startup delays.
Isolate-aware script listing
- BEHAVIOR CHANGE:
list-scriptsnow follows the normal implicit fan-out model, aggregating scripts from the main isolate and all current workers. JSON entries include anisolatetag; human rows append isolate as a third tab-separated column. Explicit isolate selectors still narrow the command.
- Added opt-in
Fixes 3
Notes
Exclusive debugger sessions
- Refuse a second local
cf-inspectorsession for the same inspector target before it opens a debugger WebSocket. The dedicatedTARGET_ALREADY_DEBUGGEDerror identifies the live owner and explains the risk to application traffic. - Added process-owned, target-scoped lock files under
~/.saptools/cf-inspector/locks, with automatic dead-PID reclamation and ownership-safe cleanup on normal, error, timeout, and signal exits. - Added local real-inspector coverage for deterministic CDP breakpoint-ID collisions, competing resume behavior, command-wide exclusion, first-session continuity, and SIGKILL stale-lock recovery.
- Refuse a second local
Breaking 1, Other 6
Notes
Automatic isolate fan-out
- BREAKING: snapshot, watch, exception, and log now attach to the main isolate plus every current and newly-attached NodeWorker when no explicit isolate selector is passed. Results identify the winning/emitting isolate.
- Added stable
--worker-id <id>selection and explicit--main-only; retained the existing meanings of--worker <index>and--target <index>. - Added per-session breakpoint outcomes, first-pause racing, paused-loser resume, bounded explicit breakpoint cleanup, and dynamic late-worker arming.
Tunnel and breakpoint reliability
- Reused Cloud Foundry tunnel ports must pass
/json/versionliveness polling before they are reported ready; stale/not-yet-ready ports now fail with an actionable error at tunnel acquisition. - Long-running watch sessions probe tunnel liveness and fail after three consecutive unsuccessful probes instead of hanging indefinitely.
- Added
check-breakpoint, which distinguishes unloaded/path-mismatched scripts from loaded scripts whose requested line is not breakable.
Other 1
Notes
- Require
@saptools/cf-debugger@^0.1.16so consumers pick up the app-port Node process auto-selection when an instance runs several Node processes.
- Require
Other 1
Notes
Async call stack depth
- Added
setAsyncCallStackDepth(session, maxDepth)wrappingDebugger.setAsyncCallStackDepth, so consumers can request async stack frames on paused events and reset the depth during cleanup.
- Added
Other 9
Notes
Exact function tracing primitives
- Added loaded script metadata and exact source retrieval through
Debugger.getScriptSource. - Added validated
Debugger.getPossibleBreakpointsand exact-location breakpoint APIs with requested/actual location metadata. - Exact-location setup now removes and rejects bindings that V8 relocates to another script, line, or zero-based column.
- Added step into/over/out and remote object/group release wrappers.
- Preserved rich pause frames, scopes,
this, return values, script metadata, and async stack data. - Remote descriptors now mark proxy values unavailable and internal-slot subtypes truncated when own-property traversal cannot represent their complete logical value.
- Made pause waits abortable without leaking event listeners or timers.
- Forwarded Cloud Foundry process, instance, and Node PID selectors through
openCfTunnel. - Added fail-closed
openOwnedCfTunnelfor consumers that must own cleanup. The existingopenCfTunnelkeeps its legacy same-machine port-reuse behavior for backward compatibility.
- Added loaded script metadata and exact source retrieval through
Breaking 2, Other 16
Notes
Deterministic Cloud Foundry targeting
- BREAKING:
--appno longer fills missing selectors from ambientcf targetstate. Every command now requires explicit--region,--org, and--spacealongside--app;--portbehavior is unchanged. - Missing selectors fail before any Cloud Foundry shell-out and name every required flag.
Mutation guardrails
- BREAKING:
snapshot,watch, andexceptionnow evaluate both--captureand--stack-captureswith V8's side-effect guard by default. Expressions V8 cannot prove side-effect-free return a blockedCapturedExpressionwithblocked: true,mutationRisk: true, and aMUTATION_NOT_ALLOWEDerror. - Added
--allow-mutationto those capture commands. Recognized mutation syntax runs under the opt-in and is annotated withmutationRisk: true. - Added the optional
throwOnSideEffectargument to the exportedevaluateOnFrameAPI. Its programmatic default remains unrestricted for backward compatibility; the capture commands opt into the safe mode. - Mutation-shaped native
snapshot/watch --conditionexpressions require the same explicit opt-in. Mutation-capablelog,eval, and--setup-evalsurfaces remain executable and now emit advisory warnings.
Worker sessions and diagnostics
- Added
--worker <index>and Node's nestedNodeWorkertransport so snapshot, watch, exception, eval, log, and list-scripts can inspect a live worker beneath a raw inspector target. - Extended
list-targetswith raw-target counts, worker labels, nested worker metadata, and actionable single-target guidance. Implicit target or worker selection and bound breakpoints that produce no hit now emit targeted hints. - Preserved
--target <index>for runtimes that publish workers as independent/json/listendpoints. - Removed ignored
--target/--workeroptions fromattachand the ignored--targetoption fromlist-targets; those commands operate at the inspector port level or enumerate all raw targets, respectively. - Verified the NodeWorker path with real Node.js 20, 22, 23, 24, and 25
processes, including a worker alive before post-hoc
SIGUSR1inspector activation, and added a real-inspector worker E2E fixture.
Explicit truncation
- BEHAVIOR CHANGE: one-shot
snapshotandexceptioncaptures now default to131072characters; repeatedwatchandlogevents retain the compact4096-character default. - BEHAVIOR CHANGE: truncated JSON text is cut to exactly
--max-value-lengthand no longer appends an in-band...suffix. - Added optional
truncated: trueand exactoriginalLengthmetadata to text capture shapes. AddedomittedCountfor bounded object, scope, and frame expansion, plus field-local exception lengths and logpoint metadata. - Untruncated output remains unchanged and omits all truncation fields.
Reliability
- Inspector discovery now fails immediately on malformed JSON responses instead of retrying an invalid payload until the discovery timeout expires.
- BREAKING:
Others providing
Compare all 22 on the Debugging page →- dbg_value_statementdbg_value_statementAn ABAP Debugger script that turns a variable's structure or table into the VALUE statement that recreates it, with a tree explorer, also callable as a function module from a report.independent, abapDebugger script generating a VALUE statement for a variablefrom ABAP
- dump_infodump_infoDisplays the variables an ABAP short dump captured, for dumps selected on a selection screen; its documentation does not state which dumps it reads, which releases it runs on or how it installs.independent, abapDisplays the variables of ABAP short dumpsfrom ABAP
- com.sap.cds:cds-feature-consolecom.sap.cds:cds-feature-consoleA CAP Java plugin that connects a running application to CAP console, which consumes its remote monitoring data; CAP console itself is installed separately and still being developed.sap official, capConnects a running CAP Java app to CAP console for monitoringfrom CAP
- com.sap.cds:cds-feature-dev-dashboardcom.sap.cds:cds-feature-dev-dashboardA browser dashboard for a running CAP Java service that shows and manages its messaging, resilience and multitenancy state while developing, open to users with the cds.Developer role.sap official, capBrowser dashboard for a running CAP Java service during developmentfrom CAP
- @cap-js/console@cap-js/consoleConnects a running CAP application to the CAP console.sap official, capConnects a running CAP application to the CAP consolefrom CAP
- @cap-js/data-inspector@cap-js/data-inspectorInspects database entities and their contents at the CAP service level.sap official, capInspects database entities and contents at CAP service levelfrom CAP
- abapsmithabapsmithAn MCP server that lets an LLM develop in ABAP on a live system over ADT, with writes off by default and every mutation recorded in a local journal that can undo it.independent, abapABAP MCP server over ADT with a local journal that can undo writesfrom ABAP
- sap-cap-debuggersap-cap-debuggerAttaches a debugger to a CAP application already running on Cloud Foundry — enables the Node inspector inside the container, assigns a port and hands you a Chrome DevTools or VS Code attach.independent, capAttaches a debugger to a CAP app running on Cloud Foundryfrom CAP
- ABAP-Object-VisualizerABAP-Object-VisualizerA debugger script that writes an object hierarchy to the clipboard at a breakpoint as a Graphviz description, for pasting into a renderer such as webgraphviz.com.independent, abapDebugger script exporting an object hierarchy as Graphvizfrom ABAP
- abap_debugger_object_graph_extensionabap_debugger_object_graph_extensionAn ABAP debugger extension that shows the object on the debugger's Objects tab as a Graphviz graph in a browser window, built on ABAP Graph and Graphviz-browser and tested on NetWeaver 7.5.independent, abapABAP debugger extension drawing objects as a Graphviz graphfrom ABAP
- Graphviz-browserGraphviz-browserA static page that draws a Graphviz graph in a browser, written for the ABAP debugger object graph extension and deployable as BSP pages in the system the debugger runs in.independent, abapStatic page drawing Graphviz graphs for the ABAP debugger extensionfrom ABAP
- ABAP remote filesystemABAP remote filesystemA Visual Studio Code extension that opens an ABAP system over ADT as an editable filesystem and exposes system tools to GitHub Copilot and external MCP clients.independent, abapVS Code extension editing ABAP systems over ADT as a filesystemfrom ABAP
- @sap/cds-dk@sap/cds-dkThe command line client and development toolkit for CAP projects, with the commands cds build, cds deploy, cds watch and cds add among them.sap official, capCommand line client and development toolkit for CAPfrom CAP
- @saptools/cf-debugger@saptools/cf-debuggerOpens a verified Node.js inspector tunnel over cf ssh to an app on SAP BTP Cloud Foundry from one command, with its own local port for each concurrent session.independent, btpNode.js inspector tunnel over cf ssh to Cloud Foundry appsfrom BTP/BAIP
- @saptools/cf-explorer@saptools/cf-explorerSearches the files of an app deployed to SAP BTP Cloud Foundry over cf ssh, finding the app root, grepping the deployed code and turning matches into file and line candidates.independent, btpFile search in apps deployed to SAP BTP Cloud Foundry over cf sshfrom BTP/BAIP
- @saptools/cf-function-trace@saptools/cf-function-traceRecords how one JavaScript function's state changes, step by step, in a Node.js process running locally or on SAP BTP Cloud Foundry, and stores the run as a redacted timeline that can be queried later.independent, btpStep-by-step state recorder for one JavaScript function in Node.jsfrom BTP/BAIP
- @saptools/cf-hana@saptools/cf-hanaA command line and TypeScript API that runs SQL against the SAP HANA Cloud database bound to a Cloud Foundry app, addressed by a region/org/space/app selector.independent, btpSQL runner against the SAP HANA Cloud database bound to a CF appfrom BTP/BAIP
- @saptools/cf-live-trace@saptools/cf-live-traceInjects a bounded HTTP trace hook into a running Node.js app on SAP BTP Cloud Foundry and streams each request and response to the terminal as JSON.independent, btpHTTP trace hook streaming a running CF Node.js app's trafficfrom BTP/BAIP
- @saptools/cf-request-runner@saptools/cf-request-runnerDiscovers every API endpoint of a CAP service already deployed to Cloud Foundry by walking its service documents, $metadata and endpoints catalog — falling back to container SSH to parse .cds files.independent, capEndpoint discovery for CAP services deployed to Cloud Foundryfrom CAP
- @saptools/service-flow@saptools/service-flowTraces one CAP operation end to end across several repositories without running anything — handlers, helper packages, local database access, remote OData calls, external HTTP and async channels.independent, capStatic end-to-end tracer of one CAP operation across repositoriesfrom CAP
- Smart DebuggerSmart DebuggerA debugger script for the SAP GUI New Debugger that records every step and variable change of an ABAP program, so execution can be stepped backwards with F5 and F8.independent, abapSAP GUI debugger script recording steps so execution can run backwardsfrom ABAP
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
History in the atlas
- First seen.
For your agent
- Ref
artifact:saptools-cf-inspector- JSON
- /artifacts/saptools-cf-inspector.json
- MCP
- Connecting an agent