diff --git a/apps/x/packages/core/src/application/assistant/skills/slack/skill.ts b/apps/x/packages/core/src/application/assistant/skills/slack/skill.ts index 140c6ab7..8b6b09d6 100644 --- a/apps/x/packages/core/src/application/assistant/skills/slack/skill.ts +++ b/apps/x/packages/core/src/application/assistant/skills/slack/skill.ts @@ -1,121 +1,129 @@ -import { slackToolCatalogMarkdown } from "./tool-catalog.js"; - const skill = String.raw` -# Slack Integration Skill +# Slack Integration Skill (agent-slack CLI) -You can interact with Slack to help users communicate with their team. This includes sending messages, viewing channel history, finding users, and searching conversations. +You interact with Slack by running **agent-slack** commands through \`executeCommand\`. -## Prerequisites +--- + +## 1. Check Authentication + +Before any Slack operation, verify credentials: -Before using Slack tools, ALWAYS check if Slack is connected: \`\`\` -slack-checkConnection({}) +executeCommand({ command: "agent-slack auth test" }) \`\`\` -If not connected, inform the user they need to connect Slack from the settings/onboarding. +If auth fails, guide the user: +- **Easiest (macOS):** \`agent-slack auth import-desktop\` — imports tokens from Slack Desktop (no need to quit Slack) +- **Chrome:** \`agent-slack auth import-chrome\` — imports from a logged-in Slack tab in Google Chrome +- **Manual:** \`agent-slack auth add --workspace-url https://team.slack.com --token xoxp-...\` +- **Check configured workspaces:** \`agent-slack auth whoami\` -## Available Tools +--- -### Check Connection -\`\`\` -slack-checkConnection({}) -\`\`\` -Returns whether Slack is connected and ready to use. +## 2. Core Commands -### List Users -\`\`\` -slack-listUsers({ limit: 100 }) -\`\`\` -Lists users in the workspace. Use this to resolve a name to a user ID. +### Messages -### List DM Conversations -\`\`\` -slack-getDirectMessages({ limit: 50 }) -\`\`\` -Lists DM channels (type "im"). Each entry includes the DM channel ID and the user ID. +| Action | Command | +|--------|---------| +| List recent messages | \`agent-slack message list "#channel-name" --limit 25\` | +| List thread replies | \`agent-slack message list "#channel" --thread-ts 1234567890.123456\` | +| Get a single message | \`agent-slack message get "https://team.slack.com/archives/C.../p..."\` | +| Send a message | \`agent-slack message send "#channel-name" "Hello team!"\` | +| Reply in thread | \`agent-slack message send "#channel-name" "Reply text" --thread-ts 1234567890.123456\` | +| Edit a message | \`agent-slack message edit "#channel-name" --ts 1234567890.123456 "Updated text"\` | +| Delete a message | \`agent-slack message delete "#channel-name" --ts 1234567890.123456\` | -### List Channels -\`\`\` -slack-listChannels({ types: "public_channel,private_channel", limit: 100 }) -\`\`\` -Lists channels the user has access to. +**Targets** can be: +- A full Slack URL: \`https://team.slack.com/archives/C01234567/p1234567890123456\` +- A channel name: \`"#general"\` or \`"general"\` +- A channel ID: \`C01234567\` -### Get Conversation History -\`\`\` -slack-getChannelHistory({ channel: "C01234567", limit: 20 }) -\`\`\` -Fetches recent messages for a channel or DM. +### Reactions -### Search Messages \`\`\` -slack-searchMessages({ query: "in:@username", count: 20 }) -\`\`\` -Searches Slack messages using Slack search syntax. - -### Send a Message -\`\`\` -slack-sendMessage({ channel: "C01234567", text: "Hello team!" }) -\`\`\` -Sends a message to a channel or DM. Always show the draft first. - -### Execute a Slack Action -\`\`\` -slack-executeAction({ - toolSlug: "EXACT_TOOL_SLUG_FROM_DISCOVERY", - input: { /* tool-specific parameters */ } -}) -\`\`\` -Executes any Slack tool using its exact slug discovered from \`slack-listAvailableTools\`. - -### Discover Available Tools (Fallback) -\`\`\` -slack-listAvailableTools({ search: "conversation" }) -\`\`\` -Lists available Slack tools from Composio. Use this only if a builtin Slack tool fails and you need a specific slug. - -## Composio Slack Tool Catalog (Pinned) -Use the exact tool slugs below with \`slack-executeAction\` when needed. Prefer these over \`slack-listAvailableTools\` to avoid redundant discovery. - -${slackToolCatalogMarkdown} - -## Workflow - -### Step 1: Check Connection -\`\`\` -slack-checkConnection({}) +agent-slack message react add "" --ts +agent-slack message react remove "" --ts \`\`\` -### Step 2: Choose the Builtin Tool -Use the builtin Slack tools above for common tasks. Only fall back to \`slack-listAvailableTools\` + \`slack-executeAction\` if something is missing. +### Search -## Common Tasks +\`\`\` +agent-slack search messages "query text" --limit 20 +agent-slack search messages "query" --channel "#channel-name" --user "@username" +agent-slack search messages "query" --after 2025-01-01 --before 2025-02-01 +agent-slack search files "query" --limit 10 +\`\`\` -### Find the Most Recent DM with Someone -1. Search messages first: \`slack-searchMessages({ query: "in:@Name", count: 1 })\` -2. If you need exact DM history: - - \`slack-listUsers({})\` to find the user ID - - \`slack-getDirectMessages({})\` to find the DM channel for that user - - \`slack-getChannelHistory({ channel: "D...", limit: 20 })\` +### Channels -### Send a Message -1. Draft the message and show it to the user -2. ONLY after user approval, send using \`slack-sendMessage\` +\`\`\` +agent-slack channel new --name "project-x" --workspace https://team.slack.com +agent-slack channel new --name "secret-project" --private +agent-slack channel invite --channel "#project-x" --users "@alice,@bob" +\`\`\` -### Search Messages -1. Use \`slack-searchMessages({ query: "...", count: 20 })\` +### Users + +\`\`\` +agent-slack user list --limit 200 +agent-slack user get "@username" +agent-slack user get U01234567 +\`\`\` + +### Canvases + +\`\`\` +agent-slack canvas get "https://team.slack.com/docs/F01234567" +agent-slack canvas get F01234567 --workspace https://team.slack.com +\`\`\` + +--- + +## 3. Multi-Workspace + +If the user has multiple workspaces configured, use \`--workspace \` to disambiguate: + +\`\`\` +agent-slack message list "#general" --workspace https://team.slack.com +\`\`\` + +Use \`agent-slack auth whoami\` to see all configured workspaces. + +--- + +## 4. Token Budget Control + +Use \`--limit\` to control how many messages/results are returned. Use \`--max-body-chars\` or \`--max-content-chars\` to truncate long message bodies: + +\`\`\` +agent-slack message list "#channel" --limit 10 +agent-slack search messages "query" --limit 5 --max-content-chars 2000 +\`\`\` + +--- + +## 5. Discovering More Commands + +For any command you're unsure about: + +\`\`\` +agent-slack --help +agent-slack message --help +agent-slack search --help +agent-slack channel --help +\`\`\` + +--- ## Best Practices -- **Always show drafts before sending** - Never send Slack messages without user confirmation -- **Summarize, don't dump** - When showing channel history, summarize the key points -- **Cross-reference with knowledge base** - Check if mentioned people have notes in the knowledge base - -## Error Handling - -If a Slack operation fails: -1. Try \`slack-listAvailableTools\` to verify the tool slug is correct -2. Check if Slack is still connected with \`slack-checkConnection\` -3. Inform the user of the specific error +- **Always show drafts before sending** — Never send Slack messages without user confirmation +- **Summarize, don't dump** — When showing channel history, summarize the key points rather than pasting everything +- **Prefer Slack URLs** — When referring to messages, use Slack URLs over raw channel names when available +- **Use --limit** — Always set reasonable limits to keep output concise and token-efficient +- **Cross-reference with knowledge base** — Check if mentioned people have notes in the knowledge base `; export default skill; diff --git a/apps/x/packages/core/src/application/assistant/skills/slack/tool-catalog.ts b/apps/x/packages/core/src/application/assistant/skills/slack/tool-catalog.ts deleted file mode 100644 index d720c9d9..00000000 --- a/apps/x/packages/core/src/application/assistant/skills/slack/tool-catalog.ts +++ /dev/null @@ -1,117 +0,0 @@ -export type SlackToolDefinition = { - name: string; - slug: string; - description: string; -}; - -export const slackToolCatalog: SlackToolDefinition[] = [ - { name: "Add Emoji Alias", slug: "SLACK_ADD_AN_EMOJI_ALIAS_IN_SLACK", description: "Adds an alias for an existing custom emoji." }, - { name: "Add Remote File", slug: "SLACK_ADD_A_REMOTE_FILE_FROM_A_SERVICE", description: "Adds a reference to an external file (e.g., GDrive, Dropbox) to Slack." }, - { name: "Add Star to Item", slug: "SLACK_ADD_A_STAR_TO_AN_ITEM", description: "Stars a channel, file, comment, or message." }, - { name: "Add Call Participants", slug: "SLACK_ADD_CALL_PARTICIPANTS", description: "Registers new participants added to a Slack call." }, - { name: "Add Emoji", slug: "SLACK_ADD_EMOJI", description: "Adds a custom emoji to a workspace via a unique name and URL." }, - { name: "Add Reaction", slug: "SLACK_ADD_REACTION_TO_AN_ITEM", description: "Adds a specified emoji reaction to a message." }, - { name: "Archive Channel", slug: "SLACK_ARCHIVE_A_PUBLIC_OR_PRIVATE_CHANNEL", description: "Archives a public or private channel." }, - { name: "Archive Conversation", slug: "SLACK_ARCHIVE_A_SLACK_CONVERSATION", description: "Archives a conversation by its ID." }, - { name: "Close DM/MPDM", slug: "SLACK_CLOSE_DM_OR_MULTI_PERSON_DM", description: "Closes a DM or MPDM sidebar view for the user." }, - { name: "Create Reminder", slug: "SLACK_CREATE_A_REMINDER", description: "Creates a reminder with text and time (natural language supported)." }, - { name: "Create User Group", slug: "SLACK_CREATE_A_SLACK_USER_GROUP", description: "Creates a new user group (subteam)." }, - { name: "Create Channel", slug: "SLACK_CREATE_CHANNEL", description: "Initiates a public or private channel conversation." }, - { name: "Create Channel Conversation", slug: "SLACK_CREATE_CHANNEL_BASED_CONVERSATION", description: "Creates a new channel with specific org-wide or team settings." }, - { name: "Customize URL Unfurl", slug: "SLACK_CUSTOMIZE_URL_UNFURL", description: "Defines custom content for URL previews in a specific message." }, - { name: "Delete File Comment", slug: "SLACK_DELETE_A_COMMENT_ON_A_FILE", description: "Deletes a specific comment from a file." }, - { name: "Delete File", slug: "SLACK_DELETE_A_FILE_BY_ID", description: "Permanently deletes a file by its ID." }, - { name: "Delete Channel", slug: "SLACK_DELETE_A_PUBLIC_OR_PRIVATE_CHANNEL", description: "Irreversibly deletes a channel and its history (Enterprise only)." }, - { name: "Delete Scheduled Message", slug: "SLACK_DELETE_A_SCHEDULED_MESSAGE_IN_A_CHAT", description: "Deletes a pending scheduled message." }, - { name: "Delete Reminder", slug: "SLACK_DELETE_A_SLACK_REMINDER", description: "Deletes an existing reminder." }, - { name: "Delete Message", slug: "SLACK_DELETES_A_MESSAGE_FROM_A_CHAT", description: "Deletes a message by channel ID and timestamp." }, - { name: "Delete Profile Photo", slug: "SLACK_DELETE_USER_PROFILE_PHOTO", description: "Reverts the user's profile photo to the default avatar." }, - { name: "Disable User Group", slug: "SLACK_DISABLE_AN_EXISTING_SLACK_USER_GROUP", description: "Disables (archives) a user group." }, - { name: "Enable User Group", slug: "SLACK_ENABLE_A_SPECIFIED_USER_GROUP", description: "Reactivates a disabled user group." }, - { name: "Share File Publicly", slug: "SLACK_ENABLE_PUBLIC_SHARING_OF_A_FILE", description: "Generates a public URL for a file." }, - { name: "End Call", slug: "SLACK_END_A_CALL_WITH_DURATION_AND_ID", description: "Ends an ongoing call." }, - { name: "End Snooze", slug: "SLACK_END_SNOOZE", description: "Ends the current user's snooze mode immediately." }, - { name: "End DND Session", slug: "SLACK_END_USER_DO_NOT_DISTURB_SESSION", description: "Ends the current DND session." }, - { name: "Fetch Bot Info", slug: "SLACK_FETCH_BOT_USER_INFORMATION", description: "Fetches metadata for a specific bot user." }, - { name: "Fetch History", slug: "SLACK_FETCH_CONVERSATION_HISTORY", description: "Fetches chronological messages and events from a channel." }, - { name: "Fetch Item Reactions", slug: "SLACK_FETCH_ITEM_REACTIONS", description: "Fetches all reactions for a message, file, or comment." }, - { name: "Retrieve Replies", slug: "SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION", description: "Retrieves replies to a specific parent message." }, - { name: "Fetch Team Info", slug: "SLACK_FETCH_TEAM_INFO", description: "Fetches comprehensive metadata about the team." }, - { name: "Fetch Workspace Settings", slug: "SLACK_FETCH_WORKSPACE_SETTINGS_INFORMATION", description: "Retrieves detailed settings for a specific workspace." }, - { name: "Find Channels", slug: "SLACK_FIND_CHANNELS", description: "Searches channels by name, topic, or purpose." }, - { name: "Find User by Email", slug: "SLACK_FIND_USER_BY_EMAIL_ADDRESS", description: "Finds a user object using their email address." }, - { name: "Find Users", slug: "SLACK_FIND_USERS", description: "Searches users by name, email, or display name." }, - { name: "Get Conversation Preferences", slug: "SLACK_GET_CHANNEL_CONVERSATION_PREFERENCES", description: "Retrieves posting/threading preferences for a channel." }, - { name: "Get Reminder Info", slug: "SLACK_GET_REMINDER_INFORMATION", description: "Retrieves detailed information for a specific reminder." }, - { name: "Get Remote File", slug: "SLACK_GET_REMOTE_FILE", description: "Retrieves info about a previously added remote file." }, - { name: "Get Team DND Status", slug: "SLACK_GET_TEAM_DND_STATUS", description: "Retrieves the DND status for specific users." }, - { name: "Get User Presence", slug: "SLACK_GET_USER_PRESENCE_INFO", description: "Retrieves real-time presence (active/away)." }, - { name: "Invite to Channel", slug: "SLACK_INVITE_USERS_TO_A_SLACK_CHANNEL", description: "Invites users to a channel by their user IDs." }, - { name: "Invite to Workspace", slug: "SLACK_INVITE_USER_TO_WORKSPACE", description: "Invites a user to a workspace and channels via email." }, - { name: "Join Conversation", slug: "SLACK_JOIN_AN_EXISTING_CONVERSATION", description: "Joins a conversation by channel ID." }, - { name: "Leave Conversation", slug: "SLACK_LEAVE_A_CONVERSATION", description: "Leaves a conversation." }, - { name: "List All Channels", slug: "SLACK_LIST_ALL_CHANNELS", description: "Lists all conversations with various filters." }, - { name: "List All Users", slug: "SLACK_LIST_ALL_USERS", description: "Retrieves a paginated list of all users in the workspace." }, - { name: "List User Group Members", slug: "SLACK_LIST_ALL_USERS_IN_A_USER_GROUP", description: "Lists all user IDs within a group." }, - { name: "List Conversations", slug: "SLACK_LIST_CONVERSATIONS", description: "Retrieves conversations accessible to a specific user." }, - { name: "List Files", slug: "SLACK_LIST_FILES_WITH_FILTERS_IN_SLACK", description: "Lists files and metadata with filtering options." }, - { name: "List Reminders", slug: "SLACK_LIST_REMINDERS", description: "Lists all reminders for the authenticated user." }, - { name: "List Remote Files", slug: "SLACK_LIST_REMOTE_FILES", description: "Retrieves info about a team's remote files." }, - { name: "List Scheduled Messages", slug: "SLACK_LIST_SCHEDULED_MESSAGES", description: "Lists pending scheduled messages." }, - { name: "List Pinned Items", slug: "SLACK_LISTS_PINNED_ITEMS_IN_A_CHANNEL", description: "Retrieves all messages/files pinned to a channel." }, - { name: "List Starred Items", slug: "SLACK_LIST_STARRED_ITEMS", description: "Lists items starred by the user." }, - { name: "List Custom Emojis", slug: "SLACK_LIST_TEAM_CUSTOM_EMOJIS", description: "Lists all workspace custom emojis and their URLs." }, - { name: "List User Groups", slug: "SLACK_LIST_USER_GROUPS_FOR_TEAM_WITH_OPTIONS", description: "Lists user-created and default user groups." }, - { name: "List User Reactions", slug: "SLACK_LIST_USER_REACTIONS", description: "Lists all reactions added by a specific user." }, - { name: "List Admin Users", slug: "SLACK_LIST_WORKSPACE_USERS", description: "Retrieves a paginated list of workspace administrators." }, - { name: "Set User Presence", slug: "SLACK_MANUALLY_SET_USER_PRESENCE", description: "Manually overrides automated presence status." }, - { name: "Mark Reminder Complete", slug: "SLACK_MARK_REMINDER_AS_COMPLETE", description: "Marks a reminder as complete (deprecated by Slack in March 2023)." }, - { name: "Open DM", slug: "SLACK_OPEN_DM", description: "Opens/resumes a DM or MPDM." }, - { name: "Pin Item", slug: "SLACK_PINS_AN_ITEM_TO_A_CHANNEL", description: "Pins a message to a channel." }, - { name: "Remove Remote File", slug: "SLACK_REMOVE_A_REMOTE_FILE", description: "Removes a reference to an external file." }, - { name: "Remove Star", slug: "SLACK_REMOVE_A_STAR_FROM_AN_ITEM", description: "Unstars an item." }, - { name: "Remove from Channel", slug: "SLACK_REMOVE_A_USER_FROM_A_CONVERSATION", description: "Removes a specified user from a conversation." }, - { name: "Remove Call Participants", slug: "SLACK_REMOVE_CALL_PARTICIPANTS", description: "Registers the removal of participants from a call." }, - { name: "Remove Reaction", slug: "SLACK_REMOVE_REACTION_FROM_ITEM", description: "Removes an emoji reaction from an item." }, - { name: "Rename Conversation", slug: "SLACK_RENAME_A_CONVERSATION", description: "Renames a channel ID/Conversation." }, - { name: "Rename Emoji", slug: "SLACK_RENAME_AN_EMOJI", description: "Renames a custom emoji." }, - { name: "Rename Channel", slug: "SLACK_RENAME_A_SLACK_CHANNEL", description: "Renames a public or private channel." }, - { name: "Retrieve Identity", slug: "SLACK_RETRIEVE_A_USER_S_IDENTITY_DETAILS", description: "Retrieves basic user/team identity details." }, - { name: "Retrieve Call Info", slug: "SLACK_RETRIEVE_CALL_INFORMATION", description: "Retrieves a snapshot of a call's status." }, - { name: "Retrieve Conversation Info", slug: "SLACK_RETRIEVE_CONVERSATION_INFORMATION", description: "Retrieves metadata for a specific conversation." }, - { name: "Get Conversation Members", slug: "SLACK_RETRIEVE_CONVERSATION_MEMBERS_LIST", description: "Lists active user IDs in a conversation." }, - { name: "Retrieve User DND", slug: "SLACK_RETRIEVE_CURRENT_USER_DND_STATUS", description: "Retrieves DND status for a user." }, - { name: "Retrieve File Details", slug: "SLACK_RETRIEVE_DETAILED_INFORMATION_ABOUT_A_FILE", description: "Retrieves metadata and comments for a file." }, - { name: "Retrieve User Details", slug: "SLACK_RETRIEVE_DETAILED_USER_INFORMATION", description: "Retrieves comprehensive info for a specific user ID." }, - { name: "Get Message Permalink", slug: "SLACK_RETRIEVE_MESSAGE_PERMALINK_URL", description: "Gets the permalink URL for a specific message." }, - { name: "Retrieve Team Profile", slug: "SLACK_RETRIEVE_TEAM_PROFILE_DETAILS", description: "Retrieves the profile field structure for a team." }, - { name: "Retrieve User Profile", slug: "SLACK_RETRIEVE_USER_PROFILE_INFORMATION", description: "Retrieves specific profile info for a user." }, - { name: "Revoke Public File", slug: "SLACK_REVOKE_PUBLIC_SHARING_ACCESS_FOR_A_FILE", description: "Revokes a file's public sharing URL." }, - { name: "Schedule Message", slug: "SLACK_SCHEDULE_MESSAGE", description: "Schedules a message for a future time (up to 120 days)." }, - { name: "Search Messages", slug: "SLACK_SEARCH_MESSAGES", description: "Workspace-wide message search with advanced filters." }, - { name: "Send Ephemeral", slug: "SLACK_SEND_EPHEMERAL_MESSAGE", description: "Sends a message visible only to a specific user." }, - { name: "Send Message", slug: "SLACK_SEND_MESSAGE", description: "Posts a message to a channel, DM, or group." }, - { name: "Set Conversation Purpose", slug: "SLACK_SET_A_CONVERSATION_S_PURPOSE", description: "Updates the purpose description of a channel." }, - { name: "Set DND Duration", slug: "SLACK_SET_DND_DURATION", description: "Turns on DND or changes its current duration." }, - { name: "Set Profile Photo", slug: "SLACK_SET_PROFILE_PHOTO", description: "Sets the user's profile image with cropping." }, - { name: "Set Read Cursor", slug: "SLACK_SET_READ_CURSOR_IN_A_CONVERSATION", description: "Marks a specific timestamp as read." }, - { name: "Set User Profile", slug: "SLACK_SET_SLACK_USER_PROFILE_INFORMATION", description: "Updates individual or multiple user profile fields." }, - { name: "Set Conversation Topic", slug: "SLACK_SET_THE_TOPIC_OF_A_CONVERSATION", description: "Updates the topic of a conversation." }, - { name: "Share Me Message", slug: "SLACK_SHARE_A_ME_MESSAGE_IN_A_CHANNEL", description: "Sends a third-person user action message (/me)." }, - { name: "Share Remote File", slug: "SLACK_SHARE_REMOTE_FILE_IN_CHANNELS", description: "Shares a registered remote file into channels." }, - { name: "Start Call", slug: "SLACK_START_CALL", description: "Registers a new call for third-party integration." }, - { name: "Start RTM Session", slug: "SLACK_START_REAL_TIME_MESSAGING_SESSION", description: "Initiates a real-time messaging WebSocket session." }, - { name: "Unarchive Channel", slug: "SLACK_UNARCHIVE_A_PUBLIC_OR_PRIVATE_CHANNEL", description: "Unarchives a specific channel." }, - { name: "Unarchive Conversation", slug: "SLACK_UNARCHIVE_CHANNEL", description: "Reverses archival for a conversation." }, - { name: "Unpin Item", slug: "SLACK_UNPIN_ITEM_FROM_CHANNEL", description: "Unpins a message from a channel." }, - { name: "Update User Group", slug: "SLACK_UPDATE_AN_EXISTING_SLACK_USER_GROUP", description: "Updates name, handle, or channels for a user group." }, - { name: "Update Remote File", slug: "SLACK_UPDATES_AN_EXISTING_REMOTE_FILE", description: "Updates metadata for a remote file reference." }, - { name: "Update Message", slug: "SLACK_UPDATES_A_SLACK_MESSAGE", description: "Modifies the content of an existing message." }, - { name: "Update Call Info", slug: "SLACK_UPDATE_SLACK_CALL_INFORMATION", description: "Updates call title or join URLs." }, - { name: "Update Group Members", slug: "SLACK_UPDATE_USER_GROUP_MEMBERS", description: "Replaces the member list of a user group." }, - { name: "Upload File", slug: "SLACK_UPLOAD_OR_CREATE_A_FILE_IN_SLACK", description: "Uploads content or binary files to Slack." }, -]; - -export const slackToolCatalogMarkdown = slackToolCatalog - .map((tool) => `- ${tool.name} (${tool.slug}) - ${tool.description}`) - .join("\n"); diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index feb41a7f..33e426c1 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -12,9 +12,6 @@ import { McpServerDefinition } from "@x/shared/dist/mcp.js"; import * as workspace from "../../workspace/workspace.js"; import { IAgentsRepo } from "../../agents/repo.js"; import { WorkDir } from "../../config/config.js"; -import { composioAccountsRepo } from "../../composio/repo.js"; -import { executeAction as executeComposioAction, isConfigured as isComposioConfigured, listToolkitTools } from "../../composio/client.js"; -import { slackToolCatalog } from "../assistant/skills/slack/tool-catalog.js"; import type { ToolContext } from "./exec-tool.js"; import { generateText } from "ai"; import { createProvider } from "../../models/models.js"; @@ -36,232 +33,6 @@ const BuiltinToolsSchema = z.record(z.string(), z.object({ isAvailable: z.custom<() => Promise>().optional(), })); -type SlackToolHint = { - search?: string; - patterns: string[]; - fallbackSlugs?: string[]; - preferSlugIncludes?: string[]; - excludePatterns?: string[]; - minScore?: number; -}; - -const slackToolHints: Record = { - sendMessage: { - search: "message", - patterns: ["send", "message", "channel"], - fallbackSlugs: [ - "SLACK_SEND_MESSAGE", - "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", - "SLACK_SEND_A_MESSAGE", - ], - }, - listConversations: { - search: "conversation", - patterns: ["list", "conversation", "channel"], - fallbackSlugs: [ - "SLACK_LIST_CONVERSATIONS", - "SLACK_LIST_ALL_CHANNELS", - "SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS", - "SLACK_LIST_CHANNELS", - "SLACK_LIST_CHANNEL", - ], - preferSlugIncludes: ["list", "conversation"], - minScore: 2, - }, - getConversationHistory: { - search: "history", - patterns: ["history", "conversation", "message"], - fallbackSlugs: [ - "SLACK_FETCH_CONVERSATION_HISTORY", - "SLACK_FETCHES_CONVERSATION_HISTORY", - "SLACK_GET_CONVERSATION_HISTORY", - "SLACK_GET_CHANNEL_HISTORY", - ], - preferSlugIncludes: ["history"], - minScore: 2, - }, - listUsers: { - search: "user", - patterns: ["list", "user"], - fallbackSlugs: [ - "SLACK_LIST_ALL_USERS", - "SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION", - "SLACK_LIST_USERS", - "SLACK_GET_USERS", - "SLACK_USERS_LIST", - ], - preferSlugIncludes: ["list", "user"], - excludePatterns: ["find", "by name", "by email", "by_email", "by_name", "lookup", "profile", "info"], - minScore: 2, - }, - getUserInfo: { - search: "user", - patterns: ["user", "info", "profile"], - fallbackSlugs: [ - "SLACK_GET_USER_INFO", - "SLACK_GET_USER", - "SLACK_USER_INFO", - ], - preferSlugIncludes: ["user", "info"], - minScore: 1, - }, - searchMessages: { - search: "search", - patterns: ["search", "message"], - fallbackSlugs: [ - "SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY", - "SLACK_SEARCH_MESSAGES", - "SLACK_SEARCH_MESSAGE", - ], - preferSlugIncludes: ["search"], - minScore: 1, - }, -}; - -const slackToolSlugCache = new Map(); - -const slackToolSlugOverrides: Partial> = { - sendMessage: "SLACK_SEND_MESSAGE", - listConversations: "SLACK_LIST_CONVERSATIONS", - getConversationHistory: "SLACK_FETCH_CONVERSATION_HISTORY", - listUsers: "SLACK_LIST_ALL_USERS", - getUserInfo: "SLACK_RETRIEVE_DETAILED_USER_INFORMATION", - searchMessages: "SLACK_SEARCH_MESSAGES", -}; - -const compactObject = (input: Record) => - Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined)); - -type SlackToolResult = { success: boolean; data?: unknown; error?: string }; - -/** Helper to execute a Slack tool with consistent account validation and error handling */ -async function executeSlackTool( - hintKey: keyof typeof slackToolHints, - params: Record -): Promise { - const account = composioAccountsRepo.getAccount('slack'); - if (!account || account.status !== 'ACTIVE') { - return { success: false, error: 'Slack is not connected' }; - } - try { - const toolSlug = await resolveSlackToolSlug(hintKey); - return await executeComposioAction(toolSlug, account.id, compactObject(params)); - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -} - -const normalizeSlackTool = (tool: { slug: string; name?: string; description?: string }) => - `${tool.slug} ${tool.name || ""} ${tool.description || ""}`.toLowerCase(); - -const scoreSlackTool = (tool: { slug: string; name?: string; description?: string }, patterns: string[]) => { - const slug = tool.slug.toLowerCase(); - const name = (tool.name || "").toLowerCase(); - const description = (tool.description || "").toLowerCase(); - - let score = 0; - for (const pattern of patterns) { - const needle = pattern.toLowerCase(); - if (slug.includes(needle)) score += 3; - if (name.includes(needle)) score += 2; - if (description.includes(needle)) score += 1; - } - return score; -}; - -const pickSlackTool = ( - tools: Array<{ slug: string; name?: string; description?: string }>, - hint: SlackToolHint, -) => { - let candidates = tools; - - if (hint.excludePatterns && hint.excludePatterns.length > 0) { - candidates = candidates.filter((tool) => { - const haystack = normalizeSlackTool(tool); - return !hint.excludePatterns!.some((pattern) => haystack.includes(pattern.toLowerCase())); - }); - } - - if (hint.preferSlugIncludes && hint.preferSlugIncludes.length > 0) { - const preferred = candidates.filter((tool) => - hint.preferSlugIncludes!.every((pattern) => tool.slug.toLowerCase().includes(pattern.toLowerCase())) - ); - if (preferred.length > 0) { - candidates = preferred; - } - } - - let best: { slug: string; name?: string; description?: string } | null = null; - let bestScore = 0; - - for (const tool of candidates) { - const score = scoreSlackTool(tool, hint.patterns); - if (score > bestScore) { - bestScore = score; - best = tool; - } - } - - if (!best || (hint.minScore !== undefined && bestScore < hint.minScore)) { - return null; - } - - return best; -}; - -const resolveSlackToolSlug = async (hintKey: keyof typeof slackToolHints) => { - const cached = slackToolSlugCache.get(hintKey); - if (cached) return cached; - - const hint = slackToolHints[hintKey]; - - const override = slackToolSlugOverrides[hintKey]; - if (override && slackToolCatalog.some((tool) => tool.slug === override)) { - slackToolSlugCache.set(hintKey, override); - return override; - } - const resolveFromTools = (tools: Array<{ slug: string; name?: string; description?: string }>) => { - if (hint.fallbackSlugs && hint.fallbackSlugs.length > 0) { - const fallbackSet = new Set(hint.fallbackSlugs.map((slug) => slug.toLowerCase())); - const fallback = tools.find((tool) => fallbackSet.has(tool.slug.toLowerCase())); - if (fallback) return fallback.slug; - } - - const best = pickSlackTool(tools, hint); - return best?.slug || null; - }; - - const initialTools = slackToolCatalog; - - if (!initialTools.length) { - throw new Error("No Slack tools returned from Composio"); - } - - const initialSlug = resolveFromTools(initialTools); - if (initialSlug) { - slackToolSlugCache.set(hintKey, initialSlug); - return initialSlug; - } - - const allSlug = resolveFromTools(slackToolCatalog); - - if (!allSlug) { - const fallback = await listToolkitTools("slack", hint.search || null); - const fallbackSlug = resolveFromTools(fallback.items || []); - if (!fallbackSlug) { - throw new Error(`Unable to resolve Slack tool for ${hintKey}. Try slack-listAvailableTools.`); - } - slackToolSlugCache.set(hintKey, fallbackSlug); - return fallbackSlug; - } - - slackToolSlugCache.set(hintKey, allSlug); - return allSlug; -}; - const LLMPARSE_MIME_TYPES: Record = { '.pdf': 'application/pdf', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', @@ -1109,164 +880,6 @@ export const BuiltinTools: z.infer = { }, }, - // ============================================================================ - // Slack Tools (via Composio) - // ============================================================================ - - 'slack-checkConnection': { - description: 'Check if Slack is connected and ready to use. Use this before other Slack operations.', - inputSchema: z.object({}), - execute: async () => { - if (!isComposioConfigured()) { - return { - connected: false, - error: 'Composio is not configured. Please set up your Composio API key first.', - }; - } - const account = composioAccountsRepo.getAccount('slack'); - if (!account || account.status !== 'ACTIVE') { - return { - connected: false, - error: 'Slack is not connected. Please connect Slack from the settings.', - }; - } - return { - connected: true, - accountId: account.id, - }; - }, - }, - - 'slack-listAvailableTools': { - description: 'List available Slack tools from Composio. Use this to discover the correct tool slugs before executing actions. Call this first if other Slack tools return errors.', - inputSchema: z.object({ - search: z.string().optional().describe('Optional search query to filter tools (e.g., "message", "channel", "user")'), - }), - execute: async ({ search }: { search?: string }) => { - if (!isComposioConfigured()) { - return { success: false, error: 'Composio is not configured' }; - } - - try { - const result = await listToolkitTools('slack', search || null); - return { - success: true, - tools: result.items, - count: result.items.length, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }, - }, - - 'slack-executeAction': { - description: 'Execute a Slack action by its Composio tool slug. Use slack-listAvailableTools first to discover correct slugs. Pass the exact slug and the required input parameters.', - inputSchema: z.object({ - toolSlug: z.string().describe('The exact Composio tool slug (e.g., "SLACKBOT_SEND_A_MESSAGE_TO_A_SLACK_CHANNEL")'), - input: z.record(z.string(), z.unknown()).describe('Input parameters for the tool (check the tool description for required fields)'), - }), - execute: async ({ toolSlug, input }: { toolSlug: string; input: Record }) => { - const account = composioAccountsRepo.getAccount('slack'); - if (!account || account.status !== 'ACTIVE') { - return { success: false, error: 'Slack is not connected' }; - } - - try { - const result = await executeComposioAction(toolSlug, account.id, input); - return result; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }, - }, - - 'slack-sendMessage': { - description: 'Send a message to a Slack channel or user. Requires channel ID (starts with C for channels, D for DMs) or user ID.', - inputSchema: z.object({ - channel: z.string().describe('Channel ID (e.g., C01234567) or user ID (e.g., U01234567) to send the message to'), - text: z.string().describe('The message text to send'), - }), - execute: async ({ channel, text }: { channel: string; text: string }) => { - return executeSlackTool("sendMessage", { channel, text }); - }, - }, - - 'slack-listChannels': { - description: 'List Slack channels the user has access to. Returns channel IDs and names.', - inputSchema: z.object({ - types: z.string().optional().describe('Comma-separated channel types: public_channel, private_channel, mpim, im (default: public_channel,private_channel)'), - limit: z.number().optional().describe('Maximum number of channels to return (default: 100)'), - }), - execute: async ({ types, limit }: { types?: string; limit?: number }) => { - return executeSlackTool("listConversations", { - types: types || "public_channel,private_channel", - limit: limit ?? 100, - }); - }, - }, - - 'slack-getChannelHistory': { - description: 'Get recent messages from a Slack channel. Returns message history with timestamps and user IDs.', - inputSchema: z.object({ - channel: z.string().describe('Channel ID to get history from (e.g., C01234567)'), - limit: z.number().optional().describe('Maximum number of messages to return (default: 20, max: 100)'), - }), - execute: async ({ channel, limit }: { channel: string; limit?: number }) => { - return executeSlackTool("getConversationHistory", { - channel, - limit: limit !== undefined ? Math.min(limit, 100) : 20, - }); - }, - }, - - 'slack-listUsers': { - description: 'List users in the Slack workspace. Returns user IDs, names, and profile info.', - inputSchema: z.object({ - limit: z.number().optional().describe('Maximum number of users to return (default: 100)'), - }), - execute: async ({ limit }: { limit?: number }) => { - return executeSlackTool("listUsers", { limit: limit ?? 100 }); - }, - }, - - 'slack-getUserInfo': { - description: 'Get detailed information about a specific Slack user by their user ID.', - inputSchema: z.object({ - user: z.string().describe('User ID to get info for (e.g., U01234567)'), - }), - execute: async ({ user }: { user: string }) => { - return executeSlackTool("getUserInfo", { user }); - }, - }, - - 'slack-searchMessages': { - description: 'Search for messages in Slack. Find messages containing specific text across channels.', - inputSchema: z.object({ - query: z.string().describe('Search query text'), - count: z.number().optional().describe('Maximum number of results (default: 20)'), - }), - execute: async ({ query, count }: { query: string; count?: number }) => { - return executeSlackTool("searchMessages", { query, count: count ?? 20 }); - }, - }, - - 'slack-getDirectMessages': { - description: 'List direct message (DM) channels. Returns IDs of DM conversations with other users.', - inputSchema: z.object({ - limit: z.number().optional().describe('Maximum number of DM channels to return (default: 50)'), - }), - execute: async ({ limit }: { limit?: number }) => { - return executeSlackTool("listConversations", { types: "im", limit: limit ?? 50 }); - }, - }, - // ============================================================================ // Web Search (Brave Search API) // ============================================================================