7c3e98eaa9
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
4.1 KiB
JavaScript
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 };
|