/** * Slack Integration Types * * Types for Slack bot integration enabling mobile communication * with the Crewly orchestrator. * * @module types/slack */ /** * How inbound Slack events reach this instance. * * - `socket` — the self-hosted app: Bolt opens a Socket Mode connection with * the app token (the original transport). * - `cloud` — Crewly Cloud owns the Slack app, receives events over HTTP and * pushes them to this instance as `slack_event` relay messages. No socket * is opened; outbound calls still go straight to the Slack Web API. */ export type SlackTransport = 'socket' | 'cloud'; /** * Slack bot configuration */ export interface SlackConfig { /** Bot OAuth token (xoxb-...) */ botToken: string; /** App-level token for Socket Mode (xapp-...). Empty for the cloud transport. */ appToken: string; /** Signing secret for request verification. Empty for the cloud transport. */ signingSecret: string; /** Channel ID for orchestrator notifications */ defaultChannelId?: string; /** Allowed user IDs (empty = all users) */ allowedUserIds?: string[]; /** Enable Socket Mode for real-time events */ socketMode: boolean; /** Inbound transport; defaults to `socket` */ transport?: SlackTransport; /** Bot user id (`U…`) when already known (cloud config) — skips `auth.test` */ botUserId?: string; } /** * Raw Slack event object as delivered by the Events API / Socket Mode for * `message` and `app_mention`. Only the fields the inbound handler reads. */ export interface SlackRawInboundEvent { /** `message` | `app_mention` (anything else is ignored) */ type: string; ts?: string; text?: string; user?: string; channel?: string; thread_ts?: string; team?: string; event_ts?: string; /** Message subtype (`file_share`, `bot_message`, `message_changed`, …) */ subtype?: string; /** `im` | `channel` | `group` | `mpim` on Events API `message` events */ channel_type?: string; /** Set when a bot posted the message */ bot_id?: string; files?: SlackFile[]; /** * Legacy attachments. A message the user SHARED into this one (Slack's * "Share message" / forward) arrives here, not in `text` (`is_share` / * `is_msg_unfurl`, with the original's `text`, author and permalink). */ attachments?: SlackRawAttachment[]; /** `reaction_added`: emoji name (no colons) */ reaction?: string; /** `reaction_added`: what was reacted to */ item?: { type?: string; channel?: string; ts?: string; }; } /** The fields of a legacy attachment Crewly reads (a shared message or a link preview). */ export interface SlackRawAttachment { is_share?: boolean; is_msg_unfurl?: boolean; text?: string; fallback?: string; pretext?: string; title?: string; title_link?: string; author_name?: string; author_subname?: string; from_url?: string; original_url?: string; image_url?: string; footer?: string; } /** Provenance attached to an inbound event by the transport that received it. */ export interface SlackInboundMeta { source: SlackTransport; /** Slack `event_id` (cloud transport) */ eventId?: string; /** App the event was delivered to (cloud transport) */ apiAppId?: string; /** Set when the event came through a per-agent app */ agentSession?: string; /** Set when one of the account's agents (possibly on another machine) wrote the message */ authorAgentSession?: string; authorDisplayName?: string; /** Agent whose own app delivered this channel copy (cloud transport; see SlackIncomingMessage.receivedVia) */ receivedVia?: string; /** Account agents @-mentioned, on any machine (see SlackIncomingMessage.mentionedAgentSessions) */ mentionedAgentSessions?: string[]; /** Who is in the channel across machines, and who is awake (cloud transport) */ room?: SlackRoomPresence; /** The agent this message was handed to by whoever routed it (cloud transport) */ handoffTo?: string; } /** One agent in a Slack room, on any machine of the account. */ export interface SlackRoomMember { /** As Cloud knows it (an orchestrator carries `@`) */ agentSession: string; displayName: string; instanceId: string; deviceName: string; /** Running now, as its machine last reported */ awake: boolean; /** * Whether its machine is proven to be in the room (a team channel, or a * copy that came through one of its agents' own apps). Cloud sends a * member of an unproven machine as asleep. Absent from older Cloud builds, * or when Cloud had proof for no machine. */ verified?: boolean; /** Leads a team on its machine (Cloud prefers a machine with an awake leader as responder) */ leader?: boolean; } /** Which rule Cloud used to pick a channel message's machines (auth ≥ 1.10.4). */ export type SlackRoomDeliveryRule = 'single-owner' | 'all-room-machines' | 'uncertain' | 'nobody-awake' | 'addressed' | 'thread' | 'agent-author' | 'not-a-room' | 'owner-unreachable'; /** How Cloud routed one room message — every machine acts on the same facts. */ export interface SlackRoomDelivery { /** The one machine that takes it (Cloud pushes it first); null when Cloud named none */ owner: string | null; /** Every machine Cloud pushed this message to */ targets: string[]; rule: SlackRoomDeliveryRule; /** One sentence, for logs */ reason: string; } /** * Room roster plus who wakes up when nobody in it is awake — computed by * Cloud from every machine's heartbeat and sent with each channel message. */ export interface SlackRoomPresence { members: SlackRoomMember[]; /** Only when nobody is awake: the one machine that wakes someone, and whom */ fallback?: { instanceId: string; agentSession: string; kind: 'team-leader' | 'orchestrator'; }; /** * The machine whose team channel this room is (from heartbeat * `teams[].channelId`). When set, only that machine's awake agents take * an un-@'d message (2026-10-03). Optional: older Cloud builds omit it. */ home?: { instanceId: string; }; /** How Cloud routed this message (crewly-services, 2026-10-04). Optional: older Cloud builds omit it. */ delivery?: SlackRoomDelivery; } /** * One entry of Cloud's Slack routing log (`GET /api/cloud/slack/routing-decisions`, * kept 14 days): where a channel message was pushed, and why. */ export interface CloudRoutingDecision { key: string; channelId: string; ts: string; threadTs: string | null; rule: SlackRoomDeliveryRule; owner: string | null; targets: string[]; reason: string; memberInstances?: string[]; verifiedInstances?: string[]; /** Per instance: `pushed`, or `queued` with the error */ deliveries?: Record; ownerUnreachable?: boolean; createdAt: string; } /** A request to put this machine's agents in an existing Slack channel (Cloud-pushed). */ export interface SlackRoomJoinRequest { slackChannelId: string; slackChannelName: string; /** Agent sessions on THIS machine to add as members */ sessions: string[]; requestedByInstanceId: string; } /** What became of a room-join request, reported by the machine that was asked (crewly#1430). */ export interface SlackRoomJoinResult { slackChannelId: string; slackChannelName: string; /** The machine that did (or could not do) the joining */ instanceId: string; ok: boolean; joined: string[]; rejected: string[]; /** Why not, when `ok` is false */ error?: string; } /** * `data` of a `slack_event` relay message pushed by Crewly Cloud (contract: * Slack v3 events). `event` is the raw Slack event object. */ export interface SlackCloudEventEnvelope { eventId: string; slackTeamId: string; apiAppId: string; /** `master` = the account's workspace app; `agent` = a per-agent app */ source: 'master' | 'agent'; agentSession?: string; /** The account's agent that wrote the message (agent-to-agent @-mention), when any */ authorAgentSession?: string; authorDisplayName?: string; /** Session names of the account's agents @-mentioned in the text */ mentionedAgentSessions?: string[]; /** Channel roster and presence across machines (channel messages, Cloud ≥ auth 1.5.12) */ room?: SlackRoomPresence; /** Set on a hand-off: the message is re-delivered, addressed to this agent */ handoffTo?: string; /** * Set on a room-join request (`event.type === 'crewly_room_join'`): another * machine's orchestrator put some of this machine's agents in a Slack channel. * Not a message — never routed as one. */ roomJoin?: SlackRoomJoinRequest; /** Set on a room-join result (`event.type === 'crewly_room_join_result'`): what the asked machine did. Not a message. */ roomJoinResult?: SlackRoomJoinResult; event: SlackRawInboundEvent; /** * `block_actions` only (Cloud ≥ auth 1.9.0): the full interactive payload * as Slack sent it — Cloud verified the signature and routed it here by * the button value's `i` (instance id). */ interaction?: unknown; receivedAt: string; } /** A Slack interactive payload handed to listeners (`interaction` event). */ export interface SlackInteractionEvent { /** The `block_actions` payload */ payload: unknown; /** How it arrived */ source: SlackTransport | 'http'; eventId?: string; } /** A reaction on a message (`reaction` event). */ export interface SlackReactionEvent { user: string; reaction: string; channelId: string; messageTs: string; source: SlackTransport; } /** The master workspace half of `GET /api/cloud/slack/config`. */ export interface SlackCloudWorkspaceConfig { slackTeamId: string; slackTeamName: string; botUserId: string; botToken: string; appId: string; /** Slack user id of the installer (the owner); invited into every channel Crewly creates. */ installedBy?: string; } /** One provisioned + installed per-agent app from `GET /api/cloud/slack/config`. */ export interface SlackCloudAgentConfig { agentSession: string; teamId?: string; botUserId: string; botToken: string; appId: string; displayName: string; } /** `GET /api/cloud/slack/config` payload (the Cloud-owned Slack setup). */ export interface SlackCloudConfig { workspace: SlackCloudWorkspaceConfig; agents: SlackCloudAgentConfig[]; transport: 'cloud'; } /** On-disk cache of {@link SlackCloudConfig} (`~/.crewly/slack-cloud-config.json`, 0600). */ export interface SlackCloudConfigFile { version: 1; fetchedAt: string; config: SlackCloudConfig; } /** Per-instance Slack settings (`~/.crewly/slack-instance.json`). */ export interface SlackInstanceSettingsFile { version: 1; /** This instance receives DMs / unmapped channels for the account */ primary: boolean; /** Slack team id this instance serves when the account has several workspaces */ slackTeamId?: string; } /** One workspace on the account, as listed by Cloud (never a token). */ export interface SlackCloudWorkspaceSummary { slackTeamId: string; slackTeamName: string; botUserId?: string; installedAt?: string; agentIdentities?: number; } /** One team as reported to the Cloud instance registry. */ export interface SlackRegistryTeam { teamId: string; name: string; /** Slack channel id when the team has one */ channelId?: string; /** Member session names (orchestrator excluded) */ agents: string[]; /** The team leader's session — whom Cloud wakes when nobody in the team channel is awake */ leader?: string; } /** Body of `PUT /api/cloud/slack/instances/:instanceId`. */ export interface SlackInstanceRegistryPayload { deviceName: string; relayQueueId: string; primary?: boolean; /** Workspace this instance serves (omitted = keep Cloud's binding) */ slackTeamId?: string; teams: SlackRegistryTeam[]; /** Ad-hoc Slack rooms this instance's agents are in (1.20.88+) */ rooms?: Array<{ channelId: string; agents: string[]; }>; /** Agents running right now (1.20.88+) */ awakeAgents?: string[]; /** * Every agent on this machine with its display name (Cloud Talk): Cloud * lists agents that have no messages yet. Local session names. */ roster?: Array<{ agentSession: string; displayName?: string; role?: string; teamName?: string; }>; /** What this machine handles, e.g. `talk_message` */ capabilities?: string[]; crewlyVersion: string; } /** Body of `POST /api/cloud/slack/agents/sync`. */ export interface SlackAgentsSyncPayload { teams: Array<{ teamId: string; name: string; agents: Array<{ agentSession: string; displayName: string; avatar?: string; }>; }>; /** Delete the apps of agents that left one of these teams. */ prune?: boolean; /** Slack workspace this instance serves — its config token creates the apps. */ slackTeamId?: string; } /** One agent still waiting for its one-time install click. */ export interface SlackPendingInstall { agentSession: string; url: string; } /** Response of `POST /api/cloud/slack/agents/sync`. */ export interface SlackAgentsSyncResult { installUrls: SlackPendingInstall[]; /** Per-agent failures; a non-empty list means the roster is only partly on Cloud. */ errors?: { agentSession: string; code: string; error: string; }[]; } /** * Slack user information */ export interface SlackUser { id: string; name: string; realName?: string; email?: string; isAdmin?: boolean; isOwner?: boolean; teamId: string; } /** * Slack channel information */ export interface SlackChannel { id: string; name: string; isPrivate: boolean; isIm: boolean; isMpim: boolean; } /** * Slack file object from event payload. * Represents a file attached to a Slack message, including download URLs * and optional thumbnail/dimension metadata for images. */ export interface SlackFile { /** Slack file ID (e.g., F0123ABC456) */ id: string; /** Original file name */ name: string; /** MIME type (e.g., image/png) */ mimetype: string; /** Slack file type identifier (e.g., png, jpg) */ filetype: string; /** File size in bytes */ size: number; /** Private URL requiring Bearer token to download */ url_private: string; /** Private download URL requiring Bearer token */ url_private_download: string; /** Optional 360px thumbnail URL */ thumb_360?: string; /** Original image width in pixels */ original_w?: number; /** Original image height in pixels */ original_h?: number; /** Permalink to view the file in Slack */ permalink: string; /** Slack file subtype (`slack_audio` / `slack_video` for clips recorded in Slack) */ subtype?: string; /** Slack's transcript of a recorded clip, when it made one */ transcription?: { status?: string; locale?: string; preview?: { content?: string; has_more?: boolean; }; }; } /** * Downloaded image info with local file path. * Created after successfully downloading a SlackFile to the local filesystem. */ export interface SlackImageInfo { /** Slack file ID */ id: string; /** Original file name */ name: string; /** MIME type of the image */ mimetype: string; /** Absolute path to the downloaded file on disk */ localPath: string; /** Image width in pixels (from Slack metadata) */ width?: number; /** Image height in pixels (from Slack metadata) */ height?: number; /** Permalink to the original file in Slack */ permalink: string; } /** * Downloaded non-image file info with local file path. * Created after successfully downloading a non-image SlackFile to the local filesystem. */ export interface SlackFileInfo { /** Slack file ID */ id: string; /** Original file name */ name: string; /** MIME type of the file */ mimetype: string; /** Absolute path to the downloaded file on disk */ localPath: string; /** File size in bytes */ size: number; /** Permalink to the original file in Slack */ permalink: string; /** Extracted text content (for PDFs and text-based files) */ extractedText?: string; } /** One message of the fetched Slack context. */ export interface SlackContextMessage { /** Slack message timestamp */ ts: string; /** Author user id (bot user id for a bot that posted with its own token) */ userId?: string; /** Slack `bot_id` when a bot wrote it */ botId?: string; /** Whether a bot / app wrote it */ isBot: boolean; /** Display name of the author, resolved */ authorName: string; /** * True when the name comes from a per-message `username` override — how an * agent without its own Slack app posts through the workspace bot. */ usernameOverride?: boolean; /** Message text with `<@U…>` mentions turned into `@name` */ text: string; /** User / bot ids the message @-mentioned (`<@U…>`), in order */ mentionIds?: string[]; } /** What {@link SlackThreadContextService.getContext} returns. */ export interface SlackThreadContext { /** `thread` = `conversations.replies`; `channel` = recent `conversations.history` */ kind: 'thread' | 'channel'; /** Slack channel id */ channelId: string; /** Thread root ts, for a thread */ threadTs?: string; /** Messages before the triggering one, oldest first (already capped by count) */ messages: SlackContextMessage[]; /** How many earlier messages Slack returned before the count cap */ totalBefore: number; } /** * Incoming Slack message */ export interface SlackIncomingMessage { id: string; type: 'message' | 'app_mention' | 'command'; text: string; userId: string; channelId: string; threadTs?: string; ts: string; teamId: string; eventTs: string; user?: SlackUser; channel?: SlackChannel; /** Raw Slack file objects from the event payload */ files?: SlackFile[]; /** Downloaded image info with local paths (populated after download) */ images?: SlackImageInfo[]; /** Whether the message contains image attachments */ hasImages?: boolean; /** Downloaded non-image file info with local paths (populated after download) */ attachments?: SlackFileInfo[]; /** * Slack files LINKED in the text (a permalink a colleague pasted), fetched * like attachments so a hand-off carries its file. Kept apart from * `attachments`, which counts against `files` for the "could not be read" note. */ linkedFiles?: SlackFileInfo[]; /** Linked Slack files that could not be fetched, with the reason (shown to the agent). */ linkedFileFailures?: Array<{ link: string; reason: string; }>; /** * Attachments that could not be downloaded, with the reason. Shown to the * agent only (never posted into the Slack thread): the agent explains in its own words. */ fileFailures?: Array<{ name: string; reason: string; }>; /** Whether the message has any file attachments (images or other) */ hasFiles?: boolean; /** Transport that delivered the event (`socket` when omitted) */ source?: SlackTransport; /** Slack `event_id` when known (cloud transport) */ eventId?: string; /** Agent whose per-agent app received the event (cloud transport) */ agentSession?: string; /** Agent (on any machine of the account) that wrote the message, when a bot did */ authorAgentSession?: string; authorDisplayName?: string; /** * The agent whose own Slack app delivered this copy of a *channel* * message. Slack gives every app in a channel its own copy, so this proves * the agent is in the room. Kept apart from `agentSession`, which means * "a DM to this agent" and routes the message to it. */ receivedVia?: string; /** * Every agent of the account the message @-mentions, on any machine * (cloud transport). The local @-resolver only knows this machine's * agents; without this, an @ of an agent elsewhere read as "nobody * addressed" and the message went to the agents awake here. */ mentionedAgentSessions?: string[]; /** Who is in the channel across machines, and who is awake (cloud transport) */ room?: SlackRoomPresence; /** * The local agent this message was handed to: the room's router (its * leader, or an orchestrator) decided it should answer. Delivered as if * that agent had been @'d, even though the message was seen before. */ handoffTo?: string; /** * What the Slack thread (or, for a top-level @-mention, the channel) said * before this message, read from Slack at delivery time. Includes posts by * agents on other machines, which Cloud never forwards here (own-bot loop * guard). Prompt context only — never recorded locally. Resolves to null * when not applicable or unreadable, and never rejects. A promise so the * read runs alongside routing (eyes, placeholders) and is awaited only * right before the prompt is built. */ threadContext?: Promise; } /** * Outgoing Slack message */ export interface SlackOutgoingMessage { channelId: string; text: string; threadTs?: string; blocks?: SlackBlock[]; attachments?: SlackAttachment[]; unfurlLinks?: boolean; unfurlMedia?: boolean; /** * Per-message display name (requires the `chat:write.customize` scope). * Used by Slack team channels so each agent posts under its own name. */ username?: string; /** Per-message icon as a Slack emoji name, e.g. `:robot_face:`. */ iconEmoji?: string; /** Per-message icon as an image URL. Ignored when `iconEmoji` is set. */ iconUrl?: string; /** * Post as a different bot user: the agent's own Slack app token (Slack * agent identities). When set, `username`/icon are ignored — the message * already carries a real identity. */ botToken?: string; /** * Skip the best-effort mirror of this reply into the `slack--` * chat-v2 channel. Set by callers that already persisted the message in * chat-v2 under a different channel (team-channel outbound mirror), so the * same reply is not stored twice. */ skipChatV2Mirror?: boolean; /** The agent session this post is on behalf of — only for log attribution. */ senderSession?: string; /** * The caller is probing candidate channels and expects some to be * unreachable (notification fallback). Such failures do not count * toward the outbound reachability health. */ reachabilityProbe?: boolean; /** * The post is harness chrome, not an answer (a "working on it" * placeholder, the watchdog's own note, a routing hint). The * unanswered-owner-message watchdog does not take it as the reply. */ notAnAnswer?: boolean; } /** * One Crewly team bound to one Slack channel (and the chat-v2 huddle that * backs it). Persisted in `~/.crewly/slack-team-channels.json`. */ export interface SlackTeamChannelMapping { /** Crewly team id */ teamId: string; /** Slack channel id (C…) */ slackChannelId: string; /** Slack channel name without the leading `#`, as last known */ slackChannelName: string; /** chat-v2 huddle id that receives the channel's messages */ chatChannelId: string; /** ISO timestamp the mapping was created */ createdAt: string; /** True when Crewly created the Slack channel (vs. linked an existing one) */ autoCreated: boolean; /** * The channel name Crewly last derived from the team name. * * Kept so a later rename can tell "the team was renamed" (the derived name * changed) from "the owner renamed the channel in Slack" (the live name no * longer matches what we derived) — only the first should move the channel. * Absent on mappings made before 2026-09-20; `slackChannelName` stands in, * which is what those were derived from anyway. */ derivedName?: string; /** Ad-hoc channels only: the local agents @'d there so far (the huddle roster). */ members?: string[]; /** * Ad-hoc channels only: agents the owner removed in Crewly. They are not * added back automatically (by a copy arriving through their own app, or * by the periodic Slack member read) — only an explicit add or an @-mention * brings them back. Matters when Slack refused to take their bot out. */ excluded?: string[]; /** * Ad-hoc channels only: when a member agent's bot asked, in the channel, * for the Crewly app to be added. Neither the agent bots (no `*:read` * scopes) nor the workspace bot (not a member of a private channel) can * read its name, so it shows as its id until the app is in it. Asked once. */ crewlyAppHintAt?: string; /** * True once the workspace owner was invited into this channel (or found * already in it). A channel Crewly creates is invisible to people until * they are in it; an invite that could not happen at creation — no owner * id known yet at boot — is retried until this is set, and never after, * so a channel the owner chose to leave stays left. */ ownerInvited?: boolean; /** * True once Slack answered `is_archived` for this channel. The boot rejoin * still tries it (an unarchived channel clears the flag) but says so only * the first time, and the owner is not invited into it meanwhile — an * archived channel used to produce four warnings on every restart. */ slackArchived?: boolean; } /** * On-disk shape of the team-channel mapping store. */ export interface SlackTeamChannelsFile { version: 1; /** Create a Slack channel + huddle automatically for every new team */ autoCreate: boolean; /** Optional prefix for auto-created channel names (e.g. `crew-`) */ channelPrefix: string; mappings: SlackTeamChannelMapping[]; } /** * Minimal Slack channel descriptor returned by the conversations API helpers. */ export interface SlackChannelInfo { id: string; name: string; isArchived: boolean; isPrivate: boolean; } /** * Slack Block Kit block types */ export type SlackBlockType = 'section' | 'divider' | 'header' | 'context' | 'actions' | 'image'; /** * Slack Block Kit block */ export interface SlackBlock { type: SlackBlockType; text?: SlackTextObject; fields?: SlackTextObject[]; accessory?: SlackAccessory; elements?: SlackElement[]; block_id?: string; } /** * Slack text object (mrkdwn or plain_text) */ export interface SlackTextObject { type: 'mrkdwn' | 'plain_text'; text: string; emoji?: boolean; verbatim?: boolean; } /** * Slack accessory element */ export interface SlackAccessory { type: 'button' | 'image' | 'overflow' | 'datepicker' | 'static_select'; action_id?: string; text?: SlackTextObject; value?: string; url?: string; image_url?: string; alt_text?: string; } /** * Slack interactive element */ export interface SlackElement { type: string; action_id?: string; text?: SlackTextObject; value?: string; [key: string]: unknown; } /** * Slack attachment (legacy but still useful) */ export interface SlackAttachment { color?: string; fallback?: string; title?: string; title_link?: string; text?: string; fields?: SlackAttachmentField[]; footer?: string; footer_icon?: string; ts?: number; } /** * Slack attachment field */ export interface SlackAttachmentField { title: string; value: string; short?: boolean; } /** * Slack slash command payload */ export interface SlackSlashCommand { command: string; text: string; responseUrl: string; triggerId: string; userId: string; userName: string; channelId: string; channelName: string; teamId: string; teamDomain: string; } /** * Slack event types we handle */ export type SlackEventType = 'message' | 'app_mention' | 'app_home_opened' | 'member_joined_channel' | 'reaction_added'; /** * Slack event wrapper */ export interface SlackEvent { type: SlackEventType; eventTs: string; user?: string; channel?: string; ts?: string; payload: T; } /** * Message routing destination */ export type MessageDestination = { type: 'orchestrator'; } | { type: 'agent'; agentId: string; } | { type: 'team'; teamId: string; } | { type: 'project'; projectId: string; }; /** * Parsed command from Slack message */ export interface ParsedSlackCommand { intent: SlackCommandIntent; target?: MessageDestination; parameters: Record; rawText: string; } /** * Recognized command intents */ export type SlackCommandIntent = 'status' | 'assign' | 'create_task' | 'create_project' | 'list_projects' | 'list_teams' | 'list_agents' | 'pause' | 'resume' | 'help' | 'conversation' | 'unknown'; /** * Slack notification types */ export type SlackNotificationType = 'task_completed' | 'task_failed' | 'task_blocked' | 'agent_error' | 'agent_question' | 'project_update' | 'daily_summary' | 'okr_reminder' | 'alert'; /** * Slack notification payload */ export interface SlackNotification { type: SlackNotificationType; title: string; message: string; urgency: 'low' | 'normal' | 'high' | 'critical'; metadata?: { projectId?: string; teamId?: string; agentId?: string; taskId?: string; missionId?: string; offTrack?: number; atRisk?: number; errorDetails?: string; }; timestamp: string; /** Reply in this Slack thread instead of top-level */ threadTs?: string; /** Post to this channel (fallback: defaultChannelId) */ channelId?: string; } /** * Conversation thread context */ export interface SlackConversationContext { threadTs: string; channelId: string; userId: string; conversationId: string; startedAt: string; lastActivityAt: string; messageCount: number; /** * Per-message fields of the inbound message being handled. Set on the * bridge's own copy of the context (never on the shared per-thread one) so * the conversation log keeps the Slack ids of each message * (specs/unified-conversations-cloud-store.md §A.3 G5). */ /** Slack ts of the message being handled */ messageTs?: string; /** Slack user who wrote the message being handled (`userId` is the thread starter) */ messageUserId?: string; /** Slack workspace (team) id of the message being handled */ slackTeamId?: string; } /** * Slack service status */ export interface SlackServiceStatus { connected: boolean; socketMode: boolean; lastEventAt?: string; lastErrorAt?: string; lastError?: string; messagesSent: number; messagesReceived: number; /** * True when the Slack integration is offline because Slack itself refused * the credentials (or could not be reached) — the backend keeps running * without Slack. Cleared on the next successful connect. */ degraded?: boolean; /** Why the integration is degraded, e.g. "invalid_auth" or "ECONNREFUSED". */ degradedReason?: string; /** * Outbound posts the connected bot could not deliver because it cannot see * the channel (`channel_not_found` / `not_in_channel`), counted since the * last successful post. Present only while such failures are outstanding. * Reaching the threshold sets `degraded` (#753). */ deliveryFailures?: SlackDeliveryFailureSummary; } /** * Outstanding outbound reachability failures (see * {@link SlackServiceStatus.deliveryFailures}). */ export interface SlackDeliveryFailureSummary { /** Consecutive failing posts since the last success. */ consecutive: number; /** Slack error code of the latest failure. */ code: string; /** Distinct channel ids that failed (most recent last, capped). */ channels: string[]; /** When the first failure of this run happened (ISO). */ since: string; /** When the latest failure happened (ISO). */ lastAt: string; } /** * Valid notification urgency levels */ export declare const NOTIFICATION_URGENCIES: readonly ["low", "normal", "high", "critical"]; /** * Command intent patterns for parsing */ export declare const COMMAND_PATTERNS: Record; /** * Check if user is allowed to interact with the bot * * @param userId - Slack user ID to check * @param config - Slack configuration with allowed users * @returns True if user is allowed to interact */ export declare function isUserAllowed(userId: string, config: SlackConfig): boolean; /** * Parse command intent from message text * * @param text - Message text to parse * @returns Detected command intent */ export declare function parseCommandIntent(text: string): SlackCommandIntent; /** * One agent's Slack identity as cached by the OSS install * (`~/.crewly/slack-agent-identities.json`, mode 0600). Mirrors the Cloud's * `SlackAgentView` plus local bookkeeping. */ export interface SlackAgentIdentityRecord { agentSession: string; displayName: string; appId: string; status: 'pending_install' | 'installed' | 'error'; botUserId?: string; teamId?: string; /** The agent's own bot token — never leaves this machine except to Slack. */ botToken?: string; installUrl?: string; /** Installed, but the token predates a scope the app now needs; `installUrl` re-authorises it. */ reinstall?: boolean; error?: string; /** Slack channel ids where the install link has been announced. */ announcedIn: string[]; /** Slack channel ids the bot user has been invited into. */ invitedTo: string[]; updatedAt: string; } /** On-disk shape of the identity cache. */ export interface SlackAgentIdentitiesFile { version: 1; identities: SlackAgentIdentityRecord[]; } //# sourceMappingURL=slack.types.d.ts.map