diff --git a/packages/agent-cdp/src/__tests__/rozenite-plugin.test.ts b/packages/agent-cdp/src/__tests__/rozenite-plugin.test.ts new file mode 100644 index 0000000..c848b34 --- /dev/null +++ b/packages/agent-cdp/src/__tests__/rozenite-plugin.test.ts @@ -0,0 +1,490 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { CdpEventMessage } from "../types.js"; +import type { + AgentPluginCommandContext, + AgentPluginTargetContext, + AgentPluginTargetSession, +} from "../plugin.js"; +import { PluginOrchestrator } from "../plugin-orchestrator.js"; +import { RozenitePlugin } from "../plugins/rozenite/index.js"; +import { AGENT_PLUGIN_ID, ROZENITE_DOMAIN } from "../plugins/rozenite/protocol.js"; +import type { IpcResponse, TargetDescriptor } from "../types.js"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const BINDING_NAME = "__CHROME_DEVTOOLS_FRONTEND_BINDING__"; + +// --------------------------------------------------------------------------- +// Targets +// --------------------------------------------------------------------------- + +const RN_TARGET: TargetDescriptor = { + id: "react-native:bG9jYWxob3N0OjgwODE:page-1", + rawId: "page-1", + title: "Example", + kind: "react-native", + description: "", + webSocketDebuggerUrl: "ws://localhost:8081/devtools/page/page-1", + sourceUrl: "http://localhost:8081", + reactNative: { logicalDeviceId: "test-device-id", capabilities: {} }, +}; + +const CHROME_TARGET: TargetDescriptor = { + ...RN_TARGET, + id: "chrome:bG9jYWxob3N0OjgwODE:page-1", + kind: "chrome", +}; + +// --------------------------------------------------------------------------- +// Fake session +// --------------------------------------------------------------------------- + +class FakeSession implements AgentPluginTargetSession { + readonly target: TargetDescriptor; + private readonly eventListeners: Array<(event: CdpEventMessage) => void> = []; + private readonly disconnectListeners: Array<(error?: Error) => void> = []; + private connected = true; + + readonly sendHistory: Array<{ method: string; params?: Record }> = []; + sendImpl: (method: string, params?: Record) => Promise = () => + Promise.resolve({}); + + constructor(target: TargetDescriptor = RN_TARGET) { + this.target = target; + } + + isConnected(): boolean { + return this.connected; + } + + send(method: string, params?: Record): Promise { + this.sendHistory.push({ method, params }); + return this.sendImpl(method, params); + } + + onEvent(listener: (event: CdpEventMessage) => void): () => void { + this.eventListeners.push(listener); + return () => { + const i = this.eventListeners.indexOf(listener); + if (i >= 0) this.eventListeners.splice(i, 1); + }; + } + + onDisconnected(listener: (error?: Error) => void): () => void { + this.disconnectListeners.push(listener); + return () => { + const i = this.disconnectListeners.indexOf(listener); + if (i >= 0) this.disconnectListeners.splice(i, 1); + }; + } + + emitEvent(event: CdpEventMessage): void { + for (const listener of [...this.eventListeners]) listener(event); + } + + emitDisconnect(error?: Error): void { + this.connected = false; + for (const listener of [...this.disconnectListeners]) listener(error); + } +} + +// --------------------------------------------------------------------------- +// Bootstrap mock +// --------------------------------------------------------------------------- + +const { mockRunBootstrap } = vi.hoisted(() => ({ + mockRunBootstrap: vi + .fn<() => Promise>() + .mockResolvedValue("__CHROME_DEVTOOLS_FRONTEND_BINDING__"), +})); + +vi.mock("../plugins/rozenite/bootstrap.js", () => ({ + runBootstrap: mockRunBootstrap, +})); + +// --------------------------------------------------------------------------- +// Event helpers +// --------------------------------------------------------------------------- + +function makeRozeniteEvent(type: string, payload: unknown): CdpEventMessage { + return { + method: "Runtime.bindingCalled", + params: { + name: BINDING_NAME, + payload: JSON.stringify({ + domain: ROZENITE_DOMAIN, + message: { pluginId: AGENT_PLUGIN_ID, type, payload }, + }), + }, + }; +} + +const ECHO_TOOL = { name: "app.echo", description: "Echoes text", inputSchema: { type: "object" } }; +const TS_TOOL = { name: "app.getTimestamp", description: "Returns timestamp", inputSchema: {} }; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeTargetContext(session: FakeSession): AgentPluginTargetContext { + return { pluginId: "rozenite", session }; +} + +function makeCommandContext( + plugin: RozenitePlugin, + session: FakeSession | null, +): AgentPluginCommandContext { + return { + pluginId: "rozenite", + session, + getState: () => plugin.getState(), + }; +} + +async function runCommand( + plugin: RozenitePlugin, + name: string, + session: FakeSession | null, + input?: unknown, +): Promise { + const cmd = (plugin.commands as ReturnType[]).find( + (c) => c?.name === name, + ); + if (!cmd) return { ok: false, error: `Unknown command '${name}'` }; + const ctx = makeCommandContext(plugin, session); + try { + const data = await cmd.execute(ctx, input); + return { ok: true, data }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +async function flushAttach(): Promise { + for (let i = 0; i < 5; i++) await Promise.resolve(); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("RozenitePlugin", () => { + describe("supportsTarget", () => { + it("returns true for react-native targets", () => { + const plugin = new RozenitePlugin(); + expect(plugin.supportsTarget(RN_TARGET)).toBe(true); + }); + + it("returns false for chrome targets", () => { + const plugin = new RozenitePlugin(); + expect(plugin.supportsTarget(CHROME_TARGET)).toBe(false); + }); + }); + + describe("connect", () => { + afterEach(() => { + mockRunBootstrap.mockResolvedValue(BINDING_NAME); + }); + + it("transitions to ready after successful bootstrap", async () => { + const plugin = new RozenitePlugin(); + const session = new FakeSession(); + + expect(plugin.getState()).toEqual({ kind: "idle" }); + await plugin.onTargetSelected(makeTargetContext(session)); + expect(plugin.getState()).toEqual({ + kind: "waiting-for-runtime", + reason: expect.any(String), + }); + + await flushAttach(); + + expect(plugin.getState()).toEqual({ kind: "ready" }); + }); + + it("calls initializeDomain and agent-session-ready after bootstrap", async () => { + const plugin = new RozenitePlugin(); + const session = new FakeSession(); + + await plugin.onTargetSelected(makeTargetContext(session)); + await flushAttach(); + + const initCall = session.sendHistory.find( + (c) => + c.method === "Runtime.evaluate" && + typeof c.params?.expression === "string" && + (c.params.expression as string).includes("initializeDomain"), + ); + expect(initCall).toBeDefined(); + const readyCall = session.sendHistory.find( + (c) => + c.method === "Runtime.evaluate" && + typeof c.params?.expression === "string" && + (c.params.expression as string).includes("agent-session-ready"), + ); + expect(readyCall).toBeDefined(); + }); + + it("transitions to error when bootstrap rejects", async () => { + mockRunBootstrap.mockRejectedValueOnce(new Error("Bootstrap failed")); + + const plugin = new RozenitePlugin(); + await plugin.onTargetSelected(makeTargetContext(new FakeSession())); + await flushAttach(); + + expect(plugin.getState()).toEqual({ kind: "error", reason: "Bootstrap failed" }); + }); + + it("does not transition to error when target is cleared during bootstrap", async () => { + mockRunBootstrap.mockImplementationOnce(() => new Promise(() => {})); + + const plugin = new RozenitePlugin(); + const session = new FakeSession(); + + await plugin.onTargetSelected(makeTargetContext(session)); + await plugin.onTargetCleared({ + pluginId: "rozenite", + target: RN_TARGET, + reason: "target-cleared", + }); + + await flushAttach(); + expect(plugin.getState()).toEqual({ kind: "idle" }); + }); + + it("transitions back to idle after onTargetCleared", async () => { + const plugin = new RozenitePlugin(); + const session = new FakeSession(); + + await plugin.onTargetSelected(makeTargetContext(session)); + await flushAttach(); + expect(plugin.getState()).toEqual({ kind: "ready" }); + + await plugin.onTargetCleared({ + pluginId: "rozenite", + target: RN_TARGET, + reason: "target-cleared", + }); + expect(plugin.getState()).toEqual({ kind: "idle" }); + }); + + it("registers event listener before bootstrap completes", async () => { + // The event listener must be registered synchronously (before the async bootstrap) so + // early binding events are not missed. + let bootstrapResolved = false; + mockRunBootstrap.mockImplementationOnce( + () => + new Promise((resolve) => + setTimeout(() => { + bootstrapResolved = true; + resolve(BINDING_NAME); + }, 10), + ), + ); + + const plugin = new RozenitePlugin(); + const session = new FakeSession(); + + // Don't await — just kick off + void plugin.onTargetSelected(makeTargetContext(session)); + + // The listener is registered synchronously (before the first await in onTargetSelected), + // so it's already in place even before bootstrap resolves. + expect(bootstrapResolved).toBe(false); + expect( + (session as unknown as { eventListeners: unknown[] })["eventListeners"].length, + ).toBeGreaterThan(0); + }); + }); + + describe("tool registration via binding events", () => { + let plugin: RozenitePlugin; + let session: FakeSession; + + beforeEach(async () => { + plugin = new RozenitePlugin(); + session = new FakeSession(); + await plugin.onTargetSelected(makeTargetContext(session)); + await flushAttach(); + }); + + afterEach(async () => { + await plugin.onTargetCleared({ + pluginId: "rozenite", + target: RN_TARGET, + reason: "target-cleared", + }); + mockRunBootstrap.mockResolvedValue(BINDING_NAME); + }); + + it("registers tools from register-tool event", () => { + session.emitEvent(makeRozeniteEvent("register-tool", { tools: [ECHO_TOOL, TS_TOOL] })); + expect(plugin.getState()).toEqual({ kind: "ready" }); + }); + + it("unregisters tools from unregister-tool event", () => { + session.emitEvent(makeRozeniteEvent("register-tool", { tools: [ECHO_TOOL, TS_TOOL] })); + session.emitEvent(makeRozeniteEvent("unregister-tool", { toolNames: ["app.echo"] })); + }); + + it("ignores binding events with non-rozenite domain", () => { + session.emitEvent({ + method: "Runtime.bindingCalled", + params: { + name: BINDING_NAME, + payload: JSON.stringify({ domain: "react-devtools", message: {} }), + }, + }); + expect(plugin.getState()).toEqual({ kind: "ready" }); + }); + + it("ignores binding events with wrong pluginId", () => { + session.emitEvent({ + method: "Runtime.bindingCalled", + params: { + name: BINDING_NAME, + payload: JSON.stringify({ + domain: ROZENITE_DOMAIN, + message: { pluginId: "other-plugin", type: "register-tool", payload: { tools: [] } }, + }), + }, + }); + expect(plugin.getState()).toEqual({ kind: "ready" }); + }); + }); + + describe("commands", () => { + let plugin: RozenitePlugin; + let session: FakeSession; + + beforeEach(async () => { + plugin = new RozenitePlugin(); + session = new FakeSession(); + await plugin.onTargetSelected(makeTargetContext(session)); + await flushAttach(); + session.emitEvent(makeRozeniteEvent("register-tool", { tools: [ECHO_TOOL, TS_TOOL] })); + }); + + afterEach(async () => { + await plugin.onTargetCleared({ + pluginId: "rozenite", + target: RN_TARGET, + reason: "target-cleared", + }); + mockRunBootstrap.mockResolvedValue(BINDING_NAME); + }); + + it("status returns state, toolCount, and target", async () => { + const result = await runCommand(plugin, "status", session); + expect(result).toEqual({ + ok: true, + data: { state: "ready", toolCount: 2, target: RN_TARGET }, + }); + }); + + it("status works in waiting-for-runtime state (alwaysExecutable)", async () => { + mockRunBootstrap.mockImplementationOnce(() => new Promise(() => {})); + + const plugin2 = new RozenitePlugin(); + const session2 = new FakeSession(); + const orchestrator = new PluginOrchestrator([plugin2]); + + void plugin2.onTargetSelected(makeTargetContext(session2)); + + const result = await orchestrator.dispatch("rozenite", "status"); + expect(result.ok).toBe(true); + expect((result.data as { state: string }).state).toBe("waiting-for-runtime"); + expect((result.data as { toolCount: number }).toolCount).toBe(0); + + await plugin2.onTargetCleared({ + pluginId: "rozenite", + target: RN_TARGET, + reason: "target-cleared", + }); + }); + + it("tools returns list of tool names and descriptions", async () => { + const result = await runCommand(plugin, "tools", session); + expect(result).toEqual({ + ok: true, + data: [ + { name: "app.echo", description: "Echoes text" }, + { name: "app.getTimestamp", description: "Returns timestamp" }, + ], + }); + }); + + it("tool-schema returns inputSchema for a registered tool", async () => { + const result = await runCommand(plugin, "tool-schema", session, { name: "app.echo" }); + expect(result).toEqual({ ok: true, data: { type: "object" } }); + }); + + it("tool-schema returns error for unknown tool", async () => { + const result = await runCommand(plugin, "tool-schema", session, { name: "unknown" }); + expect(result).toEqual({ ok: false, error: expect.stringContaining("unknown") }); + }); + + it("call sends tool-call and resolves when tool-result arrives", async () => { + const callPromise = runCommand(plugin, "call", session, { + name: "app.echo", + arguments: { text: "hi" }, + }); + + await Promise.resolve(); // let the send happen + + // Find the callId from the sendDomainMessage evaluate call + const callEval = session.sendHistory.find( + (c) => + c.method === "Runtime.evaluate" && + typeof c.params?.expression === "string" && + (c.params.expression as string).includes("tool-call"), + ); + expect(callEval).toBeDefined(); + const expr = callEval!.params!.expression as string; + // Extract callId from the expression + const match = /\\"callId\\":\\"([^"\\]+)\\"/.exec(expr); + expect(match).toBeTruthy(); + const callId = match![1]; + + session.emitEvent( + makeRozeniteEvent("tool-result", { callId, success: true, result: { echo: "hi" } }), + ); + + const result = await callPromise; + expect(result).toEqual({ ok: true, data: { echo: "hi" } }); + }); + + it("call rejects when tool-result reports failure", async () => { + const callPromise = runCommand(plugin, "call", session, { name: "app.echo" }); + await Promise.resolve(); + + const callEval = session.sendHistory.find( + (c) => + c.method === "Runtime.evaluate" && + typeof c.params?.expression === "string" && + (c.params.expression as string).includes("tool-call"), + ); + const match = /\\"callId\\":\\"([^"\\]+)\\"/.exec(callEval!.params!.expression as string); + const callId = match![1]; + + session.emitEvent( + makeRozeniteEvent("tool-result", { callId, success: false, error: "Tool threw an error" }), + ); + + const result = await callPromise; + expect(result).toEqual({ ok: false, error: expect.stringContaining("Tool threw an error") }); + }); + + it("call returns error when no active session", async () => { + const freshPlugin = new RozenitePlugin(); + const result = await runCommand(freshPlugin, "call", null, { name: "app.echo" }); + expect(result).toEqual({ + ok: false, + error: expect.stringContaining("No active Rozenite session"), + }); + }); + }); +}); diff --git a/packages/agent-cdp/src/cli/index.ts b/packages/agent-cdp/src/cli/index.ts index 214571e..9399aec 100644 --- a/packages/agent-cdp/src/cli/index.ts +++ b/packages/agent-cdp/src/cli/index.ts @@ -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 { diff --git a/packages/agent-cdp/src/daemon.ts b/packages/agent-cdp/src/daemon.ts index 278bc24..e7ca139 100644 --- a/packages/agent-cdp/src/daemon.ts +++ b/packages/agent-cdp/src/daemon.ts @@ -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"; @@ -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, diff --git a/packages/agent-cdp/src/plugin-orchestrator.ts b/packages/agent-cdp/src/plugin-orchestrator.ts index d150a08..c1e6eec 100644 --- a/packages/agent-cdp/src/plugin-orchestrator.ts +++ b/packages/agent-cdp/src/plugin-orchestrator.ts @@ -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); diff --git a/packages/agent-cdp/src/plugin.ts b/packages/agent-cdp/src/plugin.ts index fe6a795..e7bfd97 100644 --- a/packages/agent-cdp/src/plugin.ts +++ b/packages/agent-cdp/src/plugin.ts @@ -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; } diff --git a/packages/agent-cdp/src/plugins/rozenite/bootstrap.ts b/packages/agent-cdp/src/plugins/rozenite/bootstrap.ts new file mode 100644 index 0000000..2efda0c --- /dev/null +++ b/packages/agent-cdp/src/plugins/rozenite/bootstrap.ts @@ -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 { + 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((resolve) => setTimeout(resolve, BOOTSTRAP_POLL_INTERVAL_MS)); + } +} + +async function getBindingName(session: AgentPluginTargetSession): Promise { + 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 { + 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; +} diff --git a/packages/agent-cdp/src/plugins/rozenite/cli.ts b/packages/agent-cdp/src/plugins/rozenite/cli.ts new file mode 100644 index 0000000..48436c1 --- /dev/null +++ b/packages/agent-cdp/src/plugins/rozenite/cli.ts @@ -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 ") + .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 ") + .description("Call a Rozenite tool") + .option("--input ", "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); + }); +} \ No newline at end of file diff --git a/packages/agent-cdp/src/plugins/rozenite/index.ts b/packages/agent-cdp/src/plugins/rozenite/index.ts new file mode 100644 index 0000000..91d11f9 --- /dev/null +++ b/packages/agent-cdp/src/plugins/rozenite/index.ts @@ -0,0 +1,273 @@ +import type { TargetDescriptor } from "@agent-cdp/protocol"; + +import type { + AgentPlugin, + AgentPluginCommand, + AgentPluginDetachContext, + AgentPluginState, + AgentPluginTargetContext, + AgentPluginTargetSession, +} from "../../plugin.js"; +import { runBootstrap } from "./bootstrap.js"; +import { + AGENT_PLUGIN_ID, + ROZENITE_DOMAIN, + RUNTIME_GLOBAL, + type RozeniteDevToolsMessage, + type RozeniteRegisterToolPayload, + type RozeniteToolCallPayload, + type RozeniteToolResultPayload, + type RozeniteUnregisterToolPayload, +} from "./protocol.js"; +import { ToolRegistry } from "./tool-registry.js"; + +interface PendingCall { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + timeoutId: ReturnType; +} + +export class RozenitePlugin implements AgentPlugin { + readonly id = "rozenite"; + readonly displayName = "Rozenite"; + readonly description = "Rozenite React Native agent bridge"; + readonly commands: readonly AgentPluginCommand[]; + + private state: AgentPluginState = { kind: "idle" }; + private session: AgentPluginTargetSession | null = null; + private removeEventListener: (() => void) | null = null; + private abortController: AbortController | null = null; + private readonly toolRegistry = new ToolRegistry(); + private readonly pendingCalls = new Map(); + + constructor() { + this.commands = this.buildCommands(); + } + + getState(): AgentPluginState { + return this.state; + } + + supportsTarget(target: TargetDescriptor): boolean { + return target.kind === "react-native"; + } + + async onTargetSelected(ctx: AgentPluginTargetContext): Promise { + this.detach(); + this.state = { kind: "waiting-for-runtime", reason: "Bootstrapping Rozenite CDP bridge..." }; + this.abortController = new AbortController(); + + const session = ctx.session; + + // Register event listener BEFORE calling initializeDomain to avoid missing early events. + this.removeEventListener = session.onEvent((message) => { + if ( + message.method === "Runtime.executionContextsCleared" || + message.method === "Runtime.executionContextCreated" + ) { + void this.reattach(session); + return; + } + if (message.method !== "Runtime.bindingCalled") return; + void this.handleBindingCalled(message.params); + }); + + void this.attach(session); + } + + async onTargetReconnected(ctx: AgentPluginTargetContext): Promise { + return this.onTargetSelected(ctx); + } + + async onTargetCleared(_ctx: AgentPluginDetachContext): Promise { + this.detach(); + } + + private detach(): void { + this.abortController?.abort(); + this.abortController = null; + this.removeEventListener?.(); + this.removeEventListener = null; + this.session = null; + this.toolRegistry.clear(); + for (const [, pending] of this.pendingCalls) { + clearTimeout(pending.timeoutId); + pending.reject(new Error("Session detached")); + } + this.pendingCalls.clear(); + this.state = { kind: "idle" }; + } + + private async attach(session: AgentPluginTargetSession): Promise { + const signal = this.abortController?.signal; + + try { + await runBootstrap(session, signal); + if (signal?.aborted) return; + + this.session = session; + + await session.send("Runtime.evaluate", { + expression: `void globalThis.${RUNTIME_GLOBAL}.initializeDomain(${JSON.stringify(ROZENITE_DOMAIN)})`, + }); + + await this.sendDomainMessage(session, { + pluginId: AGENT_PLUGIN_ID, + type: "agent-session-ready", + payload: { sessionId: session.target.id }, + }); + + this.state = { kind: "ready" }; + } catch (err) { + const error = err as Error; + if (error.name !== "AbortError" && !signal?.aborted) { + this.state = { kind: "error", reason: error.message }; + } + } + } + + private async reattach(session: AgentPluginTargetSession): Promise { + if (this.session !== session || !session.isConnected()) return; + this.toolRegistry.clear(); + this.state = { kind: "waiting-for-runtime", reason: "Reconnecting after context reload..." }; + try { + await runBootstrap(session, this.abortController?.signal); + await session.send("Runtime.evaluate", { + expression: `void globalThis.${RUNTIME_GLOBAL}.initializeDomain(${JSON.stringify(ROZENITE_DOMAIN)})`, + }); + await this.sendDomainMessage(session, { + pluginId: AGENT_PLUGIN_ID, + type: "agent-session-ready", + payload: { sessionId: session.target.id }, + }); + this.state = { kind: "ready" }; + } catch { + // A later reconnect will retry; ignore errors here. + } + } + + private async sendDomainMessage( + session: AgentPluginTargetSession, + message: RozeniteDevToolsMessage, + ): Promise { + const serialized = JSON.stringify(message); + const escaped = JSON.stringify(serialized); + await session.send("Runtime.evaluate", { + expression: `globalThis.${RUNTIME_GLOBAL}.sendMessage(${JSON.stringify(ROZENITE_DOMAIN)}, ${escaped})`, + }); + } + + private handleBindingCalled(params: Record | undefined): void { + const rawPayload = params?.payload; + if (typeof rawPayload !== "string") return; + + let envelope: { domain?: unknown; message?: unknown }; + try { + envelope = JSON.parse(rawPayload) as typeof envelope; + } catch { + return; + } + + if (envelope.domain !== ROZENITE_DOMAIN) return; + + const msg = envelope.message as RozeniteDevToolsMessage | undefined; + if (!msg || msg.pluginId !== AGENT_PLUGIN_ID) return; + + switch (msg.type) { + case "register-tool": { + const { tools } = msg.payload as RozeniteRegisterToolPayload; + this.toolRegistry.register(tools); + break; + } + case "unregister-tool": { + const { toolNames } = msg.payload as RozeniteUnregisterToolPayload; + this.toolRegistry.unregister(toolNames); + break; + } + case "tool-result": { + const { callId, success, result, error } = msg.payload as RozeniteToolResultPayload; + const pending = this.pendingCalls.get(callId); + if (!pending) return; + this.pendingCalls.delete(callId); + clearTimeout(pending.timeoutId); + if (success) { + pending.resolve(result); + } else { + pending.reject(new Error(error ?? "Tool call failed")); + } + break; + } + } + } + + private buildCommands(): AgentPluginCommand[] { + return [ + { + name: "status", + summary: "Show Rozenite plugin state and registered tool count", + alwaysExecutable: true, + execute: async (ctx) => { + const state = ctx.getState(); + return { + state: state.kind, + ...(state.kind === "error" ? { error: state.reason } : {}), + toolCount: this.toolRegistry.size(), + target: ctx.session?.target ?? null, + }; + }, + }, + { + name: "tools", + summary: "List registered Rozenite tools", + execute: async () => { + return this.toolRegistry + .getAll() + .map((t) => ({ name: t.name, description: t.description })); + }, + }, + { + name: "tool-schema", + summary: "Show input schema for a Rozenite tool", + execute: async (_ctx, input) => { + const { name } = input as { name: string }; + const tool = this.toolRegistry.get(name); + if (!tool) throw new Error(`Tool '${name}' not found`); + return tool.inputSchema; + }, + }, + { + name: "call", + summary: "Call a Rozenite tool", + execute: async (_ctx, input) => { + const { name, arguments: args } = input as { name: string; arguments?: unknown }; + const session = this.session; + if (!session) throw new Error("No active Rozenite session"); + + const callId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + const resultPromise = new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + this.pendingCalls.delete(callId); + reject(new Error("Tool call timeout")); + }, 30_000); + this.pendingCalls.set(callId, { resolve, reject, timeoutId }); + }); + + const payload: RozeniteToolCallPayload = { + callId, + toolName: name, + arguments: args ?? null, + }; + + await this.sendDomainMessage(session, { + pluginId: AGENT_PLUGIN_ID, + type: "tool-call", + payload, + }); + + return resultPromise; + }, + }, + ]; + } +} diff --git a/packages/agent-cdp/src/plugins/rozenite/protocol.ts b/packages/agent-cdp/src/plugins/rozenite/protocol.ts new file mode 100644 index 0000000..f15e3c1 --- /dev/null +++ b/packages/agent-cdp/src/plugins/rozenite/protocol.ts @@ -0,0 +1,42 @@ +export const RUNTIME_GLOBAL = "__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__"; +export const ROZENITE_DOMAIN = "rozenite"; +export const AGENT_PLUGIN_ID = "rozenite-agent"; +export const BOOTSTRAP_POLL_INTERVAL_MS = 250; +export const BOOTSTRAP_POLL_MAX_ATTEMPTS = 40; + +export interface RozeniteRegisteredTool { + name: string; + description: string; + inputSchema: object; +} + +export interface RozeniteDevToolsMessage { + pluginId: string; + type: string; + payload: unknown; +} + +export interface RozeniteRegisterToolPayload { + tools: RozeniteRegisteredTool[]; +} + +export interface RozeniteUnregisterToolPayload { + toolNames: string[]; +} + +export interface RozeniteToolResultPayload { + callId: string; + success: boolean; + result?: unknown; + error?: string; +} + +export interface RozeniteToolCallPayload { + callId: string; + toolName: string; + arguments: unknown; +} + +export interface RozeniteAgentSessionReadyPayload { + sessionId: string; +} \ No newline at end of file diff --git a/packages/agent-cdp/src/plugins/rozenite/tool-registry.ts b/packages/agent-cdp/src/plugins/rozenite/tool-registry.ts new file mode 100644 index 0000000..af91a04 --- /dev/null +++ b/packages/agent-cdp/src/plugins/rozenite/tool-registry.ts @@ -0,0 +1,33 @@ +import type { RozeniteRegisteredTool } from "./protocol.js"; + +export class ToolRegistry { + private readonly tools = new Map(); + + register(tools: RozeniteRegisteredTool[]): void { + for (const tool of tools) { + this.tools.set(tool.name, tool); + } + } + + unregister(toolNames: string[]): void { + for (const name of toolNames) { + this.tools.delete(name); + } + } + + getAll(): RozeniteRegisteredTool[] { + return [...this.tools.values()]; + } + + get(name: string): RozeniteRegisteredTool | undefined { + return this.tools.get(name); + } + + size(): number { + return this.tools.size; + } + + clear(): void { + this.tools.clear(); + } +} diff --git a/playground/metro.config.js b/playground/metro.config.js new file mode 100644 index 0000000..ca627da --- /dev/null +++ b/playground/metro.config.js @@ -0,0 +1,8 @@ +const { getDefaultConfig } = require('expo/metro-config'); +const { withRozenite } = require('@rozenite/metro'); + +const config = getDefaultConfig(__dirname); + +module.exports = withRozenite(config, { + enabled: process.env.WITH_ROZENITE === 'true', +}); \ No newline at end of file diff --git a/playground/package.json b/playground/package.json index 0d7e85b..22aa68b 100644 --- a/playground/package.json +++ b/playground/package.json @@ -14,6 +14,7 @@ "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/elements": "^2.9.10", "@react-navigation/native": "^7.1.33", + "@rozenite/agent-bridge": "^1.10.0", "expo": "~55.0.24", "expo-constants": "~55.0.16", "expo-device": "~55.0.17", @@ -38,6 +39,7 @@ "react-native-worklets": "0.7.4" }, "devDependencies": { + "@rozenite/metro": "^1.10.0", "@types/react": "~19.2.2", "eslint": "^9.0.0", "eslint-config-expo": "~55.0.1", diff --git a/playground/src/app/index.tsx b/playground/src/app/index.tsx index 3ffba37..3ab0e7b 100644 --- a/playground/src/app/index.tsx +++ b/playground/src/app/index.tsx @@ -16,6 +16,8 @@ import { type TraceStopResponse, } from '@agent-cdp/sdk'; import { useState } from 'react'; + +import { ROZENITE_TOOL_COUNT, useRozeniteBridge } from '@/hooks/use-rozenite-bridge'; import { Pressable, ScrollView, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -203,6 +205,38 @@ function formatBytes(value: number) { return `${Math.round(value / 1024)} KB`; } +const rozeniteAvailable = + typeof (globalThis as Record).__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__ !== 'undefined'; + +function RozeniteBridgeSection() { + const { lastCall } = useRozeniteBridge(); + + return ( + + Rozenite Agent Tools + + {ROZENITE_TOOL_COUNT} test tools registered: app.echo, app.getTimestamp, app.getPlaygroundInfo. + Call them with: agent-cdp rozenite call app.echo --input '{`{"text":"hello"}`}' + + + Tools registered: {ROZENITE_TOOL_COUNT} + {lastCall ? ( + <> + Last call: {lastCall.name} + + {JSON.stringify(lastCall.result)} + + + ) : ( + + No tool calls yet. Use agent-cdp rozenite call to invoke a tool. + + )} + + + ); +} + export default function HomeScreen() { const [sdkBusy, setSdkBusy] = useState(false); const [sdkFlowMessage, setSdkFlowMessage] = useState('Select the app target with agent-cdp, then run the SDK CPU profile flow.'); @@ -655,6 +689,7 @@ export default function HomeScreen() { + {rozeniteAvailable && } SDK Testing diff --git a/playground/src/hooks/use-rozenite-bridge.ts b/playground/src/hooks/use-rozenite-bridge.ts new file mode 100644 index 0000000..0524d8a --- /dev/null +++ b/playground/src/hooks/use-rozenite-bridge.ts @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import { useRozeniteInAppAgentTool } from '@rozenite/agent-bridge'; + +export type RozeniteBridgeState = { + lastCall: { name: string; result: unknown; ts: number } | null; +}; + +const echoTool = { + name: 'echo', + description: 'Returns the provided text argument unchanged', + inputSchema: { + type: 'object', + properties: { text: { type: 'string', description: 'Text to echo back' } }, + required: ['text'], + }, +}; + +const getTimestampTool = { + name: 'getTimestamp', + description: 'Returns the current device date/time as an ISO string', + inputSchema: { type: 'object', properties: {} }, +}; + +const getPlaygroundInfoTool = { + name: 'getPlaygroundInfo', + description: 'Returns basic information about the playground runtime environment', + inputSchema: { type: 'object', properties: {} }, +}; + +// Total number of test tools registered by useRozeniteBridge +export const ROZENITE_TOOL_COUNT = 3; + +export function useRozeniteBridge(): RozeniteBridgeState { + const [lastCall, setLastCall] = useState(null); + + useRozeniteInAppAgentTool({ + tool: echoTool, + handler: (args: { text?: string }) => { + const result = { echo: args?.text ?? '' }; + setLastCall({ name: 'app.echo', result, ts: Date.now() }); + return result; + }, + }); + + useRozeniteInAppAgentTool({ + tool: getTimestampTool, + handler: () => { + const result = { timestamp: new Date().toISOString() }; + setLastCall({ name: 'app.getTimestamp', result, ts: Date.now() }); + return result; + }, + }); + + useRozeniteInAppAgentTool({ + tool: getPlaygroundInfoTool, + handler: () => { + const result = { + hermes: typeof (globalThis as Record).HermesInternal !== 'undefined', + now: Date.now(), + }; + setLastCall({ name: 'app.getPlaygroundInfo', result, ts: Date.now() }); + return result; + }, + }); + + return { lastCall }; +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca30146..e84ae75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: '@react-navigation/native': specifier: ^7.1.33 version: 7.2.4(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@rozenite/agent-bridge': + specifier: ^1.10.0 + version: 1.10.0(react@19.2.0) expo: specifier: ~55.0.24 version: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) @@ -158,6 +161,9 @@ importers: specifier: 0.7.4 version: 0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) devDependencies: + '@rozenite/metro': + specifier: ^1.10.0 + version: 1.10.0 '@types/react': specifier: ~19.2.2 version: 19.2.14 @@ -1170,21 +1176,25 @@ packages: resolution: {integrity: sha512-KlwzidgvHznbUaaglZT1goTS30osTV553pfbKve9B1PyTDkluNDfm/polOaf3SVLN7wL/NNLFZRMupvJ1eJXAw==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/linux-arm64-musl@0.17.0': resolution: {integrity: sha512-+tbYJTocF4BNLaQQbc/xrBWTNgiU6zmYeF4NvRDxuuQjDOnmUZPn0EED3PZBRJyg4/YllhplHDo8x+gfcb9G3A==} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/linux-x64-gnu@0.17.0': resolution: {integrity: sha512-pEmv7zJIw2HpnA4Tn1xrfJNGi2wOH2+usT14Pkvf/c5DdB+pOir6k/5jzfe70+V3nEtmtV9Lm+spndN/y6+X7A==} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/linux-x64-musl@0.17.0': resolution: {integrity: sha512-+DrFSCZWyFdtEAWR5xIBTV8GX0RA9iB+y7ZlJPRAXrNG8TdBY9vc7/MIGolIgrkMPK4mGMn07YG/qEyPY+iKaw==} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/win32-arm64@0.17.0': resolution: {integrity: sha512-FoUZRR7mVpTYIaY/qz2BYwzqMnL+HsUxmMWAIy6nl29UEkDgxNygULJ4rIGY4/Axne41fhtldLrSGBOpwNm3jA==} @@ -1210,21 +1220,25 @@ packages: resolution: {integrity: sha512-NoAWscdfVj6Sci3NdbHHc1OivSSKpwtkLff5SoAM8XgJ9t7flf+zW7XOy3OeSgZAxNbcF4QGruv+XcBLR7tWMA==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/linux-arm64-musl@0.17.0': resolution: {integrity: sha512-VQRmSdbuc0rpSZoLqdhKJG9nUjJmEymOU60dO3lKlCT5YXME4dxA+jf1AigtnmJS5FgOxQm54ECF9lh6dyP0ew==} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/linux-x64-gnu@0.17.0': resolution: {integrity: sha512-KcaXWkBfqt7weerU1EXJysEupEHB8AtJytufBOQMuLE5vx2OoAbjAk0vt2V14W8Lss9Sz+ET7nXo6ZAEku4vCA==} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/linux-x64-musl@0.17.0': resolution: {integrity: sha512-yWbFXWKKTrH4zR0FI1V6KDJp5NqOLFe1LZbKNeaoS1wtq5/aFPeM+d9dttGLoA5u6G9uhIK/nSnrPmtuNLPU4Q==} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/win32-arm64@0.17.0': resolution: {integrity: sha512-zdoB3mbvcx3eGOh6ElPJ01S2MOzyZo/gijeHw7yb2PXcRis+3clVje6kpnG7/TN69zoHv7WwDX6poJu8FURTqg==} @@ -1611,66 +1625,79 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} @@ -1702,6 +1729,37 @@ packages: cpu: [x64] os: [win32] + '@rozenite/agent-bridge@1.10.0': + resolution: {integrity: sha512-Rnfr85m3Ya91VG5r1x9+gSJ5gOCZLw4Yec0zBijdUSXxhH9LoChIuniG41hgxBYYKvMG22oeJnpI2atMdzFAgw==} + engines: {node: '>=20'} + peerDependencies: + react: '>=17' + + '@rozenite/agent-shared@1.10.0': + resolution: {integrity: sha512-iZimNxQFE0qsF4nucVYLt1AYrI6WRXlscL6QVHKkfaI0/AdI14cdGbJEoxuS7Twpkru4/4v0QScsM1OIPZxgqg==} + engines: {node: '>=20'} + + '@rozenite/metro@1.10.0': + resolution: {integrity: sha512-0+FVgu4+UAr0c/qm6+iP+mY38XtfKgcWzX1Jh9QNVFVYAEo3nmr9DwOJsGIB++sOhnHwtqh4Ry21+ouXS8P4Kw==} + engines: {node: '>=20'} + + '@rozenite/middleware@1.10.0': + resolution: {integrity: sha512-w5HrGKwSNC8HRJe8BleL/i4EF4BXJq8uU37oZ6EnL4amGKnbOobfCGCoj4XSEwX5EJYHwVchRfZMAtHtCwUlWQ==} + engines: {node: '>=20'} + + '@rozenite/plugin-bridge@1.10.0': + resolution: {integrity: sha512-kUu4UdDOoemJqB8wxfDaSgGKQuPjJPYCiC0QU5c/tNp16dIzrSA4IX12c7bYzkeM55YMU17MXnXpXH1MEGW1qw==} + engines: {node: '>=20'} + peerDependencies: + react: '>=17' + + '@rozenite/runtime@1.10.0': + resolution: {integrity: sha512-RvCBUl9g+fUTrGgKoY/0pVzrYXpKJzYddNDa0O/Pfgrevxn8E/hQ/5nzSXONH0I6X8lYDeQyDNvGLbbb96pgYg==} + engines: {node: '>=20'} + + '@rozenite/tools@1.10.0': + resolution: {integrity: sha512-tbEz6fc7yXMCzRQwHQIOasaj/XvKVG/m+fnegl6sP8SVMR+xP141Zw6rMJS3OD5AO1ln1mH4CY78dpnPt7td1A==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1878,41 +1936,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2187,6 +2253,10 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -2376,9 +2446,29 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} @@ -2872,6 +2962,10 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2923,6 +3017,10 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2951,10 +3049,18 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3113,6 +3219,10 @@ packages: hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3151,6 +3261,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3230,6 +3344,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3432,24 +3549,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3529,12 +3650,20 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3854,6 +3983,9 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3949,10 +4081,18 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -3964,6 +4104,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} @@ -4164,6 +4308,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} @@ -4179,6 +4327,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -4209,6 +4360,10 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -4217,6 +4372,10 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -4555,6 +4714,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6639,6 +6802,52 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true + '@rozenite/agent-bridge@1.10.0(react@19.2.0)': + dependencies: + '@rozenite/agent-shared': 1.10.0 + '@rozenite/plugin-bridge': 1.10.0(react@19.2.0) + react: 19.2.0 + tslib: 2.8.1 + + '@rozenite/agent-shared@1.10.0': + dependencies: + tslib: 2.8.1 + + '@rozenite/metro@1.10.0': + dependencies: + '@rozenite/middleware': 1.10.0 + '@rozenite/tools': 1.10.0 + tslib: 2.8.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@rozenite/middleware@1.10.0': + dependencies: + '@rozenite/agent-shared': 1.10.0 + '@rozenite/runtime': 1.10.0 + '@rozenite/tools': 1.10.0 + express: 5.2.1 + semver: 7.8.0 + tslib: 2.8.1 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@rozenite/plugin-bridge@1.10.0(react@19.2.0)': + dependencies: + react: 19.2.0 + tslib: 2.8.1 + + '@rozenite/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + + '@rozenite/tools@1.10.0': {} + '@rtsao/scc@1.1.0': {} '@sinclair/typebox@0.27.10': {} @@ -7215,6 +7424,20 @@ snapshots: big-integer@1.6.52: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -7416,8 +7639,18 @@ snapshots: consola@3.4.2: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + core-js-compat@3.49.0: dependencies: browserslist: 4.28.2 @@ -8084,6 +8317,39 @@ snapshots: exponential-backoff@3.1.3: {} + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -8138,6 +8404,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8169,8 +8446,12 @@ snapshots: dependencies: is-callable: 1.2.7 + forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -8330,6 +8611,10 @@ snapshots: hyphenate-style-name@1.1.0: {} + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -8366,6 +8651,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -8445,6 +8732,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -8746,10 +9035,14 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.0: {} + memoize-one@5.2.1: {} memoize-one@6.0.0: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} metro-babel-transformer@0.83.7: @@ -9182,6 +9475,8 @@ snapshots: lru-cache: 11.3.6 minipass: 7.1.3 + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -9262,8 +9557,17 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -9277,6 +9581,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-devtools-core@6.1.5: dependencies: shell-quote: 1.8.3 @@ -9564,6 +9875,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 @@ -9585,6 +9906,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sax@1.6.0: {} scheduler@0.27.0: {} @@ -9615,6 +9938,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serve-static@1.16.3: @@ -9626,6 +9965,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + server-only@0.0.1: {} set-function-length@1.2.2: @@ -9971,6 +10319,12 @@ snapshots: type-fest@0.7.1: {} + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4