feat(rkd): RKD channel plugin for Claude Code (#917) #1

Merged
dozken merged 2 commits from feat/917-rkd-channel into main 2026-09-19 18:55:38 +02:00
Owner

Summary

Phase 0 of nanoya-biz/rkd#917: the rkd Claude Code channel plugin, so a paired RKD user can @mention or DM a bot account and drive a Claude Code session on the operator's laptop. Replies land in the same thread, and tool use is approved from chat. No RKD server or client change: the bot is an ordinary user account.

Part of nanoya-biz/rkd#917 (phases 0.5 and 1 remain open there).

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup (no user-visible change)
  • UI / UX
  • Build, CI, or tooling
  • Docs

Test plan

Automated:

  • pnpm test passes locally (from rkd/): 77 unit tests covering the trigger gate, pairing, access.json, the refresh rotation (single-flight, persist-before-use, device id), the bridge (threading, chunking, edit throttle and edit-window fallback, outbound allowlist, credential-upload refusal, permission relay routing) and the version floor
  • pnpm typecheck and pnpm check:dist pass
  • Integration test against a real rkd-server (main at 2ae81d2b, own database on :18090): RKD_TEST_SERVER=http://127.0.0.1:18090 pnpm test:integration, 8/8. It drives dist/server.js through the MCP SDK client the way Claude Code does: the owner's @mention arrives while plain chatter and a stranger's @mention don't; reply lands threaded (reply_to), then edit and react; a permission prompt is posted in the thread and only the owner's yes <id> approves (the stranger's is ignored); an unpaired DM gets a pairing code; known-channels.json lists the channel
  • Mutation check: with the unpaired-sender drop removed from the gate, the integration test goes red; with it restored, green
  • claude plugin validate --strict passes for the marketplace and the plugin
  • The bundle boots and answers initialize / tools/list / status on Node 24 and Node 20 (scripts/smoke-handshake.mjs)

Manual:

  • Real Claude Code 2.1.278 with --plugin-dir rkd: the model sees all five tools (mcp__plugin_rkd_rkd__reply, edit_message, react, download_attachment, status) and both skills, and status reports the client version. This answers #917's open question about reply not reaching the model
  • Interactive session with --dangerously-load-development-channels: channel messages reaching the model and Claude Code's own permission prompts relaying. Not run: it needs the dev-channel consent dialog confirmed by a person. Steps are in rkd/README.md under "Manual smoke"

Review checklist

  • No console.log / dbg! / commented-out code left behind (the login CLI prints to the terminal on purpose)
  • No secrets, tokens, or personal URLs added
  • New dependencies justified: @modelcontextprotocol/sdk 1.27.1 (the version the Telegram channel runs), ws (Node 20 has no global WebSocket), zod (the SDK's notification schemas). All are bundled into dist/server.js, so nothing is installed on a user's machine
  • README updated

Notes for the reviewer

Deliberate differences from the issue text:

  • Password never goes through Claude. #917 has /rkd:configure <server> <email> <password>, but a skill argument ends up in the session transcript. Instead the operator runs node <plugin>/dist/server.js login <server> <email> in their own terminal with a hidden prompt, and /rkd:configure prints that line. Only the refresh token and device id are stored.
  • DM policy defaults to pairing, not allowlist, as in the Telegram plugin. Otherwise the operator has no way to find out their own user id. /rkd:configure pushes to allowlist once they're paired, and a stranger can hold at most 3 pending codes (1 h expiry), answered twice at most.
  • Channels are opt-in (/rkd:access channel add dev). The bot subscribes to every text channel it can see, because the server only fans MessageCreate out to explicit subscribers, but it drops everything outside the configured ones before Claude reads it. It writes known-channels.json so the skill can resolve names.

Worth a hard look:

  • src/triggers.ts: the prompt-injection boundary. Approvals come only from the global allowFrom, never from a channel-level allowFrom.
  • src/rkd-api.ts: refresh rotation. The server revokes the whole chain on a replayed token (and on a device-id mismatch), so the token is read only from .env, never from the environment, where a stale copy could be replayed on the next start.
  • Permission prompts go to the thread of the last delivered trigger, so everyone in that channel sees the command preview (as #917 intends). With no trigger yet, or if that channel has since been removed, they go by DM to each paired user.

Open:

  • CI (.forgejo/workflows/ci.yml) is the first workflow in the rkd org. I couldn't see whether that org's repos get a runner, so this PR may show no checks. The integration test needs a live server and is a local gate only for now.
  • No LICENSE yet. The repo is public, and package.json says UNLICENSED until someone decides.
  • The bot account itself (claude-code-bot on dev/prod) isn't created. That's the operator's step in the README.

https://claude.ai/code/session_01GdkP2Y3vcxcGz5XCFFWW16

## Summary Phase 0 of nanoya-biz/rkd#917: the `rkd` Claude Code channel plugin, so a paired RKD user can @mention or DM a bot account and drive a Claude Code session on the operator's laptop. Replies land in the same thread, and tool use is approved from chat. No RKD server or client change: the bot is an ordinary user account. Part of nanoya-biz/rkd#917 (phases 0.5 and 1 remain open there). ## Type of change - [ ] Bug fix - [x] New feature - [ ] Refactor / cleanup (no user-visible change) - [ ] UI / UX - [x] Build, CI, or tooling - [x] Docs ## Test plan Automated: - [x] `pnpm test` passes locally (from `rkd/`): 77 unit tests covering the trigger gate, pairing, access.json, the refresh rotation (single-flight, persist-before-use, device id), the bridge (threading, chunking, edit throttle and edit-window fallback, outbound allowlist, credential-upload refusal, permission relay routing) and the version floor - [x] `pnpm typecheck` and `pnpm check:dist` pass - [x] Integration test against a real `rkd-server` (main at 2ae81d2b, own database on :18090): `RKD_TEST_SERVER=http://127.0.0.1:18090 pnpm test:integration`, 8/8. It drives `dist/server.js` through the MCP SDK client the way Claude Code does: the owner's @mention arrives while plain chatter and a stranger's @mention don't; reply lands threaded (`reply_to`), then edit and react; a permission prompt is posted in the thread and only the owner's `yes <id>` approves (the stranger's is ignored); an unpaired DM gets a pairing code; `known-channels.json` lists the channel - [x] Mutation check: with the unpaired-sender drop removed from the gate, the integration test goes red; with it restored, green - [x] `claude plugin validate --strict` passes for the marketplace and the plugin - [x] The bundle boots and answers `initialize` / `tools/list` / `status` on Node 24 and Node 20 (`scripts/smoke-handshake.mjs`) Manual: - [x] Real Claude Code 2.1.278 with `--plugin-dir rkd`: the model sees all five tools (`mcp__plugin_rkd_rkd__reply`, `edit_message`, `react`, `download_attachment`, `status`) and both skills, and `status` reports the client version. This answers #917's open question about `reply` not reaching the model - [ ] Interactive session with `--dangerously-load-development-channels`: channel messages reaching the model and Claude Code's own permission prompts relaying. Not run: it needs the dev-channel consent dialog confirmed by a person. Steps are in `rkd/README.md` under "Manual smoke" ## Review checklist - [x] No `console.log` / `dbg!` / commented-out code left behind (the `login` CLI prints to the terminal on purpose) - [x] No secrets, tokens, or personal URLs added - [x] New dependencies justified: `@modelcontextprotocol/sdk` 1.27.1 (the version the Telegram channel runs), `ws` (Node 20 has no global WebSocket), `zod` (the SDK's notification schemas). All are bundled into `dist/server.js`, so nothing is installed on a user's machine - [x] README updated ## Notes for the reviewer Deliberate differences from the issue text: - **Password never goes through Claude.** #917 has `/rkd:configure <server> <email> <password>`, but a skill argument ends up in the session transcript. Instead the operator runs `node <plugin>/dist/server.js login <server> <email>` in their own terminal with a hidden prompt, and `/rkd:configure` prints that line. Only the refresh token and device id are stored. - **DM policy defaults to `pairing`, not `allowlist`**, as in the Telegram plugin. Otherwise the operator has no way to find out their own user id. `/rkd:configure` pushes to `allowlist` once they're paired, and a stranger can hold at most 3 pending codes (1 h expiry), answered twice at most. - **Channels are opt-in** (`/rkd:access channel add dev`). The bot subscribes to every text channel it can see, because the server only fans `MessageCreate` out to explicit subscribers, but it drops everything outside the configured ones before Claude reads it. It writes `known-channels.json` so the skill can resolve names. Worth a hard look: - `src/triggers.ts`: the prompt-injection boundary. Approvals come only from the global `allowFrom`, never from a channel-level `allowFrom`. - `src/rkd-api.ts`: refresh rotation. The server revokes the whole chain on a replayed token (and on a device-id mismatch), so the token is read only from `.env`, never from the environment, where a stale copy could be replayed on the next start. - Permission prompts go to the thread of the last delivered trigger, so everyone in that channel sees the command preview (as #917 intends). With no trigger yet, or if that channel has since been removed, they go by DM to each paired user. Open: - CI (`.forgejo/workflows/ci.yml`) is the first workflow in the `rkd` org. I couldn't see whether that org's repos get a runner, so this PR may show no checks. The integration test needs a live server and is a local gate only for now. - No LICENSE yet. The repo is public, and `package.json` says `UNLICENSED` until someone decides. - The bot account itself (`claude-code-bot` on dev/prod) isn't created. That's the operator's step in the README. https://claude.ai/code/session_01GdkP2Y3vcxcGz5XCFFWW16
Phase 0 of #917: an MCP channel server that signs in as a bot account,
forwards @mentions, replies to the bot and DMs from paired users into the
Claude Code session, and answers in the same thread with reply,
edit_message, react and download_attachment. Permission prompts are
relayed into that thread; only a paired sender's `yes <id>` approves.

Shaped like the official Telegram channel (0.0.7): access.json with
pairing, an allowlist and trigger channels, /rkd:configure and
/rkd:access, a single-session PID lock. No RKD server change: the bot
is an ordinary user session (refresh rotation, device-bound).

- The bot subscribes to every text channel it can see: the server only
  fans MessageCreate out to explicit channel subscribers.
- Refresh is single-flight and the rotated token is persisted before
  use; a replayed token revokes the whole session server-side.
- The password never passes through Claude: `dist/server.js login` is
  run by the operator in their terminal with a hidden prompt.
- Below Claude Code 2.1.234 permission relay turns itself off.
- dist/server.js is committed (the plugin installs from git and runs
  with plain node); `pnpm check:dist` fails when it is stale.

Claude-Session: https://claude.ai/code/session_01GdkP2Y3vcxcGz5XCFFWW16
ci: typecheck, test, bundle freshness and manifest validation
All checks were successful
CI / rkd-plugin (pull_request) Successful in 31s
967ca4c154
Also boots the committed bundle on Node 20, the oldest Node the
plugin supports. The integration test needs a live rkd-server and
stays a local gate for now.

Claude-Session: https://claude.ai/code/session_01GdkP2Y3vcxcGz5XCFFWW16
dozken merged commit 2746ffc441 into main 2026-09-19 18:55:38 +02:00
dozken deleted branch feat/917-rkd-channel 2026-09-19 18:55:38 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rkd/claude-plugins!1
No description provided.