Codebase Structure Notes
SkillMediaExplains command/interaction handler patterns, slash command option builders, Sharp image processing usage, and the sudoku-gen package status. Use when adding or modifying commands, interaction handlers (buttons/autocomplete), slash command options/subcommands, or any image generation code.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Codebase Structure Notes skill
What this skill tells your AI
The instructions your AI receives, as published by mxz7/nypsi in .agents/skills/codebase-structure/SKILL.md and read by ahel’s review.
Command System
Do not create a src/commands/helpers/ directory or place shared helper modules under
src/commands/; this project does not use that structure. Keep command-specific helpers in the
command file, and put reusable domain logic under src/utils/functions/ in the appropriate domain.
Command Definition Pattern
Located in src/commands/ (~180 commands)
// General structure:
const cmd = new Command("name", "description", "category")
.setAliases(["alias"])
.setDocs("https://nypsi.xyz/wiki/...")
.setRun(async (message, send, args) => { … });
export default cmd;
Slash Command Options (SlashCommandBuilder)
Commands use cmd.slashData to build Discord slash commands:
.addSubcommand()— define subcommands (e.g.,/chess puzzle,/chess duel).addStringOption()— string parameters with optional.addChoices().addUserOption()— mention/select users.addIntegerOption()— number parameters.addSubcommandGroup()— nested grouping
Example from /chess command:
cmd.slashData
.addSubcommand((option) =>
option
.setName("puzzle")
.setDescription("play a random chess puzzle")
.addStringOption((difficulty) =>
difficulty
.setName("difficulty")
.setDescription("select puzzle difficulty")
.setRequired(false)
.addChoices(CHESS_PUZZLE_DIFFICULTIES.map((d) => ({ name: d, value: d }))),
),
)
.addSubcommand((option) =>
option
.setName("duel")
.setDescription("challenge another member to a chess game")
.addUserOption((user) =>
user.setName("member").setDescription("member you want to play against").setRequired(true),
),
);
Command Types
// From src/models/Command.ts
export class Command {
public name: string;
public description: string;
public category: CommandCategory;
public slashData?: SlashCommandBuilder;
public slashEnabled: boolean;
public run: (message, send, args) => void;
// Chainable methods:
setPermissions(permissions: string[]);
setAliases(aliases: string[]);
setShorthands(shorthands: Record<string, string>);
setRun(run: (message, send, args) => void);
setDocs(url: string);
}
export type CommandCategory =
| "none"
| "animals"
| "fun"
| "info"
| "money"
| "moderation"
| "admin"
| "minecraft"
| "music"
| "utility";
export interface NypsiCommandInteraction extends CommandInteraction {
author?: User;
member: GuildMember;
interaction?: boolean;
content?: string;
}
Interaction Handlers
Located in src/interactions/. Routed by src/utils/handlers/interactions.ts.
Types (src/types/InteractionHandler.ts)
export type InteractionHandler = {
name: string;
type: "interaction";
run: (interaction: MessageComponentInteraction) => Promise<any>;
};
export type AutocompleteHandler = {
name: string;
type: "autocomplete";
run: (interaction: AutocompleteInteraction) => Promise<any>;
};
Autocomplete Example
File: src/interactions/item.ts
export default {
name: "item",
type: "autocomplete",
async run(interaction) {
const focused = interaction.options.getFocused(true);
focused.value = focused.value.toLowerCase();
const inventory = await getInventory(interaction.user.id);
const items = getItems();
let options = inventory.entries
.map((i) => i.item)
.filter(
(item) =>
item.includes(focused.value) ||
items[item].name.includes(focused.value) ||
items[item].aliases?.includes(focused.value),
);
return await interaction.respond(
options.map((i) => ({
name: `${items[i].emoji} ${items[i].name}`,
value: i,
})),
);
},
} as AutocompleteHandler;
Button Interaction Example
File: src/interactions/accept_offer.ts
export default {
name: "btn-accept-offer",
type: "interaction",
async run(interaction) {
if (!interaction.isButton()) return;
// Handler logic with Prisma, Redis, inventory checks, etc.
},
} as InteractionHandler;
Routing Logic
File: src/utils/handlers/interactions.ts
- Loads all handlers from
dist/interactions/at startup - Maps by handler name into
Map<string, InteractionHandler>andMap<string, AutocompleteHandler> - For autocomplete: retrieves handler by
interaction.options.getFocused(true).name - For button/select menu: retrieves handler by
interaction.customId(fallback to reaction role logic) - Namespaced component IDs use
<handler-name>:<context>; routing first checks the exact custom ID, then falls back to the segment before the first:. Keep dynamic state outside the custom ID when it is too large or sensitive. - Profile transfer checks block interactions except special IDs like
btn-confirm-profile-transfer
Image Processing with Sharp
Sharp is used in 5 files for image generation and optimization:
1. Chess Board Rendering (src/utils/functions/chess/board.ts)
import sharp = require("sharp");
// SVG → PNG pipeline with piece overlays
const res = await sharp(Buffer.from(baseSvg))
.png()
.composite(pieces) // pieces: sharp.OverlayOptions[]
.png()
.toBuffer();
// Individual piece SVG → PNG resize:
const png = await sharp(Buffer.from(svg))
.resize(SQUARE_SIZE, SQUARE_SIZE, { fit: "contain" })
.png()
.toBuffer();
2. Guild Avatar Upload (src/commands/guild.ts)
const buffer = await sharp(arrayBuffer, { animated: contentType.split("/")[1] === "gif" })
.resize({ width: 256, height: 256, fit: "cover" })
.toBuffer();
await uploadImage(id, buffer, contentType);
3. Color Circle Generation (src/commands/color.ts)
const circleImage = await sharp(arrayBuffer)
.resize(128, 128, { fit: "fill" })
.composite([{ input: roundedEdges, blend: "dest-in" }])
.png()
.toBuffer();
4. Evidence Compression (src/utils/functions/guilds/evidence.ts)
if (contentType.split("/")[1] === "png") {
image = await sharp(buffer).webp({ nearLossless: true }).toBuffer();
contentType = "image/webp";
}
Common Sharp Patterns
.resize(w, h, { fit: "cover"|"contain"|"fill" })— resize with aspect ratio modes.composite([{ input: Buffer, blend: "dest-in", left, top }])— overlay images.png()/.webp()— format conversion with options.rotate(deg)— rotate image.toBuffer()— emit as Buffer
sudoku-gen Package
Status: Used by src/utils/functions/sudoku/game.ts to generate puzzles in createSudokuGame().
Exports and Imports
Command Model Exports
Commandclass with fluent APINypsiCommandInteraction— extended CommandInteraction with author + interaction flagSendMessagetype —(data) => Promise<Message<boolean>>- Helper:
createNypsiInteraction()— adds author field to slash command interactions
Interaction Handler Auto-Loading
loadInteractions()— scansdist/interactions/, imports as ESM, registers by namerunInteraction()— dispatcher that branches on interaction type- Profile transfer middleware intercepts non-whitelisted interactions
Signals
- GitHub stars
- 69
- Forks
- 31
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
codebase-structure- Source
- github.com/mxz7/nypsi