icoa-cli 2.19.399 → 2.19.400

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 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
+ import chalk from"chalk";import{Interface as e,createInterface as o}from"node:readline";import{execSync as t,spawn as n}from"node:child_process";import{getMainRl as r}from"../lib/main-rl.js";import{ai4ioaiPromptLabel as l,handleAi4ioaiMessage as s,startAi4ioaiChat as a}from"./ai4ioai.js";import{existsSync as i,mkdirSync as c,readFileSync as d,writeFileSync as u}from"node:fs";import{dirname as g,join as m}from"node:path";import{fileURLToPath as y}from"node:url";import{loadCurriculumById as h,validateEAToken as p,syncProgress as f,syncCardFeedback as w}from"../lib/learn-curricula.js";import{normalizeTokenBody as b}from"../lib/token-format.js";import{fetchMyScores as k,streamLearnTutor as $,submitPqs as v,viewPqsTop as C,viewHonors as A}from"../lib/learn-ai.js";import{contestReset as _,contestStart as I,contestSubmit as T,fetchGate as E,timeLeft as M}from"../lib/learn-contest.js";import{getConfig as x,saveConfig as P}from"../lib/config.js";import{loadLearnState as L,saveLearnState as S,newLearnState as D,updateStreak as O,markCardComplete as q,recordMCQ as R,recordCheck as j,markPracticalComplete as N,addAchievement as W}from"../lib/learn-state.js";import{renderWelcome as B,renderKnowledgeCard as F,renderMCQCard as z,renderMCQFeedback as U,renderPracticalCard as J,renderPracticalSuccess as Z,renderSimDemoCard as K,renderMilestone as H,renderArenaTaskCard as Q,renderComingSoonCard as V,renderStatus as Y,renderPhaseComplete as X,renderCurriculumComplete as G,setLearnAiEnabled as ee,setLearnPqsViewCards as oe,renderHonorWall as te}from"../lib/learn-render.js";import{printError as ne}from"../lib/ui.js";export function registerLearnCommand(re){re.command("learn [token]").description("Enter learn mode (free <TRACK>DEMO01 demo, or team-issued token for full curriculum)").action(async re=>{re&&re.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(),re="LEARNDEMO01");let le=re.trim().toUpperCase(),se=null;const ae=/^[A-Z0-9]+DEMO01$/i.test(le),ie=/^[A-Z]{2}[A-Z0-9]{8}$/i.test(le);let ce=!1,de=!1;if(ae)console.log(),console.log(chalk.gray(" Loading demo curriculum...")),se=await h(le,le);else if(ie){console.log(),console.log(chalk.gray(" Validating token..."));let e=await p(le);if(!e.ok){const o=b(le);if(o!==le){const t=await p(o);t.ok&&(console.log(chalk.yellow(` Corrected look-alike characters: ${le} → ${o}`)),e=t,le=o)}}if(!e.ok)return ne(`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"):"")),ce=!0===e.aiEnabled,de=!0===e.gated,console.log(chalk.gray(" Loading curriculum… (downloads ~1 MB; up to a minute on slow venue Wi-Fi — please wait)")),se=await h(e.curriculumId||"LEARNDEMO01",le),!se)return ne(`Could not open a learn curriculum for token '${le}'.`),console.log(),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${le}`)+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 ${le}`)+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(!se)return ne(`Unknown learn token: ${le}`),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 ue=L(),ge=!1;try{ue&&ue.token===le?O(ue):(ue=D(le,se.id,se.totalCards),ge=!0),S(ue),B(se,ue,ge)}catch{return ne("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 ${le}`)+chalk.gray(".")),void console.log()}const me=r(),ye=null!==me,he=ye?me.listeners("line").slice():[];ye&&me.removeAllListeners("line");const pe=ye?me:o({input:process.stdin,output:process.stdout,terminal:!0}),fe=ye?pe.prompt:null;ye&&(pe.prompt=e.prototype.prompt);const we=()=>{ye&&fe&&(pe.prompt=fe)},be=()=>{pe.setPrompt(Ie?chalk.bold.green(`icoa ${l()}> `):chalk.bold.cyan(Ae?"icoa learn ai> ":"icoa learn> ")),pe.prompt()};let ke=null,$e=null,ve=null,Ce=null,Ae=!1,_e=!1,Ie=!1;const Te=new Map,Ee=()=>{let e=Te.get(ue.currentCard);return e||(e=[],Te.set(ue.currentCard,e)),e};let Me=!1;const xe=new Set;let Pe=!1,Le=0;const Se=(x().language||"en").toLowerCase().startsWith("zh");ee(ce),oe(xe);const De=[];let Oe=null;const qe=new Promise(e=>{Oe=e}),Re=()=>{const e=Oe;Oe=null,e?.()},je="1"===process.env.ICOA_LEARN_EMBEDDED,Ne=e=>se.cards.find(o=>o.number===e);let We=null,Be=null;const Fe=!le.endsWith("DEMO01")&&/^[A-Z]{2}[A-Z0-9]{8}$/i.test(le),ze=async()=>{if(!Fe)return;const e=await E(le);e.ok&&e.data&&(We=e.data)},Ue=e=>{const o=se.modules.find(o=>e>=o.cardRange[0]&&e<=o.cardRange[1]);return o?o.number:1},Je=e=>{if(!de||!We)return null;const o=Ue(e);if(o<=We.unlocked_through_phase)return null;const t=We.phases.find(e=>e.phase===We.unlocked_through_phase),n=[chalk.yellow(Se?` 🔒 阶段 ${o} 未解锁 — 先完成阶段 ${We.unlocked_through_phase} 的核心任务:`:` 🔒 Phase ${o} is locked — finish phase ${We.unlocked_through_phase}'s core tasks first:`)];return t?.contest&&!t.contest.passed&&n.push(chalk.gray(Se?` ⚖ 判分赛: 最好 ${t.contest.best_ac}/${t.contest.n} AC,需 ${t.contest.need} — 输入 `:` ⚖ contest: best ${t.contest.best_ac}/${t.contest.n} AC, need ${t.contest.need} — type `)+chalk.bold.green("contest")),t?.research&&!t.research.passed&&n.push(chalk.gray(Se?` ⭐ 提问研究: ${t.research.good_cards}/${t.research.need_cards} 卡达到 ${t.research.min_score} 分 — 用 /ai 研究,再 /submit 评分`:` ⭐ research: ${t.research.good_cards}/${t.research.need_cards} cards at ${t.research.min_score}+ — research with /ai, then /submit`)),n.join("\n")},Ze=async e=>de?null===Je(e)?null:(await ze(),Je(e)):null;de&&await ze();const Ke=async e=>{const o=se.modules.find(o=>o.cardRange[1]===e);if(!o)return!1;const t=se.modules.find(e=>e.number===o.number+1);let n;if(ce){const e=await k({token:le,cardLo:o.cardRange[0],cardHi:o.cardRange[1]}).catch(()=>({ok:!1,scores:void 0}));n=e.ok?e.scores??[]:void 0}Fe&&await ze();const r=We?.phases.find(e=>e.phase===o.number);return X(se,ue,o,t,n,r),!0},He=()=>{Pe=!0,console.log(chalk.gray(Se?" 按 Enter 进入下一阶段…":" Press Enter to continue to the next phase…"))},Qe=()=>{const e=Ne(ue.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":F(e,se),e.check?(ve=e.number,Le=Date.now()):(q(ue,e.number),S(ue));break;case"mcq":z(e,se),ke=e.number;break;case"practical":J(e,se),$e=e.number;break;case"sim_demo":K(e,se),q(ue,e.number),S(ue);break;case"milestone":H(e,se),W(ue,e.badge),q(ue,e.number),S(ue);break;case"arena_task":Q(e,se),q(ue,e.number),S(ue);break;case"coming_soon":V(e,se),q(ue,e.number),S(ue)}},Ve=()=>{le.endsWith("DEMO01")?console.log(chalk.yellow(Se?" 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(Se?" 此 token 未开通 AI 导师(高级功能)。":" This token does not include the AI tutor (premium).")),console.log(chalk.gray(Se?" 找你的领队 / 协调人换开通 AI 的 token。":" Ask your team leader / coordinator for an AI-enabled token."))),console.log()},Ye=async(e,o)=>{if(ce){_e=!0;try{console.log(),console.log(chalk.gray("deep"===o?Se?" 🤖 AI 导师(深度思考中,思考过程为灰色小字)…":" 🤖 AI tutor (deepthink — reasoning shown in gray)…":Se?" 🤖 AI 导师(快速模式)…":" 🤖 AI tutor (quick mode)…")),console.log();let t=!1,n=!1;const r=await $({token:le,cardNumber:ue.currentCard,question:e,mode:o,history:Ee().slice(-8)},e=>{"reasoning"===e.kind?(t||(process.stdout.write(" "),t=!0),process.stdout.write(chalk.dim(e.text.replace(/\n/g,"\n ")))):(n||(process.stdout.write(t?"\n\n ":" "),n=!0),process.stdout.write(chalk.white(e.text.replace(/\n/g,"\n "))))});(t||n)&&console.log(),r.ok?r.offtopic?(console.log(),console.log(chalk.yellow(Se?" (被判定与本卡主题无关 — 跑题的问题不会被回答,并会拉低本卡的提问评分)":" (judged unrelated to this card — off-topic questions aren't answered and will lower this card's prompt score)"))):(Ee().push({role:"user",content:e},{role:"assistant",content:r.answer}),null!=r.spent&&null!=r.cap&&r.spent/r.cap>.8&&console.log(chalk.gray(` (${Se?"AI 额度":"AI budget"}: ${Math.round(r.spent/1e3)}k / ${Math.round(r.cap/1e3)}k tokens)`))):(console.log(),console.log(chalk.yellow(` ${r.message||(Se?"AI 导师暂不可用。":"AI tutor unavailable.")}`))),null!==ve&&console.log(chalk.gray(Se?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()}finally{_e=!1}}else Ve()},Xe=e=>e.match(/.{1,64}(\s|$)/g)?.map(e=>e.trim())??[e],Ge=async e=>{const o=e.trim().toLowerCase();if(!_e){if(null!==Ce){const o=e.trim(),t=Ce;if(Ce=null,""===o)console.log(chalk.gray(Se?" 已取消(未提交)。":" Cancelled (nothing submitted)."));else{const e=Le>0?Date.now()-Le:void 0;await w(le,{curriculum_id:se.id,card_number:t,feedback_text:o,time_on_card_ms:e})?console.log(chalk.green(Se?" ✓ 已收到,谢谢帮助完善这张卡。":" ✓ Thanks — your report was recorded.")):console.log(chalk.yellow(Se?" ⚠ 未能送达服务器 — 这条报告没有保存,请稍后再按 e 重试。":" ⚠ Could not reach the server — this report was NOT saved. Try e again later."))}return console.log(),void be()}if(Me){Me=!1;const o=e.trim().toLowerCase();return"y"===o||"yes"===o?await(async()=>{_e=!0;try{console.log(),console.log(chalk.gray(Se?" 📊 提交中 — Panda 正在评审你的提问(通常几十秒,最长一两分钟,请稍候)…":" 📊 Submitting — Panda is reviewing how you asked (usually well under two minutes)…"));const e=await v({token:le,cardNumber:ue.currentCard});if(console.log(),!e.ok)return console.log(chalk.yellow(` ${e.message||(Se?"评审暂不可用。":"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(Se?" 提问评分 ":" Prompt score ")+o(`${e.score??0} / 100`)+chalk.gray(" "+(e=>{switch(e){case"green":return"🟢 "+(Se?"你把 AI 当作被审视的研究伙伴":"you used the AI as a research partner");case"blue":return"🔵 "+(Se?"你把 AI 当作实验助手":"you used the AI as a lab assistant");case"yellow":return"🟡 "+(Se?"你把 AI 当作讲师":"you used the AI as a lecturer");default:return"🔴 "+(Se?"你把 AI 当作答案机":"you used the AI as an answer machine")}})(e.tier))),e.manipulation&&console.log(chalk.red(Se?" ⚠ 检测到试图操纵评分,分数已按规则大幅降低":" ⚠ an attempt to manipulate the score was detected — the score was reduced sharply")),null!=e.rank&&null!=e.entrants){const o=!0===e.inTop3,t=Se?` 本卡排名 #${e.rank} / ${e.entrants}`:` Rank on this card: #${e.rank} of ${e.entrants}`;console.log((o?chalk.bold.green(t):chalk.white(t))+(o?chalk.bold.magenta(Se?" — 进入 TOP 3!输入 /view 看另两名的提问":" — TOP 3! type /view to read the other top prompts"):""))}if(console.log(chalk.gray(Se?` 第 ${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 Xe(e.feedback))console.log(chalk.white(" "+o))}e.improve&&(console.log(),console.log(chalk.cyan(Se?" 改进提问: ":" 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(` (${Se?"AI 额度":"AI budget"}: ${Math.round(e.spent/1e3)}k / ${Math.round(e.cap/1e3)}k tokens)`)),console.log()})(e),!0===e.inTop3?xe.add(ue.currentCard):xe.delete(ue.currentCard);const o=await A({token:le,curriculumId:se.id,cardNumber:ue.currentCard});o.ok&&o.totals&&o.chapters&&(te({totals:o.totals,chapters:o.chapters,floor:o.floor}),console.log())}finally{_e=!1}})():(console.log(chalk.gray(Se?" 已取消(未提交,不消耗次数)。":" Cancelled — nothing submitted, no attempt used.")),console.log()),null!==ve&&(console.log(chalk.gray(Se?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()),void be()}if(Pe)return Pe=!1,Qe(),void be();if(o){if(Ie)return"exit"===await s(e.trim())&&(Ie=!1),void be();if(Ae){if(["q","quit","exit","back","b","menu"].includes(o))return Ae=!1,console.log(chalk.gray(Se?" 已退出 AI 导师,回到卡片。":" Left the AI tutor — back to the card.")),console.log(),void be();const t=e.trim().startsWith("!"),n=t?e.trim().slice(1).trim():e.trim();return n&&await Ye(n,t?"fast":"deep"),void be()}if("tmux"===o||o.startsWith("tmux ")){try{t("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(Se?" — 或 quit 回到 icoa> 后运行 ":" — or quit to icoa> and run ")+chalk.bold.cyan("env setup")+chalk.gray(".")),console.log(),void be()}return await new Promise(o=>{const t=process.stdin,r=!!t.isTTY&&!!t.isRaw;if(pe.pause(),t.isTTY&&"function"==typeof t.setRawMode)try{t.setRawMode(!1)}catch{}const l=n(e.trim(),{shell:!0,stdio:"inherit"}),s=()=>{if(t.isTTY&&"function"==typeof t.setRawMode&&r)try{t.setRawMode(!0)}catch{}pe.resume(),o()};l.on("close",s),l.on("error",s)}),console.log(),void be()}if("ai4ioai"===o||"/ai4ioai"===o)return await a()&&(Ie=!0),void be();if("/ai"===o||"/ai!"===o||o.startsWith("/ai ")||o.startsWith("/ai! ")){if(!ce)return Ve(),void be();const t=o.startsWith("/ai!"),n=e.trim().slice(t?4:3).trim();if(!n){Ae=!0;const e=Ne(ue.currentCard),o=e?e.title??e.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 🤖 AI ")+chalk.bold.white(Se?"AI 导师":"AI tutor")+(o?chalk.gray(Se?` — 主题「${o}」`:` — topic “${o}”`):"")),console.log(chalk.gray(Se?" 直接输入问题(默认深度思考) · ! 开头 = 快答 · q = 返回卡片":" Type a question (deepthink by default) · prefix ! = quick answer · q = back to card")),console.log(),void be()}return await Ye(n,t?"fast":"deep"),void be()}if("/submit"===o||o.startsWith("/submit ")||"/research"===o||o.startsWith("/research ")){if(!ce)return Ve(),void be();const e=Ne(ue.currentCard),o=e?e.title??e.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 📊 ")+chalk.bold.white(Se?"Panda 提问评审":"Panda's prompt review")+(o?chalk.gray(Se?` — 主题「${o}」`:` — topic “${o}”`):"")),console.log(chalk.gray(Se?" 评的是你在本卡上「怎么问 AI」— 你与 /ai 导师的整段对话会被":" It scores HOW you asked the AI on this card — your whole /ai")),console.log(chalk.gray(Se?" 整体评审,给出分数、名次和改进建议。":" conversation is reviewed for a score, a rank, and advice.")),console.log(chalk.gray(Se?" 每张卡最多 2 次评分;评出的最好成绩进本卡 Top-3 榜。":" Max 2 graded attempts per card; your best enters the card’s top-3 board.")),console.log(),console.log(chalk.white(Se?" 确认提交本卡对话评分?":" Submit this card’s conversation for grading?")+chalk.gray(Se?" y = 提交 · 其他 = 取消":" y = submit · anything else = cancel")),console.log(),Me=!0,void be()}if("/view"===o)return ce?(await(async()=>{_e=!0;try{console.log();const e=await C({token:le,cardNumber:ue.currentCard});if(!e.ok)return console.log(chalk.yellow(` ${e.message||(Se?"暂时无法查看。":"View unavailable right now.")}`)),"not_top3"===e.code&&xe.delete(ue.currentCard),void console.log();null!=e.myRank&&e.myRank<=3&&xe.add(ue.currentCard),console.log(chalk.bold.white(Se?` 🏆 本卡 Top-3 提问 — 第 ${ue.currentCard} 张 · ${e.entrants??0} 人上榜`:` 🏆 Top-3 prompts — card ${ue.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 t=Xe(e);console.log(chalk.gray(` Q${o+1} `)+chalk.white(t[0]??""));for(const e of t.slice(1))console.log(chalk.white(" "+e))}),0===o.questions.length&&console.log(chalk.gray(Se?" (无可显示的提问)":" (no questions to show)"))}console.log()}finally{_e=!1}})(),void be()):(Ve(),void be());if("/honors"===o||"honors"===o)return ae?(console.log(),console.log(chalk.gray(Se?" 荣誉榜属于正式 learn token — 用团队签发的 token 进来即可查看和参赛。":" The honor board belongs to full learn tokens — join with a team-issued token to view and compete.")),console.log(),void be()):(await(async()=>{_e=!0;try{console.log();const e=await A({token:le,curriculumId:se.id,cardNumber:ue.currentCard});if(!e.ok||!e.totals||!e.chapters)return console.log(chalk.yellow(` ${e.message||(Se?"荣誉榜暂时不可用。":"Honor board unavailable right now.")}`)),void console.log();if(console.log(chalk.bold.white(Se?` 🏆 本卡实时 Top-3 — 第 ${ue.currentCard} 张`:` 🏆 Live top-3 — card ${ue.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(Se?" (本卡还没有人上榜)":" (no one has medaled on this card yet)"));console.log(),te({totals:e.totals,chapters:e.chapters,floor:e.floor}),console.log()}finally{_e=!1}})(),void be());if("menu"!==o&&"menu confirm"!==o)if("quit"!==o&&"exit"!==o&&"q"!==o){if("status"===o)return Y(se,ue),void be();if("lang"===o||o.startsWith("lang ")){const e=o.slice(4).trim();if(!e)return console.log(chalk.gray(" 当前语言 / current: ")+chalk.white(x().language||"en")+chalk.gray(" · ")+chalk.bold.green("lang zh")+chalk.gray(" / ")+chalk.bold.green("lang en")),void be();const t=se.cards.some(e=>null!=e._zh),n=e.toLowerCase().startsWith("zh")?"zh":e.toLowerCase(),r="en"===n||"zh"===n&&t,l=r?n:"en";return P({language:l}),r?console.log(chalk.green(" ✓ ")+chalk.gray("语言 / language → ")+chalk.white(l)):console.log(chalk.yellow(" ! ")+chalk.gray(`${e} 本课暂无翻译,已用英文 / not available — using English`)),ve=null,ke=null,$e=null,Ce=null,Pe=!1,Me=!1,Qe(),void be()}if("sim"===o){const e=Ne(ue.currentCard);return e&&"sim_demo"===e.type?(function(e){const o=function(){const e=g(y(import.meta.url)),o=[m(e,"..","..","panda","mujoco-launcher.py"),m(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 t={baseline:"baseline",prompt_injected:"prompt_inj",patch_attacked:"patch",modality_confused:"confused"}[e]||"baseline";console.log(chalk.gray(` Launching MuJoCo viewer (scenario: ${t})...`)),console.log(chalk.gray(" Close the window or press ESC to return to learn mode.")),n("python3",[o,t,"--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 be()):(console.log(chalk.gray(" (sim only available on simulation cards)")),void be())}if("bookmark"===o){const e=ue.currentCard;return ue.bookmarks.includes(e)||ue.bookmarks.push(e),S(ue),console.log(chalk.gray(` ✓ Card ${e} bookmarked.`)),void be()}if("e"===o)return Ce=ue.currentCard,console.log(chalk.gray(Se?" 描述问题(回车提交 / 空行取消):":" Describe the issue (Enter to submit / blank to cancel):")),void be();if("back"===o||"b"===o)return ue.currentCard>1&&(ue.currentCard-=1),ke=null,$e=null,ve=null,S(ue),Qe(),void be();if("contest"===o||"contest start"===o||"contest status"===o||"contest reset"===o){if(!Fe)return console.log(chalk.yellow(Se?" 判分赛需要正式课程 token(demo 不支持)。":" Contests need a full curriculum token (demos not supported).")),void be();const e=Ue(ue.currentCard);if("contest reset"===o){const o=await _(le,e);return o.ok?(console.log(chalk.yellow(Se?` 已放弃当前计时(阶段 ${e})。输入 contest 重新开赛。`:` Live attempt abandoned (phase ${e}). Type contest to start a fresh one.`)),Be=null):console.log(chalk.red(` ✗ ${o.message||"reset failed"}`)),void be()}console.log(chalk.gray(Se?" 连接判分赛…":" Contacting contest server…"));const t=await I(le,e);if(!t.ok||!t.data)return"no_contest"===t.code?console.log(chalk.gray(Se?` 本阶段(${e})暂无判分赛。`:` No contest configured for phase ${e} yet.`)):console.log(chalk.red(` ✗ ${t.message||"contest unavailable"}`)),void be();const n=t.data,r=m(process.cwd(),"icoa-contest",`${se.id}-phase${e}`);c(r,{recursive:!0});for(const e of n.problems){const o=[`# ${e.title} (${e.id})`,"",e.statement,""];let t=0;for(const n of e.samples??[])t+=1,o.push(`## Sample ${t}`,"","Input:","```",n.in,"```","Output:","```",n.out,"```","");u(m(r,`problem-${e.id}.md`),o.join("\n"));const n=m(r,`sol-${e.id}.py`);i(n)||u(n,`# ${e.title} — read stdin, print the answer to stdout\n`)}Be={phase:e,dir:r,data:n};const l=M(n.deadline);return console.log(),console.log(chalk.bold.cyan(` ⚖ ${n.title||(Se?`阶段 ${e} 判分赛`:`Phase ${e} contest`)}`)+chalk.gray(Se?` · 第 ${n.attempt} 次尝试${n.resumed?"(续)":""}`:` · attempt ${n.attempt}${n.resumed?" (resumed)":""}`)),console.log(chalk.white(Se?` ${n.n} 题 · AC ${n.need} 题过关 · 限时 ${n.duration_minutes} 分钟`:` ${n.n} problems · AC ${n.need} to pass · ${n.duration_minutes} min`)+(l?chalk.bold.yellow(Se?` ⏱ 剩 ${l}`:` ⏱ ${l} left`):chalk.red(Se?" ⏱ 已超时 — contest reset 重开":" ⏱ time up — contest reset to retry"))),console.log(),n.problems.forEach((e,o)=>{const t=n.solved.includes(e.id);console.log(chalk.gray(` ${o+1}. `)+(t?chalk.green("✓ "):chalk.gray("· "))+chalk.white(e.title)+chalk.gray(` (${e.id})`))}),console.log(),console.log(chalk.gray(Se?` 题面已写入 ${r}`:` Statements written to ${r}`)),console.log(chalk.gray(Se?" 写好 sol-<题号>.py 后: ":" Edit sol-<id>.py, then: ")+chalk.bold.green("submit <n>")+chalk.gray(Se?" (或 submit <题号> <文件路径> · contest reset 重开计时)":" (or submit <id> <file> · contest reset to restart the clock)")),void be()}if(o.startsWith("submit ")){const e=o.split(/\s+/);let t=e[1]||"";const n=e[2];if(!Be)return console.log(chalk.yellow(Se?" 先输入 contest 进入本阶段判分赛。":" Type contest first to open this phase's contest.")),void be();const r=Be.data.problems,l=parseInt(t,10);if(Number.isInteger(l)&&l>=1&&l<=r.length&&!r.some(e=>e.id===t)&&(t=r[l-1].id),!r.some(e=>e.id===t))return console.log(chalk.yellow(Se?` 未知题号 ${t} — 输入 contest 查看题目列表。`:` Unknown problem ${t} — type contest to list problems.`)),void be();const s=n||m(Be.dir,`sol-${t}.py`);let a;try{a=d(s,"utf-8")}catch{return console.log(chalk.red(Se?` 读不到文件: ${s}`:` Cannot read file: ${s}`)),void be()}console.log(chalk.gray(Se?" ⚖ 判题中…(隐藏用例,最长约 1-2 分钟)":" ⚖ Judging… (hidden tests, may take a minute)"));const i=await T(le,Be.phase,t,a);if(!i.ok||!i.data)return"no_active_session"===i.code?console.log(chalk.yellow(Se?" 没有进行中的计时(可能已超时)— 输入 contest 查看/重开。":" No live timed attempt (time may be up) — type contest.")):console.log(chalk.red(` ✗ ${i.message||"judging failed"}`)),void be();const c=i.data;if(c.already_ac)console.log(chalk.green(Se?" 该题已 AC(不重复判)。":" Already AC on this problem."));else if("AC"===c.verdict)console.log(chalk.bold.green(" ✓ AC")+chalk.gray(` · ${c.total_tests??"?"} tests · ${c.time_ms??"?"}ms`)),Be.data.solved.includes(t)||Be.data.solved.push(t);else{const e="TLE"===c.verdict?chalk.yellow:chalk.red;console.log(e(` ✗ ${c.verdict}`)+chalk.gray(c.test?Se?` · 挂在第 ${c.test} 个用例(过 ${c.passed_tests}/${c.total_tests})`:` · failed test ${c.test} (passed ${c.passed_tests}/${c.total_tests})`:""))}c.late&&console.log(chalk.yellow(Se?" ⏱ 超时提交 — 不计入本次成绩。":" ⏱ Submitted past the deadline — not counted."));const u=M(c.deadline||"");return console.log(chalk.white(Se?` 进度: ${c.ac_count}/${c.need} 题(共 ${c.n})`:` Progress: ${c.ac_count}/${c.need} AC (of ${c.n})`)+(u?chalk.gray(` · ⏱ ${u}`):"")),c.passed&&!c.late&&(console.log(chalk.bold.green(Se?" 🏁 本阶段判分赛已过关!":" 🏁 Contest passed!")),await ze()),void be()}if(o.startsWith("card ")){const e=o.slice(5).trim(),t=parseInt(e,10);if(!Number.isInteger(t)||t<1||t>se.totalCards)return console.log(chalk.yellow(` Card number must be 1..${se.totalCards}. Try: `)+chalk.bold.green("card 1")),void be();const n=await Ze(t);return n?(console.log(n),void be()):(ue.currentCard=t,ke=null,$e=null,ve=null,S(ue),Qe(),void be())}if(null!==ve&&["y","yes","n","no"].includes(o)){const e=Ne(ve);if(e&&"knowledge"===e.type&&e.check){const t=o.startsWith("y")?"y":"n",n=t===e.check.answer,r=Date.now()-Le;q(ue,e.number),j(ue,e.number,{answer:t,correct:n,submittedAt:(new Date).toISOString()}),S(ue);const{renderCheckFeedback:l}=await import("../lib/learn-render.js");if(l(e,t,n,!1),De.push(f(le,{card_number:e.number,event_type:"check_answered",check_answer:t,check_correct:n,time_on_card_ms:r}).catch(()=>{})),ve=null,e.solution&&e.solution.length>0)return void be();if(ue.currentCard<se.totalCards){const e=ue.currentCard,o=await Ze(e+1);if(o)return await Ke(e),console.log(o),void be();ue.currentCard+=1,S(ue),await Ke(e)?He():Qe(),be()}else G(se,ue),be();return}}if(null!==ke&&["a","b","c","d"].includes(o)){const e=Ne(ke);if(e&&"mcq"===e.type){const t=o.toUpperCase(),n=t===e.answer;if(R(ue,e.number,{answer:t,correct:n,submittedAt:(new Date).toISOString()}),q(ue,e.number),S(ue),U(e,t,n,ue),De.push(f(le,{card_number:e.number,event_type:"mcq_answered",mcq_answer:t,mcq_correct:n}).catch(()=>{})),ke=null,n)if(ue.currentCard<se.totalCards){const e=await Ze(ue.currentCard+1);if(e)return console.log(e),void be();ue.currentCard+=1,S(ue),Qe()}else G(se,ue),be();else be();return}}if(null!==$e){if("done"===o){const e=Ne($e);if(e&&"practical"===e.type)return N(ue,e.number),q(ue,e.number),S(ue),Z(e),$e=null,void be()}if("skip"===o)return q(ue,$e),S(ue),console.log(chalk.gray(" Skipped (counts as not completed).")),console.log(),$e=null,void be()}if("ok"===o||"next"===o||"continue"===o||"n"===o){if(null!==ke)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void be();if(null!==$e)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void be();if(null!==ve)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void be();const e=ue.currentCard,o=e<se.totalCards?await Ze(e+1):null;return o?(await Ke(e),console.log(o),void be()):(ue.currentCard+=1,S(ue),ue.currentCard>se.totalCards?G(se,ue):await Ke(e)?He():Qe(),void be())}if(null!==ve)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void be();if(null!==ke)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void be();if(null!==$e)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void be();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")),be()}else if(De.length>0&&await Promise.race([Promise.allSettled(De),new Promise(e=>setTimeout(e,5e3))]),console.log(),console.log(chalk.gray(" Saved. See you next session.")),console.log(chalk.gray(" Streak: ")+chalk.yellow(`🔥 ${ue.streakDays} day(s)`)),console.log(),ye){pe.removeAllListeners("line"),we();for(const e of he)pe.on("line",e);pe.prompt(),Re()}else pe.removeAllListeners("line"),pe.close();else if(De.length>0&&await Promise.race([Promise.allSettled(De),new Promise(e=>setTimeout(e,3e3))]),pe.removeAllListeners("line"),we(),ye||!je){const{returnToMainMenu:e}=await import("../lib/menu-nav.js");e(ye?pe:void 0),Re()}else pe.close()}else be()}};pe.on("line",e=>{Ge(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.")),be()})}),ye||pe.on("close",async()=>{De.length>0&&await Promise.race([Promise.allSettled(De),new Promise(e=>setTimeout(e,5e3))]),je?Re():process.exit(0)});try{Qe(),be()}catch{if(ne("Could not render this card — the curriculum may be malformed."),ye){pe.removeAllListeners("line"),we();for(const e of he)pe.on("line",e);pe.prompt()}else pe.close();Re()}await qe}),re.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 t=0,n=0,r=0;for(const e of o){process.stdout.write(` ${e.padEnd(22)} `);const o=Date.now(),l=await h(e),s=Date.now()-o;if(l){const e=l.cards,o=Array.isArray(e)?e.length:0,n=JSON.stringify(l).length;r+=n,console.log(chalk.green("✓ ")+chalk.gray(`${o.toString().padStart(3)} cards · ${(n/1024).toFixed(1).padStart(6)} KB · ${s.toString().padStart(4)} ms`)),t++}else console.log(chalk.red("✗ failed (server unreachable or unknown id)")),n++}console.log(),console.log(chalk.gray(" Summary: ")+chalk.green.bold(`${t} cached`)+chalk.gray(` · ${n} failed · total ${(r/1024).toFixed(0)} KB`)),console.log(chalk.gray(" Cache: ")+chalk.cyan("~/.icoa/learn-cache/")),console.log()})}
@@ -28,3 +28,5 @@ declare const SCENARIOS: Record<string, {
28
28
  }>;
29
29
  export declare function registerSimCommand(program: Command): void;
30
30
  export { SCENARIOS };
31
+ /** Render one scenario, tail-free. Entry point for the hidden card decks. */
32
+ export declare function runSimQuiet(scenarioName: string): Promise<void>;
@@ -1 +1 @@
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 l,join as s,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=s(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(([,o])=>!o.hidden)),j={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 x(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 l=await t.json();if(!l.success||!l.data?.mp4_b64)return console.log(chalk.yellow(" Renderer unavailable.")),!1;const i=process.env.TMPDIR||"/tmp",c=s(i,`icoa-sim-${e.scenarioName}-${Date.now()}.mp4`);a(c,Buffer.from(l.data.mp4_b64,"base64")),h(Date.now()),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Rendered.")),l.data.description&&console.log(chalk.gray(" Action: ")+chalk.white(l.data.description)),console.log(),console.log(chalk.gray(" ")+e.hint),console.log();const d=T();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 A(t){const g=t.toLowerCase();if("arm"===g)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(j)){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> "))},l=async s=>{const i=s.trim();if(a)return;if("q"===i||"exit"===i||"quit"===i)return console.log(chalk.gray(" Leaving arm mode.")),console.log(),r.removeListener("line",l),(()=>{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(j[e])return j[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 x({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",l),t()})}();const u=w[g];if(!u){console.log(),console.log(chalk.yellow(` Unknown scenario: "${t}"`)),console.log(chalk.gray(" Available:"));for(const[o,e]of Object.entries(v))console.log(chalk.gray(" ")+chalk.bold.cyan(o.padEnd(10))+chalk.gray(e.label));return void console.log()}if(u.bundled)return void await async function(t,c){const g=await async function(o){const e=function(o){const e=l(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=s(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 fetch(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 l=Buffer.from(await n.arrayBuffer());return a(t,l),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=s(p,`icoa-sim-${t}-${Date.now()}.mp4`);e(g,u),b(Date.now()),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();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();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()}(g,u);await x({scenarioName:g,label:u.label,hint:u.hint,action:u.action,frames:u.frames,narrative:u.narrative,panda2_pose:u.panda2_pose});const y="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Try other scenarios:"));for(const o of Object.keys(v))o!==g&&console.log(chalk.gray(" "+y+"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 A(o||"ensemble")})}export{w as SCENARIOS};
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)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)}
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 g}from"./commands/lang.js";import{registerSetupCommand as d}from"./commands/setup.js";import{registerEnvCommand as p}from"./commands/env.js";import{registerAienvCommand as y}from"./commands/aienv.js";import{registerIpynbCommand as f}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 T}from"./commands/theme.js";import{registerLearnCommand as b}from"./commands/learn.js";import{registerCtf4VlaCommand as v}from"./commands/ctf4vla.js";import{registerDemo2Command as j}from"./commands/demo2.js";import{registerSimCommand as A}from"./commands/sim.js";import{registerArenaCommand as I}from"./commands/arena.js";import{registerKaggleCommand as $}from"./commands/kaggle.js";import{registerBoardCommand as C}from"./commands/board.js";import{getConfig as E,saveConfig as _}from"./lib/config.js";import{startRepl as S}from"./repl.js";import{RELAUNCH_CODE as x}from"./lib/menu-nav.js";import{setTerminalTheme as O}from"./lib/theme.js";import{checkForUpdates as L}from"./lib/update-check.js";import{detectIcoaInstalls as k}from"./lib/platform.js";import{ICOA_BIG as N}from"./lib/banner.js";import{readFileSync as R}from"node:fs";import{fileURLToPath as F}from"node:url";import{dirname as U,join as P}from"node:path";const M=U(F(import.meta.url)),G=JSON.parse(R(P(M,"..","package.json"),"utf-8")).version,D=chalk.cyan(" ─────────────────────────────────────────────────────"),H=`\n${D}\n\n${N.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${G}`)}\n\n${D}\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=o(process.execPath,process.argv.slice(1),{stdio:"inherit",env:{...process.env,ICOA_SUPERVISED:"1",...n?{ICOA_RELAUNCH:"1"}:{}}});if(s.signal){e=0;break}e=s.status,n=!0}while(e===x);process.exit(e??0)}}const J=new o;if(J.name("icoa").version(G).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=E();O("high-contrast"===e.themeVariant?"high-contrast":"dark"),L(),function(){const o=k();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(H);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 S(J,s)}),e(J),n(J),s(J),r(J),t(J),i(J),a(J),l(J),m(J),c(J),g(J),d(J),p(J),y(J),f(J),h(J),u(J),w(J),T(J),b(J),v(J),j(J),A(J),I(J),$(J),C(J),J.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=E().geminiModel||"gemini-2.5-flash-lite";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)))}J.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 T}from"./commands/theme.js";import{registerLearnCommand as b}from"./commands/learn.js";import{registerCtf4VlaCommand as v}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=o(process.execPath,process.argv.slice(1),{stdio:"inherit",env:{...process.env,ICOA_SUPERVISED:"1",...n?{ICOA_RELAUNCH:"1"}:{}}});if(s.signal){e=0;break}e=s.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),T(Y),b(Y),v(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();
@@ -0,0 +1,45 @@
1
+ export interface CardChoice {
2
+ key: string;
3
+ text: string;
4
+ }
5
+ export interface Card {
6
+ /** Short all-caps label shown in the card header. */
7
+ title: string;
8
+ /** Optional chartart block — pre-rendered ASCII, one string per line. */
9
+ chartart?: string[];
10
+ /** Narrative body lines. */
11
+ body: string[];
12
+ choices: CardChoice[];
13
+ /** Key of the correct choice, lower-case. */
14
+ correct: string;
15
+ /** Shown after the user answers, regardless of right or wrong. */
16
+ reveal: string[];
17
+ }
18
+ export interface Deck {
19
+ /** Command word — also the sub-REPL prompt token (`icoa demo4> `). */
20
+ id: string;
21
+ /** Banner title line. */
22
+ title: string;
23
+ /** Banner subtitle / tagline. */
24
+ tagline: string;
25
+ /** Accent colour applied to the frame. */
26
+ accent: 'cyan' | 'magenta' | 'green' | 'yellow';
27
+ cards: Card[];
28
+ /** Terminal payload — the render reward. */
29
+ finale: () => Promise<void>;
30
+ /** Closing hook lines shown after the finale. */
31
+ outro: string[];
32
+ }
33
+ /** A deck bundle carries both language variants; the runner picks one. */
34
+ export interface DeckPack {
35
+ en: Deck;
36
+ zh: Deck;
37
+ }
38
+ /** True while a card deck owns stdin. The main REPL's prompt wrapper checks
39
+ * this so it does not stamp `icoa> ` over our `icoa demo4> `. */
40
+ export declare function isCardDeckActive(): boolean;
41
+ /**
42
+ * Run a two-language card pack. Typing `zh` / `en` (or `lang zh` / `lang en`)
43
+ * at any card prompt switches language and restarts the deck.
44
+ */
45
+ export declare function runDeckPack(pack: DeckPack, initialLang: string): Promise<void>;
@@ -0,0 +1 @@
1
+ import{createInterface as o}from"node:readline";import{Interface as e}from"node:readline";import chalk from"chalk";import{getMainRl as n}from"./main-rl.js";let t=[],l=null,c=null,r=!1;export function isCardDeckActive(){return r}function s(e){return new Promise(t=>{const l=n();if(l)return c=t,l.setPrompt(e),void l.prompt();const r=o({input:process.stdin,output:process.stdout});r.question(e,o=>{r.close(),t(o)})})}function i(o){return chalk[o.accent]}function a(o,e="─"){return i(o)(" "+e.repeat(62))}function f(o,e,n){const t=i(o);if(console.log(),console.log(t(" ┌─ ")+chalk.bold.white(`${n+1}/${o.cards.length} ${e.title}`)),console.log(t(" │")),e.chartart){for(const o of e.chartart)console.log(t(" │ ")+chalk.white(o));console.log(t(" │"))}for(const o of e.body)console.log(t(" │ ")+chalk.white(o));console.log(t(" │"));for(const o of e.choices)console.log(t(" │ ")+chalk.bold.cyan(o.key+")")+chalk.white(" "+o.text));console.log(t(" └─")),console.log()}const g=new Set(["quit","q","exit","menu","back"]);export async function runDeckPack(o,s){!function(){r=!0;const o=n();o&&(t=o.listeners("line").slice(),o.removeAllListeners("line"),l=o.prompt.bind(o),o.prompt=e.prototype.prompt.bind(o),o.on("line",o=>{const e=c;c=null,e&&e(o)}))}();let i=s.toLowerCase().startsWith("zh")?"zh":"en";try{for(;;){const e=await u("zh"===i?o.zh:o.en,i);if("done"===e)break;i=e}}finally{!function(){r=!1;const o=n();if(o){o.removeAllListeners("line"),l&&(o.prompt=l);for(const e of t)o.on("line",e);t=[],l=null,c=null}}()}}async function u(o,e){const n="zh"===e;!function(o){console.clear(),console.log(),console.log(a(o,"═")),console.log(chalk.bold.white(" "+o.title)),console.log(chalk.gray(" "+o.tagline)),console.log(a(o,"═")),console.log()}(o);const t=n?" 输入选项字母作答 · quit 退出 · lang en 切英文":" Type the option letter · quit to leave · lang zh for Chinese";console.log(chalk.gray(t));for(let t=0;t<o.cards.length;t++){const l=o.cards[t];for(f(o,l,t);;){const c=(await s(chalk.bold.cyan(`icoa ${o.id}> `))).trim().toLowerCase(),r=c.startsWith("lang ")?c.slice(5).trim():c;if(g.has(c))return console.log(),console.log(chalk.gray(n?" 已退出。":" Left the deck.")),console.log(),"done";if("zh"!==r&&"en"!==r){if(o.cards[t].choices.find(o=>o.key===c)){if(console.log(),c===l.correct)console.log(chalk.bold.green(n?" ✓ 正确":" ✓ Correct"));else{const o=l.choices.find(o=>o.key===l.correct);console.log(chalk.bold.yellow(n?" ✗ 不对 — 正确答案是 ":" ✗ Not it — the answer was ")+chalk.bold.cyan(l.correct+")")+chalk.white(" "+(o?o.text:"")))}for(const o of l.reveal)console.log(chalk.gray(" "+o));console.log();break}console.log(chalk.yellow(n?" 请输入选项字母(或 quit 退出)。":" Type an option letter (or quit to leave)."))}else if(r!==e)return r}}console.log(a(o)),await o.finale(),console.log();for(const e of o.outro)console.log(chalk.gray(" "+e));return console.log(),"done"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Block until the sim cooldown clears, rendering a single-line countdown.
3
+ * Returns immediately when no cooldown is pending (the common case).
4
+ *
5
+ * @param line in-fiction label, e.g. "watchdog cooling down"
6
+ */
7
+ export declare function awaitRenderWindow(line: string): Promise<void>;
@@ -0,0 +1 @@
1
+ import chalk from"chalk";import{cooldownRemaining as o}from"./sim-cooldown.js";export async function awaitRenderWindow(t){let r=o();if(r<=0)return;const e=!0===process.stdout.isTTY;for(;r>0;){const s="█".repeat(Math.max(0,20-Math.ceil(r/3))).padEnd(20,"░"),a=chalk.gray(` ${t} `)+chalk.cyan(s)+chalk.gray(` ${r}s`);e?process.stdout.write("\r"+a):console.log(a),await new Promise(o=>setTimeout(o,1e3)),r=o()}e&&process.stdout.write("\r"+" ".repeat(60)+"\r")}
@@ -59,6 +59,21 @@ export declare function viewPqsTop(opts: {
59
59
  token: string;
60
60
  cardNumber: number;
61
61
  }): Promise<PqsTopResult>;
62
+ export type MyScoreEntry = {
63
+ card: number;
64
+ score: number;
65
+ tier: string;
66
+ };
67
+ export declare function fetchMyScores(opts: {
68
+ token: string;
69
+ cardLo: number;
70
+ cardHi: number;
71
+ }): Promise<{
72
+ ok: boolean;
73
+ scores?: MyScoreEntry[];
74
+ code?: string;
75
+ message?: string;
76
+ }>;
62
77
  export type HonorBoardEntry = {
63
78
  rank: number;
64
79
  score: number;
@@ -1 +1 @@
1
- import{getDeviceFingerprint as e}from"./access.js";import{LEARN_SERVER as t}from"./learn-curricula.js";export async function submitPqs(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/submit";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_number:r.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 n=o.data;return{ok:!0,score:n.score,tier:n.tier,manipulation:n.manipulation,feedback:n.feedback,improve:n.improve,submissionIndex:n.submission_index,submissionsLeft:n.submissions_left,rank:n.rank,entrants:n.entrants,inTop3:n.in_top3,turns:n.turns,spent:n.spent,cap:n.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(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/view";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_number:r.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 viewHonors(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/honors";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),curriculum_id:r.curriculumId,card_number:r.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,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 streamLearnTutor(r,a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/ai",n=new AbortController;let s=setTimeout(()=>n.abort(),6e4);const i=()=>{clearTimeout(s),s=setTimeout(()=>n.abort(),6e4)};try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_number:r.cardNumber,question:r.question,mode:r.mode,history:r.history?.slice(-6),device_fingerprint:e()}),signal:n.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="",u=null,p=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 r;try{r=JSON.parse(t.slice(5).trim())}catch{continue}r.r&&a({kind:"reasoning",text:r.r}),r.c&&(m+=r.c,a({kind:"content",text:r.c})),r.error&&(p=r.error),r.done&&(u=r)}}return p&&!m?{ok:!1,answer:"",message:p}:m?{ok:!0,answer:m,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(s)}}
1
+ import{getDeviceFingerprint as e}from"./access.js";import{LEARN_SERVER as t}from"./learn-curricula.js";export async function submitPqs(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/submit";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_number:r.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 n=o.data;return{ok:!0,score:n.score,tier:n.tier,manipulation:n.manipulation,feedback:n.feedback,improve:n.improve,submissionIndex:n.submission_index,submissionsLeft:n.submissions_left,rank:n.rank,entrants:n.entrants,inTop3:n.in_top3,turns:n.turns,spent:n.spent,cap:n.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(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/view";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_number:r.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(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/myscores";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_lo:r.cardLo,card_hi:r.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(r){const a=t.replace(/\/$/,"")+"/api/icoa/learn/honors";try{const t=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),curriculum_id:r.curriculumId,card_number:r.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,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 streamLearnTutor(r,a){const o=t.replace(/\/$/,"")+"/api/icoa/learn/ai",n=new AbortController;let s=setTimeout(()=>n.abort(),6e4);const i=()=>{clearTimeout(s),s=setTimeout(()=>n.abort(),6e4)};try{const t=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r.token.toUpperCase(),card_number:r.cardNumber,question:r.question,mode:r.mode,history:r.history?.slice(-6),device_fingerprint:e()}),signal:n.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 r;try{r=JSON.parse(t.slice(5).trim())}catch{continue}r.r&&a({kind:"reasoning",text:r.r}),r.c&&(m+=r.c,a({kind:"content",text:r.c})),r.error&&(u=r.error),r.done&&(p=r)}}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)}}
@@ -0,0 +1,73 @@
1
+ export type ContestProblem = {
2
+ id: string;
3
+ title: string;
4
+ statement: string;
5
+ samples?: Array<{
6
+ in: string;
7
+ out: string;
8
+ }>;
9
+ };
10
+ export type ContestStartData = {
11
+ phase: number;
12
+ title?: string;
13
+ attempt: number;
14
+ resumed: boolean;
15
+ started_at: string;
16
+ deadline: string;
17
+ duration_minutes: number;
18
+ n: number;
19
+ need: number;
20
+ solved: string[];
21
+ problems: ContestProblem[];
22
+ };
23
+ export type ContestSubmitData = {
24
+ verdict: string;
25
+ late?: boolean;
26
+ already_ac?: boolean;
27
+ test?: number | null;
28
+ passed_tests?: number | null;
29
+ total_tests?: number | null;
30
+ time_ms?: number | null;
31
+ ac_count: number;
32
+ need: number;
33
+ n: number;
34
+ passed: boolean;
35
+ deadline?: string;
36
+ };
37
+ export type GatePhase = {
38
+ phase: number;
39
+ configured: boolean;
40
+ passed: boolean;
41
+ contest?: {
42
+ passed: boolean;
43
+ best_ac: number;
44
+ need: number;
45
+ n: number;
46
+ attempts: number;
47
+ };
48
+ research?: {
49
+ good_cards: number;
50
+ need_cards: number;
51
+ min_score: number;
52
+ passed: boolean;
53
+ };
54
+ };
55
+ export type GateData = {
56
+ gated: number;
57
+ unlocked_through_phase: number;
58
+ phases: GatePhase[];
59
+ };
60
+ type ApiResult<T> = {
61
+ ok: boolean;
62
+ data?: T;
63
+ code?: string;
64
+ message?: string;
65
+ };
66
+ export declare function contestStart(token: string, phase: number): Promise<ApiResult<ContestStartData>>;
67
+ export declare function contestSubmit(token: string, phase: number, problemId: string, code: string): Promise<ApiResult<ContestSubmitData>>;
68
+ export declare function contestReset(token: string, phase: number): Promise<ApiResult<{
69
+ reset_sessions: number;
70
+ }>>;
71
+ export declare function fetchGate(token: string): Promise<ApiResult<GateData>>;
72
+ export declare function timeLeft(deadlineIso: string): string | null;
73
+ export {};
@@ -0,0 +1 @@
1
+ import{getDeviceFingerprint as t}from"./access.js";import{LEARN_SERVER as e}from"./learn-curricula.js";async function o(o,r,a){const n=e.replace(/\/$/,"")+o;try{const e=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...r,device_fingerprint:t()}),signal:AbortSignal.timeout(a)}),o=await e.json().catch(()=>({}));return e.ok&&o.success&&void 0!==o.data?{ok:!0,data:o.data}:{ok:!1,message:o.message||`HTTP ${e.status}`,code:o.code}}catch(t){return{ok:!1,message:`Network error: ${t instanceof Error?t.message:String(t)}`}}}export function contestStart(t,e){return o("/api/icoa/learn/contest/start",{token:t.toUpperCase(),phase:e},15e3)}export function contestSubmit(t,e,r,a){return o("/api/icoa/learn/contest/submit",{token:t.toUpperCase(),phase:e,problem_id:r,code:a},12e4)}export function contestReset(t,e){return o("/api/icoa/learn/contest/reset",{token:t.toUpperCase(),phase:e},15e3)}export function fetchGate(t){return o("/api/icoa/learn/gate",{token:t.toUpperCase()},15e3)}export function timeLeft(t){const e=Date.parse(t);if(Number.isNaN(e))return null;const o=e-Date.now();if(o<=0)return null;const r=Math.floor(o/6e4),a=Math.floor(o%6e4/1e3);return`${r}:${String(a).padStart(2,"0")}`}
@@ -226,6 +226,7 @@ type ValidateResult = {
226
226
  message?: string;
227
227
  code?: string;
228
228
  aiEnabled?: boolean;
229
+ gated?: boolean;
229
230
  };
230
231
  export declare function validateEAToken(token: string): Promise<ValidateResult>;
231
232
  /**
@@ -1 +1 @@
1
- export function localized(r,e){if(!e.startsWith("zh")||!r._zh)return r;const t=r._zh,n={...r};for(const r of Object.keys(t))void 0!==t[r]&&(n[r]=t[r]);return n}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 n,statSync as a,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(!n(t))return null;try{const e=a(t),n=Date.now()-e.mtimeMs;return{curriculum:JSON.parse(r(t,"utf-8")),ageMs:n}}catch{return null}}(i);if(u&&u.ageMs<6048e5&&isValidCurriculum(u.curriculum))return u.curriculum;const f=await async function(e,t){const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(e),c={},i=function(e){const t=m(e);if(!n(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(a,{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,n,a){try{t(d,{recursive:!0}),e(l(r),JSON.stringify(n)),a&&e(m(r),a)}catch{}}(i,f.curriculum,f.etag),f.curriculum):"unchanged"===f.kind&&u&&isValidCurriculum(u.curriculum)?(function(r){try{const e=l(r);if(n(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 n=await t.json();return n.success&&n.data?{ok:!0,curriculumId:n.data.curriculum_id,status:n.data.status,validUntil:n.data.valid_until,aiEnabled:1===n.data.ai_enabled||!0===n.data.ai_enabled}:{ok:!1,message:n.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,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}}
@@ -39,6 +39,25 @@ export declare function renderPhaseComplete(curriculum: Curriculum, state: Learn
39
39
  number: number;
40
40
  name: string;
41
41
  cardRange: [number, number];
42
+ }, pqsScores?: Array<{
43
+ card: number;
44
+ score: number;
45
+ tier: string;
46
+ }>, gatePhase?: {
47
+ configured: boolean;
48
+ contest?: {
49
+ passed: boolean;
50
+ best_ac: number;
51
+ need: number;
52
+ n: number;
53
+ attempts: number;
54
+ };
55
+ research?: {
56
+ good_cards: number;
57
+ need_cards: number;
58
+ min_score: number;
59
+ passed: boolean;
60
+ };
42
61
  }): void;
43
62
  export declare function renderCurriculumComplete(curriculum: Curriculum, state: LearnState): void;
44
63
  export declare function renderMCQCard(card: Extract<Card, {