npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mrciphersmith/roomyx

v0.12.2

Published

MCP server + terminal UI for startup-room: live room status, chat, per-agent modal, an installable room registry, and skill-sync.

Readme

roomyx

An MCP server and terminal UI for startup-room: it reads a room's append-only log and exposes what's happening in it — roster, goal contract, transcript, per-agent detail — both to MCP clients as tools and to a human as a live TUI. It also ships a room registry, so several rooms can run at once and be found by id, and a skill-sync path that installs the bundled startup-room skill into a project without ever silently overwriting local edits.

A live room has exactly one writer, and it is never roomyx's server. Owner commands exist — room.post_owner_command takes a veto, constraint, added participant or goal edit — but the tool only forwards them to the dispatcher running the room. That single-writer rule is what the room's consistency rests on.

roomyx room new and roomyx room append do write, and that is not an exception to the rule: creating a log nobody serves takes the writer count from zero to one.

What roomyx enforces is narrower than the rule, and it is worth being exact about which. append refuses when it can see another writer — a held writer lease, or a room reporting an attached dispatcher. An embedder that dispatches in-process is visible that way. An agent dispatcher is not: it runs roomyx serve and writes with roomyx room append, a fresh process each time, so it holds no lease and the room reports no dispatcher. In that arrangement append notes on stderr that a live room is serving the log, and proceeds.

So the single-writer rule is a discipline the dispatcher keeps, and roomyx enforces it only where it has evidence. The log itself is safe either way — seq allocation is under a lock, and three writers racing it are tested. What is at risk without the discipline is the conversation: two dispatchers produce a transcript where every message is intact and the discussion is nonsense. See D-01a, and docs/roomyx/skill-review-2026-09-11.md for why closing the gap needs a dispatcher identity that survives across processes.

Requirements

Bun 1.1 or newer must be on your PATH. roomyx ships TypeScript and runs it directly — there is no build step, so Bun is the interpreter rather than a build-time dependency.

Installing on a machine without Bun succeeds, and the commands then tell you so and point at https://bun.sh. They will not install it for you.

Install

npm install -g @mrciphersmith/roomyx
# or
bun install -g @mrciphersmith/roomyx

Quick start

cd your-project
roomyx init                                 # scaffold, then pick what to install
roomyx room new room.jsonl --goal "Pick a database"   # create a room log
roomyx serve room.jsonl                     # serve it over MCP

serve prints the room's URL and its generated id:

roomyx serving /abs/path/room.jsonl at http://127.0.0.1:4319/mcp
room ID: r-a1b2c3 — attach with `roomyx-client --room r-a1b2c3`

Then, in a second terminal, attach the TUI:

roomyx-client                # auto-attaches when exactly one room is live
roomyx-client --room r-a1b2c3

In the TUI, the transcript owns the arrows and the roster has its own keys:

| Key | | | --- | --- | | ↑ ↓ | scroll the transcript a line | | PgUp PgDn | scroll a page; Ctrl-U / Ctrl-D a half page | | g G | jump to the top, or back to following the newest message | | j k | move the roster selection | | Enter | filter the stream to the selected participant; Esc clears | | / | search, then n / N for next and previous match | | : | owner command — then veto, constraint, add participant or goal edit, type the body, Enter sends | | w | write what is on screen to a file in the working directory | | ? | the full keymap | | q Ctrl-C | quit |

Selecting a participant filters the stream where it is rather than opening a window over it, so the filter composes with scrolling and with search, and leaving it is Esc rather than a mode you have to remember you are in. A live filter or search is named in the footer — a filter that is on but invisible makes a busy room look like a quiet one.

w exists because a terminal is a bad place to search and a good place to read. It never overwrites: the next free numbered name is used, and a filtered view gets its own file.

The bottom line carries the same keys, so nothing here has to be memorised, and next to them the room's liveness: a message count and the age of the newest message, or a DISCONNECTED that stays put. Scroll away from the bottom and it tells you so, and tells you how to get back.

While the owner prompt is open it owns the keyboard, so a q in a veto is text rather than a quit.

Below 80 columns the roster gives up its gutter to the transcript and the footer carries the participant count instead; widen the terminal and the list comes back.

Up to 0.5.0 the arrows moved the roster and nothing scrolled at all, o opened the owner prompt, and Enter opened a modal window over the roster. o still works, so a habit does not break on upgrade, but : is the documented key.

Commands

The TUI ships as its own binary, roomyx-client, because it is a separate process with a lifetime independent of serve and mcp — closing it does nothing to the room. roomyx client, with a space, starts that same TUI from the single roomyx binary; it is an alias for convenience, not a second implementation, and it does not tie the TUI's lifetime to anything.

roomyx init [flags]

Sets the project up. On a terminal it shows what it can do and waits for you to pick; nothing is written until you submit.

┌─ roomyx init — pick what to set up ──────────────────────────────┐
│Skill — every project on this machine                             │
│  [x] Claude Code    replaces  /home/you/.claude/skills/startup-… │
│  [ ] Cursor         /home/you/.cursor/skills/startup-room/SKILL… │
│                                                                  │
│Skill — this project only (travels with the repo)                 │
│  [x] Claude Code    /home/you/proj/.claude/skills/startup-room/… │
│                                                                  │
│Also                                                              │
│  [x] Room-log directory         /home/you/proj/.roomyx/rooms/lo… │
│  [x] Ignore registry, lockfiles /home/you/proj/.gitignore        │
│  [x] MCP server for Claude Code /home/you/proj/.mcp.json         │
│  [ Install ]  ↵                                                  │
│  ↑/↓ or j/k move · Space toggles · Enter installs · q cancels    │
└──────────────────────────────────────────────────────────────────┘

Always, first: .roomyx/ with config.json, an empty room registry, and a staged copy of the bundled skill. It never clobbers a registry that already has rooms in it, and never overwrites an existing config.json.

Then, whatever you tick — the skill installed where your agents actually read it, the room-log directory, .gitignore lines for the machine-local state (not for the logs), and the MCP server registered over stdio.

| Flag | Meaning | | --- | --- | | --yes | Apply the default selection without asking. | | --no-interactive | Scaffold .roomyx/ only. |

Through a pipe or in CI it never prompts — it scaffolds and exits, printing what to run next. A prompt nobody is there to answer is a hang, and a hung installer is worse than one that did nothing.

Boxes start ticked only where the answer is not in doubt: a runtime whose directory exists on this machine, a project that already uses that runtime, the log directory the config has always named. A default tick must never surprise you with a file — one in ~/.cursor on a machine without Cursor would.

roomyx room new <path> [flags] / roomyx room append <path> [flags]

Creates a room log, and appends messages to one.

| Flag | Meaning | | --- | --- | | --goal <s> | Required by new. A room without a stated goal has nothing to converge on. | | --criteria <s> | The success criteria, stored in the goal contract. | | --roster id:Name,... | Participants. id is what room.get_agent_detail takes. | | --from <id> / --body <s> | Required by append. | | --kind <k> / --in-reply-to <n> | Optional message structure. | | --take-over | Write when the room is live but its writer is provably gone. |

new refuses to overwrite an existing log — it is append-only, and clobbering one loses a session.

append refuses only when it can see another writer: a held writer lease, or a room reporting an attached dispatcher. When it cannot — which is the case for an agent dispatcher running roomyx serve and appending from a separate process — it prints a note naming the room that is serving the log, and writes. There is no flag that overrides a writer that is really there; the only honest move is to stop it.

--take-over is for a room whose writer is provably gone — its server has stopped, or the lease it left has expired without being refreshed. It records the take-over in the log, naming whose lease it took.

roomyx room delta <log> --for <id> / roomyx room commands <log> / roomyx room ack <log> <id>

What a dispatcher needs from a room that is already running. All three read the room's own MCP server, so the room has to be live — they resolve its port through the registry, the same way append finds a live room.

| | | | --- | --- | | room delta <log> --for <id> | The messages that participant has not seen, never including their own. Reports since_seq and cursor_from, so an empty delta is distinguishable from a wrong cursor. --since <seq> overrides the cursor. | | room commands <log> | Owner commands waiting in the room's queue. The owner posts these from the TUI with o; nothing pushes them, so a dispatcher has to look. | | room ack <log> <id> | Marks one as dealt with, so pending keeps meaning "not yet handled" instead of growing forever. |

They exist because the bundled skill told dispatchers to call three MCP tools that nothing could reach: no command exposed them, .mcp.json registers the management server rather than a room, and a room's own server binds an ephemeral port recorded only in the registry. Measured at ~220 ms per call, against ~225 ms for the runtime to start at all — so calling one every turn costs a dispatcher nothing.

roomyx serve <logPath> [flags]

Serves one room log as an MCP server over StreamableHTTP, and registers the room in the local registry for the duration of the process. On SIGINT/SIGTERM it deregisters itself and shuts down cleanly.

| Flag | Default | Meaning | | --- | --- | --- | | --port <n> | 4319 | Listen port. 0 picks an ephemeral one. | | --host <addr> | 127.0.0.1 | Bind address. | | --acknowledge-non-loopback | off | Required to bind anything other than loopback. Without it, a non-loopback --host is refused. | | --registry <path> | .roomyx/rooms/registry.json | Registry file to register into. |

Multiple independent MCP client sessions against one serve process are supported: each new session gets its own transport and server instance, keyed by mcp-session-id.

roomyx rooms list

Lists rooms that are confirmed live, not merely present in the registry — each entry is verified with a real MCP round-trip, and entries that don't answer are pruned from the registry file as a side effect. A pruned room is also recorded in the history index at that moment — for a room that crashed, this is the only point at which anything notices it ended.

roomyx setup [flags]

Sets the machine up, without touching the current project: the same picker roomyx init shows, narrowed to the rows that land under your home directory — the skill for each agent runtime you use, and the persona library in ~/.roomyx/personas. Nothing is created in the working directory.

This is what to run right after npm install -g @mrciphersmith/roomyx. An npm lifecycle script cannot ask instead: npm runs one with no terminal (CI, npm ci, Docker builds), skips it entirely under --ignore-scripts, re-runs it on every npm update, and under npm i -g may run it as a different user. A prompt there either hangs the install or never happens — and a postinstall writing into ~/.claude is exactly what supply-chain hardening exists to stop.

| Flag | Meaning | | --- | --- | | --yes | Apply the default selection without asking. |

roomyx personas [flags]

Installs the bundled persona library into .roomyx/personas/. roomyx init offers the same thing as a ticked box.

| What | Where | | --- | --- | | 50 interview personas | 01-…md … 50-…md, one ordinary life each | | founder roles | founders/ | | engineering roles | tech/ | | judging panel | panel/ | | a 50-question interview script | questionnaire-50.md |

A room is built out of these — the startup-room skill casts its participants from this directory. It looks in .roomyx/personas/ first and falls back to ~/.roomyx/personas/, so a project copy overrides the machine-wide one — the same order every runtime uses for skills. The 50-criteria scoring rubric travels inside the skill, in reference/goal-startup-idea.md.

Every profile is in English, with the persona's name carrying its original Cyrillic in parentheses — Ngozi Adeagbo (Нгози Адеагбо). The library and the rubric were written in Russian and shipped that way until 0.12.1; the names keep both spellings so rooms cast before the translation stay findable.

| Flag | Default | Meaning | | --- | --- | --- | | --target <dir> | .roomyx/personas | Where the library goes. | | --global | off | Install for every project, into ~/.roomyx/personas. | | --force | off | Replace the library, moving the existing directory aside as .bak-<timestamp>. |

Files that already exist are skipped, never overwritten. They ship as markdown so you can edit them; an install that silently reverted your edits would be the thing this package refuses to do everywhere else.

roomyx rooms history

The rooms that have closed, newest first — the goal each had, how many people and messages, when it closed, and where its log is.

What is stored is an index, not a copy: .roomyx/rooms/history.jsonl records what a room was and where its transcript is, and the transcript itself stays exactly where you put it. A room log is append-only and is the single source of truth for what was said, so a second copy would be a second source of truth — and an append to the original would leave the two disagreeing with nothing recording which is current. The trade is that a log you move or delete is genuinely gone; the listing says so, which a shadow copy could never have told you. Reasoning: decision D-14.

A room is recorded when roomyx serve shuts down, and — for a room killed by a signal no handler runs for — when roomyx rooms list next finds it gone.

roomyx mcp [flags]

Starts the management MCP server (room listing + skill-sync). Default port 4320. See Management server below for tools and flags.

--stdio speaks MCP over stdin/stdout instead, which is what an MCP client spawns and what roomyx init registers. Over stdio there is no port and nothing left running: the client owns the process. Nothing is printed to stdout in that mode — stdout is the protocol channel.

roomyx skills sync --target <runtime|all|path> [flags]

Installs the bundled startup-room skill into a runtime's skill location. roomyx init does this for you with a picker; this is the same thing for a script.

| Target | Path | | --- | --- | | claude | ~/.claude/skills/startup-room/SKILL.md | | codex | ~/.codex/skills/startup-room/SKILL.md | | cursor | ~/.cursor/skills/startup-room/SKILL.md | | grok | ~/.grok/skills/startup-room/SKILL.md | | claude-project … grok-project | the same, under <cwd>/ instead of ~/ | | keryx | <cwd>/.metaproject/project-skills/startup-room/SKILL.md | | all | every row above | | anything else | taken as a literal path |

Project-scoped locations rank higher than user-scoped ones in every one of these runtimes, and they travel with the repository — which is usually what a team wants.

| Flag | Default | Meaning | | --- | --- | --- | | --yes | off | Actually write. Without it nothing is written — the run reports what it would do and stops. | | --dry-run | — | Says the same thing explicitly. Redundant unless paired with --yes, which it overrides. | | --config <path> | .roomyx/config.json | Where the last-synced hashes are recorded. |

Requires roomyx init to have run, since that is what creates the config the sync records into.

This copies over the target; it does not merge into it. If you already maintain a fuller startup-room skill, fold the bundled text into it by hand rather than pointing --yes at it. See Skill sync safety below for what roomyx refuses outright.

roomyx-client [flags]

The terminal UI. roomyx client (space, not hyphen) is the same entry — an alias so a single roomyx binary can attach without a second command on PATH. roomyx-client remains the dedicated binary: a separate process, independent lifetime from serve/mcp.

| Flag | Meaning | | --- | --- | | --room <id> | Attach to a specific live room by id. | | --connect <url> | Attach to an explicit MCP URL. Wins outright; the registry is not consulted. | | --registry <path> | Registry file to resolve rooms from. | | --archive | Pick a closed room from the history index and open it read-only. | | --open <logPath> | Reread one closed room directly, read-only. No server needed. |

With no flags it auto-attaches when exactly one room is live, and refuses with a list of candidate ids when more than one is.

Read-only mode. --open and --archive load a finished log once — no server, no polling, no reconnect. Scrolling, per-participant filtering, search, export and help behave exactly as in a live room; the owner command does not work, and the footer does not print its key, because there is nothing to send it to.

MCP tools

Per-room server — roomyx serve

| Tool | Input | Returns | | --- | --- | --- | | room.get_state | — | Roster and current goal contract. | | room.get_transcript | since_seq: number | Messages with seq greater than since_seq, in order. | | room.get_agent_detail | agent_id: string | One participant's own messages and last-seen status. | | room.post_owner_command | kind: veto \| constraint \| add_participant \| goal_edit, body: string | { accepted, reason? } from the dispatcher. |

room.post_owner_command forwards; it never writes. A dispatcher supplies a handler when it embeds the server (onOwnerCommand in serve()). A room served by bare roomyx serve has no dispatcher, so the tool answers accepted: false and says so — better than accepting a command nothing will act on.

Management server

A second, standalone MCP server exposing the installer surface — room listing and skill-sync — to any MCP client.

| Tool | Input | Returns | | --- | --- | --- | | roomyx.rooms.list | — | Live, liveness-checked rooms. | | roomyx.skills.sync | target: claude \| codex \| keryx \| all, dryRun?: boolean, yes?: boolean | One result per target: whether it would write, whether it did, and where the backup went. |

Named targets only, deliberately. This tool once accepted a literal targetPath, which made it an arbitrary-path file writer on an unauthenticated loopback port — a page in any browser tab could drive it cross-origin. The CLI keeps its literal-path escape hatch, because that is the operator on their own machine. The network surface does not get one. keryx resolves project-locally against the server's cwd option, which defaults to the process's.

Start it with roomyx mcp (default port 4320, so it does not collide with roomyx serve's 4319). Same loopback / --acknowledge-non-loopback rules as serve. It is also a library entry point (serveManagement in src/mcp-management/server.ts) for embedding hosts to bind.

| Flag | Default | Meaning | | --- | --- | --- | | --port <n> | 4320 | Listen port. 0 picks an ephemeral one. | | --host <addr> | 127.0.0.1 | Bind address. | | --acknowledge-non-loopback | off | Required to bind anything other than loopback. | | --registry <path> | .roomyx/rooms/registry.json | Registry file to list rooms from. | | --config <path> | .roomyx/config.json | Config file skill-sync records hashes in. |

Skill sync safety

Neither roomyx skills sync nor roomyx.skills.sync will silently discard local work. Both refuse to write, and report a warning instead, in two cases — unless --yes / yes: true is passed:

  • the target has changed since roomyx last synced it (hand-edited), or
  • the target already exists but roomyx has no record of ever having written it, so its content is not roomyx's to overwrite.

When it does write, it first copies the previous content to a timestamped .bak-<iso> file alongside the target, and records the new content's hash in .roomyx/config.json under lastSyncedHashes.

On-disk layout

.roomyx/
├── config.json                     # schemaVersion, defaultPort, roomLogDir, lastSyncedHashes
├── rooms/
│   └── registry.json               # live room registry
└── skills/
    └── startup-room/SKILL.md       # staged copy of the bundled skill

config.json's roomLogDir (default .roomyx/rooms/logs) is a convention for where room logs are expected to live; init records it but does not create the directory, and serve takes whatever path you hand it.

The registry is written under an exclusive lockfile and replaced by write-to-temp-then-rename, so concurrent serve processes can't lose each other's registrations or be read mid-write. A lock whose holder crashed is reclaimed after 30 seconds, and a release only unlinks the lockfile when it still holds the token that call wrote.

Room log format (JSONL)

One JSON object per line (\n-terminated). The first line is a state record; every subsequent line is a message record.

{"type":"state","goal_contract":{"version":1,"goal_statement":"...","criteria":"...","threshold":{"fail_below":300,"pass_at_or_above":325}},"roster":[{"id":"yuki","name":"Юки"}]}
{"type":"message","seq":1,"from":"yuki","body":"..."}
{"type":"message","seq":2,"from":"omar","in_reply_to":1,"kind":"challenge","body":"..."}
  • state.goal_contract matches the goal-contract schema from the startup-room framework's specification (arena project).
  • Each message record matches that framework's message-envelope schema (plus the literal "type":"message" discriminator, which is a log-format detail, not part of the envelope schema itself).
  • roster entries are { id, name } — id is what room.get_agent_detail takes as agent_id; name is the display name used in from.

Edits: how a room's state changes

The first line is a room's original state and it is never rewritten — the log is append-only, and it cannot be followed by a second state line either, because every line after the first must be a message. So an edit is a message:

{"type":"message","seq":5,"from":"owner","kind":"goal_edit","body":"raise the pass mark to 85","change":{"type":"goal_contract","goal_contract":{"version":1,"goal_statement":"...","criteria":"...","threshold":{"fail_below":60,"pass_at_or_above":85}}}}
{"type":"message","seq":6,"from":"owner","kind":"add_participant","body":"adding Omar","change":{"type":"roster","add":[{"id":"omar","name":"Омар"}]}}
  • change is optional, so every message written by an earlier version still validates. It is present only on the two edit kinds, and its type must match the kind: goal_edit carries a goal_contract, add_participant carries roster.
  • What a room reads as is its first line folded with every edit, in seq order, last one winning. That is what room.get_state returns. The file keeps the original contract — the fold is an interpretation, never a rewrite — so a transcript stays exactly as it was written.
  • An edit is also an ordinary message, so it appears in the transcript: this project puts state changes in the transcript as text precisely so a person can see what happened, rather than having it land in a record nobody reads.
  • A goal_edit with no change is a valid message that simply is not an edit — the fold ignores it, and nothing about it is an error. Whether an edit kind should carry its change is a rule about applying a message, not about the message being well-formed, and an ill-formed line would make a room unreadable forever (the log has no repair command). A change whose type does not match its kind is ill-formed, and the writer refuses it.
  • The body of an edit is prose ("raise the pass mark to 85"), not JSON: the transcript is what a person reads, and the structure travels beside it in change.

This format is this package's own design choice, not dictated by either JSON Schema: the specification defers the on-disk representation to implementation, and the schemas describe one message and one goal contract, not the log file as a whole. It is documented here because the tests in this package assume it.

Development

bun install
bun run check      # lint + typecheck + test

Releases are cut by pushing a v* tag whose version matches package.json; the release workflow verifies, packs, smoke-tests, and publishes to npm with provenance via trusted publishing.

Further reading

  • docs/roomyx/testing-story.md — a verified end-to-end walkthrough, from npm install to driving a live room, including what keryx shell can and cannot do with roomyx.

Full architecture and the reasoning behind each decision:

License

MIT

Soak testing

bun run soak                    # ~2 min, about 3 hours of a live room
bun scripts/soak.ts --polls 20000   # ~7 min, about a working day

The three worst defects this project has shipped were invisible to a unit test by construction: an idle client that exhausted the native renderable pool after about 2.7 hours, a server that retained every session it ever accepted, and one malformed log line that took a client from one connection to 510 in ten seconds. Each is a function of volume over time, and the suite's longest test runs forty seconds and asserts a state.

scripts/soak.ts compresses the clock by poll count rather than by wall time — the 2.7-hour death is 3275 state polls, which at a 20 ms interval is sixty-five seconds — and drives a real ChatView and a real RoomClient against a real roomyx serve, through a proxy that counts every request. It reports trends, not thresholds: a threshold has to be guessed and is wrong on someone else's machine, while "the renderable count grew with the transcript and not with the poll loop" is true or false everywhere.

It is not in bun test on purpose. It takes minutes, and a suite people skip is worse than one they run.