icoa-cli 2.16.17 → 2.17.1

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.
@@ -1,9 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import { createChatSession } from '../lib/gemini.js';
3
- import { isTokenCapReached, addTokenUsage, getTokenUsage } from '../lib/budget.js';
3
+ import { addTokenUsage } from '../lib/budget.js';
4
4
  import { getConfig } from '../lib/config.js';
5
5
  import { logCommand } from '../lib/logger.js';
6
- import { printMarkdown, printError, printInfo } from '../lib/ui.js';
6
+ import { printMarkdown, printError } from '../lib/ui.js';
7
7
  function getChallengeContext() {
8
8
  const config = getConfig();
9
9
  if (config.currentChallengeName && config.currentChallengeCategory) {
@@ -14,24 +14,60 @@ function getChallengeContext() {
14
14
  // Chat state — shared with REPL
15
15
  let chatActive = false;
16
16
  let chatSession = null;
17
+ let chatTokensUsed = 0;
18
+ const DEMO_TOKEN_CAP = 5000;
17
19
  export function isChatActive() {
18
20
  return chatActive;
19
21
  }
22
+ function drawTokenBar() {
23
+ const cap = DEMO_TOKEN_CAP;
24
+ const used = chatTokensUsed;
25
+ const pct = Math.min(Math.round((used / cap) * 100), 100);
26
+ const width = 20;
27
+ const filled = Math.round((pct / 100) * width);
28
+ const empty = width - filled;
29
+ const color = pct > 80 ? chalk.red : pct > 50 ? chalk.yellow : chalk.green;
30
+ console.log(chalk.gray(' Tokens: ') + color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty)) + chalk.gray(` ${used}/${cap} (${pct}%)`));
31
+ }
20
32
  export async function handleChatMessage(input) {
21
33
  if (!chatSession)
22
34
  return 'exit';
23
35
  if (input === 'exit' || input === 'back' || input === 'quit') {
24
36
  chatActive = false;
25
37
  chatSession = null;
38
+ // Anonymous stats
39
+ fetch('https://practice.icoa2026.au:9090/api/icoa/demo-stats', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ type: 'ai4ctf', tokensUsed: chatTokensUsed, timestamp: new Date().toISOString() }),
43
+ signal: AbortSignal.timeout(5000),
44
+ }).catch(() => { });
45
+ console.log();
46
+ console.log(chalk.gray(' ─────────────────────────────────────────'));
47
+ console.log(chalk.white(' AI4CTF Session Report'));
48
+ console.log(chalk.gray(` Tokens used: ${chatTokensUsed}/${DEMO_TOKEN_CAP}`));
49
+ console.log(chalk.gray(` AI Model: Google Gemma 4 (gemma-4-31b-it)`));
50
+ console.log(chalk.gray(' ─────────────────────────────────────────'));
26
51
  console.log();
27
- printInfo('Returning to ICOA terminal.');
52
+ console.log(chalk.white(' Next: try ') + chalk.bold.red('ctf4ai') + chalk.white(' — trick the AI into saying "koala"'));
28
53
  console.log();
29
54
  return 'exit';
30
55
  }
31
- if (isTokenCapReached()) {
56
+ if (chatTokensUsed >= DEMO_TOKEN_CAP) {
32
57
  chatActive = false;
33
58
  chatSession = null;
34
- printError('Token budget exhausted. Exiting chat mode.');
59
+ console.log();
60
+ console.log(chalk.yellow(' Token limit reached!'));
61
+ drawTokenBar();
62
+ console.log();
63
+ console.log(chalk.gray(' ─────────────────────────────────────────'));
64
+ console.log(chalk.white(' AI4CTF Session Report'));
65
+ console.log(chalk.gray(` Tokens used: ${chatTokensUsed}/${DEMO_TOKEN_CAP}`));
66
+ console.log(chalk.gray(` AI Model: Google Gemma 4 (gemma-4-31b-it)`));
67
+ console.log(chalk.gray(' ─────────────────────────────────────────'));
68
+ console.log();
69
+ console.log(chalk.white(' Next: try ') + chalk.bold.red('ctf4ai') + chalk.white(' — trick the AI into saying "koala"'));
70
+ console.log();
35
71
  return 'exit';
36
72
  }
37
73
  logCommand(`ai4ctf: ${input}`);
@@ -39,11 +75,11 @@ export async function handleChatMessage(input) {
39
75
  try {
40
76
  const response = await chatSession.sendMessage(input);
41
77
  process.stdout.write('\x1b[1A\x1b[2K');
78
+ chatTokensUsed += response.tokensUsed;
42
79
  addTokenUsage(response.tokensUsed);
43
- const updated = getTokenUsage();
44
80
  console.log();
45
81
  printMarkdown(response.text);
46
- console.log(chalk.gray(` [${response.tokensUsed.toLocaleString()} tokens | ${updated.used.toLocaleString()}/${updated.cap.toLocaleString()} total]`));
82
+ drawTokenBar();
47
83
  console.log();
48
84
  }
49
85
  catch (err) {
@@ -59,32 +95,55 @@ export function registerAi4ctfCommand(program) {
59
95
  .description('Chat with your AI teammate')
60
96
  .action(async () => {
61
97
  logCommand('ai4ctf');
62
- if (isTokenCapReached()) {
63
- printError('Token budget exhausted. No more AI interactions available.');
64
- return;
65
- }
66
- const context = getChallengeContext();
67
- const tokenState = getTokenUsage();
98
+ const config = getConfig();
99
+ const modelName = config.geminiModel || 'gemma-4-31b-it';
100
+ // Demo challenge context
101
+ const demoContext = {
102
+ name: 'Hidden Message',
103
+ category: 'Cryptography',
104
+ };
68
105
  try {
69
- chatSession = await createChatSession(context);
106
+ chatSession = await createChatSession(demoContext);
70
107
  }
71
108
  catch (err) {
72
109
  printError(err.message);
73
110
  return;
74
111
  }
75
112
  chatActive = true;
76
- // Welcome banner
113
+ chatTokensUsed = 0;
114
+ // Guided welcome
77
115
  console.log();
78
- console.log(chalk.magenta(' ┌─────────────────────────────────────────┐'));
79
- console.log(chalk.magenta(' │') + chalk.bold.white(' AI Teammate — Chat Mode') + chalk.magenta(' │'));
80
- if (context) {
81
- const ctxStr = ` Challenge: ${context.name} (${context.category})`;
82
- console.log(chalk.magenta(' ') + chalk.gray(ctxStr.padEnd(41)) + chalk.magenta('│'));
83
- }
84
- const tokenStr = ` Tokens: ${tokenState.used.toLocaleString()}/${tokenState.cap.toLocaleString()}`;
85
- console.log(chalk.magenta(' │') + chalk.gray(tokenStr.padEnd(41)) + chalk.magenta('│'));
86
- console.log(chalk.magenta(' │') + chalk.gray(" Type 'exit' to return".padEnd(41)) + chalk.magenta('│'));
87
- console.log(chalk.magenta(' └─────────────────────────────────────────┘'));
116
+ console.log(chalk.green.bold(' ═══ AI4CTF Demo — AI as Your Teammate ═══'));
117
+ console.log();
118
+ console.log(chalk.white(' Here\'s a sample CTF challenge:'));
119
+ console.log();
120
+ console.log(chalk.cyan(' ┌─────────────────────────────────────────────────┐'));
121
+ console.log(chalk.cyan(' │') + chalk.bold.white(' Challenge: Hidden Message [Cryptography] ') + chalk.cyan('│'));
122
+ console.log(chalk.cyan(' │') + chalk.white(' ') + chalk.cyan('│'));
123
+ console.log(chalk.cyan(' │') + chalk.white(' You intercepted this encoded text: ') + chalk.cyan('│'));
124
+ console.log(chalk.cyan(' │') + chalk.green(' aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ== ') + chalk.cyan('│'));
125
+ console.log(chalk.cyan(' ') + chalk.white(' ') + chalk.cyan('│'));
126
+ console.log(chalk.cyan(' │') + chalk.white(' Can you decode it to find the flag? ') + chalk.cyan('│'));
127
+ console.log(chalk.cyan(' │') + chalk.gray(' Flag format: icoa{...} ') + chalk.cyan('│'));
128
+ console.log(chalk.cyan(' └─────────────────────────────────────────────────┘'));
129
+ console.log();
130
+ console.log(chalk.white(' In competition, you get AI help at 3 levels:'));
131
+ console.log();
132
+ console.log(chalk.yellow(' hint a') + chalk.gray(' General guidance — "What type of encoding is this?"'));
133
+ console.log(chalk.gray(' 50 uses per competition. Safe to use freely.'));
134
+ console.log(chalk.yellow(' hint b') + chalk.gray(' Deep analysis — "How do I decode Base64?"'));
135
+ console.log(chalk.gray(' 10 uses. Use when you\'re stuck.'));
136
+ console.log(chalk.yellow(' hint c') + chalk.gray(' Critical assist — Nearly gives you the answer'));
137
+ console.log(chalk.gray(' 2 uses only. Last resort!'));
138
+ console.log();
139
+ console.log(chalk.gray(' ─────────────────────────────────────────'));
140
+ console.log(chalk.white(' Try now: ask the AI anything about the challenge above.'));
141
+ console.log(chalk.gray(' Example: "What encoding is aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ==?"'));
142
+ console.log(chalk.gray(' Or just chat freely! You can also try hint a, hint b, hint c.'));
143
+ console.log();
144
+ drawTokenBar();
145
+ console.log(chalk.gray(` AI Model: Google Gemma 4 (${modelName})`));
146
+ console.log(chalk.gray(' Type "exit" when done.'));
88
147
  console.log();
89
148
  });
90
149
  }
@@ -27,6 +27,12 @@ export async function handleCtf4aiMessage(input) {
27
27
  if (input === 'exit' || input === 'back' || input === 'quit') {
28
28
  ctf4aiActive = false;
29
29
  ctf4aiSession = null;
30
+ fetch('https://practice.icoa2026.au:9090/api/icoa/demo-stats', {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ type: 'ctf4ai', solved: false, tokensUsed: ctf4aiTokens, timestamp: new Date().toISOString() }),
34
+ signal: AbortSignal.timeout(5000),
35
+ }).catch(() => { });
30
36
  console.log();
31
37
  console.log(chalk.gray(' CTF4AI challenge ended.'));
32
38
  console.log(chalk.gray(` Tokens used: ${ctf4aiTokens}/${CTF4AI_TOKEN_LIMIT}`));
@@ -67,6 +73,12 @@ export async function handleCtf4aiMessage(input) {
67
73
  console.log();
68
74
  ctf4aiActive = false;
69
75
  ctf4aiSession = null;
76
+ fetch('https://practice.icoa2026.au:9090/api/icoa/demo-stats', {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({ type: 'ctf4ai', solved: true, tokensUsed: ctf4aiTokens, timestamp: new Date().toISOString() }),
80
+ signal: AbortSignal.timeout(5000),
81
+ }).catch(() => { });
70
82
  return 'solved';
71
83
  }
72
84
  return 'continue';
@@ -581,6 +581,22 @@ export function registerExamCommand(program) {
581
581
  }
582
582
  drawProgress(100, 'Complete!');
583
583
  console.log();
584
+ // Anonymous stats (fire-and-forget)
585
+ const helpInfo = getHelpState(state);
586
+ const statsLang = getConfig().language || 'en';
587
+ fetch('https://practice.icoa2026.au:9090/api/icoa/demo-stats', {
588
+ method: 'POST',
589
+ headers: { 'Content-Type': 'application/json' },
590
+ body: JSON.stringify({
591
+ score,
592
+ total: state.questions.length,
593
+ lang: statsLang,
594
+ helpUsed: helpInfo.used,
595
+ helpMax: helpInfo.max,
596
+ timestamp: new Date().toISOString(),
597
+ }),
598
+ signal: AbortSignal.timeout(5000),
599
+ }).catch(() => { });
584
600
  clearExamState();
585
601
  const pct = Math.round(score / state.questions.length * 100);
586
602
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.16.17",
3
+ "version": "2.17.1",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {