// Circle Suite — Email Engine (L3). Same engine, same compliance layer as the // Copy Engine; the difference is shape: it writes SEQUENCES, not single pieces. // // Output contract is the same trick that works elsewhere: labelled plain text // parsed deterministically, with a forgiving parser (models drop colons and // add markdown emphasis). 'use strict'; const suiteAI = require('./suite-ai'); const KINDS = { welcome: { label: 'Welcome sequence', count: 4, brief: 'a 4-email welcome sequence for someone who just joined the team under this member. Email 1 welcomes them and points at the first 48 hours. Email 2 covers getting their first two. Email 3 handles the doubt that shows up in week one. Email 4 is about teaching their two to do the same.' }, followup: { label: 'Follow-up sequence', count: 4, brief: 'a 4-email follow-up sequence for someone who looked at the opportunity but has not joined. Space the emails a few days apart in tone. No pressure, no fake deadlines, no guilt. Each email should be useful on its own and easy to ignore.' }, reengage: { label: 'Re-engagement sequence', count: 3, brief: 'a 3-email re-engagement sequence for a member who joined but went quiet. Warm, no shaming, assume life got busy. Make the next step small and specific.' }, broadcast: { label: 'Single broadcast', count: 1, brief: 'ONE standalone broadcast email to a list about the team build.' } }; function buildPrompt(kind, brief, member) { const k = KINDS[kind] || KINDS.broadcast; const link = (member && member.link) || ''; return ( 'Write ' + k.brief + '\n\n' + 'CONTEXT FROM THE MEMBER (who the list is, what to emphasise):\n' + (brief || '(nothing specific — write something honest and general)') + '\n\n' + 'Return EXACTLY this shape, repeated ' + k.count + ' time(s), and nothing else:\n' + 'EMAIL 1\n' + 'SUBJECT: one line, under 60 characters, no clickbait, no ALL CAPS\n' + 'BODY: 90-170 words, plain text, short paragraphs separated by blank lines, ending with a sign-off line\n' + (k.count > 1 ? 'EMAIL 2\nSUBJECT: ...\nBODY: ...\n(and so on)\n' : '') + '\n' + (link ? 'Where a link belongs, use exactly: ' + link + '\n' : 'Do not invent a link.\n') + 'No HTML, no markdown headers, no emoji in subject lines, no merge tags.' ); } // Tolerant parse: EMAIL n / SUBJECT / BODY, colon and emphasis optional. function parseEmails(raw) { const text = String(raw || '').replace(/\r/g, ''); const marks = []; const re = /(?:^|\n)[ \t]*[*#>\s]*EMAIL[ \t]*#?\s*(\d+)[ \t]*[*#]*[ \t]*:?[ \t]*/gi; let m; while ((m = re.exec(text)) !== null) marks.push({ n: Number(m[1]), at: m.index, from: m.index + m[0].length }); const chunks = []; if (!marks.length) { chunks.push(text); } else { marks.forEach(function (mk, i) { chunks.push(text.slice(mk.from, i + 1 < marks.length ? marks[i + 1].at : text.length)); }); } const out = []; chunks.forEach(function (c) { const sm = c.match(/(?:^|\n)[ \t]*[*#>\s]*SUBJECT[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*(.*)/i); const bm = c.match(/(?:^|\n)[ \t]*[*#>\s]*BODY[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*([\s\S]*)/i); const subject = sm ? sm[1].replace(/[*#]+/g, '').trim().slice(0, 120) : ''; let body = bm ? bm[1] : (sm ? c.slice(c.indexOf(sm[0]) + sm[0].length) : c); body = String(body).replace(/^[*#\s]+/, '').trim(); if (subject || body) out.push({ subject: subject || '(no subject)', body: body }); }); return out.filter(function (e) { return e.body && e.body.length > 20; }); } async function generate(kind, brief, member) { const raw = await suiteAI.generateRaw(buildPrompt(kind, brief, member)); let emails = parseEmails(raw); if (!emails.length) throw new Error('The writer came back incomplete — try again.'); const link = member && member.link; if (link) { emails = emails.map(function (e) { let b = e.body.replace(/https?:\/\/(?:www\.)?rmcircle\.team\/\S*/gi, link); b = b.replace(/\[LINK\]|\{link\}/gi, link); return { subject: e.subject, body: b }; }); } return emails; } module.exports = { KINDS, generate, parseEmails };