Kord Annotations generates Discord command metadata and dispatch code from Kotlin handlers. It uses KSP at compile time, then loads generated modules through ServiceLoader at runtime.
Requires Java 21.
Add Maven Central to plugin and dependency resolution:
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}Apply the plugin:
// build.gradle.kts
plugins {
id("io.github.nightmarepog.kord-annotations") version "0.2.0"
}The plugin applies Kotlin/JVM and KSP, then adds matching versions of the core library and processor. No GitHub credentials are required. The Gradle Plugin Portal submission is awaiting its initial review, so mavenCentral() is currently required in pluginManagement.
Generated names can be changed if they conflict with existing code:
kordAnnotations {
generatedPackage.set("com.example.bot.generated")
moduleName.set("BotCommandModule")
}class GeneralCommands {
@Command("ping")
@Description("Replies with pong")
suspend fun ping(context: CommandContext) = context.respond("Pong!")
}Create the runtime with every handler and extension instance, install its listeners, then log in:
val commands = KordAnnotations(
modules = CommandModules.load(),
handlerResolver = InstanceHandlerResolver(GeneralCommands()),
)
commands.install(kord)
commands.syncGlobalCommands(kord)
kord.login()syncGlobalCommands replaces the complete global command set. Commands not present in the generated modules are removed.
The first command parameter must be CommandContext. Remaining parameters become Discord options. Nullable parameters and parameters with Kotlin defaults are optional.
class ModerationCommands {
@Command("warn")
@Description("Warns a server member")
@PrivateResponse
@Cooldown(seconds = 10)
suspend fun warn(
context: CommandContext,
@Description("Member to warn") member: Member,
@Description("Reason shown to the member") reason: String = "No reason supplied",
) = context.respond("Warned ${member.displayName}: $reason")
}@Description contains literal Discord text. It is not a translation key.
Supported option values include strings, integers, numbers, booleans, users, members, channels, roles, mentionables, and attachments. @Option changes the generated option name. @Choices, @Range, and @Length add Discord constraints.
Annotating a class creates a slash-command root. Annotated methods become subcommands:
@Command("settings")
@Description("Changes bot settings")
class SettingsCommands {
@Command("language")
@Description("Changes the language")
suspend fun language(
context: CommandContext,
@Description("Locale tag") locale: String,
) = context.respond("Language changed to $locale")
}@UserCommand and @MessageCommand create context-menu commands.
@PrivateResponsemakes plain-text responses private.@AvailableInselects Discord interaction contexts.@BotDMenables bot direct messages.@Cooldownlimits repeated use by user, channel, guild, or globally.@Timeoutchanges the default 30-second timeout.@NoTimeoutdisables the timeout.@LoadingResponsechanges the response sent when a handler takes longer than two seconds.@NoLoadingResponsedisables that response.
Policy annotations on a handler override the same policy on its class.
Attach a checker to a reusable annotation:
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@CheckedBy(AdminOnlyCheck::class)
annotation class AdminOnly
class AdminOnlyCheck : AnnotationCheck<AdminOnly> {
override suspend fun check(annotation: AdminOnly, context: CommandContext): CheckResult =
if (isAdmin(context.identity.userId)) CheckResult.Allowed
else CheckResult.Denied(CommandFailure("Only administrators can use this command."))
}Register the checker with the handler resolver. A denied check sends its message privately and does not invoke the handler.
An autocomplete provider returns up to 25 choices:
class LocaleAutocomplete : AutocompleteProvider {
override suspend fun complete(input: String, context: CommandContext) =
supportedLocales
.filter { it.startsWith(input, ignoreCase = true) }
.map { AutocompleteChoice(it, it) }
}Apply it with @Autocomplete(LocaleAutocomplete::class) and register the provider with the handler resolver.
@ConvertWith maps a Discord option into a domain type:
data class DurationSeconds(val value: Long)
class DurationConverter : OptionConverter<DurationSeconds> {
override suspend fun convert(value: Any, context: CommandContext) =
DurationSeconds(value as Long)
}
class UtilityCommands {
@Command("delay")
@Description("Sets a delay")
suspend fun delay(
context: CommandContext,
@Description("Delay in seconds")
@ConvertWith(DurationConverter::class, OptionType.INTEGER)
duration: DurationSeconds,
) = context.respond("Delay: ${duration.value}s")
}Buttons, select menus, and modal submissions use generated handlers too:
class TicketComponents {
@Button("ticket close")
@PrivateResponse
suspend fun close(context: ComponentContext) = context.respond("Ticket closed")
}A component handler receives one ComponentContext. Select-menu values are available through context.values.
InMemoryComponentStateStore creates tokens tied to one Discord user. Tokens last 15 minutes by default. Pass reusable = false when state should be consumed once.
dependencies {
implementation("io.github.nightmarepog:kord-annotations-spring:0.2.0")
}With a Kord bean present, Spring discovers generated handlers, installs listeners, and synchronizes global commands during startup.
kord-annotations.enabled=true
kord-annotations.sync-global-commands=true
kord-annotations.maximum-sync-attempts=3Set sync-global-commands to false when command synchronization is handled elsewhere.
dependencies {
testImplementation("io.github.nightmarepog:kord-annotations-testkit:0.2.0")
}val command = CommandModules.load()
.flatMap { it.commands }
.single { it.descriptor.name == "ping" }
val result = CommandTestHarness(InstanceHandlerResolver(GeneralCommands()))
.execute(command)
assertEquals("Pong!", result.responses.single().content)The testkit runs the same checks, cooldowns, loading behavior, timeout handling, and generated invocation path without connecting to Discord.
| Artifact | Contents |
|---|---|
kord-annotations-core |
Annotations, descriptors, runtime, policies, and component state |
kord-annotations-processor |
KSP validation and code generation |
kord-annotations-gradle-plugin |
Kotlin/JVM, KSP, and dependency setup |
kord-annotations-spring |
Spring Boot discovery and auto-configuration |
kord-annotations-help |
Help entries generated from command descriptors |
kord-annotations-testkit |
In-memory command execution and response recording |