icoa-cli 2.19.349 → 2.19.350
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.
- package/dist/commands/ai4ctf.js +1 -1
- package/dist/commands/connect.js +1 -1
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/exam.js +1 -1
- package/dist/index.js +1 -355
- package/dist/lib/access.js +1 -184
- package/dist/lib/arena-submit.js +1 -21
- package/dist/lib/budget.js +1 -6
- package/dist/lib/challenge-dir.js +1 -16
- package/dist/lib/comms.js +1 -212
- package/dist/lib/config.js +1 -93
- package/dist/lib/country-lang.js +1 -39
- package/dist/lib/demo-exam.js +1 -478
- package/dist/lib/demo-flags.js +1 -27
- package/dist/lib/demo-stats.js +1 -62
- package/dist/lib/demo2-progress.js +1 -102
- package/dist/lib/exam-client.js +1 -54
- package/dist/lib/exam-state.js +1 -273
- package/dist/lib/gemini.js +1 -247
- package/dist/lib/integrity-snapshot.js +1 -88
- package/dist/lib/interactive-spawn.js +1 -55
- package/dist/lib/ipynb-input.js +1 -65
- package/dist/lib/kernel-protocol.js +1 -88
- package/dist/lib/kernel.js +2 -146
- package/dist/lib/learn-curricula.js +1 -309
- package/dist/lib/learn-i18n.js +1 -184
- package/dist/lib/log-sync.js +1 -155
- package/dist/lib/logger.js +1 -49
- package/dist/lib/open-file.js +1 -55
- package/dist/lib/paper-upgrade.js +1 -119
- package/dist/lib/render-card.js +1 -112
- package/dist/lib/repl-asker.js +1 -67
- package/dist/lib/sample-runner.js +1 -227
- package/dist/lib/shell-split.js +1 -69
- package/dist/lib/sim-cooldown.js +1 -75
- package/dist/lib/theme.js +1 -119
- package/dist/lib/token-format.js +1 -74
- package/dist/lib/toolset-hash.js +1 -48
- package/dist/lib/translations-fetcher.js +1 -95
- package/dist/lib/ui.js +1 -99
- package/dist/lib/version.js +1 -24
- package/dist/postinstall.js +1 -48
- package/dist/repl.js +1 -2251
- package/dist/types/index.js +1 -63
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -1,2251 +1 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
|
-
import { spawn, execSync as execSyncFn } from 'node:child_process';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { isConnected, getConfig, saveConfig } from './lib/config.js';
|
|
5
|
-
import { ensureLangCache } from './lib/translations-fetcher.js';
|
|
6
|
-
import { loadUiPack } from './lib/i18n.js';
|
|
7
|
-
import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume, isFirstRunOrUpgrade, markVersionSeen, } from './lib/access.js';
|
|
8
|
-
import { setReplMode } from './lib/ui.js';
|
|
9
|
-
import { returnToMainMenu, printMenuHint, menuHintInline } from './lib/menu-nav.js';
|
|
10
|
-
import { setMainRl } from './lib/main-rl.js';
|
|
11
|
-
import { shellSplit } from './lib/shell-split.js';
|
|
12
|
-
import { isChatActive, handleChatMessage } from './commands/ai4ctf.js';
|
|
13
|
-
import { renderToolMan } from './lib/tool-man.js';
|
|
14
|
-
import { isCtf4aiActive, handleCtf4aiMessage } from './commands/ctf4ai-demo.js';
|
|
15
|
-
import { isCtf4VlaActive, handleCtf4VlaMessage } from './commands/ctf4vla.js';
|
|
16
|
-
import { isDemo2Active } from './commands/demo2.js';
|
|
17
|
-
import { loadDemo2Progress } from './lib/demo2-progress.js';
|
|
18
|
-
import { hasAnySimHistory, lastArmAt, lastBundledAt, lastSimAt } from './lib/sim-cooldown.js';
|
|
19
|
-
import { getExamState, getRealExamState, getDemoState, practicalChatCommand } from './lib/exam-state.js';
|
|
20
|
-
import { resetTerminalTheme } from './lib/theme.js';
|
|
21
|
-
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
22
|
-
import { checkShellRisk, logShellAudit, getActiveCwd } from './lib/exam-sandbox.js';
|
|
23
|
-
import { logCommand } from './lib/logger.js';
|
|
24
|
-
import { startLogSync, stopLogSync } from './lib/log-sync.js';
|
|
25
|
-
import { startBroadcastPoll, stopBroadcastPoll, showBroadcast, isCompetitionJoined, isMelCompetition, isLeaderAccount, isLeaderSession, fetchPublicScoreboard, isCommsActive, } from './lib/comms.js';
|
|
26
|
-
import { enterMemoScreen, enterJuryScreen } from './commands/comms.js';
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
28
|
-
import { join } from 'node:path';
|
|
29
|
-
import { homedir } from 'node:os';
|
|
30
|
-
// Pre-import commonly-used CTF modules on interactive Python startup.
|
|
31
|
-
// Failures are silent — selectors import only what's available so a partial
|
|
32
|
-
// exam setup doesn't crash the shell. Creates ~/.icoa/python-startup.py once.
|
|
33
|
-
const PYTHON_STARTUP_CONTENT = `# ICOA exam interactive startup — auto-loaded by PYTHONSTARTUP
|
|
34
|
-
import base64, struct, hashlib, re, json, os, sys, binascii
|
|
35
|
-
try: import requests
|
|
36
|
-
except ImportError: pass
|
|
37
|
-
try: from Crypto.Cipher import AES
|
|
38
|
-
except ImportError: pass
|
|
39
|
-
try: from Crypto.Util.Padding import pad, unpad
|
|
40
|
-
except ImportError: pass
|
|
41
|
-
try: from pwn import xor, p32, u32, p64, u64
|
|
42
|
-
except ImportError: pass
|
|
43
|
-
try: import bs4
|
|
44
|
-
except ImportError: pass
|
|
45
|
-
try: import numpy as np
|
|
46
|
-
except ImportError: pass
|
|
47
|
-
try: from PIL import Image
|
|
48
|
-
except ImportError: pass
|
|
49
|
-
`;
|
|
50
|
-
function ensurePythonStartup() {
|
|
51
|
-
const icoaDir = join(homedir(), '.icoa');
|
|
52
|
-
if (!existsSync(icoaDir))
|
|
53
|
-
mkdirSync(icoaDir, { recursive: true });
|
|
54
|
-
const path = join(icoaDir, 'python-startup.py');
|
|
55
|
-
// Rewrite whenever the bundled content has changed. The old "write once"
|
|
56
|
-
// guard left a stale ~/.icoa/python-startup.py on every machine that had
|
|
57
|
-
// ever booted the REPL, so toolkit additions (e.g. PIL) never reached
|
|
58
|
-
// returning users until they manually deleted the file.
|
|
59
|
-
let current = '';
|
|
60
|
-
if (existsSync(path)) {
|
|
61
|
-
try {
|
|
62
|
-
current = readFileSync(path, 'utf-8');
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
/* unreadable — fall through to rewrite */
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (current !== PYTHON_STARTUP_CONTENT) {
|
|
69
|
-
writeFileSync(path, PYTHON_STARTUP_CONTENT);
|
|
70
|
-
}
|
|
71
|
-
return path;
|
|
72
|
-
}
|
|
73
|
-
function printPythonBanner() {
|
|
74
|
-
console.log();
|
|
75
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
76
|
-
console.log(chalk.bold.white(' Python ready — ICOA exam toolkit pre-loaded'));
|
|
77
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
78
|
-
console.log();
|
|
79
|
-
console.log(chalk.white(' Already imported: ') + chalk.gray('base64, struct, hashlib, re, json, binascii'));
|
|
80
|
-
console.log(chalk.white(' Also available: ') +
|
|
81
|
-
chalk.gray('requests, bs4, numpy, PIL.Image, AES, pad/unpad, xor, p32/u32/p64/u64'));
|
|
82
|
-
console.log();
|
|
83
|
-
console.log(chalk.yellow(' Quick examples:'));
|
|
84
|
-
console.log(chalk.gray(' base64.b64decode("aGVsbG8=") ') + chalk.gray('# decode base64'));
|
|
85
|
-
console.log(chalk.gray(' bytes.fromhex("48656c6c6f") ') + chalk.gray('# hex → bytes'));
|
|
86
|
-
console.log(chalk.gray(' "ICOA{x}".encode() ') + chalk.gray('# str → bytes'));
|
|
87
|
-
console.log(chalk.gray(' [chr(c) for c in [73,67,79,65]] ') + chalk.gray('# ASCII codes'));
|
|
88
|
-
console.log(chalk.gray(' xor(bytes.fromhex("0a2b"), b"IC") ') + chalk.gray('# pwntools XOR'));
|
|
89
|
-
console.log();
|
|
90
|
-
console.log(chalk.gray(' Exit: ') + chalk.white('exit()') + chalk.gray(' or Ctrl-D'));
|
|
91
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
92
|
-
console.log();
|
|
93
|
-
}
|
|
94
|
-
// Compute the REPL prompt based on current state.
|
|
95
|
-
// Real exam wins over demo (matches getExamState() priority). Chat modes have
|
|
96
|
-
// their own prompts and are handled separately.
|
|
97
|
-
// A demo is "active" only if it was started DURING THIS PROCESS. A leftover
|
|
98
|
-
// demo-state.json from a prior run (user ran demo, quit, relaunched into
|
|
99
|
-
// National Selection) must NOT make the prompt claim "icoa demo>" when the user
|
|
100
|
-
// isn't in a demo. The earlier 30-min wall-clock window failed exactly this
|
|
101
|
-
// case — quit→relaunch within 30 min still showed demo. Process-start time is
|
|
102
|
-
// the right boundary: a fresh launch is a new process, so any demo started
|
|
103
|
-
// before it belongs to a past session. File is NOT deleted (so `exam q 1` could
|
|
104
|
-
// still resume) — we only stop the prompt from lying.
|
|
105
|
-
const PROCESS_START_MS = Date.now();
|
|
106
|
-
function isActiveDemo() {
|
|
107
|
-
const d = getDemoState();
|
|
108
|
-
if (!d || d.session?.examId !== 'demo-free')
|
|
109
|
-
return false;
|
|
110
|
-
const started = new Date(d.session.startedAt || 0).getTime();
|
|
111
|
-
return started >= PROCESS_START_MS;
|
|
112
|
-
}
|
|
113
|
-
function computePrompt() {
|
|
114
|
-
// Hierarchical prompt rule (CLAUDE.md "Hierarchical Prompt"):
|
|
115
|
-
// level 1: "icoa> " — at the root REPL
|
|
116
|
-
// level 2: "icoa <subcmd>> " — inside any sub-mode
|
|
117
|
-
// User always sees they're inside icoa AND which mode. They type just
|
|
118
|
-
// commands (e.g. `quit`), never "icoa quit".
|
|
119
|
-
// mel competition takes priority: a user joined to the live mel box should
|
|
120
|
-
// always see `icoa 2026>`, even if a stale real-exam state lingers from before.
|
|
121
|
-
// ("2026" not "mel" — competitors don't know the internal Melbourne-box name.)
|
|
122
|
-
if (isMelCompetition())
|
|
123
|
-
return chalk.yellow('icoa 2026> ');
|
|
124
|
-
if (getRealExamState())
|
|
125
|
-
return chalk.cyan('icoa exam> ');
|
|
126
|
-
if (isActiveDemo())
|
|
127
|
-
return chalk.yellow('icoa demo> ');
|
|
128
|
-
return chalk.green('icoa> ');
|
|
129
|
-
}
|
|
130
|
-
// Competition workspace — all system commands restricted here
|
|
131
|
-
const WORKSPACE = join(homedir(), 'icoa-workspace');
|
|
132
|
-
function ensureWorkspace() {
|
|
133
|
-
if (!existsSync(WORKSPACE))
|
|
134
|
-
mkdirSync(WORKSPACE, { recursive: true });
|
|
135
|
-
return WORKSPACE;
|
|
136
|
-
}
|
|
137
|
-
// Hint list — NOT a security boundary.
|
|
138
|
-
//
|
|
139
|
-
// This list catches the common "oh I meant to do this in a real shell" cases
|
|
140
|
-
// and produces a friendly message instead of letting them through to a shell
|
|
141
|
-
// that doesn't exist (the REPL is not a shell — there's no `!` prefix here).
|
|
142
|
-
// It is trivially bypassed (`\sudo whoami`, `bash -c sudo`, etc.) and was
|
|
143
|
-
// never intended as a sandbox. The actual contestant sandbox is the docker
|
|
144
|
-
// container reached via `!cmd` in real-exam mode. Anything that runs on the
|
|
145
|
-
// contestant's host (including REPL-typed text that escapes to the user's
|
|
146
|
-
// own shell via system or backticks) is contestant-trusted code, not a
|
|
147
|
-
// security perimeter. See docs/exam-system.md for the threat model.
|
|
148
|
-
const REPL_NOT_A_SHELL_HINTS = new Set([
|
|
149
|
-
'sudo',
|
|
150
|
-
'su',
|
|
151
|
-
'doas',
|
|
152
|
-
'pkexec',
|
|
153
|
-
'brew',
|
|
154
|
-
'apt',
|
|
155
|
-
'apt-get',
|
|
156
|
-
'yum',
|
|
157
|
-
'choco',
|
|
158
|
-
'npm',
|
|
159
|
-
'npx',
|
|
160
|
-
'pip',
|
|
161
|
-
'pip3',
|
|
162
|
-
'shutdown',
|
|
163
|
-
'reboot',
|
|
164
|
-
'halt',
|
|
165
|
-
'mkfs',
|
|
166
|
-
'fdisk',
|
|
167
|
-
'dd',
|
|
168
|
-
'iptables',
|
|
169
|
-
'ufw',
|
|
170
|
-
]);
|
|
171
|
-
// Kept under the old name for any older import paths.
|
|
172
|
-
const BLOCKED_COMMANDS = REPL_NOT_A_SHELL_HINTS;
|
|
173
|
-
const INTERCEPT = '__REPL_NO_EXIT__';
|
|
174
|
-
const VERSION = '2.5.1';
|
|
175
|
-
function printSelectionMenu() {
|
|
176
|
-
console.log();
|
|
177
|
-
console.log(` ${chalk.cyan.bold('🎯 ICOA 2026 · National Selection')}`);
|
|
178
|
-
console.log();
|
|
179
|
-
console.log(' ' +
|
|
180
|
-
chalk.bold.cyan('learn ') +
|
|
181
|
-
chalk.white('ACxxxxxxxx') +
|
|
182
|
-
chalk.gray(' → ai4ctf-12 (AI as your CTF teammate)'));
|
|
183
|
-
console.log(' ' +
|
|
184
|
-
chalk.bold.cyan('learn ') +
|
|
185
|
-
chalk.white('CAxxxxxxxx') +
|
|
186
|
-
chalk.gray(' → ctf4ai-12 (red-team software AI)'));
|
|
187
|
-
console.log(' ' +
|
|
188
|
-
chalk.bold.cyan('learn ') +
|
|
189
|
-
chalk.white('EAxxxxxxxx') +
|
|
190
|
-
chalk.gray(' → ctf4eai-12 (red-team embodied AI)'));
|
|
191
|
-
console.log(' ' +
|
|
192
|
-
chalk.bold.cyan('exam ') +
|
|
193
|
-
chalk.white('<CC>xxxxxxxx') +
|
|
194
|
-
chalk.gray(' → Paper A/B/C/D/E (country exam token)'));
|
|
195
|
-
console.log(' ' + chalk.bold.cyan('demo') + chalk.gray(' → free practice'));
|
|
196
|
-
console.log(' ' + chalk.bold.cyan('lang') + chalk.white(' es') + chalk.gray(' → switch language'));
|
|
197
|
-
console.log(' ' + chalk.gray('help · menu · quit'));
|
|
198
|
-
console.log();
|
|
199
|
-
}
|
|
200
|
-
// Embodied AI Security landing — Pioneer Round (CTF4EAI) intro with
|
|
201
|
-
// adaptive path guidance based on prior sim/demo2 progress. Extracted from
|
|
202
|
-
// the legacy `mode === 'embodied'` block (Phase 2) so it can be reused by
|
|
203
|
-
// the new 🧠 Learn → EA-track picker as well as the legacy embodied mode.
|
|
204
|
-
function printEmbodiedLanding() {
|
|
205
|
-
console.log(chalk.cyan.bold(' [🤖 ctf4eai — Embodied AI Security]'));
|
|
206
|
-
console.log();
|
|
207
|
-
console.log(chalk.bold.cyan(' CTF4EAI') + chalk.gray(" — ICOA 2026's ") + chalk.bold.white('Pioneer Round'));
|
|
208
|
-
console.log(chalk.gray(' an independent parallel track alongside the scored main events: ') +
|
|
209
|
-
chalk.green('AI4CTF') +
|
|
210
|
-
chalk.gray(' (Day 1) and ') +
|
|
211
|
-
chalk.red('CTF4AI') +
|
|
212
|
-
chalk.gray(' (Day 2)'));
|
|
213
|
-
console.log();
|
|
214
|
-
console.log(chalk.white(' Embodied AI is at its ImageNet moment.'));
|
|
215
|
-
console.log(chalk.gray(' VLA models, MuJoCo physics, real robot arms.'));
|
|
216
|
-
console.log(chalk.gray(' Real models, real physics, real attack surfaces.'));
|
|
217
|
-
console.log();
|
|
218
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
219
|
-
const _seenBundled = lastBundledAt() > 0 || lastSimAt() > 0;
|
|
220
|
-
const _seenArm = lastArmAt() > 0;
|
|
221
|
-
const _anySim = hasAnySimHistory();
|
|
222
|
-
const _progress = loadDemo2Progress();
|
|
223
|
-
const _demo2Done = _progress !== null && typeof _progress.completedAt === 'number' && _progress.nextCardIndex >= _progress.totalCards;
|
|
224
|
-
const _demo2Partial = _progress !== null && !_demo2Done;
|
|
225
|
-
if (_demo2Done) {
|
|
226
|
-
console.log(chalk.bold.yellow(' Continue: ') +
|
|
227
|
-
chalk.bold.cyan('④ learn LEARNDEMO01') +
|
|
228
|
-
chalk.gray(' (demo2 ✓ · sim') +
|
|
229
|
-
(_seenArm ? chalk.gray(' ✓ · sim arm ✓)') : chalk.gray(')')));
|
|
230
|
-
}
|
|
231
|
-
else if (_demo2Partial) {
|
|
232
|
-
const at = `${_progress.nextCardIndex} / ${_progress.totalCards}`;
|
|
233
|
-
console.log(chalk.bold.yellow(' Continue: ') +
|
|
234
|
-
chalk.bold.cyan('③ demo2') +
|
|
235
|
-
chalk.gray(' (last left at card ') +
|
|
236
|
-
chalk.bold.yellow(at) +
|
|
237
|
-
chalk.gray(')'));
|
|
238
|
-
}
|
|
239
|
-
else if (_seenBundled && _seenArm) {
|
|
240
|
-
console.log(chalk.bold.yellow(' Next up: ') +
|
|
241
|
-
chalk.bold.cyan('③ demo2') +
|
|
242
|
-
chalk.gray(' → ') +
|
|
243
|
-
chalk.bold.cyan('④ learn LEARNDEMO01') +
|
|
244
|
-
chalk.gray(' (sim ✓ · sim arm ✓)'));
|
|
245
|
-
}
|
|
246
|
-
else if (_anySim) {
|
|
247
|
-
console.log(chalk.bold.yellow(' Next up: ') +
|
|
248
|
-
(_seenArm
|
|
249
|
-
? chalk.bold.cyan('① sim') + chalk.gray(' → ') + chalk.bold.cyan('③ demo2')
|
|
250
|
-
: chalk.bold.cyan('② sim arm') + chalk.gray(' → ') + chalk.bold.cyan('③ demo2')) +
|
|
251
|
-
chalk.gray(' → ') +
|
|
252
|
-
chalk.bold.cyan('④ learn LEARNDEMO01'));
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
console.log(chalk.bold.yellow(' First time? Try in order: ') +
|
|
256
|
-
chalk.bold.cyan('① sim') +
|
|
257
|
-
chalk.gray(' → ') +
|
|
258
|
-
chalk.bold.cyan('② sim arm') +
|
|
259
|
-
chalk.gray(' → ') +
|
|
260
|
-
chalk.bold.cyan('③ demo2') +
|
|
261
|
-
chalk.gray(' → ') +
|
|
262
|
-
chalk.bold.cyan('④ learn LEARNDEMO01'));
|
|
263
|
-
}
|
|
264
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
265
|
-
console.log(chalk.bold.green(' sim') +
|
|
266
|
-
chalk.gray(' ') +
|
|
267
|
-
chalk.bold.cyan('①') +
|
|
268
|
-
chalk.gray(' ') +
|
|
269
|
-
chalk.bold.yellow('★ INSTANT') +
|
|
270
|
-
chalk.gray(' — 9-robot group dance, no wait, no setup'));
|
|
271
|
-
console.log(chalk.bold.green(' sim arm') +
|
|
272
|
-
chalk.gray(' ') +
|
|
273
|
-
chalk.bold.cyan('②') +
|
|
274
|
-
chalk.gray(' interactive 6-DOF arm sandbox — type joint values, watch'));
|
|
275
|
-
console.log(chalk.bold.green(' demo2') +
|
|
276
|
-
chalk.gray(' ') +
|
|
277
|
-
chalk.bold.cyan('③') +
|
|
278
|
-
chalk.gray(' ~8 min interactive intro + live MuJoCo sim'));
|
|
279
|
-
console.log(chalk.bold.green(' learn LEARNDEMO01') +
|
|
280
|
-
chalk.gray(' ') +
|
|
281
|
-
chalk.bold.cyan('④') +
|
|
282
|
-
chalk.gray(' free 11-card intro (paste this command as-is)'));
|
|
283
|
-
console.log(chalk.bold.green(' learn ') +
|
|
284
|
-
chalk.bold.yellow('EA') +
|
|
285
|
-
chalk.gray(' + 8 chars full 96 / 360 curriculum (team-issued)'));
|
|
286
|
-
console.log(chalk.bold.green(' exam ') +
|
|
287
|
-
chalk.bold.yellow('PDxxxxxxxx') +
|
|
288
|
-
chalk.gray(' Paper D (CTF4EAI) — national selection contestants'));
|
|
289
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
290
|
-
console.log();
|
|
291
|
-
console.log(chalk.gray(' Three open tiers: ') +
|
|
292
|
-
chalk.green('free 11') +
|
|
293
|
-
chalk.gray(' · ') +
|
|
294
|
-
chalk.yellow('registered 100') +
|
|
295
|
-
chalk.gray(' · ') +
|
|
296
|
-
chalk.bold.magenta('national team 480') +
|
|
297
|
-
chalk.gray(' (PhD-ready)'));
|
|
298
|
-
console.log(chalk.gray(' Country team contact: ') + chalk.cyan('australia@icoa2026.au'));
|
|
299
|
-
console.log();
|
|
300
|
-
printMenuHint();
|
|
301
|
-
console.log();
|
|
302
|
-
}
|
|
303
|
-
export async function startRepl(program, resumeMode) {
|
|
304
|
-
// Mark this process as the interactive REPL so child commands (sim, demo2,
|
|
305
|
-
// learn etc.) can suppress the `icoa` prefix in their "try next" hints —
|
|
306
|
-
// typing `icoa sim` inside the icoa> prompt is wrong; bare `sim` is right.
|
|
307
|
-
process.env.ICOA_INSIDE_REPL = '1';
|
|
308
|
-
// mel daily-expiry — wipe a stale (previous Sydney calendar day) session
|
|
309
|
-
// BEFORE the banner/`connected` are computed, so the banner shows "join"
|
|
310
|
-
// instead of "Welcome back / Not you?". The user-facing notice is deferred
|
|
311
|
-
// to after rl setup (melDailyExpired). Rationale: one match per day
|
|
312
|
-
// (~10:00–17:00 Sydney); yesterday's sign-in must not carry into today's.
|
|
313
|
-
let melDailyExpired = false;
|
|
314
|
-
if (isMelCompetition()) {
|
|
315
|
-
const j = getConfig().joinedAt;
|
|
316
|
-
const sydneyDay = (d) => new Intl.DateTimeFormat('en-CA', {
|
|
317
|
-
timeZone: 'Australia/Sydney',
|
|
318
|
-
year: 'numeric',
|
|
319
|
-
month: '2-digit',
|
|
320
|
-
day: '2-digit',
|
|
321
|
-
}).format(d);
|
|
322
|
-
if (!j || sydneyDay(new Date(j)) !== sydneyDay(new Date())) {
|
|
323
|
-
saveConfig({
|
|
324
|
-
ctfdUrl: '',
|
|
325
|
-
token: '',
|
|
326
|
-
sessionCookie: '',
|
|
327
|
-
userId: null,
|
|
328
|
-
userName: '',
|
|
329
|
-
teamId: null,
|
|
330
|
-
country: '',
|
|
331
|
-
joinedAt: '',
|
|
332
|
-
});
|
|
333
|
-
melDailyExpired = true;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const config = getConfig();
|
|
337
|
-
const connected = isConnected();
|
|
338
|
-
const realExit = process.exit.bind(process);
|
|
339
|
-
const activated = isActivated();
|
|
340
|
-
// Lazy-load the active language's UI pack. Non-EN UI strings moved out of the
|
|
341
|
-
// npm bundle (i18n.ts → server <lang>.tar.gz/ui.json). ensureLangCache refetches
|
|
342
|
-
// the pack when it lacks ui.json — e.g. after upgrading from a version that
|
|
343
|
-
// bundled the strings — then loadUiPack applies it. Offline / missing pack →
|
|
344
|
-
// t() falls back to English. EN users: no-op, never blocks boot.
|
|
345
|
-
if (config.language && config.language !== 'en') {
|
|
346
|
-
await ensureLangCache(config.language, { silent: true });
|
|
347
|
-
loadUiPack(config.language);
|
|
348
|
-
}
|
|
349
|
-
// Auto-cleanup: clear demo state once per version upgrade.
|
|
350
|
-
// Demo is free practice with no time pressure, no scoring, no real loss
|
|
351
|
-
// for users. Old versions may have left demo state in incompatible formats
|
|
352
|
-
// (pre-v2.19.45 shared state, pre-v2.19.67 timestamp confusion, etc.).
|
|
353
|
-
// Real exam state is NEVER auto-cleared — it may contain in-progress answers.
|
|
354
|
-
if (config.demoCleanedForVersion !== VERSION) {
|
|
355
|
-
try {
|
|
356
|
-
const { existsSync, unlinkSync } = await import('node:fs');
|
|
357
|
-
const { join } = await import('node:path');
|
|
358
|
-
const { getIcoaDir } = await import('./lib/config.js');
|
|
359
|
-
const demoFile = join(getIcoaDir(), 'demo-state.json');
|
|
360
|
-
if (existsSync(demoFile))
|
|
361
|
-
unlinkSync(demoFile);
|
|
362
|
-
}
|
|
363
|
-
catch { }
|
|
364
|
-
saveConfig({ demoCleanedForVersion: VERSION });
|
|
365
|
-
}
|
|
366
|
-
// ─── Mode selection (every launch) ───
|
|
367
|
-
const { select: selectMode, confirm: confirmMode } = await import('@inquirer/prompts');
|
|
368
|
-
// Defensive: returning users on an older release may have saved 'tokenentry'
|
|
369
|
-
// (removed in Phase 1 pyramid) or 'embodied' (folded into 🧠 Learn in Phase 2).
|
|
370
|
-
// Map both to 'selection' so the inquirer default lands on a valid choice.
|
|
371
|
-
const rawSavedMode = config.mode || '';
|
|
372
|
-
const savedMode = rawSavedMode === ''
|
|
373
|
-
? // Genuine first-time user (no saved mode) → default to ⚡ Instant, the
|
|
374
|
-
// top of the viral pyramid / zero-token front door. A newcomer who just
|
|
375
|
-
// hits Enter must NOT land on 🎯 National Selection (needs an exam token
|
|
376
|
-
// they don't have). Returning users keep their last mode below.
|
|
377
|
-
'instant'
|
|
378
|
-
: ['instant', 'learn', 'selection', 'olympiad'].includes(rawSavedMode)
|
|
379
|
-
? rawSavedMode
|
|
380
|
-
: // Legacy saved modes ('tokenentry'/'embodied', removed in the pyramid
|
|
381
|
-
// redesign) map to a safe valid choice.
|
|
382
|
-
'selection';
|
|
383
|
-
// ⚡🧠🎯🏆 viral pyramid — top-down Hero's Journey:
|
|
384
|
-
// spark → train → aim → win
|
|
385
|
-
// Each tier reads in 5ms across any language/script. Emoji-led so
|
|
386
|
-
// screenshots travel without translation. Country counts intentionally
|
|
387
|
-
// suppressed (BD copy, not student-facing). See Panda 2 conversation
|
|
388
|
-
// 2026-05-28 for the K-factor rationale.
|
|
389
|
-
const modeChoices = [
|
|
390
|
-
{
|
|
391
|
-
name: ` ${chalk.bold('⚡ Try ICOA in 30 seconds')} ${chalk.gray('—')} ${chalk.gray('watch · no token needed')}`,
|
|
392
|
-
value: 'instant',
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
name: ` ${chalk.bold('🧠 Learn')} ${chalk.gray('—')} ${chalk.gray('AC / CA / EA token · skill up')}`,
|
|
396
|
-
value: 'learn',
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
name: ` ${chalk.bold('🎯 National Selection')} ${chalk.gray('—')} ${chalk.gray('country exam token · aim for finals')}`,
|
|
400
|
-
value: 'selection',
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
name: ` ${chalk.bold('🏆 International Olympiad')} ${chalk.gray('—')} ${chalk.gray('finals stage · Sydney 2026 · full sandbox')}`,
|
|
404
|
-
value: 'olympiad',
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
name: ` ${chalk.gray('ℹ️ About ICOA')} ${chalk.gray('·')} ${chalk.gray('what this is · how to join')}`,
|
|
408
|
-
value: 'about',
|
|
409
|
-
},
|
|
410
|
-
];
|
|
411
|
-
// Inquirer's `select` adds its own "(Use arrow keys)" indicator next to
|
|
412
|
-
// the prompt, so we don't repeat that here. Just remind users about the
|
|
413
|
-
// global escape: typing `menu` from anywhere returns to this screen.
|
|
414
|
-
console.log(chalk.gray(' Type ') + chalk.bold.cyan('menu') + chalk.gray(' from anywhere to return here.'));
|
|
415
|
-
console.log();
|
|
416
|
-
let mode = '';
|
|
417
|
-
while (!mode) {
|
|
418
|
-
let selected;
|
|
419
|
-
try {
|
|
420
|
-
selected = await selectMode({
|
|
421
|
-
message: 'Mode',
|
|
422
|
-
choices: modeChoices,
|
|
423
|
-
default: savedMode || 'instant',
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
catch (err) {
|
|
427
|
-
// Ctrl+C / Ctrl+D / EOF at the boot menu throws inquirer's
|
|
428
|
-
// ExitPromptError. The sub-pickers all catch this; the top-level
|
|
429
|
-
// selector used to let it bubble to the global unhandledRejection
|
|
430
|
-
// handler, which printed a raw red "Error: User force closed the
|
|
431
|
-
// prompt with 0 null" and exited 1. Exit cleanly instead.
|
|
432
|
-
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
433
|
-
console.log();
|
|
434
|
-
console.log(chalk.gray(' Goodbye.'));
|
|
435
|
-
process.exit(0);
|
|
436
|
-
}
|
|
437
|
-
throw err;
|
|
438
|
-
}
|
|
439
|
-
if (selected === 'about') {
|
|
440
|
-
console.clear();
|
|
441
|
-
console.log();
|
|
442
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════════'));
|
|
443
|
-
console.log(chalk.bold.yellow(' ICOA') + chalk.white(' — AI-Native CLI OS for Cyber & AI Security'));
|
|
444
|
-
console.log(chalk.gray(' Olympiad & Competition · K-12 to University'));
|
|
445
|
-
console.log(chalk.cyan(' ───────────────────────────────────────────────────'));
|
|
446
|
-
console.log();
|
|
447
|
-
console.log(chalk.bold.white(' What Makes ICOA Different'));
|
|
448
|
-
console.log(chalk.gray(' · AI-native AI teammate, AI adversary, AI translation'));
|
|
449
|
-
console.log(chalk.gray(' · CLI OS Complete competition environment in terminal'));
|
|
450
|
-
console.log(chalk.gray(' · 110 tools pwntools, z3, gdb, nmap, sleuthkit... pre-configured'));
|
|
451
|
-
console.log(chalk.gray(' · Global scale 15,000+ concurrent exams · 15 languages'));
|
|
452
|
-
console.log();
|
|
453
|
-
console.log(chalk.bold.white(' Competition Format (2-day standard + 1-day Pioneer)'));
|
|
454
|
-
console.log(` ${chalk.green.bold('AI4CTF')}${chalk.gray(' [Day 1] AI as teammate — speed track, dynamic decay + top-3 bonus')}`);
|
|
455
|
-
console.log(` ${chalk.red.bold('CTF4AI')}${chalk.gray(' [Day 2] AI as target — knowledge track, static value per challenge')}`);
|
|
456
|
-
console.log(` ${chalk.cyan.bold('CTF4EAI')}${chalk.gray(' [Pioneer / Practical Round] Embodied AI — VLA + world models + sim-to-real')}`);
|
|
457
|
-
console.log();
|
|
458
|
-
console.log(chalk.bold.white(' Scoring (in brief)'));
|
|
459
|
-
console.log(chalk.gray(' · Standard medals: AI4CTF + CTF4AI combined · 1/12 Gold · 2/12 Silver · 3/12 Bronze'));
|
|
460
|
-
console.log(chalk.gray(' · HMA: top 50% on either day, total below bronze cutoff'));
|
|
461
|
-
console.log(chalk.gray(' · CTF4EAI: separate 15-finalist pool, G/S/B + 12 Finalist Awards'));
|
|
462
|
-
console.log();
|
|
463
|
-
console.log(chalk.white(' Sydney, Australia') + chalk.gray(' · Jun 27 - Jul 2, 2026'));
|
|
464
|
-
console.log();
|
|
465
|
-
console.log(chalk.bold.white(' Organized by') + chalk.gray(' ASRA (Australia) · ICO Foundation Inc'));
|
|
466
|
-
console.log(chalk.bold.white(' Contact ') + chalk.cyan(' australia@icoa2026.au · accreditation@icoa2026.au'));
|
|
467
|
-
console.log(chalk.bold.white(' Website ') + chalk.cyan.underline(' https://icoa2026.au'));
|
|
468
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════════'));
|
|
469
|
-
console.log();
|
|
470
|
-
console.log(chalk.gray(' Press ') + chalk.yellow('Enter') + chalk.gray(' to return...'));
|
|
471
|
-
await new Promise((resolve) => {
|
|
472
|
-
const onKey = (_chunk) => {
|
|
473
|
-
process.stdin.removeListener('data', onKey);
|
|
474
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
475
|
-
process.stdin.setRawMode(false);
|
|
476
|
-
}
|
|
477
|
-
process.stdin.pause();
|
|
478
|
-
resolve();
|
|
479
|
-
};
|
|
480
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
481
|
-
process.stdin.setRawMode(true);
|
|
482
|
-
}
|
|
483
|
-
process.stdin.resume();
|
|
484
|
-
process.stdin.once('data', onKey);
|
|
485
|
-
});
|
|
486
|
-
console.clear();
|
|
487
|
-
// Loop back to mode selection
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
mode = selected;
|
|
491
|
-
}
|
|
492
|
-
if (mode === 'olympiad' && savedMode !== 'olympiad') {
|
|
493
|
-
console.log();
|
|
494
|
-
console.log(chalk.yellow(' This mode will download ~500MB of CTF tools and AI models.'));
|
|
495
|
-
let proceed = true;
|
|
496
|
-
try {
|
|
497
|
-
proceed = await confirmMode({ message: 'Continue?', default: true });
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
// Ctrl+C / Ctrl+D at the confirm — treat as "no" and drop to Selection
|
|
501
|
-
// rather than crashing with a raw ExitPromptError.
|
|
502
|
-
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
503
|
-
proceed = false;
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
throw err;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
if (!proceed) {
|
|
510
|
-
mode = 'selection';
|
|
511
|
-
console.log(chalk.gray(' Switched to National Selection mode.'));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (mode !== savedMode)
|
|
515
|
-
saveConfig({ mode });
|
|
516
|
-
console.log();
|
|
517
|
-
// First run or upgrade: prompt to install env (olympiad only)
|
|
518
|
-
if (mode === 'olympiad' && isFirstRunOrUpgrade(VERSION)) {
|
|
519
|
-
markVersionSeen(VERSION);
|
|
520
|
-
console.log(chalk.gray(' Checking competition environment...'));
|
|
521
|
-
// Quick check: count missing Python libs
|
|
522
|
-
const { execSync } = await import('node:child_process');
|
|
523
|
-
const checks = [
|
|
524
|
-
{ name: 'pwntools', cmd: 'python3 -c "import pwn"' },
|
|
525
|
-
{ name: 'z3-solver', cmd: 'python3 -c "import z3"' },
|
|
526
|
-
{ name: 'numpy', cmd: 'python3 -c "import numpy"' },
|
|
527
|
-
{ name: 'requests', cmd: 'python3 -c "import requests"' },
|
|
528
|
-
];
|
|
529
|
-
let missing = 0;
|
|
530
|
-
for (const c of checks) {
|
|
531
|
-
try {
|
|
532
|
-
execSync(c.cmd, { stdio: 'ignore' });
|
|
533
|
-
}
|
|
534
|
-
catch {
|
|
535
|
-
missing++;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (missing > 0) {
|
|
539
|
-
// Manual-only: NEVER auto-run setup on boot. A default-yes confirm here
|
|
540
|
-
// (re-fired on every version bump, which is frequent) meant returning
|
|
541
|
-
// olympiad users would launch `env setup` just by pressing Enter at
|
|
542
|
-
// startup — "it installs without me asking". Show a passive nudge instead;
|
|
543
|
-
// the user must explicitly type `env setup` to install.
|
|
544
|
-
console.log(chalk.yellow(` ${missing} core competition libraries missing.`));
|
|
545
|
-
console.log(chalk.gray(' Run ') + chalk.white('env setup') + chalk.gray(' when you are ready to install them.'));
|
|
546
|
-
console.log();
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
console.log(chalk.green(' All core libraries ready.'));
|
|
550
|
-
console.log();
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
// Handle resume
|
|
554
|
-
if (resumeMode) {
|
|
555
|
-
const info = recordResume();
|
|
556
|
-
if (info) {
|
|
557
|
-
const mins = Math.floor(info.awaySeconds / 60);
|
|
558
|
-
const secs = info.awaySeconds % 60;
|
|
559
|
-
console.log(chalk.yellow(` Session resumed. Away: ${mins}m ${secs}s | Total exits: ${info.exitCount}`));
|
|
560
|
-
console.log();
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
// ─── Mode-specific welcome ───
|
|
564
|
-
if (mode === 'selection') {
|
|
565
|
-
// Exam-hall atmospheric subline — shown only on first entry into Selection
|
|
566
|
-
// mode (not on every return from sub-flows, which would noise up the REPL).
|
|
567
|
-
console.log(chalk.gray(' ─── exam hall · 90 min per paper once started · no pause ───'));
|
|
568
|
-
console.log();
|
|
569
|
-
printSelectionMenu();
|
|
570
|
-
}
|
|
571
|
-
else if (mode === 'embodied') {
|
|
572
|
-
printEmbodiedLanding();
|
|
573
|
-
}
|
|
574
|
-
else if (mode === 'instant') {
|
|
575
|
-
// ⚡ Try ICOA in 30 seconds — viral front door, zero token required.
|
|
576
|
-
//
|
|
577
|
-
// Design (2026-06-04, user directive): NO nested arrow-key picker. The
|
|
578
|
-
// only ↑↓ menu in the whole CLI is the top-level mode selector; every
|
|
579
|
-
// deeper choice is a TYPED command at the `icoa>` prompt (the bash/venv
|
|
580
|
-
// precedent — you type `ls`, not arrow-select it). The old inquirer
|
|
581
|
-
// sub-picker here was both a UX inconsistency and, when it dispatched the
|
|
582
|
-
// non-self-contained `exam demo`, the source of the "进入 demo 闪退到 menu"
|
|
583
|
-
// bounce (see git history / BUG-008 class).
|
|
584
|
-
//
|
|
585
|
-
// So ⚡ now prints clear guidance and drops STRAIGHT into the REPL (mode
|
|
586
|
-
// 'selection', which already permits `demo`/`demo2`/`menu`). `demo` and
|
|
587
|
-
// `demo2` run IN-PROCESS and return to `icoa>` — no respawn, no process
|
|
588
|
-
// stacking. `menu` returns to the top selector (clean relaunch via the
|
|
589
|
-
// index.ts supervisor). Falling through (no early return) lands us in the
|
|
590
|
-
// rl loop at ~line 879.
|
|
591
|
-
console.log(chalk.cyan.bold(' [⚡ Try ICOA in 30 seconds — no token needed]'));
|
|
592
|
-
console.log();
|
|
593
|
-
console.log(chalk.gray(' Two free tastes — just type the word and press Enter:'));
|
|
594
|
-
console.log();
|
|
595
|
-
console.log(chalk.bold.green(' demo ') +
|
|
596
|
-
chalk.gray(' Paper A/B/C taste — 10 MCQs, no timer ') +
|
|
597
|
-
chalk.bold.cyan('🎯 selection-style'));
|
|
598
|
-
console.log(chalk.bold.green(' demo2') +
|
|
599
|
-
chalk.gray(' Embodied-AI mini-demo — robots & MuJoCo ') +
|
|
600
|
-
chalk.bold.yellow('🤖 hands-on'));
|
|
601
|
-
console.log();
|
|
602
|
-
console.log(chalk.gray(' Type ') +
|
|
603
|
-
chalk.bold.cyan('demo') +
|
|
604
|
-
chalk.gray(' or ') +
|
|
605
|
-
chalk.bold.cyan('demo2') +
|
|
606
|
-
chalk.gray(' to start · ') +
|
|
607
|
-
chalk.bold.cyan('menu') +
|
|
608
|
-
chalk.gray(' to go back · ') +
|
|
609
|
-
chalk.bold.cyan('quit') +
|
|
610
|
-
chalk.gray(' to exit'));
|
|
611
|
-
console.log();
|
|
612
|
-
// Land in the REPL using Selection mode's command set (demo/demo2/menu are
|
|
613
|
-
// all permitted below). saveConfig keeps the user's next default at
|
|
614
|
-
// 'selection' so a returning user doesn't loop back into ⚡ forever.
|
|
615
|
-
saveConfig({ mode: 'selection' });
|
|
616
|
-
mode = 'selection';
|
|
617
|
-
// fall through to the rl loop — NO returnToMainMenu()/return.
|
|
618
|
-
}
|
|
619
|
-
else if (mode === 'learn') {
|
|
620
|
-
// 🧠 Learn — flat token-input entry. No arrow-key track picker; the 3
|
|
621
|
-
// free 12-card demo codes are advertised inline, and any learn token
|
|
622
|
-
// (AC/CA/EA/EI/IO/IA + 8 chars, or future hidden curricula) is accepted
|
|
623
|
-
// via the same input field. The `learn` command itself routes by prefix.
|
|
624
|
-
console.log(chalk.cyan.bold(' [🧠 Learn]'));
|
|
625
|
-
console.log();
|
|
626
|
-
console.log(chalk.gray(' Three tracks · same difficulty curve.'));
|
|
627
|
-
console.log();
|
|
628
|
-
console.log(chalk.white(' Free 12-card demos — paste the full command:'));
|
|
629
|
-
console.log();
|
|
630
|
-
console.log(chalk.gray(' 🤝 ai4ctf AI as your teammate ') + chalk.bold.yellow('learn AI4CTFDEMO01'));
|
|
631
|
-
console.log(chalk.gray(' 🔓 ctf4ai AI as your target ') + chalk.bold.yellow('learn CTF4AIDEMO01'));
|
|
632
|
-
console.log(chalk.gray(' 🤖 ctf4eai Embodied AI Security ') + chalk.bold.yellow('learn LEARNDEMO01'));
|
|
633
|
-
console.log();
|
|
634
|
-
console.log(chalk.gray(' Got a cohort token? Type ') +
|
|
635
|
-
chalk.bold.yellow('learn <TOKEN>') +
|
|
636
|
-
chalk.gray(' (e.g. ') +
|
|
637
|
-
chalk.bold.yellow('learn EAABCD1234') +
|
|
638
|
-
chalk.gray(').'));
|
|
639
|
-
console.log();
|
|
640
|
-
// Single inquirer call (no loop) — keeps stdin handling simple and
|
|
641
|
-
// avoids BUG-008 dual-readline cascades when re-prompting inside the
|
|
642
|
-
// boot-time menu where the main REPL may already be listening.
|
|
643
|
-
// Fuzzy resolver still handles "3" / "ctf4eai" / "learn LEARNDEMO01" /
|
|
644
|
-
// case variants. On unrecognised input, fall back to main menu — user
|
|
645
|
-
// can re-enter Learn and try again (one extra menu hop, no chaos).
|
|
646
|
-
try {
|
|
647
|
-
const { input: inputPrompt } = await import('@inquirer/prompts');
|
|
648
|
-
const { resolveLearnInput } = await import('./lib/learn-input.js');
|
|
649
|
-
console.log(chalk.gray(' Enter a token (or just press Enter to go back).'));
|
|
650
|
-
console.log();
|
|
651
|
-
// Hierarchical-prompt rule (CLAUDE.md): every prompt starts with `icoa`.
|
|
652
|
-
// Strip inquirer's `?`/`✔` prefix so this reads exactly like the in-learn
|
|
653
|
-
// REPL prompt `icoa learn> ` (same pattern as exam.ts token entry).
|
|
654
|
-
const raw = await inputPrompt({
|
|
655
|
-
message: chalk.bold.cyan('icoa learn>'),
|
|
656
|
-
theme: { prefix: '', style: { message: (t) => t } },
|
|
657
|
-
});
|
|
658
|
-
const res = resolveLearnInput(raw);
|
|
659
|
-
if (res.kind === 'token') {
|
|
660
|
-
console.log();
|
|
661
|
-
console.log(chalk.gray(' Opening ') + chalk.bold.cyan('learn') + chalk.gray(`: ${res.token}`));
|
|
662
|
-
console.log();
|
|
663
|
-
saveConfig({ mode: 'selection' });
|
|
664
|
-
// Tell learn it is embedded so it hands control back (resolves) on exit
|
|
665
|
-
// instead of process.exit — the returnToMainMenu() below is the single
|
|
666
|
-
// respawn. Cleared before that respawn so the child doesn't inherit it.
|
|
667
|
-
process.env.ICOA_LEARN_EMBEDDED = '1';
|
|
668
|
-
try {
|
|
669
|
-
await program.parseAsync(['node', 'icoa', 'learn', res.token]);
|
|
670
|
-
}
|
|
671
|
-
finally {
|
|
672
|
-
delete process.env.ICOA_LEARN_EMBEDDED;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
else if (res.kind === 'ambiguous' || res.kind === 'unknown') {
|
|
676
|
-
console.log();
|
|
677
|
-
console.log(chalk.yellow(' ' + res.hint));
|
|
678
|
-
console.log(chalk.gray(' Returning to main menu — pick 🧠 Learn again to retry.'));
|
|
679
|
-
console.log();
|
|
680
|
-
}
|
|
681
|
-
// res.kind === 'back' → silent fall-through to main menu
|
|
682
|
-
}
|
|
683
|
-
catch {
|
|
684
|
-
// user cancelled — fall through to main menu return
|
|
685
|
-
}
|
|
686
|
-
// UX principle: 2nd-level path completes → return to MAIN menu,
|
|
687
|
-
// never cross-over into another tier's REPL.
|
|
688
|
-
saveConfig({ mode: 'selection' });
|
|
689
|
-
returnToMainMenu();
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
else if (mode === 'organizer') {
|
|
693
|
-
console.log(chalk.yellow.bold(' [National/Regional Partner]'));
|
|
694
|
-
console.log();
|
|
695
|
-
console.log(chalk.bold.white(' ██╗ ██████╗ ██████╗ █████╗'));
|
|
696
|
-
console.log(chalk.bold.white(' ██║██╔════╝██╔═══██╗██╔══██╗'));
|
|
697
|
-
console.log(chalk.bold.white(' ██║██║ ██║ ██║███████║'));
|
|
698
|
-
console.log(chalk.bold.white(' ██║██║ ██║ ██║██╔══██║'));
|
|
699
|
-
console.log(chalk.bold.white(' ██║╚██████╗╚██████╔╝██║ ██║'));
|
|
700
|
-
console.log(chalk.bold.white(' ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝'));
|
|
701
|
-
console.log();
|
|
702
|
-
console.log(chalk.yellow(' International Cyber Olympiad in AI 2026'));
|
|
703
|
-
console.log(chalk.bold.magenta(" The World's First AI-Native CLI Operating System"));
|
|
704
|
-
console.log(chalk.bold.magenta(' for Cybersecurity & AI Security Competition'));
|
|
705
|
-
console.log(chalk.bold.magenta(' and Olympiad for K-12'));
|
|
706
|
-
console.log(chalk.gray(' Sydney, Australia · Jun 27 - Jul 2, 2026'));
|
|
707
|
-
console.log();
|
|
708
|
-
console.log(chalk.white(' Vision'));
|
|
709
|
-
console.log(chalk.gray(' Building a global pipeline for youth cyber & AI'));
|
|
710
|
-
console.log(chalk.gray(' security talent through education and competition.'));
|
|
711
|
-
console.log();
|
|
712
|
-
console.log(chalk.white(' Capacity'));
|
|
713
|
-
console.log(chalk.gray(' 15,000+ concurrent online examinations'));
|
|
714
|
-
console.log(chalk.gray(' National selection, training, and education support'));
|
|
715
|
-
console.log();
|
|
716
|
-
console.log(chalk.white(' Olympic Spirit'));
|
|
717
|
-
console.log(chalk.gray(' Excellence · Friendship · Respect'));
|
|
718
|
-
console.log();
|
|
719
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
720
|
-
console.log(chalk.white(' New country accreditation & support:'));
|
|
721
|
-
console.log(chalk.cyan(' australia@icoa2026.au'));
|
|
722
|
-
console.log(chalk.cyan(' accreditation@icoa2026.au'));
|
|
723
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
724
|
-
console.log();
|
|
725
|
-
if (connected) {
|
|
726
|
-
console.log(chalk.green(` Logged in as ${config.userName}`));
|
|
727
|
-
console.log(chalk.white(' exam list') + chalk.gray(' Manage exams'));
|
|
728
|
-
console.log(chalk.white(' logout') + chalk.gray(' Disconnect'));
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
console.log(chalk.white(' join <url>') + chalk.gray(' Connect to manage exams'));
|
|
732
|
-
}
|
|
733
|
-
console.log();
|
|
734
|
-
printMenuHint();
|
|
735
|
-
console.log();
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
// Olympiad mode: full flow with activate/device checks
|
|
739
|
-
console.log(` ${chalk.cyan.bold('🏆 ICOA 2026 · International Olympiad')}`);
|
|
740
|
-
console.log();
|
|
741
|
-
if (activated && !isDeviceMatch()) {
|
|
742
|
-
console.log(chalk.red(' Token was activated on a different device.'));
|
|
743
|
-
console.log(chalk.gray(' Contact organizer for assistance.'));
|
|
744
|
-
console.log();
|
|
745
|
-
}
|
|
746
|
-
else if (connected) {
|
|
747
|
-
console.log(chalk.green.bold(` Welcome back, ${config.userName}!`));
|
|
748
|
-
console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
|
|
749
|
-
console.log(chalk.gray(' Not you? Type ') +
|
|
750
|
-
chalk.cyan('logout') +
|
|
751
|
-
chalk.gray(' to switch accounts'));
|
|
752
|
-
console.log();
|
|
753
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
754
|
-
console.log(chalk.white(' Ready to compete? Start here:'));
|
|
755
|
-
console.log();
|
|
756
|
-
console.log(chalk.bold.cyan(' challenges') + chalk.gray(' Browse challenges by category'));
|
|
757
|
-
console.log(chalk.white(' status') + chalk.gray(' Your score & timer'));
|
|
758
|
-
console.log(chalk.white(' scoreboard') + chalk.gray(' Live rankings'));
|
|
759
|
-
console.log(chalk.white(' memo') + chalk.gray(' Report an issue to organizers (one-way)'));
|
|
760
|
-
console.log();
|
|
761
|
-
console.log(chalk.gray(' Tool environment:'));
|
|
762
|
-
console.log(chalk.white(' env') + chalk.gray(' See which of the 110 CTF tools are installed'));
|
|
763
|
-
console.log(chalk.white(' env setup') + chalk.gray(' Install anything missing (~5 min, one-time)'));
|
|
764
|
-
console.log(chalk.bold.yellow(' ★ ') +
|
|
765
|
-
chalk.bold.cyan('man <tool>') +
|
|
766
|
-
chalk.gray(' How to USE a tool (steghide, nmap…) · ') +
|
|
767
|
-
chalk.cyan('man tools') +
|
|
768
|
-
chalk.gray(' = list'));
|
|
769
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
770
|
-
console.log(chalk.gray(' Tip: ') +
|
|
771
|
-
chalk.cyan('help') +
|
|
772
|
-
chalk.gray(' · ') +
|
|
773
|
-
menuHintInline() +
|
|
774
|
-
chalk.gray(' · ') +
|
|
775
|
-
chalk.cyan('Ctrl+C') +
|
|
776
|
-
chalk.gray(' pauses · ') +
|
|
777
|
-
chalk.cyan('quit') +
|
|
778
|
-
chalk.gray(' closes'));
|
|
779
|
-
console.log();
|
|
780
|
-
}
|
|
781
|
-
else if (activated) {
|
|
782
|
-
ensureWorkspace();
|
|
783
|
-
console.log(chalk.green.bold(' Welcome, competitor!'));
|
|
784
|
-
console.log(chalk.gray(` Workspace: ${WORKSPACE}`));
|
|
785
|
-
console.log();
|
|
786
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
787
|
-
console.log(chalk.white(' Get started:'));
|
|
788
|
-
console.log();
|
|
789
|
-
console.log(chalk.white(' Step 1 ') + chalk.bold.cyan('join <url>') + chalk.gray(' Connect to competition server'));
|
|
790
|
-
console.log(chalk.white(' Step 2 ') + chalk.bold.cyan('challenges') + chalk.gray(' Browse & solve challenges'));
|
|
791
|
-
console.log(chalk.white(' Step 3 ') + chalk.bold.cyan('ai4ctf') + chalk.gray(' Ask AI when stuck'));
|
|
792
|
-
console.log();
|
|
793
|
-
console.log(chalk.gray(' Before Step 1 — make sure your tools are ready:'));
|
|
794
|
-
console.log(chalk.white(' env') + chalk.gray(' See which of the 110 CTF tools are installed'));
|
|
795
|
-
console.log(chalk.white(' env setup') + chalk.gray(' Install anything missing (~5 min, one-time)'));
|
|
796
|
-
console.log(chalk.bold.yellow(' ★ ') +
|
|
797
|
-
chalk.bold.cyan('man <tool>') +
|
|
798
|
-
chalk.gray(' How to USE a tool (steghide, nmap…) · ') +
|
|
799
|
-
chalk.cyan('man tools') +
|
|
800
|
-
chalk.gray(' = list'));
|
|
801
|
-
console.log();
|
|
802
|
-
console.log(chalk.gray(' Also: ') + chalk.white('help') + chalk.gray(' all commands'));
|
|
803
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
804
|
-
console.log(chalk.gray(' Tip: ') +
|
|
805
|
-
chalk.cyan('Ctrl+C') +
|
|
806
|
-
chalk.gray(' pauses · ') +
|
|
807
|
-
menuHintInline() +
|
|
808
|
-
chalk.gray(' · ') +
|
|
809
|
-
chalk.cyan('quit') +
|
|
810
|
-
chalk.gray(' closes CLI'));
|
|
811
|
-
console.log();
|
|
812
|
-
}
|
|
813
|
-
else {
|
|
814
|
-
console.log(chalk.bold.yellow(' 🏆 You reached the finals tier.'));
|
|
815
|
-
console.log(chalk.gray(' Full sandbox · 110 CTF tools · Sydney 2026 · this is where finalists compete.'));
|
|
816
|
-
console.log();
|
|
817
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
818
|
-
console.log(chalk.white(' To begin, activate your competition token:'));
|
|
819
|
-
console.log();
|
|
820
|
-
console.log(chalk.bold.cyan(' activate <token>'));
|
|
821
|
-
console.log();
|
|
822
|
-
console.log(chalk.gray(' While waiting, explore:'));
|
|
823
|
-
console.log(chalk.white(' ref linux') + chalk.gray(' Quick reference for Linux'));
|
|
824
|
-
console.log(chalk.white(' ref web') + chalk.gray(' Quick reference for Web'));
|
|
825
|
-
console.log(chalk.white(' env') + chalk.gray(' See which of the 110 CTF tools are installed'));
|
|
826
|
-
console.log(chalk.white(' env setup') + chalk.gray(' Install anything missing (~5 min, one-time)'));
|
|
827
|
-
console.log(chalk.white(' help') + chalk.gray(' All available commands'));
|
|
828
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
829
|
-
console.log(chalk.gray(' Tip: ') +
|
|
830
|
-
chalk.cyan('Ctrl+C') +
|
|
831
|
-
chalk.gray(' pauses · ') +
|
|
832
|
-
menuHintInline() +
|
|
833
|
-
chalk.gray(' · ') +
|
|
834
|
-
chalk.cyan('quit') +
|
|
835
|
-
chalk.gray(' closes CLI'));
|
|
836
|
-
console.log();
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
program.exitOverride();
|
|
840
|
-
program.configureOutput({
|
|
841
|
-
writeErr: () => { },
|
|
842
|
-
writeOut: (str) => {
|
|
843
|
-
console.log(str);
|
|
844
|
-
},
|
|
845
|
-
});
|
|
846
|
-
const rl = createInterface({
|
|
847
|
-
input: process.stdin,
|
|
848
|
-
output: process.stdout,
|
|
849
|
-
prompt: computePrompt(),
|
|
850
|
-
terminal: true,
|
|
851
|
-
});
|
|
852
|
-
// Expose the main REPL's readline to linear-flow commands (demo2, etc.)
|
|
853
|
-
// so they can pause stdin echo before opening their own readline.
|
|
854
|
-
// Without this, two readline.Interface instances on the same TTY
|
|
855
|
-
// double every keystroke ("lang" → "llaanngg").
|
|
856
|
-
setMainRl(rl);
|
|
857
|
-
let processing = false;
|
|
858
|
-
setReplMode(true);
|
|
859
|
-
startLogSync();
|
|
860
|
-
// mel daily-expiry notice — the wipe already happened at the top of startRepl
|
|
861
|
-
// (before the banner). Just show the deferred explanation here.
|
|
862
|
-
if (melDailyExpired) {
|
|
863
|
-
console.log();
|
|
864
|
-
console.log(chalk.yellow(' ⏱ A new competition day has started (Sydney time).'));
|
|
865
|
-
console.log(chalk.gray(' For fairness, yesterday’s sign-in has been cleared. Type ') +
|
|
866
|
-
chalk.cyan('join <url>') +
|
|
867
|
-
chalk.gray(' to sign in with your account.'));
|
|
868
|
-
console.log();
|
|
869
|
-
}
|
|
870
|
-
// mel identity gate — machines are shared across a multi-day on-site event and
|
|
871
|
-
// students may forget to `logout`. isConnected()/isMelCompetition() trust only
|
|
872
|
-
// the local config, so the next person on a machine would otherwise compete as
|
|
873
|
-
// whoever left it signed in (flags scored under the wrong account). Force an
|
|
874
|
-
// explicit "is this you?" confirmation before the prompt; Enter continues,
|
|
875
|
-
// `logout` clears the session so the next student signs in fresh. Mel only.
|
|
876
|
-
// Runs BEFORE the rl.prompt wrap below so rl.question keeps its own `> ` prompt.
|
|
877
|
-
// (After daily-expiry above; if it wiped the session this is now a no-op.)
|
|
878
|
-
if (isMelCompetition()) {
|
|
879
|
-
const who = getConfig().userName || 'this account';
|
|
880
|
-
console.log();
|
|
881
|
-
console.log(chalk.yellow(' ⚠ This machine is signed in as ') + chalk.bold.white(who) + chalk.yellow('.'));
|
|
882
|
-
console.log(chalk.gray(' Press ') +
|
|
883
|
-
chalk.cyan('Enter') +
|
|
884
|
-
chalk.gray(` to continue as ${who}, or type `) +
|
|
885
|
-
chalk.cyan('logout') +
|
|
886
|
-
chalk.gray(' to switch accounts.'));
|
|
887
|
-
const answer = await new Promise((resolve) => {
|
|
888
|
-
rl.question(chalk.white(' > '), (a) => resolve(a.trim()));
|
|
889
|
-
});
|
|
890
|
-
if (answer.toLowerCase() === 'logout') {
|
|
891
|
-
saveConfig({
|
|
892
|
-
ctfdUrl: '',
|
|
893
|
-
token: '',
|
|
894
|
-
sessionCookie: '',
|
|
895
|
-
userId: null,
|
|
896
|
-
userName: '',
|
|
897
|
-
teamId: null,
|
|
898
|
-
country: '',
|
|
899
|
-
});
|
|
900
|
-
console.log();
|
|
901
|
-
console.log(chalk.green(' ✓ Logged out. ') +
|
|
902
|
-
chalk.gray('Run ') +
|
|
903
|
-
chalk.cyan('join <url>') +
|
|
904
|
-
chalk.gray(' to sign in with your own account.'));
|
|
905
|
-
}
|
|
906
|
-
console.log();
|
|
907
|
-
}
|
|
908
|
-
if (isCompetitionJoined()) {
|
|
909
|
-
startBroadcastPoll(() => rl.prompt());
|
|
910
|
-
}
|
|
911
|
-
// Wrap rl.prompt() so it always picks up current state (icoa/demo/exam mode).
|
|
912
|
-
// This keeps the prompt accurate after exam token entry, exam submit, demo finish, etc.
|
|
913
|
-
// Chat modes (ai4ctf/ctf4ai) set their own prompt via setPrompt and bypass this.
|
|
914
|
-
const _origPrompt = rl.prompt.bind(rl);
|
|
915
|
-
rl.prompt = (preserveCursor) => {
|
|
916
|
-
if (!isChatActive() &&
|
|
917
|
-
!isCtf4aiActive() &&
|
|
918
|
-
!isCtf4VlaActive() &&
|
|
919
|
-
!isDemo2Active() &&
|
|
920
|
-
!isCommsActive()) {
|
|
921
|
-
rl.setPrompt(computePrompt());
|
|
922
|
-
}
|
|
923
|
-
_origPrompt(preserveCursor);
|
|
924
|
-
};
|
|
925
|
-
rl.prompt();
|
|
926
|
-
rl.on('line', async (line) => {
|
|
927
|
-
if (processing)
|
|
928
|
-
return;
|
|
929
|
-
const input = line.trim();
|
|
930
|
-
if (!input) {
|
|
931
|
-
rl.setPrompt(isChatActive() ? chalk.magenta('icoa ai4ctf> ') : computePrompt());
|
|
932
|
-
rl.prompt();
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
// ─── Global `menu` — always return to boot screen ─────────────────────
|
|
936
|
-
// Sits ABOVE all sub-REPL routing so it works from inside ai4ctf,
|
|
937
|
-
// ctf4ai, ctf4vla, exam, demo, anywhere. Real-exam state requires
|
|
938
|
-
// `menu confirm` to prevent accidental exit; auto-save keeps progress.
|
|
939
|
-
if (input === 'menu' || input === 'menu confirm') {
|
|
940
|
-
const realExam = getRealExamState();
|
|
941
|
-
if (realExam && input !== 'menu confirm') {
|
|
942
|
-
console.log();
|
|
943
|
-
console.log(chalk.yellow(' Real exam active. Progress is auto-saved.'));
|
|
944
|
-
console.log(chalk.gray(' To return to main menu anyway, type ') + chalk.white('menu confirm'));
|
|
945
|
-
console.log();
|
|
946
|
-
rl.prompt();
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
returnToMainMenu(rl);
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
// If in AI chat mode, route to chat handler
|
|
953
|
-
if (isChatActive()) {
|
|
954
|
-
processing = true;
|
|
955
|
-
const result = await handleChatMessage(input);
|
|
956
|
-
processing = false;
|
|
957
|
-
if (result === 'exit') {
|
|
958
|
-
rl.setPrompt(computePrompt());
|
|
959
|
-
}
|
|
960
|
-
rl.prompt();
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
// If in CTF4AI challenge mode, route to ctf4ai handler
|
|
964
|
-
if (isCtf4aiActive()) {
|
|
965
|
-
processing = true;
|
|
966
|
-
const result = await handleCtf4aiMessage(input);
|
|
967
|
-
processing = false;
|
|
968
|
-
if (result === 'exit' || result === 'solved') {
|
|
969
|
-
rl.setPrompt(computePrompt());
|
|
970
|
-
}
|
|
971
|
-
rl.prompt();
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
// If in CTF4EAI mode (Paper D), route to ctf4vla handler
|
|
975
|
-
if (isCtf4VlaActive()) {
|
|
976
|
-
processing = true;
|
|
977
|
-
const result = await handleCtf4VlaMessage(input);
|
|
978
|
-
processing = false;
|
|
979
|
-
if (result === 'exit') {
|
|
980
|
-
rl.setPrompt(computePrompt());
|
|
981
|
-
}
|
|
982
|
-
rl.setPrompt(isCtf4VlaActive() ? chalk.bold.cyan('icoa ctf4eai> ') : computePrompt());
|
|
983
|
-
rl.prompt();
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
// Log ALL commands for audit trail
|
|
987
|
-
logCommand(input);
|
|
988
|
-
// `exit` — soft exit: acts like `back` and surfaces the menu. Only
|
|
989
|
-
// `quit` / `q` actually close the CLI. Rationale: in the demo flow the
|
|
990
|
-
// user bounces between main prompt and sub-flows (ai4ctf / ctf4ai /
|
|
991
|
-
// demo exam); typing `exit` at the main prompt to mean "leave the demo"
|
|
992
|
-
// is a very common mistake and should never nuke the whole session.
|
|
993
|
-
if (input === 'exit') {
|
|
994
|
-
if (getExamState()) {
|
|
995
|
-
console.log();
|
|
996
|
-
console.log(chalk.yellow(' ⚠ An exam is in progress.'));
|
|
997
|
-
console.log(chalk.white(' To return to menu without losing progress, type: ') + chalk.bold.cyan('back'));
|
|
998
|
-
console.log(chalk.white(' To fully close ICOA CLI, type: ') + chalk.bold.cyan('quit'));
|
|
999
|
-
console.log(chalk.gray(' Your progress is auto-saved either way.'));
|
|
1000
|
-
console.log();
|
|
1001
|
-
rl.prompt();
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
console.log();
|
|
1005
|
-
console.log(chalk.gray(' ') +
|
|
1006
|
-
chalk.white('exit') +
|
|
1007
|
-
chalk.gray(' returns to the main menu. To fully close ICOA CLI, type ') +
|
|
1008
|
-
chalk.bold.cyan('quit') +
|
|
1009
|
-
chalk.gray('.'));
|
|
1010
|
-
if (mode === 'selection') {
|
|
1011
|
-
printSelectionMenu();
|
|
1012
|
-
}
|
|
1013
|
-
rl.prompt();
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
// Explicit quit — `quit` / `q` close the CLI. During a live **real** exam
|
|
1017
|
-
// (token-gated, timed, graded) we require `quit confirm` as a second step
|
|
1018
|
-
// to prevent accidental loss of a 90-minute session to a single keystroke.
|
|
1019
|
-
// Demo has no time pressure and no scoring, so demo quit stays one-step.
|
|
1020
|
-
if (input === 'quit' || input === 'q' || input === 'quit confirm') {
|
|
1021
|
-
const state = getExamState();
|
|
1022
|
-
const isRealExam = state && state.session.examId !== 'demo-free';
|
|
1023
|
-
if (isRealExam && input !== 'quit confirm') {
|
|
1024
|
-
console.log();
|
|
1025
|
-
console.log(chalk.yellow(' ⚠ A real exam is in progress.'));
|
|
1026
|
-
console.log(chalk.gray(' Your answers are auto-saved on the server, but the exam timer keeps ticking'));
|
|
1027
|
-
console.log(chalk.gray(' on the server side even if you close the CLI.'));
|
|
1028
|
-
console.log();
|
|
1029
|
-
console.log(chalk.white(' To leave the CLI but keep the exam alive, type: ') + chalk.bold.cyan('back'));
|
|
1030
|
-
console.log(chalk.gray(' (recommended — you can resume with ') +
|
|
1031
|
-
chalk.cyan('exam q 1') +
|
|
1032
|
-
chalk.gray(' after relaunching icoa)'));
|
|
1033
|
-
console.log();
|
|
1034
|
-
console.log(chalk.white(' To really close ICOA CLI, type: ') + chalk.bold.cyan('quit confirm'));
|
|
1035
|
-
console.log();
|
|
1036
|
-
rl.prompt();
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
if (state && state.session.examId === 'demo-free') {
|
|
1040
|
-
console.log();
|
|
1041
|
-
console.log(chalk.gray(' Demo paused. Resume with: ') +
|
|
1042
|
-
chalk.white('demo') +
|
|
1043
|
-
chalk.gray(' (fresh) or ') +
|
|
1044
|
-
chalk.white('exam q 1') +
|
|
1045
|
-
chalk.gray(' (continue).'));
|
|
1046
|
-
}
|
|
1047
|
-
stopLogSync();
|
|
1048
|
-
stopBroadcastPoll();
|
|
1049
|
-
recordExit();
|
|
1050
|
-
console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
|
|
1051
|
-
resetTerminalTheme();
|
|
1052
|
-
realExit(0);
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
// "back" — return to main menu.
|
|
1056
|
-
// Real exam: show "Exam paused", preserve state (server timer is ticking).
|
|
1057
|
-
// Active demo: show pause message, keep state so `exam q N` can resume.
|
|
1058
|
-
// Stale demo: auto-clear and show menu (demo from a previous session the
|
|
1059
|
-
// user long abandoned — hanging state makes the menu lie).
|
|
1060
|
-
// Nothing: show selection menu.
|
|
1061
|
-
// A demo is "active" if `startedAt` is within the last 30 minutes. That window
|
|
1062
|
-
// covers an intentional "back to check something and come right back" case but
|
|
1063
|
-
// clears anything left over from a prior session.
|
|
1064
|
-
// `back` — pause exam / return to current-mode menu (intra-mode nav).
|
|
1065
|
-
// For boot-screen escape (cross-mode), see global `menu` handler at top.
|
|
1066
|
-
if (input === 'back') {
|
|
1067
|
-
const state = getExamState();
|
|
1068
|
-
const isRealExam = state && state.session.examId !== 'demo-free';
|
|
1069
|
-
const isActiveDemo = state &&
|
|
1070
|
-
state.session.examId === 'demo-free' &&
|
|
1071
|
-
(() => {
|
|
1072
|
-
const started = new Date(state.session.startedAt || 0).getTime();
|
|
1073
|
-
return Date.now() - started < 30 * 60 * 1000;
|
|
1074
|
-
})();
|
|
1075
|
-
if (isRealExam) {
|
|
1076
|
-
console.log();
|
|
1077
|
-
console.log(chalk.gray(' Exam paused. Your progress is saved.'));
|
|
1078
|
-
console.log(chalk.white(' Resume: exam q 1') +
|
|
1079
|
-
chalk.gray(' · ') +
|
|
1080
|
-
chalk.white('exam review') +
|
|
1081
|
-
chalk.gray(' · ') +
|
|
1082
|
-
chalk.white('exam submit'));
|
|
1083
|
-
console.log();
|
|
1084
|
-
}
|
|
1085
|
-
else if (isActiveDemo) {
|
|
1086
|
-
const answered = Object.keys(state.answers).length;
|
|
1087
|
-
const total = state.session.questionCount;
|
|
1088
|
-
console.log();
|
|
1089
|
-
console.log(chalk.gray(` Demo paused (${answered}/${total} answered). Resume with: `) + chalk.white(`exam q 1`));
|
|
1090
|
-
console.log(chalk.gray(' Or type ') + chalk.white('demo') + chalk.gray(' to restart.'));
|
|
1091
|
-
console.log();
|
|
1092
|
-
}
|
|
1093
|
-
else {
|
|
1094
|
-
// Stale demo state from a past session — clear it so the menu reflects
|
|
1095
|
-
// reality. Demo is free-practice, user can always restart.
|
|
1096
|
-
if (state && state.session.examId === 'demo-free') {
|
|
1097
|
-
const { clearExamState } = await import('./lib/exam-state.js');
|
|
1098
|
-
clearExamState('demo-free');
|
|
1099
|
-
}
|
|
1100
|
-
const cfg = getConfig();
|
|
1101
|
-
fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
|
|
1102
|
-
method: 'POST',
|
|
1103
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1104
|
-
body: JSON.stringify({
|
|
1105
|
-
type: 'post-report-back',
|
|
1106
|
-
lang: cfg.language || 'en',
|
|
1107
|
-
timestamp: new Date().toISOString(),
|
|
1108
|
-
}),
|
|
1109
|
-
signal: AbortSignal.timeout(5000),
|
|
1110
|
-
}).catch(() => { });
|
|
1111
|
-
if (mode === 'selection') {
|
|
1112
|
-
printSelectionMenu();
|
|
1113
|
-
}
|
|
1114
|
-
else {
|
|
1115
|
-
console.log(chalk.gray(' Already at main menu.'));
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
rl.prompt();
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
// Help — during exam, route to exam help (= elimination lifeline);
|
|
1122
|
-
// otherwise show REPL help. `man` (with `manual` alias) is the unambiguous
|
|
1123
|
-
// "show me the command list" command that works in every state — Unix-
|
|
1124
|
-
// idiomatic, resolves the in-exam ambiguity where typing `help` to see
|
|
1125
|
-
// commands accidentally burns one of the contestant's elimination uses.
|
|
1126
|
-
// `manual` was the original spelling (v2.19.192); `man` adopted as the
|
|
1127
|
-
// primary in v2.19.196 to align with Unix convention.
|
|
1128
|
-
if (input === 'help' || input === '?') {
|
|
1129
|
-
if (getExamState()) {
|
|
1130
|
-
processing = true;
|
|
1131
|
-
try {
|
|
1132
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'help']);
|
|
1133
|
-
}
|
|
1134
|
-
catch { }
|
|
1135
|
-
processing = false;
|
|
1136
|
-
rl.prompt();
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
printReplHelp(isActivated(), mode);
|
|
1140
|
-
rl.prompt();
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
if (input === 'man' || input === 'manual') {
|
|
1144
|
-
printReplHelp(isActivated(), mode);
|
|
1145
|
-
console.log();
|
|
1146
|
-
console.log(chalk.bold.yellow(' ★ Stuck on a tool? ') +
|
|
1147
|
-
chalk.bold.cyan('man <tool>') +
|
|
1148
|
-
chalk.gray(' e.g. ') +
|
|
1149
|
-
chalk.cyan('man steghide') +
|
|
1150
|
-
chalk.gray(' · ') +
|
|
1151
|
-
chalk.cyan('man tools') +
|
|
1152
|
-
chalk.gray(' = full list'));
|
|
1153
|
-
rl.prompt();
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
// `man <tool>` / `man tools` — CTF tool usage cheatsheet (not Python env; that's `env`).
|
|
1157
|
-
if (/^(?:man|manual)\s+\S/i.test(input)) {
|
|
1158
|
-
renderToolMan(input.replace(/^(?:man|manual)\s+/i, ''));
|
|
1159
|
-
rl.prompt();
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
// "more help" — during exam, unlock bonus helps
|
|
1163
|
-
if (input.toLowerCase() === 'more help') {
|
|
1164
|
-
if (getExamState()) {
|
|
1165
|
-
processing = true;
|
|
1166
|
-
try {
|
|
1167
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'more-help']);
|
|
1168
|
-
}
|
|
1169
|
-
catch { }
|
|
1170
|
-
processing = false;
|
|
1171
|
-
rl.prompt();
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
// "continue" — after demo, guide to ai4ctf then ctf4ai
|
|
1176
|
-
if (input.toLowerCase() === 'continue') {
|
|
1177
|
-
console.log();
|
|
1178
|
-
console.log(chalk.green.bold(' ═══ AI4CTF — AI as Your Teammate ═══'));
|
|
1179
|
-
console.log();
|
|
1180
|
-
console.log(chalk.white(' In AI4CTF, you solve cybersecurity challenges'));
|
|
1181
|
-
console.log(chalk.white(' with AI by your side.'));
|
|
1182
|
-
console.log();
|
|
1183
|
-
console.log(chalk.white(' In this demo, your AI teammate gives help at 3 levels:'));
|
|
1184
|
-
console.log(chalk.yellow(' hint a') + chalk.gray(' General guidance'));
|
|
1185
|
-
console.log(chalk.yellow(' hint b') + chalk.gray(' Deep analysis'));
|
|
1186
|
-
console.log(chalk.yellow(' hint c') + chalk.gray(' Critical assist'));
|
|
1187
|
-
console.log();
|
|
1188
|
-
console.log(chalk.white(' Try it now! Type: ') + chalk.bold.green('ai4ctf'));
|
|
1189
|
-
console.log(chalk.gray(' Chat freely with your AI teammate. Type "exit" when done.'));
|
|
1190
|
-
console.log();
|
|
1191
|
-
console.log(chalk.gray(' After ai4ctf, try: ') +
|
|
1192
|
-
chalk.bold.red('ctf4ai') +
|
|
1193
|
-
chalk.gray(' — trick the AI into saying "koala"'));
|
|
1194
|
-
console.log();
|
|
1195
|
-
rl.prompt();
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
// ICOA exam token detection: legacy "ICOA-PE-001" or new Crockford "UAHXP7SWMM"
|
|
1199
|
-
if (/^ICOA-[A-Z]{2,3}-\d{1,6}$/i.test(input.trim())) {
|
|
1200
|
-
processing = true;
|
|
1201
|
-
try {
|
|
1202
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'token', input.trim()]);
|
|
1203
|
-
}
|
|
1204
|
-
catch { }
|
|
1205
|
-
processing = false;
|
|
1206
|
-
rl.prompt();
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
// New 10-char Crockford Base32 token (e.g., UAHXP7SWMM) — no hyphen
|
|
1210
|
-
if (/^[A-Z]{2}[0-9A-HJKMNP-TV-Z]{8}$/i.test(input.trim())) {
|
|
1211
|
-
processing = true;
|
|
1212
|
-
try {
|
|
1213
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'token', input.trim().toUpperCase()]);
|
|
1214
|
-
}
|
|
1215
|
-
catch { }
|
|
1216
|
-
processing = false;
|
|
1217
|
-
rl.prompt();
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
// "exam UAHXP7SWMM" — 10-char token after exam prefix
|
|
1221
|
-
const examTokenMatch = input.match(/^exam\s+([A-Z]{2}[0-9A-HJKMNP-TV-Z]{8})$/i);
|
|
1222
|
-
if (examTokenMatch) {
|
|
1223
|
-
processing = true;
|
|
1224
|
-
try {
|
|
1225
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'token', examTokenMatch[1].toUpperCase()]);
|
|
1226
|
-
}
|
|
1227
|
-
catch { }
|
|
1228
|
-
processing = false;
|
|
1229
|
-
rl.prompt();
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
// "exam AU" / "exam PE" — shortcut to exam list <country>
|
|
1233
|
-
const examCountryMatch = input.match(/^exam\s+([A-Z]{2,3})$/i);
|
|
1234
|
-
if (examCountryMatch) {
|
|
1235
|
-
processing = true;
|
|
1236
|
-
try {
|
|
1237
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'list', examCountryMatch[1]]);
|
|
1238
|
-
}
|
|
1239
|
-
catch { }
|
|
1240
|
-
processing = false;
|
|
1241
|
-
rl.prompt();
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
// Clear
|
|
1245
|
-
if (input === 'clear' || input === 'cls') {
|
|
1246
|
-
console.clear();
|
|
1247
|
-
rl.prompt();
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
// ─── Competition comms (mel/join flow) ───
|
|
1251
|
-
// "news" — re-show the current competition broadcast on demand.
|
|
1252
|
-
if (input === 'news') {
|
|
1253
|
-
if (isCompetitionJoined()) {
|
|
1254
|
-
showBroadcast();
|
|
1255
|
-
}
|
|
1256
|
-
else {
|
|
1257
|
-
console.log(chalk.gray(' No competition joined. Use ') + chalk.white('join <url>') + chalk.gray('.'));
|
|
1258
|
-
}
|
|
1259
|
-
rl.prompt();
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
// "memo" — contestant issue-report sub-screen (one-way).
|
|
1263
|
-
if (input === 'memo') {
|
|
1264
|
-
if (!isCompetitionJoined()) {
|
|
1265
|
-
console.log(chalk.gray(' Join a competition first: ') + chalk.white('join <url>'));
|
|
1266
|
-
rl.prompt();
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
processing = true;
|
|
1270
|
-
try {
|
|
1271
|
-
await enterMemoScreen();
|
|
1272
|
-
}
|
|
1273
|
-
catch { }
|
|
1274
|
-
processing = false;
|
|
1275
|
-
return; // enterMemoScreen restores listeners + re-prompts
|
|
1276
|
-
}
|
|
1277
|
-
const _isLeader = isLeaderSession() || isLeaderAccount(getConfig().userName || '');
|
|
1278
|
-
// "jury" — HIDDEN, leader-only. Non-leaders fall through to the normal
|
|
1279
|
-
// unknown-command path (no hint it exists). Server also enforces.
|
|
1280
|
-
if (input === 'jury' && _isLeader) {
|
|
1281
|
-
processing = true;
|
|
1282
|
-
try {
|
|
1283
|
-
await enterJuryScreen();
|
|
1284
|
-
}
|
|
1285
|
-
catch { }
|
|
1286
|
-
processing = false;
|
|
1287
|
-
return; // enterJuryScreen restores listeners + re-prompts
|
|
1288
|
-
}
|
|
1289
|
-
// Leader `scoreboard` reads the PUBLIC scoreboard (a leader-token session has
|
|
1290
|
-
// no CTFd token, so the normal authed `ctf scoreboard` would fail). Render
|
|
1291
|
-
// the same ranking from the public endpoint instead.
|
|
1292
|
-
if (_isLeader && isLeaderSession() && input.split(/\s+/)[0] === 'scoreboard') {
|
|
1293
|
-
processing = true;
|
|
1294
|
-
try {
|
|
1295
|
-
const board = await fetchPublicScoreboard();
|
|
1296
|
-
console.log();
|
|
1297
|
-
if (board.length === 0) {
|
|
1298
|
-
console.log(chalk.gray(' Scoreboard is empty.'));
|
|
1299
|
-
}
|
|
1300
|
-
else {
|
|
1301
|
-
console.log(chalk.bold.white(' Scoreboard'));
|
|
1302
|
-
for (const r of board.slice(0, 50)) {
|
|
1303
|
-
console.log(chalk.gray(` ${String(r.pos).padStart(3)}. `) +
|
|
1304
|
-
chalk.white(r.name.padEnd(28).slice(0, 28)) +
|
|
1305
|
-
chalk.cyan(String(r.score)));
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
console.log();
|
|
1309
|
-
}
|
|
1310
|
-
catch { }
|
|
1311
|
-
processing = false;
|
|
1312
|
-
rl.prompt();
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
// Team leaders (TMLD) never get the challenge bracket — block the
|
|
1316
|
-
// solve commands with a one-liner instead of building the menu.
|
|
1317
|
-
// scoreboard IS allowed (view-only rankings — handled above for leaders).
|
|
1318
|
-
if (_isLeader && isMelCompetition()) {
|
|
1319
|
-
const head = input.split(/\s+/)[0];
|
|
1320
|
-
if (['challenges', 'open', 'submit', 'hint'].includes(head)) {
|
|
1321
|
-
console.log();
|
|
1322
|
-
console.log(chalk.yellow(' Team-leader account — the challenge bracket is not available here.'));
|
|
1323
|
-
console.log(chalk.gray(' Available: ') +
|
|
1324
|
-
chalk.white('scoreboard') +
|
|
1325
|
-
chalk.gray(' · ') +
|
|
1326
|
-
chalk.white('news') +
|
|
1327
|
-
chalk.gray(' · ') +
|
|
1328
|
-
chalk.white('jury') +
|
|
1329
|
-
chalk.gray(' · ') +
|
|
1330
|
-
chalk.white('time') +
|
|
1331
|
-
chalk.gray(' · ') +
|
|
1332
|
-
chalk.white('status') +
|
|
1333
|
-
chalk.gray(' · ') +
|
|
1334
|
-
chalk.white('logout'));
|
|
1335
|
-
console.log();
|
|
1336
|
-
rl.prompt();
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
// Activate token
|
|
1341
|
-
if (input.startsWith('activate ')) {
|
|
1342
|
-
const token = input.slice(9).trim();
|
|
1343
|
-
const result = activateToken(token);
|
|
1344
|
-
if (result === 'ok') {
|
|
1345
|
-
console.log(chalk.green(' Access granted! Token bound to this device.'));
|
|
1346
|
-
}
|
|
1347
|
-
else if (result === 'already_bound') {
|
|
1348
|
-
console.log();
|
|
1349
|
-
console.log(chalk.red(' Token already activated on a different device.'));
|
|
1350
|
-
console.log(chalk.gray(' Each token binds to the first device that uses it. If you lost the device,'));
|
|
1351
|
-
console.log(chalk.gray(' contact your proctor to have the token re-issued for a new device.'));
|
|
1352
|
-
}
|
|
1353
|
-
else {
|
|
1354
|
-
console.log();
|
|
1355
|
-
console.log(chalk.red(' Token not recognized.'));
|
|
1356
|
-
console.log(chalk.gray(' Possible reasons:'));
|
|
1357
|
-
console.log(chalk.white(' • ') +
|
|
1358
|
-
chalk.gray('Typo — access tokens are case-insensitive and look like ') +
|
|
1359
|
-
chalk.cyan('ICOA-XXXX-XXXX-XXXX') +
|
|
1360
|
-
chalk.gray(' (4-char hex groups, e.g. ICOA-385E-27F2-B21B)'));
|
|
1361
|
-
console.log(chalk.white(' • ') + chalk.gray('Expired — ask your proctor or organizer for a fresh token'));
|
|
1362
|
-
console.log(chalk.white(' • ') + chalk.gray('Network — verify connection to ') + chalk.cyan('practice.icoa2026.au'));
|
|
1363
|
-
console.log(chalk.gray(' Still stuck? type ') +
|
|
1364
|
-
chalk.cyan('help') +
|
|
1365
|
-
chalk.gray(' or try ') +
|
|
1366
|
-
chalk.cyan('exam demo') +
|
|
1367
|
-
chalk.gray(' for a free practice round.'));
|
|
1368
|
-
}
|
|
1369
|
-
console.log();
|
|
1370
|
-
rl.prompt();
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
if (input === 'activate') {
|
|
1374
|
-
console.log(chalk.gray(' Usage: ') + chalk.white('activate <token>'));
|
|
1375
|
-
console.log();
|
|
1376
|
-
rl.prompt();
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
// ─── Quick exam answer shortcuts ───
|
|
1380
|
-
// "A" / "B" / "C" / "D" → answer current question (MCQ only)
|
|
1381
|
-
// "2 C" / "5 A" → answer specific question (MCQ only)
|
|
1382
|
-
// Practical questions (Q31-40) require flag format (ICOA{...}) — single
|
|
1383
|
-
// letters on those would be silently accepted as the wrong flag answer,
|
|
1384
|
-
// which is a footgun. Block and nudge the user toward the flag syntax.
|
|
1385
|
-
const examState = getExamState();
|
|
1386
|
-
if (examState) {
|
|
1387
|
-
const upper = input.toUpperCase().trim();
|
|
1388
|
-
// Helper: is question N practical (no A/B/C/D options)?
|
|
1389
|
-
const isPracticalQ = (n) => {
|
|
1390
|
-
const q = examState.questions.find((qq) => qq.number === n);
|
|
1391
|
-
if (!q)
|
|
1392
|
-
return false;
|
|
1393
|
-
return q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
1394
|
-
};
|
|
1395
|
-
// Helper: is question N a SIMOC Section B short-answer (integer) question?
|
|
1396
|
-
const isShortAnswerQ = (n) => {
|
|
1397
|
-
const q = examState.questions.find((qq) => qq.number === n);
|
|
1398
|
-
return q?.type === 'short_answer';
|
|
1399
|
-
};
|
|
1400
|
-
// Helper: suggest the right chat entry for a practical question.
|
|
1401
|
-
// Derive the chat command from the QUESTION TYPE, never the number.
|
|
1402
|
-
// The practical section is interleaved (standard paper: Q38 ctf4ai,
|
|
1403
|
-
// Q39 ai4ctf, Q40 ctf4ai; Paper E: Q37-46 ai4ctf, Q47-48 ctf4ai), so
|
|
1404
|
-
// the old `n >= 39 ? ctf4ai : n >= 31 ? ai4ctf` range misrouted Q38/Q39
|
|
1405
|
-
// (and all of Paper E's ai4ctf block) to the wrong AI chat — the exact
|
|
1406
|
-
// root cause already fixed in printSectionIntro (exam.ts). vla → ctf4eai.
|
|
1407
|
-
const practicalGuidance = (n) => {
|
|
1408
|
-
const isReal = examState.session.examId !== 'demo-free';
|
|
1409
|
-
const gq = examState.questions.find((qq) => qq.number === n);
|
|
1410
|
-
const cmd = practicalChatCommand(gq?.type, isReal);
|
|
1411
|
-
console.log();
|
|
1412
|
-
console.log(chalk.yellow(` Q${n} is a practical question — letters (A/B/C/D) don't apply here.`));
|
|
1413
|
-
if (cmd) {
|
|
1414
|
-
console.log(chalk.white(' Enter the AI chat for this question: ') + chalk.bold.cyan(cmd));
|
|
1415
|
-
console.log(chalk.gray(' Or submit a flag directly: ') + chalk.green(`exam answer ${n} ICOA{your_flag}`));
|
|
1416
|
-
}
|
|
1417
|
-
else {
|
|
1418
|
-
console.log(chalk.gray(' Submit a flag: ') + chalk.green(`exam answer ${n} ICOA{your_flag}`));
|
|
1419
|
-
}
|
|
1420
|
-
console.log();
|
|
1421
|
-
};
|
|
1422
|
-
// Single letter: A–E → answer current question (MCQ only; E is the SIMOC
|
|
1423
|
-
// 5th option "None of the above"). The `exam answer` command validates the
|
|
1424
|
-
// letter against the question's actual options, so a stray E on a 4-option
|
|
1425
|
-
// paper is rejected cleanly there.
|
|
1426
|
-
if (/^[ABCDE]$/.test(upper)) {
|
|
1427
|
-
const currentQ = examState._lastQ || 1;
|
|
1428
|
-
if (isPracticalQ(currentQ)) {
|
|
1429
|
-
practicalGuidance(currentQ);
|
|
1430
|
-
rl.prompt();
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
if (isShortAnswerQ(currentQ)) {
|
|
1434
|
-
console.log();
|
|
1435
|
-
console.log(chalk.yellow(` Q${currentQ} is a short-answer question — type a number, not a letter.`));
|
|
1436
|
-
console.log(chalk.gray(' Example: ') +
|
|
1437
|
-
chalk.green('42') +
|
|
1438
|
-
chalk.gray(' (or ') +
|
|
1439
|
-
chalk.green(`exam answer ${currentQ} 42`) +
|
|
1440
|
-
chalk.gray(')'));
|
|
1441
|
-
console.log();
|
|
1442
|
-
rl.prompt();
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
processing = true;
|
|
1446
|
-
try {
|
|
1447
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'answer', String(currentQ), upper]);
|
|
1448
|
-
}
|
|
1449
|
-
catch { }
|
|
1450
|
-
processing = false;
|
|
1451
|
-
rl.prompt();
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
// Bare integer → answer the current question if it's short-answer (SIMOC
|
|
1455
|
-
// Section B). Mirrors the bare-letter MCQ shortcut. A bare integer on an
|
|
1456
|
-
// MCQ/practical question falls through (it isn't a valid choice there).
|
|
1457
|
-
if (/^-?\d+$/.test(upper)) {
|
|
1458
|
-
const currentQ = examState._lastQ || 1;
|
|
1459
|
-
if (isShortAnswerQ(currentQ)) {
|
|
1460
|
-
processing = true;
|
|
1461
|
-
try {
|
|
1462
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'answer', String(currentQ), upper]);
|
|
1463
|
-
}
|
|
1464
|
-
catch { }
|
|
1465
|
-
processing = false;
|
|
1466
|
-
rl.prompt();
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
// "N X" pattern: answer question N. X is a letter A–E (MCQ) or an integer
|
|
1471
|
-
// (SIMOC short-answer), e.g. "15 A", "2 E", "16 642".
|
|
1472
|
-
const match = upper.match(/^(\d+)\s+([ABCDE]|-?\d+)$/);
|
|
1473
|
-
if (match) {
|
|
1474
|
-
const targetQ = parseInt(match[1], 10);
|
|
1475
|
-
if (isPracticalQ(targetQ)) {
|
|
1476
|
-
practicalGuidance(targetQ);
|
|
1477
|
-
rl.prompt();
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
processing = true;
|
|
1481
|
-
try {
|
|
1482
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'answer', match[1], match[2]]);
|
|
1483
|
-
}
|
|
1484
|
-
catch { }
|
|
1485
|
-
processing = false;
|
|
1486
|
-
rl.prompt();
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
const cmd = input.split(/\s+/)[0].toLowerCase();
|
|
1491
|
-
// ─── Mode-based command filtering ───
|
|
1492
|
-
const selectionCommands = [
|
|
1493
|
-
'exam',
|
|
1494
|
-
'demo',
|
|
1495
|
-
'demo2',
|
|
1496
|
-
'retry',
|
|
1497
|
-
'nations',
|
|
1498
|
-
'next',
|
|
1499
|
-
'prev',
|
|
1500
|
-
'continue',
|
|
1501
|
-
'setup',
|
|
1502
|
-
'lang',
|
|
1503
|
-
'ref',
|
|
1504
|
-
'ai4ctf',
|
|
1505
|
-
'ctf4ai',
|
|
1506
|
-
'mark',
|
|
1507
|
-
'unmark',
|
|
1508
|
-
'review',
|
|
1509
|
-
'submit',
|
|
1510
|
-
'env',
|
|
1511
|
-
'aienv',
|
|
1512
|
-
'ipynb',
|
|
1513
|
-
'sim',
|
|
1514
|
-
];
|
|
1515
|
-
const organizerCommands = [
|
|
1516
|
-
'join',
|
|
1517
|
-
'exam',
|
|
1518
|
-
'demo',
|
|
1519
|
-
'retry',
|
|
1520
|
-
'next',
|
|
1521
|
-
'prev',
|
|
1522
|
-
'logout',
|
|
1523
|
-
'setup',
|
|
1524
|
-
'lang',
|
|
1525
|
-
'ref',
|
|
1526
|
-
'ctf',
|
|
1527
|
-
'mark',
|
|
1528
|
-
'unmark',
|
|
1529
|
-
'review',
|
|
1530
|
-
'submit',
|
|
1531
|
-
];
|
|
1532
|
-
const embodiedCommands = [
|
|
1533
|
-
'demo2',
|
|
1534
|
-
'learn',
|
|
1535
|
-
'exam',
|
|
1536
|
-
'ctf4eai',
|
|
1537
|
-
'ctf4vla',
|
|
1538
|
-
// The embodied-AI red-team arena — its natural home is this mode. Both
|
|
1539
|
-
// the friendly `eai` command and the underlying `arena eai` form are
|
|
1540
|
-
// allowed so the documented entry points work without a mode switch.
|
|
1541
|
-
'eai',
|
|
1542
|
-
'arena',
|
|
1543
|
-
'lang',
|
|
1544
|
-
'ref',
|
|
1545
|
-
'setup',
|
|
1546
|
-
'env',
|
|
1547
|
-
'aienv',
|
|
1548
|
-
'ipynb',
|
|
1549
|
-
'sim',
|
|
1550
|
-
];
|
|
1551
|
-
// Selection mode allows shell commands prefixed with ! — contestants
|
|
1552
|
-
// need !python3, !cat, !base64 etc. for practical questions (Q31-Q40).
|
|
1553
|
-
// Also allow bare `python` / `python3` / `python3.12` since contestants
|
|
1554
|
-
// naturally type that when they want interactive Python.
|
|
1555
|
-
// BLOCKED_COMMANDS check downstream still rejects dangerous ones.
|
|
1556
|
-
const isPythonCmd = /^python3?(\.\d+)?$/.test(cmd);
|
|
1557
|
-
// Bare `ls` / `pwd` auto-promote to shell — the two most-typed read-only
|
|
1558
|
-
// display commands shouldn't require the `!` prefix dance. The shell-exec
|
|
1559
|
-
// path that follows already runs them in the active workspace (per-exam
|
|
1560
|
-
// tmpdir during a real exam), so contestants see only what's in scope.
|
|
1561
|
-
const autoShellCmd = cmd === 'ls' || cmd === 'pwd';
|
|
1562
|
-
const isShellCommand = input.startsWith('!') || cmd.startsWith('!') || isPythonCmd || autoShellCmd;
|
|
1563
|
-
// Common shell tools contestants try to run without `!` prefix — when
|
|
1564
|
-
// detected, point them at the right syntax instead of the generic
|
|
1565
|
-
// "Not available in X mode." dead-end.
|
|
1566
|
-
const SHELL_TOOL_HINTS = new Set([
|
|
1567
|
-
'nano',
|
|
1568
|
-
'vi',
|
|
1569
|
-
'vim',
|
|
1570
|
-
'emacs',
|
|
1571
|
-
'pico',
|
|
1572
|
-
'ed',
|
|
1573
|
-
'code',
|
|
1574
|
-
'cat',
|
|
1575
|
-
'less',
|
|
1576
|
-
'more',
|
|
1577
|
-
'head',
|
|
1578
|
-
'tail',
|
|
1579
|
-
'ls',
|
|
1580
|
-
'pwd',
|
|
1581
|
-
'cd',
|
|
1582
|
-
'base64',
|
|
1583
|
-
'xxd',
|
|
1584
|
-
'hexdump',
|
|
1585
|
-
'od',
|
|
1586
|
-
'openssl',
|
|
1587
|
-
'md5sum',
|
|
1588
|
-
'sha256sum',
|
|
1589
|
-
'grep',
|
|
1590
|
-
'find',
|
|
1591
|
-
'awk',
|
|
1592
|
-
'sed',
|
|
1593
|
-
'strings',
|
|
1594
|
-
'file',
|
|
1595
|
-
'curl',
|
|
1596
|
-
'wget',
|
|
1597
|
-
'nc',
|
|
1598
|
-
'ping',
|
|
1599
|
-
'nmap',
|
|
1600
|
-
'echo',
|
|
1601
|
-
'bash',
|
|
1602
|
-
'sh',
|
|
1603
|
-
'zsh',
|
|
1604
|
-
'pip',
|
|
1605
|
-
'pip3',
|
|
1606
|
-
'node',
|
|
1607
|
-
'npm',
|
|
1608
|
-
]);
|
|
1609
|
-
const shellHint = () => {
|
|
1610
|
-
console.log(chalk.gray(' Shell commands need ') +
|
|
1611
|
-
chalk.bold.cyan('!') +
|
|
1612
|
-
chalk.gray(' prefix. Try: ') +
|
|
1613
|
-
chalk.bold.cyan(`!${input}`));
|
|
1614
|
-
console.log();
|
|
1615
|
-
};
|
|
1616
|
-
if (mode === 'embodied' && !isShellCommand && !embodiedCommands.includes(cmd)) {
|
|
1617
|
-
if (SHELL_TOOL_HINTS.has(cmd)) {
|
|
1618
|
-
shellHint();
|
|
1619
|
-
rl.prompt();
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
console.log(chalk.gray(' Not available in Embodied AI Security mode.'));
|
|
1623
|
-
console.log(chalk.white(' Try ') +
|
|
1624
|
-
chalk.bold.cyan('demo2') +
|
|
1625
|
-
chalk.gray(' (intro) · ') +
|
|
1626
|
-
chalk.bold.cyan('learn') +
|
|
1627
|
-
chalk.gray(' (curriculum) · ') +
|
|
1628
|
-
chalk.bold.cyan('exam <PD-token>') +
|
|
1629
|
-
chalk.gray(' (Paper D)'));
|
|
1630
|
-
console.log();
|
|
1631
|
-
rl.prompt();
|
|
1632
|
-
return;
|
|
1633
|
-
}
|
|
1634
|
-
if (mode === 'selection' && !isShellCommand && !selectionCommands.includes(cmd)) {
|
|
1635
|
-
if (SHELL_TOOL_HINTS.has(cmd)) {
|
|
1636
|
-
shellHint();
|
|
1637
|
-
rl.prompt();
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
console.log(chalk.gray(' Not available in Selection mode.'));
|
|
1641
|
-
if (examState) {
|
|
1642
|
-
const currentQ = examState._lastQ || 1;
|
|
1643
|
-
console.log(chalk.white(` Resume exam: exam q ${currentQ}`) +
|
|
1644
|
-
chalk.gray(' · ') +
|
|
1645
|
-
chalk.white('A/B/C/D') +
|
|
1646
|
-
chalk.gray(' to answer'));
|
|
1647
|
-
}
|
|
1648
|
-
else {
|
|
1649
|
-
console.log(chalk.gray(' Try: demo · setup to switch mode'));
|
|
1650
|
-
}
|
|
1651
|
-
console.log();
|
|
1652
|
-
rl.prompt();
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
if (mode === 'organizer' && !organizerCommands.includes(cmd)) {
|
|
1656
|
-
console.log(chalk.gray(' Not available in Organizer mode. Switch via: setup'));
|
|
1657
|
-
console.log();
|
|
1658
|
-
rl.prompt();
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
// Token check — only in olympiad mode
|
|
1662
|
-
if (mode === 'olympiad' && (!isActivated() || !isDeviceMatch()) && !isFreeCommand(cmd)) {
|
|
1663
|
-
console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Enter your access token:'));
|
|
1664
|
-
console.log(chalk.white(' activate <token>'));
|
|
1665
|
-
console.log();
|
|
1666
|
-
console.log(chalk.gray(' Free commands: ') +
|
|
1667
|
-
chalk.white('ref [topic]') +
|
|
1668
|
-
chalk.gray(', ') +
|
|
1669
|
-
chalk.white('help') +
|
|
1670
|
-
chalk.gray(', ') +
|
|
1671
|
-
chalk.white('exit'));
|
|
1672
|
-
console.log();
|
|
1673
|
-
rl.prompt();
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
// Check if it's a known ICOA command or a system command
|
|
1677
|
-
const knownCommands = [
|
|
1678
|
-
'join',
|
|
1679
|
-
'activate',
|
|
1680
|
-
'challenges',
|
|
1681
|
-
'ch',
|
|
1682
|
-
'open',
|
|
1683
|
-
'submit',
|
|
1684
|
-
'flag',
|
|
1685
|
-
'scoreboard',
|
|
1686
|
-
'sb',
|
|
1687
|
-
'status',
|
|
1688
|
-
'time',
|
|
1689
|
-
'ref',
|
|
1690
|
-
'shell',
|
|
1691
|
-
'files',
|
|
1692
|
-
'view',
|
|
1693
|
-
'connect',
|
|
1694
|
-
'challenge',
|
|
1695
|
-
'note',
|
|
1696
|
-
'notes',
|
|
1697
|
-
'logbook',
|
|
1698
|
-
'log',
|
|
1699
|
-
'man',
|
|
1700
|
-
'manual',
|
|
1701
|
-
'lang',
|
|
1702
|
-
'setup',
|
|
1703
|
-
'env',
|
|
1704
|
-
'aienv',
|
|
1705
|
-
'ipynb',
|
|
1706
|
-
'ai4ctf',
|
|
1707
|
-
'model',
|
|
1708
|
-
'ctf',
|
|
1709
|
-
'exam',
|
|
1710
|
-
'demo',
|
|
1711
|
-
'retry',
|
|
1712
|
-
'nations',
|
|
1713
|
-
'next',
|
|
1714
|
-
'prev',
|
|
1715
|
-
'continue',
|
|
1716
|
-
'logout',
|
|
1717
|
-
'ctf4ai',
|
|
1718
|
-
'mark',
|
|
1719
|
-
'unmark',
|
|
1720
|
-
'review',
|
|
1721
|
-
'submit',
|
|
1722
|
-
'learn',
|
|
1723
|
-
'demo2',
|
|
1724
|
-
'ctf4eai',
|
|
1725
|
-
'ctf4vla',
|
|
1726
|
-
'theme',
|
|
1727
|
-
'sim',
|
|
1728
|
-
'arena',
|
|
1729
|
-
'eai',
|
|
1730
|
-
'learn-pull',
|
|
1731
|
-
];
|
|
1732
|
-
if (!knownCommands.includes(cmd)) {
|
|
1733
|
-
// Friendly hint — the REPL isn't a shell, so these have nowhere to run.
|
|
1734
|
-
// This is NOT a sandbox; see REPL_NOT_A_SHELL_HINTS comment above.
|
|
1735
|
-
if (BLOCKED_COMMANDS.has(cmd)) {
|
|
1736
|
-
console.log(chalk.yellow(` "${cmd}" is a shell command — the ICOA prompt isn't a shell.`));
|
|
1737
|
-
console.log(chalk.gray(' In real-exam mode, prefix with ! to run inside the sandbox: ') + chalk.cyan(`!${input}`));
|
|
1738
|
-
console.log();
|
|
1739
|
-
rl.prompt();
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
// Block path escape attempts
|
|
1743
|
-
if (/(?:^|\s)(?:\/(?!home\/|Users\/|tmp\/)|\.\.\/|~\/)/.test(input) && !input.startsWith('cd ')) {
|
|
1744
|
-
// Allow relative paths within workspace, block absolute paths outside
|
|
1745
|
-
const hasAbsPath = /(?:^|\s)\/(?!home\/\w+\/icoa-workspace|Users\/\w+\/icoa-workspace|tmp\/)/.test(input);
|
|
1746
|
-
const hasParentPath = /\.\./.test(input);
|
|
1747
|
-
if (hasAbsPath || hasParentPath) {
|
|
1748
|
-
console.log(chalk.red(' Blocked: access outside workspace is not allowed.'));
|
|
1749
|
-
console.log(chalk.gray(` Workspace: ${WORKSPACE}`));
|
|
1750
|
-
console.log();
|
|
1751
|
-
rl.prompt();
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
// Force Python 3.12 — rewrite python/python3 to correct binary
|
|
1756
|
-
// Strip optional ! prefix that contestants use to disambiguate shell
|
|
1757
|
-
// commands from CLI commands (e.g. !python3 vs cli's "python" command).
|
|
1758
|
-
let resolvedInput = input.startsWith('!') ? input.slice(1).trim() : input;
|
|
1759
|
-
if (process.platform === 'darwin') {
|
|
1760
|
-
const py12 = '/opt/homebrew/opt/python@3.12/bin/python3.12';
|
|
1761
|
-
resolvedInput = resolvedInput.replace(/^python3?\s/, `${py12} `).replace(/^(python3|python)$/, py12);
|
|
1762
|
-
}
|
|
1763
|
-
else if (process.platform === 'win32') {
|
|
1764
|
-
// Windows: the binary is `python` (not `python3`). Python Launcher `py -3`
|
|
1765
|
-
// is also common. Rewrite `python3 xyz` → `python xyz` or `py -3 xyz` so
|
|
1766
|
-
// students who followed Unix-oriented practical-question hints don't get
|
|
1767
|
-
// "'python3' is not recognized". Prefer `py -3` if the launcher exists
|
|
1768
|
-
// (handles multi-version installs); fall back to plain `python`.
|
|
1769
|
-
const pyCmd = (() => {
|
|
1770
|
-
try {
|
|
1771
|
-
execSyncFn('py -3 --version', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 1500 });
|
|
1772
|
-
return 'py -3';
|
|
1773
|
-
}
|
|
1774
|
-
catch { }
|
|
1775
|
-
return 'python';
|
|
1776
|
-
})();
|
|
1777
|
-
resolvedInput = resolvedInput.replace(/^python3?(\.\d+)?\s/, `${pyCmd} `).replace(/^python3?(\.\d+)?$/, pyCmd);
|
|
1778
|
-
}
|
|
1779
|
-
else {
|
|
1780
|
-
// Linux/WSL: python → python3 (or python3.12 if available)
|
|
1781
|
-
const py12 = (() => {
|
|
1782
|
-
try {
|
|
1783
|
-
execSyncFn('which python3.12', { stdio: 'ignore' });
|
|
1784
|
-
return 'python3.12';
|
|
1785
|
-
}
|
|
1786
|
-
catch {
|
|
1787
|
-
return 'python3';
|
|
1788
|
-
}
|
|
1789
|
-
})();
|
|
1790
|
-
resolvedInput = resolvedInput.replace(/^python\s/, `${py12} `).replace(/^python$/, py12);
|
|
1791
|
-
}
|
|
1792
|
-
// Active workspace — per-exam tmpdir during a real exam (L1), otherwise
|
|
1793
|
-
// the legacy ~/icoa-workspace. getActiveCwd() handles the switch.
|
|
1794
|
-
const cwd = getActiveCwd();
|
|
1795
|
-
// L2 — audit shell escapes during real exams. Risk pattern hits print
|
|
1796
|
-
// a warning to the contestant and log to server + local audit file.
|
|
1797
|
-
const riskFlags = checkShellRisk(input);
|
|
1798
|
-
logShellAudit({ cwd, input, riskFlags });
|
|
1799
|
-
if (riskFlags.length > 0 && getRealExamState()) {
|
|
1800
|
-
console.log(chalk.yellow(` ⚠ Action flagged: ${riskFlags.join(', ')} — logged.`));
|
|
1801
|
-
console.log();
|
|
1802
|
-
}
|
|
1803
|
-
// Interactive Python (no script args, no -c): print welcome banner and
|
|
1804
|
-
// pre-import common CTF modules via a startup file. Makes entry painless
|
|
1805
|
-
// for contestants — they land in a shell with base64/struct/hashlib/pwn
|
|
1806
|
-
// already imported and see a 4-line cheat sheet of common one-liners.
|
|
1807
|
-
const isInteractivePython = /^(\S*python3?(\.\d+)?)\s*$/.test(resolvedInput);
|
|
1808
|
-
if (isInteractivePython) {
|
|
1809
|
-
const startupPath = ensurePythonStartup();
|
|
1810
|
-
resolvedInput = `PYTHONSTARTUP="${startupPath}" ${resolvedInput}`;
|
|
1811
|
-
printPythonBanner();
|
|
1812
|
-
}
|
|
1813
|
-
// Route to Docker sandbox if available, otherwise system shell (in workspace)
|
|
1814
|
-
processing = true;
|
|
1815
|
-
try {
|
|
1816
|
-
if (isDockerAvailable()) {
|
|
1817
|
-
const ready = await ensureSandbox();
|
|
1818
|
-
if (ready) {
|
|
1819
|
-
await runInSandbox(resolvedInput, rl);
|
|
1820
|
-
}
|
|
1821
|
-
else {
|
|
1822
|
-
await runSystemCommand(resolvedInput, rl, cwd);
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
else {
|
|
1826
|
-
await runSystemCommand(resolvedInput, rl, cwd);
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
catch {
|
|
1830
|
-
console.log(chalk.yellow(` Command failed: ${cmd}`));
|
|
1831
|
-
}
|
|
1832
|
-
processing = false;
|
|
1833
|
-
console.log();
|
|
1834
|
-
rl.prompt();
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
processing = true;
|
|
1838
|
-
// During an exam, `submit` must mean "submit my exam", not "submit a CTF
|
|
1839
|
-
// flag". The `mapCommand` shortcut table always points `submit` at CTF,
|
|
1840
|
-
// which silently fails in Selection mode and leaves users stuck. Route
|
|
1841
|
-
// to exam submit here instead.
|
|
1842
|
-
//
|
|
1843
|
-
// Two distinct cases:
|
|
1844
|
-
// 1. Bare `submit` (no flag) → exam submit (final-submit the exam)
|
|
1845
|
-
// 2. `submit ICOA{...}` (with flag) → exam answer <current_q> <flag>
|
|
1846
|
-
// Q31-Q38 hint text in selection JSON tells students to use
|
|
1847
|
-
// `submit ICOA{...}` (matches demo muscle memory). Without this
|
|
1848
|
-
// shortcut they'd type the literal hint and hit silent failure
|
|
1849
|
-
// (real bug 2026-05-05; reproed by Tina, Ubuntu 22). Mapping it to
|
|
1850
|
-
// `exam answer` here means the on-screen hint becomes correct
|
|
1851
|
-
// without rewriting 910 occurrences across 245 country JSON files.
|
|
1852
|
-
const trimmed = input.trim();
|
|
1853
|
-
const lc = trimmed.toLowerCase();
|
|
1854
|
-
let submitInExam = null;
|
|
1855
|
-
let flagArg = '';
|
|
1856
|
-
if (examState) {
|
|
1857
|
-
if (lc === 'submit') {
|
|
1858
|
-
submitInExam = 'final';
|
|
1859
|
-
}
|
|
1860
|
-
else if (lc.startsWith('submit ')) {
|
|
1861
|
-
let rest = trimmed.slice('submit '.length).trim();
|
|
1862
|
-
// Tolerate accidental double-submit / wrapping quotes
|
|
1863
|
-
if (/^submit\s+/i.test(rest))
|
|
1864
|
-
rest = rest.replace(/^submit\s+/i, '').trim();
|
|
1865
|
-
rest = rest.replace(/^["'`]+|["'`]+$/g, '').trim();
|
|
1866
|
-
if (rest && /^ICOA\{[^}]*\}?$/i.test(rest)) {
|
|
1867
|
-
submitInExam = 'flag';
|
|
1868
|
-
flagArg = rest;
|
|
1869
|
-
}
|
|
1870
|
-
else if (rest && /^[ABCDE]$/i.test(rest)) {
|
|
1871
|
-
// `submit B` on an MCQ — common muscle memory carried over from the
|
|
1872
|
-
// practical `submit ICOA{...}` flow. Without this, a bare letter
|
|
1873
|
-
// fell through to mapCommand → `ctf submit B` → silent failure in
|
|
1874
|
-
// paper mode ("Q11 无法提交"). Route it to `exam answer <currentQ> B`;
|
|
1875
|
-
// exam answer validates the letter against the question's real
|
|
1876
|
-
// options (and rejects a stray letter on a practical question).
|
|
1877
|
-
submitInExam = 'flag';
|
|
1878
|
-
flagArg = rest.toUpperCase();
|
|
1879
|
-
}
|
|
1880
|
-
// submit followed by anything else (e.g. `submit q1` typo) → fall
|
|
1881
|
-
// through to mapCommand, which produces a clearer error than silent.
|
|
1882
|
-
}
|
|
1883
|
-
else if (/^ICOA\{[^}]+\}$/i.test(trimmed)) {
|
|
1884
|
-
// Bare ICOA{...} paste — same auto-detect as ai4ctf chat.
|
|
1885
|
-
submitInExam = 'flag';
|
|
1886
|
-
flagArg = trimmed;
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
let args;
|
|
1890
|
-
if (submitInExam === 'final') {
|
|
1891
|
-
args = ['exam', 'submit'];
|
|
1892
|
-
}
|
|
1893
|
-
else if (submitInExam === 'flag') {
|
|
1894
|
-
const currentQ = examState?._lastQ || 1;
|
|
1895
|
-
args = ['exam', 'answer', String(currentQ), flagArg];
|
|
1896
|
-
}
|
|
1897
|
-
else {
|
|
1898
|
-
args = mapCommand(input);
|
|
1899
|
-
}
|
|
1900
|
-
// Pause REPL readline for commands that read stdin (join)
|
|
1901
|
-
const needsPause = args[0] === 'ctf' && args[1] === 'join';
|
|
1902
|
-
if (needsPause)
|
|
1903
|
-
rl.pause();
|
|
1904
|
-
process.exit = (() => {
|
|
1905
|
-
throw new Error(INTERCEPT);
|
|
1906
|
-
});
|
|
1907
|
-
try {
|
|
1908
|
-
await program.parseAsync(['node', 'icoa', ...args]);
|
|
1909
|
-
}
|
|
1910
|
-
catch (err) {
|
|
1911
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1912
|
-
if (msg === INTERCEPT) {
|
|
1913
|
-
// Command tried to exit — continue REPL
|
|
1914
|
-
}
|
|
1915
|
-
else if (msg.includes('commander.unknownCommand')) {
|
|
1916
|
-
// T4-12: typo suggestion via Levenshtein distance. Shorten the
|
|
1917
|
-
// "unknown command" frustration by pointing to the likely-intended
|
|
1918
|
-
// command when the user's input is within 2 edits of a real one.
|
|
1919
|
-
const { distance } = await import('fastest-levenshtein');
|
|
1920
|
-
const KNOWN_CMDS = [
|
|
1921
|
-
'ctf',
|
|
1922
|
-
'ref',
|
|
1923
|
-
'shell',
|
|
1924
|
-
'files',
|
|
1925
|
-
'view',
|
|
1926
|
-
'connect',
|
|
1927
|
-
'challenge',
|
|
1928
|
-
'note',
|
|
1929
|
-
'notes',
|
|
1930
|
-
'logbook',
|
|
1931
|
-
'log',
|
|
1932
|
-
'manual',
|
|
1933
|
-
'lang',
|
|
1934
|
-
'setup',
|
|
1935
|
-
'env',
|
|
1936
|
-
'aienv',
|
|
1937
|
-
'ipynb',
|
|
1938
|
-
'ai4ctf',
|
|
1939
|
-
'exam',
|
|
1940
|
-
'ctf4ai',
|
|
1941
|
-
'theme',
|
|
1942
|
-
'eai',
|
|
1943
|
-
'clear',
|
|
1944
|
-
'cls',
|
|
1945
|
-
'quit',
|
|
1946
|
-
'exit',
|
|
1947
|
-
'back',
|
|
1948
|
-
'menu',
|
|
1949
|
-
'help',
|
|
1950
|
-
'continue',
|
|
1951
|
-
'activate',
|
|
1952
|
-
'demo',
|
|
1953
|
-
'challenges',
|
|
1954
|
-
'status',
|
|
1955
|
-
'scoreboard',
|
|
1956
|
-
'join',
|
|
1957
|
-
'logout',
|
|
1958
|
-
];
|
|
1959
|
-
const firstWord = cmd.split(/\s+/)[0] || cmd;
|
|
1960
|
-
let best = { word: '', dist: Infinity };
|
|
1961
|
-
for (const known of KNOWN_CMDS) {
|
|
1962
|
-
const d = distance(firstWord.toLowerCase(), known);
|
|
1963
|
-
if (d < best.dist)
|
|
1964
|
-
best = { word: known, dist: d };
|
|
1965
|
-
}
|
|
1966
|
-
console.log(chalk.yellow(` Unknown command: ${cmd}.`));
|
|
1967
|
-
if (best.dist > 0 && best.dist <= 2) {
|
|
1968
|
-
console.log(chalk.gray(' Did you mean: ') + chalk.bold.cyan(best.word) + chalk.gray('?'));
|
|
1969
|
-
}
|
|
1970
|
-
console.log(chalk.gray(' Type ') + chalk.cyan('help') + chalk.gray(' for the full command list.'));
|
|
1971
|
-
}
|
|
1972
|
-
else if (msg.includes('commander.missingArgument') ||
|
|
1973
|
-
err?.code === 'commander.missingArgument') {
|
|
1974
|
-
// Incomplete command. Commander's own stderr is suppressed
|
|
1975
|
-
// (configureOutput writeErr is a no-op), so without this branch the
|
|
1976
|
-
// user sees NOTHING — the classic "submit 44 → 提交后没有反应" where the
|
|
1977
|
-
// flag was left off. Print an actionable usage line instead of silence.
|
|
1978
|
-
if (cmd === 'submit' || cmd === 'flag') {
|
|
1979
|
-
console.log(chalk.yellow(' Incomplete — usage: ') + chalk.cyan('submit <challenge#> <flag>'));
|
|
1980
|
-
console.log(chalk.gray(' Put the number AND the flag on one line, e.g. ') + chalk.cyan('submit 44 pecan{...}'));
|
|
1981
|
-
}
|
|
1982
|
-
else {
|
|
1983
|
-
console.log(chalk.yellow(` Incomplete command: ${cmd}`));
|
|
1984
|
-
console.log(chalk.gray(' Type ') + chalk.cyan('help') + chalk.gray(' for usage.'));
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
else if (msg.includes('commander.')) {
|
|
1988
|
-
// Internal Commander errors — ignore
|
|
1989
|
-
}
|
|
1990
|
-
else if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT')) {
|
|
1991
|
-
console.log(chalk.yellow(' Network error. Check your connection.'));
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
finally {
|
|
1995
|
-
process.exit = realExit;
|
|
1996
|
-
processing = false;
|
|
1997
|
-
if (needsPause)
|
|
1998
|
-
rl.resume();
|
|
1999
|
-
}
|
|
2000
|
-
// Switch prompt if entering chat/challenge mode
|
|
2001
|
-
if (isChatActive()) {
|
|
2002
|
-
rl.setPrompt(chalk.magenta('icoa ai4ctf> '));
|
|
2003
|
-
}
|
|
2004
|
-
else if (isCtf4aiActive()) {
|
|
2005
|
-
rl.setPrompt(chalk.red('icoa ctf4ai> '));
|
|
2006
|
-
}
|
|
2007
|
-
else if (isCtf4VlaActive()) {
|
|
2008
|
-
rl.setPrompt(chalk.bold.cyan('icoa ctf4eai> '));
|
|
2009
|
-
}
|
|
2010
|
-
console.log();
|
|
2011
|
-
rl.prompt();
|
|
2012
|
-
});
|
|
2013
|
-
// SIGINT (Ctrl+C) — intercept gracefully so beginners don't lose confidence.
|
|
2014
|
-
// Without this listener, readline's default is to raise SIGINT which our
|
|
2015
|
-
// theme.ts handler converts to process.exit(130). Installing this listener
|
|
2016
|
-
// swallows that path and lets the user get oriented. If they want to exit,
|
|
2017
|
-
// they type `quit` or hit Ctrl+D (sends EOF → 'close' event below).
|
|
2018
|
-
rl.on('SIGINT', () => {
|
|
2019
|
-
console.log();
|
|
2020
|
-
if (isChatActive() || isCtf4aiActive()) {
|
|
2021
|
-
console.log(chalk.yellow(' Ctrl+C did not close ICOA CLI — you are still in the AI chat.'));
|
|
2022
|
-
console.log(chalk.white(' Type ') + chalk.bold.cyan('exit') + chalk.white(' to leave the chat and return to the menu.'));
|
|
2023
|
-
}
|
|
2024
|
-
else if (getExamState()) {
|
|
2025
|
-
const isReal = getExamState().session.examId !== 'demo-free';
|
|
2026
|
-
console.log(chalk.yellow(' Ctrl+C did NOT close ICOA CLI.'));
|
|
2027
|
-
console.log(chalk.gray(` Your ${isReal ? 'exam' : 'demo'} is paused and every answer is auto-saved.`));
|
|
2028
|
-
console.log();
|
|
2029
|
-
console.log(chalk.white(' Resume: ') +
|
|
2030
|
-
chalk.cyan('exam q 1') +
|
|
2031
|
-
chalk.gray(' · Back to menu: ') +
|
|
2032
|
-
chalk.cyan('back') +
|
|
2033
|
-
chalk.gray(' · Close CLI: ') +
|
|
2034
|
-
chalk.cyan(isReal ? 'quit confirm' : 'quit'));
|
|
2035
|
-
}
|
|
2036
|
-
else {
|
|
2037
|
-
console.log(chalk.yellow(' Ctrl+C did not close ICOA CLI — you are still at the ') +
|
|
2038
|
-
chalk.cyan('icoa>') +
|
|
2039
|
-
chalk.yellow(' prompt.'));
|
|
2040
|
-
console.log(chalk.gray(' Keep typing — ') +
|
|
2041
|
-
chalk.cyan('help') +
|
|
2042
|
-
chalk.gray(' lists commands. (Only ') +
|
|
2043
|
-
chalk.cyan('quit') +
|
|
2044
|
-
chalk.gray(' or Ctrl+D actually close the CLI.)'));
|
|
2045
|
-
}
|
|
2046
|
-
console.log();
|
|
2047
|
-
rl.prompt();
|
|
2048
|
-
});
|
|
2049
|
-
rl.on('close', () => {
|
|
2050
|
-
stopLogSync();
|
|
2051
|
-
stopBroadcastPoll();
|
|
2052
|
-
recordExit();
|
|
2053
|
-
resetTerminalTheme();
|
|
2054
|
-
realExit(0);
|
|
2055
|
-
});
|
|
2056
|
-
}
|
|
2057
|
-
function runSystemCommand(input, rl, cwd) {
|
|
2058
|
-
return new Promise((resolve) => {
|
|
2059
|
-
// Fully release the TTY so interactive children (python3, bash, etc.)
|
|
2060
|
-
// get a clean terminal with proper echo. `rl.pause()` alone doesn't
|
|
2061
|
-
// reset raw mode or release stdin — readline keeps it in line-editing
|
|
2062
|
-
// mode where typed characters don't display until our REPL reads a line.
|
|
2063
|
-
const stdin = process.stdin;
|
|
2064
|
-
const wasRaw = stdin.isTTY ? !!stdin.isRaw : false;
|
|
2065
|
-
rl.pause();
|
|
2066
|
-
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
2067
|
-
try {
|
|
2068
|
-
stdin.setRawMode(false);
|
|
2069
|
-
}
|
|
2070
|
-
catch { }
|
|
2071
|
-
}
|
|
2072
|
-
const child = spawn(input, {
|
|
2073
|
-
shell: true,
|
|
2074
|
-
stdio: 'inherit',
|
|
2075
|
-
cwd: cwd || process.cwd(),
|
|
2076
|
-
});
|
|
2077
|
-
const restore = () => {
|
|
2078
|
-
if (stdin.isTTY && typeof stdin.setRawMode === 'function' && wasRaw) {
|
|
2079
|
-
try {
|
|
2080
|
-
stdin.setRawMode(true);
|
|
2081
|
-
}
|
|
2082
|
-
catch { }
|
|
2083
|
-
}
|
|
2084
|
-
rl.resume();
|
|
2085
|
-
resolve();
|
|
2086
|
-
};
|
|
2087
|
-
child.on('close', restore);
|
|
2088
|
-
child.on('error', restore);
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
2091
|
-
function mapCommand(input) {
|
|
2092
|
-
const parts = input.split(/\s+/);
|
|
2093
|
-
const cmd = parts[0].toLowerCase();
|
|
2094
|
-
const rest = parts.slice(1);
|
|
2095
|
-
// `aienv run` / `aienv python` forward everything after the subcommand to the
|
|
2096
|
-
// venv python child, so the args must survive shell-style quoting (spaced
|
|
2097
|
-
// paths, -c "expr"). Re-tokenize the raw line quote-aware instead of using the
|
|
2098
|
-
// naive whitespace split above — otherwise the quotes/spaces reach python
|
|
2099
|
-
// mangled. Only these arg-forwarding subcommands need it.
|
|
2100
|
-
if (cmd === 'aienv' && ['run', 'python', 'py'].includes((rest[0] ?? '').toLowerCase())) {
|
|
2101
|
-
return shellSplit(input);
|
|
2102
|
-
}
|
|
2103
|
-
// `submit <id> <flag>` / `flag <id> <flag>` — the flag may contain spaces
|
|
2104
|
-
// (multi-word flags are common, e.g. song-lyric answers like
|
|
2105
|
-
// `pecan{Keep you second guessing}`). The generic `...rest` spread below would
|
|
2106
|
-
// pass each word as a separate operand → commander.excessArguments → the REPL
|
|
2107
|
-
// swallows it silently ("submit … 提交后没有反应"). Re-join everything after the
|
|
2108
|
-
// id into one flag argument, and strip any wrapping quotes the user added.
|
|
2109
|
-
if ((cmd === 'submit' || cmd === 'flag') && rest.length >= 2) {
|
|
2110
|
-
const id = rest[0];
|
|
2111
|
-
const flagText = rest
|
|
2112
|
-
.slice(1)
|
|
2113
|
-
.join(' ')
|
|
2114
|
-
.trim()
|
|
2115
|
-
.replace(/^["'`]+|["'`]+$/g, '');
|
|
2116
|
-
return ['ctf', 'submit', id, flagText];
|
|
2117
|
-
}
|
|
2118
|
-
const ctfShortcuts = {
|
|
2119
|
-
demo: ['exam', 'demo'],
|
|
2120
|
-
retry: ['exam', 'demo-retry'],
|
|
2121
|
-
nations: ['exam', 'nations'],
|
|
2122
|
-
next: ['exam', 'next'],
|
|
2123
|
-
prev: ['exam', 'prev'],
|
|
2124
|
-
mark: ['exam', 'mark', ...rest],
|
|
2125
|
-
unmark: ['exam', 'unmark', ...rest],
|
|
2126
|
-
review: ['exam', 'review'],
|
|
2127
|
-
logout: ['ctf', 'logout'],
|
|
2128
|
-
join: ['ctf', 'join', ...rest],
|
|
2129
|
-
activate: ['ctf', 'activate', ...rest],
|
|
2130
|
-
challenges: ['ctf', 'challenges'],
|
|
2131
|
-
ch: ['ctf', 'challenges'],
|
|
2132
|
-
open: ['ctf', 'open', ...rest],
|
|
2133
|
-
submit: ['ctf', 'submit', ...rest],
|
|
2134
|
-
flag: ['ctf', 'submit', ...rest],
|
|
2135
|
-
scoreboard: ['ctf', 'scoreboard', ...rest],
|
|
2136
|
-
sb: ['ctf', 'scoreboard', ...rest],
|
|
2137
|
-
status: ['ctf', 'status'],
|
|
2138
|
-
time: ['ctf', 'time'],
|
|
2139
|
-
};
|
|
2140
|
-
if (ctfShortcuts[cmd]) {
|
|
2141
|
-
return ctfShortcuts[cmd];
|
|
2142
|
-
}
|
|
2143
|
-
const directCommands = [
|
|
2144
|
-
'ref',
|
|
2145
|
-
'shell',
|
|
2146
|
-
'files',
|
|
2147
|
-
'view',
|
|
2148
|
-
'connect',
|
|
2149
|
-
'challenge',
|
|
2150
|
-
'note',
|
|
2151
|
-
'notes',
|
|
2152
|
-
'logbook',
|
|
2153
|
-
'log',
|
|
2154
|
-
'man',
|
|
2155
|
-
'manual',
|
|
2156
|
-
'lang',
|
|
2157
|
-
'setup',
|
|
2158
|
-
'env',
|
|
2159
|
-
'aienv',
|
|
2160
|
-
'ipynb',
|
|
2161
|
-
'ai4ctf',
|
|
2162
|
-
'model',
|
|
2163
|
-
'ctf',
|
|
2164
|
-
'exam',
|
|
2165
|
-
'ctf4ai',
|
|
2166
|
-
];
|
|
2167
|
-
if (directCommands.includes(cmd)) {
|
|
2168
|
-
return [cmd, ...rest];
|
|
2169
|
-
}
|
|
2170
|
-
return parts;
|
|
2171
|
-
}
|
|
2172
|
-
function printReplHelp(activated, mode = 'olympiad') {
|
|
2173
|
-
console.log();
|
|
2174
|
-
// ─── Selection / Organizer: lightweight help ───
|
|
2175
|
-
if (mode === 'selection' || mode === 'organizer') {
|
|
2176
|
-
console.log(chalk.bold.white(' Exam'));
|
|
2177
|
-
console.log(chalk.white(' join <url> ') + chalk.gray('Connect to exam server'));
|
|
2178
|
-
console.log(chalk.white(' exam list ') + chalk.gray('Available exams'));
|
|
2179
|
-
console.log(chalk.white(' exam start <id> ') + chalk.gray('Begin an exam'));
|
|
2180
|
-
console.log(chalk.white(' exam q [n] ') + chalk.gray('View questions'));
|
|
2181
|
-
console.log(chalk.white(' exam answer <n> <X> ') + chalk.gray('Answer question'));
|
|
2182
|
-
console.log(chalk.white(' exam review ') + chalk.gray('Review all answers'));
|
|
2183
|
-
console.log(chalk.white(' exam submit ') + chalk.gray('Submit for grading'));
|
|
2184
|
-
console.log(chalk.white(' exam result ') + chalk.gray('View your score'));
|
|
2185
|
-
console.log();
|
|
2186
|
-
console.log(chalk.bold.white(' System'));
|
|
2187
|
-
console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
|
|
2188
|
-
console.log(chalk.white(' setup ') + chalk.gray('Settings / switch mode'));
|
|
2189
|
-
console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language'));
|
|
2190
|
-
console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
|
|
2191
|
-
console.log(chalk.white(' exit ') + chalk.gray('Quit'));
|
|
2192
|
-
console.log();
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
// ─── Olympiad: full help ───
|
|
2196
|
-
if (!activated) {
|
|
2197
|
-
console.log(chalk.bold.yellow(' Restricted Mode — activate with a token to unlock all commands'));
|
|
2198
|
-
console.log();
|
|
2199
|
-
console.log(chalk.white(' activate <token> ') + chalk.gray('Unlock full access'));
|
|
2200
|
-
console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
|
|
2201
|
-
console.log(chalk.white(' exit ') + chalk.gray('Quit'));
|
|
2202
|
-
console.log();
|
|
2203
|
-
return;
|
|
2204
|
-
}
|
|
2205
|
-
// How it works — workflow overview
|
|
2206
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════'));
|
|
2207
|
-
console.log(chalk.bold.white(' How it works'));
|
|
2208
|
-
console.log();
|
|
2209
|
-
console.log(chalk.gray(' 1. Browse ') + chalk.white('challenges') + chalk.gray(' and pick one'));
|
|
2210
|
-
console.log(chalk.gray(' 2. ') + chalk.white('open <id>') + chalk.gray(' to read the challenge'));
|
|
2211
|
-
console.log(chalk.gray(' 3. Use ') + chalk.white('ai4ctf') + chalk.gray(' to chat with AI when stuck'));
|
|
2212
|
-
console.log(chalk.gray(' 4. ') + chalk.white('submit <id> <flag>') + chalk.gray(' to score points'));
|
|
2213
|
-
console.log(chalk.gray(' 5. Check ') + chalk.white('scoreboard') + chalk.gray(' to track your rank'));
|
|
2214
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════'));
|
|
2215
|
-
console.log();
|
|
2216
|
-
console.log(chalk.bold.white(' Competition'));
|
|
2217
|
-
console.log(chalk.white(' join <url> ') + chalk.gray('Connect to CTFd'));
|
|
2218
|
-
console.log(chalk.white(' challenges (ch) ') + chalk.gray('List challenges by category'));
|
|
2219
|
-
console.log(chalk.white(' open <id> ') + chalk.gray('Read challenge + get next steps'));
|
|
2220
|
-
console.log(chalk.white(' submit <id> <flag> ') + chalk.gray('Submit a flag'));
|
|
2221
|
-
console.log(chalk.white(' scoreboard (sb) ') + chalk.gray('Live rankings'));
|
|
2222
|
-
console.log(chalk.white(' status ') + chalk.gray('Your score & timer'));
|
|
2223
|
-
console.log(chalk.white(' time ') + chalk.gray('Countdown timer'));
|
|
2224
|
-
console.log();
|
|
2225
|
-
console.log(chalk.bold.white(' AI Teammate'));
|
|
2226
|
-
console.log(chalk.white(' ai4ctf ') + chalk.gray('Free-chat with your AI teammate'));
|
|
2227
|
-
console.log();
|
|
2228
|
-
console.log(chalk.bold.white(' Tools'));
|
|
2229
|
-
console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference (linux, web, crypto...)'));
|
|
2230
|
-
console.log(chalk.white(' shell ') + chalk.gray('Interactive CTF shell (sandbox if present, else host)'));
|
|
2231
|
-
console.log(chalk.white(' files <id> ') + chalk.gray('Download challenge files'));
|
|
2232
|
-
console.log(chalk.white(' connect <id> ') + chalk.gray('Connect to remote target'));
|
|
2233
|
-
console.log(chalk.white(' note [text] ') + chalk.gray('Add a logbook entry (in-exam: tags current Q)'));
|
|
2234
|
-
console.log(chalk.white(' logbook [n] ') + chalk.gray('View your logbook (in-exam: filter by Q)'));
|
|
2235
|
-
console.log(chalk.white(' log ') + chalk.gray('Session history'));
|
|
2236
|
-
console.log(chalk.white(' man <tool> ') +
|
|
2237
|
-
chalk.bold.yellow('★ ') +
|
|
2238
|
-
chalk.gray('How to use a tool (steghide, nmap…); ') +
|
|
2239
|
-
chalk.cyan('man tools') +
|
|
2240
|
-
chalk.gray(' = list. Bare ') +
|
|
2241
|
-
chalk.cyan('man') +
|
|
2242
|
-
chalk.gray(' = this list.'));
|
|
2243
|
-
console.log();
|
|
2244
|
-
console.log(chalk.bold.white(' System'));
|
|
2245
|
-
console.log(chalk.white(' setup ') + chalk.gray('Configure settings'));
|
|
2246
|
-
console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language (15 supported)'));
|
|
2247
|
-
console.log(chalk.white(' logout ') + chalk.gray('Disconnect'));
|
|
2248
|
-
console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
|
|
2249
|
-
console.log(chalk.white(' exit ') + chalk.gray('Quit (session saved)'));
|
|
2250
|
-
console.log();
|
|
2251
|
-
}
|
|
1
|
+
import{createInterface as o}from"node:readline";import{spawn as e,execSync as l}from"node:child_process";import chalk from"chalk";import{isConnected as n,getConfig as t,saveConfig as a}from"./lib/config.js";import{ensureLangCache as s}from"./lib/translations-fetcher.js";import{loadUiPack as r}from"./lib/i18n.js";import{isActivated as i,activateToken as c,isFreeCommand as g,isDeviceMatch as y,recordExit as m,recordResume as d,isFirstRunOrUpgrade as p,markVersionSeen as u}from"./lib/access.js";import{setReplMode as h}from"./lib/ui.js";import{returnToMainMenu as w,printMenuHint as f,menuHintInline as b}from"./lib/menu-nav.js";import{setMainRl as x}from"./lib/main-rl.js";import{shellSplit as v}from"./lib/shell-split.js";import{isChatActive as A,handleChatMessage as C}from"./commands/ai4ctf.js";import{renderToolMan as I}from"./lib/tool-man.js";import{isCtf4aiActive as k,handleCtf4aiMessage as T}from"./commands/ctf4ai-demo.js";import{isCtf4VlaActive as E,handleCtf4VlaMessage as S}from"./commands/ctf4vla.js";import{isDemo2Active as $}from"./commands/demo2.js";import{loadDemo2Progress as O}from"./lib/demo2-progress.js";import{hasAnySimHistory as j,lastArmAt as D,lastBundledAt as L,lastSimAt as N}from"./lib/sim-cooldown.js";import{getExamState as R,getRealExamState as P,getDemoState as F,practicalChatCommand as q}from"./lib/exam-state.js";import{resetTerminalTheme as M}from"./lib/theme.js";import{ensureSandbox as B,runInSandbox as U,isDockerAvailable as _}from"./lib/sandbox.js";import{checkShellRisk as Q,logShellAudit as z,getActiveCwd as W}from"./lib/exam-sandbox.js";import{logCommand as X}from"./lib/logger.js";import{startLogSync as Y,stopLogSync as J}from"./lib/log-sync.js";import{startBroadcastPoll as V,stopBroadcastPoll as G,showBroadcast as H,isCompetitionJoined as Z,isMelCompetition as K,isLeaderAccount as oo,isLeaderSession as eo,fetchPublicScoreboard as lo,isCommsActive as no}from"./lib/comms.js";import{enterMemoScreen as to,enterJuryScreen as ao}from"./commands/comms.js";import{existsSync as so,mkdirSync as ro,readFileSync as io,writeFileSync as co}from"node:fs";import{join as go}from"node:path";import{homedir as yo}from"node:os";const mo="# ICOA exam interactive startup — auto-loaded by PYTHONSTARTUP\nimport base64, struct, hashlib, re, json, os, sys, binascii\ntry: import requests\nexcept ImportError: pass\ntry: from Crypto.Cipher import AES\nexcept ImportError: pass\ntry: from Crypto.Util.Padding import pad, unpad\nexcept ImportError: pass\ntry: from pwn import xor, p32, u32, p64, u64\nexcept ImportError: pass\ntry: import bs4\nexcept ImportError: pass\ntry: import numpy as np\nexcept ImportError: pass\ntry: from PIL import Image\nexcept ImportError: pass\n",po=Date.now();function uo(){return K()?chalk.yellow("icoa 2026> "):P()?chalk.cyan("icoa exam> "):function(){const o=F();return!(!o||"demo-free"!==o.session?.examId)&&new Date(o.session.startedAt||0).getTime()>=po}()?chalk.yellow("icoa demo> "):chalk.green("icoa> ")}const ho=go(yo(),"icoa-workspace"),wo=new Set(["sudo","su","doas","pkexec","brew","apt","apt-get","yum","choco","npm","npx","pip","pip3","shutdown","reboot","halt","mkfs","fdisk","dd","iptables","ufw"]),fo="__REPL_NO_EXIT__",bo="2.5.1";function xo(){console.log(),console.log(` ${chalk.cyan.bold("🎯 ICOA 2026 · National Selection")}`),console.log(),console.log(" "+chalk.bold.cyan("learn ")+chalk.white("ACxxxxxxxx")+chalk.gray(" → ai4ctf-12 (AI as your CTF teammate)")),console.log(" "+chalk.bold.cyan("learn ")+chalk.white("CAxxxxxxxx")+chalk.gray(" → ctf4ai-12 (red-team software AI)")),console.log(" "+chalk.bold.cyan("learn ")+chalk.white("EAxxxxxxxx")+chalk.gray(" → ctf4eai-12 (red-team embodied AI)")),console.log(" "+chalk.bold.cyan("exam ")+chalk.white("<CC>xxxxxxxx")+chalk.gray(" → Paper A/B/C/D/E (country exam token)")),console.log(" "+chalk.bold.cyan("demo")+chalk.gray(" → free practice")),console.log(" "+chalk.bold.cyan("lang")+chalk.white(" es")+chalk.gray(" → switch language")),console.log(" "+chalk.gray("help · menu · quit")),console.log()}export async function startRepl(e,F){process.env.ICOA_INSIDE_REPL="1";let po=!1;if(K()){const o=t().joinedAt,e=o=>new Intl.DateTimeFormat("en-CA",{timeZone:"Australia/Sydney",year:"numeric",month:"2-digit",day:"2-digit"}).format(o);o&&e(new Date(o))===e(new Date)||(a({ctfdUrl:"",token:"",sessionCookie:"",userId:null,userName:"",teamId:null,country:"",joinedAt:""}),po=!0)}const Co=t(),Io=n(),ko=process.exit.bind(process),To=i();if(Co.language&&"en"!==Co.language&&(await s(Co.language,{silent:!0}),r(Co.language)),Co.demoCleanedForVersion!==bo){try{const{existsSync:o,unlinkSync:e}=await import("node:fs"),{join:l}=await import("node:path"),{getIcoaDir:n}=await import("./lib/config.js"),t=l(n(),"demo-state.json");o(t)&&e(t)}catch{}a({demoCleanedForVersion:bo})}const{select:Eo,confirm:So}=await import("@inquirer/prompts"),$o=Co.mode||"",Oo=""===$o?"instant":["instant","learn","selection","olympiad"].includes($o)?$o:"selection",jo=[{name:` ${chalk.bold("⚡ Try ICOA in 30 seconds")} ${chalk.gray("—")} ${chalk.gray("watch · no token needed")}`,value:"instant"},{name:` ${chalk.bold("🧠 Learn")} ${chalk.gray("—")} ${chalk.gray("AC / CA / EA token · skill up")}`,value:"learn"},{name:` ${chalk.bold("🎯 National Selection")} ${chalk.gray("—")} ${chalk.gray("country exam token · aim for finals")}`,value:"selection"},{name:` ${chalk.bold("🏆 International Olympiad")} ${chalk.gray("—")} ${chalk.gray("finals stage · Sydney 2026 · full sandbox")}`,value:"olympiad"},{name:` ${chalk.gray("ℹ️ About ICOA")} ${chalk.gray("·")} ${chalk.gray("what this is · how to join")}`,value:"about"}];console.log(chalk.gray(" Type ")+chalk.bold.cyan("menu")+chalk.gray(" from anywhere to return here.")),console.log();let Do="";for(;!Do;){let o;try{o=await Eo({message:"Mode",choices:jo,default:Oo||"instant"})}catch(o){throw o instanceof Error&&"ExitPromptError"===o.name&&(console.log(),console.log(chalk.gray(" Goodbye.")),process.exit(0)),o}"about"!==o?Do=o:(console.clear(),console.log(),console.log(chalk.cyan(" ═══════════════════════════════════════════════════")),console.log(chalk.bold.yellow(" ICOA")+chalk.white(" — AI-Native CLI OS for Cyber & AI Security")),console.log(chalk.gray(" Olympiad & Competition · K-12 to University")),console.log(chalk.cyan(" ───────────────────────────────────────────────────")),console.log(),console.log(chalk.bold.white(" What Makes ICOA Different")),console.log(chalk.gray(" · AI-native AI teammate, AI adversary, AI translation")),console.log(chalk.gray(" · CLI OS Complete competition environment in terminal")),console.log(chalk.gray(" · 110 tools pwntools, z3, gdb, nmap, sleuthkit... pre-configured")),console.log(chalk.gray(" · Global scale 15,000+ concurrent exams · 15 languages")),console.log(),console.log(chalk.bold.white(" Competition Format (2-day standard + 1-day Pioneer)")),console.log(` ${chalk.green.bold("AI4CTF")}${chalk.gray(" [Day 1] AI as teammate — speed track, dynamic decay + top-3 bonus")}`),console.log(` ${chalk.red.bold("CTF4AI")}${chalk.gray(" [Day 2] AI as target — knowledge track, static value per challenge")}`),console.log(` ${chalk.cyan.bold("CTF4EAI")}${chalk.gray(" [Pioneer / Practical Round] Embodied AI — VLA + world models + sim-to-real")}`),console.log(),console.log(chalk.bold.white(" Scoring (in brief)")),console.log(chalk.gray(" · Standard medals: AI4CTF + CTF4AI combined · 1/12 Gold · 2/12 Silver · 3/12 Bronze")),console.log(chalk.gray(" · HMA: top 50% on either day, total below bronze cutoff")),console.log(chalk.gray(" · CTF4EAI: separate 15-finalist pool, G/S/B + 12 Finalist Awards")),console.log(),console.log(chalk.white(" Sydney, Australia")+chalk.gray(" · Jun 27 - Jul 2, 2026")),console.log(),console.log(chalk.bold.white(" Organized by")+chalk.gray(" ASRA (Australia) · ICO Foundation Inc")),console.log(chalk.bold.white(" Contact ")+chalk.cyan(" australia@icoa2026.au · accreditation@icoa2026.au")),console.log(chalk.bold.white(" Website ")+chalk.cyan.underline(" https://icoa2026.au")),console.log(chalk.cyan(" ═══════════════════════════════════════════════════")),console.log(),console.log(chalk.gray(" Press ")+chalk.yellow("Enter")+chalk.gray(" to return...")),await new Promise(o=>{const e=l=>{process.stdin.removeListener("data",e),process.stdin.isTTY&&process.stdin.setRawMode&&process.stdin.setRawMode(!1),process.stdin.pause(),o()};process.stdin.isTTY&&process.stdin.setRawMode&&process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.once("data",e)}),console.clear())}if("olympiad"===Do&&"olympiad"!==Oo){console.log(),console.log(chalk.yellow(" This mode will download ~500MB of CTF tools and AI models."));let o=!0;try{o=await So({message:"Continue?",default:!0})}catch(e){if(!(e instanceof Error&&"ExitPromptError"===e.name))throw e;o=!1}o||(Do="selection",console.log(chalk.gray(" Switched to National Selection mode.")))}if(Do!==Oo&&a({mode:Do}),console.log(),"olympiad"===Do&&p(bo)){u(bo),console.log(chalk.gray(" Checking competition environment..."));const{execSync:o}=await import("node:child_process"),e=[{name:"pwntools",cmd:'python3 -c "import pwn"'},{name:"z3-solver",cmd:'python3 -c "import z3"'},{name:"numpy",cmd:'python3 -c "import numpy"'},{name:"requests",cmd:'python3 -c "import requests"'}];let l=0;for(const n of e)try{o(n.cmd,{stdio:"ignore"})}catch{l++}l>0?(console.log(chalk.yellow(` ${l} core competition libraries missing.`)),console.log(chalk.gray(" Run ")+chalk.white("env setup")+chalk.gray(" when you are ready to install them.")),console.log()):(console.log(chalk.green(" All core libraries ready.")),console.log())}if(F){const o=d();if(o){const e=Math.floor(o.awaySeconds/60),l=o.awaySeconds%60;console.log(chalk.yellow(` Session resumed. Away: ${e}m ${l}s | Total exits: ${o.exitCount}`)),console.log()}}if("selection"===Do)console.log(chalk.gray(" ─── exam hall · 90 min per paper once started · no pause ───")),console.log(),xo();else if("embodied"===Do)!function(){console.log(chalk.cyan.bold(" [🤖 ctf4eai — Embodied AI Security]")),console.log(),console.log(chalk.bold.cyan(" CTF4EAI")+chalk.gray(" — ICOA 2026's ")+chalk.bold.white("Pioneer Round")),console.log(chalk.gray(" an independent parallel track alongside the scored main events: ")+chalk.green("AI4CTF")+chalk.gray(" (Day 1) and ")+chalk.red("CTF4AI")+chalk.gray(" (Day 2)")),console.log(),console.log(chalk.white(" Embodied AI is at its ImageNet moment.")),console.log(chalk.gray(" VLA models, MuJoCo physics, real robot arms.")),console.log(chalk.gray(" Real models, real physics, real attack surfaces.")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────"));const o=L()>0||N()>0,e=D()>0,l=j(),n=O(),t=null!==n&&"number"==typeof n.completedAt&&n.nextCardIndex>=n.totalCards,a=null!==n&&!t;if(t)console.log(chalk.bold.yellow(" Continue: ")+chalk.bold.cyan("④ learn LEARNDEMO01")+chalk.gray(" (demo2 ✓ · sim")+(e?chalk.gray(" ✓ · sim arm ✓)"):chalk.gray(")")));else if(a){const o=`${n.nextCardIndex} / ${n.totalCards}`;console.log(chalk.bold.yellow(" Continue: ")+chalk.bold.cyan("③ demo2")+chalk.gray(" (last left at card ")+chalk.bold.yellow(o)+chalk.gray(")"))}else o&&e?console.log(chalk.bold.yellow(" Next up: ")+chalk.bold.cyan("③ demo2")+chalk.gray(" → ")+chalk.bold.cyan("④ learn LEARNDEMO01")+chalk.gray(" (sim ✓ · sim arm ✓)")):l?console.log(chalk.bold.yellow(" Next up: ")+(e?chalk.bold.cyan("① sim")+chalk.gray(" → ")+chalk.bold.cyan("③ demo2"):chalk.bold.cyan("② sim arm")+chalk.gray(" → ")+chalk.bold.cyan("③ demo2"))+chalk.gray(" → ")+chalk.bold.cyan("④ learn LEARNDEMO01")):console.log(chalk.bold.yellow(" First time? Try in order: ")+chalk.bold.cyan("① sim")+chalk.gray(" → ")+chalk.bold.cyan("② sim arm")+chalk.gray(" → ")+chalk.bold.cyan("③ demo2")+chalk.gray(" → ")+chalk.bold.cyan("④ learn LEARNDEMO01"));console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.bold.green(" sim")+chalk.gray(" ")+chalk.bold.cyan("①")+chalk.gray(" ")+chalk.bold.yellow("★ INSTANT")+chalk.gray(" — 9-robot group dance, no wait, no setup")),console.log(chalk.bold.green(" sim arm")+chalk.gray(" ")+chalk.bold.cyan("②")+chalk.gray(" interactive 6-DOF arm sandbox — type joint values, watch")),console.log(chalk.bold.green(" demo2")+chalk.gray(" ")+chalk.bold.cyan("③")+chalk.gray(" ~8 min interactive intro + live MuJoCo sim")),console.log(chalk.bold.green(" learn LEARNDEMO01")+chalk.gray(" ")+chalk.bold.cyan("④")+chalk.gray(" free 11-card intro (paste this command as-is)")),console.log(chalk.bold.green(" learn ")+chalk.bold.yellow("EA")+chalk.gray(" + 8 chars full 96 / 360 curriculum (team-issued)")),console.log(chalk.bold.green(" exam ")+chalk.bold.yellow("PDxxxxxxxx")+chalk.gray(" Paper D (CTF4EAI) — national selection contestants")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(),console.log(chalk.gray(" Three open tiers: ")+chalk.green("free 11")+chalk.gray(" · ")+chalk.yellow("registered 100")+chalk.gray(" · ")+chalk.bold.magenta("national team 480")+chalk.gray(" (PhD-ready)")),console.log(chalk.gray(" Country team contact: ")+chalk.cyan("australia@icoa2026.au")),console.log(),f(),console.log()}();else if("instant"===Do)console.log(chalk.cyan.bold(" [⚡ Try ICOA in 30 seconds — no token needed]")),console.log(),console.log(chalk.gray(" Two free tastes — just type the word and press Enter:")),console.log(),console.log(chalk.bold.green(" demo ")+chalk.gray(" Paper A/B/C taste — 10 MCQs, no timer ")+chalk.bold.cyan("🎯 selection-style")),console.log(chalk.bold.green(" demo2")+chalk.gray(" Embodied-AI mini-demo — robots & MuJoCo ")+chalk.bold.yellow("🤖 hands-on")),console.log(),console.log(chalk.gray(" Type ")+chalk.bold.cyan("demo")+chalk.gray(" or ")+chalk.bold.cyan("demo2")+chalk.gray(" to start · ")+chalk.bold.cyan("menu")+chalk.gray(" to go back · ")+chalk.bold.cyan("quit")+chalk.gray(" to exit")),console.log(),a({mode:"selection"}),Do="selection";else{if("learn"===Do){console.log(chalk.cyan.bold(" [🧠 Learn]")),console.log(),console.log(chalk.gray(" Three tracks · same difficulty curve.")),console.log(),console.log(chalk.white(" Free 12-card demos — paste the full command:")),console.log(),console.log(chalk.gray(" 🤝 ai4ctf AI as your teammate ")+chalk.bold.yellow("learn AI4CTFDEMO01")),console.log(chalk.gray(" 🔓 ctf4ai AI as your target ")+chalk.bold.yellow("learn CTF4AIDEMO01")),console.log(chalk.gray(" 🤖 ctf4eai Embodied AI Security ")+chalk.bold.yellow("learn LEARNDEMO01")),console.log(),console.log(chalk.gray(" Got a cohort token? Type ")+chalk.bold.yellow("learn <TOKEN>")+chalk.gray(" (e.g. ")+chalk.bold.yellow("learn EAABCD1234")+chalk.gray(").")),console.log();try{const{input:o}=await import("@inquirer/prompts"),{resolveLearnInput:l}=await import("./lib/learn-input.js");console.log(chalk.gray(" Enter a token (or just press Enter to go back).")),console.log();const n=l(await o({message:chalk.bold.cyan("icoa learn>"),theme:{prefix:"",style:{message:o=>o}}}));if("token"===n.kind){console.log(),console.log(chalk.gray(" Opening ")+chalk.bold.cyan("learn")+chalk.gray(`: ${n.token}`)),console.log(),a({mode:"selection"}),process.env.ICOA_LEARN_EMBEDDED="1";try{await e.parseAsync(["node","icoa","learn",n.token])}finally{delete process.env.ICOA_LEARN_EMBEDDED}}else"ambiguous"!==n.kind&&"unknown"!==n.kind||(console.log(),console.log(chalk.yellow(" "+n.hint)),console.log(chalk.gray(" Returning to main menu — pick 🧠 Learn again to retry.")),console.log())}catch{}return a({mode:"selection"}),void w()}"organizer"===Do?(console.log(chalk.yellow.bold(" [National/Regional Partner]")),console.log(),console.log(chalk.bold.white(" ██╗ ██████╗ ██████╗ █████╗")),console.log(chalk.bold.white(" ██║██╔════╝██╔═══██╗██╔══██╗")),console.log(chalk.bold.white(" ██║██║ ██║ ██║███████║")),console.log(chalk.bold.white(" ██║██║ ██║ ██║██╔══██║")),console.log(chalk.bold.white(" ██║╚██████╗╚██████╔╝██║ ██║")),console.log(chalk.bold.white(" ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝")),console.log(),console.log(chalk.yellow(" International Cyber Olympiad in AI 2026")),console.log(chalk.bold.magenta(" The World's First AI-Native CLI Operating System")),console.log(chalk.bold.magenta(" for Cybersecurity & AI Security Competition")),console.log(chalk.bold.magenta(" and Olympiad for K-12")),console.log(chalk.gray(" Sydney, Australia · Jun 27 - Jul 2, 2026")),console.log(),console.log(chalk.white(" Vision")),console.log(chalk.gray(" Building a global pipeline for youth cyber & AI")),console.log(chalk.gray(" security talent through education and competition.")),console.log(),console.log(chalk.white(" Capacity")),console.log(chalk.gray(" 15,000+ concurrent online examinations")),console.log(chalk.gray(" National selection, training, and education support")),console.log(),console.log(chalk.white(" Olympic Spirit")),console.log(chalk.gray(" Excellence · Friendship · Respect")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" New country accreditation & support:")),console.log(chalk.cyan(" australia@icoa2026.au")),console.log(chalk.cyan(" accreditation@icoa2026.au")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(),Io?(console.log(chalk.green(` Logged in as ${Co.userName}`)),console.log(chalk.white(" exam list")+chalk.gray(" Manage exams")),console.log(chalk.white(" logout")+chalk.gray(" Disconnect"))):console.log(chalk.white(" join <url>")+chalk.gray(" Connect to manage exams")),console.log(),f(),console.log()):(console.log(` ${chalk.cyan.bold("🏆 ICOA 2026 · International Olympiad")}`),console.log(),To&&!y()?(console.log(chalk.red(" Token was activated on a different device.")),console.log(chalk.gray(" Contact organizer for assistance.")),console.log()):Io?(console.log(chalk.green.bold(` Welcome back, ${Co.userName}!`)),console.log(chalk.gray(` Connected to ${Co.ctfdUrl}`)),console.log(chalk.gray(" Not you? Type ")+chalk.cyan("logout")+chalk.gray(" to switch accounts")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" Ready to compete? Start here:")),console.log(),console.log(chalk.bold.cyan(" challenges")+chalk.gray(" Browse challenges by category")),console.log(chalk.white(" status")+chalk.gray(" Your score & timer")),console.log(chalk.white(" scoreboard")+chalk.gray(" Live rankings")),console.log(chalk.white(" memo")+chalk.gray(" Report an issue to organizers (one-way)")),console.log(),console.log(chalk.gray(" Tool environment:")),console.log(chalk.white(" env")+chalk.gray(" See which of the 110 CTF tools are installed")),console.log(chalk.white(" env setup")+chalk.gray(" Install anything missing (~5 min, one-time)")),console.log(chalk.bold.yellow(" ★ ")+chalk.bold.cyan("man <tool>")+chalk.gray(" How to USE a tool (steghide, nmap…) · ")+chalk.cyan("man tools")+chalk.gray(" = list")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Tip: ")+chalk.cyan("help")+chalk.gray(" · ")+b()+chalk.gray(" · ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+chalk.cyan("quit")+chalk.gray(" closes")),console.log()):To?(so(ho)||ro(ho,{recursive:!0}),console.log(chalk.green.bold(" Welcome, competitor!")),console.log(chalk.gray(` Workspace: ${ho}`)),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" Get started:")),console.log(),console.log(chalk.white(" Step 1 ")+chalk.bold.cyan("join <url>")+chalk.gray(" Connect to competition server")),console.log(chalk.white(" Step 2 ")+chalk.bold.cyan("challenges")+chalk.gray(" Browse & solve challenges")),console.log(chalk.white(" Step 3 ")+chalk.bold.cyan("ai4ctf")+chalk.gray(" Ask AI when stuck")),console.log(),console.log(chalk.gray(" Before Step 1 — make sure your tools are ready:")),console.log(chalk.white(" env")+chalk.gray(" See which of the 110 CTF tools are installed")),console.log(chalk.white(" env setup")+chalk.gray(" Install anything missing (~5 min, one-time)")),console.log(chalk.bold.yellow(" ★ ")+chalk.bold.cyan("man <tool>")+chalk.gray(" How to USE a tool (steghide, nmap…) · ")+chalk.cyan("man tools")+chalk.gray(" = list")),console.log(),console.log(chalk.gray(" Also: ")+chalk.white("help")+chalk.gray(" all commands")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Tip: ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+b()+chalk.gray(" · ")+chalk.cyan("quit")+chalk.gray(" closes CLI")),console.log()):(console.log(chalk.bold.yellow(" 🏆 You reached the finals tier.")),console.log(chalk.gray(" Full sandbox · 110 CTF tools · Sydney 2026 · this is where finalists compete.")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" To begin, activate your competition token:")),console.log(),console.log(chalk.bold.cyan(" activate <token>")),console.log(),console.log(chalk.gray(" While waiting, explore:")),console.log(chalk.white(" ref linux")+chalk.gray(" Quick reference for Linux")),console.log(chalk.white(" ref web")+chalk.gray(" Quick reference for Web")),console.log(chalk.white(" env")+chalk.gray(" See which of the 110 CTF tools are installed")),console.log(chalk.white(" env setup")+chalk.gray(" Install anything missing (~5 min, one-time)")),console.log(chalk.white(" help")+chalk.gray(" All available commands")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Tip: ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+b()+chalk.gray(" · ")+chalk.cyan("quit")+chalk.gray(" closes CLI")),console.log()))}e.exitOverride(),e.configureOutput({writeErr:()=>{},writeOut:o=>{console.log(o)}});const Lo=o({input:process.stdin,output:process.stdout,prompt:uo(),terminal:!0});x(Lo);let No=!1;if(h(!0),Y(),po&&(console.log(),console.log(chalk.yellow(" ⏱ A new competition day has started (Sydney time).")),console.log(chalk.gray(" For fairness, yesterday’s sign-in has been cleared. Type ")+chalk.cyan("join <url>")+chalk.gray(" to sign in with your account.")),console.log()),K()){const o=t().userName||"this account";console.log(),console.log(chalk.yellow(" ⚠ This machine is signed in as ")+chalk.bold.white(o)+chalk.yellow(".")),console.log(chalk.gray(" Press ")+chalk.cyan("Enter")+chalk.gray(` to continue as ${o}, or type `)+chalk.cyan("logout")+chalk.gray(" to switch accounts.")),"logout"===(await new Promise(o=>{Lo.question(chalk.white(" > "),e=>o(e.trim()))})).toLowerCase()&&(a({ctfdUrl:"",token:"",sessionCookie:"",userId:null,userName:"",teamId:null,country:""}),console.log(),console.log(chalk.green(" ✓ Logged out. ")+chalk.gray("Run ")+chalk.cyan("join <url>")+chalk.gray(" to sign in with your own account."))),console.log()}Z()&&V(()=>Lo.prompt());const Ro=Lo.prompt.bind(Lo);Lo.prompt=o=>{A()||k()||E()||$()||no()||Lo.setPrompt(uo()),Ro(o)},Lo.prompt(),Lo.on("line",async o=>{if(No)return;const n=o.trim();if(!n)return Lo.setPrompt(A()?chalk.magenta("icoa ai4ctf> "):uo()),void Lo.prompt();if("menu"===n||"menu confirm"===n)return P()&&"menu confirm"!==n?(console.log(),console.log(chalk.yellow(" Real exam active. Progress is auto-saved.")),console.log(chalk.gray(" To return to main menu anyway, type ")+chalk.white("menu confirm")),console.log(),void Lo.prompt()):void w(Lo);if(A()){No=!0;const o=await C(n);return No=!1,"exit"===o&&Lo.setPrompt(uo()),void Lo.prompt()}if(k()){No=!0;const o=await T(n);return No=!1,"exit"!==o&&"solved"!==o||Lo.setPrompt(uo()),void Lo.prompt()}if(E()){No=!0;const o=await S(n);return No=!1,"exit"===o&&Lo.setPrompt(uo()),Lo.setPrompt(E()?chalk.bold.cyan("icoa ctf4eai> "):uo()),void Lo.prompt()}if(X(n),"exit"===n)return R()?(console.log(),console.log(chalk.yellow(" ⚠ An exam is in progress.")),console.log(chalk.white(" To return to menu without losing progress, type: ")+chalk.bold.cyan("back")),console.log(chalk.white(" To fully close ICOA CLI, type: ")+chalk.bold.cyan("quit")),console.log(chalk.gray(" Your progress is auto-saved either way.")),console.log(),void Lo.prompt()):(console.log(),console.log(chalk.gray(" ")+chalk.white("exit")+chalk.gray(" returns to the main menu. To fully close ICOA CLI, type ")+chalk.bold.cyan("quit")+chalk.gray(".")),"selection"===Do&&xo(),void Lo.prompt());if("quit"===n||"q"===n||"quit confirm"===n){const o=R();return o&&"demo-free"!==o.session.examId&&"quit confirm"!==n?(console.log(),console.log(chalk.yellow(" ⚠ A real exam is in progress.")),console.log(chalk.gray(" Your answers are auto-saved on the server, but the exam timer keeps ticking")),console.log(chalk.gray(" on the server side even if you close the CLI.")),console.log(),console.log(chalk.white(" To leave the CLI but keep the exam alive, type: ")+chalk.bold.cyan("back")),console.log(chalk.gray(" (recommended — you can resume with ")+chalk.cyan("exam q 1")+chalk.gray(" after relaunching icoa)")),console.log(),console.log(chalk.white(" To really close ICOA CLI, type: ")+chalk.bold.cyan("quit confirm")),console.log(),void Lo.prompt()):(o&&"demo-free"===o.session.examId&&(console.log(),console.log(chalk.gray(" Demo paused. Resume with: ")+chalk.white("demo")+chalk.gray(" (fresh) or ")+chalk.white("exam q 1")+chalk.gray(" (continue)."))),J(),G(),m(),console.log(chalk.gray(" Session saved. Use ")+chalk.white("icoa --resume")+chalk.gray(" to continue.")),M(),void ko(0))}if("back"===n){const o=R(),e=o&&"demo-free"!==o.session.examId,l=o&&"demo-free"===o.session.examId&&(()=>{const e=new Date(o.session.startedAt||0).getTime();return Date.now()-e<18e5})();if(e)console.log(),console.log(chalk.gray(" Exam paused. Your progress is saved.")),console.log(chalk.white(" Resume: exam q 1")+chalk.gray(" · ")+chalk.white("exam review")+chalk.gray(" · ")+chalk.white("exam submit")),console.log();else if(l){const e=Object.keys(o.answers).length,l=o.session.questionCount;console.log(),console.log(chalk.gray(` Demo paused (${e}/${l} answered). Resume with: `)+chalk.white("exam q 1")),console.log(chalk.gray(" Or type ")+chalk.white("demo")+chalk.gray(" to restart.")),console.log()}else{if(o&&"demo-free"===o.session.examId){const{clearExamState:o}=await import("./lib/exam-state.js");o("demo-free")}const e=t();fetch("https://practice.icoa2026.au/api/icoa/demo-stats",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"post-report-back",lang:e.language||"en",timestamp:(new Date).toISOString()}),signal:AbortSignal.timeout(5e3)}).catch(()=>{}),"selection"===Do?xo():console.log(chalk.gray(" Already at main menu."))}return void Lo.prompt()}if("help"===n||"?"===n){if(R()){No=!0;try{await e.parseAsync(["node","icoa","exam","help"])}catch{}return No=!1,void Lo.prompt()}return Ao(i(),Do),void Lo.prompt()}if("man"===n||"manual"===n)return Ao(i(),Do),console.log(),console.log(chalk.bold.yellow(" ★ Stuck on a tool? ")+chalk.bold.cyan("man <tool>")+chalk.gray(" e.g. ")+chalk.cyan("man steghide")+chalk.gray(" · ")+chalk.cyan("man tools")+chalk.gray(" = full list")),void Lo.prompt();if(/^(?:man|manual)\s+\S/i.test(n))return I(n.replace(/^(?:man|manual)\s+/i,"")),void Lo.prompt();if("more help"===n.toLowerCase()&&R()){No=!0;try{await e.parseAsync(["node","icoa","exam","more-help"])}catch{}return No=!1,void Lo.prompt()}if("continue"===n.toLowerCase())return console.log(),console.log(chalk.green.bold(" ═══ AI4CTF — AI as Your Teammate ═══")),console.log(),console.log(chalk.white(" In AI4CTF, you solve cybersecurity challenges")),console.log(chalk.white(" with AI by your side.")),console.log(),console.log(chalk.white(" In this demo, your AI teammate gives help at 3 levels:")),console.log(chalk.yellow(" hint a")+chalk.gray(" General guidance")),console.log(chalk.yellow(" hint b")+chalk.gray(" Deep analysis")),console.log(chalk.yellow(" hint c")+chalk.gray(" Critical assist")),console.log(),console.log(chalk.white(" Try it now! Type: ")+chalk.bold.green("ai4ctf")),console.log(chalk.gray(' Chat freely with your AI teammate. Type "exit" when done.')),console.log(),console.log(chalk.gray(" After ai4ctf, try: ")+chalk.bold.red("ctf4ai")+chalk.gray(' — trick the AI into saying "koala"')),console.log(),void Lo.prompt();if(/^ICOA-[A-Z]{2,3}-\d{1,6}$/i.test(n.trim())){No=!0;try{await e.parseAsync(["node","icoa","exam","token",n.trim()])}catch{}return No=!1,void Lo.prompt()}if(/^[A-Z]{2}[0-9A-HJKMNP-TV-Z]{8}$/i.test(n.trim())){No=!0;try{await e.parseAsync(["node","icoa","exam","token",n.trim().toUpperCase()])}catch{}return No=!1,void Lo.prompt()}const a=n.match(/^exam\s+([A-Z]{2}[0-9A-HJKMNP-TV-Z]{8})$/i);if(a){No=!0;try{await e.parseAsync(["node","icoa","exam","token",a[1].toUpperCase()])}catch{}return No=!1,void Lo.prompt()}const s=n.match(/^exam\s+([A-Z]{2,3})$/i);if(s){No=!0;try{await e.parseAsync(["node","icoa","exam","list",s[1]])}catch{}return No=!1,void Lo.prompt()}if("clear"===n||"cls"===n)return console.clear(),void Lo.prompt();if("news"===n)return Z()?H():console.log(chalk.gray(" No competition joined. Use ")+chalk.white("join <url>")+chalk.gray(".")),void Lo.prompt();if("memo"===n){if(!Z())return console.log(chalk.gray(" Join a competition first: ")+chalk.white("join <url>")),void Lo.prompt();No=!0;try{await to()}catch{}return void(No=!1)}const r=eo()||oo(t().userName||"");if("jury"===n&&r){No=!0;try{await ao()}catch{}return void(No=!1)}if(r&&eo()&&"scoreboard"===n.split(/\s+/)[0]){No=!0;try{const o=await lo();if(console.log(),0===o.length)console.log(chalk.gray(" Scoreboard is empty."));else{console.log(chalk.bold.white(" Scoreboard"));for(const e of o.slice(0,50))console.log(chalk.gray(` ${String(e.pos).padStart(3)}. `)+chalk.white(e.name.padEnd(28).slice(0,28))+chalk.cyan(String(e.score)))}console.log()}catch{}return No=!1,void Lo.prompt()}if(r&&K()){const o=n.split(/\s+/)[0];if(["challenges","open","submit","hint"].includes(o))return console.log(),console.log(chalk.yellow(" Team-leader account — the challenge bracket is not available here.")),console.log(chalk.gray(" Available: ")+chalk.white("scoreboard")+chalk.gray(" · ")+chalk.white("news")+chalk.gray(" · ")+chalk.white("jury")+chalk.gray(" · ")+chalk.white("time")+chalk.gray(" · ")+chalk.white("status")+chalk.gray(" · ")+chalk.white("logout")),console.log(),void Lo.prompt()}if(n.startsWith("activate ")){const o=n.slice(9).trim(),e=c(o);return"ok"===e?console.log(chalk.green(" Access granted! Token bound to this device.")):"already_bound"===e?(console.log(),console.log(chalk.red(" Token already activated on a different device.")),console.log(chalk.gray(" Each token binds to the first device that uses it. If you lost the device,")),console.log(chalk.gray(" contact your proctor to have the token re-issued for a new device."))):(console.log(),console.log(chalk.red(" Token not recognized.")),console.log(chalk.gray(" Possible reasons:")),console.log(chalk.white(" • ")+chalk.gray("Typo — access tokens are case-insensitive and look like ")+chalk.cyan("ICOA-XXXX-XXXX-XXXX")+chalk.gray(" (4-char hex groups, e.g. ICOA-385E-27F2-B21B)")),console.log(chalk.white(" • ")+chalk.gray("Expired — ask your proctor or organizer for a fresh token")),console.log(chalk.white(" • ")+chalk.gray("Network — verify connection to ")+chalk.cyan("practice.icoa2026.au")),console.log(chalk.gray(" Still stuck? type ")+chalk.cyan("help")+chalk.gray(" or try ")+chalk.cyan("exam demo")+chalk.gray(" for a free practice round."))),console.log(),void Lo.prompt()}if("activate"===n)return console.log(chalk.gray(" Usage: ")+chalk.white("activate <token>")),console.log(),void Lo.prompt();const d=R();if(d){const o=n.toUpperCase().trim(),l=o=>{const e=d.questions.find(e=>e.number===o);return!!e&&("ai4ctf"===e.type||"ctf4ai"===e.type||e.options&&!e.options.A&&!e.options.B)},t=o=>{const e=d.questions.find(e=>e.number===o);return"short_answer"===e?.type},a=o=>{const e="demo-free"!==d.session.examId,l=d.questions.find(e=>e.number===o),n=q(l?.type,e);console.log(),console.log(chalk.yellow(` Q${o} is a practical question — letters (A/B/C/D) don't apply here.`)),n?(console.log(chalk.white(" Enter the AI chat for this question: ")+chalk.bold.cyan(n)),console.log(chalk.gray(" Or submit a flag directly: ")+chalk.green(`exam answer ${o} ICOA{your_flag}`))):console.log(chalk.gray(" Submit a flag: ")+chalk.green(`exam answer ${o} ICOA{your_flag}`)),console.log()};if(/^[ABCDE]$/.test(o)){const n=d._lastQ||1;if(l(n))return a(n),void Lo.prompt();if(t(n))return console.log(),console.log(chalk.yellow(` Q${n} is a short-answer question — type a number, not a letter.`)),console.log(chalk.gray(" Example: ")+chalk.green("42")+chalk.gray(" (or ")+chalk.green(`exam answer ${n} 42`)+chalk.gray(")")),console.log(),void Lo.prompt();No=!0;try{await e.parseAsync(["node","icoa","exam","answer",String(n),o])}catch{}return No=!1,void Lo.prompt()}if(/^-?\d+$/.test(o)){const l=d._lastQ||1;if(t(l)){No=!0;try{await e.parseAsync(["node","icoa","exam","answer",String(l),o])}catch{}return No=!1,void Lo.prompt()}}const s=o.match(/^(\d+)\s+([ABCDE]|-?\d+)$/);if(s){const o=parseInt(s[1],10);if(l(o))return a(o),void Lo.prompt();No=!0;try{await e.parseAsync(["node","icoa","exam","answer",s[1],s[2]])}catch{}return No=!1,void Lo.prompt()}}const p=n.split(/\s+/)[0].toLowerCase(),u=/^python3?(\.\d+)?$/.test(p),h="ls"===p||"pwd"===p,f=n.startsWith("!")||p.startsWith("!")||u||h,b=new Set(["nano","vi","vim","emacs","pico","ed","code","cat","less","more","head","tail","ls","pwd","cd","base64","xxd","hexdump","od","openssl","md5sum","sha256sum","grep","find","awk","sed","strings","file","curl","wget","nc","ping","nmap","echo","bash","sh","zsh","pip","pip3","node","npm"]),x=()=>{console.log(chalk.gray(" Shell commands need ")+chalk.bold.cyan("!")+chalk.gray(" prefix. Try: ")+chalk.bold.cyan(`!${n}`)),console.log()};if("embodied"===Do&&!f&&!["demo2","learn","exam","ctf4eai","ctf4vla","eai","arena","lang","ref","setup","env","aienv","ipynb","sim"].includes(p))return b.has(p)?(x(),void Lo.prompt()):(console.log(chalk.gray(" Not available in Embodied AI Security mode.")),console.log(chalk.white(" Try ")+chalk.bold.cyan("demo2")+chalk.gray(" (intro) · ")+chalk.bold.cyan("learn")+chalk.gray(" (curriculum) · ")+chalk.bold.cyan("exam <PD-token>")+chalk.gray(" (Paper D)")),console.log(),void Lo.prompt());if("selection"===Do&&!f&&!["exam","demo","demo2","retry","nations","next","prev","continue","setup","lang","ref","ai4ctf","ctf4ai","mark","unmark","review","submit","env","aienv","ipynb","sim"].includes(p)){if(b.has(p))return x(),void Lo.prompt();if(console.log(chalk.gray(" Not available in Selection mode.")),d){const o=d._lastQ||1;console.log(chalk.white(` Resume exam: exam q ${o}`)+chalk.gray(" · ")+chalk.white("A/B/C/D")+chalk.gray(" to answer"))}else console.log(chalk.gray(" Try: demo · setup to switch mode"));return console.log(),void Lo.prompt()}if("organizer"===Do&&!["join","exam","demo","retry","next","prev","logout","setup","lang","ref","ctf","mark","unmark","review","submit"].includes(p))return console.log(chalk.gray(" Not available in Organizer mode. Switch via: setup")),console.log(),void Lo.prompt();if(!("olympiad"!==Do||i()&&y()||g(p)))return console.log(chalk.yellow(" Restricted mode. ")+chalk.gray("Enter your access token:")),console.log(chalk.white(" activate <token>")),console.log(),console.log(chalk.gray(" Free commands: ")+chalk.white("ref [topic]")+chalk.gray(", ")+chalk.white("help")+chalk.gray(", ")+chalk.white("exit")),console.log(),void Lo.prompt();if(!["join","activate","challenges","ch","open","submit","flag","scoreboard","sb","status","time","ref","shell","files","view","connect","challenge","note","notes","logbook","log","man","manual","lang","setup","env","aienv","ipynb","ai4ctf","model","ctf","exam","demo","retry","nations","next","prev","continue","logout","ctf4ai","mark","unmark","review","submit","learn","demo2","ctf4eai","ctf4vla","theme","sim","arena","eai","learn-pull"].includes(p)){if(wo.has(p))return console.log(chalk.yellow(` "${p}" is a shell command — the ICOA prompt isn't a shell.`)),console.log(chalk.gray(" In real-exam mode, prefix with ! to run inside the sandbox: ")+chalk.cyan(`!${n}`)),console.log(),void Lo.prompt();if(/(?:^|\s)(?:\/(?!home\/|Users\/|tmp\/)|\.\.\/|~\/)/.test(n)&&!n.startsWith("cd ")){const o=/(?:^|\s)\/(?!home\/\w+\/icoa-workspace|Users\/\w+\/icoa-workspace|tmp\/)/.test(n),e=/\.\./.test(n);if(o||e)return console.log(chalk.red(" Blocked: access outside workspace is not allowed.")),console.log(chalk.gray(` Workspace: ${ho}`)),console.log(),void Lo.prompt()}let o=n.startsWith("!")?n.slice(1).trim():n;if("darwin"===process.platform){const e="/opt/homebrew/opt/python@3.12/bin/python3.12";o=o.replace(/^python3?\s/,`${e} `).replace(/^(python3|python)$/,e)}else if("win32"===process.platform){const e=(()=>{try{return l("py -3 --version",{stdio:["ignore","ignore","ignore"],timeout:1500}),"py -3"}catch{}return"python"})();o=o.replace(/^python3?(\.\d+)?\s/,`${e} `).replace(/^python3?(\.\d+)?$/,e)}else{const e=(()=>{try{return l("which python3.12",{stdio:"ignore"}),"python3.12"}catch{return"python3"}})();o=o.replace(/^python\s/,`${e} `).replace(/^python$/,e)}const e=W(),t=Q(n);z({cwd:e,input:n,riskFlags:t}),t.length>0&&P()&&(console.log(chalk.yellow(` ⚠ Action flagged: ${t.join(", ")} — logged.`)),console.log()),/^(\S*python3?(\.\d+)?)\s*$/.test(o)&&(o=`PYTHONSTARTUP="${function(){const o=go(yo(),".icoa");so(o)||ro(o,{recursive:!0});const e=go(o,"python-startup.py");let l="";if(so(e))try{l=io(e,"utf-8")}catch{}return l!==mo&&co(e,mo),e}()}" ${o}`,console.log(),console.log(chalk.cyan(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(chalk.bold.white(" Python ready — ICOA exam toolkit pre-loaded")),console.log(chalk.cyan(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(),console.log(chalk.white(" Already imported: ")+chalk.gray("base64, struct, hashlib, re, json, binascii")),console.log(chalk.white(" Also available: ")+chalk.gray("requests, bs4, numpy, PIL.Image, AES, pad/unpad, xor, p32/u32/p64/u64")),console.log(),console.log(chalk.yellow(" Quick examples:")),console.log(chalk.gray(' base64.b64decode("aGVsbG8=") ')+chalk.gray("# decode base64")),console.log(chalk.gray(' bytes.fromhex("48656c6c6f") ')+chalk.gray("# hex → bytes")),console.log(chalk.gray(' "ICOA{x}".encode() ')+chalk.gray("# str → bytes")),console.log(chalk.gray(" [chr(c) for c in [73,67,79,65]] ")+chalk.gray("# ASCII codes")),console.log(chalk.gray(' xor(bytes.fromhex("0a2b"), b"IC") ')+chalk.gray("# pwntools XOR")),console.log(),console.log(chalk.gray(" Exit: ")+chalk.white("exit()")+chalk.gray(" or Ctrl-D")),console.log(chalk.cyan(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log()),No=!0;try{_()&&await B()?await U(o,Lo):await vo(o,Lo,e)}catch{console.log(chalk.yellow(` Command failed: ${p}`))}return No=!1,console.log(),void Lo.prompt()}No=!0;const $=n.trim(),O=$.toLowerCase();let j,D=null,L="";if(d)if("submit"===O)D="final";else if(O.startsWith("submit ")){let o=$.slice(7).trim();/^submit\s+/i.test(o)&&(o=o.replace(/^submit\s+/i,"").trim()),o=o.replace(/^["'`]+|["'`]+$/g,"").trim(),o&&/^ICOA\{[^}]*\}?$/i.test(o)?(D="flag",L=o):o&&/^[ABCDE]$/i.test(o)&&(D="flag",L=o.toUpperCase())}else/^ICOA\{[^}]+\}$/i.test($)&&(D="flag",L=$);j="final"===D?["exam","submit"]:"flag"===D?["exam","answer",String(d?._lastQ||1),L]:function(o){const e=o.split(/\s+/),l=e[0].toLowerCase(),n=e.slice(1);if("aienv"===l&&["run","python","py"].includes((n[0]??"").toLowerCase()))return v(o);if(("submit"===l||"flag"===l)&&n.length>=2)return["ctf","submit",n[0],n.slice(1).join(" ").trim().replace(/^["'`]+|["'`]+$/g,"")];if("connect"===l&&n.length>=2){const e=v(o);return["connect",e[1]??n[0],e.slice(2).join(" ")]}const t={demo:["exam","demo"],retry:["exam","demo-retry"],nations:["exam","nations"],next:["exam","next"],prev:["exam","prev"],mark:["exam","mark",...n],unmark:["exam","unmark",...n],review:["exam","review"],logout:["ctf","logout"],join:["ctf","join",...n],activate:["ctf","activate",...n],challenges:["ctf","challenges"],ch:["ctf","challenges"],open:["ctf","open",...n],submit:["ctf","submit",...n],flag:["ctf","submit",...n],scoreboard:["ctf","scoreboard",...n],sb:["ctf","scoreboard",...n],status:["ctf","status"],time:["ctf","time"]};return t[l]?t[l]:["ref","shell","files","view","connect","challenge","note","notes","logbook","log","man","manual","lang","setup","env","aienv","ipynb","ai4ctf","model","ctf","exam","ctf4ai"].includes(l)?[l,...n]:e}(n);const N="ctf"===j[0]&&"join"===j[1];N&&Lo.pause(),process.exit=()=>{throw new Error(fo)};try{await e.parseAsync(["node","icoa",...j])}catch(o){const e=o instanceof Error?o.message:String(o);if(e===fo);else if(e.includes("commander.unknownCommand")){const{distance:o}=await import("fastest-levenshtein"),e=["ctf","ref","shell","files","view","connect","challenge","note","notes","logbook","log","manual","lang","setup","env","aienv","ipynb","ai4ctf","exam","ctf4ai","theme","eai","clear","cls","quit","exit","back","menu","help","continue","activate","demo","challenges","status","scoreboard","join","logout"],l=p.split(/\s+/)[0]||p;let n={word:"",dist:1/0};for(const t of e){const e=o(l.toLowerCase(),t);e<n.dist&&(n={word:t,dist:e})}console.log(chalk.yellow(` Unknown command: ${p}.`)),n.dist>0&&n.dist<=2&&console.log(chalk.gray(" Did you mean: ")+chalk.bold.cyan(n.word)+chalk.gray("?")),console.log(chalk.gray(" Type ")+chalk.cyan("help")+chalk.gray(" for the full command list."))}else e.includes("commander.missingArgument")||"commander.missingArgument"===o?.code?"submit"===p||"flag"===p?(console.log(chalk.yellow(" Incomplete — usage: ")+chalk.cyan("submit <challenge#> <flag>")),console.log(chalk.gray(" Put the number AND the flag on one line, e.g. ")+chalk.cyan("submit 44 pecan{...}"))):(console.log(chalk.yellow(` Incomplete command: ${p}`)),console.log(chalk.gray(" Type ")+chalk.cyan("help")+chalk.gray(" for usage."))):e.includes("commander.")||(e.includes("fetch failed")||e.includes("ECONNREFUSED")||e.includes("ETIMEDOUT"))&&console.log(chalk.yellow(" Network error. Check your connection."))}finally{process.exit=ko,No=!1,N&&Lo.resume()}A()?Lo.setPrompt(chalk.magenta("icoa ai4ctf> ")):k()?Lo.setPrompt(chalk.red("icoa ctf4ai> ")):E()&&Lo.setPrompt(chalk.bold.cyan("icoa ctf4eai> ")),console.log(),Lo.prompt()}),Lo.on("SIGINT",()=>{if(console.log(),A()||k())console.log(chalk.yellow(" Ctrl+C did not close ICOA CLI — you are still in the AI chat.")),console.log(chalk.white(" Type ")+chalk.bold.cyan("exit")+chalk.white(" to leave the chat and return to the menu."));else if(R()){const o="demo-free"!==R().session.examId;console.log(chalk.yellow(" Ctrl+C did NOT close ICOA CLI.")),console.log(chalk.gray(` Your ${o?"exam":"demo"} is paused and every answer is auto-saved.`)),console.log(),console.log(chalk.white(" Resume: ")+chalk.cyan("exam q 1")+chalk.gray(" · Back to menu: ")+chalk.cyan("back")+chalk.gray(" · Close CLI: ")+chalk.cyan(o?"quit confirm":"quit"))}else console.log(chalk.yellow(" Ctrl+C did not close ICOA CLI — you are still at the ")+chalk.cyan("icoa>")+chalk.yellow(" prompt.")),console.log(chalk.gray(" Keep typing — ")+chalk.cyan("help")+chalk.gray(" lists commands. (Only ")+chalk.cyan("quit")+chalk.gray(" or Ctrl+D actually close the CLI.)"));console.log(),Lo.prompt()}),Lo.on("close",()=>{J(),G(),m(),M(),ko(0)})}function vo(o,l,n){return new Promise(t=>{const a=process.stdin,s=!!a.isTTY&&!!a.isRaw;if(l.pause(),a.isTTY&&"function"==typeof a.setRawMode)try{a.setRawMode(!1)}catch{}const r=e(o,{shell:!0,stdio:"inherit",cwd:n||process.cwd()}),i=()=>{if(a.isTTY&&"function"==typeof a.setRawMode&&s)try{a.setRawMode(!0)}catch{}l.resume(),t()};r.on("close",i),r.on("error",i)})}function Ao(o,e="olympiad"){return console.log(),"selection"===e||"organizer"===e?(console.log(chalk.bold.white(" Exam")),console.log(chalk.white(" join <url> ")+chalk.gray("Connect to exam server")),console.log(chalk.white(" exam list ")+chalk.gray("Available exams")),console.log(chalk.white(" exam start <id> ")+chalk.gray("Begin an exam")),console.log(chalk.white(" exam q [n] ")+chalk.gray("View questions")),console.log(chalk.white(" exam answer <n> <X> ")+chalk.gray("Answer question")),console.log(chalk.white(" exam review ")+chalk.gray("Review all answers")),console.log(chalk.white(" exam submit ")+chalk.gray("Submit for grading")),console.log(chalk.white(" exam result ")+chalk.gray("View your score")),console.log(),console.log(chalk.bold.white(" System")),console.log(chalk.white(" ref [topic] ")+chalk.gray("Quick reference")),console.log(chalk.white(" setup ")+chalk.gray("Settings / switch mode")),console.log(chalk.white(" lang [code] ")+chalk.gray("Switch language")),console.log(chalk.white(" clear ")+chalk.gray("Clear screen")),console.log(chalk.white(" exit ")+chalk.gray("Quit")),void console.log()):o?(console.log(chalk.cyan(" ═══════════════════════════════════════════════")),console.log(chalk.bold.white(" How it works")),console.log(),console.log(chalk.gray(" 1. Browse ")+chalk.white("challenges")+chalk.gray(" and pick one")),console.log(chalk.gray(" 2. ")+chalk.white("open <id>")+chalk.gray(" to read the challenge")),console.log(chalk.gray(" 3. Use ")+chalk.white("ai4ctf")+chalk.gray(" to chat with AI when stuck")),console.log(chalk.gray(" 4. ")+chalk.white("submit <id> <flag>")+chalk.gray(" to score points")),console.log(chalk.gray(" 5. Check ")+chalk.white("scoreboard")+chalk.gray(" to track your rank")),console.log(chalk.cyan(" ═══════════════════════════════════════════════")),console.log(),console.log(chalk.bold.white(" Competition")),console.log(chalk.white(" join <url> ")+chalk.gray("Connect to CTFd")),console.log(chalk.white(" challenges (ch) ")+chalk.gray("List challenges by category")),console.log(chalk.white(" open <id> ")+chalk.gray("Read challenge + get next steps")),console.log(chalk.white(" submit <id> <flag> ")+chalk.gray("Submit a flag")),console.log(chalk.white(" scoreboard (sb) ")+chalk.gray("Live rankings")),console.log(chalk.white(" status ")+chalk.gray("Your score & timer")),console.log(chalk.white(" time ")+chalk.gray("Countdown timer")),console.log(),console.log(chalk.bold.white(" AI Teammate")),console.log(chalk.white(" ai4ctf ")+chalk.gray("Free-chat with your AI teammate")),console.log(),console.log(chalk.bold.white(" Tools")),console.log(chalk.white(" ref [topic] ")+chalk.gray("Quick reference (linux, web, crypto...)")),console.log(chalk.white(" shell ")+chalk.gray("Interactive CTF shell (sandbox if present, else host)")),console.log(chalk.white(" files <id> ")+chalk.gray("Download challenge files")),console.log(chalk.white(" connect <id> ")+chalk.gray("Connect to remote target")),console.log(chalk.white(" note [text] ")+chalk.gray("Add a logbook entry (in-exam: tags current Q)")),console.log(chalk.white(" logbook [n] ")+chalk.gray("View your logbook (in-exam: filter by Q)")),console.log(chalk.white(" log ")+chalk.gray("Session history")),console.log(chalk.white(" man <tool> ")+chalk.bold.yellow("★ ")+chalk.gray("How to use a tool (steghide, nmap…); ")+chalk.cyan("man tools")+chalk.gray(" = list. Bare ")+chalk.cyan("man")+chalk.gray(" = this list.")),console.log(),console.log(chalk.bold.white(" System")),console.log(chalk.white(" setup ")+chalk.gray("Configure settings")),console.log(chalk.white(" lang [code] ")+chalk.gray("Switch language (15 supported)")),console.log(chalk.white(" logout ")+chalk.gray("Disconnect")),console.log(chalk.white(" clear ")+chalk.gray("Clear screen")),console.log(chalk.white(" exit ")+chalk.gray("Quit (session saved)")),void console.log()):(console.log(chalk.bold.yellow(" Restricted Mode — activate with a token to unlock all commands")),console.log(),console.log(chalk.white(" activate <token> ")+chalk.gray("Unlock full access")),console.log(chalk.white(" ref [topic] ")+chalk.gray("Quick reference")),console.log(chalk.white(" exit ")+chalk.gray("Quit")),void console.log())}
|