diff --git a/AGENTS.md b/AGENTS.md index 3c5f6b9..7f68b10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,257 +1,211 @@ -# AGENTS.md - AI Coding Agent Guidelines +# CLAUDE.md -## Project Overview +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -AuroraBot is a Discord bot with a REST API built using Bun, Discord.js, and PostgreSQL with Drizzle ORM. - -## Build/Lint/Test Commands +## Commands ```bash # Development -bun --watch bot/index.ts # Run bot + API server with hot reload +bun --watch bot/index.ts # Run bot + API with hot reload +docker compose up # Start all services (bot, API, database) +docker compose up app # Start just the app (bot + API) +docker compose up db # Start just the database # Testing bun test # Run all tests -bun test path/to/file.test.ts # Run single test file +bun test path/to/file.test.ts # Run a single test file +bun test shared/modules/economy # Run tests in a directory bun test --watch # Watch mode -bun test shared/modules/economy # Run tests in directory # Database -bun run generate # Generate Drizzle migrations (Docker) -bun run migrate # Run migrations (Docker) -bun run db:push # Push schema changes (Docker) bun run db:push:local # Push schema changes (local) -bun run db:studio # Open Drizzle Studio +bun run db:studio # Open Drizzle Studio (localhost:4983) +bun run generate # Generate Drizzle migrations (Docker) +bun run migrate # Apply migrations (Docker) -# Docker (recommended for local dev) -docker compose up # Start bot, API, and database -docker compose up app # Start just the app (bot + API) -docker compose up db # Start just the database +# Admin Panel +bun run panel:dev # Start Vite dev server for dashboard +bun run panel:build # Build React dashboard for production ``` -## Project Structure +## Architecture + +Aurora is a Discord RPG bot + REST API running as a **single Bun process**. The bot and API share the same database client and services. ``` bot/ # Discord bot -├── commands/ # Slash commands by category +├── commands/ # Slash commands by category (admin, economy, inventory, etc.) ├── events/ # Discord event handlers -├── lib/ # Bot core (BotClient, handlers, loaders) -├── modules/ # Feature modules (views, interactions) -└── graphics/ # Canvas image generation +├── lib/ # BotClient, handlers, loaders, embed helpers, commandUtils +├── modules/ # Feature modules (views, interactions per domain) +└── graphics/ # Canvas-based image generation (@napi-rs/canvas) -shared/ # Shared between bot and web -├── db/ # Database schema and migrations -├── lib/ # Utils, config, errors, types -└── modules/ # Domain services (economy, user, etc.) +shared/ # Shared between bot and API +├── db/ # Drizzle ORM client + schema (users, economy, inventory, quests, etc.) +├── lib/ # env, config, errors, logger, types, utils +└── modules/ # Domain services (economy, user, inventory, quest, moderation, etc.) -web/ # API server -└── src/routes/ # API route handlers +api/ # REST API (Bun HTTP server) +└── src/routes/ # Route handlers for each domain + +panel/ # React admin dashboard (Vite + Tailwind + Radix UI) ``` +**Key architectural details:** +- Bot and API both import from `shared/` — do not duplicate logic. +- Services in `shared/modules/` are singleton objects, not classes. +- The database uses PostgreSQL 16+ via Drizzle ORM with `bigint` mode for Discord IDs and currency. +- Feature modules follow a strict file suffix convention (see below). + ## Import Conventions -Use path aliases defined in tsconfig.json: +Use path aliases (defined in `tsconfig.json`). Order: external packages → aliases → relative. ```typescript -// External packages first -import { SlashCommandBuilder } from "discord.js"; -import { eq } from "drizzle-orm"; - -// Path aliases second -import { economyService } from "@shared/modules/economy/economy.service"; -import { UserError } from "@shared/lib/errors"; -import { users } from "@db/schema"; -import { createErrorEmbed } from "@lib/embeds"; -import { handleTradeInteraction } from "@modules/trade/trade.interaction"; - -// Relative imports last -import { localHelper } from "./helper"; +import { SlashCommandBuilder } from "discord.js"; // external +import { economyService } from "@shared/modules/economy/economy.service"; // alias +import { users } from "@db/schema"; // alias +import { createErrorEmbed } from "@lib/embeds"; // alias +import { localHelper } from "./helper"; // relative ``` -**Available Aliases:** - -- `@/*` - bot/ -- `@shared/*` - shared/ -- `@db/*` - shared/db/ -- `@lib/*` - bot/lib/ -- `@modules/*` - bot/modules/ -- `@commands/*` - bot/commands/ - -## Naming Conventions - -| Element | Convention | Example | -| ---------------- | ----------------------- | ---------------------------------------- | -| Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` | -| Classes | PascalCase | `CommandHandler`, `UserError` | -| Functions | camelCase | `createCommand`, `handleShopInteraction` | -| Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` | -| Enums | PascalCase | `TimerType`, `TransactionType` | -| Services | camelCase singleton | `economyService`, `userService` | -| Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` | -| DB tables | snake_case | `users`, `moderation_cases` | -| Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` | +**Aliases:** +- `@/*` → `bot/` +- `@shared/*` → `shared/` +- `@db/*` → `shared/db/` +- `@lib/*` → `bot/lib/` +- `@modules/*` → `bot/modules/` +- `@commands/*` → `bot/commands/` ## Code Patterns +### Module File Suffixes + +- `*.view.ts` — Creates Discord embeds/components +- `*.interaction.ts` — Handles button/select/modal interactions +- `*.service.ts` — Business logic (lives in `shared/modules/`) +- `*.types.ts` — Module-specific TypeScript types +- `*.test.ts` — Tests (co-located with source) + +### Interaction Routing + +Component interactions (buttons, select menus, modals) flow through a centralized routing system: + +``` +Discord event → interactionCreate → ComponentInteractionHandler → interaction.routes.ts → *.interaction.ts +``` + +`ComponentInteractionHandler` (`bot/lib/handlers/ComponentInteractionHandler.ts`) iterates over the route table in `bot/lib/interaction.routes.ts`. Each route has a `predicate` that matches on `customId`, a lazy `handler` import, and a `method` name to call. The handler also provides centralized `UserError` / system error handling. + +**Route table (custom ID prefix → handler):** + +| Custom ID prefix | Handler file | Method | +| ------------------ | ----------------------------------------------- | ------------------------------ | +| `trade_`, `amount` | `bot/modules/trade/trade.interaction.ts` | `handleTradeInteraction` | +| `shop_buy_` | `bot/modules/economy/shop.interaction.ts` | `handleShopInteraction` | +| `lootdrop_` | `bot/modules/economy/lootdrop.interaction.ts` | `handleLootdropInteraction` | +| `trivia_` | `bot/modules/trivia/trivia.interaction.ts` | `handleTriviaInteraction` | +| `createitem_` | `bot/modules/admin/item_wizard.ts` | `handleItemWizardInteraction` | +| `enrollment` | `bot/modules/user/enrollment.interaction.ts` | `handleEnrollmentInteraction` | +| `feedback_` | `bot/modules/feedback/feedback.interaction.ts` | `handleFeedbackInteraction` | + +Routes are evaluated in order — the first matching predicate wins. Some modules (e.g., inventory with `inv_` prefix) handle interactions locally via message component collectors instead of the global route table. + ### Command Definition ```typescript export const commandName = createCommand({ - data: new SlashCommandBuilder() - .setName("commandname") - .setDescription("Description"), + data: new SlashCommandBuilder().setName("name").setDescription("desc"), execute: async (interaction) => { - await interaction.deferReply(); - // Implementation + await withCommandErrorHandling(interaction, async () => { + const result = await service.method(); + await interaction.editReply({ embeds: [createSuccessEmbed(result)] }); + }, { ephemeral: true }); }, }); ``` -### Service Pattern (Singleton Object) +`withCommandErrorHandling` (from `@lib/commandUtils`) handles `deferReply`, `UserError` display, and unexpected error logging automatically. + +### Service Pattern ```typescript export const serviceName = { methodName: async (params: ParamType): Promise => { return await withTransaction(async (tx) => { - // Database operations + // database operations }); }, }; ``` -### Module File Organization - -- `*.view.ts` - Creates Discord embeds/components -- `*.interaction.ts` - Handles button/select/modal interactions -- `*.types.ts` - Module-specific TypeScript types -- `*.service.ts` - Business logic (in shared/modules/) -- `*.test.ts` - Test files (co-located with source) - -## Error Handling - -### Custom Error Classes +### Error Handling ```typescript import { UserError, SystemError } from "@shared/lib/errors"; -// User-facing errors (shown to user) -throw new UserError("You don't have enough coins!"); - -// System errors (logged, generic message shown) -throw new SystemError("Database connection failed"); +throw new UserError("You don't have enough coins!"); // shown to user +throw new SystemError("DB connection failed"); // logged, generic message shown ``` -### Recommended: `withCommandErrorHandling` - -Use the `withCommandErrorHandling` utility from `@lib/commandUtils` to standardize -error handling across all commands. It handles `deferReply`, `UserError` display, -and unexpected error logging automatically. - -```typescript -import { withCommandErrorHandling } from "@lib/commandUtils"; - -export const myCommand = createCommand({ - data: new SlashCommandBuilder() - .setName("mycommand") - .setDescription("Does something"), - execute: async (interaction) => { - await withCommandErrorHandling( - interaction, - async () => { - const result = await service.method(); - await interaction.editReply({ embeds: [createSuccessEmbed(result)] }); - }, - { ephemeral: true } // optional: makes the deferred reply ephemeral - ); - }, -}); -``` - -Options: -- `ephemeral` — whether `deferReply` should be ephemeral -- `successMessage` — a simple string to send on success -- `onSuccess` — a callback invoked with the operation result -``` - -## Database Patterns - -### Transaction Usage +### Database Transactions ```typescript import { withTransaction } from "@/lib/db"; return await withTransaction(async (tx) => { - const user = await tx.query.users.findFirst({ - where: eq(users.id, discordId), - }); - - await tx - .update(users) - .set({ coins: newBalance }) - .where(eq(users.id, discordId)); - await tx.insert(transactions).values({ userId: discordId, amount, type }); - + const user = await tx.query.users.findFirst({ where: eq(users.id, id) }); + await tx.update(users).set({ coins: newBalance }).where(eq(users.id, id)); return user; -}, existingTx); // Pass existing tx if in nested transaction +}, existingTx); // pass existing tx for nested transactions ``` -### Schema Notes +### Testing -- Use `bigint` mode for Discord IDs and currency amounts -- Relations defined separately from table definitions -- Schema modules: `shared/db/schema/*.ts` (users, inventory, economy, quests, moderation) - -## Testing - -### Test File Structure +Mock modules **before** imports. Use `bun:test`. ```typescript import { describe, it, expect, mock, beforeEach } from "bun:test"; -// Mock modules BEFORE imports mock.module("@shared/db/DrizzleClient", () => ({ DrizzleClient: { query: mockQuery }, })); describe("serviceName", () => { - beforeEach(() => { - mockFn.mockClear(); - }); - + beforeEach(() => mockFn.mockClear()); it("should handle expected case", async () => { - // Arrange mockFn.mockResolvedValue(testData); - - // Act const result = await service.method(input); - - // Assert expect(result).toEqual(expected); - expect(mockFn).toHaveBeenCalledWith(expectedArgs); }); }); ``` -## Tech Stack +## Naming Conventions -- **Runtime:** Bun 1.0+ -- **Bot:** Discord.js 14.x -- **Web:** Bun HTTP Server (REST API) -- **Database:** PostgreSQL 16+ with Drizzle ORM -- **UI:** Discord embeds and components -- **Validation:** Zod -- **Testing:** Bun Test -- **Container:** Docker +| Element | Convention | Example | +| ---------------- | ---------------------- | -------------------------------- | +| Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` | +| Classes | PascalCase | `CommandHandler`, `UserError` | +| Functions | camelCase | `createCommand`, `handleShopInteraction` | +| Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` | +| Enums | PascalCase | `TimerType`, `TransactionType` | +| Services | camelCase singleton | `economyService`, `userService` | +| Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` | +| DB tables | snake_case | `users`, `moderation_cases` | +| Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` | +| API routes | kebab-case | `/api/guild-settings` | -## Key Files Reference +## Key Files -| Purpose | File | -| ------------- | ---------------------- | -| Bot entry | `bot/index.ts` | -| DB schema | `shared/db/schema.ts` | -| Error classes | `shared/lib/errors.ts` | -| Config loader | `shared/lib/config.ts` | -| Environment | `shared/lib/env.ts` | -| Embed helpers | `bot/lib/embeds.ts` | -| Command utils | `shared/lib/utils.ts` | -| Error handler | `bot/lib/commandUtils.ts` | +| Purpose | File | +| ----------------- | -------------------------- | +| Bot entry point | `bot/index.ts` | +| Discord client | `bot/lib/BotClient.ts` | +| DB schema index | `shared/db/schema.ts` | +| Error classes | `shared/lib/errors.ts` | +| Environment vars | `shared/lib/env.ts` | +| Config loader | `shared/lib/config.ts` | +| Embed helpers | `bot/lib/embeds.ts` | +| Command utils | `bot/lib/commandUtils.ts` | +| API server | `api/src/server.ts` | diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 7f68b10..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,211 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -```bash -# Development -bun --watch bot/index.ts # Run bot + API with hot reload -docker compose up # Start all services (bot, API, database) -docker compose up app # Start just the app (bot + API) -docker compose up db # Start just the database - -# Testing -bun test # Run all tests -bun test path/to/file.test.ts # Run a single test file -bun test shared/modules/economy # Run tests in a directory -bun test --watch # Watch mode - -# Database -bun run db:push:local # Push schema changes (local) -bun run db:studio # Open Drizzle Studio (localhost:4983) -bun run generate # Generate Drizzle migrations (Docker) -bun run migrate # Apply migrations (Docker) - -# Admin Panel -bun run panel:dev # Start Vite dev server for dashboard -bun run panel:build # Build React dashboard for production -``` - -## Architecture - -Aurora is a Discord RPG bot + REST API running as a **single Bun process**. The bot and API share the same database client and services. - -``` -bot/ # Discord bot -├── commands/ # Slash commands by category (admin, economy, inventory, etc.) -├── events/ # Discord event handlers -├── lib/ # BotClient, handlers, loaders, embed helpers, commandUtils -├── modules/ # Feature modules (views, interactions per domain) -└── graphics/ # Canvas-based image generation (@napi-rs/canvas) - -shared/ # Shared between bot and API -├── db/ # Drizzle ORM client + schema (users, economy, inventory, quests, etc.) -├── lib/ # env, config, errors, logger, types, utils -└── modules/ # Domain services (economy, user, inventory, quest, moderation, etc.) - -api/ # REST API (Bun HTTP server) -└── src/routes/ # Route handlers for each domain - -panel/ # React admin dashboard (Vite + Tailwind + Radix UI) -``` - -**Key architectural details:** -- Bot and API both import from `shared/` — do not duplicate logic. -- Services in `shared/modules/` are singleton objects, not classes. -- The database uses PostgreSQL 16+ via Drizzle ORM with `bigint` mode for Discord IDs and currency. -- Feature modules follow a strict file suffix convention (see below). - -## Import Conventions - -Use path aliases (defined in `tsconfig.json`). Order: external packages → aliases → relative. - -```typescript -import { SlashCommandBuilder } from "discord.js"; // external -import { economyService } from "@shared/modules/economy/economy.service"; // alias -import { users } from "@db/schema"; // alias -import { createErrorEmbed } from "@lib/embeds"; // alias -import { localHelper } from "./helper"; // relative -``` - -**Aliases:** -- `@/*` → `bot/` -- `@shared/*` → `shared/` -- `@db/*` → `shared/db/` -- `@lib/*` → `bot/lib/` -- `@modules/*` → `bot/modules/` -- `@commands/*` → `bot/commands/` - -## Code Patterns - -### Module File Suffixes - -- `*.view.ts` — Creates Discord embeds/components -- `*.interaction.ts` — Handles button/select/modal interactions -- `*.service.ts` — Business logic (lives in `shared/modules/`) -- `*.types.ts` — Module-specific TypeScript types -- `*.test.ts` — Tests (co-located with source) - -### Interaction Routing - -Component interactions (buttons, select menus, modals) flow through a centralized routing system: - -``` -Discord event → interactionCreate → ComponentInteractionHandler → interaction.routes.ts → *.interaction.ts -``` - -`ComponentInteractionHandler` (`bot/lib/handlers/ComponentInteractionHandler.ts`) iterates over the route table in `bot/lib/interaction.routes.ts`. Each route has a `predicate` that matches on `customId`, a lazy `handler` import, and a `method` name to call. The handler also provides centralized `UserError` / system error handling. - -**Route table (custom ID prefix → handler):** - -| Custom ID prefix | Handler file | Method | -| ------------------ | ----------------------------------------------- | ------------------------------ | -| `trade_`, `amount` | `bot/modules/trade/trade.interaction.ts` | `handleTradeInteraction` | -| `shop_buy_` | `bot/modules/economy/shop.interaction.ts` | `handleShopInteraction` | -| `lootdrop_` | `bot/modules/economy/lootdrop.interaction.ts` | `handleLootdropInteraction` | -| `trivia_` | `bot/modules/trivia/trivia.interaction.ts` | `handleTriviaInteraction` | -| `createitem_` | `bot/modules/admin/item_wizard.ts` | `handleItemWizardInteraction` | -| `enrollment` | `bot/modules/user/enrollment.interaction.ts` | `handleEnrollmentInteraction` | -| `feedback_` | `bot/modules/feedback/feedback.interaction.ts` | `handleFeedbackInteraction` | - -Routes are evaluated in order — the first matching predicate wins. Some modules (e.g., inventory with `inv_` prefix) handle interactions locally via message component collectors instead of the global route table. - -### Command Definition - -```typescript -export const commandName = createCommand({ - data: new SlashCommandBuilder().setName("name").setDescription("desc"), - execute: async (interaction) => { - await withCommandErrorHandling(interaction, async () => { - const result = await service.method(); - await interaction.editReply({ embeds: [createSuccessEmbed(result)] }); - }, { ephemeral: true }); - }, -}); -``` - -`withCommandErrorHandling` (from `@lib/commandUtils`) handles `deferReply`, `UserError` display, and unexpected error logging automatically. - -### Service Pattern - -```typescript -export const serviceName = { - methodName: async (params: ParamType): Promise => { - return await withTransaction(async (tx) => { - // database operations - }); - }, -}; -``` - -### Error Handling - -```typescript -import { UserError, SystemError } from "@shared/lib/errors"; - -throw new UserError("You don't have enough coins!"); // shown to user -throw new SystemError("DB connection failed"); // logged, generic message shown -``` - -### Database Transactions - -```typescript -import { withTransaction } from "@/lib/db"; - -return await withTransaction(async (tx) => { - const user = await tx.query.users.findFirst({ where: eq(users.id, id) }); - await tx.update(users).set({ coins: newBalance }).where(eq(users.id, id)); - return user; -}, existingTx); // pass existing tx for nested transactions -``` - -### Testing - -Mock modules **before** imports. Use `bun:test`. - -```typescript -import { describe, it, expect, mock, beforeEach } from "bun:test"; - -mock.module("@shared/db/DrizzleClient", () => ({ - DrizzleClient: { query: mockQuery }, -})); - -describe("serviceName", () => { - beforeEach(() => mockFn.mockClear()); - it("should handle expected case", async () => { - mockFn.mockResolvedValue(testData); - const result = await service.method(input); - expect(result).toEqual(expected); - }); -}); -``` - -## Naming Conventions - -| Element | Convention | Example | -| ---------------- | ---------------------- | -------------------------------- | -| Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` | -| Classes | PascalCase | `CommandHandler`, `UserError` | -| Functions | camelCase | `createCommand`, `handleShopInteraction` | -| Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` | -| Enums | PascalCase | `TimerType`, `TransactionType` | -| Services | camelCase singleton | `economyService`, `userService` | -| Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` | -| DB tables | snake_case | `users`, `moderation_cases` | -| Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` | -| API routes | kebab-case | `/api/guild-settings` | - -## Key Files - -| Purpose | File | -| ----------------- | -------------------------- | -| Bot entry point | `bot/index.ts` | -| Discord client | `bot/lib/BotClient.ts` | -| DB schema index | `shared/db/schema.ts` | -| Error classes | `shared/lib/errors.ts` | -| Environment vars | `shared/lib/env.ts` | -| Config loader | `shared/lib/config.ts` | -| Embed helpers | `bot/lib/embeds.ts` | -| Command utils | `bot/lib/commandUtils.ts` | -| API server | `api/src/server.ts` | diff --git a/api/src/CLAUDE.md b/api/src/AGENTS.md similarity index 100% rename from api/src/CLAUDE.md rename to api/src/AGENTS.md diff --git a/panel/CLAUDE.md b/panel/AGENTS.md similarity index 100% rename from panel/CLAUDE.md rename to panel/AGENTS.md diff --git a/shared/db/CLAUDE.md b/shared/db/AGENTS.md similarity index 100% rename from shared/db/CLAUDE.md rename to shared/db/AGENTS.md diff --git a/shared/modules/economy/CLAUDE.md b/shared/modules/economy/AGENTS.md similarity index 100% rename from shared/modules/economy/CLAUDE.md rename to shared/modules/economy/AGENTS.md diff --git a/shared/modules/feature-flags/CLAUDE.md b/shared/modules/feature-flags/AGENTS.md similarity index 100% rename from shared/modules/feature-flags/CLAUDE.md rename to shared/modules/feature-flags/AGENTS.md diff --git a/shared/modules/guild-settings/CLAUDE.md b/shared/modules/guild-settings/AGENTS.md similarity index 100% rename from shared/modules/guild-settings/CLAUDE.md rename to shared/modules/guild-settings/AGENTS.md diff --git a/shared/modules/inventory/CLAUDE.md b/shared/modules/inventory/AGENTS.md similarity index 100% rename from shared/modules/inventory/CLAUDE.md rename to shared/modules/inventory/AGENTS.md diff --git a/shared/modules/leveling/CLAUDE.md b/shared/modules/leveling/AGENTS.md similarity index 100% rename from shared/modules/leveling/CLAUDE.md rename to shared/modules/leveling/AGENTS.md diff --git a/shared/modules/moderation/CLAUDE.md b/shared/modules/moderation/AGENTS.md similarity index 100% rename from shared/modules/moderation/CLAUDE.md rename to shared/modules/moderation/AGENTS.md diff --git a/shared/modules/quest/CLAUDE.md b/shared/modules/quest/AGENTS.md similarity index 100% rename from shared/modules/quest/CLAUDE.md rename to shared/modules/quest/AGENTS.md diff --git a/shared/modules/trade/CLAUDE.md b/shared/modules/trade/AGENTS.md similarity index 100% rename from shared/modules/trade/CLAUDE.md rename to shared/modules/trade/AGENTS.md diff --git a/shared/modules/trivia/CLAUDE.md b/shared/modules/trivia/AGENTS.md similarity index 100% rename from shared/modules/trivia/CLAUDE.md rename to shared/modules/trivia/AGENTS.md