icoa-cli 2.19.37 → 2.19.39

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,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { createChatSession } from '../lib/gemini.js';
2
3
  import { logCommand } from '../lib/logger.js';
3
4
  import { printError } from '../lib/ui.js';
4
5
  import { getConfig } from '../lib/config.js';
@@ -200,23 +201,7 @@ export function registerCtf4aiDemoCommand(program) {
200
201
  console.log(chalk.gray(` ${t('ctf4aiQuit')}`));
201
202
  console.log();
202
203
  try {
203
- // Create chat with restrictive system prompt
204
- const { GoogleGenAI } = await import('@google/genai');
205
- const apiKey = process.env.GEMINI_API_KEY || config.geminiApiKey || 'AIzaSyBLjo2UjaFqWmFaCap0TpiXjo9cDqFmY-E';
206
- const ai = new GoogleGenAI({ apiKey });
207
- const chat = ai.chats.create({
208
- model: modelName,
209
- config: { systemInstruction: CTF4AI_SYSTEM_PROMPT },
210
- });
211
- ctf4aiSession = {
212
- async sendMessage(msg) {
213
- const response = await chat.sendMessage({ message: msg });
214
- const text = response.text ?? '';
215
- const usage = response.usageMetadata;
216
- const tokensUsed = usage?.totalTokenCount || ((usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0));
217
- return { text, tokensUsed };
218
- },
219
- };
204
+ ctf4aiSession = await createChatSession(undefined, CTF4AI_SYSTEM_PROMPT);
220
205
  ctf4aiActive = true;
221
206
  ctf4aiTokens = 0;
222
207
  console.log(chalk.red(' ctf4ai> ') + chalk.gray(`${t('ctf4aiPrompt')}`));
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { registerCtf4aiDemoCommand } from './commands/ctf4ai-demo.js';
18
18
  import { getConfig, saveConfig } from './lib/config.js';
19
19
  import { startRepl } from './repl.js';
20
20
  import { setTerminalTheme } from './lib/theme.js';
21
+ import { checkForUpdates } from './lib/update-check.js';
21
22
  import { readFileSync } from 'node:fs';
22
23
  import { fileURLToPath } from 'node:url';
23
24
  import { dirname, join } from 'node:path';
@@ -73,6 +74,7 @@ program
73
74
  .action((opts) => {
74
75
  // Force hacker theme: black background + green text
75
76
  setTerminalTheme();
77
+ checkForUpdates();
76
78
  console.log(BANNER);
77
79
  // If running interactively (no extra args or --resume), start REPL
78
80
  if (process.argv.length <= 2 || opts.resume) {
@@ -6,7 +6,7 @@ export declare function generateHint(level: HintLevel, question: string, context
6
6
  }>;
7
7
  export declare function translateText(text: string, targetLang: string): Promise<string>;
8
8
  export declare function setApiKey(key: string): void;
9
- export declare function createChatSession(context?: ChallengeContext): Promise<{
9
+ export declare function createChatSession(context?: ChallengeContext, customSystemPrompt?: string): Promise<{
10
10
  sendMessage: (msg: string) => Promise<{
11
11
  text: string;
12
12
  tokensUsed: number;
@@ -41,16 +41,12 @@ function buildSystemPrompt(level, context) {
41
41
  export function filterFlagPatterns(text) {
42
42
  return text.replace(/icoa\{[^}]*\}/gi, '[FLAG REDACTED]');
43
43
  }
44
- // Default shared API key for competition (free tier)
45
- const DEFAULT_API_KEY = 'AIzaSyBLjo2UjaFqWmFaCap0TpiXjo9cDqFmY-E';
44
+ // No DEFAULT_API_KEY in client code keys stay server-side only.
45
+ // For demo mode (no user key), createChatSession routes through the
46
+ // server proxy at /api/icoa/ai/chat. For users with their own key
47
+ // (via setup or env var), we go direct to the Gemini SDK.
46
48
  function getApiKey() {
47
- const envKey = process.env.GEMINI_API_KEY;
48
- if (envKey)
49
- return envKey;
50
- const config = getConfig();
51
- if (config.geminiApiKey)
52
- return config.geminiApiKey;
53
- return DEFAULT_API_KEY;
49
+ return process.env.GEMINI_API_KEY || getConfig().geminiApiKey || '';
54
50
  }
55
51
  function getClient(apiKey) {
56
52
  return new GoogleGenAI({ apiKey });
@@ -141,47 +137,59 @@ RULES:
141
137
  - If you don't know something, say so honestly
142
138
  - Keep responses concise unless the user asks for detail
143
139
  - When the user opens a challenge, use the context to give relevant advice`;
144
- export async function createChatSession(context) {
145
- let apiKey = getApiKey();
146
- if (!apiKey) {
147
- try {
148
- const { input } = await import('@inquirer/prompts');
149
- console.log();
150
- console.log(chalk.yellow(' Gemini API key not configured.'));
151
- console.log(chalk.gray(' Get one free at: ') + chalk.cyan('https://aistudio.google.com/apikey'));
152
- console.log();
153
- apiKey = await input({ message: 'Enter your Gemini API Key:' });
154
- if (apiKey.trim()) {
155
- apiKey = apiKey.trim();
156
- saveConfig({ geminiApiKey: apiKey });
157
- console.log(chalk.green(' Key saved for future use.'));
158
- console.log();
159
- }
160
- else {
161
- throw new Error('No API key provided.');
162
- }
163
- }
164
- catch {
165
- throw new Error('Gemini API key not configured. Run: setup');
166
- }
167
- }
140
+ export async function createChatSession(context, customSystemPrompt) {
168
141
  const config = getConfig();
169
- const modelName = config.geminiModel || 'gemma-4-31b-it';
170
- const ai = getClient(apiKey);
171
- let systemPrompt = CHAT_SYSTEM_PROMPT;
142
+ const apiKey = getApiKey();
143
+ let systemPrompt = customSystemPrompt || CHAT_SYSTEM_PROMPT;
172
144
  if (context) {
173
145
  systemPrompt += `\n\nThe competitor is currently working on:\nChallenge: ${context.name}\nCategory: ${context.category}`;
174
146
  }
175
- const chat = ai.chats.create({
176
- model: modelName,
177
- config: { systemInstruction: systemPrompt },
178
- });
147
+ // ─── Path A: user has their own key → direct Gemini SDK ───
148
+ if (apiKey) {
149
+ const modelName = config.geminiModel || 'gemini-2.5-flash';
150
+ const ai = getClient(apiKey);
151
+ const chat = ai.chats.create({
152
+ model: modelName,
153
+ config: { systemInstruction: systemPrompt },
154
+ });
155
+ return {
156
+ async sendMessage(msg) {
157
+ const response = await chat.sendMessage({ message: msg });
158
+ const text = filterFlagPatterns(response.text ?? '');
159
+ const usage = response.usageMetadata;
160
+ const tokensUsed = usage?.totalTokenCount || ((usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0));
161
+ return { text, tokensUsed };
162
+ },
163
+ };
164
+ }
165
+ // ─── Path B: no key → server proxy (key stays server-side) ───
166
+ const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
167
+ const fp = config.deviceFingerprint || '';
168
+ const modelName = config.geminiModel || 'gemini-2.5-flash';
169
+ const messages = [];
179
170
  return {
180
171
  async sendMessage(msg) {
181
- const response = await chat.sendMessage({ message: msg });
182
- const text = filterFlagPatterns(response.text ?? '');
183
- const usage = response.usageMetadata;
184
- const tokensUsed = usage?.totalTokenCount || ((usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0));
172
+ messages.push({ role: 'user', text: msg });
173
+ const res = await fetch(`${serverUrl}/api/icoa/ai/chat`, {
174
+ method: 'POST',
175
+ headers: { 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({
177
+ systemPrompt,
178
+ messages,
179
+ model: modelName,
180
+ maxTokens: 2048,
181
+ deviceFingerprint: fp,
182
+ }),
183
+ signal: AbortSignal.timeout(60_000),
184
+ });
185
+ if (!res.ok) {
186
+ const err = await res.json().catch(() => ({ message: 'AI proxy error' }));
187
+ throw new Error(err.message || `AI proxy returned ${res.status}`);
188
+ }
189
+ const json = await res.json();
190
+ const text = filterFlagPatterns(json.data?.text || '');
191
+ const tokensUsed = json.data?.tokensUsed || 0;
192
+ messages.push({ role: 'model', text });
185
193
  return { text, tokensUsed };
186
194
  },
187
195
  };
@@ -0,0 +1 @@
1
+ export declare function checkForUpdates(): void;
@@ -0,0 +1,83 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { platform } from 'node:os';
5
+ import chalk from 'chalk';
6
+ import { getIcoaDir } from './config.js';
7
+ const installCmd = platform() === 'win32'
8
+ ? 'npm install -g icoa-cli@latest'
9
+ : platform() === 'darwin'
10
+ ? 'npm install -g icoa-cli@latest'
11
+ : 'sudo npm install -g icoa-cli@latest';
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
14
+ function isNewer(latest, current) {
15
+ const l = latest.split('.').map(Number);
16
+ const c = current.split('.').map(Number);
17
+ for (let i = 0; i < Math.max(l.length, c.length); i++) {
18
+ const lv = l[i] ?? 0;
19
+ const cv = c[i] ?? 0;
20
+ if (lv > cv)
21
+ return true;
22
+ if (lv < cv)
23
+ return false;
24
+ }
25
+ return false;
26
+ }
27
+ export function checkForUpdates() {
28
+ // Fire-and-forget async check
29
+ setTimeout(async () => {
30
+ try {
31
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
32
+ const current = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
33
+ const cacheFile = join(getIcoaDir(), 'update-check.json');
34
+ // Check if we already checked recently
35
+ if (existsSync(cacheFile)) {
36
+ try {
37
+ const cache = JSON.parse(readFileSync(cacheFile, 'utf-8'));
38
+ if (Date.now() - cache.lastCheck < SIX_HOURS_MS) {
39
+ // Still within cooldown — but show banner if cached version is newer
40
+ if (cache.latestVersion && isNewer(cache.latestVersion, current)) {
41
+ console.log(chalk.yellow(' \u2B06 Update available: ') +
42
+ chalk.white('v' + current + ' \u2192 v' + cache.latestVersion) +
43
+ chalk.gray(` Run: ${installCmd}`));
44
+ }
45
+ return;
46
+ }
47
+ }
48
+ catch {
49
+ // Corrupted cache, continue with fresh check
50
+ }
51
+ }
52
+ // Fetch latest version from npm with 5-second timeout
53
+ const controller = new AbortController();
54
+ const timeout = setTimeout(() => controller.abort(), 5000);
55
+ const res = await fetch('https://registry.npmjs.org/icoa-cli/latest', {
56
+ signal: controller.signal,
57
+ headers: { 'Accept': 'application/json' },
58
+ });
59
+ clearTimeout(timeout);
60
+ if (!res.ok)
61
+ return;
62
+ const data = await res.json();
63
+ const latest = data.version;
64
+ if (!latest)
65
+ return;
66
+ // Save cache
67
+ const cache = {
68
+ lastCheck: Date.now(),
69
+ latestVersion: latest,
70
+ };
71
+ writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
72
+ // Print banner if newer
73
+ if (isNewer(latest, current)) {
74
+ console.log(chalk.yellow(' \u2B06 Update available: ') +
75
+ chalk.white('v' + current + ' \u2192 v' + latest) +
76
+ chalk.gray(` Run: ${installCmd}`));
77
+ }
78
+ }
79
+ catch {
80
+ // Completely silent on any failure
81
+ }
82
+ }, 0);
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.37",
3
+ "version": "2.19.39",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {