From b3ae50c71cb80b49ae30c589c8f385846e9683d8 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 18 Mar 2026 02:07:26 +0000 Subject: [PATCH] Slack: move action runtime into extension --- extensions/slack/runtime-api.ts | 1 + .../slack/src/action-runtime.test.ts | 352 ++++++++++-------- .../slack/src/action-runtime.ts | 110 +++--- src/channels/plugins/slack.actions.ts | 7 +- src/plugin-sdk/slack.ts | 2 +- .../runtime/runtime-slack-ops.runtime.ts | 2 +- src/plugins/runtime/types-channel.ts | 2 +- 7 files changed, 264 insertions(+), 212 deletions(-) rename src/agents/tools/slack-actions.test.ts => extensions/slack/src/action-runtime.test.ts (64%) rename src/agents/tools/slack-actions.ts => extensions/slack/src/action-runtime.ts (79%) diff --git a/extensions/slack/runtime-api.ts b/extensions/slack/runtime-api.ts index b40f24e4177..68281fd83d3 100644 --- a/extensions/slack/runtime-api.ts +++ b/extensions/slack/runtime-api.ts @@ -1,3 +1,4 @@ +export * from "./src/action-runtime.js"; export * from "./src/directory-live.js"; export * from "./src/index.js"; export * from "./src/resolve-channels.js"; diff --git a/src/agents/tools/slack-actions.test.ts b/extensions/slack/src/action-runtime.test.ts similarity index 64% rename from src/agents/tools/slack-actions.test.ts rename to extensions/slack/src/action-runtime.test.ts index bf28c2bed01..803118b877a 100644 --- a/src/agents/tools/slack-actions.test.ts +++ b/extensions/slack/src/action-runtime.test.ts @@ -1,7 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../config/config.js"; -import { handleSlackAction } from "./slack-actions.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import { handleSlackAction, slackActionRuntime } from "./action-runtime.js"; +import { parseSlackBlocksInput } from "./blocks-input.js"; +const originalSlackActionRuntime = { ...slackActionRuntime }; const deleteSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); const downloadSlackFile = vi.fn(async (..._args: unknown[]) => null); const editSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); @@ -14,31 +16,10 @@ const reactSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); const readSlackMessages = vi.fn(async (..._args: unknown[]) => ({})); const removeOwnSlackReactions = vi.fn(async (..._args: unknown[]) => ["thumbsup"]); const removeSlackReaction = vi.fn(async (..._args: unknown[]) => ({})); -const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); +const recordSlackThreadParticipation = vi.fn(); +const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({ channelId: "C123" })); const unpinSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); -vi.mock("../../../extensions/slack/src/actions.js", () => ({ - deleteSlackMessage: (...args: Parameters) => - deleteSlackMessage(...args), - downloadSlackFile: (...args: Parameters) => downloadSlackFile(...args), - editSlackMessage: (...args: Parameters) => editSlackMessage(...args), - getSlackMemberInfo: (...args: Parameters) => - getSlackMemberInfo(...args), - listSlackEmojis: (...args: Parameters) => listSlackEmojis(...args), - listSlackPins: (...args: Parameters) => listSlackPins(...args), - listSlackReactions: (...args: Parameters) => - listSlackReactions(...args), - pinSlackMessage: (...args: Parameters) => pinSlackMessage(...args), - reactSlackMessage: (...args: Parameters) => reactSlackMessage(...args), - readSlackMessages: (...args: Parameters) => readSlackMessages(...args), - removeOwnSlackReactions: (...args: Parameters) => - removeOwnSlackReactions(...args), - removeSlackReaction: (...args: Parameters) => - removeSlackReaction(...args), - sendSlackMessage: (...args: Parameters) => sendSlackMessage(...args), - unpinSlackMessage: (...args: Parameters) => unpinSlackMessage(...args), -})); - describe("handleSlackAction", () => { function slackConfig(overrides?: Record): OpenClawConfig { return { @@ -105,6 +86,24 @@ describe("handleSlackAction", () => { beforeEach(() => { vi.clearAllMocks(); + Object.assign(slackActionRuntime, originalSlackActionRuntime, { + deleteSlackMessage, + downloadSlackFile, + editSlackMessage, + getSlackMemberInfo, + listSlackEmojis, + listSlackPins, + listSlackReactions, + parseSlackBlocksInput, + pinSlackMessage, + reactSlackMessage, + readSlackMessages, + recordSlackThreadParticipation, + removeOwnSlackReactions, + removeSlackReaction, + sendSlackMessage, + unpinSlackMessage, + }); }); it.each([ @@ -261,6 +260,7 @@ describe("handleSlackAction", () => { { action: "sendMessage", to: "channel:C123", + content: "", blocks, }, slackConfig(), @@ -275,7 +275,7 @@ describe("handleSlackAction", () => { it.each([ { name: "invalid blocks JSON", - blocks: "{bad-json", + blocks: "{not json", expectedError: /blocks must be valid JSON/i, }, { name: "empty blocks arrays", blocks: "[]", expectedError: /at least one block/i }, @@ -285,6 +285,7 @@ describe("handleSlackAction", () => { { action: "sendMessage", to: "channel:C123", + content: "", blocks, }, slackConfig(), @@ -311,8 +312,9 @@ describe("handleSlackAction", () => { { action: "sendMessage", to: "channel:C123", - blocks: [{ type: "divider" }], - mediaUrl: "https://example.com/image.png", + content: "hello", + mediaUrl: "https://example.com/file.png", + blocks: JSON.stringify([{ type: "divider" }]), }, slackConfig(), ), @@ -322,13 +324,13 @@ describe("handleSlackAction", () => { it.each([ { name: "JSON blocks", - blocks: JSON.stringify([{ type: "section", text: { type: "mrkdwn", text: "Updated" } }]), - expectedBlocks: [{ type: "section", text: { type: "mrkdwn", text: "Updated" } }], + blocks: JSON.stringify([{ type: "divider" }]), + expectedBlocks: [{ type: "divider" }], }, { name: "array blocks", - blocks: [{ type: "divider" }], - expectedBlocks: [{ type: "divider" }], + blocks: [{ type: "section", text: { type: "mrkdwn", text: "updated" } }], + expectedBlocks: [{ type: "section", text: { type: "mrkdwn", text: "updated" } }], }, ])("passes $name to editSlackMessage", async ({ blocks, expectedBlocks }) => { await handleSlackAction( @@ -336,6 +338,7 @@ describe("handleSlackAction", () => { action: "editMessage", channelId: "C123", messageId: "123.456", + content: "", blocks, }, slackConfig(), @@ -360,40 +363,32 @@ describe("handleSlackAction", () => { }); it("auto-injects threadTs from context when replyToMode=all", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); await handleSlackAction( { action: "sendMessage", to: "channel:C123", - content: "Auto-threaded", + content: "Threaded reply", }, - cfg, + slackConfig(), { currentChannelId: "C123", currentThreadTs: "1111111111.111111", replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Auto-threaded", { - mediaUrl: undefined, - threadTs: "1111111111.111111", - blocks: undefined, - }); + expectLastSlackSend("Threaded reply", "1111111111.111111"); }); it("replyToMode=first threads first message then stops", async () => { - const { cfg, context, hasRepliedRef } = createReplyToFirstScenario(); + const { cfg, context } = createReplyToFirstScenario(); - // First message should be threaded await handleSlackAction( { action: "sendMessage", to: "channel:C123", content: "First" }, cfg, context, ); - expectLastSlackSend("First", "1111111111.111111"); - expect(hasRepliedRef.value).toBe(true); + expectLastSlackSend("First", "1111111111.111111"); await sendSecondMessageAndExpectNoThread({ cfg, context }); }); @@ -405,73 +400,54 @@ describe("handleSlackAction", () => { action: "sendMessage", to: "channel:C123", content: "Explicit", - threadTs: "2222222222.222222", + threadTs: "9999999999.999999", }, cfg, context, ); - expectLastSlackSend("Explicit", "2222222222.222222"); - expect(hasRepliedRef.value).toBe(true); + expectLastSlackSend("Explicit", "9999999999.999999"); + expect(hasRepliedRef.value).toBe(true); await sendSecondMessageAndExpectNoThread({ cfg, context }); }); it("replyToMode=first without hasRepliedRef does not thread", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); - await handleSlackAction({ action: "sendMessage", to: "channel:C123", content: "No ref" }, cfg, { - currentChannelId: "C123", - currentThreadTs: "1111111111.111111", - replyToMode: "first", - // no hasRepliedRef - }); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "No ref", { - mediaUrl: undefined, - threadTs: undefined, - blocks: undefined, - }); + await handleSlackAction( + { action: "sendMessage", to: "channel:C123", content: "No ref" }, + slackConfig(), + { + currentChannelId: "C123", + currentThreadTs: "1111111111.111111", + replyToMode: "first", + }, + ); + expectLastSlackSend("No ref"); }); it("does not auto-inject threadTs when replyToMode=off", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); await handleSlackAction( - { - action: "sendMessage", - to: "channel:C123", - content: "Off mode", - }, - cfg, + { action: "sendMessage", to: "channel:C123", content: "No thread" }, + slackConfig(), { currentChannelId: "C123", currentThreadTs: "1111111111.111111", replyToMode: "off", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Off mode", { - mediaUrl: undefined, - threadTs: undefined, - blocks: undefined, - }); + expectLastSlackSend("No thread"); }); it("does not auto-inject threadTs when sending to different channel", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); await handleSlackAction( - { - action: "sendMessage", - to: "channel:C999", - content: "Different channel", - }, - cfg, + { action: "sendMessage", to: "channel:C999", content: "Other channel" }, + slackConfig(), { currentChannelId: "C123", currentThreadTs: "1111111111.111111", replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Different channel", { + expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Other channel", { mediaUrl: undefined, threadTs: undefined, blocks: undefined, @@ -479,46 +455,34 @@ describe("handleSlackAction", () => { }); it("explicit threadTs overrides context threadTs", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); await handleSlackAction( { action: "sendMessage", to: "channel:C123", - content: "Explicit thread", - threadTs: "2222222222.222222", + content: "Explicit wins", + threadTs: "9999999999.999999", }, - cfg, + slackConfig(), { currentChannelId: "C123", currentThreadTs: "1111111111.111111", replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Explicit thread", { - mediaUrl: undefined, - threadTs: "2222222222.222222", - blocks: undefined, - }); + expectLastSlackSend("Explicit wins", "9999999999.999999"); }); it("handles channel target without prefix when replyToMode=all", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); await handleSlackAction( - { - action: "sendMessage", - to: "C123", - content: "No prefix", - }, - cfg, + { action: "sendMessage", to: "C123", content: "Bare target" }, + slackConfig(), { currentChannelId: "C123", currentThreadTs: "1111111111.111111", replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith("C123", "No prefix", { + expect(sendSlackMessage).toHaveBeenCalledWith("C123", "Bare target", { mediaUrl: undefined, threadTs: "1111111111.111111", blocks: undefined, @@ -526,104 +490,164 @@ describe("handleSlackAction", () => { }); it("adds normalized timestamps to readMessages payloads", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; readSlackMessages.mockResolvedValueOnce({ - messages: [{ ts: "1735689600.456", text: "hi" }], + messages: [{ ts: "1712345678.123456", text: "hi" }], hasMore: false, }); - const result = await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg); - const payload = result.details as { - messages: Array<{ timestampMs?: number; timestampUtc?: string }>; - }; + const result = await handleSlackAction( + { action: "readMessages", channelId: "C1" }, + slackConfig(), + ); - const expectedMs = Math.round(1735689600.456 * 1000); - expect(payload.messages[0].timestampMs).toBe(expectedMs); - expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString()); + expect(result).toMatchObject({ + details: { + ok: true, + hasMore: false, + messages: [ + expect.objectContaining({ + ts: "1712345678.123456", + timestampMs: 1712345678123, + }), + ], + }, + }); }); it("passes threadId through to readSlackMessages", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - readSlackMessages.mockClear(); readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false }); await handleSlackAction( - { action: "readMessages", channelId: "C1", threadId: "12345.6789" }, - cfg, + { action: "readMessages", channelId: "C1", threadId: "1712345678.123456" }, + slackConfig(), ); - const opts = readSlackMessages.mock.calls[0]?.[1] as { threadId?: string } | undefined; - expect(opts?.threadId).toBe("12345.6789"); + expect(readSlackMessages).toHaveBeenCalledWith("C1", { + threadId: "1712345678.123456", + limit: undefined, + before: undefined, + after: undefined, + }); }); it("adds normalized timestamps to pin payloads", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - listSlackPins.mockResolvedValueOnce([ - { - type: "message", - message: { ts: "1735689600.789", text: "pinned" }, + listSlackPins.mockResolvedValueOnce([{ message: { ts: "1712345678.123456", text: "pin" } }]); + + const result = await handleSlackAction({ action: "listPins", channelId: "C1" }, slackConfig()); + + expect(result).toMatchObject({ + details: { + ok: true, + pins: [ + { + message: expect.objectContaining({ + ts: "1712345678.123456", + timestampMs: 1712345678123, + }), + }, + ], }, - ]); - - const result = await handleSlackAction({ action: "listPins", channelId: "C1" }, cfg); - const payload = result.details as { - pins: Array<{ message?: { timestampMs?: number; timestampUtc?: string } }>; - }; - - const expectedMs = Math.round(1735689600.789 * 1000); - expect(payload.pins[0].message?.timestampMs).toBe(expectedMs); - expect(payload.pins[0].message?.timestampUtc).toBe(new Date(expectedMs).toISOString()); + }); }); it("uses user token for reads when available", async () => { - const cfg = { - channels: { slack: { botToken: "xoxb-1", userToken: "xoxp-1" } }, - } as OpenClawConfig; - expect(await resolveReadToken(cfg)).toBe("xoxp-1"); + const token = await resolveReadToken( + slackConfig({ + accounts: { + default: { + botToken: "xoxb-bot", + userToken: "xoxp-user", + }, + }, + }), + ); + expect(token).toBe("xoxp-user"); }); it("falls back to bot token for reads when user token missing", async () => { - const cfg = { - channels: { slack: { botToken: "xoxb-1" } }, - } as OpenClawConfig; - expect(await resolveReadToken(cfg)).toBeUndefined(); + const token = await resolveReadToken( + slackConfig({ + accounts: { + default: { + botToken: "xoxb-bot", + }, + }, + }), + ); + expect(token).toBeUndefined(); }); it("uses bot token for writes when userTokenReadOnly is true", async () => { - const cfg = { - channels: { slack: { botToken: "xoxb-1", userToken: "xoxp-1" } }, - } as OpenClawConfig; - expect(await resolveSendToken(cfg)).toBeUndefined(); + const token = await resolveSendToken( + slackConfig({ + accounts: { + default: { + botToken: "xoxb-bot", + userToken: "xoxp-user", + userTokenReadOnly: true, + }, + }, + }), + ); + expect(token).toBeUndefined(); }); it("allows user token writes when bot token is missing", async () => { - const cfg = { + const token = await resolveSendToken({ channels: { - slack: { userToken: "xoxp-1", userTokenReadOnly: false }, + slack: { + accounts: { + default: { + userToken: "xoxp-user", + userTokenReadOnly: false, + }, + }, + }, }, - } as OpenClawConfig; - expect(await resolveSendToken(cfg)).toBe("xoxp-1"); + } as OpenClawConfig); + expect(token).toBe("xoxp-user"); }); it("returns all emojis when no limit is provided", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - const emojiMap = { wave: "url1", smile: "url2", heart: "url3" }; - listSlackEmojis.mockResolvedValueOnce({ ok: true, emoji: emojiMap }); - const result = await handleSlackAction({ action: "emojiList" }, cfg); - const payload = result.details as { ok: boolean; emojis: { emoji: Record } }; - expect(payload.ok).toBe(true); - expect(Object.keys(payload.emojis.emoji)).toHaveLength(3); + listSlackEmojis.mockResolvedValueOnce({ + ok: true, + emoji: { party: "https://example.com/party.png", wave: "https://example.com/wave.png" }, + }); + + const result = await handleSlackAction({ action: "emojiList" }, slackConfig()); + + expect(result).toMatchObject({ + details: { + ok: true, + emojis: { + emoji: { party: "https://example.com/party.png", wave: "https://example.com/wave.png" }, + }, + }, + }); }); it("applies limit to emoji-list results", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - const emojiMap = { wave: "url1", smile: "url2", heart: "url3", fire: "url4", star: "url5" }; - listSlackEmojis.mockResolvedValueOnce({ ok: true, emoji: emojiMap }); - const result = await handleSlackAction({ action: "emojiList", limit: 2 }, cfg); - const payload = result.details as { ok: boolean; emojis: { emoji: Record } }; - expect(payload.ok).toBe(true); - const emojiKeys = Object.keys(payload.emojis.emoji); - expect(emojiKeys).toHaveLength(2); - expect(emojiKeys.every((k) => k in emojiMap)).toBe(true); + listSlackEmojis.mockResolvedValueOnce({ + ok: true, + emoji: { + wave: "https://example.com/wave.png", + party: "https://example.com/party.png", + tada: "https://example.com/tada.png", + }, + }); + + const result = await handleSlackAction({ action: "emojiList", limit: 2 }, slackConfig()); + + expect(result).toMatchObject({ + details: { + ok: true, + emojis: { + emoji: { + party: "https://example.com/party.png", + tada: "https://example.com/tada.png", + }, + }, + }, + }); }); }); diff --git a/src/agents/tools/slack-actions.ts b/extensions/slack/src/action-runtime.ts similarity index 79% rename from src/agents/tools/slack-actions.ts rename to extensions/slack/src/action-runtime.ts index 11283394ec8..deb5eb0218e 100644 --- a/src/agents/tools/slack-actions.ts +++ b/extensions/slack/src/action-runtime.ts @@ -1,6 +1,15 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import type { OpenClawConfig } from "../../config/config.js"; -import { resolveSlackAccount } from "../../plugin-sdk/account-resolution.js"; +import { withNormalizedTimestamp } from "../../../src/agents/date-time.js"; +import { + createActionGate, + imageResultFromFile, + jsonResult, + readNumberParam, + readReactionParams, + readStringParam, +} from "../../../src/agents/tools/common.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import { resolveSlackAccount } from "./accounts.js"; import { deleteSlackMessage, downloadSlackFile, @@ -16,22 +25,10 @@ import { removeSlackReaction, sendSlackMessage, unpinSlackMessage, -} from "../../plugin-sdk/slack.js"; -import { - parseSlackBlocksInput, - parseSlackTarget, - recordSlackThreadParticipation, - resolveSlackChannelId, -} from "../../plugin-sdk/slack.js"; -import { withNormalizedTimestamp } from "../date-time.js"; -import { - createActionGate, - imageResultFromFile, - jsonResult, - readNumberParam, - readReactionParams, - readStringParam, -} from "./common.js"; +} from "./actions.js"; +import { parseSlackBlocksInput } from "./blocks-input.js"; +import { recordSlackThreadParticipation } from "./sent-thread-cache.js"; +import { parseSlackTarget, resolveSlackChannelId } from "./targets.js"; const messagingActions = new Set([ "sendMessage", @@ -44,6 +41,25 @@ const messagingActions = new Set([ const reactionsActions = new Set(["react", "reactions"]); const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]); +export const slackActionRuntime = { + deleteSlackMessage, + downloadSlackFile, + editSlackMessage, + getSlackMemberInfo, + listSlackEmojis, + listSlackPins, + listSlackReactions, + parseSlackBlocksInput, + pinSlackMessage, + reactSlackMessage, + readSlackMessages, + recordSlackThreadParticipation, + removeOwnSlackReactions, + removeSlackReaction, + sendSlackMessage, + unpinSlackMessage, +}; + export type SlackActionContext = { /** Current channel ID for auto-threading. */ currentChannelId?: string; @@ -102,7 +118,7 @@ function resolveThreadTsFromContext( } function readSlackBlocksParam(params: Record) { - return parseSlackBlocksInput(params.blocks); + return slackActionRuntime.parseSlackBlocksInput(params.blocks); } export async function handleSlackAction( @@ -163,28 +179,28 @@ export async function handleSlackAction( }); if (remove) { if (writeOpts) { - await removeSlackReaction(channelId, messageId, emoji, writeOpts); + await slackActionRuntime.removeSlackReaction(channelId, messageId, emoji, writeOpts); } else { - await removeSlackReaction(channelId, messageId, emoji); + await slackActionRuntime.removeSlackReaction(channelId, messageId, emoji); } return jsonResult({ ok: true, removed: emoji }); } if (isEmpty) { const removed = writeOpts - ? await removeOwnSlackReactions(channelId, messageId, writeOpts) - : await removeOwnSlackReactions(channelId, messageId); + ? await slackActionRuntime.removeOwnSlackReactions(channelId, messageId, writeOpts) + : await slackActionRuntime.removeOwnSlackReactions(channelId, messageId); return jsonResult({ ok: true, removed }); } if (writeOpts) { - await reactSlackMessage(channelId, messageId, emoji, writeOpts); + await slackActionRuntime.reactSlackMessage(channelId, messageId, emoji, writeOpts); } else { - await reactSlackMessage(channelId, messageId, emoji); + await slackActionRuntime.reactSlackMessage(channelId, messageId, emoji); } return jsonResult({ ok: true, added: emoji }); } const reactions = readOpts - ? await listSlackReactions(channelId, messageId, readOpts) - : await listSlackReactions(channelId, messageId); + ? await slackActionRuntime.listSlackReactions(channelId, messageId, readOpts) + : await slackActionRuntime.listSlackReactions(channelId, messageId); return jsonResult({ ok: true, reactions }); } @@ -211,7 +227,7 @@ export async function handleSlackAction( to, context, ); - const result = await sendSlackMessage(to, content ?? "", { + const result = await slackActionRuntime.sendSlackMessage(to, content ?? "", { ...writeOpts, mediaUrl: mediaUrl ?? undefined, mediaLocalRoots: context?.mediaLocalRoots, @@ -220,7 +236,11 @@ export async function handleSlackAction( }); if (threadTs && result.channelId && account.accountId) { - recordSlackThreadParticipation(account.accountId, result.channelId, threadTs); + slackActionRuntime.recordSlackThreadParticipation( + account.accountId, + result.channelId, + threadTs, + ); } // Keep "first" mode consistent even when the agent explicitly provided @@ -248,12 +268,12 @@ export async function handleSlackAction( throw new Error("Slack editMessage requires content or blocks."); } if (writeOpts) { - await editSlackMessage(channelId, messageId, content ?? "", { + await slackActionRuntime.editSlackMessage(channelId, messageId, content ?? "", { ...writeOpts, blocks, }); } else { - await editSlackMessage(channelId, messageId, content ?? "", { + await slackActionRuntime.editSlackMessage(channelId, messageId, content ?? "", { blocks, }); } @@ -265,9 +285,9 @@ export async function handleSlackAction( required: true, }); if (writeOpts) { - await deleteSlackMessage(channelId, messageId, writeOpts); + await slackActionRuntime.deleteSlackMessage(channelId, messageId, writeOpts); } else { - await deleteSlackMessage(channelId, messageId); + await slackActionRuntime.deleteSlackMessage(channelId, messageId); } return jsonResult({ ok: true }); } @@ -279,7 +299,7 @@ export async function handleSlackAction( const before = readStringParam(params, "before"); const after = readStringParam(params, "after"); const threadId = readStringParam(params, "threadId"); - const result = await readSlackMessages(channelId, { + const result = await slackActionRuntime.readSlackMessages(channelId, { ...readOpts, limit, before: before ?? undefined, @@ -302,7 +322,7 @@ export async function handleSlackAction( const maxBytes = account.config?.mediaMaxMb ? account.config.mediaMaxMb * 1024 * 1024 : 20 * 1024 * 1024; - const downloaded = await downloadSlackFile(fileId, { + const downloaded = await slackActionRuntime.downloadSlackFile(fileId, { ...readOpts, maxBytes, channelId, @@ -336,9 +356,9 @@ export async function handleSlackAction( required: true, }); if (writeOpts) { - await pinSlackMessage(channelId, messageId, writeOpts); + await slackActionRuntime.pinSlackMessage(channelId, messageId, writeOpts); } else { - await pinSlackMessage(channelId, messageId); + await slackActionRuntime.pinSlackMessage(channelId, messageId); } return jsonResult({ ok: true }); } @@ -347,15 +367,15 @@ export async function handleSlackAction( required: true, }); if (writeOpts) { - await unpinSlackMessage(channelId, messageId, writeOpts); + await slackActionRuntime.unpinSlackMessage(channelId, messageId, writeOpts); } else { - await unpinSlackMessage(channelId, messageId); + await slackActionRuntime.unpinSlackMessage(channelId, messageId); } return jsonResult({ ok: true }); } const pins = writeOpts - ? await listSlackPins(channelId, readOpts) - : await listSlackPins(channelId); + ? await slackActionRuntime.listSlackPins(channelId, readOpts) + : await slackActionRuntime.listSlackPins(channelId); const normalizedPins = pins.map((pin) => { const message = pin.message ? withNormalizedTimestamp( @@ -374,8 +394,8 @@ export async function handleSlackAction( } const userId = readStringParam(params, "userId", { required: true }); const info = writeOpts - ? await getSlackMemberInfo(userId, readOpts) - : await getSlackMemberInfo(userId); + ? await slackActionRuntime.getSlackMemberInfo(userId, readOpts) + : await slackActionRuntime.getSlackMemberInfo(userId); return jsonResult({ ok: true, info }); } @@ -383,7 +403,9 @@ export async function handleSlackAction( if (!isActionEnabled("emojiList")) { throw new Error("Slack emoji list is disabled."); } - const result = readOpts ? await listSlackEmojis(readOpts) : await listSlackEmojis(); + const result = readOpts + ? await slackActionRuntime.listSlackEmojis(readOpts) + : await slackActionRuntime.listSlackEmojis(); const limit = readNumberParam(params, "limit", { integer: true }); if (limit != null && limit > 0 && result.emoji != null) { const entries = Object.entries(result.emoji).toSorted(([a], [b]) => a.localeCompare(b)); diff --git a/src/channels/plugins/slack.actions.ts b/src/channels/plugins/slack.actions.ts index 8920923bc46..c9cf3e9d883 100644 --- a/src/channels/plugins/slack.actions.ts +++ b/src/channels/plugins/slack.actions.ts @@ -1,5 +1,8 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import { handleSlackAction, type SlackActionContext } from "../../agents/tools/slack-actions.js"; +import { + handleSlackAction, + type SlackActionContext, +} from "../../../extensions/slack/runtime-api.js"; import { extractSlackToolSend, isSlackInteractiveRepliesEnabled, @@ -7,6 +10,7 @@ import { resolveSlackChannelId, handleSlackMessageAction, } from "../../plugin-sdk/slack.js"; +import { createLegacyMessageToolDiscoveryMethods } from "./message-tool-legacy.js"; import { createSlackMessageToolBlocksSchema } from "./message-tool-schema.js"; import type { ChannelMessageActionAdapter, ChannelMessageToolDiscovery } from "./types.js"; @@ -48,6 +52,7 @@ export function createSlackActions( return { describeMessageTool, + ...createLegacyMessageToolDiscoveryMethods(describeMessageTool), extractToolSend: ({ args }) => extractSlackToolSend(args), handleAction: async (ctx) => { return await handleSlackMessageAction({ diff --git a/src/plugin-sdk/slack.ts b/src/plugin-sdk/slack.ts index 4b78d14480d..bb3dcfe7c59 100644 --- a/src/plugin-sdk/slack.ts +++ b/src/plugin-sdk/slack.ts @@ -80,4 +80,4 @@ export { export { recordSlackThreadParticipation } from "../../extensions/slack/api.js"; export { handleSlackMessageAction } from "./slack-message-actions.js"; export { createSlackActions } from "../channels/plugins/slack.actions.js"; -export type { SlackActionContext } from "../agents/tools/slack-actions.js"; +export type { SlackActionContext } from "../../extensions/slack/runtime-api.js"; diff --git a/src/plugins/runtime/runtime-slack-ops.runtime.ts b/src/plugins/runtime/runtime-slack-ops.runtime.ts index 65b7ed9e884..8c06f2dda34 100644 --- a/src/plugins/runtime/runtime-slack-ops.runtime.ts +++ b/src/plugins/runtime/runtime-slack-ops.runtime.ts @@ -7,7 +7,7 @@ import { probeSlack as probeSlackImpl } from "../../../extensions/slack/runtime- import { resolveSlackChannelAllowlist as resolveSlackChannelAllowlistImpl } from "../../../extensions/slack/runtime-api.js"; import { resolveSlackUserAllowlist as resolveSlackUserAllowlistImpl } from "../../../extensions/slack/runtime-api.js"; import { sendMessageSlack as sendMessageSlackImpl } from "../../../extensions/slack/runtime-api.js"; -import { handleSlackAction as handleSlackActionImpl } from "../../agents/tools/slack-actions.js"; +import { handleSlackAction as handleSlackActionImpl } from "../../../extensions/slack/runtime-api.js"; import type { PluginRuntimeChannel } from "./types-channel.js"; type RuntimeSlackOps = Pick< diff --git a/src/plugins/runtime/types-channel.ts b/src/plugins/runtime/types-channel.ts index a346a2f8e3a..b3e46b35bb8 100644 --- a/src/plugins/runtime/types-channel.ts +++ b/src/plugins/runtime/types-channel.ts @@ -144,7 +144,7 @@ export type PluginRuntimeChannel = { resolveUserAllowlist: typeof import("../../../extensions/slack/runtime-api.js").resolveSlackUserAllowlist; sendMessageSlack: typeof import("../../../extensions/slack/runtime-api.js").sendMessageSlack; monitorSlackProvider: typeof import("../../../extensions/slack/runtime-api.js").monitorSlackProvider; - handleSlackAction: typeof import("../../agents/tools/slack-actions.js").handleSlackAction; + handleSlackAction: typeof import("../../../extensions/slack/runtime-api.js").handleSlackAction; }; telegram: { auditGroupMembership: typeof import("../../../extensions/telegram/runtime-api.js").auditTelegramGroupMembership;