diff --git a/hermes-k8s.yaml b/hermes-k8s.yaml index 0068633..1a610c1 100644 --- a/hermes-k8s.yaml +++ b/hermes-k8s.yaml @@ -22,7 +22,7 @@ spec: fsGroup: 1000 containers: - name: hermes-mcp - image: localhost:32000/hermes-mcp@sha256:54488f625b5a065f3cfb30d9d0afe269dac65aadd8206652d27da034daf1dee4 + image: localhost:32000/hermes-mcp@sha256:0bd3355cc4b5a3727e56cd62364127deb8801d41f4e3b9dc48e9051e11f52b8f imagePullPolicy: Always securityContext: allowPrivilegeEscalation: false diff --git a/src/email-poller.ts b/src/email-poller.ts new file mode 100644 index 0000000..36de088 --- /dev/null +++ b/src/email-poller.ts @@ -0,0 +1,42 @@ +import { searchMessages } from './imap.js'; +import { sendSupportEmailAlert } from './notifications/slack.js'; +import redis from './redis.js'; + +const REDIS_KEY = 'support:email:alerted_uids'; +const POLL_INTERVAL_MS = 5 * 60 * 1000; + +async function pollSupportInbox() { + try { + const messages = await searchMessages('', 20, 'sqcp_support'); + if (messages.length === 0) return; + + const alreadyAlerted = await redis.sMembers(REDIS_KEY); + const alerted = new Set(alreadyAlerted); + + for (const msg of messages) { + if (msg.seen) continue; + const key = String(msg.uid); + if (alerted.has(key)) continue; + + await sendSupportEmailAlert({ + uid: msg.uid, + subject: msg.subject, + from: msg.from, + date: msg.date, + }); + + await redis.sAdd(REDIS_KEY, key); + // Keep the set from growing unbounded — trim to last 500 UIDs + await redis.expire(REDIS_KEY, 60 * 60 * 24 * 30); // 30-day TTL + console.log(`[email-poller] alerted on uid=${msg.uid} from=${msg.from}`); + } + } catch (err) { + console.error('[email-poller] poll error:', (err as Error).message); + } +} + +export function startEmailPoller() { + console.log('[email-poller] starting — polling support@squaremcp.com every 5 min'); + pollSupportInbox(); + setInterval(pollSupportInbox, POLL_INTERVAL_MS); +} diff --git a/src/index.ts b/src/index.ts index 54871ce..2c51c00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,8 @@ 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'; +import { startEmailPoller } from './email-poller.js'; +import { sendChatEscalationAlert } from './notifications/slack.js'; process.on('uncaughtException', (err) => { console.error('FATAL uncaughtException:', err); @@ -1997,6 +1999,24 @@ app.post('/api/facebook/video', requireAuth, async (req, res) => { }); // ── Chat widget endpoint ──────────────────────────────────────── +const ESCALATION_PATTERNS = [ + /\b(human|person|agent|someone|anybody|real person)\b/i, + /\bspeak\s+to\b|\btalk\s+to\b|\bcontact\s+(you|someone|support|sales)\b/i, + /\b(pricing|price|cost|how much|plans?|subscribe|sign\s*up|get\s+started|trial|demo)\b/i, + /\b(help me|need help|can you help|support)\b/i, + /\bemail\s+(you|garfield|support|sales)\b/i, +]; + +function detectEscalation(messages: ChatMessage[]): string | null { + const lastUser = [...messages].reverse().find(m => m.role === 'user'); + if (!lastUser) return null; + for (const pattern of ESCALATION_PATTERNS) { + const match = lastUser.content.match(pattern); + if (match) return match[0]; + } + return null; +} + app.post('/api/chat', async (req, res) => { const { messages } = req.body as { messages?: ChatMessage[] }; if (!Array.isArray(messages) || messages.length === 0) { @@ -2006,6 +2026,15 @@ app.post('/api/chat', async (req, res) => { try { const { reply, toolsUsed } = await handleChat(messages); res.json({ reply, toolsUsed }); + + const trigger = detectEscalation(messages); + if (trigger) { + sendChatEscalationAlert({ + trigger, + conversation: messages, + userAgent: (req as { get: (h: string) => string | undefined }).get('user-agent'), + }).catch(err => console.error('[chat] escalation alert error:', err)); + } } catch (err) { console.error('[chat] error:', (err as Error).message); res.status(500).json({ error: 'Chat unavailable' }); @@ -2153,6 +2182,8 @@ async function main() { ]); } + startEmailPoller(); + app.listen(PORT, () => { console.log(`Hermes MCP server running on port ${PORT}`); console.log(` Streamable HTTP: ${SERVER_URL}/mcp`); diff --git a/src/notifications/slack.ts b/src/notifications/slack.ts index b8b0bef..8cc8b7e 100644 --- a/src/notifications/slack.ts +++ b/src/notifications/slack.ts @@ -1,5 +1,28 @@ const SLACK_WEBHOOK_URL = process.env['SLACK_PILOT_WEBHOOK_URL'] ?? ''; +async function postToSlack(blocks: object[]): Promise { + if (!SLACK_WEBHOOK_URL) { + console.warn('[notification:slack] SLACK_PILOT_WEBHOOK_URL not set, skipping'); + return false; + } + try { + const res = await fetch(SLACK_WEBHOOK_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ blocks }), + signal: AbortSignal.timeout(10_000), + }); + if (!res.ok) { + console.error(`[notification:slack] HTTP ${res.status}: ${await res.text()}`); + return false; + } + return true; + } catch (err) { + console.error('[notification:slack] fetch error:', (err as Error).message); + return false; + } +} + export interface PilotAlertPayload { requestId: string; name: string; @@ -72,24 +95,70 @@ export async function sendSlackAlert(payload: PilotAlertPayload): Promise; + userAgent?: string; +} + +export async function sendChatEscalationAlert(payload: ChatEscalationPayload): Promise { + const preview = payload.conversation + .slice(-6) + .map(m => `*${m.role === 'user' ? 'Visitor' : 'Bot'}:* ${m.content.slice(0, 300)}`) + .join('\n'); + + const blocks = [ + { + type: 'header', + text: { type: 'plain_text', text: '💬 Chat Widget — Visitor Needs Help', emoji: true }, + }, + { + type: 'section', + text: { type: 'mrkdwn', text: `*Trigger phrase:* \`${payload.trigger}\`` }, + }, + { + type: 'section', + text: { type: 'mrkdwn', text: `*Recent conversation:*\n${preview}` }, + }, + { + type: 'context', + elements: [{ type: 'mrkdwn', text: payload.userAgent ? `UA: ${payload.userAgent.slice(0, 120)}` : 'UA: unknown' }], + }, + ]; + + return postToSlack(blocks); +} + +export interface SupportEmailPayload { + uid: number; + subject: string; + from: string; + date: string; +} + +export async function sendSupportEmailAlert(payload: SupportEmailPayload): Promise { + const blocks = [ + { + type: 'header', + text: { type: 'plain_text', text: '📧 New Email → support@squaremcp.com', emoji: true }, + }, + { + type: 'section', + fields: [ + { type: 'mrkdwn', text: `*From:*\n${payload.from}` }, + { type: 'mrkdwn', text: `*Date:*\n${payload.date}` }, + ], + }, + { + type: 'section', + text: { type: 'mrkdwn', text: `*Subject:*\n${payload.subject}` }, + }, + ]; + + return postToSlack(blocks); }