icoa-cli 2.19.42 → 2.19.44

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.
@@ -226,12 +226,34 @@ function getHelpState(state) {
226
226
  eliminated: state._eliminated || {},
227
227
  };
228
228
  }
229
+ // Track whether the practical section intro has been shown this session
230
+ let _practicalIntroShown = false;
229
231
  function printQuestion(q, answer) {
230
232
  const state = getExamState();
231
233
  const total = Number(state?.session.questionCount || 30);
232
234
  const answered = Object.keys(state?.answers || {}).length;
233
235
  const help = getHelpState(state);
234
236
  const eliminated = help.eliminated[q.number] || [];
237
+ const isPractical = q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
238
+ // Show practical section intro once when first entering Q31+
239
+ if (isPractical && !_practicalIntroShown && state?.session.examId !== 'demo-free') {
240
+ _practicalIntroShown = true;
241
+ console.log();
242
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
243
+ console.log(chalk.bold.white(' Practical Section — Python Required'));
244
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
245
+ console.log();
246
+ console.log(chalk.white(' Three ways to run Python:'));
247
+ console.log();
248
+ console.log(chalk.yellow(' 1.') + chalk.white(' One-liner: ') + chalk.green('!python3 -c "print(1+1)"'));
249
+ console.log(chalk.yellow(' 2.') + chalk.white(' Interactive: ') + chalk.green('!python3') + chalk.gray(' → >>> import struct ...'));
250
+ console.log(chalk.yellow(' 3.') + chalk.white(' Script: ') + chalk.green("!cat << 'EOF' > s.py") + chalk.gray(' → ') + chalk.green('!python3 s.py'));
251
+ console.log();
252
+ console.log(chalk.white(' Need help? Type ') + chalk.bold.cyan('hint') + chalk.white(' to ask AI (25K tokens per section)'));
253
+ console.log(chalk.white(' Submit flag: ') + chalk.green('exam answer <n> ICOA{your_flag}'));
254
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
255
+ console.log();
256
+ }
235
257
  // Progress bar
236
258
  printQuestionProgress(q.number, total, answered);
237
259
  // Easter egg (position calculated by percentage of total)
@@ -242,22 +264,43 @@ function printQuestion(q, answer) {
242
264
  console.log();
243
265
  if (q.category)
244
266
  console.log(chalk.cyan(` [${q.category}]`));
245
- console.log(chalk.bold.white(` Q${q.number}. `) + chalk.white(q.text));
246
- console.log();
247
- for (const key of ['A', 'B', 'C', 'D']) {
248
- const isEliminated = eliminated.includes(key);
249
- const selected = answer === key;
250
- if (isEliminated) {
251
- console.log(chalk.gray.strikethrough(` ${key}. ${q.options[key]}`) + chalk.red(` (${t('wrong')})`));
267
+ if (q.points && q.points > 2) {
268
+ console.log(chalk.cyan(` [${q.points} points]`));
269
+ }
270
+ if (isPractical) {
271
+ // Practical question display
272
+ const displayText = q.description || q.text;
273
+ console.log(chalk.bold.white(` Q${q.number}. `) + chalk.white(displayText.split('\n')[0]));
274
+ // Show remaining lines with proper indentation
275
+ const lines = displayText.split('\n').slice(1);
276
+ for (const line of lines) {
277
+ console.log(chalk.white(` ${line}`));
252
278
  }
253
- else if (selected) {
254
- console.log(chalk.green.bold(` ▸ ${key}. ${q.options[key]}`));
279
+ console.log();
280
+ if (answer) {
281
+ console.log(chalk.green(` Your answer: ${answer}`));
282
+ console.log();
255
283
  }
256
- else {
257
- console.log(chalk.gray(` ${key}.`) + ' ' + chalk.white(q.options[key]));
284
+ }
285
+ else {
286
+ // MCQ display
287
+ console.log(chalk.bold.white(` Q${q.number}. `) + chalk.white(q.text));
288
+ console.log();
289
+ for (const key of ['A', 'B', 'C', 'D']) {
290
+ const isEliminated = eliminated.includes(key);
291
+ const selected = answer === key;
292
+ if (isEliminated) {
293
+ console.log(chalk.gray.strikethrough(` ${key}. ${q.options[key]}`) + chalk.red(` (${t('wrong')})`));
294
+ }
295
+ else if (selected) {
296
+ console.log(chalk.green.bold(` ▸ ${key}. ${q.options[key]}`));
297
+ }
298
+ else {
299
+ console.log(chalk.gray(` ${key}.`) + ' ' + chalk.white(q.options[key]));
300
+ }
258
301
  }
302
+ console.log();
259
303
  }
260
- console.log();
261
304
  // Q2 tutorial hint
262
305
  const isDemo = state?.session.examId === 'demo-free';
263
306
  const q2Helps = help.perQ[2] || 0;
@@ -265,14 +308,21 @@ function printQuestion(q, answer) {
265
308
  console.log(chalk.yellow.bold(` ${t('qTutorial')}`));
266
309
  console.log();
267
310
  }
268
- // Full menu on every question
311
+ // Full menu
269
312
  const remaining = help.max - help.used;
270
- const helpLabel = remaining > 0
271
- ? chalk.gray(`${t('helpRemove')} `) + chalk.yellow(`(${remaining}/${help.max})`)
272
- : (help.max < 8 ? chalk.gray(`${t('helpUsedUp')}`) : chalk.gray(`${t('helpAllUsed')} (${help.used}/${help.max})`));
273
313
  console.log(chalk.gray(' ─────────────────────────────────────────'));
274
- console.log(chalk.yellow(' A/B/C/D') + chalk.gray(` ${t('answerThis')}`));
275
- console.log(chalk.yellow(' help') + ' ' + helpLabel);
314
+ if (isPractical) {
315
+ console.log(chalk.yellow(' exam answer <n> ICOA{...}') + chalk.gray(' submit flag'));
316
+ console.log(chalk.yellow(' hint') + chalk.gray(' ask AI for help'));
317
+ console.log(chalk.yellow(' !python3') + chalk.gray(' start Python'));
318
+ }
319
+ else {
320
+ const helpLabel = remaining > 0
321
+ ? chalk.gray(`${t('helpRemove')} `) + chalk.yellow(`(${remaining}/${help.max})`)
322
+ : (help.max < 8 ? chalk.gray(`${t('helpUsedUp')}`) : chalk.gray(`${t('helpAllUsed')} (${help.used}/${help.max})`));
323
+ console.log(chalk.yellow(' A/B/C/D') + chalk.gray(` ${t('answerThis')}`));
324
+ console.log(chalk.yellow(' help') + ' ' + helpLabel);
325
+ }
276
326
  console.log(chalk.yellow(' next') + chalk.gray(' / ') + chalk.yellow('prev') + chalk.gray(` ${t('htpNav')}`));
277
327
  console.log(chalk.yellow(` exam q 1..${total}`) + chalk.gray(` ${t('htpJump')}`));
278
328
  console.log(chalk.yellow(' exam review') + chalk.gray(` ${t('htpReview')}`));
@@ -648,9 +698,10 @@ export function registerExamCommand(program) {
648
698
  });
649
699
  // ─── exam answer <n> <choice> ───
650
700
  exam
651
- .command('answer <n> <choice>')
652
- .description('Answer question N with choice A/B/C/D')
653
- .action(async (n, choice) => {
701
+ .command('answer <n> <choice...>')
702
+ .description('Answer question N (A/B/C/D for MCQ, ICOA{flag} for practical)')
703
+ .action(async (n, choiceParts) => {
704
+ const choice = choiceParts.join(' ');
654
705
  logCommand(`exam answer ${n} ${choice}`);
655
706
  const state = getExamState();
656
707
  if (!state) {
@@ -665,10 +716,23 @@ export function registerExamCommand(program) {
665
716
  printError(`Question ${n} not found (1-${state.questions.length}).`);
666
717
  return;
667
718
  }
668
- const c = choice.toUpperCase();
669
- if (!['A', 'B', 'C', 'D'].includes(c)) {
670
- printError('Choice must be A, B, C, or D.');
671
- return;
719
+ // Detect question type: practical (ai4ctf/ctf4ai) or MCQ
720
+ const isPractical = q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
721
+ let c;
722
+ if (isPractical) {
723
+ // Accept flag format: ICOA{...} or any string
724
+ c = choice.trim();
725
+ if (!c) {
726
+ printError('Please provide your flag: exam answer <n> ICOA{your_flag}');
727
+ return;
728
+ }
729
+ }
730
+ else {
731
+ c = choice.toUpperCase();
732
+ if (!['A', 'B', 'C', 'D'].includes(c)) {
733
+ printError('Choice must be A, B, C, or D.');
734
+ return;
735
+ }
672
736
  }
673
737
  // Q2 tutorial: must use help first before answering
674
738
  if (state.session.examId === 'demo-free' && num === 2) {
@@ -1437,6 +1501,42 @@ export function registerExamCommand(program) {
1437
1501
  else {
1438
1502
  printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
1439
1503
  }
1504
+ // Python usage tutorial for beginners
1505
+ console.log();
1506
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1507
+ console.log(chalk.bold.white(' How to use Python in ICOA CLI'));
1508
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1509
+ console.log();
1510
+ console.log(chalk.bold.yellow(' Method 1: One-liner') + chalk.gray(' (quick tasks)'));
1511
+ console.log(chalk.white(' Just add ! before the command:'));
1512
+ console.log(chalk.green(' !python3 -c "import base64; print(base64.b64decode(\'SGVsbG8=\'))"'));
1513
+ console.log(chalk.gray(' → b\'Hello\''));
1514
+ console.log();
1515
+ console.log(chalk.bold.yellow(' Method 2: Interactive Python') + chalk.gray(' (explore & experiment)'));
1516
+ console.log(chalk.white(' Start a Python session, type commands one by one:'));
1517
+ console.log(chalk.green(' !python3'));
1518
+ console.log(chalk.gray(' >>> from Crypto.Cipher import AES'));
1519
+ console.log(chalk.gray(' >>> key = bytes.fromhex("49434f41...")'));
1520
+ console.log(chalk.gray(' >>> cipher = AES.new(key, AES.MODE_CBC, iv)'));
1521
+ console.log(chalk.gray(' >>> print(cipher.decrypt(data))'));
1522
+ console.log(chalk.gray(' >>> exit()'));
1523
+ console.log();
1524
+ console.log(chalk.bold.yellow(' Method 3: Script file') + chalk.gray(' (complex solutions)'));
1525
+ console.log(chalk.white(' Write a .py file, then run it:'));
1526
+ console.log(chalk.green(" !cat << 'EOF' > solve.py"));
1527
+ console.log(chalk.gray(' from pwn import xor'));
1528
+ console.log(chalk.gray(' ct = bytes.fromhex("0a2b0e1c...")'));
1529
+ console.log(chalk.gray(' key = xor(ct[:5], b"ICOA{")'));
1530
+ console.log(chalk.gray(' print(xor(ct, key).decode())'));
1531
+ console.log(chalk.green(' EOF'));
1532
+ console.log(chalk.green(' !python3 solve.py'));
1533
+ console.log();
1534
+ console.log(chalk.bold.yellow(' AI Assistant'));
1535
+ console.log(chalk.white(' During the exam, type ') + chalk.bold.cyan('hint') + chalk.white(' to ask AI for help:'));
1536
+ console.log(chalk.gray(' "How do I decrypt AES-CBC in Python?"'));
1537
+ console.log(chalk.gray(' "Show me how to use struct.unpack"'));
1538
+ console.log(chalk.gray(' AI budget: 25K tokens for AI4CTF + 25K for CTF4AI'));
1539
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1440
1540
  console.log();
1441
1541
  console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
1442
1542
  console.log();
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.42",
3
+ "version": "2.19.44",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {