icoa-cli 2.19.37 → 2.19.38
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/ctf4ai-demo.js +2 -17
- package/dist/index.js +2 -0
- package/dist/lib/gemini.d.ts +1 -1
- package/dist/lib/gemini.js +52 -44
- package/dist/lib/update-check.d.ts +1 -0
- package/dist/lib/update-check.js +77 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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) {
|
package/dist/lib/gemini.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/gemini.js
CHANGED
|
@@ -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
|
-
//
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
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,77 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getIcoaDir } from './config.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
|
|
8
|
+
function isNewer(latest, current) {
|
|
9
|
+
const l = latest.split('.').map(Number);
|
|
10
|
+
const c = current.split('.').map(Number);
|
|
11
|
+
for (let i = 0; i < Math.max(l.length, c.length); i++) {
|
|
12
|
+
const lv = l[i] ?? 0;
|
|
13
|
+
const cv = c[i] ?? 0;
|
|
14
|
+
if (lv > cv)
|
|
15
|
+
return true;
|
|
16
|
+
if (lv < cv)
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export function checkForUpdates() {
|
|
22
|
+
// Fire-and-forget async check
|
|
23
|
+
setTimeout(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
26
|
+
const current = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
27
|
+
const cacheFile = join(getIcoaDir(), 'update-check.json');
|
|
28
|
+
// Check if we already checked recently
|
|
29
|
+
if (existsSync(cacheFile)) {
|
|
30
|
+
try {
|
|
31
|
+
const cache = JSON.parse(readFileSync(cacheFile, 'utf-8'));
|
|
32
|
+
if (Date.now() - cache.lastCheck < SIX_HOURS_MS) {
|
|
33
|
+
// Still within cooldown — but show banner if cached version is newer
|
|
34
|
+
if (cache.latestVersion && isNewer(cache.latestVersion, current)) {
|
|
35
|
+
console.log(chalk.yellow(' \u2B06 Update available: ') +
|
|
36
|
+
chalk.white('v' + current + ' \u2192 v' + cache.latestVersion) +
|
|
37
|
+
chalk.gray(' Run: npm install -g icoa-cli@latest'));
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Corrupted cache, continue with fresh check
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Fetch latest version from npm with 5-second timeout
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
49
|
+
const res = await fetch('https://registry.npmjs.org/icoa-cli/latest', {
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
headers: { 'Accept': 'application/json' },
|
|
52
|
+
});
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
return;
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
const latest = data.version;
|
|
58
|
+
if (!latest)
|
|
59
|
+
return;
|
|
60
|
+
// Save cache
|
|
61
|
+
const cache = {
|
|
62
|
+
lastCheck: Date.now(),
|
|
63
|
+
latestVersion: latest,
|
|
64
|
+
};
|
|
65
|
+
writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
66
|
+
// Print banner if newer
|
|
67
|
+
if (isNewer(latest, current)) {
|
|
68
|
+
console.log(chalk.yellow(' \u2B06 Update available: ') +
|
|
69
|
+
chalk.white('v' + current + ' \u2192 v' + latest) +
|
|
70
|
+
chalk.gray(' Run: npm install -g icoa-cli@latest'));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Completely silent on any failure
|
|
75
|
+
}
|
|
76
|
+
}, 0);
|
|
77
|
+
}
|