Imported from theHimanshuShekhar/BhayanakBot (
AGENTS.md). Install upstream withnpx skills add theHimanshuShekhar/BhayanakBot. Copyright stays with the author.
Repository Notes
Commands
pnpm dev # Run the bot with tsx watch
pnpm build # Compile TypeScript bot source to dist/
pnpm start # Run compiled bot output
pnpm lint # Biome lint
pnpm format # Biome format --write
pnpm check # Biome check --write
pnpm test # Run Vitest tests
pnpm test:watch # Run Vitest in watch mode
pnpm test:coverage # Run Vitest with coverage
pnpm db:push # Push schema changes directly in dev
pnpm db:generate # Generate migration files
pnpm db:migrate # Run migrations
pnpm db:studio # Open Drizzle Studio UI
pnpm web:dev # Run the Astro web frontend
pnpm web:build # Build the Astro web frontend to web/dist/
pnpm web:preview # Preview the built Astro frontend
Vitest tests live under tests/. tests/setup/globalSetup.ts points DATABASE_URL at TEST_DATABASE_URL or postgresql://postgres:postgres@localhost:5432/bhayanakbot_test, then runs migrations if that database is reachable. Integration tests may fail if the test Postgres database is not running.
Architecture
Framework: Sapphire Framework on Discord.js v14. Sapphire auto-discovers and loads stores from their directories; manual registration is not needed for standard stores.
Feature switches: src/lib/features.ts holds compile-time switches — RPG_ENABLED, LOCAL_LLM_ENABLED, PERSONALITY_GENERATION_ENABLED. All are currently false: RPG commands/handlers/tasks, Ollama startup + fallback, and personality generation are inactive, though their code remains. The custom loader strategy skips pieces for disabled subsystems, and help omits categories with no loaded commands. Flip a flag and its wiring comes back.
Stores:
| Store | Directory | Base class |
|---|---|---|
| Commands | src/commands/<category>/ |
Command / Subcommand |
| Listeners | src/listeners/<category>/ |
Listener |
| Interaction handlers | src/interaction-handlers/ |
InteractionHandler |
| Preconditions | src/preconditions/ |
AllFlowsPrecondition |
| Scheduled tasks | src/scheduled-tasks/ |
ScheduledTask |
Client: src/lib/BhayanakClient.ts extends SapphireClient. It adds player, bounded in-memory caches for snipe/edit-snipe data, anti-raid join tracking, user personality profiles, and guild personality profiles. It installs a custom Sapphire loader strategy so tsx development can load .ts, .cts, and .mts pieces.
Entry point: src/index.ts loads dotenv and Sapphire plugins, ensures/pulls and warms the Ollama model only when LOCAL_LLM_ENABLED, loads discord-player extractors plus discord-player-youtubei, registers music player events, logs in, starts scheduled tasks, and installs shutdown/process error handlers.
Database: src/lib/database.ts uses Drizzle ORM over a pg connection pool. Schema is in src/db/schema.ts; query helpers live in src/db/queries/.
Key query helpers:
guildSettings.ts— per-guild config for channels, roles, XP, auto-mod, anti-raid, personality, and random responses.archivedChannelMessages.ts— durable archive of non-bot messages from the Guess Who channel, with edit/delete tracking and filtered random selection for/guess_who.personalityTraining.ts— archive-backed personality training eligibility and cursor-window queries.personality.tsandguildPersonality.ts— generated user/guild personality profile storage and lookup helpers.rpg.ts— profiles, stats, XP, coins, jail, cooldowns, inventory, pets, properties, daily rewards, daily quests, and quest progress.modCases.ts— auto-incrementing per-guild case numbers, mutes/tempbans withexpiresAtandactiveflags.autoResponses.ts— static and LLM auto-responses with matching, regex, channel filters, mention requirements, chance, and trigger deletion.users.ts,roles.ts,tickets.ts,polls.ts,giveaways.ts,reminders.ts,suggestions.ts,afk.ts— feature-specific persistence helpers.
Commands And Web Docs
Major Discord command areas:
- RPG (currently disabled):
/profile,/train,/work,/crime,/shop,/inventory,/pet,/property,/daily,/quests. - Leveling:
/rank,/leaderboard,/rewards,/level-reset. - Moderation:
/ban,/kick,/mute,/unmute,/warn,/unban,/purge,/case,/history. - Music:
/play,/controls,/queue,/nowplaying,/volume,/shuffle,/loop. - Utility:
/ping,/serverinfo,/userinfo,/avatar,/snipe,/editsnipe,/afk,/remind,/help,/summarize,/personality. - Fun and games:
/8ball,/coinflip,/choose,/meme,/poll,/guess_who. - Server systems:
/config,/ticket-panel,/ticket,/suggest,/suggestion,/autorespond,/reaction-roles,/role-menu,/giveaway. - Minecraft:
/minecraftshowsmc.bhayanak.netstatus, Homestead version, live map link, required mods, and recommended mods.
When any Discord command is added, deleted, renamed, or behaviorally modified, update the web app command catalog and any relevant command documentation in web/src/data/commands.ts and web/src/content/commands/ in the same change.
Web Frontend
The web/ directory is an Astro site built with MDX content collections and Tailwind CSS v4.
- Source lives in
web/src/. - Pages live in
web/src/pages/. - Command catalog data is in
web/src/data/commands.ts. - Rich command detail docs live in
web/src/content/commands/. - Components live in
web/src/components/. pnpm web:devruns Astro withweb/astro.config.mjs.pnpm web:buildoutputs toweb/dist/.
The web app is not included in the bot TypeScript build because root tsconfig.json only includes src/**/*.
Environment Variables
Keep .env.example, README.md, Docker Compose, and this table in sync when environment variables change.
| Variable | Default | Purpose |
|---|---|---|
DISCORD_TOKEN |
required | Bot token |
DISCORD_CLIENT_ID |
optional | Discord application/client ID |
DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/bhayanakbot |
Postgres connection |
TEST_DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/bhayanakbot_test |
Vitest integration DB |
VALKEY_URL |
redis://localhost:6379 |
Valkey/Redis for Sapphire scheduled-task BullMQ backing |
POSTGRES_PASSWORD |
postgres |
Docker Postgres password |
OLLAMA_URL |
http://localhost:11434 |
Local Ollama instance; unused while local LLM infra is disabled |
OLLAMA_MODEL |
phi3:mini |
Model used by local Ollama features and fallback paths; unused while local LLM infra is disabled |
OLLAMA_DEBUG_CONTENT_LOGS |
false |
Set true only for local debugging to log raw Ollama prompts/responses |
OLLAMA_MAX_QUEUE_LENGTH |
25 |
Maximum queued Ollama requests before new requests are dropped |
OLLAMA_MAX_LOW_PRIORITY_QUEUE_LENGTH |
10 |
Maximum queued low-priority/background Ollama requests |
OLLAMA_QUEUE_WAIT_TIMEOUT_MS |
60000 |
Maximum time an Ollama request may wait in queue before being dropped |
ZEN_API_KEY |
unset | opencode Zen API key; responder and summary replies require it (plus ZEN_ALLOW_DISCORD_CONTENT=true). Personality generation is disabled independently |
ZEN_ALLOW_DISCORD_CONTENT |
false |
Must be true before Discord message content is sent to Zen; with local LLM disabled there is no other provider |
ZEN_BASE_URL |
https://opencode.ai/zen/go/v1 |
OpenAI-compatible Zen API base URL |
ZEN_MODEL |
deepseek-v4-flash |
Zen model for autoresponder, summaries, and user/guild personality generation |
WEB_PORT |
3000 in .env.example, 4321 Compose fallback |
Host port for the web service |
PUBLIC_BOT_INVITE_URL |
Discord OAuth URL | Public invite link shown by the web app |
PUBLIC_STATS_INTERVAL_MS |
300000 |
Bot stats snapshot refresh interval |
DASHBOARD_PRESENCE_INTERVAL_MS |
60000 |
Dashboard presence snapshot refresh interval |
YOUTUBE_COOKIE |
unset | Optional cookie for discord-player-youtubei |
NODE_ENV |
unset | Controls log level (debug outside production, info in production) |
TARGET_GUILD_ID |
199168135935295488 |
Guild gate for some LLM features |
TARGET_TEXT_CHANNEL_ID |
199168135935295488 |
Text-channel gate for responder features |
GUESS_WHO_CHANNEL_ID |
199168135935295488 |
Channel whose messages are archived and where /guess_who can run |
GUESS_WHO_BACKFILL_LIMIT |
1000 |
Maximum Discord messages scanned during startup backfill for Guess Who archive |
BOT_OWNER_ID |
unset | Optional privileged Discord user ID; blank/unset disables owner bypasses |
PALWORLD_API_URL |
http://127.0.0.1:8212 |
Palworld REST API base URL; the default assumes the server shares a host with the bot, which is false under Docker |
PALWORLD_ADMIN_KEY |
unset | Palworld AdminPassword, sent as HTTP Basic auth with username admin; setting it enables the live player tracker |
Message Archive And Personality
/guess_who and personality training use the durable archived_channel_messages table as source material. The archive stores non-bot messages from GUESS_WHO_CHANNEL_ID with original Discord message ID, guild/channel ID, author user ID, global username, server display name, content, Discord message timestamp, archive/update timestamps, and nullable edit/delete timestamps.
Startup runs backfillGuessWhoMessages() after clientReady to scan up to GUESS_WHO_BACKFILL_LIMIT accessible messages and upsert them by original Discord message ID. Live messageCreate, messageUpdate, and messageDelete listeners keep the archive current. Deleted messages stay in the archive for DBA-side history but are excluded from future game and personality training queries.
Personality profile builders read eligible archived messages in bounded cursor windows. User profile creation needs 100 eligible messages; later refreshes need 20. Guild culture profile creation needs 200 eligible messages; later refreshes need 40. Profiles must not quote source messages directly.
personalityEnabled is a guild admin operational toggle, not consent or opt-in/opt-out language. Normal AI replies may use personality context silently; /personality view user and /personality view guild are the explicit inspection surfaces. With PERSONALITY_GENERATION_ENABLED=false, profile generation (startup backfill, the refresh task, and /personality refresh) answers that generation is disabled.
Responder and summarize LLM calls use src/lib/llmProvider.ts: Zen only, and only when ZEN_API_KEY is configured and ZEN_ALLOW_DISCORD_CONTENT=true; otherwise calls return null and features degrade gracefully. The local Ollama fallback is inactive while LOCAL_LLM_ENABLED=false.
RPG Module (currently disabled)
src/lib/rpg/ is split into catalogs and helpers.
Catalogs have static data and no DB access:
jobs.ts— work/crime jobs withpayRange,cooldownMs,baseSuccessChance,dropTable, andjailSentenceMs.items.ts— shop items including tools, consumables, and boosts.pets.ts— pet catalog withprice,rarity, and bonus stat modifiers.properties.ts— property catalog withpriceandincomePerHour.questTemplates.ts— templates used by daily quest generation.
Helpers contain logic:
outcome.ts—rollOutcome(): stat bonus =(stat - 50) * 0.003per relevant stat, capped 5%-70%.cooldown.ts—getRemainingCooldown()andformatDuration()wrappers over DB cooldown queries.rewards.ts—applyJobRewards(): pays coins and resolves drop table rolls.flavorText.ts— local-Ollama narrative generation with fallback pools.
XP formula: level = floor(0.05 * sqrt(xp)) for RPG profiles, implemented in addXpToProfile().
Scheduled Tasks
Scheduled tasks are declared as ScheduledTask classes but scheduled manually in src/index.ts. Startup runs expireMutes, expireTempBans, sendReminders, endGiveaways, endPolls, reloadOnRestart, and syncPalworldTracker once in a non-blocking cold-start pass; generateDailyQuests and refreshPersonalityProfiles join only when their feature flags are on. Runtime intervals run moderation/reminder/poll/giveaway tasks every 30 seconds, Palworld sync every 10 minutes, plus the two flagged tasks at 6-hour/hourly cadence when enabled.
Music
Music uses discord-player v7 with DefaultExtractors and discord-player-youtubei. Event wiring is in src/lib/music/events.ts; embeds/components/errors/cache helpers are under src/lib/music/. Music commands are gated by IsDJ where appropriate. YOUTUBE_COOKIE may be passed to the YouTube extractor.
Interaction Handlers
customId uses : as a delimiter. Convention: <prefix>:<action>[:<page>]. The parse() method usually uses startsWith("<prefix>:") to claim interactions.
Current handlers include ticket buttons, music buttons, role menu select, poll votes, giveaway entry, and help menu/buttons. RPG jail actions and shop pagination stay unloaded while RPG is disabled.
Preconditions
Available preconditions in command constructors: GuildOnly, IsModerator, IsAdmin, IsDJ, TicketChannel. Moderator/Admin/DJ roles resolve from guildSettings, falling back to Discord permission flags.
Code Style
Biome enforces tabs, double quotes, trailing commas, semicolons, and 120-character line width. Run pnpm check before committing when code or formatted docs change.
All local imports use .js extensions for ESM resolution, even when importing .ts source files.
Root TypeScript uses module and moduleResolution set to NodeNext, strict mode, decorators enabled, declarations/source maps, and the #/* path alias for src/*. Vitest also maps # to src.
New pgEnum values in Drizzle schema require a migration (pnpm db:generate + pnpm db:migrate) because db:push can silently skip enum changes.
Testing
Use pnpm test for the full suite. DB-backed tests need a reachable test Postgres database.
Real-Ollama personality e2e coverage is opt-in with RUN_OLLAMA_E2E=1 pnpm vitest run tests/e2e/personality/ollama-profile-generation.test.ts; default runs skip it when not opted in.
Deployment
docker-compose.yml runs Postgres, Valkey, Ollama, the bot, and the Astro web server on the botnet bridge network. The production bot container runs migrations and starts the compiled bot with node dist/index.js.
The Dockerfile has these stages:
baseinstalls full dependencies and copies source/config.migrationis a small image target that can rundrizzle-kit migrateif used separately.productionis Debian-based, installs runtime dependencies, copies source, and runs the bot throughtsx.
Compose injects service hostnames for DATABASE_URL, VALKEY_URL, and OLLAMA_URL, so local .env values are mainly for non-Docker development.