icoa-cli 2.2.3 → 2.3.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.
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerAi4ctfCommand(program: Command): void;
@@ -0,0 +1,110 @@
1
+ import { createInterface } from 'node:readline';
2
+ import chalk from 'chalk';
3
+ import { createChatSession } from '../lib/gemini.js';
4
+ import { isTokenCapReached, addTokenUsage, getTokenUsage } from '../lib/budget.js';
5
+ import { getConfig } from '../lib/config.js';
6
+ import { logCommand } from '../lib/logger.js';
7
+ import { printMarkdown, printError, printInfo } from '../lib/ui.js';
8
+ function getChallengeContext() {
9
+ const config = getConfig();
10
+ if (config.currentChallengeName && config.currentChallengeCategory) {
11
+ return { name: config.currentChallengeName, category: config.currentChallengeCategory };
12
+ }
13
+ return undefined;
14
+ }
15
+ export function registerAi4ctfCommand(program) {
16
+ program
17
+ .command('ai4ctf')
18
+ .description('Chat with your AI teammate')
19
+ .action(async () => {
20
+ logCommand('ai4ctf');
21
+ // Check token cap
22
+ if (isTokenCapReached()) {
23
+ printError('Token budget exhausted. No more AI interactions available.');
24
+ return;
25
+ }
26
+ const context = getChallengeContext();
27
+ const tokenState = getTokenUsage();
28
+ // Create chat session
29
+ let chat;
30
+ try {
31
+ chat = await createChatSession(context);
32
+ }
33
+ catch (err) {
34
+ printError(err.message);
35
+ return;
36
+ }
37
+ // Welcome banner
38
+ console.log();
39
+ console.log(chalk.magenta(' ┌─────────────────────────────────────────┐'));
40
+ console.log(chalk.magenta(' │') + chalk.bold.white(' AI Teammate — Chat Mode') + chalk.magenta(' │'));
41
+ if (context) {
42
+ const ctxStr = ` Challenge: ${context.name} (${context.category})`;
43
+ console.log(chalk.magenta(' │') + chalk.gray(ctxStr.padEnd(41)) + chalk.magenta('│'));
44
+ }
45
+ const tokenStr = ` Tokens: ${tokenState.used.toLocaleString()}/${tokenState.cap.toLocaleString()}`;
46
+ console.log(chalk.magenta(' │') + chalk.gray(tokenStr.padEnd(41)) + chalk.magenta('│'));
47
+ console.log(chalk.magenta(' │') + chalk.gray(" Type 'exit' to return".padEnd(41)) + chalk.magenta('│'));
48
+ console.log(chalk.magenta(' └─────────────────────────────────────────┘'));
49
+ console.log();
50
+ // Inner chat REPL
51
+ return new Promise((resolve) => {
52
+ const chatRl = createInterface({
53
+ input: process.stdin,
54
+ output: process.stdout,
55
+ prompt: chalk.magenta('ai> '),
56
+ terminal: true,
57
+ });
58
+ chatRl.prompt();
59
+ chatRl.on('line', async (line) => {
60
+ const input = line.trim();
61
+ if (!input) {
62
+ chatRl.prompt();
63
+ return;
64
+ }
65
+ // Exit commands
66
+ if (input === 'exit' || input === 'back' || input === 'quit') {
67
+ chatRl.close();
68
+ return;
69
+ }
70
+ // Token cap check
71
+ if (isTokenCapReached()) {
72
+ console.log();
73
+ printError('Token budget exhausted. Exiting chat mode.');
74
+ chatRl.close();
75
+ return;
76
+ }
77
+ // Log the chat message
78
+ logCommand(`ai4ctf: ${input}`);
79
+ // Send message
80
+ console.log(chalk.gray(' Thinking...'));
81
+ try {
82
+ const response = await chat.sendMessage(input);
83
+ // Clear "Thinking..." line
84
+ process.stdout.write('\x1b[1A\x1b[2K');
85
+ // Track tokens
86
+ addTokenUsage(response.tokensUsed);
87
+ const updated = getTokenUsage();
88
+ // Display response
89
+ console.log();
90
+ printMarkdown(response.text);
91
+ console.log(chalk.gray(` [${response.tokensUsed.toLocaleString()} tokens | ${updated.used.toLocaleString()}/${updated.cap.toLocaleString()} total]`));
92
+ console.log();
93
+ }
94
+ catch (err) {
95
+ // Clear "Thinking..." line
96
+ process.stdout.write('\x1b[1A\x1b[2K');
97
+ printError(`AI error: ${err.message}`);
98
+ console.log();
99
+ }
100
+ chatRl.prompt();
101
+ });
102
+ chatRl.on('close', () => {
103
+ console.log();
104
+ printInfo('Returning to ICOA terminal.');
105
+ console.log();
106
+ resolve();
107
+ });
108
+ });
109
+ });
110
+ }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerLogCommand } from './commands/log.js';
12
12
  import { registerLangCommand } from './commands/lang.js';
13
13
  import { registerSetupCommand } from './commands/setup.js';
14
14
  import { registerEnvCommand } from './commands/env.js';
15
+ import { registerAi4ctfCommand } from './commands/ai4ctf.js';
15
16
  import { getConfig, saveConfig } from './lib/config.js';
16
17
  import { startRepl } from './repl.js';
17
18
  import { setTerminalTheme } from './lib/theme.js';
@@ -36,7 +37,7 @@ ${LINE}
36
37
  ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
37
38
  ${chalk.cyan.underline('https://icoa2026.au')}
38
39
 
39
- ${chalk.gray('CLI-Native Competition Terminal v2.2.3')}
40
+ ${chalk.gray('CLI-Native Competition Terminal v2.3.1')}
40
41
 
41
42
  ${LINE}
42
43
  `;
@@ -81,6 +82,7 @@ registerLogCommand(program);
81
82
  registerLangCommand(program);
82
83
  registerSetupCommand(program);
83
84
  registerEnvCommand(program);
85
+ registerAi4ctfCommand(program);
84
86
  // Hidden command: switch AI model
85
87
  program
86
88
  .command('model', { hidden: true })
@@ -6,3 +6,9 @@ export declare function checkBudget(level: HintLevel): {
6
6
  export declare function deductBudget(level: HintLevel, tokensUsed: number): void;
7
7
  export declare function getBudgetDisplay(): string;
8
8
  export declare function isTokenCapReached(): boolean;
9
+ export declare function addTokenUsage(tokensUsed: number): void;
10
+ export declare function getTokenUsage(): {
11
+ used: number;
12
+ cap: number;
13
+ remaining: number;
14
+ };
@@ -27,3 +27,16 @@ export function isTokenCapReached() {
27
27
  const budget = getBudget();
28
28
  return budget.tokensUsed >= budget.tokenCap;
29
29
  }
30
+ export function addTokenUsage(tokensUsed) {
31
+ const budget = getBudget();
32
+ budget.tokensUsed += tokensUsed;
33
+ saveBudget(budget);
34
+ }
35
+ export function getTokenUsage() {
36
+ const budget = getBudget();
37
+ return {
38
+ used: budget.tokensUsed,
39
+ cap: budget.tokenCap,
40
+ remaining: Math.max(0, budget.tokenCap - budget.tokensUsed),
41
+ };
42
+ }
@@ -1,7 +1,14 @@
1
1
  import type { HintLevel, ChallengeContext } from '../types/index.js';
2
+ export declare function filterFlagPatterns(text: string): string;
2
3
  export declare function generateHint(level: HintLevel, question: string, context?: ChallengeContext): Promise<{
3
4
  text: string;
4
5
  tokensUsed: number;
5
6
  }>;
6
7
  export declare function translateText(text: string, targetLang: string): Promise<string>;
7
8
  export declare function setApiKey(key: string): void;
9
+ export declare function createChatSession(context?: ChallengeContext): Promise<{
10
+ sendMessage: (msg: string) => Promise<{
11
+ text: string;
12
+ tokensUsed: number;
13
+ }>;
14
+ }>;
@@ -37,9 +37,11 @@ function buildSystemPrompt(level, context) {
37
37
  }
38
38
  return prompt;
39
39
  }
40
- function filterFlagPatterns(text) {
40
+ export function filterFlagPatterns(text) {
41
41
  return text.replace(/icoa\{[^}]*\}/gi, '[FLAG REDACTED]');
42
42
  }
43
+ // Default shared API key for competition (free tier)
44
+ const DEFAULT_API_KEY = 'AIzaSyB7XgD1n1S5sCtSabkaXKSVk3L2D9es6As';
43
45
  function getApiKey() {
44
46
  const envKey = process.env.GEMINI_API_KEY;
45
47
  if (envKey)
@@ -47,7 +49,7 @@ function getApiKey() {
47
49
  const config = getConfig();
48
50
  if (config.geminiApiKey)
49
51
  return config.geminiApiKey;
50
- return '';
52
+ return DEFAULT_API_KEY;
51
53
  }
52
54
  function getClient(apiKey) {
53
55
  return new GoogleGenAI({ apiKey });
@@ -112,3 +114,62 @@ ${text}`;
112
114
  export function setApiKey(key) {
113
115
  saveConfig({ geminiApiKey: key });
114
116
  }
117
+ const CHAT_SYSTEM_PROMPT = `You are an AI teammate in the ICOA cybersecurity CTF competition (International Cyber Olympiad in AI 2026, Sydney).
118
+
119
+ You're a friendly, knowledgeable hacking partner — like a fellow competitor sitting next to the user. Be conversational, encouraging, and collaborative.
120
+
121
+ RULES:
122
+ - Help the competitor think through challenges, brainstorm approaches, explain concepts
123
+ - You MAY discuss vulnerability types, tools, techniques, and methodologies
124
+ - You MAY suggest approaches and help debug code
125
+ - Do NOT provide complete working exploits or full solution scripts
126
+ - Do NOT provide flags or flag fragments
127
+ - Never output anything matching flag format: icoa{...}
128
+ - If you don't know something, say so honestly
129
+ - Keep responses concise unless the user asks for detail
130
+ - When the user opens a challenge, use the context to give relevant advice`;
131
+ export async function createChatSession(context) {
132
+ let apiKey = getApiKey();
133
+ if (!apiKey) {
134
+ try {
135
+ const { input } = await import('@inquirer/prompts');
136
+ console.log();
137
+ console.log(' Gemini API key not configured.');
138
+ console.log(' Get one free at: https://aistudio.google.com/apikey');
139
+ console.log();
140
+ apiKey = await input({ message: 'Enter your Gemini API Key:' });
141
+ if (apiKey.trim()) {
142
+ apiKey = apiKey.trim();
143
+ saveConfig({ geminiApiKey: apiKey });
144
+ console.log(' Key saved for future use.');
145
+ console.log();
146
+ }
147
+ else {
148
+ throw new Error('No API key provided.');
149
+ }
150
+ }
151
+ catch {
152
+ throw new Error('Gemini API key not configured. Run: setup');
153
+ }
154
+ }
155
+ const config = getConfig();
156
+ const modelName = config.geminiModel || 'gemini-2.5-flash';
157
+ const ai = getClient(apiKey);
158
+ let systemPrompt = CHAT_SYSTEM_PROMPT;
159
+ if (context) {
160
+ systemPrompt += `\n\nThe competitor is currently working on:\nChallenge: ${context.name}\nCategory: ${context.category}`;
161
+ }
162
+ const chat = ai.chats.create({
163
+ model: modelName,
164
+ config: { systemInstruction: systemPrompt },
165
+ });
166
+ return {
167
+ async sendMessage(msg) {
168
+ const response = await chat.sendMessage({ message: msg });
169
+ const text = filterFlagPatterns(response.text ?? '');
170
+ const usage = response.usageMetadata;
171
+ const tokensUsed = usage?.totalTokenCount || ((usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0));
172
+ return { text, tokensUsed };
173
+ },
174
+ };
175
+ }
package/dist/repl.js CHANGED
@@ -27,7 +27,7 @@ const BLOCKED_COMMANDS = new Set([
27
27
  'iptables', 'ufw', // firewall
28
28
  ]);
29
29
  const INTERCEPT = '__REPL_NO_EXIT__';
30
- const VERSION = '2.2.3';
30
+ const VERSION = '2.3.1';
31
31
  export async function startRepl(program, resumeMode) {
32
32
  const config = getConfig();
33
33
  const connected = isConnected();
@@ -212,7 +212,7 @@ export async function startRepl(program, resumeMode) {
212
212
  'join', 'activate', 'challenges', 'ch', 'open', 'submit', 'flag',
213
213
  'scoreboard', 'sb', 'status', 'time', 'hint', 'hint-b', 'hint-c',
214
214
  'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
215
- 'log', 'lang', 'setup', 'env', 'model', 'ctf',
215
+ 'log', 'lang', 'setup', 'env', 'ai4ctf', 'model', 'ctf',
216
216
  ];
217
217
  if (!knownCommands.includes(cmd)) {
218
218
  // Block dangerous commands
@@ -287,6 +287,8 @@ export async function startRepl(program, resumeMode) {
287
287
  process.exit = (() => {
288
288
  throw new Error(INTERCEPT);
289
289
  });
290
+ // Pause main readline so sub-REPLs (ai4ctf) can use stdin
291
+ rl.pause();
290
292
  try {
291
293
  await program.parseAsync(['node', 'icoa', ...args]);
292
294
  }
@@ -308,6 +310,7 @@ export async function startRepl(program, resumeMode) {
308
310
  finally {
309
311
  process.exit = realExit;
310
312
  processing = false;
313
+ rl.resume();
311
314
  }
312
315
  console.log();
313
316
  rl.prompt();
@@ -360,7 +363,7 @@ function mapCommand(input) {
360
363
  const directCommands = [
361
364
  'hint', 'hint-b', 'hint-c', 'hint-budget',
362
365
  'ref', 'shell', 'files', 'connect', 'note',
363
- 'log', 'lang', 'setup', 'model',
366
+ 'log', 'lang', 'setup', 'env', 'ai4ctf', 'model',
364
367
  'ctf',
365
368
  ];
366
369
  if (directCommands.includes(cmd)) {
@@ -388,7 +391,8 @@ function printReplHelp(activated) {
388
391
  console.log(chalk.white(' status ') + chalk.gray('Competition status'));
389
392
  console.log(chalk.white(' time ') + chalk.gray('Countdown timer'));
390
393
  console.log();
391
- console.log(chalk.bold.white(' AI Hints'));
394
+ console.log(chalk.bold.white(' AI'));
395
+ console.log(chalk.white(' ai4ctf ') + chalk.gray('Chat with your AI teammate'));
392
396
  console.log(chalk.white(' hint <question> ') + chalk.gray('Level A — General guidance'));
393
397
  console.log(chalk.white(' hint-b <question> ') + chalk.gray('Level B — Deep analysis'));
394
398
  console.log(chalk.white(' hint-c <question> ') + chalk.gray('Level C — Critical assist'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {