@chat-adapter/slack 4.18.0 → 4.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,15 +3,17 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@chat-adapter/slack)](https://www.npmjs.com/package/@chat-adapter/slack)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/slack)](https://www.npmjs.com/package/@chat-adapter/slack)
5
5
 
6
- Slack adapter for [Chat SDK](https://chat-sdk.dev/docs). Supports single-workspace and multi-workspace OAuth deployments.
6
+ Slack adapter for [Chat SDK](https://chat-sdk.dev). Configure single-workspace or multi-workspace OAuth deployments.
7
7
 
8
8
  ## Installation
9
9
 
10
10
  ```bash
11
- npm install chat @chat-adapter/slack
11
+ pnpm add @chat-adapter/slack
12
12
  ```
13
13
 
14
- ## Usage
14
+ ## Single-workspace mode
15
+
16
+ For bots deployed to a single Slack workspace. The adapter auto-detects `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from environment variables:
15
17
 
16
18
  ```typescript
17
19
  import { Chat } from "chat";
@@ -20,10 +22,7 @@ import { createSlackAdapter } from "@chat-adapter/slack";
20
22
  const bot = new Chat({
21
23
  userName: "mybot",
22
24
  adapters: {
23
- slack: createSlackAdapter({
24
- botToken: process.env.SLACK_BOT_TOKEN!,
25
- signingSecret: process.env.SLACK_SIGNING_SECRET!,
26
- }),
25
+ slack: createSlackAdapter(),
27
26
  },
28
27
  });
29
28
 
@@ -32,9 +31,325 @@ bot.onNewMention(async (thread, message) => {
32
31
  });
33
32
  ```
34
33
 
35
- ## Documentation
34
+ ## Multi-workspace mode
35
+
36
+ For apps installed across multiple Slack workspaces via OAuth, omit `botToken` and provide OAuth credentials instead. The adapter resolves tokens dynamically from your state adapter using the `team_id` from incoming webhooks.
37
+
38
+ When you pass any auth-related config (like `clientId`), the adapter won't fall back to env vars for other auth fields, preventing accidental mixing of auth modes.
39
+
40
+ ```typescript
41
+ import { createSlackAdapter } from "@chat-adapter/slack";
42
+ import { createRedisState } from "@chat-adapter/state-redis";
43
+
44
+ const slackAdapter = createSlackAdapter({
45
+ clientId: process.env.SLACK_CLIENT_ID!,
46
+ clientSecret: process.env.SLACK_CLIENT_SECRET!,
47
+ });
48
+
49
+ const bot = new Chat({
50
+ userName: "mybot",
51
+ adapters: { slack: slackAdapter },
52
+ state: createRedisState(),
53
+ });
54
+ ```
55
+
56
+ ### OAuth callback
57
+
58
+ The adapter handles the full Slack OAuth V2 exchange. Point your OAuth redirect URL to a route that calls `handleOAuthCallback`:
59
+
60
+ ```typescript
61
+ import { slackAdapter } from "@/lib/bot";
62
+
63
+ export async function GET(request: Request) {
64
+ const { teamId } = await slackAdapter.handleOAuthCallback(request);
65
+ return new Response(`Installed for team ${teamId}!`);
66
+ }
67
+ ```
68
+
69
+ ### Using the adapter outside webhooks
70
+
71
+ During webhook handling, the adapter resolves tokens automatically from `team_id`. Outside that context (e.g. cron jobs or background workers), use `getInstallation` and `withBotToken`:
72
+
73
+ ```typescript
74
+ const install = await slackAdapter.getInstallation(teamId);
75
+ if (!install) throw new Error("Workspace not installed");
76
+
77
+ await slackAdapter.withBotToken(install.botToken, async () => {
78
+ const thread = bot.thread("slack:C12345:1234567890.123456");
79
+ await thread.post("Hello from a cron job!");
80
+ });
81
+ ```
82
+
83
+ `withBotToken` uses `AsyncLocalStorage` under the hood, so concurrent calls with different tokens are isolated.
84
+
85
+ ### Removing installations
86
+
87
+ ```typescript
88
+ await slackAdapter.deleteInstallation(teamId);
89
+ ```
90
+
91
+ ### Token encryption
92
+
93
+ Pass a base64-encoded 32-byte key as `encryptionKey` to encrypt bot tokens at rest using AES-256-GCM:
94
+
95
+ ```bash
96
+ openssl rand -base64 32
97
+ ```
98
+
99
+ When `encryptionKey` is set, `setInstallation()` encrypts the token before storing and `getInstallation()` decrypts it transparently.
100
+
101
+ ## Slack app setup
102
+
103
+ ### 1. Create a Slack app from manifest
104
+
105
+ 1. Go to [api.slack.com/apps](https://api.slack.com/apps)
106
+ 2. Click **Create New App** then **From an app manifest**
107
+ 3. Select your workspace and paste the following manifest:
108
+
109
+ ```yaml
110
+ display_information:
111
+ name: My Bot
112
+ description: A bot built with chat-sdk
113
+
114
+ features:
115
+ bot_user:
116
+ display_name: My Bot
117
+ always_online: true
118
+
119
+ oauth_config:
120
+ scopes:
121
+ bot:
122
+ - app_mentions:read
123
+ - channels:history
124
+ - channels:read
125
+ - chat:write
126
+ - groups:history
127
+ - groups:read
128
+ - im:history
129
+ - im:read
130
+ - mpim:history
131
+ - mpim:read
132
+ - reactions:read
133
+ - reactions:write
134
+ - users:read
135
+
136
+ settings:
137
+ event_subscriptions:
138
+ request_url: https://your-domain.com/api/webhooks/slack
139
+ bot_events:
140
+ - app_mention
141
+ - message.channels
142
+ - message.groups
143
+ - message.im
144
+ - message.mpim
145
+ - member_joined_channel
146
+ - assistant_thread_started
147
+ - assistant_thread_context_changed
148
+ interactivity:
149
+ is_enabled: true
150
+ request_url: https://your-domain.com/api/webhooks/slack
151
+ org_deploy_enabled: false
152
+ socket_mode_enabled: false
153
+ token_rotation_enabled: false
154
+ ```
155
+
156
+ 4. Replace `https://your-domain.com/api/webhooks/slack` with your deployed webhook URL
157
+ 5. Click **Create**
158
+
159
+ ### 2. Get credentials
160
+
161
+ After creating the app, go to **Basic Information** → **App Credentials** and copy:
162
+
163
+ - **Signing Secret** as `SLACK_SIGNING_SECRET`
164
+ - **Client ID** as `SLACK_CLIENT_ID` (multi-workspace only)
165
+ - **Client Secret** as `SLACK_CLIENT_SECRET` (multi-workspace only)
166
+
167
+ **Single workspace:** Go to **OAuth & Permissions**, click **Install to Workspace**, and copy the **Bot User OAuth Token** (`xoxb-...`) as `SLACK_BOT_TOKEN`.
168
+
169
+ **Multi-workspace:** Enable **Manage Distribution** under **Basic Information** and set up an OAuth redirect URL pointing to your callback route.
170
+
171
+ ### 3. Configure slash commands (optional)
172
+
173
+ 1. Go to **Slash Commands** in your app settings
174
+ 2. Click **Create New Command**
175
+ 3. Set **Command** (e.g., `/feedback`)
176
+ 4. Set **Request URL** to `https://your-domain.com/api/webhooks/slack`
177
+ 5. Add a description and click **Save**
178
+
179
+ ## Configuration
180
+
181
+ All options are auto-detected from environment variables when not provided. You can call `createSlackAdapter()` with no arguments if the env vars are set.
182
+
183
+ | Option | Required | Description |
184
+ |--------|----------|-------------|
185
+ | `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
186
+ | `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
187
+ | `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
188
+ | `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
189
+ | `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
190
+ | `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
191
+ | `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
192
+
193
+ *`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
194
+
195
+ ## Environment variables
196
+
197
+ ```bash
198
+ SLACK_BOT_TOKEN=xoxb-... # Single-workspace only
199
+ SLACK_SIGNING_SECRET=...
200
+ SLACK_CLIENT_ID=... # Multi-workspace only
201
+ SLACK_CLIENT_SECRET=... # Multi-workspace only
202
+ SLACK_ENCRYPTION_KEY=... # Optional, for token encryption
203
+ ```
204
+
205
+ ## Features
206
+
207
+ ### Messaging
208
+
209
+ | Feature | Supported |
210
+ |---------|-----------|
211
+ | Post message | Yes |
212
+ | Edit message | Yes |
213
+ | Delete message | Yes |
214
+ | File uploads | Yes |
215
+ | Streaming | Native API |
216
+ | Scheduled messages | Yes (native, with cancel) |
217
+
218
+ ### Rich content
219
+
220
+ | Feature | Supported |
221
+ |---------|-----------|
222
+ | Card format | Block Kit |
223
+ | Buttons | Yes |
224
+ | Link buttons | Yes |
225
+ | Select menus | Yes |
226
+ | Tables | Block Kit |
227
+ | Fields | Yes |
228
+ | Images in cards | Yes |
229
+ | Modals | Yes |
230
+
231
+ ### Conversations
232
+
233
+ | Feature | Supported |
234
+ |---------|-----------|
235
+ | Slash commands | Yes |
236
+ | Mentions | Yes |
237
+ | Add reactions | Yes |
238
+ | Remove reactions | Yes |
239
+ | Typing indicator | Yes |
240
+ | DMs | Yes |
241
+ | Ephemeral messages | Yes (native) |
242
+
243
+ ### Message history
244
+
245
+ | Feature | Supported |
246
+ |---------|-----------|
247
+ | Fetch messages | Yes |
248
+ | Fetch single message | Yes |
249
+ | Fetch thread info | Yes |
250
+ | Fetch channel messages | Yes |
251
+ | List threads | Yes |
252
+ | Fetch channel info | Yes |
253
+ | Post channel message | Yes |
254
+
255
+ ### Platform-specific
256
+
257
+ | Feature | Supported |
258
+ |---------|-----------|
259
+ | Assistants API | Yes |
260
+ | Member joined channel | Yes |
261
+ | App Home tab | Yes |
262
+
263
+ ## Slack Assistants API
264
+
265
+ The adapter supports Slack's [Assistants API](https://api.slack.com/docs/apps/ai) for building AI-powered assistant experiences. This enables suggested prompts, status indicators, and thread titles in assistant DM threads.
266
+
267
+ ### Event handlers
268
+
269
+ Register handlers on the `Chat` instance:
270
+
271
+ ```typescript
272
+ bot.onAssistantThreadStarted(async (event) => {
273
+ const slack = bot.getAdapter("slack") as SlackAdapter;
274
+ await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
275
+ { title: "Summarize", message: "Summarize this channel" },
276
+ { title: "Draft", message: "Help me draft a message" },
277
+ ]);
278
+ });
279
+
280
+ bot.onAssistantContextChanged(async (event) => {
281
+ // User navigated to a different channel with the assistant panel open
282
+ });
283
+ ```
284
+
285
+ ### Adapter methods
286
+
287
+ The `SlackAdapter` exposes these methods for the Assistants API:
288
+
289
+ | Method | Description |
290
+ |--------|-------------|
291
+ | `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
292
+ | `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
293
+ | `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
294
+ | `publishHomeView(userId, view)` | Publish a Home tab view for a user |
295
+ | `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |
296
+
297
+ ### Required scopes and events
298
+
299
+ Add these to your Slack app manifest for Assistants API support:
300
+
301
+ ```yaml
302
+ oauth_config:
303
+ scopes:
304
+ bot:
305
+ - assistant:write
306
+
307
+ settings:
308
+ event_subscriptions:
309
+ bot_events:
310
+ - assistant_thread_started
311
+ - assistant_thread_context_changed
312
+ ```
313
+
314
+ ### Stream with stop blocks
315
+
316
+ When streaming in an assistant thread, you can attach Block Kit elements to the final message:
317
+
318
+ ```typescript
319
+ await thread.stream(textStream, {
320
+ stopBlocks: [
321
+ { type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: "Retry" }, action_id: "retry" }] },
322
+ ],
323
+ });
324
+ ```
325
+
326
+ ## Troubleshooting
327
+
328
+ ### `handleOAuthCallback` throws "Adapter not initialized"
329
+
330
+ - Call `await bot.initialize()` before `handleOAuthCallback()` in your callback route.
331
+ - In a Next.js app, this ensures:
332
+ - state adapter is connected
333
+ - the Slack adapter is attached to Chat
334
+ - installation writes succeed
335
+
336
+ ```typescript
337
+ const slackAdapter = bot.getAdapter("slack");
338
+
339
+ await bot.initialize();
340
+ await slackAdapter.handleOAuthCallback(request);
341
+ ```
342
+
343
+ ### "Invalid signature" error
344
+
345
+ - Verify `SLACK_SIGNING_SECRET` is correct
346
+ - Check that the request timestamp is within 5 minutes (clock sync issue)
347
+
348
+ ### Bot not responding to messages
36
349
 
37
- Full setup instructions, configuration reference, and features at [chat-sdk.dev/docs/adapters/slack](https://chat-sdk.dev/docs/adapters/slack).
350
+ - Verify event subscriptions are configured
351
+ - Check that the bot has been added to the channel
352
+ - Ensure the webhook URL is correct and accessible
38
353
 
39
354
  ## License
40
355
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger, Adapter, ChatInstance, WebhookOptions, RawMessage, EphemeralMessage, ModalElement, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, Message, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from 'chat';
1
+ import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger, Adapter, ChatInstance, WebhookOptions, RawMessage, EphemeralMessage, ScheduledMessage, ModalElement, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, Message, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from 'chat';
2
2
 
3
3
  /**
4
4
  * Slack Block Kit converter for cross-platform cards.
@@ -320,6 +320,9 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
320
320
  private renderWithTableBlocks;
321
321
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
322
322
  postEphemeral(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage>;
323
+ scheduleMessage(threadId: string, message: AdapterPostableMessage, options: {
324
+ postAt: Date;
325
+ }): Promise<ScheduledMessage>;
323
326
  openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
324
327
  viewId: string;
325
328
  }>;
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "async_hooks";
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
4
  import {
5
5
  AdapterRateLimitError,
6
+ AuthenticationError,
6
7
  extractCard,
7
8
  extractFiles,
8
9
  NetworkError,
@@ -11,7 +12,6 @@ import {
11
12
  } from "@chat-adapter/shared";
12
13
  import { WebClient } from "@slack/web-api";
13
14
  import {
14
- ChatError,
15
15
  ConsoleLogger,
16
16
  convertEmojiPlaceholders,
17
17
  defaultEmojiResolver,
@@ -772,9 +772,9 @@ var SlackAdapter = class _SlackAdapter {
772
772
  if (this.defaultBotToken) {
773
773
  return this.defaultBotToken;
774
774
  }
775
- throw new ChatError(
776
- "No bot token available. In multi-workspace mode, ensure the webhook is being processed.",
777
- "MISSING_BOT_TOKEN"
775
+ throw new AuthenticationError(
776
+ "slack",
777
+ "No bot token available. In multi-workspace mode, ensure the webhook is being processed."
778
778
  );
779
779
  }
780
780
  /**
@@ -819,9 +819,9 @@ var SlackAdapter = class _SlackAdapter {
819
819
  */
820
820
  async setInstallation(teamId, installation) {
821
821
  if (!this.chat) {
822
- throw new ChatError(
823
- "Adapter not initialized. Ensure chat.initialize() has been called first.",
824
- "NOT_INITIALIZED"
822
+ throw new ValidationError(
823
+ "slack",
824
+ "Adapter not initialized. Ensure chat.initialize() has been called first."
825
825
  );
826
826
  }
827
827
  const state = this.chat.getState();
@@ -841,9 +841,9 @@ var SlackAdapter = class _SlackAdapter {
841
841
  */
842
842
  async getInstallation(teamId) {
843
843
  if (!this.chat) {
844
- throw new ChatError(
845
- "Adapter not initialized. Ensure chat.initialize() has been called first.",
846
- "NOT_INITIALIZED"
844
+ throw new ValidationError(
845
+ "slack",
846
+ "Adapter not initialized. Ensure chat.initialize() has been called first."
847
847
  );
848
848
  }
849
849
  const state = this.chat.getState();
@@ -870,17 +870,17 @@ var SlackAdapter = class _SlackAdapter {
870
870
  */
871
871
  async handleOAuthCallback(request) {
872
872
  if (!(this.clientId && this.clientSecret)) {
873
- throw new ChatError(
874
- "clientId and clientSecret are required for OAuth. Pass them in createSlackAdapter().",
875
- "MISSING_OAUTH_CONFIG"
873
+ throw new ValidationError(
874
+ "slack",
875
+ "clientId and clientSecret are required for OAuth. Pass them in createSlackAdapter()."
876
876
  );
877
877
  }
878
878
  const url = new URL(request.url);
879
879
  const code = url.searchParams.get("code");
880
880
  if (!code) {
881
- throw new ChatError(
882
- "Missing 'code' query parameter in OAuth callback request.",
883
- "MISSING_OAUTH_CODE"
881
+ throw new ValidationError(
882
+ "slack",
883
+ "Missing 'code' query parameter in OAuth callback request."
884
884
  );
885
885
  }
886
886
  const redirectUri = url.searchParams.get("redirect_uri") ?? void 0;
@@ -891,9 +891,9 @@ var SlackAdapter = class _SlackAdapter {
891
891
  redirect_uri: redirectUri
892
892
  });
893
893
  if (!(result.ok && result.access_token && result.team?.id)) {
894
- throw new ChatError(
895
- `Slack OAuth failed: ${result.error || "missing access_token or team.id"}`,
896
- "OAUTH_FAILED"
894
+ throw new AuthenticationError(
895
+ "slack",
896
+ `Slack OAuth failed: ${result.error || "missing access_token or team.id"}`
897
897
  );
898
898
  }
899
899
  const teamId = result.team.id;
@@ -910,9 +910,9 @@ var SlackAdapter = class _SlackAdapter {
910
910
  */
911
911
  async deleteInstallation(teamId) {
912
912
  if (!this.chat) {
913
- throw new ChatError(
914
- "Adapter not initialized. Ensure chat.initialize() has been called first.",
915
- "NOT_INITIALIZED"
913
+ throw new ValidationError(
914
+ "slack",
915
+ "Adapter not initialized. Ensure chat.initialize() has been called first."
916
916
  );
917
917
  }
918
918
  const state = this.chat.getState();
@@ -1329,7 +1329,10 @@ var SlackAdapter = class _SlackAdapter {
1329
1329
  if (isJSX(modal)) {
1330
1330
  const converted = toModalElement(modal);
1331
1331
  if (!converted) {
1332
- throw new Error("Invalid JSX element: must be a Modal element");
1332
+ throw new ValidationError(
1333
+ "slack",
1334
+ "Invalid JSX element: must be a Modal element"
1335
+ );
1333
1336
  }
1334
1337
  return converted;
1335
1338
  }
@@ -1403,7 +1406,7 @@ var SlackAdapter = class _SlackAdapter {
1403
1406
  channel: event.channel,
1404
1407
  threadTs
1405
1408
  });
1406
- const isMention = isDM || event.type === "app_mention";
1409
+ const isMention = event.type === "app_mention";
1407
1410
  const factory = async () => {
1408
1411
  const msg = await this.parseSlackMessage(event, threadId);
1409
1412
  if (isMention) {
@@ -1416,7 +1419,7 @@ var SlackAdapter = class _SlackAdapter {
1416
1419
  /**
1417
1420
  * Handle reaction events from Slack (reaction_added, reaction_removed).
1418
1421
  */
1419
- handleReactionEvent(event, options) {
1422
+ async handleReactionEvent(event, options) {
1420
1423
  if (!this.chat) {
1421
1424
  this.logger.warn("Chat instance not initialized, ignoring reaction");
1422
1425
  return;
@@ -1427,9 +1430,32 @@ var SlackAdapter = class _SlackAdapter {
1427
1430
  });
1428
1431
  return;
1429
1432
  }
1433
+ let parentTs = event.item.ts;
1434
+ try {
1435
+ const result = await this.client.conversations.replies(
1436
+ this.withToken({
1437
+ channel: event.item.channel,
1438
+ ts: event.item.ts,
1439
+ limit: 1
1440
+ })
1441
+ );
1442
+ const firstMessage = result.messages?.[0];
1443
+ if (firstMessage?.thread_ts) {
1444
+ parentTs = firstMessage.thread_ts;
1445
+ }
1446
+ } catch (error) {
1447
+ this.logger.warn(
1448
+ "Failed to resolve parent thread for reaction, using message ts",
1449
+ {
1450
+ error: String(error),
1451
+ channel: event.item.channel,
1452
+ ts: event.item.ts
1453
+ }
1454
+ );
1455
+ }
1430
1456
  const threadId = this.encodeThreadId({
1431
1457
  channel: event.item.channel,
1432
- threadTs: event.item.ts
1458
+ threadTs: parentTs
1433
1459
  });
1434
1460
  const messageId = event.item.ts;
1435
1461
  const rawEmoji = event.reaction;
@@ -1996,6 +2022,95 @@ var SlackAdapter = class _SlackAdapter {
1996
2022
  this.handleSlackError(error);
1997
2023
  }
1998
2024
  }
2025
+ async scheduleMessage(threadId, message, options) {
2026
+ const { channel, threadTs } = this.decodeThreadId(threadId);
2027
+ const postAtUnix = Math.floor(options.postAt.getTime() / 1e3);
2028
+ if (postAtUnix <= Math.floor(Date.now() / 1e3)) {
2029
+ throw new ValidationError("slack", "postAt must be in the future");
2030
+ }
2031
+ const files = extractFiles(message);
2032
+ if (files.length > 0) {
2033
+ throw new ValidationError(
2034
+ "slack",
2035
+ "File uploads are not supported in scheduled messages"
2036
+ );
2037
+ }
2038
+ const token = this.getToken();
2039
+ try {
2040
+ const card = extractCard(message);
2041
+ if (card) {
2042
+ const blocks = cardToBlockKit(card);
2043
+ const fallbackText = cardToFallbackText(card);
2044
+ this.logger.debug("Slack API: chat.scheduleMessage (blocks)", {
2045
+ channel,
2046
+ threadTs,
2047
+ postAt: postAtUnix,
2048
+ blockCount: blocks.length
2049
+ });
2050
+ const result2 = await this.client.chat.scheduleMessage({
2051
+ token,
2052
+ channel,
2053
+ thread_ts: threadTs || void 0,
2054
+ post_at: postAtUnix,
2055
+ text: fallbackText,
2056
+ blocks,
2057
+ unfurl_links: false,
2058
+ unfurl_media: false
2059
+ });
2060
+ const scheduledMessageId2 = result2.scheduled_message_id;
2061
+ const adapter2 = this;
2062
+ return {
2063
+ scheduledMessageId: scheduledMessageId2,
2064
+ channelId: channel,
2065
+ postAt: options.postAt,
2066
+ raw: result2,
2067
+ async cancel() {
2068
+ await adapter2.client.chat.deleteScheduledMessage({
2069
+ token,
2070
+ channel,
2071
+ scheduled_message_id: scheduledMessageId2
2072
+ });
2073
+ }
2074
+ };
2075
+ }
2076
+ const text = convertEmojiPlaceholders(
2077
+ this.formatConverter.renderPostable(message),
2078
+ "slack"
2079
+ );
2080
+ this.logger.debug("Slack API: chat.scheduleMessage", {
2081
+ channel,
2082
+ threadTs,
2083
+ postAt: postAtUnix,
2084
+ textLength: text.length
2085
+ });
2086
+ const result = await this.client.chat.scheduleMessage({
2087
+ token,
2088
+ channel,
2089
+ thread_ts: threadTs || void 0,
2090
+ post_at: postAtUnix,
2091
+ text,
2092
+ unfurl_links: false,
2093
+ unfurl_media: false
2094
+ });
2095
+ const scheduledMessageId = result.scheduled_message_id;
2096
+ const adapter = this;
2097
+ return {
2098
+ scheduledMessageId,
2099
+ channelId: channel,
2100
+ postAt: options.postAt,
2101
+ raw: result,
2102
+ async cancel() {
2103
+ await adapter.client.chat.deleteScheduledMessage({
2104
+ token,
2105
+ channel,
2106
+ scheduled_message_id: scheduledMessageId
2107
+ });
2108
+ }
2109
+ };
2110
+ } catch (error) {
2111
+ this.handleSlackError(error);
2112
+ }
2113
+ }
1999
2114
  async openModal(triggerId, modal, contextId) {
2000
2115
  const metadata = encodeModalMetadata({
2001
2116
  contextId,
@@ -2313,9 +2428,9 @@ var SlackAdapter = class _SlackAdapter {
2313
2428
  */
2314
2429
  async stream(threadId, textStream, options) {
2315
2430
  if (!(options?.recipientUserId && options?.recipientTeamId)) {
2316
- throw new ChatError(
2317
- "Slack streaming requires recipientUserId and recipientTeamId in options",
2318
- "MISSING_STREAM_OPTIONS"
2431
+ throw new ValidationError(
2432
+ "slack",
2433
+ "Slack streaming requires recipientUserId and recipientTeamId in options"
2319
2434
  );
2320
2435
  }
2321
2436
  const { channel, threadTs } = this.decodeThreadId(threadId);
@@ -2958,9 +3073,9 @@ var SlackAdapter = class _SlackAdapter {
2958
3073
  } else {
2959
3074
  const message = options?.message;
2960
3075
  if (!message) {
2961
- throw new ChatError(
2962
- "Message required for replace action",
2963
- "INVALID_ARGS"
3076
+ throw new ValidationError(
3077
+ "slack",
3078
+ "Message required for replace action"
2964
3079
  );
2965
3080
  }
2966
3081
  const card = extractCard(message);
@@ -3008,9 +3123,9 @@ var SlackAdapter = class _SlackAdapter {
3008
3123
  status: response.status,
3009
3124
  body: errorText
3010
3125
  });
3011
- throw new ChatError(
3012
- `Failed to ${action} via response_url: ${errorText}`,
3013
- response.status.toString()
3126
+ throw new NetworkError(
3127
+ "slack",
3128
+ `Failed to ${action} via response_url: ${errorText}`
3014
3129
  );
3015
3130
  }
3016
3131
  const responseText = await response.text();