Chat-platform collaboration skill for AI agents.
Today: drive a Feishu (Lark) bot account from a Claude Code session — your agent can join group chats, talk to humans and peer agents, escalate when blocked, and stream every inbound message back into your conversation in real time over WebSocket. Tomorrow: same model, more transports (Slack, Discord — contributions welcome).
Status: alpha. The Feishu adapter is in daily use; the public API and event schema may still shift before v1.
LLM agents are good at producing output but bad at being present. Most "AI assistant" integrations are one-shot: a webhook fires, the model responds, the conversation ends. rooma flips that: the agent lives in the chat, listening to ambient context, deciding when to speak, escalating to humans when stuck. The host process is just Claude Code (or any agent harness that can tail stdout), so you get a long-lived collaborator without standing up new infrastructure.
The core primitive is a thin CLI (rooma send / pull / chats / members / ...) plus a WS monitor that emits one JSON event per stdout line. Your agent reads those events as notifications and decides what to do — no servers, no databases, no message queues.
git clone https://github.com/lesscap/rooma.git
cd rooma
python3 -m venv .venv
.venv/bin/pip install -e .
cp .env.example .env
# then edit .env with your Feishu app credentials (see "Feishu setup" below)Optional: copy config/contacts.yaml.example to config/contacts.yaml and add chat / user aliases for nicer CLI ergonomics.
You need a custom app in open.feishu.cn with bot capabilities enabled. The fastest path:
- Create a custom app in the Feishu developer console.
- Enable the Bot capability and grant these scopes:
im:message,im:message.group_msg,im:chat,im:resource,im:chat.announcement:readonly. - Enable WebSocket events (under "Event Subscriptions" → choose "Long-connection / 长连接"). Subscribe to
im.message.receive_v1. - Publish a version (apps must be published before they can connect, even in dev).
- Add the bot to at least one group you want to test in.
- Read off your credentials into
.env:FEISHU_APP_IDandFEISHU_APP_SECRETfrom the app's "Credentials" page.FEISHU_BOT_OPEN_ID— the bot's ownopen_id. Easiest way: have the bot post one message in a test chat, then callrooma pull --chat <chat_id> --limit 1and look at thesender.idfield.
Once .env is filled in, rooma chats should list the rooms the bot is in.
All commands print JSON to stdout. The shapes match Feishu's API (lightly cleaned).
# discovery
rooma chats # list every chat the bot is in
rooma members --chat <chat_id> # who's in the room
rooma chat-info --chat <chat_id> # name, description, announcement
# read & write
rooma pull --chat <chat_id> --limit 10 # latest 10 messages, oldest first
rooma send --chat <chat_id> --text "hello" # plain text
rooma send --chat <chat_id> --text "hi" --mention <ou_…> # @-mention someone
rooma file --chat <chat_id> --path /abs/path/to/file # upload + post a file
# reactions
rooma react --message-id <om_…> --emoji DONE # mark as handled
# escalation (posts a 🆘 message and @-mentions humans)
rooma help --topic "stuck on X" --detail "tried Y; need Z" --chat <chat_id>When you omit --chat, the CLI tries the FEISHU_DEFAULT_CHAT_ID env var, then falls back to "the only chat the bot is in" if there's exactly one. Aliases configured in config/contacts.yaml also work in place of a raw oc_… / ou_… id.
The CLI lets a script talk to Feishu. The monitor lets an agent listen to Feishu in real time:
.venv/bin/python -m rooma.monitorThis holds a long-lived WebSocket to Feishu and prints one JSON event per line to stdout for every inbound message. Run it as a background process from your agent (Claude Code calls Bash(run_in_background=true), then subscribes to the task's stdout via the Monitor tool); each new line arrives as a notification in the conversation. The agent reads mentioned: true|false to decide whether to respond.
Single-instance per app: the monitor takes a file lock keyed by app_id, so accidentally launching two won't split the WS stream.
The bundled Claude Code skill at .claude/skills/rooma/SKILL.md documents the full agent-side workflow (event schema, room-awareness rules, escalation patterns). If you're driving rooma from Claude Code, read that file — it's the operating manual.
rooma can post an "I'm here now" message when you start a session and a goodbye when you end it — useful so groupmates (humans and peer agents) know whether the bot is actually being watched. Three pieces fit together:
- Discovery —
rooma sync-chatsseedsdata/known_chats.jsonfrom the live Feishu chat list. After that, the monitor'sim.chat.member.bot.added_v1/_deleted_v1handlers keep the file fresh as the bot joins / leaves groups (emittingrooma.chat_joined/rooma.chat_leftevents on stdout). - On-shift — the Claude Code skill, on startup, runs
rooma chats-knownand asks you (via the AskUserQuestion picker) which chats to greet and what text to send. It then runsrooma broadcast --chat ... --text "..."and seeds the SessionEnd hook withrooma session set --chat ... --farewell-text "...". - Off-shift — a Claude Code SessionEnd hook runs
rooma session farewell, which reads the chats + farewell text fromdata/active_session.jsonand broadcasts. The agent can re-tune the farewell text mid-session withrooma session set-farewell --text "..."whenever the situation changes ("I'm signing off" vs "in a meeting" vs "back in 5").
To enable the hook, copy .claude/settings.local.json.example to .claude/settings.local.json — the example includes a hooks.SessionEnd block. Both files are git-ignored.
Direct CLI usage (if you want to script these yourself):
rooma sync-chats # seed known chats
rooma chats-known # list locally-tracked chats
rooma broadcast --chat oc_a --chat oc_b --text "hello, all" # pure multi-send
rooma session set --chat oc_a --farewell-text "🌙 see you" # arm the farewell
rooma session set-farewell --text "📞 in a meeting" # change just the text
rooma session show # inspect current state
rooma session farewell # send + clear (hook calls this)Hard crashes / kill -9 / OS shutdown don't fire the hook; the farewell silently won't go out in those cases.
- More transports. The internals today are 100% Feishu (
lark_oapiSDK calls inactions.py/monitor.py). Adding Slack / Discord / others means extracting a transport interface and writing parallel adapters. Happy to take PRs; happy to discuss the abstraction shape in an issue first. - Threading / replies as first-class events. Today every message is flat; group threads exist but aren't surfaced cleanly in the event payload.
- Persistent dedupe / replay on monitor restart (today's
_handledset is in-memory only).
Issues and PRs welcome. The codebase is small (~500 lines of Python) and deliberately function-first — no classes where a function does, no abstractions without a second consumer.
Conventions:
- Files under ~200 lines, functions under ~40.
- Comments explain why, not what.
- Tests focus on core behavior and safety boundaries, not coverage for its own sake.
MIT — see LICENSE.