icoa-cli 2.3.2 → 2.5.2

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.
@@ -44,8 +44,8 @@ export function registerRefCommand(program) {
44
44
  const rows = files.map((f) => [chalk.white(f)]);
45
45
  printTable(['Topic'], rows);
46
46
  console.log();
47
- console.log(chalk.gray(` Usage: icoa ref <topic>`));
48
- console.log(chalk.gray(` Example: icoa ref python`));
47
+ console.log(chalk.gray(` Usage: ref <topic>`));
48
+ console.log(chalk.gray(` Example: ref python`));
49
49
  console.log();
50
50
  return;
51
51
  }
@@ -53,7 +53,7 @@ export function registerRefCommand(program) {
53
53
  const filePath = join(refsDir, `${topic}.txt`);
54
54
  if (!existsSync(filePath)) {
55
55
  printError(`Reference not found: ${topic}`);
56
- printInfo('Run "icoa ref" to see available topics.');
56
+ printInfo('Run "ref" to see available topics.');
57
57
  return;
58
58
  }
59
59
  const content = readFileSync(filePath, 'utf-8');
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { input, select } from '@inquirer/prompts';
3
- import { getConfig } from '../lib/config.js';
3
+ import { getConfig, saveConfig } from '../lib/config.js';
4
4
  import { setApiKey } from '../lib/gemini.js';
5
5
  import { printSuccess, printError, printInfo, printHeader, printKeyValue } from '../lib/ui.js';
6
6
  import { logCommand } from '../lib/logger.js';
@@ -18,11 +18,13 @@ export function registerSetupCommand(program) {
18
18
  printKeyValue('CTFd Token', config.token ? chalk.green('Configured') : chalk.gray('Not configured'));
19
19
  printKeyValue('Gemini API Key', config.geminiApiKey || process.env.GEMINI_API_KEY ? chalk.green('Configured') : chalk.gray('Not configured'));
20
20
  printKeyValue('Language', config.language);
21
+ printKeyValue('Mode', config.mode || chalk.gray('Not set'));
21
22
  printKeyValue('Session ID', config.sessionId.substring(0, 8) + '...');
22
23
  console.log();
23
24
  const action = await select({
24
25
  message: 'What would you like to configure?',
25
26
  choices: [
27
+ { name: 'Switch Mode', value: 'mode' },
26
28
  { name: 'Gemini API Key', value: 'gemini' },
27
29
  { name: 'CTFd Connection', value: 'ctfd' },
28
30
  { name: 'Reset Hint Budget', value: 'budget' },
@@ -31,6 +33,19 @@ export function registerSetupCommand(program) {
31
33
  ],
32
34
  });
33
35
  switch (action) {
36
+ case 'mode': {
37
+ const newMode = await select({
38
+ message: 'Select mode:',
39
+ choices: [
40
+ { name: 'National Selection — Exam only, lightweight', value: 'selection' },
41
+ { name: 'International Olympiad — Full CTF with AI assistance', value: 'olympiad' },
42
+ { name: 'National/Regional Partner — Organizer management', value: 'organizer' },
43
+ ],
44
+ });
45
+ saveConfig({ mode: newMode });
46
+ printSuccess(`Mode switched to: ${newMode}. Restart ICOA CLI to apply.`);
47
+ break;
48
+ }
34
49
  case 'gemini': {
35
50
  console.log();
36
51
  printInfo('Get your API key from: https://aistudio.google.com/apikey');
@@ -46,7 +61,7 @@ export function registerSetupCommand(program) {
46
61
  break;
47
62
  }
48
63
  case 'ctfd': {
49
- printInfo('Use "icoa ctf join <url>" to connect to a CTFd instance.');
64
+ printInfo('Use "join <url>" to connect to a CTFd instance.');
50
65
  break;
51
66
  }
52
67
  case 'budget': {
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { registerLangCommand } from './commands/lang.js';
13
13
  import { registerSetupCommand } from './commands/setup.js';
14
14
  import { registerEnvCommand } from './commands/env.js';
15
15
  import { registerAi4ctfCommand } from './commands/ai4ctf.js';
16
+ import { registerExamCommand } from './commands/exam.js';
16
17
  import { getConfig, saveConfig } from './lib/config.js';
17
18
  import { startRepl } from './repl.js';
18
19
  import { setTerminalTheme } from './lib/theme.js';
@@ -37,7 +38,7 @@ ${LINE}
37
38
  ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
38
39
  ${chalk.cyan.underline('https://icoa2026.au')}
39
40
 
40
- ${chalk.gray('CLI-Native Competition Terminal v2.3.2')}
41
+ ${chalk.gray('CLI-Native Competition Terminal v2.5.2')}
41
42
 
42
43
  ${LINE}
43
44
  `;
@@ -83,6 +84,7 @@ registerLangCommand(program);
83
84
  registerSetupCommand(program);
84
85
  registerEnvCommand(program);
85
86
  registerAi4ctfCommand(program);
87
+ registerExamCommand(program);
86
88
  // Hidden command: switch AI model
87
89
  program
88
90
  .command('model', { hidden: true })
@@ -0,0 +1,14 @@
1
+ import type { ExamListItem, ExamQuestion, ExamSession, ExamResult } from '../types/index.js';
2
+ export declare class ExamClient {
3
+ private baseUrl;
4
+ private token;
5
+ constructor(baseUrl: string, token: string);
6
+ private request;
7
+ getExams(): Promise<ExamListItem[]>;
8
+ startExam(examId: string): Promise<{
9
+ session: ExamSession;
10
+ questions: ExamQuestion[];
11
+ }>;
12
+ submitExam(examId: string, answers: Record<number, string>): Promise<ExamResult>;
13
+ getResult(examId: string): Promise<ExamResult>;
14
+ }
@@ -0,0 +1,41 @@
1
+ export class ExamClient {
2
+ baseUrl;
3
+ token;
4
+ constructor(baseUrl, token) {
5
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
6
+ this.token = token;
7
+ }
8
+ async request(method, path, body) {
9
+ const url = `${this.baseUrl}:9090/api/icoa/exams${path}`;
10
+ const res = await fetch(url, {
11
+ method,
12
+ headers: {
13
+ Authorization: `Token ${this.token}`,
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ body: body ? JSON.stringify(body) : undefined,
17
+ signal: AbortSignal.timeout(10000),
18
+ });
19
+ if (!res.ok) {
20
+ const text = await res.text().catch(() => 'Unknown error');
21
+ throw new Error(`Exam API error (${res.status}): ${text}`);
22
+ }
23
+ const json = await res.json();
24
+ if (json.success === false) {
25
+ throw new Error(json.message || 'Exam API error');
26
+ }
27
+ return json.data;
28
+ }
29
+ async getExams() {
30
+ return this.request('GET', '');
31
+ }
32
+ async startExam(examId) {
33
+ return this.request('POST', `/${examId}/start`);
34
+ }
35
+ async submitExam(examId, answers) {
36
+ return this.request('POST', `/${examId}/submit`, { answers });
37
+ }
38
+ async getResult(examId) {
39
+ return this.request('GET', `/${examId}/result`);
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ import type { ExamState } from '../types/index.js';
2
+ export declare function getExamState(): ExamState | null;
3
+ export declare function saveExamState(state: ExamState): void;
4
+ export declare function clearExamState(): void;
5
+ export declare function isExamActive(): boolean;
6
+ export declare function getExamDeadline(): Date | null;
@@ -0,0 +1,35 @@
1
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getIcoaDir } from './config.js';
4
+ function stateFile() {
5
+ return join(getIcoaDir(), 'exam-state.json');
6
+ }
7
+ export function getExamState() {
8
+ const f = stateFile();
9
+ if (!existsSync(f))
10
+ return null;
11
+ try {
12
+ return JSON.parse(readFileSync(f, 'utf-8'));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function saveExamState(state) {
19
+ writeFileSync(stateFile(), JSON.stringify(state, null, 2));
20
+ }
21
+ export function clearExamState() {
22
+ const f = stateFile();
23
+ if (existsSync(f))
24
+ unlinkSync(f);
25
+ }
26
+ export function isExamActive() {
27
+ return getExamState() !== null;
28
+ }
29
+ export function getExamDeadline() {
30
+ const state = getExamState();
31
+ if (!state)
32
+ return null;
33
+ const start = new Date(state.session.startedAt).getTime();
34
+ return new Date(start + state.session.durationMinutes * 60 * 1000);
35
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install script: shows a progress bar during ICOA CLI setup.
4
+ * Runs automatically after `npm install -g icoa-cli`.
5
+ */
6
+ export {};
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install script: shows a progress bar during ICOA CLI setup.
4
+ * Runs automatically after `npm install -g icoa-cli`.
5
+ */
6
+ const steps = [
7
+ 'Initializing ICOA CLI...',
8
+ 'Loading competition modules...',
9
+ 'Configuring exam system...',
10
+ 'Setting up references...',
11
+ 'Finalizing installation...',
12
+ ];
13
+ const BAR_WIDTH = 30;
14
+ const TOTAL = 100;
15
+ function drawBar(percent, label) {
16
+ const filled = Math.round((percent / TOTAL) * BAR_WIDTH);
17
+ const empty = BAR_WIDTH - filled;
18
+ const bar = '\x1b[32m' + '█'.repeat(filled) + '\x1b[90m' + '░'.repeat(empty) + '\x1b[0m';
19
+ const pct = String(percent).padStart(3) + '%';
20
+ process.stdout.write(`\r ${bar} ${pct} \x1b[90m${label}\x1b[0m`);
21
+ }
22
+ async function run() {
23
+ console.log();
24
+ console.log(' \x1b[1m\x1b[37m██╗ ██████╗ ██████╗ █████╗\x1b[0m');
25
+ console.log(' \x1b[1m\x1b[37m██║██╔════╝██╔═══██╗██╔══██╗\x1b[0m');
26
+ console.log(' \x1b[1m\x1b[37m██║██║ ██║ ██║███████║\x1b[0m');
27
+ console.log(' \x1b[1m\x1b[37m██║██║ ██║ ██║██╔══██║\x1b[0m');
28
+ console.log(' \x1b[1m\x1b[37m██║╚██████╗╚██████╔╝██║ ██║\x1b[0m');
29
+ console.log(' \x1b[1m\x1b[37m╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝\x1b[0m');
30
+ console.log();
31
+ for (let i = 0; i < steps.length; i++) {
32
+ const startPct = Math.round((i / steps.length) * TOTAL);
33
+ const endPct = Math.round(((i + 1) / steps.length) * TOTAL);
34
+ for (let p = startPct; p <= endPct; p++) {
35
+ drawBar(p, steps[i]);
36
+ await new Promise((r) => setTimeout(r, 15));
37
+ }
38
+ }
39
+ console.log();
40
+ console.log();
41
+ console.log(' \x1b[32m✓\x1b[0m ICOA CLI installed successfully!');
42
+ console.log();
43
+ console.log(' \x1b[90mGet started:\x1b[0m');
44
+ console.log(' \x1b[1m\x1b[37micoa\x1b[0m \x1b[90mLaunch and select your mode\x1b[0m');
45
+ console.log(' \x1b[1m\x1b[37micoa --help\x1b[0m \x1b[90mShow all commands\x1b[0m');
46
+ console.log();
47
+ }
48
+ run().catch(() => { });
49
+ export {};
package/dist/repl.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import { spawn, execSync as execSyncFn } from 'node:child_process';
3
3
  import chalk from 'chalk';
4
- import { isConnected, getConfig } from './lib/config.js';
4
+ import { isConnected, getConfig, saveConfig } from './lib/config.js';
5
5
  import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume, isFirstRunOrUpgrade, markVersionSeen } from './lib/access.js';
6
6
  import { setReplMode } from './lib/ui.js';
7
+ import { isChatActive, handleChatMessage } from './commands/ai4ctf.js';
7
8
  import { resetTerminalTheme } from './lib/theme.js';
8
9
  import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
9
10
  import { logCommand } from './lib/logger.js';
@@ -27,14 +28,42 @@ const BLOCKED_COMMANDS = new Set([
27
28
  'iptables', 'ufw', // firewall
28
29
  ]);
29
30
  const INTERCEPT = '__REPL_NO_EXIT__';
30
- const VERSION = '2.3.2';
31
+ const VERSION = '2.5.1';
31
32
  export async function startRepl(program, resumeMode) {
32
33
  const config = getConfig();
33
34
  const connected = isConnected();
34
35
  const realExit = process.exit.bind(process);
35
36
  const activated = isActivated();
36
- // First run or upgrade: prompt to install env
37
- if (isFirstRunOrUpgrade(VERSION)) {
37
+ // ─── Mode selection (first run) ───
38
+ let mode = config.mode || '';
39
+ if (!mode) {
40
+ const { select, confirm } = await import('@inquirer/prompts');
41
+ console.log(chalk.white(' Welcome! Please select your mode.'));
42
+ console.log(chalk.gray(' Use ↑↓ arrow keys to move, Enter to confirm.'));
43
+ console.log(chalk.gray(' You can switch mode later via: setup'));
44
+ console.log();
45
+ mode = await select({
46
+ message: 'Mode',
47
+ choices: [
48
+ { name: ` ${chalk.bold('National Selection')} ${chalk.gray('·')} ${chalk.gray('Exam only, lightweight')}`, value: 'selection' },
49
+ { name: ` ${chalk.bold('International Olympiad')} ${chalk.gray('·')} ${chalk.gray('CTF x AI (~500MB)')}`, value: 'olympiad' },
50
+ { name: ` ${chalk.bold('National/Regional Partner')} ${chalk.gray('·')} ${chalk.gray('Organizer management')}`, value: 'organizer' },
51
+ ],
52
+ });
53
+ if (mode === 'olympiad') {
54
+ console.log();
55
+ console.log(chalk.yellow(' This mode will download ~500MB of CTF tools and AI models.'));
56
+ const proceed = await confirm({ message: 'Continue?', default: true });
57
+ if (!proceed) {
58
+ mode = 'selection';
59
+ console.log(chalk.gray(' Switched to National Selection mode.'));
60
+ }
61
+ }
62
+ saveConfig({ mode });
63
+ console.log();
64
+ }
65
+ // First run or upgrade: prompt to install env (olympiad only)
66
+ if (mode === 'olympiad' && isFirstRunOrUpgrade(VERSION)) {
38
67
  markVersionSeen(VERSION);
39
68
  console.log(chalk.gray(' Checking competition environment...'));
40
69
  // Quick check: count missing Python libs
@@ -90,41 +119,66 @@ export async function startRepl(program, resumeMode) {
90
119
  console.log();
91
120
  }
92
121
  }
93
- // Device mismatch check
94
- if (activated && !isDeviceMatch()) {
95
- console.log(chalk.red(' Token was activated on a different device.'));
96
- console.log(chalk.gray(' Contact organizer for assistance.'));
97
- console.log();
98
- }
99
- else if (connected) {
100
- console.log(chalk.green(` Welcome back, ${config.userName}!`));
101
- console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
102
- console.log();
103
- }
104
- else if (activated) {
105
- ensureWorkspace();
106
- console.log(chalk.green(' Welcome, competitor! Ready to hack.'));
107
- console.log(chalk.gray(` Workspace: ${WORKSPACE}`));
108
- console.log();
109
- console.log(chalk.gray(' Quick Start'));
110
- console.log(chalk.gray(' ─────────────'));
111
- console.log(chalk.white(' join <url> ') + chalk.gray('Connect to competition'));
112
- console.log(chalk.white(' challenges ') + chalk.gray('View challenges'));
113
- console.log(chalk.white(' hint <question> ') + chalk.gray('Ask AI for help'));
114
- console.log(chalk.white(' env ') + chalk.gray('Check your tools'));
115
- console.log(chalk.white(' help ') + chalk.gray('All commands'));
116
- console.log();
122
+ // ─── Mode-specific welcome ───
123
+ if (mode === 'selection' || mode === 'organizer') {
124
+ // Lightweight modes: skip activate/device checks
125
+ const modeLabel = mode === 'selection'
126
+ ? chalk.cyan.bold('[Selection Mode]')
127
+ : chalk.yellow.bold('[Organizer Mode]');
128
+ if (connected) {
129
+ console.log(chalk.green(` Welcome back, ${config.userName}!`) + ' ' + modeLabel);
130
+ console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
131
+ console.log(chalk.gray(' Switch mode: setup'));
132
+ console.log();
133
+ }
134
+ else {
135
+ console.log(' ' + modeLabel + chalk.gray(' (switch via: setup)'));
136
+ console.log();
137
+ console.log(chalk.gray(' Quick Start'));
138
+ console.log(chalk.gray(' ─────────────'));
139
+ console.log(chalk.white(' join <url> ') + chalk.gray('Connect to exam server'));
140
+ console.log(chalk.white(' exam list ') + chalk.gray('View available exams'));
141
+ console.log(chalk.white(' help ') + chalk.gray('All commands'));
142
+ console.log();
143
+ }
117
144
  }
118
145
  else {
119
- console.log(chalk.white(' Welcome to ICOA CLI!'));
120
- console.log();
121
- console.log(chalk.gray(' Quick Start'));
122
- console.log(chalk.gray(' ─────────────'));
123
- console.log(chalk.white(' activate <token> ') + chalk.gray('Unlock with your access token'));
124
- console.log(chalk.white(' ref <topic> ') + chalk.gray('Browse tool references'));
125
- console.log(chalk.white(' env ') + chalk.gray('Check your tools'));
126
- console.log(chalk.white(' help ') + chalk.gray('All commands'));
127
- console.log();
146
+ // Olympiad mode: full flow with activate/device checks
147
+ if (activated && !isDeviceMatch()) {
148
+ console.log(chalk.red(' Token was activated on a different device.'));
149
+ console.log(chalk.gray(' Contact organizer for assistance.'));
150
+ console.log();
151
+ }
152
+ else if (connected) {
153
+ console.log(chalk.green(` Welcome back, ${config.userName}!`));
154
+ console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
155
+ console.log();
156
+ }
157
+ else if (activated) {
158
+ ensureWorkspace();
159
+ console.log(chalk.green(' Welcome, competitor! Ready to hack.'));
160
+ console.log(chalk.gray(` Workspace: ${WORKSPACE}`));
161
+ console.log();
162
+ console.log(chalk.gray(' Quick Start'));
163
+ console.log(chalk.gray(' ─────────────'));
164
+ console.log(chalk.white(' join <url> ') + chalk.gray('Connect to competition'));
165
+ console.log(chalk.white(' challenges ') + chalk.gray('View challenges'));
166
+ console.log(chalk.white(' hint <question> ') + chalk.gray('Ask AI for help'));
167
+ console.log(chalk.white(' env ') + chalk.gray('Check your tools'));
168
+ console.log(chalk.white(' help ') + chalk.gray('All commands'));
169
+ console.log();
170
+ }
171
+ else {
172
+ console.log(chalk.white(' Welcome to ICOA CLI!'));
173
+ console.log();
174
+ console.log(chalk.gray(' Quick Start'));
175
+ console.log(chalk.gray(' ─────────────'));
176
+ console.log(chalk.white(' activate <token> ') + chalk.gray('Unlock with your access token'));
177
+ console.log(chalk.white(' ref <topic> ') + chalk.gray('Browse tool references'));
178
+ console.log(chalk.white(' env ') + chalk.gray('Check your tools'));
179
+ console.log(chalk.white(' help ') + chalk.gray('All commands'));
180
+ console.log();
181
+ }
128
182
  }
129
183
  program.exitOverride();
130
184
  program.configureOutput({
@@ -147,6 +201,18 @@ export async function startRepl(program, resumeMode) {
147
201
  return;
148
202
  const input = line.trim();
149
203
  if (!input) {
204
+ rl.setPrompt(isChatActive() ? chalk.magenta('ai4ctf> ') : chalk.green('icoa> '));
205
+ rl.prompt();
206
+ return;
207
+ }
208
+ // If in AI chat mode, route to chat handler
209
+ if (isChatActive()) {
210
+ processing = true;
211
+ const result = await handleChatMessage(input);
212
+ processing = false;
213
+ if (result === 'exit') {
214
+ rl.setPrompt(chalk.green('icoa> '));
215
+ }
150
216
  rl.prompt();
151
217
  return;
152
218
  }
@@ -163,7 +229,7 @@ export async function startRepl(program, resumeMode) {
163
229
  }
164
230
  // Help
165
231
  if (input === 'help' || input === '?') {
166
- printReplHelp(isActivated());
232
+ printReplHelp(isActivated(), mode);
167
233
  rl.prompt();
168
234
  return;
169
235
  }
@@ -196,9 +262,24 @@ export async function startRepl(program, resumeMode) {
196
262
  rl.prompt();
197
263
  return;
198
264
  }
199
- // Token check — only ref allowed without activation or device mismatch
200
265
  const cmd = input.split(/\s+/)[0].toLowerCase();
201
- if ((!isActivated() || !isDeviceMatch()) && !isFreeCommand(cmd)) {
266
+ // ─── Mode-based command filtering ───
267
+ const selectionCommands = ['join', 'exam', 'setup', 'lang', 'ref', 'ctf'];
268
+ const organizerCommands = ['join', 'exam', 'setup', 'lang', 'ref', 'ctf'];
269
+ if (mode === 'selection' && !selectionCommands.includes(cmd)) {
270
+ console.log(chalk.gray(' Not available in Selection mode. Switch via: setup'));
271
+ console.log();
272
+ rl.prompt();
273
+ return;
274
+ }
275
+ if (mode === 'organizer' && !organizerCommands.includes(cmd)) {
276
+ console.log(chalk.gray(' Not available in Organizer mode. Switch via: setup'));
277
+ console.log();
278
+ rl.prompt();
279
+ return;
280
+ }
281
+ // Token check — only in olympiad mode
282
+ if (mode === 'olympiad' && (!isActivated() || !isDeviceMatch()) && !isFreeCommand(cmd)) {
202
283
  console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Enter your access token:'));
203
284
  console.log(chalk.white(' activate <token>'));
204
285
  console.log();
@@ -213,6 +294,7 @@ export async function startRepl(program, resumeMode) {
213
294
  'scoreboard', 'sb', 'status', 'time', 'hint', 'hint-b', 'hint-c',
214
295
  'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
215
296
  'log', 'lang', 'setup', 'env', 'ai4ctf', 'model', 'ctf',
297
+ 'exam',
216
298
  ];
217
299
  if (!knownCommands.includes(cmd)) {
218
300
  // Block dangerous commands
@@ -287,8 +369,6 @@ export async function startRepl(program, resumeMode) {
287
369
  process.exit = (() => {
288
370
  throw new Error(INTERCEPT);
289
371
  });
290
- // Pause main readline so sub-REPLs (ai4ctf) can use stdin
291
- rl.pause();
292
372
  try {
293
373
  await program.parseAsync(['node', 'icoa', ...args]);
294
374
  }
@@ -310,7 +390,10 @@ export async function startRepl(program, resumeMode) {
310
390
  finally {
311
391
  process.exit = realExit;
312
392
  processing = false;
313
- rl.resume();
393
+ }
394
+ // Switch prompt if entering chat mode
395
+ if (isChatActive()) {
396
+ rl.setPrompt(chalk.magenta('ai4ctf> '));
314
397
  }
315
398
  console.log();
316
399
  rl.prompt();
@@ -364,15 +447,37 @@ function mapCommand(input) {
364
447
  'hint', 'hint-b', 'hint-c', 'hint-budget',
365
448
  'ref', 'shell', 'files', 'connect', 'note',
366
449
  'log', 'lang', 'setup', 'env', 'ai4ctf', 'model',
367
- 'ctf',
450
+ 'ctf', 'exam',
368
451
  ];
369
452
  if (directCommands.includes(cmd)) {
370
453
  return [cmd, ...rest];
371
454
  }
372
455
  return parts;
373
456
  }
374
- function printReplHelp(activated) {
457
+ function printReplHelp(activated, mode = 'olympiad') {
375
458
  console.log();
459
+ // ─── Selection / Organizer: lightweight help ───
460
+ if (mode === 'selection' || mode === 'organizer') {
461
+ console.log(chalk.bold.white(' Exam'));
462
+ console.log(chalk.white(' join <url> ') + chalk.gray('Connect to exam server'));
463
+ console.log(chalk.white(' exam list ') + chalk.gray('Available exams'));
464
+ console.log(chalk.white(' exam start <id> ') + chalk.gray('Begin an exam'));
465
+ console.log(chalk.white(' exam q [n] ') + chalk.gray('View questions'));
466
+ console.log(chalk.white(' exam answer <n> <X> ') + chalk.gray('Answer question'));
467
+ console.log(chalk.white(' exam review ') + chalk.gray('Review all answers'));
468
+ console.log(chalk.white(' exam submit ') + chalk.gray('Submit for grading'));
469
+ console.log(chalk.white(' exam result ') + chalk.gray('View your score'));
470
+ console.log();
471
+ console.log(chalk.bold.white(' System'));
472
+ console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
473
+ console.log(chalk.white(' setup ') + chalk.gray('Settings / switch mode'));
474
+ console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language'));
475
+ console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
476
+ console.log(chalk.white(' exit ') + chalk.gray('Quit'));
477
+ console.log();
478
+ return;
479
+ }
480
+ // ─── Olympiad: full help ───
376
481
  if (!activated) {
377
482
  console.log(chalk.bold.yellow(' Restricted Mode — activate with a token to unlock all commands'));
378
483
  console.log();
@@ -391,6 +496,15 @@ function printReplHelp(activated) {
391
496
  console.log(chalk.white(' status ') + chalk.gray('Competition status'));
392
497
  console.log(chalk.white(' time ') + chalk.gray('Countdown timer'));
393
498
  console.log();
499
+ console.log(chalk.bold.white(' Exam'));
500
+ console.log(chalk.white(' exam list ') + chalk.gray('Available exams'));
501
+ console.log(chalk.white(' exam start <id> ') + chalk.gray('Begin an exam'));
502
+ console.log(chalk.white(' exam q [n] ') + chalk.gray('View questions'));
503
+ console.log(chalk.white(' exam answer <n> <X> ') + chalk.gray('Answer question'));
504
+ console.log(chalk.white(' exam review ') + chalk.gray('Review all answers'));
505
+ console.log(chalk.white(' exam submit ') + chalk.gray('Submit for grading'));
506
+ console.log(chalk.white(' exam result ') + chalk.gray('View your score'));
507
+ console.log();
394
508
  console.log(chalk.bold.white(' AI'));
395
509
  console.log(chalk.white(' ai4ctf ') + chalk.gray('Chat with your AI teammate'));
396
510
  console.log(chalk.white(' hint <question> ') + chalk.gray('Level A — General guidance'));
@@ -102,8 +102,11 @@ export interface IcoaConfig {
102
102
  deviceFingerprint: string;
103
103
  lastVersion: string;
104
104
  sessionCookie: string;
105
+ country: string;
106
+ mode: IcoaMode | '';
105
107
  }
106
108
  export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
109
+ export type IcoaMode = 'selection' | 'olympiad' | 'organizer';
107
110
  export type HintLevel = 'A' | 'B' | 'C';
108
111
  export interface HintBudget {
109
112
  a: number;
@@ -128,3 +131,44 @@ export declare const DEFAULT_BUDGET: HintBudget;
128
131
  export declare const DEFAULT_CONFIG: IcoaConfig;
129
132
  export declare const SUPPORTED_LANGUAGES: readonly ["en", "zh", "ja", "ko", "es"];
130
133
  export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
134
+ export interface ExamListItem {
135
+ id: string;
136
+ name: string;
137
+ country: string;
138
+ questionCount: number;
139
+ durationMinutes: number;
140
+ status: 'available' | 'in_progress' | 'submitted';
141
+ }
142
+ export interface ExamQuestion {
143
+ number: number;
144
+ text: string;
145
+ category?: string;
146
+ options: {
147
+ A: string;
148
+ B: string;
149
+ C: string;
150
+ D: string;
151
+ };
152
+ }
153
+ export interface ExamSession {
154
+ examId: string;
155
+ examName: string;
156
+ startedAt: string;
157
+ durationMinutes: number;
158
+ questionCount: number;
159
+ country: string;
160
+ }
161
+ export interface ExamState {
162
+ session: ExamSession;
163
+ questions: ExamQuestion[];
164
+ answers: Record<number, string>;
165
+ }
166
+ export interface ExamResult {
167
+ examId: string;
168
+ examName: string;
169
+ score: number;
170
+ total: number;
171
+ percentage: number;
172
+ passed: boolean;
173
+ submittedAt: string;
174
+ }
@@ -30,5 +30,7 @@ export const DEFAULT_CONFIG = {
30
30
  deviceFingerprint: '',
31
31
  lastVersion: '',
32
32
  sessionCookie: '',
33
+ country: '',
34
+ mode: '',
33
35
  };
34
36
  export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko', 'es'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.3.2",
3
+ "version": "2.5.2",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "scripts": {
14
14
  "build": "tsc",
15
15
  "dev": "tsc --watch",
16
- "start": "node dist/index.js"
16
+ "start": "node dist/index.js",
17
+ "postinstall": "node dist/postinstall.js 2>/dev/null || true"
17
18
  },
18
19
  "keywords": [
19
20
  "ctf",