icoa-cli 2.19.355 → 2.19.357

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.
Files changed (81) hide show
  1. package/dist/commands/ai4ctf.js +1 -1
  2. package/dist/commands/arena.js +1 -1
  3. package/dist/commands/ctf.js +787 -1
  4. package/dist/commands/ctf4ai-demo.js +1 -1
  5. package/dist/commands/ctf4vla.js +1 -1
  6. package/dist/commands/demo2.js +1502 -1
  7. package/dist/commands/exam.js +1 -1
  8. package/dist/commands/files.js +59 -1
  9. package/dist/commands/ipynb.d.ts +10 -4
  10. package/dist/commands/ipynb.js +1 -1
  11. package/dist/commands/lang.js +202 -1
  12. package/dist/commands/log.js +171 -1
  13. package/dist/commands/shell.d.ts +15 -0
  14. package/dist/commands/shell.js +151 -1
  15. package/dist/commands/sim.js +389 -1
  16. package/dist/index.js +355 -1
  17. package/dist/lib/access.js +184 -1
  18. package/dist/lib/aienv.js +205 -1
  19. package/dist/lib/arena-submit.js +21 -1
  20. package/dist/lib/banner.js +31 -1
  21. package/dist/lib/budget.js +6 -1
  22. package/dist/lib/challenge-dir.js +16 -1
  23. package/dist/lib/colors.js +17 -1
  24. package/dist/lib/comms.js +212 -1
  25. package/dist/lib/config.js +93 -1
  26. package/dist/lib/countdown.js +43 -1
  27. package/dist/lib/country-lang.js +39 -1
  28. package/dist/lib/ctfd-client.js +417 -1
  29. package/dist/lib/demo-exam.js +478 -1
  30. package/dist/lib/demo-flags.js +27 -1
  31. package/dist/lib/demo-stats.js +62 -1
  32. package/dist/lib/demo2-progress.js +102 -1
  33. package/dist/lib/docker-probe.d.ts +45 -0
  34. package/dist/lib/docker-probe.js +118 -0
  35. package/dist/lib/editor-spawn.d.ts +23 -0
  36. package/dist/lib/editor-spawn.js +53 -0
  37. package/dist/lib/exam-client.js +54 -1
  38. package/dist/lib/exam-sandbox.js +201 -1
  39. package/dist/lib/exam-setup.js +36 -1
  40. package/dist/lib/exam-state.js +273 -1
  41. package/dist/lib/gemini.js +247 -1
  42. package/dist/lib/i18n.js +302 -1
  43. package/dist/lib/integrity-snapshot.js +88 -1
  44. package/dist/lib/interactive-spawn.js +55 -1
  45. package/dist/lib/ipynb-input.js +65 -1
  46. package/dist/lib/kernel-protocol.js +88 -1
  47. package/dist/lib/kernel.js +146 -2
  48. package/dist/lib/learn-curricula.js +309 -1
  49. package/dist/lib/learn-i18n.js +184 -1
  50. package/dist/lib/learn-input.js +101 -1
  51. package/dist/lib/learn-render.js +863 -1
  52. package/dist/lib/learn-state.js +103 -1
  53. package/dist/lib/log-sync.js +155 -1
  54. package/dist/lib/logger.js +49 -1
  55. package/dist/lib/main-rl.js +7 -1
  56. package/dist/lib/menu-nav.js +105 -1
  57. package/dist/lib/notebook-doc.d.ts +38 -0
  58. package/dist/lib/notebook-doc.js +137 -0
  59. package/dist/lib/open-file.js +55 -1
  60. package/dist/lib/paper-upgrade.js +119 -1
  61. package/dist/lib/platform.js +99 -1
  62. package/dist/lib/render-card.js +112 -1
  63. package/dist/lib/repl-asker.js +67 -1
  64. package/dist/lib/sample-runner.js +227 -1
  65. package/dist/lib/sandbox.d.ts +25 -1
  66. package/dist/lib/sandbox.js +144 -1
  67. package/dist/lib/shell-split.js +69 -1
  68. package/dist/lib/sim-cooldown.js +75 -1
  69. package/dist/lib/theme.js +119 -1
  70. package/dist/lib/token-format.js +74 -1
  71. package/dist/lib/tool-man.js +418 -1
  72. package/dist/lib/toolset-hash.js +48 -1
  73. package/dist/lib/translation.js +80 -1
  74. package/dist/lib/translations-fetcher.js +95 -1
  75. package/dist/lib/ui.js +99 -1
  76. package/dist/lib/update-check.js +114 -1
  77. package/dist/lib/version.js +24 -1
  78. package/dist/postinstall.js +48 -1
  79. package/dist/repl.js +2391 -1
  80. package/dist/types/index.js +63 -1
  81. package/package.json +1 -1
@@ -1 +1,59 @@
1
- import chalk from"chalk";import{CTFdClient as o}from"../lib/ctfd-client.js";import{getConfig as e,isConnected as i}from"../lib/config.js";import{challengeDownloadDir as l}from"../lib/challenge-dir.js";import{logCommand as t}from"../lib/logger.js";import{printError as n,createSpinner as s}from"../lib/ui.js";export function registerFilesCommand(c){c.command("files <id>").description("Download challenge files").action(async c=>{t(`files ${c}`);const a=e();if(!i())return void n("Not connected. Run: join <url>");const r=a.sessionCookie||"",f=a.token&&!a.token.includes("session=")?a.token:"",g=new o(a.ctfdUrl,f,r||a.token),d=l(c),h=s("Fetching challenge files...");h.start();try{const o=await g.getChallengeFiles(parseInt(c,10));if(!o||0===o.length)return void h.info("No files attached to this challenge.");h.text=`Downloading ${o.length} file(s)...`;const e=[];for(const i of o)try{const o=await g.downloadFile(i,d);e.push(o)}catch(o){h.warn(`Failed to download: ${i}`)}h.succeed(`Downloaded ${e.length} file(s)`),console.log(chalk.gray(` Location: ${d}`));for(const o of e)console.log(chalk.gray(` → ${o.split("/").pop()}`));const i=e.length?e[0].split("/").pop():"<file>";console.log(chalk.gray(` Run tools on them directly, e.g. ${chalk.white(`file ${c}/${i}`)}`)),console.log(chalk.gray(` Open an image/audio file with: ${chalk.white(`view ${c} <file>`)}`)),console.log()}catch(o){h.fail("Failed to download files"),n(o.message)}})}
1
+ import chalk from 'chalk';
2
+ import { CTFdClient } from '../lib/ctfd-client.js';
3
+ import { getConfig, isConnected } from '../lib/config.js';
4
+ import { challengeDownloadDir } from '../lib/challenge-dir.js';
5
+ import { logCommand } from '../lib/logger.js';
6
+ import { printError, createSpinner } from '../lib/ui.js';
7
+ export function registerFilesCommand(program) {
8
+ program
9
+ .command('files <id>')
10
+ .description('Download challenge files')
11
+ .action(async (id) => {
12
+ logCommand(`files ${id}`);
13
+ const config = getConfig();
14
+ if (!isConnected()) {
15
+ printError('Not connected. Run: join <url>');
16
+ return;
17
+ }
18
+ // Session-mode logins store the cookie string in config.token; passing it
19
+ // as the API token sends `Authorization: Token session=…` → CTFd serves the
20
+ // login page (HTML) and JSON parsing fails. Split token vs session cookie.
21
+ const session = config.sessionCookie || '';
22
+ const token = config.token && !config.token.includes('session=') ? config.token : '';
23
+ const client = new CTFdClient(config.ctfdUrl, token, session || config.token);
24
+ const destDir = challengeDownloadDir(id);
25
+ const spinner = createSpinner('Fetching challenge files...');
26
+ spinner.start();
27
+ try {
28
+ const files = await client.getChallengeFiles(parseInt(id, 10));
29
+ if (!files || files.length === 0) {
30
+ spinner.info('No files attached to this challenge.');
31
+ return;
32
+ }
33
+ spinner.text = `Downloading ${files.length} file(s)...`;
34
+ const downloaded = [];
35
+ for (const filePath of files) {
36
+ try {
37
+ const dest = await client.downloadFile(filePath, destDir);
38
+ downloaded.push(dest);
39
+ }
40
+ catch (_err) {
41
+ spinner.warn(`Failed to download: ${filePath}`);
42
+ }
43
+ }
44
+ spinner.succeed(`Downloaded ${downloaded.length} file(s)`);
45
+ console.log(chalk.gray(` Location: ${destDir}`));
46
+ for (const f of downloaded) {
47
+ console.log(chalk.gray(` → ${f.split('/').pop()}`));
48
+ }
49
+ const firstName = downloaded.length ? downloaded[0].split('/').pop() : '<file>';
50
+ console.log(chalk.gray(` Run tools on them directly, e.g. ${chalk.white(`file ${id}/${firstName}`)}`));
51
+ console.log(chalk.gray(` Open an image/audio file with: ${chalk.white(`view ${id} <file>`)}`));
52
+ console.log();
53
+ }
54
+ catch (err) {
55
+ spinner.fail('Failed to download files');
56
+ printError(err.message);
57
+ }
58
+ });
59
+ }
@@ -1,8 +1,14 @@
1
1
  /**
2
- * `icoa ipynb` — an in-terminal Python notebook (Phase 1 UI of the CLI notebook
3
- * arena, see `project_cli_notebook_arena_plan`). The free, ungraded engine —
4
- * "a free Colab in your terminal" — and the cell surface the scored `arena`
5
- * will later wrap.
2
+ * `icoa ipynb [file]` — an in-terminal Python notebook (Phase 1 UI of the CLI
3
+ * notebook arena, see `project_cli_notebook_arena_plan`). The free, ungraded
4
+ * engine — "a free Colab in your terminal" — and the cell surface the scored
5
+ * `arena` will later wrap.
6
+ *
7
+ * Cell-stepper: the session is a real document. Typed blocks append as cells;
8
+ * `cells / show / edit / add / del / run / save` step through them, `edit N`
9
+ * hands the terminal to $EDITOR (nano/vi) for that one cell, `save` writes a
10
+ * Jupyter-openable .ipynb. Opening a `.py` splits it on `# %%` markers (the
11
+ * arena starter ships with them).
6
12
  *
7
13
  * Runs against the aienv venv kernel via NotebookKernel (Node ↔ Python bridge).
8
14
  * Lives in the main REPL using the BUG-008 listener-swap (no second readline).
@@ -1 +1 @@
1
- import{existsSync as e,mkdirSync as o,writeFileSync as n}from"node:fs";import{homedir as t}from"node:os";import{createInterface as r}from"node:readline";import{join as l}from"node:path";import chalk from"chalk";import{aienvPaths as a}from"../lib/aienv.js";import{shouldExecuteCell as i}from"../lib/ipynb-input.js";import{NotebookKernel as s}from"../lib/kernel.js";import{getMainRl as c}from"../lib/main-rl.js";import{openFile as g}from"../lib/open-file.js";const y=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");export function registerIpynbCommand(o){o.command("ipynb").description("Open an in-terminal Python notebook (AI/ML — needs `aienv setup`)").action(async()=>{await async function(){const o=a(t()),n=l(t(),".icoa","ipynb-out");if(p(),!e(o.python))return console.log(chalk.yellow(" The AI/ML environment is not set up yet.")),console.log(chalk.gray(" Run ")+chalk.bold.cyan("aienv setup")+chalk.gray(" first (one-time, ~300 MB).")),void console.log();const f=new s({venvPython:o.python,venvRoot:o.root});process.stdout.write(chalk.gray(" Starting Python kernel…"));try{await f.start()}catch{return console.log(chalk.red(" failed.")),console.log(chalk.gray(" Check the environment: ")+chalk.cyan("aienv status")),void console.log()}console.log(chalk.green(" ready.")),console.log(chalk.gray(" Type Python and press Enter. Blocks (def/for/…) continue until a blank line.")),console.log(chalk.gray(" ")+chalk.cyan("help")+chalk.gray(" · ")+chalk.cyan("clear")+chalk.gray(" · ")+chalk.cyan("restart")+chalk.gray(" (fresh kernel) · ")+chalk.cyan("quit")),console.log();const m=c(),h=null!==m,v=h?m.listeners("line").slice():[];h&&m.removeAllListeners("line");const w=h?m:r({input:process.stdin,output:process.stdout,terminal:!0}),b=[];let k=!1;const x=()=>{w.setPrompt(chalk.bold.cyan(b.length?"icoa ipynb ...> ":"icoa ipynb> ")),w.prompt()};let P=()=>{};const $=new Promise(e=>{P=e}),C=async e=>{if(k)return;const o=e.trim();if(0===b.length){const e=o.toLowerCase();if("quit"===e||"exit"===e||"back"===e||"q"===e)return void await(async()=>{if(w.removeAllListeners("line"),console.log(),console.log(chalk.gray(" Notebook closed. Kernel stopped.")),console.log(),await f.shutdown().catch(()=>{}),h){w.setPrompt(chalk.bold.cyan("icoa> "));for(const e of v)w.on("line",e);w.prompt(),P()}else P(),w.close()})();if("clear"===e||"cls"===e)return console.clear(),p(),void x();if("restart"===e)return await(async()=>{process.stdout.write(chalk.gray(" Restarting kernel…")),await f.shutdown().catch(()=>{});try{await f.start(),console.log(chalk.green(" fresh kernel ready.")+chalk.gray(" (all variables cleared)"))}catch{console.log(chalk.red(" restart failed.")+chalk.gray(" Leave and re-enter the notebook."))}})(),void x();if("help"===e||"?"===e)return console.log(),console.log(chalk.bold.white(" Notebook commands")),console.log(chalk.cyan(" help")+chalk.gray(" show this")),console.log(chalk.cyan(" clear")+chalk.gray(" clear the screen")),console.log(chalk.cyan(" restart")+chalk.gray(" restart the kernel (wipes all variables)")),console.log(chalk.cyan(" open")+chalk.gray(" reopen the last figure in your image viewer")),console.log(chalk.cyan(" quit")+chalk.gray(" leave the notebook")),console.log(chalk.gray(" Everything else runs as Python in the persistent kernel.")),console.log(),void x();if("open"===e)return u&&g(u)?console.log(chalk.gray(" reopening ")+chalk.cyan(u)):console.log(chalk.gray(" no figure yet — plot something (e.g. matplotlib) first.")),void x();if(""===o)return void x()}if(b.push(e),!i(b))return void x();const t=b.join("\n").replace(/\s+$/,"");if(b.length=0,""!==t.trim()){k=!0;try{!function(e,o){for(const n of e.outputs)if("stream"===n.kind){const e=n.text.replace(/\n$/,"");if(0===e.length)continue;console.log("stderr"===n.name?chalk.yellow(e):chalk.white(e))}else if("result"===n.kind)console.log(chalk.cyan(` Out[${e.execCount??"*"}]: `)+chalk.white(n.text));else if("error"===n.kind){console.log(chalk.bold.red(` ${n.ename}: ${n.evalue}`));for(const e of n.traceback)console.log(chalk.gray(` ${e.replace(y,"")}`))}else"display"===n.kind&&d(n.data,o)}(await f.execute(t),n)}catch(e){console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}k=!1,x()}else x()};w.on("line",e=>{C(e)}),h||w.on("close",async()=>{await f.shutdown().catch(()=>{}),process.exit(0)}),x(),await $}()})}function p(){console.log(),console.log(chalk.bold.white(" ICOA Notebook ")+chalk.gray("· in-terminal Python · AI/ML notebook arena")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}let f=0,u=null;function d(e,t){if(e["image/png"])try{o(t,{recursive:!0}),f+=1;const r=l(t,`figure-${f}.png`);return n(r,Buffer.from(e["image/png"],"base64")),u=r,console.log(chalk.magenta(" 🖼 figure saved → ")+chalk.cyan(r)),void(g(r)?console.log(chalk.gray(" opening in your image viewer… (type ")+chalk.cyan("open")+chalk.gray(" to reopen)")):console.log(chalk.gray(" open it with your file browser (inline images need a graphical viewer).")))}catch{}e["text/plain"]&&console.log(chalk.white(` ${e["text/plain"]}`))}
1
+ import{existsSync as e,mkdirSync as o,readFileSync as n,writeFileSync as l}from"node:fs";import{homedir as t}from"node:os";import{createInterface as r}from"node:readline";import{isAbsolute as s,join as c,resolve as a}from"node:path";import chalk from"chalk";import{aienvPaths as i}from"../lib/aienv.js";import{editTextInEditor as g}from"../lib/editor-spawn.js";import{shouldExecuteCell as y}from"../lib/ipynb-input.js";import{NotebookKernel as d}from"../lib/kernel.js";import{getMainRl as u}from"../lib/main-rl.js";import{cellPreview as p,parseIpynb as h,parsePercentPy as f,parseRunRange as m,serializeIpynb as w,statusChar as v}from"../lib/notebook-doc.js";import{openFile as b}from"../lib/open-file.js";const k=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");export function registerIpynbCommand(o){o.command("ipynb").argument("[file]","notebook (.ipynb) or script (.py, split on # %%) to open").description("Open an in-terminal Python notebook (AI/ML — needs `aienv setup`)").action(async o=>{await async function(o){const k=i(t()),x=c(t(),".icoa","ipynb-out");if($(),!e(k.python))return console.log(chalk.yellow(" The AI/ML environment is not set up yet.")),console.log(chalk.gray(" Run ")+chalk.bold.cyan("aienv setup")+chalk.gray(" first (one-time, ~300 MB).")),void console.log();const P=o?function(o){const l=s(o)?o:a(process.cwd(),o);if(!e(l))return{doc:{cells:[],path:l,dirty:!1},note:chalk.gray(" new notebook — will be created on ")+chalk.cyan("save")};try{const e=n(l,"utf8"),o=l.endsWith(".ipynb")?h(e):f(e);return{doc:{cells:o,path:l,dirty:!1},note:chalk.gray(" loaded ")+chalk.cyan(l)+chalk.gray(` — ${o.length} cell${1===o.length?"":"s"}. Type `)+chalk.cyan("cells")+chalk.gray(" to list them.")}}catch(e){return{doc:{cells:[],path:null,dirty:!1},note:chalk.yellow(` could not open ${l}: ${e instanceof Error?e.message:String(e)}`)+chalk.gray(" — starting an empty notebook.")}}}(o):null,C=P?P.doc:{cells:[],path:null,dirty:!1},I=new d({venvPython:k.python,venvRoot:k.root});process.stdout.write(chalk.gray(" Starting Python kernel…"));try{await I.start()}catch{return console.log(chalk.red(" failed.")),console.log(chalk.gray(" Check the environment: ")+chalk.cyan("aienv status")),void console.log()}console.log(chalk.green(" ready.")),P&&console.log(P.note),console.log(chalk.gray(" Type Python and press Enter. Blocks (def/for/…) continue until a blank line.")),console.log(chalk.gray(" Cells: ")+chalk.cyan("cells")+chalk.gray(" list · ")+chalk.cyan("edit N")+chalk.gray(" edit in $EDITOR · ")+chalk.cyan("run N")+chalk.gray(" / ")+chalk.cyan("run all")+chalk.gray(" · ")+chalk.cyan("save")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" · ")+chalk.cyan("quit")),console.log();const j=u(),S=null!==j,q=S?j.listeners("line").slice():[];S&&j.removeAllListeners("line");const L=S?j:r({input:process.stdin,output:process.stdout,terminal:!0}),O=[];let R=!1,A=!1;const T=()=>{L.setPrompt(chalk.bold.cyan(O.length?"icoa ipynb ...> ":"icoa ipynb> ")),L.prompt()};let M=()=>{};const B=new Promise(e=>{M=e}),D=e=>{const o=Number(e.trim());return!Number.isInteger(o)||o<1||o>C.cells.length?(console.log(0===C.cells.length?chalk.yellow(" no cells yet — type Python or use ")+chalk.cyan("add"):chalk.yellow(` no cell ${e.trim()||"?"} — pick 1-${C.cells.length} (see `)+chalk.cyan("cells")+chalk.yellow(")")),null):{cell:C.cells[o-1],idx:o}},K=async e=>{if(R)return void(e.trim()&&console.log(chalk.gray(" (busy — wait for the current cell to finish, then retype)")));const o=e.trim();if(0===O.length){const e=o.split(/\s+/),n=(e[0]??"").toLowerCase(),t=o.slice(e[0]?.length??0).trim();if("quit"!==n&&"exit"!==n&&"q"!==n&&(A=!1),"quit"===n||"exit"===n||"back"===n||"q"===n)return C.dirty&&!A?(A=!0,console.log(chalk.yellow(" unsaved changes — ")+chalk.cyan("save")+chalk.yellow(" first, or type ")+chalk.cyan("quit")+chalk.yellow(" again to discard.")),void T()):void await(async()=>{if(L.removeAllListeners("line"),console.log(),console.log(chalk.gray(" Notebook closed. Kernel stopped.")),console.log(),await I.shutdown().catch(()=>{}),S){L.setPrompt(chalk.bold.cyan("icoa> "));for(const e of q)L.on("line",e);L.prompt(),M()}else M(),L.close()})();if("clear"===n||"cls"===n)return console.clear(),$(),void T();if("restart"===n)return await(async()=>{process.stdout.write(chalk.gray(" Restarting kernel…")),await I.shutdown().catch(()=>{});try{await I.start(),console.log(chalk.green(" fresh kernel ready.")+chalk.gray(" (all variables cleared)"))}catch{console.log(chalk.red(" restart failed.")+chalk.gray(" Leave and re-enter the notebook."))}})(),void T();if("help"===n||"?"===n)return console.log(),console.log(chalk.bold.white(" Notebook commands")),console.log(chalk.cyan(" cells")+chalk.gray(" list cells (○ not run · ✓ ok · ✗ error)")),console.log(chalk.cyan(" show N")+chalk.gray(" print cell N source + outputs")),console.log(chalk.cyan(" edit N")+chalk.gray(" edit cell N in your editor ($EDITOR, default nano)")),console.log(chalk.cyan(" add")+chalk.gray(" write a new cell in your editor")),console.log(chalk.cyan(" del N")+chalk.gray(" delete cell N")),console.log(chalk.cyan(" run N")+chalk.gray(" run cell N (also: run 2-5, run all — stops on error)")),console.log(chalk.cyan(" save [file]")+chalk.gray(" save as a real .ipynb")),console.log(chalk.cyan(" restart")+chalk.gray(" restart the kernel (wipes all variables)")),console.log(chalk.cyan(" open")+chalk.gray(" reopen the last figure in your image viewer")),console.log(chalk.cyan(" clear")+chalk.gray(" clear the screen · ")+chalk.cyan("quit")+chalk.gray(" leave")),console.log(chalk.gray(" Everything else runs as Python in the persistent kernel (and is appended as a cell).")),console.log(),void T();if("open"===n)return N&&b(N)?console.log(chalk.gray(" reopening ")+chalk.cyan(N)):console.log(chalk.gray(" no figure yet — plot something (e.g. matplotlib) first.")),void T();if("cells"===n||"ls"===n)return(()=>{if(0===C.cells.length)return void console.log(chalk.gray(" no cells yet — type Python, or ")+chalk.cyan("add")+chalk.gray(" to write one in your editor."));console.log();const e=Math.max(20,(process.stdout.columns??80)-12);C.cells.forEach((o,n)=>{const l=v(o),t="✓"===l?chalk.green(l.padEnd(2)):"✗"===l?chalk.red(l.padEnd(2)):chalk.gray(l.padEnd(2));console.log(`${chalk.cyan(String(n+1).padStart(4))} ${t} ${chalk.white(p(o,e))}`)}),console.log(),console.log(chalk.gray(" ")+chalk.cyan("show N")+chalk.gray(" · ")+chalk.cyan("edit N")+chalk.gray(" · ")+chalk.cyan("run N")+chalk.gray(" · ")+chalk.cyan("run all"))})(),void T();if("show"===n)return(e=>{const o=D(e);if(o){console.log(),console.log(chalk.gray(` ── cell ${o.idx} ${"markdown"===o.cell.kind?"(markdown) ":""}──`));for(const e of o.cell.source.split("\n"))console.log(chalk.white(` ${e}`));o.cell.result&&(console.log(chalk.gray(" ── output ──")),E(o.cell.result,x)),console.log()}})(t),void T();if("edit"===n)return(e=>{const o=D(e);if(!o)return;L.pause();const n=g(o.cell.source,"markdown"===o.cell.kind?".md":".py");if(L.resume(),null===n)return void console.log(chalk.yellow(" editor cancelled or failed — cell unchanged.")+chalk.gray(" (set $EDITOR to choose your editor)"));const l=n.replace(/\s+$/,"");l!==o.cell.source?(o.cell.source=l,o.cell.result=null,o.cell.rawOutputs=void 0,o.cell.rawExecCount=void 0,C.dirty=!0,console.log(chalk.green(` ✓ cell ${o.idx} updated`)+chalk.gray(" — ")+chalk.cyan(`run ${o.idx}`)+chalk.gray(" to execute."))):console.log(chalk.gray(" no changes."))})(t),void T();if("add"===n)return(()=>{L.pause();const e=g("");L.resume();const o=null===e?"":e.replace(/\s+$/,"");o?(C.cells.push({kind:"code",source:o,result:null}),C.dirty=!0,console.log(chalk.green(` ✓ added cell ${C.cells.length}`)+chalk.gray(" — ")+chalk.cyan(`run ${C.cells.length}`)+chalk.gray(" to execute."))):console.log(chalk.gray(" nothing added."))})(),void T();if("del"===n||"delete"===n)return(e=>{const o=D(e);o&&(C.cells.splice(o.idx-1,1),C.dirty=!0,console.log(chalk.gray(` deleted cell ${o.idx} — ${C.cells.length} left.`)))})(t),void T();if("run"===n)return R=!0,await(async e=>{const o=m(e,C.cells.length);if(o)for(const e of o){const n=C.cells[e-1];if("markdown"!==n.kind){console.log(chalk.gray(` ── run cell ${e}: `)+chalk.white(p(n,50))+chalk.gray(" ──"));try{n.result=await I.execute(n.source)}catch(e){return void console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}if(C.dirty=!0,E(n.result,x),!n.result.ok)return void(e!==o[o.length-1]&&console.log(chalk.yellow(` ✗ cell ${e} raised — stopping here.`)+chalk.gray(" Fix it (")+chalk.cyan(`edit ${e}`)+chalk.gray(") and re-run.")))}else{if(o.length>1)continue;console.log(chalk.gray(` cell ${e} is markdown — nothing to run.`))}}else console.log(0===C.cells.length?chalk.yellow(" no cells yet — type Python or use ")+chalk.cyan("add"):chalk.yellow(" usage: ")+chalk.cyan("run 3")+chalk.yellow(" · ")+chalk.cyan("run 1-5")+chalk.yellow(" · ")+chalk.cyan("run all"))})(t),R=!1,void T();if("save"===n)return(e=>{if(0===C.cells.length)return void console.log(chalk.yellow(" nothing to save — the notebook is empty."));let o;o=e.trim()?s(e.trim())?e.trim():a(process.cwd(),e.trim()):C.path?C.path.endsWith(".ipynb")?C.path:`${C.path.replace(/\.[^./]+$/,"")}.ipynb`:a(process.cwd(),"notebook.ipynb"),o.endsWith(".ipynb")||(o+=".ipynb");try{l(o,w(C.cells)),C.path=o,C.dirty=!1,console.log(chalk.green(" ✓ saved ")+chalk.cyan(o)+chalk.gray(` (${C.cells.length} cells)`))}catch(e){console.log(chalk.red(" save failed: ")+chalk.gray(e instanceof Error?e.message:String(e)))}})(t),void T();if(""===o)return void T()}if(O.push(e),!y(O))return void T();const n=O.join("\n").replace(/\s+$/,"");if(O.length=0,""!==n.trim()){R=!0;try{const e=await I.execute(n);C.cells.push({kind:"code",source:n,result:e}),C.dirty=!0,E(e,x)}catch(e){console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}R=!1,T()}else T()};L.on("line",e=>{K(e)}),S||L.on("close",async()=>{await I.shutdown().catch(()=>{}),process.exit(0)}),T(),await B}(o)})}function $(){console.log(),console.log(chalk.bold.white(" ICOA Notebook ")+chalk.gray("· in-terminal Python · AI/ML notebook arena")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}let x=0,N=null;function E(e,o){for(const n of e.outputs)if("stream"===n.kind){const e=n.text.replace(/\n$/,"");if(0===e.length)continue;console.log("stderr"===n.name?chalk.yellow(e):chalk.white(e))}else if("result"===n.kind)console.log(chalk.cyan(` Out[${e.execCount??"*"}]: `)+chalk.white(n.text));else if("error"===n.kind){console.log(chalk.bold.red(` ${n.ename}: ${n.evalue}`));for(const e of n.traceback)console.log(chalk.gray(` ${e.replace(k,"")}`))}else"display"===n.kind&&P(n.data,o)}function P(e,n){if(e["image/png"])try{o(n,{recursive:!0}),x+=1;const t=c(n,`figure-${x}.png`);return l(t,Buffer.from(e["image/png"],"base64")),N=t,console.log(chalk.magenta(" 🖼 figure saved → ")+chalk.cyan(t)),void(b(t)?console.log(chalk.gray(" opening in your image viewer… (type ")+chalk.cyan("open")+chalk.gray(" to reopen)")):console.log(chalk.gray(" open it with your file browser (inline images need a graphical viewer).")))}catch{}e["text/plain"]&&console.log(chalk.white(` ${e["text/plain"]}`))}
@@ -1 +1,202 @@
1
- import chalk from"chalk";import{getConfig as e,saveConfig as o}from"../lib/config.js";import{COUNTRY_LANG as s}from"../lib/country-lang.js";import{getExamState as a,getRealExamState as t}from"../lib/exam-state.js";import{logCommand as n}from"../lib/logger.js";import{ensureLangCache as r}from"../lib/translations-fetcher.js";import{loadUiPack as i}from"../lib/i18n.js";import{printSuccess as l,printError as c,printInfo as g}from"../lib/ui.js";import{SUPPORTED_LANGUAGES as u}from"../types/index.js";const m={en:"English",zh:"中文 (Chinese)",ja:"日本語 (Japanese)",ko:"한국어 (Korean)",es:"Español (Spanish)",ar:"العربية (Arabic)",fr:"Français (French)",pt:"Português (Portuguese)",ru:"Русский (Russian)",hi:"हिन्दी (Hindi)",de:"Deutsch (German)",id:"Bahasa (Indonesian)",th:"ไทย (Thai)",vi:"Tiếng Việt (Vietnamese)",tr:"Türkçe (Turkish)",uk:"Українська (Ukrainian)",ht:"Kreyòl (Haitian Creole)",sw:"Kiswahili (Swahili)",uz:"Oʻzbek (Uzbek)",lo:"ລາວ (Lao)"};export function registerLangCommand(p){p.command("lang [code]").description("Switch display language").action(async p=>{if(n(`lang ${p||""}`),!p){const o=e();g(`Current language: ${chalk.white(m[o.language]||o.language)}`),console.log(),console.log(chalk.gray(" Supported languages:"));for(const e of u){const s=o.language===e?chalk.yellow(" ← current"):"";console.log(` ${chalk.white(e)} ${m[e]}${s}`)}return console.log(),console.log(chalk.gray(" Switch now: ")+chalk.cyan("lang <code>")+chalk.gray(" (e.g. ")+chalk.cyan("lang es")+chalk.gray(")")),console.log(chalk.gray(' No "back" needed — you are still at the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log()}if(!u.includes(p)){c(`Unsupported language: ${p}`);const e=u.map(e=>`${e} (${m[e]?.split(" ")[0]||e})`).join(", ");return void g(`Supported: ${e}`)}const d=t(),y=d?.session?.token;if(y){const e=y.substring(0,2).toUpperCase(),o=s[e];if(o&&p!==o&&"en"!==p)return c("Language is locked during your exam."),g(`Your token (${e}xxx) supports two languages: ${chalk.cyan("en")} (English) or ${chalk.cyan(o)} (${m[o]||o}).`),void g("To change language, finish or quit the current exam first.")}o({language:p}),l(`Language set to: ${m[p]||p}`),await r(p),i(p);const h=a();if(h&&"demo-free"===h.session.examId)try{const{pickDemoQuestions:e,getLocalizedDemoSession:o,getLocalizedDemoQuestions:s,getLocalizedExplanations:a,DEMO_PICK_SIZE:t}=await import("../lib/demo-exam.js"),{saveExamState:n}=await import("../lib/exam-state.js");if(h.questions.every(e=>null!=e.sourceNumber&&Array.isArray(e.sourceOrder)&&4===e.sourceOrder.length)){const e=s(),t=a();h.questions=h.questions.map(o=>{const s=e.find(e=>e.number===o.sourceNumber);return s&&o.sourceOrder?{...o,text:s.text,category:s.category,options:{A:s.options[o.sourceOrder[0]],B:s.options[o.sourceOrder[1]],C:s.options[o.sourceOrder[2]],D:s.options[o.sourceOrder[3]]},explanation:t[o.sourceNumber]}:o}),h.session.examName=o().examName,n(h);const r=h._lastQ||1;console.log(),console.log(chalk.green(` Demo continues in ${m[p]||p}. Your progress is kept.`)),console.log(chalk.white(` Resume: exam q ${r}`))}else h.questions=e(t),h.answers={},h.session.examName=o().examName,h.session.startedAt=(new Date).toISOString(),h._lastQ=1,n(h),console.log(),console.log(chalk.green(` Demo restarted in ${m[p]||p}.`)),console.log(chalk.white(" Type: exam q 1"))}catch{console.log(chalk.gray(" Language changed. Type: demo"))}else if(h&&h.session.token)try{const{getConfig:e}=await import("../lib/config.js"),{saveExamState:o}=await import("../lib/exam-state.js"),{getDeviceFingerprint:s}=await import("../lib/access.js"),a=e().ctfdUrl||"https://practice.icoa2026.au",t=h.session.token,n=await fetch(`${a}/api/icoa/exam-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t,deviceHash:s(),lang:p}),signal:AbortSignal.timeout(1e4)});if(n.ok){const e=(await n.json()).data.questions;h.questions=e,o(h);const s=h._lastQ||1;console.log(),l(`Exam questions updated to ${m[p]||p}. Your answers are kept.`),console.log(chalk.white(` Resume: exam q ${s}`))}else if(429===n.status){const e=await n.json().catch(()=>null),o=e?.message?.match(/(\d+)\s*s/i),s=o?parseInt(o[1],10):60;console.log(),g(`Server rate-limited that token (wait ~${s}s).`),g(`Your exam session is intact — try ${chalk.cyan(`lang ${p}`)} again after the cooldown.`)}else{const{clearExamState:e}=await import("../lib/exam-state.js");e(),console.log(),console.log(chalk.green(" ✓ Old session record cleared — your scores are safe.")),console.log(chalk.gray(" To start (or resume) your exam, type your token:")),console.log(chalk.gray(" → ")+chalk.bold.cyan("exam <your-token>"))}}catch{console.log(chalk.yellow(" Could not reach server. Language changed for UI only."))}else if(h){const e=h._lastQ||1;console.log(),console.log(chalk.gray(` Exam in progress — resuming Q${e}:`)),console.log(chalk.white(` Type: exam q ${e}`))}})}
1
+ import chalk from 'chalk';
2
+ import { getConfig, saveConfig } from '../lib/config.js';
3
+ import { COUNTRY_LANG } from '../lib/country-lang.js';
4
+ import { getExamState, getRealExamState } from '../lib/exam-state.js';
5
+ import { logCommand } from '../lib/logger.js';
6
+ import { ensureLangCache } from '../lib/translations-fetcher.js';
7
+ import { loadUiPack } from '../lib/i18n.js';
8
+ import { printSuccess, printError, printInfo } from '../lib/ui.js';
9
+ import { SUPPORTED_LANGUAGES } from '../types/index.js';
10
+ const LANG_NAMES = {
11
+ en: 'English',
12
+ zh: '中文 (Chinese)',
13
+ ja: '日本語 (Japanese)',
14
+ ko: '한국어 (Korean)',
15
+ es: 'Español (Spanish)',
16
+ ar: 'العربية (Arabic)',
17
+ fr: 'Français (French)',
18
+ pt: 'Português (Portuguese)',
19
+ ru: 'Русский (Russian)',
20
+ hi: 'हिन्दी (Hindi)',
21
+ de: 'Deutsch (German)',
22
+ id: 'Bahasa (Indonesian)',
23
+ th: 'ไทย (Thai)',
24
+ vi: 'Tiếng Việt (Vietnamese)',
25
+ tr: 'Türkçe (Turkish)',
26
+ uk: 'Українська (Ukrainian)',
27
+ ht: 'Kreyòl (Haitian Creole)',
28
+ sw: 'Kiswahili (Swahili)',
29
+ uz: 'Oʻzbek (Uzbek)',
30
+ lo: 'ລາວ (Lao)',
31
+ };
32
+ export function registerLangCommand(program) {
33
+ program
34
+ .command('lang [code]')
35
+ .description('Switch display language')
36
+ .action(async (code) => {
37
+ logCommand(`lang ${code || ''}`);
38
+ if (!code) {
39
+ const config = getConfig();
40
+ printInfo(`Current language: ${chalk.white(LANG_NAMES[config.language] || config.language)}`);
41
+ console.log();
42
+ console.log(chalk.gray(' Supported languages:'));
43
+ for (const lang of SUPPORTED_LANGUAGES) {
44
+ const current = config.language === lang ? chalk.yellow(' ← current') : '';
45
+ console.log(` ${chalk.white(lang)} ${LANG_NAMES[lang]}${current}`);
46
+ }
47
+ console.log();
48
+ console.log(chalk.gray(' Switch now: ') +
49
+ chalk.cyan('lang <code>') +
50
+ chalk.gray(' (e.g. ') +
51
+ chalk.cyan('lang es') +
52
+ chalk.gray(')'));
53
+ console.log(chalk.gray(' No "back" needed — you are still at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
54
+ console.log();
55
+ return;
56
+ }
57
+ if (!SUPPORTED_LANGUAGES.includes(code)) {
58
+ printError(`Unsupported language: ${code}`);
59
+ const supported = SUPPORTED_LANGUAGES.map((l) => `${l} (${LANG_NAMES[l]?.split(' ')[0] || l})`).join(', ');
60
+ printInfo(`Supported: ${supported}`);
61
+ return;
62
+ }
63
+ // Country lock: during an active real exam, only allow English or the
64
+ // language auto-detected from the token's country prefix. Demo exams
65
+ // are not affected (getRealExamState returns null for demos).
66
+ const realExam = getRealExamState();
67
+ const examToken = realExam?.session?.token;
68
+ if (examToken) {
69
+ const prefix = examToken.substring(0, 2).toUpperCase();
70
+ const expectedLang = COUNTRY_LANG[prefix];
71
+ if (expectedLang && code !== expectedLang && code !== 'en') {
72
+ printError(`Language is locked during your exam.`);
73
+ printInfo(`Your token (${prefix}xxx) supports two languages: ` +
74
+ `${chalk.cyan('en')} (English) or ${chalk.cyan(expectedLang)} (${LANG_NAMES[expectedLang] || expectedLang}).`);
75
+ printInfo(`To change language, finish or quit the current exam first.`);
76
+ return;
77
+ }
78
+ }
79
+ saveConfig({ language: code });
80
+ printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
81
+ // Fetch the translation pack on first switch to this language (no-op
82
+ // when already cached or when code === 'en'). Non-fatal if it fails;
83
+ // per-question lookups will fall back to the on-the-fly translator.
84
+ await ensureLangCache(code);
85
+ // Apply the freshly-fetched UI pack immediately (the pack's ui.json now
86
+ // drives interface strings via t(); see src/lib/i18n.ts lazy-load).
87
+ loadUiPack(code);
88
+ // If demo in progress, re-translate each drawn question in place using
89
+ // sourceNumber + sourceOrder so the user's answers and option positions are
90
+ // preserved. If an older state lacks these fields (pre-v2.19.22), fall back
91
+ // to restart-with-fresh-pick so nothing crashes.
92
+ const state = getExamState();
93
+ if (state && state.session.examId === 'demo-free') {
94
+ try {
95
+ const { pickDemoQuestions, getLocalizedDemoSession, getLocalizedDemoQuestions, getLocalizedExplanations, DEMO_PICK_SIZE, } = await import('../lib/demo-exam.js');
96
+ const { saveExamState } = await import('../lib/exam-state.js');
97
+ const canRetranslate = state.questions.every((q) => q.sourceNumber != null && Array.isArray(q.sourceOrder) && q.sourceOrder.length === 4);
98
+ if (canRetranslate) {
99
+ const pool = getLocalizedDemoQuestions();
100
+ const explanations = getLocalizedExplanations();
101
+ state.questions = state.questions.map((q) => {
102
+ const src = pool.find((p) => p.number === q.sourceNumber);
103
+ if (!src || !q.sourceOrder)
104
+ return q;
105
+ return {
106
+ ...q,
107
+ text: src.text,
108
+ category: src.category,
109
+ options: {
110
+ A: src.options[q.sourceOrder[0]],
111
+ B: src.options[q.sourceOrder[1]],
112
+ C: src.options[q.sourceOrder[2]],
113
+ D: src.options[q.sourceOrder[3]],
114
+ },
115
+ explanation: explanations[q.sourceNumber],
116
+ };
117
+ });
118
+ state.session.examName = getLocalizedDemoSession().examName;
119
+ saveExamState(state);
120
+ const currentQ = state._lastQ || 1;
121
+ console.log();
122
+ console.log(chalk.green(` Demo continues in ${LANG_NAMES[code] || code}. Your progress is kept.`));
123
+ console.log(chalk.white(` Resume: exam q ${currentQ}`));
124
+ }
125
+ else {
126
+ // Legacy state from before v2.19.22 — safely reset
127
+ state.questions = pickDemoQuestions(DEMO_PICK_SIZE);
128
+ state.answers = {};
129
+ state.session.examName = getLocalizedDemoSession().examName;
130
+ state.session.startedAt = new Date().toISOString();
131
+ state._lastQ = 1;
132
+ saveExamState(state);
133
+ console.log();
134
+ console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
135
+ console.log(chalk.white(' Type: exam q 1'));
136
+ }
137
+ }
138
+ catch {
139
+ console.log(chalk.gray(' Language changed. Type: demo'));
140
+ }
141
+ }
142
+ else if (state && state.session.token) {
143
+ // Real exam with token: re-fetch questions in new language from server
144
+ try {
145
+ const { getConfig } = await import('../lib/config.js');
146
+ const { saveExamState } = await import('../lib/exam-state.js');
147
+ const { getDeviceFingerprint } = await import('../lib/access.js');
148
+ const config = getConfig();
149
+ const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
150
+ const token = state.session.token;
151
+ const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ token, deviceHash: getDeviceFingerprint(), lang: code }),
155
+ signal: AbortSignal.timeout(10000),
156
+ });
157
+ if (res.ok) {
158
+ const json = (await res.json());
159
+ const newQuestions = json.data.questions;
160
+ // Keep answers, interactions, aiUsage — only update question text
161
+ state.questions = newQuestions;
162
+ saveExamState(state);
163
+ const currentQ = state._lastQ || 1;
164
+ console.log();
165
+ printSuccess(`Exam questions updated to ${LANG_NAMES[code] || code}. Your answers are kept.`);
166
+ console.log(chalk.white(` Resume: exam q ${currentQ}`));
167
+ }
168
+ else if (res.status === 429) {
169
+ // V8 anti-bruteforce cooldown — token is alive, just throttled.
170
+ // Do NOT clear local state, or contestants lose their real session
171
+ // because the server briefly rate-limited a language switch.
172
+ const errBody = (await res.json().catch(() => null));
173
+ const m = errBody?.message?.match(/(\d+)\s*s/i);
174
+ const waitS = m ? parseInt(m[1], 10) : 60;
175
+ console.log();
176
+ printInfo(`Server rate-limited that token (wait ~${waitS}s).`);
177
+ printInfo(`Your exam session is intact — try ${chalk.cyan(`lang ${code}`)} again after the cooldown.`);
178
+ }
179
+ else {
180
+ // Other non-200: token revoked / submitted / window closed. Auto-
181
+ // clear the dead local session so contestants never need to learn
182
+ // the hidden `exam reset` command — they just retype their token.
183
+ const { clearExamState } = await import('../lib/exam-state.js');
184
+ clearExamState();
185
+ console.log();
186
+ console.log(chalk.green(' ✓ Old session record cleared — your scores are safe.'));
187
+ console.log(chalk.gray(' To start (or resume) your exam, type your token:'));
188
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('exam <your-token>'));
189
+ }
190
+ }
191
+ catch {
192
+ console.log(chalk.yellow(' Could not reach server. Language changed for UI only.'));
193
+ }
194
+ }
195
+ else if (state) {
196
+ const currentQ = state._lastQ || 1;
197
+ console.log();
198
+ console.log(chalk.gray(` Exam in progress — resuming Q${currentQ}:`));
199
+ console.log(chalk.white(` Type: exam q ${currentQ}`));
200
+ }
201
+ });
202
+ }
@@ -1 +1,171 @@
1
- import chalk from"chalk";import{readFileSync as o,existsSync as t}from"node:fs";import{join as e}from"node:path";import{getSessionLog as n}from"../lib/logger.js";import{getIcoaDir as s,getConfig as i}from"../lib/config.js";import{printHeader as l,printInfo as a,printTable as r}from"../lib/ui.js";export function registerLogCommand(c){const g=c.command("log").description("Display session history").action(()=>{!function(){const o=n();if(0===o.length)return void a("No session log entries yet.");l("Session Log");const t=o.map(o=>{const t=o.timestamp.replace("T"," ").substring(0,19),e={A:chalk.green,B:chalk.yellow,C:chalk.red,command:chalk.blue,submit:chalk.magenta}[o.level]||chalk.gray,n=o.input.length>60?`${o.input.substring(0,57)}...`:o.input;return[chalk.gray(t),e(o.level.padEnd(7)),n]});r(["Time","Type","Content"],t),console.log(chalk.gray(` ${o.length} entries total`)),console.log(),console.log(chalk.gray(" You are at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt. Also: ")+chalk.cyan("log stats")+chalk.gray(" · ")+chalk.cyan("log export")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" all commands.")),console.log()}()});g.command("export").description("Export full audit log for review").action(async()=>{await async function(){const l=i(),a=s(),r=(e(a,"session.log"),e(a,"session-state.json")),c=(e(a,"config.json"),(new Date).toISOString().replace(/[:.]/g,"-").substring(0,19)),g=`icoa-audit-${l.userName||"unknown"}-${c}.json`,m=e(process.cwd(),g),y={exportedAt:(new Date).toISOString(),version:"1.7.2",competitor:{userName:l.userName,userId:l.userId,teamName:l.teamName,teamId:l.teamId,sessionId:l.sessionId},connection:{ctfdUrl:l.ctfdUrl},session:t(r)?JSON.parse(o(r,"utf-8")):null,commands:n()},d=n(),u={};for(const o of d)u[o.level]=(u[o.level]||0)+1;y.summary={totalCommands:d.length,byType:u,firstEntry:d[0]?.timestamp||null,lastEntry:d[d.length-1]?.timestamp||null};const{writeFileSync:p}=await import("node:fs");p(m,JSON.stringify(y,null,2)),console.log(),console.log(chalk.green(" ✓ Audit log exported")),console.log(chalk.white(` ${m}`)),console.log(),console.log(chalk.gray(" Contents:")),console.log(chalk.gray(` Commands: ${d.length}`)),Object.entries(u).forEach(([o,t])=>{console.log(chalk.gray(` ${o}: ${t}`))}),console.log(),console.log(chalk.gray(" This file contains the complete session audit trail.")),console.log(chalk.gray(" Submit to organizers for post-competition verification.")),console.log()}()}),g.command("stats").description("Show session statistics").action(()=>{!function(){const i=n(),l=s(),a=e(l,"session-state.json");if(console.log(),console.log(chalk.bold.white(" Session Statistics")),console.log(chalk.gray(" ─────────────────────────────────────────────")),0===i.length)return console.log(chalk.gray(" No activity recorded yet.")),void console.log();const r=new Date(i[0].timestamp),c=new Date(i[i.length-1].timestamp),g=Math.round((c.getTime()-r.getTime())/6e4);console.log(chalk.gray(" First activity: ")+chalk.white(r.toLocaleString())),console.log(chalk.gray(" Last activity: ")+chalk.white(c.toLocaleString())),console.log(chalk.gray(" Duration: ")+chalk.white(`${g} min`)),console.log();const m={};for(const o of i)m[o.level]=(m[o.level]||0)+1;if(console.log(chalk.gray(" Total commands: ")+chalk.white(String(i.length))),m.command&&console.log(chalk.blue(" commands: ")+chalk.white(String(m.command))),m.A&&console.log(chalk.green(" hint A: ")+chalk.white(String(m.A))),m.B&&console.log(chalk.yellow(" hint B: ")+chalk.white(String(m.B))),m.C&&console.log(chalk.red(" hint C: ")+chalk.white(String(m.C))),m.submit&&console.log(chalk.magenta(" submissions: ")+chalk.white(String(m.submit))),t(a))try{const t=JSON.parse(o(a,"utf-8"));if(console.log(),console.log(chalk.gray(" Exit count: ")+chalk.white(String(t.exitCount||0))),t.totalAwaySeconds){const o=Math.round(t.totalAwaySeconds/60);console.log(chalk.gray(" Total away: ")+chalk.white(`${o} min`))}}catch{}console.log()}()})}
1
+ import chalk from 'chalk';
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { getSessionLog } from '../lib/logger.js';
5
+ import { getIcoaDir, getConfig } from '../lib/config.js';
6
+ import { printHeader, printInfo, printTable } from '../lib/ui.js';
7
+ export function registerLogCommand(program) {
8
+ const logCmd = program
9
+ .command('log')
10
+ .description('Display session history')
11
+ .action(() => {
12
+ showLog();
13
+ });
14
+ // icoa log export — export full audit log for post-competition review
15
+ logCmd
16
+ .command('export')
17
+ .description('Export full audit log for review')
18
+ .action(async () => {
19
+ await exportLog();
20
+ });
21
+ // icoa log stats — show summary statistics
22
+ logCmd
23
+ .command('stats')
24
+ .description('Show session statistics')
25
+ .action(() => {
26
+ showStats();
27
+ });
28
+ }
29
+ function showLog() {
30
+ const entries = getSessionLog();
31
+ if (entries.length === 0) {
32
+ printInfo('No session log entries yet.');
33
+ return;
34
+ }
35
+ printHeader('Session Log');
36
+ const rows = entries.map((entry) => {
37
+ const time = entry.timestamp.replace('T', ' ').substring(0, 19);
38
+ const levelColor = {
39
+ A: chalk.green,
40
+ B: chalk.yellow,
41
+ C: chalk.red,
42
+ command: chalk.blue,
43
+ submit: chalk.magenta,
44
+ };
45
+ const colorFn = levelColor[entry.level] || chalk.gray;
46
+ const input = entry.input.length > 60 ? `${entry.input.substring(0, 57)}...` : entry.input;
47
+ return [chalk.gray(time), colorFn(entry.level.padEnd(7)), input];
48
+ });
49
+ printTable(['Time', 'Type', 'Content'], rows);
50
+ console.log(chalk.gray(` ${entries.length} entries total`));
51
+ console.log();
52
+ console.log(chalk.gray(' You are at the ') +
53
+ chalk.cyan('icoa>') +
54
+ chalk.gray(' prompt. Also: ') +
55
+ chalk.cyan('log stats') +
56
+ chalk.gray(' · ') +
57
+ chalk.cyan('log export') +
58
+ chalk.gray(' · ') +
59
+ chalk.cyan('help') +
60
+ chalk.gray(' all commands.'));
61
+ console.log();
62
+ }
63
+ async function exportLog() {
64
+ const config = getConfig();
65
+ const icoaDir = getIcoaDir();
66
+ const _logFile = join(icoaDir, 'session.log');
67
+ const sessionFile = join(icoaDir, 'session-state.json');
68
+ const _configFile = join(icoaDir, 'config.json');
69
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
70
+ const userName = config.userName || 'unknown';
71
+ const exportName = `icoa-audit-${userName}-${timestamp}.json`;
72
+ const exportPath = join(process.cwd(), exportName);
73
+ // Gather all audit data
74
+ const audit = {
75
+ exportedAt: new Date().toISOString(),
76
+ version: '1.7.2',
77
+ competitor: {
78
+ userName: config.userName,
79
+ userId: config.userId,
80
+ teamName: config.teamName,
81
+ teamId: config.teamId,
82
+ sessionId: config.sessionId,
83
+ },
84
+ connection: {
85
+ ctfdUrl: config.ctfdUrl,
86
+ },
87
+ session: existsSync(sessionFile) ? JSON.parse(readFileSync(sessionFile, 'utf-8')) : null,
88
+ commands: getSessionLog(),
89
+ };
90
+ // Count by type
91
+ const entries = getSessionLog();
92
+ const counts = {};
93
+ for (const e of entries) {
94
+ counts[e.level] = (counts[e.level] || 0) + 1;
95
+ }
96
+ audit.summary = {
97
+ totalCommands: entries.length,
98
+ byType: counts,
99
+ firstEntry: entries[0]?.timestamp || null,
100
+ lastEntry: entries[entries.length - 1]?.timestamp || null,
101
+ };
102
+ // Write export
103
+ const { writeFileSync } = await import('node:fs');
104
+ writeFileSync(exportPath, JSON.stringify(audit, null, 2));
105
+ console.log();
106
+ console.log(chalk.green(` ✓ Audit log exported`));
107
+ console.log(chalk.white(` ${exportPath}`));
108
+ console.log();
109
+ console.log(chalk.gray(' Contents:'));
110
+ console.log(chalk.gray(` Commands: ${entries.length}`));
111
+ Object.entries(counts).forEach(([type, count]) => {
112
+ console.log(chalk.gray(` ${type}: ${count}`));
113
+ });
114
+ console.log();
115
+ console.log(chalk.gray(' This file contains the complete session audit trail.'));
116
+ console.log(chalk.gray(' Submit to organizers for post-competition verification.'));
117
+ console.log();
118
+ }
119
+ function showStats() {
120
+ const entries = getSessionLog();
121
+ const icoaDir = getIcoaDir();
122
+ const sessionFile = join(icoaDir, 'session-state.json');
123
+ console.log();
124
+ console.log(chalk.bold.white(' Session Statistics'));
125
+ console.log(chalk.gray(' ─────────────────────────────────────────────'));
126
+ if (entries.length === 0) {
127
+ console.log(chalk.gray(' No activity recorded yet.'));
128
+ console.log();
129
+ return;
130
+ }
131
+ // Time range
132
+ const first = new Date(entries[0].timestamp);
133
+ const last = new Date(entries[entries.length - 1].timestamp);
134
+ const durationMin = Math.round((last.getTime() - first.getTime()) / 60000);
135
+ console.log(chalk.gray(' First activity: ') + chalk.white(first.toLocaleString()));
136
+ console.log(chalk.gray(' Last activity: ') + chalk.white(last.toLocaleString()));
137
+ console.log(chalk.gray(' Duration: ') + chalk.white(`${durationMin} min`));
138
+ console.log();
139
+ // Count by type
140
+ const counts = {};
141
+ for (const e of entries) {
142
+ counts[e.level] = (counts[e.level] || 0) + 1;
143
+ }
144
+ console.log(chalk.gray(' Total commands: ') + chalk.white(String(entries.length)));
145
+ if (counts.command)
146
+ console.log(chalk.blue(' commands: ') + chalk.white(String(counts.command)));
147
+ if (counts.A)
148
+ console.log(chalk.green(' hint A: ') + chalk.white(String(counts.A)));
149
+ if (counts.B)
150
+ console.log(chalk.yellow(' hint B: ') + chalk.white(String(counts.B)));
151
+ if (counts.C)
152
+ console.log(chalk.red(' hint C: ') + chalk.white(String(counts.C)));
153
+ if (counts.submit)
154
+ console.log(chalk.magenta(' submissions: ') + chalk.white(String(counts.submit)));
155
+ // Exit info
156
+ if (existsSync(sessionFile)) {
157
+ try {
158
+ const session = JSON.parse(readFileSync(sessionFile, 'utf-8'));
159
+ console.log();
160
+ console.log(chalk.gray(' Exit count: ') + chalk.white(String(session.exitCount || 0)));
161
+ if (session.totalAwaySeconds) {
162
+ const awayMin = Math.round(session.totalAwaySeconds / 60);
163
+ console.log(chalk.gray(' Total away: ') + chalk.white(`${awayMin} min`));
164
+ }
165
+ }
166
+ catch {
167
+ /* ignore */
168
+ }
169
+ }
170
+ console.log();
171
+ }
@@ -17,4 +17,19 @@ export declare function resolveShellLaunch(opts: {
17
17
  mode: 'docker' | 'host';
18
18
  command: string;
19
19
  };
20
+ export { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
21
+ /**
22
+ * docker-run args as an ARRAY (spawnSync — no shell, so a cwd with spaces
23
+ * survives untouched). The two mounts are the aienv × docker interlock:
24
+ * - cwd → /work: challenge files flow both ways instead of being invisible
25
+ * from inside the container (and work done in /work outlives --rm).
26
+ * - icoa-aienv named volume → /root/.icoa: docker's copy-up seeds it from the
27
+ * image-baked venv on first use, then in-container pip installs survive
28
+ * --rm across sessions. Reset anytime: `docker volume rm icoa-aienv`.
29
+ */
30
+ export declare function buildSandboxRunArgs(opts: {
31
+ cwd: string;
32
+ image: string;
33
+ containerName: string;
34
+ }): string[];
20
35
  export declare function registerShellCommand(program: Command): void;