diff --git a/public/suite-founder.html b/public/suite-founder.html new file mode 100644 index 0000000..cacf539 --- /dev/null +++ b/public/suite-founder.html @@ -0,0 +1,77 @@ +Founder Desk | The Circle Suite + + + +
+
CIRCLE SUITE Β· CORONA (LEVEL 8)
+

Founder Desk.

+

Every tool below this builds one thing at a time, which is right when you are still finding your words. At this level the problem is different: you are running an organisation and you do not have an afternoon to spend clicking. So this builds the whole week at once.

+ +
+ + + +

Everything here obeys the same rules as the rest of the Suite β€” no income claims, no invented figures, POL quantities only, Premium only. Generating a week does not post anything anywhere; it hands you the words and you decide what goes out.

Independent team resource Β· No income is guaranteed Β· Cryptocurrency involves risk.

+
+ + + diff --git a/public/suite-founder.js b/public/suite-founder.js new file mode 100644 index 0000000..3412dcc --- /dev/null +++ b/public/suite-founder.js @@ -0,0 +1,203 @@ +// Founder Desk client β€” builds the weekly pack and manages the API key. +(function () { + 'use strict'; + var $ = function (id) { return document.getElementById(id); }; + function esc(s) { + return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; + }); + } + + function gate(msg) { + $('dGate').style.display = 'block'; + $('dGate').innerHTML = msg; + } + + function copyBtn(getText) { + var b = document.createElement('button'); + b.className = 'btn btn-secondary btn-sm d-copy'; + b.textContent = 'Copy'; + b.addEventListener('click', function () { + if (navigator.clipboard) navigator.clipboard.writeText(getText()); + b.textContent = 'Copied'; + setTimeout(function () { b.textContent = 'Copy'; }, 1400); + }); + return b; + } + + function item(tag, subject, body) { + var d = document.createElement('div'); + d.className = 'd-item'; + var html = '
' + esc(tag) + '
'; + if (subject) html += '
' + esc(subject) + '
'; + html += '
' + esc(body) + '
'; + d.innerHTML = html; + d.appendChild(copyBtn(function () { return (subject ? subject + '\n\n' : '') + body; })); + return d; + } + + function section(title, why) { + var s = document.createElement('div'); + s.className = 'd-sec'; + s.innerHTML = '

' + esc(title) + '

' + esc(why) + '
'; + return s; + } + + function renderPack(pack) { + var host = $('dPack'); + host.innerHTML = ''; + if (!pack) return; + + if ((pack.posts || []).length) { + var s1 = section('Social posts β€” one per angle', + 'Post one a day. They deliberately take different angles so the week does not read as the same message five times.'); + pack.posts.forEach(function (p) { s1.appendChild(item(p.angle, '', p.text)); }); + host.appendChild(s1); + } + + if ((pack.messages || []).length) { + var s2 = section('Outreach messages', + 'For the conversations that actually build the team β€” a first approach, a follow-up for someone gone quiet, and the pyramid-scheme answer.'); + pack.messages.forEach(function (mm) { s2.appendChild(item(mm.label, '', mm.text)); }); + host.appendChild(s2); + } + + if ((pack.emails || []).length) { + var s3 = section('Email for your list', 'Drop straight into your autoresponder.'); + pack.emails.forEach(function (em) { s3.appendChild(item('Email', em.subject, em.body)); }); + host.appendChild(s3); + } + + if ((pack.textAds || []).length) { + var s4 = section('Text ads', 'Ready to run from the Traffic Desk.'); + var grid = document.createElement('div'); + grid.className = 'd-ads'; + pack.textAds.forEach(function (a) { + var c = document.createElement('div'); + c.className = 'd-ad'; + var h = '
' + esc(a.subject) + '
'; + (a.lines || []).forEach(function (l) { if (l) h += '
' + esc(l) + '
'; }); + c.innerHTML = h; + grid.appendChild(c); + }); + s4.appendChild(grid); + host.appendChild(s4); + } + + // Be straight about anything the engine could not produce, rather than + // quietly handing over a short pack. + if ((pack.problems || []).length) { + var warn = document.createElement('div'); + warn.className = 'd-item'; + warn.style.borderColor = '#f0a05a'; + warn.innerHTML = '
Incomplete
' + + 'The engine could not finish: ' + esc(pack.problems.join(', ')) + + '. Everything else above is fine β€” build again to retry the missing pieces.
'; + host.appendChild(warn); + } + } + + function renderKey(info, freshKey) { + var host = $('dKeyState'); + if (freshKey) { + host.innerHTML = '
Copy this now β€” it is shown once and never again.
' + + '
' + esc(freshKey) + '
'; + var b = copyBtn(function () { return freshKey; }); + b.style.position = 'static'; + b.style.marginTop = '8px'; + host.appendChild(b); + $('dIssue').textContent = 'Replace this key'; + $('dRevoke').style.display = ''; + return; + } + if (info && info.exists) { + host.innerHTML = '
A key is active (' + esc(info.hint) + '), created ' + + esc(String(info.createdAt || '').slice(0, 10)) + + (info.lastUsedAt ? ', last used ' + esc(String(info.lastUsedAt).slice(0, 10)) : ', never used yet') + + '. The key itself is stored hashed, so it cannot be shown again β€” create a new one if you have lost it.
'; + $('dIssue').textContent = 'Replace this key'; + $('dRevoke').style.display = ''; + } else { + host.innerHTML = '
No key yet.
'; + $('dIssue').textContent = 'Create a key'; + $('dRevoke').style.display = 'none'; + } + } + + async function build(btn) { + btn.disabled = true; + btn.innerHTML = 'Building your week β€” this takes a minute…'; + $('dErr').style.display = 'none'; + try { + var r = await fetch('/api/public/suite-founder', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' + }); + var d = await r.json(); + if (!r.ok) { + $('dErr').textContent = d.error || 'That did not build β€” try again.'; + $('dErr').style.display = 'block'; + } else { + renderPack(d.pack); + if (d.meter) $('dMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' packs left this month'; + } + } catch (e) { + $('dErr').textContent = 'Connection hiccup β€” try again.'; + $('dErr').style.display = 'block'; + } + btn.disabled = false; + btn.innerHTML = 'πŸ‘‘ Build my week'; + } + + async function issue() { + if (!window.confirm('Create a new API key? Any existing key stops working immediately.')) return; + try { + var r = await fetch('/api/public/suite-founder', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ issueKey: true }) + }); + var d = await r.json(); + if (r.ok) renderKey(d.info, d.key); + } catch (e) {} + } + + async function revoke() { + if (!window.confirm('Revoke your API key? Anything using it stops working.')) return; + try { + var r = await fetch('/api/public/suite-founder', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ revokeKey: true }) + }); + var d = await r.json(); + if (r.ok) renderKey(d.info, null); + } catch (e) {} + } + + async function boot() { + try { + var r = await fetch('/api/public/suite-founder'); + if (r.status === 401) { gate('You’re not signed in yet. Open the Suite and connect the wallet that holds your position, then come back.'); return; } + if (r.status === 403) { + var g = await r.json(); + gate((g.error || 'Not open for this position yet.') + ' See your Suite.'); + return; + } + if (!r.ok) return; + var d = await r.json(); + $('dBody').style.display = 'block'; + renderPack(d.pack); + renderKey(d.key, null); + if (d.meter) $('dMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' packs left this month'; + if (d.writerReady === false) { + $('dGo').disabled = true; + $('dGo').textContent = 'The writer is warming up'; + } + } catch (e) {} + } + + document.addEventListener('DOMContentLoaded', function () { + boot(); + $('dGo').addEventListener('click', function () { build(this); }); + $('dIssue').addEventListener('click', issue); + $('dRevoke').addEventListener('click', revoke); + }); +})(); diff --git a/public/suite.js b/public/suite.js index 83544f3..4f9a7eb 100644 --- a/public/suite.js +++ b/public/suite.js @@ -22,7 +22,7 @@ { lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising β€” run banners or AI-written text ads on the team’s own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true }, { lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'A separate hosted landing page for every audience you talk to β€” each on its own address, each one something you can point a different ad at.', href: '/suite/funnel', live: true }, { lv: 7, ico: '🧭', name: 'Leader Ops', desc: 'Everyone below you, read live from the contract and sorted by who needs you most β€” plus a written coaching plan you can teach forward to your own two.', href: '/suite/leader', live: true }, - { lv: 8, ico: 'πŸ‘‘', name: 'Founder Desk', desc: 'Every tool in the Suite at its highest allowance, and the top of the ladder β€” nothing above this to unlock.', live: false } + { lv: 8, ico: 'πŸ‘‘', name: 'Founder Desk', desc: 'A whole week of promotion built in one pass β€” posts, outreach messages, an email and text ads β€” plus an API key to pull your position, your organisation and the writer into anything you already run.', href: '/suite/founder', live: true } ]; var $ = function (id) { return document.getElementById(id); }; diff --git a/server.js b/server.js index 8233618..770a801 100644 --- a/server.js +++ b/server.js @@ -28,6 +28,7 @@ const suiteTextAds = require('./suite-textads'); const suiteVoice = require('./suite-voice'); suiteVoice.init({ dataDir: DATA_DIR }); const suiteSplit = require('./suite-split'); suiteSplit.init({ dataDir: DATA_DIR }); const suiteLeader = require('./suite-leader'); +const suiteFounder = require('./suite-founder'); suiteFounder.init({ dataDir: DATA_DIR }); const tgbot = require('./tgbot'); tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' }); const SESSION_TTL = 8 * 60 * 60 * 1000; @@ -959,6 +960,66 @@ async function handleApi(req,res,pathname){ } } + // -- Founder Desk (level 8) ----------------------------------------------- + if(pathname==='/api/public/suite-founder'){ + const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500})); + if(e.error)return json(res,e.code||500,{error:e.error}); + if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'}); + if(Number(e.d.level)'}); + let md=null; + try{ md=await chain.memberPublic(memberId); }catch(err){} + if(!md||!md.registered)return json(res,404,{error:'Position not found.'}); + if(pathname==='/api/suite/v1/me'&&req.method==='GET'){ + return json(res,200,{id:memberId,level:md.level,levelName:md.levelName,tier:md.tier, + directCount:md.directCount,qualified:(md.directCount||0)>=2, + totalEarnedPol:md.totalEarnedPol,link:'https://rmcircle.team/join/'+memberId}); + } + if(pathname==='/api/suite/v1/team'&&req.method==='GET'){ + return json(res,200,{scan:suiteLeader.scan(memberId,25)}); + } + if(pathname==='/api/suite/v1/generate'&&req.method==='POST'){ + const b=await bodyJson(req)||{}; + const kind=String(b.kind||'post'); + if(!suiteAI.KINDS[kind])return json(res,400,{error:'Unknown kind. One of: '+Object.keys(suiteAI.KINDS).join(', ')}); + const brief=String(b.brief||'').trim().slice(0,1200); + if(brief.length<3)return json(res,400,{error:'Send a brief describing what the piece is about.'}); + const gate=suiteMeter.check(memberId,md.level,'copy'); + if(!gate.allowed)return json(res,429,{error:'Monthly generation allowance used.',meter:gate}); + try{ + const text=await suiteAI.generate(kind,brief,{link:'https://rmcircle.team/join/'+memberId,id:memberId,voice:suiteVoice.promptBlock(memberId)}); + suiteMeter.record(memberId,'copy',1); + return json(res,200,{text:text,meter:suiteMeter.check(memberId,md.level,'copy')}); + }catch(err){ return json(res,502,{error:String(err.message||err)}); } + } + return json(res,404,{error:'Unknown endpoint. Available: GET /api/suite/v1/me, GET /api/suite/v1/team, POST /api/suite/v1/generate'}); + } + if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){ const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup β€” try again.',code:500})); if(e.error)return json(res,e.code||500,{error:e.error}); @@ -1303,7 +1364,7 @@ const server=http.createServer(async(req,res)=>{ if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); } let file; - if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ + if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/suite/founder'||pathname==='/suite/founder/')file=path.join(PUBLIC_DIR,'suite-founder.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404); diff --git a/suite-founder.js b/suite-founder.js new file mode 100644 index 0000000..0daf8f8 --- /dev/null +++ b/suite-founder.js @@ -0,0 +1,167 @@ +// Circle Suite β€” Founder Desk (level 8, Corona). +// +// Every tier below this produces ONE thing at a time: a post, a page, a +// sequence, an ad. That is right for someone still learning what to say. At the +// top of the ladder the constraint is different β€” these are people running +// organisations who do not have an afternoon to spend clicking one tool at a +// time. So the Founder Desk does the whole week in a single pass, across every +// channel, and hands it over as one pack. +// +// Second half of the tool is API access: a personal key so a founder can pull +// their own position, their organisation triage and generated copy into +// whatever they already run β€” a spreadsheet, a bot, their own site. +'use strict'; +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const suiteAi = require('./suite-ai'); +const suiteEmail = require('./suite-email'); +const suiteTextAds = require('./suite-textads'); + +const MIN_LEVEL = 8; + +let DATA_DIR = null; +function init(opts) { + DATA_DIR = opts.dataDir; + try { fs.mkdirSync(packDir(), { recursive: true }); } catch (e) {} +} +function packDir() { return path.join(DATA_DIR, 'founder'); } +function packFile(id) { return path.join(packDir(), String(Number(id)) + '.json'); } +function keysFile() { return path.join(DATA_DIR, 'suite-api-keys.json'); } + +// ── API keys ──────────────────────────────────────────────────────────────── +// Stored hashed. We show the key exactly once, at creation, the way every other +// API does it β€” if it leaks later we cannot un-leak it, but at least the file +// on disk is not a list of live credentials. +function readKeys() { try { return JSON.parse(fs.readFileSync(keysFile(), 'utf8')); } catch (e) { return {}; } } +function writeKeys(v) { try { fs.writeFileSync(keysFile(), JSON.stringify(v), { mode: 0o600 }); } catch (e) {} } +function hashKey(k) { return crypto.createHash('sha256').update(String(k)).digest('hex'); } + +function keyInfo(memberId) { + const all = readKeys(); + const rec = all[String(memberId)]; + if (!rec) return { exists: false }; + return { exists: true, createdAt: rec.createdAt, lastUsedAt: rec.lastUsedAt || null, hint: rec.hint }; +} + +function issueKey(memberId) { + const raw = 'rmc_' + crypto.randomBytes(24).toString('hex'); + const all = readKeys(); + all[String(memberId)] = { + hash: hashKey(raw), createdAt: new Date().toISOString(), + hint: raw.slice(0, 8) + '…' + raw.slice(-4) + }; + writeKeys(all); + return raw; // the only time this is ever returned +} + +function revokeKey(memberId) { + const all = readKeys(); + delete all[String(memberId)]; + writeKeys(all); + return true; +} + +// Constant-time compare against every stored hash. The key set is small (only +// level-8 positions), so a linear scan is fine and keeps the key itself +// unguessable from the file. +function memberForKey(raw) { + if (!raw || typeof raw !== 'string' || raw.length < 16) return null; + const h = Buffer.from(hashKey(raw), 'hex'); + const all = readKeys(); + let found = null; + Object.keys(all).forEach(function (id) { + const stored = Buffer.from(String(all[id].hash || ''), 'hex'); + if (stored.length === h.length && crypto.timingSafeEqual(stored, h)) found = Number(id); + }); + if (found != null) { + all[String(found)].lastUsedAt = new Date().toISOString(); + writeKeys(all); + } + return found; +} + +// ── The weekly campaign pack ──────────────────────────────────────────────── +function loadPack(id) { try { return JSON.parse(fs.readFileSync(packFile(id), 'utf8')); } catch (e) { return null; } } +function savePack(id, pack) { try { fs.writeFileSync(packFile(id), JSON.stringify(pack)); } catch (e) {} } + +// Angles rotate through the hooks the rest of the site already uses, so the +// week has variety built in rather than seven versions of the same post. +const WEEK_ANGLES = [ + { key: 'two', brief: 'the whole job is two people and helping them get their two' }, + { key: 'graveyard', brief: 'why this one does not disappear like every other program β€” public contract, holds nobody\'s money' }, + { key: 'pocket', brief: 'the entry is small and one-time, not a subscription that bleeds you every month' }, + { key: 'verify', brief: 'do not trust anyone including me β€” read the contract yourself before you spend a cent' }, + { key: 'phone', brief: 'it runs entirely from a phone; no laptop, no stock, no shipping' } +]; + +function splitNumbered(text, want) { + // Tolerant split on "1." / "POST 1" / "---" style separators. + const parts = String(text || '') + .split(/(?:^|\n)\s*(?:[-*#]{3,}|(?:POST|ITEM)?\s*\d+[.)]?\s*)(?=\n|\s)/i) + .map(function (x) { return x.replace(/^[\s*#>-]+/, '').trim(); }) + .filter(function (x) { return x.length > 25; }); + return parts.slice(0, want); +} + +async function buildPack(opts) { + const id = Number(opts.id); + const link = 'https://rmcircle.team/join/' + id; + const member = { link: link, id: id, voice: opts.voice || '' }; + + const pack = { id: id, at: new Date().toISOString(), posts: [], messages: [], emails: [], textAds: [] }; + const problems = []; + + // 1) A week of social posts, one per angle. + try { + const postInstruction = + 'Write 5 separate social media posts for a member of the RM Circle team build, one for each of these angles:\n' + + WEEK_ANGLES.map(function (a, i) { return (i + 1) + '. ' + a.brief; }).join('\n') + '\n\n' + + 'Each post: 40-90 words, punchy opening line, no hashtags, at most one emoji, ending with a soft invitation to look rather than a hard sell, then this exact link on its own final line: ' + link + '\n\n' + + 'ABSOLUTE RULES: never promise, guarantee, project or imply income, earnings or returns. Never quote dollar values β€” POL quantities only. Never mention the Standard tier. Be honest that this is real cryptocurrency with real risk. Do not invent numbers.\n\n' + + (opts.voice ? opts.voice + '\n\n' : '') + + 'Separate each post with a line containing only ---. Output nothing else: no headings, no commentary, no numbering.'; + const raw = await suiteAi.generateRaw(postInstruction); + pack.posts = splitNumbered(raw, 5).map(function (t, i) { + return { angle: WEEK_ANGLES[i] ? WEEK_ANGLES[i].key : 'general', text: t }; + }); + if (!pack.posts.length) problems.push('posts'); + } catch (e) { problems.push('posts'); } + + // 2) Outreach messages β€” a first approach, a follow-up, an objection reply. + const msgKinds = [ + { kind: 'dm', label: 'First message', brief: 'reaching out to someone you know but have not spoken to in a while' }, + { kind: 'followup', label: 'Follow-up', brief: 'they looked at your link a few days ago and went quiet' }, + { kind: 'objection', label: 'Objection reply', brief: 'they said this sounds like a pyramid scheme' } + ]; + for (const mk of msgKinds) { + try { + const t = await suiteAi.generate(mk.kind, mk.brief, member); + pack.messages.push({ label: mk.label, kind: mk.kind, text: t }); + } catch (e) { problems.push(mk.kind); } + } + + // 3) An email the founder can drop into their autoresponder. + try { + pack.emails = await suiteEmail.generate('broadcast', 'a weekly update to your list about the team build', member); + } catch (e) { problems.push('email'); } + + // 4) Text ads ready for the Traffic Desk. + try { + const t = await suiteTextAds.generate({ angle: 'general', count: 3 }); + pack.textAds = t.variants; + } catch (e) { problems.push('text ads'); } + + pack.problems = problems; + const produced = pack.posts.length + pack.messages.length + pack.emails.length + pack.textAds.length; + if (!produced) throw new Error('The engine did not come back with anything usable β€” try again in a moment.'); + + savePack(id, pack); + return pack; +} + +module.exports = { + init, MIN_LEVEL, buildPack, loadPack, + keyInfo, issueKey, revokeKey, memberForKey, WEEK_ANGLES +}; diff --git a/suite-meter.js b/suite-meter.js index d8cd6e1..b996975 100644 --- a/suite-meter.js +++ b/suite-meter.js @@ -25,7 +25,9 @@ const TOOLS = { video: { minLevel: 3, quota: [0, 0, 4, 8, 12, 20, 20, 20], label: 'Video Maker' }, // Text ads live in the Traffic Desk, which is a level-1 tool, so generation // has to start at level 1 too. One call returns five ready ads. - textad: { minLevel: 1, quota: [20, 30, 50, 75, 100, 150, 200, 250], label: 'Text Ad Writer' } + textad: { minLevel: 1, quota: [20, 30, 50, 75, 100, 150, 200, 250], label: 'Text Ad Writer' }, + // One pack is several engine calls, so the allowance is small by design. + founder: { minLevel: 8, quota: [0, 0, 0, 0, 0, 0, 0, 12], label: 'Founder Desk' } }; function quotaFor(tool, level) { diff --git a/suite-tools.js b/suite-tools.js index bac9951..e0fc9df 100644 --- a/suite-tools.js +++ b/suite-tools.js @@ -17,7 +17,7 @@ const LEVEL_TOOLS = { 5: { name: 'Apex', live: true, short: 'the Split Tester, and your ad allowance jumps to 50,000 impressions a month' }, 6: { name: 'Fastigium', live: true, short: 'the Funnel Factory β€” a separate hosted landing page for every audience you talk to' }, 7: { name: 'Vertex', live: true, short: 'Leader Ops β€” your whole organisation triaged live from the contract, plus a coaching plan to teach forward' }, - 8: { name: 'Corona', live: true, short: 'the Founder Desk β€” every tool at its highest allowance, and the top of the ladder' } + 8: { name: 'Corona', live: true, short: 'the Founder Desk β€” a whole week of promotion built in one pass, plus API access to the Suite' } }; // Very short form for character-limited surfaces (X/Twitter).