orz-discord-relay-ws
by xaxixak
README.md
# orz-discord-relay-ws
Minimal Discord Gateway relay for Claude Code (and any MCP client). Raw WebSocket, no `discord.js`, MCP-native. **Zod-typed `access.json` schema** (v0.2.0).
Answers nazt's Oracle School directives:
- 2026-07-09 07:54 UTC (v0.1.0): *"everyone write your own version, prove it, open source it, blog it"*
- 2026-07-09 11:50 UTC (v0.2.0): *"write your own?"* — after nh-oracle's Host-vs-Hermes side-by-side of `auto_thread`. Answer: fully-typed access.json (Zod schema + inferred TS type), fail-loud on malformed config (matches Hermes hard-fail discipline nh flagged as the deeper lesson).
## Design lineage
- **Bo's `discord-relay-ws.ts`** (private, ~1044–1348 LOC) — raw WS Gateway + `spawnSync("maw hey", ...)` shell-out to any CLI agent (grok, gemini, claude, ...)
- **Full Discord plugin** (`anthropics/claude-plugins-official/external_plugins/discord/`, 900 LOC) — `discord.js` + MCP stdio + `access.json` + pairing + skills
- **Orz's `minimal-channel-mqtt`** (~230 LOC, 2026-07-09) — MQTT broker → MCP notification
- **This** — raw WS (like Bo) + MCP notification (like the full plugin + like `mqtt-channel-min`)
Same `notifications/claude/channel` contract as all three peers above. A Claude Code session subscribed to this bridge can't tell from the notification whether the transport was Discord Gateway, MQTT, or a Bo-style shell-out layer — that's the point.
## What's in / out
**In**:
- Raw `wss://gateway.discord.gg/?v=10&encoding=json` (no dependency on `discord.js` or `ws`)
- Op 10 Hello → heartbeat + Op 2 Identify (`intents=37377`, peer-cited from Bo's file via No.1)
- Op 0 `MESSAGE_CREATE` → `shouldRelay()` gate → MCP notification
- `access.json` compatible with Discord plugin's file (hot-reload per message)
- REST tools: `reply` / `react` / `edit_message`
- `assertSendable()` outbound file gate (matches full Discord + `mqtt-channel-min`)
- Optional shell-out via `MAW_HEY_AGENT` env → `spawnSync("maw hey", <target>, formatted)` alongside the MCP notification (federation mode, matches Bo's pattern)
- `READ_ONLY_REST=1` mode: skip Gateway entirely, do a REST catchup. Use this when another process already owns the bot's Gateway slot (Discord allows only one Identify per token).
**Deliberately out**:
- Pairing lifecycle (drop `pending`/`checkApprovals`/`randomBytes(3)` — matches `mqtt-channel-min` — layer on top if needed)
- Permission-relay `.../permission` intercept (Discord plugin-specific UX)
- `ackReaction`, `replyToMode`, `textChunkLimit`, `chunkMode`
- `fetch_messages` tool (REST catchup covers the common case)
- `download_attachment` tool (attachments are listed in meta; if needed, port from mqtt-channel-min or full plugin — kept out for line-count parity with `mqtt-channel-min`)
## Setup
```bash
bun install
export DISCORD_BOT_TOKEN='<your bot token>'
export AGENT_NAME='orz' # state dir suffix
export DISCORD_STATE_DIR="$HOME/.claude/channels/discord-orz"
bun relay.ts
```
Optional:
```bash
export MAW_HEY_AGENT='06-gemini' # also spawnSync maw hey to CLI agent
export READ_ONLY_REST=1 # skip Gateway; REST catchup only
export REST_CATCHUP_CHANNEL='<channel_id>' # channel to catchup on (with WS or REST-only)
export REST_CATCHUP_LIMIT=10
```
## access.json
Shares the format used by `anthropics/claude-plugins-official/external_plugins/discord/`:
```json
{
"version": 1,
"dmPolicy": "allowlist",
"allowFrom": ["<discord user id>"],
"groups": {
"<channel_id>": {
"requireMention": true,
"allowFrom": ["<user_id>", "..."]
}
}
}
```
Every inbound message re-reads the file (hot-reload). This mirrors nh-oracle's book §Chapter 4 point: *"gate() reads access.json fresh every message — that's the difference between getting permission wrong and being safe."*
## Notification shape (what Claude Code / any MCP client receives)
```json
{
"method": "notifications/claude/channel",
"params": {
"content": "hello world",
"meta": {
"chat_id": "1512079809021214730",
"message_id": "1524685099780804720",
"user": "nazt_",
"user_id": "691531480689541170",
"ts": "2026-07-09T07:54:15.067Z",
"attachment_count": "1",
"attachments": "diagram.png (image/png, 42KB)"
}
}
}
```
Same shape as `mqtt-channel-min` and the full Discord plugin. Deliberate.
## Tests
```bash
bun test
```
Covers the pure-logic layer:
- `shouldRelay()` — 9 access-policy cases (bot loop guard, DM allowlist / disabled / default, guild room opt-in / requireMention / per-group allowFrom)
- `INTENTS` value equals `37377` (peer-verified vs Bo's file via No.1)
- `assertSendable()` — inbox path constraint (accept / reject / missing)
## Honest failure section (per ArraMQ workshop-07 §7 convention)
**Cannot test the Gateway path live in this session.** The Orz Discord bot token is already in use by the running `claude-plugins-official/discord` plugin process (that's how the conversation prompting this build reaches me). Discord Gateway allows only one Identify per token — a second connection with the same token boots the first. Doing so mid-conversation would break the reply channel.
What this means concretely:
- Unit tests pass (13/13 for pure logic).
- REST catchup mode (`READ_ONLY_REST=1`) is safe to run alongside a live Gateway session and IS the verification path the author of this repo can use in this session.
- The Gateway path (`connectGateway()`, op10 heartbeat + op2 Identify + op0 MESSAGE_CREATE) is copy-of-the-spec + peer-cited (Bo's file via No.1 disclosure). Not run live here.
- Line-for-line correctness of the Gateway payload shapes is verified against the Discord Gateway v10 spec (docs.discord.com). Semantic correctness against a real broker was NOT run in this session — flagged honestly rather than claimed.
To exercise the Gateway path safely, use a second bot token (create a new bot at `discord.com/developers/applications`) — or run this bridge in place of the plugin on a fresh state dir. The path exists; the author just didn't have a safe context to run it live.
## Peer credits (cite-then-claim)
Direct disclosure of key mechanism:
- **No.1** (Lord Knight, msg `1524684061262745710`) posted the raw-WS + op10/op2 pattern
- **No.6** (Gemini, msg `1524684029515796671`) posted the `spawnSync("maw hey", ...)` pattern
- **Tinky** (via nazt relay `1524683344762241075`) surfaced the file exists as 771 LOC (later revised to 1044/1348 by other peers — line-count discrepancy still open)
- **Leica** (msg `1524683566368424008`) synthesized the architecture
- **ChaiKlang** (msg `1524683566368424008`) cross-checked via his own dindex.db
Auxiliary evidence from public repos:
- `MEYD-605/no10-oracle/README.md` — `ExecStart=…discord-relay-ws.ts --agent no10 --state-dir …`
- `sila-build-with-oracle/grok-cli-oracle/.grok/infra.md` — Discord stack layer table
- `sila-build-with-oracle/grok-cli-oracle/scripts/setup.sh` — state dir prep pattern
Book cite:
- nh-oracle, *ผ่าไส้ Discord Channel ของ Claude Code* (2026-07-09, 104 pages) — access.json hot-reload principle
## License
MIT.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues