icoa-cli 2.19.263 → 2.19.265

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 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{registerAi4ctfCommand as d}from"./commands/ai4ctf.js";import{registerExamCommand as p}from"./commands/exam.js";import{registerCtf4aiDemoCommand as y}from"./commands/ctf4ai-demo.js";import{registerThemeCommand as h}from"./commands/theme.js";import{registerLearnCommand as f}from"./commands/learn.js";import{registerCtf4VlaCommand as u}from"./commands/ctf4vla.js";import{registerDemo2Command as w}from"./commands/demo2.js";import{registerSimCommand as T}from"./commands/sim.js";import{registerArenaCommand as b}from"./commands/arena.js";import{getConfig as v,saveConfig as I}from"./lib/config.js";import{startRepl as $}from"./repl.js";import{RELAUNCH_CODE as A}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 j}from"./lib/platform.js";import{ICOA_BIG as _}from"./lib/banner.js";import{readFileSync as S}from"node:fs";import{fileURLToPath as x}from"node:url";import{dirname as L,join as N}from"node:path";const O=L(x(import.meta.url)),k=JSON.parse(S(N(O,"..","package.json"),"utf-8")).version,F=chalk.cyan(" ─────────────────────────────────────────────────────"),R=`\n${F}\n\n${_.map(o=>` ${chalk.bold.white(o)}`).join("\n")}\n\n ${chalk.yellow("International Cyber Olympiad in AI 2026")}\n ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}\n ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}\n ${chalk.bold.magenta("and Olympiad for K-12")}\n\n ${chalk.green.bold("AI4CTF")} ${chalk.gray("[Day 1]")} ${chalk.white("AI as your teammate")}\n ${chalk.red.bold("CTF4AI")} ${chalk.gray("[Day 2]")} ${chalk.white("Challenge & evaluate AI systems")}\n ${chalk.cyan.bold("CTF4EAI")} ${chalk.gray("[Pioneer]")} ${chalk.white("Embodied AI security")}\n ${chalk.bold.yellow("AI is your teammate. AI is your target. AI is embodied.")}\n\n ${chalk.white("Sydney, Australia")} ${chalk.gray("Jun 27 - Jul 2, 2026")}\n ${chalk.cyan.underline("https://icoa2026.au")}\n\n ${chalk.gray(`CLI-Native Competition Terminal v${k}`)}\n\n${F}\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===A);process.exit(e??0)}}const U=new o;if(U.name("icoa").version(k).description("ICOA CLI — CLI-Native CTF Competition Terminal").option("--resume","Resume previous session").action(async o=>{const e=v();C("high-contrast"===e.themeVariant?"high-contrast":"dark"),E(),function(){const o=j();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(R),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 $(U,!!o.resume)}),e(U),n(U),s(U),r(U),t(U),i(U),a(U),l(U),m(U),c(U),g(U),d(U),p(U),y(U),h(U),f(U),u(U),w(U),T(U),b(U),U.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=v().geminiModel||"gemini-2.5-flash-lite";o?(I({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)))}U.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,23 @@
1
+ /**
2
+ * arena-submit — pure builder for an arena submission payload (Phase 3 of the
3
+ * CLI notebook arena). The all-in-CLI loop: a student computes in `icoa ipynb`,
4
+ * saves a local predictions.csv, and submits it here.
5
+ *
6
+ * If the input is a path that exists on disk, we send its CONTENT inline
7
+ * (`predictions_csv`) — this is what makes local submission work against a
8
+ * REMOTE server (prod can't read the student's disk, so a bare path is useless;
9
+ * the legacy Google-Drive link existed only to get the file TO the server).
10
+ * Otherwise we fall back to the Drive link (`gdrive_url`). Kept pure (fs deps
11
+ * injected) so the routing decision is unit-tested.
12
+ */
13
+ export interface SubmitPayload {
14
+ arena_token: string;
15
+ predictions_csv?: string;
16
+ gdrive_url?: string;
17
+ source: 'local' | 'gdrive';
18
+ }
19
+ export interface SubmitFsDeps {
20
+ exists: (path: string) => boolean;
21
+ read: (path: string) => string;
22
+ }
23
+ export declare function buildSubmitPayload(input: string, arenaToken: string, deps: SubmitFsDeps): SubmitPayload;
@@ -0,0 +1 @@
1
+ export function buildSubmitPayload(e,r,t){let i=e.trim();return i.startsWith("file://")&&(i=i.slice(7)),i&&t.exists(i)?{arena_token:r,predictions_csv:t.read(i),source:"local"}:{arena_token:r,gdrive_url:e.trim(),source:"gdrive"}}
@@ -1 +1 @@
1
- (function(a,b){const v=a0b,c=a();while(!![]){try{const d=-parseInt(v(0x145))/(-0x1*-0x2185+0x1e83+-0x25*0x1bb)*(-parseInt(v(0x138))/(0x46*-0x49+0x23ac+-0xc9*0x14))+-parseInt(v(0x148))/(-0x1f68+0x94e*0x1+0x161d)*(-parseInt(v(0x12a))/(0x18be+0x6e*-0x13+0x424*-0x4))+parseInt(v(0x130))/(-0x1a79+-0x3*-0xa28+-0x3fa)*(parseInt(v(0x14b))/(0x17af*0x1+-0x14e*0x2+-0x13d*0x11))+-parseInt(v(0x137))/(-0xb88+0x1*0x389+0x806)*(-parseInt(v(0x142))/(-0x32+0x653+-0x7*0xdf))+parseInt(v(0x133))/(0x156b+0x3*0xb7f+0x1*-0x37df)*(parseInt(v(0x12d))/(0x20*-0xc4+-0x23bb*0x1+0x8b*0x6f))+-parseInt(v(0x135))/(0x1f*0x1d+-0x160b+-0xf*-0x13d)*(-parseInt(v(0x12c))/(-0x1*-0x1258+0xa*0x61+0xb*-0x202))+-parseInt(v(0x13d))/(0x82a+0x102d*0x1+-0x184a);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a0a,-0x3*0x2de1c+-0x1*-0x8a973+0x7d82e));function a0b(a,b){a=a-(-0x217d+-0x13*-0x115+0xe18);const c=a0a();let d=c[a];if(a0b['dqtyzH']===undefined){var e=function(i){const j='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let l='',m='';for(let n=0x112b+-0x1*0x61a+-0x1*0xb11,o,p,q=-0x1bdd+-0x119f+0x2d7c;p=i['charAt'](q++);~p&&(o=n%(-0x2b3*0x3+0x2123+-0x1906)?o*(0x309*-0x6+0x13e2+-0x16c)+p:p,n++%(0xb2*-0x4+0x239*0x1+0x93*0x1))?l+=String['fromCharCode'](-0x3a7+0x1*0x118c+-0xce6*0x1&o>>(-(-0x2*-0x98e+0x2083+-0x339d)*n&0x49*-0xe+-0x1e99*-0x1+-0x1a95)):-0x1003+0x457+0xbac){p=j['indexOf'](p);}for(let r=0x18d1*-0x1+0x47*-0x5e+0x32e3,s=l['length'];r<s;r++){m+='%'+('00'+l['charCodeAt'](r)['toString'](-0x18f9+0x38f+0x157a))['slice'](-(0x1494+0x25b0+-0x3a42));}return decodeURIComponent(m);};a0b['OYuOVf']=e,a0b['UYukMM']={},a0b['dqtyzH']=!![];}const f=c[0x51*-0x6f+-0x1d*0x4d+0x2bd8],g=a+f,h=a0b['UYukMM'][g];return!h?(d=a0b['OYuOVf'](d),a0b['UYukMM'][g]=d):d=h,d;}import{getConfig as a0c}from'./config.js';export async function requestHint(d){const w=a0b,f=a0c(),g=f[w(0x12e)]||w(0x149),h=d[w(0x12b)]||f[w(0x13b)]||'en',j=d[w(0x14d)]??-0x341d+-0x2*-0x193a+0x20e9,k=[g+w(0x147)+d[w(0x13a)]+w(0x143),g+w(0x139)+d[w(0x13a)]+'/hint'];let l=null;for(const p of k)try{const q=await fetch(p,{'method':w(0x136),'headers':{'Content-Type':w(0x12f),'User-Agent':w(0x13c)},'body':JSON['stringify']({'token':d['token'],'question':d['question'],'level':d[w(0x140)],'lang':h}),'signal':AbortSignal['timeout'](j)}),r=await q[w(0x132)]()[w(0x134)](()=>({}));if(!q['ok']||!(-0x119f+-0x1fcd+0x316d)===r[w(0x131)]){if(l={'status':q[w(0x144)],'message':r?.[w(0x146)]||w(0x141)+q['status']+')'},q[w(0x144)]>=0x2123+0x23ee+-0x4381&&q[w(0x144)]<0x309*-0x6+0x13e2+0x48)throw l;continue;}return r[w(0x13e)];}catch(u){if(u&&w(0x13f)==typeof u&&w(0x144)in u)throw u;l={'status':0x0,'message':u?.[w(0x146)]||w(0x14c)};}const m={};m[w(0x144)]=0x0,m[w(0x146)]=w(0x14a);throw l||m;}function a0a(){const x=['Ahr0Chm6lY9WCMfJDgLJzs5Py29HmJaYnI5HDq','AgLUDcbbueKGDw5YzwfJAgfIBgu','nda1oty1ngn1uu1wBG','BMv0D29YAYbLCNjVCG','DgLTzw91De1Z','mtG3mdmYs2zQq3D3','BgfUzW','nZa0mZi4ANnmD1bj','nZbQBePbDvy','y3rMzfvYBa','yxbWBgLJyxrPB24VANnVBG','nwnowMjMyq','C3vJy2vZCW','ANnVBG','mtmZmtmZnfvAEhzuAa','y2f0y2G','mJjerejUAhe','ue9tva','nJKXmtyWnhftwKvKDG','mte3otmYyMDWq3nT','oJKWotaVyxbPl2LJB2eVzxHHBxmV','zxHHBuLK','BgfUz3vHz2u','AwnVys1JBgK','mZG1ntqXnZHHtMv1wxO','zgf0yq','B2jQzwn0','Bgv2zwW','AgLUDcbYzxf1zxn0igzHAwXLzcaO','ohLAANH5tG','l2HPBNq','C3rHDhvZ','mwj4vu1prq','BwvZC2fNzq','l2fWAs9Py29Hl2v4yw1ZlW','mZLOAMLwuK0'];a0a=function(){return x;};return a0a();}
1
+ (function(a,b){const v=a0b,c=a();while(!![]){try{const d=-parseInt(v(0x15f))/(0xf94*-0x1+0x75f+0x2*0x41b)+parseInt(v(0x16c))/(-0x133+0x6*-0x3f5+0x18f3)+parseInt(v(0x15a))/(0x14eb+0x1*0x2227+-0x370f)*(-parseInt(v(0x16f))/(-0x88b+0x242a+0x1b9b*-0x1))+parseInt(v(0x159))/(-0x48+0x68b+-0x31f*0x2)+-parseInt(v(0x169))/(-0x183e+0xbbc+0xc88)*(-parseInt(v(0x16b))/(0x3*0xc45+0x251*0x1+-0x2719))+-parseInt(v(0x16e))/(0xfb*-0x2+-0xb*-0x2a7+0x1b2f*-0x1)*(-parseInt(v(0x15c))/(0x26ee+0xaa*-0x2c+0x9ad*-0x1))+parseInt(v(0x164))/(-0x1e04+0xe7f+0xf8f);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a0a,0x73ace+-0x7e259+0x11*0x5284));import{getConfig as a0c}from'./config.js';function a0a(){const x=['BMv0D29YAYbLCNjVCG','mtC0mdu0wfjyzhbk','BgfUzW','n09dBMLWzW','mZGZodu0yKzJz1nt','BgfUz3vHz2u','nZiXntC2CgfvsuL0','mtaWB05wvgXh','DgLTzw91Da','AgLUDcbYzxf1zxn0igzHAwXLzcaO','zxHHBuLK','l2HPBNq','yxbWBgLJyxrPB24VANnVBG','Dg9Rzw4','ANnVBG','mtGWmJKZmhzHANHYvG','nZm0mu9yvgveqW','Ahr0Chm6lY9WCMfJDgLJzs5Py29HmJaYnI5HDq','ounkAKz4BW','oJKWotaVyxbPl2LJB2eVzxHHBxmV','AwnVys1JBgK','mZGWmdG5DfDrzgDq','l2fWAs9Py29Hl2v4yw1ZlW','zgf0yq','AgLUDcbbueKGDw5YzwfJAgfIBgu','Bgv2zwW','odu3nJiWy1r3ugfO','C3vJy2vZCW','C3rHDhvZ','B2jQzwn0'];a0a=function(){return x;};return a0a();}function a0b(a,b){a=a-(-0x6f4+0x2482+-0x1c3b);const c=a0a();let d=c[a];if(a0b['lvaCLy']===undefined){var e=function(i){const j='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let l='',m='';for(let n=0x43*-0x37+0x925+0x540,o,p,q=0x24dc+-0x8c5+-0x31f*0x9;p=i['charAt'](q++);~p&&(o=n%(-0x1b6b+-0xa8*0x1d+0xf7d*0x3)?o*(0x1*-0x2567+0x41*-0x19+-0x1600*-0x2)+p:p,n++%(-0x1*0x144b+0x1*-0x177a+-0x1*-0x2bc9))?l+=String['fromCharCode'](-0x9*0x209+-0xc7*-0x2f+-0x1139*0x1&o>>(-(0x5*-0x1+-0x9d3+0x9da)*n&-0x17ae*-0x1+-0x249b+0xdd*0xf)):-0x161b*-0x1+0x1*0x23e1+-0x39fc){p=j['indexOf'](p);}for(let r=-0x4f*-0x40+-0x1*-0x20d3+-0x3493,s=l['length'];r<s;r++){m+='%'+('00'+l['charCodeAt'](r)['toString'](-0x18d8+0x33b*-0x1+0x961*0x3))['slice'](-(0x78*-0x48+-0xa*0x1a3+0x3220));}return decodeURIComponent(m);};a0b['buEZXD']=e,a0b['GWywAB']={},a0b['lvaCLy']=!![];}const f=c[-0xfc6+-0x1cde+0x4*0xb29],g=a+f,h=a0b['GWywAB'][g];return!h?(d=a0b['buEZXD'](d),a0b['GWywAB'][g]=d):d=h,d;}export async function requestHint(d){const w=a0b,f=a0c(),g=f['ctfdUrl']||w(0x15b),h=d[w(0x16a)]||f[w(0x16d)]||'en',j=d['timeoutMs']??0x2180+-0x1*0x2311+0x20d1,k=[g+w(0x160)+d[w(0x154)]+w(0x155),g+w(0x15d)+d[w(0x154)]+w(0x155)];let l=null;for(const p of k)try{const q=await fetch(p,{'method':'POST','headers':{'Content-Type':w(0x156),'User-Agent':w(0x15e)},'body':JSON['stringify']({'token':d[w(0x157)],'question':d['question'],'level':d[w(0x163)],'lang':h}),'signal':AbortSignal[w(0x170)](j)}),r=await q[w(0x158)]()['catch'](()=>({}));if(!q['ok']||!(-0x1cc6+0x53b+0x178c)===r[w(0x165)]){if(l={'status':q[w(0x166)],'message':r?.['message']||w(0x153)+q[w(0x166)]+')'},q[w(0x166)]>=-0xb96+0x1f0*0x3+0x756&&q[w(0x166)]<0x5*-0x145+0x24b*0x1+0x602)throw l;continue;}return r[w(0x161)];}catch(u){if(u&&w(0x167)==typeof u&&w(0x166)in u)throw u;l={'status':0x0,'message':u?.['message']||w(0x168)};}const m={};m['status']=0x0,m['message']=w(0x162);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)}}}