npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@saptools/cf-inspector

v0.4.1

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.

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.

npm version license node

InstallQuick StartCLIAPIHow it works


✨ Features

  • 🎯 One-shot snapshotcf-inspector snapshot --bp src/handler.ts:42 sets 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 5 skips the first N − 1 hits and pauses on the Nth, on every command (snapshot, log, watch)
  • 🎭 Multi-breakpoint — repeat --bp to race several locations; first hit wins
  • 🪜 Stack capture--stack-depth N --stack-captures 'this, args' walks call frames and evaluates expressions per frame
  • 🔁 Watch streamingcf-inspector watch --bp file:line --capture user.id --duration 30 re-captures on every hit and emits JSON Lines (the streaming counterpart of snapshot)
  • 💥 Exception breakpointscf-inspector exception --type uncaught --capture err.message pauses on the next thrown error and materializes the exception value
  • 📡 Non-pausing logpointscf-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:42 is matched against the remote URL via a urlRegex, with optional --remote-root literal or regex (same DSL as cds-debug)
  • 🔁 Composes with cf-debugger — pass --app/--region/--org/--space and the tunnel is opened automatically; pass --port to attach to anything CDP-speaking
  • 🪶 Tiny dependency footprintcommander + ws only, 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 | | --keep-paused | Skip Debugger.resume after capture | | --fail-on-unmatched-pause | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |

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 (and optionally --cf-timeout <seconds> for the tunnel).

📡 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 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 | | --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 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 | | --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 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 | | --- | --- | | --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


👨‍💻 Author

dongtran

📄 License

MIT


Made with ❤️ to make your work life easier!