feat: Slack platform + Claude-powered chat support widget

- Add Slack as customer-facing messaging platform (client, 4 MCP tools, dashboard card)
- Add /api/chat endpoint powered by Claude Haiku with SquareMCP system prompt
- Add embeddable chat-widget.js injected into all 3 sites (docs, app, www)
- Add ANTHROPIC_API_KEY, serve product/ as static files
- Update Platform type to include slack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garfield
2026-05-15 10:44:24 -04:00
parent 05b4a30759
commit 4bf93d6763
15 changed files with 547 additions and 4 deletions

View File

@@ -227,5 +227,6 @@ app.<span class="fn">listen</span>(<span class="num">3000</span>);</code></pre>
</div>
</div>
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
</body>
</html>

View File

@@ -208,5 +208,6 @@ function switchTab(btn, id) {
document.getElementById('tab-' + id).classList.add('active');
}
</script>
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
</body>
</html>

View File

@@ -90,5 +90,6 @@ document.querySelectorAll('.nav-links a').forEach(a => {
if (a.href === location.href) a.classList.add('active');
});
</script>
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
</body>
</html>

View File

@@ -284,5 +284,6 @@ Search Twitter for tweets mentioning "SquareMCP"</code></pre>
Get the latest Telegram messages in my channel</code></pre>
</div>
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
</body>
</html>

72
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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',
};

View File

@@ -194,6 +194,17 @@
<button class="btn btn-connect" data-platform="whatsapp">Connect</button>
</div>
<!-- Slack -->
<div class="platform-card" data-platform="slack">
<div class="platform-icon" style="background:#4a154b;">💬</div>
<div class="platform-info">
<h3>Slack</h3>
<p class="platform-desc">Send messages to channels</p>
<span class="status-badge disconnected">Not connected</span>
</div>
<button class="btn btn-connect" data-platform="slack">Connect</button>
</div>
<!-- Email -->
<div class="platform-card" data-platform="email">
<div class="platform-icon" style="background:#ea4335;">✉️</div>
@@ -271,5 +282,6 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script src="app.js"></script>
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
</body>
</html>

180
product/chat-widget.js Normal file
View File

@@ -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', `
<button id="smcp-btn" aria-label="Chat with SquareMCP">
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</button>
<div id="smcp-window" class="hidden">
<div id="smcp-header">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<span>SquareMCP Support</span>
<button id="smcp-close">&times;</button>
</div>
<div id="smcp-messages"></div>
<div id="smcp-input-row">
<textarea id="smcp-input" rows="1" placeholder="Ask a question…"></textarea>
<button id="smcp-send">&#9658;</button>
</div>
</div>
`);
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();
}
})();

View File

@@ -301,5 +301,6 @@
</footer>
<script src="./script.js?v=20260424b"></script>
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
</body>
</html>

49
src/chat.ts Normal file
View File

@@ -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<string> {
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;
}

113
src/clients/slack.ts Normal file
View File

@@ -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<SlackCredentials>('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<string, unknown>) {
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<Array<{ id: string; name: string; is_member: boolean; num_members: number }>> {
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<Array<{ ts: string; text: string; user: string }>> {
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 }));
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<string, unknown>, 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({