icoa-cli 2.19.357 → 2.19.359

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 (75) hide show
  1. package/dist/commands/ai4ctf.js +1 -1
  2. package/dist/commands/ctf.js +1 -787
  3. package/dist/commands/ctf4ai-demo.js +1 -1
  4. package/dist/commands/ctf4vla.js +1 -1
  5. package/dist/commands/demo2.js +1 -1502
  6. package/dist/commands/doctor.d.ts +2 -0
  7. package/dist/commands/doctor.js +1 -0
  8. package/dist/commands/exam.js +1 -1
  9. package/dist/commands/files.js +1 -59
  10. package/dist/commands/lang.js +1 -202
  11. package/dist/commands/log.js +1 -171
  12. package/dist/commands/shell.js +1 -151
  13. package/dist/commands/sim.js +1 -389
  14. package/dist/index.js +1 -355
  15. package/dist/lib/access.js +1 -184
  16. package/dist/lib/aienv.js +1 -205
  17. package/dist/lib/arena-submit.js +1 -21
  18. package/dist/lib/banner.js +1 -31
  19. package/dist/lib/budget.js +1 -6
  20. package/dist/lib/challenge-dir.js +1 -16
  21. package/dist/lib/colors.js +1 -17
  22. package/dist/lib/comms.js +1 -212
  23. package/dist/lib/config.js +1 -93
  24. package/dist/lib/countdown.js +1 -43
  25. package/dist/lib/country-lang.js +1 -39
  26. package/dist/lib/ctfd-client.js +1 -417
  27. package/dist/lib/demo-exam.js +1 -478
  28. package/dist/lib/demo-flags.js +1 -27
  29. package/dist/lib/demo-stats.js +1 -62
  30. package/dist/lib/demo2-progress.js +1 -102
  31. package/dist/lib/docker-probe.js +1 -118
  32. package/dist/lib/editor-spawn.js +1 -53
  33. package/dist/lib/exam-client.js +1 -54
  34. package/dist/lib/exam-sandbox.js +1 -201
  35. package/dist/lib/exam-setup.js +1 -36
  36. package/dist/lib/exam-state.js +1 -273
  37. package/dist/lib/gemini.js +1 -247
  38. package/dist/lib/i18n.js +1 -302
  39. package/dist/lib/integrity-snapshot.js +1 -88
  40. package/dist/lib/interactive-spawn.js +1 -55
  41. package/dist/lib/ipynb-input.js +1 -65
  42. package/dist/lib/kernel-protocol.js +1 -88
  43. package/dist/lib/kernel.js +2 -146
  44. package/dist/lib/learn-curricula.js +1 -309
  45. package/dist/lib/learn-i18n.js +1 -184
  46. package/dist/lib/learn-input.js +1 -101
  47. package/dist/lib/learn-render.js +1 -863
  48. package/dist/lib/learn-state.js +1 -103
  49. package/dist/lib/log-sync.js +1 -155
  50. package/dist/lib/logger.js +1 -49
  51. package/dist/lib/main-rl.js +1 -7
  52. package/dist/lib/menu-nav.js +1 -105
  53. package/dist/lib/notebook-doc.js +1 -137
  54. package/dist/lib/open-file.js +1 -55
  55. package/dist/lib/paper-upgrade.js +1 -119
  56. package/dist/lib/platform.js +1 -99
  57. package/dist/lib/render-card.js +1 -112
  58. package/dist/lib/repl-asker.js +1 -67
  59. package/dist/lib/sample-runner.js +1 -227
  60. package/dist/lib/sandbox.js +1 -144
  61. package/dist/lib/shell-split.js +1 -69
  62. package/dist/lib/sim-cooldown.js +1 -75
  63. package/dist/lib/theme.js +1 -119
  64. package/dist/lib/token-format.js +1 -74
  65. package/dist/lib/tool-man.js +1 -418
  66. package/dist/lib/toolset-hash.js +1 -48
  67. package/dist/lib/translation.js +1 -80
  68. package/dist/lib/translations-fetcher.js +1 -95
  69. package/dist/lib/ui.js +1 -99
  70. package/dist/lib/update-check.js +1 -114
  71. package/dist/lib/version.js +1 -24
  72. package/dist/postinstall.js +1 -48
  73. package/dist/repl.js +1 -2391
  74. package/dist/types/index.js +1 -63
  75. package/package.json +1 -1
@@ -1,144 +1 @@
1
- import { execSync, spawn } from 'node:child_process';
2
- import chalk from 'chalk';
3
- import { isDockerRunning } from './docker-probe.js';
4
- /**
5
- * Sandbox images, newest first. `2026.1` bakes the aienv ML venv (core+data+
6
- * physics groups, mujoco==3.9.0 judge-parity pin) at ~/.icoa/aienv — the same
7
- * path `aienv setup` provisions on a host — so arena / self_check / notebook
8
- * flows work inside the container without teams self-installing an ML stack
9
- * that drifts from the server judge (the 2026-07 finals feedback). The legacy
10
- * `2026` tag stays as a fallback so the sandbox keeps working for users who
11
- * only have (or can only reach) the old image.
12
- */
13
- export const SANDBOX_IMAGES = ['icoa/sandbox:2026.1', 'icoa/sandbox:2026'];
14
- /**
15
- * Pick the sandbox image: any LOCAL image first (offline-graceful — never block
16
- * a session on a multi-GB pull when a working sandbox is already on disk), then
17
- * pull newest-first. null → caller decides the fallback (host shell / local
18
- * build). Probes are injected so the ladder stays pure + unit-tested (tests
19
- * live in test/shell-sandbox-mounts.test.js via the shell.ts re-export).
20
- */
21
- export function resolveSandboxImage(hasLocal, pull, images = SANDBOX_IMAGES) {
22
- for (const image of images)
23
- if (hasLocal(image))
24
- return image;
25
- for (const image of images)
26
- if (pull(image))
27
- return image;
28
- return null;
29
- }
30
- const CONTAINER = 'icoa-sandbox';
31
- /**
32
- * Is Docker usable right now? Probes the daemon socket directly (never the
33
- * `docker` CLI) so a stopped Docker Desktop is NOT auto-started — the sandbox
34
- * is optional and the host shell is the fallback, so merely *checking* must not
35
- * wake Docker. See lib/docker-probe.ts.
36
- */
37
- export async function isDockerAvailable() {
38
- return isDockerRunning();
39
- }
40
- function isSandboxRunning() {
41
- try {
42
- const out = execSync(`docker inspect -f '{{.State.Running}}' ${CONTAINER} 2>/dev/null`, {
43
- encoding: 'utf-8',
44
- });
45
- return out.trim() === 'true';
46
- }
47
- catch {
48
- return false;
49
- }
50
- }
51
- export async function ensureSandbox() {
52
- if (!(await isDockerAvailable())) {
53
- console.log(chalk.yellow(' Docker not found. Install Docker Desktop to use sandbox tools.'));
54
- console.log(chalk.gray(' https://www.docker.com/products/docker-desktop'));
55
- return false;
56
- }
57
- if (isSandboxRunning())
58
- return true;
59
- // Check if container exists but stopped
60
- try {
61
- execSync(`docker start ${CONTAINER}`, { stdio: 'ignore' });
62
- return true;
63
- }
64
- catch {
65
- // Container doesn't exist, create it
66
- }
67
- // Resolve an image via the ladder: local-first, then pull newest-first.
68
- const hasLocal = (image) => {
69
- try {
70
- execSync(`docker image inspect ${image}`, { stdio: 'ignore' });
71
- return true;
72
- }
73
- catch {
74
- return false;
75
- }
76
- };
77
- const pull = (image) => {
78
- console.log(chalk.gray(` Pulling ${image} (first time only)...`));
79
- try {
80
- execSync(`docker pull ${image}`, { stdio: 'inherit' });
81
- return true;
82
- }
83
- catch {
84
- return false;
85
- }
86
- };
87
- let image = resolveSandboxImage(hasLocal, pull);
88
- if (!image) {
89
- // Nothing pullable — build the derived 2026.1 locally (Dockerfile.aienv
90
- // pulls the frozen CTF base `2026` itself as its FROM). We never rebuild
91
- // the base from scratch: its 27 exact-pinned libs are integrity-frozen.
92
- console.log(chalk.gray(' Building sandbox from local Dockerfile.aienv...'));
93
- try {
94
- const dockerDir = new URL('../../docker', import.meta.url).pathname;
95
- execSync(`docker build -f ${dockerDir}/Dockerfile.aienv -t ${SANDBOX_IMAGES[0]} ${dockerDir}`, {
96
- stdio: 'inherit',
97
- });
98
- image = SANDBOX_IMAGES[0];
99
- }
100
- catch {
101
- console.log(chalk.red(' Failed to set up sandbox.'));
102
- return false;
103
- }
104
- }
105
- // Create and start container
106
- try {
107
- execSync(`docker run -d --name ${CONTAINER} ` +
108
- `-v icoa-challenges:/home/competitor/challenges ` +
109
- // Default bridge network, NOT `--network host`. Host networking shares
110
- // the contestant's network namespace, so sandboxed solve-code could reach
111
- // services bound to the host's 127.0.0.1 (internal eval/admin endpoints).
112
- // Bridge still allows outbound to remote challenge targets + internet via
113
- // NAT — only host-localhost is cut off, which no legitimate practical
114
- // needs. See B9 in docs/POST_COMPETITION_AUDIT_STANDARD.md §10.
115
- // KNOWN FOLLOW-UP (B9, deferred): the single shared `icoa-challenges`
116
- // volume lets one challenge's code read another's files. Per-task
117
- // ephemeral volumes need reworking the persistent-container model.
118
- `--network bridge ` +
119
- `${image} sleep infinity`, { stdio: 'ignore' });
120
- return true;
121
- }
122
- catch {
123
- console.log(chalk.red(' Failed to start sandbox container.'));
124
- return false;
125
- }
126
- }
127
- export function runInSandbox(command, rl) {
128
- return new Promise((resolve) => {
129
- rl.pause();
130
- const opts = {
131
- stdio: 'inherit',
132
- shell: true,
133
- };
134
- const child = spawn('docker', ['exec', '-it', CONTAINER, 'bash', '-c', command], opts);
135
- child.on('close', () => {
136
- rl.resume();
137
- resolve();
138
- });
139
- child.on('error', () => {
140
- rl.resume();
141
- resolve();
142
- });
143
- });
144
- }
1
+ import{execSync as o,spawn as e}from"node:child_process";import chalk from"chalk";import{isDockerRunning as r}from"./docker-probe.js";export const SANDBOX_IMAGES=["icoa/sandbox:2026.1","icoa/sandbox:2026"];export function resolveSandboxImage(o,e,r=SANDBOX_IMAGES){for(const e of r)if(o(e))return e;for(const o of r)if(e(o))return o;return null}const t="icoa-sandbox";export async function isDockerAvailable(){return r()}export async function ensureSandbox(){if(!await isDockerAvailable())return console.log(chalk.yellow(" Docker not found. Install Docker Desktop to use sandbox tools.")),console.log(chalk.gray(" https://www.docker.com/products/docker-desktop")),!1;if(function(){try{return"true"===o(`docker inspect -f '{{.State.Running}}' ${t} 2>/dev/null`,{encoding:"utf-8"}).trim()}catch{return!1}}())return!0;try{return o(`docker start ${t}`,{stdio:"ignore"}),!0}catch{}let e=resolveSandboxImage(e=>{try{return o(`docker image inspect ${e}`,{stdio:"ignore"}),!0}catch{return!1}},e=>{console.log(chalk.gray(` Pulling ${e} (first time only)...`));try{return o(`docker pull ${e}`,{stdio:"inherit"}),!0}catch{return!1}});if(!e){console.log(chalk.gray(" Building sandbox from local Dockerfile.aienv..."));try{const r=new URL("../../docker",import.meta.url).pathname;o(`docker build -f ${r}/Dockerfile.aienv -t ${SANDBOX_IMAGES[0]} ${r}`,{stdio:"inherit"}),e=SANDBOX_IMAGES[0]}catch{return console.log(chalk.red(" Failed to set up sandbox.")),!1}}try{return o(`docker run -d --name ${t} -v icoa-challenges:/home/competitor/challenges --network bridge ${e} sleep infinity`,{stdio:"ignore"}),!0}catch{return console.log(chalk.red(" Failed to start sandbox container.")),!1}}export function runInSandbox(o,r){return new Promise(n=>{r.pause();const c=e("docker",["exec","-it",t,"bash","-c",o],{stdio:"inherit",shell:!0});c.on("close",()=>{r.resume(),n()}),c.on("error",()=>{r.resume(),n()})})}
@@ -1,69 +1 @@
1
- /**
2
- * shellSplit — a minimal, quote-aware tokenizer for REPL commands that forward
3
- * raw args straight to a child process (currently `aienv run` / `aienv python`).
4
- *
5
- * The icoa REPL is NOT a shell: the default command split on /\s+/ turns quotes
6
- * into literal characters and tears spaced args apart, so `aienv run "my file.py"`
7
- * and `aienv python -c "print(1 + 1)"` reach python mangled. This honours
8
- * single/double quotes (and backslash escapes inside double quotes / outside
9
- * quotes) the way a real shell would tokenize them. (Direct-CLI invocation never
10
- * needs this — there the system shell does the tokenization for us.)
11
- *
12
- * Scope is deliberately small: word-splitting + quote removal + backslash
13
- * escapes. No globbing, no variable/`$()` expansion, no `~` expansion — the
14
- * child process / caller handles paths, and we explicitly do not want shell
15
- * metacharacter semantics in the REPL.
16
- */
17
- export function shellSplit(input) {
18
- const tokens = [];
19
- let cur = '';
20
- let inSingle = false;
21
- let inDouble = false;
22
- let hasTok = false; // so an explicit "" / '' still yields an empty-string arg
23
- for (let i = 0; i < input.length; i++) {
24
- const c = input[i];
25
- if (inSingle) {
26
- // Inside single quotes everything is literal until the closing quote.
27
- if (c === "'")
28
- inSingle = false;
29
- else
30
- cur += c;
31
- hasTok = true;
32
- }
33
- else if (inDouble) {
34
- if (c === '"')
35
- inDouble = false;
36
- else if (c === '\\' && (input[i + 1] === '"' || input[i + 1] === '\\'))
37
- cur += input[++i];
38
- else
39
- cur += c;
40
- hasTok = true;
41
- }
42
- else if (c === "'") {
43
- inSingle = true;
44
- hasTok = true;
45
- }
46
- else if (c === '"') {
47
- inDouble = true;
48
- hasTok = true;
49
- }
50
- else if (c === '\\' && i + 1 < input.length) {
51
- cur += input[++i];
52
- hasTok = true;
53
- }
54
- else if (/\s/.test(c)) {
55
- if (hasTok) {
56
- tokens.push(cur);
57
- cur = '';
58
- hasTok = false;
59
- }
60
- }
61
- else {
62
- cur += c;
63
- hasTok = true;
64
- }
65
- }
66
- if (hasTok)
67
- tokens.push(cur);
68
- return tokens;
69
- }
1
+ export function shellSplit(t){const e=[];let l="",n=!1,s=!1,h=!1;for(let o=0;o<t.length;o++){const p=t[o];n?("'"===p?n=!1:l+=p,h=!0):s?('"'===p?s=!1:"\\"!==p||'"'!==t[o+1]&&"\\"!==t[o+1]?l+=p:l+=t[++o],h=!0):"'"===p?(n=!0,h=!0):'"'===p?(s=!0,h=!0):"\\"===p&&o+1<t.length?(l+=t[++o],h=!0):/\s/.test(p)?h&&(e.push(l),l="",h=!1):(l+=p,h=!0)}return h&&e.push(l),e}
@@ -1,75 +1 @@
1
- /**
2
- * Shared 60s cooldown for any client-side caller of the MuJoCo sim
3
- * endpoint (`/api/ai/vla/41/sim`). Both `icoa sim <scenario>` and
4
- * `icoa demo2`'s parameter mode hit the same render pipeline; the
5
- * cooldown lives in a file so they share one budget.
6
- *
7
- * Why: render is CPU-heavy shared infrastructure. 60s/student keeps
8
- * concurrent load survivable.
9
- *
10
- * Two additional fields piggy-back on the same file so demo2 can detect
11
- * what a user has already experienced (bundled dance / interactive arm).
12
- * These do NOT gate anything — they are continuity signals only.
13
- */
14
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
- import { join } from 'node:path';
16
- import { homedir } from 'node:os';
17
- export const COOLDOWN_SECONDS = 60;
18
- const COOLDOWN_FILE = join(homedir(), '.icoa', 'sim-cooldown.json');
19
- function readFile() {
20
- try {
21
- const raw = readFileSync(COOLDOWN_FILE, 'utf-8');
22
- const j = JSON.parse(raw);
23
- return typeof j === 'object' && j !== null ? j : {};
24
- }
25
- catch {
26
- return {};
27
- }
28
- }
29
- function writeMerge(patch) {
30
- try {
31
- mkdirSync(join(homedir(), '.icoa'), { recursive: true });
32
- const merged = { ...readFile(), ...patch };
33
- writeFileSync(COOLDOWN_FILE, JSON.stringify(merged));
34
- }
35
- catch {
36
- // Non-fatal: cooldown is convenience, not security
37
- }
38
- }
39
- export function lastSimAt() {
40
- const f = readFile();
41
- return typeof f.lastCallAt === 'number' ? f.lastCallAt : 0;
42
- }
43
- export function markSimAt(t) {
44
- writeMerge({ lastCallAt: t });
45
- }
46
- /** Seconds remaining (0 if clear). */
47
- export function cooldownRemaining() {
48
- const since = (Date.now() - lastSimAt()) / 1000;
49
- return Math.max(0, Math.ceil(COOLDOWN_SECONDS - since));
50
- }
51
- // ─── Continuity signals (not used for gating) ────────────────────────────
52
- export function lastBundledAt() {
53
- const f = readFile();
54
- return typeof f.lastBundledAt === 'number' ? f.lastBundledAt : 0;
55
- }
56
- export function markBundledAt(t) {
57
- writeMerge({ lastBundledAt: t });
58
- }
59
- export function lastArmAt() {
60
- const f = readFile();
61
- return typeof f.lastArmAt === 'number' ? f.lastArmAt : 0;
62
- }
63
- export function markArmAt(t) {
64
- writeMerge({ lastArmAt: t });
65
- }
66
- /**
67
- * Has the user seen any sim render — bundled, server, or arm — at least once?
68
- * Used by demo2 + boot screen to switch from "first time" to "continue" copy.
69
- */
70
- export function hasAnySimHistory() {
71
- const f = readFile();
72
- return ((typeof f.lastBundledAt === 'number' && f.lastBundledAt > 0) ||
73
- (typeof f.lastCallAt === 'number' && f.lastCallAt > 0) ||
74
- (typeof f.lastArmAt === 'number' && f.lastArmAt > 0));
75
- }
1
+ import{mkdirSync as t,readFileSync as n,writeFileSync as o}from"node:fs";import{join as r}from"node:path";import{homedir as e}from"node:os";export const COOLDOWN_SECONDS=60;const l=r(e(),".icoa","sim-cooldown.json");function a(){try{const t=n(l,"utf-8"),o=JSON.parse(t);return"object"==typeof o&&null!==o?o:{}}catch{return{}}}function s(n){try{t(r(e(),".icoa"),{recursive:!0});const s={...a(),...n};o(l,JSON.stringify(s))}catch{}}export function lastSimAt(){const t=a();return"number"==typeof t.lastCallAt?t.lastCallAt:0}export function markSimAt(t){s({lastCallAt:t})}export function cooldownRemaining(){const t=(Date.now()-lastSimAt())/1e3;return Math.max(0,Math.ceil(60-t))}export function lastBundledAt(){const t=a();return"number"==typeof t.lastBundledAt?t.lastBundledAt:0}export function markBundledAt(t){s({lastBundledAt:t})}export function lastArmAt(){const t=a();return"number"==typeof t.lastArmAt?t.lastArmAt:0}export function markArmAt(t){s({lastArmAt:t})}export function hasAnySimHistory(){const t=a();return"number"==typeof t.lastBundledAt&&t.lastBundledAt>0||"number"==typeof t.lastCallAt&&t.lastCallAt>0||"number"==typeof t.lastArmAt&&t.lastArmAt>0}
package/dist/lib/theme.js CHANGED
@@ -1,119 +1 @@
1
- // Unified Darcula terminal theme works across macOS Terminal.app, iTerm2,
2
- // GNOME Terminal, Konsole, Windows Terminal (cmd/PowerShell/WSL).
3
- //
4
- // Three mechanisms are combined so every modern terminal gets the best it can:
5
- //
6
- // 1. OSC 10/11/12 sets the terminal's *default* fg/bg/cursor colors.
7
- // Honored by iTerm2, GNOME Terminal, Konsole, Windows Terminal → lossless
8
- // background, no scrollback or resize artifacts. Ignored by Terminal.app.
9
- //
10
- // 2. SGR 38;2/48;2 + \x1b[2J paints with 24-bit truecolor Darcula on every
11
- // terminal that supports truecolor. This is the default path.
12
- //
13
- // 3. SGR 38;5/48;5 + \x1b[2J paints with 256-color approximation on macOS
14
- // Terminal.app. Terminal.app does NOT support truecolor SGR and mis-parses
15
- // `\x1b[48;2;43;43;43m` as a sequence of 16-color codes — the trailing
16
- // `43` becomes ANSI "bg yellow", which is why v2.19.23/24 rendered with a
17
- // yellow background there. Color 235 ≈ #262626 (dark gray, ~#2B2B2B) and
18
- // color 250 ≈ #BCBCBC (light gray, ~#A9B7C6) are close enough.
19
- //
20
- // Legacy cmd.exe (pre-Win10 1809) can't run Node 22 anyway, so no separate
21
- // fallback path is needed.
22
- const OSC_INIT_DARK = '\x1b]10;#A9B7C6\x07' + // default fg
23
- '\x1b]11;#2B2B2B\x07' + // default bg
24
- '\x1b]12;#A9B7C6\x07'; // cursor color
25
- // High-contrast: pure black bg + pure white fg. For students with low vision
26
- // or screens where Darcula's subtle grays wash out (e.g., projectors, cheap
27
- // LCDs under fluorescent light). Still works with existing chalk colors —
28
- // cyan/green/yellow/red all show up clearly against pure black.
29
- const OSC_INIT_HC = '\x1b]10;#FFFFFF\x07' + '\x1b]11;#000000\x07' + '\x1b]12;#FFFFFF\x07';
30
- const OSC_RESET = '\x1b]110\x07' + // reset default fg
31
- '\x1b]111\x07' + // reset default bg
32
- '\x1b]112\x07'; // reset cursor color
33
- const SGR_INIT_TRUECOLOR_DARK = '\x1b[38;2;169;183;198m' + // fg #A9B7C6
34
- '\x1b[48;2;43;43;43m' + // bg #2B2B2B
35
- '\x1b[2J' +
36
- '\x1b[H';
37
- const SGR_INIT_256_DARK = '\x1b[38;5;250m' + // fg ≈ #BCBCBC
38
- '\x1b[48;5;235m' + // bg ≈ #262626
39
- '\x1b[2J' +
40
- '\x1b[H';
41
- const SGR_INIT_TRUECOLOR_HC = '\x1b[38;2;255;255;255m' + // fg pure white
42
- '\x1b[48;2;0;0;0m' + // bg pure black
43
- '\x1b[2J' +
44
- '\x1b[H';
45
- const SGR_INIT_256_HC = '\x1b[38;5;231m' + // fg white (231 = pure white in 256)
46
- '\x1b[48;5;16m' + // bg black (16 = pure black in 256)
47
- '\x1b[2J' +
48
- '\x1b[H';
49
- const SGR_RESET = '\x1b[0m\x1b[2J\x1b[H';
50
- function supportsAnsi() {
51
- if (!process.stdout.isTTY)
52
- return false;
53
- const depth = process.stdout.getColorDepth?.();
54
- if (typeof depth === 'number')
55
- return depth >= 8;
56
- return true;
57
- }
58
- // When icoa-cli runs inside the ICOA Terminal (Tauri + xterm.js), the host is
59
- // already pre-themed to the exact Darcula palette we'd be setting. Every OSC
60
- // and SGR we'd emit is a no-op in terms of color, but the \x1b[2J inside our
61
- // init/reset sequences would clear the grid visibly. Skip the paint entirely
62
- // in that environment so the banner simply appears in the shell cursor
63
- // position and scrollback is preserved on exit.
64
- function isIcoaTerminal() {
65
- return process.env.ICOA_TERMINAL === '1';
66
- }
67
- // macOS Terminal.app does not implement SGR truecolor (\x1b[38;2;… / \x1b[48;2;…)
68
- // and mis-parses those sequences as 16-color codes, producing e.g. a yellow bg.
69
- // Detect it and fall back to 256-color SGR which Terminal.app handles correctly.
70
- function isAppleTerminal() {
71
- return process.env.TERM_PROGRAM === 'Apple_Terminal';
72
- }
73
- let armed = false;
74
- export function setTerminalTheme(variant = 'dark') {
75
- if (!supportsAnsi())
76
- return;
77
- if (isIcoaTerminal())
78
- return; // host is already Darcula; nothing to do
79
- const osc = variant === 'high-contrast' ? OSC_INIT_HC : OSC_INIT_DARK;
80
- const sgr = isAppleTerminal()
81
- ? variant === 'high-contrast'
82
- ? SGR_INIT_256_HC
83
- : SGR_INIT_256_DARK
84
- : variant === 'high-contrast'
85
- ? SGR_INIT_TRUECOLOR_HC
86
- : SGR_INIT_TRUECOLOR_DARK;
87
- process.stdout.write(osc + sgr);
88
- if (!armed) {
89
- armed = true;
90
- // Belt-and-braces cleanup on every exit path. Without these, Ctrl+C leaves
91
- // the user's shell stuck with our SGR state.
92
- const cleanup = () => {
93
- try {
94
- process.stdout.write(OSC_RESET + SGR_RESET);
95
- }
96
- catch { }
97
- };
98
- process.on('exit', cleanup);
99
- process.on('SIGINT', () => {
100
- cleanup();
101
- process.exit(130);
102
- });
103
- process.on('SIGTERM', () => {
104
- cleanup();
105
- process.exit(143);
106
- });
107
- process.on('SIGHUP', () => {
108
- cleanup();
109
- process.exit(129);
110
- });
111
- }
112
- }
113
- export function resetTerminalTheme() {
114
- if (!supportsAnsi())
115
- return;
116
- if (isIcoaTerminal())
117
- return; // nothing to undo
118
- process.stdout.write(OSC_RESET + SGR_RESET);
119
- }
1
+ const e="]110]111]112",t="";function s(){if(!process.stdout.isTTY)return!1;const e=process.stdout.getColorDepth?.();return"number"!=typeof e||e>=8}function r(){return"1"===process.env.ICOA_TERMINAL}let o=!1;export function setTerminalTheme(n="dark"){if(!s())return;if(r())return;const c="high-contrast"===n?"]10;#FFFFFF]11;#000000]12;#FFFFFF":"]10;#A9B7C6]11;#2B2B2B]12;#A9B7C6",i="Apple_Terminal"===process.env.TERM_PROGRAM?"high-contrast"===n?"":"":"high-contrast"===n?"":"";if(process.stdout.write(c+i),!o){o=!0;const s=()=>{try{process.stdout.write(e+t)}catch{}};process.on("exit",s),process.on("SIGINT",()=>{s(),process.exit(130)}),process.on("SIGTERM",()=>{s(),process.exit(143)}),process.on("SIGHUP",()=>{s(),process.exit(129)})}}export function resetTerminalTheme(){s()&&(r()||process.stdout.write(e+t))}
@@ -1,74 +1 @@
1
- /**
2
- * Client mirror of panda/token_alphabet.py — the confusable-free token rule.
3
- *
4
- * Tokens are <2-char prefix> + <7 random Crockford chars> + <1 checksum char>.
5
- * Crockford Base32 excludes I, L, O, U (look like 1, 1, 0, V) so a token copied
6
- * off a PDF or dictated aloud can't be mistranscribed.
7
- *
8
- * Two helpers used at token entry:
9
- * - normalizeTokenBody(): fix look-alikes a human typed (O→0, I/L→1, U→V) in
10
- * the BODY only (prefix is a fixed track/country code, may contain I/O).
11
- * - validTokenChecksum(): verify the trailing Crockford checksum.
12
- *
13
- * IMPORTANT backward-compat note: tokens issued before 2026-06-07 used the full
14
- * 36-char alphabet and have NO valid checksum (and ~61% of learn tokens contain
15
- * a real I/L/O/U in the body). So on the LEARN path normalization must be a
16
- * FALLBACK (try literal first) and checksum must NEVER hard-block — the server
17
- * stays authoritative (BUG-001). Exam tokens have always been Crockford+checksum
18
- * so both helpers are safe to apply directly there.
19
- */
20
- export const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
21
- const CROCKFORD_SET = new Set(CROCKFORD);
22
- const CROCKFORD_INDEX = {};
23
- for (let i = 0; i < CROCKFORD.length; i++)
24
- CROCKFORD_INDEX[CROCKFORD[i]] = i;
25
- export const PREFIX_LEN = 2;
26
- export const BODY_RANDOM_LEN = 7;
27
- export const TOKEN_LEN = PREFIX_LEN + BODY_RANDOM_LEN + 1; // 10
28
- // Look-alikes a human types → their Crockford canonical char.
29
- const CONFUSABLE_MAP = { O: '0', I: '1', L: '1', U: 'V' };
30
- /** Replace confusable letters in a string (uppercased first). */
31
- export function normalizeConfusables(s) {
32
- let out = '';
33
- for (const ch of s.toUpperCase())
34
- out += CONFUSABLE_MAP[ch] ?? ch;
35
- return out;
36
- }
37
- /**
38
- * Normalize a token's BODY only — the 2-char prefix is a fixed track/country
39
- * code (e.g. EI / IO legitimately contain I/O) and must NOT be transformed.
40
- * Returns the trimmed, upper-cased token with body confusables canonicalized.
41
- */
42
- export function normalizeTokenBody(token) {
43
- const t = token.trim().toUpperCase();
44
- if (t.length <= PREFIX_LEN)
45
- return t;
46
- return t.slice(0, PREFIX_LEN) + normalizeConfusables(t.slice(PREFIX_LEN));
47
- }
48
- /** 1-char Crockford mod-32 checksum over the payload chars. */
49
- export function checksumChar(payload) {
50
- let total = 0;
51
- for (const ch of payload)
52
- total += CROCKFORD_INDEX[ch] ?? 0;
53
- return CROCKFORD[total % 32];
54
- }
55
- /** True iff `body` is 7 Crockford chars + 1 correct checksum char. */
56
- export function validBody(body) {
57
- if (body.length !== BODY_RANDOM_LEN + 1)
58
- return false;
59
- for (const ch of body)
60
- if (!CROCKFORD_SET.has(ch))
61
- return false;
62
- return checksumChar(body.slice(0, BODY_RANDOM_LEN)) === body[BODY_RANDOM_LEN];
63
- }
64
- /**
65
- * Whole-token checksum check for a 10-char `<prefix><body>` token. Returns
66
- * false for old (pre-checksum) tokens — only meaningful as a *soft* signal on
67
- * the learn path; authoritative only for exam tokens.
68
- */
69
- export function validTokenChecksum(token) {
70
- const t = token.trim().toUpperCase();
71
- if (t.length !== TOKEN_LEN)
72
- return false;
73
- return validBody(t.slice(PREFIX_LEN));
74
- }
1
+ export const CROCKFORD="0123456789ABCDEFGHJKMNPQRSTVWXYZ";const o=new Set(CROCKFORD),e={};for(let n=0;n<32;n++)e[CROCKFORD[n]]=n;export const PREFIX_LEN=2;export const BODY_RANDOM_LEN=7;export const TOKEN_LEN=10;const t={O:"0",I:"1",L:"1",U:"V"};export function normalizeConfusables(o){let e="";for(const n of o.toUpperCase())e+=t[n]??n;return e}export function normalizeTokenBody(o){const e=o.trim().toUpperCase();return e.length<=2?e:e.slice(0,2)+normalizeConfusables(e.slice(2))}export function checksumChar(o){let t=0;for(const n of o)t+=e[n]??0;return CROCKFORD[t%32]}export function validBody(e){if(8!==e.length)return!1;for(const t of e)if(!o.has(t))return!1;return checksumChar(e.slice(0,7))===e[7]}export function validTokenChecksum(o){const e=o.trim().toUpperCase();return 10===e.length&&validBody(e.slice(2))}