Slack: add new slack connection

This commit is contained in:
Shadow
2026-01-03 21:27:18 -06:00
committed by Peter Steinberger
parent 4b3ca29404
commit bf3d120f8c
4 changed files with 243 additions and 0 deletions

View File

@@ -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<string, unknown>,
cfg: ClawdisConfig,
): Promise<AgentToolResult<unknown>> {
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}`);
}

View File

@@ -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"),
}),
]);

View File

@@ -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<string, unknown>;
const cfg = loadConfig();
return await handleSlackAction(params, cfg);
},
};
}

View File

@@ -8,6 +8,7 @@ const BLOCK_CHUNK_SURFACES = new Set<TextChunkSurface>([
"whatsapp",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"webchat",