Hazel

Commands

Type-safe slash commands with Command.make and CommandGroup

The Bot SDK provides a type-safe command system using Effect Schema for argument validation.

Command.make

Create a command definition.

import { Schema } from "effect"
import { Command } from "@hazel/bot-sdk"

const GreetCommand = Command.make("greet", {
	description: "Greet someone",
	args: {
		name: Schema.String,
	},
	usageExample: "/greet Alice",
})

Parameters

ParameterTypeRequiredDescription
namestringYesCommand name (used in /name)
descriptionstringYesShown in autocomplete
argsSchema.Struct.FieldsNoTyped arguments
usageExamplestringNoExample shown to users

Argument Types

Use Effect Schema for type-safe arguments:

import { Schema } from "effect"

const MyCommand = Command.make("example", {
	description: "Example command",
	args: {
		// Required string
		name: Schema.String,

		// Optional string
		description: Schema.optional(Schema.String),

		// Number
		count: Schema.Number,

		// Boolean
		enabled: Schema.Boolean,
	},
})

CommandGroup.make

Group multiple commands together.

import { Command, CommandGroup } from "@hazel/bot-sdk"

const AddCommand = Command.make("task-add", {
	description: "Add a task",
	args: { title: Schema.String },
})

const ListCommand = Command.make("task-list", {
	description: "List tasks",
})

const commands = CommandGroup.make(AddCommand, ListCommand)

Pass the group to runHazelBot:

runHazelBot({
	commands,
	setup: (bot) =>
		Effect.gen(function* () {
			// ...
		}),
})

Handling Commands

Use bot.onCommand to register a handler. The context is fully typed based on your command definition.

yield *
	bot.onCommand(GreetCommand, (ctx) =>
		Effect.gen(function* () {
			// ctx.args.name is typed as string!
			yield* bot.message.send(ctx.channelId, `Hello, ${ctx.args.name}!`)
		}),
	)

Command Context

PropertyTypeDescription
commandNamestringThe command that was invoked
channelIdChannelIdChannel where command was run
userIdUserIdUser who ran the command
orgIdOrganizationIdOrganization ID
argsTTyped arguments based on Schema
timestampnumberWhen command was invoked

Optional Arguments

Use Schema.optional for optional arguments:

const CreateCommand = Command.make("create", {
	description: "Create something",
	args: {
		name: Schema.String, // Required
		description: Schema.optional(Schema.String), // Optional
	},
})

yield *
	bot.onCommand(CreateCommand, (ctx) =>
		Effect.gen(function* () {
			// ctx.args.name is string
			// ctx.args.description is string | undefined
			const desc = ctx.args.description ?? "No description"
		}),
	)

Commands Without Arguments

For commands with no arguments, omit the args field:

const PingCommand = Command.make("ping", {
	description: "Check if bot is alive",
})

yield *
	bot.onCommand(PingCommand, (ctx) =>
		Effect.gen(function* () {
			// ctx.args is {} (empty object)
			yield* bot.message.send(ctx.channelId, "Pong!")
		}),
	)

Command Sync

When the bot starts, commands are automatically synced to Hazel. They appear in the / autocomplete menu in the chat.

Type Utilities

CommandNames<G>

Extract a union of command names from a group:

type Names = CommandNames<typeof commands>
// "task-add" | "task-list"

CommandArgs<G, N>

Extract the args type for a specific command:

type AddArgs = CommandArgs<typeof commands, "task-add">
// { title: string }

Example

Complete example with multiple commands:

import { Effect, Schema } from "effect"
import { Command, CommandGroup, runHazelBot } from "@hazel/bot-sdk"

const EchoCommand = Command.make("echo", {
	description: "Echo text back",
	args: { text: Schema.String },
	usageExample: "/echo Hello world",
})

const RollCommand = Command.make("roll", {
	description: "Roll dice",
	args: {
		sides: Schema.optional(Schema.Number),
	},
	usageExample: "/roll 20",
})

const HelpCommand = Command.make("help", {
	description: "Show available commands",
})

const commands = CommandGroup.make(EchoCommand, RollCommand, HelpCommand)

runHazelBot({
	commands,
	setup: (bot) =>
		Effect.gen(function* () {
			yield* bot.onCommand(EchoCommand, (ctx) => bot.message.send(ctx.channelId, ctx.args.text))

			yield* bot.onCommand(RollCommand, (ctx) =>
				Effect.gen(function* () {
					const sides = ctx.args.sides ?? 6
					const result = Math.floor(Math.random() * sides) + 1
					yield* bot.message.send(ctx.channelId, `🎲 Rolled a ${result}!`)
				}),
			)

			yield* bot.onCommand(HelpCommand, (ctx) =>
				bot.message.send(
					ctx.channelId,
					`
**Available Commands:**
- /echo <text> - Echo text back
- /roll [sides] - Roll dice (default: 6 sides)
- /help - Show this message
      `.trim(),
				),
			)
		}),
})

On this page