0.2.0. Check what you have with npm ls @cloak-software/bot-sdk.
0.2.0
Breaking: send() takes an options object
The fourth argument used to be a positional groupId. It is now a SendOptions object, and the positional form is gone.
SendOptions carries three fields, two of them new:
string
The channel’s group, for the case where the SDK has not already cached it from an inbound frame. This is the value that used to be positional.
MentionTarget[]
Who to notify. New in 0.2.0. Mention targets ride the wire in plaintext, because the server routes notification fanout without decrypting the body. See Mentions and replies.
ReplyTarget
Makes the message a true reply. New in 0.2.0.
string is not assignable to SendOptions. Run tsc --noEmit and fix everything it flags.
Sending is fire-and-forget in every version. A resolved promise means the frame is on the wire, not that the server accepted it. A server-side denial arrives later on the
sendRejected event. If your 0.1.x bot never listened for it, add a listener while you are in here.Breaking: msg.reply() is now a true reply
In 0.1.x, msg.reply() was a plain send into the same channel. In 0.2.0 it posts a real reply: the client renders a quote header, and the author gets a “replied to you” notification that pierces their mute settings.
For a bot that is simply answering, that notification is usually wrong. Use msg.channel.send(), which is the old behavior under a new name.
msg.reply() for the cases where a quote header genuinely helps, such as an answer that arrives long after the question, or a moderation notice aimed at one person. ctx.reply() inside a command handler follows the same rule.
Breaking: Message.serverId is string | null
A direct message has no server, so Message.serverId (and MessageLocation.serverId) are now nullable. Inbound DMs arrive on the same messageCreate event as channel messages, which means any handler you already have will start seeing them.
Guard before you pass msg.serverId to anything server-scoped:
strictNullChecks, TypeScript flags these too. msg.reply() and msg.channel.send() need no guard: they route themselves at the DM when the message is one. Six actions do reject on a DM message: react, unreact, edit, delete, pin, and unpin. See Direct messages.
Migration checklist
1
Run the type checker first
tsc --noEmit finds both the positional groupId calls and the serverId nullability. Start from its output rather than reading every file.2
Rewrite positional groupId calls
send(s, c, text, groupId) becomes send(s, c, text, { groupId }). Same for guild().send().3
Decide reply or channel.send at every call site
Ask whether the author should get a mute-piercing notification. If not, switch to
msg.channel.send().4
Add a DM guard to every messageCreate handler
if (msg.isDM || !msg.serverId) return; at the top, unless the handler is meant to answer DMs.5
Remove any call to fetchMembers()
It cannot work against a current backend. See Known broken below.
6
Check your Node version
^20.19.0 || >=22.12.0. Node 21.x does not qualify. See Install and requirements.Added
- Direct messages.
client.sendDM(),Message.isDM,Message.dmId, and inbound DMs on the unifiedmessageCreate. 1:1 only. See Direct messages. - Mentions and replies.
SendOptions.mentionsandSendOptions.replyTo, theuserMention(),roleMention(),EVERYONE,HERE,mentionPairs()andparseMentions()helpers, andMessage.mentions/Message.mentionsMe. See Mentions and replies. - Slash commands. A typed command registry declared before
login(), dispatched through one handler whether the invocation arrives as an encrypted sidecar or as parsed message text. See Slash commands. - Rich cards.
sendCard(), the encrypted embed lane. Same conversation key and epoch as a normal message, posted under the bot’s own identity. See Rich cards. - Webhook embeds.
sendEmbed(),client.webhooks, andpostToWebhook(). This lane is not end-to-end encrypted, which is why the payload type is namedUnencryptedWebhookPost. See Webhook embeds. - Voice control plane.
joinVoice()andleaveVoice(), the roster and its events, andclient.voiceKeys. See Voice overview. - Voice audio. The opt-in media layer:
VoiceMediaSession,VoicePlayer,ffmpegResource(), and theytdlpResource()/resolveMediaUrl()extractor. See Playing audio. - Typing and presence.
sendTyping(),setStatus(), and thetypingevent. See Typing, presence, and profile. - REST surfaces.
client.rest, off by default and gated server-side. See The REST API. - Environment doctor.
generateDependencyReport()and friends, which report what your host actually resolved. See Troubleshooting. - HPKE key wrap. The key-wrap format cut over to RFC 9180 HPKE. Both ends of a key handover must speak it, so a human handing your bot a server key needs a current client build. The SDK names the offending peer in a warning if it receives an older wrap.
Known broken
Status
The SDK is validated live end to end against a real Cloak stack. A bot logged in, published its identity, received and unwrapped a server key wrapped by a real desktop client, decrypted an incoming message, and sent an encrypted reply the client displayed. Both crypto directions are wire-compatible with the shipped clients. Auto-reconnect is validated by bouncing the service under a live bot. The default WebSocket lane is live-verified too, through an integration harness that drives a realClient over ws:// against the whole stack in Docker.
A green harness run is not evidence that voice media works. The harness publishes no audio, subscribes to none, and decrypts no frame.
@livekit/rtc-node is deliberately absent from its image, so frame-key interop between the SDK and the shipped clients is not covered by it.Not implemented
- Group DMs. 1:1 DMs ship in 0.2.0. The group-DM lane uses a different, versioned key model and is not implemented.
- DM history, reactions, edits, and pins. The backend implements them. None of their hooks reach a bot, so the SDK does not pretend to.
Next
Sending messages
The current send surface, options object and all.
Direct messages
Why
serverId went nullable, and what a DM message can and cannot do.Mentions and replies
Pills, pings, and when a true reply is the right call.
Install and requirements
The supported Node range and why it is what it is.