icoa-cli 2.19.417 → 2.19.418

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.
@@ -1 +1 @@
1
- import{appendFileSync as o,readFileSync as t,existsSync as e}from"node:fs";import{join as n}from"node:path";import{homedir as s}from"node:os";import chalk from"chalk";import{printSuccess as r,printInfo as a,printHeader as i,printError as l}from"../lib/ui.js";import{logCommand as c}from"../lib/logger.js";const g=n(s(),"icoa-notes.txt");export function registerNoteCommand(t){t.command("note [text...]").description("Add a logbook entry (inside an exam: tagged to current question; outside: global scratchpad)").action(async t=>{c(`note ${t?.join(" ")||""}`);const{getRealExamState:e,saveExamState:n}=await import("../lib/exam-state.js"),s=e(),a=!!s&&"demo-free"!==s.session.examId;if(!t||0===t.length)return void(a?u(s):d());let i,y=[...t];const m=t[0].match(/^q?(\d{1,3})$/i);m&&(i=parseInt(m[1],10),i>=1&&i<=200?y=t.slice(1):i=void 0);const f=y.join(" ").trim();if(!f)return l("Note text required. Examples:"),console.log(chalk.gray(" ")+chalk.cyan("note try base64 first")),void console.log(chalk.gray(" ")+chalk.cyan("note 5 specific to Q5"));if(a){const o=s._lastQ||1,t=i??o;s.notes||(s.notes=[]),s.notes.push({ts:(new Date).toISOString(),q:t,text:f}),s.interactions||(s.interactions=[]),s.interactions.push({ts:(new Date).toISOString(),q:t,type:"note_added",input:f.slice(0,200)}),n(s);const e=null!=i?chalk.cyan(`Q${t}`):chalk.gray(`Q${t}, current`);return r(`Added to your logbook (${e}): ${chalk.white(f)}`),void console.log(chalk.gray(" ")+chalk.gray(`(${s.notes.length} entries, submits with your answers)`))}const p=(new Date).toISOString().replace("T"," ").substring(0,19);o(g,`[${p}]${null!=i?` Q${i}`:""} ${f}\n`),r(`Note saved: ${chalk.gray(f)}`),console.log(chalk.gray(" (saved to ~/icoa-notes.txt — not tied to any exam)"))});const e=async(o,t)=>{c(`${t||"notes"} ${o||""}`);const{getRealExamState:e}=await import("../lib/exam-state.js"),n=e(),s=!!n&&"demo-free"!==n.session.examId;let r;if(o){const t=o.match(/^q?(\d{1,3})$/i);t&&(r=parseInt(t[1],10))}s?u(n,r):d()};t.command("notes [filter]").description('View your contestant logbook (inside exam: from current paper). Filter: "q5", "Q5", "5"').action(async o=>e(o,"notes")),t.command("logbook [filter]").description('View your contestant logbook (alias of `notes`). Filter: "q5", "Q5", "5"').action(async o=>e(o,"logbook"))}function u(o,t){const e=o.notes||[],n=null!=t?e.filter(o=>o.q===t):e;if(0===n.length)return a(null!=t?`No logbook entries for Q${t} yet.`:"Your logbook is empty for this paper."),console.log(),console.log(chalk.gray(" Add one: ")+chalk.cyan("note your idea here")+chalk.gray(" (auto-tags current question)")),console.log(chalk.gray(" or ")+chalk.cyan("note 5 specific to Q5")+chalk.gray(" (explicit question tag)")),void console.log();i(null!=t?`Logbook — Q${t}`:`Logbook — ${o.session.examId}`);const s=new Map,r=[];for(const o of n)"number"==typeof o.q?(s.has(o.q)||s.set(o.q,[]),s.get(o.q).push({ts:o.ts,text:o.text})):r.push({ts:o.ts,text:o.text});const l=[...s.keys()].sort((o,t)=>o-t);for(const o of l){console.log(chalk.bold.cyan(` Q${o}`));for(const t of s.get(o)){const o=t.ts.substring(11,19);console.log(chalk.gray(` ${o} `)+chalk.white(t.text))}}if(r.length){console.log(chalk.bold.gray(" (no Q tag)"));for(const o of r){const t=o.ts.substring(11,19);console.log(chalk.gray(` ${t} `)+chalk.white(o.text))}}console.log(),console.log(chalk.gray(` ${n.length} entr${1===n.length?"y":"ies"} in your logbook. Submits with answers when you finish the exam.`)),console.log()}function d(){if(!e(g))return a("Your logbook is empty. Add one with: note your text here"),console.log(),console.log(chalk.gray(" During an exam, entries auto-attach to the current question and submit with your answers.")),void console.log();const o=t(g,"utf-8");i("Logbook (global scratchpad, outside-exam)"),console.log(o),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Add one: ")+chalk.cyan("note your text")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" for all commands")),console.log()}
1
+ import{appendFileSync as o,readFileSync as t,existsSync as e}from"node:fs";import{join as n}from"node:path";import{homedir as s}from"node:os";import chalk from"chalk";import{c as r}from"../lib/colors.js";import{printSuccess as a,printInfo as i,printHeader as l,printError as c}from"../lib/ui.js";import{logCommand as g}from"../lib/logger.js";const u=n(s(),"icoa-notes.txt");export function registerNoteCommand(t){t.command("note [text...]").description("Add a logbook entry (inside an exam: tagged to current question; outside: global scratchpad)").action(async t=>{g(`note ${t?.join(" ")||""}`);const{getRealExamState:e,saveExamState:n}=await import("../lib/exam-state.js"),s=e(),r=!!s&&"demo-free"!==s.session.examId;if(!t||0===t.length)return void(r?d(s):m());let i,l=[...t];const y=t[0].match(/^q?(\d{1,3})$/i);y&&(i=parseInt(y[1],10),i>=1&&i<=200?l=t.slice(1):i=void 0);const f=l.join(" ").trim();if(!f)return c("Note text required. Examples:"),console.log(chalk.gray(" ")+chalk.cyan("note try base64 first")),void console.log(chalk.gray(" ")+chalk.cyan("note 5 specific to Q5"));if(r){const o=s._lastQ||1,t=i??o;s.notes||(s.notes=[]),s.notes.push({ts:(new Date).toISOString(),q:t,text:f}),s.interactions||(s.interactions=[]),s.interactions.push({ts:(new Date).toISOString(),q:t,type:"note_added",input:f.slice(0,200)}),n(s);const e=null!=i?chalk.cyan(`Q${t}`):chalk.gray(`Q${t}, current`);return a(`Added to your logbook (${e}): ${chalk.white(f)}`),void console.log(chalk.gray(" ")+chalk.gray(`(${s.notes.length} entries, submits with your answers)`))}const p=(new Date).toISOString().replace("T"," ").substring(0,19);o(u,`[${p}]${null!=i?` Q${i}`:""} ${f}\n`),a(`Note saved: ${chalk.gray(f)}`),console.log(chalk.gray(" (saved to ~/icoa-notes.txt — not tied to any exam)"))});const e=async(o,t)=>{g(`${t||"notes"} ${o||""}`);const{getRealExamState:e}=await import("../lib/exam-state.js"),n=e(),s=!!n&&"demo-free"!==n.session.examId;let r;if(o){const t=o.match(/^q?(\d{1,3})$/i);t&&(r=parseInt(t[1],10))}s?d(n,r):m()};t.command("notes [filter]").description('View your contestant logbook (inside exam: from current paper). Filter: "q5", "Q5", "5"').action(async o=>e(o,"notes")),t.command("logbook [filter]").description('View your contestant logbook (alias of `notes`). Filter: "q5", "Q5", "5"').action(async o=>e(o,"logbook"))}function d(o,t){const e=o.notes||[],n=null!=t?e.filter(o=>o.q===t):e;if(0===n.length)return i(null!=t?`No logbook entries for Q${t} yet.`:"Your logbook is empty for this paper."),console.log(),console.log(chalk.gray(" Add one: ")+chalk.cyan("note your idea here")+chalk.gray(" (auto-tags current question)")),console.log(chalk.gray(" or ")+chalk.cyan("note 5 specific to Q5")+chalk.gray(" (explicit question tag)")),void console.log();l(null!=t?`Logbook — Q${t}`:`Logbook — ${o.session.examId}`);const s=new Map,r=[];for(const o of n)"number"==typeof o.q?(s.has(o.q)||s.set(o.q,[]),s.get(o.q).push({ts:o.ts,text:o.text})):r.push({ts:o.ts,text:o.text});const a=[...s.keys()].sort((o,t)=>o-t);for(const o of a){console.log(chalk.bold.cyan(` Q${o}`));for(const t of s.get(o)){const o=t.ts.substring(11,19);console.log(chalk.gray(` ${o} `)+chalk.white(t.text))}}if(r.length){console.log(chalk.bold.gray(" (no Q tag)"));for(const o of r){const t=o.ts.substring(11,19);console.log(chalk.gray(` ${t} `)+chalk.white(o.text))}}console.log(),console.log(chalk.gray(` ${n.length} entr${1===n.length?"y":"ies"} in your logbook. Submits with answers when you finish the exam.`)),console.log()}function m(){if(!e(u))return i("Your logbook is empty. Add one with: note your text here"),console.log(),console.log(chalk.gray(" During an exam, entries auto-attach to the current question and submit with your answers.")),void console.log();const o=t(u,"utf-8");l("Logbook (global scratchpad, outside-exam)"),console.log(r.fg(o)),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Add one: ")+chalk.cyan("note your text")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" for all commands")),console.log()}
@@ -1 +1 @@
1
- import{readdirSync as o,readFileSync as e,existsSync as r}from"node:fs";import{join as n,dirname as t}from"node:path";import{fileURLToPath as c}from"node:url";import chalk from"chalk";import{printHeader as a,printInfo as i,printError as f,printTable as l}from"../lib/ui.js";import{logCommand as s}from"../lib/logger.js";export function registerRefCommand(p){p.command("ref [topic]").description("Quick reference for tools and commands").action(p=>{s(`ref ${p||""}`);const g=function(){const o=t(c(import.meta.url)),e=[n(o,"..","refs"),n(o,"..","..","refs"),n(process.cwd(),"refs")];for(const o of e)if(r(o))return o;return e[0]}();if(!p){if(a("Available References"),!r(g))return void f("Reference files not found.");const e=o(g).filter(o=>o.endsWith(".txt")).map(o=>o.replace(".txt","")).sort();if(0===e.length)return void i("No reference files available.");const n=e.map(o=>[chalk.white(o)]);return l(["Topic"],n),console.log(),console.log(chalk.gray(" Open one: ")+chalk.cyan("ref <topic>")+chalk.gray(" (e.g. ")+chalk.cyan("ref python")+chalk.gray(")")),console.log(chalk.gray(' No "back" needed — ref just prints and returns to the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log()}const m=n(g,`${p}.txt`);if(!r(m))return f(`Reference not found: ${p}`),void i('Run "ref" to see available topics.');const y=e(m,"utf-8");a(`Reference: ${p}`),console.log(y),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" End of ")+chalk.cyan(`ref ${p}`)+chalk.gray(". You are back at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt.")),console.log(chalk.gray(" Next: ")+chalk.cyan("ref")+chalk.gray(" list topics · ")+chalk.cyan("ref <topic>")+chalk.gray(" open another · ")+chalk.cyan("help")+chalk.gray(" all commands")),console.log()})}
1
+ import{readdirSync as o,readFileSync as e,existsSync as r}from"node:fs";import{join as n,dirname as t}from"node:path";import{fileURLToPath as c}from"node:url";import chalk from"chalk";import{c as a}from"../lib/colors.js";import{printHeader as i,printInfo as f,printError as l,printTable as s}from"../lib/ui.js";import{logCommand as p}from"../lib/logger.js";export function registerRefCommand(g){g.command("ref [topic]").description("Quick reference for tools and commands").action(g=>{p(`ref ${g||""}`);const m=function(){const o=t(c(import.meta.url)),e=[n(o,"..","refs"),n(o,"..","..","refs"),n(process.cwd(),"refs")];for(const o of e)if(r(o))return o;return e[0]}();if(!g){if(i("Available References"),!r(m))return void l("Reference files not found.");const e=o(m).filter(o=>o.endsWith(".txt")).map(o=>o.replace(".txt","")).sort();if(0===e.length)return void f("No reference files available.");const n=e.map(o=>[chalk.white(o)]);return s(["Topic"],n),console.log(),console.log(chalk.gray(" Open one: ")+chalk.cyan("ref <topic>")+chalk.gray(" (e.g. ")+chalk.cyan("ref python")+chalk.gray(")")),console.log(chalk.gray(' No "back" needed — ref just prints and returns to the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log()}const y=n(m,`${g}.txt`);if(!r(y))return l(`Reference not found: ${g}`),void f('Run "ref" to see available topics.');const d=e(y,"utf-8");i(`Reference: ${g}`),console.log(a.fg(d)),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" End of ")+chalk.cyan(`ref ${g}`)+chalk.gray(". You are back at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt.")),console.log(chalk.gray(" Next: ")+chalk.cyan("ref")+chalk.gray(" list topics · ")+chalk.cyan("ref <topic>")+chalk.gray(" open another · ")+chalk.cyan("help")+chalk.gray(" all commands")),console.log()})}
@@ -1 +1 @@
1
- import chalk from"chalk";import{spawn as e}from"node:child_process";import{copyFileSync as o,existsSync as n,mkdirSync as r,writeFileSync as a}from"node:fs";import{homedir as t}from"node:os";import{dirname as s,join as l,resolve as i}from"node:path";import{createInterface as c}from"node:readline";import{fileURLToPath as d}from"node:url";import{fetchWithCountdown as g}from"../lib/countdown.js";const m=l(t(),".icoa","cache","sim");import{getMainRl as p}from"../lib/main-rl.js";import{COOLDOWN_SECONDS as u,lastSimAt as y,markArmAt as f,markBundledAt as b,markSimAt as h}from"../lib/sim-cooldown.js";const w={baseline:{action:null,label:"Baseline — robot doing its job",hint:"Approach → grasp → lift → place → return. This is what normal looks like.",frames:90,narrative:!0},release:{action:{xyz:[.1,0,.3],rotation:[0,0,0],gripper_close:.05},label:'Prompt Injection — "release the gripper"',hint:"Gripper opens (0.05 < 0.10 win threshold). This is Paper D Q41 success."},patch:{action:{xyz:[-.3,.2,.5],rotation:[.5,0,.3],gripper_close:.95},label:"Adversarial Patch — sticker redirects the arm",hint:"Same instruction, but arm reaches LEFT instead of RIGHT. Paper D Q42 success."},confused:{action:{xyz:[.001,0,.001],rotation:[0,0,0],gripper_close:.5},label:'Modality Confusion — "pick up the purple object"',hint:"No purple object exists. VLA freezes near origin. Paper D Q43 success."},color:{action:{xyz:[.4,-.3,.6],rotation:[-.4,.2,-.1],gripper_close:.2},label:"Cross-Modal Attack — image + text manipulated together",hint:"Action drifts hard on multiple axes. Paper D Q49-class attack (future).",hidden:!0},ensemble:{action:null,label:"Ensemble — 9 robots dancing on one beat",hint:"Pre-rendered group dance. Instant. No cooldown.",bundled:"ensemble-v2.mp4"},arm:{action:null,label:"Arm mode — interactive 6-DOF companion-arm sandbox",hint:"Type 6 joint amplitudes (or a preset). Each render takes ~15s, 60s cooldown.",bundled:void 0}},v=Object.fromEntries(Object.entries(w).filter(([,e])=>!e.hidden));let j=!1;const x={1:{vec:[1,1,1,1,1.4,1],label:"full demo — every joint at full amplitude"},2:{vec:[1,0,0,0,0,0],label:"base spin only (joint 1 swings)"},3:{vec:[0,.8,.8,0,0,0],label:"shoulder + elbow (lift cycle)"},4:{vec:[0,0,0,.8,1.4,1],label:"wrist drill — j4 roll + j5 bend + j6 twist"},5:{vec:[-.7,-.8,.6,.7,-1,.5],label:"mixed asymmetric (chaotic)"}};function T(){return"darwin"===process.platform?"open":"win32"===process.platform?"start":"xdg-open"}async function A(o){const n=(Date.now()-y())/1e3;if(n<u){const e=Math.ceil(u-n);return console.log(chalk.yellow(` Please wait ${e}s before the next render.`)),console.log(chalk.gray(" (One render per minute keeps things fast for everyone.)")),!1}const r=`${process.env.ICOA_SERVER_URL?.replace(/\/+$/,"")||"https://practice.icoa2026.au"}/api/ai/vla/41/sim`;o.quietHeader||(console.log(),console.log(chalk.bold.cyan(" ICOA · MuJoCo Sim")),console.log(chalk.gray(" ")+chalk.white(o.label)),console.log());try{const n=fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:o.action,frames:o.frames,narrative:o.narrative,...o.panda2_pose?{panda2_pose:o.panda2_pose}:{}}),signal:AbortSignal.timeout(6e4)}),t=await g(n);if(!t.ok)return console.log(chalk.yellow(` Server returned HTTP ${t.status}.`)),!1;const s=await t.json();if(!s.success||!s.data?.mp4_b64)return console.log(chalk.yellow(" Renderer unavailable.")),!1;const i=process.env.TMPDIR||"/tmp",c=l(i,`icoa-sim-${o.scenarioName}-${Date.now()}.mp4`);a(c,Buffer.from(s.data.mp4_b64,"base64")),h(Date.now()),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Rendered.")),s.data.description&&console.log(chalk.gray(" Action: ")+chalk.white(s.data.description)),console.log(),console.log(chalk.gray(" ")+o.hint),console.log();const d=T();return d?(e(d,[c],{stdio:"ignore",detached:!0}).unref(),console.log(chalk.gray(" Opening in your default video player..."))):console.log(chalk.gray(" Video at: ")+chalk.white(c)),console.log(),!0}catch(e){const o=e instanceof Error?e.message:String(e);return console.log(chalk.yellow(` Sim error: ${o}`)),!1}}async function S(t,g=!1){const u=t.toLowerCase();if(j=g,"arm"===u)return void await async function(){console.log(),console.log(chalk.bold.cyan(" ICOA · Companion Arm Sandbox")),console.log(chalk.gray(" Drive the printable 6-DOF arm directly. Server renders, video opens.")),console.log(),console.log(chalk.gray(" Joint amplitudes (radians, suggested range −1.0 to +1.0):")),console.log(chalk.gray(" j1=base spin j2=shoulder j3=elbow")),console.log(chalk.gray(" j4=forearm roll j5=wrist pitch j6=wrist roll")),console.log(),console.log(chalk.gray(" Type 6 numbers separated by spaces, or pick a preset:"));for(const[e,o]of Object.entries(x)){const n=o.vec.map(e=>e.toFixed(1).padStart(4)).join(" ");console.log(chalk.gray(" ")+chalk.bold.cyan(e)+chalk.gray(" → ["+n+"] "+o.label))}console.log(),console.log(chalk.gray(" Each render: ~15s countdown + 60s cooldown between renders.")),console.log(chalk.gray(" Type ")+chalk.bold.cyan("q")+chalk.gray(" or ")+chalk.bold.cyan("exit")+chalk.gray(" to leave arm mode.")),console.log();const e=p(),o=null!==e,n=o?e.listeners("line").slice():[];o&&e.removeAllListeners("line");const r=o?e:c({input:process.stdin,output:process.stdout,terminal:!0});return new Promise(e=>{let a=!1;const t=()=>{process.stdout.write(chalk.bold.cyan(" icoa arm> "))},s=async l=>{const i=l.trim();if(a)return;if("q"===i||"exit"===i||"quit"===i||"back"===i||"menu"===i)return console.log(chalk.gray(" Leaving arm mode.")),console.log(),r.removeListener("line",s),(()=>{if(o){r.removeAllListeners("line");for(const e of n)r.on("line",e);r.prompt()}else r.close()})(),void e();if("help"===i||"?"===i)return console.log(chalk.gray(" 6 numbers (e.g. 0.5 0.7 0.5 0.3 0.6 0.3) or preset 1/2/3/4.")),void t();const c=(e=>{const o=e.trim();if(!o)return null;if(x[o])return x[o].vec;const n=o.split(/[\s,]+/).map(e=>Number(e));return 6!==n.length||n.some(e=>!Number.isFinite(e))?null:n.map(e=>Math.max(-1.5,Math.min(1.5,e)))})(i);if(!c)return console.log(chalk.yellow(" Need 6 numbers or a preset (1-4). Try ?")),void t();a=!0;const d=c.map(e=>e.toFixed(2)).join(" ");console.log(chalk.gray(" Sending pose [")+chalk.white(d)+chalk.gray("]")),await A({scenarioName:"arm",label:"Companion arm — your pose",hint:"Rendered from your 6-vector. Try another, or "+chalk.bold.cyan("q")+chalk.gray(" to leave."),action:null,frames:90,narrative:!0,panda2_pose:c,quietHeader:!0})&&f(Date.now()),a=!1,t()};r.on("line",s),t()})}();const y=w[u];if(!y){console.log(),console.log(chalk.yellow(` Unknown scenario: "${t}"`)),console.log(chalk.gray(" Available:"));for(const[e,o]of Object.entries(v))console.log(chalk.gray(" ")+chalk.bold.cyan(e.padEnd(10))+chalk.gray(o.label));return void console.log()}if(y.bundled)return void await async function(t,c){const g=await async function(e){const o=function(e){const o=s(d(import.meta.url));return[i(o,"..","..","assets","sim",e),i(o,"..","..","..","assets","sim",e)].find(e=>n(e))||null}(e);if(o)return o;const t=l(m,e);if(n(t))return t;console.log(),console.log(chalk.white("\n ██╗ ██████╗ ██████╗ █████╗\n ██║██╔════╝██╔═══██╗██╔══██╗\n ██║██║ ██║ ██║███████║\n ██║██║ ██║ ██║██╔══██║\n ██║╚██████╗╚██████╔╝██║ ██║\n ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝\n")),console.log(chalk.gray(" First-time setup — fetching the sim asset (~700 KB) ...")),console.log();try{r(m,{recursive:!0});const o=`https://icoa2026.au/assets/sim/${e}`,n=await fetch(o);if(!n.ok)return console.log(chalk.yellow(` Download failed (HTTP ${n.status}).`)),console.log(chalk.gray(" Try ")+chalk.bold.cyan("icoa sim baseline")+chalk.gray(" (server-rendered alternative).")),console.log(),null;const s=Buffer.from(await n.arrayBuffer());return a(t,s),console.log(chalk.green(" ✓ Cached — future runs are instant.")),console.log(),t}catch(e){return console.log(chalk.yellow(` Network error: ${e instanceof Error?e.message:String(e)}`)),console.log(chalk.gray(" Try ")+chalk.bold.cyan("icoa sim baseline")+chalk.gray(" instead.")),console.log(),null}}(c.bundled);if(!g)return;const p=process.env.TMPDIR||"/tmp",u=l(p,`icoa-sim-${t}-${Date.now()}.mp4`);o(g,u),b(Date.now()),j||(console.log(),console.log(chalk.bold.cyan(" ICOA · MuJoCo Sim")),console.log(chalk.gray(" ")+chalk.white(c.label)),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Ready (pre-rendered, instant playback).")),console.log(),console.log(chalk.gray(" ")+c.hint),console.log());const y=T();if(y?(e(y,[u],{stdio:"ignore",detached:!0}).unref(),console.log(chalk.gray(" Opening in your default video player..."))):console.log(chalk.gray(" Video at: ")+chalk.white(u)),console.log(),j)return;const f="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Want to drive the companion arm yourself? ")+chalk.bold.cyan(f+"sim arm")),console.log()}(u,y);if(await A({scenarioName:u,label:y.label,hint:y.hint,quietHeader:j,action:y.action,frames:y.frames,narrative:y.narrative,panda2_pose:y.panda2_pose}),j)return;const h="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Try other scenarios:"));for(const e of Object.keys(v))e!==u&&console.log(chalk.gray(" "+h+"sim ")+chalk.cyan(e));console.log()}export function registerSimCommand(e){e.command("sim [scenario]").description("MuJoCo VLA — sim (group dance) · sim arm (interactive companion arm) · sim baseline/release/patch/confused").action(async e=>{await S(e||"ensemble")})}export{w as SCENARIOS};export async function runSimQuiet(e){await S(e,!0)}
1
+ import chalk from"chalk";import{spawn as o}from"node:child_process";import{copyFileSync as e,existsSync as n,mkdirSync as r,writeFileSync as a}from"node:fs";import{homedir as t}from"node:os";import{dirname as s,join as l,resolve as i}from"node:path";import{createInterface as c}from"node:readline";import{fileURLToPath as d}from"node:url";import{fetchWithCountdown as g}from"../lib/countdown.js";const m=l(t(),".icoa","cache","sim");import{getMainRl as p}from"../lib/main-rl.js";import{COOLDOWN_SECONDS as u,lastSimAt as y,markArmAt as f,markBundledAt as b,markSimAt as h}from"../lib/sim-cooldown.js";import{httpFetch as w}from"../lib/http.js";const v={baseline:{action:null,label:"Baseline — robot doing its job",hint:"Approach → grasp → lift → place → return. This is what normal looks like.",frames:90,narrative:!0},release:{action:{xyz:[.1,0,.3],rotation:[0,0,0],gripper_close:.05},label:'Prompt Injection — "release the gripper"',hint:"Gripper opens (0.05 < 0.10 win threshold). This is Paper D Q41 success."},patch:{action:{xyz:[-.3,.2,.5],rotation:[.5,0,.3],gripper_close:.95},label:"Adversarial Patch — sticker redirects the arm",hint:"Same instruction, but arm reaches LEFT instead of RIGHT. Paper D Q42 success."},confused:{action:{xyz:[.001,0,.001],rotation:[0,0,0],gripper_close:.5},label:'Modality Confusion — "pick up the purple object"',hint:"No purple object exists. VLA freezes near origin. Paper D Q43 success."},color:{action:{xyz:[.4,-.3,.6],rotation:[-.4,.2,-.1],gripper_close:.2},label:"Cross-Modal Attack — image + text manipulated together",hint:"Action drifts hard on multiple axes. Paper D Q49-class attack (future).",hidden:!0},ensemble:{action:null,label:"Ensemble — 9 robots dancing on one beat",hint:"Pre-rendered group dance. Instant. No cooldown.",bundled:"ensemble-v2.mp4"},arm:{action:null,label:"Arm mode — interactive 6-DOF companion-arm sandbox",hint:"Type 6 joint amplitudes (or a preset). Each render takes ~15s, 60s cooldown.",bundled:void 0}},j=Object.fromEntries(Object.entries(v).filter(([,o])=>!o.hidden));let x=!1;const T={1:{vec:[1,1,1,1,1.4,1],label:"full demo — every joint at full amplitude"},2:{vec:[1,0,0,0,0,0],label:"base spin only (joint 1 swings)"},3:{vec:[0,.8,.8,0,0,0],label:"shoulder + elbow (lift cycle)"},4:{vec:[0,0,0,.8,1.4,1],label:"wrist drill — j4 roll + j5 bend + j6 twist"},5:{vec:[-.7,-.8,.6,.7,-1,.5],label:"mixed asymmetric (chaotic)"}};function A(){return"darwin"===process.platform?"open":"win32"===process.platform?"start":"xdg-open"}async function S(e){const n=(Date.now()-y())/1e3;if(n<u){const o=Math.ceil(u-n);return console.log(chalk.yellow(` Please wait ${o}s before the next render.`)),console.log(chalk.gray(" (One render per minute keeps things fast for everyone.)")),!1}const r=`${process.env.ICOA_SERVER_URL?.replace(/\/+$/,"")||"https://practice.icoa2026.au"}/api/ai/vla/41/sim`;e.quietHeader||(console.log(),console.log(chalk.bold.cyan(" ICOA · MuJoCo Sim")),console.log(chalk.gray(" ")+chalk.white(e.label)),console.log());try{const n=fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e.action,frames:e.frames,narrative:e.narrative,...e.panda2_pose?{panda2_pose:e.panda2_pose}:{}}),signal:AbortSignal.timeout(6e4)}),t=await g(n);if(!t.ok)return console.log(chalk.yellow(` Server returned HTTP ${t.status}.`)),!1;const s=await t.json();if(!s.success||!s.data?.mp4_b64)return console.log(chalk.yellow(" Renderer unavailable.")),!1;const i=process.env.TMPDIR||"/tmp",c=l(i,`icoa-sim-${e.scenarioName}-${Date.now()}.mp4`);a(c,Buffer.from(s.data.mp4_b64,"base64")),h(Date.now()),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Rendered.")),s.data.description&&console.log(chalk.gray(" Action: ")+chalk.white(s.data.description)),console.log(),console.log(chalk.gray(" ")+e.hint),console.log();const d=A();return d?(o(d,[c],{stdio:"ignore",detached:!0}).unref(),console.log(chalk.gray(" Opening in your default video player..."))):console.log(chalk.gray(" Video at: ")+chalk.white(c)),console.log(),!0}catch(o){const e=o instanceof Error?o.message:String(o);return console.log(chalk.yellow(` Sim error: ${e}`)),!1}}async function _(t,g=!1){const u=t.toLowerCase();if(x=g,"arm"===u)return void await async function(){console.log(),console.log(chalk.bold.cyan(" ICOA · Companion Arm Sandbox")),console.log(chalk.gray(" Drive the printable 6-DOF arm directly. Server renders, video opens.")),console.log(),console.log(chalk.gray(" Joint amplitudes (radians, suggested range −1.0 to +1.0):")),console.log(chalk.gray(" j1=base spin j2=shoulder j3=elbow")),console.log(chalk.gray(" j4=forearm roll j5=wrist pitch j6=wrist roll")),console.log(),console.log(chalk.gray(" Type 6 numbers separated by spaces, or pick a preset:"));for(const[o,e]of Object.entries(T)){const n=e.vec.map(o=>o.toFixed(1).padStart(4)).join(" ");console.log(chalk.gray(" ")+chalk.bold.cyan(o)+chalk.gray(" → ["+n+"] "+e.label))}console.log(),console.log(chalk.gray(" Each render: ~15s countdown + 60s cooldown between renders.")),console.log(chalk.gray(" Type ")+chalk.bold.cyan("q")+chalk.gray(" or ")+chalk.bold.cyan("exit")+chalk.gray(" to leave arm mode.")),console.log();const o=p(),e=null!==o,n=e?o.listeners("line").slice():[];e&&o.removeAllListeners("line");const r=e?o:c({input:process.stdin,output:process.stdout,terminal:!0});return new Promise(o=>{let a=!1;const t=()=>{process.stdout.write(chalk.bold.cyan(" icoa arm> "))},s=async l=>{const i=l.trim();if(a)return;if("q"===i||"exit"===i||"quit"===i||"back"===i||"menu"===i)return console.log(chalk.gray(" Leaving arm mode.")),console.log(),r.removeListener("line",s),(()=>{if(e){r.removeAllListeners("line");for(const o of n)r.on("line",o);r.prompt()}else r.close()})(),void o();if("help"===i||"?"===i)return console.log(chalk.gray(" 6 numbers (e.g. 0.5 0.7 0.5 0.3 0.6 0.3) or preset 1/2/3/4.")),void t();const c=(o=>{const e=o.trim();if(!e)return null;if(T[e])return T[e].vec;const n=e.split(/[\s,]+/).map(o=>Number(o));return 6!==n.length||n.some(o=>!Number.isFinite(o))?null:n.map(o=>Math.max(-1.5,Math.min(1.5,o)))})(i);if(!c)return console.log(chalk.yellow(" Need 6 numbers or a preset (1-4). Try ?")),void t();a=!0;const d=c.map(o=>o.toFixed(2)).join(" ");console.log(chalk.gray(" Sending pose [")+chalk.white(d)+chalk.gray("]")),await S({scenarioName:"arm",label:"Companion arm — your pose",hint:"Rendered from your 6-vector. Try another, or "+chalk.bold.cyan("q")+chalk.gray(" to leave."),action:null,frames:90,narrative:!0,panda2_pose:c,quietHeader:!0})&&f(Date.now()),a=!1,t()};r.on("line",s),t()})}();const y=v[u];if(!y){console.log(),console.log(chalk.yellow(` Unknown scenario: "${t}"`)),console.log(chalk.gray(" Available:"));for(const[o,e]of Object.entries(j))console.log(chalk.gray(" ")+chalk.bold.cyan(o.padEnd(10))+chalk.gray(e.label));return void console.log()}if(y.bundled)return void await async function(t,c){const g=await async function(o){const e=function(o){const e=s(d(import.meta.url));return[i(e,"..","..","assets","sim",o),i(e,"..","..","..","assets","sim",o)].find(o=>n(o))||null}(o);if(e)return e;const t=l(m,o);if(n(t))return t;console.log(),console.log(chalk.white("\n ██╗ ██████╗ ██████╗ █████╗\n ██║██╔════╝██╔═══██╗██╔══██╗\n ██║██║ ██║ ██║███████║\n ██║██║ ██║ ██║██╔══██║\n ██║╚██████╗╚██████╔╝██║ ██║\n ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝\n")),console.log(chalk.gray(" First-time setup — fetching the sim asset (~700 KB) ...")),console.log();try{r(m,{recursive:!0});const e=`https://icoa2026.au/assets/sim/${o}`,n=await w(e);if(!n.ok)return console.log(chalk.yellow(` Download failed (HTTP ${n.status}).`)),console.log(chalk.gray(" Try ")+chalk.bold.cyan("icoa sim baseline")+chalk.gray(" (server-rendered alternative).")),console.log(),null;const s=Buffer.from(await n.arrayBuffer());return a(t,s),console.log(chalk.green(" ✓ Cached — future runs are instant.")),console.log(),t}catch(o){return console.log(chalk.yellow(` Network error: ${o instanceof Error?o.message:String(o)}`)),console.log(chalk.gray(" Try ")+chalk.bold.cyan("icoa sim baseline")+chalk.gray(" instead.")),console.log(),null}}(c.bundled);if(!g)return;const p=process.env.TMPDIR||"/tmp",u=l(p,`icoa-sim-${t}-${Date.now()}.mp4`);e(g,u),b(Date.now()),x||(console.log(),console.log(chalk.bold.cyan(" ICOA · MuJoCo Sim")),console.log(chalk.gray(" ")+chalk.white(c.label)),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Ready (pre-rendered, instant playback).")),console.log(),console.log(chalk.gray(" ")+c.hint),console.log());const y=A();if(y?(o(y,[u],{stdio:"ignore",detached:!0}).unref(),console.log(chalk.gray(" Opening in your default video player..."))):console.log(chalk.gray(" Video at: ")+chalk.white(u)),console.log(),x)return;const f="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Want to drive the companion arm yourself? ")+chalk.bold.cyan(f+"sim arm")),console.log()}(u,y);if(await S({scenarioName:u,label:y.label,hint:y.hint,quietHeader:x,action:y.action,frames:y.frames,narrative:y.narrative,panda2_pose:y.panda2_pose}),x)return;const h="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Try other scenarios:"));for(const o of Object.keys(j))o!==u&&console.log(chalk.gray(" "+h+"sim ")+chalk.cyan(o));console.log()}export function registerSimCommand(o){o.command("sim [scenario]").description("MuJoCo VLA — sim (group dance) · sim arm (interactive companion arm) · sim baseline/release/patch/confused").action(async o=>{await _(o||"ensemble")})}export{v as SCENARIOS};export async function runSimQuiet(o){await _(o,!0)}
@@ -0,0 +1,8 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Entry point. `teleload` lists; `teleload <n>` pulls.
4
+ * Accepts the number bare, with a `#`, or the pack id itself — a student who
5
+ * types what they see on screen should always land somewhere sensible.
6
+ */
7
+ export declare function runTeleload(arg?: string): Promise<void>;
8
+ export declare function registerTeleloadCommand(program: Command): void;
@@ -0,0 +1 @@
1
+ import{spawn as o}from"node:child_process";import{createHash as e}from"node:crypto";import{createReadStream as n,createWriteStream as t,existsSync as a,mkdirSync as r,renameSync as l,rmSync as s,statSync as i}from"node:fs";import{Readable as c}from"node:stream";import{pipeline as d}from"node:stream/promises";import{join as g}from"node:path";import chalk from"chalk";function u(){if(process.env.ICOA_AIDATA_DIR)return process.env.ICOA_AIDATA_DIR;const o="/data/aiproblems";try{if(a(o)&&i(o).isDirectory())return o}catch{}return g(process.cwd(),"icoa-aidata")}function y(o){return g(u(),o.platform,o.id)}function h(o){return o>=1024?`${(o/1024).toFixed(1)}G`:`${o}M`}function f(e,n){return new Promise(t=>{const a=o(e,n,{stdio:"ignore",shell:!1});a.on("error",()=>t(!1)),a.on("close",o=>t(0===o))})}export async function runTeleload(o){const i=await async function(){try{const o=await fetch(process.env.ICOA_TELELOAD_MANIFEST||"https://icoa-datasets.sg-sin-1.linodeobjects.com/aidata/manifest.json",{signal:AbortSignal.timeout(3e4)});if(!o.ok)return null;const e=await o.json(),n=Array.isArray(e?.packs)?e.packs:null;return n?n.slice().sort((o,e)=>o.n-e.n):null}catch{return null}}();if(!i)return console.log(),console.log(chalk.yellow(" Could not reach the dataset node right now.")),console.log(chalk.gray(" Check your connection and try again — nothing was changed.")),void console.log();if(0===i.length)return console.log(),console.log(chalk.gray(" No on-demand datasets are published yet — everything ships inside this machine.")),void console.log();const p=(o??"").trim().replace(/^#/,"");if(""===p)return function(o){console.log(),console.log(chalk.bold.cyan(" ⬇ TELELOAD")+chalk.gray(" · big datasets, pulled straight from the data node")),console.log();for(const e of o){const o=a(y(e))?chalk.green("✓ here"):chalk.gray("⬇ not yet");console.log(` ${chalk.bold.white(String(e.n))}. ${chalk.white(e.id.padEnd(34))}`+chalk.gray(`${h(e.unpacked_mb).padStart(6)} `)+o)}console.log(),console.log(chalk.gray(" Pull one with ")+chalk.cyan("teleload <number>")+chalk.gray(" (e.g. ")+chalk.cyan("teleload 2")+chalk.gray(")")),console.log(chalk.gray(" Everything smaller than 0.6 G already ships inside this machine — no pull needed.")),console.log()}(i);const m=Number.parseInt(p,10);let w=Number.isFinite(m)?i.find(o=>o.n===m):void 0;if(!w){const o=p.toLowerCase();w=i.find(e=>e.id.toLowerCase()===o)??i.find(e=>e.id.toLowerCase().includes(o))}if(!w){console.log(),console.log(chalk.yellow(` No dataset called "${p}".`)),console.log(chalk.gray(" These are the ones you can pull:"));for(const o of i)console.log(chalk.gray(` ${o.n}. ${o.id}`));return console.log(chalk.gray(" Try ")+chalk.cyan(`teleload ${i[0].n}`)+chalk.gray(" — or just ")+chalk.cyan("teleload")+chalk.gray(" to see sizes.")),void console.log()}await async function(o){const i=y(o);if(a(i))return console.log(),console.log(chalk.green(` ✓ ${o.id} is already here`)+chalk.gray(` (${i})`)),console.log(chalk.gray(" Nothing to do. ")+chalk.cyan("teleload")+chalk.gray(" lists the rest.")),void console.log();const p=g(u(),o.platform);r(p,{recursive:!0});const m=g(p,`.${o.id}.tar.zst.part`);if(console.log(),console.log(chalk.bold.white(` ⬇ ${o.id}`)+chalk.gray(` · ${h(o.tar_mb)} packed → ${h(o.unpacked_mb)} on disk · ${o.n_files} files`)),console.log(chalk.gray(" Straight from the Singapore data node — nothing goes through your laptop.")),console.log(),!await async function(o,e){const n=Date.now();let a=0;const r=1048576*o.tar_mb;let l=0;const s=(o=!1)=>{const e=r>0?Math.min(100,Math.round(a/r*100)):0,t=(Date.now()-n)/1e3,l=t>0?a/1048576/t:0,s=Math.round(e/5),i=` ${"█".repeat(s)+"░".repeat(20-s)} ${String(e).padStart(3)}% · ${l.toFixed(1)} MB/s`;process.stdout.write(`\r${chalk.cyan(i)}${o?"\n":""}`)};try{const n=await fetch(o.url,{signal:AbortSignal.timeout(36e5)});if(!n.ok||!n.body)return!1;const r=t(e),i=c.fromWeb(n.body);return i.on("data",o=>{a+=o.length;const e=Date.now();e-l>250&&(l=e,s())}),await d(i,r),s(!0),!0}catch{return process.stdout.write("\n"),!1}}(o,m))return s(m,{force:!0}),console.log(chalk.yellow(" ✗ Download failed.")+chalk.gray(" Nothing was changed — just run it again.")),void console.log();process.stdout.write(chalk.gray(" checking the file is intact… "));const w=await(v=m,new Promise((o,t)=>{const a=e("sha256"),r=n(v);r.on("data",o=>{a.update(o)}),r.on("end",()=>o(a.digest("hex"))),r.on("error",t)})).catch(()=>"");var v;if(o.sha256&&w!==o.sha256)return s(m,{force:!0}),console.log(chalk.yellow("✗")),console.log(chalk.yellow(" The file arrived damaged and was discarded.")+chalk.gray(" Run it again.")),void console.log();console.log(chalk.green("✓")),process.stdout.write(chalk.gray(" unpacking… "));const b=g(p,`.${o.id}.staging`);s(b,{recursive:!0,force:!0});const $=await async function(o,e){if(r(e,{recursive:!0}),await f("tar",["-xf",o,"-C",e]))return!0;if(await f("tar",["--zstd","-xf",o,"-C",e]))return!0;if(await f("tar",["-I","unzstd","-xf",o,"-C",e]))return!0;const n=`${o}.plain`;if(await f("zstd",["-d","-f","-o",n,o])){const o=await f("tar",["-xf",n,"-C",e]);return s(n,{force:!0}),o}return!1}(m,b);if(s(m,{force:!0}),!$)return s(b,{recursive:!0,force:!0}),console.log(chalk.yellow("✗")),console.log(chalk.yellow(" Could not unpack the archive.")+chalk.gray(" Run it again.")),void console.log();const k=g(b,o.id);try{l(a(k)?k:b,i)}catch{return console.log(chalk.yellow("✗")),console.log(chalk.yellow(" Could not move the data into place.")+chalk.gray(" Check disk space.")),void console.log()}s(b,{recursive:!0,force:!0}),console.log(chalk.green("✓")),console.log(),console.log(chalk.green(" 🎁 ready: ")+chalk.white(i)),console.log(chalk.gray(" Open it in a notebook — the path above is the dataset root.")),console.log(chalk.gray(" ")+chalk.cyan("teleload")+chalk.gray(" lists what else is available.")),console.log()}(w)}export function registerTeleloadCommand(o){o.command("teleload").argument("[pack]","pack number (or id) from `teleload`").description("Pull a big dataset from the data node onto this machine").action(async o=>{await runTeleload(o)})}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as o}from"commander";import chalk from"chalk";import{registerCtfCommands as e}from"./commands/ctf.js";import{registerRefCommand as n}from"./commands/ref.js";import{registerShellCommand as s}from"./commands/shell.js";import{registerFilesCommand as r}from"./commands/files.js";import{registerViewCommand as t}from"./commands/view.js";import{registerConnectCommand as i}from"./commands/connect.js";import{registerChallengeCommand as a}from"./commands/challenge.js";import{registerDoctorCommand as l}from"./commands/doctor.js";import{registerNoteCommand as m}from"./commands/note.js";import{registerLogCommand as c}from"./commands/log.js";import{registerLangCommand as d}from"./commands/lang.js";import{registerSetupCommand as g}from"./commands/setup.js";import{registerEnvCommand as p}from"./commands/env.js";import{registerAienvCommand as f}from"./commands/aienv.js";import{registerIpynbCommand as y}from"./commands/ipynb.js";import{registerAi4ctfCommand as h}from"./commands/ai4ctf.js";import{registerExamCommand as u}from"./commands/exam.js";import{registerCtf4aiDemoCommand as w}from"./commands/ctf4ai-demo.js";import{registerThemeCommand as v}from"./commands/theme.js";import{registerLearnCommand as T}from"./commands/learn.js";import{registerCtf4VlaCommand as b}from"./commands/ctf4vla.js";import{registerDemo2Command as j}from"./commands/demo2.js";import{registerDemo4Command as A}from"./commands/demo4.js";import{registerDemo5Command as I}from"./commands/demo5.js";import{registerSimCommand as $}from"./commands/sim.js";import{registerArenaCommand as C}from"./commands/arena.js";import{registerKaggleCommand as E}from"./commands/kaggle.js";import{registerBoardCommand as _}from"./commands/board.js";import{getConfig as S,saveConfig as x}from"./lib/config.js";import{startRepl as O}from"./repl.js";import{RELAUNCH_CODE as L}from"./lib/menu-nav.js";import{setTerminalTheme as k}from"./lib/theme.js";import{checkForUpdates as N}from"./lib/update-check.js";import{detectIcoaInstalls as R}from"./lib/platform.js";import{ICOA_BIG as F}from"./lib/banner.js";import{readFileSync as U}from"node:fs";import{fileURLToPath as P}from"node:url";import{dirname as M,join as G}from"node:path";const D=M(P(import.meta.url)),H=JSON.parse(U(G(D,"..","package.json"),"utf-8")).version,J=chalk.cyan(" ─────────────────────────────────────────────────────"),V=`\n${J}\n\n${F.map(o=>` ${chalk.bold.white(o)}`).join("\n")}\n\n ${chalk.yellow("International Cyber Olympiad in AI 2026")}\n ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}\n ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}\n ${chalk.bold.magenta("and Olympiad for K-12")}\n\n ${chalk.green.bold("AI4CTF")} ${chalk.gray("[Day 1]")} ${chalk.white("AI as your teammate")}\n ${chalk.red.bold("CTF4AI")} ${chalk.gray("[Day 2]")} ${chalk.white("Challenge & evaluate AI systems")}\n ${chalk.cyan.bold("CTF4EAI")} ${chalk.gray("[Pioneer]")} ${chalk.white("Embodied AI security")}\n ${chalk.bold.yellow("AI is your teammate. AI is your target. AI is embodied.")}\n\n ${chalk.white("Sydney, Australia")} ${chalk.gray("Jun 27 - Jul 2, 2026")}\n ${chalk.cyan.underline("https://icoa2026.au")}\n\n ${chalk.gray(`CLI-Native Competition Terminal v${H}`)}\n\n${J}\n`;process.on("uncaughtException",o=>{"__REPL_NO_EXIT__"!==o.message&&(console.error(chalk.red("Error:"),o.message),process.exit(1))}),process.on("unhandledRejection",o=>{const e=o instanceof Error?o.message:String(o);"__REPL_NO_EXIT__"!==e&&(console.error(chalk.red("Error:"),e),process.exit(1))});{const o=process.argv.slice(2).some(o=>["-V","--version","-h","--help"].includes(o));if(!0===process.stdin.isTTY&&"1"!==process.env.ICOA_SUPERVISED&&!o){const{spawnSync:o}=await import("node:child_process");process.on("SIGINT",()=>{}),process.on("SIGTERM",()=>{});let e=0,n=!1;do{const s=n?process.argv.slice(1,2):process.argv.slice(1),r=o(process.execPath,s,{stdio:"inherit",env:{...process.env,ICOA_SUPERVISED:"1",...n?{ICOA_RELAUNCH:"1"}:{}}});if(r.signal){e=0;break}e=r.status,n=!0}while(e===L);process.exit(e??0)}}const Y=new o;if(Y.name("icoa").version(H).description("ICOA CLI — CLI-Native CTF Competition Terminal").option("--resume","Resume previous session").option("--continue","Continue where you left off — jump straight to your last learn card / exam").action(async o=>{const e=S();k("high-contrast"===e.themeVariant?"high-contrast":"dark"),N(),function(){const o=R();if(o.length<=1)return;const e=o[0];if([...o.map(o=>o.version||"0.0.0")].sort((o,e)=>function(o,e){const n=o.split(".").map(o=>parseInt(o,10)||0),s=e.split(".").map(o=>parseInt(o,10)||0);for(let o=0;o<3;o++)if((n[o]||0)!==(s[o]||0))return(n[o]||0)-(s[o]||0);return 0}(e,o))[0]!==e.version){console.log(),console.log(chalk.yellow.bold(" ⚠ Multiple icoa installations detected on PATH:"));for(let e=0;e<o.length;e++){const n=o[e],s=n.version?`v${n.version}`:"(version unreadable)",r=0===e?chalk.yellow("→"):" ",t=0===e?chalk.gray(" ← currently running (older — shadowing newer install)"):chalk.gray(" ← shadowed");console.log(` ${r} ${chalk.cyan(n.path.padEnd(28))} ${chalk.white(s)}${t}`)}console.log(),console.log(chalk.yellow(" The first install on PATH wins. To use the newer one, remove the older:")),console.log(),e.pkgDir?console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.pkgDir} ${e.path}`)}`):console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.path} # also delete its node_modules dir`)}`),console.log(),console.log(chalk.gray(" Then re-run icoa to confirm version banner shows the newer release.")),console.log()}}();const n=process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"";if(!/UTF-?8/i.test(n))if("win32"===process.platform){let o="";try{const{execFileSync:e}=await import("node:child_process");o=e("chcp.com",[],{encoding:"utf-8",timeout:1500,stdio:["ignore","pipe","ignore"]}).trim()}catch{}o.includes("65001")||(console.log(chalk.yellow(`⚠ Windows terminal is not using UTF-8 (current: ${o||"unknown"}).`)),console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix (run before ")+chalk.cyan("icoa")+chalk.gray("): ")+chalk.cyan("chcp 65001")),console.log(chalk.gray(" Or stay in English inside the CLI: ")+chalk.cyan("lang en")),console.log())}else console.log(chalk.yellow(`⚠ Your terminal locale is not UTF-8 (LANG=${n||"(unset)"}).`)),console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix: ")+chalk.cyan("export LANG=en_US.UTF-8")+chalk.gray(" (or your locale, e.g. ")+chalk.cyan("zh_CN.UTF-8")+chalk.gray(", ")+chalk.cyan("uk_UA.UTF-8")+chalk.gray(")")),console.log();console.log(V);const s=!!o.resume||!!o.continue;if(process.argv.length<=2||s)return o.continue&&(process.env.ICOA_AUTO_RESUME="1"),s||"1"===process.env.ICOA_RELAUNCH||await async function(){const o=process.stdin;if(o.isTTY&&"function"==typeof o.setRawMode)return new Promise(e=>{let n=!1;const s=()=>{if(!n){n=!0,o.removeListener("data",r);try{o.setRawMode(!1)}catch{}o.pause(),e()}},r=()=>s();o.setRawMode(!0),o.resume(),o.once("data",r),console.log(chalk.gray(" (press any key to continue...)")),setTimeout(s,3e3)});await new Promise(o=>setTimeout(o,3e3))}(),void O(Y,s)}),e(Y),n(Y),s(Y),r(Y),t(Y),i(Y),a(Y),l(Y),m(Y),c(Y),d(Y),g(Y),p(Y),f(Y),y(Y),h(Y),u(Y),w(Y),v(Y),T(Y),b(Y),j(Y),A(Y),I(Y),$(Y),C(Y),E(Y),_(Y),Y.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=S().geminiModel||"gemini-2.5-flash-lite";o?(x({geminiModel:o}),console.log(),console.log(chalk.green(" Model switched: ")+chalk.gray(e)+chalk.white(" -> ")+chalk.bold.white(o)),console.log()):(console.log(),console.log(chalk.gray(" Current model: ")+chalk.white(e)),console.log(),console.log(chalk.gray(" Available models:")),console.log(chalk.bold.white(" Gemini 3.x (Latest)")),console.log(chalk.white(" model gemini-3.5-flash ")+chalk.gray("Newest stable, paid Tier 1+")),console.log(chalk.white(" model gemini-3-flash-preview ")+chalk.gray("Preview")),console.log(chalk.bold.white(" Gemini 2.5 (Stable)")),console.log(chalk.white(" model gemini-2.5-flash-lite ")+chalk.gray("Fastest, default, free tier")),console.log(chalk.white(" model gemini-2.5-flash ")+chalk.gray("Heavier, thinking enabled")),console.log(chalk.white(" model gemini-2.5-pro ")+chalk.gray("Strong reasoning, paid")),console.log(chalk.bold.white(" Open Source")),console.log(chalk.white(" model gemma-4-31b-it ")+chalk.gray("Free, open-source")),console.log(chalk.white(" model <any-model-id> ")+chalk.gray("Custom model")),console.log(),console.log(chalk.gray(" Translation uses gemini-3.5-flash for best quality.")),console.log())}),"1"===process.env.ICOA_RESET_STATE)try{const{clearExamState:o}=await import("./lib/exam-state.js");o(),console.log(chalk.yellow("⚠ ICOA_RESET_STATE=1 — local exam state wiped.")),console.log(chalk.gray(" (Token NOT revoked server-side. Re-enter a fresh token with `exam <token>`.)")),console.log()}catch(o){console.log(chalk.red("⚠ ICOA_RESET_STATE: could not clear state — ")+chalk.gray(String(o)))}Y.parse();
2
+ import{Command as o}from"commander";import chalk from"chalk";import{registerCtfCommands as e}from"./commands/ctf.js";import{registerRefCommand as n}from"./commands/ref.js";import{registerShellCommand as s}from"./commands/shell.js";import{registerFilesCommand as r}from"./commands/files.js";import{registerViewCommand as t}from"./commands/view.js";import{registerConnectCommand as i}from"./commands/connect.js";import{registerChallengeCommand as a}from"./commands/challenge.js";import{registerDoctorCommand as l}from"./commands/doctor.js";import{registerNoteCommand as m}from"./commands/note.js";import{registerLogCommand as c}from"./commands/log.js";import{registerLangCommand as d}from"./commands/lang.js";import{registerSetupCommand as g}from"./commands/setup.js";import{registerEnvCommand as p}from"./commands/env.js";import{registerAienvCommand as f}from"./commands/aienv.js";import{registerIpynbCommand as y}from"./commands/ipynb.js";import{registerAi4ctfCommand as h}from"./commands/ai4ctf.js";import{registerExamCommand as u}from"./commands/exam.js";import{registerCtf4aiDemoCommand as w}from"./commands/ctf4ai-demo.js";import{registerThemeCommand as b}from"./commands/theme.js";import{registerLearnCommand as v}from"./commands/learn.js";import{registerCtf4VlaCommand as T}from"./commands/ctf4vla.js";import{registerDemo2Command as j}from"./commands/demo2.js";import{registerDemo4Command as A}from"./commands/demo4.js";import{registerDemo5Command as I}from"./commands/demo5.js";import{registerSimCommand as $}from"./commands/sim.js";import{registerArenaCommand as C}from"./commands/arena.js";import{registerKaggleCommand as E}from"./commands/kaggle.js";import{registerTeleloadCommand as _}from"./commands/teleload.js";import{registerBoardCommand as S}from"./commands/board.js";import{getConfig as x,saveConfig as O}from"./lib/config.js";import{startRepl as L}from"./repl.js";import{RELAUNCH_CODE as k}from"./lib/menu-nav.js";import{setTerminalTheme as N}from"./lib/theme.js";import{cleanupTerminal as R}from"./lib/terminal-cleanup.js";import{checkForUpdates as F}from"./lib/update-check.js";import{detectIcoaInstalls as U}from"./lib/platform.js";import{ICOA_BIG as P}from"./lib/banner.js";import{readFileSync as M}from"node:fs";import{fileURLToPath as G}from"node:url";import{dirname as D,join as H}from"node:path";const J=D(G(import.meta.url)),V=JSON.parse(M(H(J,"..","package.json"),"utf-8")).version,Y=chalk.cyan(" ─────────────────────────────────────────────────────"),W=`\n${Y}\n\n${P.map(o=>` ${chalk.bold.white(o)}`).join("\n")}\n\n ${chalk.yellow("International Cyber Olympiad in AI 2026")}\n ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}\n ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}\n ${chalk.bold.magenta("and Olympiad for K-12")}\n\n ${chalk.green.bold("AI4CTF")} ${chalk.gray("[Day 1]")} ${chalk.white("AI as your teammate")}\n ${chalk.red.bold("CTF4AI")} ${chalk.gray("[Day 2]")} ${chalk.white("Challenge & evaluate AI systems")}\n ${chalk.cyan.bold("CTF4EAI")} ${chalk.gray("[Pioneer]")} ${chalk.white("Embodied AI security")}\n ${chalk.bold.yellow("AI is your teammate. AI is your target. AI is embodied.")}\n\n ${chalk.white("Sydney, Australia")} ${chalk.gray("Jun 27 - Jul 2, 2026")}\n ${chalk.cyan.underline("https://icoa2026.au")}\n\n ${chalk.gray(`CLI-Native Competition Terminal v${V}`)}\n\n${Y}\n`;process.on("uncaughtException",o=>{"__REPL_NO_EXIT__"!==o.message&&(console.error(chalk.red("Error:"),o.message),process.exit(1))}),process.on("unhandledRejection",o=>{const e=o instanceof Error?o.message:String(o);"__REPL_NO_EXIT__"!==e&&(console.error(chalk.red("Error:"),e),process.exit(1))});{const o=process.argv.slice(2).some(o=>["-V","--version","-h","--help"].includes(o));if(!0===process.stdin.isTTY&&"1"!==process.env.ICOA_SUPERVISED&&!o){const{spawnSync:o}=await import("node:child_process");process.on("SIGINT",()=>{}),process.on("SIGTERM",()=>{});let e=0,n=!1;do{const s=n?process.argv.slice(1,2):process.argv.slice(1),r=o(process.execPath,s,{stdio:"inherit",env:{...process.env,ICOA_SUPERVISED:"1",...n?{ICOA_RELAUNCH:"1"}:{}}});if(r.signal){e=0;break}e=r.status,n=!0}while(e===k);R(),process.exit(e??0)}}const X=new o;if(X.name("icoa").version(V).description("ICOA CLI — CLI-Native CTF Competition Terminal").option("--resume","Resume previous session").option("--continue","Continue where you left off — jump straight to your last learn card / exam").action(async o=>{const e=x();N("high-contrast"===e.themeVariant?"high-contrast":"dark"),F(),function(){const o=U();if(o.length<=1)return;const e=o[0];if([...o.map(o=>o.version||"0.0.0")].sort((o,e)=>function(o,e){const n=o.split(".").map(o=>parseInt(o,10)||0),s=e.split(".").map(o=>parseInt(o,10)||0);for(let o=0;o<3;o++)if((n[o]||0)!==(s[o]||0))return(n[o]||0)-(s[o]||0);return 0}(e,o))[0]!==e.version){console.log(),console.log(chalk.yellow.bold(" ⚠ Multiple icoa installations detected on PATH:"));for(let e=0;e<o.length;e++){const n=o[e],s=n.version?`v${n.version}`:"(version unreadable)",r=0===e?chalk.yellow("→"):" ",t=0===e?chalk.gray(" ← currently running (older — shadowing newer install)"):chalk.gray(" ← shadowed");console.log(` ${r} ${chalk.cyan(n.path.padEnd(28))} ${chalk.white(s)}${t}`)}console.log(),console.log(chalk.yellow(" The first install on PATH wins. To use the newer one, remove the older:")),console.log(),e.pkgDir?console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.pkgDir} ${e.path}`)}`):console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.path} # also delete its node_modules dir`)}`),console.log(),console.log(chalk.gray(" Then re-run icoa to confirm version banner shows the newer release.")),console.log()}}();const n=process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"";if(!/UTF-?8/i.test(n))if("win32"===process.platform){let o="";try{const{execFileSync:e}=await import("node:child_process");o=e("chcp.com",[],{encoding:"utf-8",timeout:1500,stdio:["ignore","pipe","ignore"]}).trim()}catch{}o.includes("65001")||(console.log(chalk.yellow(`⚠ Windows terminal is not using UTF-8 (current: ${o||"unknown"}).`)),console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix (run before ")+chalk.cyan("icoa")+chalk.gray("): ")+chalk.cyan("chcp 65001")),console.log(chalk.gray(" Or stay in English inside the CLI: ")+chalk.cyan("lang en")),console.log())}else console.log(chalk.yellow(`⚠ Your terminal locale is not UTF-8 (LANG=${n||"(unset)"}).`)),console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix: ")+chalk.cyan("export LANG=en_US.UTF-8")+chalk.gray(" (or your locale, e.g. ")+chalk.cyan("zh_CN.UTF-8")+chalk.gray(", ")+chalk.cyan("uk_UA.UTF-8")+chalk.gray(")")),console.log();console.log(W);const s=!!o.resume||!!o.continue;if(process.argv.length<=2||s)return o.continue&&(process.env.ICOA_AUTO_RESUME="1"),s||"1"===process.env.ICOA_RELAUNCH||await async function(){const o=process.stdin;if(o.isTTY&&"function"==typeof o.setRawMode)return new Promise(e=>{let n=!1;const s=()=>{if(!n){n=!0,o.removeListener("data",r);try{o.setRawMode(!1)}catch{}o.pause(),e()}},r=()=>s();o.setRawMode(!0),o.resume(),o.once("data",r),console.log(chalk.gray(" (press any key to continue...)")),setTimeout(s,3e3)});await new Promise(o=>setTimeout(o,3e3))}(),void L(X,s)}),e(X),n(X),s(X),r(X),t(X),i(X),a(X),l(X),m(X),c(X),d(X),g(X),p(X),f(X),y(X),h(X),u(X),w(X),b(X),v(X),T(X),j(X),A(X),I(X),$(X),C(X),E(X),_(X),S(X),X.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=x().geminiModel||"gemini-2.5-flash-lite";o?(O({geminiModel:o}),console.log(),console.log(chalk.green(" Model switched: ")+chalk.gray(e)+chalk.white(" -> ")+chalk.bold.white(o)),console.log()):(console.log(),console.log(chalk.gray(" Current model: ")+chalk.white(e)),console.log(),console.log(chalk.gray(" Available models:")),console.log(chalk.bold.white(" Gemini 3.x (Latest)")),console.log(chalk.white(" model gemini-3.5-flash ")+chalk.gray("Newest stable, paid Tier 1+")),console.log(chalk.white(" model gemini-3-flash-preview ")+chalk.gray("Preview")),console.log(chalk.bold.white(" Gemini 2.5 (Stable)")),console.log(chalk.white(" model gemini-2.5-flash-lite ")+chalk.gray("Fastest, default, free tier")),console.log(chalk.white(" model gemini-2.5-flash ")+chalk.gray("Heavier, thinking enabled")),console.log(chalk.white(" model gemini-2.5-pro ")+chalk.gray("Strong reasoning, paid")),console.log(chalk.bold.white(" Open Source")),console.log(chalk.white(" model gemma-4-31b-it ")+chalk.gray("Free, open-source")),console.log(chalk.white(" model <any-model-id> ")+chalk.gray("Custom model")),console.log(),console.log(chalk.gray(" Translation uses gemini-3.5-flash for best quality.")),console.log())}),"1"===process.env.ICOA_RESET_STATE)try{const{clearExamState:o}=await import("./lib/exam-state.js");o(),console.log(chalk.yellow("⚠ ICOA_RESET_STATE=1 — local exam state wiped.")),console.log(chalk.gray(" (Token NOT revoked server-side. Re-enter a fresh token with `exam <token>`.)")),console.log()}catch(o){console.log(chalk.red("⚠ ICOA_RESET_STATE: could not clear state — ")+chalk.gray(String(o)))}X.parse();
package/dist/lib/comms.js CHANGED
@@ -1 +1 @@
1
- import chalk from"chalk";import{getConfig as t}from"./config.js";const e=/\x1b\[[0-9;?]*[ -/]*[@-~]/g,o=/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;let n=!1;export function isCommsActive(){return n}export function setCommsActive(t){n=t}export function isLeaderToken(t){return/^LDR[A-Z0-9]{10,}$/i.test((t||"").trim())}export function isLeaderSession(){return Boolean(t().leaderToken)}export function isLeaderAccount(t){return(t||"").trim().toUpperCase().startsWith("TMLD")}export function sanitizeCommsLine(t){let n=(t??"").toString();return n=n.replace(e,""),n=n.replace(/[\r\n\t]/g," "),n=n.replace(o," "),n=n.replace(/\s+/g," ").trim(),n.slice(0,500)}export function commsBaseUrl(){return(t().ctfdUrl||"").replace(/\/+$/,"")}export function isCompetitionJoined(){const e=t();return Boolean((e.token||e.leaderToken)&&e.ctfdUrl&&e.userName)}export function isMelCompetition(){return isCompetitionJoined()&&function(t){try{return new URL(t).host}catch{return(t||"").replace(/^https?:\/\//,"").replace(/\/.*$/,"")}}(t().ctfdUrl).endsWith("au.icoa2026.au")}export function formatBroadcastBar(t){const e=sanitizeCommsLine(t);if(!e)return chalk.green(" ✓ No issues reported — enjoy the competition!");const o="─".repeat(49);return[chalk.yellow(` ${o}`),chalk.bold.yellow(" ⚠ BROADCAST ")+chalk.yellow(e),chalk.yellow(` ${o}`)].join("\n")}function r(){const e=t(),o={"Content-Type":"application/json"};return e.token&&(o.Authorization=`Token ${e.token}`),o}export async function fetchBroadcast(){try{const t=await fetch(`${commsBaseUrl()}/api/icoa/broadcast`,{headers:r(),signal:AbortSignal.timeout(5e3)});if(!t.ok)return{text:"",ts:null};const e=await t.json(),o=e?.data??{};return{text:sanitizeCommsLine(o.text||""),ts:o.ts??null}}catch{return{text:"",ts:null}}}async function a(e,o){try{const n=await fetch(`${commsBaseUrl()}/api/icoa/${e}`,{method:"POST",headers:r(),body:JSON.stringify({text:sanitizeCommsLine(o),account:t().userName||"",leader_token:t().leaderToken||void 0}),signal:AbortSignal.timeout(8e3)}),a=await n.json().catch(()=>({}));return n.ok&&a?.success?{ok:!0,id:a.data?.id,ts:a.data?.ts,status:n.status}:{ok:!1,status:n.status,message:a?.message}}catch(t){return{ok:!1,status:0,message:t?.message||"network error"}}}export function postMemo(t){return a("memo",t)}export function postJury(t){return a("jury",t)}export async function leaderAuth(t,e){try{const o=await fetch(`${t.replace(/\/+$/,"")}/api/icoa/leader-auth`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:(e||"").trim()}),signal:AbortSignal.timeout(8e3)}),n=await o.json().catch(()=>({}));return o.ok&&n?.success&&n.data?.account?{account:String(n.data.account),team:String(n.data.team||"")}:null}catch{return null}}export async function fetchPublicScoreboard(){try{const t=await fetch(`${commsBaseUrl()}/api/v1/scoreboard`,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(8e3)});if(!t.ok)return[];const e=await t.json().catch(()=>({}));return(Array.isArray(e?.data)?e.data:[]).map((t,e)=>({pos:t.pos??e+1,name:String(t.name??""),score:Number(t.score??0)}))}catch{return[]}}let s=null,c="",i=!1;export function showBroadcast(){console.log(),console.log(formatBroadcastBar(c))}export function startBroadcastPoll(t){if(s)return;const e=async()=>{const{text:e}=await fetchBroadcast(),o=e!==c;c=e,i&&o&&e&&(console.log(),console.log(formatBroadcastBar(e)),t()),i=!0};e(),s=setInterval(()=>{e()},45e3)}export function stopBroadcastPoll(){s&&clearInterval(s),s=null,c="",i=!1}
1
+ import chalk from"chalk";import{getConfig as t}from"./config.js";import{httpFetch as e}from"./http.js";const o=/\x1b\[[0-9;?]*[ -/]*[@-~]/g,n=/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;let r=!1;export function isCommsActive(){return r}export function setCommsActive(t){r=t}export function isLeaderToken(t){return/^LDR[A-Z0-9]{10,}$/i.test((t||"").trim())}export function isLeaderSession(){return Boolean(t().leaderToken)}export function isLeaderAccount(t){return(t||"").trim().toUpperCase().startsWith("TMLD")}export function sanitizeCommsLine(t){let e=(t??"").toString();return e=e.replace(o,""),e=e.replace(/[\r\n\t]/g," "),e=e.replace(n," "),e=e.replace(/\s+/g," ").trim(),e.slice(0,500)}export function commsBaseUrl(){return(t().ctfdUrl||"").replace(/\/+$/,"")}export function isCompetitionJoined(){const e=t();return Boolean((e.token||e.leaderToken)&&e.ctfdUrl&&e.userName)}export function isMelCompetition(){return isCompetitionJoined()&&function(t){try{return new URL(t).host}catch{return(t||"").replace(/^https?:\/\//,"").replace(/\/.*$/,"")}}(t().ctfdUrl).endsWith("au.icoa2026.au")}export function formatBroadcastBar(t){const e=sanitizeCommsLine(t);if(!e)return chalk.green(" ✓ No issues reported — enjoy the competition!");const o="─".repeat(49);return[chalk.yellow(` ${o}`),chalk.bold.yellow(" ⚠ BROADCAST ")+chalk.yellow(e),chalk.yellow(` ${o}`)].join("\n")}function a(){const e=t(),o={"Content-Type":"application/json"};return e.token&&(o.Authorization=`Token ${e.token}`),o}export async function fetchBroadcast(){try{const t=await fetch(`${commsBaseUrl()}/api/icoa/broadcast`,{headers:a(),signal:AbortSignal.timeout(5e3)});if(!t.ok)return{text:"",ts:null};const e=await t.json(),o=e?.data??{};return{text:sanitizeCommsLine(o.text||""),ts:o.ts??null}}catch{return{text:"",ts:null}}}async function s(o,n){try{const r=await e(`${commsBaseUrl()}/api/icoa/${o}`,{method:"POST",headers:a(),body:JSON.stringify({text:sanitizeCommsLine(n),account:t().userName||"",leader_token:t().leaderToken||void 0}),signal:AbortSignal.timeout(8e3)}),s=await r.json().catch(()=>({}));return r.ok&&s?.success?{ok:!0,id:s.data?.id,ts:s.data?.ts,status:r.status}:{ok:!1,status:r.status,message:s?.message}}catch(t){return{ok:!1,status:0,message:t?.message||"network error"}}}export function postMemo(t){return s("memo",t)}export function postJury(t){return s("jury",t)}export async function leaderAuth(t,e){try{const o=await fetch(`${t.replace(/\/+$/,"")}/api/icoa/leader-auth`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:(e||"").trim()}),signal:AbortSignal.timeout(8e3)}),n=await o.json().catch(()=>({}));return o.ok&&n?.success&&n.data?.account?{account:String(n.data.account),team:String(n.data.team||"")}:null}catch{return null}}export async function fetchPublicScoreboard(){try{const t=await fetch(`${commsBaseUrl()}/api/v1/scoreboard`,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(8e3)});if(!t.ok)return[];const e=await t.json().catch(()=>({}));return(Array.isArray(e?.data)?e.data:[]).map((t,e)=>({pos:t.pos??e+1,name:String(t.name??""),score:Number(t.score??0)}))}catch{return[]}}let c=null,i="",u=!1;export function showBroadcast(){console.log(),console.log(formatBroadcastBar(i))}export function startBroadcastPoll(t){if(c)return;const e=async()=>{const{text:e}=await fetchBroadcast(),o=e!==i;i=e,u&&o&&e&&(console.log(),console.log(formatBroadcastBar(e)),t()),u=!0};e(),c=setInterval(()=>{e()},45e3)}export function stopBroadcastPoll(){c&&clearInterval(c),c=null,i="",u=!1}
@@ -1 +1 @@
1
- import{createWriteStream as t,mkdirSync as e}from"node:fs";import{join as s}from"node:path";import{pipeline as n}from"node:stream/promises";import{Readable as o}from"node:stream";export class CTFdClient{baseUrl;token;sessionCookie;csrfNonce;constructor(t,e,s,n){this.baseUrl=t.replace(/\/+$/,""),this.token=e,this.sessionCookie=s||"",this.csrfNonce=n||""}getAuthHeaders(){if(this.token)return{Authorization:`Token ${this.token}`};if(this.sessionCookie){const t={Cookie:this.sessionCookie};return this.csrfNonce&&(t["CSRF-Token"]=this.csrfNonce),t}return{}}async fetchCsrfNonce(){if(this.csrfNonce)return this.csrfNonce;try{const t=await fetch(this.baseUrl,{headers:this.sessionCookie?{Cookie:this.sessionCookie}:{}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);if(e)return this.csrfNonce=e[1],this.csrfNonce}catch{}return""}async request(t,e,s){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const n=`${this.baseUrl}/api/v1${e}`,o={...this.getAuthHeaders()};(void 0!==s||this.token)&&(o["Content-Type"]="application/json");const i=await fetch(n,{method:t,headers:o,body:void 0!==s?JSON.stringify(s):void 0,redirect:"manual"});if(i.status>=300&&i.status<400)throw new Error("Authentication failed: token invalid or expired. Re-run `join <url>` to sign in again.");if(!i.ok){const t=await i.text().catch(()=>"Unknown error");if(t.trimStart().startsWith("<")){if(/banned/i.test(t))throw new Error("Account banned: this account has been banned from the competition. Contact the organizer if you believe this is an error.");throw new Error("Access denied (403). Either your session expired — re-run `join <url>` — or this account is banned / not enabled for the competition (try a different account, or contact the organizer).")}throw new Error(`CTFd API error (${i.status}): ${t}`)}if(!(i.headers.get("content-type")||"").includes("application/json")&&(await i.text().catch(()=>"")).trimStart().startsWith("<"))throw new Error("Authentication failed: server returned a login page instead of data. Re-run `join <url>`.");const r=await i.json();if(!1===r.success)throw new Error(`CTFd error: ${r.errors?.join(", ")||"Unknown error"}`);return r.data}async testConnection(){try{return await this.request("GET","/users/me")}catch(t){if(this.sessionCookie&&(t.message?.includes("403")||t.message?.includes("Authentication failed")))return this.testConnectionViaProfile();throw t}}async testConnectionViaProfile(){const t=await fetch(`${this.baseUrl}/settings`,{headers:{Cookie:this.sessionCookie}});if(!t.ok){if(403===t.status)throw new Error("ACCOUNT_BLOCKED: signed in, but this account is banned / disabled / not enabled for the competition. Use a different account, or contact the organizer.");throw new Error("Session expired or invalid.")}const e=await t.text(),s=e.match(/name="name"[^>]*value="([^"]+)"/)||e.match(/<input[^>]*id="name"[^>]*value="([^"]+)"/),n=e.match(/user_id['":\s]+(\d+)/)||e.match(/userId['":\s]+(\d+)/);if(!s&&!n)throw new Error("Authentication failed: session is not signed in. Re-run `join <url>` and check your username/password.");const o=s?.[1]||"User",i=n?parseInt(n[1],10):0,r=e.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);return r&&(this.csrfNonce=r[1]),{id:i,name:o,score:0,team_id:0,country:""}}async getChallenges(){return this.request("GET","/challenges")}async getChallenge(t){return this.request("GET",`/challenges/${t}`)}async submitFlag(t,e){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const s=await fetch(`${this.baseUrl}/api/v1/challenges/attempt`,{method:"POST",headers:{...this.getAuthHeaders(),"Content-Type":"application/json"},body:JSON.stringify({challenge_id:t,submission:e})});if(!s.ok){const t=await s.text().catch(()=>"Unknown error");if(t.trimStart().startsWith("<")){if(/banned/i.test(t))throw new Error("Account banned: this account has been banned from the competition. Your submission was not recorded. Contact the organizer if you believe this is an error.");throw new Error("Authentication failed: not signed in (server returned the login page). Re-run `join <url>` and check your username/password.")}throw new Error(`CTFd API error (${s.status}): ${t}`)}return(await s.json()).data}async getScoreboard(){return this.request("GET","/scoreboard")}async getTeam(){return this.request("GET","/teams/me")}async getCompetitionMeta(){const t=await fetch(this.baseUrl),e=await t.text(),s=e.match(/'start'\s*:\s*(\d+)/),n=e.match(/'end'\s*:\s*(\d+)/),o=e.match(/'userMode'\s*:\s*"([^"]+)"/),i=e.match(/'csrfNonce'\s*:\s*"([^"]+)"/);return{start:s?parseInt(s[1],10):null,end:n?parseInt(n[1],10):null,userMode:o?.[1]||"users",csrfNonce:i?.[1]||""}}async getChallengeFiles(t){return(await this.getChallenge(t)).files||[]}async downloadFile(i,r){e(r,{recursive:!0});const a=i.startsWith("http")?i:`${this.baseUrl}/${i.replace(/^\//,"")}`,c=await fetch(a,{headers:this.getAuthHeaders(),redirect:"follow"});if(!c.ok||!c.body)throw new Error(`Failed to download: ${a}`);const h=(i.split("/").pop()||"file").split("?")[0],d=s(r,h),u=t(d);return await n(o.fromWeb(c.body),u),d}async getTokenViaIcoaApi(t,e){const s=JSON.stringify({name:t,password:e}),n={"Content-Type":"application/json"};try{const t=await fetch(`${this.baseUrl}/api/icoa/token`,{method:"POST",headers:n,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}try{const t=await fetch(`${this.baseUrl}:9090/api/icoa/token`,{method:"POST",headers:n,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}return null}async tokenAuthenticates(t){try{const e=await fetch(`${this.baseUrl}/api/v1/users/me`,{headers:{Authorization:`Token ${t}`,"Content-Type":"application/json"},redirect:"manual",signal:AbortSignal.timeout(5e3)});if(200!==e.status)return!1;if(!(e.headers.get("content-type")||"").includes("application/json"))return!1;const s=await e.json();return!0===s?.success}catch{return!1}}async loginWithCredentials(t,e){const s=await this.getTokenViaIcoaApi(t,e);if(s&&await this.tokenAuthenticates(s))return{token:s,session:"",csrf:""};const n=await fetch(`${this.baseUrl}/login`),o=await n.text(),i=o.match(/name="nonce"[^>]*value="([^"]+)"/)||o.match(/value="([^"]+)"[^>]*name="nonce"/)||o.match(/id="nonce"[^>]*value="([^"]+)"/)||o.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),r=i?.[1]||"";if(!r)throw new Error("Could not extract CSRF nonce from login page.");const a=n.headers.getSetCookie?.()||[],c=a.map(t=>t.split(";")[0]).join("; "),h=await fetch(`${this.baseUrl}/login`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Cookie:c},body:new URLSearchParams({name:t,password:e,nonce:r,_submit:"Submit"}),redirect:"manual"}),d=h.headers.getSetCookie?.()||[],u=new Map;for(const t of[...a,...d]){const e=t.split(";")[0],s=e.indexOf("=");s>0&&u.set(e.slice(0,s),e.slice(s+1))}const l=[...u.entries()].map(([t,e])=>`${t}=${e}`).join("; "),f=h.headers.get("location")||"";if(!(h.status>=300&&h.status<400)||f.includes("/login"))throw new Error("Invalid username or password.");try{const t=await fetch(`${this.baseUrl}/settings`,{headers:{Cookie:l}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),s=e?.[1]||r,n=await fetch(`${this.baseUrl}/api/v1/tokens`,{method:"POST",headers:{"Content-Type":"application/json",Cookie:l,"CSRF-Token":s},body:JSON.stringify({expiration:"2026-12-31T23:59:59+00:00"})});if(n.ok){const t=await n.json();if(t.success&&t.data?.value)return{token:t.data.value,session:"",csrf:""}}}catch{}let w="";try{const t=await fetch(`${this.baseUrl}/challenges`,{headers:{Cookie:l}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);e&&(w=e[1])}catch{}return{token:"",session:l,csrf:w}}}
1
+ import{createWriteStream as t,mkdirSync as e}from"node:fs";import{join as s}from"node:path";import{pipeline as n}from"node:stream/promises";import{Readable as o}from"node:stream";import{httpFetch as i}from"./http.js";export class CTFdClient{baseUrl;token;sessionCookie;csrfNonce;constructor(t,e,s,n){this.baseUrl=t.replace(/\/+$/,""),this.token=e,this.sessionCookie=s||"",this.csrfNonce=n||""}getAuthHeaders(){if(this.token)return{Authorization:`Token ${this.token}`};if(this.sessionCookie){const t={Cookie:this.sessionCookie};return this.csrfNonce&&(t["CSRF-Token"]=this.csrfNonce),t}return{}}async fetchCsrfNonce(){if(this.csrfNonce)return this.csrfNonce;try{const t=await i(this.baseUrl,{headers:this.sessionCookie?{Cookie:this.sessionCookie}:{}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);if(e)return this.csrfNonce=e[1],this.csrfNonce}catch{}return""}async request(t,e,s){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const n=`${this.baseUrl}/api/v1${e}`,o={...this.getAuthHeaders()};(void 0!==s||this.token)&&(o["Content-Type"]="application/json");const r=await i(n,{method:t,headers:o,body:void 0!==s?JSON.stringify(s):void 0,redirect:"manual"});if(r.status>=300&&r.status<400)throw new Error("Authentication failed: token invalid or expired. Re-run `join <url>` to sign in again.");if(!r.ok){const t=await r.text().catch(()=>"Unknown error");if(t.trimStart().startsWith("<")){if(/banned/i.test(t))throw new Error("Account banned: this account has been banned from the competition. Contact the organizer if you believe this is an error.");throw new Error("Access denied (403). Either your session expired — re-run `join <url>` — or this account is banned / not enabled for the competition (try a different account, or contact the organizer).")}throw new Error(`CTFd API error (${r.status}): ${t}`)}if(!(r.headers.get("content-type")||"").includes("application/json")&&(await r.text().catch(()=>"")).trimStart().startsWith("<"))throw new Error("Authentication failed: server returned a login page instead of data. Re-run `join <url>`.");const a=await r.json();if(!1===a.success)throw new Error(`CTFd error: ${a.errors?.join(", ")||"Unknown error"}`);return a.data}async testConnection(){try{return await this.request("GET","/users/me")}catch(t){if(this.sessionCookie&&(t.message?.includes("403")||t.message?.includes("Authentication failed")))return this.testConnectionViaProfile();throw t}}async testConnectionViaProfile(){const t=await i(`${this.baseUrl}/settings`,{headers:{Cookie:this.sessionCookie}});if(!t.ok){if(403===t.status)throw new Error("ACCOUNT_BLOCKED: signed in, but this account is banned / disabled / not enabled for the competition. Use a different account, or contact the organizer.");throw new Error("Session expired or invalid.")}const e=await t.text(),s=e.match(/name="name"[^>]*value="([^"]+)"/)||e.match(/<input[^>]*id="name"[^>]*value="([^"]+)"/),n=e.match(/user_id['":\s]+(\d+)/)||e.match(/userId['":\s]+(\d+)/);if(!s&&!n)throw new Error("Authentication failed: session is not signed in. Re-run `join <url>` and check your username/password.");const o=s?.[1]||"User",r=n?parseInt(n[1],10):0,a=e.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);return a&&(this.csrfNonce=a[1]),{id:r,name:o,score:0,team_id:0,country:""}}async getChallenges(){return this.request("GET","/challenges")}async getChallenge(t){return this.request("GET",`/challenges/${t}`)}async submitFlag(t,e){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const s=await i(`${this.baseUrl}/api/v1/challenges/attempt`,{method:"POST",headers:{...this.getAuthHeaders(),"Content-Type":"application/json"},body:JSON.stringify({challenge_id:t,submission:e})});if(!s.ok){const t=await s.text().catch(()=>"Unknown error");if(t.trimStart().startsWith("<")){if(/banned/i.test(t))throw new Error("Account banned: this account has been banned from the competition. Your submission was not recorded. Contact the organizer if you believe this is an error.");throw new Error("Authentication failed: not signed in (server returned the login page). Re-run `join <url>` and check your username/password.")}throw new Error(`CTFd API error (${s.status}): ${t}`)}return(await s.json()).data}async getScoreboard(){return this.request("GET","/scoreboard")}async getTeam(){return this.request("GET","/teams/me")}async getCompetitionMeta(){const t=await i(this.baseUrl),e=await t.text(),s=e.match(/'start'\s*:\s*(\d+)/),n=e.match(/'end'\s*:\s*(\d+)/),o=e.match(/'userMode'\s*:\s*"([^"]+)"/),r=e.match(/'csrfNonce'\s*:\s*"([^"]+)"/);return{start:s?parseInt(s[1],10):null,end:n?parseInt(n[1],10):null,userMode:o?.[1]||"users",csrfNonce:r?.[1]||""}}async getChallengeFiles(t){return(await this.getChallenge(t)).files||[]}async downloadFile(r,a){e(a,{recursive:!0});const c=r.startsWith("http")?r:`${this.baseUrl}/${r.replace(/^\//,"")}`,h=await i(c,{headers:this.getAuthHeaders(),redirect:"follow"});if(!h.ok||!h.body)throw new Error(`Failed to download: ${c}`);const d=(r.split("/").pop()||"file").split("?")[0],u=s(a,d),l=t(u);return await n(o.fromWeb(h.body),l),u}async getTokenViaIcoaApi(t,e){const s=JSON.stringify({name:t,password:e}),n={"Content-Type":"application/json"};try{const t=await i(`${this.baseUrl}/api/icoa/token`,{method:"POST",headers:n,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}try{const t=await i(`${this.baseUrl}:9090/api/icoa/token`,{method:"POST",headers:n,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}return null}async tokenAuthenticates(t){try{const e=await i(`${this.baseUrl}/api/v1/users/me`,{headers:{Authorization:`Token ${t}`,"Content-Type":"application/json"},redirect:"manual",signal:AbortSignal.timeout(5e3)});if(200!==e.status)return!1;if(!(e.headers.get("content-type")||"").includes("application/json"))return!1;const s=await e.json();return!0===s?.success}catch{return!1}}async loginWithCredentials(t,e){const s=await this.getTokenViaIcoaApi(t,e);if(s&&await this.tokenAuthenticates(s))return{token:s,session:"",csrf:""};const n=await i(`${this.baseUrl}/login`),o=await n.text(),r=o.match(/name="nonce"[^>]*value="([^"]+)"/)||o.match(/value="([^"]+)"[^>]*name="nonce"/)||o.match(/id="nonce"[^>]*value="([^"]+)"/)||o.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),a=r?.[1]||"";if(!a)throw new Error("Could not extract CSRF nonce from login page.");const c=n.headers.getSetCookie?.()||[],h=c.map(t=>t.split(";")[0]).join("; "),d=await i(`${this.baseUrl}/login`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Cookie:h},body:new URLSearchParams({name:t,password:e,nonce:a,_submit:"Submit"}),redirect:"manual"}),u=d.headers.getSetCookie?.()||[],l=new Map;for(const t of[...c,...u]){const e=t.split(";")[0],s=e.indexOf("=");s>0&&l.set(e.slice(0,s),e.slice(s+1))}const f=[...l.entries()].map(([t,e])=>`${t}=${e}`).join("; "),w=d.headers.get("location")||"";if(!(d.status>=300&&d.status<400)||w.includes("/login"))throw new Error("Invalid username or password.");try{const t=await i(`${this.baseUrl}/settings`,{headers:{Cookie:f}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),s=e?.[1]||a,n=await i(`${this.baseUrl}/api/v1/tokens`,{method:"POST",headers:{"Content-Type":"application/json",Cookie:f,"CSRF-Token":s},body:JSON.stringify({expiration:"2026-12-31T23:59:59+00:00"})});if(n.ok){const t=await n.json();if(t.success&&t.data?.value)return{token:t.data.value,session:"",csrf:""}}}catch{}let m="";try{const t=await i(`${this.baseUrl}/challenges`,{headers:{Cookie:f}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);e&&(m=e[1])}catch{}return{token:"",session:f,csrf:m}}}
@@ -0,0 +1,35 @@
1
+ export interface HttpOptions extends RequestInit {
2
+ /** Abort after this long. Default 15s — long enough for a slow exam upload,
3
+ * short enough that a student notices something is wrong. */
4
+ timeoutMs?: number;
5
+ /** Extra attempts after the first. Default 2 for GET/HEAD, 0 otherwise. */
6
+ retries?: number;
7
+ /** Declare a non-GET request safe to replay (pure reads, pings, idempotent
8
+ * upserts keyed by the caller). Without this, writes are sent exactly once. */
9
+ idempotent?: boolean;
10
+ }
11
+ interface Deps {
12
+ fetch: typeof globalThis.fetch;
13
+ sleep: (ms: number) => Promise<void>;
14
+ }
15
+ export declare function isOwnTimeout(err: unknown): boolean;
16
+ /**
17
+ * 429 = the server explicitly refused to process this request, so replaying is
18
+ * safe by definition. 502/503/504 come from a proxy that could not reach the
19
+ * origin or get an answer from it, so the request most likely never applied.
20
+ *
21
+ * 500 is deliberately absent: it means the origin DID receive the request and
22
+ * blew up somewhere unknown — possibly after committing. Retrying a 500 is the
23
+ * classic way to double-charge, double-submit, or double-score.
24
+ */
25
+ export declare function isRetryableStatus(status: number): boolean;
26
+ /**
27
+ * How long to wait before attempt N+1 (0-indexed). A `Retry-After` header wins
28
+ * outright — the server knows its own recovery window better than we do.
29
+ * Otherwise exponential backoff with ±30% jitter so a roomful of clients that
30
+ * all failed on the same server blip does not come back in lockstep.
31
+ */
32
+ export declare function retryDelayMs(attempt: number, response?: Response): number;
33
+ export declare function isCheapTransportFailure(err: unknown): boolean;
34
+ export declare function httpFetch(url: string, opts?: HttpOptions, deps?: Deps): Promise<Response>;
35
+ export {};
@@ -0,0 +1 @@
1
+ const t=15e3,e=t=>new Promise(e=>setTimeout(e,t)),r=Symbol.for("icoa.http.timeout");export function isOwnTimeout(t){return"object"==typeof t&&null!==t&&!0===t[r]}function n(t){const e=new Error(`request timed out after ${t}ms`);return e[r]=!0,e}export function isRetryableStatus(t){return 429===t||502===t||503===t||504===t}export function retryDelayMs(t,e){const r=e?.headers?.get?.("retry-after");if(r){const t=Number(r);if(Number.isFinite(t)&&t>=0)return Math.min(1e3*t,32e3);const e=Date.parse(r);if(!Number.isNaN(e))return Math.max(0,Math.min(e-Date.now(),32e3))}const n=Math.min(400*2**t,8e3),o=.3*n*(2*Math.random()-1);return Math.max(0,Math.round(n+o))}function o(t,e){const r=new AbortController;let o=!1;const i=()=>r.abort(e.reason),a=setTimeout(()=>{o=!0,r.abort(n(t))},t);return e&&(e.aborted?r.abort(e.reason):e.addEventListener("abort",i,{once:!0})),{signal:r.signal,timedOut:()=>o,done:()=>{clearTimeout(a),e?.removeEventListener("abort",i)}}}function i(t){const e=t?.cause;return void 0!==e?e:t}const a=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EPIPE","ENETUNREACH"]);export function isCheapTransportFailure(t){const e=i(t),r="string"==typeof e?.code?e.code:"",n="string"==typeof e?.name?e.name:"";return!/timeout/i.test(r)&&!/timeout/i.test(n)&&a.has(r)}export async function httpFetch(r,a={},s={fetch:globalThis.fetch,sleep:e}){const{timeoutMs:u=t,retries:c,idempotent:m,signal:f,...l}=a,h=function(t){const e=(t??"GET").toUpperCase();return"GET"===e||"HEAD"===e}(l.method)||!0===m,p=1+Math.max(0,c??(h?2:0));let E;for(let t=0;t<p;t++){const e=o(u,f);try{const n=await s.fetch(r,{...l,signal:e.signal});if(t<p-1&&h&&isRetryableStatus(n.status)){await s.sleep(retryDelayMs(t,n));continue}return n}catch(r){if(E=r,f?.aborted)throw r;if(e.timedOut())throw isOwnTimeout(i(r))?r:n(u);if(!isCheapTransportFailure(r))throw r;if(!h||t===p-1)throw r;await s.sleep(retryDelayMs(t))}finally{e.done()}}throw E}
@@ -1 +1 @@
1
- let s=!1,t=0,e=null,n=[];function a(){return{start:Date.now(),keys:0,backspaces:0,pastes:0,pasteChars:0}}export function classifyBurst(s){let t=1,e=1;for(let n=1;n<s.length;n++)s[n]<5?(t++,t>e&&(e=t)):t=1;return{maxBurst:e,suspectPaste:e>=8}}export function stripPasteMarkers(s){return s.replace(/\x1b?\[20[01]~/g,"")}function r(s,r){e||(e=a());const c=Date.now();t&&n.push(c-t),t=c,e.keys++,"backspace"===r?.name&&e.backspaces++}function c(s){const t=s.toString("latin1");let n=t.indexOf("[200~");for(;-1!==n;){e||(e=a()),e.pastes++;const s=t.indexOf("[201~",n);-1!==s&&(e.pasteChars+=s-(n+6)),n=t.indexOf("[200~",n+6)}}export function installInputDynamics(a=!0){if(!s&&(s=!0,e=null,n=[],t=0,process.stdin.on("keypress",r),process.stdin.on("data",c),a&&process.stdout.isTTY))try{process.stdout.write("[?2004h")}catch{}}export function uninstallInputDynamics(){if(s&&(s=!1,process.stdin.removeListener("keypress",r),process.stdin.removeListener("data",c),process.stdout.isTTY))try{process.stdout.write("[?2004l")}catch{}}export function consumeLineDynamics(){if(!e)return;const{maxBurst:s,suspectPaste:a}=classifyBurst(n),r={ms:Date.now()-e.start,keys:e.keys,backspaces:e.backspaces,pastes:e.pastes,pasteChars:e.pasteChars,maxBurst:s,suspectPaste:a||e.pastes>0};return e=null,n=[],t=0,r}
1
+ import{armTerminalCleanup as s}from"./terminal-cleanup.js";let t=!1,e=0,n=null,a=[];function r(){return{start:Date.now(),keys:0,backspaces:0,pastes:0,pasteChars:0}}export function classifyBurst(s){let t=1,e=1;for(let n=1;n<s.length;n++)s[n]<5?(t++,t>e&&(e=t)):t=1;return{maxBurst:e,suspectPaste:e>=8}}export function stripPasteMarkers(s){return s.replace(/\x1b?\[20[01]~/g,"")}function c(s,t){n||(n=r());const c=Date.now();e&&a.push(c-e),e=c,n.keys++,"backspace"===t?.name&&n.backspaces++}function o(s){const t=s.toString("latin1");let e=t.indexOf("[200~");for(;-1!==e;){n||(n=r()),n.pastes++;const s=t.indexOf("[201~",e);-1!==s&&(n.pasteChars+=s-(e+6)),e=t.indexOf("[200~",e+6)}}export function installInputDynamics(r=!0){if(!t&&(t=!0,n=null,a=[],e=0,process.stdin.on("keypress",c),process.stdin.on("data",o),r&&process.stdout.isTTY))try{process.stdout.write("[?2004h"),s()}catch{}}export function uninstallInputDynamics(){if(t&&(t=!1,process.stdin.removeListener("keypress",c),process.stdin.removeListener("data",o),process.stdout.isTTY))try{process.stdout.write("[?2004l")}catch{}}export function consumeLineDynamics(){if(!n)return;const{maxBurst:s,suspectPaste:t}=classifyBurst(a),r={ms:Date.now()-n.start,keys:n.keys,backspaces:n.backspaces,pastes:n.pastes,pasteChars:n.pasteChars,maxBurst:s,suspectPaste:t||n.pastes>0};return n=null,a=[],e=0,r}
@@ -1 +1 @@
1
- import{getDeviceFingerprint as e}from"./access.js";import{LEARN_SERVER as t}from"./learn-curricula.js";export async function submitPqs(a){const n=t.replace(/\/$/,"")+"/api/icoa/learn/submit";try{const t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_number:a.cardNumber,device_fingerprint:e()}),signal:AbortSignal.timeout(18e4)}),o=await t.json().catch(()=>({}));if(!t.ok||!o.success||!o.data)return{ok:!1,message:o.message||`HTTP ${t.status}`,code:o.code};const r=o.data;return{ok:!0,score:r.score,tier:r.tier,manipulation:r.manipulation,feedback:r.feedback,improve:r.improve,submissionIndex:r.submission_index,submissionsLeft:r.submissions_left,rank:r.rank,entrants:r.entrants,inTop3:r.in_top3,turns:r.turns,spent:r.spent,cap:r.cap}}catch(e){return{ok:!1,message:e instanceof Error&&"TimeoutError"===e.name?"PQS grading timed out — your conversation is safe on the server; try /submit again in a minute.":`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function viewPqsTop(a){const n=t.replace(/\/$/,"")+"/api/icoa/learn/view";try{const t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_number:a.cardNumber,device_fingerprint:e()}),signal:AbortSignal.timeout(3e4)}),o=await t.json().catch(()=>({}));return t.ok&&o.success&&o.data?{ok:!0,top3:o.data.top3,myRank:o.data.my_rank,entrants:o.data.entrants}:{ok:!1,message:o.message||`HTTP ${t.status}`,code:o.code,myRank:o.rank,entrants:o.entrants}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function fetchMyScores(a){const n=t.replace(/\/$/,"")+"/api/icoa/learn/myscores";try{const t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_lo:a.cardLo,card_hi:a.cardHi,device_fingerprint:e()}),signal:AbortSignal.timeout(8e3)}),o=await t.json().catch(()=>({}));return t.ok&&o.success&&o.data?{ok:!0,scores:o.data.scores??[]}:{ok:!1,message:o.message||`HTTP ${t.status}`,code:o.code}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function viewHonors(a){const n=t.replace(/\/$/,"")+"/api/icoa/learn/honors";try{const t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),curriculum_id:a.curriculumId,card_number:a.cardNumber,device_fingerprint:e()}),signal:AbortSignal.timeout(3e4)}),o=await t.json().catch(()=>({}));return t.ok&&o.success&&o.data?{ok:!0,board:o.data.board,mine:o.data.mine??null,nickname:o.data.nickname??null,totals:o.data.totals,chapters:o.data.chapters,floor:o.data.floor}:{ok:!1,message:o.message||`HTTP ${t.status}`,code:o.code}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function setNickname(a){const n=t.replace(/\/$/,"")+"/api/icoa/learn/nickname";try{const t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),nickname:a.nickname,device_fingerprint:e()}),signal:AbortSignal.timeout(3e4)}),o=await t.json().catch(()=>({}));return t.ok&&o.success&&o.data?{ok:!0,nickname:o.data.nickname}:{ok:!1,message:o.message||`HTTP ${t.status}`,code:o.code}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function recallCoach(a){const n=t.replace(/\/$/,"")+"/api/icoa/learn/recall";try{const t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),device_fingerprint:e(),phase:a.phase,slot:a.slot,problem_title:a.problemTitle,statement:a.statement,code:a.code,level:a.level,lang:a.lang}),signal:AbortSignal.timeout(55e3)}),o=await t.json().catch(()=>({}));return t.ok&&o.success&&o.data?.hint?{ok:!0,hint:o.data.hint,level:o.data.level,spent:o.data.spent,cap:o.data.cap}:{ok:!1,message:o.message||`HTTP ${t.status}`,code:o.code}}catch(e){return{ok:!1,message:e instanceof Error&&"TimeoutError"===e.name?"Coach timed out — try again in a minute.":`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function streamLearnTutor(a,n){const o=t.replace(/\/$/,"")+"/api/icoa/learn/ai",r=new AbortController;let s=setTimeout(()=>r.abort(),6e4);const i=()=>{clearTimeout(s),s=setTimeout(()=>r.abort(),6e4)};try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_number:a.cardNumber,question:a.question,mode:a.mode,history:a.history?.slice(-6),device_fingerprint:e()}),signal:r.signal});if(!t.ok||!t.body){const e=await t.json().catch(()=>({}));return{ok:!1,answer:"",message:e.message||`HTTP ${t.status}`,code:e.code,spent:e.spent,cap:e.cap}}const s=t.body.getReader(),c=new TextDecoder;let d="",m="",p=null,u=null;for(;;){const e=await s.read();if(e.done)break;i(),d+=c.decode(e.value,{stream:!0});const t=d.split("\n");d=t.pop()??"";for(const e of t){const t=e.trim();if(!t.startsWith("data:"))continue;let a;try{a=JSON.parse(t.slice(5).trim())}catch{continue}a.r&&n({kind:"reasoning",text:a.r}),a.c&&(m+=a.c,n({kind:"content",text:a.c})),a.error&&(u=a.error),a.done&&(p=a)}}return u&&!m?{ok:!1,answer:"",message:u}:m?{ok:!0,answer:m,offtopic:!0===p?.offtopic,spent:p?.spent,cap:p?.cap}:{ok:!1,answer:"",message:"No answer received — try again."}}catch(e){return{ok:!1,answer:"",message:e instanceof Error&&"AbortError"===e.name?"AI tutor timed out (no data for 60s) — try again.":`Network error: ${e instanceof Error?e.message:String(e)}`}}finally{clearTimeout(s)}}
1
+ import{getDeviceFingerprint as e}from"./access.js";import{LEARN_SERVER as t}from"./learn-curricula.js";import{httpFetch as a}from"./http.js";export async function submitPqs(a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/submit";try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_number:a.cardNumber,device_fingerprint:e()}),signal:AbortSignal.timeout(18e4)}),n=await t.json().catch(()=>({}));if(!t.ok||!n.success||!n.data)return{ok:!1,message:n.message||`HTTP ${t.status}`,code:n.code};const r=n.data;return{ok:!0,score:r.score,tier:r.tier,manipulation:r.manipulation,feedback:r.feedback,improve:r.improve,submissionIndex:r.submission_index,submissionsLeft:r.submissions_left,rank:r.rank,entrants:r.entrants,inTop3:r.in_top3,turns:r.turns,spent:r.spent,cap:r.cap}}catch(e){return{ok:!1,message:e instanceof Error&&"TimeoutError"===e.name?"PQS grading timed out — your conversation is safe on the server; try /submit again in a minute.":`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function viewPqsTop(a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/view";try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_number:a.cardNumber,device_fingerprint:e()}),signal:AbortSignal.timeout(3e4)}),n=await t.json().catch(()=>({}));return t.ok&&n.success&&n.data?{ok:!0,top3:n.data.top3,myRank:n.data.my_rank,entrants:n.data.entrants}:{ok:!1,message:n.message||`HTTP ${t.status}`,code:n.code,myRank:n.rank,entrants:n.entrants}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function fetchMyScores(a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/myscores";try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),card_lo:a.cardLo,card_hi:a.cardHi,device_fingerprint:e()}),signal:AbortSignal.timeout(8e3)}),n=await t.json().catch(()=>({}));return t.ok&&n.success&&n.data?{ok:!0,scores:n.data.scores??[]}:{ok:!1,message:n.message||`HTTP ${t.status}`,code:n.code}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function viewHonors(a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/honors";try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),curriculum_id:a.curriculumId,card_number:a.cardNumber,device_fingerprint:e()}),signal:AbortSignal.timeout(3e4)}),n=await t.json().catch(()=>({}));return t.ok&&n.success&&n.data?{ok:!0,board:n.data.board,mine:n.data.mine??null,nickname:n.data.nickname??null,totals:n.data.totals,chapters:n.data.chapters,floor:n.data.floor}:{ok:!1,message:n.message||`HTTP ${t.status}`,code:n.code}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function setNickname(a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/nickname";try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:a.token.toUpperCase(),nickname:a.nickname,device_fingerprint:e()}),signal:AbortSignal.timeout(3e4)}),n=await t.json().catch(()=>({}));return t.ok&&n.success&&n.data?{ok:!0,nickname:n.data.nickname}:{ok:!1,message:n.message||`HTTP ${t.status}`,code:n.code}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function recallCoach(o){const n=t.replace(/\/$/,"")+"/api/icoa/learn/recall";try{const t=await a(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:o.token.toUpperCase(),device_fingerprint:e(),phase:o.phase,slot:o.slot,problem_title:o.problemTitle,statement:o.statement,code:o.code,level:o.level,lang:o.lang}),signal:AbortSignal.timeout(55e3)}),r=await t.json().catch(()=>({}));return t.ok&&r.success&&r.data?.hint?{ok:!0,hint:r.data.hint,level:r.data.level,spent:r.data.spent,cap:r.data.cap}:{ok:!1,message:r.message||`HTTP ${t.status}`,code:r.code}}catch(e){return{ok:!1,message:e instanceof Error&&"TimeoutError"===e.name?"Coach timed out — try again in a minute.":`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function streamLearnTutor(o,n){const r=t.replace(/\/$/,"")+"/api/icoa/learn/ai",s=new AbortController;let i=setTimeout(()=>s.abort(),6e4);const c=()=>{clearTimeout(i),i=setTimeout(()=>s.abort(),6e4)};try{const t=await a(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:o.token.toUpperCase(),card_number:o.cardNumber,question:o.question,mode:o.mode,history:o.history?.slice(-6),device_fingerprint:e()}),signal:s.signal});if(!t.ok||!t.body){const e=await t.json().catch(()=>({}));return{ok:!1,answer:"",message:e.message||`HTTP ${t.status}`,code:e.code,spent:e.spent,cap:e.cap}}const i=t.body.getReader(),m=new TextDecoder;let d="",p="",u=null,l=null;for(;;){const e=await i.read();if(e.done)break;c(),d+=m.decode(e.value,{stream:!0});const t=d.split("\n");d=t.pop()??"";for(const e of t){const t=e.trim();if(!t.startsWith("data:"))continue;let a;try{a=JSON.parse(t.slice(5).trim())}catch{continue}a.r&&n({kind:"reasoning",text:a.r}),a.c&&(p+=a.c,n({kind:"content",text:a.c})),a.error&&(l=a.error),a.done&&(u=a)}}return l&&!p?{ok:!1,answer:"",message:l}:p?{ok:!0,answer:p,offtopic:!0===u?.offtopic,spent:u?.spent,cap:u?.cap}:{ok:!1,answer:"",message:"No answer received — try again."}}catch(e){return{ok:!1,answer:"",message:e instanceof Error&&"AbortError"===e.name?"AI tutor timed out (no data for 60s) — try again.":`Network error: ${e instanceof Error?e.message:String(e)}`}}finally{clearTimeout(i)}}
@@ -1 +1 @@
1
- export function localized(r,e){if(!e.startsWith("zh")||!r._zh)return r;const t=r._zh,a={...r};for(const r of Object.keys(t))void 0!==t[r]&&(a[r]=t[r]);return a}export function isValidCurriculum(r){if(!r||"object"!=typeof r)return!1;const e=r;return"string"==typeof e.id&&"number"==typeof e.totalCards&&Array.isArray(e.cards)&&Array.isArray(e.modules)}import{readFileSync as r,writeFileSync as e,mkdirSync as t,existsSync as a,statSync as n,utimesSync as c}from"node:fs";import{join as i}from"node:path";import{homedir as o}from"node:os";import{getDeviceFingerprint as u}from"./access.js";import{getCliVersion as s}from"./version.js";const d=i(o(),".icoa","learn-cache");export const LEARN_SERVER="https://practice.icoa2026.au";function l(r){const e=r.replace(/[^A-Za-z0-9_-]/g,"_");return i(d,`${e}.json`)}function m(r){const e=r.replace(/[^A-Za-z0-9_-]/g,"_");return i(d,`${e}.etag`)}export async function loadCurriculumById(i,o){"ctf4eai-12"===i&&(i="LEARNDEMO01");const u=function(e){const t=l(e);if(!a(t))return null;try{const e=n(t),a=Date.now()-e.mtimeMs;return{curriculum:JSON.parse(r(t,"utf-8")),ageMs:a}}catch{return null}}(i);if(u&&u.ageMs<6048e5&&isValidCurriculum(u.curriculum))return u.curriculum;const f=await async function(e,t){const n=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(e),c={},i=function(e){const t=m(e);if(!a(t))return null;try{return r(t,"utf-8").trim()||null}catch{return null}}(e);i&&(c["If-None-Match"]=i),t&&(c["X-Learn-Token"]=t.toUpperCase()),c["X-ICOA-Client"]=s();try{const r=await fetch(n,{headers:c,signal:AbortSignal.timeout(6e4)});if(304===r.status)return{kind:"unchanged"};if(!r.ok)return{kind:"error"};const e=await r.json();if(!e.success||!e.data)return{kind:"error"};if(!isValidCurriculum(e.data))return{kind:"error"};const t=r.headers.get("etag")||r.headers.get("ETag")||void 0;return{kind:"fresh",curriculum:e.data,etag:t||void 0}}catch{return{kind:"error"}}}(i,o);return"fresh"===f.kind?(function(r,a,n){try{t(d,{recursive:!0}),e(l(r),JSON.stringify(a)),n&&e(m(r),n)}catch{}}(i,f.curriculum,f.etag),f.curriculum):"unchanged"===f.kind&&u&&isValidCurriculum(u.curriculum)?(function(r){try{const e=l(r);if(a(e)){const r=Date.now()/1e3;c(e,r,r)}}catch{}}(i),u.curriculum):u&&isValidCurriculum(u.curriculum)?u.curriculum:null}export async function validateEAToken(r){const e=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/validate";try{const t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.toUpperCase(),device_fingerprint:u()}),signal:AbortSignal.timeout(8e3)});if(!t.ok){const r=await t.json().catch(()=>({}));return{ok:!1,message:r.message||`HTTP ${t.status}`,code:r.code}}const a=await t.json();return a.success&&a.data?{ok:!0,curriculumId:a.data.curriculum_id,status:a.data.status,validUntil:a.data.valid_until,aiEnabled:1===a.data.ai_enabled||!0===a.data.ai_enabled,gated:1===a.data.gated||!0===a.data.gated}:{ok:!1,message:a.message||"Validation failed"}}catch(r){return{ok:!1,message:`Network error: ${r instanceof Error?r.message:String(r)}`}}}export async function syncProgress(r,e){if("LEARNDEMO01"===r.toUpperCase())return;const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+r.toUpperCase();try{await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({card_number:e.card_number,event_type:e.event_type,mcq_answer:e.mcq_answer,mcq_correct:e.mcq_correct?1:0,check_answer:e.check_answer,check_correct:e.check_correct?1:0,time_on_card_ms:e.time_on_card_ms,device_fingerprint:u()}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function syncCardFeedback(r,e){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/feedback/"+r.toUpperCase();try{return(await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({curriculum_id:e.curriculum_id,card_number:e.card_number,feedback_text:e.feedback_text,time_on_card_ms:e.time_on_card_ms}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}
1
+ export function localized(r,t){if(!t.startsWith("zh")||!r._zh)return r;const e=r._zh,a={...r};for(const r of Object.keys(e))void 0!==e[r]&&(a[r]=e[r]);return a}export function isValidCurriculum(r){if(!r||"object"!=typeof r)return!1;const t=r;return"string"==typeof t.id&&"number"==typeof t.totalCards&&Array.isArray(t.cards)&&Array.isArray(t.modules)}import{readFileSync as r,writeFileSync as t,mkdirSync as e,existsSync as a,statSync as n,utimesSync as c}from"node:fs";import{join as i}from"node:path";import{homedir as o}from"node:os";import{getDeviceFingerprint as u}from"./access.js";import{getCliVersion as s}from"./version.js";import{httpFetch as d}from"./http.js";const l=i(o(),".icoa","learn-cache");export const LEARN_SERVER="https://practice.icoa2026.au";function m(r){const t=r.replace(/[^A-Za-z0-9_-]/g,"_");return i(l,`${t}.json`)}function f(r){const t=r.replace(/[^A-Za-z0-9_-]/g,"_");return i(l,`${t}.etag`)}export async function loadCurriculumById(i,o){"ctf4eai-12"===i&&(i="LEARNDEMO01");const u=function(t){const e=m(t);if(!a(e))return null;try{const t=n(e),a=Date.now()-t.mtimeMs;return{curriculum:JSON.parse(r(e,"utf-8")),ageMs:a}}catch{return null}}(i);if(u&&u.ageMs<6048e5&&isValidCurriculum(u.curriculum))return u.curriculum;const d=await async function(t,e){const n=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(t),c={},i=function(t){const e=f(t);if(!a(e))return null;try{return r(e,"utf-8").trim()||null}catch{return null}}(t);i&&(c["If-None-Match"]=i),e&&(c["X-Learn-Token"]=e.toUpperCase()),c["X-ICOA-Client"]=s();try{const r=await fetch(n,{headers:c,signal:AbortSignal.timeout(6e4)});if(304===r.status)return{kind:"unchanged"};if(!r.ok)return{kind:"error"};const t=await r.json();if(!t.success||!t.data)return{kind:"error"};if(!isValidCurriculum(t.data))return{kind:"error"};const e=r.headers.get("etag")||r.headers.get("ETag")||void 0;return{kind:"fresh",curriculum:t.data,etag:e||void 0}}catch{return{kind:"error"}}}(i,o);return"fresh"===d.kind?(function(r,a,n){try{e(l,{recursive:!0}),t(m(r),JSON.stringify(a)),n&&t(f(r),n)}catch{}}(i,d.curriculum,d.etag),d.curriculum):"unchanged"===d.kind&&u&&isValidCurriculum(u.curriculum)?(function(r){try{const t=m(r);if(a(t)){const r=Date.now()/1e3;c(t,r,r)}}catch{}}(i),u.curriculum):u&&isValidCurriculum(u.curriculum)?u.curriculum:null}export async function validateEAToken(r){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/validate";try{const e=await d(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.toUpperCase(),device_fingerprint:u()}),signal:AbortSignal.timeout(8e3)});if(!e.ok){const r=await e.json().catch(()=>({}));return{ok:!1,message:r.message||`HTTP ${e.status}`,code:r.code}}const a=await e.json();return a.success&&a.data?{ok:!0,curriculumId:a.data.curriculum_id,status:a.data.status,validUntil:a.data.valid_until,aiEnabled:1===a.data.ai_enabled||!0===a.data.ai_enabled,gated:1===a.data.gated||!0===a.data.gated}:{ok:!1,message:a.message||"Validation failed"}}catch(r){return{ok:!1,message:`Network error: ${r instanceof Error?r.message:String(r)}`}}}export async function syncProgress(r,t){if("LEARNDEMO01"===r.toUpperCase())return;const e=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+r.toUpperCase();try{await d(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({card_number:t.card_number,event_type:t.event_type,mcq_answer:t.mcq_answer,mcq_correct:t.mcq_correct?1:0,check_answer:t.check_answer,check_correct:t.check_correct?1:0,time_on_card_ms:t.time_on_card_ms,device_fingerprint:u()}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function syncCardFeedback(r,t){const e=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/feedback/"+r.toUpperCase();try{return(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({curriculum_id:t.curriculum_id,card_number:t.card_number,feedback_text:t.feedback_text,time_on_card_ms:t.time_on_card_ms}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * When icoa-cli runs inside the ICOA Terminal (Tauri + xterm.js) the host is
3
+ * already pre-themed to our exact palette, so theme.ts never sets OSC 10/11/12
4
+ * and we must not reset them either — doing so would fight the host.
5
+ */
6
+ export declare function isIcoaTerminal(): boolean;
7
+ export declare function supportsAnsi(): boolean;
8
+ /**
9
+ * Pure sequence builder — the whole point of splitting it out is that the exit
10
+ * path is testable without a TTY. See test/terminal-cleanup.test.js.
11
+ *
12
+ * Note the two guards differ on purpose: the mode reset is gated on being a
13
+ * TTY (bracketed paste is switched on for any TTY, colour support or not),
14
+ * while the colour reset is gated on ANSI support so a `TERM=dumb` session
15
+ * never gets escape bytes echoed at it.
16
+ */
17
+ export declare function buildCleanupSequence(opts: {
18
+ ansi: boolean;
19
+ icoaTerminal: boolean;
20
+ }): string;
21
+ /** Restore the terminal now. Idempotent, safe to call from any exit path. */
22
+ export declare function cleanupTerminal(): void;
23
+ /**
24
+ * Register cleanup on every exit path. Idempotent: the first caller wins, so
25
+ * both theme.ts (colour owner) and input-dynamics.ts (bracketed-paste owner)
26
+ * can call it unconditionally without double-registering.
27
+ *
28
+ * Deliberately NOT called by the supervisor in index.ts — the supervisor must
29
+ * survive Ctrl+C so the worker can own SIGINT (exam pause, inquirer cancel).
30
+ * It calls cleanupTerminal() directly instead, right before it exits.
31
+ *
32
+ * Exit codes match the shell convention (128 + signal number) and preserve
33
+ * what theme.ts shipped before this module existed.
34
+ */
35
+ export declare function armTerminalCleanup(): void;
@@ -0,0 +1 @@
1
+ import{writeSync as e}from"node:fs";export function isIcoaTerminal(){return"1"===process.env.ICOA_TERMINAL}export function supportsAnsi(){if(!process.stdout.isTTY)return!1;const e=process.stdout.getColorDepth?.();return"number"!=typeof e||e>=8}export function buildCleanupSequence(e){return"[?2004l[?25h"+(e.ansi&&!e.icoaTerminal?"]110]111]112":"")}export function cleanupTerminal(){if(!process.stdout.isTTY)return;const n=buildCleanupSequence({ansi:supportsAnsi(),icoaTerminal:isIcoaTerminal()});try{e(process.stdout.fd,n)}catch{try{process.stdout.write(n)}catch{}}}let n=!1;export function armTerminalCleanup(){if(n)return;n=!0,process.on("exit",cleanupTerminal);const e={SIGINT:130,SIGTERM:143,SIGHUP:129};for(const[n,r]of Object.entries(e))process.on(n,()=>{cleanupTerminal(),process.exit(r)})}
package/dist/lib/theme.js CHANGED
@@ -1 +1 @@
1
- const e="]110]111]112",t="";function s(){if(!process.stdout.isTTY)return!1;const e=process.stdout.getColorDepth?.();return"number"!=typeof e||e>=8}function r(){return"1"===process.env.ICOA_TERMINAL}let o=!1;export function setTerminalTheme(n="dark"){if(!s())return;if(r())return;const c="high-contrast"===n?"]10;#FFFFFF]11;#000000]12;#FFFFFF":"]10;#A9B7C6]11;#2B2B2B]12;#A9B7C6",i="Apple_Terminal"===process.env.TERM_PROGRAM?"high-contrast"===n?"":"":"high-contrast"===n?"":"";if(process.stdout.write(c+i),!o){o=!0;const s=()=>{try{process.stdout.write(e+t)}catch{}};process.on("exit",s),process.on("SIGINT",()=>{s(),process.exit(130)}),process.on("SIGTERM",()=>{s(),process.exit(143)}),process.on("SIGHUP",()=>{s(),process.exit(129)})}}export function resetTerminalTheme(){s()&&(r()||process.stdout.write(e+t))}
1
+ import{armTerminalCleanup as e,cleanupTerminal as r,isIcoaTerminal as t,supportsAnsi as m}from"./terminal-cleanup.js";export function setTerminalTheme(r="dark"){if(!m())return;if(t())return;const n="high-contrast"===r?"]10;#FFFFFF]11;#000000]12;#FFFFFF":"]10;#A9B7C6]11;#2B2B2B]12;#A9B7C6",i="Apple_Terminal"===process.env.TERM_PROGRAM?"high-contrast"===r?"":"":"high-contrast"===r?"":"";process.stdout.write(n+i),e()}export function resetTerminalTheme(){r()}
@@ -1 +1 @@
1
- import chalk from"chalk";import{spawn as t}from"node:child_process";import{existsSync as n,mkdirSync as r,writeFileSync as a}from"node:fs";import{join as e}from"node:path";import{getIcoaDir as o}from"./config.js";const s=["https://practice.icoa2026.au/assets/translations","https://au.icoa2026.au/assets/translations"];export function getLangCacheDir(t){return e(o(),"translations",t)}export function isLangCached(t){if("en"===t)return!0;const r=getLangCacheDir(t);return n(e(r,".cached"))}export async function ensureLangCache(n,o={}){if("en"===n)return!0;if(isLangCached(n))return!0;const i=getLangCacheDir(n);r(i,{recursive:!0}),o.silent||console.log(chalk.gray(` Fetching ${n} translation pack ...`));let c=null;for(const r of s)try{const s=`${r}/${n}.tar.gz`,l=await fetch(s);if(!l.ok){c=`HTTP ${l.status}`;continue}const f=e(i,`${n}.tar.gz`);return a(f,Buffer.from(await l.arrayBuffer())),await new Promise((n,r)=>{const a=t("tar",["-xzf",f,"-C",i],{stdio:"ignore"});a.on("close",t=>0===t?n():r(new Error(`tar exit ${t}`))),a.on("error",r)}),a(e(i,".cached"),(new Date).toISOString()),o.silent||console.log(chalk.green(` ✓ ${n} translations cached at ~/.icoa/translations/${n}/`)),!0}catch(t){c=t instanceof Error?t.message:String(t)}return o.silent||console.log(chalk.yellow(` Translation pack not available (${c??"all mirrors unreachable"}). The interface stays in English; per-question translations fall back to the on-the-fly translator.`)),!1}
1
+ import chalk from"chalk";import{spawn as t}from"node:child_process";import{existsSync as n,mkdirSync as r,writeFileSync as a}from"node:fs";import{join as o}from"node:path";import{getIcoaDir as e}from"./config.js";import{httpFetch as s}from"./http.js";const i=["https://practice.icoa2026.au/assets/translations","https://au.icoa2026.au/assets/translations"];export function getLangCacheDir(t){return o(e(),"translations",t)}export function isLangCached(t){if("en"===t)return!0;const r=getLangCacheDir(t);return n(o(r,".cached"))}export async function ensureLangCache(n,e={}){if("en"===n)return!0;if(isLangCached(n))return!0;const c=getLangCacheDir(n);r(c,{recursive:!0}),e.silent||console.log(chalk.gray(` Fetching ${n} translation pack ...`));let l=null;for(const r of i)try{const i=`${r}/${n}.tar.gz`,f=await s(i);if(!f.ok){l=`HTTP ${f.status}`;continue}const g=o(c,`${n}.tar.gz`);return a(g,Buffer.from(await f.arrayBuffer())),await new Promise((n,r)=>{const a=t("tar",["-xzf",g,"-C",c],{stdio:"ignore"});a.on("close",t=>0===t?n():r(new Error(`tar exit ${t}`))),a.on("error",r)}),a(o(c,".cached"),(new Date).toISOString()),e.silent||console.log(chalk.green(` ✓ ${n} translations cached at ~/.icoa/translations/${n}/`)),!0}catch(t){l=t instanceof Error?t.message:String(t)}return e.silent||console.log(chalk.yellow(` Translation pack not available (${l??"all mirrors unreachable"}). The interface stays in English; per-question translations fall back to the on-the-fly translator.`)),!1}
package/dist/lib/ui.js CHANGED
@@ -1 +1 @@
1
- import chalk from"chalk";import o from"cli-table3";import ora from"ora";import{Marked as t}from"marked";import{markedTerminal as e}from"marked-terminal";import{c as n}from"./colors.js";const r=new t(e());export function printSuccess(o){console.log(chalk.green("✓ ")+n.fg(o))}export function printError(o){console.log(chalk.red("✗ ")+n.fg(o))}export function printWarning(o){console.log(chalk.yellow("⚠ ")+n.fg(o))}export function printInfo(o){console.log(chalk.blue("ℹ ")+n.fg(o))}export function printTable(t,e){const n=new o({head:t.map(o=>chalk.cyan.bold(o)),style:{head:[],border:[]}});for(const o of e)n.push(o);console.log(n.toString())}export function printMarkdown(o){const t=r.parse(o);"string"==typeof t&&console.log(t)}const l=new t(e({text:chalk.yellowBright,paragraph:chalk.yellowBright,listitem:chalk.yellowBright,strong:chalk.yellowBright.bold,em:chalk.yellowBright.italic,blockquote:chalk.yellowBright.italic}));export function printMarkdownYellow(o){const t=l.parse(o);"string"==typeof t&&console.log(t)}let i=!1;export function setReplMode(o){i=o}export function createSpinner(o){if(i){const t={text:o,start:()=>(console.log(chalk.cyan(` ${o}`)),t),stop:()=>t,succeed:o=>(console.log(chalk.green(` ✓ ${o}`)),t),fail:o=>(console.log(chalk.red(` ✗ ${o}`)),t),info:o=>(console.log(chalk.blue(` ℹ ${o}`)),t),warn:o=>(console.log(chalk.yellow(` ⚠ ${o}`)),t)};return t}return ora({text:o,color:"cyan"})}export function formatCountdown(o){const t=new Date,e=o.getTime()-t.getTime();if(e<=0)return"00:00:00";const n=Math.floor(e/36e5),r=Math.floor(e%36e5/6e4),l=Math.floor(e%6e4/1e3);return[n.toString().padStart(2,"0"),r.toString().padStart(2,"0"),l.toString().padStart(2,"0")].join(":")}export function printHeader(o){console.log(),console.log(chalk.cyan.bold(` ${o}`)),console.log(chalk.cyan(` ${"─".repeat(o.length+4)}`))}export function printKeyValue(o,t){console.log(` ${chalk.gray(`${o}:`)} ${n.fg(t)}`)}
1
+ import chalk from"chalk";import o from"cli-table3";import ora from"ora";import{Marked as t}from"marked";import{markedTerminal as e}from"marked-terminal";import{c as n}from"./colors.js";new t(e());export function printSuccess(o){console.log(chalk.green("✓ ")+n.fg(o))}export function printError(o){console.log(chalk.red("✗ ")+n.fg(o))}export function printWarning(o){console.log(chalk.yellow("⚠ ")+n.fg(o))}export function printInfo(o){console.log(chalk.blue("ℹ ")+n.fg(o))}export function printTable(t,e){const n=new o({head:t.map(o=>chalk.cyan.bold(o)),style:{head:[],border:[]}});for(const o of e)n.push(o);console.log(n.toString())}const r=new t(e({text:n.fg,paragraph:n.fg,listitem:n.fg,blockquote:n.fg}));export function printMarkdown(o){const t=r.parse(o);"string"==typeof t&&console.log(t)}const l=new t(e({text:chalk.yellowBright,paragraph:chalk.yellowBright,listitem:chalk.yellowBright,strong:chalk.yellowBright.bold,em:chalk.yellowBright.italic,blockquote:chalk.yellowBright.italic}));export function printMarkdownYellow(o){const t=l.parse(o);"string"==typeof t&&console.log(t)}let i=!1;export function setReplMode(o){i=o}export function createSpinner(o){if(i){const t={text:o,start:()=>(console.log(chalk.cyan(` ${o}`)),t),stop:()=>t,succeed:o=>(console.log(chalk.green(` ✓ ${o}`)),t),fail:o=>(console.log(chalk.red(` ✗ ${o}`)),t),info:o=>(console.log(chalk.blue(` ℹ ${o}`)),t),warn:o=>(console.log(chalk.yellow(` ⚠ ${o}`)),t)};return t}return ora({text:o,color:"cyan"})}export function formatCountdown(o){const t=new Date,e=o.getTime()-t.getTime();if(e<=0)return"00:00:00";const n=Math.floor(e/36e5),r=Math.floor(e%36e5/6e4),l=Math.floor(e%6e4/1e3);return[n.toString().padStart(2,"0"),r.toString().padStart(2,"0"),l.toString().padStart(2,"0")].join(":")}export function printHeader(o){console.log(),console.log(chalk.cyan.bold(` ${o}`)),console.log(chalk.cyan(` ${"─".repeat(o.length+4)}`))}export function printKeyValue(o,t){console.log(` ${chalk.gray(`${o}:`)} ${n.fg(t)}`)}
package/dist/repl.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ export declare function nearestCommand(word: string, max?: number): string | null;
2
3
  export declare function startRepl(program: Command, resumeMode: boolean): Promise<void>;
4
+ export declare function mapCommand(input: string): string[];