cornilius 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,8 +31,10 @@ NO_COLOR=1 cornilius # plain ASCII (no color)
31
31
  **Auth:** `/login` opens your browser and signs you in (RFC 8252 native-app
32
32
  loopback: PKCE + CSRF `state`, a one-time local `127.0.0.1` callback, code →
33
33
  token exchange). It reuses Cornilius's existing OAuth server and the cornilius.ai
34
- sign-in — no new accounts. The token is stored at `~/.cornilius/credentials.json`
35
- (`0600`). `CORNILIUS_TOKEN` works as a manual override (e.g. CI); the token is
34
+ sign-in — no new accounts. The token is kept **only in memory for the session** —
35
+ it is never written to disk, so you sign in again each time you open the CLI
36
+ (only the non-secret OAuth client id is cached at `~/.cornilius/credentials.json`,
37
+ `0600`). `CORNILIUS_TOKEN` works as a manual override (e.g. CI); the token is
36
38
  refused over non-HTTPS, and a non-default host is warned.
37
39
 
38
40
  Env: `CORNILIUS_API` (default `https://api.cornilius.ai`), `CORNILIUS_TOKEN`,
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import K from"node:readline";import h from"node:fs";import J from"node:os";import A from"node:path";import R from"node:crypto";import Y from"node:http";import{spawn as G}from"node:child_process";var j=process.argv.slice(2),b=j.includes("--demo")||process.env.CORNILIUS_DEMO==="1",f=(process.env.CORNILIUS_API||"https://api.cornilius.ai").replace(/\/+$/,""),F=A.join(J.homedir(),".cornilius"),m=A.join(F,"credentials.json"),Q=process.stdout.isTTY&&!process.env.NO_COLOR,O=o=>e=>Q?`\x1B[${o}m${e}\x1B[0m`:e,I=O("38;5;208"),a=o=>o,r=O("38;5;245"),H=O("1"),l=O("38;5;203"),V={C:[" \u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D"],O:[" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D "],R:["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u255D"],N:["\u2588\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551","\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"],I:["\u2588\u2588\u2557","\u2588\u2588\u2551","\u2588\u2588\u2551","\u2588\u2588\u2551","\u2588\u2588\u2551","\u255A\u2550\u255D"],L:["\u2588\u2588\u2557 ","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"],U:["\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D "],S:["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"]};function X(o){let e=["","","","","",""];for(let t of o){let n=V[t],i=Math.max(...n.map(s=>[...s].length));for(let s=0;s<6;s++)e[s]+=n[s].padEnd(i," ")+" "}return e.join(`
3
- `)}function z(){let o=b?"demo mode (offline, sample data)":new URL(f).host;console.log(),console.log(X("CORNILIUS")),console.log(),console.log(" "+H(a("Head of Analytics"))+r(" \xB7 ")+a(o));let e=b?r("(demo)"):D()?I("\u25CF token loaded"):r("\u25CB not signed in \u2014 /login");console.log(" "+e),console.log(" "+r("Type your analytics question, or ")+a("/help")+r(" for commands. ")+a("/exit")+r(" to quit.")),console.log()}function D(){if(process.env.CORNILIUS_TOKEN)return process.env.CORNILIUS_TOKEN.trim();try{return(JSON.parse(h.readFileSync(m,"utf8")).token||"").trim()||null}catch{return null}}function M(){try{return JSON.parse(h.readFileSync(m,"utf8"))}catch{return{}}}function W(o){h.mkdirSync(F,{recursive:!0,mode:448});let e=`${m}.tmp`;h.writeFileSync(e,JSON.stringify(o),{mode:384}),h.renameSync(e,m);try{h.chmodSync(m,384);let t=h.statSync(m).mode&511;t!==384&&console.log(" "+l(`warning: ${m} is ${t.toString(8)}, not 600 \u2014 your token may be readable by others.`))}catch(t){console.log(" "+l(`warning: could not secure ${m} (${t.message}).`))}}function Z(){try{h.rmSync(m)}catch{}}var T="api.cornilius.ai",U=!1,ee=3e4;async function L(o){if(b)return le(o);let e=D();if(!e)return{status:401,body:{error:"no token"}};let t;try{let u=new URL(f);if(t=u.host,u.protocol!=="https:"&&process.env.CORNILIUS_INSECURE!=="1")return{status:0,networkError:`refusing to send your token over ${u.protocol}// \u2014 set CORNILIUS_INSECURE=1 to override`}}catch{return{status:0,networkError:"invalid CORNILIUS_API"}}t!==T&&!U&&(U=!0,console.log(" "+l(`sending your token to ${t}`)+r(` (not the default ${T})`)));let n=new AbortController,i=setTimeout(()=>n.abort(),ee),s;try{s=await fetch(`${f}/api/command`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({command:o}),signal:n.signal})}catch(u){return{status:0,networkError:u.name==="AbortError"?"request timed out":String(u.cause?.code||u.message||u)}}finally{clearTimeout(i)}let c=null;try{c=await s.json()}catch{c=null}return{status:s.status,body:c}}var q=[],k=null;function E(o,e=a){let t=o&&o.content||[];for(let n of t)n&&typeof n.text=="string"&&console.log(" "+e(n.text))}function P(o,{status:e,body:t,networkError:n}){if(n!==void 0){console.log(" "+l(`Couldn't reach Cornilius (${n}).`)+r(` [${f}]`));return}if(e===0)return;if(e===401){console.log(" "+l("Not signed in.")+r(" Run ")+a("/login")+r(" (or set CORNILIUS_TOKEN)."));return}if(e===429){let c=t&&(t.message||t.content&&t.content[0]&&t.content[0].text)||"Quota exceeded.";console.log(" "+l("\u26A0 "+c)),t&&t.upgrade_url&&console.log(" "+r(t.upgrade_url));return}if(!t){console.log(" "+l(`Unexpected response (HTTP ${e}).`));return}let i=t.kind;t.structuredContent&&Array.isArray(t.structuredContent.playbooks)&&(q=t.structuredContent.playbooks);let s=t.structuredContent&&t.structuredContent.confirmation_token;if(s&&/\/delete\b/.test(o)&&!/--confirm\b/.test(o)){let c=o.match(/\/delete\s+(\S+)\s+(\S+)/);if(c){k={type:c[1],id:c[2],token:s},E(t,l),console.log(" "+a("Type the word ")+H(a("delete"))+a(" to confirm, or anything else to cancel."));return}}if(i==="error"||t.isError)return E(t,l);i!=="noop"&&E(t,a)}function te(){let o=[["just type\u2026","ask anything \u2014 Cornilius runs the analysis"],["/playbooks","list saved playbooks (numbered)"],["/run <#|id>","run an exact playbook \u2014 no guessing"],["/recall [topic]","what Cornilius remembers (issues, insights, history)"],["/delete <type> <id>","delete something \u2014 two-step, asks you to confirm"],["/status","tenant, session, usage"],["/whoami","who am I \u2014 your tenant id"],["/login /logout","sign in / sign out"],["/clear","clear the screen"],["/help","this help"],["/exit","quit (or Ctrl-D)"]];console.log();for(let[e,t]of o)console.log(" "+a(e.padEnd(22))+r(t));console.log()}function $(o){return o.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function oe(){let o=$(R.randomBytes(32)),e=$(R.createHash("sha256").update(o,"ascii").digest());return{verifier:o,challenge:e}}function ne(){return $(R.randomBytes(24))}async function re(o){let e=M();if(e.client_id&&e.api===f)return e.client_id;let t=await fetch(`${f}/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({client_name:"Cornilius CLI",redirect_uris:[o],grant_types:["authorization_code"],token_endpoint_auth_method:"none"})});if(!t.ok)throw new Error(`registration failed (${t.status}): ${(await t.text()).slice(0,200)}`);let n=await t.json();if(!n.client_id)throw new Error("registration returned no client_id");return W({...e,client_id:n.client_id,api:f}),n.client_id}function se(o){let e=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",t=process.platform==="win32"?["/c","start","",o]:[o];try{return G(e,t,{stdio:"ignore",detached:!0}).unref(),!0}catch{return!1}}function ie(o){return`<!doctype html><meta charset="utf-8"><title>Cornilius</title><body style="font:16px system-ui;background:#0A0A0A;color:#FFF7ED;display:grid;place-items:center;height:100vh;margin:0"><div style="max-width:32rem;padding:2rem;text-align:center"><div style="color:#F59E0B;font-size:1.4rem;font-weight:700;margin-bottom:.75rem">Cornilius</div><p>${o==="success"?"Signed in to Cornilius. You can close this tab and return to your terminal.":o==="denied"?"Sign-in was cancelled. You can close this tab.":"Something went wrong. Return to your terminal and try again."}</p></div></body>`}async function ce(){if(b){console.log(" "+r("(demo) sign-in is simulated."));return}if(process.env.CORNILIUS_TOKEN){console.log(" "+r("CORNILIUS_TOKEN is set \u2014 that token is already used; /login not needed."));return}let o="/callback",e=18e4,{verifier:t,challenge:n}=oe(),i=ne(),s,c,u=new Promise((d,g)=>{s=d,c=g}),S=Y.createServer((d,g)=>{let w=new URL(d.url,"http://127.0.0.1");if(w.pathname!==o){g.writeHead(404).end();return}let p=w.searchParams.get("error"),_=w.searchParams.get("code"),v=w.searchParams.get("state"),C=!p&&_&&v===i;g.writeHead(C?200:400,{"Content-Type":"text/html; charset=utf-8"}),g.end(ie(C?"success":p==="access_denied"?"denied":"error")),p?c(new Error(p==="access_denied"?"you cancelled sign-in.":`authorization error: ${p}`)):v!==i?c(new Error("state mismatch \u2014 possible CSRF; aborting.")):_?s(_):c(new Error("no authorization code in callback."))});await new Promise(d=>S.listen(0,"127.0.0.1",d));let x=`http://127.0.0.1:${S.address().port}${o}`;try{let d=await re(x),g=`${f}/authorize?`+new URLSearchParams({response_type:"code",client_id:d,redirect_uri:x,state:i,code_challenge:n,code_challenge_method:"S256",scope:"mcp"}).toString();console.log(" "+I("Opening your browser to sign in\u2026")),console.log(" "+r(`If it doesn't open, paste this URL:
4
- `)+a(g)),se(g);let w=await Promise.race([u,new Promise((v,C)=>setTimeout(()=>C(new Error("timed out after 3 minutes waiting for sign-in.")),e))]),p=await fetch(`${f}/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:w,redirect_uri:x,client_id:d,code_verifier:t}).toString()});if(!p.ok)throw new Error(`token exchange failed (${p.status}): ${(await p.text()).slice(0,200)}`);let _=await p.json();if(!_.access_token)throw new Error("token endpoint returned no access_token");W({...M(),token:_.access_token,client_id:d,api:f}),console.log(" "+I("\u2713 Signed in.")+r(` Token stored at ${m} (0600). Run `)+a("/status")+r(" to verify."))}catch(d){console.log(" "+l(`Sign-in failed: ${d.message}`)),process.exitCode=1}finally{S.close()}}function ae(){Z(),console.log(" "+r("Signed out \u2014 local token cleared."))}async function B(o){let e=o.trim();if(!e)return;if(k){let{type:i,id:s,token:c}=k;k=null,e==="delete"?P(`/delete ${i} ${s}`,await L(`/delete ${i} ${s} --token ${c} --confirm delete`)):console.log(" "+r("cancelled \u2014 nothing was deleted."));return}if(e==="/help"||e==="/?"||e==="/")return te();if(e==="/clear"){console.clear(),z();return}if((e==="/exit"||e==="/quit")&&(console.log(r(" bye.")),process.exit(0)),e==="/logout")return ae();if(e==="/login")return ce();let t=e,n=e.match(/^\/run\s+(\d+)\s*$/);if(n){let i=q.find(s=>s.number===Number(n[1]));if(i&&i.id)t=`/run ${i.id}`;else{console.log(" "+l("Run /playbooks first, then /run <#>")+r(" (or /run <playbook-id> directly)."));return}}P(t,await L(t))}function le(o){let e=o.trim(),t=[{number:1,id:"pb-builtin-funnel-pa-step-dropoff",name:"Funnel step drop-off scan"},{number:2,id:"pb-builtin-retention-pa-cohort-curve",name:"Cohort retention curve diagnosis"},{number:3,id:"pb-builtin-activation-aha-finder",name:"Activation aha-moment finder"}];if(e==="/playbooks")return{status:200,body:{kind:"dispatch",structuredContent:{playbooks:t},content:[{type:"text",text:`\u{1F4CB} Saved playbooks (demo)
5
- `+t.map(n=>` ${n.number}. ${n.name}`).join(`
6
- `)}]}};if(e.startsWith("/run "))return{status:200,body:{kind:"dispatch",content:[{type:"text",text:`\u25B8 running ${e.slice(5)} (demo) \u2192 analyze(playbook_id=\u2026)`}]}};if(e.startsWith("/recall"))return{status:200,body:{kind:"dispatch",content:[{type:"text",text:"\u{1F9E0} Remembered (demo): 2 open issues \xB7 5 insights"}]}};if(e.startsWith("/status")||e.startsWith("/whoami"))return{status:200,body:{kind:"local",content:[{type:"text",text:"tenant acme-corp \xB7 ready (demo)"}]}};if(e.startsWith("/delete ")){let n=e.match(/\/delete\s+(\S+)\s+(\S+)/);return/--confirm\b/.test(e)?{status:200,body:{kind:"dispatch",content:[{type:"text",text:`\u2713 deleted ${n?n[1]+" '"+n[2]+"'":"item"} (demo) \u2014 recoverable for 30 days`}]}}:{status:200,body:{kind:"dispatch",isError:!0,structuredContent:{confirmation_token:"demo-token"},content:[{type:"text",text:`\u26A0 This will delete ${n?n[1]+" '"+n[2]+"'":"an item"} (demo).`}]}}}return e.startsWith("/")?{status:400,body:{kind:"error",content:[{type:"text",text:"unknown command (demo)"}]}}:{status:200,body:{kind:"dispatch",content:[{type:"text",text:`\u25B8 analyzing (demo) \u2192 analyze(type='custom', prompt='${e}')`}]}}}function de(){console.log(`
2
+ import J from"node:readline";import w from"node:fs";import Y from"node:os";import F from"node:path";import R from"node:crypto";import G from"node:http";import{spawn as Q}from"node:child_process";var H=process.argv.slice(2),I=H.includes("--demo")||process.env.CORNILIUS_DEMO==="1",m=(process.env.CORNILIUS_API||"https://api.cornilius.ai").replace(/\/+$/,""),z=F.join(Y.homedir(),".cornilius"),_=F.join(z,"credentials.json"),T=null,V=process.stdout.isTTY&&!process.env.NO_COLOR,x=t=>e=>V?`\x1B[${t}m${e}\x1B[0m`:e,b=x("38;5;208"),a=t=>t,r=x("38;5;245"),D=x("1"),p=x("38;5;203"),X={C:[" \u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D"],O:[" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D "],R:["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u255D"],N:["\u2588\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551","\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"],I:["\u2588\u2588\u2557","\u2588\u2588\u2551","\u2588\u2588\u2551","\u2588\u2588\u2551","\u2588\u2588\u2551","\u255A\u2550\u255D"],L:["\u2588\u2588\u2557 ","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u2588\u2588\u2551 ","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"],U:["\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2551 \u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D "],S:["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557","\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551","\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"]};function Z(t){let e=["","","","","",""];for(let o of t){let n=X[o],i=Math.max(...n.map(s=>[...s].length));for(let s=0;s<6;s++)e[s]+=n[s].padEnd(i," ")+" "}return e.join(`
3
+ `)}function M(){let t=I?"demo mode (offline, sample data)":new URL(m).host;console.log(),console.log(Z("CORNILIUS")),console.log(),console.log(" "+D(a("Head of Analytics"))+r(" \xB7 ")+a(t));let e=I?r("(demo)"):W()?b("\u25CF token loaded"):r("\u25CB not signed in \u2014 /login");console.log(" "+e),console.log(" "+r("Type your analytics question, or ")+a("/help")+r(" for commands. ")+a("/exit")+r(" to quit.")),console.log()}function W(){return process.env.CORNILIUS_TOKEN?process.env.CORNILIUS_TOKEN.trim():T}function q(){try{return JSON.parse(w.readFileSync(_,"utf8"))}catch{return{}}}function L(t){let{token:e,...o}=t;w.mkdirSync(z,{recursive:!0,mode:448});let n=`${_}.tmp`;w.writeFileSync(n,JSON.stringify(o),{mode:384}),w.renameSync(n,_);try{w.chmodSync(_,384)}catch{}}function ee(){try{let t=JSON.parse(w.readFileSync(_,"utf8"));t&&t.token&&L(t)}catch{}}var U="api.cornilius.ai",P=!1,te=3e4;async function A(t){if(I)return de(t);let e=W();if(!e)return{status:401,body:{error:"no token"}};let o;try{let d=new URL(m);if(o=d.host,d.protocol!=="https:"&&process.env.CORNILIUS_INSECURE!=="1")return{status:0,networkError:`refusing to send your token over ${d.protocol}// \u2014 set CORNILIUS_INSECURE=1 to override`}}catch{return{status:0,networkError:"invalid CORNILIUS_API"}}o!==U&&!P&&(P=!0,console.log(" "+p(`sending your token to ${o}`)+r(` (not the default ${U})`)));let n=new AbortController,i=setTimeout(()=>n.abort(),te),s;try{s=await fetch(`${m}/api/command`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({command:t}),signal:n.signal})}catch(d){return{status:0,networkError:d.name==="AbortError"?"request timed out":String(d.cause?.code||d.message||d)}}finally{clearTimeout(i)}let c=null;try{c=await s.json()}catch{c=null}return{status:s.status,body:c}}var B=[],k=null;function E(t,e=a){let o=t&&t.content||[];for(let n of o)n&&typeof n.text=="string"&&console.log(" "+e(n.text))}function j(t,{status:e,body:o,networkError:n}){if(n!==void 0){console.log(" "+p(`Couldn't reach Cornilius (${n}).`)+r(` [${m}]`));return}if(e===0)return;if(e===401){console.log(" "+p("Not signed in.")+r(" Run ")+a("/login")+r(" (or set CORNILIUS_TOKEN)."));return}if(e===429){let c=o&&(o.message||o.content&&o.content[0]&&o.content[0].text)||"Quota exceeded.";console.log(" "+p("\u26A0 "+c)),o&&o.upgrade_url&&console.log(" "+r(o.upgrade_url));return}if(!o){console.log(" "+p(`Unexpected response (HTTP ${e}).`));return}let i=o.kind;o.structuredContent&&Array.isArray(o.structuredContent.playbooks)&&(B=o.structuredContent.playbooks);let s=o.structuredContent&&o.structuredContent.confirmation_token;if(s&&/\/delete\b/.test(t)&&!/--confirm\b/.test(t)){let c=t.match(/\/delete\s+(\S+)\s+(\S+)/);if(c){k={type:c[1],id:c[2],token:s},E(o,p),console.log(" "+a("Type the word ")+D(a("delete"))+a(" to confirm, or anything else to cancel."));return}}if(i==="error"||o.isError)return E(o,p);i!=="noop"&&E(o,a)}function oe(){let t=[["just type\u2026","ask anything \u2014 Cornilius runs the analysis"],["/playbooks","list saved playbooks (numbered)"],["/run <#|id>","run an exact playbook \u2014 no guessing"],["/recall [topic]","what Cornilius remembers (issues, insights, history)"],["/delete <type> <id>","delete something \u2014 two-step, asks you to confirm"],["/status","tenant, session, usage"],["/whoami","who am I \u2014 your tenant id"],["/login /logout","sign in / sign out"],["/clear","clear the screen"],["/help","this help"],["/exit","quit (or Ctrl-D)"]];console.log();for(let[e,o]of t)console.log(" "+a(e.padEnd(22))+r(o));console.log()}function N(t){return t.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function ne(){let t=N(R.randomBytes(32)),e=N(R.createHash("sha256").update(t,"ascii").digest());return{verifier:t,challenge:e}}function re(){return N(R.randomBytes(24))}async function se(t){let e=q();if(e.client_id&&e.api===m)return e.client_id;let o=await fetch(`${m}/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({client_name:"Cornilius CLI",redirect_uris:[t],grant_types:["authorization_code"],token_endpoint_auth_method:"none"})});if(!o.ok)throw new Error(`registration failed (${o.status}): ${(await o.text()).slice(0,200)}`);let n=await o.json();if(!n.client_id)throw new Error("registration returned no client_id");return L({...e,client_id:n.client_id,api:m}),n.client_id}function ie(t){let e=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",o=process.platform==="win32"?["/c","start","",t]:[t];try{return Q(e,o,{stdio:"ignore",detached:!0}).unref(),!0}catch{return!1}}function ce(t){return`<!doctype html><meta charset="utf-8"><title>Cornilius</title><body style="font:16px system-ui;background:#0A0A0A;color:#FFF7ED;display:grid;place-items:center;height:100vh;margin:0"><div style="max-width:32rem;padding:2rem;text-align:center"><div style="color:#F59E0B;font-size:1.4rem;font-weight:700;margin-bottom:.75rem">Cornilius</div><p>${t==="success"?"Signed in to Cornilius. You can close this tab and return to your terminal.":t==="denied"?"Sign-in was cancelled. You can close this tab.":"Something went wrong. Return to your terminal and try again."}</p></div></body>`}async function ae(){if(I){console.log(" "+r("(demo) sign-in is simulated."));return}if(process.env.CORNILIUS_TOKEN){console.log(" "+r("CORNILIUS_TOKEN is set \u2014 that token is already used; /login not needed."));return}let t="/callback",e=18e4,{verifier:o,challenge:n}=ne(),i=re(),s,c,d=new Promise((l,f)=>{s=l,c=f}),S=G.createServer((l,f)=>{let g=new URL(l.url,"http://127.0.0.1");if(g.pathname!==t){f.writeHead(404).end();return}let u=g.searchParams.get("error"),y=g.searchParams.get("code"),v=g.searchParams.get("state"),C=!u&&y&&v===i;f.writeHead(C?200:400,{"Content-Type":"text/html; charset=utf-8"}),f.end(ce(C?"success":u==="access_denied"?"denied":"error")),u?c(new Error(u==="access_denied"?"you cancelled sign-in.":`authorization error: ${u}`)):v!==i?c(new Error("state mismatch \u2014 possible CSRF; aborting.")):y?s(y):c(new Error("no authorization code in callback."))});await new Promise(l=>S.listen(0,"127.0.0.1",l));let O=`http://127.0.0.1:${S.address().port}${t}`;try{let l=await se(O),f=`${m}/authorize?`+new URLSearchParams({response_type:"code",client_id:l,redirect_uri:O,state:i,code_challenge:n,code_challenge_method:"S256",scope:"mcp"}).toString();console.log(" "+b("Opening your browser to sign in\u2026")),console.log(" "+r(`If it doesn't open, paste this URL:
4
+ `)+a(f)),ie(f);let g=await Promise.race([d,new Promise((v,C)=>setTimeout(()=>C(new Error("timed out after 3 minutes waiting for sign-in.")),e))]),u=await fetch(`${m}/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:g,redirect_uri:O,client_id:l,code_verifier:o}).toString()});if(!u.ok)throw new Error(`token exchange failed (${u.status}): ${(await u.text()).slice(0,200)}`);let y=await u.json();if(!y.access_token)throw new Error("token endpoint returned no access_token");T=y.access_token,L({...q(),client_id:l,api:m}),console.log(" "+b("\u2713 Signed in for this session.")+r(" Run ")+a("/status")+r(" to verify. You'll sign in again next time you open the CLI."))}catch(l){console.log(" "+p(`Sign-in failed: ${l.message}`)),process.exitCode=1}finally{S.close()}}function le(){T=null,console.log(" "+r("Signed out \u2014 session token cleared."))}async function K(t){let e=t.trim();if(!e)return;if(k){let{type:i,id:s,token:c}=k;k=null,e==="delete"?j(`/delete ${i} ${s}`,await A(`/delete ${i} ${s} --token ${c} --confirm delete`)):console.log(" "+r("cancelled \u2014 nothing was deleted."));return}if(e==="/help"||e==="/?"||e==="/")return oe();if(e==="/clear"){console.clear(),M();return}if((e==="/exit"||e==="/quit")&&(console.log(r(" bye.")),process.exit(0)),e==="/logout")return le();if(e==="/login")return ae();let o=e,n=e.match(/^\/run\s+(\d+)\s*$/);if(n){let i=B.find(s=>s.number===Number(n[1]));if(i&&i.id)o=`/run ${i.id}`;else{console.log(" "+p("Run /playbooks first, then /run <#>")+r(" (or /run <playbook-id> directly)."));return}}j(o,await A(o))}function de(t){let e=t.trim(),o=[{number:1,id:"pb-builtin-funnel-pa-step-dropoff",name:"Funnel step drop-off scan"},{number:2,id:"pb-builtin-retention-pa-cohort-curve",name:"Cohort retention curve diagnosis"},{number:3,id:"pb-builtin-activation-aha-finder",name:"Activation aha-moment finder"}];if(e==="/playbooks")return{status:200,body:{kind:"dispatch",structuredContent:{playbooks:o},content:[{type:"text",text:`\u{1F4CB} Saved playbooks (demo)
5
+ `+o.map(n=>` ${n.number}. ${n.name}`).join(`
6
+ `)}]}};if(e.startsWith("/run "))return{status:200,body:{kind:"dispatch",content:[{type:"text",text:`\u25B8 running ${e.slice(5)} (demo) \u2192 analyze(playbook_id=\u2026)`}]}};if(e.startsWith("/recall"))return{status:200,body:{kind:"dispatch",content:[{type:"text",text:"\u{1F9E0} Remembered (demo): 2 open issues \xB7 5 insights"}]}};if(e.startsWith("/status")||e.startsWith("/whoami"))return{status:200,body:{kind:"local",content:[{type:"text",text:"tenant acme-corp \xB7 ready (demo)"}]}};if(e.startsWith("/delete ")){let n=e.match(/\/delete\s+(\S+)\s+(\S+)/);return/--confirm\b/.test(e)?{status:200,body:{kind:"dispatch",content:[{type:"text",text:`\u2713 deleted ${n?n[1]+" '"+n[2]+"'":"item"} (demo) \u2014 recoverable for 30 days`}]}}:{status:200,body:{kind:"dispatch",isError:!0,structuredContent:{confirmation_token:"demo-token"},content:[{type:"text",text:`\u26A0 This will delete ${n?n[1]+" '"+n[2]+"'":"an item"} (demo).`}]}}}return e.startsWith("/")?{status:400,body:{kind:"error",content:[{type:"text",text:"unknown command (demo)"}]}}:{status:200,body:{kind:"dispatch",content:[{type:"text",text:`\u25B8 analyzing (demo) \u2192 analyze(type='custom', prompt='${e}')`}]}}}function ue(){console.log(`
7
7
  Cornilius \u2014 operate your Head of Analytics from the terminal.
8
8
 
9
9
  Usage:
@@ -18,5 +18,5 @@ import K from"node:readline";import h from"node:fs";import J from"node:os";impor
18
18
 
19
19
  Env: CORNILIUS_API (default https://api.cornilius.ai), CORNILIUS_TOKEN (override),
20
20
  CORNILIUS_DEMO=1, NO_COLOR=1.
21
- `)}var y=j.filter(o=>o!=="--demo");(y[0]==="--version"||y[0]==="-v")&&(console.log("1.0.2"),process.exit(0));(y[0]==="--help"||y[0]==="-h")&&(de(),process.exit(0));y.length&&y[0]!=="--splash"&&(await B(y.join(" ")),k&&console.log(" "+r("Two-step delete needs interactive mode \u2014 run ")+a("cornilius")+r(" with no arguments.")),process.exit(0));z();y[0]==="--splash"&&process.exit(0);var N=K.createInterface({input:process.stdin,output:process.stdout,prompt:I("cornilius> ")});N.prompt();for await(let o of N){try{await B(o)}catch(e){console.log(" "+l("error: "+(e.message||e)))}N.prompt()}console.log(r(`
21
+ `)}var h=H.filter(t=>t!=="--demo");(h[0]==="--version"||h[0]==="-v")&&(console.log("1.0.3"),process.exit(0));(h[0]==="--help"||h[0]==="-h")&&(ue(),process.exit(0));ee();h.length&&h[0]!=="--splash"&&(await K(h.join(" ")),k&&console.log(" "+r("Two-step delete needs interactive mode \u2014 run ")+a("cornilius")+r(" with no arguments.")),process.exit(0));M();h[0]==="--splash"&&process.exit(0);var $=J.createInterface({input:process.stdin,output:process.stdout,prompt:b("cornilius> ")});$.prompt();for await(let t of $){try{await K(t)}catch(e){console.log(" "+p("error: "+(e.message||e)))}$.prompt()}console.log(r(`
22
22
  bye.`));process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cornilius",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Cornilius CLI — operate your Head of Analytics from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {