diff --git a/docs/agent-tutorial.html b/docs/agent-tutorial.html index 846e0ed..033b62f 100644 --- a/docs/agent-tutorial.html +++ b/docs/agent-tutorial.html @@ -227,5 +227,6 @@ app.listen(3000); + diff --git a/docs/getting-started.html b/docs/getting-started.html index b7ffa31..86721f6 100644 --- a/docs/getting-started.html +++ b/docs/getting-started.html @@ -208,5 +208,6 @@ function switchTab(btn, id) { document.getElementById('tab-' + id).classList.add('active'); } + diff --git a/docs/index.html b/docs/index.html index e6c29c8..1233678 100644 --- a/docs/index.html +++ b/docs/index.html @@ -90,5 +90,6 @@ document.querySelectorAll('.nav-links a').forEach(a => { if (a.href === location.href) a.classList.add('active'); }); + diff --git a/docs/platforms.html b/docs/platforms.html index 8e13069..8f52cc3 100644 --- a/docs/platforms.html +++ b/docs/platforms.html @@ -284,5 +284,6 @@ Search Twitter for tweets mentioning "SquareMCP" Get the latest Telegram messages in my channel + diff --git a/package-lock.json b/package-lock.json index 28ced2d..2aa0a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "hermes-mcp", "version": "1.0.0", "dependencies": { + "@anthropic-ai/sdk": "^0.96.0", "@modelcontextprotocol/sdk": "^1.0.0", "@types/cors": "^2.8.19", "bcryptjs": "^3.0.3", @@ -39,6 +40,27 @@ "vitest": "^4.1.6" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.96.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.96.0.tgz", + "integrity": "sha512-KlCsODtTyb17bLUVCSDC2HtSvAbJf60sEiPEax9dInF+aDF92vS4TZJ5XD7YCQXNb1/5icYaw8Y7wMjPlIV9Zg==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -75,6 +97,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -1334,6 +1365,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2436,6 +2473,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -2873,6 +2916,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4241,6 +4297,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4419,6 +4485,12 @@ "node": ">=0.6" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index 517b7fa..c1d0cc8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:watch": "vitest" }, "dependencies": { + "@anthropic-ai/sdk": "^0.96.0", "@modelcontextprotocol/sdk": "^1.0.0", "@types/cors": "^2.8.19", "bcryptjs": "^3.0.3", diff --git a/product/app/app.js b/product/app/app.js index f74744a..24571e1 100644 --- a/product/app/app.js +++ b/product/app/app.js @@ -103,6 +103,15 @@ const PLATFORM_CONFIG = { ], help: 'From Meta Business Settings → WhatsApp → API Setup. You need a verified business phone number.', }, + slack: { + name: 'Slack', + type: 'token', + fields: [ + { key: 'accessToken', label: 'Bot Token', type: 'password' }, + { key: 'channelId', label: 'Default Channel ID', type: 'text' }, + ], + help: 'Go to api.slack.com/apps → Create App → OAuth & Permissions → install to workspace. Copy the Bot User OAuth Token.', + }, email: { name: 'Email', type: 'token', @@ -403,7 +412,7 @@ async function loadInvoices() { const PLATFORM_COLORS = { email: '#ea4335', linkedin: '#0a66c2', twitter: '#000000', instagram: '#e1306c', facebook: '#1877f2', tiktok: '#010101', - whatsapp: '#25d366', telegram: '#0088cc', discord: '#5865f2', + whatsapp: '#25d366', telegram: '#0088cc', discord: '#5865f2', slack: '#4a154b', snapchat: '#fffc00', obsidian: '#7c3aed', }; diff --git a/product/app/index.html b/product/app/index.html index 83ff784..220c17d 100644 --- a/product/app/index.html +++ b/product/app/index.html @@ -194,6 +194,17 @@ + +
+
💬
+
+

Slack

+

Send messages to channels

+ Not connected +
+ +
+
✉️
@@ -271,5 +282,6 @@ + diff --git a/product/chat-widget.js b/product/chat-widget.js new file mode 100644 index 0000000..b8dfa77 --- /dev/null +++ b/product/chat-widget.js @@ -0,0 +1,180 @@ +(function () { + 'use strict'; + + const API = 'https://hermes.squaremcp.com/api/chat'; + const BRAND = '#6c47ff'; + const BRAND_DARK = '#5535e0'; + + const css = ` + #smcp-btn { + position: fixed; bottom: 24px; right: 24px; z-index: 9998; + width: 56px; height: 56px; border-radius: 50%; + background: ${BRAND}; border: none; cursor: pointer; + box-shadow: 0 4px 16px rgba(0,0,0,0.18); + display: flex; align-items: center; justify-content: center; + transition: background 0.2s; + } + #smcp-btn:hover { background: ${BRAND_DARK}; } + #smcp-btn svg { width: 26px; height: 26px; } + #smcp-window { + position: fixed; bottom: 92px; right: 24px; z-index: 9999; + width: 360px; height: 520px; border-radius: 16px; + background: #fff; box-shadow: 0 8px 40px rgba(0,0,0,0.18); + display: flex; flex-direction: column; overflow: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 14px; + } + #smcp-window.hidden { display: none; } + #smcp-header { + background: ${BRAND}; color: #fff; + padding: 14px 16px; display: flex; align-items: center; gap: 10px; + font-weight: 600; font-size: 15px; flex-shrink: 0; + } + #smcp-header span { flex: 1; } + #smcp-close { + background: none; border: none; color: #fff; cursor: pointer; + font-size: 20px; line-height: 1; padding: 0; + } + #smcp-messages { + flex: 1; overflow-y: auto; padding: 16px; + display: flex; flex-direction: column; gap: 10px; + } + .smcp-msg { + max-width: 80%; padding: 9px 13px; border-radius: 12px; + line-height: 1.45; word-break: break-word; + } + .smcp-msg.bot { + background: #f1f0fe; color: #1a1a2e; border-bottom-left-radius: 4px; align-self: flex-start; + } + .smcp-msg.user { + background: ${BRAND}; color: #fff; border-bottom-right-radius: 4px; align-self: flex-end; + } + .smcp-msg.typing { color: #888; font-style: italic; } + #smcp-input-row { + padding: 10px 12px; border-top: 1px solid #eee; + display: flex; gap: 8px; flex-shrink: 0; + } + #smcp-input { + flex: 1; border: 1px solid #ddd; border-radius: 8px; + padding: 8px 12px; font-size: 14px; outline: none; + font-family: inherit; resize: none; line-height: 1.4; + } + #smcp-input:focus { border-color: ${BRAND}; } + #smcp-send { + background: ${BRAND}; color: #fff; border: none; + border-radius: 8px; padding: 0 14px; cursor: pointer; + font-size: 18px; transition: background 0.2s; + } + #smcp-send:hover { background: ${BRAND_DARK}; } + #smcp-send:disabled { background: #ccc; cursor: default; } + @media (max-width: 420px) { + #smcp-window { width: calc(100vw - 24px); right: 12px; bottom: 80px; } + } + `; + + const WELCOME = "Hi! I'm the SquareMCP assistant. Ask me anything about connecting your AI agent to social platforms."; + + function inject() { + const style = document.createElement('style'); + style.textContent = css; + document.head.appendChild(style); + + document.body.insertAdjacentHTML('beforeend', ` + + + `); + + const win = document.getElementById('smcp-window'); + const btn = document.getElementById('smcp-btn'); + const closeBtn = document.getElementById('smcp-close'); + const messages = document.getElementById('smcp-messages'); + const input = document.getElementById('smcp-input'); + const send = document.getElementById('smcp-send'); + + const history = []; + let busy = false; + + function addMsg(text, role, typing) { + const div = document.createElement('div'); + div.className = 'smcp-msg ' + role + (typing ? ' typing' : ''); + div.textContent = text; + messages.appendChild(div); + messages.scrollTop = messages.scrollHeight; + return div; + } + + addMsg(WELCOME, 'bot'); + + btn.addEventListener('click', () => { + win.classList.toggle('hidden'); + if (!win.classList.contains('hidden')) input.focus(); + }); + closeBtn.addEventListener('click', () => win.classList.add('hidden')); + + async function submit() { + const text = input.value.trim(); + if (!text || busy) return; + busy = true; + send.disabled = true; + input.value = ''; + input.style.height = ''; + + addMsg(text, 'user'); + history.push({ role: 'user', content: text }); + + const indicator = addMsg('Typing…', 'bot', true); + + try { + const res = await fetch(API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: history }), + }); + const data = await res.json(); + const reply = data.reply || 'Sorry, something went wrong. Try again.'; + indicator.textContent = reply; + indicator.classList.remove('typing'); + history.push({ role: 'assistant', content: reply }); + } catch { + indicator.textContent = 'Network error. Please try again.'; + indicator.classList.remove('typing'); + } + + busy = false; + send.disabled = false; + messages.scrollTop = messages.scrollHeight; + } + + send.addEventListener('click', submit); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); } + }); + input.addEventListener('input', () => { + input.style.height = ''; + input.style.height = Math.min(input.scrollHeight, 96) + 'px'; + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', inject); + } else { + inject(); + } +})(); diff --git a/product/site/index.html b/product/site/index.html index 2b441ec..9fb7e08 100644 --- a/product/site/index.html +++ b/product/site/index.html @@ -301,5 +301,6 @@ - + + diff --git a/src/chat.ts b/src/chat.ts new file mode 100644 index 0000000..74f1e31 --- /dev/null +++ b/src/chat.ts @@ -0,0 +1,49 @@ +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +const SYSTEM_PROMPT = `You are a friendly and knowledgeable support assistant for SquareMCP — an AI Social Media Gateway that lets AI agents (Claude, ChatGPT, Codex CLI, etc.) post to social platforms via the Model Context Protocol (MCP). + +What SquareMCP does: +- Connects AI coding assistants to social platforms: LinkedIn, TikTok, WhatsApp, Instagram, Twitter/X, Facebook, Telegram, Discord, Slack, and Email +- Works with any MCP-compatible client: Claude Desktop, Claude Code, Cursor, Windsurf, opencode, Codex CLI +- Provides a multi-tenant SaaS platform where each customer securely stores their own platform credentials +- Offers a simple dashboard to connect platforms, view usage, and manage webhooks +- Plans: Free (100 calls/month), Pro, Business + +How it works: +1. Customer signs up at the SquareMCP dashboard +2. Connects their social accounts (bot tokens, API keys) +3. Adds the SquareMCP MCP server to their AI client config +4. Their AI agent can now send messages, post content, read analytics — on any connected platform + +Common questions: +- Pricing: Free tier available, paid plans for higher usage +- Supported platforms: LinkedIn, TikTok, WhatsApp Business, Instagram, Twitter/X, Facebook, Telegram, Discord, Slack, Email +- No coding required to use the dashboard; coding experience helps to get the most from MCP tool calls +- Webhooks: customers can receive real-time events when messages arrive +- Security: credentials are encrypted at rest, each customer's data is isolated + +Keep answers concise (2-4 sentences max). If you don't know something, say so and suggest emailing support@squaremcp.com. Never make up features or pricing. Speak in a warm, direct tone.`; + +export interface ChatMessage { + role: 'user' | 'assistant'; + content: string; +} + +export async function handleChat(messages: ChatMessage[]): Promise { + if (!process.env.ANTHROPIC_API_KEY) { + throw new Error('ANTHROPIC_API_KEY not configured'); + } + + const response = await client.messages.create({ + model: 'claude-haiku-4-5-20251001', + max_tokens: 512, + system: SYSTEM_PROMPT, + messages: messages.map(m => ({ role: m.role, content: m.content })), + }); + + const block = response.content[0]; + if (block.type !== 'text') throw new Error('Unexpected response type'); + return block.text; +} diff --git a/src/clients/slack.ts b/src/clients/slack.ts new file mode 100644 index 0000000..a20bbe6 --- /dev/null +++ b/src/clients/slack.ts @@ -0,0 +1,113 @@ +import type { Customer } from '../billing/middleware.js'; +import type { OAuthCredentials } from '../multitenancy/credential-store.js'; +import { createToolAudit } from '../multitenancy/audit-log.js'; + +const SLACK_API_BASE = 'https://slack.com/api'; + +interface SlackCredentials extends OAuthCredentials { + channelId?: string; +} + +function getEnvToken(account: string): string { + const envKey = `SLACK_${account.toUpperCase()}_BOT_TOKEN`; + return process.env[envKey] ?? ''; +} + +async function resolveCredentials( + args: { account?: string }, + customer?: Customer +): Promise<{ token: string; defaultChannelId?: string }> { + if (customer) { + const creds = await customer.getCredential('slack'); + if (!creds) throw new Error('Slack not connected for this account'); + return { token: creds.accessToken, defaultChannelId: creds.channelId }; + } + const token = getEnvToken(args.account ?? 'default'); + if (!token) throw new Error('Missing Slack credentials. Set SLACK_{ACCOUNT}_BOT_TOKEN'); + return { token }; +} + +async function slackRequest(token: string, method: string, body?: Record) { + const res = await fetch(`${SLACK_API_BASE}/${method}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(15000), + }); + + if (!res.ok) { + throw new Error(`Slack HTTP error (${res.status})`); + } + + const data = await res.json() as { ok: boolean; error?: string; [key: string]: unknown }; + if (!data.ok) { + throw new Error(`Slack API error: ${data.error}`); + } + + return data; +} + +export async function getMe( + args: { account?: string }, + customer?: Customer +): Promise<{ user_id: string; user: string; team: string; team_id: string }> { + const { token } = await resolveCredentials(args, customer); + const data = await slackRequest(token, 'auth.test'); + return { + user_id: data.user_id as string, + user: data.user as string, + team: data.team as string, + team_id: data.team_id as string, + }; +} + +export async function getChannels( + args: { limit?: number; account?: string }, + customer?: Customer +): Promise> { + const { token } = await resolveCredentials(args, customer); + const data = await slackRequest(token, 'conversations.list', { + limit: args.limit ?? 100, + types: 'public_channel,private_channel', + exclude_archived: true, + }); + const channels = data.channels as Array<{ id: string; name: string; is_member: boolean; num_members: number }>; + return channels.map(c => ({ id: c.id, name: c.name, is_member: c.is_member, num_members: c.num_members })); +} + +export async function sendMessage( + args: { channel_id?: string; text: string; account?: string }, + customer?: Customer +): Promise<{ ts: string; channel: string }> { + const { token, defaultChannelId } = await resolveCredentials(args, customer); + const channel = args.channel_id ?? defaultChannelId; + if (!channel) throw new Error('channel_id is required (or set a default channel when connecting)'); + + const audit = customer ? createToolAudit(customer.id, 'slack:sendMessage') : null; + const auditArgs = { channel }; + + try { + const data = await slackRequest(token, 'chat.postMessage', { channel, text: args.text }); + if (audit) await audit.success(auditArgs); + return { ts: data.ts as string, channel: data.channel as string }; + } catch (err) { + if (audit) await audit.error(auditArgs, String(err)); + throw err; + } +} + +export async function getMessages( + args: { channel_id: string; limit?: number; account?: string }, + customer?: Customer +): Promise> { + const { token } = await resolveCredentials(args, customer); + const data = await slackRequest(token, 'conversations.history', { + channel: args.channel_id, + limit: args.limit ?? 10, + }); + const messages = data.messages as Array<{ ts: string; text: string; user: string }>; + return messages.map(m => ({ ts: m.ts, text: m.text, user: m.user })); +} diff --git a/src/index.ts b/src/index.ts index b573b9d..1babe85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ import { getAllPlatformHealth } from './multitenancy/platform-health.js'; import { deliverWebhook, isValidWebhookUrl } from './webhooks/delivery.js'; import { notifyNewPilotRequest } from './notifications/index.js'; import redis from './redis.js'; +import { handleChat, type ChatMessage } from './chat.js'; const app = express(); app.use(cookieParser()); @@ -57,6 +58,7 @@ app.use(express.urlencoded({ extended: true })); // ── Static files (videos, assets) ────────────────────────────────────────── app.use('/public', express.static('/vaults/public')); +app.use(express.static(new URL('../../product', import.meta.url).pathname)); // ── Config ───────────────────────────────────────────────────────────────── const PORT = process.env.PORT ?? 3456; @@ -1978,6 +1980,22 @@ app.post('/api/facebook/video', requireAuth, async (req, res) => { } }); +// ── Chat widget endpoint ──────────────────────────────────────── +app.post('/api/chat', async (req, res) => { + const { messages } = req.body as { messages?: ChatMessage[] }; + if (!Array.isArray(messages) || messages.length === 0) { + res.status(400).json({ error: 'messages array required' }); + return; + } + try { + const reply = await handleChat(messages); + res.json({ reply }); + } catch (err) { + console.error('[chat] error:', (err as Error).message); + res.status(500).json({ error: 'Chat unavailable' }); + } +}); + // ── TikTok REST endpoints ─────────────────────────────────────── app.get('/api/tiktok/profile', requireAuth, async (req, res) => { const account = req.query.account as string | undefined; diff --git a/src/multitenancy/credential-store.ts b/src/multitenancy/credential-store.ts index fceec69..980bf1c 100644 --- a/src/multitenancy/credential-store.ts +++ b/src/multitenancy/credential-store.ts @@ -23,7 +23,7 @@ function decrypt(ciphertext: string): string { return decipher.update(encrypted) + decipher.final('utf8'); } -export type Platform = 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'discord' | 'instagram' | 'twitter' | 'tiktok' | 'snapchat' | 'facebook' | 'obsidian'; +export type Platform = 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'discord' | 'instagram' | 'twitter' | 'tiktok' | 'snapchat' | 'facebook' | 'obsidian' | 'slack'; export interface EmailCredentials { host: string; diff --git a/src/tools.ts b/src/tools.ts index 747a52a..300be63 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -14,6 +14,7 @@ import { searchTweets, getUserProfile, getUserTweets, createTweet, uploadVideoAn import { getUserProfile as getTikTokProfile, getCreatorInfo, createVideo, getVideoStatus } from './clients/tiktok.js'; import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js'; import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js'; +import { getMe as getSlackMe, getChannels as getSlackChannels, sendMessage as sendSlackMessage, getMessages as getSlackMessages } from './clients/slack.js'; const ACCOUNT_PARAM = { account: { @@ -443,6 +444,59 @@ export const tools: Tool[] = [ }, }, + // ── Slack tools ────────────────────────────────────────────── + { + name: 'slack_get_me', + description: + 'Verify the Slack bot is connected and return workspace info. Use to confirm credentials are working.', + inputSchema: { + type: 'object', + properties: { + account: { type: 'string', description: 'Which Slack account to use (default: "default")' }, + }, + }, + }, + { + name: 'slack_get_channels', + description: + 'List Slack channels the bot has access to. Use to find channel IDs before sending messages.', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Max channels to return (default: 100)' }, + account: { type: 'string', description: 'Which Slack account to use (default: "default")' }, + }, + }, + }, + { + name: 'slack_send_message', + description: + 'Send a message to a Slack channel. Uses the default channel if channel_id is omitted.', + inputSchema: { + type: 'object', + required: ['text'], + properties: { + channel_id: { type: 'string', description: 'Slack channel ID (e.g. C0123456). Uses default channel if omitted.' }, + text: { type: 'string', description: 'Message text. Supports Slack mrkdwn formatting.' }, + account: { type: 'string', description: 'Which Slack account to use (default: "default")' }, + }, + }, + }, + { + name: 'slack_get_messages', + description: + 'Get recent messages from a Slack channel.', + inputSchema: { + type: 'object', + required: ['channel_id'], + properties: { + channel_id: { type: 'string', description: 'Slack channel ID' }, + limit: { type: 'number', description: 'Max messages to return (default: 10)' }, + account: { type: 'string', description: 'Which Slack account to use (default: "default")' }, + }, + }, + }, + // ── Instagram tools ────────────────────────────────────────── { name: 'instagram_get_profile', @@ -738,7 +792,7 @@ async function resolveEmailCtx(args: Record, customer?: Custome const PLATFORM_PREFIXES = [ 'linkedin', 'obsidian', 'whatsapp', 'telegram', 'discord', - 'instagram', 'twitter', 'tiktok', 'snapchat', 'facebook', + 'instagram', 'twitter', 'tiktok', 'snapchat', 'facebook', 'slack', ]; function toolPlatform(name: string): string { @@ -984,6 +1038,36 @@ export async function handleToolCall( }, customer); break; + // ── Slack ─────────────────────────────────────────────────── + case 'slack_get_me': + result = await getSlackMe({ + account: args.account as string | undefined, + }, customer); + break; + + case 'slack_get_channels': + result = await getSlackChannels({ + limit: args.limit as number | undefined, + account: args.account as string | undefined, + }, customer); + break; + + case 'slack_send_message': + result = await sendSlackMessage({ + channel_id: args.channel_id as string | undefined, + text: args.text as string, + account: args.account as string | undefined, + }, customer); + break; + + case 'slack_get_messages': + result = await getSlackMessages({ + channel_id: args.channel_id as string, + limit: args.limit as number | undefined, + account: args.account as string | undefined, + }, customer); + break; + // ── Instagram ─────────────────────────────────────────────── case 'instagram_get_profile': result = await getInstagramProfile({