cornilius 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2026 Cornilius. All rights reserved.
2
+
3
+ This software is proprietary and confidential. Unauthorized copying,
4
+ distribution, modification, or use of this software, via any medium, is
5
+ strictly prohibited without prior written permission.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Cornilius CLI
2
+
3
+ The installable, branded command-line client for **Cornilius** — your Head of
4
+ Analytics, operated from the terminal. It's the human "front door" that compiles
5
+ to the **same `dispatch`** the Claude.ai MCP tools use (one vocabulary, two
6
+ front doors).
7
+
8
+ This is a **proprietary** client (UNLICENSED), shipped as a single **minified**
9
+ Node bundle — zero runtime dependencies, Node 18+. The published package is the
10
+ built artifact.
11
+
12
+ ## Install (Node 18+)
13
+
14
+ ```bash
15
+ npx cornilius # run without installing
16
+ npm i -g cornilius # or install globally, then just: cornilius
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```bash
22
+ cornilius # interactive — splash + prompt (sign in with /login)
23
+ cornilius /playbooks # one-shot mode (run one command and exit)
24
+ cornilius --demo # offline UX preview (no token, no network)
25
+ cornilius --splash # print the branded splash and exit
26
+ cornilius --version # print the version
27
+ cornilius --help # usage
28
+ NO_COLOR=1 cornilius # plain ASCII (no color)
29
+ ```
30
+
31
+ **Auth:** `/login` opens your browser and signs you in (RFC 8252 native-app
32
+ loopback: PKCE + CSRF `state`, a one-time local `127.0.0.1` callback, code →
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
36
+ refused over non-HTTPS, and a non-default host is warned.
37
+
38
+ Env: `CORNILIUS_API` (default `https://api.cornilius.ai`), `CORNILIUS_TOKEN`,
39
+ `CORNILIUS_DEMO=1`, `CORNILIUS_INSECURE=1` (allow non-HTTPS — dev only),
40
+ `NO_COLOR=1`.
41
+
42
+ ## Command vocabulary
43
+
44
+ Modelled on **Claude Code** (natural language is the primary input; slash
45
+ commands handle control/meta) and peer CLIs (`gh`/Vercel/Heroku resource→verb).
46
+
47
+ **Just type your question** → Cornilius runs the analysis.
48
+
49
+ | Command | Does |
50
+ |---|---|
51
+ | `/playbooks` | list saved playbooks (numbered) |
52
+ | `/run <#\|id>` | run an exact playbook — no guessing |
53
+ | `/recall [topic]` | what Cornilius remembers (issues, insights, history) |
54
+ | `/delete <type> <id>` | delete something — two-step, asks you to confirm |
55
+ | `/status` | tenant, session, usage |
56
+ | `/whoami` | your tenant id |
57
+ | `/login` `/logout` | sign in / sign out (browser OAuth) |
58
+ | `/clear` `/help` `/exit` | local / session |
59
+
60
+ ## Develop & release (maintainers)
61
+
62
+ ```bash
63
+ npm install # esbuild (build-time only)
64
+ npm run build # bundle + minify src/cornilius.mjs → dist/cornilius.mjs
65
+ node src/cornilius.mjs --demo # run the source directly during development
66
+ npm pack # build + pack the publishable tarball (for smoke tests)
67
+ ```
68
+
69
+ Releases publish automatically — bump the version and push a tag:
70
+
71
+ ```bash
72
+ npm version patch # or minor / major
73
+ git push --follow-tags # the publish.yml GitHub Action builds + publishes to npm
74
+ ```
75
+
76
+ The first `v*` tag claims the unscoped `cornilius` name; subsequent tags publish
77
+ new versions. See `.github/workflows/publish.yml`.
@@ -0,0 +1,22 @@
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),O=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,C=o=>e=>Q?`\x1B[${o}m${e}\x1B[0m`:e,S=C("38;5;208"),a=C("38;5;230"),r=C("38;5;245"),H=C("1"),l=C("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.map(t=>S(t)).join(`
3
+ `)}function z(){let o=O?"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=O?r("(demo)"):D()?S("\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(O)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(O){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}),b=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"),I=!p&&_&&v===i;g.writeHead(I?200:400,{"Content-Type":"text/html; charset=utf-8"}),g.end(ie(I?"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=>b.listen(0,"127.0.0.1",d));let x=`http://127.0.0.1:${b.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(" "+S("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,I)=>setTimeout(()=>I(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(" "+S("\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{b.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(`
7
+ Cornilius \u2014 operate your Head of Analytics from the terminal.
8
+
9
+ Usage:
10
+ cornilius start the interactive prompt (sign in with /login)
11
+ cornilius <command> run one command and exit, e.g. cornilius /playbooks
12
+ cornilius --demo offline UX preview (no token, no network)
13
+ cornilius --splash print the branded splash and exit
14
+ cornilius --version print the version
15
+ cornilius --help this help
16
+
17
+ In the prompt: just type a question, or /help for the command list.
18
+
19
+ Env: CORNILIUS_API (default https://api.cornilius.ai), CORNILIUS_TOKEN (override),
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.0"),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:S("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(`
22
+ bye.`));process.exit(0);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "cornilius",
3
+ "version": "1.0.0",
4
+ "description": "Cornilius CLI — operate your Head of Analytics from the terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cornilius": "dist/cornilius.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "dist/cornilius.mjs",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "node scripts/build.mjs",
19
+ "prepack": "npm run build",
20
+ "start": "node src/cornilius.mjs"
21
+ },
22
+ "keywords": [
23
+ "cornilius",
24
+ "analytics",
25
+ "cli",
26
+ "mcp",
27
+ "head-of-analytics"
28
+ ],
29
+ "license": "UNLICENSED",
30
+ "homepage": "https://cornilius.ai",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "devDependencies": {
35
+ "esbuild": "^0.28.0"
36
+ }
37
+ }