icoa-cli 2.19.34 → 2.19.36

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.
@@ -17,6 +17,10 @@ function getChallengeContext() {
17
17
  let chatActive = false;
18
18
  let chatSession = null;
19
19
  let chatTokensUsed = 0;
20
+ // Set true when the user burns through DEMO_TOKEN_CAP without solving. The
21
+ // chat session stays alive so `!<shell>` and `submit <flag>` still work, but
22
+ // further AI messages are blocked. See the reveal path in handleChatMessage.
23
+ let tokensLocked = false;
20
24
  const DEMO_TOKEN_CAP = 5000;
21
25
  export function isChatActive() {
22
26
  return chatActive;
@@ -39,36 +43,45 @@ const DEMO_FLAG = 'icoa{w3lc0me_2_ai4ctf}';
39
43
  // already enough. Instead c explains the hint principle and reminds the
40
44
  // user how to run a shell command inside ai4ctf (the `!` prefix).
41
45
  // This mirrors real competition behavior where trivial challenges cap at hint b.
42
- const DEMO_HINTS = {
43
- a: [
44
- 'The string ends with "==" and uses only letters, digits, "+", "/", and "=".',
45
- 'That pattern is a fingerprint you have seen it before.',
46
- 'Where is "=" typically used as padding?',
47
- ],
48
- b: [
49
- 'This is Base64 encoded. Decode the string to find the flag.',
50
- ],
51
- c: [
52
- 'Hints never give you the answer directly — they only point you in the right direction.',
53
- 'For simple challenges like this one, hint c adds nothing extra:',
54
- 'hint b already tells you everything you need.',
55
- '',
56
- 'If you do not remember how to decode Base64, ask your AI teammate —',
57
- 'just type a question like: how do I decode Base64 on the command line?',
58
- '',
59
- 'To run a shell command inside ai4ctf, prefix it with "!". For example:',
60
- '',
61
- ' !echo aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ== | base64 -d',
62
- ],
63
- };
46
+ // Multi-line hint bodies live in i18n.ts (ai4ctfHintABody/BBody/CBody).
47
+ // Shown when the user hits the 5000-token demo cap without solving. Keeps
48
+ // the session alive (no chatActive=false) so they can still paste the shell
49
+ // command below and then `submit <flag>`.
50
+ function showTokenCapReveal() {
51
+ console.log();
52
+ console.log(chalk.yellow(' ─────────────────────────────────────────────'));
53
+ console.log(chalk.bold.yellow(` ${t('ai4ctfRevealTitle')}`));
54
+ console.log(chalk.yellow(' ─────────────────────────────────────────────'));
55
+ console.log();
56
+ for (const line of t('ai4ctfRevealBody').split('\n')) {
57
+ if (line === '')
58
+ console.log();
59
+ else
60
+ console.log(chalk.white(' ' + line));
61
+ }
62
+ console.log();
63
+ console.log(chalk.cyan(' !echo aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ== | base64 -d'));
64
+ console.log();
65
+ console.log(chalk.white(` ${t('ai4ctfRevealSeeFlag')}`));
66
+ console.log();
67
+ console.log(chalk.green(' icoa{w3lc0me_2_ai4ctf}'));
68
+ console.log();
69
+ console.log(chalk.white(` ${t('ai4ctfRevealThenSubmit')}`));
70
+ console.log();
71
+ console.log(chalk.cyan(' submit icoa{w3lc0me_2_ai4ctf}'));
72
+ console.log();
73
+ console.log(chalk.gray(` ${t('ai4ctfRevealLockNote')}`));
74
+ console.log();
75
+ }
64
76
  function showDemoHint(tier) {
65
77
  const title = tier === 'a' ? t('ai4ctfHintA') : tier === 'b' ? t('ai4ctfHintB') : t('ai4ctfHintC');
78
+ const body = tier === 'a' ? t('ai4ctfHintABody') : tier === 'b' ? t('ai4ctfHintBBody') : t('ai4ctfHintCBody');
66
79
  const tierLabel = `Hint ${tier.toUpperCase()}`;
67
80
  const color = tier === 'a' ? chalk.green : tier === 'b' ? chalk.yellow : chalk.red;
68
81
  console.log();
69
82
  console.log(color.bold(` ▸ ${tierLabel} `) + chalk.gray(title));
70
83
  console.log();
71
- for (const line of DEMO_HINTS[tier]) {
84
+ for (const line of body.split('\n')) {
72
85
  if (line === '')
73
86
  console.log();
74
87
  else
@@ -76,11 +89,11 @@ function showDemoHint(tier) {
76
89
  }
77
90
  console.log();
78
91
  if (tier === 'a') {
79
- console.log(chalk.gray(' Stuck? Try: ') + chalk.cyan('hint b'));
92
+ console.log(chalk.gray(` ${t('ai4ctfHintNextA')} `) + chalk.cyan('hint b'));
80
93
  console.log();
81
94
  }
82
95
  else if (tier === 'b') {
83
- console.log(chalk.gray(' Really stuck? Try: ') + chalk.cyan('hint c'));
96
+ console.log(chalk.gray(` ${t('ai4ctfHintNextB')} `) + chalk.cyan('hint c'));
84
97
  console.log();
85
98
  }
86
99
  // No trailing CTA after hint c — the content itself explains everything.
@@ -125,41 +138,53 @@ export async function handleChatMessage(input) {
125
138
  await sleep(1500);
126
139
  console.log();
127
140
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
128
- console.log(chalk.bold.white(' ✨ What you just learned'));
141
+ console.log(chalk.bold.white(` ${t('ai4ctfWrapTitle')}`));
129
142
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
130
143
  console.log();
131
- console.log(chalk.white(' 1. Need a command? ') + chalk.gray('Just ask your AI teammate in natural'));
132
- console.log(chalk.gray(' language "how do I decode Base64 on the command line?"'));
133
- console.log(chalk.gray(' You do not need to memorize every tool. The AI era is'));
134
- console.log(chalk.gray(' about ') + chalk.white('thinking') + chalk.gray(', not mechanical memorization.'));
144
+ const wrap1Head = t('ai4ctfWrapLine1Head');
145
+ const wrap1Body = t('ai4ctfWrapLine1Body');
146
+ console.log(chalk.white(` 1. ${wrap1Head} `) + chalk.gray(wrap1Body.split('\n')[0] || ''));
147
+ for (const line of wrap1Body.split('\n').slice(1)) {
148
+ if (line.trim())
149
+ console.log(chalk.gray(' ' + line.replace(/\{thinking\}/g, 'thinking')));
150
+ }
135
151
  console.log();
136
- console.log(chalk.white(' 2. Need to run something? ') + chalk.gray('Prefix shell commands with "!":'));
152
+ console.log(chalk.white(` 2. ${t('ai4ctfWrapLine2Head')} `) + chalk.gray(t('ai4ctfWrapLine2Body')));
137
153
  console.log();
138
154
  console.log(chalk.cyan(' !echo aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ== | base64 -d'));
139
155
  console.log();
140
156
  await sleep(2000);
141
157
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
142
- console.log(chalk.bold.white(' 🎯 Real competition hint quotas'));
158
+ console.log(chalk.bold.white(` ${t('ai4ctfWrapQuotasTitle')}`));
143
159
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
144
160
  console.log();
145
- console.log(chalk.yellow(' hint a ') + chalk.white('50 uses per competition ') + chalk.gray('(use freely)'));
146
- console.log(chalk.yellow(' hint b ') + chalk.white('10 uses ') + chalk.gray('(when stuck)'));
147
- console.log(chalk.yellow(' hint c ') + chalk.white(' 2 uses ') + chalk.gray('(last resort)'));
161
+ console.log(chalk.yellow(' hint a ') + chalk.white(t('ai4ctfWrapQuotaA').padEnd(26)) + chalk.gray(t('ai4ctfWrapQuotaAHint')));
162
+ console.log(chalk.yellow(' hint b ') + chalk.white(t('ai4ctfWrapQuotaB').padEnd(26)) + chalk.gray(t('ai4ctfWrapQuotaBHint')));
163
+ console.log(chalk.yellow(' hint c ') + chalk.white(t('ai4ctfWrapQuotaC').padEnd(26)) + chalk.gray(t('ai4ctfWrapQuotaCHint')));
148
164
  console.log();
149
- console.log(chalk.gray(' Prefer hint a → b → c. And when you just need to look'));
150
- console.log(chalk.gray(' something up, chat with the AI first — it costs nothing'));
151
- console.log(chalk.gray(' against your hint quota.'));
165
+ for (const line of t('ai4ctfWrapQuotaFooter').split('\n')) {
166
+ console.log(chalk.gray(' ' + line));
167
+ }
152
168
  console.log();
153
169
  await sleep(2000);
154
170
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
155
- console.log(chalk.bold.white(' 🔜 Next up: ') + chalk.red.bold('ctf4ai') + chalk.white('trick the AI into saying "koala"'));
171
+ console.log(chalk.bold.white(` ${t('ai4ctfWrapNextTitle')} `) + chalk.red.bold('ctf4ai') + chalk.white(`${t('ai4ctfWrapNextSub')}`));
156
172
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
157
173
  console.log();
158
- console.log(chalk.white(' This next demo ') + chalk.bold.white('does not need programming') + chalk.white(' — just your'));
159
- console.log(chalk.white(' wit and creativity. Can you make a "safe" AI break its'));
160
- console.log(chalk.white(' own rules?'));
174
+ const nextBody = t('ai4ctfWrapNextBody');
175
+ const noProg = t('ai4ctfWrapNoProg');
176
+ const lines = nextBody.split('\n');
177
+ // First line contains the {noProg} placeholder to bold-emphasize.
178
+ if (lines[0]) {
179
+ const [before, after] = lines[0].split('{noProg}');
180
+ console.log(chalk.white(' ' + (before || '')) + chalk.bold.white(noProg) + chalk.white(after || ''));
181
+ }
182
+ for (const line of lines.slice(1)) {
183
+ if (line.trim())
184
+ console.log(chalk.white(' ' + line));
185
+ }
161
186
  console.log();
162
- console.log(chalk.gray(' Type: ') + chalk.bold.red('ctf4ai'));
187
+ console.log(chalk.gray(` ${t('ai4ctfWrapTypeCtf4ai')} `) + chalk.bold.red('ctf4ai'));
163
188
  console.log();
164
189
  return 'exit';
165
190
  }
@@ -214,12 +239,14 @@ export async function handleChatMessage(input) {
214
239
  console.log();
215
240
  return 'exit';
216
241
  }
217
- if (chatTokensUsed >= DEMO_TOKEN_CAP) {
218
- chatActive = false;
219
- chatSession = null;
220
- // Report the token-cap session so the server can count "hit the wall"
221
- // outcomes. Without this, a user who burned 5000 tokens without solving
222
- // was invisible to the admin dashboard.
242
+ // Token cap: first hit → reveal the answer, lock AI, but keep the session
243
+ // alive so the user can still paste the shell command and then submit.
244
+ // `tokensLocked` prevents any further sendMessage calls.
245
+ if (!tokensLocked && chatTokensUsed >= DEMO_TOKEN_CAP) {
246
+ tokensLocked = true;
247
+ // Report the token-cap session once (solved:false). If the user then
248
+ // submits the revealed flag, the submit-success path fires another POST
249
+ // with solved:true which is the canonical record.
223
250
  fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
224
251
  method: 'POST',
225
252
  headers: { 'Content-Type': 'application/json' },
@@ -229,16 +256,18 @@ export async function handleChatMessage(input) {
229
256
  console.log();
230
257
  console.log(chalk.yellow(` ${t('tokenLimit')}`));
231
258
  drawTokenBar();
259
+ showTokenCapReveal();
260
+ return 'continue';
261
+ }
262
+ // If AI is locked (post-reveal), bounce any non-shell, non-submit input
263
+ // back to the user with a reminder of what actually works now.
264
+ if (tokensLocked) {
232
265
  console.log();
233
- console.log(chalk.gray(' ─────────────────────────────────────────'));
234
- console.log(chalk.white(` ${t('ai4ctfReport')}`));
235
- console.log(chalk.gray(` ${t('ai4ctfTokens')}: ${chatTokensUsed}/${DEMO_TOKEN_CAP}`));
236
- console.log(chalk.gray(` ${t('ai4ctfModel')}: Google Gemma 4 (gemma-4-31b-it)`));
237
- console.log(chalk.gray(' ─────────────────────────────────────────'));
238
- console.log();
239
- console.log(chalk.white(` ${t('ai4ctfNext')}`));
266
+ console.log(chalk.yellow(` ${t('ai4ctfLockedTitle')}`));
267
+ console.log(chalk.gray(` ${t('ai4ctfLockedUse')} `) + chalk.cyan('!echo aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ== | base64 -d'));
268
+ console.log(chalk.gray(` ${t('ai4ctfLockedThen')} `) + chalk.cyan('submit icoa{w3lc0me_2_ai4ctf}'));
240
269
  console.log();
241
- return 'exit';
270
+ return 'continue';
242
271
  }
243
272
  console.log(chalk.gray(` ${t('ai4ctfThinking')}`));
244
273
  try {
@@ -280,6 +309,7 @@ export function registerAi4ctfCommand(program) {
280
309
  }
281
310
  chatActive = true;
282
311
  chatTokensUsed = 0;
312
+ tokensLocked = false;
283
313
  // Guided welcome
284
314
  console.log();
285
315
  console.log(chalk.green.bold(` ═══ ${t('ai4ctfTitle')} ═══`));
@@ -308,19 +338,19 @@ export function registerAi4ctfCommand(program) {
308
338
  console.log(chalk.gray(` ${t('ai4ctfHintCUses')}`));
309
339
  console.log();
310
340
  console.log(chalk.gray(' ─────────────────────────────────────────'));
311
- console.log(chalk.bold.white(' 👉 New here? Start with the hints in order:'));
312
- console.log(' ' + chalk.cyan('hint a') + chalk.gray('nudge'));
313
- console.log(' ' + chalk.cyan('hint b') + chalk.gray('technique'));
314
- console.log(' ' + chalk.cyan('hint c') + chalk.gray('principle + shell reminder'));
315
- console.log(chalk.gray(' (hints guide you — they never give the answer directly)'));
341
+ console.log(chalk.bold.white(` ${t('ai4ctfWelcomeCta')}`));
342
+ console.log(' ' + chalk.cyan('hint a') + chalk.gray(`${t('ai4ctfHintNudge')}`));
343
+ console.log(' ' + chalk.cyan('hint b') + chalk.gray(`${t('ai4ctfHintTechnique')}`));
344
+ console.log(' ' + chalk.cyan('hint c') + chalk.gray(`${t('ai4ctfHintPrinciple')}`));
345
+ console.log(chalk.gray(` ${t('ai4ctfWelcomeNoReveal')}`));
316
346
  console.log();
317
- console.log(chalk.white(' Or chat freely with your AI teammate — ask anything'));
318
- console.log(chalk.gray(' about the challenge. Example: ') + chalk.white('"what encoding is this?"'));
347
+ console.log(chalk.white(` ${t('ai4ctfOrChat')}`));
348
+ console.log(chalk.gray(` ${t('ai4ctfOrChatExample')} `) + chalk.white('"what encoding is this?"'));
319
349
  console.log();
320
350
  console.log(chalk.yellow(` ${t('ai4ctfCommands')}`));
321
- console.log(chalk.white(' hint a / b / c ') + chalk.gray('pre-written hints (safe to use)'));
351
+ console.log(chalk.white(' hint a / b / c ') + chalk.gray(t('ai4ctfCmdHintLine')));
322
352
  console.log(chalk.white(' submit <flag> ') + chalk.gray(t('ai4ctfSubmitCmd')));
323
- console.log(chalk.white(' !<shell cmd> ') + chalk.gray('run a shell command'));
353
+ console.log(chalk.white(' !<shell cmd> ') + chalk.gray(t('ai4ctfCmdShellLine')));
324
354
  console.log(chalk.gray(' e.g. ') + chalk.white('!echo aWNv... | base64 -d'));
325
355
  console.log(chalk.gray(` exit ${t('ai4ctfEndSession')}`));
326
356
  console.log();
@@ -50,18 +50,20 @@ function printDemoReport(ctf4aiSolved, ctf4aiTokens) {
50
50
  console.log(chalk.white(` ${t('theoryDone2')}`));
51
51
  console.log();
52
52
  if (hasWrongAnswers) {
53
- console.log(chalk.white(' 💪 Want to nail the ones you missed? Type: ') + chalk.bold.cyan('retry'));
53
+ console.log(chalk.white(` 💪 ${t('reportRetryCta')} `) + chalk.bold.cyan('retry'));
54
54
  console.log();
55
55
  }
56
56
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
57
57
  console.log(chalk.white(` ${t('reportReady')}`));
58
58
  if (hasWrongAnswers) {
59
- console.log(chalk.cyan(' retry') + chalk.gray(` retry the ${retryQueue.length} wrong question${retryQueue.length > 1 ? 's' : ''}`));
59
+ const n = retryQueue.length;
60
+ const tmpl = n === 1 ? t('reportRetryWrongN') : t('reportRetryWrongNPlural');
61
+ console.log(chalk.cyan(' retry') + chalk.gray(` ${tmpl.replace('{n}', String(n))}`));
60
62
  }
61
- console.log(chalk.white(' back') + chalk.gray(` return to main menu`));
63
+ console.log(chalk.white(' back') + chalk.gray(` ${t('reportBackHint')}`));
62
64
  console.log(chalk.white(' demo') + chalk.gray(` ${t('reportDemo')}`));
63
65
  console.log(chalk.white(' nations') + chalk.gray(` ${t('reportNations')}`));
64
- console.log(chalk.white(' about') + chalk.gray(` ${t('reportAbout')}`));
66
+ console.log(chalk.white(' about') + chalk.gray(` ${t('reportAboutHint')}`));
65
67
  console.log();
66
68
  console.log(chalk.yellow(' ICOA 2026 · Sydney, Australia · Jun 27 - Jul 2'));
67
69
  console.log(chalk.cyan.underline(' https://icoa2026.au'));
@@ -260,8 +260,8 @@ function printQuestion(q, answer) {
260
260
  console.log(chalk.yellow(' A/B/C/D') + chalk.gray(` ${t('answerThis')}`));
261
261
  console.log(chalk.yellow(' help') + ' ' + helpLabel);
262
262
  console.log(chalk.yellow(' next') + chalk.gray(' / ') + chalk.yellow('prev') + chalk.gray(` ${t('htpNav')}`));
263
- console.log(chalk.yellow(` exam q 1..${total}`) + chalk.gray(' jump to a specific question'));
264
- console.log(chalk.yellow(' exam review') + chalk.gray(' check progress'));
263
+ console.log(chalk.yellow(` exam q 1..${total}`) + chalk.gray(` ${t('htpJump')}`));
264
+ console.log(chalk.yellow(' exam review') + chalk.gray(` ${t('htpReview')}`));
265
265
  console.log(chalk.yellow(' back') + chalk.gray(` ${t('htpBack')}`));
266
266
  console.log(chalk.yellow(' lang') + chalk.gray(` ${t('htpLang')}`));
267
267
  console.log(chalk.gray(' ─────────────────────────────────────────'));
@@ -929,7 +929,7 @@ export function registerExamCommand(program) {
929
929
  await sleep(2000);
930
930
  console.log();
931
931
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
932
- console.log(chalk.white(' Next: ') + chalk.bold.green('ai4ctf') + chalk.gray(` — ${t('ai4ctfDesc')}`));
932
+ console.log(chalk.white(` ${t('nextLabel')} `) + chalk.bold.green('ai4ctf') + chalk.gray(` — ${t('ai4ctfDesc')}`));
933
933
  console.log(chalk.gray(` ${t('ai4ctfSub')}`));
934
934
  console.log(chalk.cyan(' ─────────────────────────────────────────────'));
935
935
  console.log();
@@ -1,5 +1,5 @@
1
- type Strings = typeof EN;
2
- declare const EN: {
1
+ type Strings = Partial<typeof EN>;
2
+ export declare const EN: {
3
3
  howToPlay: string;
4
4
  htpAnswer: string;
5
5
  htpHelp: string;
@@ -139,6 +139,54 @@ declare const EN: {
139
139
  forNational: string;
140
140
  viewRegions: string;
141
141
  enterExam: string;
142
+ htpJump: string;
143
+ htpReview: string;
144
+ nextLabel: string;
145
+ reportRetryCta: string;
146
+ reportRetryWrongN: string;
147
+ reportRetryWrongNPlural: string;
148
+ reportBackHint: string;
149
+ reportAboutHint: string;
150
+ ai4ctfWelcomeCta: string;
151
+ ai4ctfHintNudge: string;
152
+ ai4ctfHintTechnique: string;
153
+ ai4ctfHintPrinciple: string;
154
+ ai4ctfWelcomeNoReveal: string;
155
+ ai4ctfOrChat: string;
156
+ ai4ctfOrChatExample: string;
157
+ ai4ctfCmdHintLine: string;
158
+ ai4ctfCmdShellLine: string;
159
+ ai4ctfHintABody: string;
160
+ ai4ctfHintBBody: string;
161
+ ai4ctfHintCBody: string;
162
+ ai4ctfHintNextA: string;
163
+ ai4ctfHintNextB: string;
164
+ ai4ctfWrapTitle: string;
165
+ ai4ctfWrapLine1Head: string;
166
+ ai4ctfWrapLine1Body: string;
167
+ ai4ctfWrapLine2Head: string;
168
+ ai4ctfWrapLine2Body: string;
169
+ ai4ctfWrapQuotasTitle: string;
170
+ ai4ctfWrapQuotaA: string;
171
+ ai4ctfWrapQuotaAHint: string;
172
+ ai4ctfWrapQuotaB: string;
173
+ ai4ctfWrapQuotaBHint: string;
174
+ ai4ctfWrapQuotaC: string;
175
+ ai4ctfWrapQuotaCHint: string;
176
+ ai4ctfWrapQuotaFooter: string;
177
+ ai4ctfWrapNextTitle: string;
178
+ ai4ctfWrapNextSub: string;
179
+ ai4ctfWrapNextBody: string;
180
+ ai4ctfWrapNoProg: string;
181
+ ai4ctfWrapTypeCtf4ai: string;
182
+ ai4ctfRevealTitle: string;
183
+ ai4ctfRevealBody: string;
184
+ ai4ctfRevealSeeFlag: string;
185
+ ai4ctfRevealThenSubmit: string;
186
+ ai4ctfRevealLockNote: string;
187
+ ai4ctfLockedTitle: string;
188
+ ai4ctfLockedUse: string;
189
+ ai4ctfLockedThen: string;
142
190
  };
143
191
  export declare function t(key: keyof Strings): string;
144
192
  export declare function hasFullTranslation(lang: string): boolean;