mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-27 17:36:25 +02:00
Slack2 (#410)
* agent slack * add to settings * select workspace * skill changes * chain commands * fix: Use Git Bash on Windows instead of hardcoded /bin/sh for command execution * Refactor command execution shell handling Replace the getShell() function with EXECUTION_SHELL for consistency in command execution. This change simplifies the code and aligns with the recent updates from the dev branch. --------- Co-authored-by: Tushar Magar <tushmag@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
70635ad245
commit
0be3e60560
12 changed files with 502 additions and 848 deletions
|
|
@ -33,7 +33,7 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects,
|
|||
|
||||
**Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. This is required for any document creation or editing task. The skill provides structured guidance for creating, editing, and refining documents in the knowledge base.
|
||||
|
||||
**Slack:** When users ask about Slack messages, want to send messages to teammates, check channel conversations, or find someone on Slack, load the \`slack\` skill. You can send messages, view channel history, search conversations, and find users. Always check if Slack is connected first with \`slack-checkConnection\`, and always show message drafts to the user before sending.
|
||||
**Slack:** When users ask about Slack messages, want to send messages to teammates, check channel conversations, or find someone on Slack, load the \`slack\` skill. You can send messages, view channel history, search conversations, and find users. Always show message drafts to the user before sending.
|
||||
|
||||
## Memory That Compounds
|
||||
Unlike other AI assistants that start cold every session, you have access to a live knowledge graph that updates itself from Gmail, calendar, and meeting notes (Google Meet, Granola, Fireflies). This isn't just summaries - it's structured extraction of decisions, commitments, open questions, and context, routed to long-lived notes for each person, project, and topic.
|
||||
|
|
|
|||
|
|
@ -1,121 +1,124 @@
|
|||
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 Connection
|
||||
|
||||
Before any Slack operation, read \`~/.rowboat/config/slack.json\`. If \`enabled\` is \`false\` or the \`workspaces\` array is empty, simply tell the user: "Slack is not enabled. You can enable it in the Connectors settings." Do not attempt any agent-slack commands.
|
||||
|
||||
If enabled, use the workspace URLs from the config for all commands.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Commands
|
||||
|
||||
### Messages
|
||||
|
||||
| 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\` |
|
||||
|
||||
**Targets** can be:
|
||||
- A full Slack URL: \`https://team.slack.com/archives/C01234567/p1234567890123456\`
|
||||
- A channel name: \`"#general"\` or \`"general"\`
|
||||
- A channel ID: \`C01234567\`
|
||||
|
||||
### Reactions
|
||||
|
||||
Before using Slack tools, ALWAYS check if Slack is connected:
|
||||
\`\`\`
|
||||
slack-checkConnection({})
|
||||
agent-slack message react add "<target>" <emoji> --ts <ts>
|
||||
agent-slack message react remove "<target>" <emoji> --ts <ts>
|
||||
\`\`\`
|
||||
|
||||
If not connected, inform the user they need to connect Slack from the settings/onboarding.
|
||||
### Search
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Check Connection
|
||||
\`\`\`
|
||||
slack-checkConnection({})
|
||||
\`\`\`
|
||||
Returns whether Slack is connected and ready to use.
|
||||
|
||||
### List Users
|
||||
\`\`\`
|
||||
slack-listUsers({ limit: 100 })
|
||||
\`\`\`
|
||||
Lists users in the workspace. Use this to resolve a name to a user ID.
|
||||
|
||||
### List DM Conversations
|
||||
\`\`\`
|
||||
slack-getDirectMessages({ limit: 50 })
|
||||
\`\`\`
|
||||
Lists DM channels (type "im"). Each entry includes the DM channel ID and the user ID.
|
||||
|
||||
### List Channels
|
||||
\`\`\`
|
||||
slack-listChannels({ types: "public_channel,private_channel", limit: 100 })
|
||||
\`\`\`
|
||||
Lists channels the user has access to.
|
||||
|
||||
### Get Conversation History
|
||||
\`\`\`
|
||||
slack-getChannelHistory({ channel: "C01234567", limit: 20 })
|
||||
\`\`\`
|
||||
Fetches recent messages for a channel or DM.
|
||||
|
||||
### 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 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
|
||||
\`\`\`
|
||||
|
||||
### 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.
|
||||
### Channels
|
||||
|
||||
## Common Tasks
|
||||
\`\`\`
|
||||
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"
|
||||
\`\`\`
|
||||
|
||||
### 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 })\`
|
||||
### Users
|
||||
|
||||
### Send a Message
|
||||
1. Draft the message and show it to the user
|
||||
2. ONLY after user approval, send using \`slack-sendMessage\`
|
||||
\`\`\`
|
||||
agent-slack user list --limit 200
|
||||
agent-slack user get "@username"
|
||||
agent-slack user get U01234567
|
||||
\`\`\`
|
||||
|
||||
### Search Messages
|
||||
1. Use \`slack-searchMessages({ query: "...", count: 20 })\`
|
||||
### Canvases
|
||||
|
||||
\`\`\`
|
||||
agent-slack canvas get "https://team.slack.com/docs/F01234567"
|
||||
agent-slack canvas get F01234567 --workspace https://team.slack.com
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## 3. Multi-Workspace
|
||||
|
||||
**Important:** The user has chosen which workspaces to use. Before your first Slack operation, read \`~/.rowboat/config/slack.json\` to see the selected workspaces. Only interact with workspaces listed in that config — ignore any other authenticated workspaces.
|
||||
|
||||
If the selected workspace list contains multiple entries, use \`--workspace <url>\` to disambiguate:
|
||||
|
||||
\`\`\`
|
||||
agent-slack message list "#general" --workspace https://team.slack.com
|
||||
\`\`\`
|
||||
|
||||
If only one workspace is selected, always use \`--workspace\` with its URL to avoid ambiguity with other authenticated 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
|
||||
- **Resolve user IDs** — Messages contain raw user IDs like \`U078AHJP341\`. Resolve them to real names before presenting to the user. Batch all lookups into a single \`executeCommand\` call using \`;\` separators, e.g. \`agent-slack user get U078AHJP341 --workspace ... ; agent-slack user get U090UEZCEQ0 --workspace ...\`
|
||||
- **Cross-reference with knowledge base** — Check if mentioned people have notes in the knowledge base
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -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<boolean>>().optional(),
|
||||
}));
|
||||
|
||||
type SlackToolHint = {
|
||||
search?: string;
|
||||
patterns: string[];
|
||||
fallbackSlugs?: string[];
|
||||
preferSlugIncludes?: string[];
|
||||
excludePatterns?: string[];
|
||||
minScore?: number;
|
||||
};
|
||||
|
||||
const slackToolHints: Record<string, SlackToolHint> = {
|
||||
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<string, string>();
|
||||
|
||||
const slackToolSlugOverrides: Partial<Record<keyof typeof slackToolHints, string>> = {
|
||||
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<string, unknown>) =>
|
||||
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<string, unknown>
|
||||
): Promise<SlackToolResult> {
|
||||
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<string, string> = {
|
||||
'.pdf': 'application/pdf',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
|
|
@ -1109,164 +880,6 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
},
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// 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<string, unknown> }) => {
|
||||
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)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { getSecurityAllowList } from '../../config/security.js';
|
|||
import { getExecutionShell } from '../assistant/runtime-context.js';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
const COMMAND_SPLIT_REGEX = /(?:\|\||&&|;|\||\n|`|\$\(|\(|\))/;
|
||||
const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
|
||||
const WRAPPER_COMMANDS = new Set(['sudo', 'env', 'time', 'command']);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/re
|
|||
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
|
||||
import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js";
|
||||
import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
|
||||
import { FSSlackConfigRepo, ISlackConfigRepo } from "../slack/repo.js";
|
||||
|
||||
const container = createContainer({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
|
|
@ -37,6 +38,7 @@ container.register({
|
|||
granolaConfigRepo: asClass<IGranolaConfigRepo>(FSGranolaConfigRepo).singleton(),
|
||||
agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(),
|
||||
agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(),
|
||||
slackConfigRepo: asClass<ISlackConfigRepo>(FSSlackConfigRepo).singleton(),
|
||||
});
|
||||
|
||||
export default container;
|
||||
41
apps/x/packages/core/src/slack/repo.ts
Normal file
41
apps/x/packages/core/src/slack/repo.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { SlackConfig } from './types.js';
|
||||
|
||||
export interface ISlackConfigRepo {
|
||||
getConfig(): Promise<SlackConfig>;
|
||||
setConfig(config: SlackConfig): Promise<void>;
|
||||
}
|
||||
|
||||
export class FSSlackConfigRepo implements ISlackConfigRepo {
|
||||
private readonly configPath = path.join(WorkDir, 'config', 'slack.json');
|
||||
private readonly defaultConfig: SlackConfig = { enabled: false, workspaces: [] };
|
||||
|
||||
constructor() {
|
||||
this.ensureConfigFile();
|
||||
}
|
||||
|
||||
private async ensureConfigFile(): Promise<void> {
|
||||
try {
|
||||
await fs.access(this.configPath);
|
||||
} catch {
|
||||
await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
async getConfig(): Promise<SlackConfig> {
|
||||
try {
|
||||
const content = await fs.readFile(this.configPath, 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
return SlackConfig.parse(parsed);
|
||||
} catch {
|
||||
return this.defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
async setConfig(config: SlackConfig): Promise<void> {
|
||||
const validated = SlackConfig.parse(config);
|
||||
await fs.writeFile(this.configPath, JSON.stringify(validated, null, 2));
|
||||
}
|
||||
}
|
||||
13
apps/x/packages/core/src/slack/types.ts
Normal file
13
apps/x/packages/core/src/slack/types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import z from "zod";
|
||||
|
||||
export const SlackWorkspace = z.object({
|
||||
url: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
export type SlackWorkspace = z.infer<typeof SlackWorkspace>;
|
||||
|
||||
export const SlackConfig = z.object({
|
||||
enabled: z.boolean(),
|
||||
workspaces: z.array(SlackWorkspace).default([]),
|
||||
});
|
||||
export type SlackConfig = z.infer<typeof SlackConfig>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue