npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cuylabs/agent-channel-slack

v4.0.0

Published

Slack channel adapter for @cuylabs/agent-core — bridges agent-core turn sources to Slack via @slack/bolt

Readme

@cuylabs/agent-channel-slack

Slack channel integration for @cuylabs/agent-core.

This package now supports two real deployment modes:

  1. Direct Slack mode using @slack/bolt
  2. M365-backed Slack mode using Azure Bot Service + @microsoft/agents-hosting

That split is intentional. Some teams want the native Slack surface and no Azure dependency. Others want Slack to share the same Azure Bot registration and hosting model they already use for Teams.

The direct mode is built on the official Slack TypeScript SDKs:

  • @slack/bolt for inbound requests, event routing, and OAuth install flows
  • @slack/web-api for outbound Slack API calls, progressive response updates, and native chatStream streaming

Choose a Mode

| Mode | Primary API | Transport | Best when | | ----------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- | | Direct Slack | mountSlackAgentApp() / mountSlackAgent() / createSlackChannelAdapter() | @slack/bolt | You want the native Slack app model and direct Slack credentials | | M365-backed Slack | mountM365SlackAgent() / createM365SlackChannelAdapter() | Azure Bot Service Slack channel -> M365 CloudAdapter | You already run on the M365 stack and want Slack to share that hosting/auth model |

Stack Position

Direct Slack

Slack
  -> @slack/bolt
    -> @cuylabs/agent-channel-slack
      -> @cuylabs/agent-core

M365-backed Slack

Slack
  -> Azure Bot Service Slack channel
    -> @microsoft/agents-hosting
      -> @cuylabs/agent-channel-slack
        -> @cuylabs/agent-channel-m365
          -> @cuylabs/agent-core

Install

Direct Slack mode

pnpm add @cuylabs/agent-channel-slack @slack/bolt @slack/web-api express

M365-backed Slack mode

pnpm add @cuylabs/agent-channel-slack @cuylabs/agent-channel-m365 @microsoft/agents-hosting @microsoft/agents-activity express

Prefer the mode-specific subpaths for new code:

import { mountSlackAgent } from "@cuylabs/agent-channel-slack/direct";
import { mountM365SlackAgent } from "@cuylabs/agent-channel-slack/m365";

The root export is shared-only. It exposes transport-neutral Slack types, parsers, formatting, session helpers, ambient turn context, and event-bridge utilities. Import transport APIs from /direct or /m365 so optional peer dependencies stay isolated.

Express Compatibility

This package is authored against express + @types/express@5, but some upstream SDKs in the Slack/M365 hosting path still expose public APIs typed against older Express declarations.

That mismatch is a TypeScript boundary problem, not a runtime problem. The runtime objects are compatible, but the static types are not always structurally assignable across Express 4 and Express 5 declarations.

The package follows a deliberate policy:

  • keep our package surface on modern Express 5 types
  • localize third-party type skew to small compat boundaries
  • avoid leaking upstream Express version assumptions through our public API

In practice, that is why the M365 path uses a dedicated compat shim in @cuylabs/agent-channel-m365/express-compat, and why the direct Slack Bolt boundary keeps its Express interop casts internal instead of spreading them through user-facing APIs.

Quick Start

Direct Slack mode

import { createAgent } from "@cuylabs/agent-core";
import { openai } from "@ai-sdk/openai";
import { mountSlackAgent } from "@cuylabs/agent-channel-slack/direct";

const agent = createAgent({
  model: openai("gpt-4o-mini"),
});

await mountSlackAgent(agent, {
  path: "/slack/events",
});

Required environment variables:

OPENAI_API_KEY=...
SLACK_BOT_TOKEN=xoxb-...
SLACK_SIGNING_SECRET=...
PORT=3000

Important: path is the exact Slack Events API callback path, not a mount prefix. If you keep the default, configure Slack to call https://<your-host>/slack/events.

Direct Slack mode now has three auth shapes:

  • single-workspace: one bot token for one workspace
  • oauth: Bolt-managed install/callback flow with an installation store
  • authorize: custom Bolt authorize callback for fully external token resolution

The direct-mode API is intentionally split into two layers:

  • mountSlackAgent(...)
    • one-call host + adapter setup
  • createSlackBoltApp(...)
    • lower-level Bolt App / ExpressReceiver builder for advanced hosting and OAuth control

For local OAuth demos, the package also exposes createInMemorySlackInstallationStore(). Use a persistent store in production.

M365-backed Slack mode

import { createAgent } from "@cuylabs/agent-core";
import { openai } from "@ai-sdk/openai";
import { mountM365SlackAgent } from "@cuylabs/agent-channel-slack/m365";

const agent = createAgent({
  model: openai("gpt-4o-mini"),
});

await mountM365SlackAgent(agent, {
  path: "/api/messages",
});

Required environment variables:

OPENAI_API_KEY=...
MicrosoftAppId=...
MicrosoftAppPassword=...
MicrosoftAppTenantId=...
PORT=3978

This mode also requires Azure Bot Service to be configured with the Slack channel connector so Slack events arrive as Bot Framework activities at /api/messages.

What the Package Gives You

  • direct Slack transport via Bolt
  • Bolt Assistant lifecycle wired to an AgentTurnSource (setStatus, setSuggestedPrompts, setTitle, chatStream, feedback buttons)
  • M365-backed Slack transport via CloudAdapter
  • Slack-aware session mapping and thread-aware defaults
  • Slack activity parsing for both the direct and M365-backed paths
  • shared ambient turn context via currentSlackTurnContext()
  • progressive Slack response updates using chat.update
  • native Slack streaming using WebClient.chatStream
  • markdown-to-mrkdwn response formatting
  • typed helpers for Azure Bot Service Slack channelData

Core APIs

Direct mode

  • createSlackBoltApp(...)
  • createSlackChannelAdapter(...) — direct app.message/app.event adapter
  • mountSlackAgent(...) — same, with Express scaffolding
  • createSlackAssistantBridge(...) — Bolt Assistant middleware bridge
  • mountSlackAssistantAgent(...) — assistant pane only, Express
  • mountSlackAssistantAgentSocket(...) — assistant pane only, Socket Mode (recommended for local dev)
  • mountSlackAgentApp(...)unified: assistant pane + app_mention + DMs through one mount, all using chatStream
  • mountSlackAgentAppSocket(...) — unified Assistant pane + app_mention + DMs over Socket Mode
  • createSlackInteractiveController(...) — Slack-native approval / human-input Block Kit controller
  • createSlackMcpServerConfig({ userToken }) — Slack hosted MCP server descriptor consumable by @cuylabs/agent-core/mcp
  • createSlackFeedbackBlock(...) / registerSlackFeedbackAction(...)
  • currentSlackTurnContext() — exposes auth (bot/user tokens, team/enterprise IDs), threadContext, and assistant utilities (setStatus, setSuggestedPrompts, setTitle) inside tools and middleware
  • parseSlackMessageActivity(...)
  • parseSlackMentionActivity(...)
  • parseSlackMessageActivityFromMessageEvent(...)

M365-backed mode

  • createM365SlackChannelAdapter(...)
  • mountM365SlackAgent(...)
  • currentM365SlackTurnContext()
  • parseSlackChannelData(...)
  • parseSlackActivity(...)

currentM365SlackTurnContext() is the M365-path alias for the same Slack ambient context model. Use whichever name is clearer in your codebase.

Slack Assistant (recommended for new direct apps)

Slack's Assistant pane (introduced with Bolt's app.assistant(...) middleware) is the modern surface for AI agents in Slack. The package ships an opinionated bridge that wires the full lifecycle to an AgentTurnSource from @cuylabs/agent-core:

| Lifecycle event | Bridge behavior | | -------------------------------- | ----------------------------------------------------------------------------------------------------- | | assistant_thread_started | Optional say(initialReply) + setSuggestedPrompts(...) + saveThreadContext() | | assistant_thread_context_changed | saveThreadContext() | | message (user prompt in thread) | setStatus("Thinking...") -> client.chatStream(...) -> agent events bridged into chunks -> stop({ blocks: feedbackBlock }) | | Block action agent_feedback | parses verdict, calls onFeedback, posts ephemeral acknowledgement |

AgentEvent mapping inside userMessage:

| AgentEvent | Slack effect | | --------------------------- | ---------------------------------------------------------------------------------- | | text-delta | streamer.append({ markdown_text }) after markdown-to-mrkdwn formatting | | tool-start / tool-error | setStatus(...) + task_update chunk when taskDisplayMode: "plan" | | tool-result | task_update chunk with status: "complete" when taskDisplayMode: "plan" | | reasoning-start | setStatus("Thinking...") when showReasoning: true | | complete | streamer.stop({ blocks: [feedbackBlock] }) | | error | error chunk appended + streamer.stop({ blocks }); original error reaches the host's logger |

Quick start

import { createAgent } from "@cuylabs/agent-core";
import { openai } from "@ai-sdk/openai";
import { mountSlackAssistantAgent } from "@cuylabs/agent-channel-slack/direct";

const agent = createAgent({ model: openai("gpt-4o-mini") });

await mountSlackAssistantAgent({
  source: agent,                                        // any AgentTurnSource
  taskDisplayMode: "timeline",                          // or "plan" for the task list UI
  timeoutMs: 120_000,
  getSuggestedPrompts: () => ({
    title: "Try one of these:",
    prompts: [
      { title: "Roll some dice", message: "Roll 2d20" },
      { title: "Summarize", message: "Summarize this thread" },
    ],
  }),
  onFeedback: async ({ verdict, sessionId }) => {
    metrics.increment(`feedback.${verdict}`, { sessionId });
  },
});

Advanced wiring

For finer control, build the bridge yourself and pair it with createSlackBoltApp (single-workspace, OAuth, or custom authorize modes):

import { createSlackBoltApp, createSlackAssistantBridge } from "@cuylabs/agent-channel-slack/direct";

const { boltApp, app, receiver } = await createSlackBoltApp({ ... });
const bridge = createSlackAssistantBridge({
  source: agent,
  taskDisplayMode: "plan",
  threadContextStore: myDurableStore,                   // optional Postgres-backed store
  showReasoning: true,
  feedback: {
    actionId: "agent_feedback",
    onFeedback: async ({ verdict, channelId, messageTs }) => { ... },
    positiveAck: "Thanks for the feedback!",
    negativeAck: false,                                 // skip the ephemeral
  },
});
bridge.install(boltApp);                                // registers app.assistant(...) + feedback action
app.use(receiver.router);

createSlackAssistantBridge returns { assistant, install(app), feedbackActionId? }. Hosts that prefer manual wiring can call app.assistant(bridge.assistant) and register their own block-action handler instead.

Use mountSlackAgentApp(...) when you want one direct Slack app to handle the Assistant pane, DMs, and app_mention events together. Its prepareTurn and resolveSession hooks apply to every surface, so session routing, system prompts, and app-specific context stay consistent between assistant and channel turns.

For local development or internal deployments that prefer Socket Mode, use the same unified surface over Slack's websocket ingress:

import { mountSlackAgentAppSocket } from "@cuylabs/agent-channel-slack/direct";

await mountSlackAgentAppSocket({
  source: agent,
  appToken: process.env.SLACK_APP_TOKEN,
  botToken: process.env.SLACK_BOT_TOKEN,
  start: true,
});

Socket Mode only changes inbound delivery. Outbound streaming still uses Slack Web API calls (chatStream or chat.update), so the response behavior matches the HTTP Events API mount.

Slack-Native Approval and Human Input

createSlackInteractiveController(...) provides the Slack side of agent-core's approval and human-input primitives:

import { createAgent } from "@cuylabs/agent-core";
import {
  createSlackInteractiveController,
  mountSlackAgentAppSocket,
} from "@cuylabs/agent-channel-slack/direct";

const interactive = createSlackInteractiveController({
  // Defaults to process memory. Use a durable store when actions may resolve
  // after a restart or in another worker.
  store: myDurableInteractiveStore,
  requestTimeoutMs: 5 * 60 * 1000,
  authorize: async (record, actor) =>
    actor.userId === record.target?.userId ||
    (await acl.canApprove(actor.userId, record.request)),
  // Optional fan-out when another runtime owns the pending request.
  onResolve: async (requestId, resolution) => {
    await runtime.respondToInputRequest(
      requestId,
      resolution.kind === "approval"
        ? {
            kind: "approval",
            action: resolution.action,
            feedback: resolution.feedback,
            rememberScope: resolution.rememberScope,
          }
        : {
            kind: "human",
            response: resolution.response,
          },
    );
  },
});

const agent = createAgent({
  model,
  approval: {
    defaultAction: "ask",
    onRequest: interactive.approval.onRequest,
  },
  humanInput: {
    onRequest: interactive.humanInput.onRequest,
  },
});

await mountSlackAgentAppSocket({
  source: agent,
  interactive,
});

When the agent emits an approval or human-input request, the Slack bridge posts a Block Kit prompt in the originating thread and keeps the turn alive until the Slack action resolves it. By default only the original requester can approve or answer. Provide authorize for admin/delegated approval policy. For server-backed or durable runtimes, provide onResolve to call your runtime's input-resolution API and a durable store for pending request metadata; direct in-process turns still need the originating process to remain alive while they wait.

Call interactive.cancelAll("shutting down") during graceful shutdown to reject pending in-process waits and remove pending store records created by that controller.

If you install more than one interactive controller on the same Bolt app, pass a stable namespace or explicit actionIds so Slack block-action IDs do not collide:

const interactive = createSlackInteractiveController({
  namespace: "dory_prod",
});

Direct Slack Behavior

Direct mode is the more Slack-native path:

  • request verification comes from the Slack signing secret
  • the adapter consumes Bolt events directly
  • progressive updates use the Slack Web API
  • direct messages and @mentions are handled by default
  • plain channel-message ingestion is opt-in with respondToChannelMessages: true
  • Bolt auth can run in single-workspace, OAuth, or custom-authorize mode

Direct Slack Auth Modes

Single workspace

Use botToken or SLACK_BOT_TOKEN.

OAuth multi-workspace

Use createSlackBoltApp() or mountSlackAgent() with:

  • auth: { mode: "oauth", ... }
  • a real installation store
  • clientId, clientSecret
  • stateSecret or a custom state store

The package includes createInMemorySlackInstallationStore() for demos and local development, but not for production durability.

Custom authorize

Use auth: { mode: "authorize", authorize } when token lookup already lives in your own auth/runtime layer and Bolt should call back into that resolver.

The default session strategy is "thread-aware":

| Strategy | Session ID | Best for | | -------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------- | | "thread-aware" | channelId:threadTs in threads, channelId in DMs, channelId:messageTs for new channel messages | Natural Slack conversation grouping | | "channel-id" | channelId | One shared session per channel | | "user-per-channel" | channelId:userId | Per-user isolation in shared channels | | "user-per-thread" | channelId:threadTs:userId in threads/channels, channelId:userId in DMs | Per-user isolation inside threads | | "custom" | caller-defined | Shared external session stores |

The default streaming mode is "progressive":

  • post a placeholder
  • update it as text accumulates
  • surface tool or reasoning status before the main response arrives

If you prefer one final reply, use streamingMode: "accumulate".

For Slack-native streaming, use streamingMode: "chat-stream". This uses Slack's chat.startStream, chat.appendStream, and chat.stopStream APIs through @slack/web-api:

await mountSlackAgent(agent, {
  streamingMode: "chat-stream",
  chatStreamBufferSize: 256,
});

Model output is converted from common markdown to Slack mrkdwn by default. Disable it with formatChatMarkdown: false, or provide formatMessageText for full control.

M365-Backed Slack Behavior

The M365-backed path is not just the direct Slack adapter behind a different host. It is a different transport:

  • Slack events are normalized through Azure Bot Service
  • activity.channelId === "slack"
  • Slack-native payload details are embedded in activity.channelData
  • auth is Entra/JWT through CloudAdapter

Assistant pane limitation. Azure Bot Service's Slack channel does not currently relay assistant_thread_started / assistant_thread_context_changed events, and it does not surface Slack's assistant-pane utilities (assistant.threads.setStatus, setSuggestedPrompts, setTitle). The modern Slack Assistant UX (status pane, suggested prompts, chatStream with task_display_mode, feedback blocks) is therefore only available on the direct path via mountSlackAssistantAgent or mountSlackAgentApp. Plain DM and app_mention flows work fine on the M365-backed path.

Interactive limitation. Slack-native approval and human-input rendering uses Block Kit actions and modal submissions. Azure Bot Service's Slack connector does not preserve those direct Slack action/view callbacks as the direct Bolt path does, so createSlackInteractiveController(...) is direct Slack only.

This mode is useful when:

  • your org already deploys bots through the M365 hosting stack
  • you want one hosting/auth story across Teams and Slack
  • you accept that the Slack surface is mediated through Bot Framework activities

The Slack package adds the missing Slack-specific layer on top of agent-channel-m365 by parsing channelData.SlackMessage into SlackActivityInfo.

M365-backed Slack keeps the base M365 conversation session behavior by default. To make ABS Slack thread-aware, provide the Slack-specific resolver:

import {
  createM365SlackChannelAdapter,
  resolveM365SlackThreadSessionId,
} from "@cuylabs/agent-channel-slack/m365";

createM365SlackChannelAdapter({
  source,
  resolveSession: ({ slack }) => resolveM365SlackThreadSessionId(slack),
});

Ambient Turn Context

Use the ambient context helpers inside tools or middleware:

import { currentSlackTurnContext } from "@cuylabs/agent-channel-slack/direct";

const ctx = currentSlackTurnContext();
if (ctx) {
  ctx.slackActivity.channelId;
  ctx.slackActivity.channelType;
  ctx.user.userId;
  ctx.sessionId;
}

When you are on the M365-backed path, currentM365SlackTurnContext() returns the same Slack ambient shape.

Handling Tokens

Direct Slack turns can expose ctx.auth.botToken and, when user scopes are granted, ctx.auth.userToken. Treat both as secrets. Do not put the auth bag in logs, trace attributes, telemetry baggage, tool results, message history, or model-visible prompts. Pass only the token you need to trusted Slack API clients or to createSlackMcpServerConfig({ userToken }).

Docs

Examples

Relationship to Other Channel Packages

| Package | Purpose | | ------------------------------ | -------------------------------------------------- | | @cuylabs/agent-channel-m365 | Generic M365 Activity Protocol bridge | | @cuylabs/agent-channel-teams | Teams-native layer above the M365 bridge | | @cuylabs/agent-channel-slack | Slack-native direct mode plus Slack-over-M365 mode |

If you need Slack only, use the direct mode. If you need Slack and Teams to share the same Microsoft-hosted ingress and auth story, use the M365-backed mode.