icoa-cli 1.1.1 → 1.3.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/index.js CHANGED
@@ -11,7 +11,8 @@ import { registerNoteCommand } from './commands/note.js';
11
11
  import { registerLogCommand } from './commands/log.js';
12
12
  import { registerLangCommand } from './commands/lang.js';
13
13
  import { registerSetupCommand } from './commands/setup.js';
14
- import { isConnected, getConfig, saveConfig } from './lib/config.js';
14
+ import { getConfig, saveConfig } from './lib/config.js';
15
+ import { startRepl } from './repl.js';
15
16
  const BANNER = `
16
17
  ${chalk.cyan('╔══════════════════════════════════════════════════════════╗')}
17
18
  ${chalk.cyan('║')} ${chalk.cyan('║')}
@@ -28,47 +29,37 @@ ${chalk.cyan('║')} ${
28
29
  ${chalk.cyan('║')} ${chalk.green.bold('AI4CTF')} ${chalk.gray('Use AI to solve challenges')} ${chalk.cyan('║')}
29
30
  ${chalk.cyan('║')} ${chalk.red.bold('CTF4AI')} ${chalk.gray('Hack, attack & evaluate AI systems')} ${chalk.cyan('║')}
30
31
  ${chalk.cyan('║')} ${chalk.cyan('║')}
31
- ${chalk.cyan('║')} ${chalk.gray('CLI-Native Competition Terminal v1.1.1')} ${chalk.cyan('║')}
32
+ ${chalk.cyan('║')} ${chalk.gray('CLI-Native Competition Terminal v1.3.0')} ${chalk.cyan('║')}
32
33
  ${chalk.cyan('║')} ${chalk.cyan('║')}
33
34
  ${chalk.cyan('╚══════════════════════════════════════════════════════════╝')}
34
35
  `;
35
36
  // Global error handlers
36
37
  process.on('uncaughtException', (err) => {
38
+ if (err.message === '__REPL_NO_EXIT__')
39
+ return;
37
40
  console.error(chalk.red('Error:'), err.message);
38
41
  process.exit(1);
39
42
  });
40
43
  process.on('unhandledRejection', (reason) => {
41
44
  const msg = reason instanceof Error ? reason.message : String(reason);
45
+ if (msg === '__REPL_NO_EXIT__')
46
+ return;
42
47
  console.error(chalk.red('Error:'), msg);
43
48
  process.exit(1);
44
49
  });
45
50
  const program = new Command();
46
51
  program
47
52
  .name('icoa')
48
- .version('1.0.0')
53
+ .version('1.2.0')
49
54
  .description('ICOA CLI — CLI-Native CTF Competition Terminal')
50
- .action(() => {
55
+ .option('--resume', 'Resume previous session')
56
+ .action((opts) => {
51
57
  console.log(BANNER);
52
- // Show quick status
53
- if (isConnected()) {
54
- const config = getConfig();
55
- console.log(chalk.gray(' Connected to: ') + chalk.white(config.ctfdUrl));
56
- console.log(chalk.gray(' User: ') + chalk.white(config.userName));
57
- console.log();
58
- }
59
- else {
60
- console.log(chalk.gray(' Not connected. Start with: ') + chalk.white('icoa ctf join <url>'));
61
- console.log();
58
+ // If running interactively (no extra args or --resume), start REPL
59
+ if (process.argv.length <= 2 || opts.resume) {
60
+ startRepl(program, !!opts.resume);
61
+ return;
62
62
  }
63
- console.log(chalk.gray(' Quick start:'));
64
- console.log(chalk.white(' icoa ctf join <url> ') + chalk.gray('Connect to competition'));
65
- console.log(chalk.white(' icoa ctf challenges ') + chalk.gray('View challenges'));
66
- console.log(chalk.white(' icoa hint <question> ') + chalk.gray('Get AI hint'));
67
- console.log(chalk.white(' icoa ref <topic> ') + chalk.gray('Quick reference'));
68
- console.log(chalk.white(' icoa shell ') + chalk.gray('Open sandbox'));
69
- console.log(chalk.white(' icoa setup ') + chalk.gray('Configure settings'));
70
- console.log(chalk.white(' icoa --help ') + chalk.gray('All commands'));
71
- console.log();
72
63
  });
73
64
  registerCtfCommands(program);
74
65
  registerHintCommands(program);
@@ -0,0 +1,19 @@
1
+ export declare function validateToken(token: string): boolean;
2
+ export declare function isActivated(): boolean;
3
+ export declare function activateToken(token: string): boolean;
4
+ export declare function isFreeCommand(cmd: string): boolean;
5
+ interface SessionState {
6
+ startedAt: string;
7
+ lastExitAt: string | null;
8
+ lastResumeAt: string | null;
9
+ exitCount: number;
10
+ totalAwaySeconds: number;
11
+ }
12
+ export declare function getSession(): SessionState;
13
+ export declare function saveSession(session: Partial<SessionState>): void;
14
+ export declare function recordExit(): void;
15
+ export declare function recordResume(): {
16
+ awaySeconds: number;
17
+ exitCount: number;
18
+ } | null;
19
+ export {};
@@ -0,0 +1,174 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { getConfig, saveConfig, getIcoaDir } from './config.js';
3
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ // SHA-256 hashes of 100 pre-generated access tokens (ICOA-XXXX-XXXX-XXXX)
6
+ const TOKEN_HASHES = new Set([
7
+ "93490bf664679467b4ab5af12e240175364b7af988b72cb7d000e309b7c68c15",
8
+ "cba7a6f075a4bd8c2fc973336cc0b1966669fc481381a065ce670730bbe0b7d3",
9
+ "f6773f614341acb8d2c35e8f466f2876095a2c8026a207e77de621050fa64559",
10
+ "d060ab36056fdac449a561fd3340c4b79ad4cde10c9a21c3397aa74574583a2b",
11
+ "523c11774e103298c15eb9d138872295194078f2d29486d0a6bd210436a592c3",
12
+ "1e16864067470f0a97088f067d8df4b655753ff498a91353b5f9b75989b6be39",
13
+ "f1031aac52879c776d4db5ba43eb25bb40f1a841b91304f8adf65a838fb8c666",
14
+ "bcd9e25014633ec6556345ab43d0cd213c454bb10d4ea0bd5be5ba02e1a6622c",
15
+ "9239ba9c3db74eadbb6dd05949c2a3cbe1b22e14e375ab3ca93d050e2e2ce38b",
16
+ "80fc0a899ed4d37138ae3172dafd466de2d3b1ce9630d67e6b00bec9315964d7",
17
+ "33ed35ce028824519d3a013ca326d6a58b1ca3723ec182527a7367e1ffc2712b",
18
+ "7e90933e760ce5e8df48bdb6bab0e95962bdc1098fceedecf617c0dac37b2a11",
19
+ "33fa6e3639a691b217ed0f927a8e2e2aef5eaa15eab7bd90c57874c2df2a28ac",
20
+ "67013663fb4536a47f6ea46b434fb7a00574bd4fc4f579e9439ba07f748db31b",
21
+ "10c83ad26fc965fcb1d228018f6b54d331e4a2e6087603920b75b500e191c5e4",
22
+ "3079268d847dc7eb196ce8cdc0af8a8506b9636539a1b7cce53dc5cb3f4cbe15",
23
+ "985eb5309c546d2f9d5018976973d6da06a374b897c73c830fc9d1305e78efa0",
24
+ "3a7326d4a7f40cc356b17769f4c5fc0d832d926aff9f067efd60f34c053bf9ef",
25
+ "129f408a98ad6c1b13924cf2c831c752b0c726bf1975d6b46f212a178ced52e1",
26
+ "ec44bc0b4eb6a929b969caaa561492527c369cc07efa50e40c2c6c16162f0c2c",
27
+ "07610942d9da62dd5b91c0ba48602bc44973a9d374ed10ab6c708151677ec626",
28
+ "8ce3165bb8e23f3b67ac53b39191aca3e2f0a620b7f932fe6a43e2c42315efcc",
29
+ "69938b605e652afc1cf8bc7c0dcf6a99820878367dfb3b2ec954ba90b36014f5",
30
+ "660377ce87d378285ab7263fdad86208c8563367d2cc36b4c1402520e45dd5f0",
31
+ "bef8a3727dffc94861a1dfa7d6a336ceac30aab9f0679dcab662cb9f682079dd",
32
+ "02318a3597b64a2c7de56b01fc25f33cc4142c76315136186df680f490681294",
33
+ "b7179114b42b90e9557399ae505050111b6afc55f9295d3ee84d5db6873470c5",
34
+ "e227ec0a6b22773500b35bee46f21bc6bc6a00fb001d54e71479f15b8fdfd72b",
35
+ "1c9802ae1e932d55e73994ce173a996d170e4d274aeccb1b784c9d9b69913f3d",
36
+ "5ed51694773bd1b46a085d10e9a369786a997c50b72ec2bdb6e68921ffca5c51",
37
+ "478e457279a41e9b76a3dc5894418e38246d2af31e0f4776dda8aa89c74c88c8",
38
+ "a7ec7e047c0e62862e9be64b0a65d3d7894d41efe6eb16ca6cfca45d8007f16f",
39
+ "88cf2b75604486ac5f1a0db9afbab28e8e866f9ea00f3e4b4353a7ecc81f6b55",
40
+ "dc4daa9ef4149f122979ffd5a8b23bca3cbb83b24a87ba99a1d00dc09e39936e",
41
+ "cffd9a3637b5ae30c27cae4ae42dc21374caa27dd818b5d361f112406bf3c0e7",
42
+ "33b3e85a964f606e857280ba379de46508f9343cbe1d2722abebbed21b82a106",
43
+ "a3754074df08f9c9407c73e680eac1868175cee3beac3f0e7d2bd211bdf51f58",
44
+ "f9eb5fd82f3360e22ad45d4797f3a95b907a766ef5a61d856bf959b60a4930dc",
45
+ "1c53d3145cb38501a2422bf70f55a0c206b68c14e46da31e0e3dc63c7bee78d6",
46
+ "20bc5f2dedbf6423a543ba25183fab3c7268cdd2762e080f6d0b3387c4b3cad8",
47
+ "654faf29e2a177dba2409db4942007630674b86d3ff87d2b11d428e8b4d0db7f",
48
+ "c27979d019266887a1b86225d3c85b2d4553e85814ab2d791547ffc3f9dc664a",
49
+ "74f982351453468e39aff8373cb2649d57344b8002b85fc9c0df118d051817fc",
50
+ "891e59d7896f29a9af2bd1a9aec98ae2ebcc57d9685027e3f07067734e603923",
51
+ "4a8a234bbad11dc98aa27871ed69d351385f9476af0c15b8b426cfa5630cc3db",
52
+ "e42c967f02a434a061152d86d68fa0a3da70235f6f8262a792abeeace14fc419",
53
+ "c1560c78197abd62252f8932abf73d5f7506fcd157e98390230aec81fdf5e5ac",
54
+ "afa2cc8fe8b9b8785921f2e649d90d239f729223b2ce7460c0f22d79427c7b5e",
55
+ "d574f129616c989797b3a4a7988e9d4ca3afb59ed1de67136d459307b9a1807a",
56
+ "94b1c1e2fab8c51bb76d9d7bc07d23f8b509919009feb141f12562cab696d49a",
57
+ "79bf74936c7274d1ee529fb888f30fbf2a33a15308bedb8c77c1c50793790b42",
58
+ "2ee8962250f82c51fdbbf170c6c7fa9b69dca336434714ba607c6f816034cb08",
59
+ "6e8e665df73c6934d5b432f5aa898ba33cc9a40d630b049404a23fa5d2776298",
60
+ "3561d1bd9ae8c916eace5bdd1eb788cd743ad461cff58acf58ed5b2064b30b7b",
61
+ "46a48aa8aef9f09dabb9cf340dacc1889cf3eeebc49c6d2f26ff017cd087857c",
62
+ "561e0536b3cf214a4bf41b2bfb2209c8d659990a871e44bef9bc0136613a4ab9",
63
+ "7c1f3e331c8f59cc6dab7e310d9ad151628f72e5ca3db5a6ff592c8c976c1974",
64
+ "fb9d9d484edfa2c0a531a3e08074f52b6988f1082606f1eecaf0c58f117dbf3d",
65
+ "2810e928a4ce6e2ae016751a7e8b73496de47fe1926abe6ff486bc0eed23e3d7",
66
+ "723c3ede92f08e9ac12b7dc0b9e6a857ab0426d3418524f4eb50ad69f0228ccd",
67
+ "66a07cdf0c9d56558ad4f853203071b56adfa990a4fb065b49c10108f4996fe7",
68
+ "dc826253950962dbc2e5699dc5c138490c4233a23b39a5cf471a931cbbc689b8",
69
+ "ec29a6c60c70a25a5a238ba1d03407dadafee78c01bc9f921549da428c4fbc03",
70
+ "0564c17382a344aa22b5452989d2950f514bbd366e7b8289a458a4604c987641",
71
+ "157ca17a0c1fc9faed3c2557cdf83323210a40020e72497e8e48e1d0055e54c7",
72
+ "e643beaf8a92e574da88b9efa424852d34261be06496ca78d08acd84c38861b7",
73
+ "df982789a1598f53bbcf44f1b2991c86bfd7713b36b04a06cf52640cfc9c579d",
74
+ "e135e4dd1aebe24484909d56fb47619eb975f9790ef4541fdc87f63a3478ecf4",
75
+ "823235bafe4f9e523cc4ffeb16789a2e2d6b246ab5c248335eba364d54474241",
76
+ "a314bda9257484bbbabc1159b957d3aae1de0bca777bcd1462fdc52abf5a62fe",
77
+ "65f6699712af969820645bbe271f66fe92f5da41fd834e329dadeae7801bc112",
78
+ "17e812f25ef6d73de97b3464ca3fd2109433d9b5bc567e4e74feda35bf660492",
79
+ "37ff30e4c4384b810bd86e4472e9ada4b447cd6057daf8df3f923fac3b8b3d77",
80
+ "c55f13e79ce2bb698606f787551591e050e33e576cb845a2f40b03edac56f819",
81
+ "2124d3353d3f105c3780c937f1eaf377f756e59c6ab6a97ef780a718f3a47c7b",
82
+ "e581535019c839a7c383d0eca3de97a6a5701025d4a24ea56f01932f910cb99c",
83
+ "eb089f8a27da4b94b79e5d125ed6205849b1909d0a6c1d0f51c351be2d5b8466",
84
+ "36ee22c148c0282e112950f8e9939fed4b95d2f8a52c9cd2fd3744070df54333",
85
+ "c81b6509d23e889c5b5ec79fdfaf64fed1bf00ced8f4b224aa37b5a37821ccfe",
86
+ "b529c4de4a2fff86266d9b0318f15246ed30f4641b4156778ff9f3fca3099e32",
87
+ "647c2aa42b094baf52537a8f7ffb015b7aa212c67ca779853d263d420760d8b6",
88
+ "8043ea259b8bbb2bf488643376c2a2b9a3864e5617ea9ac5dc72966e289c07c2",
89
+ "bba0397ebaf1a6093a8adb117e84198bb5d2dcf9e8e7792221af9215cda8c10b",
90
+ "978ec66df04567ef8e4f02aa67b903535859f7bf24a5b63298cae1c938a72ab7",
91
+ "124befb26711b599a311b0b0d7352b6e980a594cd56c8447e32c1aba26818fd1",
92
+ "990ddff1fd05882796d2081ebc0d966a39c04af7120a9440562d71db2f48d65d",
93
+ "f396be48a42c9cca6b6fec0a8ee7f5d15f93a57fba3baf04ab5a7d86af68bea5",
94
+ "4f2487f0af254ea067e853bb6874d80d05c46f6322ca6e3a8815b37931533aea",
95
+ "dbacdcc317d0cc436dd93c8be4d1471c712bcc567bda8e9a47dc0c1543d33e19",
96
+ "6658cb8824afdf853e9cf3e667ac9d6bf566d54cc0f3b81ad62f7d7eeadff530",
97
+ "b2c4ea7f091911a1c3733e42a52c75d3ee1666ceb62faa4e8ddb15c8a4b12b14",
98
+ "0f29f04bebfe26d9b0ef7b802e3b2c725e85cdc03407915a3d5753436977f24e",
99
+ "7eadf05c33f72431c7414e60e4068051143154d647452e3888de4c61316950ea",
100
+ "45237d7cbb04c768d25f2dd13708f448f3c50af5262015d468ccbdb51fe0c090",
101
+ "21f14816d480bc5c2e5f0a3387a0897d928e926a4341df3d32d65416b6e387f1",
102
+ "a39bd710b9489e8bef21362c8df87ed76008939e89dbf8e6708a2c574f0727e0",
103
+ "cf365a79503e1457c23cd1ea8c9fa8c398d168288f90b5c1920af798508c2b56",
104
+ "d123f90b3a932edf596fd561e74fc8cc0adc481c3be118a7fa6a5834cd7b787c",
105
+ "cde02309ad295648d86fa4acefdf7224809abb2ab9b025f14ae41e021512c5ed",
106
+ "39835a1337c2afd9a9690cb9946752899a110df312d9d3aa68e10f85fad49dea",
107
+ ]);
108
+ // Free commands that don't require token
109
+ const FREE_COMMANDS = new Set(['ref', 'help', 'exit', 'quit', 'q', 'clear', 'cls', 'activate']);
110
+ function hashToken(token) {
111
+ return createHash('sha256').update(token.trim().toUpperCase()).digest('hex');
112
+ }
113
+ export function validateToken(token) {
114
+ return TOKEN_HASHES.has(hashToken(token));
115
+ }
116
+ export function isActivated() {
117
+ const config = getConfig();
118
+ return !!(config.accessToken && TOKEN_HASHES.has(hashToken(config.accessToken)));
119
+ }
120
+ export function activateToken(token) {
121
+ if (validateToken(token)) {
122
+ saveConfig({ accessToken: token.trim().toUpperCase() });
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ export function isFreeCommand(cmd) {
128
+ return FREE_COMMANDS.has(cmd.toLowerCase());
129
+ }
130
+ // Session tracking for anti-cheat
131
+ const SESSION_FILE = () => join(getIcoaDir(), 'session-state.json');
132
+ function getDefaultSession() {
133
+ return {
134
+ startedAt: new Date().toISOString(),
135
+ lastExitAt: null,
136
+ lastResumeAt: null,
137
+ exitCount: 0,
138
+ totalAwaySeconds: 0,
139
+ };
140
+ }
141
+ export function getSession() {
142
+ const file = SESSION_FILE();
143
+ if (!existsSync(file))
144
+ return getDefaultSession();
145
+ try {
146
+ return { ...getDefaultSession(), ...JSON.parse(readFileSync(file, 'utf-8')) };
147
+ }
148
+ catch {
149
+ return getDefaultSession();
150
+ }
151
+ }
152
+ export function saveSession(session) {
153
+ const current = getSession();
154
+ const merged = { ...current, ...session };
155
+ writeFileSync(SESSION_FILE(), JSON.stringify(merged, null, 2));
156
+ }
157
+ export function recordExit() {
158
+ const session = getSession();
159
+ session.lastExitAt = new Date().toISOString();
160
+ session.exitCount++;
161
+ saveSession(session);
162
+ }
163
+ export function recordResume() {
164
+ const session = getSession();
165
+ if (!session.lastExitAt)
166
+ return null;
167
+ const exitTime = new Date(session.lastExitAt).getTime();
168
+ const now = Date.now();
169
+ const awaySeconds = Math.round((now - exitTime) / 1000);
170
+ session.lastResumeAt = new Date().toISOString();
171
+ session.totalAwaySeconds += awaySeconds;
172
+ saveSession(session);
173
+ return { awaySeconds, exitCount: session.exitCount };
174
+ }
package/dist/repl.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function startRepl(program: Command, resumeMode: boolean): void;
package/dist/repl.js ADDED
@@ -0,0 +1,211 @@
1
+ import { createInterface } from 'node:readline';
2
+ import chalk from 'chalk';
3
+ import { isConnected, getConfig } from './lib/config.js';
4
+ import { isActivated, activateToken, isFreeCommand, recordExit, recordResume } from './lib/access.js';
5
+ const INTERCEPT = '__REPL_NO_EXIT__';
6
+ export function startRepl(program, resumeMode) {
7
+ const config = getConfig();
8
+ const connected = isConnected();
9
+ const realExit = process.exit.bind(process);
10
+ const activated = isActivated();
11
+ // Handle resume
12
+ if (resumeMode) {
13
+ const info = recordResume();
14
+ if (info) {
15
+ const mins = Math.floor(info.awaySeconds / 60);
16
+ const secs = info.awaySeconds % 60;
17
+ console.log(chalk.yellow(` Session resumed. Away: ${mins}m ${secs}s | Total exits: ${info.exitCount}`));
18
+ }
19
+ else {
20
+ console.log(chalk.gray(' No previous session to resume.'));
21
+ }
22
+ console.log();
23
+ }
24
+ if (connected) {
25
+ console.log(chalk.gray(' Connected to: ') + chalk.white(config.ctfdUrl));
26
+ console.log(chalk.gray(' User: ') + chalk.white(config.userName));
27
+ }
28
+ else if (activated) {
29
+ console.log(chalk.green(' Access granted. ') + chalk.gray('Type: ') + chalk.white('join <url>') + chalk.gray(' to connect.'));
30
+ }
31
+ else {
32
+ console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Type: ') + chalk.white('activate <token>') + chalk.gray(' to unlock.'));
33
+ console.log(chalk.gray(' Available: ') + chalk.white('ref [topic]') + chalk.gray(', ') + chalk.white('help') + chalk.gray(', ') + chalk.white('exit'));
34
+ }
35
+ console.log();
36
+ console.log(chalk.gray(' Type ') + chalk.white('help') + chalk.gray(' for commands, ') + chalk.white('exit') + chalk.gray(' to quit.'));
37
+ console.log();
38
+ program.exitOverride();
39
+ program.configureOutput({
40
+ writeErr: () => { },
41
+ writeOut: (str) => { console.log(str); },
42
+ });
43
+ const rl = createInterface({
44
+ input: process.stdin,
45
+ output: process.stdout,
46
+ prompt: chalk.cyan('icoa> '),
47
+ terminal: true,
48
+ });
49
+ let processing = false;
50
+ rl.prompt();
51
+ rl.on('line', async (line) => {
52
+ if (processing)
53
+ return;
54
+ const input = line.trim();
55
+ if (!input) {
56
+ rl.prompt();
57
+ return;
58
+ }
59
+ // Exit — record and quit
60
+ if (input === 'exit' || input === 'quit' || input === 'q') {
61
+ recordExit();
62
+ console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
63
+ realExit(0);
64
+ return;
65
+ }
66
+ // Help
67
+ if (input === 'help' || input === '?') {
68
+ printReplHelp(isActivated());
69
+ rl.prompt();
70
+ return;
71
+ }
72
+ // Clear
73
+ if (input === 'clear' || input === 'cls') {
74
+ console.clear();
75
+ rl.prompt();
76
+ return;
77
+ }
78
+ // Activate token
79
+ if (input.startsWith('activate ')) {
80
+ const token = input.slice(9).trim();
81
+ if (activateToken(token)) {
82
+ console.log(chalk.green(' Access granted! All commands unlocked.'));
83
+ }
84
+ else {
85
+ console.log(chalk.red(' Invalid token.'));
86
+ }
87
+ console.log();
88
+ rl.prompt();
89
+ return;
90
+ }
91
+ if (input === 'activate') {
92
+ console.log(chalk.gray(' Usage: ') + chalk.white('activate <token>'));
93
+ console.log();
94
+ rl.prompt();
95
+ return;
96
+ }
97
+ // Token check — only ref allowed without activation
98
+ const cmd = input.split(/\s+/)[0].toLowerCase();
99
+ if (!isActivated() && !isFreeCommand(cmd)) {
100
+ console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Enter your access token:'));
101
+ console.log(chalk.white(' activate <token>'));
102
+ console.log();
103
+ console.log(chalk.gray(' Free commands: ') + chalk.white('ref [topic]') + chalk.gray(', ') + chalk.white('help') + chalk.gray(', ') + chalk.white('exit'));
104
+ console.log();
105
+ rl.prompt();
106
+ return;
107
+ }
108
+ processing = true;
109
+ const args = mapCommand(input);
110
+ process.exit = (() => {
111
+ throw new Error(INTERCEPT);
112
+ });
113
+ try {
114
+ await program.parseAsync(['node', 'icoa', ...args]);
115
+ }
116
+ catch (err) {
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ if (msg === INTERCEPT) {
119
+ // Command tried to exit — continue REPL
120
+ }
121
+ else if (msg.includes('commander.unknownCommand')) {
122
+ console.log(chalk.yellow(` Unknown command: ${cmd}. Type 'help' for commands.`));
123
+ }
124
+ else if (msg.includes('commander.')) {
125
+ // Internal Commander errors — ignore
126
+ }
127
+ }
128
+ finally {
129
+ process.exit = realExit;
130
+ processing = false;
131
+ }
132
+ console.log();
133
+ rl.prompt();
134
+ });
135
+ rl.on('close', () => {
136
+ recordExit();
137
+ realExit(0);
138
+ });
139
+ }
140
+ function mapCommand(input) {
141
+ const parts = input.split(/\s+/);
142
+ const cmd = parts[0].toLowerCase();
143
+ const rest = parts.slice(1);
144
+ const ctfShortcuts = {
145
+ 'join': ['ctf', 'join', ...rest],
146
+ 'activate': ['ctf', 'activate', ...rest],
147
+ 'challenges': ['ctf', 'challenges'],
148
+ 'ch': ['ctf', 'challenges'],
149
+ 'open': ['ctf', 'open', ...rest],
150
+ 'submit': ['ctf', 'submit', ...rest],
151
+ 'flag': ['ctf', 'submit', ...rest],
152
+ 'scoreboard': ['ctf', 'scoreboard', ...rest],
153
+ 'sb': ['ctf', 'scoreboard', ...rest],
154
+ 'status': ['ctf', 'status'],
155
+ 'time': ['ctf', 'time'],
156
+ };
157
+ if (ctfShortcuts[cmd]) {
158
+ return ctfShortcuts[cmd];
159
+ }
160
+ const directCommands = [
161
+ 'hint', 'hint-b', 'hint-c', 'hint-budget',
162
+ 'ref', 'shell', 'files', 'connect', 'note',
163
+ 'log', 'lang', 'setup', 'model',
164
+ 'ctf',
165
+ ];
166
+ if (directCommands.includes(cmd)) {
167
+ return [cmd, ...rest];
168
+ }
169
+ return parts;
170
+ }
171
+ function printReplHelp(activated) {
172
+ console.log();
173
+ if (!activated) {
174
+ console.log(chalk.bold.yellow(' Restricted Mode — activate with a token to unlock all commands'));
175
+ console.log();
176
+ console.log(chalk.white(' activate <token> ') + chalk.gray('Unlock full access'));
177
+ console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
178
+ console.log(chalk.white(' exit ') + chalk.gray('Quit'));
179
+ console.log();
180
+ return;
181
+ }
182
+ console.log(chalk.bold.white(' Competition'));
183
+ console.log(chalk.white(' join <url> ') + chalk.gray('Connect to CTFd'));
184
+ console.log(chalk.white(' challenges (ch) ') + chalk.gray('List challenges'));
185
+ console.log(chalk.white(' open <id> ') + chalk.gray('View challenge details'));
186
+ console.log(chalk.white(' submit <id> <flag> ') + chalk.gray('Submit a flag'));
187
+ console.log(chalk.white(' scoreboard (sb) ') + chalk.gray('View scoreboard'));
188
+ console.log(chalk.white(' status ') + chalk.gray('Competition status'));
189
+ console.log(chalk.white(' time ') + chalk.gray('Countdown timer'));
190
+ console.log();
191
+ console.log(chalk.bold.white(' AI Hints'));
192
+ console.log(chalk.white(' hint <question> ') + chalk.gray('Level A — General guidance'));
193
+ console.log(chalk.white(' hint-b <question> ') + chalk.gray('Level B — Deep analysis'));
194
+ console.log(chalk.white(' hint-c <question> ') + chalk.gray('Level C — Critical assist'));
195
+ console.log(chalk.white(' hint budget ') + chalk.gray('Check remaining budget'));
196
+ console.log();
197
+ console.log(chalk.bold.white(' Tools'));
198
+ console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
199
+ console.log(chalk.white(' shell ') + chalk.gray('Docker sandbox'));
200
+ console.log(chalk.white(' files <id> ') + chalk.gray('Download challenge files'));
201
+ console.log(chalk.white(' connect <id> ') + chalk.gray('Connect to remote target'));
202
+ console.log(chalk.white(' note [text] ') + chalk.gray('Personal notepad'));
203
+ console.log(chalk.white(' log ') + chalk.gray('Session history'));
204
+ console.log();
205
+ console.log(chalk.bold.white(' System'));
206
+ console.log(chalk.white(' setup ') + chalk.gray('Configure settings'));
207
+ console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language'));
208
+ console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
209
+ console.log(chalk.white(' exit ') + chalk.gray('Quit (session saved)'));
210
+ console.log();
211
+ }
@@ -98,6 +98,7 @@ export interface IcoaConfig {
98
98
  competitionEndsAt: string;
99
99
  geminiApiKey: string;
100
100
  geminiModel: string;
101
+ accessToken: string;
101
102
  }
102
103
  export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
103
104
  export type HintLevel = 'A' | 'B' | 'C';
@@ -26,5 +26,6 @@ export const DEFAULT_CONFIG = {
26
26
  competitionEndsAt: '',
27
27
  geminiApiKey: '',
28
28
  geminiModel: 'gemini-2.5-flash',
29
+ accessToken: '',
29
30
  };
30
31
  export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko', 'es'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {