icoa-cli 2.19.357 → 2.19.359

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 (75) hide show
  1. package/dist/commands/ai4ctf.js +1 -1
  2. package/dist/commands/ctf.js +1 -787
  3. package/dist/commands/ctf4ai-demo.js +1 -1
  4. package/dist/commands/ctf4vla.js +1 -1
  5. package/dist/commands/demo2.js +1 -1502
  6. package/dist/commands/doctor.d.ts +2 -0
  7. package/dist/commands/doctor.js +1 -0
  8. package/dist/commands/exam.js +1 -1
  9. package/dist/commands/files.js +1 -59
  10. package/dist/commands/lang.js +1 -202
  11. package/dist/commands/log.js +1 -171
  12. package/dist/commands/shell.js +1 -151
  13. package/dist/commands/sim.js +1 -389
  14. package/dist/index.js +1 -355
  15. package/dist/lib/access.js +1 -184
  16. package/dist/lib/aienv.js +1 -205
  17. package/dist/lib/arena-submit.js +1 -21
  18. package/dist/lib/banner.js +1 -31
  19. package/dist/lib/budget.js +1 -6
  20. package/dist/lib/challenge-dir.js +1 -16
  21. package/dist/lib/colors.js +1 -17
  22. package/dist/lib/comms.js +1 -212
  23. package/dist/lib/config.js +1 -93
  24. package/dist/lib/countdown.js +1 -43
  25. package/dist/lib/country-lang.js +1 -39
  26. package/dist/lib/ctfd-client.js +1 -417
  27. package/dist/lib/demo-exam.js +1 -478
  28. package/dist/lib/demo-flags.js +1 -27
  29. package/dist/lib/demo-stats.js +1 -62
  30. package/dist/lib/demo2-progress.js +1 -102
  31. package/dist/lib/docker-probe.js +1 -118
  32. package/dist/lib/editor-spawn.js +1 -53
  33. package/dist/lib/exam-client.js +1 -54
  34. package/dist/lib/exam-sandbox.js +1 -201
  35. package/dist/lib/exam-setup.js +1 -36
  36. package/dist/lib/exam-state.js +1 -273
  37. package/dist/lib/gemini.js +1 -247
  38. package/dist/lib/i18n.js +1 -302
  39. package/dist/lib/integrity-snapshot.js +1 -88
  40. package/dist/lib/interactive-spawn.js +1 -55
  41. package/dist/lib/ipynb-input.js +1 -65
  42. package/dist/lib/kernel-protocol.js +1 -88
  43. package/dist/lib/kernel.js +2 -146
  44. package/dist/lib/learn-curricula.js +1 -309
  45. package/dist/lib/learn-i18n.js +1 -184
  46. package/dist/lib/learn-input.js +1 -101
  47. package/dist/lib/learn-render.js +1 -863
  48. package/dist/lib/learn-state.js +1 -103
  49. package/dist/lib/log-sync.js +1 -155
  50. package/dist/lib/logger.js +1 -49
  51. package/dist/lib/main-rl.js +1 -7
  52. package/dist/lib/menu-nav.js +1 -105
  53. package/dist/lib/notebook-doc.js +1 -137
  54. package/dist/lib/open-file.js +1 -55
  55. package/dist/lib/paper-upgrade.js +1 -119
  56. package/dist/lib/platform.js +1 -99
  57. package/dist/lib/render-card.js +1 -112
  58. package/dist/lib/repl-asker.js +1 -67
  59. package/dist/lib/sample-runner.js +1 -227
  60. package/dist/lib/sandbox.js +1 -144
  61. package/dist/lib/shell-split.js +1 -69
  62. package/dist/lib/sim-cooldown.js +1 -75
  63. package/dist/lib/theme.js +1 -119
  64. package/dist/lib/token-format.js +1 -74
  65. package/dist/lib/tool-man.js +1 -418
  66. package/dist/lib/toolset-hash.js +1 -48
  67. package/dist/lib/translation.js +1 -80
  68. package/dist/lib/translations-fetcher.js +1 -95
  69. package/dist/lib/ui.js +1 -99
  70. package/dist/lib/update-check.js +1 -114
  71. package/dist/lib/version.js +1 -24
  72. package/dist/postinstall.js +1 -48
  73. package/dist/repl.js +1 -2391
  74. package/dist/types/index.js +1 -63
  75. package/package.json +1 -1
@@ -1,59 +1 @@
1
- import chalk from 'chalk';
2
- import { CTFdClient } from '../lib/ctfd-client.js';
3
- import { getConfig, isConnected } from '../lib/config.js';
4
- import { challengeDownloadDir } from '../lib/challenge-dir.js';
5
- import { logCommand } from '../lib/logger.js';
6
- import { printError, createSpinner } from '../lib/ui.js';
7
- export function registerFilesCommand(program) {
8
- program
9
- .command('files <id>')
10
- .description('Download challenge files')
11
- .action(async (id) => {
12
- logCommand(`files ${id}`);
13
- const config = getConfig();
14
- if (!isConnected()) {
15
- printError('Not connected. Run: join <url>');
16
- return;
17
- }
18
- // Session-mode logins store the cookie string in config.token; passing it
19
- // as the API token sends `Authorization: Token session=…` → CTFd serves the
20
- // login page (HTML) and JSON parsing fails. Split token vs session cookie.
21
- const session = config.sessionCookie || '';
22
- const token = config.token && !config.token.includes('session=') ? config.token : '';
23
- const client = new CTFdClient(config.ctfdUrl, token, session || config.token);
24
- const destDir = challengeDownloadDir(id);
25
- const spinner = createSpinner('Fetching challenge files...');
26
- spinner.start();
27
- try {
28
- const files = await client.getChallengeFiles(parseInt(id, 10));
29
- if (!files || files.length === 0) {
30
- spinner.info('No files attached to this challenge.');
31
- return;
32
- }
33
- spinner.text = `Downloading ${files.length} file(s)...`;
34
- const downloaded = [];
35
- for (const filePath of files) {
36
- try {
37
- const dest = await client.downloadFile(filePath, destDir);
38
- downloaded.push(dest);
39
- }
40
- catch (_err) {
41
- spinner.warn(`Failed to download: ${filePath}`);
42
- }
43
- }
44
- spinner.succeed(`Downloaded ${downloaded.length} file(s)`);
45
- console.log(chalk.gray(` Location: ${destDir}`));
46
- for (const f of downloaded) {
47
- console.log(chalk.gray(` → ${f.split('/').pop()}`));
48
- }
49
- const firstName = downloaded.length ? downloaded[0].split('/').pop() : '<file>';
50
- console.log(chalk.gray(` Run tools on them directly, e.g. ${chalk.white(`file ${id}/${firstName}`)}`));
51
- console.log(chalk.gray(` Open an image/audio file with: ${chalk.white(`view ${id} <file>`)}`));
52
- console.log();
53
- }
54
- catch (err) {
55
- spinner.fail('Failed to download files');
56
- printError(err.message);
57
- }
58
- });
59
- }
1
+ import chalk from"chalk";import{CTFdClient as o}from"../lib/ctfd-client.js";import{getConfig as e,isConnected as i}from"../lib/config.js";import{challengeDownloadDir as l}from"../lib/challenge-dir.js";import{logCommand as t}from"../lib/logger.js";import{printError as n,createSpinner as s}from"../lib/ui.js";export function registerFilesCommand(c){c.command("files <id>").description("Download challenge files").action(async c=>{t(`files ${c}`);const a=e();if(!i())return void n("Not connected. Run: join <url>");const r=a.sessionCookie||"",f=a.token&&!a.token.includes("session=")?a.token:"",g=new o(a.ctfdUrl,f,r||a.token),d=l(c),h=s("Fetching challenge files...");h.start();try{const o=await g.getChallengeFiles(parseInt(c,10));if(!o||0===o.length)return void h.info("No files attached to this challenge.");h.text=`Downloading ${o.length} file(s)...`;const e=[];for(const i of o)try{const o=await g.downloadFile(i,d);e.push(o)}catch(o){h.warn(`Failed to download: ${i}`)}h.succeed(`Downloaded ${e.length} file(s)`),console.log(chalk.gray(` Location: ${d}`));for(const o of e)console.log(chalk.gray(` → ${o.split("/").pop()}`));const i=e.length?e[0].split("/").pop():"<file>";console.log(chalk.gray(` Run tools on them directly, e.g. ${chalk.white(`file ${c}/${i}`)}`)),console.log(chalk.gray(` Open an image/audio file with: ${chalk.white(`view ${c} <file>`)}`)),console.log()}catch(o){h.fail("Failed to download files"),n(o.message)}})}
@@ -1,202 +1 @@
1
- import chalk from 'chalk';
2
- import { getConfig, saveConfig } from '../lib/config.js';
3
- import { COUNTRY_LANG } from '../lib/country-lang.js';
4
- import { getExamState, getRealExamState } from '../lib/exam-state.js';
5
- import { logCommand } from '../lib/logger.js';
6
- import { ensureLangCache } from '../lib/translations-fetcher.js';
7
- import { loadUiPack } from '../lib/i18n.js';
8
- import { printSuccess, printError, printInfo } from '../lib/ui.js';
9
- import { SUPPORTED_LANGUAGES } from '../types/index.js';
10
- const LANG_NAMES = {
11
- en: 'English',
12
- zh: '中文 (Chinese)',
13
- ja: '日本語 (Japanese)',
14
- ko: '한국어 (Korean)',
15
- es: 'Español (Spanish)',
16
- ar: 'العربية (Arabic)',
17
- fr: 'Français (French)',
18
- pt: 'Português (Portuguese)',
19
- ru: 'Русский (Russian)',
20
- hi: 'हिन्दी (Hindi)',
21
- de: 'Deutsch (German)',
22
- id: 'Bahasa (Indonesian)',
23
- th: 'ไทย (Thai)',
24
- vi: 'Tiếng Việt (Vietnamese)',
25
- tr: 'Türkçe (Turkish)',
26
- uk: 'Українська (Ukrainian)',
27
- ht: 'Kreyòl (Haitian Creole)',
28
- sw: 'Kiswahili (Swahili)',
29
- uz: 'Oʻzbek (Uzbek)',
30
- lo: 'ລາວ (Lao)',
31
- };
32
- export function registerLangCommand(program) {
33
- program
34
- .command('lang [code]')
35
- .description('Switch display language')
36
- .action(async (code) => {
37
- logCommand(`lang ${code || ''}`);
38
- if (!code) {
39
- const config = getConfig();
40
- printInfo(`Current language: ${chalk.white(LANG_NAMES[config.language] || config.language)}`);
41
- console.log();
42
- console.log(chalk.gray(' Supported languages:'));
43
- for (const lang of SUPPORTED_LANGUAGES) {
44
- const current = config.language === lang ? chalk.yellow(' ← current') : '';
45
- console.log(` ${chalk.white(lang)} ${LANG_NAMES[lang]}${current}`);
46
- }
47
- console.log();
48
- console.log(chalk.gray(' Switch now: ') +
49
- chalk.cyan('lang <code>') +
50
- chalk.gray(' (e.g. ') +
51
- chalk.cyan('lang es') +
52
- chalk.gray(')'));
53
- console.log(chalk.gray(' No "back" needed — you are still at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
54
- console.log();
55
- return;
56
- }
57
- if (!SUPPORTED_LANGUAGES.includes(code)) {
58
- printError(`Unsupported language: ${code}`);
59
- const supported = SUPPORTED_LANGUAGES.map((l) => `${l} (${LANG_NAMES[l]?.split(' ')[0] || l})`).join(', ');
60
- printInfo(`Supported: ${supported}`);
61
- return;
62
- }
63
- // Country lock: during an active real exam, only allow English or the
64
- // language auto-detected from the token's country prefix. Demo exams
65
- // are not affected (getRealExamState returns null for demos).
66
- const realExam = getRealExamState();
67
- const examToken = realExam?.session?.token;
68
- if (examToken) {
69
- const prefix = examToken.substring(0, 2).toUpperCase();
70
- const expectedLang = COUNTRY_LANG[prefix];
71
- if (expectedLang && code !== expectedLang && code !== 'en') {
72
- printError(`Language is locked during your exam.`);
73
- printInfo(`Your token (${prefix}xxx) supports two languages: ` +
74
- `${chalk.cyan('en')} (English) or ${chalk.cyan(expectedLang)} (${LANG_NAMES[expectedLang] || expectedLang}).`);
75
- printInfo(`To change language, finish or quit the current exam first.`);
76
- return;
77
- }
78
- }
79
- saveConfig({ language: code });
80
- printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
81
- // Fetch the translation pack on first switch to this language (no-op
82
- // when already cached or when code === 'en'). Non-fatal if it fails;
83
- // per-question lookups will fall back to the on-the-fly translator.
84
- await ensureLangCache(code);
85
- // Apply the freshly-fetched UI pack immediately (the pack's ui.json now
86
- // drives interface strings via t(); see src/lib/i18n.ts lazy-load).
87
- loadUiPack(code);
88
- // If demo in progress, re-translate each drawn question in place using
89
- // sourceNumber + sourceOrder so the user's answers and option positions are
90
- // preserved. If an older state lacks these fields (pre-v2.19.22), fall back
91
- // to restart-with-fresh-pick so nothing crashes.
92
- const state = getExamState();
93
- if (state && state.session.examId === 'demo-free') {
94
- try {
95
- const { pickDemoQuestions, getLocalizedDemoSession, getLocalizedDemoQuestions, getLocalizedExplanations, DEMO_PICK_SIZE, } = await import('../lib/demo-exam.js');
96
- const { saveExamState } = await import('../lib/exam-state.js');
97
- const canRetranslate = state.questions.every((q) => q.sourceNumber != null && Array.isArray(q.sourceOrder) && q.sourceOrder.length === 4);
98
- if (canRetranslate) {
99
- const pool = getLocalizedDemoQuestions();
100
- const explanations = getLocalizedExplanations();
101
- state.questions = state.questions.map((q) => {
102
- const src = pool.find((p) => p.number === q.sourceNumber);
103
- if (!src || !q.sourceOrder)
104
- return q;
105
- return {
106
- ...q,
107
- text: src.text,
108
- category: src.category,
109
- options: {
110
- A: src.options[q.sourceOrder[0]],
111
- B: src.options[q.sourceOrder[1]],
112
- C: src.options[q.sourceOrder[2]],
113
- D: src.options[q.sourceOrder[3]],
114
- },
115
- explanation: explanations[q.sourceNumber],
116
- };
117
- });
118
- state.session.examName = getLocalizedDemoSession().examName;
119
- saveExamState(state);
120
- const currentQ = state._lastQ || 1;
121
- console.log();
122
- console.log(chalk.green(` Demo continues in ${LANG_NAMES[code] || code}. Your progress is kept.`));
123
- console.log(chalk.white(` Resume: exam q ${currentQ}`));
124
- }
125
- else {
126
- // Legacy state from before v2.19.22 — safely reset
127
- state.questions = pickDemoQuestions(DEMO_PICK_SIZE);
128
- state.answers = {};
129
- state.session.examName = getLocalizedDemoSession().examName;
130
- state.session.startedAt = new Date().toISOString();
131
- state._lastQ = 1;
132
- saveExamState(state);
133
- console.log();
134
- console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
135
- console.log(chalk.white(' Type: exam q 1'));
136
- }
137
- }
138
- catch {
139
- console.log(chalk.gray(' Language changed. Type: demo'));
140
- }
141
- }
142
- else if (state && state.session.token) {
143
- // Real exam with token: re-fetch questions in new language from server
144
- try {
145
- const { getConfig } = await import('../lib/config.js');
146
- const { saveExamState } = await import('../lib/exam-state.js');
147
- const { getDeviceFingerprint } = await import('../lib/access.js');
148
- const config = getConfig();
149
- const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
150
- const token = state.session.token;
151
- const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
152
- method: 'POST',
153
- headers: { 'Content-Type': 'application/json' },
154
- body: JSON.stringify({ token, deviceHash: getDeviceFingerprint(), lang: code }),
155
- signal: AbortSignal.timeout(10000),
156
- });
157
- if (res.ok) {
158
- const json = (await res.json());
159
- const newQuestions = json.data.questions;
160
- // Keep answers, interactions, aiUsage — only update question text
161
- state.questions = newQuestions;
162
- saveExamState(state);
163
- const currentQ = state._lastQ || 1;
164
- console.log();
165
- printSuccess(`Exam questions updated to ${LANG_NAMES[code] || code}. Your answers are kept.`);
166
- console.log(chalk.white(` Resume: exam q ${currentQ}`));
167
- }
168
- else if (res.status === 429) {
169
- // V8 anti-bruteforce cooldown — token is alive, just throttled.
170
- // Do NOT clear local state, or contestants lose their real session
171
- // because the server briefly rate-limited a language switch.
172
- const errBody = (await res.json().catch(() => null));
173
- const m = errBody?.message?.match(/(\d+)\s*s/i);
174
- const waitS = m ? parseInt(m[1], 10) : 60;
175
- console.log();
176
- printInfo(`Server rate-limited that token (wait ~${waitS}s).`);
177
- printInfo(`Your exam session is intact — try ${chalk.cyan(`lang ${code}`)} again after the cooldown.`);
178
- }
179
- else {
180
- // Other non-200: token revoked / submitted / window closed. Auto-
181
- // clear the dead local session so contestants never need to learn
182
- // the hidden `exam reset` command — they just retype their token.
183
- const { clearExamState } = await import('../lib/exam-state.js');
184
- clearExamState();
185
- console.log();
186
- console.log(chalk.green(' ✓ Old session record cleared — your scores are safe.'));
187
- console.log(chalk.gray(' To start (or resume) your exam, type your token:'));
188
- console.log(chalk.gray(' → ') + chalk.bold.cyan('exam <your-token>'));
189
- }
190
- }
191
- catch {
192
- console.log(chalk.yellow(' Could not reach server. Language changed for UI only.'));
193
- }
194
- }
195
- else if (state) {
196
- const currentQ = state._lastQ || 1;
197
- console.log();
198
- console.log(chalk.gray(` Exam in progress — resuming Q${currentQ}:`));
199
- console.log(chalk.white(` Type: exam q ${currentQ}`));
200
- }
201
- });
202
- }
1
+ import chalk from"chalk";import{getConfig as e,saveConfig as o}from"../lib/config.js";import{COUNTRY_LANG as s}from"../lib/country-lang.js";import{getExamState as a,getRealExamState as t}from"../lib/exam-state.js";import{logCommand as n}from"../lib/logger.js";import{ensureLangCache as r}from"../lib/translations-fetcher.js";import{loadUiPack as i}from"../lib/i18n.js";import{printSuccess as l,printError as c,printInfo as g}from"../lib/ui.js";import{SUPPORTED_LANGUAGES as u}from"../types/index.js";const m={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)",uz:"Oʻzbek (Uzbek)",lo:"ລາວ (Lao)"};export function registerLangCommand(p){p.command("lang [code]").description("Switch display language").action(async p=>{if(n(`lang ${p||""}`),!p){const o=e();g(`Current language: ${chalk.white(m[o.language]||o.language)}`),console.log(),console.log(chalk.gray(" Supported languages:"));for(const e of u){const s=o.language===e?chalk.yellow(" ← current"):"";console.log(` ${chalk.white(e)} ${m[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(!u.includes(p)){c(`Unsupported language: ${p}`);const e=u.map(e=>`${e} (${m[e]?.split(" ")[0]||e})`).join(", ");return void g(`Supported: ${e}`)}const d=t(),y=d?.session?.token;if(y){const e=y.substring(0,2).toUpperCase(),o=s[e];if(o&&p!==o&&"en"!==p)return c("Language is locked during your exam."),g(`Your token (${e}xxx) supports two languages: ${chalk.cyan("en")} (English) or ${chalk.cyan(o)} (${m[o]||o}).`),void g("To change language, finish or quit the current exam first.")}o({language:p}),l(`Language set to: ${m[p]||p}`),await r(p),i(p);const h=a();if(h&&"demo-free"===h.session.examId)try{const{pickDemoQuestions:e,getLocalizedDemoSession:o,getLocalizedDemoQuestions:s,getLocalizedExplanations:a,DEMO_PICK_SIZE:t}=await import("../lib/demo-exam.js"),{saveExamState:n}=await import("../lib/exam-state.js");if(h.questions.every(e=>null!=e.sourceNumber&&Array.isArray(e.sourceOrder)&&4===e.sourceOrder.length)){const e=s(),t=a();h.questions=h.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:t[o.sourceNumber]}:o}),h.session.examName=o().examName,n(h);const r=h._lastQ||1;console.log(),console.log(chalk.green(` Demo continues in ${m[p]||p}. Your progress is kept.`)),console.log(chalk.white(` Resume: exam q ${r}`))}else h.questions=e(t),h.answers={},h.session.examName=o().examName,h.session.startedAt=(new Date).toISOString(),h._lastQ=1,n(h),console.log(),console.log(chalk.green(` Demo restarted in ${m[p]||p}.`)),console.log(chalk.white(" Type: exam q 1"))}catch{console.log(chalk.gray(" Language changed. Type: demo"))}else if(h&&h.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=h.session.token,n=await fetch(`${a}/api/icoa/exam-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t,deviceHash:s(),lang:p}),signal:AbortSignal.timeout(1e4)});if(n.ok){const e=(await n.json()).data.questions;h.questions=e,o(h);const s=h._lastQ||1;console.log(),l(`Exam questions updated to ${m[p]||p}. Your answers are kept.`),console.log(chalk.white(` Resume: exam q ${s}`))}else if(429===n.status){const e=await n.json().catch(()=>null),o=e?.message?.match(/(\d+)\s*s/i),s=o?parseInt(o[1],10):60;console.log(),g(`Server rate-limited that token (wait ~${s}s).`),g(`Your exam session is intact — try ${chalk.cyan(`lang ${p}`)} again after the cooldown.`)}else{const{clearExamState:e}=await import("../lib/exam-state.js");e(),console.log(),console.log(chalk.green(" ✓ Old session record cleared — your scores are safe.")),console.log(chalk.gray(" To start (or resume) your exam, type your token:")),console.log(chalk.gray(" → ")+chalk.bold.cyan("exam <your-token>"))}}catch{console.log(chalk.yellow(" Could not reach server. Language changed for UI only."))}else if(h){const e=h._lastQ||1;console.log(),console.log(chalk.gray(` Exam in progress — resuming Q${e}:`)),console.log(chalk.white(` Type: exam q ${e}`))}})}
@@ -1,171 +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 [chalk.gray(time), colorFn(entry.level.padEnd(7)), input];
48
- });
49
- printTable(['Time', 'Type', 'Content'], rows);
50
- console.log(chalk.gray(` ${entries.length} entries total`));
51
- console.log();
52
- console.log(chalk.gray(' You are at the ') +
53
- chalk.cyan('icoa>') +
54
- chalk.gray(' prompt. Also: ') +
55
- chalk.cyan('log stats') +
56
- chalk.gray(' · ') +
57
- chalk.cyan('log export') +
58
- chalk.gray(' · ') +
59
- chalk.cyan('help') +
60
- chalk.gray(' all commands.'));
61
- console.log();
62
- }
63
- async function exportLog() {
64
- const config = getConfig();
65
- const icoaDir = getIcoaDir();
66
- const _logFile = join(icoaDir, 'session.log');
67
- const sessionFile = join(icoaDir, 'session-state.json');
68
- const _configFile = join(icoaDir, 'config.json');
69
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
70
- const userName = config.userName || 'unknown';
71
- const exportName = `icoa-audit-${userName}-${timestamp}.json`;
72
- const exportPath = join(process.cwd(), exportName);
73
- // Gather all audit data
74
- const audit = {
75
- exportedAt: new Date().toISOString(),
76
- version: '1.7.2',
77
- competitor: {
78
- userName: config.userName,
79
- userId: config.userId,
80
- teamName: config.teamName,
81
- teamId: config.teamId,
82
- sessionId: config.sessionId,
83
- },
84
- connection: {
85
- ctfdUrl: config.ctfdUrl,
86
- },
87
- session: existsSync(sessionFile) ? JSON.parse(readFileSync(sessionFile, 'utf-8')) : null,
88
- commands: getSessionLog(),
89
- };
90
- // Count by type
91
- const entries = getSessionLog();
92
- const counts = {};
93
- for (const e of entries) {
94
- counts[e.level] = (counts[e.level] || 0) + 1;
95
- }
96
- audit.summary = {
97
- totalCommands: entries.length,
98
- byType: counts,
99
- firstEntry: entries[0]?.timestamp || null,
100
- lastEntry: entries[entries.length - 1]?.timestamp || null,
101
- };
102
- // Write export
103
- const { writeFileSync } = await import('node:fs');
104
- writeFileSync(exportPath, JSON.stringify(audit, null, 2));
105
- console.log();
106
- console.log(chalk.green(` ✓ Audit log exported`));
107
- console.log(chalk.white(` ${exportPath}`));
108
- console.log();
109
- console.log(chalk.gray(' Contents:'));
110
- console.log(chalk.gray(` Commands: ${entries.length}`));
111
- Object.entries(counts).forEach(([type, count]) => {
112
- console.log(chalk.gray(` ${type}: ${count}`));
113
- });
114
- console.log();
115
- console.log(chalk.gray(' This file contains the complete session audit trail.'));
116
- console.log(chalk.gray(' Submit to organizers for post-competition verification.'));
117
- console.log();
118
- }
119
- function showStats() {
120
- const entries = getSessionLog();
121
- const icoaDir = getIcoaDir();
122
- const sessionFile = join(icoaDir, 'session-state.json');
123
- console.log();
124
- console.log(chalk.bold.white(' Session Statistics'));
125
- console.log(chalk.gray(' ─────────────────────────────────────────────'));
126
- if (entries.length === 0) {
127
- console.log(chalk.gray(' No activity recorded yet.'));
128
- console.log();
129
- return;
130
- }
131
- // Time range
132
- const first = new Date(entries[0].timestamp);
133
- const last = new Date(entries[entries.length - 1].timestamp);
134
- const durationMin = Math.round((last.getTime() - first.getTime()) / 60000);
135
- console.log(chalk.gray(' First activity: ') + chalk.white(first.toLocaleString()));
136
- console.log(chalk.gray(' Last activity: ') + chalk.white(last.toLocaleString()));
137
- console.log(chalk.gray(' Duration: ') + chalk.white(`${durationMin} min`));
138
- console.log();
139
- // Count by type
140
- const counts = {};
141
- for (const e of entries) {
142
- counts[e.level] = (counts[e.level] || 0) + 1;
143
- }
144
- console.log(chalk.gray(' Total commands: ') + chalk.white(String(entries.length)));
145
- if (counts.command)
146
- console.log(chalk.blue(' commands: ') + chalk.white(String(counts.command)));
147
- if (counts.A)
148
- console.log(chalk.green(' hint A: ') + chalk.white(String(counts.A)));
149
- if (counts.B)
150
- console.log(chalk.yellow(' hint B: ') + chalk.white(String(counts.B)));
151
- if (counts.C)
152
- console.log(chalk.red(' hint C: ') + chalk.white(String(counts.C)));
153
- if (counts.submit)
154
- console.log(chalk.magenta(' submissions: ') + chalk.white(String(counts.submit)));
155
- // Exit info
156
- if (existsSync(sessionFile)) {
157
- try {
158
- const session = JSON.parse(readFileSync(sessionFile, 'utf-8'));
159
- console.log();
160
- console.log(chalk.gray(' Exit count: ') + chalk.white(String(session.exitCount || 0)));
161
- if (session.totalAwaySeconds) {
162
- const awayMin = Math.round(session.totalAwaySeconds / 60);
163
- console.log(chalk.gray(' Total away: ') + chalk.white(`${awayMin} min`));
164
- }
165
- }
166
- catch {
167
- /* ignore */
168
- }
169
- }
170
- console.log();
171
- }
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,151 +1 @@
1
- import { execSync, spawnSync } from 'node:child_process';
2
- import chalk from 'chalk';
3
- import { printInfo, printSuccess } from '../lib/ui.js';
4
- import { logCommand } from '../lib/logger.js';
5
- import { getActiveCwd } from '../lib/exam-sandbox.js';
6
- import { isDockerRunning } from '../lib/docker-probe.js';
7
- /**
8
- * Decide which shell to launch. The Docker sandbox is the PREFERRED path (extra
9
- * isolation), but it is OPTIONAL: the 110 CTF tools are installed on the HOST by
10
- * `env setup`, and the `env` doctor explicitly marks docker "optional — not
11
- * needed". So when Docker is absent we must NOT dead-end the user with an
12
- * "install Docker" wall — we drop them into a host shell where every tool they
13
- * need already lives. Pure + testable so the no-docker → host fallback is locked
14
- * by a regression test.
15
- */
16
- export function resolveShellLaunch(opts) {
17
- if (opts.dockerAvailable) {
18
- return { mode: 'docker', command: 'docker' };
19
- }
20
- const command = opts.platform === 'win32' ? opts.comspecEnv || 'cmd.exe' : opts.shellEnv || '/bin/bash';
21
- return { mode: 'host', command };
22
- }
23
- // The image ladder is shared with the REPL's persistent-container path — one
24
- // source of truth in lib/sandbox.ts; re-exported here for the unit tests.
25
- export { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
26
- import { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
27
- /**
28
- * docker-run args as an ARRAY (spawnSync — no shell, so a cwd with spaces
29
- * survives untouched). The two mounts are the aienv × docker interlock:
30
- * - cwd → /work: challenge files flow both ways instead of being invisible
31
- * from inside the container (and work done in /work outlives --rm).
32
- * - icoa-aienv named volume → /root/.icoa: docker's copy-up seeds it from the
33
- * image-baked venv on first use, then in-container pip installs survive
34
- * --rm across sessions. Reset anytime: `docker volume rm icoa-aienv`.
35
- */
36
- export function buildSandboxRunArgs(opts) {
37
- return [
38
- 'run',
39
- '--rm',
40
- '-it',
41
- '--name',
42
- opts.containerName,
43
- '--network=host',
44
- '-v',
45
- `${opts.cwd}:/work`,
46
- '-w',
47
- '/work',
48
- '-v',
49
- 'icoa-aienv:/root/.icoa',
50
- opts.image,
51
- '/bin/bash',
52
- ];
53
- }
54
- // Probe the daemon socket, not the `docker` CLI — checking must never wake a
55
- // stopped Docker Desktop (the sandbox is optional; host shell is the fallback).
56
- // See lib/docker-probe.ts.
57
- function isDockerAvailable() {
58
- return isDockerRunning();
59
- }
60
- function openHostShell() {
61
- const cwd = getActiveCwd();
62
- const { command } = resolveShellLaunch({
63
- dockerAvailable: false,
64
- platform: process.platform,
65
- shellEnv: process.env.SHELL,
66
- comspecEnv: process.env.COMSPEC,
67
- });
68
- printInfo('Docker not found — opening a host shell.');
69
- console.log(chalk.gray(' All 110 CTF tools run on your host. If any are missing, run ') +
70
- chalk.cyan('env setup') +
71
- chalk.gray('.'));
72
- console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
73
- console.log();
74
- try {
75
- execSync(command, { stdio: 'inherit', cwd });
76
- console.log();
77
- printSuccess('Shell session ended.');
78
- }
79
- catch {
80
- // A non-zero exit (incl. the user typing `exit`) lands here — not an error.
81
- console.log();
82
- printInfo('Shell session ended.');
83
- }
84
- }
85
- export function registerShellCommand(program) {
86
- program
87
- .command('shell')
88
- .description('Open an interactive CTF shell (Docker sandbox if available, else host)')
89
- .action(async () => {
90
- logCommand('shell');
91
- // Docker is the preferred (isolated) path but is OPTIONAL — fall back to
92
- // the host shell instead of telling the user to install Docker.
93
- if (!(await isDockerAvailable())) {
94
- openHostShell();
95
- return;
96
- }
97
- const hasLocal = (image) => {
98
- try {
99
- execSync(`docker image inspect ${image}`, { stdio: 'ignore' });
100
- return true;
101
- }
102
- catch {
103
- return false;
104
- }
105
- };
106
- const pull = (image) => {
107
- printInfo(`Pulling ${image}...`);
108
- try {
109
- execSync(`docker pull ${image}`, { stdio: 'inherit' });
110
- return true;
111
- }
112
- catch {
113
- return false;
114
- }
115
- };
116
- const image = resolveSandboxImage(hasLocal, pull);
117
- if (image === null) {
118
- // No image local or pullable — don't dead-end, the host has the tools.
119
- printInfo('Could not pull a sandbox image — using your host shell instead.');
120
- openHostShell();
121
- return;
122
- }
123
- const cwd = getActiveCwd();
124
- printSuccess('Launching sandbox...');
125
- console.log(chalk.gray(' Your folder is mounted at ') +
126
- chalk.cyan('/work') +
127
- chalk.gray(' — files there persist on your host.'));
128
- if (image === SANDBOX_IMAGES[0]) {
129
- console.log(chalk.gray(' ML venv: ') +
130
- chalk.cyan('~/.icoa/aienv/bin/python') +
131
- chalk.gray(' (sklearn / jupyter / mujoco) — its pip installs persist across sessions.'));
132
- }
133
- else {
134
- console.log(chalk.gray(' This image has no ML venv — get the newer one with ') +
135
- chalk.cyan(`docker pull ${SANDBOX_IMAGES[0]}`));
136
- }
137
- console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
138
- console.log();
139
- const containerName = `icoa-sandbox-${Date.now()}`;
140
- const result = spawnSync('docker', buildSandboxRunArgs({ cwd, image, containerName }), {
141
- stdio: 'inherit',
142
- });
143
- console.log();
144
- if (result.error) {
145
- printInfo('Sandbox session ended.');
146
- }
147
- else {
148
- printSuccess('Sandbox session ended.');
149
- }
150
- });
151
- }
1
+ import{execSync as o,spawnSync as e}from"node:child_process";import chalk from"chalk";import{printInfo as n,printSuccess as r}from"../lib/ui.js";import{logCommand as s}from"../lib/logger.js";import{getActiveCwd as i}from"../lib/exam-sandbox.js";import{isDockerRunning as l}from"../lib/docker-probe.js";export function resolveShellLaunch(o){return o.dockerAvailable?{mode:"docker",command:"docker"}:{mode:"host",command:"win32"===o.platform?o.comspecEnv||"cmd.exe":o.shellEnv||"/bin/bash"}}export{SANDBOX_IMAGES,resolveSandboxImage}from"../lib/sandbox.js";import{SANDBOX_IMAGES as t,resolveSandboxImage as a}from"../lib/sandbox.js";export function buildSandboxRunArgs(o){return["run","--rm","-it","--name",o.containerName,"--network=host","-v",`${o.cwd}:/work`,"-w","/work","-v","icoa-aienv:/root/.icoa",o.image,"/bin/bash"]}function c(){const e=i(),{command:s}=resolveShellLaunch({dockerAvailable:!1,platform:process.platform,shellEnv:process.env.SHELL,comspecEnv:process.env.COMSPEC});n("Docker not found — opening a host shell."),console.log(chalk.gray(" All 110 CTF tools run on your host. If any are missing, run ")+chalk.cyan("env setup")+chalk.gray(".")),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();try{o(s,{stdio:"inherit",cwd:e}),console.log(),r("Shell session ended.")}catch{console.log(),n("Shell session ended.")}}export function registerShellCommand(d){d.command("shell").description("Open an interactive CTF shell (Docker sandbox if available, else host)").action(async()=>{if(s("shell"),!await l())return void c();const d=a(e=>{try{return o(`docker image inspect ${e}`,{stdio:"ignore"}),!0}catch{return!1}},e=>{n(`Pulling ${e}...`);try{return o(`docker pull ${e}`,{stdio:"inherit"}),!0}catch{return!1}});if(null===d)return n("Could not pull a sandbox image — using your host shell instead."),void c();const m=i();r("Launching sandbox..."),console.log(chalk.gray(" Your folder is mounted at ")+chalk.cyan("/work")+chalk.gray(" — files there persist on your host.")),d===t[0]?console.log(chalk.gray(" ML venv: ")+chalk.cyan("~/.icoa/aienv/bin/python")+chalk.gray(" (sklearn / jupyter / mujoco) — its pip installs persist across sessions.")):console.log(chalk.gray(" This image has no ML venv — get the newer one with ")+chalk.cyan(`docker pull ${t[0]}`)),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();const u=`icoa-sandbox-${Date.now()}`,g=e("docker",buildSandboxRunArgs({cwd:m,image:d,containerName:u}),{stdio:"inherit"});console.log(),g.error?n("Sandbox session ended."):r("Sandbox session ended.")})}