diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff27dca4..836080e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Docs: add gog calendar event color IDs from `gog calendar colors`. (#715) — thanks @mjrussell. - Cron/CLI: trim model overrides on cron edits and document main-session guidance. (#711) — thanks @mjrussell. - Skills: bundle `skill-creator` to guide creating and packaging skills. +- Discord: expose channel/category management actions in the message tool. (#730) — thanks @NicholasSpisak ### Fixes - Doctor: surface plugin diagnostics in the report. diff --git a/skills/discord/SKILL.md b/skills/discord/SKILL.md index b1f55e768..0b64f14e1 100644 --- a/skills/discord/SKILL.md +++ b/skills/discord/SKILL.md @@ -1,6 +1,6 @@ --- name: discord -description: Use when you need to control Discord from Clawdbot via the discord tool: send messages, react, post or upload stickers, upload emojis, run polls, manage threads/pins/search, fetch permissions or member/role/channel info, or handle moderation actions in Discord DMs or channels. +description: Use when you need to control Discord from Clawdbot via the discord tool: send messages, react, post or upload stickers, upload emojis, run polls, manage threads/pins/search, create/edit/delete channels and categories, fetch permissions or member/role/channel info, or handle moderation actions in Discord DMs or channels. --- # Discord Actions @@ -135,6 +135,7 @@ Use `discord.actions.*` to disable action groups: - `emojiUploads`, `stickerUploads` - `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events` - `roles` (role add/remove, default `false`) +- `channels` (channel/category create/edit/delete/move, default `false`) - `moderation` (timeout/kick/ban, default `false`) ### Read recent messages @@ -314,6 +315,90 @@ Use `discord.actions.*` to disable action groups: } ``` +### Channel management (disabled by default) + +Create, edit, delete, and move channels and categories. Enable via `discord.actions.channels: true`. + +**Create a text channel:** + +```json +{ + "action": "channelCreate", + "guildId": "999", + "name": "general-chat", + "type": 0, + "parentId": "888", + "topic": "General discussion" +} +``` + +- `type`: Discord channel type integer (0 = text, 2 = voice, 4 = category; other values supported) +- `parentId`: category ID to nest under (optional) +- `topic`, `position`, `nsfw`: optional + +**Create a category:** + +```json +{ + "action": "categoryCreate", + "guildId": "999", + "name": "Projects" +} +``` + +**Edit a channel:** + +```json +{ + "action": "channelEdit", + "channelId": "123", + "name": "new-name", + "topic": "Updated topic" +} +``` + +- Supports `name`, `topic`, `position`, `parentId` (null to remove from category), `nsfw`, `rateLimitPerUser` + +**Move a channel:** + +```json +{ + "action": "channelMove", + "guildId": "999", + "channelId": "123", + "parentId": "888", + "position": 2 +} +``` + +- `parentId`: target category (null to move to top level) + +**Delete a channel:** + +```json +{ + "action": "channelDelete", + "channelId": "123" +} +``` + +**Edit/delete a category:** + +```json +{ + "action": "categoryEdit", + "categoryId": "888", + "name": "Renamed Category" +} +``` + +```json +{ + "action": "categoryDelete", + "categoryId": "888" +} +``` + ### Voice status ```json diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index d3fff0a87..d79d487a4 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -55,6 +55,13 @@ const AllMessageActions = [ "role-remove", "channel-info", "channel-list", + "channel-create", + "channel-edit", + "channel-delete", + "channel-move", + "category-create", + "category-edit", + "category-delete", "voice-status", "event-list", "event-create", @@ -130,6 +137,14 @@ const MessageToolCommonSchema = { gatewayUrl: Type.Optional(Type.String()), gatewayToken: Type.Optional(Type.String()), timeoutMs: Type.Optional(Type.Number()), + name: Type.Optional(Type.String()), + type: Type.Optional(Type.Number()), + parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])), + topic: Type.Optional(Type.String()), + position: Type.Optional(Type.Number()), + nsfw: Type.Optional(Type.Boolean()), + rateLimitPerUser: Type.Optional(Type.Number()), + categoryId: Type.Optional(Type.String()), }; function buildMessageToolSchemaFromActions( diff --git a/src/providers/plugins/actions/discord.ts b/src/providers/plugins/actions/discord.ts index 8b3f8cb6b..7e4604a1b 100644 --- a/src/providers/plugins/actions/discord.ts +++ b/src/providers/plugins/actions/discord.ts @@ -57,6 +57,15 @@ export const discordMessageActions: ProviderMessageActionAdapter = { actions.add("channel-info"); actions.add("channel-list"); } + if (gate("channels", false)) { + actions.add("channel-create"); + actions.add("channel-edit"); + actions.add("channel-delete"); + actions.add("channel-move"); + actions.add("category-create"); + actions.add("category-edit"); + actions.add("category-delete"); + } if (gate("voiceStatus")) actions.add("voice-status"); if (gate("events")) { actions.add("event-list"); @@ -449,6 +458,136 @@ export const discordMessageActions: ProviderMessageActionAdapter = { return await handleDiscordAction({ action: "channelList", guildId }, cfg); } + if (action === "channel-create") { + const guildId = readStringParam(params, "guildId", { required: true }); + const name = readStringParam(params, "name", { required: true }); + const type = readNumberParam(params, "type", { integer: true }); + const parentId = + params.parentId === null + ? null + : readStringParam(params, "parentId"); + const topic = readStringParam(params, "topic"); + const position = readNumberParam(params, "position", { integer: true }); + const nsfw = typeof params.nsfw === "boolean" ? params.nsfw : undefined; + return await handleDiscordAction( + { + action: "channelCreate", + guildId, + name, + type: type ?? undefined, + parentId: parentId ?? undefined, + topic: topic ?? undefined, + position: position ?? undefined, + nsfw, + }, + cfg, + ); + } + + if (action === "channel-edit") { + const channelId = readStringParam(params, "channelId", { + required: true, + }); + const name = readStringParam(params, "name"); + const topic = readStringParam(params, "topic"); + const position = readNumberParam(params, "position", { integer: true }); + const parentId = + params.parentId === null + ? null + : readStringParam(params, "parentId"); + const nsfw = typeof params.nsfw === "boolean" ? params.nsfw : undefined; + const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", { + integer: true, + }); + return await handleDiscordAction( + { + action: "channelEdit", + channelId, + name: name ?? undefined, + topic: topic ?? undefined, + position: position ?? undefined, + parentId: parentId === undefined ? undefined : parentId, + nsfw, + rateLimitPerUser: rateLimitPerUser ?? undefined, + }, + cfg, + ); + } + + if (action === "channel-delete") { + const channelId = readStringParam(params, "channelId", { + required: true, + }); + return await handleDiscordAction( + { action: "channelDelete", channelId }, + cfg, + ); + } + + if (action === "channel-move") { + const guildId = readStringParam(params, "guildId", { required: true }); + const channelId = readStringParam(params, "channelId", { + required: true, + }); + const parentId = + params.parentId === null + ? null + : readStringParam(params, "parentId"); + const position = readNumberParam(params, "position", { integer: true }); + return await handleDiscordAction( + { + action: "channelMove", + guildId, + channelId, + parentId: parentId === undefined ? undefined : parentId, + position: position ?? undefined, + }, + cfg, + ); + } + + if (action === "category-create") { + const guildId = readStringParam(params, "guildId", { required: true }); + const name = readStringParam(params, "name", { required: true }); + const position = readNumberParam(params, "position", { integer: true }); + return await handleDiscordAction( + { + action: "categoryCreate", + guildId, + name, + position: position ?? undefined, + }, + cfg, + ); + } + + if (action === "category-edit") { + const categoryId = readStringParam(params, "categoryId", { + required: true, + }); + const name = readStringParam(params, "name"); + const position = readNumberParam(params, "position", { integer: true }); + return await handleDiscordAction( + { + action: "categoryEdit", + categoryId, + name: name ?? undefined, + position: position ?? undefined, + }, + cfg, + ); + } + + if (action === "category-delete") { + const categoryId = readStringParam(params, "categoryId", { + required: true, + }); + return await handleDiscordAction( + { action: "categoryDelete", categoryId }, + cfg, + ); + } + if (action === "voice-status") { const guildId = readStringParam(params, "guildId", { required: true,