chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
import type { SlackAddReactionParams, SlackAddReactionResponse } from '@/tools/slack/types'
import { REACTION_METADATA_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackAddReactionTool: ToolConfig<SlackAddReactionParams, SlackAddReactionResponse> = {
id: 'slack_add_reaction',
name: 'Slack Add Reaction',
description: 'Add an emoji reaction to a 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 where the message was posted (e.g., C1234567890)',
},
timestamp: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Timestamp of the message to react to (e.g., 1405894322.002768)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the emoji reaction (without colons, e.g., thumbsup, heart, eyes)',
},
},
request: {
url: '/api/tools/slack/add-reaction',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackAddReactionParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel?.trim(),
timestamp: params.timestamp?.trim(),
name: params.name?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
content: data.error || 'Failed to add reaction',
metadata: {
channel: '',
timestamp: '',
reaction: '',
},
},
error: data.error,
}
}
return {
success: true,
output: {
content: data.output.content,
metadata: data.output.metadata,
},
}
},
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Reaction metadata',
properties: REACTION_METADATA_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,101 @@
import type {
SlackArchiveConversationParams,
SlackArchiveConversationResponse,
} from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackArchiveConversationTool: ToolConfig<
SlackArchiveConversationParams,
SlackArchiveConversationResponse
> = {
id: 'slack_archive_conversation',
name: 'Slack Archive Conversation',
description: 'Archive a Slack channel so it is closed to new activity.',
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: 'ID of the channel to archive (e.g., C1234567890)',
},
},
request: {
url: 'https://slack.com/api/conversations.archive',
method: 'POST',
headers: (params: SlackArchiveConversationParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackArchiveConversationParams) => ({
channel: params.channel?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'already_archived') {
throw new Error('This channel is already archived.')
}
if (data.error === 'cant_archive_general') {
throw new Error('The #general channel cannot be archived.')
}
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'not_in_channel') {
throw new Error('The authenticated user is not a member of this channel.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to archive Slack conversation')
}
return {
success: true,
output: {
ok: true,
},
}
},
outputs: {
ok: {
type: 'boolean',
description: 'Whether the conversation was archived successfully',
},
},
}
+112
View File
@@ -0,0 +1,112 @@
import type { SlackCanvasParams, SlackCanvasResponse } from '@/tools/slack/types'
import { CANVAS_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackCanvasTool: ToolConfig<SlackCanvasParams, SlackCanvasResponse> = {
id: 'slack_canvas',
name: 'Slack Canvas Writer',
description:
'Create and share Slack canvases in channels. Canvases are collaborative documents within Slack.',
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)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the canvas',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas content in markdown format',
},
document_content: {
type: 'object',
required: false,
visibility: 'hidden',
description: 'Structured canvas document content',
},
},
request: {
url: 'https://slack.com/api/canvases.create',
method: 'POST',
headers: (params: SlackCanvasParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackCanvasParams) => {
// Use structured document content if provided, otherwise use markdown format
if (params.document_content) {
return {
title: params.title,
channel_id: params.channel,
document_content: params.document_content,
}
}
// Use the correct Canvas API format with markdown
return {
title: params.title,
channel_id: params.channel,
document_content: {
type: 'markdown',
markdown: params.content,
},
}
},
},
transformResponse: async (response: Response): Promise<SlackCanvasResponse> => {
const data = await response.json()
if (!data.ok) {
return {
success: false,
output: {
canvas_id: '',
},
error: data.error || 'Unknown error',
}
}
return {
success: true,
output: {
canvas_id: data.canvas_id ?? data.id ?? '',
},
}
},
outputs: CANVAS_OUTPUT_PROPERTIES,
}
@@ -0,0 +1,108 @@
import type {
SlackCreateChannelCanvasParams,
SlackCreateChannelCanvasResponse,
} from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackCreateChannelCanvasTool: ToolConfig<
SlackCreateChannelCanvasParams,
SlackCreateChannelCanvasResponse
> = {
id: 'slack_create_channel_canvas',
name: 'Slack Create Channel Canvas',
description: 'Create a canvas pinned to a Slack channel as its resource hub',
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 to create the canvas in (e.g., C1234567890)',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Title for the channel canvas',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Canvas content in markdown format',
},
},
request: {
url: 'https://slack.com/api/conversations.canvases.create',
method: 'POST',
headers: (params: SlackCreateChannelCanvasParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackCreateChannelCanvasParams) => {
const body: Record<string, unknown> = {
channel_id: params.channel.trim(),
}
if (params.title) {
body.title = params.title
}
if (params.content) {
body.document_content = {
type: 'markdown',
markdown: params.content,
}
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'channel_canvas_already_exists') {
throw new Error('A canvas already exists for this channel. Use Edit Canvas to modify it.')
}
throw new Error(data.error || 'Failed to create channel canvas')
}
return {
success: true,
output: {
canvas_id: data.canvas_id,
},
}
},
outputs: {
canvas_id: { type: 'string', description: 'ID of the created channel canvas' },
},
}
+140
View File
@@ -0,0 +1,140 @@
import type {
SlackCreateConversationParams,
SlackCreateConversationResponse,
} from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackCreateConversationTool: ToolConfig<
SlackCreateConversationParams,
SlackCreateConversationResponse
> = {
id: 'slack_create_conversation',
name: 'Slack Create Conversation',
description: 'Create a new public or private channel in a Slack workspace.',
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',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Name of the channel to create (lowercase, numbers, hyphens, underscores only; max 80 characters)',
},
isPrivate: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Create a private channel instead of a public one (default: false)',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Encoded team ID to create the channel in (required if using an org token)',
},
},
request: {
url: 'https://slack.com/api/conversations.create',
method: 'POST',
headers: (params: SlackCreateConversationParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackCreateConversationParams) => {
const body: Record<string, unknown> = {
name: params.name?.trim(),
}
if (params.isPrivate != null) {
body.is_private = params.isPrivate
}
if (params.teamId?.trim()) {
body.team_id = params.teamId.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'name_taken') {
throw new Error('A channel with this name already exists in the workspace.')
}
if (
data.error === 'invalid_name' ||
data.error === 'invalid_name_specials' ||
data.error === 'invalid_name_maxlength'
) {
throw new Error(
'Invalid channel name. Use only lowercase letters, numbers, hyphens, and underscores (max 80 characters).'
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
if (data.error === 'restricted_action') {
throw new Error('Workspace policy prevents channel creation.')
}
throw new Error(data.error || 'Failed to create conversation in Slack')
}
const ch = data.channel || {}
return {
success: true,
output: {
channelInfo: {
id: ch.id,
name: ch.name,
is_private: ch.is_private || false,
is_archived: ch.is_archived || false,
is_member: ch.is_member || false,
topic: ch.topic?.value || '',
purpose: ch.purpose?.value || '',
created: ch.created,
creator: ch.creator,
},
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'The newly created channel object',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
},
}
+79
View File
@@ -0,0 +1,79 @@
import type { SlackDeleteCanvasParams, SlackDeleteCanvasResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackDeleteCanvasTool: ToolConfig<SlackDeleteCanvasParams, SlackDeleteCanvasResponse> =
{
id: 'slack_delete_canvas',
name: 'Slack Delete Canvas',
description: 'Delete a Slack canvas by its canvas ID',
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',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas ID to delete (e.g., F1234ABCD)',
},
},
request: {
url: 'https://slack.com/api/canvases.delete',
method: 'POST',
headers: (params: SlackDeleteCanvasParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackDeleteCanvasParams) => ({
canvas_id: params.canvasId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'canvas_not_found') {
throw new Error('Canvas not found or not visible to the authenticated Slack user or bot.')
}
if (data.error === 'canvas_deleting_disabled') {
throw new Error('Canvas deletion is disabled for this workspace.')
}
throw new Error(data.error || 'Failed to delete canvas')
}
return {
success: true,
output: {
ok: data.ok,
},
}
},
outputs: {
ok: { type: 'boolean', description: 'Whether Slack deleted the canvas successfully' },
},
}
+99
View File
@@ -0,0 +1,99 @@
import type { SlackDeleteMessageParams, SlackDeleteMessageResponse } from '@/tools/slack/types'
import { MESSAGE_METADATA_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackDeleteMessageTool: ToolConfig<
SlackDeleteMessageParams,
SlackDeleteMessageResponse
> = {
id: 'slack_delete_message',
name: 'Slack Delete Message',
description: 'Delete a message previously sent by the bot in Slack',
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 where the message was posted (e.g., C1234567890)',
},
timestamp: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Timestamp of the message to delete (e.g., 1405894322.002768)',
},
},
request: {
url: '/api/tools/slack/delete-message',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackDeleteMessageParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel?.trim(),
timestamp: params.timestamp?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
content: data.error || 'Failed to delete message',
metadata: {
channel: '',
timestamp: '',
},
},
error: data.error,
}
}
return {
success: true,
output: {
content: data.output.content,
metadata: data.output.metadata,
},
}
},
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Deleted message metadata',
properties: MESSAGE_METADATA_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,104 @@
import type {
SlackDeleteScheduledMessageParams,
SlackDeleteScheduledMessageResponse,
} from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackDeleteScheduledMessageTool: ToolConfig<
SlackDeleteScheduledMessageParams,
SlackDeleteScheduledMessageResponse
> = {
id: 'slack_delete_scheduled_message',
name: 'Slack Delete Scheduled Message',
description: 'Delete a pending scheduled message before it posts to Slack.',
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 where the scheduled message is queued (e.g., C1234567890)',
},
scheduledMessageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Scheduled message ID from chat.scheduleMessage (e.g., Q1234ABCD)',
},
},
request: {
url: 'https://slack.com/api/chat.deleteScheduledMessage',
method: 'POST',
headers: (params: SlackDeleteScheduledMessageParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackDeleteScheduledMessageParams) => ({
channel: params.channel?.trim(),
scheduled_message_id: params.scheduledMessageId?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'invalid_scheduled_message_id') {
throw new Error(
'Invalid scheduled message ID. The message may have already posted or is set to post within 60 seconds.'
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scope (chat:write).'
)
}
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to delete scheduled Slack message')
}
return {
success: true,
output: {
ok: true,
},
}
},
outputs: {
ok: {
type: 'boolean',
description: 'Whether the scheduled message was deleted successfully',
},
},
}
+69
View File
@@ -0,0 +1,69 @@
import type { SlackDownloadParams, SlackDownloadResponse } from '@/tools/slack/types'
import { FILE_DOWNLOAD_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackDownloadTool: ToolConfig<SlackDownloadParams, SlackDownloadResponse> = {
id: 'slack_download',
name: 'Download File from Slack',
description: 'Download a file from Slack',
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',
},
fileId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the file to download',
},
fileName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional filename override',
},
},
request: {
url: '/api/tools/slack/download',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken || params.botToken,
fileId: params.fileId,
fileName: params.fileName,
}),
},
outputs: {
file: {
type: 'file',
description: 'Downloaded file stored in execution files',
properties: FILE_DOWNLOAD_OUTPUT_PROPERTIES,
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type { SlackEditCanvasParams, SlackEditCanvasResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackEditCanvasTool: ToolConfig<SlackEditCanvasParams, SlackEditCanvasResponse> = {
id: 'slack_edit_canvas',
name: 'Slack Edit Canvas',
description: 'Edit an existing Slack canvas by inserting, replacing, or deleting content',
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',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas ID to edit (e.g., F1234ABCD)',
},
operation: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Edit operation: insert_at_start, insert_at_end, insert_after, insert_before, replace, delete, or rename',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Markdown content for the operation (required for insert/replace operations)',
},
sectionId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Section ID to target (required for insert_after, insert_before, replace, and delete)',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the canvas (only used with rename operation)',
},
},
request: {
url: 'https://slack.com/api/canvases.edit',
method: 'POST',
headers: (params: SlackEditCanvasParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackEditCanvasParams) => {
const change: Record<string, unknown> = {
operation: params.operation,
}
if (params.sectionId) {
change.section_id = params.sectionId.trim()
}
if (params.operation === 'rename' && params.title) {
change.title_content = {
type: 'markdown',
markdown: params.title,
}
} else if (params.content && params.operation !== 'delete') {
change.document_content = {
type: 'markdown',
markdown: params.content,
}
}
return {
canvas_id: params.canvasId.trim(),
changes: [change],
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
throw new Error(data.error || 'Failed to edit canvas')
}
return {
success: true,
output: {
content: 'Successfully edited canvas',
},
}
},
outputs: {
content: { type: 'string', description: 'Success message' },
},
}
+114
View File
@@ -0,0 +1,114 @@
import type {
SlackEphemeralMessageParams,
SlackEphemeralMessageResponse,
} from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackEphemeralMessageTool: ToolConfig<
SlackEphemeralMessageParams,
SlackEphemeralMessageResponse
> = {
id: 'slack_ephemeral_message',
name: 'Slack Ephemeral Message',
description:
'Send an ephemeral message visible only to a specific user in a channel. Optionally reply in a thread. The message does not persist across sessions.',
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)',
},
user: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'User ID who will see the ephemeral message (e.g., U1234567890). Must be a member of the channel.',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Message text to send (supports Slack mrkdwn formatting)',
},
threadTs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Thread timestamp to reply in. When provided, the ephemeral message appears as a thread reply.',
},
blocks: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text.',
},
},
request: {
url: '/api/tools/slack/send-ephemeral',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackEphemeralMessageParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel?.trim(),
user: params.user?.trim(),
text: params.text,
thread_ts: params.threadTs?.trim() || undefined,
blocks:
typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to send ephemeral message')
}
return {
success: true,
output: data.output,
}
},
outputs: {
messageTs: {
type: 'string',
description: 'Timestamp of the ephemeral message (cannot be used with chat.update)',
},
channel: {
type: 'string',
description: 'Channel ID where the ephemeral message was sent',
},
},
}
+85
View File
@@ -0,0 +1,85 @@
import type { SlackGetCanvasParams, SlackGetCanvasResponse } from '@/tools/slack/types'
import { CANVAS_FILE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import { mapCanvasFile } from '@/tools/slack/utils'
import type { ToolConfig } from '@/tools/types'
export const slackGetCanvasTool: ToolConfig<SlackGetCanvasParams, SlackGetCanvasResponse> = {
id: 'slack_get_canvas',
name: 'Slack Get Canvas Info',
description: 'Get Slack canvas file metadata by canvas ID',
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',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas file ID to retrieve (e.g., F1234ABCD)',
},
},
request: {
url: (params: SlackGetCanvasParams) => {
const url = new URL('https://slack.com/api/files.info')
url.searchParams.append('file', params.canvasId.trim())
return url.toString()
},
method: 'GET',
headers: (params: SlackGetCanvasParams) => ({
'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 === 'file_not_found') {
throw new Error('Canvas not found. Please check the canvas ID and try again.')
}
if (data.error === 'not_visible') {
throw new Error('Canvas is not visible to the authenticated Slack user or bot.')
}
throw new Error(data.error || 'Failed to get canvas from Slack')
}
return {
success: true,
output: {
canvas: mapCanvasFile(data.file),
},
}
},
outputs: {
canvas: {
type: 'object',
description: 'Canvas file information returned by Slack',
properties: CANVAS_FILE_OUTPUT_PROPERTIES,
},
},
}
+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',
},
},
}
+115
View File
@@ -0,0 +1,115 @@
import type { SlackGetChannelInfoParams, SlackGetChannelInfoResponse } from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackGetChannelInfoTool: ToolConfig<
SlackGetChannelInfoParams,
SlackGetChannelInfoResponse
> = {
id: 'slack_get_channel_info',
name: 'Slack Get Channel Info',
description: 'Get detailed information about a Slack channel by its ID',
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 to get information about (e.g., C1234567890)',
},
includeNumMembers: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include the member count in the response',
},
},
request: {
url: (params: SlackGetChannelInfoParams) => {
const url = new URL('https://slack.com/api/conversations.info')
url.searchParams.append('channel', params.channel.trim())
url.searchParams.append('include_num_members', String(params.includeNumMembers ?? true))
return url.toString()
},
method: 'GET',
headers: (params: SlackGetChannelInfoParams) => ({
'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 === 'channel_not_found') {
throw new Error('Channel not found. Please check the channel ID and try again.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:read).'
)
}
throw new Error(data.error || 'Failed to get channel info from Slack')
}
const channel = data.channel
return {
success: true,
output: {
channelInfo: {
id: channel.id,
name: channel.name ?? '',
is_channel: channel.is_channel ?? false,
is_private: channel.is_private ?? false,
is_archived: channel.is_archived ?? false,
is_general: channel.is_general ?? false,
is_member: channel.is_member ?? false,
is_shared: channel.is_shared ?? false,
is_ext_shared: channel.is_ext_shared ?? false,
is_org_shared: channel.is_org_shared ?? false,
num_members: channel.num_members ?? null,
topic: channel.topic?.value ?? '',
purpose: channel.purpose?.value ?? '',
created: channel.created ?? null,
creator: channel.creator ?? null,
updated: channel.updated ?? null,
},
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'Detailed channel information',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
},
}
+154
View File
@@ -0,0 +1,154 @@
import { createLogger } from '@sim/logger'
import type { SlackGetMessageParams, SlackGetMessageResponse } from '@/tools/slack/types'
import { MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SlackGetMessageTool')
export const slackGetMessageTool: ToolConfig<SlackGetMessageParams, SlackGetMessageResponse> = {
id: 'slack_get_message',
name: 'Slack Get Message',
description:
'Retrieve a specific message by its timestamp. Useful for getting a thread parent 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: 'Slack channel ID (e.g., C1234567890)',
},
timestamp: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Message timestamp to retrieve (e.g., 1405894322.002768)',
},
},
request: {
url: (params: SlackGetMessageParams) => {
const url = new URL('https://slack.com/api/conversations.replies')
url.searchParams.append('channel', params.channel?.trim() ?? '')
url.searchParams.append('ts', params.timestamp?.trim() ?? '')
url.searchParams.append('limit', '1')
return url.toString()
},
method: 'GET',
headers: (params: SlackGetMessageParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
},
transformResponse: async (response: Response, params?: SlackGetMessageParams) => {
const data = await response.json()
const requestedTs = params?.timestamp?.trim() ?? ''
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, im:history, mpim:history).'
)
}
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' || data.error === 'thread_not_found') {
throw new Error(`Message not found at timestamp ${requestedTs}`)
}
throw new Error(data.error || 'Failed to get message from Slack')
}
const messages = data.messages || []
if (messages.length === 0) {
throw new Error(`Message not found at timestamp ${requestedTs}`)
}
const msg = messages[0]
if (requestedTs && msg.ts !== requestedTs) {
logger.warn('Slack returned a message with a different timestamp than requested', {
requestedTs,
returnedTs: msg.ts,
})
throw new Error(`Message not found at timestamp ${requestedTs}`)
}
const message = {
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,
}
return {
success: true,
output: {
message,
},
}
},
outputs: {
message: {
type: 'object',
description: 'The retrieved message object',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
},
}
+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',
},
},
}
+183
View File
@@ -0,0 +1,183 @@
import type { SlackGetThreadParams, SlackGetThreadResponse } from '@/tools/slack/types'
import { MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackGetThreadTool: ToolConfig<SlackGetThreadParams, SlackGetThreadResponse> = {
id: 'slack_get_thread',
name: 'Slack Get Thread',
description:
'Retrieve an entire thread including the parent message and all replies. Useful for getting full conversation context.',
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)',
},
threadTs: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thread timestamp (thread_ts) to retrieve (e.g., 1405894322.002768)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of messages to return (default: 100, max: 200)',
},
},
request: {
url: (params: SlackGetThreadParams) => {
const url = new URL('https://slack.com/api/conversations.replies')
url.searchParams.append('channel', params.channel?.trim() ?? '')
url.searchParams.append('ts', params.threadTs?.trim() ?? '')
url.searchParams.append('inclusive', 'true')
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
return url.toString()
},
method: 'GET',
headers: (params: SlackGetThreadParams) => ({
'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 === 'missing_scope') {
throw new Error(
'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') {
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 === 'thread_not_found') {
throw new Error('Thread not found. Please check the thread timestamp.')
}
throw new Error(data.error || 'Failed to get thread from Slack')
}
const rawMessages = data.messages || []
if (rawMessages.length === 0) {
throw new Error('Thread not found')
}
const messages = rawMessages.map((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,
}))
// First message is always the parent
const parentMessage = messages[0]
// Remaining messages are replies
const replies = messages.slice(1)
return {
success: true,
output: {
parentMessage,
replies,
messages,
replyCount: replies.length,
hasMore: data.has_more ?? false,
},
}
},
outputs: {
parentMessage: {
type: 'object',
description: 'The thread parent message',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
replies: {
type: 'array',
description: 'Array of reply messages in the thread (excluding the parent)',
items: {
type: 'object',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
},
messages: {
type: 'array',
description: 'All messages in the thread (parent + replies) in chronological order',
items: {
type: 'object',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
},
replyCount: {
type: 'number',
description: 'Number of replies returned in this response',
},
hasMore: {
type: 'boolean',
description: 'Whether there are more messages in the thread (pagination needed)',
},
},
}
+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',
},
},
}
+126
View File
@@ -0,0 +1,126 @@
import type { SlackGetUserParams, SlackGetUserResponse } from '@/tools/slack/types'
import { USER_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackGetUserTool: ToolConfig<SlackGetUserParams, SlackGetUserResponse> = {
id: 'slack_get_user',
name: 'Slack Get User Info',
description: 'Get detailed information about a specific Slack user by their user ID.',
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',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID to look up (e.g., U1234567890)',
},
},
request: {
url: (params: SlackGetUserParams) => {
const url = new URL('https://slack.com/api/users.info')
url.searchParams.append('user', params.userId.trim())
return url.toString()
},
method: 'GET',
headers: (params: SlackGetUserParams) => ({
'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 === 'user_not_found') {
throw new Error('User not found. Please check the user ID and try again.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (users:read).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to get user info from Slack')
}
const user = data.user
const profile = user.profile || {}
return {
success: true,
output: {
user: {
id: user.id,
team_id: user.team_id ?? null,
name: user.name,
real_name: user.real_name || profile.real_name || '',
display_name: profile.display_name ?? '',
first_name: profile.first_name ?? '',
last_name: profile.last_name ?? '',
title: profile.title ?? '',
email: profile.email ?? '',
phone: profile.phone ?? '',
skype: profile.skype ?? '',
is_bot: user.is_bot ?? false,
is_admin: user.is_admin ?? false,
is_owner: user.is_owner ?? false,
is_primary_owner: user.is_primary_owner ?? false,
is_restricted: user.is_restricted ?? false,
is_ultra_restricted: user.is_ultra_restricted ?? false,
is_app_user: user.is_app_user ?? false,
deleted: user.deleted ?? false,
color: user.color ?? null,
timezone: user.tz ?? null,
timezone_label: user.tz_label ?? null,
timezone_offset: user.tz_offset ?? null,
avatar_24: profile.image_24 ?? null,
avatar_48: profile.image_48 ?? null,
avatar_72: profile.image_72 ?? null,
avatar_192: profile.image_192 ?? null,
avatar_512: profile.image_512 ?? null,
status_text: profile.status_text ?? '',
status_emoji: profile.status_emoji ?? '',
status_expiration: profile.status_expiration ?? null,
updated: user.updated ?? null,
has_2fa: user.has_2fa ?? false,
},
},
}
},
outputs: {
user: {
type: 'object',
description: 'Detailed user information',
properties: USER_OUTPUT_PROPERTIES,
},
},
}
+122
View File
@@ -0,0 +1,122 @@
import type { SlackGetUserPresenceParams, SlackGetUserPresenceResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackGetUserPresenceTool: ToolConfig<
SlackGetUserPresenceParams,
SlackGetUserPresenceResponse
> = {
id: 'slack_get_user_presence',
name: 'Slack Get User Presence',
description: 'Check whether a Slack user is currently active or away',
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',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID to check presence for (e.g., U1234567890)',
},
},
request: {
url: (params: SlackGetUserPresenceParams) => {
const url = new URL('https://slack.com/api/users.getPresence')
url.searchParams.append('user', params.userId.trim())
return url.toString()
},
method: 'GET',
headers: (params: SlackGetUserPresenceParams) => ({
'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 === 'user_not_found') {
throw new Error('User not found. Please check the user ID and try again.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (users:read).'
)
}
throw new Error(data.error || 'Failed to get user presence from Slack')
}
return {
success: true,
output: {
presence: data.presence,
online: data.online ?? null,
autoAway: data.auto_away ?? null,
manualAway: data.manual_away ?? null,
connectionCount: data.connection_count ?? null,
lastActivity: data.last_activity ?? null,
},
}
},
outputs: {
presence: {
type: 'string',
description: 'User presence status: "active" or "away"',
},
online: {
type: 'boolean',
description:
'Whether user has an active client connection (only available when checking own presence)',
optional: true,
},
autoAway: {
type: 'boolean',
description:
'Whether user was automatically set to away due to inactivity (only available when checking own presence)',
optional: true,
},
manualAway: {
type: 'boolean',
description:
'Whether user manually set themselves as away (only available when checking own presence)',
optional: true,
},
connectionCount: {
type: 'number',
description:
'Total number of active connections for the user (only available when checking own presence)',
optional: true,
},
lastActivity: {
type: 'number',
description:
'Unix timestamp of last detected activity (only available when checking own presence)',
optional: true,
},
},
}
+89
View File
@@ -0,0 +1,89 @@
import { slackAddReactionTool } from '@/tools/slack/add_reaction'
import { slackArchiveConversationTool } from '@/tools/slack/archive_conversation'
import { slackCanvasTool } from '@/tools/slack/canvas'
import { slackCreateChannelCanvasTool } from '@/tools/slack/create_channel_canvas'
import { slackCreateConversationTool } from '@/tools/slack/create_conversation'
import { slackDeleteCanvasTool } from '@/tools/slack/delete_canvas'
import { slackDeleteMessageTool } from '@/tools/slack/delete_message'
import { slackDeleteScheduledMessageTool } from '@/tools/slack/delete_scheduled_message'
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'
import { slackListCanvasesTool } from '@/tools/slack/list_canvases'
import { slackListChannelsTool } from '@/tools/slack/list_channels'
import { slackListMembersTool } from '@/tools/slack/list_members'
import { slackListScheduledMessagesTool } from '@/tools/slack/list_scheduled_messages'
import { slackListUsersTool } from '@/tools/slack/list_users'
import { slackLookupCanvasSectionsTool } from '@/tools/slack/lookup_canvas_sections'
import { slackMessageTool } from '@/tools/slack/message'
import { slackMessageReaderTool } from '@/tools/slack/message_reader'
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 { slackRenameConversationTool } from '@/tools/slack/rename_conversation'
import { slackScheduleMessageTool } from '@/tools/slack/schedule_message'
import { slackSetConversationPurposeTool } from '@/tools/slack/set_conversation_purpose'
import { slackSetConversationTopicTool } from '@/tools/slack/set_conversation_topic'
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'
export {
slackMessageTool,
slackCanvasTool,
slackCreateConversationTool,
slackCreateChannelCanvasTool,
slackGetCanvasTool,
slackListCanvasesTool,
slackLookupCanvasSectionsTool,
slackDeleteCanvasTool,
slackMessageReaderTool,
slackDownloadTool,
slackEditCanvasTool,
slackEphemeralMessageTool,
slackUpdateMessageTool,
slackDeleteMessageTool,
slackAddReactionTool,
slackRemoveReactionTool,
slackGetChannelInfoTool,
slackListChannelsTool,
slackListMembersTool,
slackListUsersTool,
slackGetUserTool,
slackGetUserPresenceTool,
slackOpenViewTool,
slackUpdateViewTool,
slackPushViewTool,
slackPublishViewTool,
slackGetMessageTool,
slackGetThreadTool,
slackGetThreadRepliesTool,
slackGetChannelHistoryTool,
slackGetPermalinkTool,
slackSetStatusTool,
slackSetTitleTool,
slackSetSuggestedPromptsTool,
slackInviteToConversationTool,
slackScheduleMessageTool,
slackListScheduledMessagesTool,
slackDeleteScheduledMessageTool,
slackArchiveConversationTool,
slackRenameConversationTool,
slackSetConversationTopicTool,
slackSetConversationPurposeTool,
}
export * from './types'
@@ -0,0 +1,166 @@
import type {
SlackInviteToConversationParams,
SlackInviteToConversationResponse,
} from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackInviteToConversationTool: ToolConfig<
SlackInviteToConversationParams,
SlackInviteToConversationResponse
> = {
id: 'slack_invite_to_conversation',
name: 'Slack Invite to Conversation',
description: 'Invite one or more users to a Slack channel. Supports up to 100 users at a time.',
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: 'The ID of the channel to invite users to',
},
users: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of user IDs to invite (up to 100)',
},
force: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'When true, continues inviting valid users while skipping invalid ones (default: false)',
},
},
request: {
url: 'https://slack.com/api/conversations.invite',
method: 'POST',
headers: (params: SlackInviteToConversationParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackInviteToConversationParams) => {
const body: Record<string, unknown> = {
channel: params.channel?.trim(),
users: params.users?.trim(),
}
if (params.force != null) {
body.force = params.force
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
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') {
throw new Error('You cannot invite yourself to a channel.')
}
if (data.error === 'already_in_channel') {
throw new Error('One or more users are already in the channel.')
}
if (data.error === 'is_archived') {
throw new Error('The channel is archived and cannot accept new members.')
}
if (data.error === 'not_in_channel') {
throw new Error('The authenticated user is not a member of this channel.')
}
if (data.error === 'cant_invite') {
throw new Error('This user cannot be invited to the channel.')
}
if (data.error === 'no_permission') {
throw new Error('You do not have permission to invite this user to the channel.')
}
if (data.error === 'org_user_not_in_team') {
throw new Error(
'One or more invited members are in the Enterprise org but not this workspace.'
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to invite users to Slack conversation')
}
const ch = data.channel || {}
return {
success: true,
output: {
channelInfo: {
id: ch.id,
name: ch.name,
is_private: ch.is_private || false,
is_archived: ch.is_archived || false,
is_member: ch.is_member || false,
topic: ch.topic?.value || '',
purpose: ch.purpose?.value || '',
created: ch.created,
creator: ch.creator,
},
...(data.errors?.length ? { errors: data.errors } : {}),
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'The channel object after inviting users',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
errors: {
type: 'array',
description: 'Per-user errors when force is true and some invitations failed',
optional: true,
items: {
type: 'object',
properties: {
user: { type: 'string', description: 'User ID that failed' },
ok: { type: 'boolean', description: 'Always false for error entries' },
error: { type: 'string', description: 'Error code for this user' },
},
},
},
},
}
+142
View File
@@ -0,0 +1,142 @@
import type { SlackListCanvasesParams, SlackListCanvasesResponse } from '@/tools/slack/types'
import { CANVAS_FILE_OUTPUT_PROPERTIES, CANVAS_PAGING_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import { mapCanvasFile } from '@/tools/slack/utils'
import type { ToolConfig } from '@/tools/types'
export const slackListCanvasesTool: ToolConfig<SlackListCanvasesParams, SlackListCanvasesResponse> =
{
id: 'slack_list_canvases',
name: 'Slack List Canvases',
description: 'List Slack canvases available to the authenticated user or bot',
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: false,
visibility: 'user-or-llm',
description: 'Filter canvases appearing in a specific channel ID',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of canvases to return per page',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number to return',
},
user: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases created by a single user ID',
},
tsFrom: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases created after this Unix timestamp',
},
tsTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter canvases created before this Unix timestamp',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Encoded team ID, required when using an org-level token',
},
},
request: {
url: (params: SlackListCanvasesParams) => {
const url = new URL('https://slack.com/api/files.list')
url.searchParams.append('types', 'canvas')
if (params.channel) url.searchParams.append('channel', params.channel.trim())
if (params.count) url.searchParams.append('count', String(params.count))
if (params.page) url.searchParams.append('page', String(params.page))
if (params.user) url.searchParams.append('user', params.user.trim())
if (params.tsFrom) url.searchParams.append('ts_from', params.tsFrom.trim())
if (params.tsTo) url.searchParams.append('ts_to', params.tsTo.trim())
if (params.teamId) url.searchParams.append('team_id', params.teamId.trim())
return url.toString()
},
method: 'GET',
headers: (params: SlackListCanvasesParams) => ({
'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 === 'unknown_type') {
throw new Error('Slack did not recognize the canvas file type filter.')
}
throw new Error(data.error || 'Failed to list canvases from Slack')
}
return {
success: true,
output: {
canvases: (data.files ?? []).map(mapCanvasFile),
paging: {
count: data.paging?.count ?? 0,
total: data.paging?.total ?? 0,
page: data.paging?.page ?? 0,
pages: data.paging?.pages ?? 0,
},
},
}
},
outputs: {
canvases: {
type: 'array',
description: 'Canvas file objects returned by Slack',
items: {
type: 'object',
properties: CANVAS_FILE_OUTPUT_PROPERTIES,
},
},
paging: {
type: 'object',
description: 'Pagination information from Slack',
properties: CANVAS_PAGING_OUTPUT_PROPERTIES,
},
},
}
+169
View File
@@ -0,0 +1,169 @@
import type { SlackListChannelsParams, SlackListChannelsResponse } from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackListChannelsResponse> =
{
id: 'slack_list_channels',
name: 'Slack List Channels',
description:
'List all channels in a Slack workspace. Returns public and private channels the bot has access to.',
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',
},
includePrivate: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include private channels the bot is a member of (default: true)',
},
excludeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Exclude archived channels (default: true)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of channels to return (default: 100, max: 200)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response.next_cursor',
},
},
request: {
url: (params: SlackListChannelsParams) => {
const url = new URL('https://slack.com/api/conversations.list')
// Determine channel types to include
const includePrivate = params.includePrivate !== false
if (includePrivate) {
url.searchParams.append('types', 'public_channel,private_channel')
} else {
url.searchParams.append('types', 'public_channel')
}
// Exclude archived by default
const excludeArchived = params.excludeArchived !== false
url.searchParams.append('exclude_archived', String(excludeArchived))
// Set limit (default 100, max 200)
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
const cursor = params.cursor?.trim()
if (cursor) {
url.searchParams.append('cursor', cursor)
}
return url.toString()
},
method: 'GET',
headers: (params: SlackListChannelsParams) => ({
'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 === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:read, groups:read).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to list channels from Slack')
}
const channels = (data.channels || []).map((channel: any) => ({
id: channel.id,
name: channel.name,
is_private: channel.is_private || false,
is_archived: channel.is_archived || false,
is_member: channel.is_member || false,
num_members: channel.num_members,
topic: channel.topic?.value || '',
purpose: channel.purpose?.value || '',
created: channel.created,
creator: channel.creator,
}))
const ids = channels.map((channel: { id: string }) => channel.id)
const names = channels.map((channel: { name: string }) => channel.name)
return {
success: true,
output: {
channels,
ids,
names,
count: channels.length,
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
outputs: {
channels: {
type: 'array',
description: 'Array of channel objects from the workspace',
items: {
type: 'object',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
},
ids: {
type: 'array',
description: 'Array of channel IDs for easy access',
items: { type: 'string', description: 'Channel ID' },
},
names: {
type: 'array',
description: 'Array of channel names for easy access',
items: { type: 'string', description: 'Channel name' },
},
count: {
type: 'number',
description: 'Total number of channels returned',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page; null if no more pages',
optional: true,
},
},
}
+126
View File
@@ -0,0 +1,126 @@
import type { SlackListMembersParams, SlackListMembersResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackListMembersTool: ToolConfig<SlackListMembersParams, SlackListMembersResponse> = {
id: 'slack_list_members',
name: 'Slack List Channel Members',
description:
'List all members (user IDs) in a Slack channel. Use with Get User Info to resolve IDs to names.',
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 to list members from',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of members to return (default: 100, max: 200)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response.next_cursor',
},
},
request: {
url: (params: SlackListMembersParams) => {
const url = new URL('https://slack.com/api/conversations.members')
url.searchParams.append('channel', params.channel.trim())
// Set limit (default 100, max 200)
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
const cursor = params.cursor?.trim()
if (cursor) {
url.searchParams.append('cursor', cursor)
}
return url.toString()
},
method: 'GET',
headers: (params: SlackListMembersParams) => ({
'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 === 'channel_not_found') {
throw new Error('Channel not found. Please check the channel ID and try again.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:read, groups:read).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to list channel members from Slack')
}
const members = data.members || []
return {
success: true,
output: {
members,
count: members.length,
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
outputs: {
members: {
type: 'array',
description: 'Array of user IDs who are members of the channel (e.g., U1234567890)',
items: {
type: 'string',
},
},
count: {
type: 'number',
description: 'Total number of members returned',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page; null if no more pages',
optional: true,
},
},
}
@@ -0,0 +1,145 @@
import type {
SlackListScheduledMessagesParams,
SlackListScheduledMessagesResponse,
} from '@/tools/slack/types'
import { SCHEDULED_MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackListScheduledMessagesTool: ToolConfig<
SlackListScheduledMessagesParams,
SlackListScheduledMessagesResponse
> = {
id: 'slack_list_scheduled_messages',
name: 'Slack List Scheduled Messages',
description:
'List pending scheduled messages in a Slack workspace, optionally filtered by channel.',
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: false,
visibility: 'user-or-llm',
description: 'Optional channel ID to filter scheduled messages (e.g., C1234567890)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of scheduled messages to return',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor (next_cursor) from a previous response',
},
oldest: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Unix timestamp of the oldest scheduled message to include',
},
latest: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Unix timestamp of the latest scheduled message to include',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Encoded team ID (required only with org-level tokens)',
},
},
request: {
url: 'https://slack.com/api/chat.scheduledMessages.list',
method: 'POST',
headers: (params: SlackListScheduledMessagesParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackListScheduledMessagesParams) => {
const body: Record<string, unknown> = {}
if (params.channel?.trim()) {
body.channel = params.channel.trim()
}
if (params.limit != null) {
body.limit = params.limit
}
if (params.cursor?.trim()) {
body.cursor = params.cursor.trim()
}
if (params.oldest?.trim()) {
body.oldest = params.oldest.trim()
}
if (params.latest?.trim()) {
body.latest = params.latest.trim()
}
if (params.teamId?.trim()) {
body.team_id = params.teamId.trim()
}
return body
},
},
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.')
}
throw new Error(data.error || 'Failed to list scheduled Slack messages')
}
return {
success: true,
output: {
scheduledMessages: data.scheduled_messages || [],
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
outputs: {
scheduledMessages: {
type: 'array',
description: 'Array of pending scheduled message objects',
items: {
type: 'object',
properties: SCHEDULED_MESSAGE_OUTPUT_PROPERTIES,
},
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page (null when there are no more pages)',
optional: true,
},
},
}
+162
View File
@@ -0,0 +1,162 @@
import type { SlackListUsersParams, SlackListUsersResponse } from '@/tools/slack/types'
import { USER_SUMMARY_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsersResponse> = {
id: 'slack_list_users',
name: 'Slack List Users',
description: 'List all users in a Slack workspace. Returns user profiles with names and avatars.',
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',
},
includeDeleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include deactivated/deleted users (default: false)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of users to return (default: 100, max: 200)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response.next_cursor',
},
},
request: {
url: (params: SlackListUsersParams) => {
const url = new URL('https://slack.com/api/users.list')
// Set limit (default 100, max 200)
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
const cursor = params.cursor?.trim()
if (cursor) {
url.searchParams.append('cursor', cursor)
}
return url.toString()
},
method: 'GET',
headers: (params: SlackListUsersParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
},
transformResponse: async (response: Response, params?: SlackListUsersParams) => {
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 (users:read).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to list users from Slack')
}
const includeDeleted = params?.includeDeleted === true
const users = (data.members || [])
.filter((user: any) => {
// Always filter out Slackbot
if (user.id === 'USLACKBOT') return false
// Filter deleted users unless includeDeleted is true
if (!includeDeleted && user.deleted) return false
return true
})
.map((user: any) => ({
id: user.id,
name: user.name,
real_name: user.real_name || user.profile?.real_name || '',
display_name: user.profile?.display_name || '',
email: user.profile?.email || '',
is_bot: user.is_bot || false,
is_admin: user.is_admin || false,
is_owner: user.is_owner || false,
deleted: user.deleted || false,
timezone: user.tz,
avatar: user.profile?.image_72 || user.profile?.image_48 || '',
status_text: user.profile?.status_text || '',
status_emoji: user.profile?.status_emoji || '',
}))
const ids = users.map((user: { id: string }) => user.id)
const names = users.map((user: { name: string }) => user.name)
return {
success: true,
output: {
users,
ids,
names,
count: users.length,
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of user objects from the workspace',
items: {
type: 'object',
properties: USER_SUMMARY_OUTPUT_PROPERTIES,
},
},
ids: {
type: 'array',
description: 'Array of user IDs for easy access',
items: { type: 'string', description: 'User ID' },
},
names: {
type: 'array',
description: 'Array of usernames for easy access',
items: { type: 'string', description: 'Username' },
},
count: {
type: 'number',
description: 'Total number of users returned',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page; null if no more pages',
optional: true,
},
},
}
@@ -0,0 +1,114 @@
import type {
SlackLookupCanvasSectionsParams,
SlackLookupCanvasSectionsResponse,
} from '@/tools/slack/types'
import { CANVAS_SECTION_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
const parseCriteria = (criteria: SlackLookupCanvasSectionsParams['criteria']) => {
if (typeof criteria !== 'string') {
return criteria
}
try {
return JSON.parse(criteria)
} catch {
throw new Error('Canvas section criteria must be a valid JSON object')
}
}
export const slackLookupCanvasSectionsTool: ToolConfig<
SlackLookupCanvasSectionsParams,
SlackLookupCanvasSectionsResponse
> = {
id: 'slack_lookup_canvas_sections',
name: 'Slack Lookup Canvas Sections',
description: 'Find Slack canvas section IDs matching criteria for later edits',
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',
},
canvasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Canvas ID to search (e.g., F1234ABCD)',
},
criteria: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Section lookup criteria, such as {"section_types":["h1"],"contains_text":"Roadmap"}',
},
},
request: {
url: 'https://slack.com/api/canvases.sections.lookup',
method: 'POST',
headers: (params: SlackLookupCanvasSectionsParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackLookupCanvasSectionsParams) => ({
canvas_id: params.canvasId.trim(),
criteria: parseCriteria(params.criteria),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'canvas_not_found') {
throw new Error('Canvas not found or not visible to the authenticated Slack user or bot.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the canvases:read scope.'
)
}
throw new Error(data.error || 'Failed to look up canvas sections')
}
return {
success: true,
output: {
sections: data.sections ?? [],
},
}
},
outputs: {
sections: {
type: 'array',
description: 'Canvas sections matching the lookup criteria',
items: {
type: 'object',
properties: CANVAS_SECTION_OUTPUT_PROPERTIES,
},
},
},
}
+130
View File
@@ -0,0 +1,130 @@
import type { SlackMessageParams, SlackMessageResponse } from '@/tools/slack/types'
import { MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageResponse> = {
id: 'slack_message',
name: 'Slack Message',
description:
'Send messages to Slack channels or direct messages. Supports Slack mrkdwn formatting.',
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',
},
destinationType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Destination type: channel or dm',
},
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: false,
visibility: 'user-or-llm',
description: 'Slack channel ID (e.g., C1234567890)',
},
dmUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Slack user ID for direct messages (e.g., U1234567890)',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Message text to send (supports Slack mrkdwn formatting)',
},
threadTs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Thread timestamp to reply to (creates thread reply)',
},
blocks: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text.',
},
files: {
type: 'file[]',
required: false,
visibility: 'user-only',
description: 'Files to attach to the message',
},
},
request: {
url: '/api/tools/slack/send-message',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackMessageParams) => {
const isDM = params.destinationType === 'dm'
return {
accessToken: params.accessToken || params.botToken,
channel: isDM ? undefined : params.channel?.trim(),
userId: isDM ? params.dmUserId?.trim() : undefined,
text: params.text,
thread_ts: params.threadTs?.trim() || undefined,
blocks:
typeof params.blocks === 'string'
? JSON.parse(params.blocks)
: params.blocks || undefined,
files: params.files || null,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to send Slack message')
}
return {
success: true,
output: data.output,
}
},
outputs: {
message: {
type: 'object',
description: 'Complete message object with all properties returned by Slack',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
// Legacy properties for backward compatibility
ts: { type: 'string', description: 'Message timestamp' },
channel: { type: 'string', description: 'Channel ID where message was sent' },
fileCount: {
type: 'number',
description: 'Number of files uploaded (when files are attached)',
},
files: { type: 'file[]', description: 'Files attached to the message' },
},
}
+119
View File
@@ -0,0 +1,119 @@
import type { SlackMessageReaderParams, SlackMessageReaderResponse } from '@/tools/slack/types'
import { MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackMessageReaderTool: ToolConfig<
SlackMessageReaderParams,
SlackMessageReaderResponse
> = {
id: 'slack_message_reader',
name: 'Slack Message Reader',
description:
'Read the latest messages from Slack channels. Retrieve conversation history with filtering options.',
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',
},
destinationType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Destination type: channel or dm',
},
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: false,
visibility: 'user-or-llm',
description: 'Slack channel ID to read messages from (e.g., C1234567890)',
},
dmUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Slack user ID for DM conversation (e.g., U1234567890)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of messages to retrieve (default: 10, max: 15)',
},
oldest: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Start of time range (timestamp)',
},
latest: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End of time range (timestamp)',
},
},
request: {
url: '/api/tools/slack/read-messages',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackMessageReaderParams) => {
const isDM = params.destinationType === 'dm'
return {
accessToken: params.accessToken || params.botToken,
channel: isDM ? undefined : params.channel?.trim(),
userId: isDM ? params.dmUserId?.trim() : undefined,
limit: params.limit,
oldest: params.oldest,
latest: params.latest,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to fetch messages from Slack')
}
return {
success: true,
output: data.output,
}
},
outputs: {
messages: {
type: 'array',
description: 'Array of message objects from the channel',
items: {
type: 'object',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
},
},
}
+166
View File
@@ -0,0 +1,166 @@
import type { SlackOpenViewParams, SlackOpenViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackOpenViewTool: ToolConfig<SlackOpenViewParams, SlackOpenViewResponse> = {
id: 'slack_open_view',
name: 'Slack Open View',
description:
'Open a modal view in Slack using a trigger_id from an interaction payload. Used to display forms, confirmations, and other interactive modals.',
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',
},
triggerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Exchange a trigger to post to the user. Obtained from an interaction payload (e.g., slash command, button click)',
},
interactivityPointer: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Alternative to trigger_id for posting to user',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the modal. Must include type ("modal"), title, and blocks array',
},
},
request: {
url: 'https://slack.com/api/views.open',
method: 'POST',
headers: (params: SlackOpenViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackOpenViewParams) => {
const body: Record<string, unknown> = {
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.triggerId) {
body.trigger_id = params.triggerId.trim()
}
if (params.interactivityPointer) {
body.interactivity_pointer = params.interactivityPointer.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'expired_trigger_id') {
throw new Error(
'The trigger_id has expired. Trigger IDs are only valid for 3 seconds after the interaction.'
)
}
if (data.error === 'invalid_trigger_id') {
throw new Error(
'Invalid trigger_id. Ensure you are using a trigger_id from a valid interaction payload.'
)
}
if (data.error === 'exchanged_trigger_id') {
throw new Error(
'This trigger_id has already been used. Each trigger_id can only be used once.'
)
}
if (data.error === 'view_too_large') {
throw new Error('The view payload is too large. Reduce the number of blocks or content.')
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to open view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The opened modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}
+163
View File
@@ -0,0 +1,163 @@
import type { SlackPublishViewParams, SlackPublishViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackPublishViewTool: ToolConfig<SlackPublishViewParams, SlackPublishViewResponse> = {
id: 'slack_publish_view',
name: 'Slack Publish View',
description:
"Publish a static view to a user's Home tab in Slack. Used to create or update the app's Home tab experience.",
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',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID to publish the Home tab view to (e.g., U0BPQUNTA)',
},
hash: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'View state hash to protect against race conditions. Obtained from a previous views response',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the Home tab. Must include type ("home") and blocks array',
},
},
request: {
url: 'https://slack.com/api/views.publish',
method: 'POST',
headers: (params: SlackPublishViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackPublishViewParams) => {
const body: Record<string, unknown> = {
user_id: params.userId.trim(),
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.hash) {
body.hash = params.hash.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'not_found') {
throw new Error('User not found. Please check the user ID and try again.')
}
if (data.error === 'not_enabled') {
throw new Error(
'The Home tab is not enabled for this app. Enable it in your app configuration.'
)
}
if (data.error === 'hash_conflict') {
throw new Error(
'The view has been modified since the hash was generated. Retrieve the latest view and try again.'
)
}
if (data.error === 'view_too_large') {
throw new Error(
'The view payload is too large (max 250kb). Reduce the number of blocks or content.'
)
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to publish view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The published Home tab view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}
+173
View File
@@ -0,0 +1,173 @@
import type { SlackPushViewParams, SlackPushViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackPushViewTool: ToolConfig<SlackPushViewParams, SlackPushViewResponse> = {
id: 'slack_push_view',
name: 'Slack Push View',
description:
'Push a new view onto an existing modal stack in Slack. Limited to 2 additional views after the initial modal is opened.',
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',
},
triggerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Exchange a trigger to post to the user. Obtained from an interaction payload (e.g., button click within an existing modal)',
},
interactivityPointer: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Alternative to trigger_id for posting to user',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the modal to push. Must include type ("modal"), title, and blocks array',
},
},
request: {
url: 'https://slack.com/api/views.push',
method: 'POST',
headers: (params: SlackPushViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackPushViewParams) => {
const body: Record<string, unknown> = {
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.triggerId) {
body.trigger_id = params.triggerId.trim()
}
if (params.interactivityPointer) {
body.interactivity_pointer = params.interactivityPointer.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'expired_trigger_id') {
throw new Error(
'The trigger_id has expired. Trigger IDs are only valid for 3 seconds after the interaction.'
)
}
if (data.error === 'invalid_trigger_id') {
throw new Error(
'Invalid trigger_id. Ensure you are using a trigger_id from a valid interaction payload.'
)
}
if (data.error === 'exchanged_trigger_id') {
throw new Error(
'This trigger_id has already been used. Each trigger_id can only be used once.'
)
}
if (data.error === 'push_limit_reached') {
throw new Error(
'Cannot push more views. After a modal is opened, only 2 additional views can be pushed onto the stack.'
)
}
if (data.error === 'view_too_large') {
throw new Error(
'The view payload is too large (max 250kb). Reduce the number of blocks or content.'
)
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to push view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The pushed modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}
+108
View File
@@ -0,0 +1,108 @@
import type { SlackRemoveReactionParams, SlackRemoveReactionResponse } from '@/tools/slack/types'
import { REACTION_METADATA_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackRemoveReactionTool: ToolConfig<
SlackRemoveReactionParams,
SlackRemoveReactionResponse
> = {
id: 'slack_remove_reaction',
name: 'Slack Remove Reaction',
description: 'Remove an emoji reaction from a 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 where the message was posted (e.g., C1234567890)',
},
timestamp: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Timestamp of the message to remove reaction from (e.g., 1405894322.002768)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Name of the emoji reaction to remove (without colons, e.g., thumbsup, heart, eyes)',
},
},
request: {
url: '/api/tools/slack/remove-reaction',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackRemoveReactionParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel?.trim(),
timestamp: params.timestamp?.trim(),
name: params.name?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
content: data.error || 'Failed to remove reaction',
metadata: {
channel: '',
timestamp: '',
reaction: '',
},
},
error: data.error,
}
}
return {
success: true,
output: {
content: data.output.content,
metadata: data.output.metadata,
},
}
},
outputs: {
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Reaction metadata',
properties: REACTION_METADATA_OUTPUT_PROPERTIES,
},
},
}
+133
View File
@@ -0,0 +1,133 @@
import type {
SlackRenameConversationParams,
SlackRenameConversationResponse,
} from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackRenameConversationTool: ToolConfig<
SlackRenameConversationParams,
SlackRenameConversationResponse
> = {
id: 'slack_rename_conversation',
name: 'Slack Rename Conversation',
description: 'Rename an existing Slack channel.',
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: 'ID of the channel to rename (e.g., C1234567890)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'New channel name (lowercase letters, numbers, hyphens, underscores only; max 80 characters)',
},
},
request: {
url: 'https://slack.com/api/conversations.rename',
method: 'POST',
headers: (params: SlackRenameConversationParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackRenameConversationParams) => ({
channel: params.channel?.trim(),
name: params.name?.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'name_taken') {
throw new Error('A channel with this name already exists in the workspace.')
}
if (
data.error === 'invalid_name' ||
data.error === 'invalid_name_specials' ||
data.error === 'invalid_name_maxlength' ||
data.error === 'invalid_name_required'
) {
throw new Error(
'Invalid channel name. Use only lowercase letters, numbers, hyphens, and underscores (max 80 characters).'
)
}
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'not_in_channel') {
throw new Error('The authenticated user is not a member of this channel.')
}
if (data.error === 'not_authorized') {
throw new Error('You do not have permission to rename this channel.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to rename Slack conversation')
}
const ch = data.channel || {}
return {
success: true,
output: {
channelInfo: {
id: ch.id,
name: ch.name,
is_private: ch.is_private || false,
is_archived: ch.is_archived || false,
is_member: ch.is_member || false,
topic: ch.topic?.value || '',
purpose: ch.purpose?.value || '',
created: ch.created,
creator: ch.creator,
},
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'The channel object after renaming',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
},
}
+138
View File
@@ -0,0 +1,138 @@
import type { SlackScheduleMessageParams, SlackScheduleMessageResponse } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackScheduleMessageTool: ToolConfig<
SlackScheduleMessageParams,
SlackScheduleMessageResponse
> = {
id: 'slack_schedule_message',
name: 'Slack Schedule Message',
description: 'Schedule a message to be sent to a Slack channel or DM at a future time.',
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, private group, or DM to receive the message (e.g., C1234567890)',
},
postAt: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Unix timestamp (seconds) representing the future time the message should post',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Message text to send (supports Slack mrkdwn formatting)',
},
blocks: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text.',
},
threadTs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Thread timestamp to reply to (creates a scheduled thread reply)',
},
},
request: {
url: 'https://slack.com/api/chat.scheduleMessage',
method: 'POST',
headers: (params: SlackScheduleMessageParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackScheduleMessageParams) => {
const body: Record<string, unknown> = {
channel: params.channel?.trim(),
post_at: params.postAt,
}
if (params.text) {
body.text = params.text
}
if (params.blocks) {
body.blocks = typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks
}
if (params.threadTs?.trim()) {
body.thread_ts = params.threadTs.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'time_in_past' || data.error === 'time_too_far') {
throw new Error(
'The scheduled time is invalid. It must be in the future and within 120 days.'
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scope (chat:write).'
)
}
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to schedule Slack message')
}
return {
success: true,
output: {
scheduledMessageId: data.scheduled_message_id,
postAt: data.post_at,
channel: data.channel,
message: data.message || {},
},
}
},
outputs: {
scheduledMessageId: {
type: 'string',
description: 'Identifier of the scheduled message (used to delete it before it posts)',
},
postAt: { type: 'number', description: 'Unix timestamp when the message will post' },
channel: { type: 'string', description: 'Channel ID where the message is scheduled' },
message: { type: 'object', description: 'The scheduled message object returned by Slack' },
},
}
@@ -0,0 +1,105 @@
import type {
SlackSetConversationPurposeParams,
SlackSetConversationPurposeResponse,
} from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackSetConversationPurposeTool: ToolConfig<
SlackSetConversationPurposeParams,
SlackSetConversationPurposeResponse
> = {
id: 'slack_set_conversation_purpose',
name: 'Slack Set Conversation Purpose',
description: 'Set the purpose (description) for a Slack channel (max 250 characters).',
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: 'ID of the channel to update (e.g., C1234567890)',
},
purpose: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New purpose/description text (max 250 characters)',
},
},
request: {
url: 'https://slack.com/api/conversations.setPurpose',
method: 'POST',
headers: (params: SlackSetConversationPurposeParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackSetConversationPurposeParams) => ({
channel: params.channel?.trim(),
purpose: params.purpose,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'too_long') {
throw new Error('The purpose is too long. The maximum length is 250 characters.')
}
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'not_in_channel') {
throw new Error('The authenticated user is not a member of this channel.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to set Slack conversation purpose')
}
return {
success: true,
output: {
purpose: data.purpose ?? '',
},
}
},
outputs: {
purpose: {
type: 'string',
description: 'The purpose/description that was set on the channel',
},
},
}
@@ -0,0 +1,119 @@
import type {
SlackSetConversationTopicParams,
SlackSetConversationTopicResponse,
} from '@/tools/slack/types'
import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackSetConversationTopicTool: ToolConfig<
SlackSetConversationTopicParams,
SlackSetConversationTopicResponse
> = {
id: 'slack_set_conversation_topic',
name: 'Slack Set Conversation Topic',
description: 'Set the topic for a Slack channel (max 250 characters).',
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: 'ID of the channel to update (e.g., C1234567890)',
},
topic: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New topic text (max 250 characters; no formatting or linkification)',
},
},
request: {
url: 'https://slack.com/api/conversations.setTopic',
method: 'POST',
headers: (params: SlackSetConversationTopicParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackSetConversationTopicParams) => ({
channel: params.channel?.trim(),
topic: params.topic,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'too_long') {
throw new Error('The topic is too long. The maximum length is 250 characters.')
}
if (data.error === 'channel_not_found') {
throw new Error('Channel not found. Please verify the channel ID.')
}
if (data.error === 'not_in_channel') {
throw new Error('The authenticated user is not a member of this channel.')
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).'
)
}
if (data.error === 'invalid_auth') {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to set Slack conversation topic')
}
const ch = data.channel || {}
return {
success: true,
output: {
channelInfo: {
id: ch.id,
name: ch.name,
is_private: ch.is_private || false,
is_archived: ch.is_archived || false,
is_member: ch.is_member || false,
topic: ch.topic?.value || '',
purpose: ch.purpose?.value || '',
created: ch.created,
creator: ch.creator,
},
},
}
},
outputs: {
channelInfo: {
type: 'object',
description: 'The channel object after updating the topic',
properties: CHANNEL_OUTPUT_PROPERTIES,
},
},
}
+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',
},
},
}
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
import type { SlackUpdateMessageParams, SlackUpdateMessageResponse } from '@/tools/slack/types'
import { MESSAGE_METADATA_OUTPUT_PROPERTIES, MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackUpdateMessageTool: ToolConfig<
SlackUpdateMessageParams,
SlackUpdateMessageResponse
> = {
id: 'slack_update_message',
name: 'Slack Update Message',
description: 'Update a message previously sent by the bot in Slack',
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 where the message was posted (e.g., C1234567890)',
},
timestamp: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Timestamp of the message to update (e.g., 1405894322.002768)',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New message text (supports Slack mrkdwn formatting)',
},
blocks: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text.',
},
},
request: {
url: '/api/tools/slack/update-message',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: SlackUpdateMessageParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel?.trim(),
timestamp: params.timestamp?.trim(),
text: params.text,
blocks:
typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to update message')
}
return {
success: true,
output: data.output,
}
},
outputs: {
message: {
type: 'object',
description: 'Complete updated message object with all properties returned by Slack',
properties: MESSAGE_OUTPUT_PROPERTIES,
},
// Legacy properties for backward compatibility
content: { type: 'string', description: 'Success message' },
metadata: {
type: 'object',
description: 'Updated message metadata',
properties: {
...MESSAGE_METADATA_OUTPUT_PROPERTIES,
text: { type: 'string', description: 'Updated message text' },
},
},
},
}
+175
View File
@@ -0,0 +1,175 @@
import type { SlackUpdateViewParams, SlackUpdateViewResponse } from '@/tools/slack/types'
import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types'
import type { ToolConfig } from '@/tools/types'
export const slackUpdateViewTool: ToolConfig<SlackUpdateViewParams, SlackUpdateViewResponse> = {
id: 'slack_update_view',
name: 'Slack Update View',
description:
'Update an existing modal view in Slack. Identify the view by view_id or external_id, and provide the updated view payload.',
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',
},
viewId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Unique identifier of the view to update. Either viewId or externalId is required',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Developer-set unique identifier of the view to update (max 255 chars). Either viewId or externalId is required',
},
hash: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'View state hash to protect against race conditions. Obtained from a previous views response',
},
view: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'A view payload object defining the updated modal. Must include type ("modal"), title, and blocks array. Use identical block_id and action_id values to preserve input data',
},
},
request: {
url: 'https://slack.com/api/views.update',
method: 'POST',
headers: (params: SlackUpdateViewParams) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken || params.botToken}`,
}),
body: (params: SlackUpdateViewParams) => {
const body: Record<string, unknown> = {
view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view,
}
if (params.viewId) {
body.view_id = params.viewId.trim()
}
if (params.externalId) {
body.external_id = params.externalId.trim()
}
if (params.hash) {
body.hash = params.hash.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.ok) {
if (data.error === 'not_found') {
throw new Error(
'View not found. The provided view_id or external_id does not match an existing view.'
)
}
if (data.error === 'hash_conflict') {
throw new Error(
'The view has been modified since the hash was generated. Retrieve the latest view and try again.'
)
}
if (data.error === 'view_too_large') {
throw new Error(
'The view payload is too large (max 250kb). Reduce the number of blocks or content.'
)
}
if (data.error === 'duplicate_external_id') {
throw new Error(
'A view with this external_id already exists. Use a unique external_id per workspace.'
)
}
if (data.error === 'invalid_arguments') {
const messages = data.response_metadata?.messages ?? []
throw new Error(
`Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}`
)
}
if (data.error === 'missing_scope') {
throw new Error(
'Missing required permissions. Please reconnect your Slack account with the necessary scopes.'
)
}
if (
data.error === 'invalid_auth' ||
data.error === 'not_authed' ||
data.error === 'token_expired'
) {
throw new Error('Invalid authentication. Please check your Slack credentials.')
}
throw new Error(data.error || 'Failed to update view in Slack')
}
const view = data.view
return {
success: true,
output: {
view: {
id: view.id,
team_id: view.team_id ?? null,
type: view.type,
title: view.title ?? null,
submit: view.submit ?? null,
close: view.close ?? null,
blocks: view.blocks ?? [],
private_metadata: view.private_metadata ?? null,
callback_id: view.callback_id ?? null,
external_id: view.external_id ?? null,
state: view.state ?? null,
hash: view.hash ?? null,
clear_on_close: view.clear_on_close ?? false,
notify_on_close: view.notify_on_close ?? false,
root_view_id: view.root_view_id ?? null,
previous_view_id: view.previous_view_id ?? null,
app_id: view.app_id ?? null,
bot_id: view.bot_id ?? null,
},
},
}
},
outputs: {
view: {
type: 'object',
description: 'The updated modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}
+192
View File
@@ -0,0 +1,192 @@
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 => ({
id: file.id,
created: file.created ?? null,
timestamp: file.timestamp ?? null,
name: file.name ?? null,
title: file.title ?? null,
mimetype: file.mimetype ?? null,
filetype: file.filetype ?? null,
pretty_type: file.pretty_type ?? null,
user: file.user ?? null,
editable: file.editable ?? null,
size: file.size ?? null,
mode: file.mode ?? null,
is_external: file.is_external ?? null,
is_public: file.is_public ?? null,
url_private: file.url_private ?? null,
url_private_download: file.url_private_download ?? null,
permalink: file.permalink ?? null,
channels: file.channels ?? [],
groups: file.groups ?? [],
ims: file.ims ?? [],
canvas_readtime: file.canvas_readtime ?? null,
is_channel_space: file.is_channel_space ?? null,
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,
}
}