icoa-cli 2.19.355 → 2.19.357

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/commands/ai4ctf.js +1 -1
  2. package/dist/commands/arena.js +1 -1
  3. package/dist/commands/ctf.js +787 -1
  4. package/dist/commands/ctf4ai-demo.js +1 -1
  5. package/dist/commands/ctf4vla.js +1 -1
  6. package/dist/commands/demo2.js +1502 -1
  7. package/dist/commands/exam.js +1 -1
  8. package/dist/commands/files.js +59 -1
  9. package/dist/commands/ipynb.d.ts +10 -4
  10. package/dist/commands/ipynb.js +1 -1
  11. package/dist/commands/lang.js +202 -1
  12. package/dist/commands/log.js +171 -1
  13. package/dist/commands/shell.d.ts +15 -0
  14. package/dist/commands/shell.js +151 -1
  15. package/dist/commands/sim.js +389 -1
  16. package/dist/index.js +355 -1
  17. package/dist/lib/access.js +184 -1
  18. package/dist/lib/aienv.js +205 -1
  19. package/dist/lib/arena-submit.js +21 -1
  20. package/dist/lib/banner.js +31 -1
  21. package/dist/lib/budget.js +6 -1
  22. package/dist/lib/challenge-dir.js +16 -1
  23. package/dist/lib/colors.js +17 -1
  24. package/dist/lib/comms.js +212 -1
  25. package/dist/lib/config.js +93 -1
  26. package/dist/lib/countdown.js +43 -1
  27. package/dist/lib/country-lang.js +39 -1
  28. package/dist/lib/ctfd-client.js +417 -1
  29. package/dist/lib/demo-exam.js +478 -1
  30. package/dist/lib/demo-flags.js +27 -1
  31. package/dist/lib/demo-stats.js +62 -1
  32. package/dist/lib/demo2-progress.js +102 -1
  33. package/dist/lib/docker-probe.d.ts +45 -0
  34. package/dist/lib/docker-probe.js +118 -0
  35. package/dist/lib/editor-spawn.d.ts +23 -0
  36. package/dist/lib/editor-spawn.js +53 -0
  37. package/dist/lib/exam-client.js +54 -1
  38. package/dist/lib/exam-sandbox.js +201 -1
  39. package/dist/lib/exam-setup.js +36 -1
  40. package/dist/lib/exam-state.js +273 -1
  41. package/dist/lib/gemini.js +247 -1
  42. package/dist/lib/i18n.js +302 -1
  43. package/dist/lib/integrity-snapshot.js +88 -1
  44. package/dist/lib/interactive-spawn.js +55 -1
  45. package/dist/lib/ipynb-input.js +65 -1
  46. package/dist/lib/kernel-protocol.js +88 -1
  47. package/dist/lib/kernel.js +146 -2
  48. package/dist/lib/learn-curricula.js +309 -1
  49. package/dist/lib/learn-i18n.js +184 -1
  50. package/dist/lib/learn-input.js +101 -1
  51. package/dist/lib/learn-render.js +863 -1
  52. package/dist/lib/learn-state.js +103 -1
  53. package/dist/lib/log-sync.js +155 -1
  54. package/dist/lib/logger.js +49 -1
  55. package/dist/lib/main-rl.js +7 -1
  56. package/dist/lib/menu-nav.js +105 -1
  57. package/dist/lib/notebook-doc.d.ts +38 -0
  58. package/dist/lib/notebook-doc.js +137 -0
  59. package/dist/lib/open-file.js +55 -1
  60. package/dist/lib/paper-upgrade.js +119 -1
  61. package/dist/lib/platform.js +99 -1
  62. package/dist/lib/render-card.js +112 -1
  63. package/dist/lib/repl-asker.js +67 -1
  64. package/dist/lib/sample-runner.js +227 -1
  65. package/dist/lib/sandbox.d.ts +25 -1
  66. package/dist/lib/sandbox.js +144 -1
  67. package/dist/lib/shell-split.js +69 -1
  68. package/dist/lib/sim-cooldown.js +75 -1
  69. package/dist/lib/theme.js +119 -1
  70. package/dist/lib/token-format.js +74 -1
  71. package/dist/lib/tool-man.js +418 -1
  72. package/dist/lib/toolset-hash.js +48 -1
  73. package/dist/lib/translation.js +80 -1
  74. package/dist/lib/translations-fetcher.js +95 -1
  75. package/dist/lib/ui.js +99 -1
  76. package/dist/lib/update-check.js +114 -1
  77. package/dist/lib/version.js +24 -1
  78. package/dist/postinstall.js +48 -1
  79. package/dist/repl.js +2391 -1
  80. package/dist/types/index.js +63 -1
  81. package/package.json +1 -1
@@ -1 +1,151 @@
1
- import{execSync as o}from"node:child_process";import chalk from"chalk";import{printInfo as e,printSuccess as n}from"../lib/ui.js";import{logCommand as s}from"../lib/logger.js";import{getActiveCwd as r}from"../lib/exam-sandbox.js";export function resolveShellLaunch(o){return o.dockerAvailable?{mode:"docker",command:"docker"}:{mode:"host",command:"win32"===o.platform?o.comspecEnv||"cmd.exe":o.shellEnv||"/bin/bash"}}function l(){const s=r(),{command:l}=resolveShellLaunch({dockerAvailable:!1,platform:process.platform,shellEnv:process.env.SHELL,comspecEnv:process.env.COMSPEC});e("Docker not found — opening a host shell."),console.log(chalk.gray(" All 110 CTF tools run on your host. If any are missing, run ")+chalk.cyan("env setup")+chalk.gray(".")),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();try{o(l,{stdio:"inherit",cwd:s}),console.log(),n("Shell session ended.")}catch{console.log(),e("Shell session ended.")}}export function registerShellCommand(r){r.command("shell").description("Open an interactive CTF shell (Docker sandbox if available, else host)").action(async()=>{if(s("shell"),!function(){try{return o("docker info",{stdio:"ignore"}),!0}catch{return!1}}())return void l();const r="icoa/sandbox:2026",t=`icoa-sandbox-${Date.now()}`;try{o(`docker image inspect ${r}`,{stdio:"ignore"})}catch{e(`Sandbox image not found. Pulling ${r}...`);try{o(`docker pull ${r}`,{stdio:"inherit"})}catch{return e(`Could not pull ${r} — using your host shell instead.`),void l()}}n("Launching sandbox..."),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();try{o(`docker run --rm -it --name ${t} --network=host ${r} /bin/bash`,{stdio:"inherit"}),console.log(),n("Sandbox session ended.")}catch{console.log(),e("Sandbox session ended.")}})}
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ import { printInfo, printSuccess } from '../lib/ui.js';
4
+ import { logCommand } from '../lib/logger.js';
5
+ import { getActiveCwd } from '../lib/exam-sandbox.js';
6
+ import { isDockerRunning } from '../lib/docker-probe.js';
7
+ /**
8
+ * Decide which shell to launch. The Docker sandbox is the PREFERRED path (extra
9
+ * isolation), but it is OPTIONAL: the 110 CTF tools are installed on the HOST by
10
+ * `env setup`, and the `env` doctor explicitly marks docker "optional — not
11
+ * needed". So when Docker is absent we must NOT dead-end the user with an
12
+ * "install Docker" wall — we drop them into a host shell where every tool they
13
+ * need already lives. Pure + testable so the no-docker → host fallback is locked
14
+ * by a regression test.
15
+ */
16
+ export function resolveShellLaunch(opts) {
17
+ if (opts.dockerAvailable) {
18
+ return { mode: 'docker', command: 'docker' };
19
+ }
20
+ const command = opts.platform === 'win32' ? opts.comspecEnv || 'cmd.exe' : opts.shellEnv || '/bin/bash';
21
+ return { mode: 'host', command };
22
+ }
23
+ // The image ladder is shared with the REPL's persistent-container path — one
24
+ // source of truth in lib/sandbox.ts; re-exported here for the unit tests.
25
+ export { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
26
+ import { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
27
+ /**
28
+ * docker-run args as an ARRAY (spawnSync — no shell, so a cwd with spaces
29
+ * survives untouched). The two mounts are the aienv × docker interlock:
30
+ * - cwd → /work: challenge files flow both ways instead of being invisible
31
+ * from inside the container (and work done in /work outlives --rm).
32
+ * - icoa-aienv named volume → /root/.icoa: docker's copy-up seeds it from the
33
+ * image-baked venv on first use, then in-container pip installs survive
34
+ * --rm across sessions. Reset anytime: `docker volume rm icoa-aienv`.
35
+ */
36
+ export function buildSandboxRunArgs(opts) {
37
+ return [
38
+ 'run',
39
+ '--rm',
40
+ '-it',
41
+ '--name',
42
+ opts.containerName,
43
+ '--network=host',
44
+ '-v',
45
+ `${opts.cwd}:/work`,
46
+ '-w',
47
+ '/work',
48
+ '-v',
49
+ 'icoa-aienv:/root/.icoa',
50
+ opts.image,
51
+ '/bin/bash',
52
+ ];
53
+ }
54
+ // Probe the daemon socket, not the `docker` CLI — checking must never wake a
55
+ // stopped Docker Desktop (the sandbox is optional; host shell is the fallback).
56
+ // See lib/docker-probe.ts.
57
+ function isDockerAvailable() {
58
+ return isDockerRunning();
59
+ }
60
+ function openHostShell() {
61
+ const cwd = getActiveCwd();
62
+ const { command } = resolveShellLaunch({
63
+ dockerAvailable: false,
64
+ platform: process.platform,
65
+ shellEnv: process.env.SHELL,
66
+ comspecEnv: process.env.COMSPEC,
67
+ });
68
+ printInfo('Docker not found — opening a host shell.');
69
+ console.log(chalk.gray(' All 110 CTF tools run on your host. If any are missing, run ') +
70
+ chalk.cyan('env setup') +
71
+ chalk.gray('.'));
72
+ console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
73
+ console.log();
74
+ try {
75
+ execSync(command, { stdio: 'inherit', cwd });
76
+ console.log();
77
+ printSuccess('Shell session ended.');
78
+ }
79
+ catch {
80
+ // A non-zero exit (incl. the user typing `exit`) lands here — not an error.
81
+ console.log();
82
+ printInfo('Shell session ended.');
83
+ }
84
+ }
85
+ export function registerShellCommand(program) {
86
+ program
87
+ .command('shell')
88
+ .description('Open an interactive CTF shell (Docker sandbox if available, else host)')
89
+ .action(async () => {
90
+ logCommand('shell');
91
+ // Docker is the preferred (isolated) path but is OPTIONAL — fall back to
92
+ // the host shell instead of telling the user to install Docker.
93
+ if (!(await isDockerAvailable())) {
94
+ openHostShell();
95
+ return;
96
+ }
97
+ const hasLocal = (image) => {
98
+ try {
99
+ execSync(`docker image inspect ${image}`, { stdio: 'ignore' });
100
+ return true;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ };
106
+ const pull = (image) => {
107
+ printInfo(`Pulling ${image}...`);
108
+ try {
109
+ execSync(`docker pull ${image}`, { stdio: 'inherit' });
110
+ return true;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ };
116
+ const image = resolveSandboxImage(hasLocal, pull);
117
+ if (image === null) {
118
+ // No image local or pullable — don't dead-end, the host has the tools.
119
+ printInfo('Could not pull a sandbox image — using your host shell instead.');
120
+ openHostShell();
121
+ return;
122
+ }
123
+ const cwd = getActiveCwd();
124
+ printSuccess('Launching sandbox...');
125
+ console.log(chalk.gray(' Your folder is mounted at ') +
126
+ chalk.cyan('/work') +
127
+ chalk.gray(' — files there persist on your host.'));
128
+ if (image === SANDBOX_IMAGES[0]) {
129
+ console.log(chalk.gray(' ML venv: ') +
130
+ chalk.cyan('~/.icoa/aienv/bin/python') +
131
+ chalk.gray(' (sklearn / jupyter / mujoco) — its pip installs persist across sessions.'));
132
+ }
133
+ else {
134
+ console.log(chalk.gray(' This image has no ML venv — get the newer one with ') +
135
+ chalk.cyan(`docker pull ${SANDBOX_IMAGES[0]}`));
136
+ }
137
+ console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
138
+ console.log();
139
+ const containerName = `icoa-sandbox-${Date.now()}`;
140
+ const result = spawnSync('docker', buildSandboxRunArgs({ cwd, image, containerName }), {
141
+ stdio: 'inherit',
142
+ });
143
+ console.log();
144
+ if (result.error) {
145
+ printInfo('Sandbox session ended.');
146
+ }
147
+ else {
148
+ printSuccess('Sandbox session ended.');
149
+ }
150
+ });
151
+ }
@@ -1 +1,389 @@
1
- import chalk from"chalk";import{spawn as o}from"node:child_process";import{copyFileSync as e,existsSync as n,mkdirSync as r,writeFileSync as a}from"node:fs";import{homedir as t}from"node:os";import{dirname as l,join as s,resolve as i}from"node:path";import{createInterface as c}from"node:readline";import{fileURLToPath as d}from"node:url";import{fetchWithCountdown as g}from"../lib/countdown.js";const m=s(t(),".icoa","cache","sim");import{getMainRl as p}from"../lib/main-rl.js";import{COOLDOWN_SECONDS as u,lastSimAt as y,markArmAt as f,markBundledAt as b,markSimAt as h}from"../lib/sim-cooldown.js";const w={baseline:{action:null,label:"Baseline — robot doing its job",hint:"Approach → grasp → lift → place → return. This is what normal looks like.",frames:90,narrative:!0},release:{action:{xyz:[.1,0,.3],rotation:[0,0,0],gripper_close:.05},label:'Prompt Injection — "release the gripper"',hint:"Gripper opens (0.05 < 0.10 win threshold). This is Paper D Q41 success."},patch:{action:{xyz:[-.3,.2,.5],rotation:[.5,0,.3],gripper_close:.95},label:"Adversarial Patch — sticker redirects the arm",hint:"Same instruction, but arm reaches LEFT instead of RIGHT. Paper D Q42 success."},confused:{action:{xyz:[.001,0,.001],rotation:[0,0,0],gripper_close:.5},label:'Modality Confusion — "pick up the purple object"',hint:"No purple object exists. VLA freezes near origin. Paper D Q43 success."},color:{action:{xyz:[.4,-.3,.6],rotation:[-.4,.2,-.1],gripper_close:.2},label:"Cross-Modal Attack — image + text manipulated together",hint:"Action drifts hard on multiple axes. Paper D Q49-class attack (future).",hidden:!0},ensemble:{action:null,label:"Ensemble — 9 robots dancing on one beat",hint:"Pre-rendered group dance. Instant. No cooldown.",bundled:"ensemble-v2.mp4"},arm:{action:null,label:"Arm mode — interactive 6-DOF companion-arm sandbox",hint:"Type 6 joint amplitudes (or a preset). Each render takes ~15s, 60s cooldown.",bundled:void 0}},v=Object.fromEntries(Object.entries(w).filter(([,o])=>!o.hidden)),j={1:{vec:[1,1,1,1,1.4,1],label:"full demo — every joint at full amplitude"},2:{vec:[1,0,0,0,0,0],label:"base spin only (joint 1 swings)"},3:{vec:[0,.8,.8,0,0,0],label:"shoulder + elbow (lift cycle)"},4:{vec:[0,0,0,.8,1.4,1],label:"wrist drill — j4 roll + j5 bend + j6 twist"},5:{vec:[-.7,-.8,.6,.7,-1,.5],label:"mixed asymmetric (chaotic)"}};function T(){return"darwin"===process.platform?"open":"win32"===process.platform?"start":"xdg-open"}async function x(e){const n=(Date.now()-y())/1e3;if(n<u){const o=Math.ceil(u-n);return console.log(chalk.yellow(` Please wait ${o}s before the next render.`)),console.log(chalk.gray(" (One render per minute keeps things fast for everyone.)")),!1}const r=`${process.env.ICOA_SERVER_URL?.replace(/\/+$/,"")||"https://practice.icoa2026.au"}/api/ai/vla/41/sim`;e.quietHeader||(console.log(),console.log(chalk.bold.cyan(" ICOA · MuJoCo Sim")),console.log(chalk.gray(" ")+chalk.white(e.label)),console.log());try{const n=fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e.action,frames:e.frames,narrative:e.narrative,...e.panda2_pose?{panda2_pose:e.panda2_pose}:{}}),signal:AbortSignal.timeout(6e4)}),t=await g(n);if(!t.ok)return console.log(chalk.yellow(` Server returned HTTP ${t.status}.`)),!1;const l=await t.json();if(!l.success||!l.data?.mp4_b64)return console.log(chalk.yellow(" Renderer unavailable.")),!1;const i=process.env.TMPDIR||"/tmp",c=s(i,`icoa-sim-${e.scenarioName}-${Date.now()}.mp4`);a(c,Buffer.from(l.data.mp4_b64,"base64")),h(Date.now()),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Rendered.")),l.data.description&&console.log(chalk.gray(" Action: ")+chalk.white(l.data.description)),console.log(),console.log(chalk.gray(" ")+e.hint),console.log();const d=T();return d?(o(d,[c],{stdio:"ignore",detached:!0}).unref(),console.log(chalk.gray(" Opening in your default video player..."))):console.log(chalk.gray(" Video at: ")+chalk.white(c)),console.log(),!0}catch(o){const e=o instanceof Error?o.message:String(o);return console.log(chalk.yellow(` Sim error: ${e}`)),!1}}async function A(t){const g=t.toLowerCase();if("arm"===g)return void await async function(){console.log(),console.log(chalk.bold.cyan(" ICOA · Companion Arm Sandbox")),console.log(chalk.gray(" Drive the printable 6-DOF arm directly. Server renders, video opens.")),console.log(),console.log(chalk.gray(" Joint amplitudes (radians, suggested range −1.0 to +1.0):")),console.log(chalk.gray(" j1=base spin j2=shoulder j3=elbow")),console.log(chalk.gray(" j4=forearm roll j5=wrist pitch j6=wrist roll")),console.log(),console.log(chalk.gray(" Type 6 numbers separated by spaces, or pick a preset:"));for(const[o,e]of Object.entries(j)){const n=e.vec.map(o=>o.toFixed(1).padStart(4)).join(" ");console.log(chalk.gray(" ")+chalk.bold.cyan(o)+chalk.gray(" → ["+n+"] "+e.label))}console.log(),console.log(chalk.gray(" Each render: ~15s countdown + 60s cooldown between renders.")),console.log(chalk.gray(" Type ")+chalk.bold.cyan("q")+chalk.gray(" or ")+chalk.bold.cyan("exit")+chalk.gray(" to leave arm mode.")),console.log();const o=p(),e=null!==o,n=e?o.listeners("line").slice():[];e&&o.removeAllListeners("line");const r=e?o:c({input:process.stdin,output:process.stdout,terminal:!0});return new Promise(o=>{let a=!1;const t=()=>{process.stdout.write(chalk.bold.cyan(" icoa arm> "))},l=async s=>{const i=s.trim();if(a)return;if("q"===i||"exit"===i||"quit"===i)return console.log(chalk.gray(" Leaving arm mode.")),console.log(),r.removeListener("line",l),(()=>{if(e){r.removeAllListeners("line");for(const o of n)r.on("line",o);r.prompt()}else r.close()})(),void o();if("help"===i||"?"===i)return console.log(chalk.gray(" 6 numbers (e.g. 0.5 0.7 0.5 0.3 0.6 0.3) or preset 1/2/3/4.")),void t();const c=(o=>{const e=o.trim();if(!e)return null;if(j[e])return j[e].vec;const n=e.split(/[\s,]+/).map(o=>Number(o));return 6!==n.length||n.some(o=>!Number.isFinite(o))?null:n.map(o=>Math.max(-1.5,Math.min(1.5,o)))})(i);if(!c)return console.log(chalk.yellow(" Need 6 numbers or a preset (1-4). Try ?")),void t();a=!0;const d=c.map(o=>o.toFixed(2)).join(" ");console.log(chalk.gray(" Sending pose [")+chalk.white(d)+chalk.gray("]")),await x({scenarioName:"arm",label:"Companion arm — your pose",hint:"Rendered from your 6-vector. Try another, or "+chalk.bold.cyan("q")+chalk.gray(" to leave."),action:null,frames:90,narrative:!0,panda2_pose:c,quietHeader:!0})&&f(Date.now()),a=!1,t()};r.on("line",l),t()})}();const u=w[g];if(!u){console.log(),console.log(chalk.yellow(` Unknown scenario: "${t}"`)),console.log(chalk.gray(" Available:"));for(const[o,e]of Object.entries(v))console.log(chalk.gray(" ")+chalk.bold.cyan(o.padEnd(10))+chalk.gray(e.label));return void console.log()}if(u.bundled)return void await async function(t,c){const g=await async function(o){const e=function(o){const e=l(d(import.meta.url));return[i(e,"..","..","assets","sim",o),i(e,"..","..","..","assets","sim",o)].find(o=>n(o))||null}(o);if(e)return e;const t=s(m,o);if(n(t))return t;console.log(),console.log(chalk.white("\n ██╗ ██████╗ ██████╗ █████╗\n ██║██╔════╝██╔═══██╗██╔══██╗\n ██║██║ ██║ ██║███████║\n ██║██║ ██║ ██║██╔══██║\n ██║╚██████╗╚██████╔╝██║ ██║\n ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝\n")),console.log(chalk.gray(" First-time setup — fetching the sim asset (~700 KB) ...")),console.log();try{r(m,{recursive:!0});const e=`https://icoa2026.au/assets/sim/${o}`,n=await fetch(e);if(!n.ok)return console.log(chalk.yellow(` Download failed (HTTP ${n.status}).`)),console.log(chalk.gray(" Try ")+chalk.bold.cyan("icoa sim baseline")+chalk.gray(" (server-rendered alternative).")),console.log(),null;const l=Buffer.from(await n.arrayBuffer());return a(t,l),console.log(chalk.green(" ✓ Cached — future runs are instant.")),console.log(),t}catch(o){return console.log(chalk.yellow(` Network error: ${o instanceof Error?o.message:String(o)}`)),console.log(chalk.gray(" Try ")+chalk.bold.cyan("icoa sim baseline")+chalk.gray(" instead.")),console.log(),null}}(c.bundled);if(!g)return;const p=process.env.TMPDIR||"/tmp",u=s(p,`icoa-sim-${t}-${Date.now()}.mp4`);e(g,u),b(Date.now()),console.log(),console.log(chalk.bold.cyan(" ICOA · MuJoCo Sim")),console.log(chalk.gray(" ")+chalk.white(c.label)),console.log(),console.log(chalk.bold.green(" ✓ ")+chalk.gray("Ready (pre-rendered, instant playback).")),console.log(),console.log(chalk.gray(" ")+c.hint),console.log();const y=T();y?(o(y,[u],{stdio:"ignore",detached:!0}).unref(),console.log(chalk.gray(" Opening in your default video player..."))):console.log(chalk.gray(" Video at: ")+chalk.white(u)),console.log();const f="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Want to drive the companion arm yourself? ")+chalk.bold.cyan(f+"sim arm")),console.log()}(g,u);await x({scenarioName:g,label:u.label,hint:u.hint,action:u.action,frames:u.frames,narrative:u.narrative,panda2_pose:u.panda2_pose});const y="1"===process.env.ICOA_INSIDE_REPL?"":"icoa ";console.log(chalk.gray(" Try other scenarios:"));for(const o of Object.keys(v))o!==g&&console.log(chalk.gray(" "+y+"sim ")+chalk.cyan(o));console.log()}export function registerSimCommand(o){o.command("sim [scenario]").description("MuJoCo VLA — sim (group dance) · sim arm (interactive companion arm) · sim baseline/release/patch/confused").action(async o=>{await A(o||"ensemble")})}export{w as SCENARIOS};
1
+ import chalk from 'chalk';
2
+ import { spawn } from 'node:child_process';
3
+ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { fetchWithCountdown } from '../lib/countdown.js';
9
+ const SIM_ASSET_BASE_URL = 'https://icoa2026.au/assets/sim';
10
+ const SIM_CACHE_DIR = join(homedir(), '.icoa', 'cache', 'sim');
11
+ const SIM_DOWNLOAD_BANNER = `
12
+ ██╗ ██████╗ ██████╗ █████╗
13
+ ██║██╔════╝██╔═══██╗██╔══██╗
14
+ ██║██║ ██║ ██║███████║
15
+ ██║██║ ██║ ██║██╔══██║
16
+ ██║╚██████╗╚██████╔╝██║ ██║
17
+ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
18
+ `;
19
+ import { getMainRl } from '../lib/main-rl.js';
20
+ import { COOLDOWN_SECONDS, lastSimAt, markArmAt, markBundledAt, markSimAt } from '../lib/sim-cooldown.js';
21
+ const SCENARIOS = {
22
+ baseline: {
23
+ action: null, // Backend executes the VLA on "Pick up the red cup"
24
+ label: 'Baseline — robot doing its job',
25
+ hint: 'Approach → grasp → lift → place → return. This is what normal looks like.',
26
+ frames: 90, // 3 seconds
27
+ narrative: true, // multi-waypoint cinematic sweep
28
+ },
29
+ release: {
30
+ action: { xyz: [0.1, 0.0, 0.3], rotation: [0, 0, 0], gripper_close: 0.05 },
31
+ label: 'Prompt Injection — "release the gripper"',
32
+ hint: 'Gripper opens (0.05 < 0.10 win threshold). This is Paper D Q41 success.',
33
+ },
34
+ patch: {
35
+ action: { xyz: [-0.3, 0.2, 0.5], rotation: [0.5, 0, 0.3], gripper_close: 0.95 },
36
+ label: 'Adversarial Patch — sticker redirects the arm',
37
+ hint: 'Same instruction, but arm reaches LEFT instead of RIGHT. Paper D Q42 success.',
38
+ },
39
+ confused: {
40
+ action: { xyz: [0.001, 0.0, 0.001], rotation: [0, 0, 0], gripper_close: 0.5 },
41
+ label: 'Modality Confusion — "pick up the purple object"',
42
+ hint: 'No purple object exists. VLA freezes near origin. Paper D Q43 success.',
43
+ },
44
+ // ─── Hidden Easter egg — for marketing demos / promo videos only ──────
45
+ color: {
46
+ action: { xyz: [0.4, -0.3, 0.6], rotation: [-0.4, 0.2, -0.1], gripper_close: 0.2 },
47
+ label: 'Cross-Modal Attack — image + text manipulated together',
48
+ hint: 'Action drifts hard on multiple axes. Paper D Q49-class attack (future).',
49
+ hidden: true,
50
+ },
51
+ // ─── Pre-baked instant-playback bundle ───────────────────────────────
52
+ ensemble: {
53
+ action: null,
54
+ label: 'Ensemble — 9 robots dancing on one beat',
55
+ hint: 'Pre-rendered group dance. Instant. No cooldown.',
56
+ bundled: 'ensemble-v2.mp4',
57
+ },
58
+ // `arm` is handled by runArmInteractive, NOT entered via SCENARIOS.
59
+ // Kept as a marker so help listings show it. The runtime path branches
60
+ // on the literal name before consulting this table.
61
+ arm: {
62
+ action: null,
63
+ label: 'Arm mode — interactive 6-DOF companion-arm sandbox',
64
+ hint: 'Type 6 joint amplitudes (or a preset). Each render takes ~15s, 60s cooldown.',
65
+ bundled: undefined, // sentinel — interactive branch picks this up
66
+ },
67
+ };
68
+ const VISIBLE_SCENARIOS = Object.fromEntries(Object.entries(SCENARIOS).filter(([, v]) => !v.hidden));
69
+ // Presets for the interactive arm mode — give beginners a 1-tap option
70
+ // while teaching them the 6-vector convention.
71
+ const ARM_PRESETS = {
72
+ '1': { vec: [1.0, 1.0, 1.0, 1.0, 1.4, 1.0], label: 'full demo — every joint at full amplitude' },
73
+ '2': { vec: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0], label: 'base spin only (joint 1 swings)' },
74
+ '3': { vec: [0.0, 0.8, 0.8, 0.0, 0.0, 0.0], label: 'shoulder + elbow (lift cycle)' },
75
+ '4': { vec: [0.0, 0.0, 0.0, 0.8, 1.4, 1.0], label: 'wrist drill — j4 roll + j5 bend + j6 twist' },
76
+ '5': { vec: [-0.7, -0.8, 0.6, 0.7, -1.0, 0.5], label: 'mixed asymmetric (chaotic)' },
77
+ };
78
+ function systemOpener() {
79
+ if (process.platform === 'darwin')
80
+ return 'open';
81
+ if (process.platform === 'win32')
82
+ return 'start';
83
+ return 'xdg-open';
84
+ }
85
+ // Resolve a bundled asset under assets/sim/ in the local dev tree.
86
+ // Returns null when running from an npm-installed package (the asset is
87
+ // no longer bundled — see ensureBundledAsset for the lazy-download fallback).
88
+ function bundledAssetPath(filename) {
89
+ const here = dirname(fileURLToPath(import.meta.url));
90
+ const candidates = [
91
+ resolve(here, '..', '..', 'assets', 'sim', filename),
92
+ resolve(here, '..', '..', '..', 'assets', 'sim', filename),
93
+ ];
94
+ return candidates.find((p) => existsSync(p)) || null;
95
+ }
96
+ // Return a usable filesystem path to a sim asset. Priority:
97
+ // 1. dev tree (assets/sim/<filename>) — for `npm link` / source-tree usage
98
+ // 2. user cache (~/.icoa/cache/sim/<filename>) — populated by prior runs
99
+ // 3. download from icoa2026.au (first-run path) — cached on success
100
+ async function ensureBundledAsset(filename) {
101
+ const devPath = bundledAssetPath(filename);
102
+ if (devPath)
103
+ return devPath;
104
+ const cachePath = join(SIM_CACHE_DIR, filename);
105
+ if (existsSync(cachePath))
106
+ return cachePath;
107
+ console.log();
108
+ console.log(chalk.white(SIM_DOWNLOAD_BANNER));
109
+ console.log(chalk.gray(' First-time setup — fetching the sim asset (~700 KB) ...'));
110
+ console.log();
111
+ try {
112
+ mkdirSync(SIM_CACHE_DIR, { recursive: true });
113
+ const url = `${SIM_ASSET_BASE_URL}/${filename}`;
114
+ const res = await fetch(url);
115
+ if (!res.ok) {
116
+ console.log(chalk.yellow(` Download failed (HTTP ${res.status}).`));
117
+ console.log(chalk.gray(' Try ') + chalk.bold.cyan('icoa sim baseline') + chalk.gray(' (server-rendered alternative).'));
118
+ console.log();
119
+ return null;
120
+ }
121
+ const buf = Buffer.from(await res.arrayBuffer());
122
+ writeFileSync(cachePath, buf);
123
+ console.log(chalk.green(' ✓ Cached — future runs are instant.'));
124
+ console.log();
125
+ return cachePath;
126
+ }
127
+ catch (e) {
128
+ console.log(chalk.yellow(` Network error: ${e instanceof Error ? e.message : String(e)}`));
129
+ console.log(chalk.gray(' Try ') + chalk.bold.cyan('icoa sim baseline') + chalk.gray(' instead.'));
130
+ console.log();
131
+ return null;
132
+ }
133
+ }
134
+ async function playBundled(scenarioName, scenario) {
135
+ const src = await ensureBundledAsset(scenario.bundled);
136
+ if (!src)
137
+ return;
138
+ const tmpDir = process.env.TMPDIR || '/tmp';
139
+ const dst = join(tmpDir, `icoa-sim-${scenarioName}-${Date.now()}.mp4`);
140
+ copyFileSync(src, dst);
141
+ markBundledAt(Date.now());
142
+ console.log();
143
+ console.log(chalk.bold.cyan(' ICOA · MuJoCo Sim'));
144
+ console.log(chalk.gray(' ') + chalk.white(scenario.label));
145
+ console.log();
146
+ console.log(chalk.bold.green(' ✓ ') + chalk.gray('Ready (pre-rendered, instant playback).'));
147
+ console.log();
148
+ console.log(chalk.gray(' ') + scenario.hint);
149
+ console.log();
150
+ const opener = systemOpener();
151
+ if (opener) {
152
+ spawn(opener, [dst], { stdio: 'ignore', detached: true }).unref();
153
+ console.log(chalk.gray(' Opening in your default video player...'));
154
+ }
155
+ else {
156
+ console.log(chalk.gray(' Video at: ') + chalk.white(dst));
157
+ }
158
+ console.log();
159
+ const prefix = process.env.ICOA_INSIDE_REPL === '1' ? '' : 'icoa ';
160
+ console.log(chalk.gray(' Want to drive the companion arm yourself? ') + chalk.bold.cyan(prefix + 'sim arm'));
161
+ console.log();
162
+ }
163
+ // ─── Server-render helper used by both classic scenarios and arm mode ──
164
+ async function renderOnServer(opts) {
165
+ // Cooldown gate
166
+ const now = Date.now();
167
+ const sinceLast = (now - lastSimAt()) / 1000;
168
+ if (sinceLast < COOLDOWN_SECONDS) {
169
+ const wait = Math.ceil(COOLDOWN_SECONDS - sinceLast);
170
+ console.log(chalk.yellow(` Please wait ${wait}s before the next render.`));
171
+ console.log(chalk.gray(' (One render per minute keeps things fast for everyone.)'));
172
+ return false;
173
+ }
174
+ const baseUrl = process.env.ICOA_SERVER_URL?.replace(/\/+$/, '') || 'https://practice.icoa2026.au';
175
+ const url = `${baseUrl}/api/ai/vla/41/sim`;
176
+ if (!opts.quietHeader) {
177
+ console.log();
178
+ console.log(chalk.bold.cyan(' ICOA · MuJoCo Sim'));
179
+ console.log(chalk.gray(' ') + chalk.white(opts.label));
180
+ console.log();
181
+ }
182
+ try {
183
+ const fetchPromise = fetch(url, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({
187
+ action: opts.action,
188
+ frames: opts.frames,
189
+ narrative: opts.narrative,
190
+ ...(opts.panda2_pose ? { panda2_pose: opts.panda2_pose } : {}),
191
+ }),
192
+ signal: AbortSignal.timeout(60_000),
193
+ });
194
+ const res = await fetchWithCountdown(fetchPromise);
195
+ if (!res.ok) {
196
+ console.log(chalk.yellow(` Server returned HTTP ${res.status}.`));
197
+ return false;
198
+ }
199
+ const json = await res.json();
200
+ if (!json.success || !json.data?.mp4_b64) {
201
+ console.log(chalk.yellow(' Renderer unavailable.'));
202
+ return false;
203
+ }
204
+ const tmpDir = process.env.TMPDIR || '/tmp';
205
+ const path = join(tmpDir, `icoa-sim-${opts.scenarioName}-${Date.now()}.mp4`);
206
+ writeFileSync(path, Buffer.from(json.data.mp4_b64, 'base64'));
207
+ markSimAt(Date.now());
208
+ console.log();
209
+ console.log(chalk.bold.green(' ✓ ') + chalk.gray('Rendered.'));
210
+ if (json.data.description) {
211
+ console.log(chalk.gray(' Action: ') + chalk.white(json.data.description));
212
+ }
213
+ console.log();
214
+ console.log(chalk.gray(' ') + opts.hint);
215
+ console.log();
216
+ const opener = systemOpener();
217
+ if (opener) {
218
+ spawn(opener, [path], { stdio: 'ignore', detached: true }).unref();
219
+ console.log(chalk.gray(' Opening in your default video player...'));
220
+ }
221
+ else {
222
+ console.log(chalk.gray(' Video at: ') + chalk.white(path));
223
+ }
224
+ console.log();
225
+ return true;
226
+ }
227
+ catch (err) {
228
+ const msg = err instanceof Error ? err.message : String(err);
229
+ console.log(chalk.yellow(` Sim error: ${msg}`));
230
+ return false;
231
+ }
232
+ }
233
+ // ─── Interactive companion-arm sandbox ─────────────────────────────────
234
+ // Listener-swap pattern (BUG-008 in CLAUDE.md): if main REPL is alive,
235
+ // reuse its readline.Interface and stash its 'line' listeners.
236
+ async function runArmInteractive() {
237
+ console.log();
238
+ console.log(chalk.bold.cyan(' ICOA · Companion Arm Sandbox'));
239
+ console.log(chalk.gray(' Drive the printable 6-DOF arm directly. Server renders, video opens.'));
240
+ console.log();
241
+ console.log(chalk.gray(' Joint amplitudes (radians, suggested range −1.0 to +1.0):'));
242
+ console.log(chalk.gray(' j1=base spin j2=shoulder j3=elbow'));
243
+ console.log(chalk.gray(' j4=forearm roll j5=wrist pitch j6=wrist roll'));
244
+ console.log();
245
+ console.log(chalk.gray(' Type 6 numbers separated by spaces, or pick a preset:'));
246
+ for (const [k, p] of Object.entries(ARM_PRESETS)) {
247
+ const vec = p.vec.map((v) => v.toFixed(1).padStart(4)).join(' ');
248
+ console.log(chalk.gray(' ') + chalk.bold.cyan(k) + chalk.gray(' → [' + vec + '] ' + p.label));
249
+ }
250
+ console.log();
251
+ console.log(chalk.gray(' Each render: ~15s countdown + 60s cooldown between renders.'));
252
+ console.log(chalk.gray(' Type ') +
253
+ chalk.bold.cyan('q') +
254
+ chalk.gray(' or ') +
255
+ chalk.bold.cyan('exit') +
256
+ chalk.gray(' to leave arm mode.'));
257
+ console.log();
258
+ const mainRl = getMainRl();
259
+ const usingMainRl = mainRl !== null;
260
+ const savedMainListeners = usingMainRl ? mainRl.listeners('line').slice() : [];
261
+ if (usingMainRl)
262
+ mainRl.removeAllListeners('line');
263
+ const rl = usingMainRl ? mainRl : createInterface({ input: process.stdin, output: process.stdout, terminal: true });
264
+ const restoreRl = () => {
265
+ if (usingMainRl) {
266
+ rl.removeAllListeners('line');
267
+ for (const l of savedMainListeners)
268
+ rl.on('line', l);
269
+ rl.prompt();
270
+ }
271
+ else {
272
+ rl.close();
273
+ }
274
+ };
275
+ const parsePose = (line) => {
276
+ const trimmed = line.trim();
277
+ if (!trimmed)
278
+ return null;
279
+ if (ARM_PRESETS[trimmed])
280
+ return ARM_PRESETS[trimmed].vec;
281
+ const parts = trimmed.split(/[\s,]+/).map((s) => Number(s));
282
+ if (parts.length !== 6 || parts.some((n) => !Number.isFinite(n)))
283
+ return null;
284
+ // Clamp each joint to ±1.5 to keep within URDF limits server-side
285
+ return parts.map((n) => Math.max(-1.5, Math.min(1.5, n)));
286
+ };
287
+ return new Promise((resolveOuter) => {
288
+ let busy = false;
289
+ const ask = () => {
290
+ process.stdout.write(chalk.bold.cyan(' icoa arm> '));
291
+ };
292
+ const onLine = async (rawLine) => {
293
+ const line = rawLine.trim();
294
+ if (busy)
295
+ return; // ignore input while a render is in flight
296
+ if (line === 'q' || line === 'exit' || line === 'quit') {
297
+ console.log(chalk.gray(' Leaving arm mode.'));
298
+ console.log();
299
+ rl.removeListener('line', onLine);
300
+ restoreRl();
301
+ resolveOuter();
302
+ return;
303
+ }
304
+ if (line === 'help' || line === '?') {
305
+ console.log(chalk.gray(' 6 numbers (e.g. 0.5 0.7 0.5 0.3 0.6 0.3) or preset 1/2/3/4.'));
306
+ ask();
307
+ return;
308
+ }
309
+ const pose = parsePose(line);
310
+ if (!pose) {
311
+ console.log(chalk.yellow(' Need 6 numbers or a preset (1-4). Try ?'));
312
+ ask();
313
+ return;
314
+ }
315
+ busy = true;
316
+ const vecStr = pose.map((v) => v.toFixed(2)).join(' ');
317
+ console.log(chalk.gray(' Sending pose [') + chalk.white(vecStr) + chalk.gray(']'));
318
+ const ok = await renderOnServer({
319
+ scenarioName: 'arm',
320
+ label: 'Companion arm — your pose',
321
+ hint: 'Rendered from your 6-vector. Try another, or ' + chalk.bold.cyan('q') + chalk.gray(' to leave.'),
322
+ action: null,
323
+ frames: 90,
324
+ narrative: true,
325
+ panda2_pose: pose,
326
+ quietHeader: true,
327
+ });
328
+ if (ok)
329
+ markArmAt(Date.now());
330
+ busy = false;
331
+ ask();
332
+ };
333
+ rl.on('line', onLine);
334
+ ask();
335
+ });
336
+ }
337
+ async function runSim(scenarioName) {
338
+ const name = scenarioName.toLowerCase();
339
+ // Interactive arm sandbox — handled before SCENARIOS lookup
340
+ if (name === 'arm') {
341
+ await runArmInteractive();
342
+ return;
343
+ }
344
+ const scenario = SCENARIOS[name];
345
+ if (!scenario) {
346
+ console.log();
347
+ console.log(chalk.yellow(` Unknown scenario: "${scenarioName}"`));
348
+ console.log(chalk.gray(' Available:'));
349
+ for (const [k, v] of Object.entries(VISIBLE_SCENARIOS)) {
350
+ console.log(chalk.gray(' ') + chalk.bold.cyan(k.padEnd(10)) + chalk.gray(v.label));
351
+ }
352
+ console.log();
353
+ return;
354
+ }
355
+ // Pre-baked path: instant playback, no server, no cooldown
356
+ if (scenario.bundled) {
357
+ await playBundled(name, scenario);
358
+ return;
359
+ }
360
+ // Server-rendered scenario
361
+ await renderOnServer({
362
+ scenarioName: name,
363
+ label: scenario.label,
364
+ hint: scenario.hint,
365
+ action: scenario.action,
366
+ frames: scenario.frames,
367
+ narrative: scenario.narrative,
368
+ panda2_pose: scenario.panda2_pose,
369
+ });
370
+ // Tail suggestions for classic scenarios
371
+ const prefix = process.env.ICOA_INSIDE_REPL === '1' ? '' : 'icoa ';
372
+ console.log(chalk.gray(' Try other scenarios:'));
373
+ for (const k of Object.keys(VISIBLE_SCENARIOS)) {
374
+ if (k !== name) {
375
+ console.log(chalk.gray(' ' + prefix + 'sim ') + chalk.cyan(k));
376
+ }
377
+ }
378
+ console.log();
379
+ }
380
+ export function registerSimCommand(program) {
381
+ program
382
+ .command('sim [scenario]')
383
+ .description('MuJoCo VLA — sim (group dance) · sim arm (interactive companion arm) · sim baseline/release/patch/confused')
384
+ .action(async (scenarioName) => {
385
+ // Bare `sim` defaults to the instant group dance.
386
+ await runSim(scenarioName || 'ensemble');
387
+ });
388
+ }
389
+ export { SCENARIOS };