eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
951 lines (940 loc) • 38.8 kB
TypeScript
import { AsyncLocalStorage } from 'node:async_hooks';
import { WebClient } from './_slack-web-api.js';
import { BaseFormatConverter, Root, AdapterPostableMessage, CardElement, Logger, Adapter, ChatInstance, UserInfo, WebhookOptions, OptionsLoadResult, ModalResponse, ModalElement, LinkPreview, Message, Attachment, RawMessage, EphemeralMessage, ScheduledMessage, FileUpload, PlanModel, PlanContent, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, ChannelVisibility, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from '#compiled/chat/index.js';
export { EncryptedTokenData, decodeKey } from './_chat-adapter-shared.js';
/**
* Slack format conversion.
*
* Outgoing: Slack now natively renders markdown via the `markdown_text` field
* on chat.postMessage / postEphemeral / update / scheduleMessage. We pass
* markdown through there and let Slack handle it. Interactive `response_url`
* payloads do not accept `markdown_text`, so those still use Slack mrkdwn text.
*
* Incoming: Slack `message` events still deliver text as mrkdwn
* (`*bold*`, `<@U123>`, `<url|text>`), so the toAst parser stays.
*/
type SlackTextPayload = {
text: string;
} | {
markdown_text: string;
};
declare class SlackFormatConverter extends BaseFormatConverter {
/**
* Render an AST to standard markdown. Slack accepts this directly via
* `markdown_text` and the `markdown` block.
*/
fromAst(ast: Root): string;
/**
* Parse Slack mrkdwn into an AST. Used for incoming `message` events.
*/
toAst(mrkdwn: string): Root;
/**
* Build the Slack API payload fields for a message.
*
* - `string` / `{ raw }` → `{ text }` (plain — preserves literal `*`, `_`, etc.)
* - `{ markdown }` / `{ ast }` → `{ markdown_text }` (Slack renders natively)
*
* Bare `@user` mentions are rewritten to `<@user>` and `:emoji:` placeholders
* are normalized for Slack in all branches.
*
* Note: `markdown_text` has a 12,000 character limit; `text` allows ~40,000.
* Note: `markdown_text` is mutually exclusive with `text` and `blocks`.
*/
toSlackPayload(message: AdapterPostableMessage): SlackTextPayload;
/**
* Build text for Slack response_url payloads.
*
* Slack rejects `markdown_text` on response_url (`no_text`), so markdown/AST
* messages are rendered to Slack's legacy mrkdwn format for this surface.
*/
toResponseUrlText(message: AdapterPostableMessage): string;
private finalize;
private astToMrkdwn;
private nodeToMrkdwn;
}
/**
* Slack Block Kit converter for cross-platform cards.
*
* Converts CardElement to Slack Block Kit blocks.
* @see https://api.slack.com/block-kit
*/
interface SlackBlock {
block_id?: string;
type: string;
[]: unknown;
}
/**
* Convert a CardElement to Slack Block Kit blocks.
*/
declare function cardToBlockKit(card: CardElement): SlackBlock[];
/**
* Generate fallback text from a card element.
* Used when blocks aren't supported or for notifications.
*/
declare function cardToFallbackText(card: CardElement): string;
/**
* Slack modal (view) converter.
* Converts ModalElement to Slack Block Kit view format.
*/
interface SlackView {
blocks: SlackBlock[];
callback_id: string;
close?: {
type: "plain_text";
text: string;
};
notify_on_close?: boolean;
private_metadata?: string;
submit?: {
type: "plain_text";
text: string;
};
title: {
type: "plain_text";
text: string;
};
type: "modal";
}
interface SlackModalResponse {
errors?: Record<string, string>;
response_action?: "errors" | "update" | "push" | "clear";
view?: SlackView;
}
/**
* Slack adapter types.
*/
type SlackAdapterMode = "webhook" | "socket";
/**
* Bot token configuration. Can be a static string, or a function that returns
* a token (optionally asynchronously). The function is invoked each time a
* token is needed, enabling rotation or lazy retrieval from a secret manager.
*/
type SlackBotToken = string | (() => string | Promise<string>);
/** Data stored per Slack workspace installation */
interface SlackInstallation {
botToken: string;
botUserId?: string;
teamName?: string;
}
interface SlackAdapterConfig {
/** Override the Slack API base URL (e.g. "https://slack-gov.com/api/" for GovSlack). Defaults to SLACK_API_URL env var. */
apiUrl?: string;
/** App-level token (xapp-...). Required for socket mode. */
appToken?: string;
/**
* Bot token (xoxb-...). Required for single-workspace mode. Omit for multi-workspace.
* May be a string, or a function returning a string or Promise<string> (called
* on each use to support rotation or deferred resolution).
*/
botToken?: SlackBotToken;
/** Bot user ID (will be fetched if not provided) */
botUserId?: string;
/** Slack app client ID (required for OAuth / multi-workspace) */
clientId?: string;
/** Slack app client secret (required for OAuth / multi-workspace) */
clientSecret?: string;
/**
* Base64-encoded 32-byte AES-256-GCM encryption key.
* If provided, bot tokens stored via setInstallation() will be encrypted at rest.
*/
encryptionKey?: string;
/**
* Prefix for the state key used to store workspace installations.
* Defaults to `slack:installation`. The full key will be `{prefix}:{teamId}`.
*/
installationKeyPrefix?: string;
/**
* External installation provider for multi-workspace apps using external
* token management (e.g., Vercel Connect). When set, the adapter bypasses
* internal StateAdapter storage for token lookups.
*
* For Enterprise Grid org-wide installs, `installationId` will be the
* enterprise ID; otherwise it will be the team ID.
*/
installationProvider?: {
getInstallation: (installationId: string, isEnterpriseInstall: boolean) => Promise<SlackInstallation | null>;
};
/** Logger instance for error reporting. Defaults to ConsoleLogger. */
logger?: Logger;
/** Connection mode: "webhook" (default) or "socket" */
mode?: SlackAdapterMode;
/** Signing secret for webhook verification. Defaults to SLACK_SIGNING_SECRET env var. */
signingSecret?: string;
/** Shared secret for authenticating forwarded socket mode events. Auto-detected from SLACK_SOCKET_FORWARDING_SECRET. Falls back to appToken if not set. */
socketForwardingSecret?: string;
/** Override bot username (optional) */
userName?: string;
/**
* Custom webhook verifier. Used in place of `signingSecret`.
* Receives the incoming `Request` and the raw body text already
* read by the adapter. To reject the request, either
* return a falsy value (sync or async) or throw/reject; the adapter will
* respond with `401 Invalid signature`. Any truthy return value is treated
* as a successful verification. If a string is returned, it replaces the
* raw body for downstream parsing — useful when the verifier needs to
* canonicalize or substitute the verified payload.
*
* `webhookVerifier` takes precedence over `signingSecret` and the
* `SLACK_SIGNING_SECRET` env var; when it is set, those are ignored.
*
* SECURITY: When this is used in place of `signingSecret`, the built-in
* Slack timestamp tolerance check is NOT performed. Implementations are
* responsible for verifying the `x-slack-request-timestamp` header (or an
* equivalent freshness signal) to prevent replay of captured signed
* requests.
*/
webhookVerifier?: (request: Request, body: string) => unknown | Promise<unknown>;
}
/** Envelope for events forwarded from a socket mode listener via HTTP POST */
interface SlackForwardedSocketEvent {
body: Record<string, unknown>;
eventType: string;
timestamp: number;
type: "socket_event";
}
interface SlackOAuthCallbackOptions {
/** Redirect URI to send to Slack during the OAuth code exchange. */
redirectUri?: string;
}
/** Slack-specific thread ID data */
interface SlackThreadId {
channel: string;
threadTs: string;
}
/** Slack event payload (raw message format) */
interface SlackEvent {
/** Legacy attachments (unfurl previews, app unfurls, etc.) */
attachments?: Array<{
from_url?: string;
image_url?: string;
is_msg_unfurl?: boolean;
original_url?: string;
service_icon?: string;
service_name?: string;
text?: string;
thumb_url?: string;
title?: string;
title_link?: string;
}>;
/** Rich text blocks containing structured elements (links, mentions, etc.) */
blocks?: Array<{
type: string;
elements?: Array<{
type: string;
elements?: Array<{
type: string;
url?: string;
text?: string;
}>;
}>;
}>;
bot_id?: string;
channel?: string;
/** Channel type: "channel", "group", "mpim", or "im" (DM) */
channel_type?: string;
edited?: {
ts: string;
};
files?: Array<{
id?: string;
mimetype?: string;
url_private?: string;
name?: string;
size?: number;
original_w?: number;
original_h?: number;
}>;
/** Hidden flag on message_changed events (true for unfurl-only updates) */
hidden?: boolean;
/** Timestamp of the latest reply (present on thread parent messages) */
latest_reply?: string;
/** Inner message on message_changed events */
message?: SlackEvent;
/** Number of replies in the thread (present on thread parent messages) */
reply_count?: number;
subtype?: string;
team?: string;
team_id?: string;
text?: string;
thread_ts?: string;
ts?: string;
type: string;
user?: string;
username?: string;
}
/** Slack reaction event payload */
interface SlackReactionEvent {
event_ts: string;
item: {
type: string;
channel: string;
ts: string;
};
item_user?: string;
reaction: string;
type: "reaction_added" | "reaction_removed";
user: string;
}
/** Slack assistant_thread_started event payload */
interface SlackAssistantThreadStartedEvent {
assistant_thread: {
user_id: string;
channel_id: string;
thread_ts: string;
context: {
channel_id?: string;
team_id?: string;
enterprise_id?: string;
thread_entry_point?: string;
force_search?: boolean;
};
};
event_ts: string;
type: "assistant_thread_started";
}
/** Slack assistant_thread_context_changed event payload */
interface SlackAssistantContextChangedEvent {
assistant_thread: {
user_id: string;
channel_id: string;
thread_ts: string;
context: {
channel_id?: string;
team_id?: string;
enterprise_id?: string;
thread_entry_point?: string;
force_search?: boolean;
};
};
event_ts: string;
type: "assistant_thread_context_changed";
}
/** Slack app_home_opened event payload */
interface SlackAppHomeOpenedEvent {
channel: string;
event_ts: string;
tab: string;
type: "app_home_opened";
user: string;
}
/** Slack member_joined_channel event payload */
interface SlackMemberJoinedChannelEvent {
channel: string;
channel_type?: string;
event_ts: string;
inviter?: string;
team?: string;
type: "member_joined_channel";
user: string;
}
/** Slack user_change event payload */
interface SlackUserChangeEvent {
event_ts: string;
type: "user_change";
user: {
id: string;
name?: string;
real_name?: string;
profile?: {
display_name?: string;
real_name?: string;
};
};
}
/** Slack webhook payload envelope */
interface SlackWebhookPayload {
challenge?: string;
/** Enterprise ID for Enterprise Grid org-wide installs */
enterprise_id?: string;
event?: SlackEvent | SlackReactionEvent | SlackAssistantThreadStartedEvent | SlackAssistantContextChangedEvent | SlackAppHomeOpenedEvent | SlackMemberJoinedChannelEvent | SlackUserChangeEvent;
event_id?: string;
event_time?: number;
/** Whether this is an Enterprise Grid org-wide install */
is_enterprise_install?: boolean;
/** Whether this event occurred in an externally shared channel (Slack Connect) */
is_ext_shared_channel?: boolean;
team_id?: string;
type: string;
}
/** Slack interactive payload (block_actions) for button clicks */
interface SlackBlockActionsPayload {
actions: Array<{
type: string;
action_id: string;
block_id?: string;
value?: string;
action_ts?: string;
selected_option?: {
value: string;
};
}>;
channel: {
id: string;
name: string;
};
container: {
type: string;
message_ts: string;
channel_id: string;
is_ephemeral?: boolean;
thread_ts?: string;
};
message: {
ts: string;
thread_ts?: string;
};
response_url?: string;
trigger_id: string;
type: "block_actions";
user: {
id: string;
username: string;
name?: string;
};
}
interface SlackViewSubmissionPayload {
trigger_id: string;
type: "view_submission";
user: {
id: string;
username: string;
name?: string;
};
view: {
id: string;
callback_id: string;
private_metadata?: string;
state: {
values: Record<string, Record<string, {
value?: string;
selected_option?: {
value: string;
};
}>>;
};
};
}
interface SlackViewClosedPayload {
type: "view_closed";
user: {
id: string;
username: string;
name?: string;
};
view: {
id: string;
callback_id: string;
private_metadata?: string;
};
}
interface SlackBlockSuggestionPayload {
action_id: string;
block_id: string;
team?: {
id: string;
};
type: "block_suggestion";
user: {
id: string;
username?: string;
name?: string;
};
value?: string;
}
type SlackInteractivePayload = SlackBlockActionsPayload | SlackBlockSuggestionPayload | SlackViewSubmissionPayload | SlackViewClosedPayload;
/** Cached user info */
interface CachedUser {
avatarUrl?: string;
displayName: string;
email?: string;
isBot?: boolean;
realName: string;
}
declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
readonly name = "slack";
readonly userName: string;
protected readonly _client: WebClient;
protected readonly tokenClientCache: Map<string, WebClient>;
protected readonly slackApiUrl: string | undefined;
protected readonly signingSecret: string | undefined;
protected readonly webhookVerifier: ((request: Request, body: string) => unknown | Promise<unknown>) | undefined;
protected readonly defaultBotTokenProvider: (() => string | Promise<string>) | undefined;
protected chat: ChatInstance | null;
protected readonly logger: Logger;
protected _botUserId: string | null;
protected _botId: string | null;
protected readonly formatConverter: SlackFormatConverter;
protected static readonly USER_CACHE_TTL_MS: number;
protected static readonly CHANNEL_CACHE_TTL_MS: number;
protected static readonly REVERSE_INDEX_TTL_MS: number;
/**
* Cache of channel IDs known to be external/shared (Slack Connect).
* Populated from `is_ext_shared_channel` in incoming webhook payloads.
*/
private readonly _externalChannels;
protected readonly appToken: string | undefined;
protected readonly mode: SlackAdapterMode;
protected readonly socketForwardingSecret: string | undefined;
private socketClient;
protected readonly clientId: string | undefined;
protected readonly clientSecret: string | undefined;
protected readonly encryptionKey: Buffer | undefined;
protected readonly installationKeyPrefix: string;
protected readonly installationProvider: SlackAdapterConfig["installationProvider"];
protected readonly requestContext: AsyncLocalStorage<{
token: string;
botUserId?: string;
isExtSharedChannel?: boolean;
enterpriseId?: string;
isEnterpriseInstall?: boolean;
}>;
/** Bot user ID (e.g., U_BOT_123) used for mention detection */
get botUserId(): string | undefined;
get isSocketMode(): boolean;
/**
* Direct access to a [`WebClient`](https://github.com/slackapi/node-slack-sdk/tree/main/packages/web-api)
* from `@slack/web-api` bound to the bot token for the current request
* context (multi-workspace) or the configured default token
* (single-workspace). Use for any Slack Web API call not covered by the
* SDK's high-level methods — for example
* `adapter.webClient.pins.add(...)` or
* `adapter.webClient.usergroups.list(...)`.
*
* Resolution order:
* 1. Token from the current `requestContext` (set during webhook
* handling, or by `withBotToken()`).
* 2. The default bot token, when configured as a static string or
* synchronous resolver function.
*
* Throws `AuthenticationError` if neither is available — typical causes
* are calling `.webClient` outside any webhook/`withBotToken()` context
* in multi-workspace mode, or having configured `botToken` as an async
* function. In the latter case wrap the work in
* `adapter.withBotToken(token, () => adapter.webClient...)`.
*
* @example
* ```ts
* const slack = bot.getAdapter("slack").webClient;
* await slack.pins.add({
* channel: "C123ABC",
* timestamp: "1234567890.123456",
* });
* ```
*/
get webClient(): WebClient;
/**
* @deprecated Use {@link SlackAdapter.webClient | `webClient`} instead.
* This alias is preserved for backwards compatibility and will be
* removed in a future major release.
*/
get client(): WebClient;
private getClientForToken;
constructor(config?: SlackAdapterConfig);
/**
* Get the current bot token for API calls.
* Checks request context (multi-workspace) → default token provider
* (single-workspace) → throws.
*/
protected getToken(): Promise<string>;
/**
* Add the current token to API call options.
* Workaround for Slack WebClient types not including `token` in per-method args.
*/
protected withToken<T extends Record<string, any>>(options: T): Promise<T & {
token: string;
}>;
initialize(chat: ChatInstance): Promise<void>;
protected installationKey(teamId: string): string;
/**
* Save a Slack workspace installation.
* Call this from your OAuth callback route after a successful installation.
*/
setInstallation(teamId: string, installation: SlackInstallation): Promise<void>;
/**
* Retrieve a Slack workspace installation.
*/
getInstallation(teamId: string): Promise<SlackInstallation | null>;
/**
* Handle the Slack OAuth V2 callback.
* Accepts the incoming request, extracts the authorization code,
* exchanges it for tokens, and saves the installation.
*/
handleOAuthCallback(request: Request, options?: SlackOAuthCallbackOptions): Promise<{
teamId: string;
installation: SlackInstallation;
}>;
/**
* Remove a Slack workspace installation.
*/
deleteInstallation(teamId: string): Promise<void>;
/**
* Run a function with a specific bot token in context.
* Use this for operations outside webhook handling (cron jobs, workflows).
*/
withBotToken<T>(token: string, fn: () => T): T;
/**
* Resolve the bot token for an installation from the external provider or state adapter.
* @param installationId - team_id or enterprise_id depending on install type
* @param isEnterpriseInstall - true if this is an Enterprise Grid org-wide install
*/
protected resolveTokenForTeam(installationId: string, isEnterpriseInstall?: boolean): Promise<{
token: string;
botUserId?: string;
} | null>;
/**
* Extract installation info from an interactive payload (form-urlencoded).
* For Enterprise Grid org-wide installs, returns enterprise_id; otherwise team_id.
*/
protected extractInstallationFromInteractive(body: string): {
installationId: string;
isEnterpriseInstall: boolean;
enterpriseId?: string;
} | null;
/**
* Look up user info from Slack API with caching via state adapter.
* Returns null when the API call fails.
*/
protected lookupUser(userId: string): Promise<CachedUser | null>;
/**
* Look up channel name from Slack API with caching via state adapter.
* Returns channel name, or falls back to channel ID.
*/
protected lookupChannel(channelId: string): Promise<string>;
getUser(userId: string): Promise<UserInfo | null>;
handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
/** Extract and dispatch events from a validated payload */
protected processEventPayload(payload: SlackWebhookPayload, options?: WebhookOptions): void;
/**
* Handle Slack interactive payloads (button clicks, view submissions, etc.).
* These are sent as form-urlencoded with a `payload` JSON field.
*/
protected handleInteractivePayload(body: string, options?: WebhookOptions): Response | Promise<Response>;
/**
* Dispatch a pre-parsed interactive payload to the correct handler.
* Used by both webhook and socket mode paths.
*/
protected dispatchInteractivePayload(payload: SlackInteractivePayload, options?: WebhookOptions): Response | Promise<Response>;
/**
* Handle Slack slash command payloads.
* Slash commands are sent as form-urlencoded with command, text, user_id, channel_id, etc.
*/
protected handleSlashCommand(params: URLSearchParams, options?: WebhookOptions): Promise<Response>;
/**
* Handle block_actions payload (button clicks in Block Kit).
*/
protected handleBlockActions(payload: SlackBlockActionsPayload, options?: WebhookOptions): void;
protected handleBlockSuggestion(payload: SlackBlockSuggestionPayload, options?: WebhookOptions): Promise<Response>;
protected optionsLoadResponse(result: OptionsLoadResult): Response;
protected handleViewSubmission(payload: SlackViewSubmissionPayload, options?: WebhookOptions): Promise<Response>;
protected handleViewClosed(payload: SlackViewClosedPayload, options?: WebhookOptions): void;
protected modalResponseToSlack(response: ModalResponse, contextId?: string): SlackModalResponse;
protected convertModalJSX(modal: ModalElement): ModalElement;
/**
* Start Socket Mode connection.
* Creates a SocketModeClient, registers event handlers, and connects.
*/
protected startSocketMode(): Promise<void>;
/**
* Route a socket mode event to the appropriate handler.
*/
protected routeSocketEvent(body: Record<string, unknown>, eventType: string, ack: (response?: Record<string, unknown>) => Promise<void>, options?: WebhookOptions): Promise<void>;
/**
* Start a transient Socket Mode listener for serverless environments.
* The listener maintains a WebSocket for `durationMs`, acks events, and
* forwards them via HTTP POST to the webhook endpoint (or processes directly).
*
* @param options - Webhook options with waitUntil function
* @param durationMs - How long to keep listening (default: 180000ms = 3 minutes)
* @param abortSignal - Optional signal to stop the listener early
* @param webhookUrl - URL to forward socket events to (required for forwarding mode)
*/
startSocketModeListener(options: WebhookOptions, durationMs?: number, abortSignal?: AbortSignal, webhookUrl?: string): Promise<Response>;
/**
* Run the socket mode listener for a specified duration.
*/
protected runSocketModeListener(durationMs: number, abortSignal?: AbortSignal, webhookUrl?: string, options?: WebhookOptions): Promise<void>;
/**
* Forward a socket mode event to the webhook endpoint.
*/
protected forwardSocketEvent(webhookUrl: string, event: SlackForwardedSocketEvent): Promise<void>;
/**
* Disconnect the socket mode client.
* No-op if not connected.
*/
disconnect(): Promise<void>;
protected verifySignature(body: string, timestamp: string | null, signature: string | null): boolean;
/**
* Handle message events from Slack.
* Bot message filtering (isMe) is handled centrally by the Chat class.
*/
protected handleMessageEvent(event: SlackEvent, options?: WebhookOptions): void;
protected handleMessageChanged(event: SlackEvent, _options?: WebhookOptions): void;
/**
* Handle reaction events from Slack (reaction_added, reaction_removed).
*/
protected handleReactionEvent(event: SlackReactionEvent, options?: WebhookOptions): Promise<void>;
/**
* Handle assistant_thread_started events from Slack's Assistants API.
* Fires when a user opens a new assistant thread (DM with the bot).
*/
protected handleAssistantThreadStarted(event: SlackAssistantThreadStartedEvent, options?: WebhookOptions): void;
/**
* Handle assistant_thread_context_changed events from Slack's Assistants API.
* Fires when a user navigates to a different channel with the assistant panel open.
*/
protected handleAssistantContextChanged(event: SlackAssistantContextChangedEvent, options?: WebhookOptions): void;
/**
* Handle app_home_opened events from Slack.
* Fires when a user opens the bot's Home tab.
*/
protected handleAppHomeOpened(event: SlackAppHomeOpenedEvent, options?: WebhookOptions): void;
/**
* Handle member_joined_channel events from Slack.
* Fires when a user (including the bot) joins a channel.
*/
protected handleMemberJoinedChannel(event: SlackMemberJoinedChannelEvent, options?: WebhookOptions): void;
protected handleUserChange(event: SlackUserChangeEvent): Promise<void>;
/**
* Publish a Home tab view for a user.
* Slack API: views.publish
*/
publishHomeView(userId: string, view: Record<string, unknown>): Promise<void>;
/**
* Set suggested prompts for an assistant thread.
* Slack Assistants API: assistant.threads.setSuggestedPrompts
*/
setSuggestedPrompts(channelId: string, threadTs: string, prompts: Array<{
title: string;
message: string;
}>, title?: string): Promise<void>;
/**
* Set status/thinking indicator for an assistant thread.
* Slack Assistants API: assistant.threads.setStatus
*/
setAssistantStatus(channelId: string, threadTs: string, status: string, loadingMessages?: string[]): Promise<void>;
/**
* Set title for an assistant thread (shown in History tab).
* Slack Assistants API: assistant.threads.setTitle
*/
setAssistantTitle(channelId: string, threadTs: string, title: string): Promise<void>;
/**
* Resolve inline user mentions in Slack mrkdwn text.
* Converts <@U123> to <@U123|displayName> so that toAst/extractPlainText
* renders them as @displayName instead of @U123.
*
* @param skipSelfMention - When true, skips the bot's own user ID so that
* mention detection (which looks for @botUserId in the text) continues to
* work. Uses the effective request-scoped bot user ID in multi-workspace
* mode, not only the adapter's default bot user ID. Set to false when
* parsing historical/channel messages where mention detection doesn't
* apply.
*/
protected resolveInlineMentions(text: string, skipSelfMention: boolean): Promise<string>;
/**
* Extract link URLs from a Slack event.
* Uses the `blocks` field (rich_text blocks with link elements) when available,
* falling back to parsing `<url>` patterns from the text field.
*/
protected extractLinks(event: SlackEvent): LinkPreview[];
/**
* Create a LinkPreview for a URL. If the URL points to a Slack message,
* includes a `fetchMessage` callback that fetches and parses the linked message.
*/
protected createLinkPreview(url: string): LinkPreview;
protected parseSlackMessage(event: SlackEvent, threadId: string, options?: {
skipSelfMention?: boolean;
}): Promise<Message<unknown>>;
protected enrichLinks(links: LinkPreview[], messageTs?: string): Promise<LinkPreview[]>;
/**
* Create an Attachment object from a Slack file.
* Includes a fetchData method that uses the bot token for auth.
*/
protected createAttachment(file: {
id?: string;
mimetype?: string;
url_private?: string;
name?: string;
size?: number;
original_w?: number;
original_h?: number;
}, teamId?: string): Attachment;
protected fetchSlackFile(url: string, token: string): Promise<Buffer>;
rehydrateAttachment(attachment: Attachment): Attachment;
/**
* Resolve @name mentions in text to Slack <@USER_ID> format using the
* reverse user cache. When multiple users share a display name, prefers
* the one who is a participant in the given thread.
*/
protected resolveOutgoingMentions(text: string, threadId: string): Promise<string>;
/**
* Pre-process an outgoing message to resolve @name mentions before rendering.
*/
protected resolveMessageMentions(message: AdapterPostableMessage, threadId: string): Promise<AdapterPostableMessage>;
postMessage(threadId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
postEphemeral(threadId: string, userId: string, _message: AdapterPostableMessage): Promise<EphemeralMessage>;
scheduleMessage(threadId: string, _message: AdapterPostableMessage, options: {
postAt: Date;
}): Promise<ScheduledMessage>;
openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
viewId: string;
}>;
updateModal(viewId: string, modal: ModalElement): Promise<{
viewId: string;
}>;
/**
* Upload files to Slack and share them to a channel.
* Returns the file IDs of uploaded files.
*/
protected uploadFiles(files: FileUpload[], channel: string, threadTs?: string): Promise<string[]>;
editMessage(threadId: string, messageId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
postObject(threadId: string, kind: string, data: unknown): Promise<RawMessage<unknown>>;
editObject(threadId: string, messageId: string, kind: string, data: unknown): Promise<RawMessage<unknown>>;
protected renderPlanFallbackText(plan: PlanModel): string;
protected planToBlockKit(plan: PlanModel): unknown[];
protected planContentToPlainText(content: PlanContent | undefined): string;
protected planContentToRichText(content: PlanContent | undefined): {
type: "rich_text";
elements: unknown[];
} | undefined;
deleteMessage(threadId: string, messageId: string): Promise<void>;
addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
/**
* Show typing indicator with optional custom status.
*
* When status is provided, uses Slack's assistant.threads.setStatus API
* to show custom loading text (requires Agents & AI Apps feature and assistant:write scope).
* The status auto-clears when a message is posted to the thread.
*
* When status is not provided, defaults to "Typing..." with default loading messages.
*
* @param threadId - The thread to show the indicator in
* @param status - Optional custom status message (e.g., "Searching documents...")
*/
startTyping(threadId: string, status?: string): Promise<void>;
/**
* Stream a message using Slack's native streaming API.
*
* Consumes an async iterable of text chunks and/or structured StreamChunk
* objects (task_update, plan_update, markdown_text) and streams them to Slack.
*
* Plain strings are rendered through StreamingMarkdownRenderer for safe
* incremental markdown. StreamChunk objects are passed directly to Slack's
* streaming API as chunk payloads, enabling native task progress cards
* and plan displays in the Slack AI Assistant UI.
*
* Requires `recipientUserId` and `recipientTeamId` in options.
*/
stream(threadId: string, textStream: AsyncIterable<string | StreamChunk>, options?: StreamOptions): Promise<RawMessage<unknown>>;
/**
* Open a direct message conversation with a user.
* Returns a thread ID that can be used to post messages.
*/
openDM(userId: string): Promise<string>;
fetchMessages(threadId: string, options?: FetchOptions): Promise<FetchResult<unknown>>;
/**
* Fetch messages in forward direction (oldest first, efficient).
* Uses native Slack cursor pagination.
*/
protected fetchMessagesForward(channel: string, threadTs: string, threadId: string, limit: number, cursor?: string): Promise<FetchResult<unknown>>;
/**
* Fetch messages in backward direction (most recent first).
*
* Slack's API returns oldest-first, so for backward direction we:
* 1. Use `latest` parameter to fetch messages before a timestamp (cursor)
* 2. Fetch up to 1000 messages (API limit) and take the last N
* 3. Return messages in chronological order (oldest first within the page)
*
* Note: For very large threads (>1000 messages), the first backward call
* may not return the absolute most recent messages. This is a Slack API limitation.
*/
protected fetchMessagesBackward(channel: string, threadTs: string, threadId: string, limit: number, cursor?: string): Promise<FetchResult<unknown>>;
fetchThread(threadId: string): Promise<ThreadInfo>;
/**
* Fetch a single message by ID (timestamp).
*/
fetchMessage(threadId: string, messageId: string): Promise<Message<unknown> | null>;
encodeThreadId(platformData: SlackThreadId): string;
/**
* Check if a thread is a direct message conversation.
* Slack DM channel IDs start with 'D'.
*/
isDM(threadId: string): boolean;
/**
* Get the visibility scope of a channel containing the thread.
*
* - `external`: Slack Connect channel shared with external organizations
* - `private`: Private channel (starts with G) or DM (starts with D)
* - `workspace`: Public channel visible to all workspace members
* - `unknown`: Visibility cannot be determined (not yet cached)
*/
getChannelVisibility(threadId: string): ChannelVisibility;
decodeThreadId(threadId: string): SlackThreadId;
parseMessage(raw: SlackEvent): Message<unknown>;
/**
* Synchronous message parsing without user lookup.
* Used for parseMessage interface - falls back to user ID for username.
*/
protected parseSlackMessageSync(event: SlackEvent, threadId: string): Message<unknown>;
/**
* Derive channel ID from a Slack thread ID.
* Slack thread IDs are "slack:CHANNEL:THREAD_TS", channel ID is "slack:CHANNEL".
*/
channelIdFromThreadId(threadId: string): string;
/**
* Fetch channel-level messages (conversations.history, not thread replies).
*/
fetchChannelMessages(channelId: string, options?: FetchOptions): Promise<FetchResult<unknown>>;
protected fetchChannelMessagesForward(channel: string, limit: number, cursor?: string): Promise<FetchResult<unknown>>;
protected fetchChannelMessagesBackward(channel: string, limit: number, cursor?: string): Promise<FetchResult<unknown>>;
/**
* List threads in a Slack channel.
* Fetches channel history and filters for messages with replies.
*/
listThreads(channelId: string, options?: ListThreadsOptions): Promise<ListThreadsResult<unknown>>;
/**
* Fetch Slack channel info/metadata.
*/
fetchChannelInfo(channelId: string): Promise<ChannelInfo>;
/**
* Post a top-level message to a channel (not in a thread).
*/
postChannelMessage(channelId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
renderFormatted(content: FormattedContent): string;
/**
* Check if a Slack event is from this bot.
*
* Slack messages can come from:
* - User messages: have `user` field (U_xxx format)
* - Bot messages: have `bot_id` field (B_xxx format)
*
* We check both because:
* - _botUserId is the user ID (U_xxx) - matches event.user
* - _botId is the bot ID (B_xxx) - matches event.bot_id
*/
protected isMessageFromSelf(event: SlackEvent): boolean;
protected handleSlackError(error: unknown): never;
/**
* Encode response_url and userId into messageId for ephemeral messages.
* This allows edit/delete operations to work via response_url.
*/
protected encodeEphemeralMessageId(messageTs: string, responseUrl: string, userId: string): string;
/**
* Decode ephemeral messageId to extract messageTs, responseUrl, and userId.
* Returns null if the messageId is not an ephemeral encoding.
*/
protected decodeEphemeralMessageId(messageId: string): {
messageTs: string;
responseUrl: string;
userId: string;
} | null;
/**
* Send a request to Slack's response_url to modify an ephemeral message.
*/
protected sendToResponseUrl(responseUrl: string, action: "replace" | "delete", options?: {
message?: AdapterPostableMessage;
threadTs?: string;
}): Promise<Record<string, unknown>>;
}
declare function createSlackAdapter(config?: SlackAdapterConfig): SlackAdapter;
export { SlackAdapter, type SlackAdapterConfig, type SlackAdapterMode, type SlackBotToken, type SlackEvent, SlackFormatConverter, type SlackInstallation, SlackFormatConverter as SlackMarkdownConverter, type SlackOAuthCallbackOptions, type SlackReactionEvent, type SlackThreadId, cardToBlockKit, cardToFallbackText, createSlackAdapter };