feat(cli): add rozenite agent tap to stream plugin messages to stdout - #433
Merged
Conversation
Tees Rozenite plugin messages at the two chokepoints the agent session already has (inbound device messages, outbound sendMessage) into a chunked-HTTP tap route on the existing agent server, and consumes it from a new `rozenite tap` command. Streams ndjson with --json, and --plugin/--type/--payload can poke a plugin and watch what comes back without a browser or React Native DevTools. Closes #431 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvgwPizCjWq2KUhwa3FUU3
tap is session-scoped: it requires a session created by `rozenite agent session create` and talks to the agent server over the same transport every other agent command uses. Register it as `rozenite agent tap` so it sits with the rest of the session-scoped surface, and let it inherit --host/--port from the agent command instead of redeclaring them, the same way `rozenite agent targets` does. Also makes the payload-alignment test time-zone independent: it searched for a bare digit that a local timestamp can contain.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
rozenite agent tap, a CLI command that streams the plugin messages of a Rozenite Agent session to stdout as they flow, in both directions, without needing a browser or React Native DevTools attached.rozenite agent tap --session SESSION_ID --plugin PLUGIN_IDwatches the traffic for that plugin, printing a direction arrow (←for a message received from the device,→for a message sent to it), a local timestamp, the message type, and the JSON payload, one line per message.--typeand--payload(mapping onto thetype/payloadfields of the message, alongside--pluginforpluginId) send one message before watching, so the native side of a plugin can be poked from a terminal and its response observed directly, as a minimal bidirectional stand-in for DevTools.--payloaddefaults to{}and is validated as JSON on its own.--jsonswitches to newline-delimited JSON, one message per line, documented with a stable{direction, timestamp, pluginId, type, payload}shape, for another program or agent to consume.rozenite agentrather than at the top level: it is session-scoped (it cannot do anything without a session fromrozenite agent session create, and it talks to the agent server over the same transport every other agent command uses), so it sits with the rest of the session-scoped surface and inherits--host/--portfrom theagentcommand instead of redeclaring them, the same wayrozenite agent targetsdoes. This reservestapfrom the dynamic plugin-domain namespace underagent, exactly assession,targets, andlist-domainsalready do.Related Issue
Closes #431
Context
A device serves one debugger connection at a time (session.ts already handles [NEW_DEBUGGER_OPENED] by giving up rather than fighting for the socket), so a tap has to ride the connection rozenite agent already holds instead of opening a second one. I teed the existing AgentSession at its two existing chokepoints, the inbound Runtime.bindingCalled handler and the outbound sendMessage callback passed to handler.connectDevice, into a small pub/sub (packages/middleware/src/agent/tap.ts), independent of the CDP/bootstrap machinery so it can be unit-tested directly. The existing behavior at both chokepoints is unchanged; the tee is additive, an emit call added before/alongside the existing logic. AgentSession exposes the resulting subscribeTap/sendTapMessage, and AgentSessionManager forwards them by session id, matching how every other session operation (callTool, getSessionTools, and so on) is already exposed.
For the agent-server route, the existing surface is exclusively HTTP request/response JSON envelopes served over the Express app Metro runs (getMiddleware mounts createAgentRoutes(...) as a plain Router). There is no WebSocket server or SSE precedent anywhere in the codebase to match a house style against, and this server does not own the raw http.Server (Metro does), so adding a WebSocket upgrade path would mean reaching for infrastructure the codebase does not otherwise have. I used a plain GET route that keeps the response open and writes newline-delimited JSON (chunked transfer encoding, application/x-ndjson) until the client disconnects, the smallest addition consistent with treating this as just another Express route, and it happens to match the ndjson shape already used by the --json output on the CLI exactly. Sending a message reuses the same URL as a POST (the existing session route already does GET/DELETE on one pattern).
--plugin filters the stream; --type does not, even though it is also the flag used to construct the outbound poke message, since filtering the display on it would hide the very response a poke is meant to surface.
Why @rozenite/agent-sdk and @rozenite/agent-shared changed too: the only path the CLI has to the agent server is the transport in @rozenite/agent-sdk (it already implements session create/list/show/stop and tool calls the same way), so reaching the new tap route from
rozenite agent taprequired extending that transport with openSessionTap/sendSessionTapMessage, since there is no other client the CLI has. @rozenite/agent-shared is where the wire types both sides already share live (DevToolsPluginMessage, the other AGENT_SESSION_*_ROUTE constants, AgentResponseEnvelope, and so on), so the new TapEvent type and AGENT_SESSION_TAP_ROUTE_PATTERN constant belong there for the same reason the existing ones do: middleware and agent-sdk both need the identical route path and event shape. Both changes are scoped to exactly that: new route/type plumbing for tap, added the same way the existing session-lifecycle plumbing is. I did not touch the higher-level client.ts/domain-utils.ts in agent-sdk (the domain/tool-resolution layer); tap does not need domain resolution, so it stays at the transport level only, the same layer already used by the session create/list/show/stop commands on the CLI.One drive-by fix included here: the payload-alignment assertion in
agent-tap-format.test.tssearched for a bare digit (indexOf('1')) in a line whose timestamp is rendered in local time, so the digit matched inside the timestamp rather than the payload on any machine east of UTC, and the test failed there. It now uses a fixed local-constructed timestamp and searches for the payload string.Testing
Automated (all passed), run per-package with --filter rather than repo-wide:
After moving the command under
agent, the following were re-run from the repository root and all passed:turbo run build test typecheck lint --filter=rozenite(131 tests, 14 files) andoxfmt --check .(clean apart from four files already unformatted on this branch before the move). The built CLI was also exercised directly:rozenite agent tap --helprenders,rozenite agent --helplists tap alongsidetargets/session, androzenite tapnow reportsunknown command 'tap'. Real-device verification is still absent, as described below.The paragraphs below describe the state of the original submission.
Those were the last checks run before pushing. Everything added afterward was documentation-only (the changeset body, the CLI README section, the website tap.mdx page) and does not participate in any of the runs above. I did not re-run these, or any full-repo check, after that point. This container turned out to be shared with another concurrent agent session, and a repo-wide typecheck:all/lint:all (which the pre-push hook normally runs, hence pushing with --no-verify) was oversubscribing it. At the direction of the user I stopped running local validation for this final submission and am relying on CI as the verification path here.
Verification against a real device was not performed. This environment cannot run a simulator, a device, or Metro, so the tap route and
rozenite agent taphave only ever been exercised against fake session/socket and fake-manager test harnesses, never against a real CDP connection. That is the remaining risk for a reviewer.