Files
rm-circle-team-router/suite-email.js
T

93 lines
4.1 KiB
JavaScript

// 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 };