Six agents have a native adapter. Everything else, a harness you wrote, a tool Runi has never heard of, a CI job, uses one or both of the two generic channels: a standalone CLI, and the MCP server.
Which channel to use#
| Your agent | Use |
|---|---|
| Speaks MCP | The MCP server. Six tools, results straight into the conversation, no copying. Register it manually with your client, pointing at ~/.relay/daemon/mcp-server.js. |
| Does not speak MCP, but you control the prompt | relay-cli brief. Print the packet and have the harness prepend it to the first prompt. |
| You just want its file changes observed | relay-cli wrap. Run the command through it. |
The CLI#
It lives beside the daemon, at ~/.relay/daemon/relay-cli.js. Three
subcommands, all of which go through the same redaction and outbound gate as a
hook publication. There is no second, weaker path here.
brief: print what a session should know#
$ node ~/.relay/daemon/relay-cli.js brief
$ node ~/.relay/daemon/relay-cli.js brief --task tsk_9f2c
This is the same packet the hooks inject and the MCP server serves, rendered by the same function. It goes to stdout, so it pipes.
remember: file a memory for review#
$ node ~/.relay/daemon/relay-cli.js remember \
--kind decision \
--title "Retry the rollout parser rather than re-reading the file" \
--body "Re-reading raced the writer on Windows and produced half-written JSON." \
--reason "The writer is not atomic, so a read can land mid-write."
What comes back is proposed, in your name. It reaches
nobody's brief until you confirm it in the app, because an agent can be talked
into filing a memory by a page it read. A decision without a
--reason is refused outright.
wrap: observe a command Runi knows nothing about#
$ node ~/.relay/daemon/relay-cli.js wrap -- my-agent --do-the-thing
It snapshots git status before and after, runs your command with
stdio inherited so it behaves normally, and publishes one change event naming
the files that moved. File contents never leave the machine.
The detail carries a diff stat, not the diff.
Exit codes#
The hooks exit 0 on every failure, because a broken network must never break your agent. The CLI is user-invoked, so it fails loudly.
| Exit | Means |
|---|---|
0 | The answer is on stdout. |
1 | There is no answer, and the reason is on stderr: not signed in, repository untracked, or publication refused. |
An exit code a harness can branch on, and a sentence a person can act on. The two you will meet:
runi: Runi is not signed in on this machine. Connect an agent from
the Runi app, or run `node ~/.relay/daemon/login.js`.
runi: This repository is not registered to the Runi workspace.
Add it under Settings → Repository access in the Runi app.
Testing the MCP server without a client#
Two JSON-RPC lines on stdin tell you whether the server is alive and what it offers:
$ printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| node ~/.relay/daemon/mcp-server.js
Known limits of the generic path#
- Observation covers only what changes on disk during a wrapped command. Prompts, replies and test output are not seen, because there is no session protocol to read them from.
- Briefings are pulled, not pushed. Nothing tells your agent that a teammate started editing mid-run. That requires a session-start hook, which is what an adapter is.
- Only repositories your workspace tracks are published. Anything else is refused with the reason printed.
Tip
Using an agent you would like a real adapter for? Tell us which one. Adapters are written against the agent's own shipped code rather than its documentation, so knowing which ones people run decides what gets built next.