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.
@@ -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
- console.log();
1898
- console.log(chalk.green(' ✓ ') + chalk.green('Environment already set up'));
1899
- console.log(chalk.gray(` Completed: ${existingSetup.completedAt.split('T')[0]}`));
1900
- console.log(chalk.gray(` Python: ${existingSetup.pythonVersion}`));
1901
- console.log(chalk.gray(` Packages: ${existingSetup.installedPackages.length} installed`));
1902
- if (existingSetup.failedPackages.length > 0) {
1903
- console.log(chalk.yellow(` Failed: ${existingSetup.failedPackages.join(', ')}`));
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.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
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>/dev/null`, {
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
- console.log(chalk.red(` ✗ ${pkg}`));
2033
- failed.push(pkg);
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();
@@ -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: { 'Content-Type': 'application/json' },
176
- body: JSON.stringify({
177
- systemPrompt,
178
- messages,
179
- model: modelName,
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('⏳ ') + (err.message || 'Too many requests. Please wait a moment and try again.'));
224
+ throw new Error(chalk.yellow('⏳ ') + msg);
189
225
  }
190
- throw new Error(err.message || `AI proxy returned ${res.status}`);
226
+ throw new Error(msg);
191
227
  }
192
228
  const json = await res.json();
193
229
  const text = filterFlagPatterns(json.data?.text || '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.77",
3
+ "version": "2.19.79",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {