// ABOUTME: Agent email sending extension — enables agents to send emails via AgentMail through Commander. // ABOUTME: Registers a send_email tool that proxies to commander_agentmail for reports, briefings, and custom emails. import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { Text } from "@mariozechner/pi-tui"; // ── Types ──────────────────────────────────────────────────────────── interface SendEmailParams { to?: string; subject?: string; body?: string; html?: string; type?: "generic" | "report" | "briefing"; report_name?: string; format?: "markdown" | "html" | "text"; } // ── Tool Registration ──────────────────────────────────────────────── export default function (pi: ExtensionAPI) { pi.registerTool({ name: "send_email", label: "Send Email", description: [ "Send an email via AgentMail through the Commander assistant.", "Uses the same email system as Commander reports and briefings.", "Default recipient: ruizrica2@gmail.com", "", "Three modes:", " generic — send a custom email with subject and body/content", " report — send a formatted report (markdown auto-converted to styled HTML)", " briefing — send a morning briefing email", "", "Content supports markdown (auto-converted to HTML), raw HTML, or plain text.", "", "Examples:", ' { type: "report", report_name: "Feature Complete", body: "## Summary\\nAdded auth..." }', ' { type: "generic", subject: "Build Results", body: "All 42 tests passed." }', ' { type: "generic", to: "team@example.com", subject: "Deploy Done", body: "v2.1 is live" }', ].join("\n"), parameters: Type.Object({ to: Type.Optional(Type.String({ description: "Recipient email address. Default: ruizrica2@gmail.com" })), subject: Type.Optional(Type.String({ description: "Email subject line (required for generic, auto-generated for report/briefing)." })), body: Type.Optional(Type.String({ description: "Email body content — markdown (default), HTML, or plain text." })), html: Type.Optional(Type.String({ description: "Raw HTML email body (overrides body)." })), type: Type.Optional(Type.String({ description: "Email type: 'generic' (default), 'report', or 'briefing'." })), report_name: Type.Optional(Type.String({ description: "Report name for subject line (for report type)." })), format: Type.Optional(Type.String({ description: "Content format: 'markdown' (default), 'html', 'text'." })), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const p = params as SendEmailParams; const emailType = (p.type || "generic").toLowerCase(); // ── Try to call commander_agentmail via the MCP client ── const g = globalThis as any; // Check if Commander is available const gate = g.__piCommanderGate; if (!gate || gate.status !== "available") { return { content: [{ type: "text" as const, text: "Email sending failed: Commander is not connected. The send_email tool requires Commander with AgentMail configured." }], details: { success: false, error: "commander_not_available" }, }; } // Build the commander_agentmail call based on email type let agentmailParams: Record; if (emailType === "report") { if (!p.body && !p.html) { return { content: [{ type: "text" as const, text: "Email sending failed: 'body' content is required for report emails." }], details: { success: false, error: "missing_content" }, }; } agentmailParams = { operation: "send:report", report_name: p.report_name || p.subject || "Completion Report", content: p.html || p.body, format: p.html ? "html" : (p.format || "markdown"), }; if (p.to) agentmailParams.to = p.to; } else if (emailType === "briefing") { if (!p.body) { return { content: [{ type: "text" as const, text: "Email sending failed: 'body' content is required for briefing emails." }], details: { success: false, error: "missing_content" }, }; } agentmailParams = { operation: "send:briefing", content: p.body, }; if (p.to) agentmailParams.to = p.to; } else { // Generic email if (!p.subject) { return { content: [{ type: "text" as const, text: "Email sending failed: 'subject' is required for generic emails." }], details: { success: false, error: "missing_subject" }, }; } if (!p.body && !p.html) { return { content: [{ type: "text" as const, text: "Email sending failed: 'body' or 'html' is required for generic emails." }], details: { success: false, error: "missing_body" }, }; } agentmailParams = { operation: "send:custom", subject: p.subject, content: p.html || p.body, format: p.html ? "html" : (p.format || "markdown"), }; if (p.to) agentmailParams.to = p.to; } // Call commander_agentmail through the tool system try { // Use ctx.callTool if available, otherwise fall back to finding the tool if (ctx && typeof (ctx as any).callTool === "function") { const result = await (ctx as any).callTool("commander_agentmail", agentmailParams); return result; } // Fallback: call via the registered Pi tool directly const piGlobal = g.__piInstance || g.__pi; if (piGlobal && typeof piGlobal.callTool === "function") { const result = await piGlobal.callTool("commander_agentmail", agentmailParams); return result; } // Last resort: use the MCP client directly const McpClientModule = await import("./lib/mcp-client.ts"); const serverPath = "/Users/ricardo/Workshop/Github-Work/commander/services/commander-mcp/dist/server.js"; const client = new McpClientModule.McpClient(serverPath, { COMMANDER_WS_URL: process.env.COMMANDER_WS_URL || "ws://localhost:9002", AGENTMAIL_API_KEY: process.env.AGENTMAIL_API_KEY || "", }); try { await client.connect(); const result = await client.callTool("commander_agentmail", agentmailParams); return result; } finally { try { client.disconnect(); } catch {} } } catch (err: any) { return { content: [{ type: "text" as const, text: `Email sending failed: ${err.message}` }], details: { success: false, error: err.message }, }; } }, renderCall(args, theme) { const p = args as SendEmailParams; const type = p.type || "generic"; const to = p.to || "default"; const label = `${type} → ${to}`; return new Text(theme.fg("toolTitle", theme.bold("send_email ")) + theme.fg("accent", label), 0, 0); }, renderResult(result, _options, theme) { const details = result.details as any; const text = result.content?.[0]; const textStr = text?.type === "text" ? text.text : ""; if (details?.error || textStr.toLowerCase().includes("fail") || textStr.toLowerCase().includes("error")) { return new Text(theme.fg("error", `send_email failed: ${details?.error || textStr}`), 0, 0); } return new Text(theme.fg("success", `send_email ✓ ${textStr || "sent"}`), 0, 0); }, }); }