icoa-cli 2.19.62 → 2.19.64
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 +56 -23
- package/dist/commands/ctf4ai-demo.js +52 -21
- package/dist/lib/update-check.js +25 -6
- package/package.json +1 -1
package/dist/commands/ai4ctf.js
CHANGED
|
@@ -308,7 +308,7 @@ 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
314
|
const isResume = chatTokensUsed > 0;
|
|
@@ -317,6 +317,9 @@ function printExamAi4ctfWelcome(q, qNum) {
|
|
|
317
317
|
if (isResume) {
|
|
318
318
|
console.log(chalk.gray(` (resuming — prior chat is not remembered, but tokens already used stay deducted)`));
|
|
319
319
|
}
|
|
320
|
+
if (existingAnswer) {
|
|
321
|
+
console.log(chalk.gray(` Current answer: `) + chalk.yellow(existingAnswer) + chalk.gray(` (submit again to change)`));
|
|
322
|
+
}
|
|
320
323
|
console.log();
|
|
321
324
|
console.log(chalk.cyan(' ┌─────────────────────────────────────────────'));
|
|
322
325
|
console.log(chalk.cyan(' │ ') + chalk.bold.white(`Q${qNum} [${q.category}] · ${q.points || 6} pts`));
|
|
@@ -391,9 +394,27 @@ export async function startExamAi4ctfChat(qNum, question) {
|
|
|
391
394
|
chatTokensUsed = usedSoFar;
|
|
392
395
|
tokensLocked = false;
|
|
393
396
|
examChatCtx = { qNum, question, usageField: 'ai4ctf' };
|
|
394
|
-
|
|
397
|
+
const existingAnswer = state.answers?.[qNum];
|
|
398
|
+
printExamAi4ctfWelcome(question, qNum, existingAnswer);
|
|
395
399
|
return true;
|
|
396
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();
|
|
397
418
|
async function handleExamAi4ctfMessage(input) {
|
|
398
419
|
if (!examChatCtx || !chatSession)
|
|
399
420
|
return 'exit';
|
|
@@ -449,7 +470,8 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
449
470
|
console.log(chalk.red(` Q${examChatCtx.qNum} not found in state.`));
|
|
450
471
|
return 'continue';
|
|
451
472
|
}
|
|
452
|
-
// 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.
|
|
453
475
|
const prevAnswer = state.answers[examChatCtx.qNum];
|
|
454
476
|
if (!state.interactions)
|
|
455
477
|
state.interactions = [];
|
|
@@ -461,33 +483,30 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
461
483
|
result: 'via ai4ctf chat',
|
|
462
484
|
});
|
|
463
485
|
state.answers[examChatCtx.qNum] = flag;
|
|
464
|
-
|
|
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;
|
|
465
490
|
saveExamState(state);
|
|
466
491
|
console.log();
|
|
467
|
-
|
|
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
|
+
}
|
|
468
499
|
console.log(chalk.gray(' (Grading happens at exam submit — you cannot preview correctness during the exam.)'));
|
|
469
500
|
console.log();
|
|
470
|
-
|
|
471
|
-
const nextQ = examChatCtx.qNum + 1;
|
|
472
|
-
const savedQ = examChatCtx.qNum;
|
|
473
|
-
chatActive = false;
|
|
474
|
-
chatSession = null;
|
|
475
|
-
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'));
|
|
476
502
|
if (nextQ <= 38) {
|
|
477
|
-
console.log(chalk.
|
|
478
|
-
console.log(chalk.white(' Next: ') + chalk.bold.green(`Q${nextQ}`));
|
|
479
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan(`exam q ${nextQ}`) + chalk.gray(' jump to question'));
|
|
480
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('ai4ctf') + chalk.gray(' start AI chat for Q') + String(nextQ));
|
|
481
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
503
|
+
console.log(chalk.gray(' After exit: ') + chalk.cyan('ai4ctf') + chalk.gray(` will open Q${nextQ}`));
|
|
482
504
|
}
|
|
483
505
|
else {
|
|
484
|
-
console.log(chalk.
|
|
485
|
-
console.log(chalk.bold.white(' AI4CTF section complete — Q39 begins CTF4AI.'));
|
|
486
|
-
console.log(chalk.gray(' → ') + chalk.bold.red('ctf4ai') + chalk.gray(' start CTF4AI for Q39'));
|
|
487
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
506
|
+
console.log(chalk.gray(' After exit: AI4CTF section complete. Next: ') + chalk.red('ctf4ai') + chalk.gray(' on Q39'));
|
|
488
507
|
}
|
|
489
508
|
console.log();
|
|
490
|
-
return '
|
|
509
|
+
return 'continue';
|
|
491
510
|
}
|
|
492
511
|
// Shell command
|
|
493
512
|
if (input.startsWith('!')) {
|
|
@@ -511,13 +530,26 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
511
530
|
}
|
|
512
531
|
// Exit chat
|
|
513
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;
|
|
514
538
|
chatActive = false;
|
|
515
539
|
chatSession = null;
|
|
516
|
-
const savedQ = examChatCtx.qNum;
|
|
517
540
|
examChatCtx = null;
|
|
541
|
+
_ai4ctfWarned.clear();
|
|
518
542
|
console.log();
|
|
519
543
|
console.log(chalk.gray(` AI4CTF chat ended for Q${savedQ}.`));
|
|
520
|
-
|
|
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
|
+
}
|
|
521
553
|
console.log();
|
|
522
554
|
return 'exit';
|
|
523
555
|
}
|
|
@@ -556,6 +588,7 @@ async function handleExamAi4ctfMessage(input) {
|
|
|
556
588
|
console.log();
|
|
557
589
|
printMarkdown(response.text);
|
|
558
590
|
drawTokenBar();
|
|
591
|
+
maybeWarnTokenUsage(chatTokensUsed, EXAM_AI4CTF_CAP, _ai4ctfWarned);
|
|
559
592
|
console.log();
|
|
560
593
|
}
|
|
561
594
|
catch (err) {
|
|
@@ -176,7 +176,7 @@ 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
182
|
const isResume = ctf4aiTokens > 0;
|
|
@@ -185,6 +185,9 @@ function printExamCtf4aiWelcome(q, qNum) {
|
|
|
185
185
|
if (isResume) {
|
|
186
186
|
console.log(chalk.gray(` (resuming — prior chat is not remembered, but tokens already used stay deducted)`));
|
|
187
187
|
}
|
|
188
|
+
if (existingAnswer) {
|
|
189
|
+
console.log(chalk.gray(` Current answer: `) + chalk.yellow(existingAnswer) + chalk.gray(` (submit again to change)`));
|
|
190
|
+
}
|
|
188
191
|
console.log();
|
|
189
192
|
console.log(chalk.red(' ┌─────────────────────────────────────────────'));
|
|
190
193
|
console.log(chalk.red(' │ ') + chalk.bold.white(`Q${qNum} [${q.category}] · adversarial AI`));
|
|
@@ -263,9 +266,26 @@ export async function startExamCtf4aiChat(qNum, question) {
|
|
|
263
266
|
ctf4aiActive = true;
|
|
264
267
|
ctf4aiTokens = usedSoFar;
|
|
265
268
|
examCtf4aiCtx = { qNum, question };
|
|
266
|
-
|
|
269
|
+
const existingAnswer = state.answers?.[qNum];
|
|
270
|
+
printExamCtf4aiWelcome(question, qNum, existingAnswer);
|
|
267
271
|
return true;
|
|
268
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
|
+
}
|
|
269
289
|
async function handleExamCtf4aiMessage(input) {
|
|
270
290
|
if (!examCtf4aiCtx || !ctf4aiSession)
|
|
271
291
|
return 'exit';
|
|
@@ -323,32 +343,29 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
323
343
|
result: 'via ctf4ai chat',
|
|
324
344
|
});
|
|
325
345
|
state.answers[examCtf4aiCtx.qNum] = flag;
|
|
326
|
-
|
|
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;
|
|
327
349
|
saveExamState(state);
|
|
328
350
|
console.log();
|
|
329
|
-
|
|
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
|
+
}
|
|
330
358
|
console.log(chalk.gray(' (Grading happens at exam submit.)'));
|
|
331
359
|
console.log();
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
examCtf4aiCtx = null;
|
|
336
|
-
if (savedQ === 39) {
|
|
337
|
-
console.log(chalk.red(' ─────────────────────────────────────────────'));
|
|
338
|
-
console.log(chalk.white(' Next: ') + chalk.bold.red('Q40 — final question'));
|
|
339
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam q 40') + chalk.gray(' jump to Q40'));
|
|
340
|
-
console.log(chalk.gray(' → ') + chalk.bold.red('ctf4ai') + chalk.gray(' start CTF4AI for Q40'));
|
|
341
|
-
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}`));
|
|
342
363
|
}
|
|
343
364
|
else {
|
|
344
|
-
console.log(chalk.
|
|
345
|
-
console.log(chalk.bold.green(' All 40 questions answered! Time to submit.'));
|
|
346
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam review') + chalk.gray(' sanity-check your answers'));
|
|
347
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam submit') + chalk.gray(' final submission'));
|
|
348
|
-
console.log(chalk.green(' ─────────────────────────────────────────────'));
|
|
365
|
+
console.log(chalk.gray(' After exit: all 40 answered → ') + chalk.cyan('exam submit'));
|
|
349
366
|
}
|
|
350
367
|
console.log();
|
|
351
|
-
return '
|
|
368
|
+
return 'continue';
|
|
352
369
|
}
|
|
353
370
|
// Shell
|
|
354
371
|
if (input.startsWith('!')) {
|
|
@@ -373,12 +390,25 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
373
390
|
// Exit
|
|
374
391
|
if (lower === 'exit' || lower === 'back' || lower === 'quit') {
|
|
375
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;
|
|
376
397
|
ctf4aiActive = false;
|
|
377
398
|
ctf4aiSession = null;
|
|
378
399
|
examCtf4aiCtx = null;
|
|
400
|
+
_ctf4aiWarned.clear();
|
|
379
401
|
console.log();
|
|
380
402
|
console.log(chalk.gray(` CTF4AI chat ended for Q${savedQ}.`));
|
|
381
|
-
|
|
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
|
+
}
|
|
382
412
|
console.log();
|
|
383
413
|
return 'exit';
|
|
384
414
|
}
|
|
@@ -409,6 +439,7 @@ async function handleExamCtf4aiMessage(input) {
|
|
|
409
439
|
console.log();
|
|
410
440
|
const pct = Math.round((ctf4aiTokens / EXAM_CTF4AI_CAP) * 100);
|
|
411
441
|
console.log(chalk.gray(` [${ctf4aiTokens}/${EXAM_CTF4AI_CAP} CTF4AI tokens · ${pct}%]`));
|
|
442
|
+
maybeWarnCtf4aiUsage(ctf4aiTokens, EXAM_CTF4AI_CAP);
|
|
412
443
|
console.log();
|
|
413
444
|
}
|
|
414
445
|
catch (err) {
|
package/dist/lib/update-check.js
CHANGED
|
@@ -11,6 +11,29 @@ const installCmd = platform() === 'win32'
|
|
|
11
11
|
: 'sudo npm install -g icoa-cli@latest';
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
|
|
14
|
+
function versionsBehind(latest, current) {
|
|
15
|
+
// Count PATCH-level diff only (e.g. 2.19.40 vs 2.19.46 = 6)
|
|
16
|
+
const l = latest.split('.').map(Number);
|
|
17
|
+
const c = current.split('.').map(Number);
|
|
18
|
+
if (l[0] !== c[0] || l[1] !== c[1])
|
|
19
|
+
return -1; // major/minor diff
|
|
20
|
+
return (l[2] ?? 0) - (c[2] ?? 0);
|
|
21
|
+
}
|
|
22
|
+
function printUpdateBanner(current, latest) {
|
|
23
|
+
const behind = versionsBehind(latest, current);
|
|
24
|
+
const behindStr = behind > 0 ? chalk.gray(` (${behind} version${behind === 1 ? '' : 's'} behind)`) : '';
|
|
25
|
+
const line = chalk.yellow(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(line);
|
|
28
|
+
console.log(chalk.bold.yellow(' ⬆ New version available!'));
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(chalk.white(' Current: ') + chalk.gray('v' + current));
|
|
31
|
+
console.log(chalk.white(' Latest: ') + chalk.bold.green('v' + latest) + behindStr);
|
|
32
|
+
console.log();
|
|
33
|
+
console.log(chalk.white(' Update: ') + chalk.bold.cyan(installCmd));
|
|
34
|
+
console.log(line);
|
|
35
|
+
console.log();
|
|
36
|
+
}
|
|
14
37
|
function isNewer(latest, current) {
|
|
15
38
|
const l = latest.split('.').map(Number);
|
|
16
39
|
const c = current.split('.').map(Number);
|
|
@@ -38,9 +61,7 @@ export function checkForUpdates() {
|
|
|
38
61
|
if (Date.now() - cache.lastCheck < SIX_HOURS_MS) {
|
|
39
62
|
// Still within cooldown — but show banner if cached version is newer
|
|
40
63
|
if (cache.latestVersion && isNewer(cache.latestVersion, current)) {
|
|
41
|
-
|
|
42
|
-
chalk.white('v' + current + ' \u2192 v' + cache.latestVersion) +
|
|
43
|
-
chalk.gray(` Run: ${installCmd}`));
|
|
64
|
+
printUpdateBanner(current, cache.latestVersion);
|
|
44
65
|
}
|
|
45
66
|
return;
|
|
46
67
|
}
|
|
@@ -71,9 +92,7 @@ export function checkForUpdates() {
|
|
|
71
92
|
writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
72
93
|
// Print banner if newer
|
|
73
94
|
if (isNewer(latest, current)) {
|
|
74
|
-
|
|
75
|
-
chalk.white('v' + current + ' \u2192 v' + latest) +
|
|
76
|
-
chalk.gray(` Run: ${installCmd}`));
|
|
95
|
+
printUpdateBanner(current, latest);
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
98
|
catch {
|