Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .claude/commands/add-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
Create a new button interaction handler for this bot.ts project.

The user will provide: a key/name for the button, a label, a description, and any typed parameters it receives.

If the user hasn't provided details, ask for:
1. Button key (kebab-case, unique identifier, e.g. `confirm-delete`)
2. Display label (shown on the button)
3. Short description
4. Parameters the button receives (name: type pairs, e.g. `userId: string`, `amount: number`)

Then create the file at `src/buttons/<key>.ts` (Biome style: tabs, no semicolons, double quotes):

### Button without parameters

```typescript
import { Button } from "#core/button"

export default new Button({
key: "my-button",
description: "My button",
builder: (builder) => builder.setLabel("Click me"),
async run(interaction) {
await interaction.deferUpdate()
await interaction.followUp({
content: "You clicked the button!",
ephemeral: true,
})
},
})
```

### Button with typed parameters

```typescript
import { Button } from "#core/button"

export type ConfirmDeleteButtonParams = {
targetId: string
guildId: string
}

export default new Button<ConfirmDeleteButtonParams>({
key: "confirm-delete",
description: "Confirm deletion of a resource",
builder: (builder) =>
builder.setLabel("Confirm Delete").setStyle(ButtonStyle.Danger),
async run(interaction, { targetId, guildId }) {
await interaction.deferUpdate()
// perform deletion using targetId and guildId
await interaction.followUp({
content: `Deleted resource ${targetId} from guild ${guildId}.`,
ephemeral: true,
})
},
})
```

### Using the button in a command or listener

```typescript
import discord from "discord.js"
import confirmDeleteButton from "#buttons/confirm-delete"

await channel.send({
content: "Are you sure?",
components: [
new discord.ActionRowBuilder<discord.MessageActionRowComponentBuilder>().addComponents(
confirmDeleteButton.create({ targetId: "abc123", guildId: message.guildId! })
),
],
})
```

### ButtonStyle values (discord.js)

- `ButtonStyle.Primary` — blue
- `ButtonStyle.Secondary` — grey
- `ButtonStyle.Success` — green
- `ButtonStyle.Danger` — red
- `ButtonStyle.Link` — link (use `setURL` instead of `create`)

Import `ButtonStyle` from `discord.js` when needed.

### Rules

- The `key` must be unique across all button files
- Parameters are passed as a typed object to the `run` function
- Always call `interaction.deferUpdate()` or `interaction.deferReply()` first in `run`
- Use `interaction.followUp()` for responses after deferral
- Parameters are serialized into the button's custom ID — keep them short (total customId ≤ 100 chars)

After creating the file, remind the user to run `bun run format`.
60 changes: 60 additions & 0 deletions .claude/commands/add-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Create a new textual (prefix) command for this bot.ts project.

The user will provide: command name, description, channel type (guild/dm/all), and optionally arguments (positional, options, flags, rest), cooldown, and botOwnerOnly flag.

If the user hasn't provided details, ask for:
1. Command name (kebab-case, e.g. `daily-reward`)
2. Short description
3. Channel type: `guild`, `dm`, or `all`
4. Arguments needed (if any)

Then create the file at `src/commands/<name>.ts` following this pattern exactly (Biome style: tabs, no semicolons, double quotes):

```typescript
import { Command } from "#core/command"

export default new Command({
name: "<name>",
description: "<description>",
channelType: "<channelType>",
async run(message) {
// todo: implement
},
})
```

### Rules to follow

- File name matches the command `name` exactly
- Use `CooldownType` from `#core/command` if a cooldown is requested
- Add `botOwnerOnly: true` only when explicitly requested
- For positional args: use `positional: [{ name, description, type, required }]`
- For option args: use `options: [{ name, description, type }]`
- For flag args: use `flags: [{ name, flag, description }]` (single letter for `flag`)
- For rest: use `rest: { name, description, required }`
- Available argument types: `string`, `number`, `boolean`, `regex`, `date`, `duration`, `json`, `array`, `string[]`, `number[]`, `boolean[]`, `date[]`, `user`, `member`, `channel`, `role`, `emote`, `invite`, `command`, `slashCommand`
- Custom types are defined in `src/types.ts`
- Access parsed args via `message.args.<argName>`
- `message.triggerCoolDown()` must be called inside `run` if a cooldown is defined

### Example with arguments and cooldown

```typescript
import { Command, CooldownType } from "#core/command"

export default new Command({
name: "daily-reward",
description: "Claim your daily reward",
channelType: "guild",
cooldown: {
duration: 1000 * 60 * 60 * 24, // 24 hours
type: CooldownType.Global,
},
async run(message) {
message.triggerCoolDown()
await message.channel.send(`${message.author} claimed their daily reward!`)
},
})
```

After creating the file, remind the user that they can run `bun run format` to apply Biome formatting.
101 changes: 101 additions & 0 deletions .claude/commands/add-cron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
Create a new cron job (scheduled task) for this bot.ts project.

The user will provide: a name, description, schedule, and the task to execute.

If the user hasn't provided details, ask for:
1. Cron job name (camelCase, e.g. `dailyReport`)
2. Short description
3. Schedule (see options below)
4. Should it run immediately on bot start? (yes/no)

Then create the file at `src/cron/<name>.ts` (Biome style: tabs, no semicolons, double quotes).

### Schedule options

**Interval key** (simplest):
```typescript
schedule: "minutely" // | "hourly" | "daily" | "weekly" | "monthly" | "yearly"
```

**Simple interval** (every N units):
```typescript
schedule: { type: "hour", duration: 6 } // every 6 hours
// type: "minute" | "hour" | "day" | "week" | "month" | "year"
```

**Advanced** (precise cron-style):
```typescript
import { Cron, CronDayOfWeek, CronMonth } from "#core/cron"

schedule: {
minute: 30, // 0-59 or "*"
hour: 9, // 0-23 or "*"
dayOfMonth: "*", // 1-31 or "*"
month: "*", // 1-12 or CronMonth enum or "*"
dayOfWeek: CronDayOfWeek.Monday, // 0-6 or CronDayOfWeek enum or "*"
}
```

### Basic example

```typescript
import { Cron } from "#core/cron"

export default new Cron({
name: "daily-report",
description: "Sends the daily activity report",
schedule: "daily",
runOnStart: false,
async run() {
// todo: implement report logic
},
})
```

### Advanced example (every Monday at 9:30 AM)

```typescript
import { Cron, CronDayOfWeek } from "#core/cron"

export default new Cron({
name: "weekly-digest",
description: "Sends weekly digest every Monday at 9:30 AM",
schedule: {
minute: 30,
hour: 9,
dayOfWeek: CronDayOfWeek.Monday,
},
runOnStart: false,
async run() {
// todo: send weekly digest
},
})
```

### Example accessing Discord client

```typescript
import { Cron } from "#core/cron"
import * as app from "#all"

export default new Cron({
name: "status-update",
description: "Updates bot status every hour",
schedule: "hourly",
runOnStart: true,
async run() {
app.client.user?.setActivity(`Serving ${app.client.guilds.cache.size} servers`)
},
})
```

### Rules

- File name should match the cron `name` in kebab-case or camelCase
- Timezone is read from `BOT_TIMEZONE` env var — configure it in `.env`
- `runOnStart: true` triggers the cron immediately when the bot boots
- `this.ranCount` tracks how many times this cron has executed
- Import `* as app from "#all"` to access the client, env, logger, etc.
- Cron jobs start automatically when the bot is ready (via `cron.clientReady.native.ts`)

After creating the file, remind the user to run `bun run format`.
92 changes: 92 additions & 0 deletions .claude/commands/add-listener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
Create a new Discord event listener for this bot.ts project.

The user will provide: the Discord.js event name, a category/purpose name, and a description.

If the user hasn't provided details, ask for:
1. Discord.js event name (e.g. `guildMemberAdd`, `messageDelete`, `voiceStateUpdate`)
2. Category/purpose (e.g. `welcome`, `modlog`, `stats`) — used as file name prefix
3. Short description of what the listener does
4. Should it run only once? (yes/no — default no)

Then create the file at `src/listeners/<category>.<eventName>.ts` (Biome style: tabs, no semicolons, double quotes):

```typescript
import { Listener } from "#core/listener"

export default new Listener({
event: "<eventName>",
description: "<description>",
async run(<args>) {
// todo: implement
},
})
```

### Common events and their arguments

| Event | Arguments |
|---|---|
| `messageCreate` | `message: Message` |
| `messageDelete` | `message: Message \| PartialMessage` |
| `messageUpdate` | `oldMessage, newMessage` |
| `guildMemberAdd` | `member: GuildMember` |
| `guildMemberRemove` | `member: GuildMember \| PartialGuildMember` |
| `guildMemberUpdate` | `oldMember, newMember` |
| `interactionCreate` | `interaction: Interaction` |
| `voiceStateUpdate` | `oldState, newState: VoiceState` |
| `guildCreate` | `guild: Guild` |
| `guildDelete` | `guild: Guild` |
| `clientReady` | `client: Client<true>` |
| `afterReady` | `client: Client<true>` *(bot.ts custom event)* |
| `raw` | `packet: GatewayDispatchPayload` *(bot.ts custom event)* |

### afterReady event (bot.ts specific)

`afterReady` fires after ALL `clientReady` listeners have finished — use it for post-init work:

```typescript
import { Listener } from "#core/listener"
import logger from "#core/logger"

export default new Listener({
event: "afterReady",
description: "Log successful startup",
once: true,
async run(client) {
logger.success(`Logged in as ${client.user.tag}`)
},
})
```

### Example: Welcome listener

```typescript
import { Listener } from "#core/listener"
import discord from "discord.js"

export default new Listener({
event: "guildMemberAdd",
description: "Welcome new members with an embed",
async run(member) {
const channel = member.guild.systemChannel
if (!channel) return

const embed = new discord.EmbedBuilder()
.setTitle("Welcome!")
.setDescription(`Welcome to **${member.guild.name}**, ${member}!`)
.setColor(0x5865f2)

await channel.send({ embeds: [embed] })
},
})
```

### Rules

- File naming: `<category>.<eventName>.ts` — the category appears in logs
- `.native.ts` files are framework files — NEVER modify them; copy + remove `.native` to override
- Errors thrown inside listeners are caught and logged automatically — no need for try/catch unless custom handling is needed
- `once: true` removes the listener after first trigger
- For `clientReady`, prefer `afterReady` to ensure all other ready listeners have completed first

After creating the file, remind the user to run `bun run format`.
Loading
Loading