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.
@@ -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 || 5,
337
+ max: state._helpMax || HELP_BASE,
334
338
  perQ: state._helpPerQ || {},
335
339
  eliminated: state._eliminated || {},
336
340
  };
337
341
  }
338
- // Track whether the practical section intro has been shown this session
339
- let _practicalIntroShown = false;
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
- // Show practical section intro once when first entering Q31+
351
- if (isPractical && !_practicalIntroShown && state?.session.examId !== 'demo-free') {
352
- _practicalIntroShown = true;
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
- : (help.max < 8 ? chalk.gray(`${t('helpUsedUp')}`) : chalk.gray(`${t('helpAllUsed')} (${help.used}/${help.max})`));
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
- if (help.max < 8) {
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(' (+3 bonus uses)'));
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 3 bonus help uses')
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
- if (help.max >= 8) {
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
- state._helpMax = 8;
938
+ const bonusDelta = bonusTarget - help.max;
939
+ state._helpMax = bonusTarget;
873
940
  saveExamState(state);
874
941
  console.log();
875
- console.log(chalk.green(' 🎁 +3 bonus help unlocked! (3/8 remaining)'));
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
- saveExamState({ session, questions: DEMO_QUESTIONS, answers: {} });
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.56",
3
+ "version": "2.19.58",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {