icoa-cli 2.19.347 → 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.
- package/dist/commands/ai4ctf.js +1 -1
- package/dist/commands/aienv.js +1 -1
- package/dist/commands/arena-eai.js +1 -1
- package/dist/commands/connect.js +1 -1
- package/dist/commands/ctf.js +1 -1
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/exam.js +1 -1
- package/dist/index.js +355 -1
- package/dist/lib/access.js +184 -1
- package/dist/lib/aienv.d.ts +9 -0
- package/dist/lib/aienv.js +1 -1
- package/dist/lib/arena-submit.js +21 -1
- package/dist/lib/budget.js +6 -1
- package/dist/lib/challenge-dir.js +16 -1
- package/dist/lib/comms.js +212 -1
- package/dist/lib/config.js +93 -1
- package/dist/lib/country-lang.js +39 -1
- package/dist/lib/demo-exam.js +478 -1
- package/dist/lib/demo-flags.js +27 -1
- package/dist/lib/demo-stats.js +62 -1
- package/dist/lib/demo2-progress.js +102 -1
- package/dist/lib/exam-client.js +54 -1
- package/dist/lib/exam-state.js +273 -1
- package/dist/lib/gemini.js +247 -1
- package/dist/lib/integrity-snapshot.js +88 -1
- package/dist/lib/interactive-spawn.js +55 -1
- package/dist/lib/ipynb-input.js +65 -1
- package/dist/lib/kernel-protocol.js +88 -1
- package/dist/lib/kernel.js +146 -2
- package/dist/lib/learn-curricula.js +309 -1
- package/dist/lib/learn-i18n.js +184 -1
- package/dist/lib/log-sync.js +155 -1
- package/dist/lib/logger.js +49 -1
- package/dist/lib/open-file.js +55 -1
- package/dist/lib/paper-upgrade.js +119 -1
- package/dist/lib/render-card.js +112 -1
- package/dist/lib/repl-asker.js +67 -1
- package/dist/lib/sample-runner.js +227 -1
- package/dist/lib/shell-split.js +69 -1
- package/dist/lib/sim-cooldown.js +75 -1
- package/dist/lib/theme.js +119 -1
- package/dist/lib/token-format.js +74 -1
- package/dist/lib/toolset-hash.js +48 -1
- package/dist/lib/translations-fetcher.js +95 -1
- package/dist/lib/ui.js +99 -1
- package/dist/lib/version.js +24 -1
- package/dist/postinstall.js +48 -1
- package/dist/repl.js +2251 -1
- package/dist/types/index.js +63 -1
- package/package.json +1 -1
package/dist/lib/comms.js
CHANGED
|
@@ -1 +1,212 @@
|
|
|
1
|
-
import chalk from
|
|
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
|
+
}
|
package/dist/lib/config.js
CHANGED
|
@@ -1 +1,93 @@
|
|
|
1
|
-
import{mkdirSync
|
|
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
|
+
}
|
package/dist/lib/country-lang.js
CHANGED
|
@@ -1 +1,39 @@
|
|
|
1
|
-
export const COUNTRY_LANG={
|
|
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
|
+
};
|