diff --git a/packages/agent-cdp/README.md b/packages/agent-cdp/README.md index cab2c1c..ab8a950 100644 --- a/packages/agent-cdp/README.md +++ b/packages/agent-cdp/README.md @@ -1,6 +1,6 @@ # agent-cdp -**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. +**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. ## Compatibility @@ -91,6 +91,7 @@ agent-cdp target clear **3. Use the features you need** - **Console** — list and fetch log lines: `console list`, `console get ` +- **Runtime** — evaluate expressions, inspect returned object handles, and release preserved inspector references: `runtime eval`, `runtime props`, `runtime release`, `runtime release-group` - **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` - **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` - **Memory (raw)** — `memory capture --file PATH` for a heap snapshot file @@ -108,7 +109,27 @@ agent-cdp stop ## Command overview -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. +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. + +## Runtime inspection + +Use `runtime` for live state inspection when you need more than captured console output. + +Quick start: + +```sh +agent-cdp runtime eval --expr "process.version" +agent-cdp runtime eval --expr "globalThis.store" --await +agent-cdp runtime props --id --own +agent-cdp runtime release --id +agent-cdp runtime release-group +``` + +Notes: + +- Remote object handles are preserved by default so you can inspect them with `runtime props` after `runtime eval`. +- Preserved handles should be released when you no longer need them in a long-lived daemon session. +- `runtime eval` can have side effects if the expression mutates application state. ## Network inspection diff --git a/packages/agent-cdp/skills/core.md b/packages/agent-cdp/skills/core.md index 7760dee..c962434 100644 --- a/packages/agent-cdp/skills/core.md +++ b/packages/agent-cdp/skills/core.md @@ -1,6 +1,6 @@ --- name: core -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. +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. allowed-tools: Bash(agent-cdp:*) --- @@ -8,8 +8,7 @@ allowed-tools: Bash(agent-cdp:*) CLI for deep runtime analysis of Chrome and Node.js processes via Chrome DevTools Protocol (CDP). Captures heap snapshots, CPU profiles, JS memory -samples, network traffic, console output, and performance traces — all without modifying -source code. +samples, network traffic, console output, live runtime values, and performance traces — all without modifying source code. ## The core loop @@ -79,6 +78,16 @@ agent-cdp console get # get full details of a specific message Console messages are collected while the daemon is running with an active target. +## Runtime inspection + +For Runtime workflows, run: + +```bash +agent-cdp skills get runtime +``` + +That skill covers expression evaluation, object handle inspection, and release guidance for preserved Runtime objects. + ## Network inspection For network workflows, run: diff --git a/packages/agent-cdp/skills/runtime.md b/packages/agent-cdp/skills/runtime.md new file mode 100644 index 0000000..fb6d790 --- /dev/null +++ b/packages/agent-cdp/skills/runtime.md @@ -0,0 +1,109 @@ +--- +name: runtime +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. +allowed-tools: Bash(agent-cdp:*) +--- + +# agent-cdp runtime + +Use `runtime` when you need live state inspection against the currently selected target. + +This is the fastest path for an LLM agent to answer questions like: + +- What is the current value of a global or module-level variable? +- What properties exist on this object right now? +- Does the runtime state match what the logs suggest? + +## Commands + +```bash +agent-cdp runtime eval --expr EXPR [--await] [--json] +agent-cdp runtime props --id OBJECT_ID [--own] [--accessor-properties-only] +agent-cdp runtime release --id OBJECT_ID +agent-cdp runtime release-group [--group NAME] +``` + +## Safe workflow + +```bash +agent-cdp runtime eval --expr "globalThis.store" +agent-cdp runtime props --id --own +agent-cdp runtime release --id +``` + +If you evaluate several objects during a session, release the whole group when done: + +```bash +agent-cdp runtime release-group +``` + +## Preserved by default + +Runtime object handles are preserved by default. + +That means: + +- `runtime eval` may return an `objectId` for a live remote object +- you can pass that `objectId` to `runtime props` +- the handle stays available until you explicitly release it or release its group + +This is intentional because LLM agents often need a follow-up inspection step after evaluation. + +Releasing a handle does not delete the real application object. It only drops the inspector-side reference used by the debugging session. + +## `objectId` and object groups + +- `objectId` is a remote inspector handle, not a serialized value +- preserved handles are placed in the default group `agent-cdp-runtime` +- use `runtime release --id ...` for one handle +- use `runtime release-group` to clean up the default group in bulk + +In long-lived daemon sessions, release handles you no longer need. + +## Side effects + +`runtime eval` runs code in the target runtime. It is not automatically read-only. + +Prefer expressions that inspect state without mutating it, for example: + +```bash +agent-cdp runtime eval --expr "process.version" +agent-cdp runtime eval --expr "globalThis.__APP_STATE__" +agent-cdp runtime eval --expr "Array.isArray(globalThis.items) ? globalThis.items.length : null" +``` + +Avoid expressions that trigger writes, network calls, or state transitions unless you mean to do that. + +## Cross-target notes + +The Runtime commands are intended to work with: + +- Chrome / Chromium pages with CDP enabled +- Node.js processes started with `--inspect` or `--inspect-brk` +- React Native targets exposed through Metro / the RN debugger endpoint + +Examples: + +```bash +agent-cdp target list --url http://localhost:9222 +agent-cdp target list --url http://localhost:9229 +agent-cdp target list --url http://localhost:8081 +``` + +Then select the target and inspect runtime state. + +## React Native / Hermes promises + +On React Native targets backed by Hermes, do not assume `runtime eval --await` will unwrap a promise into its fulfillment value. + +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. + +If `--await` returns a promise handle instead of the resolved value: + +1. Re-run `runtime eval` without `--await` to get the remote object handle. +2. Inspect the handle with `runtime props --id --own`. +3. Look for Hermes promise internals such as `_h`, `_i`, `_j`, and `_k`. +4. In practice, `_j` often holds the fulfilled value once the promise settles, while `_k` may be `null`. +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. + +Use this workflow for debugging RN promise state instead of relying on native async syntax or promise unwrapping in the inspector path. diff --git a/packages/agent-cdp/src/__tests__/cli.test.ts b/packages/agent-cdp/src/__tests__/cli.test.ts index 1130921..3df5768 100644 --- a/packages/agent-cdp/src/__tests__/cli.test.ts +++ b/packages/agent-cdp/src/__tests__/cli.test.ts @@ -22,6 +22,8 @@ describe("cli", () => { expect(usage()).toContain("stop"); expect(usage()).toContain("target list [--url URL]"); expect(usage()).toContain("target select [--url URL]"); + expect(usage()).toContain("runtime eval --expr EXPR [--await] [--json]"); + expect(usage()).toContain("runtime props --id OBJECT_ID [--own] [--accessor-properties-only]"); expect(usage()).toContain("network start [--name NAME] [--preserve-across-navigation]"); expect(usage()).toContain("network response-body --id REQ_ID [--session ID] [--file PATH]"); expect(usage()).toContain("trace status"); diff --git a/packages/agent-cdp/src/__tests__/runtime.test.ts b/packages/agent-cdp/src/__tests__/runtime.test.ts new file mode 100644 index 0000000..faa4884 --- /dev/null +++ b/packages/agent-cdp/src/__tests__/runtime.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; +import { formatRuntimeEval, formatRuntimeEvalJson, formatRuntimeProperties } from "../runtime/formatters.js"; +import { RuntimeManager } from "../runtime/index.js"; + +describe("runtime formatters", () => { + it("formats primitive eval results", () => { + expect( + formatRuntimeEval({ + type: "number", + value: 42, + }), + ).toBe("number: 42"); + }); + + it("formats string eval results as json-safe text", () => { + expect( + formatRuntimeEval({ + type: "string", + value: "hello", + }), + ).toBe('string: "hello"'); + expect( + formatRuntimeEvalJson({ + type: "string", + value: "hello", + }), + ).toBe('"hello"'); + }); + + it("formats remote object eval results with object ids", () => { + expect( + formatRuntimeEval({ + type: "object", + subtype: "array", + description: "Array(3)", + objectId: "obj-1", + objectGroup: "agent-cdp-runtime", + }), + ).toBe("array Array(3)\nobjectId: obj-1"); + }); + + it("formats property listings with nested object handles", () => { + expect( + formatRuntimeProperties({ + objectId: "root-1", + properties: [ + { + name: "count", + enumerable: true, + isAccessor: false, + type: "number", + value: 2, + }, + { + name: "items", + enumerable: true, + isAccessor: false, + type: "object", + subtype: "array", + description: "Array(2)", + objectId: "child-1", + }, + ], + }), + ).toBe("count = number: 2\nitems = array Array(2) (objectId: child-1)"); + }); +}); + +describe("runtime manager", () => { + it("releases a runtime object", async () => { + const sent: Array<{ method: string; params?: Record }> = []; + const manager = new RuntimeManager(); + const session = { + transport: { + connect: async () => {}, + disconnect: async () => {}, + isConnected: () => true, + send: async (method: string, params?: Record) => { + sent.push({ method, params }); + return {}; + }, + onEvent: () => () => {}, + }, + }; + + await manager.releaseObject(session as never, "obj-9"); + + expect(sent).toEqual([{ method: "Runtime.releaseObject", params: { objectId: "obj-9" } }]); + }); +}); diff --git a/packages/agent-cdp/src/cli.ts b/packages/agent-cdp/src/cli.ts index 984604f..14a42d6 100644 --- a/packages/agent-cdp/src/cli.ts +++ b/packages/agent-cdp/src/cli.ts @@ -9,6 +9,12 @@ import { formatStatus, formatTargetList, } from "./formatters.js"; +import { + DEFAULT_RUNTIME_OBJECT_GROUP, + formatRuntimeEval, + formatRuntimeEvalJson, + formatRuntimeProperties, +} from "./runtime/index.js"; import { formatTraceEntries, formatTraceEntry, @@ -117,6 +123,12 @@ Console: console list [--limit N] console get +Runtime: + runtime eval --expr EXPR [--await] [--json] + runtime props --id OBJECT_ID [--own] [--accessor-properties-only] + runtime release --id OBJECT_ID + runtime release-group [--group NAME] + Network: network status network start [--name NAME] [--preserve-across-navigation] @@ -469,6 +481,74 @@ export async function main(): Promise { return; } + if (cmd === "runtime" && command[1] === "eval") { + const expression = typeof flags.expr === "string" ? flags.expr : undefined; + if (!expression) { + throw new Error("Usage: agent-cdp runtime eval --expr EXPR [--await] [--json]"); + } + + const awaitPromise = flags.await === true; + const json = flags.json === true; + await ensureDaemon(); + const response = await sendCommand({ type: "runtime-eval", expression, awaitPromise }); + if (!response.ok) { + throw new Error(response.error || "Failed to evaluate runtime expression"); + } + + console.log( + json + ? formatRuntimeEvalJson(response.data as Parameters[0]) + : formatRuntimeEval(response.data as Parameters[0], verbose), + ); + return; + } + + if (cmd === "runtime" && command[1] === "props") { + const objectId = typeof flags.id === "string" ? flags.id : undefined; + if (!objectId) { + throw new Error("Usage: agent-cdp runtime props --id OBJECT_ID [--own] [--accessor-properties-only]"); + } + + const ownProperties = flags.own === true; + const accessorPropertiesOnly = flags["accessor-properties-only"] === true; + await ensureDaemon(); + const response = await sendCommand({ type: "runtime-get-properties", objectId, ownProperties, accessorPropertiesOnly }); + if (!response.ok) { + throw new Error(response.error || "Failed to inspect runtime object properties"); + } + + console.log(formatRuntimeProperties(response.data as Parameters[0], verbose)); + return; + } + + if (cmd === "runtime" && command[1] === "release") { + const objectId = typeof flags.id === "string" ? flags.id : undefined; + if (!objectId) { + throw new Error("Usage: agent-cdp runtime release --id OBJECT_ID"); + } + + await ensureDaemon(); + const response = await sendCommand({ type: "runtime-release-object", objectId }); + if (!response.ok) { + throw new Error(response.error || "Failed to release runtime object"); + } + + console.log(`Released runtime object: ${objectId}`); + return; + } + + if (cmd === "runtime" && command[1] === "release-group") { + const objectGroup = typeof flags.group === "string" ? flags.group : DEFAULT_RUNTIME_OBJECT_GROUP; + await ensureDaemon(); + const response = await sendCommand({ type: "runtime-release-object-group", objectGroup }); + if (!response.ok) { + throw new Error(response.error || "Failed to release runtime object group"); + } + + console.log(`Released runtime object group: ${objectGroup}`); + return; + } + if (cmd === "network" && command[1] === "status") { await ensureDaemon(); const response = await sendCommand({ type: "network-status" }); diff --git a/packages/agent-cdp/src/daemon.ts b/packages/agent-cdp/src/daemon.ts index cde91ad..7af455d 100644 --- a/packages/agent-cdp/src/daemon.ts +++ b/packages/agent-cdp/src/daemon.ts @@ -11,6 +11,7 @@ import { JsProfiler } from "./js-profiler/index.js"; import { MemorySnapshotter } from "./memory.js"; import { NetworkManager } from "./network/index.js"; import { createTargetProviders } from "./providers.js"; +import { RuntimeManager } from "./runtime/index.js"; import { SessionManager } from "./session-manager.js"; import { TraceManager } from "./trace/index.js"; import type { DaemonInfo, IpcCommand, IpcResponse, StatusInfo } from "./types.js"; @@ -51,6 +52,7 @@ class Daemon { private readonly jsAllocationTimelineProfiler = new JsAllocationTimelineProfiler(this.heapSnapshotManager); private readonly jsHeapUsageMonitor = new JsHeapUsageMonitor(); private readonly providers = createTargetProviders(); + private readonly runtimeManager = new RuntimeManager(); private readonly sessionManager = new SessionManager(this.providers); private readonly traceManager = new TraceManager(); private readonly jsProfiler = new JsProfiler(); @@ -172,6 +174,41 @@ class Daemon { return { ok: true, data: "Target cleared" }; } + if (command.type === "runtime-eval") { + const session = await this.requireSession(); + return { + ok: true, + data: await this.runtimeManager.evaluate(session, { + expression: command.expression, + awaitPromise: command.awaitPromise, + }), + }; + } + + if (command.type === "runtime-get-properties") { + const session = await this.requireSession(); + return { + ok: true, + data: await this.runtimeManager.getProperties(session, { + objectId: command.objectId, + ownProperties: command.ownProperties, + accessorPropertiesOnly: command.accessorPropertiesOnly, + }), + }; + } + + if (command.type === "runtime-release-object") { + const session = await this.requireSession(); + await this.runtimeManager.releaseObject(session, command.objectId); + return { ok: true, data: null }; + } + + if (command.type === "runtime-release-object-group") { + const session = await this.requireSession(); + await this.runtimeManager.releaseObjectGroup(session, command.objectGroup); + return { ok: true, data: null }; + } + if (command.type === "network-status") { return { ok: true, data: this.networkManager.getStatus() }; } diff --git a/packages/agent-cdp/src/index.ts b/packages/agent-cdp/src/index.ts index 415de52..14739ae 100644 --- a/packages/agent-cdp/src/index.ts +++ b/packages/agent-cdp/src/index.ts @@ -4,6 +4,7 @@ export * from "./memory.js"; export * from "./types.js"; export * from "./formatters.js"; export * from "./providers.js"; +export * from "./runtime/index.js"; export * from "./session-manager.js"; export * from "./trace.js"; export * from "./transport.js"; diff --git a/packages/agent-cdp/src/runtime/formatters.ts b/packages/agent-cdp/src/runtime/formatters.ts new file mode 100644 index 0000000..5866faf --- /dev/null +++ b/packages/agent-cdp/src/runtime/formatters.ts @@ -0,0 +1,123 @@ +import type { RuntimeEvalResult, RuntimePropertiesResult, RuntimePropertyResult } from "./types.js"; + +export function formatRuntimeEval(result: RuntimeEvalResult, verbose = false): string { + const summary = summarizeRemoteValue(result); + if (!verbose) { + if (result.objectId) { + return `${summary}\nobjectId: ${result.objectId}`; + } + return summary; + } + + const lines = [summary]; + if (result.className) lines.push(`className: ${result.className}`); + if (result.description && result.description !== summary) lines.push(`description: ${result.description}`); + if (result.objectId) lines.push(`objectId: ${result.objectId}`); + if (result.objectGroup) lines.push(`objectGroup: ${result.objectGroup}`); + return lines.join("\n"); +} + +export function formatRuntimeEvalJson(result: RuntimeEvalResult): string { + if (!result.objectId) { + if (result.unserializableValue) { + return result.unserializableValue; + } + return JSON.stringify(result.value, null, 2) ?? "undefined"; + } + + return JSON.stringify( + { + type: result.type, + subtype: result.subtype, + className: result.className, + description: result.description, + objectId: result.objectId, + objectGroup: result.objectGroup, + }, + null, + 2, + ); +} + +export function formatRuntimeProperties(result: RuntimePropertiesResult, verbose = false): string { + if (result.properties.length === 0) { + return `No properties for ${result.objectId}`; + } + + const lines = result.properties.map((property) => formatProperty(property, verbose)); + if (!verbose) { + return lines.join("\n"); + } + + return [`objectId: ${result.objectId}`, ...lines].join("\n"); +} + +function formatProperty(property: RuntimePropertyResult, verbose: boolean): string { + const summary = summarizePropertyValue(property); + const flags: string[] = []; + if (!property.enumerable) flags.push("non-enumerable"); + if (property.isAccessor) flags.push("accessor"); + if (property.writable === false) flags.push("readonly"); + + const head = flags.length > 0 ? `${property.name} [${flags.join(",")}] = ${summary}` : `${property.name} = ${summary}`; + if (!verbose) { + return head; + } + + const lines = [head]; + if (property.className) lines.push(` className: ${property.className}`); + if (property.description && property.objectId) lines.push(` description: ${property.description}`); + if (property.objectId) lines.push(` objectId: ${property.objectId}`); + return lines.join("\n"); +} + +function summarizePropertyValue(property: RuntimePropertyResult): string { + if (property.isAccessor && !property.objectId && property.type === undefined && property.description === undefined) { + return "[accessor]"; + } + + const summary = summarizeRemoteValue(property); + return property.objectId ? `${summary} (objectId: ${property.objectId})` : summary; +} + +function summarizeRemoteValue(value: { + type?: string; + subtype?: string; + className?: string; + description?: string; + value?: unknown; + unserializableValue?: string; + objectId?: string; +}): string { + if (value.unserializableValue) { + return `${value.type ?? "value"}: ${value.unserializableValue}`; + } + + if (!value.objectId) { + return `${value.type ?? typeof value.value}: ${formatInlineValue(value.value)}`; + } + + const label = value.subtype || value.className || value.type || "object"; + const description = value.description && value.description !== label ? ` ${truncate(value.description, 120)}` : ""; + return `${label}${description}`; +} + +function formatInlineValue(input: unknown): string { + if (typeof input === "string") { + return JSON.stringify(truncate(input, 200)); + } + + if (input === undefined) { + return "undefined"; + } + + return truncate(JSON.stringify(input) ?? String(input), 200); +} + +function truncate(value: string, maxLength: number): string { + if (value.length <= maxLength) { + return value; + } + + return `${value.slice(0, maxLength - 3)}...`; +} diff --git a/packages/agent-cdp/src/runtime/index.ts b/packages/agent-cdp/src/runtime/index.ts new file mode 100644 index 0000000..6362de3 --- /dev/null +++ b/packages/agent-cdp/src/runtime/index.ts @@ -0,0 +1,126 @@ +import type { RuntimeSession } from "../types.js"; +import { + DEFAULT_RUNTIME_OBJECT_GROUP, + type RuntimeEvalResult, + type RuntimePropertiesResult, + type RuntimePropertyResult, +} from "./types.js"; + +interface RuntimeRemoteObject { + type?: string; + subtype?: string; + className?: string; + description?: string; + value?: unknown; + unserializableValue?: string; + objectId?: string; +} + +interface RuntimePropertyDescriptor { + name?: string; + enumerable?: boolean; + writable?: boolean; + value?: RuntimeRemoteObject; + get?: RuntimeRemoteObject; + set?: RuntimeRemoteObject; +} + +interface RuntimeEvaluateResponse { + result?: RuntimeRemoteObject; + exceptionDetails?: { + text?: string; + exception?: RuntimeRemoteObject; + }; +} + +interface RuntimeGetPropertiesResponse { + result?: RuntimePropertyDescriptor[]; +} + +export class RuntimeManager { + async evaluate( + session: RuntimeSession, + options: { expression: string; awaitPromise?: boolean; objectGroup?: string }, + ): Promise { + const response = (await session.transport.send("Runtime.evaluate", { + expression: options.expression, + awaitPromise: options.awaitPromise === true, + objectGroup: options.objectGroup || DEFAULT_RUNTIME_OBJECT_GROUP, + generatePreview: true, + })) as RuntimeEvaluateResponse; + + if (response.exceptionDetails) { + throw new Error(formatException(response.exceptionDetails)); + } + + return normalizeRemoteObject(response.result || {}, options.objectGroup || DEFAULT_RUNTIME_OBJECT_GROUP); + } + + async getProperties( + session: RuntimeSession, + options: { objectId: string; ownProperties?: boolean; accessorPropertiesOnly?: boolean }, + ): Promise { + const response = (await session.transport.send("Runtime.getProperties", { + objectId: options.objectId, + ownProperties: options.ownProperties === true, + accessorPropertiesOnly: options.accessorPropertiesOnly === true, + generatePreview: true, + })) as RuntimeGetPropertiesResponse; + + return { + objectId: options.objectId, + properties: (response.result || []).map((property) => normalizeProperty(property)), + }; + } + + async releaseObject(session: RuntimeSession, objectId: string): Promise { + await session.transport.send("Runtime.releaseObject", { objectId }); + } + + async releaseObjectGroup(session: RuntimeSession, objectGroup: string): Promise { + await session.transport.send("Runtime.releaseObjectGroup", { objectGroup }); + } +} + +function normalizeRemoteObject(result: RuntimeRemoteObject, objectGroup?: string): RuntimeEvalResult { + return { + type: result.type || "undefined", + subtype: result.subtype, + className: result.className, + description: result.description, + value: result.value, + unserializableValue: result.unserializableValue, + objectId: result.objectId, + objectGroup: result.objectId ? objectGroup : undefined, + }; +} + +function normalizeProperty(property: RuntimePropertyDescriptor): RuntimePropertyResult { + if (property.value) { + return { + name: property.name || "(unknown)", + enumerable: property.enumerable !== false, + writable: property.writable, + isAccessor: false, + ...normalizeRemoteObject(property.value), + }; + } + + const accessor = property.get || property.set; + return { + name: property.name || "(unknown)", + enumerable: property.enumerable !== false, + writable: property.writable, + isAccessor: true, + ...normalizeRemoteObject(accessor || {}), + }; +} + +function formatException(exception: NonNullable): string { + const description = exception.exception?.description || exception.text; + return description ? `Runtime evaluation failed: ${description}` : "Runtime evaluation failed"; +} + +export type { RuntimeEvalResult, RuntimePropertiesResult, RuntimePropertyResult } from "./types.js"; +export { DEFAULT_RUNTIME_OBJECT_GROUP } from "./types.js"; +export { formatRuntimeEval, formatRuntimeEvalJson, formatRuntimeProperties } from "./formatters.js"; diff --git a/packages/agent-cdp/src/runtime/types.ts b/packages/agent-cdp/src/runtime/types.ts new file mode 100644 index 0000000..017c2a0 --- /dev/null +++ b/packages/agent-cdp/src/runtime/types.ts @@ -0,0 +1,31 @@ +export const DEFAULT_RUNTIME_OBJECT_GROUP = "agent-cdp-runtime"; + +export interface RuntimeEvalResult { + type: string; + subtype?: string; + className?: string; + description?: string; + value?: unknown; + unserializableValue?: string; + objectId?: string; + objectGroup?: string; +} + +export interface RuntimePropertyResult { + name: string; + enumerable: boolean; + writable?: boolean; + isAccessor: boolean; + type?: string; + subtype?: string; + className?: string; + description?: string; + value?: unknown; + unserializableValue?: string; + objectId?: string; +} + +export interface RuntimePropertiesResult { + objectId: string; + properties: RuntimePropertyResult[]; +} diff --git a/packages/agent-cdp/src/types.ts b/packages/agent-cdp/src/types.ts index e7a429c..9ccdc0d 100644 --- a/packages/agent-cdp/src/types.ts +++ b/packages/agent-cdp/src/types.ts @@ -93,6 +93,10 @@ export type IpcCommand = | { type: "list-targets"; options: DiscoveryOptions } | { type: "select-target"; targetId: string; options: DiscoveryOptions } | { type: "clear-target" } + | { type: "runtime-eval"; expression: string; awaitPromise?: boolean } + | { type: "runtime-get-properties"; objectId: string; ownProperties?: boolean; accessorPropertiesOnly?: boolean } + | { type: "runtime-release-object"; objectId: string } + | { type: "runtime-release-object-group"; objectGroup: string } | { type: "list-console-messages"; limit?: number } | { type: "get-console-message"; id: number } | { type: "network-status" } diff --git a/runtime-demo.sh b/runtime-demo.sh new file mode 100755 index 0000000..602f5c2 --- /dev/null +++ b/runtime-demo.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +set -u -o pipefail + +LAST_OUTPUT="" +FAILURES=0 + +run_cmd() { + local cmd="$1" + local output + local status + + printf '$ %s\n' "$cmd" + output=$(eval "$cmd" 2>&1) + status=$? + + if [ -n "$output" ]; then + printf '%s\n' "$output" + fi + + printf '[exit %s]\n\n' "$status" + + LAST_OUTPUT="$output" + if [ "$status" -ne 0 ]; then + FAILURES=$((FAILURES + 1)) + fi + + return "$status" +} + +extract_object_id() { + local line + + while IFS= read -r line; do + case "$line" in + objectId:\ *) + printf '%s' "${line#objectId: }" + return 0 + ;; + esac + done <<< "$LAST_OUTPUT" + + return 1 +} + +run_cmd "pnpm run --silent agent-cdp -- start" +run_cmd "pnpm run --silent agent-cdp -- status" + +run_cmd "pnpm run --silent agent-cdp -- runtime eval --expr '1 + 2'" +run_cmd "pnpm run --silent agent-cdp -- runtime eval --await --expr 'Promise.resolve(42)'" + +case "$LAST_OUTPUT" in + *"number: 42"*) + ;; + *"Promise"*) + printf 'Async runtime eval returned a promise instead of the resolved value\n\n' + FAILURES=$((FAILURES + 1)) + ;; + *) + printf 'Async runtime eval did not return the expected resolved value\n\n' + FAILURES=$((FAILURES + 1)) + ;; +esac + +run_cmd "pnpm run --silent agent-cdp -- runtime eval --json --expr '({ title: \"runtime demo\", count: 3, nested: { ready: true }, list: [1, 2, 3] })'" +run_cmd "pnpm run --silent agent-cdp -- runtime eval --expr 'globalThis.__agentCdpRuntimeDemo = { title: \"runtime demo\", count: 3, nested: { ready: true }, list: [1, 2, 3] }; globalThis.__agentCdpRuntimeDemo'" + +OBJECT_ID="" +if OBJECT_ID=$(extract_object_id); then + run_cmd "pnpm run --silent agent-cdp -- runtime props --id \"$OBJECT_ID\" --own" + run_cmd "pnpm run --silent agent-cdp -- runtime release --id \"$OBJECT_ID\"" +else + printf 'No runtime object id found in eval output\n\n' + FAILURES=$((FAILURES + 1)) +fi + +run_cmd "pnpm run --silent agent-cdp -- runtime release-group" + +run_cmd "pnpm run --silent agent-cdp -- stop" + +if [ "$FAILURES" -ne 0 ]; then + printf 'Runtime demo finished with %s failure(s).\n' "$FAILURES" + exit 1 +fi + +printf 'Runtime demo finished successfully.\n'