icoa-cli 2.19.59 → 2.19.60
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 +23 -1
- package/dist/repl.js +32 -4
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -326,6 +326,21 @@ function printQuestionProgress(current, total, answered) {
|
|
|
326
326
|
const pct = Math.round((current / total) * 100);
|
|
327
327
|
console.log();
|
|
328
328
|
console.log(` ${bar} ${chalk.white.bold(`${current}`)}${chalk.gray(`/${total}`)} ${chalk.gray(`(${answered} answered)`)} ${chalk.gray(`${pct}%`)}`);
|
|
329
|
+
// Time remaining — colour-coded countdown so contestants always see where
|
|
330
|
+
// they stand. Authoritative clock lives on the server (confirmedAt +
|
|
331
|
+
// duration enforced at submit time); this is a display helper.
|
|
332
|
+
const deadline = getExamDeadline();
|
|
333
|
+
if (deadline) {
|
|
334
|
+
const remainingSec = Math.max(0, Math.round((deadline.getTime() - Date.now()) / 1000));
|
|
335
|
+
const mm = Math.floor(remainingSec / 60);
|
|
336
|
+
const ss = remainingSec % 60;
|
|
337
|
+
const timeStr = `${mm}:${String(ss).padStart(2, '0')}`;
|
|
338
|
+
const color = remainingSec <= 60 ? chalk.red.bold
|
|
339
|
+
: remainingSec <= 300 ? chalk.red
|
|
340
|
+
: remainingSec <= 600 ? chalk.yellow
|
|
341
|
+
: chalk.gray;
|
|
342
|
+
console.log(` ${chalk.gray('⏱ Time remaining:')} ${color(timeStr)} ${chalk.gray('(server-authoritative)')}`);
|
|
343
|
+
}
|
|
329
344
|
}
|
|
330
345
|
// Help budget per exam.md §3: 10 base + 5 hidden bonus (unlocked via `more help`).
|
|
331
346
|
// Demo still uses the lighter 5 + 3 set via _helpMax overrides at demo start.
|
|
@@ -967,12 +982,19 @@ export function registerExamCommand(program) {
|
|
|
967
982
|
const isPractical = q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
968
983
|
let c;
|
|
969
984
|
if (isPractical) {
|
|
970
|
-
// Accept flag format: ICOA{...} or any string
|
|
971
985
|
c = choice.trim();
|
|
972
986
|
if (!c) {
|
|
973
987
|
printError('Please provide your flag: exam answer <n> ICOA{your_flag}');
|
|
974
988
|
return;
|
|
975
989
|
}
|
|
990
|
+
// Reject letter-only answers on practical questions — almost certainly
|
|
991
|
+
// a user who typed `A` thinking MCQ was still in play. Submitting 'A'
|
|
992
|
+
// as the flag is a footgun that would waste an attempt silently.
|
|
993
|
+
if (/^[A-Da-d]$/.test(c)) {
|
|
994
|
+
printError(`Q${num} is a practical question — answer with a flag, not a letter.`);
|
|
995
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`exam answer ${num} ICOA{your_flag}`));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
976
998
|
}
|
|
977
999
|
else {
|
|
978
1000
|
c = choice.toUpperCase();
|
package/dist/repl.js
CHANGED
|
@@ -597,14 +597,32 @@ export async function startRepl(program, resumeMode) {
|
|
|
597
597
|
return;
|
|
598
598
|
}
|
|
599
599
|
// ─── Quick exam answer shortcuts ───
|
|
600
|
-
// "A" / "B" / "C" / "D" → answer current question
|
|
601
|
-
// "2 C" / "5 A" → answer specific question
|
|
600
|
+
// "A" / "B" / "C" / "D" → answer current question (MCQ only)
|
|
601
|
+
// "2 C" / "5 A" → answer specific question (MCQ only)
|
|
602
|
+
// Practical questions (Q31-40) require flag format (ICOA{...}) — single
|
|
603
|
+
// letters on those would be silently accepted as the wrong flag answer,
|
|
604
|
+
// which is a footgun. Block and nudge the user toward the flag syntax.
|
|
602
605
|
const examState = getExamState();
|
|
603
606
|
if (examState) {
|
|
604
607
|
const upper = input.toUpperCase().trim();
|
|
605
|
-
//
|
|
608
|
+
// Helper: is question N practical (no A/B/C/D options)?
|
|
609
|
+
const isPracticalQ = (n) => {
|
|
610
|
+
const q = examState.questions.find((qq) => qq.number === n);
|
|
611
|
+
if (!q)
|
|
612
|
+
return false;
|
|
613
|
+
return q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
614
|
+
};
|
|
615
|
+
// Single letter: A, B, C, D → answer current question (only if MCQ)
|
|
606
616
|
if (/^[ABCD]$/.test(upper)) {
|
|
607
617
|
const currentQ = examState._lastQ || 1;
|
|
618
|
+
if (isPracticalQ(currentQ)) {
|
|
619
|
+
console.log();
|
|
620
|
+
console.log(chalk.yellow(` Q${currentQ} is a practical question — answer with a flag, not a letter.`));
|
|
621
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`exam answer ${currentQ} ICOA{your_flag}`));
|
|
622
|
+
console.log();
|
|
623
|
+
rl.prompt();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
608
626
|
processing = true;
|
|
609
627
|
try {
|
|
610
628
|
await program.parseAsync(['node', 'icoa', 'exam', 'answer', String(currentQ), upper]);
|
|
@@ -614,9 +632,18 @@ export async function startRepl(program, resumeMode) {
|
|
|
614
632
|
rl.prompt();
|
|
615
633
|
return;
|
|
616
634
|
}
|
|
617
|
-
// "N X" pattern: e.g. "2 C", "15 A"
|
|
635
|
+
// "N X" pattern: e.g. "2 C", "15 A" (MCQ only — same protection)
|
|
618
636
|
const match = upper.match(/^(\d+)\s+([ABCD])$/);
|
|
619
637
|
if (match) {
|
|
638
|
+
const targetQ = parseInt(match[1], 10);
|
|
639
|
+
if (isPracticalQ(targetQ)) {
|
|
640
|
+
console.log();
|
|
641
|
+
console.log(chalk.yellow(` Q${targetQ} is a practical question — answer with a flag, not a letter.`));
|
|
642
|
+
console.log(chalk.gray(' Example: ') + chalk.green(`exam answer ${targetQ} ICOA{your_flag}`));
|
|
643
|
+
console.log();
|
|
644
|
+
rl.prompt();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
620
647
|
processing = true;
|
|
621
648
|
try {
|
|
622
649
|
await program.parseAsync(['node', 'icoa', 'exam', 'answer', match[1], match[2]]);
|
|
@@ -667,6 +694,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
667
694
|
'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
|
|
668
695
|
'log', 'lang', 'setup', 'env', 'ai4ctf', 'model', 'ctf',
|
|
669
696
|
'exam', 'demo', 'retry', 'nations', 'next', 'prev', 'continue', 'logout', 'ctf4ai',
|
|
697
|
+
'mark', 'unmark', 'review', 'submit',
|
|
670
698
|
];
|
|
671
699
|
if (!knownCommands.includes(cmd)) {
|
|
672
700
|
// Block dangerous commands
|