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
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:
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { SlackSetStatusParams, SlackSetStatusResponse } from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Normalizes the loading_messages input, which may arrive as a string array, a
|
||||
* JSON-encoded array, or a single string. Slack accepts up to 10 entries.
|
||||
*/
|
||||
function normalizeLoadingMessages(input: unknown): string[] | undefined {
|
||||
if (input == null) return undefined
|
||||
let value = input
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return undefined
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
value = Array.isArray(parsed) ? parsed : [trimmed]
|
||||
} catch {
|
||||
value = [trimmed]
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(value)) return undefined
|
||||
const messages = value
|
||||
.map((m) => (typeof m === 'string' ? m.trim() : String(m)))
|
||||
.filter((m) => m.length > 0)
|
||||
.slice(0, 10)
|
||||
return messages.length > 0 ? messages : undefined
|
||||
}
|
||||
|
||||
export const slackSetStatusTool: ToolConfig<SlackSetStatusParams, SlackSetStatusResponse> = {
|
||||
id: 'slack_set_status',
|
||||
name: 'Slack Set Assistant Status',
|
||||
description:
|
||||
'Set or clear the assistant thread status indicator (the loading shimmer) on a Slack AI app thread. Pass an empty status to clear it.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the assistant thread (e.g., C1234567890 or D1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the assistant thread (e.g., 1405894322.002768)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Status text to display, e.g. 'Working on it…'. Pass an empty string to clear.",
|
||||
},
|
||||
loadingMessages: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Optional list of messages to rotate through as an animated loading indicator (max 10).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/assistant.threads.setStatus',
|
||||
method: 'POST',
|
||||
headers: (params: SlackSetStatusParams) => ({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
body: (params: SlackSetStatusParams) => {
|
||||
const loadingMessages = normalizeLoadingMessages(params.loadingMessages)
|
||||
return {
|
||||
channel_id: params.channel?.trim(),
|
||||
thread_ts: params.threadTs?.trim(),
|
||||
status: params.status ?? '',
|
||||
...(loadingMessages ? { loading_messages: loadingMessages } : {}),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SlackSetStatusParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (assistant:write).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'invalid_thread_ts') {
|
||||
throw new Error('Invalid thread timestamp. Please check the thread_ts value.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to set Slack assistant status')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: params?.channel?.trim() ?? '',
|
||||
threadTs: params?.threadTs?.trim() ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the status was set successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID the status was set on',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp the status was set on',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type {
|
||||
SlackSetSuggestedPromptsParams,
|
||||
SlackSetSuggestedPromptsResponse,
|
||||
SlackSuggestedPrompt,
|
||||
} from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Normalizes the prompts input into Slack's `[{ title, message }]` shape. Accepts
|
||||
* a structured array or a JSON-encoded string (as supplied from a block input).
|
||||
*/
|
||||
function normalizePrompts(input: unknown): SlackSuggestedPrompt[] {
|
||||
let value = input
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return []
|
||||
try {
|
||||
value = JSON.parse(trimmed)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const title = typeof (entry as any).title === 'string' ? (entry as any).title.trim() : ''
|
||||
const message =
|
||||
typeof (entry as any).message === 'string' ? (entry as any).message.trim() : ''
|
||||
if (!title || !message) return null
|
||||
return { title, message }
|
||||
})
|
||||
.filter((p): p is SlackSuggestedPrompt => p !== null)
|
||||
}
|
||||
|
||||
export const slackSetSuggestedPromptsTool: ToolConfig<
|
||||
SlackSetSuggestedPromptsParams,
|
||||
SlackSetSuggestedPromptsResponse
|
||||
> = {
|
||||
id: 'slack_set_suggested_prompts',
|
||||
name: 'Slack Set Suggested Prompts',
|
||||
description:
|
||||
'Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the assistant thread (e.g., C1234567890 or D1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the assistant thread (e.g., 1405894322.002768)',
|
||||
},
|
||||
prompts: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of prompts, each with a "title" (shown on the chip) and a "message" (sent when clicked). Max 4.',
|
||||
},
|
||||
promptsTitle: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: "Optional heading for the prompt list, e.g. 'Suggested Prompts'",
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/assistant.threads.setSuggestedPrompts',
|
||||
method: 'POST',
|
||||
headers: (params: SlackSetSuggestedPromptsParams) => ({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
body: (params: SlackSetSuggestedPromptsParams) => {
|
||||
const prompts = normalizePrompts(params.prompts).slice(0, 4)
|
||||
if (prompts.length === 0) {
|
||||
throw new Error(
|
||||
'At least one suggested prompt with a non-empty "title" and "message" is required.'
|
||||
)
|
||||
}
|
||||
const title = params.promptsTitle?.trim()
|
||||
return {
|
||||
channel_id: params.channel?.trim(),
|
||||
thread_ts: params.threadTs?.trim(),
|
||||
prompts,
|
||||
...(title ? { title } : {}),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SlackSetSuggestedPromptsParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (assistant:write).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'invalid_thread_ts') {
|
||||
throw new Error('Invalid thread timestamp. Please check the thread_ts value.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to set Slack suggested prompts')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: params?.channel?.trim() ?? '',
|
||||
threadTs: params?.threadTs?.trim() ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the suggested prompts were set successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID the prompts were set on',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp the prompts were set on',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { SlackSetTitleParams, SlackSetTitleResponse } from '@/tools/slack/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const slackSetTitleTool: ToolConfig<SlackSetTitleParams, SlackSetTitleResponse> = {
|
||||
id: 'slack_set_title',
|
||||
name: 'Slack Set Assistant Title',
|
||||
description: 'Set the title of a Slack assistant thread (shown in the AI app thread header).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'slack',
|
||||
},
|
||||
|
||||
params: {
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Authentication method: oauth or bot_token',
|
||||
},
|
||||
botToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Bot token for Custom Bot',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token or bot token for Slack API',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Channel ID containing the assistant thread (e.g., C1234567890 or D1234567890)',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Thread timestamp (thread_ts) of the assistant thread (e.g., 1405894322.002768)',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The title to display for the assistant thread',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://slack.com/api/assistant.threads.setTitle',
|
||||
method: 'POST',
|
||||
headers: (params: SlackSetTitleParams) => ({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: `Bearer ${params.accessToken || params.botToken}`,
|
||||
}),
|
||||
body: (params: SlackSetTitleParams) => ({
|
||||
channel_id: params.channel?.trim(),
|
||||
thread_ts: params.threadTs?.trim(),
|
||||
title: params.title,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SlackSetTitleParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
if (data.error === 'missing_scope') {
|
||||
throw new Error(
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (assistant:write).'
|
||||
)
|
||||
}
|
||||
if (data.error === 'invalid_auth') {
|
||||
throw new Error('Invalid authentication. Please check your Slack credentials.')
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
throw new Error('Channel not found. Please check the channel ID.')
|
||||
}
|
||||
if (data.error === 'invalid_thread_ts') {
|
||||
throw new Error('Invalid thread timestamp. Please check the thread_ts value.')
|
||||
}
|
||||
throw new Error(data.error || 'Failed to set Slack assistant title')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ok: true,
|
||||
channel: params?.channel?.trim() ?? '',
|
||||
threadTs: params?.threadTs?.trim() ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ok: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the title was set successfully',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID the title was set on',
|
||||
},
|
||||
threadTs: {
|
||||
type: 'string',
|
||||
description: 'Thread timestamp the title was set on',
|
||||
},
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user