mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(slack): assistant thread ops, paginated history/replies, and permalink (#4934)
* feat(slack): add assistant thread ops, paginated history/replies, and permalink - New tools: slack_set_status, slack_set_title, slack_set_suggested_prompts (assistant.threads.*), slack_get_channel_history + slack_get_thread_replies (paginated conversations.history/replies via directExecution), slack_get_permalink - Wire all six as Slack block operations with subBlocks, conditions, params, outputs - Add scopes: assistant:write, im:history, mpim:read, mpim:history (+ consent labels) - Add manifest capabilities: action_read_history, action_assistant - Fix invite_to_conversation no_user error code; widen DM history scope hints - Regenerate tool docs and integrations catalog * fix(slack): correct thread parent on cursor-resume, complete DM scope hints, reject empty suggested prompts - get_thread_replies: identify thread parent by ts === thread_ts instead of assuming index 0, so cursor-resumed pages (which omit the parent) no longer mislabel a reply as the parent - get_channel_history + get_thread_replies: include im:history/mpim:history in the missing_scope hint to match the OAuth grant and DM/MPIM reads - set_suggested_prompts: throw a clear error when no valid prompt is provided instead of silently calling Slack with an empty prompts array * fix(slack): guard non-numeric limit/maxPages; drop ungranted im/mpim history scopes - Add resolvePositiveInt helper and use it for limit/maxPages in get_channel_history and get_thread_replies, so a non-numeric value (e.g. from an LLM) no longer becomes NaN and silently disables pagination (returning an empty success) - Remove im:history/mpim:read/mpim:history from the Slack OAuth grant — the Slack app does not have these scopes; assistant:write is the only added scope - Drop the explanatory inline comment in get_thread_replies * improvement(slack): move history pagination cursor/maxPages to advanced mode Matches the existing paginationCursor convention in the block — keeps the basic UI focused on channel + time filters while pagination internals live under advanced. * fix(slack): narrow set_status missing_scope hint to assistant:write A user hitting missing_scope on set_status already has chat:write (long granted); the scope they actually need is assistant:write, which is also what set_title/set_suggested_prompts point to. Narrow the hint to assistant:write for accurate, consistent guidance.
This commit is contained in:
@@ -507,6 +507,370 @@ Retrieve an entire thread including the parent message and all replies. Useful f
|
||||
| `replyCount` | number | Number of replies returned in this response |
|
||||
| `hasMore` | boolean | Whether there are more messages in the thread \(pagination needed\) |
|
||||
|
||||
### `slack_get_thread_replies`
|
||||
|
||||
Fetch every message in a Slack thread, automatically following pagination across all pages. Returns the parent message and the full set of replies.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Slack channel ID containing the thread \(e.g., C1234567890\) |
|
||||
| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the parent message \(e.g., 1405894322.002768\) |
|
||||
| `oldest` | string | No | Only include replies after this Unix timestamp \(seconds\) |
|
||||
| `latest` | string | No | Only include replies before this Unix timestamp \(seconds\) |
|
||||
| `inclusive` | boolean | No | Include messages with timestamps matching oldest or latest \(default: false\) |
|
||||
| `limit` | number | No | Messages to request per page \(default: 200, max: 999\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response.nextCursor to resume from |
|
||||
| `maxPages` | number | No | Maximum number of pages to fetch before stopping \(default: 10\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `parentMessage` | object | The thread parent message, or null if the thread is empty |
|
||||
| ↳ `type` | string | Message type \(usually "message"\) |
|
||||
| ↳ `ts` | string | Message timestamp \(unique identifier\) |
|
||||
| ↳ `text` | string | Message text content |
|
||||
| ↳ `user` | string | User ID who sent the message |
|
||||
| ↳ `bot_id` | string | Bot ID if sent by a bot |
|
||||
| ↳ `username` | string | Display username |
|
||||
| ↳ `channel` | string | Channel ID |
|
||||
| ↳ `team` | string | Team/workspace ID |
|
||||
| ↳ `thread_ts` | string | Parent message timestamp \(for threaded replies\) |
|
||||
| ↳ `parent_user_id` | string | User ID of thread parent message author |
|
||||
| ↳ `reply_count` | number | Total number of replies in thread |
|
||||
| ↳ `reply_users_count` | number | Number of unique users who replied |
|
||||
| ↳ `latest_reply` | string | Timestamp of most recent reply |
|
||||
| ↳ `subscribed` | boolean | Whether user is subscribed to thread |
|
||||
| ↳ `last_read` | string | Timestamp of last read message |
|
||||
| ↳ `unread_count` | number | Number of unread messages in thread |
|
||||
| ↳ `subtype` | string | Message subtype \(bot_message, file_share, etc.\) |
|
||||
| ↳ `is_starred` | boolean | Whether message is starred by user |
|
||||
| ↳ `pinned_to` | array | Channel IDs where message is pinned |
|
||||
| ↳ `permalink` | string | Permanent URL to the message |
|
||||
| ↳ `reactions` | array | Reactions on this message |
|
||||
| ↳ `name` | string | Emoji name \(without colons\) |
|
||||
| ↳ `count` | number | Number of times this reaction was added |
|
||||
| ↳ `users` | array | Array of user IDs who reacted |
|
||||
| ↳ `files` | array | Files attached to the message |
|
||||
| ↳ `id` | string | Unique file identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimetype` | string | MIME type of the file |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL \(requires auth\) |
|
||||
| ↳ `permalink` | string | Permanent link to the file |
|
||||
| ↳ `mode` | string | File mode \(hosted, external, etc.\) |
|
||||
| ↳ `attachments` | array | Legacy attachments on the message |
|
||||
| ↳ `id` | number | Attachment ID |
|
||||
| ↳ `fallback` | string | Plain text summary |
|
||||
| ↳ `text` | string | Main attachment text |
|
||||
| ↳ `pretext` | string | Text shown before attachment |
|
||||
| ↳ `color` | string | Color bar hex code or preset |
|
||||
| ↳ `author_name` | string | Author display name |
|
||||
| ↳ `author_link` | string | Author link URL |
|
||||
| ↳ `author_icon` | string | Author icon URL |
|
||||
| ↳ `title` | string | Attachment title |
|
||||
| ↳ `title_link` | string | Title link URL |
|
||||
| ↳ `image_url` | string | Image URL |
|
||||
| ↳ `thumb_url` | string | Thumbnail URL |
|
||||
| ↳ `footer` | string | Footer text |
|
||||
| ↳ `footer_icon` | string | Footer icon URL |
|
||||
| ↳ `ts` | string | Timestamp shown in footer |
|
||||
| ↳ `blocks` | array | Block Kit blocks in the message |
|
||||
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
|
||||
| ↳ `block_id` | string | Unique block identifier |
|
||||
| ↳ `edited` | object | Edit information if message was edited |
|
||||
| ↳ `user` | string | User ID who edited the message |
|
||||
| ↳ `ts` | string | Timestamp of the edit |
|
||||
| `replies` | array | All reply messages in the thread \(excluding the parent\) |
|
||||
| ↳ `type` | string | Message type \(usually "message"\) |
|
||||
| ↳ `ts` | string | Message timestamp \(unique identifier\) |
|
||||
| ↳ `text` | string | Message text content |
|
||||
| ↳ `user` | string | User ID who sent the message |
|
||||
| ↳ `bot_id` | string | Bot ID if sent by a bot |
|
||||
| ↳ `username` | string | Display username |
|
||||
| ↳ `channel` | string | Channel ID |
|
||||
| ↳ `team` | string | Team/workspace ID |
|
||||
| ↳ `thread_ts` | string | Parent message timestamp \(for threaded replies\) |
|
||||
| ↳ `parent_user_id` | string | User ID of thread parent message author |
|
||||
| ↳ `reply_count` | number | Total number of replies in thread |
|
||||
| ↳ `reply_users_count` | number | Number of unique users who replied |
|
||||
| ↳ `latest_reply` | string | Timestamp of most recent reply |
|
||||
| ↳ `subscribed` | boolean | Whether user is subscribed to thread |
|
||||
| ↳ `last_read` | string | Timestamp of last read message |
|
||||
| ↳ `unread_count` | number | Number of unread messages in thread |
|
||||
| ↳ `subtype` | string | Message subtype \(bot_message, file_share, etc.\) |
|
||||
| ↳ `is_starred` | boolean | Whether message is starred by user |
|
||||
| ↳ `pinned_to` | array | Channel IDs where message is pinned |
|
||||
| ↳ `permalink` | string | Permanent URL to the message |
|
||||
| ↳ `reactions` | array | Reactions on this message |
|
||||
| ↳ `name` | string | Emoji name \(without colons\) |
|
||||
| ↳ `count` | number | Number of times this reaction was added |
|
||||
| ↳ `users` | array | Array of user IDs who reacted |
|
||||
| ↳ `files` | array | Files attached to the message |
|
||||
| ↳ `id` | string | Unique file identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimetype` | string | MIME type of the file |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL \(requires auth\) |
|
||||
| ↳ `permalink` | string | Permanent link to the file |
|
||||
| ↳ `mode` | string | File mode \(hosted, external, etc.\) |
|
||||
| ↳ `attachments` | array | Legacy attachments on the message |
|
||||
| ↳ `id` | number | Attachment ID |
|
||||
| ↳ `fallback` | string | Plain text summary |
|
||||
| ↳ `text` | string | Main attachment text |
|
||||
| ↳ `pretext` | string | Text shown before attachment |
|
||||
| ↳ `color` | string | Color bar hex code or preset |
|
||||
| ↳ `author_name` | string | Author display name |
|
||||
| ↳ `author_link` | string | Author link URL |
|
||||
| ↳ `author_icon` | string | Author icon URL |
|
||||
| ↳ `title` | string | Attachment title |
|
||||
| ↳ `title_link` | string | Title link URL |
|
||||
| ↳ `image_url` | string | Image URL |
|
||||
| ↳ `thumb_url` | string | Thumbnail URL |
|
||||
| ↳ `footer` | string | Footer text |
|
||||
| ↳ `footer_icon` | string | Footer icon URL |
|
||||
| ↳ `ts` | string | Timestamp shown in footer |
|
||||
| ↳ `blocks` | array | Block Kit blocks in the message |
|
||||
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
|
||||
| ↳ `block_id` | string | Unique block identifier |
|
||||
| ↳ `edited` | object | Edit information if message was edited |
|
||||
| ↳ `user` | string | User ID who edited the message |
|
||||
| ↳ `ts` | string | Timestamp of the edit |
|
||||
| `messages` | array | All messages \(parent + replies\) in chronological order |
|
||||
| ↳ `type` | string | Message type \(usually "message"\) |
|
||||
| ↳ `ts` | string | Message timestamp \(unique identifier\) |
|
||||
| ↳ `text` | string | Message text content |
|
||||
| ↳ `user` | string | User ID who sent the message |
|
||||
| ↳ `bot_id` | string | Bot ID if sent by a bot |
|
||||
| ↳ `username` | string | Display username |
|
||||
| ↳ `channel` | string | Channel ID |
|
||||
| ↳ `team` | string | Team/workspace ID |
|
||||
| ↳ `thread_ts` | string | Parent message timestamp \(for threaded replies\) |
|
||||
| ↳ `parent_user_id` | string | User ID of thread parent message author |
|
||||
| ↳ `reply_count` | number | Total number of replies in thread |
|
||||
| ↳ `reply_users_count` | number | Number of unique users who replied |
|
||||
| ↳ `latest_reply` | string | Timestamp of most recent reply |
|
||||
| ↳ `subscribed` | boolean | Whether user is subscribed to thread |
|
||||
| ↳ `last_read` | string | Timestamp of last read message |
|
||||
| ↳ `unread_count` | number | Number of unread messages in thread |
|
||||
| ↳ `subtype` | string | Message subtype \(bot_message, file_share, etc.\) |
|
||||
| ↳ `is_starred` | boolean | Whether message is starred by user |
|
||||
| ↳ `pinned_to` | array | Channel IDs where message is pinned |
|
||||
| ↳ `permalink` | string | Permanent URL to the message |
|
||||
| ↳ `reactions` | array | Reactions on this message |
|
||||
| ↳ `name` | string | Emoji name \(without colons\) |
|
||||
| ↳ `count` | number | Number of times this reaction was added |
|
||||
| ↳ `users` | array | Array of user IDs who reacted |
|
||||
| ↳ `files` | array | Files attached to the message |
|
||||
| ↳ `id` | string | Unique file identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimetype` | string | MIME type of the file |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL \(requires auth\) |
|
||||
| ↳ `permalink` | string | Permanent link to the file |
|
||||
| ↳ `mode` | string | File mode \(hosted, external, etc.\) |
|
||||
| ↳ `attachments` | array | Legacy attachments on the message |
|
||||
| ↳ `id` | number | Attachment ID |
|
||||
| ↳ `fallback` | string | Plain text summary |
|
||||
| ↳ `text` | string | Main attachment text |
|
||||
| ↳ `pretext` | string | Text shown before attachment |
|
||||
| ↳ `color` | string | Color bar hex code or preset |
|
||||
| ↳ `author_name` | string | Author display name |
|
||||
| ↳ `author_link` | string | Author link URL |
|
||||
| ↳ `author_icon` | string | Author icon URL |
|
||||
| ↳ `title` | string | Attachment title |
|
||||
| ↳ `title_link` | string | Title link URL |
|
||||
| ↳ `image_url` | string | Image URL |
|
||||
| ↳ `thumb_url` | string | Thumbnail URL |
|
||||
| ↳ `footer` | string | Footer text |
|
||||
| ↳ `footer_icon` | string | Footer icon URL |
|
||||
| ↳ `ts` | string | Timestamp shown in footer |
|
||||
| ↳ `blocks` | array | Block Kit blocks in the message |
|
||||
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
|
||||
| ↳ `block_id` | string | Unique block identifier |
|
||||
| ↳ `edited` | object | Edit information if message was edited |
|
||||
| ↳ `user` | string | User ID who edited the message |
|
||||
| ↳ `ts` | string | Timestamp of the edit |
|
||||
| `replyCount` | number | Number of replies returned \(excluding the parent\) |
|
||||
| `hasMore` | boolean | Whether more pages remain beyond the fetched window |
|
||||
| `nextCursor` | string | Cursor to fetch the next page; null when there are no more pages |
|
||||
| `pages` | number | Number of pages fetched in this invocation |
|
||||
|
||||
### `slack_get_channel_history`
|
||||
|
||||
Fetch message history from a Slack channel, automatically following pagination. Optionally filter by a time range to scrape messages since a given timestamp.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) |
|
||||
| `oldest` | string | No | Only include messages after this Unix timestamp \(seconds, e.g., 1700000000\) |
|
||||
| `latest` | string | No | Only include messages before this Unix timestamp \(seconds\) |
|
||||
| `inclusive` | boolean | No | Include messages with timestamps matching oldest or latest \(default: false\) |
|
||||
| `limit` | number | No | Messages to request per page \(default: 200, max: 999\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response.nextCursor to resume from |
|
||||
| `maxPages` | number | No | Maximum number of pages to fetch before stopping \(default: 10\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messages` | array | Channel messages in reverse-chronological order \(newest first\) |
|
||||
| ↳ `type` | string | Message type \(usually "message"\) |
|
||||
| ↳ `ts` | string | Message timestamp \(unique identifier\) |
|
||||
| ↳ `text` | string | Message text content |
|
||||
| ↳ `user` | string | User ID who sent the message |
|
||||
| ↳ `bot_id` | string | Bot ID if sent by a bot |
|
||||
| ↳ `username` | string | Display username |
|
||||
| ↳ `channel` | string | Channel ID |
|
||||
| ↳ `team` | string | Team/workspace ID |
|
||||
| ↳ `thread_ts` | string | Parent message timestamp \(for threaded replies\) |
|
||||
| ↳ `parent_user_id` | string | User ID of thread parent message author |
|
||||
| ↳ `reply_count` | number | Total number of replies in thread |
|
||||
| ↳ `reply_users_count` | number | Number of unique users who replied |
|
||||
| ↳ `latest_reply` | string | Timestamp of most recent reply |
|
||||
| ↳ `subscribed` | boolean | Whether user is subscribed to thread |
|
||||
| ↳ `last_read` | string | Timestamp of last read message |
|
||||
| ↳ `unread_count` | number | Number of unread messages in thread |
|
||||
| ↳ `subtype` | string | Message subtype \(bot_message, file_share, etc.\) |
|
||||
| ↳ `is_starred` | boolean | Whether message is starred by user |
|
||||
| ↳ `pinned_to` | array | Channel IDs where message is pinned |
|
||||
| ↳ `permalink` | string | Permanent URL to the message |
|
||||
| ↳ `reactions` | array | Reactions on this message |
|
||||
| ↳ `name` | string | Emoji name \(without colons\) |
|
||||
| ↳ `count` | number | Number of times this reaction was added |
|
||||
| ↳ `users` | array | Array of user IDs who reacted |
|
||||
| ↳ `files` | array | Files attached to the message |
|
||||
| ↳ `id` | string | Unique file identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimetype` | string | MIME type of the file |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL \(requires auth\) |
|
||||
| ↳ `permalink` | string | Permanent link to the file |
|
||||
| ↳ `mode` | string | File mode \(hosted, external, etc.\) |
|
||||
| ↳ `attachments` | array | Legacy attachments on the message |
|
||||
| ↳ `id` | number | Attachment ID |
|
||||
| ↳ `fallback` | string | Plain text summary |
|
||||
| ↳ `text` | string | Main attachment text |
|
||||
| ↳ `pretext` | string | Text shown before attachment |
|
||||
| ↳ `color` | string | Color bar hex code or preset |
|
||||
| ↳ `author_name` | string | Author display name |
|
||||
| ↳ `author_link` | string | Author link URL |
|
||||
| ↳ `author_icon` | string | Author icon URL |
|
||||
| ↳ `title` | string | Attachment title |
|
||||
| ↳ `title_link` | string | Title link URL |
|
||||
| ↳ `image_url` | string | Image URL |
|
||||
| ↳ `thumb_url` | string | Thumbnail URL |
|
||||
| ↳ `footer` | string | Footer text |
|
||||
| ↳ `footer_icon` | string | Footer icon URL |
|
||||
| ↳ `ts` | string | Timestamp shown in footer |
|
||||
| ↳ `blocks` | array | Block Kit blocks in the message |
|
||||
| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) |
|
||||
| ↳ `block_id` | string | Unique block identifier |
|
||||
| ↳ `edited` | object | Edit information if message was edited |
|
||||
| ↳ `user` | string | User ID who edited the message |
|
||||
| ↳ `ts` | string | Timestamp of the edit |
|
||||
| `count` | number | Total number of messages returned across all fetched pages |
|
||||
| `hasMore` | boolean | Whether more pages remain beyond the fetched window |
|
||||
| `nextCursor` | string | Cursor to fetch the next page; null when there are no more pages |
|
||||
| `pages` | number | Number of pages fetched in this invocation |
|
||||
|
||||
### `slack_get_permalink`
|
||||
|
||||
Get a stable permalink URL to a specific Slack message.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Channel ID containing the message \(e.g., C1234567890\) |
|
||||
| `messageTs` | string | Yes | The message's ts value \(e.g., 1405894322.002768\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ok` | boolean | Whether the permalink was retrieved successfully |
|
||||
| `channel` | string | Channel ID containing the message |
|
||||
| `permalink` | string | The permalink URL to the message |
|
||||
|
||||
### `slack_set_status`
|
||||
|
||||
Set or clear the assistant thread status indicator (the loading shimmer) on a Slack AI app thread. Pass an empty status to clear it.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) |
|
||||
| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) |
|
||||
| `status` | string | Yes | Status text to display, e.g. 'Working on it…'. Pass an empty string to clear. |
|
||||
| `loadingMessages` | json | No | Optional list of messages to rotate through as an animated loading indicator \(max 10\). |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ok` | boolean | Whether the status was set successfully |
|
||||
| `channel` | string | Channel ID the status was set on |
|
||||
| `threadTs` | string | Thread timestamp the status was set on |
|
||||
|
||||
### `slack_set_title`
|
||||
|
||||
Set the title of a Slack assistant thread (shown in the AI app thread header).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) |
|
||||
| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) |
|
||||
| `title` | string | Yes | The title to display for the assistant thread |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ok` | boolean | Whether the title was set successfully |
|
||||
| `channel` | string | Channel ID the title was set on |
|
||||
| `threadTs` | string | Thread timestamp the title was set on |
|
||||
|
||||
### `slack_set_suggested_prompts`
|
||||
|
||||
Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) |
|
||||
| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) |
|
||||
| `prompts` | json | Yes | Array of prompts, each with a "title" \(shown on the chip\) and a "message" \(sent when clicked\). Max 4. |
|
||||
| `promptsTitle` | string | No | Optional heading for the prompt list, e.g. 'Suggested Prompts' |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ok` | boolean | Whether the suggested prompts were set successfully |
|
||||
| `channel` | string | Channel ID the prompts were set on |
|
||||
| `threadTs` | string | Thread timestamp the prompts were set on |
|
||||
|
||||
### `slack_list_channels`
|
||||
|
||||
List all channels in a Slack workspace. Returns public and private channels the bot has access to.
|
||||
|
||||
@@ -33,6 +33,12 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
{ label: 'Read Messages', id: 'read' },
|
||||
{ label: 'Get Message', id: 'get_message' },
|
||||
{ label: 'Get Thread', id: 'get_thread' },
|
||||
{ label: 'Get Thread Replies', id: 'get_thread_replies' },
|
||||
{ label: 'Get Channel History', id: 'get_channel_history' },
|
||||
{ label: 'Get Message Permalink', id: 'get_permalink' },
|
||||
{ label: 'Set Assistant Status', id: 'set_status' },
|
||||
{ label: 'Set Assistant Title', id: 'set_title' },
|
||||
{ label: 'Set Suggested Prompts', id: 'set_suggested_prompts' },
|
||||
{ label: 'List Channels', id: 'list_channels' },
|
||||
{ label: 'List Channel Members', id: 'list_members' },
|
||||
{ label: 'List Users', id: 'list_users' },
|
||||
@@ -548,7 +554,7 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
placeholder: 'Message timestamp (e.g., 1405894322.002768)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'get_message',
|
||||
value: ['get_message', 'get_permalink'],
|
||||
},
|
||||
required: true,
|
||||
wandConfig: {
|
||||
@@ -574,7 +580,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Thread timestamp (thread_ts, e.g., 1405894322.002768)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'get_thread',
|
||||
value: [
|
||||
'get_thread',
|
||||
'get_thread_replies',
|
||||
'set_status',
|
||||
'set_title',
|
||||
'set_suggested_prompts',
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
wandConfig: {
|
||||
@@ -602,6 +614,152 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
value: 'get_thread',
|
||||
},
|
||||
},
|
||||
// Set Assistant Status specific fields
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status Text',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., Working on it… (leave empty to clear)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'set_status',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'loadingMessages',
|
||||
title: 'Loading Messages',
|
||||
type: 'long-input',
|
||||
placeholder: 'Optional JSON array of phrases to animate (max 10)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'set_status',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
// Set Assistant Title specific fields
|
||||
{
|
||||
id: 'assistantTitle',
|
||||
title: 'Thread Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Title to display for the assistant thread',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'set_title',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
// Set Suggested Prompts specific fields
|
||||
{
|
||||
id: 'suggestedPrompts',
|
||||
title: 'Suggested Prompts',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"title": "Summarize", "message": "Summarize this thread"}]',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'set_suggested_prompts',
|
||||
},
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON array of Slack assistant suggested prompts from the user's description.
|
||||
Each entry must be an object with exactly two string fields:
|
||||
- "title": the short label shown on the clickable chip
|
||||
- "message": the full message sent into the thread when the chip is clicked
|
||||
Return at most 4 prompts.
|
||||
Example:
|
||||
[{"title": "Summarize", "message": "Summarize the key points of this thread"}, {"title": "Next steps", "message": "What are the next steps?"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no quotes around the array, no extra text.`,
|
||||
placeholder: 'Describe the prompts you want (e.g., "summarize and list action items")...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'promptsTitle',
|
||||
title: 'Prompts Heading',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., Suggested Prompts (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'set_suggested_prompts',
|
||||
},
|
||||
mode: 'advanced',
|
||||
required: false,
|
||||
},
|
||||
// Get Channel History / Get Thread Replies shared pagination fields
|
||||
{
|
||||
id: 'historyOldest',
|
||||
title: 'Oldest Timestamp',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix seconds, e.g., 1700000000 (only messages after)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_channel_history', 'get_thread_replies'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'historyLatest',
|
||||
title: 'Latest Timestamp',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix seconds, e.g., 1700000000 (only messages before)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_channel_history', 'get_thread_replies'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'historyLimit',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '200 (max 999)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_channel_history', 'get_thread_replies'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'historyMaxPages',
|
||||
title: 'Max Pages',
|
||||
type: 'short-input',
|
||||
placeholder: '10',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_channel_history', 'get_thread_replies'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'historyCursor',
|
||||
title: 'Start Cursor',
|
||||
type: 'short-input',
|
||||
placeholder: 'Resume from a previous nextCursor',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_channel_history', 'get_thread_replies'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'historyInclusive',
|
||||
title: 'Inclusive',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_channel_history', 'get_thread_replies'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'oldest',
|
||||
title: 'Oldest Timestamp',
|
||||
@@ -1196,6 +1354,12 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
'slack_message_reader',
|
||||
'slack_get_message',
|
||||
'slack_get_thread',
|
||||
'slack_get_thread_replies',
|
||||
'slack_get_channel_history',
|
||||
'slack_get_permalink',
|
||||
'slack_set_status',
|
||||
'slack_set_title',
|
||||
'slack_set_suggested_prompts',
|
||||
'slack_list_channels',
|
||||
'slack_list_members',
|
||||
'slack_list_users',
|
||||
@@ -1235,6 +1399,18 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
return 'slack_get_message'
|
||||
case 'get_thread':
|
||||
return 'slack_get_thread'
|
||||
case 'get_thread_replies':
|
||||
return 'slack_get_thread_replies'
|
||||
case 'get_channel_history':
|
||||
return 'slack_get_channel_history'
|
||||
case 'get_permalink':
|
||||
return 'slack_get_permalink'
|
||||
case 'set_status':
|
||||
return 'slack_set_status'
|
||||
case 'set_title':
|
||||
return 'slack_set_title'
|
||||
case 'set_suggested_prompts':
|
||||
return 'slack_set_suggested_prompts'
|
||||
case 'list_channels':
|
||||
return 'slack_list_channels'
|
||||
case 'list_members':
|
||||
@@ -1318,6 +1494,17 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
getMessageTimestamp,
|
||||
getThreadTimestamp,
|
||||
threadLimit,
|
||||
status,
|
||||
loadingMessages,
|
||||
assistantTitle,
|
||||
suggestedPrompts,
|
||||
promptsTitle,
|
||||
historyOldest,
|
||||
historyLatest,
|
||||
historyLimit,
|
||||
historyMaxPages,
|
||||
historyCursor,
|
||||
historyInclusive,
|
||||
includeNumMembers,
|
||||
presenceUserId,
|
||||
editCanvasId,
|
||||
@@ -1438,6 +1625,65 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
break
|
||||
}
|
||||
|
||||
case 'set_status': {
|
||||
baseParams.threadTs = getThreadTimestamp
|
||||
baseParams.status = status ?? ''
|
||||
if (loadingMessages) {
|
||||
baseParams.loadingMessages = loadingMessages
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'set_title': {
|
||||
baseParams.threadTs = getThreadTimestamp
|
||||
baseParams.title = assistantTitle
|
||||
break
|
||||
}
|
||||
|
||||
case 'set_suggested_prompts': {
|
||||
baseParams.threadTs = getThreadTimestamp
|
||||
baseParams.prompts = suggestedPrompts
|
||||
if (promptsTitle) {
|
||||
baseParams.promptsTitle = promptsTitle
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'get_permalink': {
|
||||
baseParams.messageTs = getMessageTimestamp
|
||||
break
|
||||
}
|
||||
|
||||
case 'get_channel_history':
|
||||
case 'get_thread_replies': {
|
||||
if (operation === 'get_thread_replies') {
|
||||
baseParams.threadTs = getThreadTimestamp
|
||||
}
|
||||
if (historyOldest) {
|
||||
baseParams.oldest = String(historyOldest).trim()
|
||||
}
|
||||
if (historyLatest) {
|
||||
baseParams.latest = String(historyLatest).trim()
|
||||
}
|
||||
if (historyLimit) {
|
||||
const parsedLimit = Number.parseInt(historyLimit, 10)
|
||||
if (!Number.isNaN(parsedLimit) && parsedLimit > 0) {
|
||||
baseParams.limit = parsedLimit
|
||||
}
|
||||
}
|
||||
if (historyMaxPages) {
|
||||
const parsedMaxPages = Number.parseInt(historyMaxPages, 10)
|
||||
if (!Number.isNaN(parsedMaxPages) && parsedMaxPages > 0) {
|
||||
baseParams.maxPages = parsedMaxPages
|
||||
}
|
||||
}
|
||||
if (historyCursor) {
|
||||
baseParams.cursor = String(historyCursor).trim()
|
||||
}
|
||||
baseParams.inclusive = historyInclusive === 'true'
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_channels': {
|
||||
baseParams.includePrivate = includePrivate !== 'false'
|
||||
baseParams.excludeArchived = true
|
||||
@@ -1682,6 +1928,36 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
type: 'string',
|
||||
description: 'Maximum number of messages to return from thread',
|
||||
},
|
||||
// Set Assistant Status inputs
|
||||
status: { type: 'string', description: 'Status text to display (empty clears the status)' },
|
||||
loadingMessages: {
|
||||
type: 'json',
|
||||
description: 'Optional array of phrases to animate as a loading indicator (max 10)',
|
||||
},
|
||||
// Set Assistant Title inputs
|
||||
assistantTitle: { type: 'string', description: 'Title to display for the assistant thread' },
|
||||
// Set Suggested Prompts inputs
|
||||
suggestedPrompts: {
|
||||
type: 'json',
|
||||
description: 'Array of { title, message } prompt objects (max 4)',
|
||||
},
|
||||
promptsTitle: { type: 'string', description: 'Optional heading for the prompt list' },
|
||||
// Get Channel History / Get Thread Replies inputs
|
||||
historyOldest: {
|
||||
type: 'string',
|
||||
description: 'Only include messages after this Unix timestamp',
|
||||
},
|
||||
historyLatest: {
|
||||
type: 'string',
|
||||
description: 'Only include messages before this Unix timestamp',
|
||||
},
|
||||
historyLimit: { type: 'string', description: 'Messages to request per page (max 999)' },
|
||||
historyMaxPages: { type: 'string', description: 'Maximum number of pages to fetch' },
|
||||
historyCursor: { type: 'string', description: 'Pagination cursor to resume from' },
|
||||
historyInclusive: {
|
||||
type: 'string',
|
||||
description: 'Include messages matching oldest/latest (true/false)',
|
||||
},
|
||||
// Get Channel Info inputs
|
||||
includeNumMembers: { type: 'string', description: 'Include member count (true/false)' },
|
||||
// Get User Presence inputs
|
||||
@@ -1808,6 +2084,22 @@ Do not include any explanations, markdown formatting, or other text outside the
|
||||
description: 'Whether there are more messages in the thread',
|
||||
},
|
||||
|
||||
// slack_get_channel_history / slack_get_thread_replies pagination outputs
|
||||
pages: {
|
||||
type: 'number',
|
||||
description: 'Number of pages fetched during a paginated history/replies read',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp an assistant status/title/prompts op was set on',
|
||||
},
|
||||
|
||||
// slack_get_permalink outputs (get_permalink operation)
|
||||
permalink: {
|
||||
type: 'string',
|
||||
description: 'Permalink URL to the message',
|
||||
},
|
||||
|
||||
// slack_list_channels outputs (list_channels operation)
|
||||
channels: {
|
||||
type: 'json',
|
||||
|
||||
@@ -13571,6 +13571,30 @@
|
||||
"name": "Get Thread",
|
||||
"description": "Retrieve an entire thread including the parent message and all replies. Useful for getting full conversation context."
|
||||
},
|
||||
{
|
||||
"name": "Get Thread Replies",
|
||||
"description": "Fetch every message in a Slack thread, automatically following pagination across all pages. Returns the parent message and the full set of replies."
|
||||
},
|
||||
{
|
||||
"name": "Get Channel History",
|
||||
"description": "Fetch message history from a Slack channel, automatically following pagination. Optionally filter by a time range to scrape messages since a given timestamp."
|
||||
},
|
||||
{
|
||||
"name": "Get Message Permalink",
|
||||
"description": "Get a stable permalink URL to a specific Slack message."
|
||||
},
|
||||
{
|
||||
"name": "Set Assistant Status",
|
||||
"description": "Set or clear the assistant thread status indicator (the loading shimmer) on a Slack AI app thread. Pass an empty status to clear it."
|
||||
},
|
||||
{
|
||||
"name": "Set Assistant Title",
|
||||
"description": "Set the title of a Slack assistant thread (shown in the AI app thread header)."
|
||||
},
|
||||
{
|
||||
"name": "Set Suggested Prompts",
|
||||
"description": "Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app)."
|
||||
},
|
||||
{
|
||||
"name": "List Channels",
|
||||
"description": "List all channels in a Slack workspace. Returns public and private channels the bot has access to."
|
||||
@@ -13664,7 +13688,7 @@
|
||||
"description": "Publish a static view to a user"
|
||||
}
|
||||
],
|
||||
"operationCount": 29,
|
||||
"operationCount": 35,
|
||||
"triggers": [
|
||||
{
|
||||
"id": "slack_webhook",
|
||||
|
||||
@@ -702,6 +702,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'groups:write',
|
||||
'chat:write',
|
||||
'chat:write.public',
|
||||
'assistant:write',
|
||||
'im:write',
|
||||
'im:read',
|
||||
'users:read',
|
||||
|
||||
@@ -275,6 +275,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'groups:history': 'Read private messages',
|
||||
'chat:write': 'Send messages',
|
||||
'chat:write.public': 'Post to public channels',
|
||||
'assistant:write': 'Set assistant thread status, title, and suggested prompts',
|
||||
'im:write': 'Send direct messages',
|
||||
'im:history': 'Read direct message history',
|
||||
'im:read': 'View direct message channels',
|
||||
|
||||
@@ -2843,8 +2843,11 @@ import {
|
||||
slackEditCanvasTool,
|
||||
slackEphemeralMessageTool,
|
||||
slackGetCanvasTool,
|
||||
slackGetChannelHistoryTool,
|
||||
slackGetChannelInfoTool,
|
||||
slackGetMessageTool,
|
||||
slackGetPermalinkTool,
|
||||
slackGetThreadRepliesTool,
|
||||
slackGetThreadTool,
|
||||
slackGetUserPresenceTool,
|
||||
slackGetUserTool,
|
||||
@@ -2860,6 +2863,9 @@ import {
|
||||
slackPublishViewTool,
|
||||
slackPushViewTool,
|
||||
slackRemoveReactionTool,
|
||||
slackSetStatusTool,
|
||||
slackSetSuggestedPromptsTool,
|
||||
slackSetTitleTool,
|
||||
slackUpdateMessageTool,
|
||||
slackUpdateViewTool,
|
||||
} from '@/tools/slack'
|
||||
@@ -3944,6 +3950,12 @@ export const tools: Record<string, ToolConfig> = {
|
||||
slack_get_user: slackGetUserTool,
|
||||
slack_get_message: slackGetMessageTool,
|
||||
slack_get_thread: slackGetThreadTool,
|
||||
slack_get_thread_replies: slackGetThreadRepliesTool,
|
||||
slack_get_channel_history: slackGetChannelHistoryTool,
|
||||
slack_get_permalink: slackGetPermalinkTool,
|
||||
slack_set_status: slackSetStatusTool,
|
||||
slack_set_title: slackSetTitleTool,
|
||||
slack_set_suggested_prompts: slackSetSuggestedPromptsTool,
|
||||
slack_canvas: slackCanvasTool,
|
||||
slack_download: slackDownloadTool,
|
||||
slack_ephemeral_message: slackEphemeralMessageTool,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import type {
|
||||
SlackGetChannelHistoryParams,
|
||||
SlackGetChannelHistoryResponse,
|
||||
} from '@/tools/slack/types'
|
||||
import { MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
|
||||
import { fetchSlackMessagesPaginated, resolvePositiveInt } from '@/tools/slack/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/** Default cap on pages fetched per invocation. */
|
||||
const DEFAULT_MAX_PAGES = 10
|
||||
|
||||
export const slackGetChannelHistoryTool: ToolConfig<
|
||||
SlackGetChannelHistoryParams,
|
||||
SlackGetChannelHistoryResponse
|
||||
> = {
|
||||
id: 'slack_get_channel_history',
|
||||
name: 'Slack Get Channel History',
|
||||
description:
|
||||
'Fetch message history from a Slack channel, automatically following pagination. Optionally filter by a time range to scrape messages since a given timestamp.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Slack channel ID (e.g., C1234567890)',
|
||||
},
|
||||
oldest: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only include messages after this Unix timestamp (seconds, e.g., 1700000000)',
|
||||
},
|
||||
latest: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only include messages before this Unix timestamp (seconds)',
|
||||
},
|
||||
inclusive: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Include messages with timestamps matching oldest or latest (default: false)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Messages to request per page (default: 200, max: 999)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from a previous response.nextCursor to resume from',
|
||||
},
|
||||
maxPages: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of pages to fetch before stopping (default: 10)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/conversations.history',
|
||||
method: 'GET',
|
||||
headers: (params: SlackGetChannelHistoryParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
directExecution: async (params: SlackGetChannelHistoryParams) => {
|
||||
const token = params.accessToken || params.botToken
|
||||
if (!token) {
|
||||
throw new Error('Missing Slack credentials. Provide an OAuth connection or a bot token.')
|
||||
}
|
||||
|
||||
const result = await fetchSlackMessagesPaginated({
|
||||
token,
|
||||
method: 'conversations.history',
|
||||
baseParams: {
|
||||
channel: params.channel,
|
||||
oldest: params.oldest,
|
||||
latest: params.latest,
|
||||
inclusive: params.inclusive ? 'true' : undefined,
|
||||
},
|
||||
limit: resolvePositiveInt(params.limit, 200),
|
||||
cursor: params.cursor,
|
||||
maxPages: resolvePositiveInt(params.maxPages, DEFAULT_MAX_PAGES),
|
||||
missingScopeHint: 'channels:history, groups:history, im:history, mpim:history',
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
messages: result.messages,
|
||||
count: result.messages.length,
|
||||
hasMore: result.hasMore,
|
||||
nextCursor: result.nextCursor,
|
||||
pages: result.pages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'Channel messages in reverse-chronological order (newest first)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: MESSAGE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Total number of messages returned across all fetched pages',
|
||||
},
|
||||
hasMore: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more pages remain beyond the fetched window',
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor to fetch the next page; null when there are no more pages',
|
||||
optional: true,
|
||||
},
|
||||
pages: {
|
||||
type: 'number',
|
||||
description: 'Number of pages fetched in this invocation',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export const slackGetMessageTool: ToolConfig<SlackGetMessageParams, SlackGetMess
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history).'
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history, im:history, mpim:history).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { SlackGetPermalinkParams, SlackGetPermalinkResponse } from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const slackGetPermalinkTool: ToolConfig<SlackGetPermalinkParams, SlackGetPermalinkResponse> =
|
||||
{
|
||||
id: 'slack_get_permalink',
|
||||
name: 'Slack Get Permalink',
|
||||
description: 'Get a stable permalink URL to a specific Slack message.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the message (e.g., C1234567890)',
|
||||
},
|
||||
messageTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: "The message's ts value (e.g., 1405894322.002768)",
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SlackGetPermalinkParams) => {
|
||||
const url = new URL('https://slack.com/api/chat.getPermalink')
|
||||
url.searchParams.append('channel', params.channel?.trim() ?? '')
|
||||
url.searchParams.append('message_ts', params.messageTs?.trim() ?? '')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: SlackGetPermalinkParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'message_not_found') {
|
||||
throw new Error('Message not found. Please check the channel ID and message timestamp.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to get Slack permalink')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: data.channel ?? '',
|
||||
permalink: data.permalink ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the permalink was retrieved successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID containing the message',
|
||||
},
|
||||
permalink: {
|
||||
type: 'string',
|
||||
description: 'The permalink URL to the message',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export const slackGetThreadTool: ToolConfig<SlackGetThreadParams, SlackGetThread
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history).'
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history, im:history, mpim:history).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import type {
|
||||
SlackGetThreadRepliesParams,
|
||||
SlackGetThreadRepliesResponse,
|
||||
} from '@/tools/slack/types'
|
||||
import { MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
|
||||
import { fetchSlackMessagesPaginated, resolvePositiveInt } from '@/tools/slack/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/** Default cap on pages fetched per invocation. */
|
||||
const DEFAULT_MAX_PAGES = 10
|
||||
|
||||
export const slackGetThreadRepliesTool: ToolConfig<
|
||||
SlackGetThreadRepliesParams,
|
||||
SlackGetThreadRepliesResponse
|
||||
> = {
|
||||
id: 'slack_get_thread_replies',
|
||||
name: 'Slack Get Thread Replies',
|
||||
description:
|
||||
'Fetch every message in a Slack thread, automatically following pagination across all pages. Returns the parent message and the full set of replies.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Slack channel ID containing the thread (e.g., C1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the parent message (e.g., 1405894322.002768)',
|
||||
},
|
||||
oldest: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only include replies after this Unix timestamp (seconds)',
|
||||
},
|
||||
latest: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only include replies before this Unix timestamp (seconds)',
|
||||
},
|
||||
inclusive: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Include messages with timestamps matching oldest or latest (default: false)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Messages to request per page (default: 200, max: 999)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from a previous response.nextCursor to resume from',
|
||||
},
|
||||
maxPages: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of pages to fetch before stopping (default: 10)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/conversations.replies',
|
||||
method: 'GET',
|
||||
headers: (params: SlackGetThreadRepliesParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
directExecution: async (params: SlackGetThreadRepliesParams) => {
|
||||
const token = params.accessToken || params.botToken
|
||||
if (!token) {
|
||||
throw new Error('Missing Slack credentials. Provide an OAuth connection or a bot token.')
|
||||
}
|
||||
|
||||
const result = await fetchSlackMessagesPaginated({
|
||||
token,
|
||||
method: 'conversations.replies',
|
||||
baseParams: {
|
||||
channel: params.channel,
|
||||
ts: params.threadTs,
|
||||
oldest: params.oldest,
|
||||
latest: params.latest,
|
||||
inclusive: params.inclusive ? 'true' : undefined,
|
||||
},
|
||||
limit: resolvePositiveInt(params.limit, 200),
|
||||
cursor: params.cursor,
|
||||
maxPages: resolvePositiveInt(params.maxPages, DEFAULT_MAX_PAGES),
|
||||
missingScopeHint: 'channels:history, groups:history, im:history, mpim:history',
|
||||
})
|
||||
|
||||
const messages = result.messages
|
||||
const threadTs = params.threadTs?.trim()
|
||||
const parentMessage = messages.find((msg) => msg.ts === threadTs) ?? null
|
||||
const replies = parentMessage ? messages.filter((msg) => msg !== parentMessage) : messages
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
parentMessage,
|
||||
replies,
|
||||
messages,
|
||||
replyCount: replies.length,
|
||||
hasMore: result.hasMore,
|
||||
nextCursor: result.nextCursor,
|
||||
pages: result.pages,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
parentMessage: {
|
||||
type: 'object',
|
||||
description: 'The thread parent message, or null if the thread is empty',
|
||||
properties: MESSAGE_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
replies: {
|
||||
type: 'array',
|
||||
description: 'All reply messages in the thread (excluding the parent)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: MESSAGE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'All messages (parent + replies) in chronological order',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: MESSAGE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
replyCount: {
|
||||
type: 'number',
|
||||
description: 'Number of replies returned (excluding the parent)',
|
||||
},
|
||||
hasMore: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more pages remain beyond the fetched window',
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor to fetch the next page; null when there are no more pages',
|
||||
optional: true,
|
||||
},
|
||||
pages: {
|
||||
type: 'number',
|
||||
description: 'Number of pages fetched in this invocation',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -8,9 +8,12 @@ import { slackDownloadTool } from '@/tools/slack/download'
|
||||
import { slackEditCanvasTool } from '@/tools/slack/edit_canvas'
|
||||
import { slackEphemeralMessageTool } from '@/tools/slack/ephemeral_message'
|
||||
import { slackGetCanvasTool } from '@/tools/slack/get_canvas'
|
||||
import { slackGetChannelHistoryTool } from '@/tools/slack/get_channel_history'
|
||||
import { slackGetChannelInfoTool } from '@/tools/slack/get_channel_info'
|
||||
import { slackGetMessageTool } from '@/tools/slack/get_message'
|
||||
import { slackGetPermalinkTool } from '@/tools/slack/get_permalink'
|
||||
import { slackGetThreadTool } from '@/tools/slack/get_thread'
|
||||
import { slackGetThreadRepliesTool } from '@/tools/slack/get_thread_replies'
|
||||
import { slackGetUserTool } from '@/tools/slack/get_user'
|
||||
import { slackGetUserPresenceTool } from '@/tools/slack/get_user_presence'
|
||||
import { slackInviteToConversationTool } from '@/tools/slack/invite_to_conversation'
|
||||
@@ -25,6 +28,9 @@ import { slackOpenViewTool } from '@/tools/slack/open_view'
|
||||
import { slackPublishViewTool } from '@/tools/slack/publish_view'
|
||||
import { slackPushViewTool } from '@/tools/slack/push_view'
|
||||
import { slackRemoveReactionTool } from '@/tools/slack/remove_reaction'
|
||||
import { slackSetStatusTool } from '@/tools/slack/set_status'
|
||||
import { slackSetSuggestedPromptsTool } from '@/tools/slack/set_suggested_prompts'
|
||||
import { slackSetTitleTool } from '@/tools/slack/set_title'
|
||||
import { slackUpdateMessageTool } from '@/tools/slack/update_message'
|
||||
import { slackUpdateViewTool } from '@/tools/slack/update_view'
|
||||
|
||||
@@ -57,6 +63,12 @@ export {
|
||||
slackPublishViewTool,
|
||||
slackGetMessageTool,
|
||||
slackGetThreadTool,
|
||||
slackGetThreadRepliesTool,
|
||||
slackGetChannelHistoryTool,
|
||||
slackGetPermalinkTool,
|
||||
slackSetStatusTool,
|
||||
slackSetTitleTool,
|
||||
slackSetSuggestedPromptsTool,
|
||||
slackInviteToConversationTool,
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export const slackInviteToConversationTool: ToolConfig<
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please verify the channel ID.')
|
||||
}
|
||||
if (data.error === 'user_not_found') {
|
||||
if (data.error === 'no_user' || data.error === 'user_not_found') {
|
||||
throw new Error('One or more user IDs were not found.')
|
||||
}
|
||||
if (data.error === 'cant_invite_self') {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { SlackSetStatusParams, SlackSetStatusResponse } from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Normalizes the loading_messages input, which may arrive as a string array, a
|
||||
* JSON-encoded array, or a single string. Slack accepts up to 10 entries.
|
||||
*/
|
||||
function normalizeLoadingMessages(input: unknown): string[] | undefined {
|
||||
if (input == null) return undefined
|
||||
let value = input
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return undefined
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
value = Array.isArray(parsed) ? parsed : [trimmed]
|
||||
} catch {
|
||||
value = [trimmed]
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(value)) return undefined
|
||||
const messages = value
|
||||
.map((m) => (typeof m === 'string' ? m.trim() : String(m)))
|
||||
.filter((m) => m.length > 0)
|
||||
.slice(0, 10)
|
||||
return messages.length > 0 ? messages : undefined
|
||||
}
|
||||
|
||||
export const slackSetStatusTool: ToolConfig<SlackSetStatusParams, SlackSetStatusResponse> = {
|
||||
id: 'slack_set_status',
|
||||
name: 'Slack Set Assistant Status',
|
||||
description:
|
||||
'Set or clear the assistant thread status indicator (the loading shimmer) on a Slack AI app thread. Pass an empty status to clear it.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the assistant thread (e.g., C1234567890 or D1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the assistant thread (e.g., 1405894322.002768)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Status text to display, e.g. 'Working on it…'. Pass an empty string to clear.",
|
||||
},
|
||||
loadingMessages: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Optional list of messages to rotate through as an animated loading indicator (max 10).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/assistant.threads.setStatus',
|
||||
method: 'POST',
|
||||
headers: (params: SlackSetStatusParams) => ({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
body: (params: SlackSetStatusParams) => {
|
||||
const loadingMessages = normalizeLoadingMessages(params.loadingMessages)
|
||||
return {
|
||||
channel_id: params.channel?.trim(),
|
||||
thread_ts: params.threadTs?.trim(),
|
||||
status: params.status ?? '',
|
||||
...(loadingMessages ? { loading_messages: loadingMessages } : {}),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SlackSetStatusParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (assistant:write).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'invalid_thread_ts') {
|
||||
throw new Error('Invalid thread timestamp. Please check the thread_ts value.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to set Slack assistant status')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: params?.channel?.trim() ?? '',
|
||||
threadTs: params?.threadTs?.trim() ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the status was set successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID the status was set on',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp the status was set on',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type {
|
||||
SlackSetSuggestedPromptsParams,
|
||||
SlackSetSuggestedPromptsResponse,
|
||||
SlackSuggestedPrompt,
|
||||
} from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Normalizes the prompts input into Slack's `[{ title, message }]` shape. Accepts
|
||||
* a structured array or a JSON-encoded string (as supplied from a block input).
|
||||
*/
|
||||
function normalizePrompts(input: unknown): SlackSuggestedPrompt[] {
|
||||
let value = input
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return []
|
||||
try {
|
||||
value = JSON.parse(trimmed)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const title = typeof (entry as any).title === 'string' ? (entry as any).title.trim() : ''
|
||||
const message =
|
||||
typeof (entry as any).message === 'string' ? (entry as any).message.trim() : ''
|
||||
if (!title || !message) return null
|
||||
return { title, message }
|
||||
})
|
||||
.filter((p): p is SlackSuggestedPrompt => p !== null)
|
||||
}
|
||||
|
||||
export const slackSetSuggestedPromptsTool: ToolConfig<
|
||||
SlackSetSuggestedPromptsParams,
|
||||
SlackSetSuggestedPromptsResponse
|
||||
> = {
|
||||
id: 'slack_set_suggested_prompts',
|
||||
name: 'Slack Set Suggested Prompts',
|
||||
description:
|
||||
'Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the assistant thread (e.g., C1234567890 or D1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the assistant thread (e.g., 1405894322.002768)',
|
||||
},
|
||||
prompts: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of prompts, each with a "title" (shown on the chip) and a "message" (sent when clicked). Max 4.',
|
||||
},
|
||||
promptsTitle: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Optional heading for the prompt list, e.g. 'Suggested Prompts'",
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/assistant.threads.setSuggestedPrompts',
|
||||
method: 'POST',
|
||||
headers: (params: SlackSetSuggestedPromptsParams) => ({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
body: (params: SlackSetSuggestedPromptsParams) => {
|
||||
const prompts = normalizePrompts(params.prompts).slice(0, 4)
|
||||
if (prompts.length === 0) {
|
||||
throw new Error(
|
||||
'At least one suggested prompt with a non-empty "title" and "message" is required.'
|
||||
)
|
||||
}
|
||||
const title = params.promptsTitle?.trim()
|
||||
return {
|
||||
channel_id: params.channel?.trim(),
|
||||
thread_ts: params.threadTs?.trim(),
|
||||
prompts,
|
||||
...(title ? { title } : {}),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SlackSetSuggestedPromptsParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (assistant:write).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'invalid_thread_ts') {
|
||||
throw new Error('Invalid thread timestamp. Please check the thread_ts value.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to set Slack suggested prompts')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: params?.channel?.trim() ?? '',
|
||||
threadTs: params?.threadTs?.trim() ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the suggested prompts were set successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID the prompts were set on',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp the prompts were set on',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { SlackSetTitleParams, SlackSetTitleResponse } from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const slackSetTitleTool: ToolConfig<SlackSetTitleParams, SlackSetTitleResponse> = {
|
||||
id: 'slack_set_title',
|
||||
name: 'Slack Set Assistant Title',
|
||||
description: 'Set the title of a Slack assistant thread (shown in the AI app thread header).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the assistant thread (e.g., C1234567890 or D1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the assistant thread (e.g., 1405894322.002768)',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The title to display for the assistant thread',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/assistant.threads.setTitle',
|
||||
method: 'POST',
|
||||
headers: (params: SlackSetTitleParams) => ({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
body: (params: SlackSetTitleParams) => ({
|
||||
channel_id: params.channel?.trim(),
|
||||
thread_ts: params.threadTs?.trim(),
|
||||
title: params.title,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SlackSetTitleParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (assistant:write).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'invalid_thread_ts') {
|
||||
throw new Error('Invalid thread timestamp. Please check the thread_ts value.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to set Slack assistant title')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: params?.channel?.trim() ?? '',
|
||||
threadTs: params?.threadTs?.trim() ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the title was set successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID the title was set on',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp the title was set on',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -779,6 +779,57 @@ export interface SlackGetThreadParams extends SlackBaseParams {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface SlackSetStatusParams extends SlackBaseParams {
|
||||
channel: string
|
||||
threadTs: string
|
||||
status: string
|
||||
loadingMessages?: string[]
|
||||
}
|
||||
|
||||
export interface SlackSetTitleParams extends SlackBaseParams {
|
||||
channel: string
|
||||
threadTs: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface SlackSuggestedPrompt {
|
||||
title: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface SlackSetSuggestedPromptsParams extends SlackBaseParams {
|
||||
channel: string
|
||||
threadTs: string
|
||||
prompts: SlackSuggestedPrompt[] | string
|
||||
promptsTitle?: string
|
||||
}
|
||||
|
||||
export interface SlackGetPermalinkParams extends SlackBaseParams {
|
||||
channel: string
|
||||
messageTs: string
|
||||
}
|
||||
|
||||
export interface SlackGetChannelHistoryParams extends SlackBaseParams {
|
||||
channel: string
|
||||
oldest?: string
|
||||
latest?: string
|
||||
inclusive?: boolean
|
||||
limit?: number
|
||||
cursor?: string
|
||||
maxPages?: number
|
||||
}
|
||||
|
||||
export interface SlackGetThreadRepliesParams extends SlackBaseParams {
|
||||
channel: string
|
||||
threadTs: string
|
||||
oldest?: string
|
||||
latest?: string
|
||||
inclusive?: boolean
|
||||
limit?: number
|
||||
cursor?: string
|
||||
maxPages?: number
|
||||
}
|
||||
|
||||
export interface SlackGetChannelInfoParams extends SlackBaseParams {
|
||||
channel: string
|
||||
includeNumMembers?: boolean
|
||||
@@ -1293,6 +1344,60 @@ export interface SlackPublishViewResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlackSetStatusResponse extends ToolResponse {
|
||||
output: {
|
||||
ok: boolean
|
||||
channel: string
|
||||
threadTs: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlackSetTitleResponse extends ToolResponse {
|
||||
output: {
|
||||
ok: boolean
|
||||
channel: string
|
||||
threadTs: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlackSetSuggestedPromptsResponse extends ToolResponse {
|
||||
output: {
|
||||
ok: boolean
|
||||
channel: string
|
||||
threadTs: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlackGetPermalinkResponse extends ToolResponse {
|
||||
output: {
|
||||
ok: boolean
|
||||
channel: string
|
||||
permalink: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlackGetChannelHistoryResponse extends ToolResponse {
|
||||
output: {
|
||||
messages: SlackMessage[]
|
||||
count: number
|
||||
hasMore: boolean
|
||||
nextCursor: string | null
|
||||
pages: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlackGetThreadRepliesResponse extends ToolResponse {
|
||||
output: {
|
||||
parentMessage: SlackMessage | null
|
||||
replies: SlackMessage[]
|
||||
messages: SlackMessage[]
|
||||
replyCount: number
|
||||
hasMore: boolean
|
||||
nextCursor: string | null
|
||||
pages: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SlackResponse =
|
||||
| SlackCanvasResponse
|
||||
| SlackMessageReaderResponse
|
||||
@@ -1309,6 +1414,12 @@ export type SlackResponse =
|
||||
| SlackEphemeralMessageResponse
|
||||
| SlackGetMessageResponse
|
||||
| SlackGetThreadResponse
|
||||
| SlackSetStatusResponse
|
||||
| SlackSetTitleResponse
|
||||
| SlackSetSuggestedPromptsResponse
|
||||
| SlackGetPermalinkResponse
|
||||
| SlackGetChannelHistoryResponse
|
||||
| SlackGetThreadRepliesResponse
|
||||
| SlackGetChannelInfoResponse
|
||||
| SlackGetUserPresenceResponse
|
||||
| SlackEditCanvasResponse
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { sleep } from '@sim/utils/helpers'
|
||||
import { parseRetryAfter } from '@sim/utils/retry'
|
||||
import type { SlackCanvasFile } from '@/tools/slack/types'
|
||||
|
||||
export const mapCanvasFile = (file: SlackCanvasFile): SlackCanvasFile => ({
|
||||
@@ -26,3 +28,165 @@ export const mapCanvasFile = (file: SlackCanvasFile): SlackCanvasFile => ({
|
||||
linked_channel_id: file.linked_channel_id ?? null,
|
||||
canvas_creator_id: file.canvas_creator_id ?? null,
|
||||
})
|
||||
|
||||
/**
|
||||
* Normalizes a raw Slack message object into the shape used by every
|
||||
* message-returning Slack tool (history, replies, thread, reader).
|
||||
*/
|
||||
export const mapSlackMessage = (msg: any) => ({
|
||||
type: msg.type ?? 'message',
|
||||
ts: msg.ts,
|
||||
text: msg.text ?? '',
|
||||
user: msg.user ?? null,
|
||||
bot_id: msg.bot_id ?? null,
|
||||
username: msg.username ?? null,
|
||||
channel: msg.channel ?? null,
|
||||
team: msg.team ?? null,
|
||||
thread_ts: msg.thread_ts ?? null,
|
||||
parent_user_id: msg.parent_user_id ?? null,
|
||||
reply_count: msg.reply_count ?? null,
|
||||
reply_users_count: msg.reply_users_count ?? null,
|
||||
latest_reply: msg.latest_reply ?? null,
|
||||
subscribed: msg.subscribed ?? null,
|
||||
last_read: msg.last_read ?? null,
|
||||
unread_count: msg.unread_count ?? null,
|
||||
subtype: msg.subtype ?? null,
|
||||
reactions: msg.reactions ?? [],
|
||||
is_starred: msg.is_starred ?? false,
|
||||
pinned_to: msg.pinned_to ?? [],
|
||||
files: (msg.files ?? []).map((f: any) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
mimetype: f.mimetype,
|
||||
size: f.size,
|
||||
url_private: f.url_private ?? null,
|
||||
permalink: f.permalink ?? null,
|
||||
mode: f.mode ?? null,
|
||||
})),
|
||||
attachments: msg.attachments ?? [],
|
||||
blocks: msg.blocks ?? [],
|
||||
edited: msg.edited ?? null,
|
||||
permalink: msg.permalink ?? null,
|
||||
})
|
||||
|
||||
/** Maximum messages to request per page. Slack caps `conversations.*` at 999. */
|
||||
const SLACK_PAGE_MAX = 999
|
||||
/** Hard ceiling on retries for a single rate-limited page. */
|
||||
const SLACK_RATE_LIMIT_MAX_RETRIES = 5
|
||||
|
||||
/**
|
||||
* Parses a positive integer from possibly string/undefined/NaN input (e.g. an
|
||||
* LLM-supplied param), falling back to `fallback` when the value is not a finite
|
||||
* positive number. Prevents a non-numeric `limit`/`maxPages` from silently
|
||||
* disabling pagination.
|
||||
*/
|
||||
export function resolvePositiveInt(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback
|
||||
}
|
||||
|
||||
export interface SlackPaginateOptions {
|
||||
/** Bot or OAuth bearer token. */
|
||||
token: string
|
||||
/** Slack Web API method, e.g. `conversations.history`. */
|
||||
method: 'conversations.history' | 'conversations.replies'
|
||||
/**
|
||||
* Query params common to every page (channel, ts, oldest, latest, inclusive).
|
||||
* Undefined/empty values are skipped.
|
||||
*/
|
||||
baseParams: Record<string, string | undefined>
|
||||
/** Page size (clamped to Slack's 999 max). */
|
||||
limit: number
|
||||
/** Starting cursor; omit to begin from the first page. */
|
||||
cursor?: string
|
||||
/** Maximum number of pages to fetch before stopping. */
|
||||
maxPages: number
|
||||
/** Human-readable scope hint surfaced on `missing_scope`. */
|
||||
missingScopeHint: string
|
||||
}
|
||||
|
||||
export interface SlackPaginateResult {
|
||||
messages: any[]
|
||||
nextCursor: string | null
|
||||
hasMore: boolean
|
||||
pages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches messages from a paginated Slack `conversations.*` method, following
|
||||
* `response_metadata.next_cursor` up to `maxPages`. Retries rate-limited pages
|
||||
* using the `Retry-After` header. Slack returns HTTP 200 for logical errors, so
|
||||
* the `ok` field is checked on every page.
|
||||
*/
|
||||
export async function fetchSlackMessagesPaginated(
|
||||
opts: SlackPaginateOptions
|
||||
): Promise<SlackPaginateResult> {
|
||||
const { token, method, baseParams, limit, maxPages, missingScopeHint } = opts
|
||||
const perPage = Math.min(Math.max(Number(limit) || 0, 1), SLACK_PAGE_MAX)
|
||||
|
||||
const messages: any[] = []
|
||||
let cursor = opts.cursor?.trim() || undefined
|
||||
let nextCursor: string | null = null
|
||||
let pages = 0
|
||||
|
||||
while (pages < maxPages) {
|
||||
const url = new URL(`https://slack.com/api/${method}`)
|
||||
for (const [key, value] of Object.entries(baseParams)) {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : value
|
||||
if (trimmed) url.searchParams.append(key, String(trimmed))
|
||||
}
|
||||
url.searchParams.append('limit', String(perPage))
|
||||
if (cursor) url.searchParams.append('cursor', cursor)
|
||||
|
||||
let response: Response
|
||||
let attempt = 0
|
||||
while (true) {
|
||||
response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
if (response.status === 429 && attempt < SLACK_RATE_LIMIT_MAX_RETRIES) {
|
||||
attempt += 1
|
||||
const retryAfter = parseRetryAfter(response.headers.get('retry-after')) ?? 1000
|
||||
await sleep(retryAfter)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
`Missing required permissions. Please reconnect your Slack account with the necessary scopes (${missingScopeHint}).`
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'ratelimited') {
|
||||
throw new Error('Slack rate limit exceeded. Please retry in a moment.')
|
||||
}
|
||||
throw new Error(data.error || `Failed to call ${method}`)
|
||||
}
|
||||
|
||||
for (const msg of data.messages ?? []) messages.push(mapSlackMessage(msg))
|
||||
|
||||
pages += 1
|
||||
nextCursor = data.response_metadata?.next_cursor || null
|
||||
if (!nextCursor) break
|
||||
cursor = nextCursor
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
nextCursor,
|
||||
hasMore: Boolean(nextCursor),
|
||||
pages,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,26 @@ export const SLACK_CAPABILITIES: readonly SlackCapability[] = [
|
||||
scopes: ['reactions:write'],
|
||||
events: [],
|
||||
},
|
||||
{
|
||||
id: 'action_read_history',
|
||||
label: 'Read message history',
|
||||
description:
|
||||
'Let the bot page through channel, thread, and DM history (conversations.history / conversations.replies).',
|
||||
defaultChecked: true,
|
||||
group: 'action',
|
||||
scopes: ['channels:history', 'groups:history', 'im:history', 'mpim:history'],
|
||||
events: [],
|
||||
},
|
||||
{
|
||||
id: 'action_assistant',
|
||||
label: 'Manage assistant threads',
|
||||
description:
|
||||
"Let the bot set the status indicator (the 'is thinking…' shimmer), title, and suggested prompts on AI app threads.",
|
||||
defaultChecked: true,
|
||||
group: 'action',
|
||||
scopes: ['assistant:write'],
|
||||
events: [],
|
||||
},
|
||||
{
|
||||
id: 'action_read_files',
|
||||
label: 'Read file attachments',
|
||||
|
||||
Reference in New Issue
Block a user