# Add prefix commands to a bot

Register prefix commands, convert arguments, add guards and cooldowns, and generate help from the same definitions

A [prefix command](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#prefix-command) is a message such as `!ping` or `!greet Maya`. Fluxerly's [command router](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#command-router) reads the name after the configured prefix and calls the matching handler. It uses the bot's existing client and does not open another connection. The SDK has no slash commands, buttons or other interaction components, so every command is a message the bot reads

These examples extend [the starter bot](https://fluxerly.neontechspace.com/docs/nightly/quick-start.md). Add commands to its existing `commands.commands` object, or replace its `commands` option with an example below

## Register commands

Each key of `commands` becomes a command name. The `reply` helper accepts a string or full message content and replies to the incoming message

```ts
import { runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        commands: {
            ping: {
                description: "Check that the bot can reply",
                execute: ({ reply }) => reply("Pong!"),
            },
            about: {
                description: "Describe this bot",
                execute: ({ reply }) => reply("Built with Fluxerly"),
            },
        },
    },
})
```

Sending `!ping` now answers `Pong!`, and `!about` describes the bot. Returning the reply's [Result](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#result) from `execute` is enough: If the reply fails, the router reports the failure with the command name without stopping other commands

<details>
<summary>When do command definitions fail?</summary>

An invalid definition, such as a duplicate name or an unknown argument type, throws `ConfigurationError` when `runBot` is called, before the bot connects.
A command failure happens later, when a matching message arrives. A handler that throws, rejects or returns an Err result has failed, and the router reports it to the `onError` hook, when one is set, or logs it in full at Error

Commands keep the order of the object's own string keys. Omit `arguments` to receive the raw `args` without conversion, and use `arguments: {}` when a command accepts no arguments. The `reply` helper also forwards the handler's cancellation `signal`, so stopping the bot cancels a pending reply

</details>

## Convert arguments before execution

The router converts the words after the command name into typed values before running it. By default it splits the text on whitespace and keeps quote characters, so `!greet "Ada Lovelace"` arrives as the two words `"Ada` and `Lovelace"`. The option `parse: commands.parseQuoted` opts in to quoting: Single or double quotes keep words together and are removed from the argument, and a backslash writes the next character literally. A double quote opens a span anywhere. A single quote opens one only at the start of a word and closes it unless a letter or digit follows, so an apostrophe inside a word, as in `!say don't`, stays literal, `'it's fine'` is `it's fine` and `'Yes', 'No'` is `Yes,` and `No`. A word that begins with an apostrophe, such as `'twas`, opens a span, so write `\'twas`. This bot uses it to accept `!greet "Ada Lovelace"` and `!remind 10m Stretch`

```ts
import { commands, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        parse: commands.parseQuoted,
        commands: {
            greet: {
                description: "Greet a name",
                arguments: { name: { type: "text" } },
                execute: ({ values, reply }) => reply(`Hello, ${values.name}!`),
            },
            remind: {
                description: "Repeat a note after a delay",
                arguments: {
                    delay: { type: "duration", min: 1_000, max: 3_600_000 },
                    note: { type: "text", rest: true },
                },
                execute: ({ client, message, values, reply }) => {
                    client.schedule((signal) => client.messages.reply(message, values.note, { signal }), {
                        delayMs: values.delay,
                    })
                    return reply("Reminder set")
                },
            },
        },
    },
})
```

The `remind` command hands the wait to [`client.schedule`](https://fluxerly.neontechspace.com/docs/nightly/starter-lifetime.md#run-work-later-or-on-a-schedule), so it finishes at once instead of keeping a command slot busy, and the note arrives later as a reply. The bot owns the reminder, so stopping the bot cancels it, and a failed reply goes to the client-level `onError` or the log

In TypeScript, `values.name` is a string and `values.delay` is a number of milliseconds. A missing, invalid or extra argument stops the command before `execute`, and `runBot` answers with the reason and the command's usage, such as `Missing name. Usage: !greet <name>`. That reply is the `onReject: "reply"` default of `runBot`. Set `onReject: "silent"` to send nothing, or pass a function for custom feedback. A router from `commands.create` gives no feedback unless `onReject` is set

A `duration` argument reads text such as `90s`, `5m` or `1h30m` and produces milliseconds. Add `wholeSeconds: true` to reject a millisecond part such as `1m500ms`, for a value that must be whole seconds, like the `durationMs` of a [ban](https://fluxerly.neontechspace.com/docs/nightly/guilds-and-permissions.md#moderate-members). Its `default`, when given, must then be whole seconds too

Other argument types include:

- Whole and decimal numbers, with `integer` and `number` and optional `min`, `max` and `default`
- One of a fixed list of strings, with `choice`
- A decimal [ID](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#id), with `id`. Add `mention: "user"`, `"channel"` or `"role"` to also accept that kind of mention, as in `{ type: "id", mention: "user" }`. The value is the ID either way
- A member of the message's [community](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#guild), with `member`, which turns a user ID or mention into a `{ guildId, userId }` reference
- One entry from a fixed list of candidates given at registration, with `userChoice`, `channelChoice` or `roleChoice`. For any user, channel or role, use `id` or `member` instead
- Any other format, with `custom` and a parse function of the application

Accepting an ID does not mean the user may act on that resource

To convert a single word outside a command, such as a button label or a stored setting, `commands.parseArgument("5m", { type: "duration" })` applies a descriptor with the same rules as the router. It returns `undefined` for a rejected word and the descriptor's `default` for a missing one. A `member` descriptor also needs `{ guildId }` as its third parameter

<details>
<summary>How are ID and member arguments checked?</summary>

An `id` or `member` argument accepts a nonzero decimal ID without leading zeroes, up to `9223372036854775807`, and the same limit applies to IDs inside mentions. Any other value is rejected as invalid before the command runs. Use a `custom` argument for other formats.
A `member` argument produces a `{ guildId, userId }` reference in the message's community without checking that the user is still a member

</details>

## Reply to messages that do not run a command

A message that starts with the prefix but runs no command is ignored by default. Pass `onUnmatched` to tell the sender why. Its second parameter names the cause in `_tag`:

- `CommandUnknownName` for a name that is not registered. Its `suggestion` names the closest registered command when one is similar
- `CommandMissingSubcommand` for a group such as `admin` typed without a subcommand. Its `path` names the group
- `CommandParserRejected` when the parser could not read the text. With `commands.parseQuoted`, its `reason` explains a quote that never closes or a backslash at the end. When the open span held a single quote followed by a letter or digit, as in `'a'b` or `'it's`, the `reason` also says that such a quote may have been meant to close the span, so a space after it or double quotes fix the input. The `reason` is absent when the text is not a command at all, such as `!` alone. Its `name` holds the command name the parser read, as typed, so a reply can say which command the mistake was for

This bot answers a syntax mistake with the parser's own explanation. Without the callback, `!greet "Ada` is ignored

```ts
import { commands, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        parse: commands.parseQuoted,
        onUnmatched: ({ reply }, unmatched) => {
            if (unmatched._tag !== "CommandParserRejected" || unmatched.reason === undefined) return undefined
            return reply(unmatched.reason)
        },
        commands: {
            greet: {
                arguments: { name: { type: "text" } },
                execute: ({ values, reply }) => reply(`Hello, ${values.name}!`),
            },
        },
    },
})
```

The router never replies to these messages by itself. The callback runs for every one of them and anyone can cause that, so an application that replies should limit how often. A channel may also hold other bots with the same prefix, so reply to an unknown name or a syntax mistake only when this bot should answer for that prefix. A custom `parse` function can give its own explanation by returning `{ reason: "Separate the words with spaces" }` instead of `undefined`. The reason reaches `onUnmatched` unchanged, so it should be fixed text that leaves the sender's input out

To suggest a name in a reply of the application's own, `commands.suggest("pnig", ["ping", "help"])` applies the rule behind `suggestion` to any list of names and returns `"ping"`. Pass only names that may be revealed, because the helper does not know which commands are hidden

Each unmatched message is also logged at Debug with the code `commands.unmatched` and the fields `reason`, `messageId`, `channelId` and, when there is one, `suggestion`. The `reason` field holds the `_tag`, and for a rejected parse the log message ends with the parser's explanation

## Guard, limit and wrap commands

A [guard](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#guard) decides whether a matched command may run. A [cooldown](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#cooldown) limits how often each user, channel or community can run it. [Middleware](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#middleware) wraps every command, for example to measure how long it takes

```ts
import { guards, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        mentionPrefix: true,
        use: [
            async (context, next) => {
                const started = Date.now()
                await next()
                console.info(`${context.name} took ${Date.now() - started} ms`)
            },
        ],
        commands: {
            purge: {
                description: "Delete recent messages",
                guard: [guards.guildOnly(), guards.requirePermissions(["ManageMessages"])],
                cooldown: { durationMs: 30_000, per: "channel" },
                arguments: { count: { type: "integer", min: 1, max: 100, default: 10 } },
                execute: ({ values, reply }) => reply(`Would delete ${values.count} messages`),
            },
        },
    },
})
```

Here `!purge` runs only in a community, for a member with the Manage Messages permission, and at most once every 30 seconds in each channel. With `mentionPrefix`, a mention of the bot also works as a prefix

Only messages a person typed can run commands. Messages from bots are skipped by default, and the notices Fluxer posts with text of its own never run commands. For example, a thread named `!ping` makes Fluxer post a notice containing that name, and it does not run `!ping`

A guard returns `true` to allow the command, `false` to deny it without an automatic reply or `{ deny: "reason" }` to deny it with a reason that `onReject: "reply"` sends. A false verdict stays silent even with the `runBot` reply default, but still counts as a rejection, logs `commands.rejected` at Debug and reaches a custom `onReject` callback. Each built-in guard denies with a specific reason and covers one common check, allowing only:

- Messages sent in a community, with `guards.guildOnly()`
- Direct and group conversations, with `guards.dmOnly()`
- The owner of the bot's application, with `guards.ownerOnly()`
- Listed user IDs, with `guards.ownerOnly(ids)`
- Members who have every named permission in the channel, with `guards.requirePermissions(names)`

<details>
<summary>How the built-in guards read data</summary>

The `requirePermissions` guard decides with `permissions.fetch` and `preferCache`: It reads the community, member, roles and channel from enabled caches first and fetches only what is missing, at most once each per command. Cached values follow the gateway events applied so far, so check again with a fresh `permissions.fetch` before an action where a stale grant matters. The denial names each missing permission by Fluxer's label, such as `Manage community`. In a thread, it also reads the parent channel the same way and decides from the parent's permissions as Fluxer does, with `SendMessages` granted exactly when `SendMessagesInThreads` is. A failed read fails the command, which is reported with the command name. When Fluxer sent the channel without its permission overwrite list, the guard denies the command because it cannot confirm the channel's permissions. Its decision does not guarantee that Fluxer allows a later action.
The `dmOnly` guard denies a message that has a `guildId` without a request. Otherwise it confirms a private conversation from the direct-message cache or the channel cache, or reads the channel once. A cached or fetched community channel denies the command. A read that fails for any other reason, such as a network failure or timeout, leaves the channel unconfirmed and fails the command, which is reported with the command name. Enable `cache.directMessages` to avoid a read for each command in the same conversation.
The `ownerOnly()` guard reads the application's owner from Fluxer on the first command and keeps it for the client's lifetime. A failed read fails the command, and the next command reads again

</details>

<details>
<summary>How cooldowns are stored</summary>

A cooldown takes `durationMs` and `per`, which is `"user"` by default, `"channel"` or `"guild"`. Without a `store`, the router keeps cooldowns in memory in this process. That store holds 10,000 keys by default. Set the router's `cooldowns: { maxEntries }` option to change the limit, for example `cooldowns: { maxEntries: 50_000 }`.
A full store never turns a user away: It removes expired keys first, then the key that would expire soonest, which lets that key run again early. Pass a `store` to share cooldowns between processes

</details>

<details>
<summary>How often a rejected command is answered</summary>

So that repeated attempts do not make the bot repeat itself, the `"reply"` feedback answers an active cooldown once until it expires, and a guard denial with a reason once per user and command every 5 seconds. A guard returning `false` sends nothing. Argument errors are answered every time.
A skipped answer still counts as a rejection and is logged at Debug. An `onReject` function receives every rejection instead, so it can apply its own limit

</details>

## Control how commands run

By default `runBot` runs up to eight commands at a time, and when more messages wait than the queue holds, it drops the oldest waiting message with a Warn record. The `commands` option also accepts the delivery settings of an event handler, `concurrency`, `partition`, `partitionReady`, `overflow`, `maxPendingMessages` and `maxPendingBytes`, and passes them to the router unchanged. Invalid values throw `ConfigurationError` when `runBot` is called. This bot runs the commands of each channel one at a time in the order they arrived, and stops instead of dropping commands when 64 messages are waiting

```ts
import { runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        partition: "channel",
        maxPendingMessages: 64,
        overflow: "stop",
        commands: {
            ping: { execute: ({ reply }) => reply("Pong!") },
        },
    },
})
```

With `overflow: "stop"`, a full queue stops the bot with `CriticalWorkerStoppedError`, which names the `messageCreate` subscription and the limit it exceeded. Leave `overflow` unset to keep the bot running and drop waiting messages instead. With a `partition`, the drops come from the longest backlog, as the [events guide](https://fluxerly.neontechspace.com/docs/nightly/events-and-collectors.md) describes

A partition can need its own setup before its commands run. With `partitionReady`, the SDK calls the hook with the key before the first waiting message of that key starts, and the key's messages wait in the queue, using no command slot, until the returned promise settles. Other keys keep running meanwhile. The hook needs `partition` and otherwise `runBot` throws `ConfigurationError`. A hook that throws, rejects or returns an `Err` result reports each waiting message of that key to the `onError` of the commands when they have one, otherwise to the client-level `onError` or the log, like a throwing `partition` function and an overflow that stops the router, and those messages run no command. The [events guide](https://fluxerly.neontechspace.com/docs/nightly/events-and-collectors.md#wait-for-a-communitys-setup) covers the timing rules, which are the same for commands

```ts
import { runBot } from "@neontechspace/fluxerly"

export function pingAfterSetup(setups: Map<string, Promise<void>>) {
    return runBot({
        token: process.env.FLUXER_BOT_TOKEN,
        commands: {
            prefix: "!",
            partition: "guild",
            // A direct message has no community and is keyed by its channel, so an unknown key counts as ready
            partitionReady: (key) => setups.get(key),
            commands: {
                ping: { execute: ({ reply }) => reply("Pong!") },
            },
        },
    })
}
```

## Use the Effect entry point

The [Effect](https://fluxerly.neontechspace.com/docs/nightly/glossary.md#effect) API accepts the same options, with `partitionReady` returning an Effect whose required services become requirements of the program. Each reply stops when the command handler is interrupted and keeps `SendError` in the Effect error channel. Services required by any command remain requirements of the returned program

```ts
import { Effect } from "effect"
import { runBot } from "@neontechspace/fluxerly/effect"

export const program = runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        commands: {
            ping: { execute: ({ reply }) => reply("Pong!").pipe(Effect.asVoid) },
            about: { execute: ({ reply }) => reply("Built with Fluxerly").pipe(Effect.asVoid) },
        },
    },
})
```

## Attach commands to an existing client

To add commands to a client the application already manages, create a router, register commands and attach it to the client. Creation and registration throw `ConfigurationError` for invalid definitions. Close the returned subscription to detach the router, or wait for its cleanup with `waitForClose()`. The second argument of `attach` takes the same delivery settings as `runBot`, including `partitionReady`

```ts
import { commands, type Client } from "@neontechspace/fluxerly"

export function attachCommands(client: Client) {
    return commands
        .create({ prefix: "!" })
        .registerMany({
            ping: { execute: ({ reply }) => reply("Pong!") },
            about: { execute: ({ reply }) => reply("Built with Fluxerly") },
        })
        .attach(client)
}
```

## Add help from the registered commands

Each command receives a `help` function that builds help pages from the registered descriptions and arguments, and a `sendHelp` function that sends them

```ts
import { runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        commands: {
            help: {
                description: "List the commands",
                execute: ({ sendHelp }) => sendHelp(),
            },
            ping: {
                description: "Check that the bot can reply",
                execute: ({ reply }) => reply("Pong!"),
            },
        },
    },
})
```

Sending `!help` lists each command with its description. Each page holds up to 2,000 characters, which fits one message. The `sendHelp` function sends a single page as a plain message, and several pages as reaction pages that the command's author flips with ◀ and ▶. The `help` function only builds the pages, for a bot that sends them its own way. A router created with `commands.create` has the same `help` method

The `paginate` function shows any list of pages the same way, as in `({ paginate }) => paginate(["Rules 1 to 5", "Rules 6 to 10"])`. Adding or removing an arrow turns the page, so the bot needs only Add Reactions and Read Message History. The pages stop listening after 60 seconds without a click or 5 minutes in total, and the bot then removes its arrows. Pass `users` to let other people flip them. [Reaction pages](https://fluxerly.neontechspace.com/docs/nightly/events-and-collectors.md#show-pages-with-reactions) explains the options

To keep a command out of help and out of unknown-command suggestions, for example an owner-only tool, register it with `hidden: true`. Hidden commands still run when someone types their name, so hiding does not restrict access. Protect each command that needs it with a guard

For a larger bot, split command definitions by feature and attach one combined router. [Groups](https://fluxerly.neontechspace.com/docs/nightly/api/js-ts/commands/#prefixcommandrouter) such as `admin` in `!admin inspect` add structure when needed. The `group` option of `help`, such as `help({ group: ["admin"] })`, describes one group. A group registered with `hidden: true` hides everything inside it, and selecting a hidden group throws `ConfigurationError` as for a missing one

## Wire it up

A complete bot places the router beside the other `runBot` options. This one adds a help command, a per-user cooldown, a larger cooldown store, its own failure hook and a hidden owner-only command

```ts
import { guards, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    commands: {
        prefix: "!",
        cooldowns: { maxEntries: 50_000 },
        onError: (report) => console.error(report.describe()),
        commands: {
            help: {
                description: "List the commands",
                execute: ({ sendHelp }) => sendHelp(),
            },
            ping: {
                description: "Check that the bot can reply",
                cooldown: { durationMs: 5_000, per: "user" },
                execute: ({ reply }) => reply("Pong!"),
            },
            uptime: {
                hidden: true,
                guard: guards.ownerOnly(),
                execute: ({ reply }) => reply(`Running for ${Math.round(process.uptime())} s`),
            },
        },
    },
})
```

The `ownerOnly()` guard allows only the owner of the bot's application, as Fluxer reports it, so the owner's ID needs no configuration. The commands `onError` hook receives command failures instead of the client-level `onError`, and without either hook the SDK logs each failure in full. If the bot itself stops with a failure, such as a rejected token, `runBot` logs it once and sets `process.exitCode` to 1
