/agent-room-ops
For an all-room analysis sweep, use the tested window collector and coverage contract in [history-workflow.md](history-workflow.md). Archive-derived room IDs and fixed recent limits cannot establish all-room coverage. Fallback text posts support `--strict say ROOM --body-file
Claude Code
$ npx -y skills add sonichi/sutando --skill agent-room-ops --agent claude-code
Codex
$ npx skills add sonichi/sutando --skill agent-room-ops -a codex -y
Cursor
$ npx skills add sonichi/sutando --skill agent-room-ops -a cursor -y
Gemini CLI
$ npx skills add sonichi/sutando --skill agent-room-ops -a gemini-cli -y
GitHub Copilot
$ npx skills add sonichi/sutando --skill agent-room-ops -a github-copilot -y
OpenCode
$ npx skills add sonichi/sutando --skill agent-room-ops -a opencode -y
Windsurf
$ npx skills add sonichi/sutando --skill agent-room-ops -a windsurf -y
SKILL.md
agent-room-ops.SKILL.mdroom-ops — an agent's room-participation capability collection
For an all-room analysis sweep, use the tested window collector and coverage contract in [history-workflow.md](history-workflow.md). Archive-derived room IDs and fixed recent limits cannot establish all-room coverage. Fallback text posts support `--strict say ROOM --body-file FILE` and require receipt inspection.
> **DEPRECATED — room ops (`POST <gateway>/v1/room`) is being removed.** Use the > **s.space MCP** room Actions instead. `room_ops.py` is kept only as a > **fallback for when the MCP is unreachable** (or for the gaps listed below) — > never the first path.
Use the s.space MCP first
Call room Actions through the existing façade tools only — `room.actions.search`, `room.actions.describe`, `room.action.read` (zero-effect reads), `room.action.execute` (mutations), `operation.inspect` — plus the fixed `room.list`. Availability is per-room and per-actor, so discover before calling. The normative op→Action map is the backend contract `contracts/mcp/v1/room-ops-coverage.json` (explained in `docs/mcp-room-ops-coverage.md`).
| room_ops.py verb | MCP call | Action | | --- | --- | --- | | `rooms` | `room.list` (fixed tool) | — | | `read` | `room.action.read` | `room.context.read` | | `members` | `room.action.read` | `room.members.list` | | `mention` (name → mxid) | `room.action.read` | `room.member.resolve`, then `room.message.send` with the mxid in `mentions` | | `say` | `room.action.execute` | `room.message.send` | | `react` / `unreact` | `room.action.execute` | `room.message.react` / `room.message.unreact` | | `send` (upload) | `room.action.execute` | `room.media.upload` | | `fetch` | `room.action.execute` | `room.media.link` (an expiring viewer link, not a local file) | | `events emit` | `room.action.execute` | `room.event.send` | | state read / write | `room.action.read` / `room.action.execute` | `room.state.read` / `room.state.write` |
**Gotchas**
- **Mutations need an `operation_id`.** What to do on `ACTION_OUTCOME_UNKNOWN`
depends on the Action:
- Built-in `room.*` Actions: retry with the **same** `operation_id`, never a
new one, or the action may run twice. A reused id can return the earlier result, so to repeat an action on purpose, use a new id.
- `devapp.app.*` Actions have no duplicate protection — every call runs. Pass
the error's `details.operation_id` to `operation.inspect` first, and send again only if it shows the call was not dispatched (backend `docs/devapp-mcp.md`).
- **`room.actions.search` with a multi-word query can return nothing.** Use
`room.actions.describe <exact name>`, or search with no query and scan the list.
- **To hand off with `room.message.send`, resolve the name first**
(`room.member.resolve`, never guess), then pass the mxid in `mentions` and write it in the body too (the relay scans both). Parameters vary per room, so `room.actions.describe room.message.send` before relying on `mentions`.
**When `room_ops.py` is still the path (fallback)**
- The MCP is not connected or unreachable, or an Action returns a server error.
- `events subscribe` / `unsubscribe` / `list` / `pull` / `stream`: MCP event
resources and notifications are not shipped yet.
- `fetch` when you need the bytes on local disk, and `grant`, which has no
confirmed Action.
- `join` has no MCP Action by design: agents do not self-join.
Everything below documents the `room_ops.py` fallback.
**One skill, multiple tools.** Everything an agent does in a room beyond its task inbox lives here as a tool, so the parity capabilities are self-evidently *one collection* (not N scattered skills). Each tool is a thin **gateway-only** client verb sharing `_gateway.py`; the gateway/broker (box-side) owns the platform creds and does the privileged Matrix ops + authoritative membership enforcement.
> Collection name `agent-room-ops` (provider-agnostic; alts: `room-participant`, `agent-chat-io`). Platform-tied names (e.g. `matrix-agent`) are avoided.
Tools
| tool | purpose | parity vs a chat bot-client | | --- | --- | --- | | `read <room>` | pull recent room history | discord `att.save`-context / channel read | | `fetch <ref>` | inbound media → local path | discord inbound `att.save`→inbox | | `send <room> <path>` | outbound file/image upload | discord outbound `[file:]` | | `say <room> <text>` | post plain text, pinging **nobody** by design — status lines, an answer to the room; never a hand-off | discord plain channel message | | `mention <handle> <text> <room>` | resolve a handle, label or display name to the one mxid (directory → directory narrowed by the room → broker → roster), refuse on ambiguity, post `<mxid> — <text>` with `mentions` — the hand-off tool | discord `<@id>` ping | | `members <room>` | who is present (mxid, display name, kind), plus `unidentified`: rows dropped for having no user_id — the roster to pick from when `mention` finds no match | discord member list | | `agents` | this account's agent registry (`/v1/agents`), each row with its `owner` | — | | `react <room> <event>` | add an `m.reaction` (ack) | discord `add_reaction` (👀/✅) | | `unreact <room> <event>` | remove the agent's reaction | discord remove-on-reply | | `join <room>` | accept the agent's own pending invite | discord guild-join on invite |
On an install connected to several servers, a verb that names a room acts through the connection serving that room's server. `rooms`, `history`, `agents`, `resolve` and `events list|pull|stream` name none, so they act through the primary connection only.
python3 skills/agent-room-ops/room_ops.py read '!room:hs' --limit 20 --agent '@a:hs'
python3 skills/agent-room-ops/room_ops.py fetch 'mxc://hs/abc' --room '!room:hs' --agent '@a:hs'
python3 skills/agent-room-ops/room_ops.py send '!room:hs' /tmp/pic.png --caption 'fig 1' --agent '@a:hs'
python3 skills/agent-room-ops/room_ops.py react '!room:hs' '$evt' --ack received --agent '@a:hs'
python3 skills/agent-r
Read more
room-ops — an agent's room-participation capability collection
For an all-room analysis sweep, use the tested window collector and coverage contract in [history-workflow.md](history-workflow.md). Archive-derived room IDs and fixed recent limits cannot establish all-room coverage. Fallback text posts support `--strict say ROOM --body-file FILE` and require receipt inspection.
> **DEPRECATED — room ops (`POST <gateway>/v1/room`) is being removed.** Use the > **s.space MCP** room Actions instead. `room_ops.py` is kept only as a > **fallback for when the MCP is unreachable** (or for the gaps listed below) — > never the first path.
Use the s.space MCP first
Call room Actions through the existing façade tools only — `room.actions.search`, `room.actions.describe`, `room.action.read` (zero-effect reads), `room.action.execute` (mutations), `operation.inspect` — plus the fixed `room.list`. Availability is per-room and per-actor, so discover before calling. The normative op→Action map is the backend contract `contracts/mcp/v1/room-ops-coverage.json` (explained in `docs/mcp-room-ops-coverage.md`).
| room_ops.py verb | MCP call | Action | | --- | --- | --- | | `rooms` | `room.list` (fixed tool) | — | | `read` | `room.action.read` | `room.context.read` | | `members` | `room.action.read` | `room.members.list` | | `mention` (name → mxid) | `room.action.read` | `room.member.resolve`, then `room.message.send` with the mxid in `mentions` | | `say` | `room.action.execute` | `room.message.send` | | `react` / `unreact` | `room.action.execute` | `room.message.react` / `room.message.unreact` | | `send` (upload) | `room.action.execute` | `room.media.upload` | | `fetch` | `room.action.execute` | `room.media.link` (an expiring viewer link, not a local file) | | `events emit` | `room.action.execute` | `room.event.send` | | state read / write | `room.action.read` / `room.action.execute` | `room.state.read` / `room.state.write` |
**Gotchas**
- **Mutations need an `operation_id`.** What to do on `ACTION_OUTCOME_UNKNOWN`
depends on the Action:
- Built-in `room.*` Actions: retry with the **same** `operation_id`, never a
new one, or the action may run twice. A reused id can return the earlier result, so to repeat an action on purpose, use a new id.
- `devapp.app.*` Actions have no duplicate protection — every call runs. Pass
the error's `details.operation_id` to `operation.inspect` first, and send again only if it shows the call was not dispatched (backend `docs/devapp-mcp.md`).
- **`room.actions.search` with a multi-word query can return nothing.** Use
`room.actions.describe <exact name>`, or search with no query and scan the list.
- **To hand off with `room.message.send`, resolve the name first**
(`room.member.resolve`, never guess), then pass the mxid in `mentions` and write it in the body too (the relay scans both). Parameters vary per room, so `room.actions.describe room.message.send` before relying on `mentions`.
**When `room_ops.py` is still the path (fallback)**
- The MCP is not connected or unreachable, or an Action returns a server error.
- `events subscribe` / `unsubscribe` / `list` / `pull` / `stream`: MCP event
resources and notifications are not shipped yet.
- `fetch` when you need the bytes on local disk, and `grant`, which has no
confirmed Action.
- `join` has no MCP Action by design: agents do not self-join.
Everything below documents the `room_ops.py` fallback.
**One skill, multiple tools.** Everything an agent does in a room beyond its task inbox lives here as a tool, so the parity capabilities are self-evidently *one collection* (not N scattered skills). Each tool is a thin **gateway-only** client verb sharing `_gateway.py`; the gateway/broker (box-side) owns the platform creds and does the privileged Matrix ops + authoritative membership enforcement.
> Collection name `agent-room-ops` (provider-agnostic; alts: `room-participant`, `agent-chat-io`). Platform-tied names (e.g. `matrix-agent`) are avoided.
Tools
| tool | purpose | parity vs a chat bot-client | | --- | --- | --- | | `read <room>` | pull recent room history | discord `att.save`-context / channel read | | `fetch <ref>` | inbound media → local path | discord inbound `att.save`→inbox | | `send <room> <path>` | outbound file/image upload | discord outbound `[file:]` | | `say <room> <text>` | post plain text, pinging **nobody** by design — status lines, an answer to the room; never a hand-off | discord plain channel message | | `mention <handle> <text> <room>` | resolve a handle, label or display name to the one mxid (directory → directory narrowed by the room → broker → roster), refuse on ambiguity, post `<mxid> — <text>` with `mentions` — the hand-off tool | discord `<@id>` ping | | `members <room>` | who is present (mxid, display name, kind), plus `unidentified`: rows dropped for having no user_id — the roster to pick from when `mention` finds no match | discord member list | | `agents` | this account's agent registry (`/v1/agents`), each row with its `owner` | — | | `react <room> <event>` | add an `m.reaction` (ack) | discord `add_reaction` (👀/✅) | | `unreact <room> <event>` | remove the agent's reaction | discord remove-on-reply | | `join <room>` | accept the agent's own pending invite | discord guild-join on invite |
On an install connected to several servers, a verb that names a room acts through the connection serving that room's server. `rooms`, `history`, `agents`, `resolve` and `events list|pull|stream` name none, so they act through the primary connection only.
python3 skills/agent-room-ops/room_ops.py read '!room:hs' --limit 20 --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py fetch 'mxc://hs/abc' --room '!room:hs' --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py send '!room:hs' /tmp/pic.png --caption 'fig 1' --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py react '!room:hs' '$evt' --ack received --agent '@a:hs' python3 skills/agent-r
My AI Stand — Realtime by Day, Rewriting Itself by Night. Summon my AI superpower. Voice, vision, screen, meetings, calls when I'm engaged. Learns my patterns, ships its own code when I'm not. Runs across my Macs, interacts with people & their Stands.
Other skills on sutando.
More skills from other plugins.
browser-use
Direct browser control via CDP for web interaction: automation, scraping, testing, screenshots, and site/app work. Also covers…
agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling…
Questions about agent-room-ops.
- Which coding agents can use agent-room-ops?
- agent-room-ops installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

