// Circle Suite AI layer — talks to the Suite's own Hermes engine (provisioned // like a BV tenant, own $10-capped OpenRouter key). Creds live in the data // volume as suite-engine.json, never in env or the repo. // // Design rule: the RM knowledge and the compliance rules are OUR system prompt, // sent with every request. They live in version control, not in the engine's // knowledge base, so a rule change is a deploy — not a retraining. 'use strict'; const fs = require('fs'); const path = require('path'); let DATA_DIR = null; function init(opts) { DATA_DIR = opts.dataDir; } function creds() { try { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'suite-engine.json'), 'utf8')); } catch (e) { return null; } } function configured() { const c = creds(); return !!(c && c.url && c.key); } // ── The non-negotiables. Every generation carries these. ──────────────────── const COMPLIANCE = [ 'NEVER promise, guarantee, project or imply income, earnings, returns or profit.', 'NEVER use hype like "guaranteed", "risk-free", "passive income", "get rich", "financial freedom is certain".', 'Do NOT invent numbers. The ONLY money figures you may state: Premium entry is 362 POL; the first upgrade (Ascensus) is 621.40 POL; upgrade prices double at each level. If you are unsure of a figure, leave it out.', 'NEVER quote dollar values — POL\'s price moves. Speak in POL quantities or plain language only.', 'We promote the PREMIUM tier only. Never mention the Standard tier or its prices.', 'Be honest that this involves real cryptocurrency and real risk, and that results depend on effort — especially in anything longer than a couple of sentences.', 'Never claim the team or the contract does something it does not. No fake urgency, no fake scarcity, no invented testimonials or member results.' ].join(' '); const FACTS = [ 'The RM Circle is a crypto team build running on a public, verified smart contract on Polygon (0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF). The code cannot be changed and holds no member funds — every payment is person-to-person in the same transaction, and anyone can verify it on-chain.', 'The whole job: get 2 personal referrals to qualify, help those 2 get their 2, and teach them to do the same. Depth over width.', 'Two income streams: your directs\' entry payments come to you, and upgrade payments travel up the matrix to the first qualified upline whose level is at or above the level the buyer is leaving ("your level is your reach").', 'Upgrades carry no fees — 100% goes to a member. The 5% admin charge exists on entries only.', 'Levels: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona.', 'Every paid position also gets a license to the Circle Suite — the team\'s marketing toolkit (promo center, printable handouts with the member\'s QR, training course, live dashboard, AI coach, and more as it ships).' ].join(' '); const VOICE = 'Write like a straight-talking internet marketer who has been around long enough to hate hype: plain words, short sentences, honest about risk, warm and confident, never salesy or breathless. Contractions are good. No corporate filler. No emoji unless the format asks for it.'; // kind -> {label, shape} const KINDS = { post: { label: 'Social post', shape: 'Write ONE social media post of 40-90 words. Punchy opening line. No hashtags. At most one emoji. End with a soft invitation to look, not a hard sell.' }, dm: { label: 'Direct message', shape: 'Write ONE short direct message to send a friend or contact, 30-60 words. Conversational, personal, zero pressure. It should sound like a text from a real person, not a pitch. Ask a question or invite them to take a look.' }, followup: { label: 'Follow-up message', shape: 'Write ONE short follow-up message (30-70 words) to someone who looked but has not decided. No guilt, no pressure, no fake urgency. Be useful and easy to say no to.' }, objection: { label: 'Objection reply', shape: 'Write ONE reply (60-120 words) that answers the objection honestly. Concede whatever is true in it first, then give the real answer, then invite them to verify for themselves. Never argue.' }, email: { label: 'Email', shape: 'Write ONE short email: a subject line on the first line prefixed "Subject: ", then a blank line, then a 90-160 word body and a sign-off line. Plain text, no HTML.' }, story: { label: 'Short story/testimonial-style post', shape: 'Write ONE first-person post of 80-140 words telling a small, believable, PERSONAL story about the member\'s own experience or thinking. Do NOT invent earnings, results, or events — keep it about motivation, doubts, or what they are learning.' } }; function buildMessages(kind, brief, member) { const k = KINDS[kind] || KINDS.post; const link = member && member.link ? member.link : ''; const sys = 'You write promotional copy for a member of the RM Circle team.\n\n' + 'WHAT IT IS: ' + FACTS + '\n\n' + 'VOICE: ' + VOICE + '\n\n' + 'COMPLIANCE (absolute, overrides everything else): ' + COMPLIANCE + '\n\n' + 'FORMAT: ' + k.shape + '\n\n' + (link ? 'CALL TO ACTION: end with this exact link on its own final line, nothing after it: ' + link + '\n\n' : 'Do not invent a link.\n\n') + 'Output ONLY the finished copy. No preamble, no explanation, no options, no quotation marks around it, no markdown headers.'; const user = 'Write ' + k.label.toLowerCase() + '.\n\nWhat this piece is about / who it is for:\n' + brief; return [{ role: 'system', content: sys }, { role: 'user', content: user }]; } // Engine call. Long timeout: the agent narrates internally before answering. function generate(kind, brief, member) { return call(buildMessages(kind, brief, member)).then(function (t) { return cleanup(t, member); }); } function call(messages) { return new Promise(function (resolve, reject) { const c = creds(); if (!c) return reject(new Error('The engine is not configured yet.')); const body = JSON.stringify({ model: c.model || 'hermes-agent', messages: messages }); const u = new URL(c.url.replace(/\/$/, '') + '/chat/completions'); const lib = u.protocol === 'https:' ? require('https') : require('http'); const req = lib.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Authorization': 'Bearer ' + c.key } }, function (res) { let data = ''; res.on('data', function (d) { data += d; }); res.on('end', function () { if (res.statusCode !== 200) return reject(new Error('Engine returned ' + res.statusCode)); try { const j = JSON.parse(data); const text = (j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content || '').trim(); if (!text) return reject(new Error('The engine came back empty — try again.')); resolve(text); } catch (e) { reject(new Error('Could not read the engine response.')); } }); }); req.on('error', function (e) { reject(new Error('Could not reach the engine: ' + e.message)); }); req.setTimeout(240000, function () { req.destroy(new Error('The engine took too long — try again.')); }); req.write(body); req.end(); }); } // Deterministic post-processing: strip scaffolding the model sometimes adds, // and make sure the member's real link is the one that ships. function cleanup(text, member) { let t = String(text || ''); t = t.replace(/^```[a-z]*\s*/i, '').replace(/```\s*$/i, ''); t = t.replace(/^(here('|’)s|here is)[^\n:]{0,60}:\s*/i, ''); t = t.replace(/^["“](.+)["”]$/s, '$1'); const link = member && member.link; if (link) { // any rmcircle link the model produced becomes the member's own t = t.replace(/https?:\/\/(?:www\.)?rmcircle\.team\/\S*/gi, link); t = t.replace(/\[LINK\]|\{link\}/gi, link); if (t.indexOf(link) === -1) t = t.replace(/\s*$/, '\n\n' + link); } return t.trim(); } // Raw generation for tools that supply their own instruction (Page Builder). // Still carries the facts + compliance + voice; only FORMAT is the caller's. function generateRaw(instruction) { return call([ { role: 'system', content: 'You write for a member of the RM Circle team.\n\nWHAT IT IS: ' + FACTS + '\n\nVOICE: ' + VOICE + '\n\nCOMPLIANCE (absolute): ' + COMPLIANCE + '\n\nFollow the requested output format EXACTLY. No preamble, no explanation, no markdown fences.' }, { role: 'user', content: instruction } ]); } module.exports = { init, configured, generate, generateRaw, KINDS };