icoa-cli 2.19.396 → 2.19.398

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.
@@ -0,0 +1 @@
1
+ export declare function runIoailabStandalone(): Promise<void>;
@@ -0,0 +1 @@
1
+ import{spawn as e}from"node:child_process";import{existsSync as o,mkdirSync as t,readFileSync as n,writeFileSync as r}from"node:fs";import{homedir as a}from"node:os";import{join as s}from"node:path";import{Interface as i,createInterface as c}from"node:readline";import chalk from"chalk";import{editTextInEditor as l}from"../lib/editor-spawn.js";import{buildSshArgs as g,leaseConnectProblems as u,parseLeaseResponse as y,queueLine as d}from"../lib/gpu.js";import{logCommand as m}from"../lib/logger.js";import{getMainRl as f}from"../lib/main-rl.js";import{openFile as p}from"../lib/open-file.js";import{printError as h}from"../lib/ui.js";const w=[{id:"pick-cube",env:"PrimeLite-PickCube-v0",blurb:"Robot arm picks a cube off the table (imitation-learning demo)"}],b=/^[a-z0-9-]{1,32}$/,v=/^MP4:(\/[A-Za-z0-9._/-]+\.mp4)\s*$/m,k=/^HDF5:(\/[A-Za-z0-9._/-]+\.hdf5)\s*$/m,$=/^CKPT:(\/[A-Za-z0-9._/-]+\.pth)\s*$/m,x=/^METRICS:(\{.*\})\s*$/m,P=/^algo(?:_used)?:\s*([a-z_]+)\s*$/im,_="/opt/ioailab-mj/work",A=`${_}/05_custom_agent.py`;function I(){return process.env.ICOA_GPU_SERVER?process.env.ICOA_GPU_SERVER.replace(/\/$/,""):"https://practice.icoa2026.au"}function U(){return s(a(),".icoa","gpu-key")}function S(){if(process.env.ICOA_GPU_KEY)return process.env.ICOA_GPU_KEY.trim();try{return n(U(),"utf-8").trim()||null}catch{return null}}function G(e){t(s(a(),".icoa"),{recursive:!0}),r(U(),`${e.trim()}\n`,{mode:384})}async function O(e,o,t){try{const n=await fetch(`${I()}/api/icoa/gpu/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t?{key:o,task:t}:{key:o}),signal:AbortSignal.timeout(2e4)});return y(await n.json())}catch{return{state:"error",message:"GPU broker not reachable"}}}async function C(e){try{const o=await fetch(`${I()}/api/icoa/gpu/queue`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:e}),signal:AbortSignal.timeout(15e3)}),t=await o.json();return t&&!0===t.success?"ok":"invalid"}catch{return"offline"}}let L=null,T=null,E=null,R=!1,j=!1,q=!1,N=!1,Y=!1,M=null;function K(){return s(a(),"icoa-ioailab","05_custom_agent.py")}function V(o,t,n,r){return new Promise(a=>{const s="local"===o.transport?{command:"/bin/sh",args:["-c",n]}:{command:"ssh",args:g(o,t,n)},i=e(s.command,s.args,{stdio:["ignore","pipe","pipe"]});let c="",l="";const u=setTimeout(()=>i.kill("SIGKILL"),r);i.stdout.on("data",e=>{c+=e.toString(),l+=e.toString();let o=l.indexOf("\n");for(;o>=0;){const e=l.slice(0,o).trimEnd();l=l.slice(o+1),e.length>0&&console.log(chalk.white(` ${e}`)),o=l.indexOf("\n")}}),i.stderr.on("data",e=>{const o=e.toString().trimEnd();o.length>0&&console.log(chalk.gray(` ${o}`))}),i.on("close",e=>{clearTimeout(u),a({code:e??1,out:c})}),i.on("error",()=>{clearTimeout(u),a({code:127,out:c})})})}function z(o,t,n,a){return new Promise(s=>{const i=g(o,t,`cat ${n}`),c=e("ssh",i,{stdio:["ignore","pipe","pipe"]}),l=[],u=setTimeout(()=>c.kill("SIGKILL"),12e4);c.stdout.on("data",e=>l.push(e)),c.on("close",e=>{if(clearTimeout(u),0!==e||0===l.length)return s(!1);try{r(a,Buffer.concat(l)),s(!0)}catch{s(!1)}}),c.on("error",()=>{clearTimeout(u),s(!1)})})}async function D(o,t){if(!L)return!1;if("local"===L.transport)try{return r(t,n(o)),!0}catch{return!1}return function(o,t,r,a){return new Promise(s=>{let i;try{i=n(r)}catch{return s(!1)}const c=g(o,t,`mkdir -p ${_} && cat > ${a}`),l=e("ssh",c,{stdio:["pipe","ignore","pipe"]}),u=setTimeout(()=>l.kill("SIGKILL"),12e4);l.on("close",e=>{clearTimeout(u),s(0===e)}),l.on("error",()=>{clearTimeout(u),s(!1)}),l.stdin&&(l.stdin.on("error",()=>{}),l.stdin.write(i),l.stdin.end())})}(L,T,o,t)}function F(){return!(!L||"ssh"===L.transport&&!T)||(console.log(chalk.yellow(" No GPU box leased — ")+chalk.cyan("on")+chalk.gray(" first.")),!1)}function B(e,o,t){let n;for(let t=0;t<e.length;t++){const r=e[t];r===`--${o}`&&t+1<e.length?n=e[t+1]:r.startsWith(`--${o}=`)?n=r.slice(o.length+3):void 0===n&&/^[0-9]+$/.test(r)&&(n=r)}if(void 0===n)return t;const r=Number.parseInt(n,10);return Number.isFinite(r)?Math.min(999,Math.max(1,r)):t}async function W(e,o,t,n){if(!L||"ssh"===L.transport&&!T)return null;const r=w[0].id;return V(L,T??"",`sudo -n /usr/local/bin/ioailab-run ${r} ${e} --${o} ${t}`,n)}function J(e){4===e?console.log(chalk.yellow(" The box says an earlier step is missing — run it first (")+chalk.cyan("guide")+chalk.yellow(" shows where you are).")):2===e?console.log(chalk.red(" The box rejected the request (bad arguments).")):console.log(chalk.red(" Step failed on the box.")+chalk.gray(" (lease may have expired — ")+chalk.cyan("status")+chalk.gray(")"))}async function Z(e,o){if(!function(e){const o=L?.outDir??"";return!(""===o||!e.startsWith(o))||e===_||e.startsWith(`${_}/`)}(e))return void console.log(chalk.red(" Rejected unexpected video path from the box."));const i=s(a(),"icoa-ioailab");t(i,{recursive:!0});const c=s(i,`${o}-${Date.now()}.mp4`);process.stdout.write(chalk.gray(" Pulling the rendered clip…")),("local"===L?.transport?(()=>{try{return r(c,n(e)),!0}catch{return!1}})():await z(L,T,e,c))?(E=c,console.log(chalk.green(" done.")),console.log(chalk.magenta(" 🎬 clip → ")+chalk.cyan(c)),p(c)?console.log(chalk.gray(" Opened in your video viewer. ")+chalk.cyan("watch")+chalk.gray(" reopens it any time.")):console.log(chalk.gray(" Open it with your video player."))):console.log(chalk.red(" failed."))}function H(e){const o=e.match(x);if(!o)return void console.log(chalk.yellow(" Finished, but no metrics came back from the box."));let t;try{t=JSON.parse(o[1])}catch{return void console.log(chalk.yellow(" (metrics came back in an unexpected format.)"))}if(M=t,"number"==typeof t.success_rate){const e="number"==typeof t.episodes?` over ${t.episodes} episode(s)`:"";console.log(chalk.bold.green(` ★ success rate: ${(100*t.success_rate).toFixed(1)}%`)+chalk.gray(e))}for(const[e,o]of Object.entries(t))"success_rate"!==e&&"episodes"!==e&&("number"!=typeof o&&"string"!=typeof o||console.log(chalk.gray(` ${e}: `)+chalk.white(String(o))))}function Q(){let e;e=R?j?q?N?"edit custom":"eval":"train":"mimic":"collect",console.log(chalk.gray(" Next: ")+chalk.cyan(e)+chalk.gray(" (")+chalk.cyan("guide")+chalk.gray(" = where you are)"))}function X(){let e=!1;const o=[["collect",R],["mimic",j],["train",q],["eval",N],["custom",Y]].map(([o,t])=>t?`${o} ${chalk.green("✓")}`:e?`${o} ${chalk.gray("○")}`:(e=!0,`${o} ${chalk.yellow("…")}`));console.log(),console.log(chalk.bold.white(" ═══ 📖 IOAI Lab — the 5-step workflow ═══")),console.log(),console.log(chalk.gray(" Where you are: ")+o.join(chalk.gray(" · "))),console.log(" "+(L?chalk.green("⚡ lease active")+chalk.gray(" (")+chalk.cyan("status")+chalk.gray(" for time left)"):chalk.gray("no lease — ")+chalk.cyan("on")+chalk.gray(" to lease a box"))),M&&"number"==typeof M.success_rate&&console.log(chalk.gray(" Last result: ")+chalk.green(`${(100*M.success_rate).toFixed(1)}% success`)),console.log(),console.log(chalk.bold.white(" Your move — drive the contest workflow:")),console.log(chalk.gray(" 1. ")+chalk.cyan("collect")+chalk.gray(" expert demos → watch the arm pick the cube")),console.log(chalk.gray(" 2. ")+chalk.cyan("mimic")+chalk.gray(" expand your demos (pose-randomized, MimicGen-lite)")),console.log(chalk.gray(" 3. ")+chalk.cyan("train")+chalk.gray(" robomimic Diffusion Policy on YOUR demos (GPU, scored)")),console.log(chalk.gray(" 4. ")+chalk.cyan("eval")+chalk.gray(" run YOUR policy → success rate → watch")),console.log(chalk.gray(" 5. ")+chalk.cyan("edit custom")+chalk.gray(" / ")+chalk.cyan("custom")+chalk.gray(" write your own BaseAgent → run it → watch")),console.log(),console.log(chalk.bold.white(" 🤝 Want AI help? Set up your cockpit:")),console.log(chalk.gray(" 1. split the screen: ")+chalk.bold.cyan("Ctrl-b %")+chalk.gray(" (lesson/work left · AI right)")),console.log(chalk.gray(" 2. in the right pane: ")+chalk.bold.cyan("icoa")+chalk.gray(" then ")+chalk.bold.cyan("ai4ioai")+chalk.gray(" → live Gemma 3")),console.log(chalk.gray(" 3. ask it to explain a step or debug your agent — then ")+chalk.bold.white("VERIFY")+chalk.gray(" by re-running ")+chalk.cyan("eval")+chalk.gray(".")),console.log(),console.log(chalk.cyan(" on")+chalk.gray("/")+chalk.cyan("off")+chalk.gray(" lease a box · ")+chalk.cyan("status")+chalk.gray(" time left · ")+chalk.cyan("watch")+chalk.gray(" replay last clip · ")+chalk.cyan("run")+chalk.gray(" quick showcase · ")+chalk.cyan("exit")+chalk.gray(" leave")),console.log()}async function ee(e=!1){const o=S();o&&L&&await O("release",o),L&&!e&&console.log(chalk.gray(" GPU lease released.")),L=null,T=null,R=!1,j=!1,q=!1,N=!1,Y=!1,M=null}async function oe(e){const i=e.trim();if(0===i.length)return"continue";const[c,...g]=i.split(/\s+/),y=c.toLowerCase();return m(`ioailab ${y}`),"exit"===y||"quit"===y||"q"===y?(await ee(!0),console.log(chalk.gray(" Leaving IOAI Lab — any GPU lease was released.")),"exit"):("guide"===y||"help"===y||"/guide"===y||"?"===y?X():"tasks"===y||"task"===y?function(){console.log();for(const e of w)console.log(chalk.cyan(` ${e.id.padEnd(12)}`)+chalk.gray(e.blurb));console.log()}():"key"===y?0===g.length?console.log(chalk.gray(" Usage: ")+chalk.cyan("key <your-access-key>")):(G(g[0]),console.log(chalk.green(" ✓ GPU access key saved.")+chalk.gray(" Next: ")+chalk.cyan("on"))):"on"===y||"lease"===y||"gpu"===y?await async function(){if(L)return void console.log(chalk.gray(" already holding a GPU lease — ")+chalk.cyan("status")+chalk.gray(" for time left."));const e=S();if(!e)return console.log(chalk.yellow(" No GPU access key found.")),void console.log(chalk.gray(" Save yours once with ")+chalk.cyan("key <your-key>")+chalk.gray(" (issued by your coach/ICOA)."));process.stdout.write(chalk.gray(" Requesting a GPU box…"));let o=await O("lease",e,"ioailab"),n=0;for(;"queued"===o.state&&n<24;)0===n&&console.log(),process.stdout.write(`\r ${chalk.yellow(d(o))} `),await new Promise(e=>setTimeout(e,5e3)),o=await O("status",e),n++;if("queued"===o.state)return console.log(),void console.log(chalk.yellow(" Still queued — your place is held server-side. Try ")+chalk.cyan("on")+chalk.gray(" again in a few minutes."));if("ready"!==o.state)return void console.log(chalk.red(" failed.")+chalk.gray(` ${o.message??"no lease granted"}`));const i=u(o);if("local"!==o.transport||process.env.ICOA_GPU_SERVER||i.push("local transport from a non-dev broker"),i.length>0)return console.log(chalk.red(" rejected: ")+chalk.gray(i.join(", "))),void await O("release",e);let c=null;if("ssh"===o.transport&&(c=function(e){const o=s(a(),".icoa","gpu","ioailab-lease-key");try{return t(s(a(),".icoa","gpu"),{recursive:!0}),r(o,e.keyPem,{mode:384}),o}catch{return null}}(o),!c))return console.log(chalk.red(" could not write the ssh key locally.")),void await O("release",e);L=o,T=c;const l=Math.round((o.timeLeftSec??0)/60);console.log(chalk.bold.green(" ⚡ GPU box leased.")+chalk.gray(` ~${l}m on this lease.`)),console.log(chalk.gray(" Next: ")+chalk.cyan("collect")+chalk.gray(" — start the 5-step chain (or ")+chalk.cyan("run")+chalk.gray(" for a quick one-shot showcase)."))}():"off"===y||"release"===y?await ee():"status"===y||"queue"===y?await async function(){const e=S();if(!e)return void console.log(chalk.yellow(" No GPU access key saved — ")+chalk.cyan("key <k>")+chalk.gray(" first."));const o=await O("status",e);if("ready"===o.state){const e=Math.round((o.timeLeftSec??0)/60);console.log(chalk.green(` ⚡ lease active · ~${e}m left.`))}else"queued"===o.state?console.log(` ${chalk.yellow(d(o))}`):console.log(chalk.gray(" no active lease — ")+chalk.cyan("on")+chalk.gray(" to request a box."))}():"run"===y?await async function(e){const o=w.find(o=>o.id===e);if(!o||!b.test(e))return void console.log(chalk.yellow(` Unknown task "${e}" — `)+chalk.cyan("tasks")+chalk.gray(" lists what is available."));if(!F())return;console.log(chalk.gray(` Running the ${o.id} task (ICOA-VLA arm) on the GPU box (headless — takes a moment)…`));const t=`sudo -n /usr/local/bin/ioailab-run ${e} 1`,{code:n,out:r}=await V(L,T??"",t,72e4);if(0!==n)return void console.log(chalk.red(" Episode failed on the box.")+chalk.gray(" (lease may have expired — ")+chalk.cyan("status")+chalk.gray(")"));const a=r.match(v);a?await Z(a[1],e):console.log(chalk.yellow(" Episode finished but no video path came back — box-side renderer issue."))}((g[0]??w[0].id).toLowerCase()):"collect"===y?await async function(e){if(!F())return;const o=B(e,"episodes",5);console.log(chalk.gray(` Collecting ${o} expert demo(s) on the ICOA-VLA arm (headless — takes a moment)…`));const t=await W("collect","episodes",o,72e4);if(!t)return;if(0!==t.code)return J(t.code);const n=t.out.match(k);n&&(R=!0,console.log(chalk.green(" ✓ expert demos recorded → ")+chalk.gray(n[1]))),console.log(chalk.gray(" The expert is a scripted reach→grasp→lift waypoint controller (our stand-in for")),console.log(chalk.gray(" the contest's motion planner) — a clean demonstration source, not a learned policy yet."));const r=t.out.match(v);r&&await Z(r[1],"collect"),Q()}(g):"mimic"===y?await async function(e){if(!F())return;if(!R)return void console.log(chalk.yellow(" No demos yet — run ")+chalk.cyan("collect")+chalk.gray(" first."));const o=B(e,"episodes",36);console.log(chalk.gray(` Expanding your demos into ${o} training episode(s)…`));const t=await W("mimic","episodes",o,72e4);if(!t)return;if(0!==t.code)return J(t.code);const n=t.out.match(k);n&&(j=!0,console.log(chalk.green(" ✓ training demos generated → ")+chalk.gray(n[1]))),console.log(chalk.gray(" MimicGen-lite: we randomize the cube pose and re-run the pose-conditioned expert —")),console.log(chalk.gray(" real rollouts, same robomimic schema, not the object-centric MimicGen the full sim uses.")),Q()}(g):"train"===y?await async function(e){if(!F())return;if(!j)return void console.log(chalk.yellow(" No training set yet — run ")+chalk.cyan("mimic")+chalk.gray(" first."));const o=B(e,"epochs",20),t=54e5,n=L?.timeLeftSec??0;if(n>0&&1e3*n<t){const e=Math.round(n/60);console.log(chalk.yellow(` ⚠ your lease has ~${e}m left`)+chalk.gray(" — a long train may outlast it. ")+chalk.cyan("off")+chalk.gray("/")+chalk.cyan("on")+chalk.gray(" to refresh first if needed."))}console.log(chalk.gray(` Training a robomimic Diffusion Policy for ${o} epoch(s) on YOUR demos — the scored step (GPU)…`));const r=await W("train","epochs",o,t);if(!r)return;if(0!==r.code)return J(r.code);const a=r.out.match($);a&&(q=!0,console.log(chalk.green(" ✓ checkpoint saved → ")+chalk.gray(a[1])));const s=r.out.match(P);s&&/bc/i.test(s[1])?console.log(chalk.gray(" (behavior-cloning fallback — real diffusion pending on this box.)")):console.log(chalk.gray(" Same open robomimic Diffusion Policy the real contest uses — high-fidelity where it counts.")),Q()}(g):"eval"===y?await async function(e){if(!F())return;if(!q)return void console.log(chalk.yellow(" No trained policy yet — run ")+chalk.cyan("train")+chalk.gray(" first."));const o=B(e,"episodes",20);console.log(chalk.gray(` Running YOUR trained policy for ${o} episode(s)…`));const t=await W("eval","episodes",o,72e4);if(!t)return;if(0!==t.code)return J(t.code);H(t.out),N=!0,console.log(chalk.gray(" Your number reflects the practice arm + your demo count/epochs — it's the workflow signal,")),console.log(chalk.gray(" not a contest leaderboard score."));const n=t.out.match(v);n&&await Z(n[1],"eval"),Q()}(g):"custom"===y?await async function(e){if(!F())return;const t=K();if(!o(t))return void console.log(chalk.yellow(" No custom agent yet — ")+chalk.cyan("edit custom")+chalk.gray(" to write one first."));process.stdout.write(chalk.gray(" Uploading your agent…"));const n=await D(t,A);if(console.log(n?chalk.green(" done."):chalk.red(" failed.")),!n)return;const r=B(e,"episodes",9);console.log(chalk.gray(` Running YOUR agent for ${r} episode(s)…`));const a=await W("custom","episodes",r,72e4);if(!a)return;if(0!==a.code)return J(a.code);H(a.out),Y=!0;const s=a.out.match(v);s?await Z(s[1],"custom"):console.log(chalk.gray(" (no replay — for it to render, your agent must be constructible with no required args. Metrics above.)")),console.log(chalk.gray(" Iterate: ")+chalk.cyan("edit custom")+chalk.gray(" → ")+chalk.cyan("custom")+chalk.gray(" → watch, repeat."))}(g):"edit"===y?"custom"===(g[0]??"").toLowerCase()?await async function(){if(!F())return;const e=s(a(),"icoa-ioailab");t(e,{recursive:!0});const i=K();o(i)||(process.stdout.write(chalk.gray(" Fetching the starter agent…")),await async function(e,o){if(!L)return!1;if("local"===L.transport)try{return r(o,n(e)),!0}catch{return!1}return z(L,T,e,o)}("/opt/ioailab-mj/examples/05_custom_agent.py",i)?console.log(chalk.green(" done.")):(r(i,'"""05_custom_agent.py — your agent for the ICOA-VLA arm.\n\nSubclass BaseAgent and implement act(env, env_ids) -> array [rows, action_dim].\nKeep the constructor argument-free so the replay can render your agent\n(like the SinusoidAgent starter below). Then: edit custom -> custom -> watch.\n"""\nimport numpy as np\n\ntry:\n from icoa_ioailab.agents import BaseAgent\nexcept Exception: # resolved on the GPU box\n BaseAgent = object\n\n\nclass SinusoidAgent(BaseAgent):\n def __init__(self):\n self._t = 0\n\n def act(self, env, env_ids):\n rows = len(env_ids)\n dim = getattr(env, "action_dim", 7)\n self._t += 1\n cmd = 0.1 * np.sin(self._t * 0.2)\n return np.full((rows, dim), cmd, dtype=np.float32)\n'),console.log(chalk.gray(" using a local starter."))));const c=n(i,"utf-8"),g=l(c,".py");if(null===g)return void console.log(chalk.yellow(" Editor cancelled — agent unchanged.")+chalk.gray(" (set $EDITOR to choose your editor)"));r(i,g),process.stdout.write(chalk.gray(" Saving to the box…"));const u=await D(i,A);console.log(u?chalk.green(" done."):chalk.red(" failed.")),console.log(chalk.gray(" Saved → run ")+chalk.cyan("custom")+chalk.gray(" to try it."))}():console.log(chalk.gray(" Usage: ")+chalk.cyan("edit custom")+chalk.gray(" (write your own agent)")):"watch"===y||"open"===y?E&&p(E)?console.log(chalk.gray(" Reopened the last clip.")):console.log(chalk.gray(" No clip yet — ")+chalk.cyan("collect")+chalk.gray(" or ")+chalk.cyan("run")+chalk.gray(" first.")):console.log(chalk.yellow(` Unknown command "${y}" — `)+chalk.cyan("guide")+chalk.gray(" lists everything.")),"continue")}export async function runIoailabStandalone(){if(!await async function(){const{input:e}=await import("@inquirer/prompts"),o=S();if(o){const e=await C(o);if("ok"===e)return!0;if("offline"===e)return console.log(chalk.yellow(" Cannot reach the GPU service to verify your key right now — try again later.")),!1;console.log(chalk.yellow(" Your saved GPU key was not accepted. Enter a valid one below."))}else console.log(chalk.gray(" IOAI Lab is a paid GPU service. Enter the GPU access key issued with your purchase."));for(let o=0;o<3;o++){let t;try{t=await e({message:chalk.bold.magenta("icoa ioailab key>"),theme:{prefix:"",style:{message:e=>e}}})}catch{return!1}const n=t.trim();if(""===n||"exit"===n.toLowerCase()||"q"===n.toLowerCase())return!1;const r=await C(n);if("ok"===r)return G(n),console.log(chalk.green(" ✓ Key accepted — saved to this device. You will not be asked again.")),!0;if("offline"===r)return console.log(chalk.yellow(" Cannot reach the GPU service to verify — try again later.")),!1;console.log(chalk.red(` Key not recognised (${o<2?2-o+" tries left":"no tries left"}).`))}return!1}())return;console.log(),console.log(chalk.bold.magenta(" 🤖 IOAI Lab — Embodied AI practice twin (ICOA-VLA arm)")),console.log(chalk.gray(" The official Team-Challenge sim uses assets we can't hand out — so this is a")),console.log(chalk.gray(" practice twin: the same 5-step workflow, the same Python API, a real arm you drive.")),console.log(chalk.gray(" The scored part — training a diffusion policy from your own demos with robomimic")),console.log(chalk.gray(" (open source) — is byte-for-byte the framework the real contest uses.")),console.log(chalk.gray(" Runs on an ICOA GPU box — nothing installs here; the video opens locally.")),console.log(),console.log(chalk.white(" You will drive the exact contest workflow:")),console.log(" "+chalk.bold.cyan("collect")+chalk.gray(" → ")+chalk.bold.cyan("mimic")+chalk.gray(" → ")+chalk.bold.cyan("train")+chalk.gray(" → ")+chalk.bold.cyan("eval")+chalk.gray(" → ")+chalk.bold.cyan("custom")),console.log(chalk.gray(" v1 is a single task (pick-cube) on a MuJoCo arm; the real contest is multi-task on a")),console.log(chalk.gray(" higher-fidelity simulator — the learning core is the same.")),X();const e=f(),o=null!==e,t=o?e.listeners("line").slice():[];o&&e.removeAllListeners("line");const n=o?e:c({input:process.stdin,output:process.stdout,terminal:!0}),r=o?n.prompt:null;o&&(n.prompt=i.prototype.prompt);const a=()=>{n.setPrompt(chalk.bold.magenta("icoa ioailab> ")),n.prompt()};return new Promise(e=>{const s=i=>{oe(i).then(i=>{"exit"===i?((()=>{if(n.removeListener("line",s),o){r&&(n.prompt=r);for(const e of t)n.on("line",e)}else n.close()})(),e()):a()}).catch(e=>{h(`ioailab error: ${e instanceof Error?e.message:String(e)}`),a()})};n.on("line",s),a()})}
@@ -1 +1 @@
1
- import chalk from"chalk";import{Interface as o,createInterface as e}from"node:readline";import{execSync as r,spawn as n}from"node:child_process";import{getMainRl as t}from"../lib/main-rl.js";import{ai4ioaiPromptLabel as l,handleAi4ioaiMessage as a,startAi4ioaiChat as s}from"./ai4ioai.js";import{existsSync as i}from"node:fs";import{dirname as c,join as u}from"node:path";import{fileURLToPath as d}from"node:url";import{loadCurriculumById as g,validateEAToken as m,syncProgress as y,syncCardFeedback as h}from"../lib/learn-curricula.js";import{normalizeTokenBody as f}from"../lib/token-format.js";import{streamLearnTutor as p,submitPqs as w,viewPqsTop as b,viewHonors as k}from"../lib/learn-ai.js";import{getConfig as v,saveConfig as A}from"../lib/config.js";import{loadLearnState as C,saveLearnState as I,newLearnState as $,updateStreak as E,markCardComplete as T,recordMCQ as _,recordCheck as x,markPracticalComplete as M,addAchievement as P}from"../lib/learn-state.js";import{renderWelcome as O,renderKnowledgeCard as L,renderMCQCard as D,renderMCQFeedback as S,renderPracticalCard as q,renderPracticalSuccess as j,renderSimDemoCard as N,renderMilestone as R,renderArenaTaskCard as W,renderComingSoonCard as F,renderStatus as B,renderPhaseComplete as z,renderCurriculumComplete as U,setLearnAiEnabled as J,setLearnPqsViewCards as Q,renderHonorWall as V}from"../lib/learn-render.js";import{printError as Y}from"../lib/ui.js";export function registerLearnCommand(H){H.command("learn [token]").description("Enter learn mode (free demo, or team-issued EA/EI/AC/CA/IO/IA token for full curriculum)").action(async H=>{H&&H.trim()||(console.log(),console.log(chalk.gray(" No token given — starting free 11-card demo (")+chalk.bold.green("LEARNDEMO01")+chalk.gray(").")),console.log(chalk.gray(" Full curriculum: ")+chalk.bold.yellow("learn EA|EI|AC|CA|IO|IA + 8 chars")+chalk.gray(" — token from your country team leader.")),console.log(),H="LEARNDEMO01");let K=H.trim().toUpperCase(),X=null;const Z=/^(LEARNDEMO01|AI4CTFDEMO01|CTF4AIDEMO01)$/i.test(K),G=/^[A-Z]{2}[A-Z0-9]{8}$/i.test(K);let oo=!1;if(Z)console.log(),console.log(chalk.gray(" Loading demo curriculum...")),X=await g(K,K);else if(G){console.log(),console.log(chalk.gray(" Validating token..."));let o=await m(K);if(!o.ok){const e=f(K);if(e!==K){const r=await m(e);r.ok&&(console.log(chalk.yellow(` Corrected look-alike characters: ${K} → ${e}`)),o=r,K=e)}}if(!o.ok)return Y(`Token validation failed: ${o.message}`),console.log(),"device_bound"===o.code?(console.log(chalk.gray(" This learn token is bound to the first computer it was used on,")),console.log(chalk.gray(" and this is a different computer.")),console.log(),console.log(chalk.gray(" If you changed computers or reinstalled, ask the coordinator who")),console.log(chalk.gray(" issued your token to move it to this device, then try again."))):(console.log(chalk.gray(" Possible causes:")),console.log(chalk.gray(" · Token expired or revoked")),console.log(chalk.gray(" · Network down (check connection)")),console.log(chalk.gray(" · Typo in token (check 0/O, 1/I/L, V/U)"))),void console.log();if(console.log(chalk.green(` ✓ Token valid · curriculum: ${o.curriculumId} · status: ${o.status}`)+(!0===o.aiEnabled?chalk.bold.magenta(" · ⭐ AI tutor"):"")),oo=!0===o.aiEnabled,console.log(chalk.gray(" Loading curriculum… (downloads ~1 MB; up to a minute on slow venue Wi-Fi — please wait)")),X=await g(o.curriculumId||"LEARNDEMO01",K),!X)return Y(`Could not open a learn curriculum for token '${K}'.`),console.log(),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${K}`)+chalk.gray(" instead.")),console.log(chalk.gray(" Otherwise: the full curriculum is ~3 MB; on a slow link the download")),console.log(chalk.gray(" can take a while — stay on a stable connection and run ")+chalk.bold.yellow(`learn ${K}`)+chalk.gray(" again")),console.log(chalk.gray(" (it caches after the first load). If it keeps failing, check ")+chalk.cyan("https://practice.icoa2026.au")+chalk.gray(" is reachable.")),void console.log()}if(!X)return Y(`Unknown learn token: ${K}`),console.log(),console.log(chalk.gray(" Available tokens:")),console.log(chalk.gray(" ")+chalk.bold.green("LEARNDEMO01")+chalk.gray(" free 11-card demo (anyone can use)")),console.log(chalk.gray(" ")+chalk.bold.yellow("EA|EI|AC|CA|IO|IA")+chalk.gray(" + 8 chars full curriculum (issued by team leader)")),console.log(),console.log(chalk.gray(" Tracks: ")+chalk.cyan("EA")+chalk.gray("=ctf4eai · ")+chalk.cyan("AC")+chalk.gray("=ai4ctf · ")+chalk.cyan("CA")+chalk.gray("=ctf4ai · ")+chalk.cyan("IO")+chalk.gray("=ioai · ")+chalk.cyan("IA")+chalk.gray("=iaio")),console.log(),console.log(chalk.gray(" To get a full curriculum, email ")),console.log(chalk.gray(" ")+chalk.cyan("australia@icoa2026.au")+chalk.gray(" or ask your country's team leader.")),void console.log();let eo=C(),ro=!1;try{eo&&eo.token===K?E(eo):(eo=$(K,X.id,X.totalCards),ro=!0),I(eo),O(X,eo,ro)}catch{return Y("Could not open this curriculum — it may be malformed or not a learn curriculum."),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${K}`)+chalk.gray(".")),void console.log()}const no=t(),to=null!==no,lo=to?no.listeners("line").slice():[];to&&no.removeAllListeners("line");const ao=to?no:e({input:process.stdin,output:process.stdout,terminal:!0}),so=to?ao.prompt:null;to&&(ao.prompt=o.prototype.prompt);const io=()=>{to&&so&&(ao.prompt=so)},co=()=>{ao.setPrompt(po?chalk.bold.green(`icoa ${l()}> `):chalk.bold.cyan(ho?"icoa learn ai> ":"icoa learn> ")),ao.prompt()};let uo=null,go=null,mo=null,yo=null,ho=!1,fo=!1,po=!1;const wo=new Map,bo=()=>{let o=wo.get(eo.currentCard);return o||(o=[],wo.set(eo.currentCard,o)),o};let ko=!1;const vo=new Set;let Ao=!1,Co=0;const Io=(v().language||"en").toLowerCase().startsWith("zh");J(oo),Q(vo);const $o=[];let Eo=null;const To=new Promise(o=>{Eo=o}),_o=()=>{const o=Eo;Eo=null,o?.()},xo="1"===process.env.ICOA_LEARN_EMBEDDED,Mo=o=>X.cards.find(e=>e.number===o),Po=o=>{const e=X.modules.find(e=>e.cardRange[1]===o);if(!e)return!1;const r=X.modules.find(o=>o.number===e.number+1);return z(X,eo,e,r),!0},Oo=()=>{Ao=!0,console.log(chalk.gray(Io?" 按 Enter 进入下一阶段…":" Press Enter to continue to the next phase…"))},Lo=()=>{const o=Mo(eo.currentCard);if(!o)return console.log(),console.log(chalk.gray(" No more cards in this curriculum.")),console.log(chalk.gray(" Type ")+chalk.bold.green("status")+chalk.gray(" for the dashboard or ")+chalk.bold.green("quit")+chalk.gray(" to exit.")),void console.log();switch(o.type){case"knowledge":L(o,X),o.check?(mo=o.number,Co=Date.now()):(T(eo,o.number),I(eo));break;case"mcq":D(o,X),uo=o.number;break;case"practical":q(o,X),go=o.number;break;case"sim_demo":N(o,X),T(eo,o.number),I(eo);break;case"milestone":R(o,X),P(eo,o.badge),T(eo,o.number),I(eo);break;case"arena_task":W(o,X),T(eo,o.number),I(eo);break;case"coming_soon":F(o,X),T(eo,o.number),I(eo)}},Do=()=>{K.endsWith("DEMO01")?console.log(chalk.yellow(Io?" AI 导师需要开通 AI 的正式课程 token(demo 是共享的,没有 AI 额度)。":" The AI tutor needs an AI-enabled curriculum token — demos are shared and carry no AI budget.")):(console.log(chalk.yellow(Io?" 此 token 未开通 AI 导师(高级功能)。":" This token does not include the AI tutor (premium).")),console.log(chalk.gray(Io?" 找你的领队 / 协调人换开通 AI 的 token。":" Ask your team leader / coordinator for an AI-enabled token."))),console.log()},So=async(o,e)=>{if(oo){fo=!0;try{console.log(),console.log(chalk.gray("deep"===e?Io?" 🤖 AI 导师(深度思考中,思考过程为灰色小字)…":" 🤖 AI tutor (deepthink — reasoning shown in gray)…":Io?" 🤖 AI 导师(快速模式)…":" 🤖 AI tutor (quick mode)…")),console.log();let r=!1,n=!1;const t=await p({token:K,cardNumber:eo.currentCard,question:o,mode:e,history:bo().slice(-8)},o=>{"reasoning"===o.kind?(r||(process.stdout.write(" "),r=!0),process.stdout.write(chalk.dim(o.text.replace(/\n/g,"\n ")))):(n||(process.stdout.write(r?"\n\n ":" "),n=!0),process.stdout.write(chalk.white(o.text.replace(/\n/g,"\n "))))});(r||n)&&console.log(),t.ok?t.offtopic?(console.log(),console.log(chalk.yellow(Io?" (被判定与本卡主题无关 — 跑题的问题不会被回答,并会拉低本卡的提问评分)":" (judged unrelated to this card — off-topic questions aren't answered and will lower this card's prompt score)"))):(bo().push({role:"user",content:o},{role:"assistant",content:t.answer}),null!=t.spent&&null!=t.cap&&t.spent/t.cap>.8&&console.log(chalk.gray(` (${Io?"AI 额度":"AI budget"}: ${Math.round(t.spent/1e3)}k / ${Math.round(t.cap/1e3)}k tokens)`))):(console.log(),console.log(chalk.yellow(` ${t.message||(Io?"AI 导师暂不可用。":"AI tutor unavailable.")}`))),null!==mo&&console.log(chalk.gray(Io?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()}finally{fo=!1}}else Do()},qo=o=>o.match(/.{1,64}(\s|$)/g)?.map(o=>o.trim())??[o],jo=async o=>{const e=o.trim().toLowerCase();if(!fo){if(null!==yo){const e=o.trim(),r=yo;if(yo=null,""===e)console.log(chalk.gray(Io?" 已取消(未提交)。":" Cancelled (nothing submitted)."));else{const o=Co>0?Date.now()-Co:void 0;await h(K,{curriculum_id:X.id,card_number:r,feedback_text:e,time_on_card_ms:o})?console.log(chalk.green(Io?" ✓ 已收到,谢谢帮助完善这张卡。":" ✓ Thanks — your report was recorded.")):console.log(chalk.yellow(Io?" ⚠ 未能送达服务器 — 这条报告没有保存,请稍后再按 e 重试。":" ⚠ Could not reach the server — this report was NOT saved. Try e again later."))}return console.log(),void co()}if(ko){ko=!1;const e=o.trim().toLowerCase();return"y"===e||"yes"===e?await(async()=>{fo=!0;try{console.log(),console.log(chalk.gray(Io?" 📊 提交中 — Panda 正在评审你的提问(通常几十秒,最长一两分钟,请稍候)…":" 📊 Submitting — Panda is reviewing how you asked (usually well under two minutes)…"));const o=await w({token:K,cardNumber:eo.currentCard});if(console.log(),!o.ok)return console.log(chalk.yellow(` ${o.message||(Io?"评审暂不可用。":"Review unavailable.")}`)),void console.log();(o=>{const e="green"===o.tier?chalk.bold.green:"blue"===o.tier?chalk.bold.cyan:"yellow"===o.tier?chalk.bold.yellow:chalk.bold.red;if(console.log(chalk.gray(" ─────────────────────────────────────────")),console.log(chalk.white(Io?" 提问评分 ":" Prompt score ")+e(`${o.score??0} / 100`)+chalk.gray(" "+(o=>{switch(o){case"green":return"🟢 "+(Io?"你把 AI 当作被审视的研究伙伴":"you used the AI as a research partner");case"blue":return"🔵 "+(Io?"你把 AI 当作实验助手":"you used the AI as a lab assistant");case"yellow":return"🟡 "+(Io?"你把 AI 当作讲师":"you used the AI as a lecturer");default:return"🔴 "+(Io?"你把 AI 当作答案机":"you used the AI as an answer machine")}})(o.tier))),o.manipulation&&console.log(chalk.red(Io?" ⚠ 检测到试图操纵评分,分数已按规则大幅降低":" ⚠ an attempt to manipulate the score was detected — the score was reduced sharply")),null!=o.rank&&null!=o.entrants){const e=!0===o.inTop3,r=Io?` 本卡排名 #${o.rank} / ${o.entrants}`:` Rank on this card: #${o.rank} of ${o.entrants}`;console.log((e?chalk.bold.green(r):chalk.white(r))+(e?chalk.bold.magenta(Io?" — 进入 TOP 3!输入 /view 看另两名的提问":" — TOP 3! type /view to read the other top prompts"):""))}if(console.log(chalk.gray(Io?` 第 ${o.submissionIndex??"?"} / 2 次提交 · 剩余 ${o.submissionsLeft??0} 次(可先追加 /ai 对话再交)`:` Attempt ${o.submissionIndex??"?"} of 2 · ${o.submissionsLeft??0} left (extend the /ai conversation before your next one)`)),o.feedback){console.log();for(const e of qo(o.feedback))console.log(chalk.white(" "+e))}o.improve&&(console.log(),console.log(chalk.cyan(Io?" 改进提问: ":" Ask better: ")+chalk.gray(o.improve))),console.log(chalk.gray(" ─────────────────────────────────────────")),null!=o.spent&&null!=o.cap&&o.spent/o.cap>.8&&console.log(chalk.gray(` (${Io?"AI 额度":"AI budget"}: ${Math.round(o.spent/1e3)}k / ${Math.round(o.cap/1e3)}k tokens)`)),console.log()})(o),!0===o.inTop3?vo.add(eo.currentCard):vo.delete(eo.currentCard);const e=await k({token:K,curriculumId:X.id,cardNumber:eo.currentCard});e.ok&&e.totals&&e.chapters&&(V({totals:e.totals,chapters:e.chapters,floor:e.floor}),console.log())}finally{fo=!1}})():(console.log(chalk.gray(Io?" 已取消(未提交,不消耗次数)。":" Cancelled — nothing submitted, no attempt used.")),console.log()),null!==mo&&(console.log(chalk.gray(Io?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()),void co()}if(Ao)return Ao=!1,Lo(),void co();if(e){if(po)return"exit"===await a(o.trim())&&(po=!1),void co();if(ho){if(["q","quit","exit","back","b","menu"].includes(e))return ho=!1,console.log(chalk.gray(Io?" 已退出 AI 导师,回到卡片。":" Left the AI tutor — back to the card.")),console.log(),void co();const r=o.trim().startsWith("!"),n=r?o.trim().slice(1).trim():o.trim();return n&&await So(n,r?"fast":"deep"),void co()}if("tmux"===e||e.startsWith("tmux ")){try{r("command -v tmux",{stdio:"ignore"})}catch{return console.log(chalk.yellow(" tmux is not installed yet.")),console.log(chalk.gray(" Install it with ")+chalk.bold.cyan("darwin"===process.platform?"brew install tmux":"sudo apt install tmux")+chalk.gray(Io?" — 或 quit 回到 icoa> 后运行 ":" — or quit to icoa> and run ")+chalk.bold.cyan("env setup")+chalk.gray(".")),console.log(),void co()}return await new Promise(e=>{const r=process.stdin,t=!!r.isTTY&&!!r.isRaw;if(ao.pause(),r.isTTY&&"function"==typeof r.setRawMode)try{r.setRawMode(!1)}catch{}const l=n(o.trim(),{shell:!0,stdio:"inherit"}),a=()=>{if(r.isTTY&&"function"==typeof r.setRawMode&&t)try{r.setRawMode(!0)}catch{}ao.resume(),e()};l.on("close",a),l.on("error",a)}),console.log(),void co()}if("ai4ioai"===e||"/ai4ioai"===e)return await s()&&(po=!0),void co();if("/ai"===e||"/ai!"===e||e.startsWith("/ai ")||e.startsWith("/ai! ")){if(!oo)return Do(),void co();const r=e.startsWith("/ai!"),n=o.trim().slice(r?4:3).trim();if(!n){ho=!0;const o=Mo(eo.currentCard),e=o?o.title??o.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 🤖 AI ")+chalk.bold.white(Io?"AI 导师":"AI tutor")+(e?chalk.gray(Io?` — 主题「${e}」`:` — topic “${e}”`):"")),console.log(chalk.gray(Io?" 直接输入问题(默认深度思考) · ! 开头 = 快答 · q = 返回卡片":" Type a question (deepthink by default) · prefix ! = quick answer · q = back to card")),console.log(),void co()}return await So(n,r?"fast":"deep"),void co()}if("/submit"===e||e.startsWith("/submit ")||"/research"===e||e.startsWith("/research ")){if(!oo)return Do(),void co();const o=Mo(eo.currentCard),e=o?o.title??o.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 📊 ")+chalk.bold.white(Io?"Panda 提问评审":"Panda's prompt review")+(e?chalk.gray(Io?` — 主题「${e}」`:` — topic “${e}”`):"")),console.log(chalk.gray(Io?" 评的是你在本卡上「怎么问 AI」— 你与 /ai 导师的整段对话会被":" It scores HOW you asked the AI on this card — your whole /ai")),console.log(chalk.gray(Io?" 整体评审,给出分数、名次和改进建议。":" conversation is reviewed for a score, a rank, and advice.")),console.log(chalk.gray(Io?" 每张卡最多 2 次评分;评出的最好成绩进本卡 Top-3 榜。":" Max 2 graded attempts per card; your best enters the card’s top-3 board.")),console.log(),console.log(chalk.white(Io?" 确认提交本卡对话评分?":" Submit this card’s conversation for grading?")+chalk.gray(Io?" y = 提交 · 其他 = 取消":" y = submit · anything else = cancel")),console.log(),ko=!0,void co()}if("/view"===e)return oo?(await(async()=>{fo=!0;try{console.log();const o=await b({token:K,cardNumber:eo.currentCard});if(!o.ok)return console.log(chalk.yellow(` ${o.message||(Io?"暂时无法查看。":"View unavailable right now.")}`)),"not_top3"===o.code&&vo.delete(eo.currentCard),void console.log();null!=o.myRank&&o.myRank<=3&&vo.add(eo.currentCard),console.log(chalk.bold.white(Io?` 🏆 本卡 Top-3 提问 — 第 ${eo.currentCard} 张 · ${o.entrants??0} 人上榜`:` 🏆 Top-3 prompts — card ${eo.currentCard} · ${o.entrants??0} entrant(s)`));for(const e of o.top3??[]){const o="green"===e.tier?"🟢":"blue"===e.tier?"🔵":"yellow"===e.tier?"🟡":"🔴";console.log(),console.log(chalk.bold.cyan(` ── Rank ${e.rank}`)+(e.me?chalk.bold.magenta(" (me)"):"")+chalk.white(` · ${e.score}`)+` ${o}`),e.questions.forEach((o,e)=>{const r=qo(o);console.log(chalk.gray(` Q${e+1} `)+chalk.white(r[0]??""));for(const o of r.slice(1))console.log(chalk.white(" "+o))}),0===e.questions.length&&console.log(chalk.gray(Io?" (无可显示的提问)":" (no questions to show)"))}console.log()}finally{fo=!1}})(),void co()):(Do(),void co());if("/honors"===e||"honors"===e)return Z?(console.log(),console.log(chalk.gray(Io?" 荣誉榜属于正式 learn token — 用团队签发的 token 进来即可查看和参赛。":" The honor board belongs to full learn tokens — join with a team-issued token to view and compete.")),console.log(),void co()):(await(async()=>{fo=!0;try{console.log();const o=await k({token:K,curriculumId:X.id,cardNumber:eo.currentCard});if(!o.ok||!o.totals||!o.chapters)return console.log(chalk.yellow(` ${o.message||(Io?"荣誉榜暂时不可用。":"Honor board unavailable right now.")}`)),void console.log();if(console.log(chalk.bold.white(Io?` 🏆 本卡实时 Top-3 — 第 ${eo.currentCard} 张`:` 🏆 Live top-3 — card ${eo.currentCard}`)),o.board&&0!==o.board.length)for(const e of o.board){const o="green"===e.tier?"🟢":"blue"===e.tier?"🔵":"yellow"===e.tier?"🟡":"🔴";console.log(chalk.white(` ${e.icon} `)+chalk.bold.cyan(`#${e.rank}`)+chalk.white(` · ${e.score} `)+o+(e.me?chalk.bold.magenta(" (me)"):""))}else console.log(chalk.gray(Io?" (本卡还没有人上榜)":" (no one has medaled on this card yet)"));console.log(),V({totals:o.totals,chapters:o.chapters,floor:o.floor}),console.log()}finally{fo=!1}})(),void co());if("menu"!==e&&"menu confirm"!==e)if("quit"!==e&&"exit"!==e&&"q"!==e){if("status"===e)return B(X,eo),void co();if("lang"===e||e.startsWith("lang ")){const o=e.slice(4).trim();if(!o)return console.log(chalk.gray(" 当前语言 / current: ")+chalk.white(v().language||"en")+chalk.gray(" · ")+chalk.bold.green("lang zh")+chalk.gray(" / ")+chalk.bold.green("lang en")),void co();const r=X.cards.some(o=>null!=o._zh),n=o.toLowerCase().startsWith("zh")?"zh":o.toLowerCase(),t="en"===n||"zh"===n&&r,l=t?n:"en";return A({language:l}),t?console.log(chalk.green(" ✓ ")+chalk.gray("语言 / language → ")+chalk.white(l)):console.log(chalk.yellow(" ! ")+chalk.gray(`${o} 本课暂无翻译,已用英文 / not available — using English`)),mo=null,uo=null,go=null,yo=null,Ao=!1,ko=!1,Lo(),void co()}if("sim"===e){const o=Mo(eo.currentCard);return o&&"sim_demo"===o.type?(function(o){const e=function(){const o=c(d(import.meta.url)),e=[u(o,"..","..","panda","mujoco-launcher.py"),u(o,"..","..","..","panda","mujoco-launcher.py")];for(const o of e)if(i(o))return o;return null}();if(!e)return console.log(chalk.yellow(" MuJoCo launcher not found.")),console.log(chalk.gray(" Get it from: https://github.com/newaipanda/ICOA_CLI/blob/main/panda/mujoco-launcher.py")),void console.log(chalk.gray(" Or use the sandbox-vla docker image (Phase 3)."));const r={baseline:"baseline",prompt_injected:"prompt_inj",patch_attacked:"patch",modality_confused:"confused"}[o]||"baseline";console.log(chalk.gray(` Launching MuJoCo viewer (scenario: ${r})...`)),console.log(chalk.gray(" Close the window or press ESC to return to learn mode.")),n("python3",[e,r,"--seconds","5"],{stdio:"inherit"}).on("exit",o=>{0!==o?console.log(chalk.yellow(` MuJoCo exited with code ${o} (install: pip install mujoco)`)):console.log(chalk.gray(" Returned from sim."))})}(o.simAction),void co()):(console.log(chalk.gray(" (sim only available on simulation cards)")),void co())}if("bookmark"===e){const o=eo.currentCard;return eo.bookmarks.includes(o)||eo.bookmarks.push(o),I(eo),console.log(chalk.gray(` ✓ Card ${o} bookmarked.`)),void co()}if("e"===e)return yo=eo.currentCard,console.log(chalk.gray(Io?" 描述问题(回车提交 / 空行取消):":" Describe the issue (Enter to submit / blank to cancel):")),void co();if("back"===e||"b"===e)return eo.currentCard>1&&(eo.currentCard-=1),uo=null,go=null,mo=null,I(eo),Lo(),void co();if(e.startsWith("card ")){const o=e.slice(5).trim(),r=parseInt(o,10);return!Number.isInteger(r)||r<1||r>X.totalCards?(console.log(chalk.yellow(` Card number must be 1..${X.totalCards}. Try: `)+chalk.bold.green("card 1")),void co()):(eo.currentCard=r,uo=null,go=null,mo=null,I(eo),Lo(),void co())}if(null!==mo&&["y","yes","n","no"].includes(e)){const o=Mo(mo);if(o&&"knowledge"===o.type&&o.check){const r=e.startsWith("y")?"y":"n",n=r===o.check.answer,t=Date.now()-Co;T(eo,o.number),x(eo,o.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),I(eo);const{renderCheckFeedback:l}=await import("../lib/learn-render.js");if(l(o,r,n,!1),$o.push(y(K,{card_number:o.number,event_type:"check_answered",check_answer:r,check_correct:n,time_on_card_ms:t}).catch(()=>{})),mo=null,o.solution&&o.solution.length>0)return void co();if(eo.currentCard<X.totalCards){const o=eo.currentCard;eo.currentCard+=1,I(eo),Po(o)?Oo():Lo(),co()}else U(X,eo),co();return}}if(null!==uo&&["a","b","c","d"].includes(e)){const o=Mo(uo);if(o&&"mcq"===o.type){const r=e.toUpperCase(),n=r===o.answer;return _(eo,o.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),T(eo,o.number),I(eo),S(o,r,n,eo),$o.push(y(K,{card_number:o.number,event_type:"mcq_answered",mcq_answer:r,mcq_correct:n}).catch(()=>{})),uo=null,void(n?eo.currentCard<X.totalCards?(eo.currentCard+=1,I(eo),Lo()):(U(X,eo),co()):co())}}if(null!==go){if("done"===e){const o=Mo(go);if(o&&"practical"===o.type)return M(eo,o.number),T(eo,o.number),I(eo),j(o),go=null,void co()}if("skip"===e)return T(eo,go),I(eo),console.log(chalk.gray(" Skipped (counts as not completed).")),console.log(),go=null,void co()}if("ok"===e||"next"===e||"continue"===e||"n"===e){if(null!==uo)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void co();if(null!==go)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void co();if(null!==mo)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void co();const o=eo.currentCard;return eo.currentCard+=1,I(eo),eo.currentCard>X.totalCards?U(X,eo):Po(o)?Oo():Lo(),void co()}if(null!==mo)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void co();if(null!==uo)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void co();if(null!==go)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void co();console.log(chalk.gray(" Unknown command. Try: ")+chalk.white("ok")+chalk.gray(" / ")+chalk.white("status")+chalk.gray(" / ")+chalk.white("card N")+chalk.gray(" / ")+chalk.white("/ai")+chalk.gray(" / ")+chalk.white("quit")),co()}else if($o.length>0&&await Promise.race([Promise.allSettled($o),new Promise(o=>setTimeout(o,5e3))]),console.log(),console.log(chalk.gray(" Saved. See you next session.")),console.log(chalk.gray(" Streak: ")+chalk.yellow(`🔥 ${eo.streakDays} day(s)`)),console.log(),to){ao.removeAllListeners("line"),io();for(const o of lo)ao.on("line",o);ao.prompt(),_o()}else ao.removeAllListeners("line"),ao.close();else if($o.length>0&&await Promise.race([Promise.allSettled($o),new Promise(o=>setTimeout(o,3e3))]),ao.removeAllListeners("line"),io(),to||!xo){const{returnToMainMenu:o}=await import("../lib/menu-nav.js");o(to?ao:void 0),_o()}else ao.close()}else co()}};ao.on("line",o=>{jo(o).catch(o=>{console.log(),console.log(chalk.red(" ⚠ Something went wrong handling that input.")),console.log(chalk.gray(` ${o instanceof Error?o.message:String(o)}`)),console.log(chalk.gray(" Progress is saved. Type ")+chalk.bold.green("ok")+chalk.gray(" to continue or ")+chalk.bold.green("quit")+chalk.gray(" to exit.")),co()})}),to||ao.on("close",async()=>{$o.length>0&&await Promise.race([Promise.allSettled($o),new Promise(o=>setTimeout(o,5e3))]),xo?_o():process.exit(0)});try{Lo(),co()}catch{if(Y("Could not render this card — the curriculum may be malformed."),to){ao.removeAllListeners("line"),io();for(const o of lo)ao.on("line",o);ao.prompt()}else ao.close();_o()}await To}),H.command("learn-pull [id]").description("Pre-download learn curricula to ~/.icoa/learn-cache/ for offline use (default: all)").action(async o=>{const e=o?[o.trim()]:["LEARNDEMO01","AI4CTFDEMO01","CTF4AIDEMO01","ai4ctf-96","ai4ctf-360","ctf4ai-96","ctf4ai-360","ctf4eai-96","ctf4eai-360","embodied-ai-100","embodied-ai-480"];console.log(),console.log(chalk.bold.cyan(" ICOA learn-pull — fetching curricula for offline use")),console.log(chalk.gray(" Server: https://practice.icoa2026.au")),console.log();let r=0,n=0,t=0;for(const o of e){process.stdout.write(` ${o.padEnd(22)} `);const e=Date.now(),l=await g(o),a=Date.now()-e;if(l){const o=l.cards,e=Array.isArray(o)?o.length:0,n=JSON.stringify(l).length;t+=n,console.log(chalk.green("✓ ")+chalk.gray(`${e.toString().padStart(3)} cards · ${(n/1024).toFixed(1).padStart(6)} KB · ${a.toString().padStart(4)} ms`)),r++}else console.log(chalk.red("✗ failed (server unreachable or unknown id)")),n++}console.log(),console.log(chalk.gray(" Summary: ")+chalk.green.bold(`${r} cached`)+chalk.gray(` · ${n} failed · total ${(t/1024).toFixed(0)} KB`)),console.log(chalk.gray(" Cache: ")+chalk.cyan("~/.icoa/learn-cache/")),console.log()})}
1
+ import chalk from"chalk";import{Interface as e,createInterface as o}from"node:readline";import{execSync as r,spawn as n}from"node:child_process";import{getMainRl as t}from"../lib/main-rl.js";import{ai4ioaiPromptLabel as l,handleAi4ioaiMessage as a,startAi4ioaiChat as s}from"./ai4ioai.js";import{existsSync as i}from"node:fs";import{dirname as c,join as u}from"node:path";import{fileURLToPath as d}from"node:url";import{loadCurriculumById as g,validateEAToken as m,syncProgress as y,syncCardFeedback as h}from"../lib/learn-curricula.js";import{normalizeTokenBody as f}from"../lib/token-format.js";import{streamLearnTutor as p,submitPqs as w,viewPqsTop as b,viewHonors as k}from"../lib/learn-ai.js";import{getConfig as v,saveConfig as C}from"../lib/config.js";import{loadLearnState as A,saveLearnState as $,newLearnState as I,updateStreak as T,markCardComplete as E,recordMCQ as _,recordCheck as x,markPracticalComplete as M,addAchievement as P}from"../lib/learn-state.js";import{renderWelcome as L,renderKnowledgeCard as D,renderMCQCard as S,renderMCQFeedback as q,renderPracticalCard as O,renderPracticalSuccess as R,renderSimDemoCard as j,renderMilestone as N,renderArenaTaskCard as W,renderComingSoonCard as B,renderStatus as F,renderPhaseComplete as z,renderCurriculumComplete as U,setLearnAiEnabled as J,setLearnPqsViewCards as K,renderHonorWall as Q}from"../lib/learn-render.js";import{printError as V}from"../lib/ui.js";export function registerLearnCommand(Y){Y.command("learn [token]").description("Enter learn mode (free <TRACK>DEMO01 demo, or team-issued token for full curriculum)").action(async Y=>{Y&&Y.trim()||(console.log(),console.log(chalk.gray(" No token given — starting free 11-card demo (")+chalk.bold.green("LEARNDEMO01")+chalk.gray(").")),console.log(chalk.gray(" Full curriculum: ")+chalk.bold.yellow("learn <your token>")+chalk.gray(" (2 letters + 8 chars) — from your country team leader.")),console.log(),Y="LEARNDEMO01");let Z=Y.trim().toUpperCase(),H=null;const X=/^[A-Z0-9]+DEMO01$/i.test(Z),G=/^[A-Z]{2}[A-Z0-9]{8}$/i.test(Z);let ee=!1;if(X)console.log(),console.log(chalk.gray(" Loading demo curriculum...")),H=await g(Z,Z);else if(G){console.log(),console.log(chalk.gray(" Validating token..."));let e=await m(Z);if(!e.ok){const o=f(Z);if(o!==Z){const r=await m(o);r.ok&&(console.log(chalk.yellow(` Corrected look-alike characters: ${Z} → ${o}`)),e=r,Z=o)}}if(!e.ok)return V(`Token validation failed: ${e.message}`),console.log(),"device_bound"===e.code?(console.log(chalk.gray(" This learn token is bound to the first computer it was used on,")),console.log(chalk.gray(" and this is a different computer.")),console.log(),console.log(chalk.gray(" If you changed computers or reinstalled, ask the coordinator who")),console.log(chalk.gray(" issued your token to move it to this device, then try again."))):(console.log(chalk.gray(" Possible causes:")),console.log(chalk.gray(" · Token expired or revoked")),console.log(chalk.gray(" · Network down (check connection)")),console.log(chalk.gray(" · Typo in token (check 0/O, 1/I/L, V/U)"))),void console.log();if(console.log(chalk.green(` ✓ Token valid · curriculum: ${e.curriculumId} · status: ${e.status}`)+(!0===e.aiEnabled?chalk.bold.magenta(" · ⭐ AI tutor"):"")),ee=!0===e.aiEnabled,console.log(chalk.gray(" Loading curriculum… (downloads ~1 MB; up to a minute on slow venue Wi-Fi — please wait)")),H=await g(e.curriculumId||"LEARNDEMO01",Z),!H)return V(`Could not open a learn curriculum for token '${Z}'.`),console.log(),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${Z}`)+chalk.gray(" instead.")),console.log(chalk.gray(" Otherwise: the full curriculum is ~3 MB; on a slow link the download")),console.log(chalk.gray(" can take a while — stay on a stable connection and run ")+chalk.bold.yellow(`learn ${Z}`)+chalk.gray(" again")),console.log(chalk.gray(" (it caches after the first load). If it keeps failing, check ")+chalk.cyan("https://practice.icoa2026.au")+chalk.gray(" is reachable.")),void console.log()}if(!H)return V(`Unknown learn token: ${Z}`),console.log(),console.log(chalk.gray(" Available tokens:")),console.log(chalk.gray(" ")+chalk.bold.green("<TRACK>DEMO01")+chalk.gray(" free demo, anyone can use — e.g. ")+chalk.bold.green("LEARNDEMO01")),console.log(chalk.gray(" ")+chalk.bold.yellow("2 letters + 8 chars")+chalk.gray(" full curriculum (issued by team leader)")),console.log(),console.log(chalk.gray(" Every track has a free demo — ask for its name if you were not given one.")),console.log(),console.log(chalk.gray(" To get a full curriculum, email ")),console.log(chalk.gray(" ")+chalk.cyan("australia@icoa2026.au")+chalk.gray(" or ask your country's team leader.")),void console.log();let oe=A(),re=!1;try{oe&&oe.token===Z?T(oe):(oe=I(Z,H.id,H.totalCards),re=!0),$(oe),L(H,oe,re)}catch{return V("Could not open this curriculum — it may be malformed or not a learn curriculum."),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${Z}`)+chalk.gray(".")),void console.log()}const ne=t(),te=null!==ne,le=te?ne.listeners("line").slice():[];te&&ne.removeAllListeners("line");const ae=te?ne:o({input:process.stdin,output:process.stdout,terminal:!0}),se=te?ae.prompt:null;te&&(ae.prompt=e.prototype.prompt);const ie=()=>{te&&se&&(ae.prompt=se)},ce=()=>{ae.setPrompt(fe?chalk.bold.green(`icoa ${l()}> `):chalk.bold.cyan(ye?"icoa learn ai> ":"icoa learn> ")),ae.prompt()};let ue=null,de=null,ge=null,me=null,ye=!1,he=!1,fe=!1;const pe=new Map,we=()=>{let e=pe.get(oe.currentCard);return e||(e=[],pe.set(oe.currentCard,e)),e};let be=!1;const ke=new Set;let ve=!1,Ce=0;const Ae=(v().language||"en").toLowerCase().startsWith("zh");J(ee),K(ke);const $e=[];let Ie=null;const Te=new Promise(e=>{Ie=e}),Ee=()=>{const e=Ie;Ie=null,e?.()},_e="1"===process.env.ICOA_LEARN_EMBEDDED,xe=e=>H.cards.find(o=>o.number===e),Me=e=>{const o=H.modules.find(o=>o.cardRange[1]===e);if(!o)return!1;const r=H.modules.find(e=>e.number===o.number+1);return z(H,oe,o,r),!0},Pe=()=>{ve=!0,console.log(chalk.gray(Ae?" 按 Enter 进入下一阶段…":" Press Enter to continue to the next phase…"))},Le=()=>{const e=xe(oe.currentCard);if(!e)return console.log(),console.log(chalk.gray(" No more cards in this curriculum.")),console.log(chalk.gray(" Type ")+chalk.bold.green("status")+chalk.gray(" for the dashboard or ")+chalk.bold.green("quit")+chalk.gray(" to exit.")),void console.log();switch(e.type){case"knowledge":D(e,H),e.check?(ge=e.number,Ce=Date.now()):(E(oe,e.number),$(oe));break;case"mcq":S(e,H),ue=e.number;break;case"practical":O(e,H),de=e.number;break;case"sim_demo":j(e,H),E(oe,e.number),$(oe);break;case"milestone":N(e,H),P(oe,e.badge),E(oe,e.number),$(oe);break;case"arena_task":W(e,H),E(oe,e.number),$(oe);break;case"coming_soon":B(e,H),E(oe,e.number),$(oe)}},De=()=>{Z.endsWith("DEMO01")?console.log(chalk.yellow(Ae?" AI 导师需要开通 AI 的正式课程 token(demo 是共享的,没有 AI 额度)。":" The AI tutor needs an AI-enabled curriculum token — demos are shared and carry no AI budget.")):(console.log(chalk.yellow(Ae?" 此 token 未开通 AI 导师(高级功能)。":" This token does not include the AI tutor (premium).")),console.log(chalk.gray(Ae?" 找你的领队 / 协调人换开通 AI 的 token。":" Ask your team leader / coordinator for an AI-enabled token."))),console.log()},Se=async(e,o)=>{if(ee){he=!0;try{console.log(),console.log(chalk.gray("deep"===o?Ae?" 🤖 AI 导师(深度思考中,思考过程为灰色小字)…":" 🤖 AI tutor (deepthink — reasoning shown in gray)…":Ae?" 🤖 AI 导师(快速模式)…":" 🤖 AI tutor (quick mode)…")),console.log();let r=!1,n=!1;const t=await p({token:Z,cardNumber:oe.currentCard,question:e,mode:o,history:we().slice(-8)},e=>{"reasoning"===e.kind?(r||(process.stdout.write(" "),r=!0),process.stdout.write(chalk.dim(e.text.replace(/\n/g,"\n ")))):(n||(process.stdout.write(r?"\n\n ":" "),n=!0),process.stdout.write(chalk.white(e.text.replace(/\n/g,"\n "))))});(r||n)&&console.log(),t.ok?t.offtopic?(console.log(),console.log(chalk.yellow(Ae?" (被判定与本卡主题无关 — 跑题的问题不会被回答,并会拉低本卡的提问评分)":" (judged unrelated to this card — off-topic questions aren't answered and will lower this card's prompt score)"))):(we().push({role:"user",content:e},{role:"assistant",content:t.answer}),null!=t.spent&&null!=t.cap&&t.spent/t.cap>.8&&console.log(chalk.gray(` (${Ae?"AI 额度":"AI budget"}: ${Math.round(t.spent/1e3)}k / ${Math.round(t.cap/1e3)}k tokens)`))):(console.log(),console.log(chalk.yellow(` ${t.message||(Ae?"AI 导师暂不可用。":"AI tutor unavailable.")}`))),null!==ge&&console.log(chalk.gray(Ae?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()}finally{he=!1}}else De()},qe=e=>e.match(/.{1,64}(\s|$)/g)?.map(e=>e.trim())??[e],Oe=async e=>{const o=e.trim().toLowerCase();if(!he){if(null!==me){const o=e.trim(),r=me;if(me=null,""===o)console.log(chalk.gray(Ae?" 已取消(未提交)。":" Cancelled (nothing submitted)."));else{const e=Ce>0?Date.now()-Ce:void 0;await h(Z,{curriculum_id:H.id,card_number:r,feedback_text:o,time_on_card_ms:e})?console.log(chalk.green(Ae?" ✓ 已收到,谢谢帮助完善这张卡。":" ✓ Thanks — your report was recorded.")):console.log(chalk.yellow(Ae?" ⚠ 未能送达服务器 — 这条报告没有保存,请稍后再按 e 重试。":" ⚠ Could not reach the server — this report was NOT saved. Try e again later."))}return console.log(),void ce()}if(be){be=!1;const o=e.trim().toLowerCase();return"y"===o||"yes"===o?await(async()=>{he=!0;try{console.log(),console.log(chalk.gray(Ae?" 📊 提交中 — Panda 正在评审你的提问(通常几十秒,最长一两分钟,请稍候)…":" 📊 Submitting — Panda is reviewing how you asked (usually well under two minutes)…"));const e=await w({token:Z,cardNumber:oe.currentCard});if(console.log(),!e.ok)return console.log(chalk.yellow(` ${e.message||(Ae?"评审暂不可用。":"Review unavailable.")}`)),void console.log();(e=>{const o="green"===e.tier?chalk.bold.green:"blue"===e.tier?chalk.bold.cyan:"yellow"===e.tier?chalk.bold.yellow:chalk.bold.red;if(console.log(chalk.gray(" ─────────────────────────────────────────")),console.log(chalk.white(Ae?" 提问评分 ":" Prompt score ")+o(`${e.score??0} / 100`)+chalk.gray(" "+(e=>{switch(e){case"green":return"🟢 "+(Ae?"你把 AI 当作被审视的研究伙伴":"you used the AI as a research partner");case"blue":return"🔵 "+(Ae?"你把 AI 当作实验助手":"you used the AI as a lab assistant");case"yellow":return"🟡 "+(Ae?"你把 AI 当作讲师":"you used the AI as a lecturer");default:return"🔴 "+(Ae?"你把 AI 当作答案机":"you used the AI as an answer machine")}})(e.tier))),e.manipulation&&console.log(chalk.red(Ae?" ⚠ 检测到试图操纵评分,分数已按规则大幅降低":" ⚠ an attempt to manipulate the score was detected — the score was reduced sharply")),null!=e.rank&&null!=e.entrants){const o=!0===e.inTop3,r=Ae?` 本卡排名 #${e.rank} / ${e.entrants}`:` Rank on this card: #${e.rank} of ${e.entrants}`;console.log((o?chalk.bold.green(r):chalk.white(r))+(o?chalk.bold.magenta(Ae?" — 进入 TOP 3!输入 /view 看另两名的提问":" — TOP 3! type /view to read the other top prompts"):""))}if(console.log(chalk.gray(Ae?` 第 ${e.submissionIndex??"?"} / 2 次提交 · 剩余 ${e.submissionsLeft??0} 次(可先追加 /ai 对话再交)`:` Attempt ${e.submissionIndex??"?"} of 2 · ${e.submissionsLeft??0} left (extend the /ai conversation before your next one)`)),e.feedback){console.log();for(const o of qe(e.feedback))console.log(chalk.white(" "+o))}e.improve&&(console.log(),console.log(chalk.cyan(Ae?" 改进提问: ":" Ask better: ")+chalk.gray(e.improve))),console.log(chalk.gray(" ─────────────────────────────────────────")),null!=e.spent&&null!=e.cap&&e.spent/e.cap>.8&&console.log(chalk.gray(` (${Ae?"AI 额度":"AI budget"}: ${Math.round(e.spent/1e3)}k / ${Math.round(e.cap/1e3)}k tokens)`)),console.log()})(e),!0===e.inTop3?ke.add(oe.currentCard):ke.delete(oe.currentCard);const o=await k({token:Z,curriculumId:H.id,cardNumber:oe.currentCard});o.ok&&o.totals&&o.chapters&&(Q({totals:o.totals,chapters:o.chapters,floor:o.floor}),console.log())}finally{he=!1}})():(console.log(chalk.gray(Ae?" 已取消(未提交,不消耗次数)。":" Cancelled — nothing submitted, no attempt used.")),console.log()),null!==ge&&(console.log(chalk.gray(Ae?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()),void ce()}if(ve)return ve=!1,Le(),void ce();if(o){if(fe)return"exit"===await a(e.trim())&&(fe=!1),void ce();if(ye){if(["q","quit","exit","back","b","menu"].includes(o))return ye=!1,console.log(chalk.gray(Ae?" 已退出 AI 导师,回到卡片。":" Left the AI tutor — back to the card.")),console.log(),void ce();const r=e.trim().startsWith("!"),n=r?e.trim().slice(1).trim():e.trim();return n&&await Se(n,r?"fast":"deep"),void ce()}if("tmux"===o||o.startsWith("tmux ")){try{r("command -v tmux",{stdio:"ignore"})}catch{return console.log(chalk.yellow(" tmux is not installed yet.")),console.log(chalk.gray(" Install it with ")+chalk.bold.cyan("darwin"===process.platform?"brew install tmux":"sudo apt install tmux")+chalk.gray(Ae?" — 或 quit 回到 icoa> 后运行 ":" — or quit to icoa> and run ")+chalk.bold.cyan("env setup")+chalk.gray(".")),console.log(),void ce()}return await new Promise(o=>{const r=process.stdin,t=!!r.isTTY&&!!r.isRaw;if(ae.pause(),r.isTTY&&"function"==typeof r.setRawMode)try{r.setRawMode(!1)}catch{}const l=n(e.trim(),{shell:!0,stdio:"inherit"}),a=()=>{if(r.isTTY&&"function"==typeof r.setRawMode&&t)try{r.setRawMode(!0)}catch{}ae.resume(),o()};l.on("close",a),l.on("error",a)}),console.log(),void ce()}if("ai4ioai"===o||"/ai4ioai"===o)return await s()&&(fe=!0),void ce();if("/ai"===o||"/ai!"===o||o.startsWith("/ai ")||o.startsWith("/ai! ")){if(!ee)return De(),void ce();const r=o.startsWith("/ai!"),n=e.trim().slice(r?4:3).trim();if(!n){ye=!0;const e=xe(oe.currentCard),o=e?e.title??e.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 🤖 AI ")+chalk.bold.white(Ae?"AI 导师":"AI tutor")+(o?chalk.gray(Ae?` — 主题「${o}」`:` — topic “${o}”`):"")),console.log(chalk.gray(Ae?" 直接输入问题(默认深度思考) · ! 开头 = 快答 · q = 返回卡片":" Type a question (deepthink by default) · prefix ! = quick answer · q = back to card")),console.log(),void ce()}return await Se(n,r?"fast":"deep"),void ce()}if("/submit"===o||o.startsWith("/submit ")||"/research"===o||o.startsWith("/research ")){if(!ee)return De(),void ce();const e=xe(oe.currentCard),o=e?e.title??e.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 📊 ")+chalk.bold.white(Ae?"Panda 提问评审":"Panda's prompt review")+(o?chalk.gray(Ae?` — 主题「${o}」`:` — topic “${o}”`):"")),console.log(chalk.gray(Ae?" 评的是你在本卡上「怎么问 AI」— 你与 /ai 导师的整段对话会被":" It scores HOW you asked the AI on this card — your whole /ai")),console.log(chalk.gray(Ae?" 整体评审,给出分数、名次和改进建议。":" conversation is reviewed for a score, a rank, and advice.")),console.log(chalk.gray(Ae?" 每张卡最多 2 次评分;评出的最好成绩进本卡 Top-3 榜。":" Max 2 graded attempts per card; your best enters the card’s top-3 board.")),console.log(),console.log(chalk.white(Ae?" 确认提交本卡对话评分?":" Submit this card’s conversation for grading?")+chalk.gray(Ae?" y = 提交 · 其他 = 取消":" y = submit · anything else = cancel")),console.log(),be=!0,void ce()}if("/view"===o)return ee?(await(async()=>{he=!0;try{console.log();const e=await b({token:Z,cardNumber:oe.currentCard});if(!e.ok)return console.log(chalk.yellow(` ${e.message||(Ae?"暂时无法查看。":"View unavailable right now.")}`)),"not_top3"===e.code&&ke.delete(oe.currentCard),void console.log();null!=e.myRank&&e.myRank<=3&&ke.add(oe.currentCard),console.log(chalk.bold.white(Ae?` 🏆 本卡 Top-3 提问 — 第 ${oe.currentCard} 张 · ${e.entrants??0} 人上榜`:` 🏆 Top-3 prompts — card ${oe.currentCard} · ${e.entrants??0} entrant(s)`));for(const o of e.top3??[]){const e="green"===o.tier?"🟢":"blue"===o.tier?"🔵":"yellow"===o.tier?"🟡":"🔴";console.log(),console.log(chalk.bold.cyan(` ── Rank ${o.rank}`)+(o.me?chalk.bold.magenta(" (me)"):"")+chalk.white(` · ${o.score}`)+` ${e}`),o.questions.forEach((e,o)=>{const r=qe(e);console.log(chalk.gray(` Q${o+1} `)+chalk.white(r[0]??""));for(const e of r.slice(1))console.log(chalk.white(" "+e))}),0===o.questions.length&&console.log(chalk.gray(Ae?" (无可显示的提问)":" (no questions to show)"))}console.log()}finally{he=!1}})(),void ce()):(De(),void ce());if("/honors"===o||"honors"===o)return X?(console.log(),console.log(chalk.gray(Ae?" 荣誉榜属于正式 learn token — 用团队签发的 token 进来即可查看和参赛。":" The honor board belongs to full learn tokens — join with a team-issued token to view and compete.")),console.log(),void ce()):(await(async()=>{he=!0;try{console.log();const e=await k({token:Z,curriculumId:H.id,cardNumber:oe.currentCard});if(!e.ok||!e.totals||!e.chapters)return console.log(chalk.yellow(` ${e.message||(Ae?"荣誉榜暂时不可用。":"Honor board unavailable right now.")}`)),void console.log();if(console.log(chalk.bold.white(Ae?` 🏆 本卡实时 Top-3 — 第 ${oe.currentCard} 张`:` 🏆 Live top-3 — card ${oe.currentCard}`)),e.board&&0!==e.board.length)for(const o of e.board){const e="green"===o.tier?"🟢":"blue"===o.tier?"🔵":"yellow"===o.tier?"🟡":"🔴";console.log(chalk.white(` ${o.icon} `)+chalk.bold.cyan(`#${o.rank}`)+chalk.white(` · ${o.score} `)+e+(o.me?chalk.bold.magenta(" (me)"):""))}else console.log(chalk.gray(Ae?" (本卡还没有人上榜)":" (no one has medaled on this card yet)"));console.log(),Q({totals:e.totals,chapters:e.chapters,floor:e.floor}),console.log()}finally{he=!1}})(),void ce());if("menu"!==o&&"menu confirm"!==o)if("quit"!==o&&"exit"!==o&&"q"!==o){if("status"===o)return F(H,oe),void ce();if("lang"===o||o.startsWith("lang ")){const e=o.slice(4).trim();if(!e)return console.log(chalk.gray(" 当前语言 / current: ")+chalk.white(v().language||"en")+chalk.gray(" · ")+chalk.bold.green("lang zh")+chalk.gray(" / ")+chalk.bold.green("lang en")),void ce();const r=H.cards.some(e=>null!=e._zh),n=e.toLowerCase().startsWith("zh")?"zh":e.toLowerCase(),t="en"===n||"zh"===n&&r,l=t?n:"en";return C({language:l}),t?console.log(chalk.green(" ✓ ")+chalk.gray("语言 / language → ")+chalk.white(l)):console.log(chalk.yellow(" ! ")+chalk.gray(`${e} 本课暂无翻译,已用英文 / not available — using English`)),ge=null,ue=null,de=null,me=null,ve=!1,be=!1,Le(),void ce()}if("sim"===o){const e=xe(oe.currentCard);return e&&"sim_demo"===e.type?(function(e){const o=function(){const e=c(d(import.meta.url)),o=[u(e,"..","..","panda","mujoco-launcher.py"),u(e,"..","..","..","panda","mujoco-launcher.py")];for(const e of o)if(i(e))return e;return null}();if(!o)return console.log(chalk.yellow(" MuJoCo launcher not found.")),console.log(chalk.gray(" Get it from: https://github.com/newaipanda/ICOA_CLI/blob/main/panda/mujoco-launcher.py")),void console.log(chalk.gray(" Or use the sandbox-vla docker image (Phase 3)."));const r={baseline:"baseline",prompt_injected:"prompt_inj",patch_attacked:"patch",modality_confused:"confused"}[e]||"baseline";console.log(chalk.gray(` Launching MuJoCo viewer (scenario: ${r})...`)),console.log(chalk.gray(" Close the window or press ESC to return to learn mode.")),n("python3",[o,r,"--seconds","5"],{stdio:"inherit"}).on("exit",e=>{0!==e?console.log(chalk.yellow(` MuJoCo exited with code ${e} (install: pip install mujoco)`)):console.log(chalk.gray(" Returned from sim."))})}(e.simAction),void ce()):(console.log(chalk.gray(" (sim only available on simulation cards)")),void ce())}if("bookmark"===o){const e=oe.currentCard;return oe.bookmarks.includes(e)||oe.bookmarks.push(e),$(oe),console.log(chalk.gray(` ✓ Card ${e} bookmarked.`)),void ce()}if("e"===o)return me=oe.currentCard,console.log(chalk.gray(Ae?" 描述问题(回车提交 / 空行取消):":" Describe the issue (Enter to submit / blank to cancel):")),void ce();if("back"===o||"b"===o)return oe.currentCard>1&&(oe.currentCard-=1),ue=null,de=null,ge=null,$(oe),Le(),void ce();if(o.startsWith("card ")){const e=o.slice(5).trim(),r=parseInt(e,10);return!Number.isInteger(r)||r<1||r>H.totalCards?(console.log(chalk.yellow(` Card number must be 1..${H.totalCards}. Try: `)+chalk.bold.green("card 1")),void ce()):(oe.currentCard=r,ue=null,de=null,ge=null,$(oe),Le(),void ce())}if(null!==ge&&["y","yes","n","no"].includes(o)){const e=xe(ge);if(e&&"knowledge"===e.type&&e.check){const r=o.startsWith("y")?"y":"n",n=r===e.check.answer,t=Date.now()-Ce;E(oe,e.number),x(oe,e.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),$(oe);const{renderCheckFeedback:l}=await import("../lib/learn-render.js");if(l(e,r,n,!1),$e.push(y(Z,{card_number:e.number,event_type:"check_answered",check_answer:r,check_correct:n,time_on_card_ms:t}).catch(()=>{})),ge=null,e.solution&&e.solution.length>0)return void ce();if(oe.currentCard<H.totalCards){const e=oe.currentCard;oe.currentCard+=1,$(oe),Me(e)?Pe():Le(),ce()}else U(H,oe),ce();return}}if(null!==ue&&["a","b","c","d"].includes(o)){const e=xe(ue);if(e&&"mcq"===e.type){const r=o.toUpperCase(),n=r===e.answer;return _(oe,e.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),E(oe,e.number),$(oe),q(e,r,n,oe),$e.push(y(Z,{card_number:e.number,event_type:"mcq_answered",mcq_answer:r,mcq_correct:n}).catch(()=>{})),ue=null,void(n?oe.currentCard<H.totalCards?(oe.currentCard+=1,$(oe),Le()):(U(H,oe),ce()):ce())}}if(null!==de){if("done"===o){const e=xe(de);if(e&&"practical"===e.type)return M(oe,e.number),E(oe,e.number),$(oe),R(e),de=null,void ce()}if("skip"===o)return E(oe,de),$(oe),console.log(chalk.gray(" Skipped (counts as not completed).")),console.log(),de=null,void ce()}if("ok"===o||"next"===o||"continue"===o||"n"===o){if(null!==ue)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void ce();if(null!==de)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void ce();if(null!==ge)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void ce();const e=oe.currentCard;return oe.currentCard+=1,$(oe),oe.currentCard>H.totalCards?U(H,oe):Me(e)?Pe():Le(),void ce()}if(null!==ge)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void ce();if(null!==ue)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void ce();if(null!==de)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void ce();console.log(chalk.gray(" Unknown command. Try: ")+chalk.white("ok")+chalk.gray(" / ")+chalk.white("status")+chalk.gray(" / ")+chalk.white("card N")+chalk.gray(" / ")+chalk.white("/ai")+chalk.gray(" / ")+chalk.white("quit")),ce()}else if($e.length>0&&await Promise.race([Promise.allSettled($e),new Promise(e=>setTimeout(e,5e3))]),console.log(),console.log(chalk.gray(" Saved. See you next session.")),console.log(chalk.gray(" Streak: ")+chalk.yellow(`🔥 ${oe.streakDays} day(s)`)),console.log(),te){ae.removeAllListeners("line"),ie();for(const e of le)ae.on("line",e);ae.prompt(),Ee()}else ae.removeAllListeners("line"),ae.close();else if($e.length>0&&await Promise.race([Promise.allSettled($e),new Promise(e=>setTimeout(e,3e3))]),ae.removeAllListeners("line"),ie(),te||!_e){const{returnToMainMenu:e}=await import("../lib/menu-nav.js");e(te?ae:void 0),Ee()}else ae.close()}else ce()}};ae.on("line",e=>{Oe(e).catch(e=>{console.log(),console.log(chalk.red(" ⚠ Something went wrong handling that input.")),console.log(chalk.gray(` ${e instanceof Error?e.message:String(e)}`)),console.log(chalk.gray(" Progress is saved. Type ")+chalk.bold.green("ok")+chalk.gray(" to continue or ")+chalk.bold.green("quit")+chalk.gray(" to exit.")),ce()})}),te||ae.on("close",async()=>{$e.length>0&&await Promise.race([Promise.allSettled($e),new Promise(e=>setTimeout(e,5e3))]),_e?Ee():process.exit(0)});try{Le(),ce()}catch{if(V("Could not render this card — the curriculum may be malformed."),te){ae.removeAllListeners("line"),ie();for(const e of le)ae.on("line",e);ae.prompt()}else ae.close();Ee()}await Te}),Y.command("learn-pull [id]").description("Pre-download learn curricula to ~/.icoa/learn-cache/ for offline use (default: all)").action(async e=>{const o=e?[e.trim()]:["LEARNDEMO01","AI4CTFDEMO01","CTF4AIDEMO01","ai4ctf-96","ai4ctf-360","ctf4ai-96","ctf4ai-360","ctf4eai-96","ctf4eai-360","embodied-ai-100","embodied-ai-480"];console.log(),console.log(chalk.bold.cyan(" ICOA learn-pull — fetching curricula for offline use")),console.log(chalk.gray(" Server: https://practice.icoa2026.au")),console.log();let r=0,n=0,t=0;for(const e of o){process.stdout.write(` ${e.padEnd(22)} `);const o=Date.now(),l=await g(e),a=Date.now()-o;if(l){const e=l.cards,o=Array.isArray(e)?e.length:0,n=JSON.stringify(l).length;t+=n,console.log(chalk.green("✓ ")+chalk.gray(`${o.toString().padStart(3)} cards · ${(n/1024).toFixed(1).padStart(6)} KB · ${a.toString().padStart(4)} ms`)),r++}else console.log(chalk.red("✗ failed (server unreachable or unknown id)")),n++}console.log(),console.log(chalk.gray(" Summary: ")+chalk.green.bold(`${r} cached`)+chalk.gray(` · ${n} failed · total ${(t/1024).toFixed(0)} KB`)),console.log(chalk.gray(" Cache: ")+chalk.cyan("~/.icoa/learn-cache/")),console.log()})}
@@ -1 +1 @@
1
- const t={ai4ctf:"AI4CTFDEMO01",ctf4ai:"CTF4AIDEMO01",ctf4eai:"LEARNDEMO01","ai4ctf-12":"AI4CTFDEMO01","ctf4ai-12":"CTF4AIDEMO01","ctf4eai-12":"LEARNDEMO01",ctf4vla:"LEARNDEMO01",vla:"LEARNDEMO01",embodied:"LEARNDEMO01",ai4ctfdemo:"AI4CTFDEMO01",ctf4aidemo:"CTF4AIDEMO01",learndemo:"LEARNDEMO01"},e={1:"AI4CTFDEMO01",2:"CTF4AIDEMO01",3:"LEARNDEMO01"},o=/^[A-Z]{2}[A-Z0-9]{6,}$/,n=/^(AI4CTFDEMO\d+|CTF4AIDEMO\d+|LEARNDEMO\d+)$/;export function resolveLearnInput(i){const r=i.trim();if(!r)return{kind:"back"};const a=r.replace(/^\s*(?:[/>])?\s*(?:icoa\s+)?learn\s+/i,"").trim();if(!a)return{kind:"ambiguous",hint:"Type just the token (e.g. LEARNDEMO01) or one of: ai4ctf / ctf4ai / ctf4eai"};if(e[a])return{kind:"token",token:e[a]};const c=a.toLowerCase();if(t[c])return{kind:"token",token:t[c]};if("demo"===c||"demos"===c)return{kind:"ambiguous",hint:"Which demo? Type: 1 (ai4ctf) / 2 (ctf4ai) / 3 (ctf4eai)"};if(/\s/.test(a))return{kind:"unknown",hint:`Don't recognize "${r}". Try one of: ai4ctf / ctf4ai / ctf4eai (for the free demos), 1/2/3, or paste your AC/CA/EA/EI/IO/IA + 8 chars cohort token. Blank to go back.`};const f=a.toUpperCase();return n.test(f)||o.test(f)||/^[A-Z0-9]{8,}$/.test(f)?{kind:"token",token:f}:{kind:"unknown",hint:`Don't recognize "${r}". Try one of: ai4ctf / ctf4ai / ctf4eai (for the free demos), 1/2/3, or paste your AC/CA/EA/EI/IO/IA + 8 chars cohort token. Blank to go back.`}}
1
+ const t={ai4ctf:"AI4CTFDEMO01",ctf4ai:"CTF4AIDEMO01",ctf4eai:"LEARNDEMO01","ai4ctf-12":"AI4CTFDEMO01","ctf4ai-12":"CTF4AIDEMO01","ctf4eai-12":"LEARNDEMO01","ioai-jr":"IOAIJRDEMO01","ioai-jr-48":"IOAIJRDEMO01",ioaijr:"IOAIJRDEMO01",junior:"IOAIJRDEMO01",jr:"IOAIJRDEMO01",ioaijrdemo:"IOAIJRDEMO01",ctf4vla:"LEARNDEMO01",vla:"LEARNDEMO01",embodied:"LEARNDEMO01",ai4ctfdemo:"AI4CTFDEMO01",ctf4aidemo:"CTF4AIDEMO01",learndemo:"LEARNDEMO01"},e={1:"AI4CTFDEMO01",2:"CTF4AIDEMO01",3:"LEARNDEMO01"},i=/^[A-Z]{2}[A-Z0-9]{6,}$/,o=/^(AI4CTFDEMO\d+|CTF4AIDEMO\d+|LEARNDEMO\d+)$/;export function resolveLearnInput(n){const a=n.trim();if(!a)return{kind:"back"};const r=a.replace(/^\s*(?:[/>])?\s*(?:icoa\s+)?learn\s+/i,"").trim();if(!r)return{kind:"ambiguous",hint:"Type just the token (e.g. LEARNDEMO01) or one of: ai4ctf / ctf4ai / ctf4eai"};if(e[r])return{kind:"token",token:e[r]};const c=r.toLowerCase();if(t[c])return{kind:"token",token:t[c]};if("demo"===c||"demos"===c)return{kind:"ambiguous",hint:"Which demo? Type: 1 (ai4ctf) / 2 (ctf4ai) / 3 (ctf4eai)"};if(/\s/.test(r))return{kind:"unknown",hint:`Don't recognize "${a}". Try a track name (ai4ctf / ctf4ai / ctf4eai / junior), 1/2/3, a demo id like LEARNDEMO01, or paste your token (2 letters + 8 chars). Blank to go back.`};const E=r.toUpperCase();return o.test(E)||i.test(E)||/^[A-Z0-9]{8,}$/.test(E)?{kind:"token",token:E}:{kind:"unknown",hint:`Don't recognize "${a}". Try one of: ai4ctf / ctf4ai / ctf4eai (for the free demos), 1/2/3, or paste your AC/CA/EA/EI/IO/IA + 8 chars cohort token. Blank to go back.`}}