{
  "id": "discord-bot-ts",
  "type": "loadout",
  "title": "Discord bot (TypeScript)",
  "summary": "discord.js v14 rules and an add-a-slash-command skill for building a Discord bot that registers commands properly and never leaks its token.",
  "description": "Discord bots are a classic first agent project and a classic way to leak a token to GitHub.\nThis loadout sets up a clean discord.js v14 structure (one file per slash command, a loader, a deploy\nscript), teaches the agent the three-second interaction rule and the intents model, and gives it a\nskill for adding new commands end to end.\n",
  "highlights": [
    "One-file-per-command structure with a separate command deploy script",
    "The three-second rule: defer, then edit the reply",
    "Minimal intents; no privileged Message Content intent unless you really need it",
    "Skill that adds, registers and documents a new slash command"
  ],
  "version": "1.1.0",
  "license": "MIT",
  "author": {
    "name": "Agent Loadout",
    "url": "https://loadout.jnw-it.com"
  },
  "categories": [
    "bots"
  ],
  "stacks": [
    "discord",
    "nodejs",
    "typescript"
  ],
  "files": [
    {
      "path": "AGENTS.md",
      "role": "instructions",
      "description": "Discord bot project rules.",
      "skill": null,
      "agent": null,
      "install_to": {
        "claude-code": "AGENTS.md",
        "codex": "AGENTS.md",
        "cursor": "AGENTS.md",
        "gemini-cli": "AGENTS.md",
        "copilot": "AGENTS.md"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/AGENTS.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/latest/AGENTS.md",
      "bytes": 3625,
      "lang": "markdown",
      "content": "# AGENTS.md: Discord bot (TypeScript, discord.js v14)\n\n## Layout\n```\nsrc/\n  index.ts            creates the Client, loads commands and events, logs in\n  deploy-commands.ts  registers slash commands with Discord (run by hand, not on every start)\n  commands/<name>.ts  one slash command per file: exports `data` (SlashCommandBuilder) and `execute(interaction)`\n  events/<name>.ts    one gateway event per file: exports `name`, `once?`, `execute(...)`\n  lib/                shared helpers (embeds, db, permissions)\n```\nRun with `npm run dev` (tsx watch) locally. `npm run deploy` runs `deploy-commands.ts`.\n\n## Command file shape\nEach file in `src/commands/` exports `data` and `execute`, nothing else:\n\n```ts\nimport { SlashCommandBuilder, type ChatInputCommandInteraction } from \"discord.js\";\n\nexport const data = new SlashCommandBuilder()\n  .setName(\"roll\")                       // lowercase, 1–32 chars, no spaces\n  .setDescription(\"Roll a dice\")         // required, 1–100 chars\n  .addIntegerOption((o) => o.setName(\"sides\").setDescription(\"Number of sides\").setMinValue(2).setMaxValue(1000));\n\nexport async function execute(interaction: ChatInputCommandInteraction) {\n  const sides = interaction.options.getInteger(\"sides\") ?? 6;\n  const result = 1 + Math.floor(Math.random() * sides);\n  await interaction.reply({ content: `🎲 ${result} (d${sides})` });\n}\n```\n\n## Token and secrets\n- `DISCORD_TOKEN`, `DISCORD_CLIENT_ID`, `DISCORD_GUILD_ID` come from `.env` (git-ignored). `.env.example` lists them with dummy values.\n- Never print the token, commit it, or paste it into an embed or error message. If it leaks, the human must reset it in the\n  Developer Portal → Bot → Reset Token. Discord also auto-revokes tokens it finds in public repos.\n\n## Intents\n- Request the minimum: `GatewayIntentBits.Guilds` covers slash commands.\n- `GuildMembers`, `GuildPresences` and `MessageContent` are **privileged**: they must be switched on in the Developer\n  Portal and are reviewed once a bot is in 100+ servers. Prefer slash commands over reading message content.\n\n## Interactions\n- Discord gives you **3 seconds** to respond. If work might take longer (API calls, database, AI), call\n  `await interaction.deferReply()` first, then `await interaction.editReply(...)`.\n- Private replies: `interaction.reply({ content, flags: MessageFlags.Ephemeral })`. (`ephemeral: true` is deprecated.)\n- Every `execute` is wrapped by the loader in try/catch. On error: log it, then reply or follow up with a short\n  ephemeral \"Something went wrong\" — never the stack trace.\n- Check permissions for moderation commands with `.setDefaultMemberPermissions(...)` on the builder *and* re-check in code.\n- Validate options: string lengths, number ranges (`.setMinValue/.setMaxValue`), and never pass user text into shell commands or eval.\n\n## Registering commands\n- During development register to one guild (`Routes.applicationGuildCommands(clientId, guildId)`) — updates are instant.\n- Global registration (`Routes.applicationCommands(clientId)`) is for release. Do not register on every bot start; there are rate limits.\n- After changing a command's `data`, remind the human to run `npm run deploy`.\n\n## Style\n- TypeScript strict, ES modules, `async/await`. No `any` on interaction types: use `ChatInputCommandInteraction`.\n- Keep command files small; move logic into `lib/` when two commands share it.\n- Rate-limit costly commands per user (a simple in-memory cooldown map is fine to start).\n\n## Before you say \"done\"\n`npm run build` (tsc) passes, the new command is in `commands/`, and you have told the human whether to re-run deploy.\n"
    },
    {
      "path": "CLAUDE.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Claude Code.",
      "skill": null,
      "agent": "claude-code",
      "install_to": {
        "claude-code": "CLAUDE.md",
        "codex": null,
        "cursor": null,
        "gemini-cli": null,
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/CLAUDE.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/latest/CLAUDE.md",
      "bytes": 376,
      "lang": "markdown",
      "content": "# CLAUDE.md\n\n@AGENTS.md\n\n## Claude Code notes\n- Skills from this loadout live in `.claude/skills/`: `add-slash-command`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From the Agent Loadout \"Discord bot (TypeScript)\" v1.1.0 — https://loadout.jnw-it.com/loadouts/discord-bot-ts/ -->\n"
    },
    {
      "path": "GEMINI.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Gemini CLI.",
      "skill": null,
      "agent": "gemini-cli",
      "install_to": {
        "claude-code": null,
        "codex": null,
        "cursor": null,
        "gemini-cli": "GEMINI.md",
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/GEMINI.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/latest/GEMINI.md",
      "bytes": 377,
      "lang": "markdown",
      "content": "# GEMINI.md\n\n@./AGENTS.md\n\n## Gemini CLI notes\n- Skills from this loadout live in `.agents/skills/`: `add-slash-command`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From the Agent Loadout \"Discord bot (TypeScript)\" v1.1.0 — https://loadout.jnw-it.com/loadouts/discord-bot-ts/ -->\n"
    },
    {
      "path": ".cursor/rules/discord-commands.mdc",
      "role": "rules",
      "description": "Rule for files in src/commands/.",
      "skill": null,
      "agent": null,
      "install_to": {
        "claude-code": null,
        "codex": null,
        "cursor": ".cursor/rules/discord-commands.mdc",
        "gemini-cli": null,
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/.cursor/rules/discord-commands.mdc",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/latest/.cursor/rules/discord-commands.mdc",
      "bytes": 1144,
      "lang": "markdown",
      "content": "---\ndescription: Shape of a discord.js v14 slash command file\nglobs: src/commands/**/*.ts\nalwaysApply: false\n---\n\nEach file exports `data` and `execute`, nothing else:\n\n```ts\nimport { SlashCommandBuilder, MessageFlags, type ChatInputCommandInteraction } from \"discord.js\";\n\nexport const data = new SlashCommandBuilder()\n  .setName(\"roll\")                       // lowercase, 1–32 chars, no spaces\n  .setDescription(\"Roll a dice\")         // required, 1–100 chars\n  .addIntegerOption((o) => o.setName(\"sides\").setDescription(\"Number of sides\").setMinValue(2).setMaxValue(1000));\n\nexport async function execute(interaction: ChatInputCommandInteraction) {\n  const sides = interaction.options.getInteger(\"sides\") ?? 6;\n  const result = 1 + Math.floor(Math.random() * sides);\n  await interaction.reply({ content: `🎲 ${result} (d${sides})` });\n}\n```\n\n- If `execute` awaits anything slow, `deferReply()` first and `editReply()` after.\n- Use `flags: MessageFlags.Ephemeral` for private replies, not `ephemeral: true`.\n- Required options must be added before optional ones.\n- Do not register commands from here; that is `src/deploy-commands.ts`.\n"
    },
    {
      "path": "skills/add-slash-command/SKILL.md",
      "role": "skill",
      "description": "Adds and registers a new slash command.",
      "skill": "add-slash-command",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/add-slash-command/SKILL.md",
        "codex": ".agents/skills/add-slash-command/SKILL.md",
        "cursor": ".agents/skills/add-slash-command/SKILL.md",
        "gemini-cli": ".agents/skills/add-slash-command/SKILL.md",
        "copilot": ".agents/skills/add-slash-command/SKILL.md"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/skills/add-slash-command/SKILL.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/latest/skills/add-slash-command/SKILL.md",
      "bytes": 1650,
      "lang": "markdown",
      "content": "---\nname: add-slash-command\ndescription: Add a new slash command to a discord.js v14 bot, register it and document it. Use when the user asks for a new bot command, e.g. \"add a /poll command\".\n---\n\n# Add a slash command\n\n1. **Clarify in one line** if needed: command name, options (name, type, required?), who can use it, public or private reply.\n   Otherwise choose sensible defaults and state them.\n2. **Check the name is free:** look in `src/commands/` and in the `data.setName(...)` calls.\n3. **Create `src/commands/<name>.ts`** following the command file shape in AGENTS.md:\n   - `SlashCommandBuilder` with name, description, typed options with limits.\n   - Moderation or admin commands: `.setDefaultMemberPermissions(PermissionFlagsBits.<Perm>)` and `.setContexts(InteractionContextType.Guild)`\n     so they cannot run in DMs.\n   - `execute`: validate, `deferReply()` if anything is slow, do the work, reply.\n   - Handle the expected failures (missing permission, bad input, external API down) with a friendly ephemeral message.\n4. **Check the loader** in `src/index.ts` picks up new files automatically. If commands are listed by hand, add it there and in\n   `deploy-commands.ts`.\n5. **Type-check:** `npm run build` or `npx tsc --noEmit`.\n6. **Register:** tell the human to run `npm run deploy` (guild registration is instant). Only run it yourself if they ask and\n   `.env` is already set up — it uses the bot token.\n7. **Document:** add the command to the commands table in `README.md` (name, options, who can use it).\n8. **Report:** the file created, the command signature (`/poll question:<text> duration:<minutes>`), and how to try it.\n"
    }
  ],
  "requires": {
    "tools": [
      "node"
    ],
    "env": [
      "DISCORD_TOKEN",
      "DISCORD_CLIENT_ID",
      "DISCORD_GUILD_ID"
    ],
    "mcp": []
  },
  "tested": {
    "status": "untested",
    "last_run": null,
    "fixture": null,
    "per_agent": {
      "claude-code": "untested",
      "codex": "untested",
      "cursor": "untested",
      "gemini-cli": "untested",
      "copilot": "untested"
    }
  },
  "security": {
    "scanned": true,
    "scanner": "intake-precheck/1",
    "verdict": "clean",
    "network": true,
    "scripts": [],
    "notes": "Registering commands calls the Discord API with your bot token."
  },
  "featured": true,
  "deprecated": false,
  "last_checked": "2026-10-16",
  "published": "2026-10-15",
  "updated": "2026-10-16",
  "compatibility": {
    "claude-code": "native",
    "codex": "native",
    "cursor": "native",
    "gemini-cli": "native",
    "copilot": "native"
  },
  "counts": {
    "rules": 2,
    "skills": 1,
    "mcp": 0
  },
  "url": "https://loadout.jnw-it.com/loadouts/discord-bot-ts/",
  "api_url": "https://loadout.jnw-it.com/api/loadouts/discord-bot-ts.json",
  "verification": {
    "level": "verified-install",
    "status": "verified",
    "label": "Verified install",
    "verified": true,
    "checked_at": "2026-10-16T22:54:03Z",
    "commit": "ad36e852567aa3c87821d2a9e2fe4052fd983374",
    "content_current": true,
    "agent_tests": "not-run",
    "counts": {
      "pass": 9,
      "fail": 0,
      "skip": 0,
      "not_applicable": 1
    },
    "meaning": "Level 1: automated checks on the files (parse, install paths, MCP config, safety scan, references, size, licence, starter build). No AI agent was run.",
    "checks": [
      {
        "id": "listing",
        "name": "Listing",
        "status": "pass",
        "summary": "Schema valid; 5 files declared, none missing or extra",
        "details": [
          "tested.status: untested (agent tests are separate from Level 1)"
        ]
      },
      {
        "id": "parse",
        "name": "Files parse",
        "status": "pass",
        "summary": "All 5 files parse; front matter valid",
        "details": []
      },
      {
        "id": "adapters",
        "name": "Adapters",
        "status": "pass",
        "summary": "CLAUDE.md and GEMINI.md import AGENTS.md and are up to date",
        "details": [
          "CLAUDE.md: matches the generator",
          "GEMINI.md: matches the generator"
        ]
      },
      {
        "id": "install-paths",
        "name": "Install paths",
        "status": "pass",
        "summary": "Correct paths for all 5 agents; layouts re-discovered",
        "details": [
          "Claude Code: 3 files, 1 skill in .claude/skills/",
          "Codex CLI: 2 files, 1 skill in .agents/skills/",
          "Cursor: 3 files, 1 skill in .agents/skills/, 1 rule",
          "Gemini CLI: 3 files, 1 skill in .agents/skills/",
          "GitHub Copilot: 2 files, 1 skill in .agents/skills/"
        ]
      },
      {
        "id": "mcp",
        "name": "MCP config",
        "status": "n/a",
        "summary": "No MCP servers in this loadout",
        "details": []
      },
      {
        "id": "safety",
        "name": "Safety scan",
        "status": "pass",
        "summary": "Clean: no credentials, risky shell or hidden text",
        "details": [
          "Scanner intake-precheck/1: verdict clean; network no (listing says yes)"
        ]
      },
      {
        "id": "references",
        "name": "References",
        "status": "pass",
        "summary": "No cross-file references to check",
        "details": []
      },
      {
        "id": "size",
        "name": "Size limits",
        "status": "pass",
        "summary": "5 files, 7.0 KB total; largest AGENTS.md 3.5 KB",
        "details": [
          "Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule"
        ]
      },
      {
        "id": "licence",
        "name": "Licence",
        "status": "pass",
        "summary": "MIT, by Agent Loadout; text in the repository LICENSE",
        "details": []
      },
      {
        "id": "starter-build",
        "name": "Starter build",
        "status": "pass",
        "summary": "discord.js v14 bot in the loadout's layout type-checks (tsc strict) and the loadout's example command builds a valid slash command",
        "details": [
          "Applied 6 loadout files at every agent's paths",
          "Example from AGENTS.md:17 (ts) → src/commands/roll.ts",
          "Example from .cursor/rules/discord-commands.mdc:9 (ts) → src/commands/roll-cursor-rule.ts",
          "✓ install: npm ci --no-audit --no-fund",
          "✓ type-check and build (tsc): npm run build",
          "✓ commands serialise for Discord: node dist/check-commands.js",
          "Covers: src/commands/<name>.ts exports data + execute, loaded by src/index.ts (as AGENTS.md describes)",
          "Covers: Example commands from AGENTS.md and the Cursor rule compile and pass discord.js's builder validation (name/description limits)",
          "Covers: Does not log in or call Discord: no token needed"
        ]
      }
    ]
  },
  "install": {
    "claude-code": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "CLAUDE.md",
          "target": "CLAUDE.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/CLAUDE.md",
          "role": "adapter"
        },
        {
          "path": "skills/add-slash-command/SKILL.md",
          "target": ".claude/skills/add-slash-command/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/skills/add-slash-command/SKILL.md",
          "role": "skill"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/discord-bot-ts/claude-code.zip"
    },
    "codex": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/add-slash-command/SKILL.md",
          "target": ".agents/skills/add-slash-command/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/skills/add-slash-command/SKILL.md",
          "role": "skill"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/discord-bot-ts/codex.zip"
    },
    "cursor": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": ".cursor/rules/discord-commands.mdc",
          "target": ".cursor/rules/discord-commands.mdc",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/.cursor/rules/discord-commands.mdc",
          "role": "rules"
        },
        {
          "path": "skills/add-slash-command/SKILL.md",
          "target": ".agents/skills/add-slash-command/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/skills/add-slash-command/SKILL.md",
          "role": "skill"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/discord-bot-ts/cursor.zip"
    },
    "gemini-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "GEMINI.md",
          "target": "GEMINI.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/GEMINI.md",
          "role": "adapter"
        },
        {
          "path": "skills/add-slash-command/SKILL.md",
          "target": ".agents/skills/add-slash-command/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/skills/add-slash-command/SKILL.md",
          "role": "skill"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/discord-bot-ts/gemini-cli.zip"
    },
    "copilot": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/add-slash-command/SKILL.md",
          "target": ".agents/skills/add-slash-command/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/discord-bot-ts/1.1.0/skills/add-slash-command/SKILL.md",
          "role": "skill"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/discord-bot-ts/copilot.zip"
    }
  },
  "source_zip_url": "https://loadout.jnw-it.com/download/discord-bot-ts/source.zip"
}