icoa-cli 2.19.375 → 2.19.377
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/ai4ctf.js +1 -1
- package/dist/commands/ai4ioai.d.ts +1 -0
- package/dist/commands/ai4ioai.js +1 -1
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/exam.js +1 -1
- package/dist/commands/learn.js +1 -1
- package/dist/types/index.d.ts +3 -0
- package/package.json +1 -1
package/dist/commands/learn.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import chalk from"chalk";import{Interface as o,createInterface as e}from"node:readline";import{spawn as r}from"node:child_process";import{getMainRl as n}from"../lib/main-rl.js";import{handleAi4ioaiMessage as t,startAi4ioaiChat as l}from"./ai4ioai.js";import{existsSync as a}from"node:fs";import{dirname as s,join as i}from"node:path";import{fileURLToPath as c}from"node:url";import{loadCurriculumById as u,validateEAToken as d,syncProgress as g,syncCardFeedback as m}from"../lib/learn-curricula.js";import{normalizeTokenBody as y}from"../lib/token-format.js";import{streamLearnTutor as h,submitPqs as f,viewPqsTop as p,viewHonors as w}from"../lib/learn-ai.js";import{getConfig as b,saveConfig as k}from"../lib/config.js";import{loadLearnState as v,saveLearnState as A,newLearnState as C,updateStreak as I,markCardComplete as $,recordMCQ as E,recordCheck as _,markPracticalComplete as T,addAchievement as M}from"../lib/learn-state.js";import{renderWelcome as P,renderKnowledgeCard as x,renderMCQCard as L,renderMCQFeedback as O,renderPracticalCard as D,renderPracticalSuccess as S,renderSimDemoCard as q,renderMilestone as j,renderArenaTaskCard as N,renderComingSoonCard as R,renderStatus as W,renderPhaseComplete as F,renderCurriculumComplete as B,setLearnAiEnabled as z,setLearnPqsViewCards as U,renderHonorWall as J}from"../lib/learn-render.js";import{printError as Q}from"../lib/ui.js";export function registerLearnCommand(V){V.command("learn [token]").description("Enter learn mode (free demo, or team-issued EA/EI/AC/CA/IO/IA token for full curriculum)").action(async V=>{V&&V.trim()||(console.log(),console.log(chalk.gray(" No token given — starting free 11-card demo (")+chalk.bold.green("LEARNDEMO01")+chalk.gray(").")),console.log(chalk.gray(" Full curriculum: ")+chalk.bold.yellow("learn EA|EI|AC|CA|IO|IA + 8 chars")+chalk.gray(" — token from your country team leader.")),console.log(),V="LEARNDEMO01");let H=V.trim().toUpperCase(),K=null;const X=/^(LEARNDEMO01|AI4CTFDEMO01|CTF4AIDEMO01)$/i.test(H),Z=/^[A-Z]{2}[A-Z0-9]{8}$/i.test(H);let G=!1;if(X)console.log(),console.log(chalk.gray(" Loading demo curriculum...")),K=await u(H,H);else if(Z){console.log(),console.log(chalk.gray(" Validating token..."));let o=await d(H);if(!o.ok){const e=y(H);if(e!==H){const r=await d(e);r.ok&&(console.log(chalk.yellow(` Corrected look-alike characters: ${H} → ${e}`)),o=r,H=e)}}if(!o.ok)return Q(`Token validation failed: ${o.message}`),console.log(),"device_bound"===o.code?(console.log(chalk.gray(" This learn token is bound to the first computer it was used on,")),console.log(chalk.gray(" and this is a different computer.")),console.log(),console.log(chalk.gray(" If you changed computers or reinstalled, ask the coordinator who")),console.log(chalk.gray(" issued your token to move it to this device, then try again."))):(console.log(chalk.gray(" Possible causes:")),console.log(chalk.gray(" · Token expired or revoked")),console.log(chalk.gray(" · Network down (check connection)")),console.log(chalk.gray(" · Typo in token (check 0/O, 1/I/L, V/U)"))),void console.log();if(console.log(chalk.green(` ✓ Token valid · curriculum: ${o.curriculumId} · status: ${o.status}`)+(!0===o.aiEnabled?chalk.bold.magenta(" · ⭐ AI tutor"):"")),G=!0===o.aiEnabled,console.log(chalk.gray(" Loading curriculum… (downloads ~1 MB; up to a minute on slow venue Wi-Fi — please wait)")),K=await u(o.curriculumId||"LEARNDEMO01",H),!K)return Q(`Could not open a learn curriculum for token '${H}'.`),console.log(),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${H}`)+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 ${H}`)+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(!K)return Q(`Unknown learn token: ${H}`),console.log(),console.log(chalk.gray(" Available tokens:")),console.log(chalk.gray(" ")+chalk.bold.green("LEARNDEMO01")+chalk.gray(" free 11-card demo (anyone can use)")),console.log(chalk.gray(" ")+chalk.bold.yellow("EA|EI|AC|CA|IO|IA")+chalk.gray(" + 8 chars full curriculum (issued by team leader)")),console.log(),console.log(chalk.gray(" Tracks: ")+chalk.cyan("EA")+chalk.gray("=ctf4eai · ")+chalk.cyan("AC")+chalk.gray("=ai4ctf · ")+chalk.cyan("CA")+chalk.gray("=ctf4ai · ")+chalk.cyan("IO")+chalk.gray("=ioai · ")+chalk.cyan("IA")+chalk.gray("=iaio")),console.log(),console.log(chalk.gray(" To get a full curriculum, email ")),console.log(chalk.gray(" ")+chalk.cyan("australia@icoa2026.au")+chalk.gray(" or ask your country's team leader.")),void console.log();let Y=v(),oo=!1;try{Y&&Y.token===H?I(Y):(Y=C(H,K.id,K.totalCards),oo=!0),A(Y),P(K,Y,oo)}catch{return Q("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 ${H}`)+chalk.gray(".")),void console.log()}const eo=n(),ro=null!==eo,no=ro?eo.listeners("line").slice():[];ro&&eo.removeAllListeners("line");const to=ro?eo:e({input:process.stdin,output:process.stdout,terminal:!0}),lo=ro?to.prompt:null;ro&&(to.prompt=o.prototype.prompt);const ao=()=>{ro&&lo&&(to.prompt=lo)},so=()=>{to.setPrompt(ho?chalk.bold.green("icoa ai4ioai> "):chalk.bold.cyan(mo?"icoa learn ai> ":"icoa learn> ")),to.prompt()};let io=null,co=null,uo=null,go=null,mo=!1,yo=!1,ho=!1;const fo=new Map,po=()=>{let o=fo.get(Y.currentCard);return o||(o=[],fo.set(Y.currentCard,o)),o};let wo=!1;const bo=new Set;let ko=!1,vo=0;const Ao=(b().language||"en").toLowerCase().startsWith("zh");z(G),U(bo);const Co=[];let Io=null;const $o=new Promise(o=>{Io=o}),Eo=()=>{const o=Io;Io=null,o?.()},_o="1"===process.env.ICOA_LEARN_EMBEDDED,To=o=>K.cards.find(e=>e.number===o),Mo=o=>{const e=K.modules.find(e=>e.cardRange[1]===o);if(!e)return!1;const r=K.modules.find(o=>o.number===e.number+1);return F(K,Y,e,r),!0},Po=()=>{ko=!0,console.log(chalk.gray(Ao?" 按 Enter 进入下一阶段…":" Press Enter to continue to the next phase…"))},xo=()=>{const o=To(Y.currentCard);if(!o)return console.log(),console.log(chalk.gray(" No more cards in this curriculum.")),console.log(chalk.gray(" Type ")+chalk.bold.green("status")+chalk.gray(" for the dashboard or ")+chalk.bold.green("quit")+chalk.gray(" to exit.")),void console.log();switch(o.type){case"knowledge":x(o,K),o.check?(uo=o.number,vo=Date.now()):($(Y,o.number),A(Y));break;case"mcq":L(o,K),io=o.number;break;case"practical":D(o,K),co=o.number;break;case"sim_demo":q(o,K),$(Y,o.number),A(Y);break;case"milestone":j(o,K),M(Y,o.badge),$(Y,o.number),A(Y);break;case"arena_task":N(o,K),$(Y,o.number),A(Y);break;case"coming_soon":R(o,K),$(Y,o.number),A(Y)}},Lo=()=>{H.endsWith("DEMO01")?console.log(chalk.yellow(Ao?" 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(Ao?" 此 token 未开通 AI 导师(高级功能)。":" This token does not include the AI tutor (premium).")),console.log(chalk.gray(Ao?" 找你的领队 / 协调人换开通 AI 的 token。":" Ask your team leader / coordinator for an AI-enabled token."))),console.log()},Oo=async(o,e)=>{if(G){yo=!0;try{console.log(),console.log(chalk.gray("deep"===e?Ao?" 🤖 AI 导师(深度思考中,思考过程为灰色小字)…":" 🤖 AI tutor (deepthink — reasoning shown in gray)…":Ao?" 🤖 AI 导师(快速模式)…":" 🤖 AI tutor (quick mode)…")),console.log();let r=!1,n=!1;const t=await h({token:H,cardNumber:Y.currentCard,question:o,mode:e,history:po().slice(-8)},o=>{"reasoning"===o.kind?(r||(process.stdout.write(" "),r=!0),process.stdout.write(chalk.dim(o.text.replace(/\n/g,"\n ")))):(n||(process.stdout.write(r?"\n\n ":" "),n=!0),process.stdout.write(chalk.white(o.text.replace(/\n/g,"\n "))))});(r||n)&&console.log(),t.ok?t.offtopic?(console.log(),console.log(chalk.yellow(Ao?" (被判定与本卡主题无关 — 跑题的问题不会被回答,并会拉低本卡的提问评分)":" (judged unrelated to this card — off-topic questions aren't answered and will lower this card's prompt score)"))):(po().push({role:"user",content:o},{role:"assistant",content:t.answer}),null!=t.spent&&null!=t.cap&&t.spent/t.cap>.8&&console.log(chalk.gray(` (${Ao?"AI 额度":"AI budget"}: ${Math.round(t.spent/1e3)}k / ${Math.round(t.cap/1e3)}k tokens)`))):(console.log(),console.log(chalk.yellow(` ${t.message||(Ao?"AI 导师暂不可用。":"AI tutor unavailable.")}`))),null!==uo&&console.log(chalk.gray(Ao?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()}finally{yo=!1}}else Lo()},Do=o=>o.match(/.{1,64}(\s|$)/g)?.map(o=>o.trim())??[o],So=async o=>{const e=o.trim().toLowerCase();if(!yo){if(null!==go){const e=o.trim(),r=go;if(go=null,""===e)console.log(chalk.gray(Ao?" 已取消(未提交)。":" Cancelled (nothing submitted)."));else{const o=vo>0?Date.now()-vo:void 0;Co.push(m(H,{curriculum_id:K.id,card_number:r,feedback_text:e,time_on_card_ms:o}).catch(()=>{})),console.log(chalk.green(Ao?" ✓ 已收到,谢谢帮助完善这张卡。":" ✓ Thanks — your report was recorded."))}return console.log(),void so()}if(wo){wo=!1;const e=o.trim().toLowerCase();return"y"===e||"yes"===e?await(async()=>{yo=!0;try{console.log(),console.log(chalk.gray(Ao?" 📊 提交中 — Panda 正在评审你的提问(通常几十秒,最长一两分钟,请稍候)…":" 📊 Submitting — Panda is reviewing how you asked (usually well under two minutes)…"));const o=await f({token:H,cardNumber:Y.currentCard});if(console.log(),!o.ok)return console.log(chalk.yellow(` ${o.message||(Ao?"评审暂不可用。":"Review unavailable.")}`)),void console.log();(o=>{const e="green"===o.tier?chalk.bold.green:"blue"===o.tier?chalk.bold.cyan:"yellow"===o.tier?chalk.bold.yellow:chalk.bold.red;if(console.log(chalk.gray(" ─────────────────────────────────────────")),console.log(chalk.white(Ao?" 提问评分 ":" Prompt score ")+e(`${o.score??0} / 100`)+chalk.gray(" "+(o=>{switch(o){case"green":return"🟢 "+(Ao?"你把 AI 当作被审视的研究伙伴":"you used the AI as a research partner");case"blue":return"🔵 "+(Ao?"你把 AI 当作实验助手":"you used the AI as a lab assistant");case"yellow":return"🟡 "+(Ao?"你把 AI 当作讲师":"you used the AI as a lecturer");default:return"🔴 "+(Ao?"你把 AI 当作答案机":"you used the AI as an answer machine")}})(o.tier))),o.manipulation&&console.log(chalk.red(Ao?" ⚠ 检测到试图操纵评分,分数已按规则大幅降低":" ⚠ an attempt to manipulate the score was detected — the score was reduced sharply")),null!=o.rank&&null!=o.entrants){const e=!0===o.inTop3,r=Ao?` 本卡排名 #${o.rank} / ${o.entrants}`:` Rank on this card: #${o.rank} of ${o.entrants}`;console.log((e?chalk.bold.green(r):chalk.white(r))+(e?chalk.bold.magenta(Ao?" — 进入 TOP 3!输入 /view 看另两名的提问":" — TOP 3! type /view to read the other top prompts"):""))}if(console.log(chalk.gray(Ao?` 第 ${o.submissionIndex??"?"} / 2 次提交 · 剩余 ${o.submissionsLeft??0} 次(可先追加 /ai 对话再交)`:` Attempt ${o.submissionIndex??"?"} of 2 · ${o.submissionsLeft??0} left (extend the /ai conversation before your next one)`)),o.feedback){console.log();for(const e of Do(o.feedback))console.log(chalk.white(" "+e))}o.improve&&(console.log(),console.log(chalk.cyan(Ao?" 改进提问: ":" Ask better: ")+chalk.gray(o.improve))),console.log(chalk.gray(" ─────────────────────────────────────────")),null!=o.spent&&null!=o.cap&&o.spent/o.cap>.8&&console.log(chalk.gray(` (${Ao?"AI 额度":"AI budget"}: ${Math.round(o.spent/1e3)}k / ${Math.round(o.cap/1e3)}k tokens)`)),console.log()})(o),!0===o.inTop3?bo.add(Y.currentCard):bo.delete(Y.currentCard);const e=await w({token:H,curriculumId:K.id,cardNumber:Y.currentCard});e.ok&&e.totals&&e.chapters&&(J({totals:e.totals,chapters:e.chapters,floor:e.floor}),console.log())}finally{yo=!1}})():(console.log(chalk.gray(Ao?" 已取消(未提交,不消耗次数)。":" Cancelled — nothing submitted, no attempt used.")),console.log()),null!==uo&&(console.log(chalk.gray(Ao?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()),void so()}if(ko)return ko=!1,xo(),void so();if(e){if(ho)return"exit"===await t(o.trim())&&(ho=!1),void so();if(mo){if(["q","quit","exit","back","b","menu"].includes(e))return mo=!1,console.log(chalk.gray(Ao?" 已退出 AI 导师,回到卡片。":" Left the AI tutor — back to the card.")),console.log(),void so();const r=o.trim().startsWith("!"),n=r?o.trim().slice(1).trim():o.trim();return n&&await Oo(n,r?"fast":"deep"),void so()}if("ai4ioai"===e||"/ai4ioai"===e)return await l()&&(ho=!0),void so();if("/ai"===e||"/ai!"===e||e.startsWith("/ai ")||e.startsWith("/ai! ")){if(!G)return Lo(),void so();const r=e.startsWith("/ai!"),n=o.trim().slice(r?4:3).trim();if(!n){mo=!0;const o=To(Y.currentCard),e=o?o.title??o.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 🤖 AI ")+chalk.bold.white(Ao?"AI 导师":"AI tutor")+(e?chalk.gray(Ao?` — 主题「${e}」`:` — topic “${e}”`):"")),console.log(chalk.gray(Ao?" 直接输入问题(默认深度思考) · ! 开头 = 快答 · q = 返回卡片":" Type a question (deepthink by default) · prefix ! = quick answer · q = back to card")),console.log(),void so()}return await Oo(n,r?"fast":"deep"),void so()}if("/submit"===e||e.startsWith("/submit ")||"/research"===e||e.startsWith("/research ")){if(!G)return Lo(),void so();const o=To(Y.currentCard),e=o?o.title??o.badge??"":"";return console.log(),console.log(chalk.bold.magenta(" 📊 ")+chalk.bold.white(Ao?"Panda 提问评审":"Panda's prompt review")+(e?chalk.gray(Ao?` — 主题「${e}」`:` — topic “${e}”`):"")),console.log(chalk.gray(Ao?" 评的是你在本卡上「怎么问 AI」— 你与 /ai 导师的整段对话会被":" It scores HOW you asked the AI on this card — your whole /ai")),console.log(chalk.gray(Ao?" 整体评审,给出分数、名次和改进建议。":" conversation is reviewed for a score, a rank, and advice.")),console.log(chalk.gray(Ao?" 每张卡最多 2 次评分;评出的最好成绩进本卡 Top-3 榜。":" Max 2 graded attempts per card; your best enters the card’s top-3 board.")),console.log(),console.log(chalk.white(Ao?" 确认提交本卡对话评分?":" Submit this card’s conversation for grading?")+chalk.gray(Ao?" y = 提交 · 其他 = 取消":" y = submit · anything else = cancel")),console.log(),wo=!0,void so()}if("/view"===e)return G?(await(async()=>{yo=!0;try{console.log();const o=await p({token:H,cardNumber:Y.currentCard});if(!o.ok)return console.log(chalk.yellow(` ${o.message||(Ao?"暂时无法查看。":"View unavailable right now.")}`)),"not_top3"===o.code&&bo.delete(Y.currentCard),void console.log();null!=o.myRank&&o.myRank<=3&&bo.add(Y.currentCard),console.log(chalk.bold.white(Ao?` 🏆 本卡 Top-3 提问 — 第 ${Y.currentCard} 张 · ${o.entrants??0} 人上榜`:` 🏆 Top-3 prompts — card ${Y.currentCard} · ${o.entrants??0} entrant(s)`));for(const e of o.top3??[]){const o="green"===e.tier?"🟢":"blue"===e.tier?"🔵":"yellow"===e.tier?"🟡":"🔴";console.log(),console.log(chalk.bold.cyan(` ── Rank ${e.rank}`)+(e.me?chalk.bold.magenta(" (me)"):"")+chalk.white(` · ${e.score}`)+` ${o}`),e.questions.forEach((o,e)=>{const r=Do(o);console.log(chalk.gray(` Q${e+1} `)+chalk.white(r[0]??""));for(const o of r.slice(1))console.log(chalk.white(" "+o))}),0===e.questions.length&&console.log(chalk.gray(Ao?" (无可显示的提问)":" (no questions to show)"))}console.log()}finally{yo=!1}})(),void so()):(Lo(),void so());if("/honors"===e||"honors"===e)return X?(console.log(),console.log(chalk.gray(Ao?" 荣誉榜属于正式 learn token — 用团队签发的 token 进来即可查看和参赛。":" The honor board belongs to full learn tokens — join with a team-issued token to view and compete.")),console.log(),void so()):(await(async()=>{yo=!0;try{console.log();const o=await w({token:H,curriculumId:K.id,cardNumber:Y.currentCard});if(!o.ok||!o.totals||!o.chapters)return console.log(chalk.yellow(` ${o.message||(Ao?"荣誉榜暂时不可用。":"Honor board unavailable right now.")}`)),void console.log();if(console.log(chalk.bold.white(Ao?` 🏆 本卡实时 Top-3 — 第 ${Y.currentCard} 张`:` 🏆 Live top-3 — card ${Y.currentCard}`)),o.board&&0!==o.board.length)for(const e of o.board){const o="green"===e.tier?"🟢":"blue"===e.tier?"🔵":"yellow"===e.tier?"🟡":"🔴";console.log(chalk.white(` ${e.icon} `)+chalk.bold.cyan(`#${e.rank}`)+chalk.white(` · ${e.score} `)+o+(e.me?chalk.bold.magenta(" (me)"):""))}else console.log(chalk.gray(Ao?" (本卡还没有人上榜)":" (no one has medaled on this card yet)"));console.log(),J({totals:o.totals,chapters:o.chapters,floor:o.floor}),console.log()}finally{yo=!1}})(),void so());if("menu"!==e&&"menu confirm"!==e)if("quit"!==e&&"exit"!==e&&"q"!==e){if("status"===e)return W(K,Y),void so();if("lang"===e||e.startsWith("lang ")){const o=e.slice(4).trim();if(!o)return console.log(chalk.gray(" 当前语言 / current: ")+chalk.white(b().language||"en")+chalk.gray(" · ")+chalk.bold.green("lang zh")+chalk.gray(" / ")+chalk.bold.green("lang en")),void so();const r=K.cards.some(o=>null!=o._zh),n=o.toLowerCase().startsWith("zh")?"zh":o.toLowerCase(),t="en"===n||"zh"===n&&r,l=t?n:"en";return k({language:l}),t?console.log(chalk.green(" ✓ ")+chalk.gray("语言 / language → ")+chalk.white(l)):console.log(chalk.yellow(" ! ")+chalk.gray(`${o} 本课暂无翻译,已用英文 / not available — using English`)),uo=null,io=null,co=null,go=null,ko=!1,wo=!1,xo(),void so()}if("sim"===e){const o=To(Y.currentCard);return o&&"sim_demo"===o.type?(function(o){const e=function(){const o=s(c(import.meta.url)),e=[i(o,"..","..","panda","mujoco-launcher.py"),i(o,"..","..","..","panda","mujoco-launcher.py")];for(const o of e)if(a(o))return o;return null}();if(!e)return console.log(chalk.yellow(" MuJoCo launcher not found.")),console.log(chalk.gray(" Get it from: https://github.com/newaipanda/ICOA_CLI/blob/main/panda/mujoco-launcher.py")),void console.log(chalk.gray(" Or use the sandbox-vla docker image (Phase 3)."));const n={baseline:"baseline",prompt_injected:"prompt_inj",patch_attacked:"patch",modality_confused:"confused"}[o]||"baseline";console.log(chalk.gray(` Launching MuJoCo viewer (scenario: ${n})...`)),console.log(chalk.gray(" Close the window or press ESC to return to learn mode.")),r("python3",[e,n,"--seconds","5"],{stdio:"inherit"}).on("exit",o=>{0!==o?console.log(chalk.yellow(` MuJoCo exited with code ${o} (install: pip install mujoco)`)):console.log(chalk.gray(" Returned from sim."))})}(o.simAction),void so()):(console.log(chalk.gray(" (sim only available on simulation cards)")),void so())}if("bookmark"===e){const o=Y.currentCard;return Y.bookmarks.includes(o)||Y.bookmarks.push(o),A(Y),console.log(chalk.gray(` ✓ Card ${o} bookmarked.`)),void so()}if("e"===e)return go=Y.currentCard,console.log(chalk.gray(Ao?" 描述问题(回车提交 / 空行取消):":" Describe the issue (Enter to submit / blank to cancel):")),void so();if("back"===e||"b"===e)return Y.currentCard>1&&(Y.currentCard-=1),io=null,co=null,uo=null,A(Y),xo(),void so();if(e.startsWith("card ")){const o=e.slice(5).trim(),r=parseInt(o,10);return!Number.isInteger(r)||r<1||r>K.totalCards?(console.log(chalk.yellow(` Card number must be 1..${K.totalCards}. Try: `)+chalk.bold.green("card 1")),void so()):(Y.currentCard=r,io=null,co=null,uo=null,A(Y),xo(),void so())}if(null!==uo&&["y","yes","n","no"].includes(e)){const o=To(uo);if(o&&"knowledge"===o.type&&o.check){const r=e.startsWith("y")?"y":"n",n=r===o.check.answer,t=Date.now()-vo;$(Y,o.number),_(Y,o.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),A(Y);const{renderCheckFeedback:l}=await import("../lib/learn-render.js");if(l(o,r,n,!1),Co.push(g(H,{card_number:o.number,event_type:"check_answered",check_answer:r,check_correct:n,time_on_card_ms:t}).catch(()=>{})),uo=null,o.solution&&o.solution.length>0)return void so();if(Y.currentCard<K.totalCards){const o=Y.currentCard;Y.currentCard+=1,A(Y),Mo(o)?Po():xo(),so()}else B(K,Y),so();return}}if(null!==io&&["a","b","c","d"].includes(e)){const o=To(io);if(o&&"mcq"===o.type){const r=e.toUpperCase(),n=r===o.answer;return E(Y,o.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),$(Y,o.number),A(Y),O(o,r,n,Y),Co.push(g(H,{card_number:o.number,event_type:"mcq_answered",mcq_answer:r,mcq_correct:n}).catch(()=>{})),io=null,void(n?Y.currentCard<K.totalCards?(Y.currentCard+=1,A(Y),xo()):(B(K,Y),so()):so())}}if(null!==co){if("done"===e){const o=To(co);if(o&&"practical"===o.type)return T(Y,o.number),$(Y,o.number),A(Y),S(o),co=null,void so()}if("skip"===e)return $(Y,co),A(Y),console.log(chalk.gray(" Skipped (counts as not completed).")),console.log(),co=null,void so()}if("ok"===e||"next"===e||"continue"===e||"n"===e){if(null!==io)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void so();if(null!==co)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void so();if(null!==uo)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void so();const o=Y.currentCard;return Y.currentCard+=1,A(Y),Y.currentCard>K.totalCards?B(K,Y):Mo(o)?Po():xo(),void so()}if(null!==uo)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void so();if(null!==io)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void so();if(null!==co)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void so();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")),so()}else if(Co.length>0&&await Promise.race([Promise.allSettled(Co),new Promise(o=>setTimeout(o,5e3))]),console.log(),console.log(chalk.gray(" Saved. See you next session.")),console.log(chalk.gray(" Streak: ")+chalk.yellow(`🔥 ${Y.streakDays} day(s)`)),console.log(),ro){to.removeAllListeners("line"),ao();for(const o of no)to.on("line",o);to.prompt(),Eo()}else to.removeAllListeners("line"),to.close();else if(Co.length>0&&await Promise.race([Promise.allSettled(Co),new Promise(o=>setTimeout(o,3e3))]),to.removeAllListeners("line"),ao(),ro||!_o){const{returnToMainMenu:o}=await import("../lib/menu-nav.js");o(ro?to:void 0),Eo()}else to.close()}else so()}};to.on("line",o=>{So(o).catch(o=>{console.log(),console.log(chalk.red(" ⚠ Something went wrong handling that input.")),console.log(chalk.gray(` ${o instanceof Error?o.message:String(o)}`)),console.log(chalk.gray(" Progress is saved. Type ")+chalk.bold.green("ok")+chalk.gray(" to continue or ")+chalk.bold.green("quit")+chalk.gray(" to exit.")),so()})}),ro||to.on("close",async()=>{Co.length>0&&await Promise.race([Promise.allSettled(Co),new Promise(o=>setTimeout(o,5e3))]),_o?Eo():process.exit(0)});try{xo(),so()}catch{if(Q("Could not render this card — the curriculum may be malformed."),ro){to.removeAllListeners("line"),ao();for(const o of no)to.on("line",o);to.prompt()}else to.close();Eo()}await $o}),V.command("learn-pull [id]").description("Pre-download learn curricula to ~/.icoa/learn-cache/ for offline use (default: all)").action(async o=>{const e=o?[o.trim()]:["LEARNDEMO01","AI4CTFDEMO01","CTF4AIDEMO01","ai4ctf-96","ai4ctf-360","ctf4ai-96","ctf4ai-360","ctf4eai-96","ctf4eai-360","embodied-ai-100","embodied-ai-480"];console.log(),console.log(chalk.bold.cyan(" ICOA learn-pull — fetching curricula for offline use")),console.log(chalk.gray(" Server: https://practice.icoa2026.au")),console.log();let r=0,n=0,t=0;for(const o of e){process.stdout.write(` ${o.padEnd(22)} `);const e=Date.now(),l=await u(o),a=Date.now()-e;if(l){const o=l.cards,e=Array.isArray(o)?o.length:0,n=JSON.stringify(l).length;t+=n,console.log(chalk.green("✓ ")+chalk.gray(`${e.toString().padStart(3)} cards · ${(n/1024).toFixed(1).padStart(6)} KB · ${a.toString().padStart(4)} ms`)),r++}else console.log(chalk.red("✗ failed (server unreachable or unknown id)")),n++}console.log(),console.log(chalk.gray(" Summary: ")+chalk.green.bold(`${r} cached`)+chalk.gray(` · ${n} failed · total ${(t/1024).toFixed(0)} KB`)),console.log(chalk.gray(" Cache: ")+chalk.cyan("~/.icoa/learn-cache/")),console.log()})}
|
|
1
|
+
import chalk from"chalk";import{Interface as e,createInterface as o}from"node:readline";import{spawn as r}from"node:child_process";import{getMainRl as n}from"../lib/main-rl.js";import{ai4ioaiPromptLabel as t,handleAi4ioaiMessage as l,startAi4ioaiChat as a}from"./ai4ioai.js";import{existsSync as s}from"node:fs";import{dirname as i,join as c}from"node:path";import{fileURLToPath as u}from"node:url";import{loadCurriculumById as d,validateEAToken as g,syncProgress as m,syncCardFeedback as y}from"../lib/learn-curricula.js";import{normalizeTokenBody as h}from"../lib/token-format.js";import{streamLearnTutor as f,submitPqs as p,viewPqsTop as w,viewHonors as b}from"../lib/learn-ai.js";import{getConfig as k,saveConfig as v}from"../lib/config.js";import{loadLearnState as A,saveLearnState as C,newLearnState as I,updateStreak as $,markCardComplete as E,recordMCQ as _,recordCheck as T,markPracticalComplete as M,addAchievement as P}from"../lib/learn-state.js";import{renderWelcome as x,renderKnowledgeCard as L,renderMCQCard as O,renderMCQFeedback as D,renderPracticalCard as S,renderPracticalSuccess as q,renderSimDemoCard as j,renderMilestone as N,renderArenaTaskCard as R,renderComingSoonCard as W,renderStatus as F,renderPhaseComplete as B,renderCurriculumComplete as z,setLearnAiEnabled as U,setLearnPqsViewCards as J,renderHonorWall as Q}from"../lib/learn-render.js";import{printError as V}from"../lib/ui.js";export function registerLearnCommand(H){H.command("learn [token]").description("Enter learn mode (free demo, or team-issued EA/EI/AC/CA/IO/IA token for full curriculum)").action(async H=>{H&&H.trim()||(console.log(),console.log(chalk.gray(" No token given — starting free 11-card demo (")+chalk.bold.green("LEARNDEMO01")+chalk.gray(").")),console.log(chalk.gray(" Full curriculum: ")+chalk.bold.yellow("learn EA|EI|AC|CA|IO|IA + 8 chars")+chalk.gray(" — token from your country team leader.")),console.log(),H="LEARNDEMO01");let K=H.trim().toUpperCase(),X=null;const Z=/^(LEARNDEMO01|AI4CTFDEMO01|CTF4AIDEMO01)$/i.test(K),G=/^[A-Z]{2}[A-Z0-9]{8}$/i.test(K);let Y=!1;if(Z)console.log(),console.log(chalk.gray(" Loading demo curriculum...")),X=await d(K,K);else if(G){console.log(),console.log(chalk.gray(" Validating token..."));let e=await g(K);if(!e.ok){const o=h(K);if(o!==K){const r=await g(o);r.ok&&(console.log(chalk.yellow(` Corrected look-alike characters: ${K} → ${o}`)),e=r,K=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"):"")),Y=!0===e.aiEnabled,console.log(chalk.gray(" Loading curriculum… (downloads ~1 MB; up to a minute on slow venue Wi-Fi — please wait)")),X=await d(e.curriculumId||"LEARNDEMO01",K),!X)return V(`Could not open a learn curriculum for token '${K}'.`),console.log(),console.log(chalk.gray(" If this is an ")+chalk.bold.yellow("EXAM token")+chalk.gray(", start it with ")+chalk.bold.green(`exam ${K}`)+chalk.gray(" instead.")),console.log(chalk.gray(" Otherwise: the full curriculum is ~3 MB; on a slow link the download")),console.log(chalk.gray(" can take a while — stay on a stable connection and run ")+chalk.bold.yellow(`learn ${K}`)+chalk.gray(" again")),console.log(chalk.gray(" (it caches after the first load). If it keeps failing, check ")+chalk.cyan("https://practice.icoa2026.au")+chalk.gray(" is reachable.")),void console.log()}if(!X)return V(`Unknown learn token: ${K}`),console.log(),console.log(chalk.gray(" Available tokens:")),console.log(chalk.gray(" ")+chalk.bold.green("LEARNDEMO01")+chalk.gray(" free 11-card demo (anyone can use)")),console.log(chalk.gray(" ")+chalk.bold.yellow("EA|EI|AC|CA|IO|IA")+chalk.gray(" + 8 chars full curriculum (issued by team leader)")),console.log(),console.log(chalk.gray(" Tracks: ")+chalk.cyan("EA")+chalk.gray("=ctf4eai · ")+chalk.cyan("AC")+chalk.gray("=ai4ctf · ")+chalk.cyan("CA")+chalk.gray("=ctf4ai · ")+chalk.cyan("IO")+chalk.gray("=ioai · ")+chalk.cyan("IA")+chalk.gray("=iaio")),console.log(),console.log(chalk.gray(" To get a full curriculum, email ")),console.log(chalk.gray(" ")+chalk.cyan("australia@icoa2026.au")+chalk.gray(" or ask your country's team leader.")),void console.log();let ee=A(),oe=!1;try{ee&&ee.token===K?$(ee):(ee=I(K,X.id,X.totalCards),oe=!0),C(ee),x(X,ee,oe)}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 ${K}`)+chalk.gray(".")),void console.log()}const re=n(),ne=null!==re,te=ne?re.listeners("line").slice():[];ne&&re.removeAllListeners("line");const le=ne?re:o({input:process.stdin,output:process.stdout,terminal:!0}),ae=ne?le.prompt:null;ne&&(le.prompt=e.prototype.prompt);const se=()=>{ne&&ae&&(le.prompt=ae)},ie=()=>{le.setPrompt(he?chalk.bold.green(`icoa ${t()}> `):chalk.bold.cyan(me?"icoa learn ai> ":"icoa learn> ")),le.prompt()};let ce=null,ue=null,de=null,ge=null,me=!1,ye=!1,he=!1;const fe=new Map,pe=()=>{let e=fe.get(ee.currentCard);return e||(e=[],fe.set(ee.currentCard,e)),e};let we=!1;const be=new Set;let ke=!1,ve=0;const Ae=(k().language||"en").toLowerCase().startsWith("zh");U(Y),J(be);const Ce=[];let Ie=null;const $e=new Promise(e=>{Ie=e}),Ee=()=>{const e=Ie;Ie=null,e?.()},_e="1"===process.env.ICOA_LEARN_EMBEDDED,Te=e=>X.cards.find(o=>o.number===e),Me=e=>{const o=X.modules.find(o=>o.cardRange[1]===e);if(!o)return!1;const r=X.modules.find(e=>e.number===o.number+1);return B(X,ee,o,r),!0},Pe=()=>{ke=!0,console.log(chalk.gray(Ae?" 按 Enter 进入下一阶段…":" Press Enter to continue to the next phase…"))},xe=()=>{const e=Te(ee.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":L(e,X),e.check?(de=e.number,ve=Date.now()):(E(ee,e.number),C(ee));break;case"mcq":O(e,X),ce=e.number;break;case"practical":S(e,X),ue=e.number;break;case"sim_demo":j(e,X),E(ee,e.number),C(ee);break;case"milestone":N(e,X),P(ee,e.badge),E(ee,e.number),C(ee);break;case"arena_task":R(e,X),E(ee,e.number),C(ee);break;case"coming_soon":W(e,X),E(ee,e.number),C(ee)}},Le=()=>{K.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()},Oe=async(e,o)=>{if(Y){ye=!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 f({token:K,cardNumber:ee.currentCard,question:e,mode:o,history:pe().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)"))):(pe().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!==de&&console.log(chalk.gray(Ae?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()}finally{ye=!1}}else Le()},De=e=>e.match(/.{1,64}(\s|$)/g)?.map(e=>e.trim())??[e],Se=async e=>{const o=e.trim().toLowerCase();if(!ye){if(null!==ge){const o=e.trim(),r=ge;if(ge=null,""===o)console.log(chalk.gray(Ae?" 已取消(未提交)。":" Cancelled (nothing submitted)."));else{const e=ve>0?Date.now()-ve:void 0;Ce.push(y(K,{curriculum_id:X.id,card_number:r,feedback_text:o,time_on_card_ms:e}).catch(()=>{})),console.log(chalk.green(Ae?" ✓ 已收到,谢谢帮助完善这张卡。":" ✓ Thanks — your report was recorded."))}return console.log(),void ie()}if(we){we=!1;const o=e.trim().toLowerCase();return"y"===o||"yes"===o?await(async()=>{ye=!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 p({token:K,cardNumber:ee.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 De(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?be.add(ee.currentCard):be.delete(ee.currentCard);const o=await b({token:K,curriculumId:X.id,cardNumber:ee.currentCard});o.ok&&o.totals&&o.chapters&&(Q({totals:o.totals,chapters:o.chapters,floor:o.floor}),console.log())}finally{ye=!1}})():(console.log(chalk.gray(Ae?" 已取消(未提交,不消耗次数)。":" Cancelled — nothing submitted, no attempt used.")),console.log()),null!==de&&(console.log(chalk.gray(Ae?" (本卡的判断题还等着你作答 — y 或 n)":" (the y/n check on this card is still waiting — answer y or n)")),console.log()),void ie()}if(ke)return ke=!1,xe(),void ie();if(o){if(he)return"exit"===await l(e.trim())&&(he=!1),void ie();if(me){if(["q","quit","exit","back","b","menu"].includes(o))return me=!1,console.log(chalk.gray(Ae?" 已退出 AI 导师,回到卡片。":" Left the AI tutor — back to the card.")),console.log(),void ie();const r=e.trim().startsWith("!"),n=r?e.trim().slice(1).trim():e.trim();return n&&await Oe(n,r?"fast":"deep"),void ie()}if("ai4ioai"===o||"/ai4ioai"===o)return await a()&&(he=!0),void ie();if("/ai"===o||"/ai!"===o||o.startsWith("/ai ")||o.startsWith("/ai! ")){if(!Y)return Le(),void ie();const r=o.startsWith("/ai!"),n=e.trim().slice(r?4:3).trim();if(!n){me=!0;const e=Te(ee.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 ie()}return await Oe(n,r?"fast":"deep"),void ie()}if("/submit"===o||o.startsWith("/submit ")||"/research"===o||o.startsWith("/research ")){if(!Y)return Le(),void ie();const e=Te(ee.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(),we=!0,void ie()}if("/view"===o)return Y?(await(async()=>{ye=!0;try{console.log();const e=await w({token:K,cardNumber:ee.currentCard});if(!e.ok)return console.log(chalk.yellow(` ${e.message||(Ae?"暂时无法查看。":"View unavailable right now.")}`)),"not_top3"===e.code&&be.delete(ee.currentCard),void console.log();null!=e.myRank&&e.myRank<=3&&be.add(ee.currentCard),console.log(chalk.bold.white(Ae?` 🏆 本卡 Top-3 提问 — 第 ${ee.currentCard} 张 · ${e.entrants??0} 人上榜`:` 🏆 Top-3 prompts — card ${ee.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=De(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{ye=!1}})(),void ie()):(Le(),void ie());if("/honors"===o||"honors"===o)return Z?(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 ie()):(await(async()=>{ye=!0;try{console.log();const e=await b({token:K,curriculumId:X.id,cardNumber:ee.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 — 第 ${ee.currentCard} 张`:` 🏆 Live top-3 — card ${ee.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{ye=!1}})(),void ie());if("menu"!==o&&"menu confirm"!==o)if("quit"!==o&&"exit"!==o&&"q"!==o){if("status"===o)return F(X,ee),void ie();if("lang"===o||o.startsWith("lang ")){const e=o.slice(4).trim();if(!e)return console.log(chalk.gray(" 当前语言 / current: ")+chalk.white(k().language||"en")+chalk.gray(" · ")+chalk.bold.green("lang zh")+chalk.gray(" / ")+chalk.bold.green("lang en")),void ie();const r=X.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 v({language:l}),t?console.log(chalk.green(" ✓ ")+chalk.gray("语言 / language → ")+chalk.white(l)):console.log(chalk.yellow(" ! ")+chalk.gray(`${e} 本课暂无翻译,已用英文 / not available — using English`)),de=null,ce=null,ue=null,ge=null,ke=!1,we=!1,xe(),void ie()}if("sim"===o){const e=Te(ee.currentCard);return e&&"sim_demo"===e.type?(function(e){const o=function(){const e=i(u(import.meta.url)),o=[c(e,"..","..","panda","mujoco-launcher.py"),c(e,"..","..","..","panda","mujoco-launcher.py")];for(const e of o)if(s(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 n={baseline:"baseline",prompt_injected:"prompt_inj",patch_attacked:"patch",modality_confused:"confused"}[e]||"baseline";console.log(chalk.gray(` Launching MuJoCo viewer (scenario: ${n})...`)),console.log(chalk.gray(" Close the window or press ESC to return to learn mode.")),r("python3",[o,n,"--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 ie()):(console.log(chalk.gray(" (sim only available on simulation cards)")),void ie())}if("bookmark"===o){const e=ee.currentCard;return ee.bookmarks.includes(e)||ee.bookmarks.push(e),C(ee),console.log(chalk.gray(` ✓ Card ${e} bookmarked.`)),void ie()}if("e"===o)return ge=ee.currentCard,console.log(chalk.gray(Ae?" 描述问题(回车提交 / 空行取消):":" Describe the issue (Enter to submit / blank to cancel):")),void ie();if("back"===o||"b"===o)return ee.currentCard>1&&(ee.currentCard-=1),ce=null,ue=null,de=null,C(ee),xe(),void ie();if(o.startsWith("card ")){const e=o.slice(5).trim(),r=parseInt(e,10);return!Number.isInteger(r)||r<1||r>X.totalCards?(console.log(chalk.yellow(` Card number must be 1..${X.totalCards}. Try: `)+chalk.bold.green("card 1")),void ie()):(ee.currentCard=r,ce=null,ue=null,de=null,C(ee),xe(),void ie())}if(null!==de&&["y","yes","n","no"].includes(o)){const e=Te(de);if(e&&"knowledge"===e.type&&e.check){const r=o.startsWith("y")?"y":"n",n=r===e.check.answer,t=Date.now()-ve;E(ee,e.number),T(ee,e.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),C(ee);const{renderCheckFeedback:l}=await import("../lib/learn-render.js");if(l(e,r,n,!1),Ce.push(m(K,{card_number:e.number,event_type:"check_answered",check_answer:r,check_correct:n,time_on_card_ms:t}).catch(()=>{})),de=null,e.solution&&e.solution.length>0)return void ie();if(ee.currentCard<X.totalCards){const e=ee.currentCard;ee.currentCard+=1,C(ee),Me(e)?Pe():xe(),ie()}else z(X,ee),ie();return}}if(null!==ce&&["a","b","c","d"].includes(o)){const e=Te(ce);if(e&&"mcq"===e.type){const r=o.toUpperCase(),n=r===e.answer;return _(ee,e.number,{answer:r,correct:n,submittedAt:(new Date).toISOString()}),E(ee,e.number),C(ee),D(e,r,n,ee),Ce.push(m(K,{card_number:e.number,event_type:"mcq_answered",mcq_answer:r,mcq_correct:n}).catch(()=>{})),ce=null,void(n?ee.currentCard<X.totalCards?(ee.currentCard+=1,C(ee),xe()):(z(X,ee),ie()):ie())}}if(null!==ue){if("done"===o){const e=Te(ue);if(e&&"practical"===e.type)return M(ee,e.number),E(ee,e.number),C(ee),q(e),ue=null,void ie()}if("skip"===o)return E(ee,ue),C(ee),console.log(chalk.gray(" Skipped (counts as not completed).")),console.log(),ue=null,void ie()}if("ok"===o||"next"===o||"continue"===o||"n"===o){if(null!==ce)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void ie();if(null!==ue)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void ie();if(null!==de)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void ie();const e=ee.currentCard;return ee.currentCard+=1,C(ee),ee.currentCard>X.totalCards?z(X,ee):Me(e)?Pe():xe(),void ie()}if(null!==de)return console.log(chalk.yellow(" Please answer the check above (")+chalk.bold.green("y")+chalk.yellow(" or ")+chalk.bold.green("n")+chalk.yellow(").")),void ie();if(null!==ce)return console.log(chalk.yellow(" Please answer the MCQ first (A / B / C / D).")),void ie();if(null!==ue)return console.log(chalk.yellow(" Please type ")+chalk.bold.green("done")+chalk.yellow(" or ")+chalk.bold.yellow("skip")+chalk.yellow(" for the practical.")),void ie();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")),ie()}else if(Ce.length>0&&await Promise.race([Promise.allSettled(Ce),new Promise(e=>setTimeout(e,5e3))]),console.log(),console.log(chalk.gray(" Saved. See you next session.")),console.log(chalk.gray(" Streak: ")+chalk.yellow(`🔥 ${ee.streakDays} day(s)`)),console.log(),ne){le.removeAllListeners("line"),se();for(const e of te)le.on("line",e);le.prompt(),Ee()}else le.removeAllListeners("line"),le.close();else if(Ce.length>0&&await Promise.race([Promise.allSettled(Ce),new Promise(e=>setTimeout(e,3e3))]),le.removeAllListeners("line"),se(),ne||!_e){const{returnToMainMenu:e}=await import("../lib/menu-nav.js");e(ne?le:void 0),Ee()}else le.close()}else ie()}};le.on("line",e=>{Se(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.")),ie()})}),ne||le.on("close",async()=>{Ce.length>0&&await Promise.race([Promise.allSettled(Ce),new Promise(e=>setTimeout(e,5e3))]),_e?Ee():process.exit(0)});try{xe(),ie()}catch{if(V("Could not render this card — the curriculum may be malformed."),ne){le.removeAllListeners("line"),se();for(const e of te)le.on("line",e);le.prompt()}else le.close();Ee()}await $e}),H.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 d(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()})}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -88,6 +88,9 @@ export interface IcoaConfig {
|
|
|
88
88
|
competitionEndsAt: string;
|
|
89
89
|
geminiApiKey: string;
|
|
90
90
|
geminiModel: string;
|
|
91
|
+
ai4ioaiEndpoint?: string;
|
|
92
|
+
ai4ioaiApiKey?: string;
|
|
93
|
+
ai4ioaiModel?: string;
|
|
91
94
|
accessToken: string;
|
|
92
95
|
deviceFingerprint: string;
|
|
93
96
|
installSalt?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icoa-cli",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.377",
|
|
4
4
|
"description": "ICOA CLI — The world's first CLI-native cyber & AI security olympiad terminal: AI4CTF (Day 1), CTF4AI (Day 2), VLA4CTF (Pioneer Round — embodied AI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|