icoa-cli 2.2.3 → 2.3.0
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.d.ts +2 -0
- package/dist/commands/ai4ctf.js +110 -0
- package/dist/index.js +3 -1
- package/dist/lib/budget.d.ts +6 -0
- package/dist/lib/budget.js +13 -0
- package/dist/lib/gemini.d.ts +7 -0
- package/dist/lib/gemini.js +60 -1
- package/dist/repl.js +8 -4
- package/package.json +1 -1
|
@@ -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.
|
|
40
|
+
${chalk.gray('CLI-Native Competition Terminal v2.3.0')}
|
|
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 })
|
package/dist/lib/budget.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/dist/lib/budget.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/gemini.d.ts
CHANGED
|
@@ -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
|
+
}>;
|
package/dist/lib/gemini.js
CHANGED
|
@@ -37,7 +37,7 @@ 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
43
|
function getApiKey() {
|
|
@@ -112,3 +112,62 @@ ${text}`;
|
|
|
112
112
|
export function setApiKey(key) {
|
|
113
113
|
saveConfig({ geminiApiKey: key });
|
|
114
114
|
}
|
|
115
|
+
const CHAT_SYSTEM_PROMPT = `You are an AI teammate in the ICOA cybersecurity CTF competition (International Cyber Olympiad in AI 2026, Sydney).
|
|
116
|
+
|
|
117
|
+
You're a friendly, knowledgeable hacking partner — like a fellow competitor sitting next to the user. Be conversational, encouraging, and collaborative.
|
|
118
|
+
|
|
119
|
+
RULES:
|
|
120
|
+
- Help the competitor think through challenges, brainstorm approaches, explain concepts
|
|
121
|
+
- You MAY discuss vulnerability types, tools, techniques, and methodologies
|
|
122
|
+
- You MAY suggest approaches and help debug code
|
|
123
|
+
- Do NOT provide complete working exploits or full solution scripts
|
|
124
|
+
- Do NOT provide flags or flag fragments
|
|
125
|
+
- Never output anything matching flag format: icoa{...}
|
|
126
|
+
- If you don't know something, say so honestly
|
|
127
|
+
- Keep responses concise unless the user asks for detail
|
|
128
|
+
- When the user opens a challenge, use the context to give relevant advice`;
|
|
129
|
+
export async function createChatSession(context) {
|
|
130
|
+
let apiKey = getApiKey();
|
|
131
|
+
if (!apiKey) {
|
|
132
|
+
try {
|
|
133
|
+
const { input } = await import('@inquirer/prompts');
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(' Gemini API key not configured.');
|
|
136
|
+
console.log(' Get one free at: https://aistudio.google.com/apikey');
|
|
137
|
+
console.log();
|
|
138
|
+
apiKey = await input({ message: 'Enter your Gemini API Key:' });
|
|
139
|
+
if (apiKey.trim()) {
|
|
140
|
+
apiKey = apiKey.trim();
|
|
141
|
+
saveConfig({ geminiApiKey: apiKey });
|
|
142
|
+
console.log(' Key saved for future use.');
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw new Error('No API key provided.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error('Gemini API key not configured. Run: setup');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const config = getConfig();
|
|
154
|
+
const modelName = config.geminiModel || 'gemini-2.5-flash';
|
|
155
|
+
const ai = getClient(apiKey);
|
|
156
|
+
let systemPrompt = CHAT_SYSTEM_PROMPT;
|
|
157
|
+
if (context) {
|
|
158
|
+
systemPrompt += `\n\nThe competitor is currently working on:\nChallenge: ${context.name}\nCategory: ${context.category}`;
|
|
159
|
+
}
|
|
160
|
+
const chat = ai.chats.create({
|
|
161
|
+
model: modelName,
|
|
162
|
+
config: { systemInstruction: systemPrompt },
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
async sendMessage(msg) {
|
|
166
|
+
const response = await chat.sendMessage({ message: msg });
|
|
167
|
+
const text = filterFlagPatterns(response.text ?? '');
|
|
168
|
+
const usage = response.usageMetadata;
|
|
169
|
+
const tokensUsed = usage?.totalTokenCount || ((usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0));
|
|
170
|
+
return { text, tokensUsed };
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
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.
|
|
30
|
+
const VERSION = '2.3.0';
|
|
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
|
|
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'));
|