CLI: Chat Appsco discord

Discord CLI (co discord)

Experimental. co discord was tested against fakes, not yet a live bot. Edit, delete and react are not implemented and say so.

Turn a Discord bot you own into a directory of files. Every message the bot can see becomes one line in a log and one file in a queue; anything that can read a file can answer it. The same verbs as co feishu, against a different platform.

co discord check                        # token, listener, connection, unread count
co discord listen                       # hold the Gateway connection; every message → ~/.co/inbox/discord/
co discord receive                      # block for the next message, take it, print one JSON line
echo "on it" | co discord reply 123456789012345678   # a reply to that message, in its channel
co discord send 234567890123456789 "deployment finished"
co discord done 123456789012345678      # took it, decided not to answer
co discord consume -- claude -p         # one command per message, stdout is the reply
co discord ls                           # what is waiting
co discord chats                        # which channels have spoken
co discord log -f                        # everything, as it arrives

The listener dials out to Discord's Gateway, so there is no public endpoint to open, and no OpenOnion credential is involved.

Setup

  1. Create an application at https://discord.com/developers/applications, then add a bot to it on the Bot page.

  2. On the same page, under Privileged Gateway Intents, turn on Message Content Intent. Without it Discord closes the connection with code 4014, and co discord listen stops and says so.

  3. Invite the bot to your server (OAuth2 → URL Generator, scope bot) with permission to View Channels, Read Message History and Send Messages.

  4. Put the token in your global credential file:

    # ~/.co/keys.env
    DISCORD_BOT_TOKEN=...
    

The token is read only by the local process. It is never sent to OpenOnion, never written into the inbox, and never included in an error. co status and co keys show whether it was found and hide its value.

What a message looks like

{"id":"123456789012345678","chat":"234567890123456789","thread":null,
 "sender":"456789012345678901","sender_name":"Aaron","text":"@OpsBot check the failed deployment",
 "kind":"text","quoted":null,"mentioned":true,"at":"2026-09-05T05:00:00.000000+00:00"}
  • id is Discord's message id, unique across the platform.
  • chat is the channel id. A thread is a channel of its own in Discord, so a message in a thread has the thread's id as its chat, and replying there answers in the thread.
  • mentioned is true for a direct message, and in a server when the message mentions the bot or replies to one of its messages. Role mentions and @everyone do not count.
  • kind is text, or image / audio / video / document for an attachment with no text; the text is then [image] and so on. The file is not downloaded yet.
  • Messages from bots (including this one) and webhooks are never written.

Sending

A reply references the message it answers and pings nobody: not the person being answered, not a role, not @everyone, whatever the text says. Discord limits a message to 2000 characters; a longer one is refused before anything is sent. One rate limit (HTTP 429) is waited out and retried; a second is reported.

A reply carries a nonce derived from the message and the text, so a reply that is retried after a timeout returns the message Discord already posted rather than posting it twice. co discord reply --again is a deliberate second post and gets a fresh one.

co discord edit, delete and react exist so the verbs match every other provider, and say plainly that they are not wired up for Discord yet.

When the connection drops

The listener heartbeats at the interval Discord asks for. If a heartbeat goes unacknowledged it closes the socket itself rather than waiting on a connection that has stopped delivering, then reconnects and resumes the session, and Discord replays what was missed. A replayed message the inbox already has is logged as a duplicate and not queued twice. Session state lives in memory, so a full restart of the listener identifies afresh: what arrived while nothing was running is not replayed.

Some closes no reconnect can fix, and those end listen with exit 3 and the thing to do: 4004 (the token was rejected), 4014 (Message Content intent is off), and the sharding and intent errors 4010–4013.

To have your own agent answer, list the channel in ~/.co/host.yaml under listen: as discord and run co ai, exactly as for Feishu.

Star us on GitHub

If ConnectOnion saves you time, a ⭐ goes a long way — and earns you a coffee chat with our founder.