- Add src/multitenancy/ with AES-256-GCM credential store, WhatsApp webhook router (phone_number_id -> customerId), and per-customer audit log (90-day Redis TTL) - Add src/billing/ with plan definitions and meterMiddleware that resolves API key -> Customer object with getCredential() closure - Refactor all src/clients/* to accept optional customer param, falling back to env vars for backward compat with single-user mode - Thread customer through handleToolCall(name, args, customer?) - Add customers table to MySQL schema initDatabase() - Add /webhook/whatsapp (immediate 200 + async routing) and /api/connect/* onboarding endpoints to index.ts - Add Redis 7 to docker-compose.yml; add REDIS_URL and CREDENTIAL_ENCRYPTION_KEY to hermes-k8s.yaml - Add product/incubation/ with architecture write-up and PlantUML diagrams (system architecture + 5 user flows) - Extend OpenAPI spec in manifest.ts with all platform endpoints Verification: 3 isolation tests (credential, webhook routing, audit log) passed against live Redis. Deployed to hermes.squaremcp.com. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
820 lines
31 KiB
TypeScript
820 lines
31 KiB
TypeScript
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
import type { Customer } from './billing/middleware.js';
|
|
import { searchMessages, readMessage, getProfile, listFolders, type Account } from './imap.js';
|
|
import { sendEmail, createDraft } from './smtp.js';
|
|
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js';
|
|
import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './clients/whatsapp.js';
|
|
import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js';
|
|
import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto as sendTelegramPhoto, getUpdates as getTelegramUpdates, getChat as getTelegramChat } from './clients/telegram.js';
|
|
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
|
|
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createPost as createInstagramPost } from './clients/instagram.js';
|
|
import { searchTweets, getUserProfile, getUserTweets, createTweet } from './clients/twitter.js';
|
|
|
|
const ACCOUNT_PARAM = {
|
|
account: {
|
|
type: 'string',
|
|
enum: ['yahoo', 'fetcherpay', 'garfield', 'sales', 'leads', 'founder', 'gmail'],
|
|
description: 'Which mailbox to use: "yahoo" (gheron01@yahoo.com), "fetcherpay" (garfield.heron@fetcherpay.com), "garfield" (garfield@fetcherpay.com), "sales" (sales@fetcherpay.com), "leads" (leads@fetcherpay.com), "founder" (founder@fetcherpay.com), or "gmail" (Gmail account). Defaults to "yahoo".',
|
|
},
|
|
};
|
|
|
|
export const tools: Tool[] = [
|
|
{
|
|
name: 'get_profile',
|
|
description: 'Get the email account profile (email address and name)',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { ...ACCOUNT_PARAM },
|
|
},
|
|
},
|
|
{
|
|
name: 'search_messages',
|
|
description: 'Search email messages by keyword, sender, or subject',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
q: { type: 'string', description: 'Search query (keyword, from:email, subject:text)' },
|
|
maxResults: { type: 'number', description: 'Max messages to return (default 20)' },
|
|
folder: { type: 'string', description: 'Folder to search (default INBOX). For Gmail, [Gmail]/All Mail searches everywhere.' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['q'],
|
|
},
|
|
},
|
|
{
|
|
name: 'read_message',
|
|
description: 'Read a full email message by UID',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
uid: { type: 'number', description: 'Message UID from search results' },
|
|
folder: { type: 'string', description: 'Folder the message was found in (default INBOX). Use the folder value from search_results.' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['uid'],
|
|
},
|
|
},
|
|
{
|
|
name: 'list_folders',
|
|
description: 'List all email folders/mailboxes',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { ...ACCOUNT_PARAM },
|
|
},
|
|
},
|
|
{
|
|
name: 'create_draft',
|
|
description: 'Create a draft email',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
to: { type: 'string', description: 'Recipient email address' },
|
|
subject: { type: 'string', description: 'Email subject' },
|
|
body: { type: 'string', description: 'Email body (plain text)' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['to', 'subject', 'body'],
|
|
},
|
|
},
|
|
{
|
|
name: 'send_email',
|
|
description: 'Send an email',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
to: { type: 'string', description: 'Recipient email address' },
|
|
subject: { type: 'string', description: 'Email subject' },
|
|
body: { type: 'string', description: 'Email body (plain text)' },
|
|
...ACCOUNT_PARAM,
|
|
},
|
|
required: ['to', 'subject', 'body'],
|
|
},
|
|
},
|
|
|
|
// ── Obsidian tools ────────────────────────────────────────────────────────
|
|
{
|
|
name: 'obsidian_search_notes',
|
|
description:
|
|
'Search across the Obsidian vault by content, tags, or title. Use when the user references "my notes", "in obsidian", "I wrote about", or needs personal knowledge retrieval. Returns note paths relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Always use these exact relative paths when calling obsidian_read_note or obsidian_append_to_note.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: { type: 'string', description: 'Text to search for in note content or title' },
|
|
tags: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'Filter by Obsidian tags (all must match)',
|
|
},
|
|
limit: { type: 'number', description: 'Max results to return (default 10)' },
|
|
path_filter: {
|
|
type: 'string',
|
|
description: 'Only return notes whose relative vault path contains this string (e.g. "Daily Notes")',
|
|
},
|
|
},
|
|
required: ['query'],
|
|
},
|
|
},
|
|
{
|
|
name: 'obsidian_read_note',
|
|
description:
|
|
'Retrieve the full content of a specific Obsidian note by relative vault path. Use when the user asks to read, open, or show a specific note. The path must be relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Do not use absolute filesystem paths.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'File path relative to the vault root (e.g. "Daily Notes/2026-04-15.md") or just the note title/filename',
|
|
},
|
|
},
|
|
required: ['path'],
|
|
},
|
|
},
|
|
{
|
|
name: 'obsidian_append_to_note',
|
|
description:
|
|
'Append markdown content to an Obsidian daily note or any specific vault note. Use for logging, journaling, capturing insights from email, or recording action items in your notes. Creates the note if it does not exist. The path must be relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Do not use absolute filesystem paths.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'File path relative to the vault root (e.g. "Daily Notes/2026-04-15.md")',
|
|
},
|
|
content: { type: 'string', description: 'Markdown content to append' },
|
|
create_if_missing: {
|
|
type: 'boolean',
|
|
description: 'Create the note if it does not exist (default true)',
|
|
},
|
|
header: {
|
|
type: 'string',
|
|
description: 'Optional H2 section header to add before the content',
|
|
},
|
|
},
|
|
required: ['path', 'content'],
|
|
},
|
|
},
|
|
{
|
|
name: 'obsidian_update_note',
|
|
description:
|
|
'Overwrite the entire content of an Obsidian note with new markdown. Use when you need to replace, rewrite, or structurally edit an existing note rather than append to it. Creates the note if it does not exist.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
path: {
|
|
type: 'string',
|
|
description: 'File path relative to vault root (e.g. "Daily Notes/2026-04-15.md")',
|
|
},
|
|
content: {
|
|
type: 'string',
|
|
description: 'Full markdown content to write — replaces existing content entirely',
|
|
},
|
|
},
|
|
required: ['path', 'content'],
|
|
},
|
|
},
|
|
{
|
|
name: 'obsidian_sync_status',
|
|
description:
|
|
'Check the sync status of the Obsidian vault via Syncthing. Use when the user asks if their notes are synced or up to date.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {},
|
|
},
|
|
},
|
|
|
|
// ── WhatsApp Business API tools ────────────────────────────────
|
|
{
|
|
name: 'whatsapp_send_message',
|
|
description:
|
|
'Send a WhatsApp message to a phone number. Use when the user asks to send a WhatsApp message, text someone on WhatsApp, or notify via WhatsApp.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
to: { type: 'string', description: 'Recipient phone number in international format (e.g. +1234567890)' },
|
|
message: { type: 'string', description: 'Message text to send' },
|
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
|
},
|
|
required: ['to', 'message'],
|
|
},
|
|
},
|
|
{
|
|
name: 'whatsapp_send_template',
|
|
description:
|
|
'Send a WhatsApp template message (for approved templates). Use when sending structured notifications or alerts via WhatsApp templates.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
to: { type: 'string', description: 'Recipient phone number in international format' },
|
|
template_name: { type: 'string', description: 'Name of the approved WhatsApp template' },
|
|
language: { type: 'string', description: 'Template language code (default: "en")' },
|
|
components: { type: 'array', items: { type: 'object' }, description: 'Template components (header, body, buttons) with parameters' },
|
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
|
},
|
|
required: ['to', 'template_name'],
|
|
},
|
|
},
|
|
{
|
|
name: 'whatsapp_get_message_status',
|
|
description:
|
|
'[DEPRECATED] Meta Cloud API does not support polling message status. Status updates are only available via webhooks.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
message_id: { type: 'string', description: 'WhatsApp message ID (not used - webhook required)' },
|
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'whatsapp_list_templates',
|
|
description:
|
|
'List all approved WhatsApp message templates for the business account. Use when the user asks what templates are available.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
|
|
// ── LinkedIn tools ─────────────────────────────────────────────
|
|
{
|
|
name: 'linkedin_get_profile',
|
|
description:
|
|
'Get the LinkedIn profile of the authenticated user. Use when the user asks about their LinkedIn profile, name, or headline.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'linkedin_create_post',
|
|
description:
|
|
'Create a post on LinkedIn. Use when the user wants to publish an update, article, or share content on LinkedIn.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
text: { type: 'string', description: 'Post content text' },
|
|
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'Visibility: PUBLIC (anyone) or CONNECTIONS (1st degree only). Default: PUBLIC' },
|
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
|
},
|
|
required: ['text'],
|
|
},
|
|
},
|
|
{
|
|
name: 'linkedin_search_connections',
|
|
description:
|
|
'Search LinkedIn connections. [REQUIRES PARTNERSHIP] LinkedIn removed public API access to connections. This tool will guide you to apply for the Partnership Program.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
keywords: { type: 'string', description: 'Search keywords for connections' },
|
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'linkedin_send_message',
|
|
description:
|
|
'Send a direct message on LinkedIn. [REQUIRES PARTNERSHIP] LinkedIn messaging is not available through the public API. This tool will guide you to apply for the Partnership Program.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
recipient_id: { type: 'string', description: 'LinkedIn person URN or ID of the recipient' },
|
|
message: { type: 'string', description: 'Message text to send' },
|
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
|
},
|
|
required: ['recipient_id', 'message'],
|
|
},
|
|
},
|
|
|
|
// ── Telegram tools ─────────────────────────────────────────────
|
|
{
|
|
name: 'telegram_get_me',
|
|
description:
|
|
'Get information about the Telegram bot. Use to verify the bot is connected and get its username.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'telegram_send_message',
|
|
description:
|
|
'Send a text message via Telegram bot. Use when the user asks to send a Telegram message, DM someone, or notify a group/channel.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['chat_id', 'text'],
|
|
properties: {
|
|
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
|
|
text: { type: 'string', description: 'Message text to send' },
|
|
parse_mode: { type: 'string', enum: ['HTML', 'Markdown', 'MarkdownV2'], description: 'Formatting mode for the message' },
|
|
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'telegram_send_photo',
|
|
description:
|
|
'Send a photo via Telegram bot. Use when the user wants to share an image through Telegram.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['chat_id', 'photo'],
|
|
properties: {
|
|
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
|
|
photo: { type: 'string', description: 'Photo URL or file_id to send' },
|
|
caption: { type: 'string', description: 'Optional caption text' },
|
|
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'telegram_get_updates',
|
|
description:
|
|
'Get recent incoming messages and updates for the Telegram bot. Use when the user asks to check messages or read Telegram DMs.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
limit: { type: 'number', description: 'Max number of updates to return (default: 10)' },
|
|
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'telegram_get_chat',
|
|
description:
|
|
'Get information about a Telegram chat or channel. Use to verify a chat exists and get its details.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['chat_id'],
|
|
properties: {
|
|
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
|
|
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
|
|
// ── Discord tools ────────────────────────────────────────────
|
|
{
|
|
name: 'discord_get_me',
|
|
description:
|
|
'Get information about the Discord bot. Use to verify the bot is connected and get its username.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'discord_get_guilds',
|
|
description:
|
|
'List Discord servers (guilds) the bot is a member of. Use to find server IDs for sending messages.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'discord_get_channels',
|
|
description:
|
|
'List channels in a Discord server. Use to find channel IDs for sending messages.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['guild_id'],
|
|
properties: {
|
|
guild_id: { type: 'string', description: 'Discord server (guild) ID' },
|
|
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'discord_send_message',
|
|
description:
|
|
'Send a message to a Discord channel. Use when the user wants to post in a Discord server.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['channel_id', 'content'],
|
|
properties: {
|
|
channel_id: { type: 'string', description: 'Discord channel ID' },
|
|
content: { type: 'string', description: 'Message content (up to 2000 chars)' },
|
|
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'discord_get_messages',
|
|
description:
|
|
'Get recent messages from a Discord channel. Use to read chat history.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['channel_id'],
|
|
properties: {
|
|
channel_id: { type: 'string', description: 'Discord channel ID' },
|
|
limit: { type: 'number', description: 'Max messages to return (default: 10, max: 100)' },
|
|
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
|
|
// ── Instagram tools ──────────────────────────────────────────
|
|
{
|
|
name: 'instagram_get_profile',
|
|
description:
|
|
'Get Instagram Business/Creator account profile info. Use when the user asks about their Instagram stats, followers, or account details.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'instagram_get_media',
|
|
description:
|
|
'Get recent posts from an Instagram Business/Creator account. Use when the user wants to see their recent content.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
limit: { type: 'number', description: 'Max posts to return (default: 10)' },
|
|
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'instagram_create_post',
|
|
description:
|
|
'Create a post on Instagram. [REQUIRES BUSINESS ACCOUNT] Only works with Instagram Business/Creator accounts connected to a Facebook Page.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['image_url'],
|
|
properties: {
|
|
image_url: { type: 'string', description: 'Publicly accessible URL of the image to post' },
|
|
caption: { type: 'string', description: 'Post caption text' },
|
|
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
|
|
// ── Twitter/X tools ──────────────────────────────────────────
|
|
{
|
|
name: 'twitter_search_tweets',
|
|
description:
|
|
'Search recent tweets on Twitter/X. Use when the user wants to find tweets about a topic, hashtag, or keyword.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['query'],
|
|
properties: {
|
|
query: { type: 'string', description: 'Search query (keyword, hashtag, or phrase)' },
|
|
max_results: { type: 'number', description: 'Max tweets to return (default: 10, max: 100)' },
|
|
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'twitter_get_user_profile',
|
|
description:
|
|
'Get a Twitter/X user profile. Use when the user wants stats, bio, or follower count for a specific account.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['username'],
|
|
properties: {
|
|
username: { type: 'string', description: 'Twitter username (without @)' },
|
|
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'twitter_get_user_tweets',
|
|
description:
|
|
'Get recent tweets from a specific Twitter/X user. Use to read someones timeline.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['username'],
|
|
properties: {
|
|
username: { type: 'string', description: 'Twitter username (without @)' },
|
|
max_results: { type: 'number', description: 'Max tweets (default: 10, max: 100)' },
|
|
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'twitter_create_tweet',
|
|
description:
|
|
'Post a tweet on Twitter/X. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
required: ['text'],
|
|
properties: {
|
|
text: { type: 'string', description: 'Tweet text (max 280 chars)' },
|
|
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
function acct(args: Record<string, unknown>): Account {
|
|
return (args.account as Account) ?? 'yahoo';
|
|
}
|
|
|
|
export async function handleToolCall(
|
|
name: string,
|
|
args: Record<string, unknown>,
|
|
customer?: Customer
|
|
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
console.log(`[tool] ${name}`, JSON.stringify(args));
|
|
const t0 = Date.now();
|
|
try {
|
|
let result: unknown;
|
|
|
|
switch (name) {
|
|
case 'get_profile':
|
|
result = await getProfile(acct(args));
|
|
break;
|
|
|
|
case 'search_messages':
|
|
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, acct(args), args.folder as string | undefined);
|
|
break;
|
|
|
|
case 'read_message':
|
|
result = await readMessage(args.uid as number, acct(args), args.folder as string | undefined);
|
|
break;
|
|
|
|
case 'list_folders':
|
|
result = await listFolders(acct(args));
|
|
break;
|
|
|
|
case 'create_draft':
|
|
result = await createDraft(args.to as string, args.subject as string, args.body as string, acct(args));
|
|
break;
|
|
|
|
case 'send_email':
|
|
result = await sendEmail(args.to as string, args.subject as string, args.body as string, acct(args));
|
|
break;
|
|
|
|
// ── Obsidian ──────────────────────────────────────────────────────────
|
|
case 'obsidian_search_notes':
|
|
result = await searchNotes(
|
|
args.query as string,
|
|
args.tags as string[] | undefined,
|
|
(args.limit as number) ?? 10,
|
|
args.path_filter as string | undefined
|
|
);
|
|
break;
|
|
|
|
case 'obsidian_read_note':
|
|
result = await getNote(args.path as string);
|
|
break;
|
|
|
|
case 'obsidian_append_to_note':
|
|
result = await appendToNote(
|
|
args.path as string,
|
|
args.content as string,
|
|
(args.create_if_missing as boolean) ?? true,
|
|
args.header as string | undefined
|
|
);
|
|
break;
|
|
|
|
case 'obsidian_update_note':
|
|
result = await updateNote(args.path as string, args.content as string);
|
|
break;
|
|
|
|
case 'obsidian_sync_status':
|
|
result = await getSyncStatus();
|
|
break;
|
|
|
|
// ── WhatsApp Business API ──────────────────────────────────
|
|
case 'whatsapp_send_message':
|
|
result = await sendMessage({
|
|
to: args.to as string,
|
|
message: args.message as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'whatsapp_send_template':
|
|
result = await sendTemplate({
|
|
to: args.to as string,
|
|
template_name: args.template_name as string,
|
|
language: args.language as string | undefined,
|
|
components: args.components as unknown[] | undefined,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'whatsapp_get_message_status':
|
|
result = await getMessageStatus({
|
|
message_id: args.message_id as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'whatsapp_list_templates':
|
|
result = await listTemplates({
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
// ── LinkedIn ───────────────────────────────────────────────
|
|
case 'linkedin_get_profile':
|
|
result = await getLinkedInProfile({
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'linkedin_create_post':
|
|
result = await createLinkedInPost({
|
|
text: args.text as string,
|
|
visibility: (args.visibility as 'PUBLIC' | 'CONNECTIONS') ?? 'PUBLIC',
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'linkedin_search_connections':
|
|
result = await searchConnections({
|
|
keywords: args.keywords as string | undefined,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'linkedin_send_message':
|
|
result = await sendLinkedInMessage({
|
|
recipient_id: args.recipient_id as string,
|
|
message: args.message as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
// ── Telegram ───────────────────────────────────────────────
|
|
case 'telegram_get_me':
|
|
result = await getTelegramMe({
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'telegram_send_message':
|
|
result = await sendTelegramMessage({
|
|
chat_id: args.chat_id as string | number,
|
|
text: args.text as string,
|
|
parse_mode: (args.parse_mode as 'HTML' | 'Markdown' | 'MarkdownV2') ?? undefined,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'telegram_send_photo':
|
|
result = await sendTelegramPhoto({
|
|
chat_id: args.chat_id as string | number,
|
|
photo: args.photo as string,
|
|
caption: args.caption as string | undefined,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'telegram_get_updates':
|
|
result = await getTelegramUpdates({
|
|
limit: (args.limit as number) ?? 10,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'telegram_get_chat':
|
|
result = await getTelegramChat({
|
|
chat_id: args.chat_id as string | number,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
// ── Discord ─────────────────────────────────────────────────
|
|
case 'discord_get_me':
|
|
result = await getDiscordMe({
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'discord_get_guilds':
|
|
result = await getGuilds({
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'discord_get_channels':
|
|
result = await getChannels({
|
|
guild_id: args.guild_id as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'discord_send_message':
|
|
result = await sendDiscordMessage({
|
|
channel_id: args.channel_id as string,
|
|
content: args.content as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'discord_get_messages':
|
|
result = await getDiscordMessages({
|
|
channel_id: args.channel_id as string,
|
|
limit: (args.limit as number) ?? 10,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
// ── Instagram ───────────────────────────────────────────────
|
|
case 'instagram_get_profile':
|
|
result = await getInstagramProfile({
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'instagram_get_media':
|
|
result = await getInstagramMedia({
|
|
limit: (args.limit as number) ?? 10,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'instagram_create_post':
|
|
result = await createInstagramPost({
|
|
image_url: args.image_url as string,
|
|
caption: args.caption as string | undefined,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
// ── Twitter/X ───────────────────────────────────────────────
|
|
case 'twitter_search_tweets':
|
|
result = await searchTweets({
|
|
query: args.query as string,
|
|
max_results: (args.max_results as number) ?? 10,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'twitter_get_user_profile':
|
|
result = await getUserProfile({
|
|
username: args.username as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'twitter_get_user_tweets':
|
|
result = await getUserTweets({
|
|
username: args.username as string,
|
|
max_results: (args.max_results as number) ?? 10,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
case 'twitter_create_tweet':
|
|
result = await createTweet({
|
|
text: args.text as string,
|
|
account: args.account as string | undefined,
|
|
}, customer);
|
|
break;
|
|
|
|
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
|
case 'yahoo_get_profile':
|
|
result = await getProfile('yahoo');
|
|
break;
|
|
case 'yahoo_search_messages':
|
|
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, 'yahoo');
|
|
break;
|
|
case 'yahoo_read_message':
|
|
result = await readMessage(args.uid as number, 'yahoo');
|
|
break;
|
|
case 'yahoo_list_folders':
|
|
result = await listFolders('yahoo');
|
|
break;
|
|
case 'yahoo_create_draft':
|
|
result = await createDraft(args.to as string, args.subject as string, args.body as string, 'yahoo');
|
|
break;
|
|
case 'yahoo_send_email':
|
|
result = await sendEmail(args.to as string, args.subject as string, args.body as string, 'yahoo');
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
|
|
console.log(`[tool] ${name} OK (${Date.now() - t0}ms)`);
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
};
|
|
} catch (error) {
|
|
const msg = (error as Error).message;
|
|
const stack = (error as Error).stack ?? '';
|
|
console.error(`[tool] ${name} ERROR (${Date.now() - t0}ms):`, msg);
|
|
console.error(stack);
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
};
|
|
}
|
|
}
|