Files
nanoclaw/.claude/skills/add-slack/modify/src/index.ts.intent.md
Darrell O'Donnell ee7f720617 /add-slack (#366)
* feat: add Slack channel skill (/add-slack)

Slack Bot integration via @slack/bolt with Socket Mode. Can replace
WhatsApp entirely (SLACK_ONLY=true) or run alongside it.

- SlackChannel implementing Channel interface (46 unit tests)
- Socket Mode connection (no public URL needed)
- @mention translation (Slack <@UBOTID> → TRIGGER_PATTERN)
- Message splitting at 4000-char Slack API limit
- Thread flattening (threaded replies delivered as channel messages)
- User name resolution with caching
- Outgoing message queue with flush-on-reconnect
- Channel metadata sync with pagination
- Proper Bolt types (GenericMessageEvent | BotMessageEvent)
- Multi-channel orchestrator changes (conditional channel creation)
- Setup guide (SLACK_SETUP.md) and known limitations documented

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* local settings

* adjusted when installing

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 15:34:34 +02:00

2.8 KiB

Intent: src/index.ts modifications

What changed

Refactored from single WhatsApp channel to multi-channel architecture supporting Slack alongside WhatsApp.

Key sections

Imports (top of file)

  • Added: SlackChannel from ./channels/slack.js
  • Added: SLACK_ONLY from ./config.js
  • Added: readEnvFile from ./env.js
  • Existing: findChannel from ./router.js and Channel type from ./types.js are already present

Module-level state

  • Kept: let whatsapp: WhatsAppChannel — still needed for syncGroupMetadata reference
  • Added: let slack: SlackChannel | undefined — direct reference for syncChannelMetadata
  • Kept: const channels: Channel[] = [] — array of all active channels

processGroupMessages()

  • Uses findChannel(channels, chatJid) lookup (already exists in base)
  • Uses channel.setTyping?.() and channel.sendMessage() (already exists in base)

startMessageLoop()

  • Uses findChannel(channels, chatJid) per group (already exists in base)
  • Uses channel.setTyping?.() for typing indicators (already exists in base)

main()

  • Added: Reads Slack tokens via readEnvFile() to check if Slack is configured
  • Added: conditional WhatsApp creation (if (!SLACK_ONLY))
  • Added: conditional Slack creation (if (hasSlackTokens))
  • Changed: scheduler sendMessage uses findChannel()channel.sendMessage()
  • Changed: IPC syncGroupMetadata syncs both WhatsApp and Slack metadata
  • Changed: IPC sendMessage uses findChannel()channel.sendMessage()

Shutdown handler

  • Changed from await whatsapp.disconnect() to for (const ch of channels) await ch.disconnect()
  • Disconnects all active channels (WhatsApp, Slack, or any future channels) on SIGTERM/SIGINT

Invariants

  • All existing message processing logic (triggers, cursors, idle timers) is preserved
  • The runAgent function is completely unchanged
  • State management (loadState/saveState) is unchanged
  • Recovery logic is unchanged
  • Container runtime check is unchanged (ensureContainerSystemRunning)

Design decisions

Double readEnvFile for Slack tokens

main() in index.ts reads SLACK_BOT_TOKEN/SLACK_APP_TOKEN via readEnvFile() to check whether Slack is configured (controls whether to instantiate SlackChannel). The SlackChannel constructor reads them again independently. This is intentional — index.ts needs to decide whether to create the channel, while SlackChannel needs the actual token values. Keeping both reads follows the security pattern of not passing secrets through intermediate variables.

Must-keep

  • The escapeXml and formatMessages re-exports
  • The _setRegisteredGroups test helper
  • The isDirectRun guard at bottom
  • All error handling and cursor rollback logic in processGroupMessages
  • The outgoing queue flush and reconnection logic (in each channel, not here)