icoa-cli 2.19.100 → 2.19.101

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 (45) hide show
  1. package/dist/commands/ai4ctf.js +1 -700
  2. package/dist/commands/connect.js +1 -66
  3. package/dist/commands/ctf.js +1 -620
  4. package/dist/commands/ctf4ai-demo.js +1 -525
  5. package/dist/commands/env.js +1 -738
  6. package/dist/commands/exam.js +1 -2353
  7. package/dist/commands/files.js +1 -52
  8. package/dist/commands/hint.js +1 -119
  9. package/dist/commands/lang.js +1 -155
  10. package/dist/commands/log.js +1 -165
  11. package/dist/commands/note.js +1 -40
  12. package/dist/commands/ref.js +1 -68
  13. package/dist/commands/setup.js +1 -122
  14. package/dist/commands/shell.js +1 -55
  15. package/dist/commands/theme.js +1 -50
  16. package/dist/index.js +1 -225
  17. package/dist/lib/access.js +1 -246
  18. package/dist/lib/budget.js +1 -42
  19. package/dist/lib/colors.js +1 -21
  20. package/dist/lib/config.js +1 -60
  21. package/dist/lib/ctfd-client.js +1 -274
  22. package/dist/lib/demo-exam.js +1 -249
  23. package/dist/lib/demo-flags.js +1 -27
  24. package/dist/lib/demo-stats.js +1 -65
  25. package/dist/lib/exam-client.js +1 -57
  26. package/dist/lib/exam-setup.js +1 -23
  27. package/dist/lib/exam-state.js +1 -112
  28. package/dist/lib/gemini.js +1 -235
  29. package/dist/lib/i18n.js +1 -273
  30. package/dist/lib/log-sync.js +1 -110
  31. package/dist/lib/logger.js +1 -59
  32. package/dist/lib/paper-upgrade.js +1 -117
  33. package/dist/lib/platform.js +1 -86
  34. package/dist/lib/sandbox.js +1 -93
  35. package/dist/lib/terminal.js +1 -49
  36. package/dist/lib/theme.js +1 -108
  37. package/dist/lib/translation.js +1 -66
  38. package/dist/lib/ui.js +1 -80
  39. package/dist/lib/update-check.js +1 -102
  40. package/dist/postinstall.js +1 -48
  41. package/dist/repl.js +1 -1281
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/index.js +1 -38
  44. package/package.json +6 -2
  45. package/translations/sw/i18n-snippet.ts +1 -0
@@ -1,52 +1 @@
1
- import { join } from 'node:path';
2
- import { homedir } from 'node:os';
3
- import chalk from 'chalk';
4
- import { CTFdClient } from '../lib/ctfd-client.js';
5
- import { getConfig, isConnected } from '../lib/config.js';
6
- import { logCommand } from '../lib/logger.js';
7
- import { printError, createSpinner } from '../lib/ui.js';
8
- export function registerFilesCommand(program) {
9
- program
10
- .command('files <id>')
11
- .description('Download challenge files')
12
- .action(async (id) => {
13
- logCommand(`files ${id}`);
14
- const config = getConfig();
15
- if (!isConnected()) {
16
- printError('Not connected. Run: join <url>');
17
- return;
18
- }
19
- const client = new CTFdClient(config.ctfdUrl, config.token);
20
- const destDir = join(homedir(), 'icoa-challenges', id);
21
- const spinner = createSpinner('Fetching challenge files...');
22
- spinner.start();
23
- try {
24
- const files = await client.getChallengeFiles(parseInt(id));
25
- if (!files || files.length === 0) {
26
- spinner.info('No files attached to this challenge.');
27
- return;
28
- }
29
- spinner.text = `Downloading ${files.length} file(s)...`;
30
- const downloaded = [];
31
- for (const filePath of files) {
32
- try {
33
- const dest = await client.downloadFile(filePath, destDir);
34
- downloaded.push(dest);
35
- }
36
- catch (err) {
37
- spinner.warn(`Failed to download: ${filePath}`);
38
- }
39
- }
40
- spinner.succeed(`Downloaded ${downloaded.length} file(s)`);
41
- console.log(chalk.gray(` Location: ${destDir}`));
42
- for (const f of downloaded) {
43
- console.log(chalk.gray(` → ${f.split('/').pop()}`));
44
- }
45
- console.log();
46
- }
47
- catch (err) {
48
- spinner.fail('Failed to download files');
49
- printError(err.message);
50
- }
51
- });
52
- }
1
+ import{join as o}from"node:path";import{homedir as e}from"node:os";import chalk from"chalk";import{CTFdClient as t}from"../lib/ctfd-client.js";import{getConfig as l,isConnected as n}from"../lib/config.js";import{logCommand as i}from"../lib/logger.js";import{printError as s,createSpinner as a}from"../lib/ui.js";export function registerFilesCommand(c){c.command("files <id>").description("Download challenge files").action(async c=>{i(`files ${c}`);const r=l();if(!n())return void s("Not connected. Run: join <url>");const f=new t(r.ctfdUrl,r.token),d=o(e(),"icoa-challenges",c),g=a("Fetching challenge files...");g.start();try{const o=await f.getChallengeFiles(parseInt(c));if(!o||0===o.length)return void g.info("No files attached to this challenge.");g.text=`Downloading ${o.length} file(s)...`;const e=[];for(const t of o)try{const o=await f.downloadFile(t,d);e.push(o)}catch(o){g.warn(`Failed to download: ${t}`)}g.succeed(`Downloaded ${e.length} file(s)`),console.log(chalk.gray(` Location: ${d}`));for(const o of e)console.log(chalk.gray(` → ${o.split("/").pop()}`));console.log()}catch(o){g.fail("Failed to download files"),s(o.message)}})}
@@ -1,119 +1 @@
1
- import chalk from 'chalk';
2
- import { generateHint } from '../lib/gemini.js';
3
- import { checkBudget, deductBudget, getBudgetDisplay, isTokenCapReached } from '../lib/budget.js';
4
- import { getConfig } from '../lib/config.js';
5
- import { logHint } from '../lib/logger.js';
6
- import { printError, printMarkdown, printHeader, createSpinner } from '../lib/ui.js';
7
- function getChallengeContext() {
8
- const config = getConfig();
9
- if (config.currentChallengeName && config.currentChallengeCategory) {
10
- return {
11
- name: config.currentChallengeName,
12
- category: config.currentChallengeCategory,
13
- };
14
- }
15
- return undefined;
16
- }
17
- async function handleHint(level, question) {
18
- const config = getConfig();
19
- // Check token cap
20
- if (isTokenCapReached()) {
21
- printError('Token cap reached. No more AI hints available.');
22
- return;
23
- }
24
- // Check budget
25
- const { allowed, remaining } = checkBudget(level);
26
- if (!allowed) {
27
- printError(`Level ${level} hint budget exhausted (0 remaining).`);
28
- return;
29
- }
30
- // Level C warning (no confirm — crashes REPL)
31
- if (level === 'C') {
32
- console.log(chalk.red.bold(` Warning: Using 1 of ${remaining} Critical Assists remaining.`));
33
- }
34
- // Log BEFORE API call
35
- logHint(level, question, config.currentChallengeId || undefined);
36
- const context = getChallengeContext();
37
- const levelNames = {
38
- A: 'General Guidance',
39
- B: 'Deep Analysis',
40
- C: 'Critical Assist',
41
- };
42
- const spinner = createSpinner(`Getting Level ${level} hint (${levelNames[level]})...`);
43
- spinner.start();
44
- try {
45
- const result = await generateHint(level, question, context);
46
- spinner.stop();
47
- // Deduct budget after successful response
48
- deductBudget(level, result.tokensUsed);
49
- printHeader(`Level ${level} Hint — ${levelNames[level]}`);
50
- printMarkdown(result.text);
51
- // Budget summary after hint
52
- const afterBudget = checkBudget(level);
53
- console.log();
54
- console.log(chalk.gray(' ─────────────────────────────────────────────'));
55
- console.log(chalk.gray(` Tokens used: ${result.tokensUsed} | Level ${level} remaining: ${afterBudget.remaining}`));
56
- // Suggest next level if stuck
57
- if (level === 'A') {
58
- const bBudget = checkBudget('B');
59
- console.log(chalk.gray(' Need more detail? ') + chalk.white('hint-b "your question"') + chalk.gray(` (${bBudget.remaining} left)`));
60
- }
61
- else if (level === 'B') {
62
- const cBudget = checkBudget('C');
63
- if (cBudget.remaining > 0) {
64
- console.log(chalk.gray(' Still stuck? ') + chalk.white('hint-c "your question"') + chalk.gray(` (${cBudget.remaining} Critical Assists left)`));
65
- }
66
- }
67
- console.log(chalk.gray(' ─────────────────────────────────────────────'));
68
- console.log();
69
- }
70
- catch (err) {
71
- spinner.fail('Hint generation failed');
72
- printError(err.message);
73
- }
74
- }
75
- function showBudget() {
76
- printHeader('Hint Budget');
77
- console.log(getBudgetDisplay());
78
- console.log();
79
- console.log(chalk.gray(' How to use hints:'));
80
- console.log(chalk.white(' hint "what is XSS?"') + chalk.gray(' Level A — General guidance'));
81
- console.log(chalk.white(' hint-b "analyze this code"') + chalk.gray(' Level B — Deep analysis'));
82
- console.log(chalk.white(' hint-c "I\'m completely stuck"') + chalk.gray(' Level C — Critical assist'));
83
- console.log();
84
- }
85
- export function registerHintCommands(program) {
86
- // ─── icoa hint <question> ───
87
- // Special case: "icoa hint budget" shows budget instead of querying AI
88
- program
89
- .command('hint <question...>')
90
- .description('Level A hint — General guidance (use "hint budget" to check remaining)')
91
- .action(async (words) => {
92
- if (words[0] === 'budget') {
93
- showBudget();
94
- return;
95
- }
96
- await handleHint('A', words.join(' '));
97
- });
98
- // ─── icoa hint-b <question> ───
99
- program
100
- .command('hint-b <question...>')
101
- .description('Level B hint — Deep analysis')
102
- .action(async (words) => {
103
- await handleHint('B', words.join(' '));
104
- });
105
- // ─── icoa hint-c <question> ───
106
- program
107
- .command('hint-c <question...>')
108
- .description('Level C hint — Critical assist (confirmation required)')
109
- .action(async (words) => {
110
- await handleHint('C', words.join(' '));
111
- });
112
- // ─── icoa hint-budget (alternative) ───
113
- program
114
- .command('hint-budget')
115
- .description('Show remaining hint budget')
116
- .action(() => {
117
- showBudget();
118
- });
119
- }
1
+ import chalk from"chalk";import{generateHint as e}from"../lib/gemini.js";import{checkBudget as n,deductBudget as i,getBudgetDisplay as o,isTokenCapReached as t}from"../lib/budget.js";import{getConfig as a}from"../lib/config.js";import{logHint as s}from"../lib/logger.js";import{printError as l,printMarkdown as r,printHeader as c,createSpinner as g}from"../lib/ui.js";async function m(o,m){const u=a();if(t())return void l("Token cap reached. No more AI hints available.");const{allowed:d,remaining:h}=n(o);if(!d)return void l(`Level ${o} hint budget exhausted (0 remaining).`);"C"===o&&console.log(chalk.red.bold(` Warning: Using 1 of ${h} Critical Assists remaining.`)),s(o,m,u.currentChallengeId||void 0);const y=function(){const e=a();if(e.currentChallengeName&&e.currentChallengeCategory)return{name:e.currentChallengeName,category:e.currentChallengeCategory}}(),f={A:"General Guidance",B:"Deep Analysis",C:"Critical Assist"},C=g(`Getting Level ${o} hint (${f[o]})...`);C.start();try{const t=await e(o,m,y);C.stop(),i(o,t.tokensUsed),c(`Level ${o} Hint — ${f[o]}`),r(t.text);const a=n(o);if(console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(` Tokens used: ${t.tokensUsed} | Level ${o} remaining: ${a.remaining}`)),"A"===o){const e=n("B");console.log(chalk.gray(" Need more detail? ")+chalk.white('hint-b "your question"')+chalk.gray(` (${e.remaining} left)`))}else if("B"===o){const e=n("C");e.remaining>0&&console.log(chalk.gray(" Still stuck? ")+chalk.white('hint-c "your question"')+chalk.gray(` (${e.remaining} Critical Assists left)`))}console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log()}catch(e){C.fail("Hint generation failed"),l(e.message)}}function u(){c("Hint Budget"),console.log(o()),console.log(),console.log(chalk.gray(" How to use hints:")),console.log(chalk.white(' hint "what is XSS?"')+chalk.gray(" Level A — General guidance")),console.log(chalk.white(' hint-b "analyze this code"')+chalk.gray(" Level B — Deep analysis")),console.log(chalk.white(' hint-c "I\'m completely stuck"')+chalk.gray(" Level C — Critical assist")),console.log()}export function registerHintCommands(e){e.command("hint <question...>").description('Level A hint — General guidance (use "hint budget" to check remaining)').action(async e=>{"budget"!==e[0]?await m("A",e.join(" ")):u()}),e.command("hint-b <question...>").description("Level B hint — Deep analysis").action(async e=>{await m("B",e.join(" "))}),e.command("hint-c <question...>").description("Level C hint — Critical assist (confirmation required)").action(async e=>{await m("C",e.join(" "))}),e.command("hint-budget").description("Show remaining hint budget").action(()=>{u()})}
@@ -1,155 +1 @@
1
- import chalk from 'chalk';
2
- import { getConfig, saveConfig } from '../lib/config.js';
3
- import { getExamState } from '../lib/exam-state.js';
4
- import { logCommand } from '../lib/logger.js';
5
- import { printSuccess, printError, printInfo } from '../lib/ui.js';
6
- import { SUPPORTED_LANGUAGES } from '../types/index.js';
7
- const LANG_NAMES = {
8
- en: 'English',
9
- zh: '中文 (Chinese)',
10
- ja: '日本語 (Japanese)',
11
- ko: '한국어 (Korean)',
12
- es: 'Español (Spanish)',
13
- ar: 'العربية (Arabic)',
14
- fr: 'Français (French)',
15
- pt: 'Português (Portuguese)',
16
- ru: 'Русский (Russian)',
17
- hi: 'हिन्दी (Hindi)',
18
- de: 'Deutsch (German)',
19
- id: 'Bahasa (Indonesian)',
20
- th: 'ไทย (Thai)',
21
- vi: 'Tiếng Việt (Vietnamese)',
22
- tr: 'Türkçe (Turkish)',
23
- uk: 'Українська (Ukrainian)',
24
- ht: 'Kreyòl (Haitian Creole)',
25
- };
26
- export function registerLangCommand(program) {
27
- program
28
- .command('lang [code]')
29
- .description('Switch display language')
30
- .action(async (code) => {
31
- logCommand(`lang ${code || ''}`);
32
- if (!code) {
33
- const config = getConfig();
34
- printInfo(`Current language: ${chalk.white(LANG_NAMES[config.language] || config.language)}`);
35
- console.log();
36
- console.log(chalk.gray(' Supported languages:'));
37
- for (const lang of SUPPORTED_LANGUAGES) {
38
- const current = config.language === lang ? chalk.yellow(' ← current') : '';
39
- console.log(` ${chalk.white(lang)} ${LANG_NAMES[lang]}${current}`);
40
- }
41
- console.log();
42
- console.log(chalk.gray(' Switch now: ') + chalk.cyan('lang <code>') + chalk.gray(' (e.g. ') + chalk.cyan('lang es') + chalk.gray(')'));
43
- console.log(chalk.gray(' No "back" needed — you are still at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
44
- console.log();
45
- return;
46
- }
47
- if (!SUPPORTED_LANGUAGES.includes(code)) {
48
- printError(`Unsupported language: ${code}`);
49
- const supported = SUPPORTED_LANGUAGES.map((l) => `${l} (${LANG_NAMES[l]?.split(' ')[0] || l})`).join(', ');
50
- printInfo(`Supported: ${supported}`);
51
- return;
52
- }
53
- saveConfig({ language: code });
54
- printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
55
- // If demo in progress, re-translate each drawn question in place using
56
- // sourceNumber + sourceOrder so the user's answers and option positions are
57
- // preserved. If an older state lacks these fields (pre-v2.19.22), fall back
58
- // to restart-with-fresh-pick so nothing crashes.
59
- const state = getExamState();
60
- if (state && state.session.examId === 'demo-free') {
61
- try {
62
- const { pickDemoQuestions, getLocalizedDemoSession, getLocalizedDemoQuestions, getLocalizedExplanations, DEMO_PICK_SIZE, } = await import('../lib/demo-exam.js');
63
- const { saveExamState } = await import('../lib/exam-state.js');
64
- const canRetranslate = state.questions.every((q) => q.sourceNumber != null && Array.isArray(q.sourceOrder) && q.sourceOrder.length === 4);
65
- if (canRetranslate) {
66
- const pool = getLocalizedDemoQuestions();
67
- const explanations = getLocalizedExplanations();
68
- state.questions = state.questions.map((q) => {
69
- const src = pool.find((p) => p.number === q.sourceNumber);
70
- if (!src || !q.sourceOrder)
71
- return q;
72
- return {
73
- ...q,
74
- text: src.text,
75
- category: src.category,
76
- options: {
77
- A: src.options[q.sourceOrder[0]],
78
- B: src.options[q.sourceOrder[1]],
79
- C: src.options[q.sourceOrder[2]],
80
- D: src.options[q.sourceOrder[3]],
81
- },
82
- explanation: explanations[q.sourceNumber],
83
- };
84
- });
85
- state.session.examName = getLocalizedDemoSession().examName;
86
- saveExamState(state);
87
- const currentQ = state._lastQ || 1;
88
- console.log();
89
- console.log(chalk.green(` Demo continues in ${LANG_NAMES[code] || code}. Your progress is kept.`));
90
- console.log(chalk.white(` Resume: exam q ${currentQ}`));
91
- }
92
- else {
93
- // Legacy state from before v2.19.22 — safely reset
94
- state.questions = pickDemoQuestions(DEMO_PICK_SIZE);
95
- state.answers = {};
96
- state.session.examName = getLocalizedDemoSession().examName;
97
- state.session.startedAt = new Date().toISOString();
98
- state._lastQ = 1;
99
- saveExamState(state);
100
- console.log();
101
- console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
102
- console.log(chalk.white(' Type: exam q 1'));
103
- }
104
- }
105
- catch {
106
- console.log(chalk.gray(' Language changed. Type: demo'));
107
- }
108
- }
109
- else if (state && state.session.token) {
110
- // Real exam with token: re-fetch questions in new language from server
111
- try {
112
- const { getConfig } = await import('../lib/config.js');
113
- const { saveExamState } = await import('../lib/exam-state.js');
114
- const { getDeviceFingerprint } = await import('../lib/access.js');
115
- const config = getConfig();
116
- const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
117
- const token = state.session.token;
118
- const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
119
- method: 'POST',
120
- headers: { 'Content-Type': 'application/json' },
121
- body: JSON.stringify({ token, deviceHash: getDeviceFingerprint(), lang: code }),
122
- signal: AbortSignal.timeout(10000),
123
- });
124
- if (res.ok) {
125
- const json = await res.json();
126
- const newQuestions = json.data.questions;
127
- // Keep answers, interactions, aiUsage — only update question text
128
- state.questions = newQuestions;
129
- saveExamState(state);
130
- const currentQ = state._lastQ || 1;
131
- console.log();
132
- printSuccess(`Exam questions updated to ${LANG_NAMES[code] || code}. Your answers are kept.`);
133
- console.log(chalk.white(` Resume: exam q ${currentQ}`));
134
- }
135
- else {
136
- // Most common cause here: the stored session's token was already
137
- // submitted on the server (409) or the exam window has closed.
138
- // Surface the recovery path so the user isn't locked out.
139
- console.log(chalk.yellow(' Could not reload questions in new language.'));
140
- console.log(chalk.gray(' The saved session may be expired or already submitted server-side.'));
141
- console.log(chalk.gray(' To abandon it and start fresh: ') + chalk.bold.cyan('back') + chalk.gray(' → ') + chalk.bold.cyan('exam reset') + chalk.gray(' → ') + chalk.bold.cyan('exam <new-token>'));
142
- }
143
- }
144
- catch {
145
- console.log(chalk.yellow(' Could not reach server. Language changed for UI only.'));
146
- }
147
- }
148
- else if (state) {
149
- const currentQ = state._lastQ || 1;
150
- console.log();
151
- console.log(chalk.gray(` Exam in progress — resuming Q${currentQ}:`));
152
- console.log(chalk.white(` Type: exam q ${currentQ}`));
153
- }
154
- });
155
- }
1
+ import chalk from"chalk";import{getConfig as e,saveConfig as o}from"../lib/config.js";import{getExamState as s}from"../lib/exam-state.js";import{logCommand as a}from"../lib/logger.js";import{printSuccess as n,printError as t,printInfo as i}from"../lib/ui.js";import{SUPPORTED_LANGUAGES as r}from"../types/index.js";const l={en:"English",zh:"中文 (Chinese)",ja:"日本語 (Japanese)",ko:"한국어 (Korean)",es:"Español (Spanish)",ar:"العربية (Arabic)",fr:"Français (French)",pt:"Português (Portuguese)",ru:"Русский (Russian)",hi:"हिन्दी (Hindi)",de:"Deutsch (German)",id:"Bahasa (Indonesian)",th:"ไทย (Thai)",vi:"Tiếng Việt (Vietnamese)",tr:"Türkçe (Turkish)",uk:"Українська (Ukrainian)",ht:"Kreyòl (Haitian Creole)",sw:"Kiswahili (Swahili)"};export function registerLangCommand(c){c.command("lang [code]").description("Switch display language").action(async c=>{if(a(`lang ${c||""}`),!c){const o=e();i(`Current language: ${chalk.white(l[o.language]||o.language)}`),console.log(),console.log(chalk.gray(" Supported languages:"));for(const e of r){const s=o.language===e?chalk.yellow(" ← current"):"";console.log(` ${chalk.white(e)} ${l[e]}${s}`)}return console.log(),console.log(chalk.gray(" Switch now: ")+chalk.cyan("lang <code>")+chalk.gray(" (e.g. ")+chalk.cyan("lang es")+chalk.gray(")")),console.log(chalk.gray(' No "back" needed — you are still at the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log()}if(!r.includes(c)){t(`Unsupported language: ${c}`);const e=r.map(e=>`${e} (${l[e]?.split(" ")[0]||e})`).join(", ");return void i(`Supported: ${e}`)}o({language:c}),n(`Language set to: ${l[c]||c}`);const g=s();if(g&&"demo-free"===g.session.examId)try{const{pickDemoQuestions:e,getLocalizedDemoSession:o,getLocalizedDemoQuestions:s,getLocalizedExplanations:a,DEMO_PICK_SIZE:n}=await import("../lib/demo-exam.js"),{saveExamState:t}=await import("../lib/exam-state.js");if(g.questions.every(e=>null!=e.sourceNumber&&Array.isArray(e.sourceOrder)&&4===e.sourceOrder.length)){const e=s(),n=a();g.questions=g.questions.map(o=>{const s=e.find(e=>e.number===o.sourceNumber);return s&&o.sourceOrder?{...o,text:s.text,category:s.category,options:{A:s.options[o.sourceOrder[0]],B:s.options[o.sourceOrder[1]],C:s.options[o.sourceOrder[2]],D:s.options[o.sourceOrder[3]]},explanation:n[o.sourceNumber]}:o}),g.session.examName=o().examName,t(g);const i=g._lastQ||1;console.log(),console.log(chalk.green(` Demo continues in ${l[c]||c}. Your progress is kept.`)),console.log(chalk.white(` Resume: exam q ${i}`))}else g.questions=e(n),g.answers={},g.session.examName=o().examName,g.session.startedAt=(new Date).toISOString(),g._lastQ=1,t(g),console.log(),console.log(chalk.green(` Demo restarted in ${l[c]||c}.`)),console.log(chalk.white(" Type: exam q 1"))}catch{console.log(chalk.gray(" Language changed. Type: demo"))}else if(g&&g.session.token)try{const{getConfig:e}=await import("../lib/config.js"),{saveExamState:o}=await import("../lib/exam-state.js"),{getDeviceFingerprint:s}=await import("../lib/access.js"),a=e().ctfdUrl||"https://practice.icoa2026.au",t=g.session.token,i=await fetch(`${a}/api/icoa/exam-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t,deviceHash:s(),lang:c}),signal:AbortSignal.timeout(1e4)});if(i.ok){const e=(await i.json()).data.questions;g.questions=e,o(g);const s=g._lastQ||1;console.log(),n(`Exam questions updated to ${l[c]||c}. Your answers are kept.`),console.log(chalk.white(` Resume: exam q ${s}`))}else console.log(chalk.yellow(" Could not reload questions in new language.")),console.log(chalk.gray(" The saved session may be expired or already submitted server-side.")),console.log(chalk.gray(" To abandon it and start fresh: ")+chalk.bold.cyan("back")+chalk.gray(" → ")+chalk.bold.cyan("exam reset")+chalk.gray(" → ")+chalk.bold.cyan("exam <new-token>"))}catch{console.log(chalk.yellow(" Could not reach server. Language changed for UI only."))}else if(g){const e=g._lastQ||1;console.log(),console.log(chalk.gray(` Exam in progress — resuming Q${e}:`)),console.log(chalk.white(` Type: exam q ${e}`))}})}
@@ -1,165 +1 @@
1
- import chalk from 'chalk';
2
- import { readFileSync, existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { getSessionLog } from '../lib/logger.js';
5
- import { getIcoaDir, getConfig } from '../lib/config.js';
6
- import { printHeader, printInfo, printTable } from '../lib/ui.js';
7
- export function registerLogCommand(program) {
8
- const logCmd = program
9
- .command('log')
10
- .description('Display session history')
11
- .action(() => {
12
- showLog();
13
- });
14
- // icoa log export — export full audit log for post-competition review
15
- logCmd
16
- .command('export')
17
- .description('Export full audit log for review')
18
- .action(async () => {
19
- await exportLog();
20
- });
21
- // icoa log stats — show summary statistics
22
- logCmd
23
- .command('stats')
24
- .description('Show session statistics')
25
- .action(() => {
26
- showStats();
27
- });
28
- }
29
- function showLog() {
30
- const entries = getSessionLog();
31
- if (entries.length === 0) {
32
- printInfo('No session log entries yet.');
33
- return;
34
- }
35
- printHeader('Session Log');
36
- const rows = entries.map((entry) => {
37
- const time = entry.timestamp.replace('T', ' ').substring(0, 19);
38
- const levelColor = {
39
- A: chalk.green,
40
- B: chalk.yellow,
41
- C: chalk.red,
42
- command: chalk.blue,
43
- submit: chalk.magenta,
44
- };
45
- const colorFn = levelColor[entry.level] || chalk.gray;
46
- const input = entry.input.length > 60 ? entry.input.substring(0, 57) + '...' : entry.input;
47
- return [
48
- chalk.gray(time),
49
- colorFn(entry.level.padEnd(7)),
50
- input,
51
- ];
52
- });
53
- printTable(['Time', 'Type', 'Content'], rows);
54
- console.log(chalk.gray(` ${entries.length} entries total`));
55
- console.log();
56
- console.log(chalk.gray(' You are at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt. Also: ') + chalk.cyan('log stats') + chalk.gray(' · ') + chalk.cyan('log export') + chalk.gray(' · ') + chalk.cyan('help') + chalk.gray(' all commands.'));
57
- console.log();
58
- }
59
- async function exportLog() {
60
- const config = getConfig();
61
- const icoaDir = getIcoaDir();
62
- const logFile = join(icoaDir, 'session.log');
63
- const sessionFile = join(icoaDir, 'session-state.json');
64
- const configFile = join(icoaDir, 'config.json');
65
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
66
- const userName = config.userName || 'unknown';
67
- const exportName = `icoa-audit-${userName}-${timestamp}.json`;
68
- const exportPath = join(process.cwd(), exportName);
69
- // Gather all audit data
70
- const audit = {
71
- exportedAt: new Date().toISOString(),
72
- version: '1.7.2',
73
- competitor: {
74
- userName: config.userName,
75
- userId: config.userId,
76
- teamName: config.teamName,
77
- teamId: config.teamId,
78
- sessionId: config.sessionId,
79
- },
80
- connection: {
81
- ctfdUrl: config.ctfdUrl,
82
- },
83
- session: existsSync(sessionFile) ? JSON.parse(readFileSync(sessionFile, 'utf-8')) : null,
84
- commands: getSessionLog(),
85
- };
86
- // Count by type
87
- const entries = getSessionLog();
88
- const counts = {};
89
- for (const e of entries) {
90
- counts[e.level] = (counts[e.level] || 0) + 1;
91
- }
92
- audit.summary = {
93
- totalCommands: entries.length,
94
- byType: counts,
95
- firstEntry: entries[0]?.timestamp || null,
96
- lastEntry: entries[entries.length - 1]?.timestamp || null,
97
- };
98
- // Write export
99
- const { writeFileSync } = await import('node:fs');
100
- writeFileSync(exportPath, JSON.stringify(audit, null, 2));
101
- console.log();
102
- console.log(chalk.green(` ✓ Audit log exported`));
103
- console.log(chalk.white(` ${exportPath}`));
104
- console.log();
105
- console.log(chalk.gray(' Contents:'));
106
- console.log(chalk.gray(` Commands: ${entries.length}`));
107
- Object.entries(counts).forEach(([type, count]) => {
108
- console.log(chalk.gray(` ${type}: ${count}`));
109
- });
110
- console.log();
111
- console.log(chalk.gray(' This file contains the complete session audit trail.'));
112
- console.log(chalk.gray(' Submit to organizers for post-competition verification.'));
113
- console.log();
114
- }
115
- function showStats() {
116
- const entries = getSessionLog();
117
- const icoaDir = getIcoaDir();
118
- const sessionFile = join(icoaDir, 'session-state.json');
119
- console.log();
120
- console.log(chalk.bold.white(' Session Statistics'));
121
- console.log(chalk.gray(' ─────────────────────────────────────────────'));
122
- if (entries.length === 0) {
123
- console.log(chalk.gray(' No activity recorded yet.'));
124
- console.log();
125
- return;
126
- }
127
- // Time range
128
- const first = new Date(entries[0].timestamp);
129
- const last = new Date(entries[entries.length - 1].timestamp);
130
- const durationMin = Math.round((last.getTime() - first.getTime()) / 60000);
131
- console.log(chalk.gray(' First activity: ') + chalk.white(first.toLocaleString()));
132
- console.log(chalk.gray(' Last activity: ') + chalk.white(last.toLocaleString()));
133
- console.log(chalk.gray(' Duration: ') + chalk.white(`${durationMin} min`));
134
- console.log();
135
- // Count by type
136
- const counts = {};
137
- for (const e of entries) {
138
- counts[e.level] = (counts[e.level] || 0) + 1;
139
- }
140
- console.log(chalk.gray(' Total commands: ') + chalk.white(String(entries.length)));
141
- if (counts['command'])
142
- console.log(chalk.blue(' commands: ') + chalk.white(String(counts['command'])));
143
- if (counts['A'])
144
- console.log(chalk.green(' hint A: ') + chalk.white(String(counts['A'])));
145
- if (counts['B'])
146
- console.log(chalk.yellow(' hint B: ') + chalk.white(String(counts['B'])));
147
- if (counts['C'])
148
- console.log(chalk.red(' hint C: ') + chalk.white(String(counts['C'])));
149
- if (counts['submit'])
150
- console.log(chalk.magenta(' submissions: ') + chalk.white(String(counts['submit'])));
151
- // Exit info
152
- if (existsSync(sessionFile)) {
153
- try {
154
- const session = JSON.parse(readFileSync(sessionFile, 'utf-8'));
155
- console.log();
156
- console.log(chalk.gray(' Exit count: ') + chalk.white(String(session.exitCount || 0)));
157
- if (session.totalAwaySeconds) {
158
- const awayMin = Math.round(session.totalAwaySeconds / 60);
159
- console.log(chalk.gray(' Total away: ') + chalk.white(`${awayMin} min`));
160
- }
161
- }
162
- catch { /* ignore */ }
163
- }
164
- console.log();
165
- }
1
+ import chalk from"chalk";import{readFileSync as o,existsSync as t}from"node:fs";import{join as e}from"node:path";import{getSessionLog as n}from"../lib/logger.js";import{getIcoaDir as s,getConfig as i}from"../lib/config.js";import{printHeader as l,printInfo as a,printTable as r}from"../lib/ui.js";export function registerLogCommand(c){const g=c.command("log").description("Display session history").action(()=>{!function(){const o=n();if(0===o.length)return void a("No session log entries yet.");l("Session Log");const t=o.map(o=>{const t=o.timestamp.replace("T"," ").substring(0,19),e={A:chalk.green,B:chalk.yellow,C:chalk.red,command:chalk.blue,submit:chalk.magenta}[o.level]||chalk.gray,n=o.input.length>60?o.input.substring(0,57)+"...":o.input;return[chalk.gray(t),e(o.level.padEnd(7)),n]});r(["Time","Type","Content"],t),console.log(chalk.gray(` ${o.length} entries total`)),console.log(),console.log(chalk.gray(" You are at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt. Also: ")+chalk.cyan("log stats")+chalk.gray(" · ")+chalk.cyan("log export")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" all commands.")),console.log()}()});g.command("export").description("Export full audit log for review").action(async()=>{await async function(){const l=i(),a=s(),r=(e(a,"session.log"),e(a,"session-state.json")),c=(e(a,"config.json"),(new Date).toISOString().replace(/[:.]/g,"-").substring(0,19)),g=`icoa-audit-${l.userName||"unknown"}-${c}.json`,m=e(process.cwd(),g),y={exportedAt:(new Date).toISOString(),version:"1.7.2",competitor:{userName:l.userName,userId:l.userId,teamName:l.teamName,teamId:l.teamId,sessionId:l.sessionId},connection:{ctfdUrl:l.ctfdUrl},session:t(r)?JSON.parse(o(r,"utf-8")):null,commands:n()},d=n(),u={};for(const o of d)u[o.level]=(u[o.level]||0)+1;y.summary={totalCommands:d.length,byType:u,firstEntry:d[0]?.timestamp||null,lastEntry:d[d.length-1]?.timestamp||null};const{writeFileSync:p}=await import("node:fs");p(m,JSON.stringify(y,null,2)),console.log(),console.log(chalk.green(" ✓ Audit log exported")),console.log(chalk.white(` ${m}`)),console.log(),console.log(chalk.gray(" Contents:")),console.log(chalk.gray(` Commands: ${d.length}`)),Object.entries(u).forEach(([o,t])=>{console.log(chalk.gray(` ${o}: ${t}`))}),console.log(),console.log(chalk.gray(" This file contains the complete session audit trail.")),console.log(chalk.gray(" Submit to organizers for post-competition verification.")),console.log()}()}),g.command("stats").description("Show session statistics").action(()=>{!function(){const i=n(),l=s(),a=e(l,"session-state.json");if(console.log(),console.log(chalk.bold.white(" Session Statistics")),console.log(chalk.gray(" ─────────────────────────────────────────────")),0===i.length)return console.log(chalk.gray(" No activity recorded yet.")),void console.log();const r=new Date(i[0].timestamp),c=new Date(i[i.length-1].timestamp),g=Math.round((c.getTime()-r.getTime())/6e4);console.log(chalk.gray(" First activity: ")+chalk.white(r.toLocaleString())),console.log(chalk.gray(" Last activity: ")+chalk.white(c.toLocaleString())),console.log(chalk.gray(" Duration: ")+chalk.white(`${g} min`)),console.log();const m={};for(const o of i)m[o.level]=(m[o.level]||0)+1;if(console.log(chalk.gray(" Total commands: ")+chalk.white(String(i.length))),m.command&&console.log(chalk.blue(" commands: ")+chalk.white(String(m.command))),m.A&&console.log(chalk.green(" hint A: ")+chalk.white(String(m.A))),m.B&&console.log(chalk.yellow(" hint B: ")+chalk.white(String(m.B))),m.C&&console.log(chalk.red(" hint C: ")+chalk.white(String(m.C))),m.submit&&console.log(chalk.magenta(" submissions: ")+chalk.white(String(m.submit))),t(a))try{const t=JSON.parse(o(a,"utf-8"));if(console.log(),console.log(chalk.gray(" Exit count: ")+chalk.white(String(t.exitCount||0))),t.totalAwaySeconds){const o=Math.round(t.totalAwaySeconds/60);console.log(chalk.gray(" Total away: ")+chalk.white(`${o} min`))}}catch{}console.log()}()})}
@@ -1,40 +1 @@
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: note "your note here"');
18
- console.log();
19
- console.log(chalk.gray(' You are at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt. Type another command or ') + chalk.cyan('help') + chalk.gray(' for the list.'));
20
- console.log();
21
- return;
22
- }
23
- const content = readFileSync(NOTES_FILE, 'utf-8');
24
- printHeader('Notes');
25
- console.log(content);
26
- console.log();
27
- console.log(chalk.gray(' ─────────────────────────────────────────────'));
28
- console.log(chalk.gray(' End of notes. You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
29
- console.log(chalk.gray(' Add one: ') + chalk.cyan('note "your text"') + chalk.gray(' · ') + chalk.cyan('help') + chalk.gray(' for all commands'));
30
- console.log();
31
- return;
32
- }
33
- // Add new note
34
- const text = words.join(' ');
35
- const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
36
- const entry = `[${timestamp}] ${text}\n`;
37
- appendFileSync(NOTES_FILE, entry);
38
- printSuccess(`Note saved: ${chalk.gray(text)}`);
39
- });
40
- }
1
+ import{appendFileSync as o,readFileSync as e,existsSync as t}from"node:fs";import{join as n}from"node:path";import{homedir as r}from"node:os";import chalk from"chalk";import{printSuccess as a,printInfo as l,printHeader as c}from"../lib/ui.js";import{logCommand as s}from"../lib/logger.js";const i=n(r(),"icoa-notes.txt");export function registerNoteCommand(n){n.command("note [text...]").description("Add or view personal notes").action(n=>{if(s(`note ${n?.join(" ")||""}`),!n||0===n.length){if(!t(i))return l('No notes yet. Add one with: note "your note here"'),console.log(),console.log(chalk.gray(" You are at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt. Type another command or ")+chalk.cyan("help")+chalk.gray(" for the list.")),void console.log();const o=e(i,"utf-8");return c("Notes"),console.log(o),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" End of notes. You are back at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt.")),console.log(chalk.gray(" Add one: ")+chalk.cyan('note "your text"')+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" for all commands")),void console.log()}const r=n.join(" "),g=(new Date).toISOString().replace("T"," ").substring(0,19);o(i,`[${g}] ${r}\n`),a(`Note saved: ${chalk.gray(r)}`)})}
@@ -1,68 +1 @@
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(' Open one: ') + chalk.cyan('ref <topic>') + chalk.gray(' (e.g. ') + chalk.cyan('ref python') + chalk.gray(')'));
48
- console.log(chalk.gray(' No "back" needed — ref just prints and returns to the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
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 "ref" to see available topics.');
57
- return;
58
- }
59
- const content = readFileSync(filePath, 'utf-8');
60
- printHeader(`Reference: ${topic}`);
61
- console.log(content);
62
- console.log();
63
- console.log(chalk.gray(' ─────────────────────────────────────────────'));
64
- console.log(chalk.gray(' End of ') + chalk.cyan(`ref ${topic}`) + chalk.gray('. You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
65
- console.log(chalk.gray(' Next: ') + chalk.cyan('ref') + chalk.gray(' list topics · ') + chalk.cyan('ref <topic>') + chalk.gray(' open another · ') + chalk.cyan('help') + chalk.gray(' all commands'));
66
- console.log();
67
- });
68
- }
1
+ import{readdirSync as o,readFileSync as e,existsSync as r}from"node:fs";import{join as n,dirname as t}from"node:path";import{fileURLToPath as c}from"node:url";import chalk from"chalk";import{printHeader as a,printInfo as i,printError as f,printTable as l}from"../lib/ui.js";import{logCommand as s}from"../lib/logger.js";export function registerRefCommand(p){p.command("ref [topic]").description("Quick reference for tools and commands").action(p=>{s(`ref ${p||""}`);const g=function(){const o=t(c(import.meta.url)),e=[n(o,"..","refs"),n(o,"..","..","refs"),n(process.cwd(),"refs")];for(const o of e)if(r(o))return o;return e[0]}();if(!p){if(a("Available References"),!r(g))return void f("Reference files not found.");const e=o(g).filter(o=>o.endsWith(".txt")).map(o=>o.replace(".txt","")).sort();if(0===e.length)return void i("No reference files available.");const n=e.map(o=>[chalk.white(o)]);return l(["Topic"],n),console.log(),console.log(chalk.gray(" Open one: ")+chalk.cyan("ref <topic>")+chalk.gray(" (e.g. ")+chalk.cyan("ref python")+chalk.gray(")")),console.log(chalk.gray(' No "back" needed — ref just prints and returns to the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log()}const m=n(g,`${p}.txt`);if(!r(m))return f(`Reference not found: ${p}`),void i('Run "ref" to see available topics.');const y=e(m,"utf-8");a(`Reference: ${p}`),console.log(y),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" End of ")+chalk.cyan(`ref ${p}`)+chalk.gray(". You are back at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt.")),console.log(chalk.gray(" Next: ")+chalk.cyan("ref")+chalk.gray(" list topics · ")+chalk.cyan("ref <topic>")+chalk.gray(" open another · ")+chalk.cyan("help")+chalk.gray(" all commands")),console.log()})}