icoa-cli 2.19.40 → 2.19.42
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/exam.js +237 -17
- package/dist/lib/exam-setup.d.ts +9 -0
- package/dist/lib/exam-setup.js +23 -0
- package/dist/lib/exam-state.d.ts +1 -0
- package/dist/lib/exam-state.js +12 -1
- package/dist/lib/gemini.js +3 -0
- package/dist/repl.js +24 -1
- package/dist/types/index.d.ts +17 -0
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -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({
|
|
970
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1129
|
+
console.log();
|
|
1130
|
+
printKeyValue('Questions', `${session.questionCount} (30 MCQ + 10 practical)`);
|
|
1087
1131
|
printKeyValue('Duration', `${session.durationMinutes} minutes`);
|
|
1088
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|
package/dist/lib/exam-state.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/exam-state.js
CHANGED
|
@@ -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
|
-
|
|
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/lib/gemini.js
CHANGED
|
@@ -184,6 +184,9 @@ export async function createChatSession(context, customSystemPrompt) {
|
|
|
184
184
|
});
|
|
185
185
|
if (!res.ok) {
|
|
186
186
|
const err = await res.json().catch(() => ({ message: 'AI proxy error' }));
|
|
187
|
+
if (res.status === 429) {
|
|
188
|
+
throw new Error(chalk.yellow('⏳ ') + (err.message || 'Too many requests. Please wait a moment and try again.'));
|
|
189
|
+
}
|
|
187
190
|
throw new Error(err.message || `AI proxy returned ${res.status}`);
|
|
188
191
|
}
|
|
189
192
|
const json = await res.json();
|
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
|
|
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) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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;
|