Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
aa3cc31
feat(cli): restructure maestro-discord into verb-based commands
chr1syy May 3, 2026
b547377
feat(commands): add /playbook, /gist, /notes, /auto-run, /agents show
chr1syy May 3, 2026
7fe2196
fix(commands): clamp embed text to Discord limits in /playbook + /age…
chr1syy May 3, 2026
4304946
fix(cli,commands): address PR #26 review feedback
chr1syy May 5, 2026
ffca9f5
fix(cli,commands,tests): address PR #26 follow-up review
chr1syy May 5, 2026
3495807
refactor(core,providers): extract provider-agnostic kernel and Discor…
chr1syy May 5, 2026
bce53e1
chore(rename): discord-maestro -> maestro-bridge
chr1syy May 5, 2026
830d79a
chore(installer): rename maestro-discord installer to maestro-bridge
chr1syy May 5, 2026
0eb2dd6
Merge feat/unified-bridge into rc: provider-agnostic kernel + Discord…
chr1syy May 6, 2026
5942b78
Merge feat/rename-to-bridge into rc: discord-maestro -> maestro-bridg…
chr1syy May 6, 2026
483bb78
Merge feat/rename-installer into rc: rename installer + ctl + service…
chr1syy May 6, 2026
c502dc2
chore(rename): transition bridge naming to maestro-relay with compat …
chr1syy May 6, 2026
16d8d52
feat: add module-switch scaffold + complete relay rename/doc cleanup …
chr1syy May 6, 2026
08a6408
docs(readme): keep npm link separate from relay ctl usage
chr1syy May 6, 2026
c8c2060
docs(readme): add one-line install and remove duplicate CLI section
chr1syy May 6, 2026
c9ec8c2
fix(release): rename release tarball to maestro-relay-${tag}.tar.gz
chr1syy May 6, 2026
a2fa4b8
fix(installer): strip surrounding quotes from ENABLED_PROVIDERS befor…
chr1syy May 6, 2026
f673a59
fix: WAL checkpoint and db.close on graceful shutdown
scriptease May 7, 2026
d2b6fd0
fix(plist): add /usr/local/bin to launchd PATH so maestro-cli resolves
scriptease May 7, 2026
f4dbcca
feat(slack): add Slack provider adapter to the bridge kernel
scriptease May 7, 2026
b9bd352
fix(slack): map Unicode emoji to Slack names in react(); log react er…
scriptease May 9, 2026
0899871
fix(slack): address code review findings
scriptease May 9, 2026
75a4fc3
test(slack): add tests for conversationsDb, config, and react emoji m…
scriptease May 9, 2026
0e9be1c
fix(slack): address chris review findings
scriptease May 9, 2026
e16deee
fix(queue): show agent response even when session exits non-zero
scriptease May 10, 2026
3dc9f68
fix(queue): don't surface raw internal errors to chat clients
scriptease May 10, 2026
6a8c130
Merge pull request #38 from RunMaestro/slack-provider
chr1syy May 10, 2026
dae0f92
fix(queue,installer): hide internal errors and normalize quoted provi…
chr1syy May 10, 2026
6c0479a
Merge origin/main into rc
chr1syy May 10, 2026
1978026
docs: add docs/slack.md and update provider-dev guide for built-in Slack
chr1syy May 10, 2026
1f2bdfa
fix(slack): align /agents readonly with Discord, log via logger.error
chr1syy May 10, 2026
d451d79
fix(slack): address PR #40 CodeRabbit review
chr1syy May 10, 2026
4812716
fix(slack): address PR #40 Codex review
chr1syy May 10, 2026
85affab
docs(slack): clarify mention-strip comment matches actual /g regex be…
chr1syy May 10, 2026
bd09b7d
fix(slack): close out remaining PR #40 follow-ups
chr1syy May 10, 2026
0850c6e
chore: bump version to 0.2.0 for Slack provider release
chr1syy May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ DISCORD_CLIENT_ID=your_application_client_id_here
DISCORD_GUILD_ID=your_guild_id_here
DISCORD_ALLOWED_USER_IDS=123,456 # comma-separated Discord user IDs allowed to use slash commands
DISCORD_MENTION_USER_ID= # optional: Discord user ID to @mention when --mention is used

# --- Slack provider (loaded only if 'slack' is in ENABLED_PROVIDERS) ---
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_SIGNING_SECRET=your_signing_secret_here
SLACK_TEAM_ID=T0123456789
SLACK_APP_ID=A0123456789
SLACK_SOCKET_MODE_TOKEN= # optional: app-level token (xapp-...) for Socket Mode; if set, Socket Mode is used instead of HTTP webhooks
SLACK_BOT_PUBLIC_URL= # optional: public HTTPS URL for Bolt's ExpressReceiver (webhook mode only)
SLACK_PORT=3000 # optional: HTTP port for Bolt's ExpressReceiver (webhook mode only, default 3000)
SLACK_ALLOWED_USER_IDS=U123,U456 # optional: comma-separated Slack user IDs allowed to use slash commands
SLACK_MENTION_USER_ID= # optional: Slack user ID to @mention when --mention is used
21 changes: 11 additions & 10 deletions AGENTS-providers.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Provider development guide

This document is the deep-dive companion to [`AGENTS.md`](AGENTS.md) (and [`docs/architecture.md`](docs/architecture.md)) for adding a new chat-platform provider to Maestro Relay. Everything below is what you'd need to know to ship a Slack, Teams, Matrix, etc. adapter without touching the kernel.
This document is the deep-dive companion to [`AGENTS.md`](AGENTS.md) (and [`docs/architecture.md`](docs/architecture.md)) for adding a new chat-platform provider to Maestro Relay. Discord and Slack are already built-in (see [`docs/discord.md`](docs/discord.md) and [`docs/slack.md`](docs/slack.md)); everything below is what you'd need to know to ship a Teams, Matrix, etc. adapter without touching the kernel.

If you're adding behavior to the existing Discord provider rather than building a new one, work in `src/providers/discord/` and consult [`docs/discord.md`](docs/discord.md) instead.
If you're adding behavior to an existing provider rather than building a new one, work in `src/providers/discord/` or `src/providers/slack/` and consult the matching `docs/<name>.md` instead.

## The kernel/provider boundary

Expand Down Expand Up @@ -107,12 +107,13 @@ Things to keep out of `src/core/`: SDK imports (`discord.js`, `@slack/bolt`, etc

### 1. Register the adapter

In `src/core/providers.ts`, add a `case` to `loadProvider`:
In `src/core/providers.ts`, add a `case` to `loadProvider` (alongside the existing `discord` and `slack` cases):

```ts
case 'slack':
const { SlackProvider } = await import('../providers/slack/adapter.js');
return new SlackProvider();
case 'teams': {
const { TeamsProvider } = await import('../providers/teams/adapter');
return new TeamsProvider();
}
```

This is the only kernel file the provider should touch.
Expand All @@ -122,10 +123,10 @@ This is the only kernel file the provider should touch.
Add a section to `.env.example`:

```env
# --- Slack provider (loaded only if 'slack' is in ENABLED_PROVIDERS) ---
SLACK_BOT_TOKEN=your_xoxb_token_here
SLACK_APP_TOKEN=your_xapp_token_here
SLACK_SIGNING_SECRET=your_signing_secret_here
# --- Teams provider (loaded only if 'teams' is in ENABLED_PROVIDERS) ---
TEAMS_BOT_TOKEN=your_token_here
TEAMS_APP_ID=your_app_id_here
TEAMS_TENANT_ID=your_tenant_id_here
```

Validate creds in `start(ctx)`, throwing a clear error if missing. Don't validate at module load — a disabled provider must not fail the bridge on missing env.
Expand Down
12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agent Guide

This repo is **Maestro Relay** — a chat-platform-to-Maestro bridge built around a provider-agnostic kernel. Discord is the first provider; Slack/Teams plug in alongside it without touching the kernel. `CLAUDE.md` is a symlink to this file.
This repo is **Maestro Relay** — a chat-platform-to-Maestro bridge built around a provider-agnostic kernel. Discord and Slack ship in the box; Teams/Matrix plug in alongside them without touching the kernel. `CLAUDE.md` is a symlink to this file.

## Development workflow

Expand Down Expand Up @@ -30,6 +30,10 @@ This repo is **Maestro Relay** — a chat-platform-to-Maestro bridge built aroun

Lives under `src/providers/discord/` (`adapter.ts`, `messageCreate.ts`, `voice.ts`, `commands/`, `deploy.ts`, `channelsDb.ts`, `threadsDb.ts`, `embed.ts`, `config.ts`). For Discord-specific runtime behavior, env vars, slash commands, and bot setup see [docs/discord.md](docs/discord.md). Voice transcription is documented in [docs/voice.md](docs/voice.md).

### Slack provider

Lives under `src/providers/slack/` (`adapter.ts`, `messageCreate.ts`, `commands/`, `channelsDb.ts`, `conversationsDb.ts`, `config.ts`). Uses `@slack/bolt` (Socket Mode in dev, ExpressReceiver in production). Thread registry (`slack_agent_conversations`) is keyed on `thread_ts`. For Slack-specific runtime behavior, env vars, slash commands, and app setup see [docs/slack.md](docs/slack.md).

### CLI

- `src/cli/maestro-relay.ts` — verb dispatcher (`send`, `notify`, `status`)
Expand Down Expand Up @@ -59,7 +63,7 @@ TL;DR:

## Installer module switch

- `install.sh` supports `MAESTRO_RELAY_MODULE` (fallback `MAESTRO_BRIDGE_MODULE`), currently accepting only `discord`.
- `install.sh` supports `MAESTRO_RELAY_MODULE` (fallback `MAESTRO_BRIDGE_MODULE`), currently accepting `discord` and `slack`.
- Keep installer module selection aligned with runtime `ENABLED_PROVIDERS` and CLI `--provider` support.
- When adding a provider, update installer validation/prompting and `maestro-relay-ctl deploy` routing so deploy behavior is module-aware.

Expand All @@ -73,7 +77,7 @@ TL;DR:

## Expectations for changes

- Follow existing patterns in `src/core/` and `src/providers/discord/` before introducing new abstractions.
- Provider-specific code (Discord types, slash commands, threads) lives in `src/providers/discord/` — keep `src/core/` free of `discord.js` imports.
- Follow existing patterns in `src/core/` and `src/providers/{discord,slack}/` before introducing new abstractions.
- Provider-specific code (Discord types, Slack Bolt handlers, slash commands, threads) lives under `src/providers/<name>/` — keep `src/core/` free of `discord.js` and `@slack/bolt` imports.
- Keep changes minimal and focused.
- Update docs when behavior or setup changes.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

[![Made with Maestro](https://raw.githubusercontent.com/RunMaestro/Maestro/main/docs/assets/made-with-maestro.svg)](https://github.com/RunMaestro/Maestro)

**Maestro Relay** connects chat platforms to [Maestro](https://runmaestro.ai) AI agents through `maestro-cli`. Discord ships in the box; Slack, Teams, and others can be added by dropping in a provider adapter — the kernel is provider-agnostic.
**Maestro Relay** connects chat platforms to [Maestro](https://runmaestro.ai) AI agents through `maestro-cli`. Discord and Slack ship in the box; Teams, Matrix, and others can be added by dropping in a provider adapter — the kernel is provider-agnostic.

> **Migrating from `discord-maestro`?** Same codebase, new name. The legacy `maestro-discord` binary is preserved as an alias and all `DISCORD_*` env vars work unchanged. See "Migration" below.

## Features

- Provider-pluggable kernel — Discord today, Slack/Teams next
- Provider-pluggable kernel — Discord and Slack today, Teams/Matrix next
- Creates dedicated channels for Maestro agents
- Per-user session threads (`/session new` or by mentioning the bot)
- Per-conversation FIFO queue with typing/reaction indicators
Expand All @@ -18,7 +18,7 @@
## Prerequisites

- Node.js 22+
- A Discord application + bot token (if running the Discord provider)
- A bot token for at least one supported provider (Discord or Slack)
- [Maestro CLI](https://docs.runmaestro.ai/cli) on your `PATH`

## Install (production one-liner)
Expand Down Expand Up @@ -49,7 +49,7 @@ The legacy aliases `maestro-bridge-ctl` and `maestro-discord-ctl` still work for
| systemd user / launchd agent | Auto-start unit |

Override any of these with `MAESTRO_RELAY_HOME`, `XDG_CONFIG_HOME`, or `MAESTRO_RELAY_BIN_DIR`. Pin a specific version with `MAESTRO_RELAY_VERSION=v1.0.0`.
Choose a provider module at install time via `MAESTRO_RELAY_MODULE` (currently only `discord` is supported).
Choose a provider module at install time via `MAESTRO_RELAY_MODULE` (`discord` or `slack`).

## Install (development from source)

Expand All @@ -70,11 +70,11 @@ cp .env.example .env
Set core values in `.env`:

```
ENABLED_PROVIDERS=discord # comma-separated; default 'discord'
ENABLED_PROVIDERS=discord # comma-separated; default 'discord'. Use 'slack' or 'discord,slack' for multi-provider deployments
API_PORT=3457 # optional, default 3457
```

Then fill in the provider-specific keys. The Discord provider needs `DISCORD_BOT_TOKEN`, `DISCORD_CLIENT_ID`, and `DISCORD_GUILD_ID` — see [docs/discord.md](docs/discord.md) for bot setup, the full env-var reference, and slash-command deployment. For optional voice transcription, see [docs/voice.md](docs/voice.md).
Then fill in the provider-specific keys. The Discord provider needs `DISCORD_BOT_TOKEN`, `DISCORD_CLIENT_ID`, and `DISCORD_GUILD_ID` — see [docs/discord.md](docs/discord.md) for bot setup, the full env-var reference, and slash-command deployment. The Slack provider needs `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `SLACK_TEAM_ID`, and `SLACK_APP_ID` — see [docs/slack.md](docs/slack.md). For optional voice transcription (Discord), see [docs/voice.md](docs/voice.md).

3. Deploy slash commands (Discord):

Expand Down Expand Up @@ -118,9 +118,10 @@ npm run build && node --test --experimental-test-coverage dist/__tests__/**/*.te
| Provider | Docs | Status |
| -------- | ---- | ------ |
| Discord | [docs/discord.md](docs/discord.md) — bot setup, env vars, slash commands, runtime behavior | Built-in |
| Slack / Teams / Matrix | [AGENTS-providers.md](AGENTS-providers.md) — provider development guide | Add your own |
| Slack | [docs/slack.md](docs/slack.md) — app setup, env vars, slash commands, runtime behavior | Built-in |
| Teams / Matrix / … | [AGENTS-providers.md](AGENTS-providers.md) — provider development guide | Add your own |

Optional voice transcription (whisper.cpp): [docs/voice.md](docs/voice.md).
Optional voice transcription (whisper.cpp, Discord-only today): [docs/voice.md](docs/voice.md).

## How it works

Expand Down
12 changes: 11 additions & 1 deletion bin/maestro-relay-ctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,17 @@ cmd_logs() {
config_complete() {
local file="$1" key value
[ -f "$file" ] || return 1
for key in DISCORD_BOT_TOKEN DISCORD_CLIENT_ID DISCORD_GUILD_ID; do
local enabled_module
enabled_module="$(sed -nE 's/^[[:space:]]*ENABLED_PROVIDERS[[:space:]]*=[[:space:]]*([^#[:space:]]+).*/\1/p' "$file" | head -n1)"
enabled_module="${enabled_module#\"}"; enabled_module="${enabled_module%\"}"
enabled_module="${enabled_module#\'}"; enabled_module="${enabled_module%\'}"
local required_keys
if [ "$enabled_module" = "slack" ]; then
required_keys="SLACK_BOT_TOKEN SLACK_SIGNING_SECRET SLACK_TEAM_ID SLACK_APP_ID"
else
required_keys="DISCORD_BOT_TOKEN DISCORD_CLIENT_ID DISCORD_GUILD_ID"
fi
for key in $required_keys; do
value="$(sed -nE "s/^${key}=([^#[:space:]]+).*/\1/p" "$file" | head -n1)"
[ -n "$value" ] || return 1
case "$value" in
Expand Down
92 changes: 92 additions & 0 deletions docs/slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Slack provider

The Slack provider lets Maestro Relay run inside a Slack workspace alongside or instead of Discord. This document covers everything Slack-specific: app creation, scopes, slash commands, and runtime behavior. For the kernel/provider boundary, see [architecture.md](architecture.md).

The provider only loads if `slack` is in `ENABLED_PROVIDERS`. To run Slack and Discord simultaneously: `ENABLED_PROVIDERS=discord,slack`.

## App setup

1. Create an app at https://api.slack.com/apps (choose **From scratch**, pick a workspace).
2. **OAuth & Permissions → Bot Token Scopes**, add:
- `app_mentions:read`
- `channels:history`, `channels:read`, `channels:join`, `channels:manage`
- `chat:write`, `chat:write.public`
- `commands`
- `reactions:write`
- `users:read`
3. **Event Subscriptions**:
- Subscribe to bot events: `app_mention`, `message.channels`.
- If you're using Socket Mode, enable it under **Settings → Socket Mode** and generate an **App-Level Token** with `connections:write`. This becomes `SLACK_SOCKET_MODE_TOKEN`.
- Otherwise (webhook mode) point the **Request URL** at `https://<your-public-host>/slack/events` and set `SLACK_BOT_PUBLIC_URL` accordingly.
4. **Slash Commands** — create one entry per command (`/health`, `/agents`, `/session`). Request URL is the same `…/slack/events` (webhook mode) or unused (Socket Mode).
5. Install the app to the workspace and copy:
- **Bot User OAuth Token** (`xoxb-…`) → `SLACK_BOT_TOKEN`
- **Signing Secret** (Basic Information → App Credentials) → `SLACK_SIGNING_SECRET`
- Workspace **Team ID** (`T…`) → `SLACK_TEAM_ID`
- **App ID** (`A…`) → `SLACK_APP_ID`

## Configuration

Slack provider keys read from `.env`:

| Key | Required | Purpose |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `SLACK_BOT_TOKEN` | yes | Bot User OAuth Token (`xoxb-…`) |
| `SLACK_SIGNING_SECRET` | yes | App-credentials signing secret (HTTP webhook verification) |
| `SLACK_TEAM_ID` | yes | Workspace ID (`T…`) |
| `SLACK_APP_ID` | yes | App ID (`A…`) |
| `SLACK_SOCKET_MODE_TOKEN` | no | App-level token (`xapp-…`); when set, Socket Mode is used instead of HTTP webhooks |
| `SLACK_BOT_PUBLIC_URL` | no | Public HTTPS URL for Bolt's `ExpressReceiver` (webhook mode only) |
| `SLACK_PORT` | no | HTTP port for `ExpressReceiver` (webhook mode only, default `3000`) |
| `SLACK_ALLOWED_USER_IDS` | no | Comma-separated Slack user IDs allowed to use slash commands; empty allows everyone |
| `SLACK_MENTION_USER_ID` | no | User ID to `@mention` when API callers pass `mention=true` |

The Slack adapter loads its config lazily, so a deployment that disables Slack (`ENABLED_PROVIDERS=discord`) does **not** fail at startup for missing `SLACK_*` keys.

### Choosing Socket Mode vs webhook mode

- **Socket Mode** (`SLACK_SOCKET_MODE_TOKEN` set) — easiest for development and self-hosted production. The relay opens an outbound WebSocket to Slack; no public HTTPS endpoint required.
- **Webhook mode** (`SLACK_SOCKET_MODE_TOKEN` empty) — requires a publicly reachable HTTPS URL pointed at the relay (set `SLACK_BOT_PUBLIC_URL`, optionally override `SLACK_PORT`). Use this when you need a stateless deployment behind a load balancer.

## Slash commands

| Command | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `/health` | Verify the relay process is healthy |
| `/agents list` | Show all available Maestro agents |
| `/agents new <agent-id>` | Create (or reopen) a dedicated public channel `#maestro-<agent-name>` and register it for the agent |
| `/agents disconnect [agent-id]` | (Run inside an agent channel) Unregister the binding and archive the channel |
| `/agents readonly <on\|off>` | (Run inside an agent channel) Toggle read-only mode for the bound agent |
| `/session new [name]` | Post a parent message in the current agent channel and bind a new owner-scoped thread to the invoking user |

The Slack provider deliberately ships a smaller command surface than Discord — the playbook, gist, notes, and auto-run flows are Discord-only today.

## Runtime behavior

- **Mentioning the bot** in a registered agent channel posts a new top-level message and binds it as a thread to the invoking user. Subsequent replies in that thread are forwarded to the agent.
- **`/session new`** does the same thing without requiring a mention; an optional name is shown in the parent message.
- **Owner-bound threads**: only the user who created the thread can drive the agent. Messages from other users are silently ignored.
- **Reactions**: `⏳` (`hourglass_flowing_sand`) while a message is queued. The Slack API requires emoji *names*, not Unicode characters; the adapter maps `⏳ 🎧 ✅ ❌` to the corresponding Slack names — pass any of them to `provider.react()` and the mapping happens automatically.
- **Typing indicator**: not exposed by Slack's Web API; `sendTyping` is a no-op on this provider.
- **Usage stats** are appended below each agent reply (tokens, cost, context %).
- **Channel naming**: agent channels are named `maestro-<sanitized-agent-name>-<id-prefix>`, where `id-prefix` is the first 8 alphanumeric characters of the agent ID. The agent ID makes the name unique even when two different agents normalize to the same display name. The whole result is capped at 80 characters. Both `/agents new` and the HTTP-API auto-create path (`POST /api/send`) use the same helper. If the channel already exists but is archived, the adapter unarchives it; if unarchive fails, it falls back to creating a fresh channel with a `-<timestamp>` suffix appended to the base name.

## Storage

- The shared `agent_channels` table stores Slack channel ↔ agent bindings with `provider='slack'`.
- `slack_agent_conversations` is a Slack-only thread registry keyed on `thread_ts`. It records `(channel_id, agent_id, owner_user_id, session_id)` and is dropped along with its parent channel when `/agents disconnect` runs.

## Security

- Slash command access can be locked down with `SLACK_ALLOWED_USER_IDS`. When empty, all workspace members may use slash commands.
- Threads created by mention or `/session new` are bound to a single owner; non-owner messages in the thread are ignored silently.
- The bot only auto-creates **public** channels (`is_private: false`). To use private channels, create them manually and run `/agents new` from inside.

## Troubleshooting

- **`/health` posts but slash commands return `dispatch_failed`** → confirm the slash commands are registered in **Slack App → Slash Commands** with the right Request URL (Socket Mode users can leave it blank).
- **`⏳` reaction never appears** → check the bot has `reactions:write` and was reinstalled to the workspace after adding the scope. The adapter logs reaction failures via `logger.error('queue:react', …)`.
- **`signing_secret_missing` or HTTP 401 from Slack** → fill in `SLACK_SIGNING_SECRET`. Required even in Socket Mode setups for parts of the Bolt SDK.
- **Bot is online but ignores thread replies** → confirm the thread is in `slack_agent_conversations` (`/session new` or a mention created it) and that the message author matches `owner_user_id`.
- **Channel creation fails with `name_taken`** → an archived channel with the same `maestro-<…>-<id-prefix>` name exists and unarchive failed. The adapter falls back to `<name>-<timestamp>`; if that also fails (e.g. workspace channel limits, scope missing), create the channel manually and re-run `/agents new`.
- **Slack rejects the emoji name** → the adapter maps `⏳ 🎧 ✅ ❌`. Other Unicode emoji are passed through unchanged; if you call `provider.react()` from custom code with a Unicode emoji that's not in the map, add it to `UNICODE_TO_SLACK` in `src/providers/slack/adapter.ts`.
Loading