diff --git a/CLAUDE.md b/CLAUDE.md index 74e9f4fc..86debadf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 2. Push commits to the current feature branch 3. **NEVER push directly to `main` or `test` branches** - always use feature branches and PRs 4. Before pushing, verify the current branch is not `main` or `test` +5. **Open PRs against the `test` branch**, not `main` ### Starting a New Task diff --git a/lib/const.ts b/lib/const.ts index 72902d2a..8717e4a5 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -18,7 +18,7 @@ export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.com"; /** Domain for sending outbound emails (e.g., support@recoupable.com) */ export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com"; -export const SUPABASE_STORAGE_BUCKET = "user-files"; - -/** Default from address for outbound emails sent by the agent */ +/** Default from address for outbound emails */ export const RECOUP_FROM_EMAIL = `Agent by Recoup `; + +export const SUPABASE_STORAGE_BUCKET = "user-files"; diff --git a/lib/emails/__tests__/getEmailFooter.test.ts b/lib/emails/__tests__/getEmailFooter.test.ts new file mode 100644 index 00000000..0e5890a9 --- /dev/null +++ b/lib/emails/__tests__/getEmailFooter.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { getEmailFooter } from "../getEmailFooter"; + +describe("getEmailFooter", () => { + it("includes reply note in all cases", () => { + const footer = getEmailFooter(); + expect(footer).toContain("you can reply directly to this email"); + }); + + it("includes horizontal rule", () => { + const footer = getEmailFooter(); + expect(footer).toContain(" { + const footer = getEmailFooter(); + expect(footer).not.toContain("chat.recoupable.com"); + expect(footer).not.toContain("Or continue the conversation"); + }); + + it("includes chat link when roomId is provided", () => { + const roomId = "test-room-123"; + const footer = getEmailFooter(roomId); + expect(footer).toContain(`https://chat.recoupable.com/chat/${roomId}`); + expect(footer).toContain("Or continue the conversation on Recoup"); + }); + + it("generates proper HTML with roomId", () => { + const roomId = "my-room-id"; + const footer = getEmailFooter(roomId); + expect(footer).toContain(`href="https://chat.recoupable.com/chat/${roomId}"`); + expect(footer).toContain('target="_blank"'); + expect(footer).toContain('rel="noopener noreferrer"'); + }); + + it("applies proper styling", () => { + const footer = getEmailFooter("room-id"); + expect(footer).toContain("font-size:12px"); + expect(footer).toContain("color:#6b7280"); + }); +}); diff --git a/lib/emails/getEmailFooter.ts b/lib/emails/getEmailFooter.ts new file mode 100644 index 00000000..2557ef35 --- /dev/null +++ b/lib/emails/getEmailFooter.ts @@ -0,0 +1,27 @@ +/** + * Generates a standardized email footer HTML. + * + * @param roomId - Optional room ID for the chat link. If not provided, only the reply note is shown. + * @returns HTML string for the email footer. + */ +export function getEmailFooter(roomId?: string): string { + const replyNote = ` +

+ Note: you can reply directly to this email to continue the conversation. +

`.trim(); + + const chatLink = roomId + ? ` +

+ Or continue the conversation on Recoup: + + https://chat.recoupable.com/chat/${roomId} + +

`.trim() + : ""; + + return ` +
+${replyNote} +${chatLink}`.trim(); +} diff --git a/lib/emails/inbound/generateEmailResponse.ts b/lib/emails/inbound/generateEmailResponse.ts index 9cfd0113..95f19c43 100644 --- a/lib/emails/inbound/generateEmailResponse.ts +++ b/lib/emails/inbound/generateEmailResponse.ts @@ -2,6 +2,7 @@ import { marked } from "marked"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; import { getEmailRoomMessages } from "@/lib/emails/inbound/getEmailRoomMessages"; +import { getEmailFooter } from "@/lib/emails/getEmailFooter"; /** * Generates the assistant response HTML for an email, including: @@ -29,20 +30,7 @@ export async function generateEmailResponse( const text = chatResponse.text; const bodyHtml = marked(text); - - const footerHtml = ` -
-

- Note: you can reply directly to this email to continue the conversation. -

-

- Or continue the conversation on Recoup: - - https://chat.recoupable.com/chat/${roomId} - -

-`.trim(); - + const footerHtml = getEmailFooter(roomId); const html = `${bodyHtml}\n\n${footerHtml}`; return { text, html }; diff --git a/lib/emails/sendEmailSchema.ts b/lib/emails/sendEmailSchema.ts index 97198294..a8512630 100644 --- a/lib/emails/sendEmailSchema.ts +++ b/lib/emails/sendEmailSchema.ts @@ -24,6 +24,10 @@ export const sendEmailSchema = z.object({ .describe("Optional custom headers for the email") .default({}) .optional(), + room_id: z + .string() + .describe("Optional room ID to include in the email footer link") + .optional(), }); export type SendEmailInput = z.infer; diff --git a/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts b/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts index 848c7406..509e1aba 100644 --- a/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts +++ b/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts @@ -49,8 +49,7 @@ describe("registerSendEmailTool", () => { to: ["test@example.com"], cc: undefined, subject: "Test Subject", - text: "Test body", - html: undefined, + html: expect.stringMatching(/Test body.*you can reply directly to this email/s), headers: {}, }); diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 69199f36..39261efa 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -33,8 +33,8 @@ export const registerAllTools = (server: McpServer): void => { registerContactTeamTool(server); registerGetLocalTimeTool(server); registerSearchWebTool(server); + registerSendEmailTool(server); registerUpdateAccountInfoTool(server); registerCreateSegmentsTool(server); registerAllYouTubeTools(server); - registerSendEmailTool(server); }; diff --git a/lib/mcp/tools/registerSendEmailTool.ts b/lib/mcp/tools/registerSendEmailTool.ts index 8f0c02ef..dd4fa3ef 100644 --- a/lib/mcp/tools/registerSendEmailTool.ts +++ b/lib/mcp/tools/registerSendEmailTool.ts @@ -4,6 +4,7 @@ import { sendEmailWithResend } from "@/lib/emails/sendEmail"; import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; import { getToolResultError } from "@/lib/mcp/getToolResultError"; import { RECOUP_FROM_EMAIL } from "@/lib/const"; +import { getEmailFooter } from "@/lib/emails/getEmailFooter"; import { NextResponse } from "next/server"; /** @@ -20,15 +21,18 @@ export function registerSendEmailTool(server: McpServer): void { inputSchema: sendEmailSchema, }, async (args: SendEmailInput) => { - const { to, cc = [], subject, text, html = "", headers = {} } = args; + const { to, cc = [], subject, text, html = "", headers = {}, room_id } = args; + + const footer = getEmailFooter(room_id); + const bodyHtml = html || (text ? `

${text}

` : ""); + const htmlWithFooter = `${bodyHtml}\n\n${footer}`; const result = await sendEmailWithResend({ from: RECOUP_FROM_EMAIL, to, cc: cc.length > 0 ? cc : undefined, subject, - text, - html: html || undefined, + html: htmlWithFooter, headers, });