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,161 @@
1
+ import { createWriteStream, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { Readable } from 'node:stream';
5
+ export class CTFdClient {
6
+ baseUrl;
7
+ token;
8
+ constructor(baseUrl, token) {
9
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
10
+ this.token = token;
11
+ }
12
+ async request(method, path, body) {
13
+ const url = `${this.baseUrl}/api/v1${path}`;
14
+ const headers = {
15
+ Authorization: `Token ${this.token}`,
16
+ 'Content-Type': 'application/json',
17
+ };
18
+ const res = await fetch(url, {
19
+ method,
20
+ headers,
21
+ body: body ? JSON.stringify(body) : undefined,
22
+ });
23
+ if (!res.ok) {
24
+ const text = await res.text().catch(() => 'Unknown error');
25
+ throw new Error(`CTFd API error (${res.status}): ${text}`);
26
+ }
27
+ const json = (await res.json());
28
+ if (json.success === false) {
29
+ throw new Error(`CTFd error: ${json.errors?.join(', ') || 'Unknown error'}`);
30
+ }
31
+ return json.data;
32
+ }
33
+ async testConnection() {
34
+ return this.request('GET', '/users/me');
35
+ }
36
+ async getChallenges() {
37
+ return this.request('GET', '/challenges');
38
+ }
39
+ async getChallenge(id) {
40
+ return this.request('GET', `/challenges/${id}`);
41
+ }
42
+ async submitFlag(challengeId, submission) {
43
+ const res = await fetch(`${this.baseUrl}/api/v1/challenges/attempt`, {
44
+ method: 'POST',
45
+ headers: {
46
+ Authorization: `Token ${this.token}`,
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ body: JSON.stringify({ challenge_id: challengeId, submission }),
50
+ });
51
+ if (!res.ok) {
52
+ const text = await res.text().catch(() => 'Unknown error');
53
+ throw new Error(`CTFd API error (${res.status}): ${text}`);
54
+ }
55
+ const json = (await res.json());
56
+ return json.data;
57
+ }
58
+ async getScoreboard() {
59
+ return this.request('GET', '/scoreboard');
60
+ }
61
+ async getTeam() {
62
+ return this.request('GET', '/teams/me');
63
+ }
64
+ async getCompetitionMeta() {
65
+ const res = await fetch(this.baseUrl);
66
+ const html = await res.text();
67
+ const startMatch = html.match(/'start'\s*:\s*(\d+)/);
68
+ const endMatch = html.match(/'end'\s*:\s*(\d+)/);
69
+ const modeMatch = html.match(/'userMode'\s*:\s*"([^"]+)"/);
70
+ const csrfMatch = html.match(/'csrfNonce'\s*:\s*"([^"]+)"/);
71
+ return {
72
+ start: startMatch ? parseInt(startMatch[1]) : null,
73
+ end: endMatch ? parseInt(endMatch[1]) : null,
74
+ userMode: modeMatch?.[1] || 'users',
75
+ csrfNonce: csrfMatch?.[1] || '',
76
+ };
77
+ }
78
+ async getChallengeFiles(id) {
79
+ const challenge = await this.getChallenge(id);
80
+ return challenge.files || [];
81
+ }
82
+ async downloadFile(filePath, destDir) {
83
+ mkdirSync(destDir, { recursive: true });
84
+ const url = filePath.startsWith('http')
85
+ ? filePath
86
+ : `${this.baseUrl}/${filePath.replace(/^\//, '')}`;
87
+ const res = await fetch(url, {
88
+ headers: { Authorization: `Token ${this.token}` },
89
+ redirect: 'follow',
90
+ });
91
+ if (!res.ok || !res.body) {
92
+ throw new Error(`Failed to download: ${url}`);
93
+ }
94
+ // Extract clean filename (strip query params like ?token=xxx)
95
+ const rawName = filePath.split('/').pop() || 'file';
96
+ const fileName = rawName.split('?')[0];
97
+ const destPath = join(destDir, fileName);
98
+ const fileStream = createWriteStream(destPath);
99
+ await pipeline(Readable.fromWeb(res.body), fileStream);
100
+ return destPath;
101
+ }
102
+ async loginWithCredentials(username, password) {
103
+ // Step 1: GET /login to get nonce
104
+ const loginPageRes = await fetch(`${this.baseUrl}/login`);
105
+ const loginHtml = await loginPageRes.text();
106
+ // Try multiple nonce patterns (CTFd versions differ)
107
+ const nonceMatch = loginHtml.match(/name="nonce"[^>]*value="([^"]+)"/) ||
108
+ loginHtml.match(/value="([^"]+)"[^>]*name="nonce"/) ||
109
+ loginHtml.match(/id="nonce"[^>]*value="([^"]+)"/) ||
110
+ loginHtml.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
111
+ const nonce = nonceMatch?.[1] || '';
112
+ if (!nonce) {
113
+ throw new Error('Could not extract CSRF nonce from login page.');
114
+ }
115
+ // Extract cookies
116
+ const cookies = loginPageRes.headers.getSetCookie?.() || [];
117
+ const cookieStr = cookies.map((c) => c.split(';')[0]).join('; ');
118
+ // Step 2: POST /login with credentials
119
+ const loginRes = await fetch(`${this.baseUrl}/login`, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'Content-Type': 'application/x-www-form-urlencoded',
123
+ Cookie: cookieStr,
124
+ },
125
+ body: new URLSearchParams({ name: username, password, nonce, _submit: 'Submit' }),
126
+ redirect: 'manual',
127
+ });
128
+ const loginCookies = loginRes.headers.getSetCookie?.() || [];
129
+ const allCookies = [...cookies, ...loginCookies].map((c) => c.split(';')[0]).join('; ');
130
+ // Check if login was successful (redirect to /challenges or /, not back to /login)
131
+ const location = loginRes.headers.get('location') || '';
132
+ if (location.includes('/login')) {
133
+ throw new Error('Invalid username or password.');
134
+ }
135
+ // Step 3: Generate API token
136
+ // First get CSRF nonce from settings page
137
+ const settingsRes = await fetch(`${this.baseUrl}/settings`, {
138
+ headers: { Cookie: allCookies },
139
+ });
140
+ const settingsHtml = await settingsRes.text();
141
+ const csrfMatch = settingsHtml.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
142
+ const csrfNonce = csrfMatch?.[1] || nonce;
143
+ const tokenRes = await fetch(`${this.baseUrl}/api/v1/tokens`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ Cookie: allCookies,
148
+ 'CSRF-Token': csrfNonce,
149
+ },
150
+ body: JSON.stringify({ expiration: '2026-12-31T23:59:59+00:00' }),
151
+ });
152
+ if (!tokenRes.ok) {
153
+ throw new Error('Failed to generate API token. Please use a manual access token instead.');
154
+ }
155
+ const tokenJson = (await tokenRes.json());
156
+ if (tokenJson.success && tokenJson.data?.value) {
157
+ return tokenJson.data.value;
158
+ }
159
+ throw new Error('Failed to generate API token. Response: ' + JSON.stringify(tokenJson));
160
+ }
161
+ }
@@ -0,0 +1,7 @@
1
+ import type { HintLevel, ChallengeContext } from '../types/index.js';
2
+ export declare function generateHint(level: HintLevel, question: string, context?: ChallengeContext): Promise<{
3
+ text: string;
4
+ tokensUsed: number;
5
+ }>;
6
+ export declare function translateText(text: string, targetLang: string): Promise<string>;
7
+ export declare function setApiKey(key: string): void;
@@ -0,0 +1,108 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { getConfig, saveConfig } from './config.js';
3
+ const SYSTEM_PROMPTS = {
4
+ A: `You are an AI assistant in a cybersecurity CTF competition called ICOA.
5
+ You are providing Level A (General Guidance) to a competitor.
6
+
7
+ STRICT RULES:
8
+ - Only answer conceptual questions
9
+ - Do NOT mention specific vulnerability names
10
+ - Do NOT provide any code, commands, or tool usage
11
+ - Do NOT mention specific attack techniques
12
+ - Use questions to guide the competitor toward their own discovery
13
+ - If the competitor asks you to solve the challenge, refuse and redirect
14
+ - Never output anything matching flag format: icoa{...}`,
15
+ B: `You are an AI assistant in ICOA CTF, providing Level B (Deep Analysis).
16
+
17
+ RULES:
18
+ - You MAY identify specific vulnerability types (e.g., "buffer overflow")
19
+ - You MAY suggest which category of tool to use (e.g., "a debugger")
20
+ - Do NOT provide complete commands or working code
21
+ - Do NOT provide exploit code or payloads
22
+ - Do NOT provide flags or flag fragments
23
+ - Never output anything matching: icoa{...}`,
24
+ C: `You are an AI assistant in ICOA CTF, providing Level C (Critical Assist).
25
+
26
+ RULES:
27
+ - You MAY provide the key conceptual breakthrough
28
+ - You MAY name specific algorithms or approaches
29
+ - Do NOT provide complete exploit code
30
+ - Do NOT provide the flag
31
+ - Never output anything matching: icoa{...}`,
32
+ };
33
+ function buildSystemPrompt(level, context) {
34
+ let prompt = SYSTEM_PROMPTS[level];
35
+ if (context) {
36
+ prompt += `\n\nThe competitor is currently working on:\nChallenge: ${context.name}\nCategory: ${context.category}`;
37
+ }
38
+ return prompt;
39
+ }
40
+ function filterFlagPatterns(text) {
41
+ return text.replace(/icoa\{[^}]*\}/gi, '[FLAG REDACTED]');
42
+ }
43
+ function getApiKey() {
44
+ // Priority: env var > config
45
+ const envKey = process.env.GEMINI_API_KEY;
46
+ if (envKey)
47
+ return envKey;
48
+ const config = getConfig();
49
+ if (config.geminiApiKey)
50
+ return config.geminiApiKey;
51
+ return '';
52
+ }
53
+ export async function generateHint(level, question, context) {
54
+ let apiKey = getApiKey();
55
+ if (!apiKey) {
56
+ // Try to prompt for key interactively
57
+ try {
58
+ const { input } = await import('@inquirer/prompts');
59
+ console.log();
60
+ console.log(' Gemini API key not configured.');
61
+ console.log(' Get one free at: https://aistudio.google.com/apikey');
62
+ console.log();
63
+ apiKey = await input({ message: 'Enter your Gemini API Key:' });
64
+ if (apiKey.trim()) {
65
+ apiKey = apiKey.trim();
66
+ saveConfig({ geminiApiKey: apiKey });
67
+ console.log(' Key saved for future use.');
68
+ console.log();
69
+ }
70
+ else {
71
+ throw new Error('No API key provided.');
72
+ }
73
+ }
74
+ catch {
75
+ throw new Error('Gemini API key not configured.\n' +
76
+ 'Set GEMINI_API_KEY environment variable, or run: icoa setup');
77
+ }
78
+ }
79
+ const genAI = new GoogleGenerativeAI(apiKey);
80
+ const model = genAI.getGenerativeModel({
81
+ model: 'gemini-2.5-pro-preview-05-06',
82
+ systemInstruction: buildSystemPrompt(level, context),
83
+ });
84
+ const result = await model.generateContent(question);
85
+ const response = result.response;
86
+ const text = filterFlagPatterns(response.text());
87
+ const usage = response.usageMetadata;
88
+ const tokensUsed = (usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0);
89
+ return { text, tokensUsed };
90
+ }
91
+ export async function translateText(text, targetLang) {
92
+ const apiKey = getApiKey();
93
+ if (!apiKey) {
94
+ throw new Error('Gemini API key not configured for translation.');
95
+ }
96
+ const genAI = new GoogleGenerativeAI(apiKey);
97
+ const model = genAI.getGenerativeModel({ model: 'gemini-2.5-pro-preview-05-06' });
98
+ const prompt = `Translate the following CTF challenge description to ${targetLang}.
99
+ Keep all technical terms, code, commands, URLs, and flag formats in English.
100
+ Only translate the narrative/descriptive text.
101
+
102
+ ${text}`;
103
+ const result = await model.generateContent(prompt);
104
+ return result.response.text();
105
+ }
106
+ export function setApiKey(key) {
107
+ saveConfig({ geminiApiKey: key });
108
+ }
@@ -0,0 +1,6 @@
1
+ import type { LogEntry, HintLevel } from '../types/index.js';
2
+ export declare function logEntry(entry: LogEntry): void;
3
+ export declare function logCommand(command: string): void;
4
+ export declare function logHint(level: HintLevel, question: string, challengeId?: number): void;
5
+ export declare function logSubmission(challengeId: number, flag: string): void;
6
+ export declare function getSessionLog(): LogEntry[];
@@ -0,0 +1,59 @@
1
+ import { appendFileSync, readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getConfig, getIcoaDir } from './config.js';
4
+ function getLogPath() {
5
+ return join(getIcoaDir(), 'session.log');
6
+ }
7
+ export function logEntry(entry) {
8
+ try {
9
+ appendFileSync(getLogPath(), JSON.stringify(entry) + '\n');
10
+ }
11
+ catch {
12
+ // Silently fail — logging should never break the CLI
13
+ }
14
+ }
15
+ export function logCommand(command) {
16
+ const config = getConfig();
17
+ logEntry({
18
+ timestamp: new Date().toISOString(),
19
+ level: 'command',
20
+ input: command,
21
+ sessionId: config.sessionId,
22
+ });
23
+ }
24
+ export function logHint(level, question, challengeId) {
25
+ const config = getConfig();
26
+ logEntry({
27
+ timestamp: new Date().toISOString(),
28
+ level: level,
29
+ input: question,
30
+ challengeId,
31
+ sessionId: config.sessionId,
32
+ });
33
+ }
34
+ export function logSubmission(challengeId, flag) {
35
+ const config = getConfig();
36
+ logEntry({
37
+ timestamp: new Date().toISOString(),
38
+ level: 'submit',
39
+ input: flag,
40
+ challengeId,
41
+ sessionId: config.sessionId,
42
+ });
43
+ }
44
+ export function getSessionLog() {
45
+ const logPath = getLogPath();
46
+ if (!existsSync(logPath))
47
+ return [];
48
+ try {
49
+ const raw = readFileSync(logPath, 'utf-8');
50
+ return raw
51
+ .trim()
52
+ .split('\n')
53
+ .filter(Boolean)
54
+ .map((line) => JSON.parse(line));
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export declare function getTranslation(text: string, challengeId: number, targetLang: string): Promise<string>;
@@ -0,0 +1,40 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getIcoaDir } from './config.js';
4
+ import { translateText } from './gemini.js';
5
+ function getCachePath(lang, challengeId) {
6
+ const dir = join(getIcoaDir(), 'translations', lang);
7
+ mkdirSync(dir, { recursive: true });
8
+ return join(dir, `${challengeId}.json`);
9
+ }
10
+ export async function getTranslation(text, challengeId, targetLang) {
11
+ if (targetLang === 'en')
12
+ return text;
13
+ const cachePath = getCachePath(targetLang, challengeId);
14
+ // Check cache
15
+ if (existsSync(cachePath)) {
16
+ try {
17
+ const cached = JSON.parse(readFileSync(cachePath, 'utf-8'));
18
+ if (cached.translation)
19
+ return cached.translation;
20
+ }
21
+ catch {
22
+ // Cache corrupt, regenerate
23
+ }
24
+ }
25
+ // Translate via Gemini
26
+ const translation = await translateText(text, getLangName(targetLang));
27
+ // Cache result
28
+ writeFileSync(cachePath, JSON.stringify({ original: text, translation, timestamp: new Date().toISOString() }));
29
+ return translation;
30
+ }
31
+ function getLangName(code) {
32
+ const names = {
33
+ en: 'English',
34
+ zh: 'Chinese (Simplified)',
35
+ ja: 'Japanese',
36
+ ko: 'Korean',
37
+ es: 'Spanish',
38
+ };
39
+ return names[code] || 'English';
40
+ }
@@ -0,0 +1,10 @@
1
+ export declare function printSuccess(msg: string): void;
2
+ export declare function printError(msg: string): void;
3
+ export declare function printWarning(msg: string): void;
4
+ export declare function printInfo(msg: string): void;
5
+ export declare function printTable(headers: string[], rows: string[][]): void;
6
+ export declare function printMarkdown(text: string): void;
7
+ export declare function createSpinner(text: string): import("ora").Ora;
8
+ export declare function formatCountdown(targetDate: Date): string;
9
+ export declare function printHeader(title: string): void;
10
+ export declare function printKeyValue(key: string, value: string): void;
package/dist/lib/ui.js ADDED
@@ -0,0 +1,59 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import ora from 'ora';
4
+ import { Marked } from 'marked';
5
+ import { markedTerminal } from 'marked-terminal';
6
+ const marked = new Marked(markedTerminal());
7
+ export function printSuccess(msg) {
8
+ console.log(chalk.green('✓ ') + msg);
9
+ }
10
+ export function printError(msg) {
11
+ console.log(chalk.red('✗ ') + msg);
12
+ }
13
+ export function printWarning(msg) {
14
+ console.log(chalk.yellow('⚠ ') + msg);
15
+ }
16
+ export function printInfo(msg) {
17
+ console.log(chalk.blue('ℹ ') + msg);
18
+ }
19
+ export function printTable(headers, rows) {
20
+ const table = new Table({
21
+ head: headers.map((h) => chalk.cyan.bold(h)),
22
+ style: { head: [], border: [] },
23
+ });
24
+ for (const row of rows) {
25
+ table.push(row);
26
+ }
27
+ console.log(table.toString());
28
+ }
29
+ export function printMarkdown(text) {
30
+ const rendered = marked.parse(text);
31
+ if (typeof rendered === 'string') {
32
+ console.log(rendered);
33
+ }
34
+ }
35
+ export function createSpinner(text) {
36
+ return ora({ text, color: 'cyan' });
37
+ }
38
+ export function formatCountdown(targetDate) {
39
+ const now = new Date();
40
+ const diff = targetDate.getTime() - now.getTime();
41
+ if (diff <= 0)
42
+ return '00:00:00';
43
+ const hours = Math.floor(diff / (1000 * 60 * 60));
44
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
45
+ const seconds = Math.floor((diff % (1000 * 60)) / 1000);
46
+ return [
47
+ hours.toString().padStart(2, '0'),
48
+ minutes.toString().padStart(2, '0'),
49
+ seconds.toString().padStart(2, '0'),
50
+ ].join(':');
51
+ }
52
+ export function printHeader(title) {
53
+ console.log();
54
+ console.log(chalk.cyan.bold(` ${title}`));
55
+ console.log(chalk.cyan(' ' + '─'.repeat(title.length + 4)));
56
+ }
57
+ export function printKeyValue(key, value) {
58
+ console.log(` ${chalk.gray(key + ':')} ${value}`);
59
+ }
@@ -0,0 +1,125 @@
1
+ export interface CTFdChallenge {
2
+ id: number;
3
+ name: string;
4
+ description: string;
5
+ category: string;
6
+ value: number;
7
+ type: string;
8
+ state: string;
9
+ max_attempts: number;
10
+ tags: string[];
11
+ files: string[];
12
+ hints: {
13
+ id: number;
14
+ cost: number;
15
+ }[];
16
+ solves: number;
17
+ solved_by_me: boolean;
18
+ connection_info?: string;
19
+ }
20
+ export interface CTFdChallengeListItem {
21
+ id: number;
22
+ name: string;
23
+ category: string;
24
+ value: number;
25
+ type: string;
26
+ solves: number;
27
+ solved_by_me: boolean;
28
+ tags: string[];
29
+ }
30
+ export interface CTFdUser {
31
+ id: number;
32
+ name: string;
33
+ email: string;
34
+ team_id: number | null;
35
+ country: string | null;
36
+ score: number;
37
+ place: string | null;
38
+ }
39
+ export interface CTFdTeam {
40
+ id: number;
41
+ name: string;
42
+ score: number;
43
+ place: string | null;
44
+ members: {
45
+ id: number;
46
+ name: string;
47
+ }[];
48
+ }
49
+ export interface CTFdScoreboardEntry {
50
+ pos: number;
51
+ account_id: number;
52
+ account_url: string;
53
+ account_type: string;
54
+ name: string;
55
+ score: number;
56
+ members?: {
57
+ id: number;
58
+ name: string;
59
+ }[];
60
+ }
61
+ export interface CTFdAttemptResponse {
62
+ success: boolean;
63
+ data: {
64
+ status: 'correct' | 'incorrect' | 'already_solved' | 'paused' | 'ratelimited';
65
+ message: string;
66
+ };
67
+ }
68
+ export interface CTFdApiResponse<T> {
69
+ success: boolean;
70
+ data: T;
71
+ errors?: string[];
72
+ }
73
+ export interface CTFdTokenResponse {
74
+ success: boolean;
75
+ data: {
76
+ id: number;
77
+ type: string;
78
+ value: string;
79
+ created: string;
80
+ expiration: string;
81
+ };
82
+ }
83
+ export interface IcoaConfig {
84
+ ctfdUrl: string;
85
+ token: string;
86
+ language: string;
87
+ sessionId: string;
88
+ competitionCode: string;
89
+ teamId: number | null;
90
+ userId: number | null;
91
+ userName: string;
92
+ teamName: string;
93
+ currentChallengeId: number | null;
94
+ currentChallengeName: string;
95
+ currentChallengeCategory: string;
96
+ competitionState: CompetitionState;
97
+ competitionStartsAt: string;
98
+ competitionEndsAt: string;
99
+ geminiApiKey: string;
100
+ }
101
+ export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
102
+ export type HintLevel = 'A' | 'B' | 'C';
103
+ export interface HintBudget {
104
+ a: number;
105
+ b: number;
106
+ c: number;
107
+ tokensUsed: number;
108
+ tokenCap: number;
109
+ }
110
+ export interface LogEntry {
111
+ timestamp: string;
112
+ level: 'A' | 'B' | 'C' | 'command' | 'submit';
113
+ input: string;
114
+ challengeId?: number;
115
+ sessionId: string;
116
+ }
117
+ export interface ChallengeContext {
118
+ name: string;
119
+ category: string;
120
+ description?: string;
121
+ }
122
+ export declare const DEFAULT_BUDGET: HintBudget;
123
+ export declare const DEFAULT_CONFIG: IcoaConfig;
124
+ export declare const SUPPORTED_LANGUAGES: readonly ["en", "zh", "ja", "ko", "es"];
125
+ export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
@@ -0,0 +1,29 @@
1
+ // ========================
2
+ // CTFd API Response Types
3
+ // ========================
4
+ export const DEFAULT_BUDGET = {
5
+ a: 50,
6
+ b: 10,
7
+ c: 2,
8
+ tokensUsed: 0,
9
+ tokenCap: 50000,
10
+ };
11
+ export const DEFAULT_CONFIG = {
12
+ ctfdUrl: '',
13
+ token: '',
14
+ language: 'en',
15
+ sessionId: '',
16
+ competitionCode: '',
17
+ teamId: null,
18
+ userId: null,
19
+ userName: '',
20
+ teamName: '',
21
+ currentChallengeId: null,
22
+ currentChallengeName: '',
23
+ currentChallengeCategory: '',
24
+ competitionState: 'unknown',
25
+ competitionStartsAt: '',
26
+ competitionEndsAt: '',
27
+ geminiApiKey: '',
28
+ };
29
+ export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko', 'es'];
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "icoa-cli",
3
+ "version": "1.0.0",
4
+ "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "icoa": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "refs"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "keywords": [
19
+ "ctf",
20
+ "cli",
21
+ "cybersecurity",
22
+ "icoa",
23
+ "competition"
24
+ ],
25
+ "license": "SEE LICENSE IN licenses/apache-2.0.txt",
26
+ "dependencies": {
27
+ "@google/generative-ai": "^0.24.0",
28
+ "chalk": "^5.4.1",
29
+ "cli-table3": "^0.6.5",
30
+ "commander": "^13.1.0",
31
+ "@inquirer/prompts": "^7.5.0",
32
+ "marked": "^15.0.7",
33
+ "marked-terminal": "^7.3.0",
34
+ "ora": "^8.2.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.15.3",
38
+ "typescript": "^5.8.3"
39
+ },
40
+ "engines": {
41
+ "node": ">=20.0.0"
42
+ }
43
+ }