Skip to content

feat(cli): add rozenite agent tap to stream plugin messages to stdout - #433

Merged
V3RON merged 2 commits into
mainfrom
claude/rozenite-isolated-testing-sow12u-431
Aug 20, 2026
Merged

feat(cli): add rozenite agent tap to stream plugin messages to stdout#433
V3RON merged 2 commits into
mainfrom
claude/rozenite-isolated-testing-sow12u-431

Conversation

@V3RON

@V3RON V3RON commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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_ID watches 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.
  • --type and --payload (mapping onto the type/payload fields of the message, alongside --plugin for pluginId) 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. --payload defaults to {} and is validated as JSON on its own.
  • --json switches 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.
  • Ctrl-C closes the tap cleanly and releases the debugger connection.
  • The command lives under rozenite agent rather than at the top level: it is session-scoped (it cannot do anything without a session from rozenite 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/--port from the agent command instead of redeclaring them, the same way rozenite agent targets does. This reserves tap from the dynamic plugin-domain namespace under agent, exactly as session, targets, and list-domains already 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 tap required 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.ts searched 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:

  • pnpm --filter @rozenite/middleware build
  • pnpm --filter @rozenite/middleware test, 126 tests passed (13 files), including the new agent-tap.test.ts, agent-routes-tap.test.ts, and tap additions to agent-session.test.ts and agent-session-manager.test.ts
  • pnpm --filter @rozenite/middleware typecheck
  • pnpm --filter @rozenite/middleware lint
  • pnpm --filter rozenite build (the CLI package is named rozenite, not @rozenite/cli)
  • pnpm --filter rozenite test, 131 tests passed (14 files), including the new agent-tap-format.test.ts and agent-tap-command.test.ts
  • pnpm --filter rozenite typecheck
  • pnpm --filter rozenite lint
  • pnpm --filter @rozenite/agent-shared build
  • pnpm --filter @rozenite/agent-sdk build
  • pnpm --filter @rozenite/metro test: the CJS-load smoke test that requires the built @rozenite/middleware, @rozenite/tools, and @rozenite/metro bundles in a real node subprocess and runs withRozenite() end-to-end, per docs/agents/metro-testing.md
  • pnpm format:all on every file this PR touches (a repo-wide run also flagged three pre-existing, unrelated files already failing on main; those were left untouched)
  • pnpm release:plan, confirming the changeset is present and valid

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) and oxfmt --check . (clean apart from four files already unformatted on this branch before the move). The built CLI was also exercised directly: rozenite agent tap --help renders, rozenite agent --help lists tap alongside targets/session, and rozenite tap now reports unknown 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 tap have 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.

claude and others added 2 commits August 20, 2026 06:22
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.
@V3RON V3RON changed the title feat(cli): add rozenite tap to stream plugin messages to stdout feat(cli): add rozenite agent tap to stream plugin messages to stdout Aug 20, 2026
@V3RON
V3RON merged commit c5a3cfc into main Aug 20, 2026
4 checks passed
@V3RON
V3RON deleted the claude/rozenite-isolated-testing-sow12u-431 branch August 20, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): rozenite tap — stream plugin messages to stdout

2 participants