docs: rename CLAUDE.md to AGENTS.md across the project
Some checks failed
Deploy to Production / test (push) Failing after 34s

This commit is contained in:
syntaxbullet
2026-04-06 14:18:56 +02:00
parent 0fc88323ea
commit 2b89fb7ede
14 changed files with 125 additions and 382 deletions

296
AGENTS.md
View File

@@ -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<ReturnType> => {
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` |

211
CLAUDE.md
View File

@@ -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<ReturnType> => {
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` |