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.
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +66 -0
- package/dist/commands/ctf.d.ts +2 -0
- package/dist/commands/ctf.js +472 -0
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.js +52 -0
- package/dist/commands/hint.d.ts +2 -0
- package/dist/commands/hint.js +107 -0
- package/dist/commands/lang.d.ts +2 -0
- package/dist/commands/lang.js +42 -0
- package/dist/commands/log.d.ts +2 -0
- package/dist/commands/log.js +36 -0
- package/dist/commands/note.d.ts +2 -0
- package/dist/commands/note.js +32 -0
- package/dist/commands/ref.d.ts +2 -0
- package/dist/commands/ref.js +63 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +88 -0
- package/dist/commands/shell.d.ts +2 -0
- package/dist/commands/shell.js +55 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +78 -0
- package/dist/lib/budget.d.ts +8 -0
- package/dist/lib/budget.js +29 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +60 -0
- package/dist/lib/ctfd-client.d.ts +22 -0
- package/dist/lib/ctfd-client.js +161 -0
- package/dist/lib/gemini.d.ts +7 -0
- package/dist/lib/gemini.js +108 -0
- package/dist/lib/logger.d.ts +6 -0
- package/dist/lib/logger.js +59 -0
- package/dist/lib/translation.d.ts +1 -0
- package/dist/lib/translation.js +40 -0
- package/dist/lib/ui.d.ts +10 -0
- package/dist/lib/ui.js +59 -0
- package/dist/types/index.d.ts +125 -0
- package/dist/types/index.js +29 -0
- package/package.json +43 -0
- package/refs/ROPgadget.txt +67 -0
- package/refs/base64.txt +63 -0
- package/refs/bash.txt +79 -0
- package/refs/binwalk.txt +43 -0
- package/refs/bs4.txt +61 -0
- package/refs/checksec.txt +57 -0
- package/refs/curl.txt +73 -0
- package/refs/cyberchef.txt +78 -0
- package/refs/exiftool.txt +50 -0
- package/refs/ffuf.txt +73 -0
- package/refs/gcc.txt +66 -0
- package/refs/gdb.txt +83 -0
- package/refs/hashcat.txt +64 -0
- package/refs/hint.txt +42 -0
- package/refs/icoa.txt +36 -0
- package/refs/john.txt +74 -0
- package/refs/linux.txt +58 -0
- package/refs/nc.txt +64 -0
- package/refs/nmap.txt +57 -0
- package/refs/numpy.txt +59 -0
- package/refs/openssl.txt +75 -0
- package/refs/pillow.txt +67 -0
- package/refs/pwntools.txt +79 -0
- package/refs/pycrypto.txt +77 -0
- package/refs/python.txt +94 -0
- package/refs/r2.txt +85 -0
- package/refs/regex.txt +73 -0
- package/refs/requests.txt +83 -0
- package/refs/rules.txt +28 -0
- package/refs/scapy.txt +80 -0
- package/refs/sqlmap.txt +69 -0
- package/refs/steghide.txt +71 -0
- package/refs/struct.txt +61 -0
- package/refs/sympy.txt +77 -0
- package/refs/tshark.txt +65 -0
- package/refs/vim.txt +74 -0
- package/refs/volatility.txt +41 -0
- 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,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,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,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,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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|
+
}
|