icoa-cli 2.19.262 → 2.19.264

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
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.
6
+ *
7
+ * Runs against the aienv venv kernel via NotebookKernel (Node ↔ Python bridge).
8
+ * Lives in the main REPL using the BUG-008 listener-swap (no second readline).
9
+ */
10
+ import type { Command } from 'commander';
11
+ export declare function registerIpynbCommand(program: Command): void;
@@ -0,0 +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 s}from"../lib/ipynb-input.js";import{NotebookKernel as i}from"../lib/kernel.js";import{getMainRl as c}from"../lib/main-rl.js";const g=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(y(),!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 p=new i({venvPython:o.python,venvRoot:o.root});process.stdout.write(chalk.gray(" Starting Python kernel…"));try{await p.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 f=c(),u=null!==f,m=u?f.listeners("line").slice():[];u&&f.removeAllListeners("line");const h=u?f:r({input:process.stdin,output:process.stdout,terminal:!0}),w=[];let b=!1;const v=()=>{h.setPrompt(chalk.bold.cyan(w.length?"icoa ipynb ...> ":"icoa ipynb> ")),h.prompt()};let k=()=>{};const x=new Promise(e=>{k=e}),P=async e=>{if(b)return;const o=e.trim();if(0===w.length){const e=o.toLowerCase();if("quit"===e||"exit"===e||"back"===e||"q"===e)return void await(async()=>{if(h.removeAllListeners("line"),console.log(),console.log(chalk.gray(" Notebook closed. Kernel stopped.")),console.log(),await p.shutdown().catch(()=>{}),u){h.setPrompt(chalk.bold.cyan("icoa> "));for(const e of m)h.on("line",e);h.prompt(),k()}else k(),h.close()})();if("clear"===e||"cls"===e)return console.clear(),y(),void v();if("restart"===e)return await(async()=>{process.stdout.write(chalk.gray(" Restarting kernel…")),await p.shutdown().catch(()=>{});try{await p.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 v();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(" quit")+chalk.gray(" leave the notebook")),console.log(chalk.gray(" Everything else runs as Python in the persistent kernel.")),console.log(),void v();if(""===o)return void v()}if(w.push(e),!s(w))return void v();const t=w.join("\n").replace(/\s+$/,"");if(w.length=0,""!==t.trim()){b=!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(g,"")}`))}else"display"===n.kind&&d(n.data,o)}(await p.execute(t),n)}catch(e){console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}b=!1,v()}else v()};h.on("line",e=>{P(e)}),u||h.on("close",async()=>{await p.shutdown().catch(()=>{}),process.exit(0)}),v(),await x}()})}function y(){console.log(),console.log(chalk.bold.white(" ICOA Notebook ")+chalk.gray("· in-terminal Python · AI/ML notebook arena")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}let p=0;function d(e,t){if(e["image/png"])try{o(t,{recursive:!0}),p+=1;const r=l(t,`figure-${p}.png`);return n(r,Buffer.from(e["image/png"],"base64")),console.log(chalk.magenta(" 🖼 figure saved → ")+chalk.cyan(r)),void 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"]}`))}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as o}from"commander";import chalk from"chalk";import{registerCtfCommands as e}from"./commands/ctf.js";import{registerRefCommand as n}from"./commands/ref.js";import{registerShellCommand as s}from"./commands/shell.js";import{registerFilesCommand as r}from"./commands/files.js";import{registerConnectCommand as t}from"./commands/connect.js";import{registerNoteCommand as i}from"./commands/note.js";import{registerLogCommand as l}from"./commands/log.js";import{registerLangCommand as a}from"./commands/lang.js";import{registerSetupCommand as m}from"./commands/setup.js";import{registerEnvCommand as c}from"./commands/env.js";import{registerAi4ctfCommand as g}from"./commands/ai4ctf.js";import{registerExamCommand as d}from"./commands/exam.js";import{registerCtf4aiDemoCommand as p}from"./commands/ctf4ai-demo.js";import{registerThemeCommand as y}from"./commands/theme.js";import{registerLearnCommand as h}from"./commands/learn.js";import{registerCtf4VlaCommand as f}from"./commands/ctf4vla.js";import{registerDemo2Command as u}from"./commands/demo2.js";import{registerSimCommand as w}from"./commands/sim.js";import{registerArenaCommand as T}from"./commands/arena.js";import{getConfig as b,saveConfig as v}from"./lib/config.js";import{startRepl as I}from"./repl.js";import{RELAUNCH_CODE as $}from"./lib/menu-nav.js";import{setTerminalTheme as A}from"./lib/theme.js";import{checkForUpdates as C}from"./lib/update-check.js";import{detectIcoaInstalls as E}from"./lib/platform.js";import{ICOA_BIG as j}from"./lib/banner.js";import{readFileSync as _}from"node:fs";import{fileURLToPath as S}from"node:url";import{dirname as x,join as L}from"node:path";const N=x(S(import.meta.url)),O=JSON.parse(_(L(N,"..","package.json"),"utf-8")).version,k=chalk.cyan(" ─────────────────────────────────────────────────────"),F=`\n${k}\n\n${j.map(o=>` ${chalk.bold.white(o)}`).join("\n")}\n\n ${chalk.yellow("International Cyber Olympiad in AI 2026")}\n ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}\n ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}\n ${chalk.bold.magenta("and Olympiad for K-12")}\n\n ${chalk.green.bold("AI4CTF")} ${chalk.gray("[Day 1]")} ${chalk.white("AI as your teammate")}\n ${chalk.red.bold("CTF4AI")} ${chalk.gray("[Day 2]")} ${chalk.white("Challenge & evaluate AI systems")}\n ${chalk.cyan.bold("CTF4EAI")} ${chalk.gray("[Pioneer]")} ${chalk.white("Embodied AI security")}\n ${chalk.bold.yellow("AI is your teammate. AI is your target. AI is embodied.")}\n\n ${chalk.white("Sydney, Australia")} ${chalk.gray("Jun 27 - Jul 2, 2026")}\n ${chalk.cyan.underline("https://icoa2026.au")}\n\n ${chalk.gray(`CLI-Native Competition Terminal v${O}`)}\n\n${k}\n`;process.on("uncaughtException",o=>{"__REPL_NO_EXIT__"!==o.message&&(console.error(chalk.red("Error:"),o.message),process.exit(1))}),process.on("unhandledRejection",o=>{const e=o instanceof Error?o.message:String(o);"__REPL_NO_EXIT__"!==e&&(console.error(chalk.red("Error:"),e),process.exit(1))});{const o=process.argv.slice(2).some(o=>["-V","--version","-h","--help"].includes(o));if(!0===process.stdin.isTTY&&"1"!==process.env.ICOA_SUPERVISED&&!o){const{spawnSync:o}=await import("node:child_process");process.on("SIGINT",()=>{}),process.on("SIGTERM",()=>{});let e=0,n=!1;do{const s=o(process.execPath,process.argv.slice(1),{stdio:"inherit",env:{...process.env,ICOA_SUPERVISED:"1",...n?{ICOA_RELAUNCH:"1"}:{}}});if(s.signal){e=0;break}e=s.status,n=!0}while(e===$);process.exit(e??0)}}const R=new o;if(R.name("icoa").version(O).description("ICOA CLI — CLI-Native CTF Competition Terminal").option("--resume","Resume previous session").action(async o=>{const e=b();A("high-contrast"===e.themeVariant?"high-contrast":"dark"),C(),function(){const o=E();if(o.length<=1)return;const e=o[0];if([...o.map(o=>o.version||"0.0.0")].sort((o,e)=>function(o,e){const n=o.split(".").map(o=>parseInt(o,10)||0),s=e.split(".").map(o=>parseInt(o,10)||0);for(let o=0;o<3;o++)if((n[o]||0)!==(s[o]||0))return(n[o]||0)-(s[o]||0);return 0}(e,o))[0]!==e.version){console.log(),console.log(chalk.yellow.bold(" ⚠ Multiple icoa installations detected on PATH:"));for(let e=0;e<o.length;e++){const n=o[e],s=n.version?`v${n.version}`:"(version unreadable)",r=0===e?chalk.yellow("→"):" ",t=0===e?chalk.gray(" ← currently running (older — shadowing newer install)"):chalk.gray(" ← shadowed");console.log(` ${r} ${chalk.cyan(n.path.padEnd(28))} ${chalk.white(s)}${t}`)}console.log(),console.log(chalk.yellow(" The first install on PATH wins. To use the newer one, remove the older:")),console.log(),e.pkgDir?console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.pkgDir} ${e.path}`)}`):console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.path} # also delete its node_modules dir`)}`),console.log(),console.log(chalk.gray(" Then re-run icoa to confirm version banner shows the newer release.")),console.log()}}();const n=process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"";if(!/UTF-?8/i.test(n))if("win32"===process.platform){let o="";try{const{execFileSync:e}=await import("node:child_process");o=e("chcp.com",[],{encoding:"utf-8",timeout:1500,stdio:["ignore","pipe","ignore"]}).trim()}catch{}o.includes("65001")||(console.log(chalk.yellow(`⚠ Windows terminal is not using UTF-8 (current: ${o||"unknown"}).`)),console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix (run before ")+chalk.cyan("icoa")+chalk.gray("): ")+chalk.cyan("chcp 65001")),console.log(chalk.gray(" Or stay in English inside the CLI: ")+chalk.cyan("lang en")),console.log())}else console.log(chalk.yellow(`⚠ Your terminal locale is not UTF-8 (LANG=${n||"(unset)"}).`)),console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix: ")+chalk.cyan("export LANG=en_US.UTF-8")+chalk.gray(" (or your locale, e.g. ")+chalk.cyan("zh_CN.UTF-8")+chalk.gray(", ")+chalk.cyan("uk_UA.UTF-8")+chalk.gray(")")),console.log();if(console.log(F),process.argv.length<=2||o.resume)return o.resume||"1"===process.env.ICOA_RELAUNCH||await async function(){const o=process.stdin;if(o.isTTY&&"function"==typeof o.setRawMode)return new Promise(e=>{let n=!1;const s=()=>{if(!n){n=!0,o.removeListener("data",r);try{o.setRawMode(!1)}catch{}o.pause(),e()}},r=()=>s();o.setRawMode(!0),o.resume(),o.once("data",r),console.log(chalk.gray(" (press any key to continue...)")),setTimeout(s,3e3)});await new Promise(o=>setTimeout(o,3e3))}(),void I(R,!!o.resume)}),e(R),n(R),s(R),r(R),t(R),i(R),l(R),a(R),m(R),c(R),g(R),d(R),p(R),y(R),h(R),f(R),u(R),w(R),T(R),R.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=b().geminiModel||"gemini-2.5-flash-lite";o?(v({geminiModel:o}),console.log(),console.log(chalk.green(" Model switched: ")+chalk.gray(e)+chalk.white(" -> ")+chalk.bold.white(o)),console.log()):(console.log(),console.log(chalk.gray(" Current model: ")+chalk.white(e)),console.log(),console.log(chalk.gray(" Available models:")),console.log(chalk.bold.white(" Gemini 3.x (Latest)")),console.log(chalk.white(" model gemini-3.5-flash ")+chalk.gray("Newest stable, paid Tier 1+")),console.log(chalk.white(" model gemini-3-flash-preview ")+chalk.gray("Preview")),console.log(chalk.bold.white(" Gemini 2.5 (Stable)")),console.log(chalk.white(" model gemini-2.5-flash-lite ")+chalk.gray("Fastest, default, free tier")),console.log(chalk.white(" model gemini-2.5-flash ")+chalk.gray("Heavier, thinking enabled")),console.log(chalk.white(" model gemini-2.5-pro ")+chalk.gray("Strong reasoning, paid")),console.log(chalk.bold.white(" Open Source")),console.log(chalk.white(" model gemma-4-31b-it ")+chalk.gray("Free, open-source")),console.log(chalk.white(" model <any-model-id> ")+chalk.gray("Custom model")),console.log(),console.log(chalk.gray(" Translation uses gemini-3.5-flash for best quality.")),console.log())}),"1"===process.env.ICOA_RESET_STATE)try{const{clearExamState:o}=await import("./lib/exam-state.js");o(),console.log(chalk.yellow("⚠ ICOA_RESET_STATE=1 — local exam state wiped.")),console.log(chalk.gray(" (Token NOT revoked server-side. Re-enter a fresh token with `exam <token>`.)")),console.log()}catch(o){console.log(chalk.red("⚠ ICOA_RESET_STATE: could not clear state — ")+chalk.gray(String(o)))}R.parse();
2
+ import{Command as o}from"commander";import chalk from"chalk";import{registerCtfCommands as e}from"./commands/ctf.js";import{registerRefCommand as n}from"./commands/ref.js";import{registerShellCommand as s}from"./commands/shell.js";import{registerFilesCommand as r}from"./commands/files.js";import{registerConnectCommand as t}from"./commands/connect.js";import{registerNoteCommand as i}from"./commands/note.js";import{registerLogCommand as a}from"./commands/log.js";import{registerLangCommand as l}from"./commands/lang.js";import{registerSetupCommand as m}from"./commands/setup.js";import{registerEnvCommand as c}from"./commands/env.js";import{registerAienvCommand as g}from"./commands/aienv.js";import{registerIpynbCommand as d}from"./commands/ipynb.js";import{registerAi4ctfCommand as p}from"./commands/ai4ctf.js";import{registerExamCommand as y}from"./commands/exam.js";import{registerCtf4aiDemoCommand as h}from"./commands/ctf4ai-demo.js";import{registerThemeCommand as f}from"./commands/theme.js";import{registerLearnCommand as u}from"./commands/learn.js";import{registerCtf4VlaCommand as w}from"./commands/ctf4vla.js";import{registerDemo2Command as T}from"./commands/demo2.js";import{registerSimCommand as b}from"./commands/sim.js";import{registerArenaCommand as v}from"./commands/arena.js";import{getConfig as I,saveConfig as $}from"./lib/config.js";import{startRepl as A}from"./repl.js";import{RELAUNCH_CODE as j}from"./lib/menu-nav.js";import{setTerminalTheme as C}from"./lib/theme.js";import{checkForUpdates as E}from"./lib/update-check.js";import{detectIcoaInstalls as _}from"./lib/platform.js";import{ICOA_BIG as S}from"./lib/banner.js";import{readFileSync as x}from"node:fs";import{fileURLToPath as L}from"node:url";import{dirname as N,join as O}from"node:path";const k=N(L(import.meta.url)),F=JSON.parse(x(O(k,"..","package.json"),"utf-8")).version,R=chalk.cyan(" ─────────────────────────────────────────────────────"),U=`\n${R}\n\n${S.map(o=>` ${chalk.bold.white(o)}`).join("\n")}\n\n ${chalk.yellow("International Cyber Olympiad in AI 2026")}\n ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}\n ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}\n ${chalk.bold.magenta("and Olympiad for K-12")}\n\n ${chalk.green.bold("AI4CTF")} ${chalk.gray("[Day 1]")} ${chalk.white("AI as your teammate")}\n ${chalk.red.bold("CTF4AI")} ${chalk.gray("[Day 2]")} ${chalk.white("Challenge & evaluate AI systems")}\n ${chalk.cyan.bold("CTF4EAI")} ${chalk.gray("[Pioneer]")} ${chalk.white("Embodied AI security")}\n ${chalk.bold.yellow("AI is your teammate. AI is your target. AI is embodied.")}\n\n ${chalk.white("Sydney, Australia")} ${chalk.gray("Jun 27 - Jul 2, 2026")}\n ${chalk.cyan.underline("https://icoa2026.au")}\n\n ${chalk.gray(`CLI-Native Competition Terminal v${F}`)}\n\n${R}\n`;process.on("uncaughtException",o=>{"__REPL_NO_EXIT__"!==o.message&&(console.error(chalk.red("Error:"),o.message),process.exit(1))}),process.on("unhandledRejection",o=>{const e=o instanceof Error?o.message:String(o);"__REPL_NO_EXIT__"!==e&&(console.error(chalk.red("Error:"),e),process.exit(1))});{const o=process.argv.slice(2).some(o=>["-V","--version","-h","--help"].includes(o));if(!0===process.stdin.isTTY&&"1"!==process.env.ICOA_SUPERVISED&&!o){const{spawnSync:o}=await import("node:child_process");process.on("SIGINT",()=>{}),process.on("SIGTERM",()=>{});let e=0,n=!1;do{const s=o(process.execPath,process.argv.slice(1),{stdio:"inherit",env:{...process.env,ICOA_SUPERVISED:"1",...n?{ICOA_RELAUNCH:"1"}:{}}});if(s.signal){e=0;break}e=s.status,n=!0}while(e===j);process.exit(e??0)}}const P=new o;if(P.name("icoa").version(F).description("ICOA CLI — CLI-Native CTF Competition Terminal").option("--resume","Resume previous session").action(async o=>{const e=I();C("high-contrast"===e.themeVariant?"high-contrast":"dark"),E(),function(){const o=_();if(o.length<=1)return;const e=o[0];if([...o.map(o=>o.version||"0.0.0")].sort((o,e)=>function(o,e){const n=o.split(".").map(o=>parseInt(o,10)||0),s=e.split(".").map(o=>parseInt(o,10)||0);for(let o=0;o<3;o++)if((n[o]||0)!==(s[o]||0))return(n[o]||0)-(s[o]||0);return 0}(e,o))[0]!==e.version){console.log(),console.log(chalk.yellow.bold(" ⚠ Multiple icoa installations detected on PATH:"));for(let e=0;e<o.length;e++){const n=o[e],s=n.version?`v${n.version}`:"(version unreadable)",r=0===e?chalk.yellow("→"):" ",t=0===e?chalk.gray(" ← currently running (older — shadowing newer install)"):chalk.gray(" ← shadowed");console.log(` ${r} ${chalk.cyan(n.path.padEnd(28))} ${chalk.white(s)}${t}`)}console.log(),console.log(chalk.yellow(" The first install on PATH wins. To use the newer one, remove the older:")),console.log(),e.pkgDir?console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.pkgDir} ${e.path}`)}`):console.log(` ${chalk.bold.cyan(`sudo rm -rf ${e.path} # also delete its node_modules dir`)}`),console.log(),console.log(chalk.gray(" Then re-run icoa to confirm version banner shows the newer release.")),console.log()}}();const n=process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"";if(!/UTF-?8/i.test(n))if("win32"===process.platform){let o="";try{const{execFileSync:e}=await import("node:child_process");o=e("chcp.com",[],{encoding:"utf-8",timeout:1500,stdio:["ignore","pipe","ignore"]}).trim()}catch{}o.includes("65001")||(console.log(chalk.yellow(`⚠ Windows terminal is not using UTF-8 (current: ${o||"unknown"}).`)),console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix (run before ")+chalk.cyan("icoa")+chalk.gray("): ")+chalk.cyan("chcp 65001")),console.log(chalk.gray(" Or stay in English inside the CLI: ")+chalk.cyan("lang en")),console.log())}else console.log(chalk.yellow(`⚠ Your terminal locale is not UTF-8 (LANG=${n||"(unset)"}).`)),console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix: ")+chalk.cyan("export LANG=en_US.UTF-8")+chalk.gray(" (or your locale, e.g. ")+chalk.cyan("zh_CN.UTF-8")+chalk.gray(", ")+chalk.cyan("uk_UA.UTF-8")+chalk.gray(")")),console.log();if(console.log(U),process.argv.length<=2||o.resume)return o.resume||"1"===process.env.ICOA_RELAUNCH||await async function(){const o=process.stdin;if(o.isTTY&&"function"==typeof o.setRawMode)return new Promise(e=>{let n=!1;const s=()=>{if(!n){n=!0,o.removeListener("data",r);try{o.setRawMode(!1)}catch{}o.pause(),e()}},r=()=>s();o.setRawMode(!0),o.resume(),o.once("data",r),console.log(chalk.gray(" (press any key to continue...)")),setTimeout(s,3e3)});await new Promise(o=>setTimeout(o,3e3))}(),void A(P,!!o.resume)}),e(P),n(P),s(P),r(P),t(P),i(P),a(P),l(P),m(P),c(P),g(P),d(P),p(P),y(P),h(P),f(P),u(P),w(P),T(P),b(P),v(P),P.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=I().geminiModel||"gemini-2.5-flash-lite";o?($({geminiModel:o}),console.log(),console.log(chalk.green(" Model switched: ")+chalk.gray(e)+chalk.white(" -> ")+chalk.bold.white(o)),console.log()):(console.log(),console.log(chalk.gray(" Current model: ")+chalk.white(e)),console.log(),console.log(chalk.gray(" Available models:")),console.log(chalk.bold.white(" Gemini 3.x (Latest)")),console.log(chalk.white(" model gemini-3.5-flash ")+chalk.gray("Newest stable, paid Tier 1+")),console.log(chalk.white(" model gemini-3-flash-preview ")+chalk.gray("Preview")),console.log(chalk.bold.white(" Gemini 2.5 (Stable)")),console.log(chalk.white(" model gemini-2.5-flash-lite ")+chalk.gray("Fastest, default, free tier")),console.log(chalk.white(" model gemini-2.5-flash ")+chalk.gray("Heavier, thinking enabled")),console.log(chalk.white(" model gemini-2.5-pro ")+chalk.gray("Strong reasoning, paid")),console.log(chalk.bold.white(" Open Source")),console.log(chalk.white(" model gemma-4-31b-it ")+chalk.gray("Free, open-source")),console.log(chalk.white(" model <any-model-id> ")+chalk.gray("Custom model")),console.log(),console.log(chalk.gray(" Translation uses gemini-3.5-flash for best quality.")),console.log())}),"1"===process.env.ICOA_RESET_STATE)try{const{clearExamState:o}=await import("./lib/exam-state.js");o(),console.log(chalk.yellow("⚠ ICOA_RESET_STATE=1 — local exam state wiped.")),console.log(chalk.gray(" (Token NOT revoked server-side. Re-enter a fresh token with `exam <token>`.)")),console.log()}catch(o){console.log(chalk.red("⚠ ICOA_RESET_STATE: could not clear state — ")+chalk.gray(String(o)))}P.parse();
@@ -0,0 +1,93 @@
1
+ /**
2
+ * aienv — the AI/ML notebook-arena environment (Phase 0 of the CLI notebook
3
+ * arena, see `project_cli_notebook_arena_plan`).
4
+ *
5
+ * This is the PURE, testable core. The orchestration (venv creation, pip
6
+ * installs, status rendering) lives in `src/commands/aienv.ts`.
7
+ *
8
+ * Hard rule (BUG3 / Team Indonesia): aienv provisions its OWN venv under
9
+ * ~/.icoa/aienv and NEVER repoints the system python3. It also stays fully
10
+ * independent of `env.ts` so the heavy ML stack (jupyter/numpy/torch...) can
11
+ * never drag down the lightweight exam-only `env setup` path, and so a broken
12
+ * ML install is fault-isolated from the competition toolkit.
13
+ */
14
+ export type AiGroup = 'core' | 'data' | 'deep';
15
+ export interface AiPkg {
16
+ /** Display name + the name `pip show` uses. */
17
+ name: string;
18
+ /** Module name for the `import x` readiness probe. */
19
+ import: string;
20
+ /** pip install spec (floor-pinned — see note below). */
21
+ spec: string;
22
+ group: AiGroup;
23
+ note?: string;
24
+ }
25
+ export interface PyVersion {
26
+ major: number;
27
+ minor: number;
28
+ }
29
+ export interface AienvPaths {
30
+ /** The venv root: <home>/.icoa/aienv */
31
+ root: string;
32
+ /** Directory holding the venv executables (bin on posix, Scripts on win). */
33
+ binDir: string;
34
+ /** Absolute path to the venv's python interpreter. */
35
+ python: string;
36
+ /** Absolute path to the venv's pip. */
37
+ pip: string;
38
+ /** Manifest written after a successful setup — records python version + groups. */
39
+ marker: string;
40
+ }
41
+ export declare const MIN_HOST_PY: PyVersion;
42
+ export declare const PREFERRED_PY: PyVersion;
43
+ export declare const AIENV_MARKER_VERSION = 1;
44
+ /**
45
+ * ML stack for the notebook arena.
46
+ *
47
+ * Versions are FLOOR-pinned (`>=`), not exact-pinned like the CTF toolkit in
48
+ * env.ts. The scientific/ML stack ships platform- and arch-specific wheels that
49
+ * churn fast; exact pins across macOS/Linux/WSL × x64/arm64 routinely fail wheel
50
+ * resolution. Phase 0 only provisions — fairness-critical determinism lives
51
+ * server-side on the (fixed-model) GPU box, not in the student's local venv.
52
+ *
53
+ * - core: the Jupyter kernel-protocol foundation Phase 1's cell loop talks to.
54
+ * - data: the everyday data-science stack students compute with on CPU.
55
+ * - deep: multi-GB DL stack — OPT-IN via `aienv setup --deep` only. Under the
56
+ * fixed-model GPU design, most students call the server's inference API and
57
+ * never need torch locally, so it must not be in the default footprint.
58
+ */
59
+ export declare const AIENV_PACKAGES: AiPkg[];
60
+ export declare function packagesForGroups(groups: AiGroup[]): AiPkg[];
61
+ /** Compute the venv paths for a given home directory + platform. */
62
+ export declare function aienvPaths(home: string, plat?: NodeJS.Platform): AienvPaths;
63
+ /**
64
+ * Ordered host-python candidates to build the venv from. We probe a versioned
65
+ * 3.12 BEFORE the bare `python3` so we don't accidentally seed the venv with a
66
+ * distro 3.10 when a 3.12 is present. We never touch the system python3 beyond
67
+ * reading its --version.
68
+ */
69
+ export declare function hostPythonCandidates(plat?: NodeJS.Platform): string[];
70
+ /** Parse `Python 3.12.13` → {major:3, minor:12}. Tolerant of trailing newline. */
71
+ export declare function parsePyVersion(out: string | null | undefined): PyVersion | null;
72
+ /** True if `v` is at least `min` (major-then-minor comparison). */
73
+ export declare function meetsMinPy(v: PyVersion, min: PyVersion): boolean;
74
+ export interface HostPython {
75
+ cmd: string;
76
+ version: PyVersion;
77
+ }
78
+ /**
79
+ * Resolve the best host python to create the venv from.
80
+ *
81
+ * `probe(cmd)` runs `cmd --version` and returns its stdout (or null if the
82
+ * command is absent / errored) — injected so this stays pure + testable.
83
+ *
84
+ * Returns the first 3.12 interpreter found; otherwise the first python3 that
85
+ * meets MIN_HOST_PY; otherwise null.
86
+ */
87
+ export declare function resolveHostPython(probe: (cmd: string) => string | null, plat?: NodeJS.Platform): HostPython | null;
88
+ /**
89
+ * Build the readiness probe for one package, run via the VENV python (absolute
90
+ * path) — never the system python3. Mirrors env.ts's `python3 -c "import x"`
91
+ * convention but always targets the venv interpreter.
92
+ */
93
+ export declare function importCheckCmd(venvPython: string, pkg: AiPkg): string;
@@ -0,0 +1 @@
1
+ import{posix as o,win32 as r}from"node:path";export const MIN_HOST_PY={major:3,minor:10};export const PREFERRED_PY={major:3,minor:12};export const AIENV_MARKER_VERSION=1;export const AIENV_PACKAGES=[{name:"jupyter_client",import:"jupyter_client",spec:"jupyter_client>=8.6",group:"core"},{name:"ipykernel",import:"ipykernel",spec:"ipykernel>=6.29",group:"core"},{name:"numpy",import:"numpy",spec:"numpy>=1.26",group:"data"},{name:"pandas",import:"pandas",spec:"pandas>=2.2",group:"data"},{name:"scikit-learn",import:"sklearn",spec:"scikit-learn>=1.4",group:"data"},{name:"matplotlib",import:"matplotlib",spec:"matplotlib>=3.8",group:"data"},{name:"torch",import:"torch",spec:"torch>=2.2",group:"deep",note:"large download (~2 GB)"},{name:"transformers",import:"transformers",spec:"transformers>=4.40",group:"deep"},{name:"datasets",import:"datasets",spec:"datasets>=2.19",group:"deep"}];export function packagesForGroups(o){const r=new Set(o);return AIENV_PACKAGES.filter(o=>r.has(o.group))}export function aienvPaths(n,t=process.platform){const e="win32"===t?r:o,p=e.join(n,".icoa","aienv");if("win32"===t){const o=e.join(p,"Scripts");return{root:p,binDir:o,python:e.join(o,"python.exe"),pip:e.join(o,"pip.exe"),marker:e.join(p,"icoa-aienv.json")}}const i=e.join(p,"bin");return{root:p,binDir:i,python:e.join(i,"python"),pip:e.join(i,"pip"),marker:e.join(p,"icoa-aienv.json")}}export function hostPythonCandidates(o=process.platform){return"win32"===o?["py -3.12","python","py -3","py"]:"darwin"===o?["python3.12","/opt/homebrew/opt/python@3.12/bin/python3.12","/usr/local/opt/python@3.12/bin/python3.12","python3"]:["python3.12","python3"]}export function parsePyVersion(o){if(!o)return null;const r=o.match(/(\d+)\.(\d+)(?:\.\d+)?/);return r?{major:Number(r[1]),minor:Number(r[2])}:null}export function meetsMinPy(o,r){return o.major!==r.major?o.major>r.major:o.minor>=r.minor}export function resolveHostPython(o,r=process.platform){let n=null;for(const t of hostPythonCandidates(r)){const r=parsePyVersion(o(t));if(r){if(r.major===PREFERRED_PY.major&&r.minor===PREFERRED_PY.minor)return{cmd:t,version:r};!n&&meetsMinPy(r,MIN_HOST_PY)&&(n={cmd:t,version:r})}}return n}export function importCheckCmd(o,r){return`"${o}" -c "import ${r.import}"`}
@@ -1 +1 @@
1
- (function(a,b){const v=a0b,c=a();while(!![]){try{const d=-parseInt(v(0xc8))/(-0x896+0x1b19+0x1*-0x1282)+-parseInt(v(0xc5))/(0x6ef*-0x5+-0x11d7+0x3484)*(parseInt(v(0xcd))/(-0x21cf+0x6e5+0x1aed))+-parseInt(v(0xcb))/(0x7*0x20b+0x184*-0x2+-0xb41)*(-parseInt(v(0xd7))/(-0x17c7+-0x1fd2+0x12*0x317))+-parseInt(v(0xdb))/(-0x1699+0x486*0x1+0x1219)+-parseInt(v(0xd8))/(-0xda2+-0x1*0x1fcb+0x2d74)+parseInt(v(0xcc))/(0x4b3*-0x2+0x181a+-0x139*0xc)+parseInt(v(0xd5))/(-0x1820+-0x161e+0xf6d*0x3);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a0a,-0x15*-0x5ad4+0x16aa7d+-0x116074));import{getConfig as a0c}from'./config.js';export async function requestHint(d){const w=a0b,f=a0c(),g=f[w(0xdc)]||w(0xc9),h=d[w(0xde)]||f[w(0xcf)]||'en',j=d['timeoutMs']??-0x1aff*-0x1+0x29d8+-0x2597,k=[g+w(0xdd)+d['examId']+w(0xdf),g+':9090/api/icoa/exams/'+d['examId']+w(0xdf)];let l=null;for(const p of k)try{const q=await fetch(p,{'method':w(0xd6),'headers':{'Content-Type':'application/json','User-Agent':w(0xce)},'body':JSON['stringify']({'token':d[w(0xd9)],'question':d[w(0xd2)],'level':d[w(0xe0)],'lang':h}),'signal':AbortSignal[w(0xca)](j)}),r=await q['json']()[w(0xd3)](()=>({}));if(!q['ok']||!(0xbb5+-0x13bb+-0x5*-0x19b)===r[w(0xd1)]){if(l={'status':q[w(0xc4)],'message':r?.[w(0xda)]||w(0xc6)+q[w(0xc4)]+')'},q[w(0xc4)]>=0x98f+-0x8*0x49a+0x1cd1*0x1&&q[w(0xc4)]<-0x4*-0x80f+0x1db1+-0x3bf9)throw l;continue;}return r[w(0xc7)];}catch(u){if(u&&'object'==typeof u&&'status'in u)throw u;l={'status':0x0,'message':u?.[w(0xda)]||w(0xd4)};}const m={};m[w(0xc4)]=0x0,m[w(0xda)]=w(0xd0);throw l||m;}function a0b(a,b){a=a-(-0x1f9e+0x172c+0x49b*0x2);const c=a0a();let d=c[a];if(a0b['fkCHGA']===undefined){var e=function(i){const j='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let l='',m='';for(let n=0x8e*-0x5+-0x2c2*-0xc+-0x1e52,o,p,q=-0x887+0xbb5+-0x32e;p=i['charAt'](q++);~p&&(o=n%(-0x258b*-0x1+-0xc03+-0x2*0xcc2)?o*(0xf6*0x4+-0x2c*-0xc1+-0x24c4)+p:p,n++%(-0x1fd4+0x18bc+-0xa*-0xb6))?l+=String['fromCharCode'](-0xf3f+-0x1251*-0x2+-0x6cc*0x3&o>>(-(-0x1a82+-0x1a48+0xd33*0x4)*n&0x73b+-0x11c7+0xa92)):0xf02+0x1*-0x193f+0xa3d){p=j['indexOf'](p);}for(let r=0x1*0x1dfe+0x40*-0x22+0x7*-0x312,s=l['length'];r<s;r++){m+='%'+('00'+l['charCodeAt'](r)['toString'](0xfc6+0x1*0x1597+-0x254d))['slice'](-(-0x131c*-0x2+-0x7d*0x35+-0x7*0x1c3));}return decodeURIComponent(m);};a0b['tkYoLn']=e,a0b['romcFW']={},a0b['fkCHGA']=!![];}const f=c[-0xeb4+-0x18d8+0x278c*0x1],g=a+f,h=a0b['romcFW'][g];return!h?(d=a0b['tkYoLn'](d),a0b['romcFW'][g]=d):d=h,d;}function a0a(){const x=['ndu2mJuWowPKBLzyqq','Dg9Rzw4','BwvZC2fNzq','mJm5ntGYngrgBfz1Cq','y3rMzfvYBa','l2fWAs9Py29Hl2v4yw1ZlW','BgfUzW','l2HPBNq','Bgv2zwW','C3rHDhvZ','mtK5nJe1nMjkANPmqW','AgLUDcbYzxf1zxn0igzHAwXLzcaO','zgf0yq','mtq5ntyYmLHvrw9XAq','Ahr0Chm6lY9WCMfJDgLJzs5Py29HmJaYnI5HDq','DgLTzw91Da','ndK2mZyYme5lseTpyW','mti1mJy5mJbAu25Vs2y','m2ruDvvdua','AwnVys1JBgK','BgfUz3vHz2u','AgLUDcbbueKGDw5YzwfJAgfIBgu','C3vJy2vZCW','CxvLC3rPB24','y2f0y2G','BMv0D29YAYbLCNjVCG','mtqXnty1mtreB3zOBvO','ue9tva','nvnlEfPPqW'];a0a=function(){return x;};return a0a();}
1
+ function a0a(){const x=['BMv0D29YAYbLCNjVCG','mti1mde1oujtuuDitq','mta2mJC4ne9Qt1HmEa','y3rMzfvYBa','ntq2nZi4rgDuB0Li','ue9tva','C3rHDhvZ','yxbWBgLJyxrPB24VANnVBG','zxHHBuLK','y2f0y2G','Bgv2zwW','DgLTzw91De1Z','mZmWota2rMjKugjg','Dg9Rzw4','ntbRwK1My2C','nJmWmde4oxvivwfwwa','mtmZmtG0mhbXzMPowG','AwnVys1JBgK','zgf0yq','BgfUzW','oJKWotaVyxbPl2LJB2eVzxHHBxmV','otbczfnoBeO','Ahr0Chm6lY9WCMfJDgLJzs5Py29HmJaYnI5HDq','BwvZC2fNzq','CxvLC3rPB24','l2fWAs9Py29Hl2v4yw1ZlW','m0L4C2vSAG','C3vJy2vZCW','mtG5nJeWofDYugTeDa'];a0a=function(){return x;};return a0a();}(function(a,b){const v=a0b,c=a();while(!![]){try{const d=parseInt(v(0x180))/(-0x25e7+-0x41e+-0x21*-0x146)+-parseInt(v(0x181))/(-0xcb9*0x3+-0x11ba*0x1+0x37e7)+-parseInt(v(0x17c))/(-0x2*0xb3e+0x6c5+0xfba)*(-parseInt(v(0x17e))/(-0x145a+-0xd07+-0x67*-0x53))+-parseInt(v(0x18d))/(0xb2f*-0x2+-0x109e+-0x7cd*-0x5)*(parseInt(v(0x18b))/(0x9*-0x3ef+-0xfc4*-0x1+0x13a9))+parseInt(v(0x18e))/(0x4*0x23b+-0x1*-0x21a3+-0x2a88)+-parseInt(v(0x183))/(0xc5a+-0x1*-0x8a1+-0x14f3)*(parseInt(v(0x194))/(-0x95b+-0x13c*-0x18+-0x1*0x143c))+-parseInt(v(0x18f))/(-0x15d3+-0x3c9+-0x19a6*-0x1);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a0a,-0xa027e+0x15ba68+-0xa8fd));import{getConfig as a0c}from'./config.js';function a0b(a,b){a=a-(-0x86e+-0x723+0x31*0x59);const c=a0a();let d=c[a];if(a0b['iAglZo']===undefined){var e=function(i){const j='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let l='',m='';for(let n=0x4*-0x873+-0x357+0x2523,o,p,q=-0x9be+-0x16f9+0x20b7;p=i['charAt'](q++);~p&&(o=n%(-0x243d+0x1c67*0x1+0xf*0x86)?o*(-0x1b20+-0xdd5+0x2935)+p:p,n++%(0x1101*0x2+0x284+-0x2*0x1241))?l+=String['fromCharCode'](0x1842+-0x126b+-0x5*0xf8&o>>(-(0x11d8+-0x484*0x2+-0x8ce)*n&0x2*0xdd+0x1*0x1ba7+-0x1f5*0xf)):-0x1*-0x1515+-0x2*-0x1190+-0x3835){p=j['indexOf'](p);}for(let r=-0x1ffd+-0x1f*0xb7+-0xef*-0x3a,s=l['length'];r<s;r++){m+='%'+('00'+l['charCodeAt'](r)['toString'](0x5f7+0x8*-0x133+0x3b1))['slice'](-(-0x23b5*0x1+-0x1d92+-0x4149*-0x1));}return decodeURIComponent(m);};a0b['SCQkvT']=e,a0b['LfjQXO']={},a0b['iAglZo']=!![];}const f=c[0x86d*0x1+-0x7c4+0xa9*-0x1],g=a+f,h=a0b['LfjQXO'][g];return!h?(d=a0b['SCQkvT'](d),a0b['LfjQXO'][g]=d):d=h,d;}export async function requestHint(d){const w=a0b,f=a0c(),g=f[w(0x182)]||w(0x178),h=d[w(0x192)]||f['language']||'en',j=d[w(0x18a)]??0x3b44+0x930+-0x2534,k=[g+w(0x17b)+d[w(0x187)]+'/hint',g+w(0x193)+d[w(0x187)]+'/hint'];let l=null;for(const p of k)try{const q=await fetch(p,{'method':w(0x184),'headers':{'Content-Type':w(0x186),'User-Agent':w(0x190)},'body':JSON['stringify']({'token':d[w(0x18c)],'question':d[w(0x17a)],'level':d[w(0x189)],'lang':h}),'signal':AbortSignal['timeout'](j)}),r=await q['json']()[w(0x188)](()=>({}));if(!q['ok']||!(-0x16f9+-0xc97+0x2391)===r[w(0x17d)]){if(l={'status':q[w(0x185)],'message':r?.['message']||'hint\x20request\x20failed\x20('+q['status']+')'},q[w(0x185)]>=0x1c67*0x1+0x1c*-0x42+-0x139f&&q[w(0x185)]<-0xdd5+-0x144c+0x1*0x2415)throw l;continue;}return r[w(0x191)];}catch(u){if(u&&'object'==typeof u&&'status'in u)throw u;l={'status':0x0,'message':u?.['message']||w(0x17f)};}const m={};m[w(0x185)]=0x0,m[w(0x179)]='hint\x20API\x20unreachable';throw l||m;}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ipynb-input — pure cell-input decision logic for the `icoa ipynb` REPL
3
+ * (Phase 1 UI of the CLI notebook arena).
4
+ *
5
+ * IPython-style model: a one-liner runs the moment you press Enter; a block
6
+ * (def/for/if/…, an open bracket, or a trailing backslash) keeps accepting
7
+ * lines until you end it with a blank line. Kept pure so the rule is unit-
8
+ * tested without a kernel or readline.
9
+ */
10
+ /**
11
+ * Does this line open a multi-line block? True when its CODE portion (trailing
12
+ * `#` comment stripped) ends with a colon. Naive comment-strip — good enough for
13
+ * opener detection (a real block opener never hides its colon in a comment).
14
+ */
15
+ export declare function isBlockOpener(line: string): boolean;
16
+ /**
17
+ * Given the lines accumulated SO FAR (including the just-entered line), decide
18
+ * whether to execute the cell now or keep taking input.
19
+ *
20
+ * - empty buffer → nothing to run (false),
21
+ * - multi-line cell → execute as soon as the last line is blank,
22
+ * - single line → execute unless it opens a block / ends with `\` / has an
23
+ * unbalanced bracket.
24
+ */
25
+ export declare function shouldExecuteCell(lines: string[]): boolean;
@@ -0,0 +1 @@
1
+ export function isBlockOpener(t){const e=t.split("#")[0];return/:\s*$/.test(e)}export function shouldExecuteCell(t){if(0===t.length)return!1;const e=t[t.length-1];return t.length>1?""===e.trim():!(""===e.trim()||isBlockOpener(e)||/\\\s*$/.test(e)||function(t){let e=0;for(const n of t)"("===n||"["===n||"{"===n?e++:")"!==n&&"]"!==n&&"}"!==n||e--;return e}(e)>0)}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Kernel protocol — the pure Node side of the Node ↔ Python-bridge boundary
3
+ * (Phase 1 of the CLI notebook arena, see `project_cli_notebook_arena_plan`).
4
+ *
5
+ * icoa is TypeScript/Node, but Jupyter kernels are driven by `jupyter_client`
6
+ * (Python). So a thin Python bridge (shipped alongside, run from the aienv
7
+ * venv) owns the kernel and speaks a line-delimited JSON protocol to Node: one
8
+ * event object per stdout line. This module parses those lines and folds them
9
+ * into renderable cell outputs.
10
+ *
11
+ * Keeping this layer pure (no I/O) means the wire contract is unit-tested
12
+ * without spawning Python — and it stays identical whether the bridge drives a
13
+ * LOCAL kernel (Phase 1) or a REMOTE one over a Kernel Gateway (Phase 2/4). The
14
+ * Node side never changes; only the bridge's connection does.
15
+ */
16
+ /** Events the Python bridge emits, one JSON object per stdout line. */
17
+ export type KernelEvent = {
18
+ type: 'ready';
19
+ } | {
20
+ type: 'stream';
21
+ name: 'stdout' | 'stderr';
22
+ text: string;
23
+ } | {
24
+ type: 'result';
25
+ text: string;
26
+ data?: Record<string, string>;
27
+ } | {
28
+ type: 'display';
29
+ data: Record<string, string>;
30
+ } | {
31
+ type: 'error';
32
+ ename: string;
33
+ evalue: string;
34
+ traceback: string[];
35
+ } | {
36
+ type: 'done';
37
+ id: number;
38
+ count: number | null;
39
+ } | {
40
+ type: 'fatal';
41
+ message: string;
42
+ };
43
+ /** Normalized, render-ready outputs for one executed cell. */
44
+ export type CellOutput = {
45
+ kind: 'stream';
46
+ name: 'stdout' | 'stderr';
47
+ text: string;
48
+ } | {
49
+ kind: 'result';
50
+ text: string;
51
+ data?: Record<string, string>;
52
+ } | {
53
+ kind: 'display';
54
+ data: Record<string, string>;
55
+ } | {
56
+ kind: 'error';
57
+ ename: string;
58
+ evalue: string;
59
+ traceback: string[];
60
+ };
61
+ export interface FoldedCell {
62
+ outputs: CellOutput[];
63
+ /** false if the cell raised. */
64
+ ok: boolean;
65
+ /** the kernel's execution_count, or null if no `done` event was seen. */
66
+ execCount: number | null;
67
+ }
68
+ /**
69
+ * Parse one stdout line from the bridge into a KernelEvent. Returns null for
70
+ * blank lines, non-JSON, or JSON without a recognized `type` (the bridge may
71
+ * interleave incidental output; we ignore anything not in the protocol).
72
+ */
73
+ export declare function parseKernelEvent(line: string): KernelEvent | null;
74
+ /**
75
+ * Fold a cell's event stream into renderable outputs.
76
+ *
77
+ * - consecutive `stream` events of the SAME name coalesce into one text block
78
+ * (matches Jupyter's stdout/stderr batching → clean rendering),
79
+ * - `result` / `display` / `error` pass through as discrete blocks,
80
+ * - any `error` sets ok=false,
81
+ * - `done.count` becomes execCount (null if the cell never finished).
82
+ */
83
+ export declare function foldCellOutputs(events: KernelEvent[]): FoldedCell;
@@ -0,0 +1 @@
1
+ const e=new Set(["ready","stream","result","display","error","done","fatal"]);export function parseKernelEvent(t){const a=t.trim();if(!a)return null;let r;try{r=JSON.parse(a)}catch{return null}if(!r||"object"!=typeof r)return null;const n=r.type;return"string"==typeof n&&e.has(n)?r:null}export function foldCellOutputs(e){const t=[];let a=!0,r=null;for(const n of e)switch(n.type){case"stream":{const e=t[t.length-1];e&&"stream"===e.kind&&e.name===n.name?e.text+=n.text:t.push({kind:"stream",name:n.name,text:n.text});break}case"result":t.push({kind:"result",text:n.text,...n.data?{data:n.data}:{}});break;case"display":t.push({kind:"display",data:n.data});break;case"error":a=!1,t.push({kind:"error",ename:n.ename,evalue:n.evalue,traceback:n.traceback});break;case"done":r=n.count;break;case"fatal":a=!1,t.push({kind:"error",ename:"KernelError",evalue:n.message,traceback:[n.message]})}return{outputs:t,ok:a,execCount:r}}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * NotebookKernel — the Node driver for a Jupyter kernel, Phase 1 of the CLI
3
+ * notebook arena (see `project_cli_notebook_arena_plan`).
4
+ *
5
+ * icoa (Node) can't talk to a Jupyter kernel directly, so we spawn a small
6
+ * Python BRIDGE under the aienv venv. The bridge owns the kernel via
7
+ * `jupyter_client` and exchanges line-delimited JSON with us (parsed by
8
+ * `kernel-protocol.ts`). This keeps the kernel a PLUGGABLE backend: Phase 1
9
+ * runs it locally; Phase 2/4 point the same bridge at a remote Kernel Gateway
10
+ * with zero change to the Node side.
11
+ *
12
+ * Isolation: the bridge runs as `<venv>/bin/python`, and it registers its
13
+ * kernelspec INSIDE the venv's own `share/jupyter` tree — so the kernel is the
14
+ * venv interpreter, never the system python3 (BUG3 invariant), and nothing is
15
+ * written outside the venv. On posix the kernel uses IPC transport (unix
16
+ * sockets) so it opens no TCP ports.
17
+ */
18
+ import { type FoldedCell } from './kernel-protocol.js';
19
+ export declare const KERNEL_BRIDGE_PY: string;
20
+ export interface KernelStartOptions {
21
+ /** Absolute path to the venv python (from aienvPaths().python). */
22
+ venvPython: string;
23
+ /** Directory to drop the bridge script in (the venv root). */
24
+ venvRoot: string;
25
+ /** Optional: max ms to wait for the kernel `ready` event. */
26
+ readyTimeoutMs?: number;
27
+ }
28
+ /**
29
+ * A live notebook kernel. Executes are serialized (one cell at a time, as a
30
+ * notebook does) so the line-buffered output demux stays unambiguous.
31
+ */
32
+ export declare class NotebookKernel {
33
+ private proc;
34
+ private buf;
35
+ private nextId;
36
+ private ready;
37
+ private pending;
38
+ private onReady;
39
+ private onExit;
40
+ private readonly opts;
41
+ private readonly bridgePath;
42
+ constructor(opts: KernelStartOptions);
43
+ /**
44
+ * Write the bridge + spawn it; resolves once the kernel reports `ready`.
45
+ * Safe to call again after shutdown() to get a fresh kernel (restart) — all
46
+ * per-run state is reset here.
47
+ */
48
+ start(): Promise<void>;
49
+ /** Run one cell; resolves with its folded outputs after the `done` event. */
50
+ execute(code: string): Promise<FoldedCell>;
51
+ /** Ask the bridge to shut the kernel down; resolves when the process exits. */
52
+ shutdown(): Promise<void>;
53
+ private onStdout;
54
+ private handleEvent;
55
+ }
@@ -0,0 +1,81 @@
1
+ import{spawn as e}from"node:child_process";import{writeFileSync as t}from"node:fs";import{join as i}from"node:path";import{foldCellOutputs as n,parseKernelEvent as r}from"./kernel-protocol.js";export const KERNEL_BRIDGE_PY=String.raw`import sys, os, json, queue
2
+
3
+ def emit(o):
4
+ sys.stdout.write(json.dumps(o) + "\n")
5
+ sys.stdout.flush()
6
+
7
+ def main():
8
+ # Self-contained kernelspec INSIDE the venv (found via sys.prefix; isolated,
9
+ # wiped with the venv). Points the kernel at this very interpreter.
10
+ specdir = os.path.join(sys.prefix, "share", "jupyter", "kernels", "icoa-aienv")
11
+ try:
12
+ os.makedirs(specdir, exist_ok=True)
13
+ with open(os.path.join(specdir, "kernel.json"), "w") as f:
14
+ json.dump({"argv": [sys.executable, "-m", "ipykernel_launcher", "-f", "{connection_file}"],
15
+ "display_name": "ICOA aienv", "language": "python"}, f)
16
+ except Exception as e:
17
+ emit({"type": "fatal", "message": "could not write kernelspec: %s" % e}); os._exit(1)
18
+
19
+ from jupyter_client.manager import KernelManager
20
+ transport = "ipc" if os.name == "posix" else "tcp"
21
+ km = KernelManager(kernel_name="icoa-aienv", transport=transport)
22
+ try:
23
+ km.start_kernel()
24
+ kc = km.client(); kc.start_channels(); kc.wait_for_ready(timeout=60)
25
+ except Exception as e:
26
+ emit({"type": "fatal", "message": "kernel did not start: %s" % e}); os._exit(1)
27
+ emit({"type": "ready"})
28
+
29
+ for line in sys.stdin:
30
+ line = line.strip()
31
+ if not line:
32
+ continue
33
+ try:
34
+ req = json.loads(line)
35
+ except Exception:
36
+ continue
37
+ if req.get("cmd") == "shutdown":
38
+ break
39
+ cid = req.get("id"); code = req.get("code", "")
40
+ mid = kc.execute(code)
41
+ count = None
42
+ # Drain iopub for THIS request until the kernel goes idle.
43
+ while True:
44
+ try:
45
+ m = kc.get_iopub_msg(timeout=1)
46
+ except queue.Empty:
47
+ if not km.is_alive():
48
+ emit({"type": "fatal", "message": "kernel died during execution"}); break
49
+ continue
50
+ if m.get("parent_header", {}).get("msg_id") != mid:
51
+ continue
52
+ t = m["msg_type"]; c = m["content"]
53
+ if t == "stream":
54
+ emit({"type": "stream", "name": c.get("name", "stdout"), "text": c.get("text", "")})
55
+ elif t == "execute_result":
56
+ count = c.get("execution_count", count)
57
+ emit({"type": "result", "text": c["data"].get("text/plain", ""), "data": c["data"]})
58
+ elif t == "display_data":
59
+ emit({"type": "display", "data": c["data"]})
60
+ elif t == "error":
61
+ emit({"type": "error", "ename": c.get("ename", ""), "evalue": c.get("evalue", ""),
62
+ "traceback": c.get("traceback", [])})
63
+ elif t == "status" and c.get("execution_state") == "idle":
64
+ break
65
+ # Shell reply carries the authoritative execution_count for In[n] numbering.
66
+ try:
67
+ reply = kc.get_shell_msg(timeout=5)
68
+ if reply.get("parent_header", {}).get("msg_id") == mid:
69
+ count = reply["content"].get("execution_count", count)
70
+ except Exception:
71
+ pass
72
+ emit({"type": "done", "id": cid, "count": count})
73
+
74
+ try:
75
+ km.shutdown_kernel(now=True)
76
+ except Exception:
77
+ pass
78
+ os._exit(0)
79
+
80
+ main()
81
+ `;export class NotebookKernel{proc=null;buf="";nextId=1;ready=!1;pending=null;onReady=null;onExit=null;opts;bridgePath;constructor(e){this.opts=e,this.bridgePath=i(e.venvRoot,"icoa-kernel-bridge.py")}start(){this.buf="",this.ready=!1,this.pending=null,this.nextId=1,t(this.bridgePath,KERNEL_BRIDGE_PY);const i=e(this.opts.venvPython,[this.bridgePath],{stdio:["pipe","pipe","pipe"]});this.proc=i,i.stdout.setEncoding("utf-8"),i.stdout.on("data",e=>this.onStdout(e)),i.on("exit",()=>{this.ready=!1,this.onExit?.()});const n=this.opts.readyTimeoutMs??6e4;return new Promise((e,t)=>{const i=setTimeout(()=>t(new Error("kernel did not become ready in time")),n);this.onReady=()=>{clearTimeout(i),e()},this.onExit=()=>{clearTimeout(i),this.ready||t(new Error("kernel process exited before ready"))}})}execute(e){if(!this.proc||!this.ready)return Promise.reject(new Error("kernel not ready"));if(this.pending)return Promise.reject(new Error("a cell is already executing"));const t=this.nextId++;return new Promise(i=>{this.pending={id:t,events:[],resolve:i},this.proc?.stdin.write(`${JSON.stringify({id:t,code:e})}\n`)})}shutdown(){const e=this.proc;return e?new Promise(t=>{this.onExit=()=>t();try{e.stdin.write(`${JSON.stringify({cmd:"shutdown"})}\n`)}catch{e.kill()}setTimeout(()=>{this.proc&&(this.proc.kill(),t())},5e3)}):Promise.resolve()}onStdout(e){this.buf+=e;let t=this.buf.indexOf("\n");for(;-1!==t;){const e=this.buf.slice(0,t);this.buf=this.buf.slice(t+1),this.handleEvent(r(e)),t=this.buf.indexOf("\n")}}handleEvent(e){if(!e)return;if("ready"===e.type)return this.ready=!0,void this.onReady?.();const t=this.pending;if(t){if("done"===e.type){if(e.id!==t.id)return;const i=n(t.events.concat(e));return this.pending=null,void t.resolve(i)}t.events.push(e)}}}