Skip to content

Commit 73167b9

Browse files
authored
Add minimal Runtime inspection commands (#11)
## Summary Adds a new `runtime` command family so tool users can evaluate live expressions, inspect returned remote object handles, and explicitly release preserved Runtime references on Chrome, Node.js, and React Native targets. ## Context This change introduces a focused `src/runtime/` module and keeps the scope intentionally small: `runtime eval`, `runtime props`, `runtime release`, and `runtime release-group`. The default behavior preserves remote object handles in the `agent-cdp-runtime` object group so an agent can evaluate first and inspect properties in a follow-up step before releasing the handle. Backward-compatibility: existing command families, daemon lifecycle, and default output formats remain unchanged. This only adds a new top-level command family, new packaged skill guidance, and README coverage for Runtime inspection. ## Risks - Runtime evaluation is intentionally powerful and can cause side effects if the expression mutates target state. - Runtime object handles are preserved by default, so callers need to release them in long-lived sessions when they are no longer needed. - Property/value previews depend on what the target runtime exposes through CDP, so Chrome, Node.js, and React Native may differ slightly in descriptions even though the command surface is shared. Fixes #8 ## Manual testing 1. Start the daemon and select a Chrome, Node.js, or React Native target that exposes CDP. 2. Run `agent-cdp runtime eval --expr "process.version"` or another read-only expression appropriate for the target, and verify primitive values print compactly. 3. Run `agent-cdp runtime eval --expr "globalThis"` and verify the output includes an `objectId` for follow-up inspection. 4. Run `agent-cdp runtime props --id <OBJECT_ID> --own` and verify returned properties include concise summaries and nested object handles when present. 5. Run `agent-cdp runtime release --id <OBJECT_ID>` or `agent-cdp runtime release-group` and verify cleanup succeeds without changing other command workflows.
1 parent b8a9dfb commit 73167b9

13 files changed

Lines changed: 724 additions & 5 deletions

File tree

packages/agent-cdp/README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# agent-cdp
22

3-
**agent-cdp** is a command-line tool that connects to apps and pages through the **Chrome DevTools Protocol (CDP)**. Use it to list debuggable targets, inspect network traffic, stream console output, record traces, inspect JavaScript heap usage, capture and analyze heap snapshots, and run JavaScript CPU profiles, all without opening DevTools yourself.
3+
**agent-cdp** is a command-line tool that connects to apps and pages through the **Chrome DevTools Protocol (CDP)**. Use it to list debuggable targets, evaluate runtime expressions, inspect network traffic, stream console output, record traces, inspect JavaScript heap usage, capture and analyze heap snapshots, and run JavaScript CPU profiles, all without opening DevTools yourself.
44

55
## Compatibility
66

@@ -91,6 +91,7 @@ agent-cdp target clear
9191
**3. Use the features you need**
9292
9393
- **Console** — list and fetch log lines: `console list`, `console get <id>`
94+
- **Runtime** — evaluate expressions, inspect returned object handles, and release preserved inspector references: `runtime eval`, `runtime props`, `runtime release`, `runtime release-group`
9495
- **Network** — bounded live capture plus persisted sessions: `network status`, `network start`, `network summary`, `network list`, `network request`, `network request-headers`, `network response-headers`, `network request-body`, `network response-body`
9596
- **Trace** — explicit trace capture plus in-memory session analysis for `performance.measure`, `performance.mark`, `console.timeStamp`, and custom DevTools tracks: `trace start`, `trace stop`, `trace summary`, `trace tracks`, `trace entries`, `trace entry`
9697
- **Memory (raw)**`memory capture --file PATH` for a heap snapshot file
@@ -108,7 +109,27 @@ agent-cdp stop
108109
109110
## Command overview
110111
111-
Commands are grouped as **daemon**, **target**, **console**, **network**, **trace**, **memory**, **mem-snapshot**, **js-memory**, **js-allocation**, **js-allocation-timeline**, **js-profile**, and **skills** (bundled reference files). See `agent-cdp --help` for exact syntax and options.
112+
Commands are grouped as **daemon**, **target**, **console**, **runtime**, **network**, **trace**, **memory**, **mem-snapshot**, **js-memory**, **js-allocation**, **js-allocation-timeline**, **js-profile**, and **skills** (bundled reference files). See `agent-cdp --help` for exact syntax and options.
113+
114+
## Runtime inspection
115+
116+
Use `runtime` for live state inspection when you need more than captured console output.
117+
118+
Quick start:
119+
120+
```sh
121+
agent-cdp runtime eval --expr "process.version"
122+
agent-cdp runtime eval --expr "globalThis.store" --await
123+
agent-cdp runtime props --id <OBJECT_ID> --own
124+
agent-cdp runtime release --id <OBJECT_ID>
125+
agent-cdp runtime release-group
126+
```
127+
128+
Notes:
129+
130+
- Remote object handles are preserved by default so you can inspect them with `runtime props` after `runtime eval`.
131+
- Preserved handles should be released when you no longer need them in a long-lived daemon session.
132+
- `runtime eval` can have side effects if the expression mutates application state.
112133
113134
## Network inspection
114135

packages/agent-cdp/skills/core.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
---
22
name: core
3-
description: Core agent-cdp usage guide. Read this before running any agent-cdp commands. Covers the daemon lifecycle, target selection, network inspection, console capture, trace recording, heap snapshot analysis, JS heap monitoring, and CPU profiling workflows. Use when you need to analyze network failures, memory leaks, CPU hotspots, or runtime behavior of a Chrome/Node.js target via Chrome DevTools Protocol.
3+
description: Core agent-cdp usage guide. Read this before running any agent-cdp commands. Covers the daemon lifecycle, target selection, runtime inspection, network inspection, console capture, trace recording, heap snapshot analysis, JS heap monitoring, and CPU profiling workflows. Use when you need to analyze network failures, memory leaks, CPU hotspots, or runtime behavior of a Chrome/Node.js target via Chrome DevTools Protocol.
44
allowed-tools: Bash(agent-cdp:*)
55
---
66

77
# agent-cdp core
88

99
CLI for deep runtime analysis of Chrome and Node.js processes via Chrome
1010
DevTools Protocol (CDP). Captures heap snapshots, CPU profiles, JS memory
11-
samples, network traffic, console output, and performance traces — all without modifying
12-
source code.
11+
samples, network traffic, console output, live runtime values, and performance traces — all without modifying source code.
1312

1413
## The core loop
1514

@@ -79,6 +78,16 @@ agent-cdp console get <id> # get full details of a specific message
7978
8079
Console messages are collected while the daemon is running with an active target.
8180
81+
## Runtime inspection
82+
83+
For Runtime workflows, run:
84+
85+
```bash
86+
agent-cdp skills get runtime
87+
```
88+
89+
That skill covers expression evaluation, object handle inspection, and release guidance for preserved Runtime objects.
90+
8291
## Network inspection
8392
8493
For network workflows, run:
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
name: runtime
3+
description: Runtime inspection workflows for agent-cdp. Use after reading the core skill and selecting a target. Covers expression evaluation, preserved remote object handles, property inspection, and explicit release for Chrome, Node.js, and React Native targets.
4+
allowed-tools: Bash(agent-cdp:*)
5+
---
6+
7+
# agent-cdp runtime
8+
9+
Use `runtime` when you need live state inspection against the currently selected target.
10+
11+
This is the fastest path for an LLM agent to answer questions like:
12+
13+
- What is the current value of a global or module-level variable?
14+
- What properties exist on this object right now?
15+
- Does the runtime state match what the logs suggest?
16+
17+
## Commands
18+
19+
```bash
20+
agent-cdp runtime eval --expr EXPR [--await] [--json]
21+
agent-cdp runtime props --id OBJECT_ID [--own] [--accessor-properties-only]
22+
agent-cdp runtime release --id OBJECT_ID
23+
agent-cdp runtime release-group [--group NAME]
24+
```
25+
26+
## Safe workflow
27+
28+
```bash
29+
agent-cdp runtime eval --expr "globalThis.store"
30+
agent-cdp runtime props --id <OBJECT_ID> --own
31+
agent-cdp runtime release --id <OBJECT_ID>
32+
```
33+
34+
If you evaluate several objects during a session, release the whole group when done:
35+
36+
```bash
37+
agent-cdp runtime release-group
38+
```
39+
40+
## Preserved by default
41+
42+
Runtime object handles are preserved by default.
43+
44+
That means:
45+
46+
- `runtime eval` may return an `objectId` for a live remote object
47+
- you can pass that `objectId` to `runtime props`
48+
- the handle stays available until you explicitly release it or release its group
49+
50+
This is intentional because LLM agents often need a follow-up inspection step after evaluation.
51+
52+
Releasing a handle does not delete the real application object. It only drops the inspector-side reference used by the debugging session.
53+
54+
## `objectId` and object groups
55+
56+
- `objectId` is a remote inspector handle, not a serialized value
57+
- preserved handles are placed in the default group `agent-cdp-runtime`
58+
- use `runtime release --id ...` for one handle
59+
- use `runtime release-group` to clean up the default group in bulk
60+
61+
In long-lived daemon sessions, release handles you no longer need.
62+
63+
## Side effects
64+
65+
`runtime eval` runs code in the target runtime. It is not automatically read-only.
66+
67+
Prefer expressions that inspect state without mutating it, for example:
68+
69+
```bash
70+
agent-cdp runtime eval --expr "process.version"
71+
agent-cdp runtime eval --expr "globalThis.__APP_STATE__"
72+
agent-cdp runtime eval --expr "Array.isArray(globalThis.items) ? globalThis.items.length : null"
73+
```
74+
75+
Avoid expressions that trigger writes, network calls, or state transitions unless you mean to do that.
76+
77+
## Cross-target notes
78+
79+
The Runtime commands are intended to work with:
80+
81+
- Chrome / Chromium pages with CDP enabled
82+
- Node.js processes started with `--inspect` or `--inspect-brk`
83+
- React Native targets exposed through Metro / the RN debugger endpoint
84+
85+
Examples:
86+
87+
```bash
88+
agent-cdp target list --url http://localhost:9222
89+
agent-cdp target list --url http://localhost:9229
90+
agent-cdp target list --url http://localhost:8081
91+
```
92+
93+
Then select the target and inspect runtime state.
94+
95+
## React Native / Hermes promises
96+
97+
On React Native targets backed by Hermes, do not assume `runtime eval --await` will unwrap a promise into its fulfillment value.
98+
99+
Some Hermes targets also reject `async`/`await` syntax at parse time, so avoid using async functions as a probe unless you have already confirmed the target accepts them.
100+
101+
If `--await` returns a promise handle instead of the resolved value:
102+
103+
1. Re-run `runtime eval` without `--await` to get the remote object handle.
104+
2. Inspect the handle with `runtime props --id <OBJECT_ID> --own`.
105+
3. Look for Hermes promise internals such as `_h`, `_i`, `_j`, and `_k`.
106+
4. In practice, `_j` often holds the fulfilled value once the promise settles, while `_k` may be `null`.
107+
5. If you only need to confirm settlement, treat a non-null `_j` as the most useful clue and avoid assuming the inspector will serialize the resolved value for you.
108+
109+
Use this workflow for debugging RN promise state instead of relying on native async syntax or promise unwrapping in the inspector path.

packages/agent-cdp/src/__tests__/cli.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ describe("cli", () => {
2222
expect(usage()).toContain("stop");
2323
expect(usage()).toContain("target list [--url URL]");
2424
expect(usage()).toContain("target select <id> [--url URL]");
25+
expect(usage()).toContain("runtime eval --expr EXPR [--await] [--json]");
26+
expect(usage()).toContain("runtime props --id OBJECT_ID [--own] [--accessor-properties-only]");
2527
expect(usage()).toContain("network start [--name NAME] [--preserve-across-navigation]");
2628
expect(usage()).toContain("network response-body --id REQ_ID [--session ID] [--file PATH]");
2729
expect(usage()).toContain("trace status");
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, expect, it } from "vitest";
2+
import { formatRuntimeEval, formatRuntimeEvalJson, formatRuntimeProperties } from "../runtime/formatters.js";
3+
import { RuntimeManager } from "../runtime/index.js";
4+
5+
describe("runtime formatters", () => {
6+
it("formats primitive eval results", () => {
7+
expect(
8+
formatRuntimeEval({
9+
type: "number",
10+
value: 42,
11+
}),
12+
).toBe("number: 42");
13+
});
14+
15+
it("formats string eval results as json-safe text", () => {
16+
expect(
17+
formatRuntimeEval({
18+
type: "string",
19+
value: "hello",
20+
}),
21+
).toBe('string: "hello"');
22+
expect(
23+
formatRuntimeEvalJson({
24+
type: "string",
25+
value: "hello",
26+
}),
27+
).toBe('"hello"');
28+
});
29+
30+
it("formats remote object eval results with object ids", () => {
31+
expect(
32+
formatRuntimeEval({
33+
type: "object",
34+
subtype: "array",
35+
description: "Array(3)",
36+
objectId: "obj-1",
37+
objectGroup: "agent-cdp-runtime",
38+
}),
39+
).toBe("array Array(3)\nobjectId: obj-1");
40+
});
41+
42+
it("formats property listings with nested object handles", () => {
43+
expect(
44+
formatRuntimeProperties({
45+
objectId: "root-1",
46+
properties: [
47+
{
48+
name: "count",
49+
enumerable: true,
50+
isAccessor: false,
51+
type: "number",
52+
value: 2,
53+
},
54+
{
55+
name: "items",
56+
enumerable: true,
57+
isAccessor: false,
58+
type: "object",
59+
subtype: "array",
60+
description: "Array(2)",
61+
objectId: "child-1",
62+
},
63+
],
64+
}),
65+
).toBe("count = number: 2\nitems = array Array(2) (objectId: child-1)");
66+
});
67+
});
68+
69+
describe("runtime manager", () => {
70+
it("releases a runtime object", async () => {
71+
const sent: Array<{ method: string; params?: Record<string, unknown> }> = [];
72+
const manager = new RuntimeManager();
73+
const session = {
74+
transport: {
75+
connect: async () => {},
76+
disconnect: async () => {},
77+
isConnected: () => true,
78+
send: async (method: string, params?: Record<string, unknown>) => {
79+
sent.push({ method, params });
80+
return {};
81+
},
82+
onEvent: () => () => {},
83+
},
84+
};
85+
86+
await manager.releaseObject(session as never, "obj-9");
87+
88+
expect(sent).toEqual([{ method: "Runtime.releaseObject", params: { objectId: "obj-9" } }]);
89+
});
90+
});

packages/agent-cdp/src/cli.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
formatStatus,
1010
formatTargetList,
1111
} from "./formatters.js";
12+
import {
13+
DEFAULT_RUNTIME_OBJECT_GROUP,
14+
formatRuntimeEval,
15+
formatRuntimeEvalJson,
16+
formatRuntimeProperties,
17+
} from "./runtime/index.js";
1218
import {
1319
formatTraceEntries,
1420
formatTraceEntry,
@@ -117,6 +123,12 @@ Console:
117123
console list [--limit N]
118124
console get <id>
119125
126+
Runtime:
127+
runtime eval --expr EXPR [--await] [--json]
128+
runtime props --id OBJECT_ID [--own] [--accessor-properties-only]
129+
runtime release --id OBJECT_ID
130+
runtime release-group [--group NAME]
131+
120132
Network:
121133
network status
122134
network start [--name NAME] [--preserve-across-navigation]
@@ -469,6 +481,74 @@ export async function main(): Promise<void> {
469481
return;
470482
}
471483

484+
if (cmd === "runtime" && command[1] === "eval") {
485+
const expression = typeof flags.expr === "string" ? flags.expr : undefined;
486+
if (!expression) {
487+
throw new Error("Usage: agent-cdp runtime eval --expr EXPR [--await] [--json]");
488+
}
489+
490+
const awaitPromise = flags.await === true;
491+
const json = flags.json === true;
492+
await ensureDaemon();
493+
const response = await sendCommand({ type: "runtime-eval", expression, awaitPromise });
494+
if (!response.ok) {
495+
throw new Error(response.error || "Failed to evaluate runtime expression");
496+
}
497+
498+
console.log(
499+
json
500+
? formatRuntimeEvalJson(response.data as Parameters<typeof formatRuntimeEvalJson>[0])
501+
: formatRuntimeEval(response.data as Parameters<typeof formatRuntimeEval>[0], verbose),
502+
);
503+
return;
504+
}
505+
506+
if (cmd === "runtime" && command[1] === "props") {
507+
const objectId = typeof flags.id === "string" ? flags.id : undefined;
508+
if (!objectId) {
509+
throw new Error("Usage: agent-cdp runtime props --id OBJECT_ID [--own] [--accessor-properties-only]");
510+
}
511+
512+
const ownProperties = flags.own === true;
513+
const accessorPropertiesOnly = flags["accessor-properties-only"] === true;
514+
await ensureDaemon();
515+
const response = await sendCommand({ type: "runtime-get-properties", objectId, ownProperties, accessorPropertiesOnly });
516+
if (!response.ok) {
517+
throw new Error(response.error || "Failed to inspect runtime object properties");
518+
}
519+
520+
console.log(formatRuntimeProperties(response.data as Parameters<typeof formatRuntimeProperties>[0], verbose));
521+
return;
522+
}
523+
524+
if (cmd === "runtime" && command[1] === "release") {
525+
const objectId = typeof flags.id === "string" ? flags.id : undefined;
526+
if (!objectId) {
527+
throw new Error("Usage: agent-cdp runtime release --id OBJECT_ID");
528+
}
529+
530+
await ensureDaemon();
531+
const response = await sendCommand({ type: "runtime-release-object", objectId });
532+
if (!response.ok) {
533+
throw new Error(response.error || "Failed to release runtime object");
534+
}
535+
536+
console.log(`Released runtime object: ${objectId}`);
537+
return;
538+
}
539+
540+
if (cmd === "runtime" && command[1] === "release-group") {
541+
const objectGroup = typeof flags.group === "string" ? flags.group : DEFAULT_RUNTIME_OBJECT_GROUP;
542+
await ensureDaemon();
543+
const response = await sendCommand({ type: "runtime-release-object-group", objectGroup });
544+
if (!response.ok) {
545+
throw new Error(response.error || "Failed to release runtime object group");
546+
}
547+
548+
console.log(`Released runtime object group: ${objectGroup}`);
549+
return;
550+
}
551+
472552
if (cmd === "network" && command[1] === "status") {
473553
await ensureDaemon();
474554
const response = await sendCommand({ type: "network-status" });

0 commit comments

Comments
 (0)