# AGENTS.md - AI Coding Agent Guidelines

## Project Overview

AuroraBot is a Discord bot with a REST API built using Bun, Discord.js, and PostgreSQL with Drizzle ORM.

## Build/Lint/Test Commands

```bash
# Development
bun --watch bot/index.ts         # Run bot + API server with hot reload

# Testing
bun test                         # Run all tests
bun test path/to/file.test.ts    # Run single test file
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

# 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
```

## Project Structure

```
bot/                    # Discord bot
├── commands/           # Slash commands by category
├── events/             # Discord event handlers
├── lib/                # Bot core (BotClient, handlers, loaders)
├── modules/            # Feature modules (views, interactions)
└── graphics/           # Canvas image generation

shared/                 # Shared between bot and web
├── db/                 # Database schema and migrations
├── lib/                # Utils, config, errors, types
└── modules/            # Domain services (economy, user, etc.)

web/                    # API server
└── src/routes/         # API route handlers
```

## Import Conventions

Use path aliases defined in tsconfig.json:

```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";
```

**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_`             |

## Code Patterns

### Command Definition

```typescript
export const commandName = createCommand({
  data: new SlashCommandBuilder()
    .setName("commandname")
    .setDescription("Description"),
  execute: async (interaction) => {
    await interaction.deferReply();
    // Implementation
  },
});
```

### Service Pattern (Singleton Object)

```typescript
export const serviceName = {
  methodName: async (params: ParamType): Promise<ReturnType> => {
    return await withTransaction(async (tx) => {
      // 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

```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");
```

### Standard Error Pattern

```typescript
try {
  const result = await service.method();
  await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
} catch (error) {
  if (error instanceof UserError) {
    await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
  } else {
    console.error("Unexpected error:", error);
    await interaction.editReply({
      embeds: [createErrorEmbed("An unexpected error occurred.")],
    });
  }
}
```

## Database Patterns

### Transaction Usage

```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 });

  return user;
}, existingTx); // Pass existing tx if in nested transaction
```

### Schema Notes

- 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

```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();
  });

  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

- **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

## Key Files Reference

| 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`  |
