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:
Waleed
2026-06-09 18:28:27 -07:00
committed by GitHub
parent efa4f27006
commit 209501e2da
19 changed files with 1883 additions and 6 deletions
+364
View File
@@ -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.
+294 -2
View File
@@ -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',
+25 -1
View File
@@ -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",
+1
View File
@@ -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',
+1
View File
@@ -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',
+12
View File
@@ -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,
+158
View File
@@ -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',
},
},
}
+1 -1
View File
@@ -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') {
+103
View File
@@ -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',
},
},
}
+1 -1
View File
@@ -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') {
+186
View File
@@ -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',
},
},
}
+12
View File
@@ -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') {
+150
View File
@@ -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',
},
},
}
+113
View File
@@ -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',
},
},
}
+111
View File
@@ -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
+164
View File
@@ -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,
}
}
+20
View File
@@ -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',