discordance
v0.4.1
Published
opencode plugin: a bi-directional bridge that streams session turns and tool calls to Discord and drives the session from Discord.
Maintainers
Readme
discordance
An opencode plugin: a bi-directional bridge between an opencode coding-agent session and Discord.
- Outbound streaming. Assistant turns and tool calls are streamed to a Discord channel using hermes-style progressive message edits (send on first token, edit as tokens arrive, roll to a new message at the 2000-char limit).
- Inbound queueing. Messages you send from Discord are forwarded via opencode's async prompt endpoint, so they land in opencode's own prompt queue: they appear in the TUI's queued-message list and run when the current turn finishes (never force-injected mid-turn).
- Interactive gates. Permission requests and agent questions surface as Discord buttons (Allow once / Always / Reject, or one button per answer) and reply straight back into the session.
- Command mirroring. In
onmode your opencode commands are exposed as Discord slash commands and run in the active session. - Runtime toggle. Everything is driven by the
/remote enable|on|offcommand from the opencode TUI (no restart needed to switch modes).
The plugin talks to Discord directly via discord.js over a live gateway connection and needs its own bot.
Modes: /remote
The bridge has three modes, toggled at runtime from the opencode TUI with the /remote command (the plugin installs this command into your global opencode config on first run, so restart opencode once after installing):
| Command | Mode | Behaviour |
| --- | --- | --- |
| /remote enable | enable | Outbound only: assistant turns and tool calls stream to Discord. |
| /remote on | on | Full two-way: outbound streaming plus inbound messages queued into the session, permission/question button gates, and your opencode commands mirrored as Discord slash commands. |
| /remote off | off | Bridge silent; slash commands deregistered. |
Running /remote with no (or an unknown) argument shows the current mode as a toast.
In on mode the plugin enumerates your opencode commands (GET /command) and registers each as a Discord slash command. Invoking one from Discord runs it in the active session via POST /session/:id/command and streams the result back into the channel. The /remote command itself is never mirrored.
Install
Add the package to the plugin array of your opencode config (opencode.json in your project, or ~/.config/opencode/opencode.json globally):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["discordance"]
}opencode auto-installs listed npm plugins with bun at startup (cached under ~/.cache/opencode/node_modules/). No manual npm install and no .opencode/plugins/ wrapper is needed for the published package.
On first run the plugin writes ~/.config/opencode/commands/remote.md (respecting OPENCODE_CONFIG_DIR/XDG_CONFIG_HOME) so the /remote command is available. Restart opencode once for it to appear.
Configuration
All configuration is via environment variables. The bridge starts in off mode unless told otherwise, and is toggled live with /remote.
Required for the Discord bridge
| Variable | Description |
| --- | --- |
| DISCORD_BOT_TOKEN | Your Discord bot token. Never commit this. |
| DISCORDANCE_STREAM_CHANNEL_ID | Target channel (or DM channel) ID for the stream. |
Optional tuning
| Variable | Default | Description |
| --- | --- | --- |
| DISCORDANCE_REMOTE_MODE | off | Initial mode on load: off, enable, or on. |
| DISCORDANCE_STREAM | off | Legacy alias — if set, starts in on mode. |
| DISCORDANCE_REQUIRE_MENTION | off | Set to 1 to only act on guild-channel messages that @-mention the bot (DMs always bypass). Recommended when the target is a shared channel so member-directed chatter isn't captured. The mention is stripped before the message is queued. |
| DISCORDANCE_LISTEN_USER_IDS | unset (allow-all) | Comma-separated Discord user-ID allow-list for inbound messages, slash commands, and button gates. Empty allows everyone (back-compat). Strongly recommended whenever the target is a shared channel — inbound messages are queued as prompts that can trigger tool calls, so this is the deny-by-default control over who can drive your session. |
| DISCORD_GUILD_ID | unset | If set, slash commands are also registered to this guild for instant updates (global registration always happens too; guild-scoped commands appear only inside that guild, not in DMs). |
| DISCORDANCE_STREAM_EDIT_INTERVAL_MS | 1200 | Minimum ms between message edits (rate-limit friendly). |
| DISCORDANCE_STREAM_BUFFER_CHARS | 60 | Chars buffered before a forced flush/edit. |
| DISCORDANCE_STREAM_CURSOR | ▉ | Cursor glyph appended while a message is still streaming. |
| DISCORDANCE_STREAM_QUIET | follows DISCORDANCE_REQUIRE_MENTION | Stream tool calls as a header only (🔧 **tool** ✓), dropping the command/path detail, to keep a shared channel legible. Defaults on when DISCORDANCE_REQUIRE_MENTION is set; force with 1/0. |
| DISCORDANCE_GATE_TTL_MS | 900000 (15m) | How long a permission/question button gate stays live before expiring. |
| DISCORDANCE_DEBUG | off | Set to 1 to write verbose inbound/gateway debug logs. |
| DISCORDANCE_DEBUG_FILE | /tmp/dd-inbound-debug.log | Debug log path (when DISCORDANCE_DEBUG=1). |
Channel choice (important on a shared channel)
Outbound streaming always fills whatever channel it targets, so it should not live in a shared channel like a team #general. Prefer one of:
- a dedicated guild channel (e.g.
#opencode), or - a DM with the bot — a DM is inherently 1:1, so no mention-gate is needed.
If you must share a channel, protect it: set DISCORDANCE_REQUIRE_MENTION=1 (so only messages that @-mention the bot are captured) and DISCORDANCE_LISTEN_USER_IDS to your own user ID (so only you can drive the session). Without an allow-list, anyone who can post in the channel can queue prompts that run tools in your live opencode session.
Event trace plugin (diagnostics only)
The package also exports a DiscordanceTrace plugin that dumps every opencode event to JSONL for debugging. It is fully independent of the Discord bridge.
| Variable | Default | Description |
| --- | --- | --- |
| DISCORDANCE_TRACE | off | Set to 1 to enable event tracing. |
| DISCORDANCE_TRACE_FILE | <project>/.opencode/discordance-trace/events.jsonl | Output path for the JSONL trace. |
Discord bot setup
- Create an application + bot at the Discord Developer Portal and copy the bot token into
DISCORD_BOT_TOKEN. - Under Bot > Privileged Gateway Intents, enable Message Content Intent.
- Invite the bot to your server, or open a DM with it. The plugin requests
Guilds,GuildMessages,MessageContentandDirectMessagesintents withChannel/Messagepartials, so both guild channels and DMs work. The standardbotinvite scope already includesapplications.commands, so no extra scope is needed for slash commands. - Get the channel ID (Discord Settings > Advanced > Developer Mode, then right-click the channel/DM > Copy ID) and set
DISCORDANCE_STREAM_CHANNEL_ID. - Slash commands (mode
on) are registered globally by default, so they show up in DMs and every guild (allow up to ~1h to propagate). SetDISCORD_GUILD_IDto also register them in one guild for instant iteration.
Local development (from this repo)
The published package needs no wrapper, but to exercise the plugin in-repo before publishing there is a thin dev wrapper at .opencode/plugins/discordance.ts that re-exports from this package's built output:
cd opencode-plugin
npm install
npm run build # emits dist/ that the wrapper importsThen start opencode from the repo root with the env vars set. Rebuild (npm run build) after editing anything under src/. npm run typecheck runs a no-emit strict type check.
Publishing
cd opencode-plugin
npm run typecheck # strict type check
npm publish --dry-run # inspect the tarball (should contain only dist/, package.json, README, LICENSE)
npm login # first time only
npm publish --access publicprepublishOnly runs the build automatically, so dist/ is always fresh in the published tarball. Only dist/ is shipped (see files in package.json); src/ and node_modules/ are excluded.
License
MIT (c) Quintin De Kok
