Skip to content

Commit ea4d20b

Browse files
authored
feat: slack support (#4)
* feat: add Slack bot support via Socket Mode Implement Slack as a second messaging platform alongside Telegram. Uses slack-bolt with Socket Mode (WebSocket, no public URL needed). Handles text, image, and audio messages. Adds generalized platform_images table for cross-platform image retrieval, setup wizard and CLI command (`memclaw slack`), and updated documentation. * docs: note Slack App Home Messages Tab requirement Without this toggle, DMs to the bot show "Sending messages to this app has been turned off" with no clear path to fix. * docs: add Slack app manifest for one-click setup Lets users create the Slack app via "From a manifest" instead of clicking through scopes, events, and the App Home toggles by hand. * feat: constrain agent replies to messaging-app-safe markdown Messaging clients render different (or no) markdown dialects, so the agent's full markdown output (`**bold**`, `# headings`, fenced code) was leaking as literal characters in Slack and WhatsApp. Pin the agent to a minimal common syntax — single-asterisk bold, underscore italic, plain dashes for bullets, no headings or backticks — that renders cleanly across Slack mrkdwn, WhatsApp, and Telegram. Also flip the Telegram handler to parse_mode="Markdown" so `*bold*` and `_italic_` actually render, with a plain-text fallback if the agent emits something the legacy parser rejects. * fix: enforce channel-scoped tokens as required during their channel's setup `memclaw slack` was prompting SLACK_BOT_TOKEN and SLACK_APP_TOKEN as "(optional)" even though the CLI immediately errored out after setup if they were missing — so the wizard happily accepted blank input that the bot couldn't run with. Same lie applied to TELEGRAM_BOT_TOKEN. Mark those tokens required=True and extend the wizard so a channel-scoped required key enforces only when invoked via that channel; `memclaw configure` still lets users edit anything without forcing slack/telegram keys on telegram-only/slack-only users. Also drop the duplicate ", optional" suffix from the SLACK_ALLOWED_CHANNELS label that was rendering as "... optional) (optional):". * docs: tell the agent it can fetch and save links Handlers already pre-fetch and summarise URLs into a `[Link summary]` block, but the agent's capability list never mentioned it — so it wouldn't surface the feature when users asked what it can do. * test: update reply_text assertion for new parse_mode argument Telegram handler now passes parse_mode="Markdown" so the agent's single-asterisk bold actually renders. The handler test was still asserting the old plain-text call signature.
1 parent 2ff9ba2 commit ea4d20b

12 files changed

Lines changed: 543 additions & 16 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ ANTHROPIC_API_KEY=your-anthropic-key
77
# Telegram bot settings
88
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
99
ALLOWED_USER_IDS=123456789,987654321
10+
11+
# Slack bot settings
12+
SLACK_BOT_TOKEN=xoxb-your-bot-token
13+
SLACK_APP_TOKEN=xapp-your-app-level-token
14+
SLACK_ALLOWED_CHANNELS=C01ABCDEF,C02GHIJKL

README.md

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Store your thoughts. Save your images and links. Ask anything, anytime.
1111
[![Anthropic API](https://img.shields.io/badge/Anthropic-API-blueviolet.svg)](https://docs.anthropic.com/)
1212
[![Telegram Bot](https://img.shields.io/badge/Telegram-Bot-26A5E4.svg)](https://core.telegram.org/bots)
1313
[![WhatsApp](https://img.shields.io/badge/WhatsApp-Bot-25D366.svg)](https://github.com/krypton-byte/neonize)
14+
[![Slack Bot](https://img.shields.io/badge/Slack-Bot-4A154B.svg)](https://api.slack.com/bolt)
1415
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
1516

1617
</div>
@@ -50,7 +51,7 @@ On first run, Memclaw will prompt you for your API keys and save them to `~/.mem
5051

5152
The main way to use Memclaw. Just talk to it naturally — no commands needed. Send text, photos, voice messages, or links. The agent figures out what to do: store it, search your memories, retrieve images, or just chat.
5253

53-
Both platforms share the same agent, memories, and search index — your data is unified regardless of how you interact.
54+
All platforms share the same agent, memories, and search index — your data is unified regardless of how you interact.
5455

5556
### Telegram Bot
5657

@@ -88,13 +89,83 @@ On first run a QR code is printed to your terminal. On your phone: **Settings
8889

8990
Only messages you send to yourself (via WhatsApp's "Message Yourself" chat) are processed. DMs from other people and group messages are ignored.
9091

92+
### Slack Bot
93+
94+
The Slack bot connects via **Socket Mode** (WebSocket) — no public URL or webhook server needed.
95+
96+
#### Setup (from manifest — recommended)
97+
98+
1. Go to [api.slack.com/apps](https://api.slack.com/apps)**Create New App****From a manifest**, pick your workspace, and paste:
99+
100+
```yaml
101+
display_information:
102+
name: Memclaw
103+
description: Your personal memory vault, powered by AI.
104+
background_color: "#1a1a1a"
105+
features:
106+
bot_user:
107+
display_name: Memclaw
108+
always_online: true
109+
app_home:
110+
home_tab_enabled: false
111+
messages_tab_enabled: true
112+
messages_tab_read_only_enabled: false
113+
oauth_config:
114+
scopes:
115+
bot:
116+
- app_mentions:read
117+
- channels:history
118+
- chat:write
119+
- files:read
120+
- files:write
121+
- im:history
122+
- im:read
123+
- im:write
124+
settings:
125+
event_subscriptions:
126+
bot_events:
127+
- app_mention
128+
- message.im
129+
interactivity:
130+
is_enabled: false
131+
socket_mode_enabled: true
132+
```
133+
134+
2. **Install to Workspace** and copy the **Bot Token** (`xoxb-...`).
135+
3. Under **Basic Information → App-Level Tokens**, generate a token with `connections:write` scope and copy it (`xapp-...`).
136+
4. Start the bot:
137+
138+
```bash
139+
memclaw slack
140+
```
141+
142+
#### Setup (from scratch)
143+
144+
If you'd rather click through the UI instead of using the manifest:
145+
146+
1. Create a [Slack App](https://api.slack.com/apps) and enable **Socket Mode**.
147+
2. Under **OAuth & Permissions**, add these bot token scopes: `app_mentions:read`, `chat:write`, `files:read`, `files:write`, `im:history`, `im:read`, `im:write`, `channels:history`.
148+
3. Under **Event Subscriptions**, subscribe to: `message.im`, `app_mention`.
149+
4. Under **App Home** → **Messages Tab**, enable the tab and check **"Allow users to send Slash commands and messages from the messages tab"** (otherwise DMs show "Sending messages to this app has been turned off").
150+
5. Install the app to your workspace and copy the **Bot Token** (`xoxb-...`).
151+
6. Generate an **App-Level Token** (`xapp-...`) with `connections:write` scope.
152+
7. Start the bot:
153+
154+
```bash
155+
memclaw slack
156+
```
157+
158+
You can DM the bot directly or mention it in channels (`@Memclaw save this...`). Optionally restrict it to specific channels with `SLACK_ALLOWED_CHANNELS`.
159+
160+
To update keys later: `memclaw configure`.
161+
91162
### What it handles
92163

93164
| Message type | What happens |
94165
|-------------|-------------|
95166
| **Text** | Agent decides: store as memory, search existing memories, or both. Links are extracted, fetched, and summarized automatically. |
96167
| **Photo** | AI-described via vision model, stored and indexed. Agent acknowledges and responds. Saved for later retrieval. |
97-
| **Voice** | Transcribed via Whisper, stored as text. Agent responds to the content. Links extracted. |
168+
| **Voice / Audio** | Transcribed via Whisper, stored as text. Agent responds to the content. Links extracted. |
98169

99170
### Examples
100171

@@ -117,6 +188,7 @@ Here's the sprint planning whiteboard you saved last week.
117188
flowchart LR
118189
TG[Telegram] <-->|text / images / links<br>response + images| Agent[Memclaw Agent]
119190
WA[WhatsApp] <-->|text / images / links<br>response + images| Agent
191+
SL[Slack] <-->|text / images / links<br>response + images| Agent
120192
121193
subgraph sandbox ["~/.memclaw/"]
122194
Agent -->|save| Tools1["memory_save<br>image_save<br>file_write"]
@@ -233,6 +305,9 @@ memclaw --memory-dir ~/my-vault # override storage location
233305
| `ANTHROPIC_API_KEY` | Yes | Powers the Claude agent |
234306
| `TELEGRAM_BOT_TOKEN` | For Telegram bot | Your Telegram bot token |
235307
| `ALLOWED_USER_IDS` | For Telegram bot | Comma-separated Telegram user IDs |
308+
| `SLACK_BOT_TOKEN` | For Slack bot | Slack bot token (`xoxb-...`) |
309+
| `SLACK_APP_TOKEN` | For Slack bot | Slack app-level token for Socket Mode (`xapp-...`) |
310+
| `SLACK_ALLOWED_CHANNELS` | For Slack bot | Comma-separated Slack channel IDs (optional) |
236311

237312
### Directory Structure
238313

memclaw/agent.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@
2525
2626
{agent_instructions}
2727
28+
=== REPLY FORMATTING ===
29+
Replies are delivered to messaging apps with limited markdown support. Use \
30+
ONLY this minimal syntax — anything else leaks as literal characters:
31+
- Bold: `*bold*` (single asterisk). NEVER use `**double asterisks**`.
32+
- Italic: `_italic_`.
33+
- Bullet lists: plain `- item` on its own line.
34+
- Paragraphs: separate with a blank line.
35+
- Headings (`#`, `##`, ...) are NOT supported — use a bold line on its own \
36+
(e.g. `*Section name*`) followed by a blank line instead.
37+
- No backticks or fenced code blocks.
38+
- No `[label](url)` links — write the bare URL.
39+
2840
=== MEMORY CONTEXT ===
2941
{context}
3042

memclaw/bot/handlers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ async def _send_response(
6464
logger.error(f"Failed to send image {img.get('file_id')}: {e}")
6565

6666
if response_text:
67-
await update.message.reply_text(response_text[:4096])
67+
try:
68+
await update.message.reply_text(response_text[:4096], parse_mode="Markdown")
69+
except Exception as e:
70+
# Fall back to plain text if the agent emitted something Telegram's
71+
# legacy Markdown parser rejects (unbalanced * or _, etc.).
72+
logger.warning(f"Markdown parse failed, sending plain: {e}")
73+
await update.message.reply_text(response_text[:4096])
6874

6975
async def _send_with_typing(
7076
self,

0 commit comments

Comments
 (0)