180 lines
7.2 KiB
TypeScript
180 lines
7.2 KiB
TypeScript
// 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<string, string | undefined>;
|
|
|
|
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);
|
|
},
|
|
});
|
|
}
|