> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloak.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Command bot

> A slash-command bot: two declarations, one handler API for both invocation lanes, and the filter that stops it answering twice.

This bot declares two slash commands, `/play` and `/volume`, and dispatches them through one handler API. It also keeps a small text command of its own, `help`, which is where the one filter every mixed bot needs comes in.

The SDK repo ships this as `examples/command-bot.ts`.

## The full bot

```ts command-bot.ts theme={null}
import { Client } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  // Keep this file. The bot's identity is published to the server once, so a
  // bot with no keystore works on its first run and fails on every one after.
  keystorePath: './command-bot.cloak-keystore.json',
  requiredPermissions: ['message_send'],
  debug: !!process.env.CLOAK_DEBUG,
});

// Everything registered here is published to the server at login(), so a human
// client's `/` picker can list it. That is why registration happens BEFORE
// login() and not inside a ready handler.

// A greedy trailing string option: the LAST option, when it is a string,
// consumes the rest of the line verbatim. `/play never gonna give you up`
// arrives as one argument, spaces and all, with no quoting needed.
client.commands.register(
  {
    name: 'play',
    description: 'play a song',
    options: [{ name: 'query', type: 'string', description: 'what to play', required: true }],
  },
  async (ctx) => {
    console.log(`[/play] from ${ctx.message.authorName} via the ${ctx.source} lane`);
    await ctx.reply(`playing: ${ctx.getString('query')}`);
  },
);

// A boolean plus a choices-constrained option. Choices are VALUES, as strings:
// the choice string is both the label and the value. Note the ordering rule:
// every required option precedes every optional one, or register() throws.
client.commands.register(
  {
    name: 'volume',
    description: 'set the playback volume',
    options: [
      {
        name: 'level',
        type: 'string',
        description: 'how loud',
        required: true,
        choices: ['quiet', 'normal', 'loud'],
      },
      { name: 'announce', type: 'boolean', description: 'say so in the channel' },
    ],
  },
  async (ctx) => {
    const level = ctx.getString('level');
    // `/volume loud announce:true`, `/volume quiet no`, `/volume normal` all work.
    if (ctx.getBoolean('announce')) await ctx.reply(`volume set to ${level}`);
    else console.log(`[/volume] ${level} (quietly)`);
  },
);

// A command that could not be run.
client.on('commandError', (e) => {
  console.warn(`/${e.name} failed (${e.reason}):`, e.error);
});

client.on('messageCreate', (msg) => {
  if (msg.authorId === client.user?.id) return; // never answer yourself
  // The filter a bot needs when it ALSO does its own text handling: a message
  // the command router already claimed has msg.command set, and its handler has
  // been dispatched. Without this you would answer twice.
  if (msg.command) return;

  if (msg.content.trim() === 'help') {
    const list = client.commands
      .list()
      .map((d) => `/${d.name}: ${d.description}`)
      .join('\n');
    void msg.channel.send(`commands:\n${list}`);
  }
});

client.on('error', (e) => console.warn('[cloak-sdk]', e));
client.on('ready', () =>
  console.log(`ready as ${client.user?.id}; ${client.commands.size} commands declared`),
);

client.login().catch((e) => {
  console.error('login failed', e);
  process.exit(1);
});
```

## Run it

<CodeGroup>
  ```bash Your own copy theme={null}
  CLOAK_TOKEN="botid.tokenid.secret" npx tsx command-bot.ts
  ```

  ```bash From the SDK repo theme={null}
  # copy .env.example to .env, put CLOAK_TOKEN in it, then:
  npm run example:command
  ```
</CodeGroup>

Then type `/play never gonna give you up` in a channel the bot can see, or pick the command from the client's `/` menu. Both routes reach the same handler.

## How it works, piece by piece

<AccordionGroup>
  <Accordion title="Register before login()" icon="clock">
    `login()` publishes whatever is registered at that moment, which is what a human's `/` picker lists. `register()` itself is synchronous and throws in your own stack trace on anything the server would reject: a bad name, a description over 100 characters, a duplicate, a required option after an optional one, too many commands.

    A command registered after `login()` still dispatches locally, but it is not in the published menu until the next login, and the SDK does not republish on its own. Publication is best effort and never blocks login: a rejection is logged behind `debug` and swallowed.
  </Accordion>

  <Accordion title="Declaring options" icon="sliders">
    Option types are `string`, `integer`, `boolean`, `user`, `channel`, `role`, and `number`. The `user`, `channel`, and `role` types arrive as opaque id strings; the SDK does no resolution, so read them with `getString()` and normalize before comparing.

    Two rules are load-bearing:

    * **Declared order is the positional text grammar.** Every required option must precede every optional one, or `register()` throws.
    * **The last option, when it is a string, is greedy.** It swallows the rest of the line verbatim once every earlier option is filled, which is what makes `/play never gonna give you up` work without quotes.

    `choices` is a flat list of strings, and each string is both the label and the value. The wire has no slot for a separate label. Booleans cannot carry choices, and for `integer` or `number` every choice must coerce.
  </Accordion>

  <Accordion title="One handler, two lanes" icon="route">
    An invocation reaches your bot on one of two lanes, and this file cannot tell which:

    * **Structured**: an encrypted `commandData` sidecar on the message frame, which names the target bot.
    * **Text**: the readable message content, parsed by the SDK's grammar with the prefix from `commandPrefix` (default `/`).

    Structured wins when both are present, and a message never dispatches twice. The text lane cannot address a bot, so every bot in the channel with `/play` registered will dispatch a text `/play`. `ctx.source` tells you which lane you got, which is mostly useful for logging.

    <Note>
      Slash commands are not dispatched from direct messages. The structured lane does not exist on the DM frame, and the menu is published per server. `messageCreate` still fires for a DM, so parse `msg.content` yourself if you want DM commands.
    </Note>
  </Accordion>

  <Accordion title="The handler context" icon="code">
    A handler receives a `CommandContext`: `name`, the raw `args` record, `source`, the typed accessors `getString`, `getInteger`, `getNumber`, `getBoolean` (each returning `null` when the option is absent or does not coerce), the originating `message`, and `reply(text, opts?)`.

    `ctx.reply()` is identical to `ctx.message.reply()`: a true reply, complete with the quote header and a "replied to you" notification that pierces mutes. That fits a command answer, which is genuinely a response to a specific message. For a plain send with no quote, use `ctx.message.channel.send()`.

    Sends stay fire-and-forget here as everywhere: a resolved promise means the frame went out, and a server-side denial arrives on `sendRejected`.
  </Accordion>

  <Accordion title="The msg.command filter" icon="filter">
    A message the router already claimed carries `msg.command`, and its handler has already been dispatched by the time your `messageCreate` listener runs (`messageCreate` fires first, then dispatch). A bot that also parses text needs `if (msg.command) return;` or it answers the same message twice.

    `msg.command` is never set on fetched history, so replaying history cannot re-execute old commands, and it is never set on the bot's own messages.
  </Accordion>

  <Accordion title="commandError, and when the SDK stays quiet" icon="triangle-exclamation">
    `commandError` carries `{ name, error, message, reason }`:

    * `reason: 'parse'` means the text matched a **registered** command but did not fit its declaration: a missing required option, a value outside `choices`, a number that will not coerce. Your handler was not invoked, and the SDK does not auto-reply, because it never speaks unprompted. Reply yourself if you want the user to hear about it.
    * `reason: 'handler'` means your own handler threw or rejected.

    An unregistered `/foo` emits nothing at all.
  </Accordion>

  <Accordion title="Two properties of v1 to design around" icon="lock-open">
    <Warning>
      **There is no ephemerality.** An invocation is an ordinary message encrypted with the channel epoch key. Every member holding that key can read every invocation, and it is a durable message row subject only to the channel or per-message TTL. Nothing here is "only you can see this", and deleting the message afterwards does not fake it.
    </Warning>

    <Warning>
      **The registry menu is plaintext server-side.** Command names, descriptions, option names, and choice labels are stored and served unencrypted so a client's `/` picker can render them without a key. Never put a secret, an internal hostname, or a customer name in a description or a choice.
    </Warning>

    Message bodies stay end-to-end encrypted throughout, including the structured sidecar. See [What is encrypted, and what is not](/concepts/encryption-lanes).
  </Accordion>

  <Accordion title="commands.list() as a help command" icon="circle-question">
    `client.commands.list()` returns the declarations you registered, and `client.commands.size` counts them, so a `help` command can be generated rather than maintained by hand. The example answers with `msg.channel.send()` instead of `reply()`: a help dump does not need to pierce anyone's mutes.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Slash commands" icon="terminal" href="/guides/slash-commands">
    The text grammar, the two lanes, and publishing the menu.
  </Card>

  <Card title="Commands reference" icon="book" href="/api-reference/commands">
    Declarations, caps, and the context API.
  </Card>

  <Card title="Card bot" icon="image" href="/examples/card-bot">
    Answer a command with an encrypted rich card.
  </Card>

  <Card title="Moderation bot" icon="gavel" href="/examples/moderation-bot">
    Actions, typed denials, and the guild() handle.
  </Card>
</CardGroup>
