Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
490 changes: 490 additions & 0 deletions packages/agent-cdp/src/__tests__/rozenite-plugin.test.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/agent-cdp/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { usage } from "./help.js";
import type { AgentPluginRegistration } from "./program.js";
import { createProgram } from "./program.js";
import { registerCliCommands as registerRuntimeBridgeCliCommands } from "../plugins/runtime-bridge/index.js";
import { registerRozeniteCliCommands } from "../plugins/rozenite/cli.js";

export { ensureTargetSelected, MULTIPLE_TARGETS_AVAILABLE_MESSAGE, usage };

const BUILT_IN_PLUGINS: AgentPluginRegistration[] = [
{ registerCliCommands: registerRuntimeBridgeCliCommands },
{ registerCliCommands: registerRozeniteCliCommands },
];

function shouldPrintHelp(argv: string[]): boolean {
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-cdp/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { JsProfiler } from "./js-profiler/index.js";
import { NetworkManager } from "./network/index.js";
import { PluginOrchestrator } from "./plugin-orchestrator.js";
import { AgentRuntimeBridgePlugin } from "./plugins/runtime-bridge/index.js";
import { RozenitePlugin } from "./plugins/rozenite/index.js";
import { createTargetProviders } from "./providers.js";
import { RuntimeManager } from "./runtime/index.js";
import { SessionManager } from "./session-manager.js";
Expand Down Expand Up @@ -52,7 +53,8 @@ class Daemon {

constructor() {
const bridgePlugin = new AgentRuntimeBridgePlugin((cmd) => this.commandDispatcher.dispatch(cmd));
this.orchestrator = new PluginOrchestrator([bridgePlugin]);
const rozenitePlugin = new RozenitePlugin();
this.orchestrator = new PluginOrchestrator([bridgePlugin, rozenitePlugin]);

this.commandDispatcher = new AgentCdpCommandDispatcher({
startedAt: this.startedAt,
Expand Down
14 changes: 8 additions & 6 deletions packages/agent-cdp/src/plugin-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,19 @@ export class PluginOrchestrator {
return { ok: false, error: `Unknown plugin '${pluginId}'` };
}

const state = plugin.getState();
const stateError = this.getStateError(pluginId, state);
if (stateError) {
return { ok: false, error: stateError };
}

const cmd = plugin.commands.find((c) => c.name === command);
if (!cmd) {
return { ok: false, error: `Unknown command '${command}' for plugin '${pluginId}'` };
}

if (!cmd.alwaysExecutable) {
const state = plugin.getState();
const stateError = this.getStateError(pluginId, state);
if (stateError) {
return { ok: false, error: stateError };
}
}

const context = this.buildCommandContext(plugin);
try {
const data = await cmd.execute(context, input);
Expand Down
1 change: 1 addition & 0 deletions packages/agent-cdp/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface AgentPluginCommand {
readonly name: string;
readonly summary: string;
readonly description?: string;
readonly alwaysExecutable?: boolean;

execute(context: AgentPluginCommandContext, input?: unknown): Promise<unknown>;
}
Expand Down
70 changes: 70 additions & 0 deletions packages/agent-cdp/src/plugins/rozenite/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { AgentPluginTargetSession } from "../../plugin.js";
import {
BOOTSTRAP_POLL_INTERVAL_MS,
BOOTSTRAP_POLL_MAX_ATTEMPTS,
RUNTIME_GLOBAL,
} from "./protocol.js";

function abortError(): Error {
const e = new Error("Aborted");
e.name = "AbortError";
return e;
}

async function pollForRuntime(
session: AgentPluginTargetSession,
signal: AbortSignal | undefined,
): Promise<void> {
for (let attempt = 0; attempt < BOOTSTRAP_POLL_MAX_ATTEMPTS; attempt++) {
if (signal?.aborted) throw abortError();

const result = (await session.send("Runtime.evaluate", {
expression: `typeof globalThis.${RUNTIME_GLOBAL} !== 'undefined'`,
returnByValue: true,
})) as { result?: { value?: unknown }; exceptionDetails?: unknown } | undefined;

if (result?.result?.value === true) return;

if (attempt === BOOTSTRAP_POLL_MAX_ATTEMPTS - 1) {
throw new Error(`Timed out waiting for ${RUNTIME_GLOBAL} to be available`);
}

await new Promise<void>((resolve) => setTimeout(resolve, BOOTSTRAP_POLL_INTERVAL_MS));
}
}

async function getBindingName(session: AgentPluginTargetSession): Promise<string> {
const result = (await session.send("Runtime.evaluate", {
expression: `globalThis.${RUNTIME_GLOBAL}.BINDING_NAME`,
returnByValue: true,
})) as { result?: { value?: unknown }; exceptionDetails?: unknown } | undefined;

if (result?.exceptionDetails) {
throw new Error("Failed to get binding name: " + JSON.stringify(result.exceptionDetails));
}

const value = result?.result?.value;
if (typeof value !== "string" || !value) {
throw new Error(`Unexpected binding name value: ${JSON.stringify(value)}`);
}

return value;
}

export async function runBootstrap(
session: AgentPluginTargetSession,
signal?: AbortSignal,
): Promise<string> {
await session.send("Runtime.enable");
if (signal?.aborted) throw abortError();

await pollForRuntime(session, signal);
if (signal?.aborted) throw abortError();

const bindingName = await getBindingName(session);
if (signal?.aborted) throw abortError();

await session.send("Runtime.addBinding", { name: bindingName });

return bindingName;
}
68 changes: 68 additions & 0 deletions packages/agent-cdp/src/plugins/rozenite/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Command } from "commander";

import type { CliDeps } from "../../cli/context.js";
import { unwrapResponse } from "../../cli/shared.js";

function printJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
}

export function registerRozeniteCliCommands(program: Command, deps: CliDeps): void {
const rozenite = program.command("rozenite").description("Rozenite React Native devtools bridge");

rozenite
.command("status")
.description("Show Rozenite plugin state and registered tool count")
.action(async () => {
const data = unwrapResponse(
await deps.sendCommand({ type: "plugin-command", pluginId: "rozenite", command: "status" }),
"Failed to get Rozenite status"
);
printJson(data);
});

rozenite
.command("tools")
.description("List registered Rozenite tools")
.action(async () => {
const data = unwrapResponse(
await deps.sendCommand({ type: "plugin-command", pluginId: "rozenite", command: "tools" }),
"Failed to list Rozenite tools"
);
printJson(data);
});

rozenite
.command("tool-schema <name>")
.description("Show input schema for a Rozenite tool")
.action(async (name: string) => {
const data = unwrapResponse(
await deps.sendCommand({
type: "plugin-command",
pluginId: "rozenite",
command: "tool-schema",
input: { name },
}),
`Failed to get schema for tool '${name}'`
);
printJson(data);
});

rozenite
.command("call <name>")
.description("Call a Rozenite tool")
.option("--input <json>", "Tool input as JSON string")
.action(async (name: string, options: { input?: string }) => {
const args = options.input !== undefined ? (JSON.parse(options.input) as unknown) : undefined;
const data = unwrapResponse(
await deps.sendCommand({
type: "plugin-command",
pluginId: "rozenite",
command: "call",
input: { name, arguments: args },
}),
`Failed to call tool '${name}'`
);
printJson(data);
});
}
Loading
Loading