icoa-cli 2.19.77 → 2.19.79
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/exam.js +82 -17
- package/dist/lib/gemini.js +46 -10
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -1891,22 +1891,33 @@ export function registerExamCommand(program) {
|
|
|
1891
1891
|
console.log();
|
|
1892
1892
|
return;
|
|
1893
1893
|
}
|
|
1894
|
-
// Skip if already set up
|
|
1894
|
+
// Skip if already set up — but only if MOST packages actually installed.
|
|
1895
|
+
// If previous run had 0 or very few successes, retry instead of saying
|
|
1896
|
+
// "already set up" (which is misleading and strands the user).
|
|
1895
1897
|
const existingSetup = getExamSetup();
|
|
1896
1898
|
if (existingSetup) {
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
console.log(chalk.
|
|
1899
|
+
const installedCount = existingSetup.installedPackages.length;
|
|
1900
|
+
const totalAttempted = installedCount + existingSetup.failedPackages.length;
|
|
1901
|
+
const successRate = totalAttempted > 0 ? installedCount / totalAttempted : 0;
|
|
1902
|
+
if (installedCount >= 8 || successRate >= 0.7) {
|
|
1903
|
+
console.log();
|
|
1904
|
+
console.log(chalk.green(' ✓ ') + chalk.green('Environment already set up'));
|
|
1905
|
+
console.log(chalk.gray(` Completed: ${existingSetup.completedAt.split('T')[0]}`));
|
|
1906
|
+
console.log(chalk.gray(` Python: ${existingSetup.pythonVersion}`));
|
|
1907
|
+
console.log(chalk.gray(` Packages: ${installedCount} installed`));
|
|
1908
|
+
if (existingSetup.failedPackages.length > 0) {
|
|
1909
|
+
console.log(chalk.yellow(` Failed: ${existingSetup.failedPackages.join(', ')}`));
|
|
1910
|
+
}
|
|
1911
|
+
console.log();
|
|
1912
|
+
console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
|
|
1913
|
+
console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
|
|
1914
|
+
console.log();
|
|
1915
|
+
return;
|
|
1904
1916
|
}
|
|
1917
|
+
// Prior setup mostly failed — retry
|
|
1905
1918
|
console.log();
|
|
1906
|
-
console.log(chalk.
|
|
1907
|
-
console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
|
|
1919
|
+
console.log(chalk.yellow(` Previous setup had only ${installedCount}/${totalAttempted} packages. Retrying...`));
|
|
1908
1920
|
console.log();
|
|
1909
|
-
return;
|
|
1910
1921
|
}
|
|
1911
1922
|
console.log();
|
|
1912
1923
|
printHeader('Exam Environment Setup');
|
|
@@ -2006,11 +2017,39 @@ export function registerExamCommand(program) {
|
|
|
2006
2017
|
console.log();
|
|
2007
2018
|
console.log(chalk.white(` Installing ${PACKAGES.length} packages...`));
|
|
2008
2019
|
console.log();
|
|
2020
|
+
// Detect PEP 668 "externally-managed-environment" — modern Ubuntu/Debian/Kali
|
|
2021
|
+
// block system-wide pip installs. Try a harmless install to detect.
|
|
2022
|
+
let pipExtraFlags = '';
|
|
2023
|
+
try {
|
|
2024
|
+
execSync(`${pythonBin} -m pip install --dry-run pip 2>&1`, { encoding: 'utf-8', timeout: 10000 });
|
|
2025
|
+
}
|
|
2026
|
+
catch (e) {
|
|
2027
|
+
const msg = String(e?.stdout || e?.stderr || e?.message || '');
|
|
2028
|
+
if (/externally-managed-environment|break-system-packages/i.test(msg)) {
|
|
2029
|
+
// Pick the safer option: --user (installs to ~/.local, no sudo needed)
|
|
2030
|
+
pipExtraFlags = '--user --break-system-packages';
|
|
2031
|
+
console.log(chalk.yellow(' ℹ PEP 668 detected — using --user install (no sudo needed)'));
|
|
2032
|
+
console.log();
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
// Python 3.13 warning: pwntools may fail to build
|
|
2036
|
+
const pyParts = pythonVersion.split('.').map(Number);
|
|
2037
|
+
if (pyParts[0] === 3 && pyParts[1] >= 13) {
|
|
2038
|
+
console.log(chalk.yellow(` ⚠ Python ${pythonVersion} detected`));
|
|
2039
|
+
console.log(chalk.gray(' Some packages (pwntools, scapy) may not have wheels yet for 3.13.'));
|
|
2040
|
+
console.log(chalk.gray(' Python 3.12 is recommended. Install:'));
|
|
2041
|
+
if (process.platform === 'linux') {
|
|
2042
|
+
console.log(chalk.green(' sudo apt install python3.12 python3.12-venv'));
|
|
2043
|
+
console.log(chalk.green(' sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1'));
|
|
2044
|
+
}
|
|
2045
|
+
console.log();
|
|
2046
|
+
}
|
|
2009
2047
|
const installed = [];
|
|
2010
2048
|
const failed = [];
|
|
2049
|
+
let firstError = '';
|
|
2011
2050
|
for (const pkg of PACKAGES) {
|
|
2012
2051
|
try {
|
|
2013
|
-
execSync(`${pythonBin} -m pip install "${pkg}" --quiet 2
|
|
2052
|
+
execSync(`${pythonBin} -m pip install "${pkg}" ${pipExtraFlags} --quiet 2>&1`, {
|
|
2014
2053
|
encoding: 'utf-8', timeout: 180000, stdio: 'pipe',
|
|
2015
2054
|
});
|
|
2016
2055
|
const importName = IMPORT_MAP[pkg] || pkg;
|
|
@@ -2028,24 +2067,50 @@ export function registerExamCommand(program) {
|
|
|
2028
2067
|
console.log(chalk.green(` ✓ ${pkg}`) + (ver ? chalk.gray(` (${ver})`) : ''));
|
|
2029
2068
|
installed.push(ver ? `${pkg}==${ver}` : pkg);
|
|
2030
2069
|
}
|
|
2031
|
-
catch {
|
|
2032
|
-
|
|
2033
|
-
|
|
2070
|
+
catch (e) {
|
|
2071
|
+
const raw = String(e?.stdout || e?.stderr || e?.message || '').slice(-400);
|
|
2072
|
+
const short = raw.split('\n').filter((l) => l.trim()).slice(-2).join(' | ').slice(0, 120);
|
|
2073
|
+
console.log(chalk.red(` ✗ ${pkg}`) + chalk.gray(` ${short}`));
|
|
2074
|
+
failed.push({ pkg, error: short });
|
|
2075
|
+
if (!firstError)
|
|
2076
|
+
firstError = raw;
|
|
2034
2077
|
}
|
|
2035
2078
|
}
|
|
2079
|
+
// If wholesale failure, show the first full error so user can debug
|
|
2080
|
+
if (installed.length === 0 && firstError) {
|
|
2081
|
+
console.log();
|
|
2082
|
+
console.log(chalk.yellow(' First error detail (for debugging):'));
|
|
2083
|
+
console.log(chalk.gray(' ' + firstError.split('\n').slice(-6).join('\n ')));
|
|
2084
|
+
}
|
|
2036
2085
|
// Save state
|
|
2037
2086
|
saveExamSetup({
|
|
2038
2087
|
completedAt: new Date().toISOString(),
|
|
2039
2088
|
pythonVersion,
|
|
2040
2089
|
installedPackages: installed,
|
|
2041
|
-
failedPackages: failed,
|
|
2090
|
+
failedPackages: failed.map((f) => f.pkg),
|
|
2042
2091
|
});
|
|
2043
2092
|
console.log();
|
|
2044
2093
|
if (failed.length === 0) {
|
|
2045
2094
|
printSuccess(`Environment ready! All ${PACKAGES.length} packages installed.`);
|
|
2046
2095
|
}
|
|
2096
|
+
else if (installed.length === 0) {
|
|
2097
|
+
printError(`Setup failed — 0 of ${PACKAGES.length} packages installed.`);
|
|
2098
|
+
console.log();
|
|
2099
|
+
console.log(chalk.yellow(' Troubleshooting:'));
|
|
2100
|
+
if (pyParts[0] === 3 && pyParts[1] >= 13) {
|
|
2101
|
+
console.log(chalk.gray(' Python 3.13 is too new — most CTF libraries lack wheels.'));
|
|
2102
|
+
console.log(chalk.gray(' Install Python 3.12 and re-run exam setup.'));
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
console.log(chalk.gray(' • Check you have internet (pip.pypa.io must be reachable)'));
|
|
2106
|
+
console.log(chalk.gray(' • Try: ') + chalk.cyan(`${pythonBin} -m pip install pwntools`) + chalk.gray(' — see full error'));
|
|
2107
|
+
console.log(chalk.gray(' • On Kali/Ubuntu 22+: might need python3.12-dev + build-essential'));
|
|
2108
|
+
}
|
|
2109
|
+
console.log();
|
|
2110
|
+
console.log(chalk.white(' Rerun when fixed: ') + chalk.bold.cyan('exam setup'));
|
|
2111
|
+
}
|
|
2047
2112
|
else {
|
|
2048
|
-
printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
|
|
2113
|
+
printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.map((f) => f.pkg).join(', ')}`);
|
|
2049
2114
|
}
|
|
2050
2115
|
// Python usage tutorial for beginners
|
|
2051
2116
|
console.log();
|
package/dist/lib/gemini.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { GoogleGenAI } from '@google/genai';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
import { getConfig, saveConfig } from './config.js';
|
|
7
|
+
import { getRealExamState } from './exam-state.js';
|
|
8
|
+
const __dirname_gemini = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
let _cachedVersion = null;
|
|
10
|
+
function getCliVersion() {
|
|
11
|
+
if (_cachedVersion)
|
|
12
|
+
return _cachedVersion;
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(__dirname_gemini, '..', '..', 'package.json'), 'utf-8'));
|
|
15
|
+
_cachedVersion = pkg.version || 'unknown';
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
_cachedVersion = 'unknown';
|
|
19
|
+
}
|
|
20
|
+
return _cachedVersion;
|
|
21
|
+
}
|
|
4
22
|
const SYSTEM_PROMPTS = {
|
|
5
23
|
A: `You are an AI assistant in a cybersecurity CTF competition called ICOA.
|
|
6
24
|
You are providing Level A (General Guidance) to a competitor.
|
|
@@ -170,24 +188,42 @@ export async function createChatSession(context, customSystemPrompt) {
|
|
|
170
188
|
return {
|
|
171
189
|
async sendMessage(msg) {
|
|
172
190
|
messages.push({ role: 'user', text: msg });
|
|
191
|
+
// Attach exam token if contestant is in a real exam — grants full
|
|
192
|
+
// 2048 maxTokens and higher rate limit. Demo/anonymous users rely on
|
|
193
|
+
// the User-Agent gate on the server side.
|
|
194
|
+
const realExam = getRealExamState();
|
|
195
|
+
const payload = {
|
|
196
|
+
systemPrompt,
|
|
197
|
+
messages,
|
|
198
|
+
model: modelName,
|
|
199
|
+
maxTokens: 2048,
|
|
200
|
+
deviceFingerprint: fp,
|
|
201
|
+
};
|
|
202
|
+
if (realExam?.session?.token) {
|
|
203
|
+
payload.examToken = realExam.session.token;
|
|
204
|
+
}
|
|
173
205
|
const res = await fetch(`${serverUrl}/api/icoa/ai/chat`, {
|
|
174
206
|
method: 'POST',
|
|
175
|
-
headers: {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
maxTokens: 2048,
|
|
181
|
-
deviceFingerprint: fp,
|
|
182
|
-
}),
|
|
207
|
+
headers: {
|
|
208
|
+
'Content-Type': 'application/json',
|
|
209
|
+
'User-Agent': `icoa-cli/${getCliVersion()}`,
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify(payload),
|
|
183
212
|
signal: AbortSignal.timeout(60_000),
|
|
184
213
|
});
|
|
185
214
|
if (!res.ok) {
|
|
186
215
|
const err = await res.json().catch(() => ({ message: 'AI proxy error' }));
|
|
216
|
+
const msg = err.message || `AI proxy returned ${res.status}`;
|
|
217
|
+
if (res.status === 401) {
|
|
218
|
+
throw new Error(chalk.yellow('⚠ ') + 'Exam token expired. Re-enter via `exam <token>`.');
|
|
219
|
+
}
|
|
220
|
+
if (res.status === 403) {
|
|
221
|
+
throw new Error(chalk.yellow('⚠ ') + msg);
|
|
222
|
+
}
|
|
187
223
|
if (res.status === 429) {
|
|
188
|
-
throw new Error(chalk.yellow('⏳ ') +
|
|
224
|
+
throw new Error(chalk.yellow('⏳ ') + msg);
|
|
189
225
|
}
|
|
190
|
-
throw new Error(
|
|
226
|
+
throw new Error(msg);
|
|
191
227
|
}
|
|
192
228
|
const json = await res.json();
|
|
193
229
|
const text = filterFlagPatterns(json.data?.text || '');
|