diff --git a/src/agents/tools/slack-actions.ts b/src/agents/tools/slack-actions.ts new file mode 100644 index 000000000..e726b710c --- /dev/null +++ b/src/agents/tools/slack-actions.ts @@ -0,0 +1,163 @@ +import type { AgentToolResult } from "@mariozechner/pi-agent-core"; + +import type { ClawdisConfig, SlackActionConfig } from "../../config/config.js"; +import { + deleteSlackMessage, + editSlackMessage, + getSlackMemberInfo, + listSlackEmojis, + listSlackPins, + listSlackReactions, + pinSlackMessage, + reactSlackMessage, + readSlackMessages, + sendSlackMessage, + unpinSlackMessage, +} from "../../slack/actions.js"; +import { jsonResult, readStringParam } from "./common.js"; + +const messagingActions = new Set([ + "sendMessage", + "editMessage", + "deleteMessage", + "readMessages", +]); + +const reactionsActions = new Set(["react", "reactions"]); +const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]); + +type ActionGate = ( + key: keyof SlackActionConfig, + defaultValue?: boolean, +) => boolean; + +export async function handleSlackAction( + params: Record, + cfg: ClawdisConfig, +): Promise> { + const action = readStringParam(params, "action", { required: true }); + const isActionEnabled: ActionGate = (key, defaultValue = true) => { + const value = cfg.slack?.actions?.[key]; + if (value === undefined) return defaultValue; + return value !== false; + }; + + if (reactionsActions.has(action)) { + if (!isActionEnabled("reactions")) { + throw new Error("Slack reactions are disabled."); + } + const channelId = readStringParam(params, "channelId", { required: true }); + const messageId = readStringParam(params, "messageId", { required: true }); + if (action === "react") { + const emoji = readStringParam(params, "emoji", { required: true }); + await reactSlackMessage(channelId, messageId, emoji); + return jsonResult({ ok: true }); + } + const reactions = await listSlackReactions(channelId, messageId); + return jsonResult({ ok: true, reactions }); + } + + if (messagingActions.has(action)) { + if (!isActionEnabled("messages")) { + throw new Error("Slack messages are disabled."); + } + switch (action) { + case "sendMessage": { + const to = readStringParam(params, "to", { required: true }); + const content = readStringParam(params, "content", { required: true }); + const mediaUrl = readStringParam(params, "mediaUrl"); + const replyTo = readStringParam(params, "replyTo"); + const result = await sendSlackMessage(to, content, { + mediaUrl: mediaUrl ?? undefined, + replyTo: replyTo ?? undefined, + }); + return jsonResult({ ok: true, result }); + } + case "editMessage": { + const channelId = readStringParam(params, "channelId", { + required: true, + }); + const messageId = readStringParam(params, "messageId", { + required: true, + }); + const content = readStringParam(params, "content", { + required: true, + }); + await editSlackMessage(channelId, messageId, content); + return jsonResult({ ok: true }); + } + case "deleteMessage": { + const channelId = readStringParam(params, "channelId", { + required: true, + }); + const messageId = readStringParam(params, "messageId", { + required: true, + }); + await deleteSlackMessage(channelId, messageId); + return jsonResult({ ok: true }); + } + case "readMessages": { + const channelId = readStringParam(params, "channelId", { + required: true, + }); + const limitRaw = params.limit; + const limit = + typeof limitRaw === "number" && Number.isFinite(limitRaw) + ? limitRaw + : undefined; + const before = readStringParam(params, "before"); + const after = readStringParam(params, "after"); + const result = await readSlackMessages(channelId, { + limit, + before: before ?? undefined, + after: after ?? undefined, + }); + return jsonResult({ ok: true, ...result }); + } + default: + break; + } + } + + if (pinActions.has(action)) { + if (!isActionEnabled("pins")) { + throw new Error("Slack pins are disabled."); + } + const channelId = readStringParam(params, "channelId", { required: true }); + if (action === "pinMessage") { + const messageId = readStringParam(params, "messageId", { + required: true, + }); + await pinSlackMessage(channelId, messageId); + return jsonResult({ ok: true }); + } + if (action === "unpinMessage") { + const messageId = readStringParam(params, "messageId", { + required: true, + }); + await unpinSlackMessage(channelId, messageId); + return jsonResult({ ok: true }); + } + const pins = await listSlackPins(channelId); + return jsonResult({ ok: true, pins }); + } + + if (action === "memberInfo") { + if (!isActionEnabled("memberInfo")) { + throw new Error("Slack member info is disabled."); + } + const userId = readStringParam(params, "userId", { required: true }); + const info = await getSlackMemberInfo(userId); + return jsonResult({ ok: true, info }); + } + + if (action === "emojiList") { + if (!isActionEnabled("emojiList")) { + throw new Error("Slack emoji list is disabled."); + } + const emojis = await listSlackEmojis(); + return jsonResult({ ok: true, emojis }); + } + + throw new Error(`Unknown action: ${action}`); +} diff --git a/src/agents/tools/slack-schema.ts b/src/agents/tools/slack-schema.ts new file mode 100644 index 000000000..46fed8e25 --- /dev/null +++ b/src/agents/tools/slack-schema.ts @@ -0,0 +1,61 @@ +import { Type } from "@sinclair/typebox"; + +export const SlackToolSchema = Type.Union([ + Type.Object({ + action: Type.Literal("react"), + channelId: Type.String(), + messageId: Type.String(), + emoji: Type.String(), + }), + Type.Object({ + action: Type.Literal("reactions"), + channelId: Type.String(), + messageId: Type.String(), + }), + Type.Object({ + action: Type.Literal("sendMessage"), + to: Type.String(), + content: Type.String(), + mediaUrl: Type.Optional(Type.String()), + replyTo: Type.Optional(Type.String()), + }), + Type.Object({ + action: Type.Literal("editMessage"), + channelId: Type.String(), + messageId: Type.String(), + content: Type.String(), + }), + Type.Object({ + action: Type.Literal("deleteMessage"), + channelId: Type.String(), + messageId: Type.String(), + }), + Type.Object({ + action: Type.Literal("readMessages"), + channelId: Type.String(), + limit: Type.Optional(Type.Number()), + before: Type.Optional(Type.String()), + after: Type.Optional(Type.String()), + }), + Type.Object({ + action: Type.Literal("pinMessage"), + channelId: Type.String(), + messageId: Type.String(), + }), + Type.Object({ + action: Type.Literal("unpinMessage"), + channelId: Type.String(), + messageId: Type.String(), + }), + Type.Object({ + action: Type.Literal("listPins"), + channelId: Type.String(), + }), + Type.Object({ + action: Type.Literal("memberInfo"), + userId: Type.String(), + }), + Type.Object({ + action: Type.Literal("emojiList"), + }), +]); diff --git a/src/agents/tools/slack-tool.ts b/src/agents/tools/slack-tool.ts new file mode 100644 index 000000000..b0f2d0146 --- /dev/null +++ b/src/agents/tools/slack-tool.ts @@ -0,0 +1,18 @@ +import { loadConfig } from "../../config/config.js"; +import type { AnyAgentTool } from "./common.js"; +import { handleSlackAction } from "./slack-actions.js"; +import { SlackToolSchema } from "./slack-schema.js"; + +export function createSlackTool(): AnyAgentTool { + return { + label: "Slack", + name: "slack", + description: "Manage Slack messages, reactions, and pins.", + parameters: SlackToolSchema, + execute: async (_toolCallId, args) => { + const params = args as Record; + const cfg = loadConfig(); + return await handleSlackAction(params, cfg); + }, + }; +} diff --git a/src/auto-reply/reply/block-streaming.ts b/src/auto-reply/reply/block-streaming.ts index ab6ff2aed..ee973b9d9 100644 --- a/src/auto-reply/reply/block-streaming.ts +++ b/src/auto-reply/reply/block-streaming.ts @@ -8,6 +8,7 @@ const BLOCK_CHUNK_SURFACES = new Set([ "whatsapp", "telegram", "discord", + "slack", "signal", "imessage", "webchat",