icoa-cli 2.19.41 → 2.19.43

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.
@@ -8,6 +8,8 @@ import { logCommand } from '../lib/logger.js';
8
8
  import { printSuccess, printError, printWarning, printInfo, printTable, printHeader, printKeyValue, createSpinner, formatCountdown, } from '../lib/ui.js';
9
9
  import { t } from '../lib/i18n.js';
10
10
  import { getDeviceFingerprint } from '../lib/access.js';
11
+ import { getExamSetup, saveExamSetup, isExamSetupComplete } from '../lib/exam-setup.js';
12
+ import { execSync } from 'node:child_process';
11
13
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12
14
  // Fire-and-forget event POST to /api/icoa/demo-stats. Used for demo-flow
13
15
  // choice points (demo enter, retry, back) where we want a server-side
@@ -582,6 +584,9 @@ export function registerExamCommand(program) {
582
584
  state._helpMax = help.max;
583
585
  state._helpPerQ = help.perQ;
584
586
  state._eliminated = help.eliminated;
587
+ if (!state.interactions)
588
+ state.interactions = [];
589
+ state.interactions.push({ ts: new Date().toISOString(), q: currentQ, type: 'help_used', result: `eliminated ${toRemove}` });
585
590
  saveExamState(state);
586
591
  console.log();
587
592
  console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
@@ -605,6 +610,9 @@ export function registerExamCommand(program) {
605
610
  state._helpMax = help.max;
606
611
  state._helpPerQ = help.perQ;
607
612
  state._eliminated = help.eliminated;
613
+ if (!state.interactions)
614
+ state.interactions = [];
615
+ state.interactions.push({ ts: new Date().toISOString(), q: currentQ, type: 'help_used', result: `eliminated ${toRemove}` });
608
616
  saveExamState(state);
609
617
  console.log();
610
618
  console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
@@ -674,6 +682,17 @@ export function registerExamCommand(program) {
674
682
  return;
675
683
  }
676
684
  }
685
+ // Log interaction
686
+ const prevAnswer = state.answers[num];
687
+ if (!state.interactions)
688
+ state.interactions = [];
689
+ state.interactions.push({
690
+ ts: new Date().toISOString(),
691
+ q: num,
692
+ type: prevAnswer ? 'answer_changed' : 'answer_submitted',
693
+ input: c,
694
+ result: prevAnswer ? `changed from ${prevAnswer}` : 'new',
695
+ });
677
696
  state.answers[num] = c;
678
697
  state._lastQ = num;
679
698
  saveExamState(state);
@@ -966,8 +985,13 @@ export function registerExamCommand(program) {
966
985
  const res = await fetch(`${serverUrl}/api/icoa/exams/${state.session.examId}/submit`, {
967
986
  method: 'POST',
968
987
  headers: { 'Content-Type': 'application/json' },
969
- body: JSON.stringify({ token: examToken, answers: state.answers }),
970
- signal: AbortSignal.timeout(10000),
988
+ body: JSON.stringify({
989
+ token: examToken,
990
+ answers: state.answers,
991
+ interactions: state.interactions || [],
992
+ aiUsage: state.aiUsage || { ai4ctf: 0, ctf4ai: 0 },
993
+ }),
994
+ signal: AbortSignal.timeout(15000),
971
995
  });
972
996
  if (!res.ok) {
973
997
  const err = await res.json().catch(() => ({ message: 'Submit failed' }));
@@ -1048,8 +1072,29 @@ export function registerExamCommand(program) {
1048
1072
  printInfo('Use "exam review" or "exam submit" first.');
1049
1073
  return;
1050
1074
  }
1075
+ // Gate: require exam setup
1076
+ if (!isExamSetupComplete()) {
1077
+ console.log();
1078
+ printWarning('Pre-exam setup required before entering a token.');
1079
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('exam setup'));
1080
+ console.log();
1081
+ return;
1082
+ }
1083
+ // Auto-set language from token country prefix
1084
+ const COUNTRY_LANG = {
1085
+ UA: 'uk', PE: 'es', CN: 'zh', AU: 'en', JP: 'ja', KR: 'ko',
1086
+ BR: 'pt', SA: 'ar', FR: 'fr', DE: 'de', IN: 'hi', ID: 'id',
1087
+ TH: 'th', VN: 'vi', TR: 'tr', RU: 'ru', EG: 'ar', HT: 'ht',
1088
+ PH: 'en', MY: 'en', SG: 'en', ZA: 'en',
1089
+ };
1090
+ const countryPrefix = code.trim().substring(0, 2).toUpperCase();
1091
+ const detectedLang = COUNTRY_LANG[countryPrefix];
1051
1092
  const config = getConfig();
1052
1093
  const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
1094
+ const lang = detectedLang || config.language || 'en';
1095
+ if (detectedLang && detectedLang !== (config.language || 'en')) {
1096
+ saveConfig({ language: detectedLang });
1097
+ }
1053
1098
  console.log();
1054
1099
  drawProgress(0, 'Validating token...');
1055
1100
  try {
@@ -1059,7 +1104,7 @@ export function registerExamCommand(program) {
1059
1104
  body: JSON.stringify({
1060
1105
  token: code.trim(),
1061
1106
  deviceHash: getDeviceFingerprint(),
1062
- lang: config.language || 'en',
1107
+ lang,
1063
1108
  }),
1064
1109
  signal: AbortSignal.timeout(10000),
1065
1110
  });
@@ -1073,19 +1118,49 @@ export function registerExamCommand(program) {
1073
1118
  }
1074
1119
  drawProgress(30, 'Loading exam...');
1075
1120
  const json = await res.json();
1076
- const { session, questions } = json.data;
1077
- drawProgress(60, 'Preparing questions...');
1121
+ const { session, questions, languages } = json.data;
1122
+ drawProgress(60, 'Preparing...');
1078
1123
  await sleep(200);
1079
- drawProgress(90, 'Starting timer...');
1080
- await sleep(150);
1081
- saveExamState({ session, questions, answers: {} });
1082
1124
  drawProgress(100, 'Ready!');
1083
1125
  console.log();
1084
1126
  console.log();
1127
+ // ── Exam info page (timer NOT started yet) ──
1085
1128
  printHeader(session.examName);
1086
- printKeyValue('Questions', String(session.questionCount));
1129
+ console.log();
1130
+ printKeyValue('Questions', `${session.questionCount} (30 MCQ + 10 practical)`);
1087
1131
  printKeyValue('Duration', `${session.durationMinutes} minutes`);
1088
- printTimeRemaining();
1132
+ printKeyValue('Total', `${session.totalScore || 150} points`);
1133
+ printKeyValue('Pass', `${session.passingScore || 75} points (50%)`);
1134
+ console.log();
1135
+ console.log(chalk.white(' Assistance:'));
1136
+ console.log(chalk.gray(' Help: 10 uses + 5 hidden bonus (MCQ only)'));
1137
+ console.log(chalk.gray(' Hints: A(5) B(3) C(1) — practical only'));
1138
+ console.log(chalk.gray(' AI: 25K AI4CTF + 25K CTF4AI tokens'));
1139
+ if (languages && languages.length > 1) {
1140
+ console.log(chalk.gray(` Lang: ${languages.join(', ')} — switch with: lang <code>`));
1141
+ }
1142
+ console.log();
1143
+ console.log(chalk.yellow(' Rules:'));
1144
+ console.log(chalk.gray(' • All interactions are recorded for audit'));
1145
+ console.log(chalk.gray(' • Timer auto-submits when time expires'));
1146
+ console.log(chalk.gray(' • You may exit and resume with the same token'));
1147
+ console.log();
1148
+ // ── Wait for confirmation ──
1149
+ const readline = await import('node:readline');
1150
+ const rlConfirm = readline.createInterface({ input: process.stdin, output: process.stdout });
1151
+ await new Promise((resolve) => {
1152
+ rlConfirm.question(chalk.bold.yellow(' Press Enter to start the exam timer... '), () => {
1153
+ rlConfirm.close();
1154
+ resolve();
1155
+ });
1156
+ });
1157
+ // ── Timer starts NOW ──
1158
+ const confirmedAt = new Date().toISOString();
1159
+ session.confirmedAt = confirmedAt;
1160
+ saveExamState({ session, questions, answers: {}, interactions: [], aiUsage: { ai4ctf: 0, ctf4ai: 0 } });
1161
+ console.log();
1162
+ printSuccess('Exam started! Timer is running.');
1163
+ printKeyValue('Time Remaining', `${session.durationMinutes}:00`);
1089
1164
  if (questions.length > 0) {
1090
1165
  printQuestion(questions[0]);
1091
1166
  }
@@ -1236,7 +1311,137 @@ export function registerExamCommand(program) {
1236
1311
  printKeyValue('Duration', 'No time limit');
1237
1312
  printQuestion(retryQuestions[0]);
1238
1313
  });
1239
- // Default action: show status or help
1314
+ // ─── exam setup ───
1315
+ exam
1316
+ .command('setup')
1317
+ .description('Install Python environment for practical exam questions')
1318
+ .action(async () => {
1319
+ logCommand('exam setup');
1320
+ // Gate: require at least 1 demo attempt
1321
+ const stats = getDemoStats();
1322
+ if (stats.attempts === 0) {
1323
+ console.log();
1324
+ printWarning('Complete the demo first before setting up.');
1325
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('exam demo'));
1326
+ console.log();
1327
+ return;
1328
+ }
1329
+ // Skip if already set up
1330
+ const existingSetup = getExamSetup();
1331
+ if (existingSetup) {
1332
+ console.log();
1333
+ console.log(chalk.green(' ✓ ') + chalk.green('Environment already set up'));
1334
+ console.log(chalk.gray(` Completed: ${existingSetup.completedAt.split('T')[0]}`));
1335
+ console.log(chalk.gray(` Python: ${existingSetup.pythonVersion}`));
1336
+ console.log(chalk.gray(` Packages: ${existingSetup.installedPackages.length} installed`));
1337
+ if (existingSetup.failedPackages.length > 0) {
1338
+ console.log(chalk.yellow(` Failed: ${existingSetup.failedPackages.join(', ')}`));
1339
+ }
1340
+ console.log();
1341
+ console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
1342
+ console.log();
1343
+ return;
1344
+ }
1345
+ console.log();
1346
+ printHeader('Exam Environment Setup');
1347
+ console.log();
1348
+ console.log(chalk.white(' Installing Python packages for practical questions.'));
1349
+ console.log(chalk.gray(' Expected: 1-2 minutes · ~150MB disk'));
1350
+ console.log();
1351
+ // Step 1: Check Python 3.12+
1352
+ const pythonBin = 'python3';
1353
+ let pythonVersion = '';
1354
+ try {
1355
+ const raw = execSync(`${pythonBin} --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
1356
+ pythonVersion = raw.replace('Python ', '');
1357
+ const parts = pythonVersion.split('.').map(Number);
1358
+ if (parts[0] < 3 || (parts[0] === 3 && parts[1] < 12)) {
1359
+ printError(`Python ${pythonVersion} found, but 3.12+ required.`);
1360
+ printInfo('Install: https://www.python.org/downloads/');
1361
+ return;
1362
+ }
1363
+ console.log(chalk.green(` ✓ Python ${pythonVersion}`));
1364
+ }
1365
+ catch {
1366
+ printError('Python 3 not found.');
1367
+ printInfo('Install: https://www.python.org/downloads/');
1368
+ return;
1369
+ }
1370
+ // Step 2: Check pip
1371
+ try {
1372
+ execSync(`${pythonBin} -m pip --version`, { stdio: 'ignore', timeout: 5000 });
1373
+ console.log(chalk.green(' ✓ pip available'));
1374
+ }
1375
+ catch {
1376
+ console.log(chalk.yellow(' ⚠ pip not found, trying ensurepip...'));
1377
+ try {
1378
+ execSync(`${pythonBin} -m ensurepip --upgrade`, { stdio: 'ignore', timeout: 30000 });
1379
+ console.log(chalk.green(' ✓ pip installed'));
1380
+ }
1381
+ catch {
1382
+ printError('Could not install pip.');
1383
+ return;
1384
+ }
1385
+ }
1386
+ // Step 3: Install packages
1387
+ const PACKAGES = [
1388
+ 'pwntools', 'cryptography', 'requests', 'beautifulsoup4',
1389
+ 'numpy', 'pillow', 'scapy', 'sympy', 'z3-solver',
1390
+ 'pycryptodome', 'ROPgadget', 'volatility3', 'regex',
1391
+ ];
1392
+ const IMPORT_MAP = {
1393
+ 'beautifulsoup4': 'bs4', 'pycryptodome': 'Crypto', 'z3-solver': 'z3',
1394
+ 'pillow': 'PIL', 'pwntools': 'pwn', 'ROPgadget': 'ropgadget',
1395
+ };
1396
+ console.log();
1397
+ console.log(chalk.white(` Installing ${PACKAGES.length} packages...`));
1398
+ console.log();
1399
+ const installed = [];
1400
+ const failed = [];
1401
+ for (const pkg of PACKAGES) {
1402
+ try {
1403
+ execSync(`${pythonBin} -m pip install "${pkg}" --quiet 2>/dev/null`, {
1404
+ encoding: 'utf-8', timeout: 180000, stdio: 'pipe',
1405
+ });
1406
+ const importName = IMPORT_MAP[pkg] || pkg;
1407
+ execSync(`${pythonBin} -c "import ${importName}"`, { stdio: 'ignore', timeout: 10000 });
1408
+ let ver = '';
1409
+ try {
1410
+ const showOut = execSync(`${pythonBin} -m pip show "${pkg}" 2>/dev/null`, {
1411
+ encoding: 'utf-8', timeout: 5000,
1412
+ });
1413
+ const m = showOut.match(/^Version:\s*(.+)$/m);
1414
+ if (m)
1415
+ ver = m[1].trim();
1416
+ }
1417
+ catch { }
1418
+ console.log(chalk.green(` ✓ ${pkg}`) + (ver ? chalk.gray(` (${ver})`) : ''));
1419
+ installed.push(ver ? `${pkg}==${ver}` : pkg);
1420
+ }
1421
+ catch {
1422
+ console.log(chalk.red(` ✗ ${pkg}`));
1423
+ failed.push(pkg);
1424
+ }
1425
+ }
1426
+ // Save state
1427
+ saveExamSetup({
1428
+ completedAt: new Date().toISOString(),
1429
+ pythonVersion,
1430
+ installedPackages: installed,
1431
+ failedPackages: failed,
1432
+ });
1433
+ console.log();
1434
+ if (failed.length === 0) {
1435
+ printSuccess(`Environment ready! All ${PACKAGES.length} packages installed.`);
1436
+ }
1437
+ else {
1438
+ printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
1439
+ }
1440
+ console.log();
1441
+ console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
1442
+ console.log();
1443
+ });
1444
+ // Default action: progressive onboarding dashboard
1240
1445
  exam.action(() => {
1241
1446
  logCommand('exam');
1242
1447
  const state = getExamState();
@@ -1247,14 +1452,29 @@ export function registerExamCommand(program) {
1247
1452
  printKeyValue('Progress', `${answered}/${state.session.questionCount} answered`);
1248
1453
  console.log();
1249
1454
  console.log(chalk.gray(' exam q [n] | exam answer <n> <A-D> | exam review | exam submit'));
1455
+ return;
1456
+ }
1457
+ const stats = getDemoStats();
1458
+ const setup = getExamSetup();
1459
+ console.log();
1460
+ printHeader('ICOA National Selection Exam');
1461
+ console.log();
1462
+ if (stats.attempts === 0) {
1463
+ console.log(chalk.yellow(' ○ ') + chalk.yellow('Recommended: complete demo first'));
1464
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('exam demo'));
1250
1465
  }
1251
1466
  else {
1252
- console.log();
1253
- console.log(chalk.white(' No exam in progress.'));
1254
- console.log();
1255
- console.log(chalk.white(' exam demo ') + chalk.gray('Try free practice exam (no account needed)'));
1256
- console.log(chalk.white(' exam list ') + chalk.gray('View exams (requires login)'));
1257
- console.log();
1467
+ console.log(chalk.green(' ● ') + chalk.green(`Demo completed (${stats.attempts} attempt${stats.attempts !== 1 ? 's' : ''})`));
1468
+ if (!setup) {
1469
+ console.log(chalk.yellow(' ○ ') + chalk.yellow('Pre-exam setup required'));
1470
+ console.log(chalk.gray(' ') + chalk.bold.cyan('exam setup'));
1471
+ }
1472
+ else {
1473
+ console.log(chalk.green(' ● ') + chalk.green('Environment ready'));
1474
+ console.log(chalk.yellow(' ○ ') + chalk.yellow('Enter exam token to begin'));
1475
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('exam <token>'));
1476
+ }
1258
1477
  }
1478
+ console.log();
1259
1479
  });
1260
1480
  }
@@ -106,6 +106,40 @@ export function registerLangCommand(program) {
106
106
  console.log(chalk.gray(' Language changed. Type: demo'));
107
107
  }
108
108
  }
109
+ else if (state && state.session.token) {
110
+ // Real exam with token: re-fetch questions in new language from server
111
+ try {
112
+ const { getConfig } = await import('../lib/config.js');
113
+ const { saveExamState } = await import('../lib/exam-state.js');
114
+ const { getDeviceFingerprint } = await import('../lib/access.js');
115
+ const config = getConfig();
116
+ const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
117
+ const token = state.session.token;
118
+ const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({ token, deviceHash: getDeviceFingerprint(), lang: code }),
122
+ signal: AbortSignal.timeout(10000),
123
+ });
124
+ if (res.ok) {
125
+ const json = await res.json();
126
+ const newQuestions = json.data.questions;
127
+ // Keep answers, interactions, aiUsage — only update question text
128
+ state.questions = newQuestions;
129
+ saveExamState(state);
130
+ const currentQ = state._lastQ || 1;
131
+ console.log();
132
+ printSuccess(`Exam questions updated to ${LANG_NAMES[code] || code}. Your answers are kept.`);
133
+ console.log(chalk.white(` Resume: exam q ${currentQ}`));
134
+ }
135
+ else {
136
+ console.log(chalk.yellow(' Could not reload questions in new language. Try again later.'));
137
+ }
138
+ }
139
+ catch {
140
+ console.log(chalk.yellow(' Could not reach server. Language changed for UI only.'));
141
+ }
142
+ }
109
143
  else if (state) {
110
144
  const currentQ = state._lastQ || 1;
111
145
  console.log();
@@ -0,0 +1,9 @@
1
+ export interface ExamSetupState {
2
+ completedAt: string;
3
+ pythonVersion: string;
4
+ installedPackages: string[];
5
+ failedPackages: string[];
6
+ }
7
+ export declare function getExamSetup(): ExamSetupState | null;
8
+ export declare function saveExamSetup(state: ExamSetupState): void;
9
+ export declare function isExamSetupComplete(): boolean;
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getIcoaDir } from './config.js';
4
+ const SETUP_FILE = () => join(getIcoaDir(), 'exam-setup.json');
5
+ export function getExamSetup() {
6
+ try {
7
+ if (!existsSync(SETUP_FILE()))
8
+ return null;
9
+ return JSON.parse(readFileSync(SETUP_FILE(), 'utf-8'));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export function saveExamSetup(state) {
16
+ try {
17
+ writeFileSync(SETUP_FILE(), JSON.stringify(state, null, 2));
18
+ }
19
+ catch { }
20
+ }
21
+ export function isExamSetupComplete() {
22
+ return getExamSetup() !== null;
23
+ }
@@ -4,3 +4,4 @@ export declare function saveExamState(state: ExamState): void;
4
4
  export declare function clearExamState(): void;
5
5
  export declare function isExamActive(): boolean;
6
6
  export declare function getExamDeadline(): Date | null;
7
+ export declare function addInteraction(interaction: import('../types/index.js').ExamInteraction): void;
@@ -32,6 +32,17 @@ export function getExamDeadline() {
32
32
  return null;
33
33
  if (!state.session.durationMinutes)
34
34
  return null; // 0 = no time limit
35
- const start = new Date(state.session.startedAt).getTime();
35
+ // Use confirmedAt (when user pressed Enter) as the real start, fallback to startedAt
36
+ const startTime = state.session.confirmedAt || state.session.startedAt;
37
+ const start = new Date(startTime).getTime();
36
38
  return new Date(start + state.session.durationMinutes * 60 * 1000);
37
39
  }
40
+ export function addInteraction(interaction) {
41
+ const state = getExamState();
42
+ if (!state)
43
+ return;
44
+ if (!state.interactions)
45
+ state.interactions = [];
46
+ state.interactions.push(interaction);
47
+ saveExamState(state);
48
+ }
package/dist/repl.js CHANGED
@@ -429,7 +429,7 @@ export async function startRepl(program, resumeMode) {
429
429
  rl.prompt();
430
430
  return;
431
431
  }
432
- // ICOA exam token detection (e.g., "ICOA-PE-001")
432
+ // ICOA exam token detection: legacy "ICOA-PE-001" or new Crockford "UAHXP7SWMM"
433
433
  if (/^ICOA-[A-Z]{2,3}-\d{1,6}$/i.test(input.trim())) {
434
434
  processing = true;
435
435
  try {
@@ -440,6 +440,29 @@ export async function startRepl(program, resumeMode) {
440
440
  rl.prompt();
441
441
  return;
442
442
  }
443
+ // New 10-char Crockford Base32 token (e.g., UAHXP7SWMM) — no hyphen
444
+ if (/^[A-Z]{2}[0-9A-HJKMNP-TV-Z]{8}$/i.test(input.trim())) {
445
+ processing = true;
446
+ try {
447
+ await program.parseAsync(['node', 'icoa', 'exam', 'token', input.trim().toUpperCase()]);
448
+ }
449
+ catch { }
450
+ processing = false;
451
+ rl.prompt();
452
+ return;
453
+ }
454
+ // "exam UAHXP7SWMM" — 10-char token after exam prefix
455
+ const examTokenMatch = input.match(/^exam\s+([A-Z]{2}[0-9A-HJKMNP-TV-Z]{8})$/i);
456
+ if (examTokenMatch) {
457
+ processing = true;
458
+ try {
459
+ await program.parseAsync(['node', 'icoa', 'exam', 'token', examTokenMatch[1].toUpperCase()]);
460
+ }
461
+ catch { }
462
+ processing = false;
463
+ rl.prompt();
464
+ return;
465
+ }
443
466
  // "exam AU" / "exam PE" — shortcut to exam list <country>
444
467
  const examCountryMatch = input.match(/^exam\s+([A-Z]{2,3})$/i);
445
468
  if (examCountryMatch) {
@@ -159,14 +159,31 @@ export interface ExamSession {
159
159
  examId: string;
160
160
  examName: string;
161
161
  startedAt: string;
162
+ confirmedAt?: string;
162
163
  durationMinutes: number;
163
164
  questionCount: number;
165
+ totalScore?: number;
166
+ passingScore?: number;
164
167
  country: string;
168
+ token?: string;
169
+ }
170
+ export interface ExamInteraction {
171
+ ts: string;
172
+ q: number;
173
+ type: 'prompt_attempt' | 'hint_used' | 'help_used' | 'ai_hint' | 'answer_submitted' | 'answer_changed';
174
+ input?: string;
175
+ result?: string;
176
+ tokens?: number;
165
177
  }
166
178
  export interface ExamState {
167
179
  session: ExamSession;
168
180
  questions: ExamQuestion[];
169
181
  answers: Record<number, string>;
182
+ interactions?: ExamInteraction[];
183
+ aiUsage?: {
184
+ ai4ctf: number;
185
+ ctf4ai: number;
186
+ };
170
187
  }
171
188
  export interface ExamResult {
172
189
  examId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.41",
3
+ "version": "2.19.43",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {