Discord book club bot for suggesting and voting on books
Discord bot where members suggest books, vote via select menu, and the bot announces the winner using JSON file storage.
typescriptdiscord.jsnodejsvitestdotenv
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add discord-book-club-bot-for-suggesting-and-voting-on-books-075f80CLAUDE.md# the app
## Purpose
A Discord bot for one book club server. Members suggest books with `/suggest`. An admin opens a vote with `/vote-start`. Members vote in a select menu. `/vote-close` announces the winner.
Read these before starting work:
- `SPEC.md` holds the requirements.
- `PLAN.md` holds the phases, each ending in a `/goal` prompt.
- `REVIEW.md` holds the review checklist.
## Stack
- Node 22 LTS, TypeScript in strict mode
- discord.js v14
- vitest for tests
- dotenv for config
- Storage: a JSON file at `data/state.json`, written atomically (write a temp file, then rename it)
## Commands
- `npm install`: install dependencies
- `npm run build`: compile TypeScript
- `npm test`: run vitest
- `npm run dev`: run the bot with `tsx watch`
- `npm run dry-run`: simulated 3-person vote with a mock client (after Phase 4)
A task is done only when `npm run build` and `npm test` both exit 0.
## Layout
- `src/index.ts`: entry point, loads `.env`, logs in
- `src/commands/`: one file per slash command
- `src/vote.ts`: pure tally logic, no Discord imports
- `src/store.ts`: JSON persistence
- `tests/`: mirrors `src/`
## Gotchas
- `DISCORD_TOKEN` lives only in `.env`. Never commit it. Never print it.
- Slash commands register on startup. Guild commands update instantly. Global commands can take up to an hour.
- For development, register commands to the guild in `DISCORD_GUILD_ID`.
- A select menu allows at most 25 options.
- Interactions must be acknowledged within 3 seconds. Use `deferReply` for slow work.
## Conventions
- One vote per user per poll. A later click replaces the earlier vote.
- Ties are broken by a random pick among the tied titles. The announcement says so.
- Only one poll can be open at a time.
- Keep `vote.ts` pure and unit tested. Tests must not call the Discord API or need a network.
## Rules
- Ask before adding a dependency.
- Ask before deleting `data/state.json`.
- Never run `git push`, `git reset --hard`, or `rm -rf` without asking.
## Context habits
- Use `/clear` between unrelated tasks.
- Use `/compact` at natural breaks.
.claude/settings.json{
"model": "sonnet",
"permissions": {
"allow": [
"Bash(npm run build)",
"Bash(npm test)",
"Bash(npm install)",
"Bash(npm run dev)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git add *)",
"Bash(git commit *)"
],
"ask": [
"Bash(git push *)",
"Bash(npm publish *)",
"Bash(rm *)"
],
"deny": [
"Read(./.env)",
"Bash(git reset --hard *)",
"Bash(git push --force *)",
"Bash(rm -rf *)"
]
}
}
.claude/skills/discord-setup/SKILL.md---
name: discord-setup
description: Walks the user through creating the Discord application, copying the bot token and server ID into their own .env file, and inviting the bot to their server. Runs only when the user types the command.
disable-model-invocation: true
---
# Discord setup
Guide the user through the Discord setup step by step. Wait for the user to confirm each step before moving on.
## Security rules
- The bot token is like a password. Anyone with it can control the bot.
- The user pastes the token into `.env` themselves. Never ask the user to paste the token into this chat.
- Never ask for, read, print, or repeat a token or a server ID. Use placeholders in every example.
- Do not open `.env` to check values. To confirm the token line was changed, ask the user to run this and tell you only the number it prints:
`grep -c "^DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE$" .env`
A result of `0` means the placeholder was replaced. Do not ask what the new value is.
## Steps
1. **Create the application.** Open the Discord Developer Portal, choose New Application, and give it a name such as `the app`.
2. **Add a bot.** Open the Bot tab. Keep the default settings. No privileged intents are needed for v1.
3. **Get the token.** On the Bot tab, choose Reset Token and copy the new value. Discord shows it only once.
4. **Save the token yourself.** Open `.env` in your editor and set:
`DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE`
Replace the placeholder with your token. Save the file. Do not paste it into the chat.
5. **Get the server ID.** In Discord, open User Settings, then Advanced, and turn on Developer Mode. Right-click your server icon and choose Copy Server ID. Put it in `.env` as `DISCORD_GUILD_ID=YOUR_SERVER_ID_HERE`, replacing the placeholder yourself.
6. **Build the invite link.** In the Developer Portal, open OAuth2, then URL Generator.
- Scopes: `bot` and `applications.commands`.
- Bot permissions: Send Messages, Embed Links, Use Slash Commands.
- Open the generated URL in a browser, pick your server, and authorize.
7. **Check the server role.** In Discord server settings, confirm the bot role is above any role it must manage. Also make sure the admin role named in `ADMIN_ROLE_NAME` exists and that you hold it.
## If the token leaks
Act right away. A leaked token lets anyone run the bot.
1. In the Developer Portal, open the Bot tab and choose Reset Token. The old token stops working immediately.
2. Replace `DISCORD_TOKEN` in `.env` with the new value.
3. Restart the bot with `npm run dev`.
4. If the token was pasted into chat, a commit, or a shared log, tell the user to reset it now and check the git history with `git log -p -- .env` to confirm `.env` was never committed.
## Final check
Ask the user to run:
`npm run dev`
The output should show the bot's name after it logs in. Then the user should check the server member list: the bot should show as online.
If the bot does not come online, check these in order:
- `DISCORD_TOKEN` is set in `.env` and the token was reset after any copy error.
- The `.env` file is in the project root, not in a subfolder.
- The bot was invited with both scopes.
Stop after these checks and report what failed. Do not attempt to fix the token value yourself.
.claude/skills/verify/SKILL.md---
name: verify
description: Use after any code change in this bot and before saying a task is done. Runs the build, tests, dry run, and a secret check, then reports pass or fail for each.
---
# Verify
Use this skill to prove a change works. Do not call a task done until every check below passes.
## Checks, in order
Run each step from the project root. Stop at a failing step only to read its output. Then run the whole list again.
1. **Build.** Run `npm run build`. It must exit 0.
2. **Tests.** Run `npm test`. It must exit 0. If any test fails, list the failing test names.
3. **Dry run.** If `src/` contains the dry-run script (added in Phase 4), run `npm run dry-run`. It must print the expected winner for the simulated 3-person vote. If the script does not exist yet, mark this check "skipped (not yet built)" and say which phase adds it.
4. **Secret check.** Look for the bot token in changed files without printing it:
- Get the changed files with `git diff --name-only` and `git diff --cached --name-only`.
- Read the token value from `.env` into a shell variable. Do not echo it.
- Run `grep -l -F "$TOKEN" <changed files>` and report only the file names that match.
- If `TOKEN` is empty or still `YOUR_BOT_TOKEN_HERE`, say the check was skipped because no token is set. Do not guess.
## Rules
- Never run the bot against the real Discord server during verification. `npm run dev` is for the human, not for this skill.
- Never print, copy, or quote a token value. Report file names and line numbers only.
- Do not read `.env` into the conversation. The secret check reads it inside a shell command only.
## Report format
Return a short table. One row per check.
| Check | Result | Detail |
|---|---|---|
| Build | pass or fail | exact error line if failed |
| Tests | pass or fail | failing test names if failed |
| Dry run | pass, fail, or skipped | expected winner line, or error line |
| Secret check | pass, fail, or skipped | file names that matched, if any |
For each failure, include the exact error line under the table. Keep the error line short and remove anything that looks like a token.
## If a check fails
1. Find the cause and fix it.
2. Run the full list again from step 1, not only the step that failed. A fix in one place can break another.
3. Report the final table only after every check passes in the same run.
If the cause is not clear after two attempts, stop, report the table and the error, and ask the user how to proceed.
.env.example# Copy this file to .env and fill in your values. Never commit .env.
DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE
DISCORD_GUILD_ID=YOUR_SERVER_ID_HERE
ADMIN_ROLE_NAME=Book Club Admin
INSTALL.md# Install the the app
This guide takes you from an empty folder to a running bot and your first Claude Code session. Follow the steps in order.
## What you need
- A computer with a terminal.
- A Discord account and a server where you have admin rights.
- Node 22 LTS from nodejs.org. Confirm with `node --version`.
- Claude Code. Follow the official install steps, then confirm with `claude --version`.
## What the project files do
- CLAUDE.md: project memory. Claude reads it automatically at the start of each session.
- SPEC.md: what the bot does and what counts as done.
- PLAN.md: four build phases. Each phase ends with a /goal prompt.
- REVIEW.md: the checklist for reviewing changes before you merge.
- .claude/skills/: instructions Claude loads only when needed. `verify` runs the checks. `discord-setup` walks you through Discord.
## Create the project
1. Make an empty folder named `the-app` and open a terminal in it.
2. Run `git init`.
3. Copy CLAUDE.md, SPEC.md, PLAN.md, REVIEW.md, INSTALL.md, .env.example, and the .claude folder into the folder.
4. Run `claude` in that folder and accept the trust prompt.
## Install the plugins
Inside Claude Code, check whether the official marketplace is there. It is usually added already. Only if it is missing, run:
```
/plugin marketplace add anthropics/claude-plugins-official
```
Then install the two plugins:
```
/plugin install code-review@claude-plugins-official
/plugin install commit-commands@claude-plugins-official
```
- code-review checks each phase's changes against REVIEW.md.
- commit-commands writes clear commit messages, so the history stays readable.
## Set up Discord
1. Run `/discord-setup` inside Claude Code and follow the steps.
2. In a terminal, run `cp .env.example .env`.
3. Open .env in an editor and paste your token and server ID yourself.
Keep your token private. It works like a password. Never paste it into the chat, and never commit .env. Check `git status` before each commit.
## Start Phase 1
Paste this kickoff prompt into Claude Code:
```
/goal Phase 1 of PLAN.md is complete. Done means: package.json, tsconfig.json (strict), vitest config, .gitignore (node_modules, .env, data/), and src/index.ts exist; src/index.ts loads .env with dotenv and logs in with DISCORD_TOKEN using discord.js v14; and running `npm run build && npm test; echo BUILD=$? TEST=$?` prints BUILD=0 TEST=0 in the output shown in this conversation. Check: run that exact command and show its full output. Constraints: never print or commit the token; do not add dependencies beyond discord.js, dotenv, typescript, vitest, tsx, and @types/node; do not implement any slash command yet; do not touch files outside this project folder; do not run git push. Turn bound: stop after 25 turns. If the condition is not met by then, stop and report exactly which check fails and why.
```
Read the final output yourself. You should see `BUILD=0 TEST=0`.
## Move through the phases
After each phase:
1. Read Claude's summary and the check output.
2. Optionally run `/code-review` for a second look against REVIEW.md.
3. Run `/clear`.
4. Paste the next phase's /goal prompt from PLAN.md.
Repeat for Phases 2 to 4.
## Keep sessions cheap
- `/clear` between unrelated tasks, so old conversation does not cost tokens.
- `/compact` at natural breaks, to shorten the conversation and keep the key points.
- `/usage` shows where your tokens went.
- `/goal clear` stops a running goal. Use it if Claude goes in circles.
## Run the bot
After Phase 4, run `npm run dev` in a terminal. The bot should log its name and show as online in your server. Stop it with Ctrl+C.
## Troubleshooting
- **Slash commands do not show:** wait a minute. Check that DISCORD_GUILD_ID in .env is your server ID. Global commands can take up to an hour.
- **Bot is offline:** check DISCORD_TOKEN in .env. If the token leaked, reset it in the Discord Developer Portal and update .env.
- **Permission errors:** check the bot's role in Server Settings. Check that the invite used the scopes `bot` and `applications.commands` with Send Messages, Embed Links, and Use Slash Commands.
PLAN.md# Build plan
Four phases. Each phase ends with a `/goal` prompt that names the check, the constraints, and a turn bound of 25 turns.
Between phases, run `/clear`. Use the verify skill at the end of every phase.
Model guidance: use Sonnet for implementation. Use Opus only for the Phase 2 tie logic review, and only if the tests disagree with SPEC.md.
## Phase 1: Scaffold
Build:
- `package.json` with scripts for build, test, dev, and dry-run
- `tsconfig.json` with strict mode on
- vitest config
- `.gitignore` covering `node_modules`, `.env`, and `data/`
- `src/index.ts` that loads `.env` with dotenv and logs in with `DISCORD_TOKEN` using discord.js v14
Done when:
- `npm run build` and `npm test` both exit 0.
Prompt to run:
```
/goal Phase 1 of PLAN.md is complete. Done means: package.json, tsconfig.json (strict), vitest config, .gitignore (node_modules, .env, data/), and src/index.ts exist. src/index.ts loads .env with dotenv and logs in with DISCORD_TOKEN using discord.js v14. Running `npm run build && npm test; echo BUILD=$? TEST=$?` prints BUILD=0 TEST=0 in the output shown in this conversation. Check: run that exact command and show its full output. Constraints: never print or commit the token; add no dependencies beyond discord.js, dotenv, typescript, vitest, tsx, and @types/node; implement no slash commands yet; do not touch files outside this project folder; do not run git push. Turn bound: stop after 25 turns. If the condition is not met by then, stop and report which check fails and why.
```
## Phase 2: Vote logic
Build:
- `src/vote.ts` with pure functions that tally votes, pick a winner, and break ties randomly.
- The random source is injected so tests are deterministic.
- `src/vote.ts` has no Discord imports.
Tests cover:
- no votes
- one vote
- a clear winner
- a tie
- a vote change
Done when:
- All tests pass.
- Coverage of `src/vote.ts` is above 90 percent.
Prompt to run:
```
/goal Phase 2 of PLAN.md is complete. Done means: src/vote.ts exports pure functions to tally votes, pick a winner, and break ties with an injected random source, with no Discord imports. Unit tests cover no votes, one vote, a clear winner, a tie, and a vote change. Check: run `npm test -- --coverage` and show the output; coverage for src/vote.ts must be above 90 percent and all tests must pass. Constraints: no network calls in tests; no new dependencies beyond what Phase 1 added; do not touch src/index.ts; do not run git push. Turn bound: stop after 25 turns. If the condition is not met by then, stop and report which check fails and why.
```
## Phase 3: Storage and commands
Build:
- `src/store.ts` with atomic JSON save and load. Save writes a temp file, then renames it.
- Bad JSON on load falls back to an empty state and logs a warning.
- Slash commands in `src/commands/`, one file each: `/suggest`, `/suggestions`, `/vote-start`, `/vote-close`, `/pick-random`.
- Suggestion validation: title 1 to 100 characters, link must be http or https.
Done when:
- Tests for the store and for suggestion validation pass.
- `npm run build` exits 0.
- The manual checklist in SPEC.md is ticked in the summary.
Prompt to run:
```
/goal Phase 3 of PLAN.md is complete. Done means: src/store.ts saves atomically (temp file then rename) and loads with a fallback to empty state on bad JSON; the five slash commands exist in src/commands/; suggestion validation enforces title length 1 to 100 and http or https links. Check: run `npm run build && npm test; echo BUILD=$? TEST=$?` and show the output; both must be 0. Then list each item in the manual checklist in SPEC.md with pass or fail, based on the tests and code. Constraints: never print or commit the token; do not call the real Discord API in tests; do not add dependencies without asking; do not run git push. Turn bound: stop after 25 turns. If the condition is not met by then, stop and report which check fails and why.
```
## Phase 4: Discord interaction and polish
Build:
- The select menu vote handler. Votes are keyed by user ID, and a later click replaces the earlier vote.
- Admin permission checks: Manage Guild or the role named in `ADMIN_ROLE_NAME`.
- Deferred replies for any work that may take more than 3 seconds.
- Error paths reply or defer, so no thinking state is left open.
- `scripts/dry-run.ts` behind `npm run dry-run`. It uses a mock client to post a simulated 3-person vote and prints the expected winner.
- `README.md` with run steps.
Done when:
- `npm run build` and `npm test` both exit 0.
- `npm run dry-run` prints the expected winner.
Prompt to run:
```
/goal Phase 4 of PLAN.md is complete. Done means: the select menu vote handler, admin checks, and deferred replies are in place; README.md has run steps; `npm run dry-run` posts a simulated 3-person vote through a mock client and prints the expected winner. Check: run `npm run build && npm test && npm run dry-run; echo BUILD=$? TEST=$? DRY=$?` and show the full output; all three must be 0 and the printed winner must match the vote set in the script. Constraints: the dry run must not contact Discord; never print or commit the token; no new dependencies without asking; do not run git push. Turn bound: stop after 25 turns. If the condition is not met by then, stop and report which check fails and why.
```
REVIEW.md# Review checklist
Check every item before merging a phase. Use it with /code-review and by hand.
## Secrets
- [ ] No tokens, server IDs, or .env contents appear in code, logs, tests, or committed files.
- [ ] .env is not staged or committed. Check with `git status` before every commit.
## Interactions
- [ ] Every slash command and select menu handler replies or defers within 3 seconds.
- [ ] Slow work uses deferReply, and the handler edits that reply when the work is done.
- [ ] Errors reply to the user. No handler leaves a "thinking" state open.
## Permissions
- [ ] Admin commands (/vote-start, /vote-close, /pick-random) check for Manage Guild or the role named in ADMIN_ROLE_NAME before doing anything.
- [ ] The permission check runs before any read or write of data/state.json.
## Data safety
- [ ] Writes to data/state.json are atomic: write a temp file, then rename it.
- [ ] A crash in the middle of a write cannot corrupt data/state.json.
- [ ] Bad JSON on load falls back to an empty state and logs a warning.
## Vote logic
- [ ] Votes are keyed by user ID, not by display name.
- [ ] A later click replaces the earlier vote. A user never counts twice.
- [ ] Ties are broken with the injected random source, so tests are deterministic.
- [ ] src/vote.ts imports nothing from discord.js.
## Input limits
- [ ] Titles are 1 to 100 characters.
- [ ] Links must be http or https.
- [ ] A select menu never has more than 25 options.
- [ ] Vote count option stays in the range 2 to 10.
## Tests
- [ ] New logic has unit tests in tests/, mirroring src/.
- [ ] No test calls the real Discord API or needs a network.
## Docs
- [ ] CLAUDE.md commands still match the scripts in package.json.
- [ ] README or INSTALL.md steps still work for a new user.
SPEC.md# the app spec
## Goal
The book club picks its next book fairly and quickly inside Discord.
## Commands
- `/suggest title:<text> [link:<url>]` adds a suggestion. One per user per title. Title is 1 to 100 characters. Link must be http or https.
- `/suggestions` lists open suggestions with who suggested each one.
- `/vote-start [count:2-10]` (admin only) opens a poll with the top suggestions. Default is 5.
- `/vote-close` (admin only) closes the poll, tallies the votes, and posts the winner.
- `/pick-random` (admin only) picks a random open suggestion.
Admin means the Manage Guild permission or the role named in `ADMIN_ROLE_NAME`.
## Voting rules
- One select menu in the channel, with at most 25 options.
- Each user has one vote. Changing a vote replaces the earlier one.
- Votes stay private to the bot until the poll closes.
- Only one poll can be open at a time.
## Result message
- Winner title, plus the link if one was given.
- Vote counts for every option.
- A note when a tie was broken randomly.
## Data
- Suggestions and polls are stored in `data/state.json`. Nothing else is stored.
- No message content is logged.
## Bot permissions
- Send Messages, Embed Links, Use Slash Commands.
- Scopes: `bot` and `applications.commands`.
## Out of scope for v1
- Multiple servers
- Reminders
- Reading history
- Goodreads integration
- A web dashboard
## Acceptance
A club of 10 people can suggest, vote, and see a winner in one evening with no manual steps beyond the admin commands.
## Manual checklist
Tick each item during the final check in PLAN.md Phase 3.
- [ ] `/suggest` adds a title and rejects a duplicate from the same user
- [ ] `/suggestions` lists open suggestions with names
- [ ] `/vote-start` opens one select menu and refuses a second poll while one is open
- [ ] Voting twice replaces the first vote
- [ ] `/vote-close` posts the winner, the link, and every count
- [ ] The tie note appears when a tie is broken randomly
- [ ] `/pick-random` picks an open suggestion
- [ ] A non-admin is refused on every admin command
- [ ] Restarting the bot keeps suggestions and polls