icoa-cli 2.19.56 → 2.19.58
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 +99 -30
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -327,16 +327,88 @@ function printQuestionProgress(current, total, answered) {
|
|
|
327
327
|
console.log();
|
|
328
328
|
console.log(` ${bar} ${chalk.white.bold(`${current}`)}${chalk.gray(`/${total}`)} ${chalk.gray(`(${answered} answered)`)} ${chalk.gray(`${pct}%`)}`);
|
|
329
329
|
}
|
|
330
|
+
// Help budget per exam.md §3: 10 base + 5 hidden bonus (unlocked via `more help`).
|
|
331
|
+
// Demo still uses the lighter 5 + 3 set via _helpMax overrides at demo start.
|
|
332
|
+
const HELP_BASE = 10;
|
|
333
|
+
const HELP_WITH_BONUS = 15;
|
|
330
334
|
function getHelpState(state) {
|
|
331
335
|
return {
|
|
332
336
|
used: state._helpUsed || 0,
|
|
333
|
-
max: state._helpMax ||
|
|
337
|
+
max: state._helpMax || HELP_BASE,
|
|
334
338
|
perQ: state._helpPerQ || {},
|
|
335
339
|
eliminated: state._eliminated || {},
|
|
336
340
|
};
|
|
337
341
|
}
|
|
338
|
-
//
|
|
339
|
-
|
|
342
|
+
// Section intros fire once per REPL session, first time the user enters
|
|
343
|
+
// that section. State-persisted via shownWarnings keys 's_ai4ctf' / 's_ctf4ai'
|
|
344
|
+
// so jumping back and forth doesn't re-fire them.
|
|
345
|
+
function printSectionIntro(state, currentQ) {
|
|
346
|
+
if (state.session.examId === 'demo-free')
|
|
347
|
+
return;
|
|
348
|
+
const total = Number(state.session.questionCount || 40);
|
|
349
|
+
// Only 40-question exams have the three-section structure
|
|
350
|
+
if (total < 40)
|
|
351
|
+
return;
|
|
352
|
+
const shown = new Set(state.shownWarnings || []);
|
|
353
|
+
// ─── AI4CTF section intro (Q31 = first practical, AI-as-teammate) ───
|
|
354
|
+
if (currentQ >= 31 && currentQ <= 38 && !shown.has('s_ai4ctf')) {
|
|
355
|
+
console.log();
|
|
356
|
+
console.log(chalk.green(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
357
|
+
console.log(chalk.bold.green(' 🚀 Section 2: AI4CTF — AI is your teammate'));
|
|
358
|
+
console.log(chalk.green(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
359
|
+
console.log();
|
|
360
|
+
console.log(chalk.white(' Welcome to the practical CTF section. Real'));
|
|
361
|
+
console.log(chalk.white(' security tasks — crypto, web, forensics, pwn.'));
|
|
362
|
+
console.log();
|
|
363
|
+
console.log(chalk.bold.white(' Q31–38 (8 questions · 6 pts each)'));
|
|
364
|
+
console.log(chalk.gray(' Solve ') + chalk.bold('with') + chalk.gray(' AI by your side. AI is your teammate.'));
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(chalk.bold.white(' How to work'));
|
|
367
|
+
console.log(chalk.gray(' Run Python: ') + chalk.green('!python3 -c "print(1+1)"'));
|
|
368
|
+
console.log(chalk.gray(' ') + chalk.green('!python3') + chalk.gray(' for REPL'));
|
|
369
|
+
console.log(chalk.gray(' Ask AI: ') + chalk.bold.cyan('hint') + chalk.gray(' — free-form question'));
|
|
370
|
+
console.log(chalk.gray(' Submit flag: ') + chalk.green('exam answer <n> ICOA{...}'));
|
|
371
|
+
console.log();
|
|
372
|
+
console.log(chalk.bold.white(' Budget'));
|
|
373
|
+
console.log(chalk.gray(' AI tokens: ') + chalk.white('25,000') + chalk.gray(' for this section'));
|
|
374
|
+
console.log(chalk.gray(' Hints A/B/C: ') + chalk.white('5 / 3 / 1') + chalk.gray(' structured hints per question'));
|
|
375
|
+
console.log();
|
|
376
|
+
console.log(chalk.yellow(' Time still counting down. Budget ~2 min per question.'));
|
|
377
|
+
console.log(chalk.green(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
378
|
+
console.log();
|
|
379
|
+
shown.add('s_ai4ctf');
|
|
380
|
+
state.shownWarnings = Array.from(shown);
|
|
381
|
+
saveExamState(state);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// ─── CTF4AI section intro (Q39 = adversarial AI, highest-value questions) ───
|
|
385
|
+
if (currentQ >= 39 && !shown.has('s_ctf4ai')) {
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(chalk.red(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
388
|
+
console.log(chalk.bold.red(' 🎯 Section 3: CTF4AI — Challenge the AI'));
|
|
389
|
+
console.log(chalk.red(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
390
|
+
console.log();
|
|
391
|
+
console.log(chalk.white(' The final section flips the script: instead of'));
|
|
392
|
+
console.log(chalk.white(' using AI, you\'re ') + chalk.bold('attacking') + chalk.white(' AI systems.'));
|
|
393
|
+
console.log();
|
|
394
|
+
console.log(chalk.bold.white(' Q39–40 (2 questions · 16 pts each — highest value!)'));
|
|
395
|
+
console.log(chalk.gray(' Prompt injection · adversarial analysis · AI auditing'));
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(chalk.bold.white(' How this differs from AI4CTF'));
|
|
398
|
+
console.log(chalk.gray(' · AI is your ') + chalk.red('target') + chalk.gray(', not your teammate'));
|
|
399
|
+
console.log(chalk.gray(' · Read the scenario carefully — rules vary'));
|
|
400
|
+
console.log(chalk.gray(' · Separate AI budget: ') + chalk.white('25,000 tokens'));
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(chalk.yellow(' These are the hardest questions. Worth 21% of total score.'));
|
|
403
|
+
console.log(chalk.yellow(' If time is tight, skim Q39/40 first to decide attack order.'));
|
|
404
|
+
console.log(chalk.red(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
405
|
+
console.log();
|
|
406
|
+
shown.add('s_ctf4ai');
|
|
407
|
+
state.shownWarnings = Array.from(shown);
|
|
408
|
+
saveExamState(state);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
340
412
|
function printQuestion(q, answer) {
|
|
341
413
|
const state = getExamState();
|
|
342
414
|
const total = Number(state?.session.questionCount || 30);
|
|
@@ -347,25 +419,9 @@ function printQuestion(q, answer) {
|
|
|
347
419
|
// Pacing hints (Q10 time check, Q30 section complete) — real exam only
|
|
348
420
|
if (state)
|
|
349
421
|
printPacingHint(state, q.number);
|
|
350
|
-
//
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
console.log();
|
|
354
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
355
|
-
console.log(chalk.bold.white(' Practical Section — Python Required'));
|
|
356
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
357
|
-
console.log();
|
|
358
|
-
console.log(chalk.white(' Three ways to run Python:'));
|
|
359
|
-
console.log();
|
|
360
|
-
console.log(chalk.yellow(' 1.') + chalk.white(' One-liner: ') + chalk.green('!python3 -c "print(1+1)"'));
|
|
361
|
-
console.log(chalk.yellow(' 2.') + chalk.white(' Interactive: ') + chalk.green('!python3') + chalk.gray(' → >>> import struct ...'));
|
|
362
|
-
console.log(chalk.yellow(' 3.') + chalk.white(' Script: ') + chalk.green("!cat << 'EOF' > s.py") + chalk.gray(' → ') + chalk.green('!python3 s.py'));
|
|
363
|
-
console.log();
|
|
364
|
-
console.log(chalk.white(' Need help? Type ') + chalk.bold.cyan('hint') + chalk.white(' to ask AI (25K tokens per section)'));
|
|
365
|
-
console.log(chalk.white(' Submit flag: ') + chalk.green('exam answer <n> ICOA{your_flag}'));
|
|
366
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
367
|
-
console.log();
|
|
368
|
-
}
|
|
422
|
+
// Section intros (AI4CTF at Q31, CTF4AI at Q39) — real exam only
|
|
423
|
+
if (state && isPractical)
|
|
424
|
+
printSectionIntro(state, q.number);
|
|
369
425
|
// Urgent countdown warnings (10 / 5 / 1 minutes remaining)
|
|
370
426
|
if (state)
|
|
371
427
|
printTimeWarningIfNeeded(state);
|
|
@@ -436,9 +492,12 @@ function printQuestion(q, answer) {
|
|
|
436
492
|
console.log(chalk.yellow(' !python3') + chalk.gray(' start Python'));
|
|
437
493
|
}
|
|
438
494
|
else {
|
|
495
|
+
// "used up" prompt shows until the bonus tier is reached; after that
|
|
496
|
+
// (already unlocked), show the full-used final state.
|
|
497
|
+
const bonusUnlocked = help.max >= HELP_WITH_BONUS;
|
|
439
498
|
const helpLabel = remaining > 0
|
|
440
499
|
? chalk.gray(`${t('helpRemove')} `) + chalk.yellow(`(${remaining}/${help.max})`)
|
|
441
|
-
: (
|
|
500
|
+
: (!bonusUnlocked ? chalk.gray(`${t('helpUsedUp')}`) : chalk.gray(`${t('helpAllUsed')} (${help.used}/${help.max})`));
|
|
442
501
|
console.log(chalk.yellow(' A/B/C/D') + chalk.gray(` ${t('answerThis')}`));
|
|
443
502
|
console.log(chalk.yellow(' help') + ' ' + helpLabel);
|
|
444
503
|
}
|
|
@@ -783,10 +842,12 @@ export function registerExamCommand(program) {
|
|
|
783
842
|
}
|
|
784
843
|
// Check budget
|
|
785
844
|
if (help.used >= help.max) {
|
|
786
|
-
|
|
845
|
+
const bonusUnlocked = help.max >= HELP_WITH_BONUS;
|
|
846
|
+
const bonusRemaining = HELP_WITH_BONUS - help.max;
|
|
847
|
+
if (!bonusUnlocked) {
|
|
787
848
|
console.log();
|
|
788
849
|
console.log(chalk.yellow(` Help used: ${help.used}/${help.max}`));
|
|
789
|
-
console.log(chalk.white(' Need more? Type: ') + chalk.bold.yellow('more help') + chalk.gray(
|
|
850
|
+
console.log(chalk.white(' Need more? Type: ') + chalk.bold.yellow('more help') + chalk.gray(` (+${bonusRemaining} bonus uses)`));
|
|
790
851
|
console.log();
|
|
791
852
|
}
|
|
792
853
|
else {
|
|
@@ -852,7 +913,7 @@ export function registerExamCommand(program) {
|
|
|
852
913
|
// ─── exam more-help ───
|
|
853
914
|
exam
|
|
854
915
|
.command('more-help')
|
|
855
|
-
.description('Unlock
|
|
916
|
+
.description('Unlock hidden bonus help uses')
|
|
856
917
|
.action(() => {
|
|
857
918
|
logCommand('exam more-help');
|
|
858
919
|
const state = getExamState();
|
|
@@ -861,7 +922,12 @@ export function registerExamCommand(program) {
|
|
|
861
922
|
return;
|
|
862
923
|
}
|
|
863
924
|
const help = getHelpState(state);
|
|
864
|
-
|
|
925
|
+
// Demo (5→8) and real exam (10→15) each add their own bonus tier. Pick
|
|
926
|
+
// the target based on what's already in state: demo-free exam keeps the
|
|
927
|
+
// smaller step, real exams get the full 15.
|
|
928
|
+
const isDemo = state.session.examId === 'demo-free';
|
|
929
|
+
const bonusTarget = isDemo ? 8 : HELP_WITH_BONUS;
|
|
930
|
+
if (help.max >= bonusTarget) {
|
|
865
931
|
console.log(chalk.gray(' Bonus help already unlocked.'));
|
|
866
932
|
return;
|
|
867
933
|
}
|
|
@@ -869,10 +935,11 @@ export function registerExamCommand(program) {
|
|
|
869
935
|
console.log(chalk.gray(` You still have ${help.max - help.used} helps remaining. Use them first!`));
|
|
870
936
|
return;
|
|
871
937
|
}
|
|
872
|
-
|
|
938
|
+
const bonusDelta = bonusTarget - help.max;
|
|
939
|
+
state._helpMax = bonusTarget;
|
|
873
940
|
saveExamState(state);
|
|
874
941
|
console.log();
|
|
875
|
-
console.log(chalk.green(
|
|
942
|
+
console.log(chalk.green(` 🎁 +${bonusDelta} bonus help unlocked! (${bonusDelta}/${bonusTarget} remaining)`));
|
|
876
943
|
console.log(chalk.gray(' Type "help" on any question to use.'));
|
|
877
944
|
console.log();
|
|
878
945
|
});
|
|
@@ -1689,7 +1756,9 @@ export function registerExamCommand(program) {
|
|
|
1689
1756
|
drawProgress(100, 'Ready!');
|
|
1690
1757
|
console.log();
|
|
1691
1758
|
console.log();
|
|
1692
|
-
|
|
1759
|
+
// Demo keeps the lighter 5 + 3 help budget (10 questions, so 5 base =
|
|
1760
|
+
// half the questions; plenty without making the exam trivial).
|
|
1761
|
+
saveExamState({ session, questions: DEMO_QUESTIONS, answers: {}, _helpMax: 5 });
|
|
1693
1762
|
printKeyValue('Questions', String(DEMO_PICK_SIZE));
|
|
1694
1763
|
printKeyValue('Duration', 'No time limit');
|
|
1695
1764
|
// Show first question
|