@saptools/cf-inspector
v0.4.3
Published
Set breakpoints, capture variable snapshots, and evaluate expressions on a SAP BTP Cloud Foundry Node.js app via the Chrome DevTools Protocol — agent-friendly, no IDE required.
Maintainers
Readme
🔍 @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 • API • How it works
✨ 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 ever pausing the inspectee (safe for production traffic), with optional--condition,--hit-count,--max-events - 🧠 Agent-friendly — JSON-by-default I/O, deterministic shape, bounded value previews for large debugger payloads
- 🧭 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 —
commander+wsonly, 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[!NOTE] Requires Node.js ≥ 20. For Cloud Foundry targets, also install
@saptools/cf-debugger(added automatically as a peer-style runtime dep).
🚀 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.
🧰 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 |
| --bp <file:line> | Required. Source location to break at. Pass multiple times to race several locations — the first one to hit wins |
| --condition <expr> | Only pause when this JS expression evaluates truthy in the paused frame. Errors in the condition are silently treated as false by V8 |
| --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 to evaluate in the paused frame; nested commas inside objects, arrays, calls, or strings are preserved. Object results are materialized to JSON strings when serializable, with fallback to CDP descriptions for non-serializable values |
| --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 to evaluate on each call frame in the captured stack |
| --timeout <seconds> | How long to wait for the breakpoint to hit (default: 30) |
| --max-value-length <chars> | Maximum characters per captured value before truncation (default: 4096) |
| --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 |
| --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.
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. --cf-timeout <seconds> overrides only the
tunnel-readiness phase; snapshot --timeout separately controls how long to
wait for a breakpoint hit.
📡 cf-inspector log
Set a non-pausing logpoint and stream the evaluated expression each time the line executes. Safe for production traffic — the inspectee never pauses.
# 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 30Output 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 |
| --at <file:line> | Required. Source location to log at |
| --expr <expression> | Required. JS expression to evaluate at each hit (wrapped in try/catch on the inspectee side) |
| --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> | Only log when this JS expression evaluates truthy on the inspectee. Composes with --hit-count via logical AND |
| --remote-root <value> | Optional path-mapping anchor (same DSL as snapshot) |
| --no-json | Print human-readable lines instead of JSON Lines |
🔁 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 50Each 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 |
| --bp <file:line> | Required. Source location to capture on (repeatable) |
| --capture <expr,…> | Top-level comma-separated expressions to evaluate per hit |
| --condition <expr> | Only emit hits where this expression evaluates truthy |
| --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 before truncation |
| --stack-depth <n> | Walk this many call frames per hit (default: 1) |
| --stack-captures <expr,…> | Expressions to evaluate on each call frame |
| --include-scopes | Include expanded paused-frame scopes per hit |
| --no-json | Print human-readable lines instead of JSON Lines |
💥 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 30Result 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 |
| --- | --- |
| --type <state> | Pause on which exceptions: uncaught (default), caught, or all |
| --capture <expr,…> | Top-level expressions to evaluate in the paused frame |
| --stack-depth <n> | Walk this many call frames (default: 1) |
| --stack-captures <expr,…> | Expressions to evaluate on each frame |
| --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 before truncation |
| --keep-paused | Skip Debugger.resume after capture |
| --no-json | Print a human-readable summary instead of JSON |
🧮 cf-inspector eval
Evaluate one expression with Runtime.evaluate in the global scope and print the result. For paused-frame values, use snapshot --capture or the programmatic evaluateOnFrame(...) API.
cf-inspector eval --port 9229 --expr 'process.uptime()'📜 cf-inspector list-scripts
Print every script the V8 instance knows about (useful for debugging path-mapping issues).
cf-inspector list-scripts --port 9229🔗 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🧑💻 Programmatic Usage
import {
connectInspector,
setBreakpoint,
waitForPause,
captureSnapshot,
evaluateOnFrame,
resume,
} from "@saptools/cf-inspector";
const session = await connectInspector({ port: 9229 });
const bp = await setBreakpoint(session, {
file: "src/handler.ts",
line: 42,
});
const pause = await waitForPause(session, { timeoutMs: 30_000 });
const snapshot = await captureSnapshot(session, pause, {
captures: ["this.user"],
maxValueLength: 4096,
});
const topFrame = pause.callFrames[0];
if (topFrame === undefined) {
throw new Error("Breakpoint paused without a call frame");
}
const customValue = await evaluateOnFrame(session, topFrame.callFrameId, "this.user");
await resume(session);
await session.dispose();
console.log({ bp, snapshot, customValue });| Export | Description |
| --- | --- |
| connectInspector(options) | Open a CDP WebSocket session against a port |
| setBreakpoint(session, location) | Set a breakpoint by file/line + optional remote root and hitCount |
| removeBreakpoint(session, id) | Remove a breakpoint by id |
| setPauseOnExceptions(session, state) | Configure exception pause state: none / uncaught / caught / all |
| waitForPause(session, options) | Resolve when the next Debugger.paused event fires; supports pauseReasons allow-list |
| captureSnapshot(session, pause, options) | Build a structured snapshot of the paused frame. Pass includeScopes: true to expand scopes, stackDepth + stackCaptures for multi-frame walks, or maxValueLength to override the default captured value limit |
| captureException(session, pause, maxValueLength) | Materialize the exception attached to a Debugger.paused event |
| walkStack(session, frames, options) | Walk a call stack and evaluate per-frame expressions |
| evaluateOnFrame(session, frameId, expression) | Evaluate in a paused frame |
| evaluateGlobal(session, expression) | Evaluate against the global Runtime |
| listScripts(session) | Return the scripts the V8 instance knows about |
| resume(session) | Resume execution |
| streamLogpoint(session, options) | Stream a non-pausing logpoint until duration / signal / max-events / transport-close |
| buildLogpointCondition(sentinel, expression, options?) | Build the CDP condition string for a logpoint with optional predicate / hit-count gates |
| buildHitCountedCondition(hitCount, key, userCondition?) | Build the hit-count gate used by setBreakpoint({ hitCount }) |
| parseRemoteRoot(value) | Parse a literal/regex remote-root setting |
| buildBreakpointUrlRegex(input) | Build a CDP urlRegex for a file path |
| CfInspectorError | Rich error class with typed code |
| Code | When |
| --- | --- |
| INVALID_ARGUMENT | A numeric flag (--port, --timeout, --duration, …) is not a positive integer |
| INVALID_BREAKPOINT | --bp / --at is not in file:line form, or line is not a positive integer |
| INVALID_REMOTE_ROOT | --remote-root regex did not compile |
| INVALID_EXPRESSION | --condition or --expr failed to parse on V8 (Runtime.compileScript reported a SyntaxError) — fast-fail before the breakpoint is set |
| INVALID_HIT_COUNT | --hit-count is not a positive integer |
| INVALID_PAUSE_TYPE | cf-inspector exception --type is not one of uncaught / caught / all |
| BREAKPOINT_DID_NOT_BIND | Reserved: a breakpoint resolved to no scripts. Currently surfaced as a stderr warning only — see BreakpointHandle.resolvedLocations for programmatic detection |
| INSPECTOR_DISCOVERY_FAILED | /json/list did not return a usable WebSocket URL |
| INSPECTOR_CONNECTION_FAILED | WebSocket handshake failed, or the connection closed mid-request |
| CDP_REQUEST_FAILED | A CDP method returned an error result, timed out, or failed to send |
| BREAKPOINT_NOT_HIT | The breakpoint did not hit before the timeout elapsed |
| UNRELATED_PAUSE | The target paused somewhere else and --fail-on-unmatched-pause was enabled |
| UNRELATED_PAUSE_TIMEOUT | The target stayed paused somewhere else until the snapshot timeout elapsed |
| EVALUATION_FAILED | Reserved for future use — current evaluation paths surface remote exceptions inline via CapturedExpression.error instead of throwing |
| MISSING_TARGET | Neither --port nor a complete CF target (--region/--org/--space/--app) was provided |
| ABORTED | Reserved for future use by long-running streams when an AbortSignal fires |
🔭 How it works
┌──────────────────────┐ 1. GET http://127.0.0.1:<port>/json/list
│ cf-inspector │ 2. Open ws:// debugger URL
│ snapshot --bp X:Y │ ─►3. Debugger.enable + Runtime.enable
└──────────────────────┘ 4. Debugger.setBreakpointByUrl({ urlRegex, lineNumber: Y - 1 })
│ 5. Wait for `Debugger.paused`
▼ 6. Debugger.evaluateOnCallFrame(...) for each --capture expression
JSON snapshot 7. Runtime.getProperties(scopeChain[i].object.objectId) when --include-scopes is set
8. Debugger.resume (unless --keep-paused)Path mapping uses CDP's first-class urlRegex:
| --remote-root | Resulting urlRegex (line 42 of src/handler.ts) |
| --- | --- |
| omitted | (?:^|/)src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$ |
| /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 but --region/--org/--space/--app are given, the CLI internally 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.
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!
