icoa-cli 1.2.0 → 1.3.1

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
@@ -29,7 +29,10 @@ ${chalk.cyan('║')} ${
29
29
  ${chalk.cyan('║')} ${chalk.green.bold('AI4CTF')} ${chalk.gray('Use AI to solve challenges')} ${chalk.cyan('║')}
30
30
  ${chalk.cyan('║')} ${chalk.red.bold('CTF4AI')} ${chalk.gray('Hack, attack & evaluate AI systems')} ${chalk.cyan('║')}
31
31
  ${chalk.cyan('║')} ${chalk.cyan('║')}
32
- ${chalk.cyan('║')} ${chalk.gray('CLI-Native Competition Terminal v1.2.0')} ${chalk.cyan('║')}
32
+ ${chalk.cyan('║')} ${chalk.white.bold('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')} ${chalk.cyan('║')}
33
+ ${chalk.cyan('║')} ${chalk.cyan.underline('https://icoa2026.au')} ${chalk.cyan('║')}
34
+ ${chalk.cyan('║')} ${chalk.cyan('║')}
35
+ ${chalk.cyan('║')} ${chalk.gray('CLI-Native Competition Terminal v1.3.1')} ${chalk.cyan('║')}
33
36
  ${chalk.cyan('║')} ${chalk.cyan('║')}
34
37
  ${chalk.cyan('╚══════════════════════════════════════════════════════════╝')}
35
38
  `;
@@ -50,13 +53,14 @@ process.on('unhandledRejection', (reason) => {
50
53
  const program = new Command();
51
54
  program
52
55
  .name('icoa')
53
- .version('1.0.0')
56
+ .version('1.2.0')
54
57
  .description('ICOA CLI — CLI-Native CTF Competition Terminal')
55
- .action(() => {
58
+ .option('--resume', 'Resume previous session')
59
+ .action((opts) => {
56
60
  console.log(BANNER);
57
- // If running interactively (no extra args), start REPL
58
- if (process.argv.length <= 2) {
59
- startRepl(program);
61
+ // If running interactively (no extra args or --resume), start REPL
62
+ if (process.argv.length <= 2 || opts.resume) {
63
+ startRepl(program, !!opts.resume);
60
64
  return;
61
65
  }
62
66
  });
@@ -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 CHANGED
@@ -1,2 +1,2 @@
1
1
  import { Command } from 'commander';
2
- export declare function startRepl(program: Command): void;
2
+ export declare function startRepl(program: Command, resumeMode: boolean): void;
package/dist/repl.js CHANGED
@@ -1,23 +1,40 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import chalk from 'chalk';
3
3
  import { isConnected, getConfig } from './lib/config.js';
4
- // Sentinel error used to intercept process.exit() calls from commands
4
+ import { isActivated, activateToken, isFreeCommand, recordExit, recordResume } from './lib/access.js';
5
5
  const INTERCEPT = '__REPL_NO_EXIT__';
6
- export function startRepl(program) {
6
+ export function startRepl(program, resumeMode) {
7
7
  const config = getConfig();
8
8
  const connected = isConnected();
9
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
+ }
10
24
  if (connected) {
11
25
  console.log(chalk.gray(' Connected to: ') + chalk.white(config.ctfdUrl));
12
26
  console.log(chalk.gray(' User: ') + chalk.white(config.userName));
13
27
  }
28
+ else if (activated) {
29
+ console.log(chalk.green(' Access granted. ') + chalk.gray('Type: ') + chalk.white('join <url>') + chalk.gray(' to connect.'));
30
+ }
14
31
  else {
15
- console.log(chalk.gray(' Not connected. Type: ') + chalk.white('join <url>'));
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'));
16
34
  }
17
35
  console.log();
18
36
  console.log(chalk.gray(' Type ') + chalk.white('help') + chalk.gray(' for commands, ') + chalk.white('exit') + chalk.gray(' to quit.'));
19
37
  console.log();
20
- // Configure Commander to throw instead of calling process.exit
21
38
  program.exitOverride();
22
39
  program.configureOutput({
23
40
  writeErr: () => { },
@@ -39,25 +56,57 @@ export function startRepl(program) {
39
56
  rl.prompt();
40
57
  return;
41
58
  }
42
- // Built-in REPL commands handled with real process.exit
59
+ // Exitrecord and quit
43
60
  if (input === 'exit' || input === 'quit' || input === 'q') {
44
- console.log(chalk.gray(' Goodbye!'));
61
+ recordExit();
62
+ console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
45
63
  realExit(0);
46
64
  return;
47
65
  }
66
+ // Help
48
67
  if (input === 'help' || input === '?') {
49
- printReplHelp();
68
+ printReplHelp(isActivated());
50
69
  rl.prompt();
51
70
  return;
52
71
  }
72
+ // Clear
53
73
  if (input === 'clear' || input === 'cls') {
54
74
  console.clear();
55
75
  rl.prompt();
56
76
  return;
57
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
+ }
58
108
  processing = true;
59
109
  const args = mapCommand(input);
60
- // Override process.exit so commands don't kill the REPL
61
110
  process.exit = (() => {
62
111
  throw new Error(INTERCEPT);
63
112
  });
@@ -67,17 +116,16 @@ export function startRepl(program) {
67
116
  catch (err) {
68
117
  const msg = err instanceof Error ? err.message : String(err);
69
118
  if (msg === INTERCEPT) {
70
- // Command tried to exit — that's fine
119
+ // Command tried to exit — continue REPL
71
120
  }
72
121
  else if (msg.includes('commander.unknownCommand')) {
73
- console.log(chalk.yellow(` Unknown command: ${input.split(' ')[0]}. Type 'help' for commands.`));
122
+ console.log(chalk.yellow(` Unknown command: ${cmd}. Type 'help' for commands.`));
74
123
  }
75
124
  else if (msg.includes('commander.')) {
76
125
  // Internal Commander errors — ignore
77
126
  }
78
127
  }
79
128
  finally {
80
- // Always restore real process.exit
81
129
  process.exit = realExit;
82
130
  processing = false;
83
131
  }
@@ -85,6 +133,7 @@ export function startRepl(program) {
85
133
  rl.prompt();
86
134
  });
87
135
  rl.on('close', () => {
136
+ recordExit();
88
137
  realExit(0);
89
138
  });
90
139
  }
@@ -119,8 +168,17 @@ function mapCommand(input) {
119
168
  }
120
169
  return parts;
121
170
  }
122
- function printReplHelp() {
171
+ function printReplHelp(activated) {
123
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
+ }
124
182
  console.log(chalk.bold.white(' Competition'));
125
183
  console.log(chalk.white(' join <url> ') + chalk.gray('Connect to CTFd'));
126
184
  console.log(chalk.white(' challenges (ch) ') + chalk.gray('List challenges'));
@@ -148,6 +206,6 @@ function printReplHelp() {
148
206
  console.log(chalk.white(' setup ') + chalk.gray('Configure settings'));
149
207
  console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language'));
150
208
  console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
151
- console.log(chalk.white(' exit ') + chalk.gray('Quit ICOA CLI'));
209
+ console.log(chalk.white(' exit ') + chalk.gray('Quit (session saved)'));
152
210
  console.log();
153
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.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {