Copy Engine ships: Suite AI layer (own engine, RM facts + compliance in our system prompt, deterministic link injection), metered generate/meters APIs, /suite/copy tool page; L2 tile now live

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 06:51:16 -05:00
parent 323c86b750
commit 2a8b311a83
5 changed files with 364 additions and 2 deletions
+58 -1
View File
@@ -17,6 +17,8 @@ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const IS_PROD = process.env.NODE_ENV === 'production';
messages.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD });
const suiteMeter = require('./suite-meter'); suiteMeter.init({ dataDir: DATA_DIR });
const suiteAI = require('./suite-ai'); suiteAI.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;
@@ -634,6 +636,61 @@ async function handleApi(req,res,pathname){
if(r.error)return json(res,401,{error:r.error});
return json(res,200,{ok:true,id:r.id},{'Set-Cookie':messages.sessionCookie(r.token)});
}
// ── Circle Suite entitlement helper (shared by suite-me and the tools) ────
async function suiteEntitlement(req){
const s=messages.authFromCookie(req);
if(!s)return {error:'Not signed in.',code:401};
const cached=memberCache.get(s.id);
let d;
if(cached&&Date.now()-cached.ts<120000)d=cached.data;
else{
d=await Promise.race([chain.memberPublic(s.id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('timeout')),20000))]);
memberCache.set(s.id,{ts:Date.now(),data:d});
}
if(!d||!d.registered)return {error:'Position not found.',code:404};
const cfg=getConfig();
const roots=String(cfg.teamRootId||cfg.orgRootId||'21').split(',').map(x=>Number(x.trim())).filter(Boolean);
const chainIds=Array.isArray(d.uplineChain)?d.uplineChain.map(Number):[];
const inOrg=roots.some(r=>Number(d.id)===r||chainIds.includes(r));
const allow=String(cfg.suiteAllowlist||'').split(',').map(x=>Number(x.trim())).filter(Boolean);
const beta=allow.length>0;
const allowed=!beta||allow.includes(Number(d.id));
return {d,inOrg,beta,allowed};
}
if(req.method==='GET'&&pathname==='/api/public/suite-meters'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'Not available for this position yet.'});
return json(res,200,{level:e.d.level,meters:suiteMeter.meters(e.d.id,e.d.level)});
}
if(req.method==='POST'&&pathname==='/api/public/suite-generate'){
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.'});
const b=await bodyJson(req)||{};
const kind=String(b.kind||'post');
const brief=String(b.brief||'').trim().slice(0,1200);
if(!suiteAI.KINDS[kind])return json(res,400,{error:'Unknown copy type.'});
if(brief.length<3)return json(res,400,{error:'Tell the engine what the piece is about.'});
if(!suiteAI.configured())return json(res,503,{error:'The Copy Engine is warming up — try again shortly.'});
const gate=suiteMeter.check(e.d.id,e.d.level,'copy');
if(!gate.allowed){
return json(res,429,{error:gate.reason==='locked'
? 'The Copy Engine unlocks at Ascensus (level 2). Your next upgrade opens it.'
: 'You have used all '+gate.limit+' Copy Engine generations for this month. It resets on the 1st — or a level upgrade raises your allowance.',meter:gate});
}
const link='https://rmcircle.team/join/'+e.d.id;
try{
const text=await suiteAI.generate(kind,brief,{link:link,id:e.d.id});
suiteMeter.record(e.d.id,'copy',1);
return json(res,200,{text:text,meter:suiteMeter.check(e.d.id,e.d.level,'copy')});
}catch(err){
return json(res,502,{error:String(err.message||err)});
}
}
if(req.method==='GET'&&pathname==='/api/public/suite-me'){
// The Circle Suite entitlement: signed-in wallet -> live level + org check.
const s=messages.authFromCookie(req);
@@ -932,7 +989,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==='/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==='/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==='/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==='/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);