icoa-cli 2.19.348 → 2.19.349

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 (48) hide show
  1. package/dist/commands/ai4ctf.js +1 -1
  2. package/dist/commands/arena-eai.js +1 -1
  3. package/dist/commands/connect.js +1 -1
  4. package/dist/commands/ctf.js +1 -1
  5. package/dist/commands/ctf4ai-demo.js +1 -1
  6. package/dist/commands/ctf4vla.js +1 -1
  7. package/dist/commands/exam.js +1 -1
  8. package/dist/index.js +355 -1
  9. package/dist/lib/access.js +184 -1
  10. package/dist/lib/arena-submit.js +21 -1
  11. package/dist/lib/budget.js +6 -1
  12. package/dist/lib/challenge-dir.js +16 -1
  13. package/dist/lib/comms.js +212 -1
  14. package/dist/lib/config.js +93 -1
  15. package/dist/lib/country-lang.js +39 -1
  16. package/dist/lib/demo-exam.js +478 -1
  17. package/dist/lib/demo-flags.js +27 -1
  18. package/dist/lib/demo-stats.js +62 -1
  19. package/dist/lib/demo2-progress.js +102 -1
  20. package/dist/lib/exam-client.js +54 -1
  21. package/dist/lib/exam-state.js +273 -1
  22. package/dist/lib/gemini.js +247 -1
  23. package/dist/lib/integrity-snapshot.js +88 -1
  24. package/dist/lib/interactive-spawn.js +55 -1
  25. package/dist/lib/ipynb-input.js +65 -1
  26. package/dist/lib/kernel-protocol.js +88 -1
  27. package/dist/lib/kernel.js +146 -2
  28. package/dist/lib/learn-curricula.js +309 -1
  29. package/dist/lib/learn-i18n.js +184 -1
  30. package/dist/lib/log-sync.js +155 -1
  31. package/dist/lib/logger.js +49 -1
  32. package/dist/lib/open-file.js +55 -1
  33. package/dist/lib/paper-upgrade.js +119 -1
  34. package/dist/lib/render-card.js +112 -1
  35. package/dist/lib/repl-asker.js +67 -1
  36. package/dist/lib/sample-runner.js +227 -1
  37. package/dist/lib/shell-split.js +69 -1
  38. package/dist/lib/sim-cooldown.js +75 -1
  39. package/dist/lib/theme.js +119 -1
  40. package/dist/lib/token-format.js +74 -1
  41. package/dist/lib/toolset-hash.js +48 -1
  42. package/dist/lib/translations-fetcher.js +95 -1
  43. package/dist/lib/ui.js +99 -1
  44. package/dist/lib/version.js +24 -1
  45. package/dist/postinstall.js +48 -1
  46. package/dist/repl.js +2251 -1
  47. package/dist/types/index.js +63 -1
  48. package/package.json +1 -1
package/dist/lib/comms.js CHANGED
@@ -1 +1,212 @@
1
- import chalk from"chalk";import{getConfig as t}from"./config.js";const e=/\x1b\[[0-9;?]*[ -/]*[@-~]/g,o=/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;let n=!1;export function isCommsActive(){return n}export function setCommsActive(t){n=t}export function isLeaderToken(t){return/^LDR[A-Z0-9]{10,}$/i.test((t||"").trim())}export function isLeaderSession(){return Boolean(t().leaderToken)}export function isLeaderAccount(t){return(t||"").trim().toUpperCase().startsWith("TMLD")}export function sanitizeCommsLine(t){let n=(t??"").toString();return n=n.replace(e,""),n=n.replace(/[\r\n\t]/g," "),n=n.replace(o," "),n=n.replace(/\s+/g," ").trim(),n.slice(0,500)}export function commsBaseUrl(){return(t().ctfdUrl||"").replace(/\/+$/,"")}export function isCompetitionJoined(){const e=t();return Boolean((e.token||e.leaderToken)&&e.ctfdUrl&&e.userName)}export function isMelCompetition(){return isCompetitionJoined()&&function(t){try{return new URL(t).host}catch{return(t||"").replace(/^https?:\/\//,"").replace(/\/.*$/,"")}}(t().ctfdUrl).endsWith("au.icoa2026.au")}export function formatBroadcastBar(t){const e=sanitizeCommsLine(t);if(!e)return chalk.green(" ✓ No issues reported — enjoy the competition!");const o="─".repeat(49);return[chalk.yellow(` ${o}`),chalk.bold.yellow(" ⚠ BROADCAST ")+chalk.yellow(e),chalk.yellow(` ${o}`)].join("\n")}function r(){const e=t(),o={"Content-Type":"application/json"};return e.token&&(o.Authorization=`Token ${e.token}`),o}export async function fetchBroadcast(){try{const t=await fetch(`${commsBaseUrl()}/api/icoa/broadcast`,{headers:r(),signal:AbortSignal.timeout(5e3)});if(!t.ok)return{text:"",ts:null};const e=await t.json(),o=e?.data??{};return{text:sanitizeCommsLine(o.text||""),ts:o.ts??null}}catch{return{text:"",ts:null}}}async function a(e,o){try{const n=await fetch(`${commsBaseUrl()}/api/icoa/${e}`,{method:"POST",headers:r(),body:JSON.stringify({text:sanitizeCommsLine(o),account:t().userName||"",leader_token:t().leaderToken||void 0}),signal:AbortSignal.timeout(8e3)}),a=await n.json().catch(()=>({}));return n.ok&&a?.success?{ok:!0,id:a.data?.id,ts:a.data?.ts,status:n.status}:{ok:!1,status:n.status,message:a?.message}}catch(t){return{ok:!1,status:0,message:t?.message||"network error"}}}export function postMemo(t){return a("memo",t)}export function postJury(t){return a("jury",t)}export async function leaderAuth(t,e){try{const o=await fetch(`${t.replace(/\/+$/,"")}/api/icoa/leader-auth`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:(e||"").trim()}),signal:AbortSignal.timeout(8e3)}),n=await o.json().catch(()=>({}));return o.ok&&n?.success&&n.data?.account?{account:String(n.data.account),team:String(n.data.team||"")}:null}catch{return null}}export async function fetchPublicScoreboard(){try{const t=await fetch(`${commsBaseUrl()}/api/v1/scoreboard`,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(8e3)});if(!t.ok)return[];const e=await t.json().catch(()=>({}));return(Array.isArray(e?.data)?e.data:[]).map((t,e)=>({pos:t.pos??e+1,name:String(t.name??""),score:Number(t.score??0)}))}catch{return[]}}let s=null,c="",i=!1;export function showBroadcast(){console.log(),console.log(formatBroadcastBar(c))}export function startBroadcastPoll(t){if(s)return;const e=async()=>{const{text:e}=await fetchBroadcast(),o=e!==c;c=e,i&&o&&e&&(console.log(),console.log(formatBroadcastBar(e)),t()),i=!0};e(),s=setInterval(()=>{e()},45e3)}export function stopBroadcastPoll(){s&&clearInterval(s),s=null,c="",i=!1}
1
+ import chalk from 'chalk';
2
+ import { getConfig } from './config.js';
3
+ const MEL_HOST = 'au.icoa2026.au';
4
+ const MAX_TEXT_LEN = 500;
5
+ // eslint-disable-next-line no-control-regex
6
+ const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
7
+ // eslint-disable-next-line no-control-regex
8
+ const CTRL_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
9
+ // Comms sub-screen (memo/jury) active flag. The main REPL wraps rl.prompt() to
10
+ // force computePrompt() (→ `icoa 2026>`) on every prompt; chat/exam sub-modes
11
+ // bypass that wrap via their own active flags. Memo/jury need the same, or the
12
+ // wrapper clobbers their `icoa memo>` / `icoa jury>` prompt back to `icoa 2026>`.
13
+ let _commsActive = false;
14
+ export function isCommsActive() {
15
+ return _commsActive;
16
+ }
17
+ export function setCommsActive(active) {
18
+ _commsActive = active;
19
+ }
20
+ // A leader token (printed on the leader's card) — `LDR` + confusable-free body.
21
+ // Typed at the join Username prompt; the CLI authenticates it via token-api
22
+ // instead of doing a CTFd login, so the leader never gets a web session.
23
+ export function isLeaderToken(s) {
24
+ return /^LDR[A-Z0-9]{10,}$/i.test((s || '').trim());
25
+ }
26
+ // A leader SESSION = authenticated by a leader token (config.leaderToken set).
27
+ // Distinct from isLeaderAccount (name-prefix), which still gates legacy logins.
28
+ export function isLeaderSession() {
29
+ return Boolean(getConfig().leaderToken);
30
+ }
31
+ export function isLeaderAccount(name) {
32
+ // Team-leader accounts use the `TMLD` prefix (confirmed 2026-06-23). This is
33
+ // collision-free: `ZZRH` and `ZZCP` are BOTH contestants (ZZRH = reused AU-camp
34
+ // student logins) and must never be blocked from challenges. Only `TMLD` gets
35
+ // the minimal leader REPL + the hidden `jury` command; everyone else competes.
36
+ // (Replaces the earlier broken `ZZRH`=leader rule and the interim fail-open.)
37
+ return (name || '').trim().toUpperCase().startsWith('TMLD');
38
+ }
39
+ export function sanitizeCommsLine(s) {
40
+ let out = (s ?? '').toString();
41
+ out = out.replace(ANSI_RE, '');
42
+ out = out.replace(/[\r\n\t]/g, ' ');
43
+ // remap (not delete) remaining control chars to space, matching comms_store.py
44
+ out = out.replace(CTRL_RE, ' ');
45
+ out = out.replace(/\s+/g, ' ').trim();
46
+ return out.slice(0, MAX_TEXT_LEN);
47
+ }
48
+ function hostOf(url) {
49
+ try {
50
+ return new URL(url).host;
51
+ }
52
+ catch {
53
+ return (url || '').replace(/^https?:\/\//, '').replace(/\/.*$/, '');
54
+ }
55
+ }
56
+ export function commsBaseUrl() {
57
+ return (getConfig().ctfdUrl || '').replace(/\/+$/, '');
58
+ }
59
+ export function isCompetitionJoined() {
60
+ const c = getConfig();
61
+ // Contestants join with a CTFd token; leaders join with a leader token (no
62
+ // CTFd login). Either counts as "in the competition" for comms gating.
63
+ return Boolean((c.token || c.leaderToken) && c.ctfdUrl && c.userName);
64
+ }
65
+ export function isMelCompetition() {
66
+ return isCompetitionJoined() && hostOf(getConfig().ctfdUrl).endsWith(MEL_HOST);
67
+ }
68
+ export function formatBroadcastBar(text) {
69
+ const clean = sanitizeCommsLine(text);
70
+ if (!clean) {
71
+ return chalk.green(' ✓ No issues reported — enjoy the competition!');
72
+ }
73
+ const rule = '─'.repeat(49);
74
+ return [
75
+ chalk.yellow(` ${rule}`),
76
+ chalk.bold.yellow(' ⚠ BROADCAST ') + chalk.yellow(clean),
77
+ chalk.yellow(` ${rule}`),
78
+ ].join('\n');
79
+ }
80
+ // ─── network ───
81
+ function authHeaders() {
82
+ const c = getConfig();
83
+ const h = { 'Content-Type': 'application/json' };
84
+ if (c.token)
85
+ h.Authorization = `Token ${c.token}`;
86
+ return h;
87
+ }
88
+ export async function fetchBroadcast() {
89
+ try {
90
+ const res = await fetch(`${commsBaseUrl()}/api/icoa/broadcast`, {
91
+ headers: authHeaders(),
92
+ signal: AbortSignal.timeout(5000),
93
+ });
94
+ if (!res.ok)
95
+ return { text: '', ts: null };
96
+ const json = await res.json();
97
+ const d = json?.data ?? {};
98
+ return { text: sanitizeCommsLine(d.text || ''), ts: d.ts ?? null };
99
+ }
100
+ catch {
101
+ return { text: '', ts: null };
102
+ }
103
+ }
104
+ async function postComms(kind, text) {
105
+ try {
106
+ const res = await fetch(`${commsBaseUrl()}/api/icoa/${kind}`, {
107
+ method: 'POST',
108
+ headers: authHeaders(),
109
+ // account = self-reported fallback for session-mode logins (no API token).
110
+ // leader_token = un-forgeable leader identity (server validates it) so a
111
+ // leader with no CTFd login can still file jury appeals.
112
+ // Server prefers token-verified > leader-token > self-reported account.
113
+ body: JSON.stringify({
114
+ text: sanitizeCommsLine(text),
115
+ account: getConfig().userName || '',
116
+ leader_token: getConfig().leaderToken || undefined,
117
+ }),
118
+ signal: AbortSignal.timeout(8000),
119
+ });
120
+ const json = await res.json().catch(() => ({}));
121
+ if (res.ok && json?.success) {
122
+ return { ok: true, id: json.data?.id, ts: json.data?.ts, status: res.status };
123
+ }
124
+ return { ok: false, status: res.status, message: json?.message };
125
+ }
126
+ catch (e) {
127
+ return { ok: false, status: 0, message: e?.message || 'network error' };
128
+ }
129
+ }
130
+ export function postMemo(text) {
131
+ return postComms('memo', text);
132
+ }
133
+ export function postJury(text) {
134
+ return postComms('jury', text);
135
+ }
136
+ // Authenticate a leader token against token-api. No CTFd login happens.
137
+ // Returns the resolved account + team on success, null otherwise.
138
+ export async function leaderAuth(baseUrl, token) {
139
+ try {
140
+ const res = await fetch(`${baseUrl.replace(/\/+$/, '')}/api/icoa/leader-auth`, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({ token: (token || '').trim() }),
144
+ signal: AbortSignal.timeout(8000),
145
+ });
146
+ const json = await res.json().catch(() => ({}));
147
+ if (res.ok && json?.success && json.data?.account) {
148
+ return { account: String(json.data.account), team: String(json.data.team || '') };
149
+ }
150
+ return null;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ // Read the PUBLIC CTFd scoreboard (score_visibility=public → no auth needed).
157
+ // Leaders use this since they have no CTFd session. Returns [] on any failure.
158
+ export async function fetchPublicScoreboard() {
159
+ try {
160
+ const res = await fetch(`${commsBaseUrl()}/api/v1/scoreboard`, {
161
+ headers: { Accept: 'application/json' },
162
+ signal: AbortSignal.timeout(8000),
163
+ });
164
+ if (!res.ok)
165
+ return [];
166
+ const json = await res.json().catch(() => ({}));
167
+ const data = Array.isArray(json?.data) ? json.data : [];
168
+ return data.map((e, i) => ({
169
+ pos: e.pos ?? i + 1,
170
+ name: String(e.name ?? ''),
171
+ score: Number(e.score ?? 0),
172
+ }));
173
+ }
174
+ catch {
175
+ return [];
176
+ }
177
+ }
178
+ // ─── background poll + render ───
179
+ let pollTimer = null;
180
+ let cachedText = '';
181
+ let primed = false;
182
+ export function showBroadcast() {
183
+ console.log();
184
+ console.log(formatBroadcastBar(cachedText));
185
+ }
186
+ export function startBroadcastPoll(redraw) {
187
+ if (pollTimer)
188
+ return;
189
+ const tick = async () => {
190
+ const { text } = await fetchBroadcast();
191
+ const changed = text !== cachedText;
192
+ cachedText = text;
193
+ // Print a fresh alert when the text changes after the first prime — never
194
+ // on the very first fetch (that would clobber the join screen), and only a
195
+ // non-empty change is loud (clearing is silent).
196
+ if (primed && changed && text) {
197
+ console.log();
198
+ console.log(formatBroadcastBar(text));
199
+ redraw();
200
+ }
201
+ primed = true;
202
+ };
203
+ void tick();
204
+ pollTimer = setInterval(() => void tick(), 45_000);
205
+ }
206
+ export function stopBroadcastPoll() {
207
+ if (pollTimer)
208
+ clearInterval(pollTimer);
209
+ pollTimer = null;
210
+ cachedText = '';
211
+ primed = false;
212
+ }
@@ -1 +1,93 @@
1
- import{mkdirSync as t,readFileSync as n,writeFileSync as o,existsSync as r,chmodSync as e,statSync as i}from"node:fs";import{join as c}from"node:path";import{homedir as f}from"node:os";import{randomUUID as s}from"node:crypto";import{DEFAULT_CONFIG as u,DEFAULT_BUDGET as g}from"../types/index.js";const p=c(f(),".icoa"),d=c(p,"config.json"),m=c(p,"budget.json");function a(t,n){o(t,n);try{e(t,384)}catch{}}function y(t){try{63&i(t).mode&&e(t,384)}catch{}}function l(){r(p)||t(p,{recursive:!0,mode:448})}export function getConfig(){if(l(),!r(d)){const t={...u,sessionId:s()};return a(d,JSON.stringify(t,null,2)),t}y(d);try{const t=n(d,"utf-8");return{...u,...JSON.parse(t)}}catch{return{...u,sessionId:s()}}}export function saveConfig(t){l();const n={...getConfig(),...t};a(d,JSON.stringify(n,null,2))}export function getBudget(){if(l(),!r(m))return a(m,JSON.stringify(g,null,2)),{...g};y(m);try{const t=n(m,"utf-8");return{...g,...JSON.parse(t)}}catch{return{...g}}}export function saveBudget(t){l(),a(m,JSON.stringify(t,null,2))}export function getIcoaDir(){return l(),p}export function isConnected(){const t=getConfig();return!(!t.ctfdUrl||!t.token)}
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { DEFAULT_CONFIG, DEFAULT_BUDGET } from '../types/index.js';
6
+ const ICOA_DIR = join(homedir(), '.icoa');
7
+ const CONFIG_FILE = join(ICOA_DIR, 'config.json');
8
+ const BUDGET_FILE = join(ICOA_DIR, 'budget.json');
9
+ // V16 fix (v2.19.181): config.json holds the exam token, geminiApiKey, and
10
+ // accessToken in plaintext. On shared/multi-user hosts (school labs, family
11
+ // machines) a world-readable 0o644 leaks them to anyone with a shell. We
12
+ // force 0o600 (owner-only read/write) at every write, and proactively re-
13
+ // chmod on read for files created before this fix.
14
+ function writePrivate(path, contents) {
15
+ // Write first so we can chmod afterwards. The Node fs.writeFile mode
16
+ // option only applies on file CREATION, not existing files — chmodSync
17
+ // is the reliable cross-platform path. Best-effort on Windows where
18
+ // POSIX modes don't apply.
19
+ writeFileSync(path, contents);
20
+ try {
21
+ chmodSync(path, 0o600);
22
+ }
23
+ catch {
24
+ /* ignore — Windows/restricted FS */
25
+ }
26
+ }
27
+ function ensurePrivateMode(path) {
28
+ // For files written by older versions at 0o644: tighten on first read.
29
+ try {
30
+ const st = statSync(path);
31
+ // Mode low bits: group + other. If any non-zero, tighten.
32
+ if ((st.mode & 0o077) !== 0) {
33
+ chmodSync(path, 0o600);
34
+ }
35
+ }
36
+ catch {
37
+ /* ignore */
38
+ }
39
+ }
40
+ function ensureDir() {
41
+ if (!existsSync(ICOA_DIR)) {
42
+ mkdirSync(ICOA_DIR, { recursive: true, mode: 0o700 });
43
+ }
44
+ }
45
+ export function getConfig() {
46
+ ensureDir();
47
+ if (!existsSync(CONFIG_FILE)) {
48
+ const config = { ...DEFAULT_CONFIG, sessionId: randomUUID() };
49
+ writePrivate(CONFIG_FILE, JSON.stringify(config, null, 2));
50
+ return config;
51
+ }
52
+ ensurePrivateMode(CONFIG_FILE);
53
+ try {
54
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
55
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
56
+ }
57
+ catch {
58
+ return { ...DEFAULT_CONFIG, sessionId: randomUUID() };
59
+ }
60
+ }
61
+ export function saveConfig(config) {
62
+ ensureDir();
63
+ const current = getConfig();
64
+ const merged = { ...current, ...config };
65
+ writePrivate(CONFIG_FILE, JSON.stringify(merged, null, 2));
66
+ }
67
+ export function getBudget() {
68
+ ensureDir();
69
+ if (!existsSync(BUDGET_FILE)) {
70
+ writePrivate(BUDGET_FILE, JSON.stringify(DEFAULT_BUDGET, null, 2));
71
+ return { ...DEFAULT_BUDGET };
72
+ }
73
+ ensurePrivateMode(BUDGET_FILE);
74
+ try {
75
+ const raw = readFileSync(BUDGET_FILE, 'utf-8');
76
+ return { ...DEFAULT_BUDGET, ...JSON.parse(raw) };
77
+ }
78
+ catch {
79
+ return { ...DEFAULT_BUDGET };
80
+ }
81
+ }
82
+ export function saveBudget(budget) {
83
+ ensureDir();
84
+ writePrivate(BUDGET_FILE, JSON.stringify(budget, null, 2));
85
+ }
86
+ export function getIcoaDir() {
87
+ ensureDir();
88
+ return ICOA_DIR;
89
+ }
90
+ export function isConnected() {
91
+ const config = getConfig();
92
+ return !!(config.ctfdUrl && config.token);
93
+ }
@@ -1 +1,39 @@
1
- export const COUNTRY_LANG={UA:"uk",PE:"es",CN:"zh",AU:"en",JP:"ja",KR:"ko",BR:"pt",SA:"ar",FR:"fr",DE:"de",IN:"hi",ID:"id",TH:"th",VN:"vi",TR:"tr",RU:"ru",EG:"ar",HT:"ht",PH:"en",MY:"en",MM:"en",SG:"en",ZA:"en",KE:"sw",TZ:"sw",MO:"zh",UZ:"uz",GH:"en",LA:"lo",BD:"bn",BI:"fr",US:"en",UK:"en",NZ:"en",LK:"si",BW:"en",VE:"es"};
1
+ export const COUNTRY_LANG = {
2
+ UA: 'uk',
3
+ PE: 'es',
4
+ CN: 'zh',
5
+ AU: 'en',
6
+ JP: 'ja',
7
+ KR: 'ko',
8
+ BR: 'pt',
9
+ SA: 'ar',
10
+ FR: 'fr',
11
+ DE: 'de',
12
+ IN: 'hi',
13
+ ID: 'id',
14
+ TH: 'th',
15
+ VN: 'vi',
16
+ TR: 'tr',
17
+ RU: 'ru',
18
+ EG: 'ar',
19
+ HT: 'ht',
20
+ PH: 'en',
21
+ MY: 'en',
22
+ MM: 'en',
23
+ SG: 'en',
24
+ ZA: 'en',
25
+ KE: 'sw',
26
+ TZ: 'sw',
27
+ MO: 'zh',
28
+ UZ: 'uz',
29
+ GH: 'en',
30
+ LA: 'lo',
31
+ BD: 'bn',
32
+ BI: 'fr',
33
+ US: 'en',
34
+ UK: 'en',
35
+ NZ: 'en',
36
+ LK: 'si',
37
+ BW: 'en',
38
+ VE: 'es',
39
+ };