icoa-cli 2.19.61 → 2.19.63
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/ai4ctf.js +70 -23
- package/dist/commands/ctf4ai-demo.js +63 -21
- package/package.json +1 -1
package/dist/commands/ai4ctf.js
CHANGED
|
@@ -308,11 +308,18 @@ export async function handleChatMessage(input) {
|
|
|
308
308
|
// ═══════════════════════════════════════════════════════════════
|
|
309
309
|
// Real-exam AI4CTF chat mode (Q31-38)
|
|
310
310
|
// ═══════════════════════════════════════════════════════════════
|
|
311
|
-
function printExamAi4ctfWelcome(q, qNum) {
|
|
311
|
+
function printExamAi4ctfWelcome(q, qNum, existingAnswer) {
|
|
312
312
|
const config = getConfig();
|
|
313
313
|
const modelName = config.geminiModel || 'gemma-4-31b-it';
|
|
314
|
+
const isResume = chatTokensUsed > 0;
|
|
314
315
|
console.log();
|
|
315
316
|
console.log(chalk.green.bold(` ═══ AI4CTF — Q${qNum}: ${q.category} ═══`));
|
|
317
|
+
if (isResume) {
|
|
318
|
+
console.log(chalk.gray(` (resuming — prior chat is not remembered, but tokens already used stay deducted)`));
|
|
319
|
+
}
|
|
320
|
+
if (existingAnswer) {
|
|
321
|
+
console.log(chalk.gray(` Current answer: `) + chalk.yellow(existingAnswer) + chalk.gray(` (submit again to change)`));
|
|
322
|
+
}
|
|
316
323
|
console.log();
|
|
317
324
|
console.log(chalk.cyan(' ┌─────────────────────────────────────────────'));
|
|
318
325
|
console.log(chalk.cyan(' │ ') + chalk.bold.white(`Q${qNum} [${q.category}] · ${q.points || 6} pts`));
|
|
@@ -387,9 +394,27 @@ export async function startExamAi4ctfChat(qNum, question) {
|
|
|
387
394
|
chatTokensUsed = usedSoFar;
|
|
388
395
|
tokensLocked = false;
|
|
389
396
|
examChatCtx = { qNum, question, usageField: 'ai4ctf' };
|
|
390
|
-
|
|
397
|
+
const existingAnswer = state.answers?.[qNum];
|
|
398
|
+
printExamAi4ctfWelcome(question, qNum, existingAnswer);
|
|
391
399
|
return true;
|
|
392
400
|
}
|
|
401
|
+
// Usage warnings shown once each when crossing thresholds. Keyed by cap.
|
|
402
|
+
function maybeWarnTokenUsage(used, cap, warnedSet) {
|
|
403
|
+
const pct = (used / cap) * 100;
|
|
404
|
+
if (pct >= 95 && !warnedSet.has('95')) {
|
|
405
|
+
console.log(chalk.red.bold(` ⚠ ${Math.round(pct)}% of section AI budget used — only ~${cap - used} tokens left.`));
|
|
406
|
+
warnedSet.add('95');
|
|
407
|
+
}
|
|
408
|
+
else if (pct >= 80 && !warnedSet.has('80')) {
|
|
409
|
+
console.log(chalk.yellow(` ⚠ 80% of section AI budget used.`));
|
|
410
|
+
warnedSet.add('80');
|
|
411
|
+
}
|
|
412
|
+
else if (pct >= 50 && !warnedSet.has('50')) {
|
|
413
|
+
console.log(chalk.gray(` Note: 50% of section AI budget used.`));
|
|
414
|
+
warnedSet.add('50');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const _ai4ctfWarned = new Set();
|
|
393
418
|
async function handleExamAi4ctfMessage(input) {
|
|
394
419
|
if (!examChatCtx || !chatSession)
|
|
395
420
|
return 'exit';
|
|
@@ -426,6 +451,16 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
426
451
|
const submitMatch = trimmed.match(/^submit\s+(.+)/i);
|
|
427
452
|
if (submitMatch) {
|
|
428
453
|
const flag = submitMatch[1].trim();
|
|
454
|
+
// Letter-only footgun: reject 'A'/'B'/'C'/'D' as flags. Same defence we
|
|
455
|
+
// added at the REPL and exam-answer layers; duplicated here because chat
|
|
456
|
+
// bypasses both.
|
|
457
|
+
if (/^[A-Da-d]$/.test(flag)) {
|
|
458
|
+
console.log();
|
|
459
|
+
console.log(chalk.yellow(` "${flag}" looks like an MCQ letter, not a flag.`));
|
|
460
|
+
console.log(chalk.gray(' Flag format: ') + chalk.green('ICOA{your_flag}') + chalk.gray('. Try again inside this chat.'));
|
|
461
|
+
console.log();
|
|
462
|
+
return 'continue';
|
|
463
|
+
}
|
|
429
464
|
const { getExamState, saveExamState } = await import('../lib/exam-state.js');
|
|
430
465
|
const state = getExamState();
|
|
431
466
|
if (!state)
|
|
@@ -435,7 +470,8 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
435
470
|
console.log(chalk.red(` Q${examChatCtx.qNum} not found in state.`));
|
|
436
471
|
return 'continue';
|
|
437
472
|
}
|
|
438
|
-
// Store answer (validated on server at exam submit)
|
|
473
|
+
// Store answer (validated on server at exam submit). Stay in chat so the
|
|
474
|
+
// contestant can iterate on the flag without having to re-enter.
|
|
439
475
|
const prevAnswer = state.answers[examChatCtx.qNum];
|
|
440
476
|
if (!state.interactions)
|
|
441
477
|
state.interactions = [];
|
|
@@ -447,33 +483,30 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
447
483
|
result: 'via ai4ctf chat',
|
|
448
484
|
});
|
|
449
485
|
state.answers[examChatCtx.qNum] = flag;
|
|
450
|
-
|
|
486
|
+
// Advance _lastQ to the next question in the section so that a subsequent
|
|
487
|
+
// `ai4ctf` (after exit) picks up Q+1 automatically, no `exam q N` needed.
|
|
488
|
+
const nextQ = examChatCtx.qNum + 1;
|
|
489
|
+
state._lastQ = nextQ <= 38 ? nextQ : examChatCtx.qNum;
|
|
451
490
|
saveExamState(state);
|
|
452
491
|
console.log();
|
|
453
|
-
|
|
492
|
+
if (prevAnswer) {
|
|
493
|
+
console.log(chalk.green(` ✓ Q${examChatCtx.qNum} answer updated: `) + chalk.yellow(flag));
|
|
494
|
+
console.log(chalk.gray(` Previous: ${prevAnswer}`));
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
console.log(chalk.green.bold(` ✓ Answer for Q${examChatCtx.qNum} recorded: ${flag}`));
|
|
498
|
+
}
|
|
454
499
|
console.log(chalk.gray(' (Grading happens at exam submit — you cannot preview correctness during the exam.)'));
|
|
455
500
|
console.log();
|
|
456
|
-
|
|
457
|
-
const nextQ = examChatCtx.qNum + 1;
|
|
458
|
-
const savedQ = examChatCtx.qNum;
|
|
459
|
-
chatActive = false;
|
|
460
|
-
chatSession = null;
|
|
461
|
-
examChatCtx = null;
|
|
501
|
+
console.log(chalk.gray(' ') + chalk.white('submit ICOA{...}') + chalk.gray(' again to change, or ') + chalk.white('exit') + chalk.gray(' to move on'));
|
|
462
502
|
if (nextQ <= 38) {
|
|
463
|
-
console.log(chalk.
|
|
464
|
-
console.log(chalk.white(' Next: ') + chalk.bold.green(`Q${nextQ}`));
|
|
465
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan(`exam q ${nextQ}`) + chalk.gray(' jump to question'));
|
|
466
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('ai4ctf') + chalk.gray(' start AI chat for Q') + String(nextQ));
|
|
467
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
503
|
+
console.log(chalk.gray(' After exit: ') + chalk.cyan('ai4ctf') + chalk.gray(` will open Q${nextQ}`));
|
|
468
504
|
}
|
|
469
505
|
else {
|
|
470
|
-
console.log(chalk.
|
|
471
|
-
console.log(chalk.bold.white(' AI4CTF section complete — Q39 begins CTF4AI.'));
|
|
472
|
-
console.log(chalk.gray(' → ') + chalk.bold.red('ctf4ai') + chalk.gray(' start CTF4AI for Q39'));
|
|
473
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
506
|
+
console.log(chalk.gray(' After exit: AI4CTF section complete. Next: ') + chalk.red('ctf4ai') + chalk.gray(' on Q39'));
|
|
474
507
|
}
|
|
475
508
|
console.log();
|
|
476
|
-
return '
|
|
509
|
+
return 'continue';
|
|
477
510
|
}
|
|
478
511
|
// Shell command
|
|
479
512
|
if (input.startsWith('!')) {
|
|
@@ -497,13 +530,26 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
497
530
|
}
|
|
498
531
|
// Exit chat
|
|
499
532
|
if (lower === 'exit' || lower === 'back' || lower === 'quit') {
|
|
533
|
+
const savedQ = examChatCtx.qNum;
|
|
534
|
+
const { getExamState } = await import('../lib/exam-state.js');
|
|
535
|
+
const exitState = getExamState();
|
|
536
|
+
const hasAnswer = exitState && exitState.answers[savedQ] != null;
|
|
537
|
+
const lastQ = (exitState?._lastQ) || savedQ;
|
|
500
538
|
chatActive = false;
|
|
501
539
|
chatSession = null;
|
|
502
|
-
const savedQ = examChatCtx.qNum;
|
|
503
540
|
examChatCtx = null;
|
|
541
|
+
_ai4ctfWarned.clear();
|
|
504
542
|
console.log();
|
|
505
543
|
console.log(chalk.gray(` AI4CTF chat ended for Q${savedQ}.`));
|
|
506
|
-
|
|
544
|
+
if (hasAnswer && lastQ > savedQ && lastQ <= 38) {
|
|
545
|
+
console.log(chalk.white(' Next: ') + chalk.bold.green(`Q${lastQ}`) + chalk.gray(' — type ') + chalk.cyan('ai4ctf') + chalk.gray(' to continue'));
|
|
546
|
+
}
|
|
547
|
+
else if (hasAnswer && savedQ >= 38) {
|
|
548
|
+
console.log(chalk.white(' AI4CTF section complete — ') + chalk.bold.red('ctf4ai') + chalk.gray(' for Q39'));
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.log(chalk.gray(' Resume: ') + chalk.white(`exam q ${savedQ}`) + chalk.gray(' · Re-enter: ') + chalk.white('ai4ctf'));
|
|
552
|
+
}
|
|
507
553
|
console.log();
|
|
508
554
|
return 'exit';
|
|
509
555
|
}
|
|
@@ -542,6 +588,7 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
542
588
|
console.log();
|
|
543
589
|
printMarkdown(response.text);
|
|
544
590
|
drawTokenBar();
|
|
591
|
+
maybeWarnTokenUsage(chatTokensUsed, EXAM_AI4CTF_CAP, _ai4ctfWarned);
|
|
545
592
|
console.log();
|
|
546
593
|
}
|
|
547
594
|
catch (err) {
|
|
@@ -176,11 +176,18 @@ export async function handleCtf4aiMessage(input) {
|
|
|
176
176
|
// ═══════════════════════════════════════════════════════════════
|
|
177
177
|
// Real-exam CTF4AI chat mode (Q39-40)
|
|
178
178
|
// ═══════════════════════════════════════════════════════════════
|
|
179
|
-
function printExamCtf4aiWelcome(q, qNum) {
|
|
179
|
+
function printExamCtf4aiWelcome(q, qNum, existingAnswer) {
|
|
180
180
|
const config = getConfig();
|
|
181
181
|
const modelName = config.geminiModel || 'gemma-4-31b-it';
|
|
182
|
+
const isResume = ctf4aiTokens > 0;
|
|
182
183
|
console.log();
|
|
183
184
|
console.log(chalk.red.bold(` ═══ CTF4AI — Q${qNum}: ${q.category} (${q.points || 16} pts) ═══`));
|
|
185
|
+
if (isResume) {
|
|
186
|
+
console.log(chalk.gray(` (resuming — prior chat is not remembered, but tokens already used stay deducted)`));
|
|
187
|
+
}
|
|
188
|
+
if (existingAnswer) {
|
|
189
|
+
console.log(chalk.gray(` Current answer: `) + chalk.yellow(existingAnswer) + chalk.gray(` (submit again to change)`));
|
|
190
|
+
}
|
|
184
191
|
console.log();
|
|
185
192
|
console.log(chalk.red(' ┌─────────────────────────────────────────────'));
|
|
186
193
|
console.log(chalk.red(' │ ') + chalk.bold.white(`Q${qNum} [${q.category}] · adversarial AI`));
|
|
@@ -259,9 +266,26 @@ export async function startExamCtf4aiChat(qNum, question) {
|
|
|
259
266
|
ctf4aiActive = true;
|
|
260
267
|
ctf4aiTokens = usedSoFar;
|
|
261
268
|
examCtf4aiCtx = { qNum, question };
|
|
262
|
-
|
|
269
|
+
const existingAnswer = state.answers?.[qNum];
|
|
270
|
+
printExamCtf4aiWelcome(question, qNum, existingAnswer);
|
|
263
271
|
return true;
|
|
264
272
|
}
|
|
273
|
+
const _ctf4aiWarned = new Set();
|
|
274
|
+
function maybeWarnCtf4aiUsage(used, cap) {
|
|
275
|
+
const pct = (used / cap) * 100;
|
|
276
|
+
if (pct >= 95 && !_ctf4aiWarned.has('95')) {
|
|
277
|
+
console.log(chalk.red.bold(` ⚠ ${Math.round(pct)}% of CTF4AI budget used — only ~${cap - used} tokens left.`));
|
|
278
|
+
_ctf4aiWarned.add('95');
|
|
279
|
+
}
|
|
280
|
+
else if (pct >= 80 && !_ctf4aiWarned.has('80')) {
|
|
281
|
+
console.log(chalk.yellow(` ⚠ 80% of CTF4AI budget used.`));
|
|
282
|
+
_ctf4aiWarned.add('80');
|
|
283
|
+
}
|
|
284
|
+
else if (pct >= 50 && !_ctf4aiWarned.has('50')) {
|
|
285
|
+
console.log(chalk.gray(` Note: 50% of CTF4AI budget used.`));
|
|
286
|
+
_ctf4aiWarned.add('50');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
265
289
|
async function handleExamCtf4aiMessage(input) {
|
|
266
290
|
if (!examCtf4aiCtx || !ctf4aiSession)
|
|
267
291
|
return 'exit';
|
|
@@ -297,6 +321,13 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
297
321
|
const submitMatch = trimmed.match(/^submit\s+(.+)/i);
|
|
298
322
|
if (submitMatch) {
|
|
299
323
|
const flag = submitMatch[1].trim();
|
|
324
|
+
if (/^[A-Da-d]$/.test(flag)) {
|
|
325
|
+
console.log();
|
|
326
|
+
console.log(chalk.yellow(` "${flag}" looks like an MCQ letter, not a flag.`));
|
|
327
|
+
console.log(chalk.gray(' Flag format: ') + chalk.green('ICOA{your_flag}') + chalk.gray('. Try again.'));
|
|
328
|
+
console.log();
|
|
329
|
+
return 'continue';
|
|
330
|
+
}
|
|
300
331
|
const { getExamState, saveExamState } = await import('../lib/exam-state.js');
|
|
301
332
|
const state = getExamState();
|
|
302
333
|
if (!state)
|
|
@@ -312,32 +343,29 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
312
343
|
result: 'via ctf4ai chat',
|
|
313
344
|
});
|
|
314
345
|
state.answers[examCtf4aiCtx.qNum] = flag;
|
|
315
|
-
|
|
346
|
+
// Advance _lastQ so `ctf4ai` after exit picks up Q40 automatically.
|
|
347
|
+
const nextQ = examCtf4aiCtx.qNum + 1;
|
|
348
|
+
state._lastQ = nextQ <= 40 ? nextQ : examCtf4aiCtx.qNum;
|
|
316
349
|
saveExamState(state);
|
|
317
350
|
console.log();
|
|
318
|
-
|
|
351
|
+
if (prevAnswer) {
|
|
352
|
+
console.log(chalk.green(` ✓ Q${examCtf4aiCtx.qNum} answer updated: `) + chalk.yellow(flag));
|
|
353
|
+
console.log(chalk.gray(` Previous: ${prevAnswer}`));
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
console.log(chalk.green.bold(` ✓ Answer for Q${examCtf4aiCtx.qNum} recorded: ${flag}`));
|
|
357
|
+
}
|
|
319
358
|
console.log(chalk.gray(' (Grading happens at exam submit.)'));
|
|
320
359
|
console.log();
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
examCtf4aiCtx = null;
|
|
325
|
-
if (savedQ === 39) {
|
|
326
|
-
console.log(chalk.red(' ─────────────────────────────────────────────'));
|
|
327
|
-
console.log(chalk.white(' Next: ') + chalk.bold.red('Q40 — final question'));
|
|
328
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam q 40') + chalk.gray(' jump to Q40'));
|
|
329
|
-
console.log(chalk.gray(' → ') + chalk.bold.red('ctf4ai') + chalk.gray(' start CTF4AI for Q40'));
|
|
330
|
-
console.log(chalk.red(' ─────────────────────────────────────────────'));
|
|
360
|
+
console.log(chalk.gray(' ') + chalk.white('submit ICOA{...}') + chalk.gray(' again to change, or ') + chalk.white('exit') + chalk.gray(' to move on'));
|
|
361
|
+
if (nextQ <= 40) {
|
|
362
|
+
console.log(chalk.gray(' After exit: ') + chalk.red('ctf4ai') + chalk.gray(` will open Q${nextQ}`));
|
|
331
363
|
}
|
|
332
364
|
else {
|
|
333
|
-
console.log(chalk.
|
|
334
|
-
console.log(chalk.bold.green(' All 40 questions answered! Time to submit.'));
|
|
335
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam review') + chalk.gray(' sanity-check your answers'));
|
|
336
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam submit') + chalk.gray(' final submission'));
|
|
337
|
-
console.log(chalk.green(' ─────────────────────────────────────────────'));
|
|
365
|
+
console.log(chalk.gray(' After exit: all 40 answered → ') + chalk.cyan('exam submit'));
|
|
338
366
|
}
|
|
339
367
|
console.log();
|
|
340
|
-
return '
|
|
368
|
+
return 'continue';
|
|
341
369
|
}
|
|
342
370
|
// Shell
|
|
343
371
|
if (input.startsWith('!')) {
|
|
@@ -362,12 +390,25 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
362
390
|
// Exit
|
|
363
391
|
if (lower === 'exit' || lower === 'back' || lower === 'quit') {
|
|
364
392
|
const savedQ = examCtf4aiCtx.qNum;
|
|
393
|
+
const { getExamState } = await import('../lib/exam-state.js');
|
|
394
|
+
const exitState = getExamState();
|
|
395
|
+
const hasAnswer = exitState && exitState.answers[savedQ] != null;
|
|
396
|
+
const lastQ = (exitState?._lastQ) || savedQ;
|
|
365
397
|
ctf4aiActive = false;
|
|
366
398
|
ctf4aiSession = null;
|
|
367
399
|
examCtf4aiCtx = null;
|
|
400
|
+
_ctf4aiWarned.clear();
|
|
368
401
|
console.log();
|
|
369
402
|
console.log(chalk.gray(` CTF4AI chat ended for Q${savedQ}.`));
|
|
370
|
-
|
|
403
|
+
if (hasAnswer && savedQ === 39 && lastQ <= 40) {
|
|
404
|
+
console.log(chalk.white(' Next: ') + chalk.bold.red('Q40') + chalk.gray(' — type ') + chalk.red('ctf4ai') + chalk.gray(' to continue'));
|
|
405
|
+
}
|
|
406
|
+
else if (hasAnswer && savedQ === 40) {
|
|
407
|
+
console.log(chalk.bold.green(' All 40 answered! ') + chalk.gray('Review + submit: ') + chalk.cyan('exam review') + chalk.gray(' · ') + chalk.cyan('exam submit'));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
console.log(chalk.gray(' Resume: ') + chalk.white(`exam q ${savedQ}`) + chalk.gray(' · Re-enter: ') + chalk.white('ctf4ai'));
|
|
411
|
+
}
|
|
371
412
|
console.log();
|
|
372
413
|
return 'exit';
|
|
373
414
|
}
|
|
@@ -398,6 +439,7 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
398
439
|
console.log();
|
|
399
440
|
const pct = Math.round((ctf4aiTokens / EXAM_CTF4AI_CAP) * 100);
|
|
400
441
|
console.log(chalk.gray(` [${ctf4aiTokens}/${EXAM_CTF4AI_CAP} CTF4AI tokens · ${pct}%]`));
|
|
442
|
+
maybeWarnCtf4aiUsage(ctf4aiTokens, EXAM_CTF4AI_CAP);
|
|
401
443
|
console.log();
|
|
402
444
|
}
|
|
403
445
|
catch (err) {
|