icoa-cli 2.19.556 → 2.19.559

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
- export const REGISTRY=[{name:"join",mapsTo:["ctf","join"],rest:!0,help:{order:0,section:"Competition",label:"join <url>",text:"Connect to CTFd"}},{name:"activate",mapsTo:["ctf","activate"],rest:!0},{name:"challenges",mapsTo:["ctf","challenges"],help:{order:1,section:"Competition",label:"challenges (ch)",text:"List challenges by category"}},{name:"ch",mapsTo:["ctf","challenges"]},{name:"open",mapsTo:["ctf","open"],rest:!0,help:{order:2,section:"Competition",label:"open <id>",text:"Read challenge + get next steps"}},{name:"submit",mapsTo:["ctf","submit"],rest:!0,help:{order:3,section:"Competition",label:"submit <id> <flag>",text:"Submit a flag"}},{name:"flag",mapsTo:["ctf","submit"],rest:!0},{name:"scoreboard",mapsTo:["ctf","scoreboard"],rest:!0,help:{order:4,section:"Competition",label:"scoreboard (sb)",text:"Live rankings"}},{name:"sb",mapsTo:["ctf","scoreboard"],rest:!0},{name:"status",mapsTo:["ctf","status"],help:{order:5,section:"Competition",label:"status",text:"Your score & timer"}},{name:"time",mapsTo:["ctf","time"],help:{order:6,section:"Competition",label:"time",text:"Countdown timer"}},{name:"ref",help:{order:8,section:"Tools",label:"ref [topic]",text:"Quick reference (linux, web, crypto...)"}},{name:"shell",help:{order:9,section:"Tools",label:"shell",text:"Interactive CTF shell (sandbox if present, else host)"}},{name:"files",help:{order:10,section:"Tools",label:"files <id>",text:"Download challenge files"}},{name:"view"},{name:"connect",help:{order:11,section:"Tools",label:"connect <id>",text:"Connect to remote target"}},{name:"challenge",subs:["start","stop","status"]},{name:"note",help:{order:12,section:"Tools",label:"note [text]",text:"Add a logbook entry (in-exam: tags current Q)"}},{name:"notes"},{name:"logbook",help:{order:13,section:"Tools",label:"logbook [n]",text:"View your logbook (in-exam: filter by Q)"}},{name:"log",help:{order:14,section:"Tools",label:"log",text:"Session history"}},{name:"man"},{name:"manual"},{name:"lang",help:{order:21,section:"System",label:"lang [code]",text:"Switch language (15 supported)"}},{name:"setup"},{name:"env"},{name:"aienv"},{name:"python",subs:["setup","status"]},{name:"ipynb"},{name:"kaggle"},{name:"teleload"},{name:"pandahelpme31"},{name:"gpufortask"},{name:"guide"},{name:"ai4ctf",help:{order:7,section:"AI Teammate",label:"ai4ctf",text:"Free-chat with your AI teammate"}},{name:"model"},{name:"ctf",subs:["challenges","open","submit","scoreboard","status","join","logout","time"]},{name:"exam",subs:["list","start","q","answer","submit","review","result","next","prev","mark","unmark","help"]},{name:"demo",mapsTo:["exam","demo"]},{name:"retry",mapsTo:["exam","demo-retry"]},{name:"nations",mapsTo:["exam","nations"]},{name:"next",mapsTo:["exam","next"]},{name:"prev",mapsTo:["exam","prev"]},{name:"continue"},{name:"logout",mapsTo:["ctf","logout"],help:{order:22,section:"System",label:"logout",text:"Disconnect"}},{name:"ctf4ai"},{name:"mark",mapsTo:["exam","mark"],rest:!0},{name:"unmark",mapsTo:["exam","unmark"],rest:!0},{name:"review",mapsTo:["exam","review"]},{name:"learn",subs:["start","continue"]},{name:"demo2"},{name:"demo4"},{name:"demo5"},{name:"gfssm"},{name:"ctf4eai"},{name:"ctf4vla"},{name:"theme"},{name:"sim"},{name:"arena",subs:["eai"]},{name:"eai"},{name:"wm"},{name:"type",subs:["15","30","60","words","python","shell","history"],help:{order:15,section:"Tools",label:"type [30]",text:"Typing speed trainer — WPM, live colours, no token"}},{name:"board"},{name:"learn-pull"},{name:"doctor",help:{order:19,section:"System",label:"doctor",text:"Self-check: version, Python, server, tools"}},{name:"bg",nav:!0,help:{order:17,section:"System",label:"bg <command>",text:"Run a shell command in the background"}},{name:"tasks",nav:!0,help:{order:18,section:"System",label:"tasks [id]",text:"Background task panel / view output"}},{name:"drafts",nav:!0,help:{order:16,section:"System",label:"drafts",text:"Review staged draft answers"}},{name:"help",nav:!0},{name:"menu",nav:!0},{name:"quit",nav:!0},{name:"q",nav:!0},{name:"exit",nav:!0,help:{order:24,section:"System",label:"exit",text:"Quit (session saved)"}},{name:"back",nav:!0},{name:"resume",nav:!0,help:{order:15,section:"System",label:"resume",text:"Continue your last learn card / exam"}},{name:"clear",nav:!0,help:{order:23,section:"System",label:"clear",text:"Clear screen"}},{name:"cls",nav:!0}];export const surface=()=>REGISTRY.map(e=>e.name);export const dispatchable=()=>REGISTRY.filter(e=>!e.nav).map(e=>e.name);export function shortcutFor(e,t){const a=REGISTRY.find(t=>t.name===e&&t.mapsTo);return a?.mapsTo?a.rest?[...a.mapsTo,...t]:[...a.mapsTo]:null}export const subcommandsOf=e=>REGISTRY.find(t=>t.name===e)?.subs;export const helpRows=e=>REGISTRY.filter(t=>t.help?.section===e).sort((e,t)=>e.help.order-t.help.order).map(e=>({label:e.help.label,text:e.help.text}));
1
+ export const REGISTRY=[{name:"join",mapsTo:["ctf","join"],rest:!0,help:{order:0,section:"Competition",label:"join <url>",text:"Connect to CTFd"}},{name:"activate",mapsTo:["ctf","activate"],rest:!0},{name:"challenges",mapsTo:["ctf","challenges"],help:{order:1,section:"Competition",label:"challenges (ch)",text:"List challenges by category"}},{name:"ch",mapsTo:["ctf","challenges"]},{name:"open",mapsTo:["ctf","open"],rest:!0,help:{order:2,section:"Competition",label:"open <id>",text:"Read challenge + get next steps"}},{name:"submit",mapsTo:["ctf","submit"],rest:!0,help:{order:3,section:"Competition",label:"submit <id> <flag>",text:"Submit a flag"}},{name:"flag",mapsTo:["ctf","submit"],rest:!0},{name:"scoreboard",mapsTo:["ctf","scoreboard"],rest:!0,help:{order:4,section:"Competition",label:"scoreboard (sb)",text:"Live rankings"}},{name:"sb",mapsTo:["ctf","scoreboard"],rest:!0},{name:"status",mapsTo:["ctf","status"],help:{order:5,section:"Competition",label:"status",text:"Your score & timer"}},{name:"time",mapsTo:["ctf","time"],help:{order:6,section:"Competition",label:"time",text:"Countdown timer"}},{name:"ref",help:{order:8,section:"Tools",label:"ref [topic]",text:"Quick reference (linux, web, crypto...)"}},{name:"shell",help:{order:9,section:"Tools",label:"shell",text:"Interactive CTF shell (sandbox if present, else host)"}},{name:"files",help:{order:10,section:"Tools",label:"files <id>",text:"Download challenge files"}},{name:"view"},{name:"connect",help:{order:11,section:"Tools",label:"connect <id>",text:"Connect to remote target"}},{name:"challenge",subs:["start","stop","status"]},{name:"note",help:{order:12,section:"Tools",label:"note [text]",text:"Add a logbook entry (in-exam: tags current Q)"}},{name:"notes"},{name:"logbook",help:{order:13,section:"Tools",label:"logbook [n]",text:"View your logbook (in-exam: filter by Q)"}},{name:"log",help:{order:14,section:"Tools",label:"log",text:"Session history"}},{name:"man"},{name:"manual"},{name:"lang",help:{order:21,section:"System",label:"lang [code]",text:"Switch language (15 supported)"}},{name:"setup"},{name:"env"},{name:"aienv"},{name:"python",subs:["setup","status"]},{name:"ipynb"},{name:"kaggle"},{name:"teleload"},{name:"pandahelpme31"},{name:"gpufortask"},{name:"guide"},{name:"ai4ctf",help:{order:7,section:"AI Teammate",label:"ai4ctf",text:"Free-chat with your AI teammate"}},{name:"model"},{name:"ctf",subs:["challenges","open","submit","scoreboard","status","join","logout","time"]},{name:"exam",subs:["list","start","q","answer","submit","review","result","next","prev","mark","unmark","help"]},{name:"demo",mapsTo:["exam","demo"]},{name:"retry",mapsTo:["exam","demo-retry"]},{name:"nations",mapsTo:["exam","nations"]},{name:"next",mapsTo:["exam","next"]},{name:"prev",mapsTo:["exam","prev"]},{name:"continue"},{name:"logout",mapsTo:["ctf","logout"],help:{order:22,section:"System",label:"logout",text:"Disconnect"}},{name:"ctf4ai"},{name:"mark",mapsTo:["exam","mark"],rest:!0},{name:"unmark",mapsTo:["exam","unmark"],rest:!0},{name:"review",mapsTo:["exam","review"]},{name:"learn",subs:["start","continue"]},{name:"demo2"},{name:"demo4"},{name:"demo5"},{name:"gfssm"},{name:"ctf4eai"},{name:"ctf4vla"},{name:"theme"},{name:"sim"},{name:"arena",subs:["eai"]},{name:"eai"},{name:"wm"},{name:"type",subs:["15","30","60","words","python","shell","history"],help:{order:15,section:"Tools",label:"type [30]",text:"Typing speed trainer — WPM, live colours, no token"}},{name:"board"},{name:"learn-pull"},{name:"doctor",help:{order:19,section:"System",label:"doctor",text:"Self-check: version, Python, network/firewall, server"}},{name:"bg",nav:!0,help:{order:17,section:"System",label:"bg <command>",text:"Run a shell command in the background"}},{name:"tasks",nav:!0,help:{order:18,section:"System",label:"tasks [id]",text:"Background task panel / view output"}},{name:"drafts",nav:!0,help:{order:16,section:"System",label:"drafts",text:"Review staged draft answers"}},{name:"help",nav:!0},{name:"menu",nav:!0},{name:"quit",nav:!0},{name:"q",nav:!0},{name:"exit",nav:!0,help:{order:24,section:"System",label:"exit",text:"Quit (session saved)"}},{name:"back",nav:!0},{name:"resume",nav:!0,help:{order:15,section:"System",label:"resume",text:"Continue your last learn card / exam"}},{name:"clear",nav:!0,help:{order:23,section:"System",label:"clear",text:"Clear screen"}},{name:"cls",nav:!0}];export const surface=()=>REGISTRY.map(e=>e.name);export const dispatchable=()=>REGISTRY.filter(e=>!e.nav).map(e=>e.name);export function shortcutFor(e,t){const a=REGISTRY.find(t=>t.name===e&&t.mapsTo);return a?.mapsTo?a.rest?[...a.mapsTo,...t]:[...a.mapsTo]:null}export const subcommandsOf=e=>REGISTRY.find(t=>t.name===e)?.subs;export const helpRows=e=>REGISTRY.filter(t=>t.help?.section===e).sort((e,t)=>e.help.order-t.help.order).map(e=>({label:e.help.label,text:e.help.text}));
package/dist/lib/http.js CHANGED
@@ -1 +1 @@
1
- const t=15e3,e=t=>new Promise(e=>setTimeout(e,t)),r=Symbol.for("icoa.http.timeout");export function isOwnTimeout(t){return"object"==typeof t&&null!==t&&!0===t[r]}function n(t){const e=new Error(`request timed out after ${t}ms`);return e[r]=!0,e}export function isRetryableStatus(t){return 429===t||502===t||503===t||504===t}export function retryDelayMs(t,e){const r=e?.headers?.get?.("retry-after");if(r){const t=Number(r);if(Number.isFinite(t)&&t>=0)return Math.min(1e3*t,32e3);const e=Date.parse(r);if(!Number.isNaN(e))return Math.max(0,Math.min(e-Date.now(),32e3))}const n=Math.min(400*2**t,8e3),o=.3*n*(2*Math.random()-1);return Math.max(0,Math.round(n+o))}function o(t,e){const r=new AbortController;let o=!1;const i=()=>r.abort(e.reason),a=setTimeout(()=>{o=!0,r.abort(n(t))},t);return e&&(e.aborted?r.abort(e.reason):e.addEventListener("abort",i,{once:!0})),{signal:r.signal,timedOut:()=>o,done:()=>{clearTimeout(a),e?.removeEventListener("abort",i)}}}function i(t){const e=t?.cause;return void 0!==e?e:t}const a=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EPIPE","ENETUNREACH"]);export function isCheapTransportFailure(t){const e=i(t),r="string"==typeof e?.code?e.code:"",n="string"==typeof e?.name?e.name:"";return!/timeout/i.test(r)&&!/timeout/i.test(n)&&a.has(r)}export async function httpFetch(r,a={},s={fetch:globalThis.fetch,sleep:e}){const{timeoutMs:u=t,retries:c,idempotent:m,signal:f,...l}=a,h=function(t){const e=(t??"GET").toUpperCase();return"GET"===e||"HEAD"===e}(l.method)||!0===m,p=1+Math.max(0,c??(h?2:0));let E;for(let t=0;t<p;t++){const e=o(u,f);try{const n=await s.fetch(r,{...l,signal:e.signal});if(t<p-1&&h&&isRetryableStatus(n.status)){await s.sleep(retryDelayMs(t,n));continue}return n}catch(r){if(E=r,f?.aborted)throw r;if(e.timedOut())throw isOwnTimeout(i(r))?r:n(u);if(!isCheapTransportFailure(r))throw r;if(!h||t===p-1)throw r;await s.sleep(retryDelayMs(t))}finally{e.done()}}throw E}
1
+ import{enableSystemTrust as t,isCertTrustError as e}from"./tls-trust.js";const r=15e3,n=t=>new Promise(e=>setTimeout(e,t)),o=Symbol.for("icoa.http.timeout");export function isOwnTimeout(t){return"object"==typeof t&&null!==t&&!0===t[o]}function i(t){const e=new Error(`request timed out after ${t}ms`);return e[o]=!0,e}export function isRetryableStatus(t){return 429===t||502===t||503===t||504===t}export function retryDelayMs(t,e){const r=e?.headers?.get?.("retry-after");if(r){const t=Number(r);if(Number.isFinite(t)&&t>=0)return Math.min(1e3*t,32e3);const e=Date.parse(r);if(!Number.isNaN(e))return Math.max(0,Math.min(e-Date.now(),32e3))}const n=Math.min(400*2**t,8e3),o=.3*n*(2*Math.random()-1);return Math.max(0,Math.round(n+o))}function a(t,e){const r=new AbortController;let n=!1;const o=()=>r.abort(e.reason),a=setTimeout(()=>{n=!0,r.abort(i(t))},t);return e&&(e.aborted?r.abort(e.reason):e.addEventListener("abort",o,{once:!0})),{signal:r.signal,timedOut:()=>n,done:()=>{clearTimeout(a),e?.removeEventListener("abort",o)}}}function s(t){const e=t?.cause;return void 0!==e?e:t}const u=new Set(["ECONNREFUSED","ECONNRESET","ENOTFOUND","EAI_AGAIN","EPIPE","ENETUNREACH"]);export function isCheapTransportFailure(t){const e=s(t),r="string"==typeof e?.code?e.code:"",n="string"==typeof e?.name?e.name:"";return!/timeout/i.test(r)&&!/timeout/i.test(n)&&u.has(r)}export async function httpFetch(o,u={},c={fetch:globalThis.fetch,sleep:n}){const{timeoutMs:m=r,retries:f,idempotent:l,signal:h,...p}=u,E=function(t){const e=(t??"GET").toUpperCase();return"GET"===e||"HEAD"===e}(p.method)||!0===l,d=1+Math.max(0,f??(E?2:0));let b,y=!1;for(let r=0;r<d;r++){const n=a(m,h);try{const t=await c.fetch(o,{...p,signal:n.signal});if(r<d-1&&E&&isRetryableStatus(t.status)){await c.sleep(retryDelayMs(r,t));continue}return t}catch(o){if(b=o,h?.aborted)throw o;if(n.timedOut())throw isOwnTimeout(s(o))?o:i(m);if(!y&&e(o)&&t()){y=!0,r--;continue}if(!isCheapTransportFailure(o))throw o;if(!E||r===d-1)throw o;await c.sleep(retryDelayMs(r))}finally{n.done()}}throw b}
@@ -1 +1 @@
1
- export function localized(e,r){if(!r.startsWith("zh")||!e._zh)return e;const t=e._zh,n={...e};for(const e of Object.keys(t))void 0!==t[e]&&(n[e]=t[e]);return n}export function isValidCurriculum(e){if(!e||"object"!=typeof e)return!1;const r=e;return"string"==typeof r.id&&"number"==typeof r.totalCards&&Array.isArray(r.cards)&&Array.isArray(r.modules)}import{readFileSync as e,writeFileSync as r,mkdirSync as t,existsSync as n,statSync as a,utimesSync as o}from"node:fs";import{join as i}from"node:path";import{homedir as c}from"node:os";import{getDeviceFingerprint as s}from"./access.js";import{getCliVersion as u}from"./version.js";import{httpFetch as l}from"./http.js";import{parseHometask as d}from"./learn-hometask.js";import{parseFaqs as p,parseReplies as m}from"./learn-replies.js";const f=i(c(),".icoa","learn-cache");export const LEARN_SERVER="https://practice.icoa2026.au";function g(e){const r=e.replace(/[^A-Za-z0-9_-]/g,"_");return i(f,`${r}.json`)}function _(e){const r=e.replace(/[^A-Za-z0-9_-]/g,"_");return i(f,`${r}.etag`)}let h=null;export function getLastCurriculumFailure(){return h}export async function loadCurriculumById(i,c,s=!1){"ctf4eai-12"===i&&(i="LEARNDEMO01");const l=function(r){const t=g(r);if(!n(t))return null;try{const r=a(t),n=Date.now()-r.mtimeMs;return{curriculum:JSON.parse(e(t,"utf-8")),ageMs:n}}catch{return null}}(i);if(!s&&l&&l.ageMs<6048e5&&isValidCurriculum(l.curriculum))return l.curriculum;h=null;const d=await async function(r,t){const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(r),o={},i=function(r){const t=_(r);if(!n(t))return null;try{return e(t,"utf-8").trim()||null}catch{return null}}(r);i&&(o["If-None-Match"]=i),t&&(o["X-Learn-Token"]=t.toUpperCase()),o["X-ICOA-Client"]=u();try{const e=await fetch(a,{headers:o,signal:AbortSignal.timeout(6e4)});if(304===e.status)return{kind:"unchanged"};if(404===e.status)return{kind:"notfound"};if(!e.ok)return{kind:"error"};const r=await e.json();if(!r.success||!r.data)return{kind:"error"};if(!isValidCurriculum(r.data))return{kind:"error"};const t=e.headers.get("etag")||e.headers.get("ETag")||void 0;return{kind:"fresh",curriculum:r.data,etag:t||void 0}}catch{return{kind:"error"}}}(i,c);return"fresh"===d.kind?(function(e,n,a){try{t(f,{recursive:!0}),r(g(e),JSON.stringify(n)),a&&r(_(e),a)}catch{}}(i,d.curriculum,d.etag),d.curriculum):"unchanged"===d.kind&&l&&isValidCurriculum(l.curriculum)?(function(e){try{const r=g(e);if(n(r)){const e=Date.now()/1e3;o(r,e,e)}}catch{}}(i),l.curriculum):l&&isValidCurriculum(l.curriculum)?l.curriculum:(h="notfound"===d.kind?"notfound":"offline",null)}export function validateResultShape(e){return{curriculumId:String(e.curriculum_id??""),status:String(e.status??""),validUntil:String(e.valid_until??""),aiEnabled:1===e.ai_enabled||!0===e.ai_enabled,gated:1===e.gated||!0===e.gated,replies:m(e.replies),hometask:d(e.hometask)}}export async function validateEAToken(e){const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/validate";try{const t=await l(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e.toUpperCase(),device_fingerprint:s()}),signal:AbortSignal.timeout(8e3)});if(!t.ok){const e=await t.json().catch(()=>({}));return{ok:!1,message:e.message||`HTTP ${t.status}`,code:e.code}}const n=await t.json();return n.success&&n.data?{ok:!0,...validateResultShape(n.data)}:{ok:!1,message:n.message||"Validation failed"}}catch(e){return{ok:!1,message:`Network error: ${e instanceof Error?e.message:String(e)}`}}}export async function syncProgress(e,r,t){if("LEARNDEMO01"===e.toUpperCase())return;const n=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+e.toUpperCase();try{await l(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({card_number:r.card_number,event_type:r.event_type,mcq_answer:r.mcq_answer,mcq_correct:r.mcq_correct?1:0,check_answer:r.check_answer,check_correct:r.check_correct?1:0,time_on_card_ms:r.time_on_card_ms,device_fingerprint:s(),reply_ack:t&&t.length>0?t:void 0}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function postFocusSession(e,r){if("LEARNDEMO01"===e.toUpperCase())return!0;const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/focus/"+e.toUpperCase();try{return(await l(t,{method:"POST",headers:{"Content-Type":"application/json","X-ICOA-Client":u()},body:JSON.stringify({...r,device_fingerprint:s()}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}export async function fetchServerProgress(e){if("LEARNDEMO01"===e.toUpperCase())return null;const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+e.toUpperCase();try{const e=await l(r,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return null;const t=await e.json();return t?.success&&Array.isArray(t.data?.events)?t.data.events:null}catch{return null}}export async function fetchCardFaqs(e){const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/faq/"+encodeURIComponent(e.toUpperCase());try{const e=await l(r,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return[];const t=await e.json();return t.success&&t.data?p(t.data.faqs):[]}catch{return[]}}export async function syncCardFeedback(e,r){const t=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/feedback/"+e.toUpperCase();try{return(await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({curriculum_id:r.curriculum_id,card_number:r.card_number,feedback_text:r.feedback_text,time_on_card_ms:r.time_on_card_ms}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}
1
+ export function localized(t,r){if(!r.startsWith("zh")||!t._zh)return t;const e=t._zh,n={...t};for(const t of Object.keys(e))void 0!==e[t]&&(n[t]=e[t]);return n}export function isValidCurriculum(t){if(!t||"object"!=typeof t)return!1;const r=t;return"string"==typeof r.id&&"number"==typeof r.totalCards&&Array.isArray(r.cards)&&Array.isArray(r.modules)}import{readFileSync as t,writeFileSync as r,mkdirSync as e,existsSync as n,statSync as a,utimesSync as o}from"node:fs";import{join as c}from"node:path";import{homedir as i}from"node:os";import{getDeviceFingerprint as s}from"./access.js";import{getCliVersion as u}from"./version.js";import{httpFetch as l}from"./http.js";import{transportAdvice as d,transportCauseCode as p}from"./tls-trust.js";import{parseHometask as m}from"./learn-hometask.js";import{parseFaqs as f,parseReplies as g}from"./learn-replies.js";const _=c(i(),".icoa","learn-cache");export const LEARN_SERVER="https://practice.icoa2026.au";function h(t){const r=t.replace(/[^A-Za-z0-9_-]/g,"_");return c(_,`${r}.json`)}function y(t){const r=t.replace(/[^A-Za-z0-9_-]/g,"_");return c(_,`${r}.etag`)}let k=null;export function getLastCurriculumFailure(){return k}export async function loadCurriculumById(c,i,s=!1){"ctf4eai-12"===c&&(c="LEARNDEMO01");const l=function(r){const e=h(r);if(!n(e))return null;try{const r=a(e),n=Date.now()-r.mtimeMs;return{curriculum:JSON.parse(t(e,"utf-8")),ageMs:n}}catch{return null}}(c);if(!s&&l&&l.ageMs<6048e5&&isValidCurriculum(l.curriculum))return l.curriculum;k=null;const d=await async function(r,e){const a=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/curriculum/"+encodeURIComponent(r),o={},c=function(r){const e=y(r);if(!n(e))return null;try{return t(e,"utf-8").trim()||null}catch{return null}}(r);c&&(o["If-None-Match"]=c),e&&(o["X-Learn-Token"]=e.toUpperCase()),o["X-ICOA-Client"]=u();try{const t=await fetch(a,{headers:o,signal:AbortSignal.timeout(6e4)});if(304===t.status)return{kind:"unchanged"};if(404===t.status)return{kind:"notfound"};if(!t.ok)return{kind:"error"};const r=await t.json();if(!r.success||!r.data)return{kind:"error"};if(!isValidCurriculum(r.data))return{kind:"error"};const e=t.headers.get("etag")||t.headers.get("ETag")||void 0;return{kind:"fresh",curriculum:r.data,etag:e||void 0}}catch{return{kind:"error"}}}(c,i);return"fresh"===d.kind?(function(t,n,a){try{e(_,{recursive:!0}),r(h(t),JSON.stringify(n)),a&&r(y(t),a)}catch{}}(c,d.curriculum,d.etag),d.curriculum):"unchanged"===d.kind&&l&&isValidCurriculum(l.curriculum)?(function(t){try{const r=h(t);if(n(r)){const t=Date.now()/1e3;o(r,t,t)}}catch{}}(c),l.curriculum):l&&isValidCurriculum(l.curriculum)?l.curriculum:(k="notfound"===d.kind?"notfound":"offline",null)}export function validateResultShape(t){return{curriculumId:String(t.curriculum_id??""),status:String(t.status??""),validUntil:String(t.valid_until??""),aiEnabled:1===t.ai_enabled||!0===t.ai_enabled,gated:1===t.gated||!0===t.gated,replies:g(t.replies),hometask:m(t.hometask)}}export async function validateEAToken(t){const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/validate";try{const e=await l(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t.toUpperCase(),device_fingerprint:s()}),signal:AbortSignal.timeout(8e3)});if(!e.ok){const t=await e.json().catch(()=>({}));return{ok:!1,message:t.message||`HTTP ${e.status}`,code:t.code}}const n=await e.json();return n.success&&n.data?{ok:!0,...validateResultShape(n.data)}:{ok:!1,message:n.message||"Validation failed"}}catch(t){const r=t instanceof Error?t.message:String(t),e=p(t);return{ok:!1,message:`Network error: ${r}${e?` (${e})`:""}`,code:"network",advice:d(t)}}}export async function syncProgress(t,r,e){if("LEARNDEMO01"===t.toUpperCase())return;const n=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+t.toUpperCase();try{await l(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({card_number:r.card_number,event_type:r.event_type,mcq_answer:r.mcq_answer,mcq_correct:r.mcq_correct?1:0,check_answer:r.check_answer,check_correct:r.check_correct?1:0,time_on_card_ms:r.time_on_card_ms,device_fingerprint:s(),reply_ack:e&&e.length>0?e:void 0}),signal:AbortSignal.timeout(5e3)})}catch{}}export async function postFocusSession(t,r){if("LEARNDEMO01"===t.toUpperCase())return!0;const e=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/focus/"+t.toUpperCase();try{return(await l(e,{method:"POST",headers:{"Content-Type":"application/json","X-ICOA-Client":u()},body:JSON.stringify({...r,device_fingerprint:s()}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}export async function fetchServerProgress(t){if("LEARNDEMO01"===t.toUpperCase())return null;const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/progress/"+t.toUpperCase();try{const t=await l(r,{signal:AbortSignal.timeout(5e3)});if(!t.ok)return null;const e=await t.json();return e?.success&&Array.isArray(e.data?.events)?e.data.events:null}catch{return null}}export async function fetchCardFaqs(t){const r=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/faq/"+encodeURIComponent(t.toUpperCase());try{const t=await l(r,{signal:AbortSignal.timeout(5e3)});if(!t.ok)return[];const e=await t.json();return e.success&&e.data?f(e.data.faqs):[]}catch{return[]}}export async function syncCardFeedback(t,r){const e=LEARN_SERVER.replace(/\/$/,"")+"/api/icoa/learn/feedback/"+t.toUpperCase();try{return(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({curriculum_id:r.curriculum_id,card_number:r.card_number,feedback_text:r.feedback_text,time_on_card_ms:r.time_on_card_ms}),signal:AbortSignal.timeout(5e3)})).ok}catch{return!1}}
@@ -1 +1 @@
1
- import{join as e}from"node:path";import{getIcoaDir as t,getInstallSalt as n}from"./config.js";import{deriveFileKey as r,readSignedFile as s,writeSignedFile as o,recordVerdict as c}from"./secure-store.js";const a="learn-state";function i(){return e(t(),"learn-state.json")}function u(){return r(n(),a)}const l="learn-sessions";export const CONTINUE_LIST_MAX=5;function m(){return e(t(),"learn-sessions.json")}function d(){return r(n(),l)}function p(){const{data:e,verdict:t}=s(m(),d());if(c(l,t),!e||"object"!=typeof e)return{};const n={};for(const[t,r]of Object.entries(e))r&&"object"==typeof r&&"string"==typeof r.token&&"string"==typeof r.curriculumId&&(n[t]=r);return n}export function listLearnSessions(){const e=loadLearnState(),t=[];e?.token&&e.curriculumId&&t.push(e);for(const[n,r]of Object.entries(p()))n!==e?.token&&t.push(r);return t.sort((e,t)=>(t.lastSeenAt??"").localeCompare(e.lastSeenAt??""))}export function activateSession(e){const t=loadLearnState(),n=p();t?.token&&t.curriculumId&&t.token!==e&&(n[t.token]=t);const r=n[e]??null;return function(e){const t=Object.keys(e);if(t.length>20){const n=t.sort((t,n)=>(e[t].lastSeenAt??"").localeCompare(e[n].lastSeenAt??"")).slice(0,t.length-20);for(const t of n)delete e[t]}o(m(),e,d())}(n),r}export function loadLearnState(){const{data:e,verdict:t}=s(i(),u());return c(a,t),e}export function saveLearnState(e){e.lastSeenAt=(new Date).toISOString(),o(i(),e,u())}export function newLearnState(e,t,n){const r=(new Date).toISOString();return{token:e,curriculumId:t,currentCard:1,totalCards:n,startedAt:r,lastSeenAt:r,streakDays:1,longestStreak:1,cardsCompleted:[],mcqResults:{},checkResults:{},practicalsCompleted:[],bookmarks:[],achievements:[],totalSecondsActive:0}}export function stateFromServerEvents(e,t,n,r){const s=newLearnState(e,t,n),o=new Set;let c="";for(const e of r){const t=Number(e.card_number);!Number.isInteger(t)||t<1||t>n||("check_answered"!==e.event_type||"y"!==e.check_answer&&"n"!==e.check_answer?"mcq_answered"===e.event_type&&e.mcq_answer&&/^[ABCD]$/.test(e.mcq_answer)?(s.mcqResults[String(t)]={answer:e.mcq_answer,correct:!!e.mcq_correct,submittedAt:e.timestamp},o.add(t)):"viewed"===e.event_type?o.add(t):"practical_done"===e.event_type?(s.practicalsCompleted.push(t),o.add(t)):"bookmarked"===e.event_type&&s.bookmarks.push(t):(s.checkResults[String(t)]={answer:e.check_answer,correct:!!e.check_correct,submittedAt:e.timestamp},o.add(t)),e.timestamp&&(!c||e.timestamp<c)&&(c=e.timestamp))}return 0===o.size?null:(s.cardsCompleted=[...o].sort((e,t)=>e-t),s.currentCard=Math.min(n,Math.max(...o)+1),c&&(s.startedAt=c),s)}export function updateStreak(e){const t=new Date(e.lastSeenAt),n=new Date,r=Math.floor(t.getTime()/864e5),s=Math.floor(n.getTime()/864e5)-r;0!==s&&(1===s?(e.streakDays+=1,e.streakDays>e.longestStreak&&(e.longestStreak=e.streakDays)):e.streakDays=1)}export function markCardComplete(e,t){e.cardsCompleted.includes(t)||e.cardsCompleted.push(t)}export function recordMCQ(e,t,n){e.mcqResults[String(t)]=n}export function recordCheck(e,t,n){e.checkResults||(e.checkResults={}),e.checkResults[String(t)]=n}export function markPracticalComplete(e,t){e.practicalsCompleted.includes(t)||e.practicalsCompleted.push(t)}export function addAchievement(e,t){e.achievements.includes(t)||e.achievements.push(t)}
1
+ import{join as e}from"node:path";import{getIcoaDir as t,getInstallSalt as n}from"./config.js";import{deriveFileKey as r,readSignedFile as s,writeSignedFile as o,recordVerdict as c}from"./secure-store.js";const a="learn-state";function i(){return e(t(),"learn-state.json")}function u(){return r(n(),a)}const l="learn-sessions";function m(e){return(e.cardsCompleted?.length??0)>0||(e.currentCard??1)>1}export const CONTINUE_LIST_MAX=5;function d(){return e(t(),"learn-sessions.json")}function p(){return r(n(),l)}function f(){const{data:e,verdict:t}=s(d(),p());if(c(l,t),!e||"object"!=typeof e)return{};const n={};for(const[t,r]of Object.entries(e))r&&"object"==typeof r&&"string"==typeof r.token&&"string"==typeof r.curriculumId&&(n[t]=r);return n}export function listLearnSessions(){const e=loadLearnState(),t=[];e?.token&&e.curriculumId&&t.push(e);for(const[n,r]of Object.entries(f()))n!==e?.token&&t.push(r);return t.sort((e,t)=>(t.lastSeenAt??"").localeCompare(e.lastSeenAt??""))}export function activateSession(e){const t=loadLearnState(),n=f();t?.token&&t.curriculumId&&t.token!==e&&(n[t.token]=t);const r=n[e]??null;return function(e){const t=Object.keys(e);if(t.length>200){const n=t.sort((t,n)=>{const r=m(e[t])?1:0,s=m(e[n])?1:0;return r!==s?r-s:(e[t].lastSeenAt??"").localeCompare(e[n].lastSeenAt??"")}).slice(0,t.length-200);for(const t of n)delete e[t]}o(d(),e,p())}(n),r}export function loadLearnState(){const{data:e,verdict:t}=s(i(),u());return c(a,t),e}export function saveLearnState(e){e.lastSeenAt=(new Date).toISOString(),o(i(),e,u())}export function newLearnState(e,t,n){const r=(new Date).toISOString();return{token:e,curriculumId:t,currentCard:1,totalCards:n,startedAt:r,lastSeenAt:r,streakDays:1,longestStreak:1,cardsCompleted:[],mcqResults:{},checkResults:{},practicalsCompleted:[],bookmarks:[],achievements:[],totalSecondsActive:0}}export function stateFromServerEvents(e,t,n,r){const s=newLearnState(e,t,n),o=new Set;let c="";for(const e of r){const t=Number(e.card_number);!Number.isInteger(t)||t<1||t>n||("check_answered"!==e.event_type||"y"!==e.check_answer&&"n"!==e.check_answer?"mcq_answered"===e.event_type&&e.mcq_answer&&/^[ABCD]$/.test(e.mcq_answer)?(s.mcqResults[String(t)]={answer:e.mcq_answer,correct:!!e.mcq_correct,submittedAt:e.timestamp},o.add(t)):"viewed"===e.event_type?o.add(t):"practical_done"===e.event_type?(s.practicalsCompleted.push(t),o.add(t)):"bookmarked"===e.event_type&&s.bookmarks.push(t):(s.checkResults[String(t)]={answer:e.check_answer,correct:!!e.check_correct,submittedAt:e.timestamp},o.add(t)),e.timestamp&&(!c||e.timestamp<c)&&(c=e.timestamp))}return 0===o.size?null:(s.cardsCompleted=[...o].sort((e,t)=>e-t),s.currentCard=Math.min(n,Math.max(...o)+1),c&&(s.startedAt=c),s)}export function updateStreak(e){const t=new Date(e.lastSeenAt),n=new Date,r=Math.floor(t.getTime()/864e5),s=Math.floor(n.getTime()/864e5)-r;0!==s&&(1===s?(e.streakDays+=1,e.streakDays>e.longestStreak&&(e.longestStreak=e.streakDays)):e.streakDays=1)}export function markCardComplete(e,t){e.cardsCompleted.includes(t)||e.cardsCompleted.push(t)}export function recordMCQ(e,t,n){e.mcqResults[String(t)]=n}export function recordCheck(e,t,n){e.checkResults||(e.checkResults={}),e.checkResults[String(t)]=n}export function markPracticalComplete(e,t){e.practicalsCompleted.includes(t)||e.practicalsCompleted.push(t)}export function addAchievement(e,t){e.achievements.includes(t)||e.achievements.push(t)}
@@ -1 +1 @@
1
- import{spawn as t}from"node:child_process";import{existsSync as e,mkdirSync as n}from"node:fs";import{homedir as o}from"node:os";import{isAbsolute as r,join as i,normalize as s,resolve as c}from"node:path";export const SH_TIMEOUT_MS=3e4;export const SH_MAX_LINES=200;export const SH_MAX_BYTES=262144;const a={vi:"edit",vim:"edit",nano:"edit",emacs:"edit",less:"use `cat` or `head -n 40`",more:"use `cat` or `head -n 40`",top:"use `ps aux`",htop:"use `ps aux`",man:"use `<cmd> --help`",ssh:"not from inside icoa",sftp:"not from inside icoa",telnet:"not from inside icoa",ftp:"not from inside icoa",python:"use `/py`",python3:"use `/py`",ipython:"use `/py`",node:"not from inside icoa",irb:"not from inside icoa",tmux:"run it in your own terminal",screen:"run it in your own terminal",sudo:"the lab never needs root",su:"the lab never needs root"};export function labRoot(t,e){return i(t,"icoa-lab",e)}export function labCardDir(t,e,n){return i(labRoot(t,e),`card-${n}`)}export function ensureLabDir(t){try{n(t,{recursive:!0})}catch{}return t}export function firstWord(t){for(const e of t.trim().split(/\s+/))if(!/^[A-Za-z_][A-Za-z0-9_]*=/.test(e))return e;return""}export function interactiveHint(t){return a[firstWord(t)]??null}export function resolveCd(t,n,o){const a=o&&"~"!==o?r(o)?o:i(n,o):t,u=s(c(a)),d=s(c(t));return(u===d||u.startsWith(`${d}/`))&&e(u)?u:null}function u(t){let e=t.length>262144?t.slice(0,262144):t;const n=e.split("\n");return n.length>200?(e=n.slice(0,200).join("\n"),{text:e,truncated:!0}):{text:e,truncated:t.length>262144}}export async function runShell(n,o){const r=interactiveHint(n);return r?{kind:"refused",reason:firstWord(n),hint:r}:e("/bin/sh")?await new Promise(e=>{const r=Date.now();let i="",s="",c=!1;const a=t("/bin/sh",["-c",n],{cwd:o,stdio:["ignore","pipe","pipe"],env:{...process.env,PAGER:"cat",GIT_PAGER:"cat",TERM:"dumb"}}),d=setTimeout(()=>{if(!c){c=!0;try{a.kill("SIGKILL")}catch{}e({kind:"timeout",ms:Date.now()-r})}},3e4);a.stdout?.on("data",t=>{i.length<262144&&(i+=String(t))}),a.stderr?.on("data",t=>{s.length<262144&&(s+=String(t))}),a.on("error",t=>{c||(c=!0,clearTimeout(d),e({kind:"ran",stdout:"",stderr:String(t.message),code:127,ms:Date.now()-r,truncated:!1}))}),a.on("close",t=>{if(c)return;c=!0,clearTimeout(d);const n=u(i),o=u(s);e({kind:"ran",stdout:n.text,stderr:o.text,code:t??0,ms:Date.now()-r,truncated:n.truncated||o.truncated})})}):{kind:"no-shell"}}export function shortCwd(t,e){const n=s(c(t)),o=s(c(e));return o===n?"lab":`lab${o.slice(n.length)}`}export function tildePath(t){const e=o();return t.startsWith(e)?`~${t.slice(e.length)}`:t}
1
+ import{spawn as t}from"node:child_process";import{existsSync as e,mkdirSync as n}from"node:fs";import{homedir as o}from"node:os";import{isAbsolute as r,join as i,normalize as s,resolve as a}from"node:path";export const SH_TIMEOUT_MS=3e4;export const SH_MAX_LINES=200;export const SH_MAX_BYTES=262144;const c={vi:"edit",vim:"edit",nano:"edit",emacs:"edit",less:"use `cat` or `head -n 40`",more:"use `cat` or `head -n 40`",top:"use `ps aux`",htop:"use `ps aux`",man:"use `<cmd> --help`",ssh:"not from inside icoa",sftp:"not from inside icoa",telnet:"not from inside icoa",ftp:"not from inside icoa",tmux:"run it in your own terminal",screen:"run it in your own terminal",sudo:"the lab never needs root",su:"the lab never needs root"};export function labRoot(t,e){return i(t,"icoa-lab",e)}export function labCardDir(t,e,n){return i(labRoot(t,e),`card-${n}`)}export function ensureLabDir(t){try{n(t,{recursive:!0})}catch{}return t}export function firstWord(t){for(const e of t.trim().split(/\s+/))if(!/^[A-Za-z_][A-Za-z0-9_]*=/.test(e))return e;return""}const u={node:'bare `node` opens a REPL — use `node -e "…"`',python:'bare `python` opens a REPL — use `/py`, or `python3 -c "…"`',python3:'bare `python3` opens a REPL — use `/py`, or `python3 -c "…"`',ipython:"use `/py`",irb:'bare `irb` opens a REPL — use `ruby -e "…"`'};export function interactiveHint(t){const e=firstWord(t);if(c[e])return c[e];if(u[e]){const n=t.trim().split(/\s+/).filter(t=>!/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)).slice(1);if(0===n.length||n.every(t=>"-i"===t||"--interactive"===t))return u[e]}return null}export function resolveCd(t,n,o){const c=o&&"~"!==o?r(o)?o:i(n,o):t,u=s(a(c)),d=s(a(t));return(u===d||u.startsWith(`${d}/`))&&e(u)?u:null}function d(t){let e=t.length>262144?t.slice(0,262144):t;const n=e.split("\n");return n.length>200?(e=n.slice(0,200).join("\n"),{text:e,truncated:!0}):{text:e,truncated:t.length>262144}}export async function runShell(n,o){const r=interactiveHint(n);return r?{kind:"refused",reason:firstWord(n),hint:r}:e("/bin/sh")?await new Promise(e=>{const r=Date.now();let i="",s="",a=!1;const c=t("/bin/sh",["-c",n],{cwd:o,stdio:["ignore","pipe","pipe"],env:{...process.env,PAGER:"cat",GIT_PAGER:"cat",TERM:"dumb"}}),u=setTimeout(()=>{if(!a){a=!0;try{c.kill("SIGKILL")}catch{}e({kind:"timeout",ms:Date.now()-r})}},3e4);c.stdout?.on("data",t=>{i.length<262144&&(i+=String(t))}),c.stderr?.on("data",t=>{s.length<262144&&(s+=String(t))}),c.on("error",t=>{a||(a=!0,clearTimeout(u),e({kind:"ran",stdout:"",stderr:String(t.message),code:127,ms:Date.now()-r,truncated:!1}))}),c.on("close",t=>{if(a)return;a=!0,clearTimeout(u);const n=d(i),o=d(s);e({kind:"ran",stdout:n.text,stderr:o.text,code:t??0,ms:Date.now()-r,truncated:n.truncated||o.truncated})})}):{kind:"no-shell"}}export function shortCwd(t,e){const n=s(a(t)),o=s(a(e));return o===n?"lab":`lab${o.slice(n.length)}`}export function tildePath(t){const e=o();return t.startsWith(e)?`~${t.slice(e.length)}`:t}
@@ -0,0 +1 @@
1
+ import t from"node:tls";import chalk from"chalk";const e=new Set(["UNABLE_TO_GET_ISSUER_CERT","UNABLE_TO_GET_ISSUER_CERT_LOCALLY","UNABLE_TO_VERIFY_LEAF_SIGNATURE","SELF_SIGNED_CERT_IN_CHAIN","DEPTH_ZERO_SELF_SIGNED_CERT","CERT_UNTRUSTED","CERT_CHAIN_TOO_LONG","CERT_SIGNATURE_FAILURE"]);export function transportCauseCode(t){let e=t;for(let t=0;t<5&&e;t++){const t=e.code;if("string"==typeof t&&t)return t;e=e.cause}return""}export function isCertTrustError(t){return e.has(transportCauseCode(t))}let o=!1;export function systemTrustActive(){return o}export function enableSystemTrust(){if(o)return!1;const e=t.getCACertificates,r=t.setDefaultCACertificates;if("function"!=typeof e||"function"!=typeof r)return!1;let n,i;try{n=e("system"),i=e("default")}catch{return!1}if(!Array.isArray(n)||0===n.length)return!1;try{r([...new Set([...i,...n])])}catch{return!1}return o=!0,console.log(),console.log(chalk.yellow(" ⚠ This network inspects HTTPS traffic (school / library / office firewall).")),console.log(chalk.gray(" Continuing with the certificates your operating system already trusts.")),console.log(chalk.gray(" 这个网络对 HTTPS 做了拦截,已改用系统证书库继续 —— 属正常现象,可以照常上课。")),console.log(),!0}export function transportAdvice(t){const o=transportCauseCode(t);return e.has(o)?["This network re-signs HTTPS traffic and Node does not trust its certificate.","Fix: run NODE_OPTIONS=--use-system-ca icoa (needs Node 22.15+ — check with: node -v)","Or ask the network admin to exempt practice.icoa2026.au from SSL inspection."]:"CERT_HAS_EXPIRED"===o||"CERT_NOT_YET_VALID"===o?["This computer's clock is wrong, so every HTTPS certificate looks invalid.","Fix: correct the system date/time, then try again."]:"ERR_TLS_CERT_ALTNAME_INVALID"===o?["Something on this network is answering for practice.icoa2026.au with the wrong certificate.","Fix: try a phone hotspot, or ask the network admin about the web filter."]:"ENOTFOUND"===o||"EAI_AGAIN"===o?["This network cannot look up practice.icoa2026.au (DNS is blocked or broken).","Fix: set DNS to 1.1.1.1 or 8.8.8.8, or use a phone hotspot."]:"ECONNREFUSED"===o||"ECONNRESET"===o||"ENETUNREACH"===o||o.includes("CONNECT_TIMEOUT")?["The connection to practice.icoa2026.au was blocked before it reached us.","If your browser CAN open https://practice.icoa2026.au this network only allows browser traffic (a proxy).","Fix: ask the admin to allow practice.icoa2026.au on TCP 443, or use a phone hotspot."]:[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.556",
3
+ "version": "2.19.559",
4
4
  "description": "ICOA CLI — AI-native competition runtime of the International Cyber Olympiad in AI: learn, practise, and compete in AI & cybersecurity from the terminal (AI4CTF · CTF4AI · CTF4EAI)",
5
5
  "author": "Charlie Zhu",
6
6
  "type": "module",