icoa-cli 1.0.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.
Files changed (77) hide show
  1. package/dist/commands/connect.d.ts +2 -0
  2. package/dist/commands/connect.js +66 -0
  3. package/dist/commands/ctf.d.ts +2 -0
  4. package/dist/commands/ctf.js +472 -0
  5. package/dist/commands/files.d.ts +2 -0
  6. package/dist/commands/files.js +52 -0
  7. package/dist/commands/hint.d.ts +2 -0
  8. package/dist/commands/hint.js +107 -0
  9. package/dist/commands/lang.d.ts +2 -0
  10. package/dist/commands/lang.js +42 -0
  11. package/dist/commands/log.d.ts +2 -0
  12. package/dist/commands/log.js +36 -0
  13. package/dist/commands/note.d.ts +2 -0
  14. package/dist/commands/note.js +32 -0
  15. package/dist/commands/ref.d.ts +2 -0
  16. package/dist/commands/ref.js +63 -0
  17. package/dist/commands/setup.d.ts +2 -0
  18. package/dist/commands/setup.js +88 -0
  19. package/dist/commands/shell.d.ts +2 -0
  20. package/dist/commands/shell.js +55 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +78 -0
  23. package/dist/lib/budget.d.ts +8 -0
  24. package/dist/lib/budget.js +29 -0
  25. package/dist/lib/config.d.ts +7 -0
  26. package/dist/lib/config.js +60 -0
  27. package/dist/lib/ctfd-client.d.ts +22 -0
  28. package/dist/lib/ctfd-client.js +161 -0
  29. package/dist/lib/gemini.d.ts +7 -0
  30. package/dist/lib/gemini.js +108 -0
  31. package/dist/lib/logger.d.ts +6 -0
  32. package/dist/lib/logger.js +59 -0
  33. package/dist/lib/translation.d.ts +1 -0
  34. package/dist/lib/translation.js +40 -0
  35. package/dist/lib/ui.d.ts +10 -0
  36. package/dist/lib/ui.js +59 -0
  37. package/dist/types/index.d.ts +125 -0
  38. package/dist/types/index.js +29 -0
  39. package/package.json +43 -0
  40. package/refs/ROPgadget.txt +67 -0
  41. package/refs/base64.txt +63 -0
  42. package/refs/bash.txt +79 -0
  43. package/refs/binwalk.txt +43 -0
  44. package/refs/bs4.txt +61 -0
  45. package/refs/checksec.txt +57 -0
  46. package/refs/curl.txt +73 -0
  47. package/refs/cyberchef.txt +78 -0
  48. package/refs/exiftool.txt +50 -0
  49. package/refs/ffuf.txt +73 -0
  50. package/refs/gcc.txt +66 -0
  51. package/refs/gdb.txt +83 -0
  52. package/refs/hashcat.txt +64 -0
  53. package/refs/hint.txt +42 -0
  54. package/refs/icoa.txt +36 -0
  55. package/refs/john.txt +74 -0
  56. package/refs/linux.txt +58 -0
  57. package/refs/nc.txt +64 -0
  58. package/refs/nmap.txt +57 -0
  59. package/refs/numpy.txt +59 -0
  60. package/refs/openssl.txt +75 -0
  61. package/refs/pillow.txt +67 -0
  62. package/refs/pwntools.txt +79 -0
  63. package/refs/pycrypto.txt +77 -0
  64. package/refs/python.txt +94 -0
  65. package/refs/r2.txt +85 -0
  66. package/refs/regex.txt +73 -0
  67. package/refs/requests.txt +83 -0
  68. package/refs/rules.txt +28 -0
  69. package/refs/scapy.txt +80 -0
  70. package/refs/sqlmap.txt +69 -0
  71. package/refs/steghide.txt +71 -0
  72. package/refs/struct.txt +61 -0
  73. package/refs/sympy.txt +77 -0
  74. package/refs/tshark.txt +65 -0
  75. package/refs/vim.txt +74 -0
  76. package/refs/volatility.txt +41 -0
  77. package/refs/z3.txt +78 -0
@@ -0,0 +1,107 @@
1
+ import chalk from 'chalk';
2
+ import { confirm } from '@inquirer/prompts';
3
+ import { generateHint } from '../lib/gemini.js';
4
+ import { checkBudget, deductBudget, getBudgetDisplay, isTokenCapReached } from '../lib/budget.js';
5
+ import { getConfig } from '../lib/config.js';
6
+ import { logHint } from '../lib/logger.js';
7
+ import { printError, printInfo, printMarkdown, printHeader, createSpinner } from '../lib/ui.js';
8
+ function getChallengeContext() {
9
+ const config = getConfig();
10
+ if (config.currentChallengeName && config.currentChallengeCategory) {
11
+ return {
12
+ name: config.currentChallengeName,
13
+ category: config.currentChallengeCategory,
14
+ };
15
+ }
16
+ return undefined;
17
+ }
18
+ async function handleHint(level, question) {
19
+ const config = getConfig();
20
+ // Check token cap
21
+ if (isTokenCapReached()) {
22
+ printError('Token cap reached. No more AI hints available.');
23
+ return;
24
+ }
25
+ // Check budget
26
+ const { allowed, remaining } = checkBudget(level);
27
+ if (!allowed) {
28
+ printError(`Level ${level} hint budget exhausted (0 remaining).`);
29
+ return;
30
+ }
31
+ // Level C confirmation
32
+ if (level === 'C') {
33
+ const proceed = await confirm({
34
+ message: `This will consume 1 of your ${remaining} remaining Critical Assists. Continue?`,
35
+ default: false,
36
+ });
37
+ if (!proceed) {
38
+ printInfo('Cancelled.');
39
+ return;
40
+ }
41
+ }
42
+ // Log BEFORE API call
43
+ logHint(level, question, config.currentChallengeId || undefined);
44
+ const context = getChallengeContext();
45
+ const levelNames = {
46
+ A: 'General Guidance',
47
+ B: 'Deep Analysis',
48
+ C: 'Critical Assist',
49
+ };
50
+ const spinner = createSpinner(`Getting Level ${level} hint (${levelNames[level]})...`);
51
+ spinner.start();
52
+ try {
53
+ const result = await generateHint(level, question, context);
54
+ spinner.stop();
55
+ // Deduct budget after successful response
56
+ deductBudget(level, result.tokensUsed);
57
+ printHeader(`Level ${level} Hint — ${levelNames[level]}`);
58
+ printMarkdown(result.text);
59
+ console.log();
60
+ console.log(chalk.gray(` Tokens used: ${result.tokensUsed} | Level ${level} remaining: ${checkBudget(level).remaining}`));
61
+ console.log();
62
+ }
63
+ catch (err) {
64
+ spinner.fail('Hint generation failed');
65
+ printError(err.message);
66
+ }
67
+ }
68
+ function showBudget() {
69
+ printHeader('Hint Budget');
70
+ console.log(getBudgetDisplay());
71
+ console.log();
72
+ }
73
+ export function registerHintCommands(program) {
74
+ // ─── icoa hint <question> ───
75
+ // Special case: "icoa hint budget" shows budget instead of querying AI
76
+ program
77
+ .command('hint <question...>')
78
+ .description('Level A hint — General guidance (use "hint budget" to check remaining)')
79
+ .action(async (words) => {
80
+ if (words[0] === 'budget') {
81
+ showBudget();
82
+ return;
83
+ }
84
+ await handleHint('A', words.join(' '));
85
+ });
86
+ // ─── icoa hint-b <question> ───
87
+ program
88
+ .command('hint-b <question...>')
89
+ .description('Level B hint — Deep analysis')
90
+ .action(async (words) => {
91
+ await handleHint('B', words.join(' '));
92
+ });
93
+ // ─── icoa hint-c <question> ───
94
+ program
95
+ .command('hint-c <question...>')
96
+ .description('Level C hint — Critical assist (confirmation required)')
97
+ .action(async (words) => {
98
+ await handleHint('C', words.join(' '));
99
+ });
100
+ // ─── icoa hint-budget (alternative) ───
101
+ program
102
+ .command('hint-budget')
103
+ .description('Show remaining hint budget')
104
+ .action(() => {
105
+ showBudget();
106
+ });
107
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerLangCommand(program: Command): void;
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import { getConfig, saveConfig } from '../lib/config.js';
3
+ import { logCommand } from '../lib/logger.js';
4
+ import { printSuccess, printError, printInfo } from '../lib/ui.js';
5
+ import { SUPPORTED_LANGUAGES } from '../types/index.js';
6
+ const LANG_NAMES = {
7
+ en: 'English',
8
+ zh: '中文 (Chinese)',
9
+ ja: '日本語 (Japanese)',
10
+ ko: '한국어 (Korean)',
11
+ es: 'Español (Spanish)',
12
+ };
13
+ export function registerLangCommand(program) {
14
+ program
15
+ .command('lang [code]')
16
+ .description('Switch display language')
17
+ .action((code) => {
18
+ logCommand(`lang ${code || ''}`);
19
+ if (!code) {
20
+ const config = getConfig();
21
+ printInfo(`Current language: ${chalk.white(LANG_NAMES[config.language] || config.language)}`);
22
+ console.log();
23
+ console.log(chalk.gray(' Supported languages:'));
24
+ for (const lang of SUPPORTED_LANGUAGES) {
25
+ const current = config.language === lang ? chalk.yellow(' ← current') : '';
26
+ console.log(` ${chalk.white(lang)} ${LANG_NAMES[lang]}${current}`);
27
+ }
28
+ console.log();
29
+ console.log(chalk.gray(' Usage: icoa lang <code>'));
30
+ console.log(chalk.gray(' Example: icoa lang zh'));
31
+ console.log();
32
+ return;
33
+ }
34
+ if (!SUPPORTED_LANGUAGES.includes(code)) {
35
+ printError(`Unsupported language: ${code}`);
36
+ printInfo(`Supported: ${SUPPORTED_LANGUAGES.join(', ')}`);
37
+ return;
38
+ }
39
+ saveConfig({ language: code });
40
+ printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
41
+ });
42
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerLogCommand(program: Command): void;
@@ -0,0 +1,36 @@
1
+ import chalk from 'chalk';
2
+ import { getSessionLog } from '../lib/logger.js';
3
+ import { printHeader, printInfo, printTable } from '../lib/ui.js';
4
+ export function registerLogCommand(program) {
5
+ program
6
+ .command('log')
7
+ .description('Display session history')
8
+ .action(() => {
9
+ const entries = getSessionLog();
10
+ if (entries.length === 0) {
11
+ printInfo('No session log entries yet.');
12
+ return;
13
+ }
14
+ printHeader('Session Log');
15
+ const rows = entries.map((entry) => {
16
+ const time = entry.timestamp.replace('T', ' ').substring(0, 19);
17
+ const levelColor = {
18
+ A: chalk.green,
19
+ B: chalk.yellow,
20
+ C: chalk.red,
21
+ command: chalk.blue,
22
+ submit: chalk.magenta,
23
+ };
24
+ const colorFn = levelColor[entry.level] || chalk.gray;
25
+ const input = entry.input.length > 60 ? entry.input.substring(0, 57) + '...' : entry.input;
26
+ return [
27
+ chalk.gray(time),
28
+ colorFn(entry.level.padEnd(7)),
29
+ input,
30
+ ];
31
+ });
32
+ printTable(['Time', 'Type', 'Content'], rows);
33
+ console.log(chalk.gray(` ${entries.length} entries total`));
34
+ console.log();
35
+ });
36
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerNoteCommand(program: Command): void;
@@ -0,0 +1,32 @@
1
+ import { appendFileSync, readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import chalk from 'chalk';
5
+ import { printSuccess, printInfo, printHeader } from '../lib/ui.js';
6
+ import { logCommand } from '../lib/logger.js';
7
+ const NOTES_FILE = join(homedir(), 'icoa-notes.txt');
8
+ export function registerNoteCommand(program) {
9
+ program
10
+ .command('note [text...]')
11
+ .description('Add or view personal notes')
12
+ .action((words) => {
13
+ logCommand(`note ${words?.join(' ') || ''}`);
14
+ if (!words || words.length === 0) {
15
+ // Display existing notes
16
+ if (!existsSync(NOTES_FILE)) {
17
+ printInfo('No notes yet. Add one with: icoa note "your note here"');
18
+ return;
19
+ }
20
+ const content = readFileSync(NOTES_FILE, 'utf-8');
21
+ printHeader('Notes');
22
+ console.log(content);
23
+ return;
24
+ }
25
+ // Add new note
26
+ const text = words.join(' ');
27
+ const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
28
+ const entry = `[${timestamp}] ${text}\n`;
29
+ appendFileSync(NOTES_FILE, entry);
30
+ printSuccess(`Note saved: ${chalk.gray(text)}`);
31
+ });
32
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerRefCommand(program: Command): void;
@@ -0,0 +1,63 @@
1
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import chalk from 'chalk';
5
+ import { printHeader, printInfo, printError, printTable } from '../lib/ui.js';
6
+ import { logCommand } from '../lib/logger.js';
7
+ function getRefsDir() {
8
+ // When bundled, refs/ is at the package root alongside dist/
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ // Try multiple possible locations
11
+ const candidates = [
12
+ join(__dirname, '..', 'refs'), // from dist/index.js
13
+ join(__dirname, '..', '..', 'refs'), // from src/commands/
14
+ join(process.cwd(), 'refs'), // from CWD
15
+ ];
16
+ for (const dir of candidates) {
17
+ if (existsSync(dir))
18
+ return dir;
19
+ }
20
+ return candidates[0]; // fallback
21
+ }
22
+ export function registerRefCommand(program) {
23
+ program
24
+ .command('ref [topic]')
25
+ .description('Quick reference for tools and commands')
26
+ .action((topic) => {
27
+ logCommand(`ref ${topic || ''}`);
28
+ const refsDir = getRefsDir();
29
+ if (!topic) {
30
+ // List all available refs
31
+ printHeader('Available References');
32
+ if (!existsSync(refsDir)) {
33
+ printError('Reference files not found.');
34
+ return;
35
+ }
36
+ const files = readdirSync(refsDir)
37
+ .filter((f) => f.endsWith('.txt'))
38
+ .map((f) => f.replace('.txt', ''))
39
+ .sort();
40
+ if (files.length === 0) {
41
+ printInfo('No reference files available.');
42
+ return;
43
+ }
44
+ const rows = files.map((f) => [chalk.white(f)]);
45
+ printTable(['Topic'], rows);
46
+ console.log();
47
+ console.log(chalk.gray(` Usage: icoa ref <topic>`));
48
+ console.log(chalk.gray(` Example: icoa ref python`));
49
+ console.log();
50
+ return;
51
+ }
52
+ // Show specific ref
53
+ const filePath = join(refsDir, `${topic}.txt`);
54
+ if (!existsSync(filePath)) {
55
+ printError(`Reference not found: ${topic}`);
56
+ printInfo('Run "icoa ref" to see available topics.');
57
+ return;
58
+ }
59
+ const content = readFileSync(filePath, 'utf-8');
60
+ printHeader(`Reference: ${topic}`);
61
+ console.log(content);
62
+ });
63
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSetupCommand(program: Command): void;
@@ -0,0 +1,88 @@
1
+ import chalk from 'chalk';
2
+ import { input, select } from '@inquirer/prompts';
3
+ import { getConfig } from '../lib/config.js';
4
+ import { setApiKey } from '../lib/gemini.js';
5
+ import { printSuccess, printError, printInfo, printHeader, printKeyValue } from '../lib/ui.js';
6
+ import { logCommand } from '../lib/logger.js';
7
+ export function registerSetupCommand(program) {
8
+ program
9
+ .command('setup')
10
+ .description('Configure ICOA CLI settings')
11
+ .action(async () => {
12
+ logCommand('setup');
13
+ const config = getConfig();
14
+ printHeader('ICOA CLI Setup');
15
+ console.log();
16
+ // Show current configuration
17
+ printKeyValue('CTFd URL', config.ctfdUrl || chalk.gray('Not configured'));
18
+ printKeyValue('CTFd Token', config.token ? chalk.green('Configured') : chalk.gray('Not configured'));
19
+ printKeyValue('Gemini API Key', config.geminiApiKey || process.env.GEMINI_API_KEY ? chalk.green('Configured') : chalk.gray('Not configured'));
20
+ printKeyValue('Language', config.language);
21
+ printKeyValue('Session ID', config.sessionId.substring(0, 8) + '...');
22
+ console.log();
23
+ const action = await select({
24
+ message: 'What would you like to configure?',
25
+ choices: [
26
+ { name: 'Gemini API Key', value: 'gemini' },
27
+ { name: 'CTFd Connection', value: 'ctfd' },
28
+ { name: 'Reset Hint Budget', value: 'budget' },
29
+ { name: 'View All Settings', value: 'view' },
30
+ { name: 'Exit', value: 'exit' },
31
+ ],
32
+ });
33
+ switch (action) {
34
+ case 'gemini': {
35
+ console.log();
36
+ printInfo('Get your API key from: https://aistudio.google.com/apikey');
37
+ console.log();
38
+ const key = await input({ message: 'Enter Gemini API Key:' });
39
+ if (key.trim()) {
40
+ setApiKey(key.trim());
41
+ printSuccess('Gemini API key saved.');
42
+ }
43
+ else {
44
+ printError('No key provided.');
45
+ }
46
+ break;
47
+ }
48
+ case 'ctfd': {
49
+ printInfo('Use "icoa ctf join <url>" to connect to a CTFd instance.');
50
+ break;
51
+ }
52
+ case 'budget': {
53
+ const { confirm } = await import('@inquirer/prompts');
54
+ const proceed = await confirm({
55
+ message: 'Reset hint budget to defaults (A:50, B:10, C:2)? This cannot be undone.',
56
+ default: false,
57
+ });
58
+ if (proceed) {
59
+ const { saveBudget } = await import('../lib/config.js');
60
+ const { DEFAULT_BUDGET } = await import('../types/index.js');
61
+ saveBudget({ ...DEFAULT_BUDGET });
62
+ printSuccess('Hint budget reset to defaults.');
63
+ }
64
+ break;
65
+ }
66
+ case 'view': {
67
+ console.log();
68
+ printHeader('Full Configuration');
69
+ const full = getConfig();
70
+ for (const [key, value] of Object.entries(full)) {
71
+ if (key === 'token' && value) {
72
+ printKeyValue(key, value.toString().substring(0, 8) + '...');
73
+ }
74
+ else if (key === 'geminiApiKey' && value) {
75
+ printKeyValue(key, value.toString().substring(0, 8) + '...');
76
+ }
77
+ else {
78
+ printKeyValue(key, String(value ?? 'null'));
79
+ }
80
+ }
81
+ console.log();
82
+ break;
83
+ }
84
+ case 'exit':
85
+ break;
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerShellCommand(program: Command): void;
@@ -0,0 +1,55 @@
1
+ import { execSync } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ import { printError, printInfo, printSuccess } from '../lib/ui.js';
4
+ import { logCommand } from '../lib/logger.js';
5
+ export function registerShellCommand(program) {
6
+ program
7
+ .command('shell')
8
+ .description('Open interactive shell in Docker sandbox')
9
+ .action(async () => {
10
+ logCommand('shell');
11
+ // Check if Docker is available
12
+ try {
13
+ execSync('docker info', { stdio: 'ignore' });
14
+ }
15
+ catch {
16
+ printError('Docker is not available.');
17
+ console.log();
18
+ console.log(chalk.gray(' Docker is required for the ICOA sandbox environment.'));
19
+ console.log(chalk.gray(' Install Docker from: https://docs.docker.com/get-docker/'));
20
+ console.log();
21
+ return;
22
+ }
23
+ const imageName = 'icoa/sandbox:2026';
24
+ const containerName = `icoa-sandbox-${Date.now()}`;
25
+ // Check if image exists
26
+ try {
27
+ execSync(`docker image inspect ${imageName}`, { stdio: 'ignore' });
28
+ }
29
+ catch {
30
+ printInfo(`Sandbox image not found. Pulling ${imageName}...`);
31
+ try {
32
+ execSync(`docker pull ${imageName}`, { stdio: 'inherit' });
33
+ }
34
+ catch {
35
+ printError(`Failed to pull ${imageName}.`);
36
+ printInfo('The sandbox image may not be published yet.');
37
+ printInfo('You can build it locally: cd docker && docker build -t icoa/sandbox:2026 .');
38
+ return;
39
+ }
40
+ }
41
+ printSuccess('Launching sandbox...');
42
+ console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
43
+ console.log();
44
+ try {
45
+ execSync(`docker run --rm -it --name ${containerName} --network=host ${imageName} /bin/bash`, { stdio: 'inherit' });
46
+ console.log();
47
+ printSuccess('Sandbox session ended.');
48
+ }
49
+ catch {
50
+ // User likely just exited
51
+ console.log();
52
+ printInfo('Sandbox session ended.');
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { registerCtfCommands } from './commands/ctf.js';
5
+ import { registerHintCommands } from './commands/hint.js';
6
+ import { registerRefCommand } from './commands/ref.js';
7
+ import { registerShellCommand } from './commands/shell.js';
8
+ import { registerFilesCommand } from './commands/files.js';
9
+ import { registerConnectCommand } from './commands/connect.js';
10
+ import { registerNoteCommand } from './commands/note.js';
11
+ import { registerLogCommand } from './commands/log.js';
12
+ import { registerLangCommand } from './commands/lang.js';
13
+ import { registerSetupCommand } from './commands/setup.js';
14
+ import { isConnected, getConfig } from './lib/config.js';
15
+ const BANNER = `
16
+ ${chalk.cyan('╔══════════════════════════════════════════════════════════╗')}
17
+ ${chalk.cyan('║')} ${chalk.cyan('║')}
18
+ ${chalk.cyan('║')} ${chalk.bold.white('██╗ ██████╗ ██████╗ █████╗')} ${chalk.cyan('║')}
19
+ ${chalk.cyan('║')} ${chalk.bold.white('██║██╔════╝██╔═══██╗██╔══██╗')} ${chalk.cyan('║')}
20
+ ${chalk.cyan('║')} ${chalk.bold.white('██║██║ ██║ ██║███████║')} ${chalk.cyan('║')}
21
+ ${chalk.cyan('║')} ${chalk.bold.white('██║██║ ██║ ██║██╔══██║')} ${chalk.cyan('║')}
22
+ ${chalk.cyan('║')} ${chalk.bold.white('██║╚██████╗╚██████╔╝██║ ██║')} ${chalk.cyan('║')}
23
+ ${chalk.cyan('║')} ${chalk.bold.white('╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝')} ${chalk.cyan('║')}
24
+ ${chalk.cyan('║')} ${chalk.cyan('║')}
25
+ ${chalk.cyan('║')} ${chalk.yellow('International Cybersecurity Olympiad 2026')} ${chalk.cyan('║')}
26
+ ${chalk.cyan('║')} ${chalk.gray('CLI-Native CTF Competition Terminal v1.0.0')} ${chalk.cyan('║')}
27
+ ${chalk.cyan('║')} ${chalk.cyan('║')}
28
+ ${chalk.cyan('╚══════════════════════════════════════════════════════════╝')}
29
+ `;
30
+ // Global error handlers
31
+ process.on('uncaughtException', (err) => {
32
+ console.error(chalk.red('Error:'), err.message);
33
+ process.exit(1);
34
+ });
35
+ process.on('unhandledRejection', (reason) => {
36
+ const msg = reason instanceof Error ? reason.message : String(reason);
37
+ console.error(chalk.red('Error:'), msg);
38
+ process.exit(1);
39
+ });
40
+ const program = new Command();
41
+ program
42
+ .name('icoa')
43
+ .version('1.0.0')
44
+ .description('ICOA CLI — CLI-Native CTF Competition Terminal')
45
+ .action(() => {
46
+ console.log(BANNER);
47
+ // Show quick status
48
+ if (isConnected()) {
49
+ const config = getConfig();
50
+ console.log(chalk.gray(' Connected to: ') + chalk.white(config.ctfdUrl));
51
+ console.log(chalk.gray(' User: ') + chalk.white(config.userName));
52
+ console.log();
53
+ }
54
+ else {
55
+ console.log(chalk.gray(' Not connected. Start with: ') + chalk.white('icoa ctf join <url>'));
56
+ console.log();
57
+ }
58
+ console.log(chalk.gray(' Quick start:'));
59
+ console.log(chalk.white(' icoa ctf join <url> ') + chalk.gray('Connect to competition'));
60
+ console.log(chalk.white(' icoa ctf challenges ') + chalk.gray('View challenges'));
61
+ console.log(chalk.white(' icoa hint <question> ') + chalk.gray('Get AI hint'));
62
+ console.log(chalk.white(' icoa ref <topic> ') + chalk.gray('Quick reference'));
63
+ console.log(chalk.white(' icoa shell ') + chalk.gray('Open sandbox'));
64
+ console.log(chalk.white(' icoa setup ') + chalk.gray('Configure settings'));
65
+ console.log(chalk.white(' icoa --help ') + chalk.gray('All commands'));
66
+ console.log();
67
+ });
68
+ registerCtfCommands(program);
69
+ registerHintCommands(program);
70
+ registerRefCommand(program);
71
+ registerShellCommand(program);
72
+ registerFilesCommand(program);
73
+ registerConnectCommand(program);
74
+ registerNoteCommand(program);
75
+ registerLogCommand(program);
76
+ registerLangCommand(program);
77
+ registerSetupCommand(program);
78
+ program.parse();
@@ -0,0 +1,8 @@
1
+ import type { HintLevel } from '../types/index.js';
2
+ export declare function checkBudget(level: HintLevel): {
3
+ allowed: boolean;
4
+ remaining: number;
5
+ };
6
+ export declare function deductBudget(level: HintLevel, tokensUsed: number): void;
7
+ export declare function getBudgetDisplay(): string;
8
+ export declare function isTokenCapReached(): boolean;
@@ -0,0 +1,29 @@
1
+ import { getBudget, saveBudget } from './config.js';
2
+ export function checkBudget(level) {
3
+ const budget = getBudget();
4
+ const key = level.toLowerCase();
5
+ return {
6
+ allowed: budget[key] > 0,
7
+ remaining: budget[key],
8
+ };
9
+ }
10
+ export function deductBudget(level, tokensUsed) {
11
+ const budget = getBudget();
12
+ const key = level.toLowerCase();
13
+ budget[key] = Math.max(0, budget[key] - 1);
14
+ budget.tokensUsed += tokensUsed;
15
+ saveBudget(budget);
16
+ }
17
+ export function getBudgetDisplay() {
18
+ const budget = getBudget();
19
+ return [
20
+ ` Level A (General Guidance): ${budget.a}/50`,
21
+ ` Level B (Deep Analysis): ${budget.b}/10`,
22
+ ` Level C (Critical Assist): ${budget.c}/2`,
23
+ ` Token Usage: ${budget.tokensUsed.toLocaleString()}/${budget.tokenCap.toLocaleString()}`,
24
+ ].join('\n');
25
+ }
26
+ export function isTokenCapReached() {
27
+ const budget = getBudget();
28
+ return budget.tokensUsed >= budget.tokenCap;
29
+ }
@@ -0,0 +1,7 @@
1
+ import type { IcoaConfig, HintBudget } from '../types/index.js';
2
+ export declare function getConfig(): IcoaConfig;
3
+ export declare function saveConfig(config: Partial<IcoaConfig>): void;
4
+ export declare function getBudget(): HintBudget;
5
+ export declare function saveBudget(budget: HintBudget): void;
6
+ export declare function getIcoaDir(): string;
7
+ export declare function isConnected(): boolean;
@@ -0,0 +1,60 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { DEFAULT_CONFIG, DEFAULT_BUDGET } from '../types/index.js';
6
+ const ICOA_DIR = join(homedir(), '.icoa');
7
+ const CONFIG_FILE = join(ICOA_DIR, 'config.json');
8
+ const BUDGET_FILE = join(ICOA_DIR, 'budget.json');
9
+ function ensureDir() {
10
+ if (!existsSync(ICOA_DIR)) {
11
+ mkdirSync(ICOA_DIR, { recursive: true });
12
+ }
13
+ }
14
+ export function getConfig() {
15
+ ensureDir();
16
+ if (!existsSync(CONFIG_FILE)) {
17
+ const config = { ...DEFAULT_CONFIG, sessionId: randomUUID() };
18
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
19
+ return config;
20
+ }
21
+ try {
22
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
23
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
24
+ }
25
+ catch {
26
+ return { ...DEFAULT_CONFIG, sessionId: randomUUID() };
27
+ }
28
+ }
29
+ export function saveConfig(config) {
30
+ ensureDir();
31
+ const current = getConfig();
32
+ const merged = { ...current, ...config };
33
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
34
+ }
35
+ export function getBudget() {
36
+ ensureDir();
37
+ if (!existsSync(BUDGET_FILE)) {
38
+ writeFileSync(BUDGET_FILE, JSON.stringify(DEFAULT_BUDGET, null, 2));
39
+ return { ...DEFAULT_BUDGET };
40
+ }
41
+ try {
42
+ const raw = readFileSync(BUDGET_FILE, 'utf-8');
43
+ return { ...DEFAULT_BUDGET, ...JSON.parse(raw) };
44
+ }
45
+ catch {
46
+ return { ...DEFAULT_BUDGET };
47
+ }
48
+ }
49
+ export function saveBudget(budget) {
50
+ ensureDir();
51
+ writeFileSync(BUDGET_FILE, JSON.stringify(budget, null, 2));
52
+ }
53
+ export function getIcoaDir() {
54
+ ensureDir();
55
+ return ICOA_DIR;
56
+ }
57
+ export function isConnected() {
58
+ const config = getConfig();
59
+ return !!(config.ctfdUrl && config.token);
60
+ }
@@ -0,0 +1,22 @@
1
+ import type { CTFdChallenge, CTFdChallengeListItem, CTFdUser, CTFdTeam, CTFdScoreboardEntry, CTFdAttemptResponse } from '../types/index.js';
2
+ export declare class CTFdClient {
3
+ private baseUrl;
4
+ private token;
5
+ constructor(baseUrl: string, token: string);
6
+ private request;
7
+ testConnection(): Promise<CTFdUser>;
8
+ getChallenges(): Promise<CTFdChallengeListItem[]>;
9
+ getChallenge(id: number): Promise<CTFdChallenge>;
10
+ submitFlag(challengeId: number, submission: string): Promise<CTFdAttemptResponse['data']>;
11
+ getScoreboard(): Promise<CTFdScoreboardEntry[]>;
12
+ getTeam(): Promise<CTFdTeam>;
13
+ getCompetitionMeta(): Promise<{
14
+ start: number | null;
15
+ end: number | null;
16
+ userMode: string;
17
+ csrfNonce: string;
18
+ }>;
19
+ getChallengeFiles(id: number): Promise<string[]>;
20
+ downloadFile(filePath: string, destDir: string): Promise<string>;
21
+ loginWithCredentials(username: string, password: string): Promise<string>;
22
+ }