icoa-cli 2.19.356 → 2.19.357
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/ctf.js +787 -1
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/demo2.js +1502 -1
- package/dist/commands/exam.js +1 -1
- package/dist/commands/files.js +59 -1
- package/dist/commands/lang.js +202 -1
- package/dist/commands/log.js +171 -1
- package/dist/commands/shell.js +151 -1
- package/dist/commands/sim.js +389 -1
- package/dist/index.js +355 -1
- package/dist/lib/access.js +184 -1
- package/dist/lib/aienv.js +205 -1
- package/dist/lib/arena-submit.js +21 -1
- package/dist/lib/banner.js +31 -1
- package/dist/lib/budget.js +6 -1
- package/dist/lib/challenge-dir.js +16 -1
- package/dist/lib/colors.js +17 -1
- package/dist/lib/comms.js +212 -1
- package/dist/lib/config.js +93 -1
- package/dist/lib/countdown.js +43 -1
- package/dist/lib/country-lang.js +39 -1
- package/dist/lib/ctfd-client.js +417 -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/docker-probe.d.ts +45 -0
- package/dist/lib/docker-probe.js +118 -0
- package/dist/lib/editor-spawn.js +53 -1
- package/dist/lib/exam-client.js +54 -1
- package/dist/lib/exam-sandbox.js +201 -1
- package/dist/lib/exam-setup.js +36 -1
- package/dist/lib/exam-state.js +273 -1
- package/dist/lib/gemini.js +247 -1
- package/dist/lib/i18n.js +302 -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/learn-input.js +101 -1
- package/dist/lib/learn-render.js +863 -1
- package/dist/lib/learn-state.js +103 -1
- package/dist/lib/log-sync.js +155 -1
- package/dist/lib/logger.js +49 -1
- package/dist/lib/main-rl.js +7 -1
- package/dist/lib/menu-nav.js +105 -1
- package/dist/lib/notebook-doc.js +137 -1
- package/dist/lib/open-file.js +55 -1
- package/dist/lib/paper-upgrade.js +119 -1
- package/dist/lib/platform.js +99 -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/sandbox.d.ts +7 -1
- package/dist/lib/sandbox.js +144 -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/tool-man.js +418 -1
- package/dist/lib/toolset-hash.js +48 -1
- package/dist/lib/translation.js +80 -1
- package/dist/lib/translations-fetcher.js +95 -1
- package/dist/lib/ui.js +99 -1
- package/dist/lib/update-check.js +114 -1
- package/dist/lib/version.js +24 -1
- package/dist/postinstall.js +48 -1
- package/dist/repl.js +2391 -1
- package/dist/types/index.js +63 -1
- package/package.json +1 -1
package/dist/lib/i18n.js
CHANGED
|
@@ -1 +1,302 @@
|
|
|
1
|
-
import{getConfig as e}from"./config.js";import{DEMO_AI4CTF_FLAG_B64 as a}from"./demo-flags.js";import{readFileSync as o,existsSync as i}from"node:fs";import{join as n}from"node:path";import{getLangCacheDir as r}from"./translations-fetcher.js";export const EN={howToPlay:"How to play:",htpAnswer:"answer the question",htpHelp:"remove a wrong option",htpNav:"move between questions",htpMoreHelp:"+3 bonus helps",htpBack:"pause and return to menu",htpLang:"view all languages / lang es to switch",egg3:"Sydney Opera House — Great start!",egg5:"A koala says g'day — 1/3 done!",egg7:"Sydney Harbour Bridge — Keep going!",egg9:"A kangaroo hops by — past halfway!",egg11:"Bondi Beach — almost there!",egg13:"Great Barrier Reef — 2 more to go!",egg15:"G'day mate! All done! 🇦🇺",answerThis:"answer this question",helpRemove:"remove a wrong option",helpUsedUp:"used up — type more help for +3",helpAllUsed:"used up",wrong:"wrong",qTutorial:'👉 Try typing "help" to see what happens!',qTutorialBlock:"Try typing help first to see what it does!",qTutorialRequired:"This question requires you to use help before answering.",allAnswered:"🎉 All questions answered!",typeSubmit:"Type exam submit to see your results!",autoSubmitting:"Auto-submitting your demo...",grading:"Grading...",complete:"Complete!",passed:"✓ PASSED",notPassed:"✗ NOT PASSED",score:"Score",incorrectIntro:"incorrect — here are the corrections:",yourAnswer:"Your answer",correct:"Correct",perfectScore:"Perfect score! All answers correct! 🎉",theoryDone:"These were theory questions. In the real ICOA",theoryDone2:"competition, everything happens in this terminal.",didYouKnow:"Did you know?",ctfHistory1:"CTF (Capture The Flag) started at DEF CON 4 in 1996,",ctfHistory2:"Las Vegas. Founded by Jeff Moss (Dark Tangent).",ctfHistory3:"2026 marks 30 years of CTF worldwide.",ctfFlags1:"In a CTF, you solve challenges to find hidden",ctfFlags2:'"flags" — secret codes like:',ctfFlags3:"Find it, submit it, score points!",timeline1:"1996 First CTF @ DEF CON, Las Vegas",timeline2:"2016 DARPA Cyber Grand Challenge — AI enters CTF",timeline3:"2026 ICOA — First AI Security Olympiad, Sydney",twoTracks:"ICOA uses TWO competition tracks:",ai4ctfDesc:"Use AI to help you solve CTF challenges",ai4ctfSub:"AI is your teammate. Chat, ask for hints, work together.",ctf4aiDesc:"Trick the AI (Prompt Injection)",ctf4aiSub:"Can you make the AI break its own safety rules?",wantToContinue:"Want to experience both? Type",orBack:'Or type "back" to return to the main menu.',ai4ctfTitle:"AI4CTF Demo — AI as Your Teammate",ai4ctfSample:"Here's a sample CTF challenge:",ai4ctfChallenge:"Challenge: Hidden Message [Cryptography]",ai4ctfIntercepted:"You intercepted this encoded text:",ai4ctfDecode:"Can you decode it to find the flag?",ai4ctfLevels:"In competition, you get AI help at 3 levels:",ai4ctfHintA:'General guidance — "What type of encoding is this?"',ai4ctfHintAUses:"50 uses per competition. Safe to use freely.",ai4ctfHintB:'Deep analysis — "How do I decode Base64?"',ai4ctfHintBUses:"10 uses. Use when you're stuck.",ai4ctfHintC:"Critical assist — Nearly gives you the answer",ai4ctfHintCUses:"2 uses only. Last resort!",ai4ctfTryNow:"Try now: ask the AI anything about the challenge above.",ai4ctfExample:`Example: "What encoding is ${a}?"`,ai4ctfFreeChat:"Or just chat freely! You can also try hint a, hint b, hint c.",ai4ctfExit:'Type "exit" when done.',ai4ctfReport:"AI4CTF Session Report",ai4ctfTokens:"Tokens used",ai4ctfModel:"AI Model",ai4ctfNext:'Next: try ctf4ai — trick the AI into saying "koala"',tokenLimit:"Token limit reached!",ctf4aiTitle:"CTF4AI Demo — Challenge the AI",ctf4aiChallenge:"Challenge: Make the AI Say a Forbidden Word",ctf4aiIntro1:"In the real world, AI systems have safety rules.",ctf4aiIntro2:"Your job: find a way to trick the AI into breaking them!",ctf4aiRule:'The AI must NEVER say the word "koala".',ctf4aiMission:'Your mission: make it say "koala" anyway!',ctf4aiIdeas:"Ideas to try:",ctf4aiIdea1:"Ask about Australian animals",ctf4aiIdea2:"Ask it to write a story or poem",ctf4aiIdea3:"Ask it to translate or spell something",ctf4aiIdea4:"Be creative — there is no wrong approach!",ctf4aiBudget:"Conversation budget",ctf4aiQuit:'Type "exit" to quit anytime.',ctf4aiSuccess:'🎉 SUCCESS! The AI said "koala"!',ctf4aiDefense:"You broke through the AI's defenses!",ctf4aiInjection:"This is prompt injection — a real AI vulnerability.",ctf4aiHeld:"The AI held its ground this time. 💪",reportTitle:"ICOA Demo — Complete Report",reportStage1:"Stage 1: Theory Exam (15 questions)",reportCompleted:"✓ Completed",reportStage2:"Stage 2: AI4CTF — AI as Your Teammate",reportExperienced:"✓ Experienced",reportStage2Sub:"You used AI to analyze a CTF challenge.",reportStage2Hints:"In competition: hint a (50x) · hint b (10x) · hint c (2x)",reportStage3:"Stage 3: CTF4AI — Trick the AI",reportSolved:'✓ Solved! You made the AI say "koala"',reportNotSolved:"✗ The AI held its ground",reportRecommend:"Recommendations:",reportRec1:"Study cryptography, web security, and networking",reportRec2:"Practice on CTF platforms like picoCTF",reportRec3:"Learn about prompt injection and AI safety",reportRec4:'Explore the 38 reference guides: type "ref"',reportReady:"Ready for the real competition?",reportNations:"View participating countries",reportAbout:"Learn more about ICOA 2026",reportDemo:"Try again",continueTitle:"AI4CTF — AI as Your Teammate",continueLevels:"In AI4CTF, you solve cybersecurity challenges\nwith AI by your side.",continueHints:"In competition, you get AI help at 3 levels:",continueHintA:"General guidance (50 uses)",continueHintB:"Deep analysis (10 uses)",continueHintC:"Critical assist (2 uses)",continueTry:"Try it now! Type:",continueChat:'Chat freely with your AI teammate. Type "exit" when done.',continueAfter:'After ai4ctf, try: ctf4ai — trick the AI into saying "koala"',ai4ctfCommands:"Commands:",ai4ctfSubmitCmd:"Submit your answer (e.g. submit icoa{...})",ai4ctfShellCmd:"Run a shell command (e.g. !echo aWNv... | base64 -d)",ai4ctfEndSession:"End session",ai4ctfCorrectFlag:"🎉 Correct! Flag accepted!",ai4ctfDecoded:"You decoded the Base64 and found the flag.",ai4ctfWouldEarn:"In the real competition, this would earn you points!",ai4ctfWrongFlag:"✗ Incorrect flag. Try again!",ai4ctfFlagHint:"Hint: decode the Base64 string. Flag format: icoa{...}",ai4ctfFoundFlag:"Found a flag? Type: submit <flag>",ai4ctfThinking:"Thinking...",ctf4aiRelaxed:"After 50%, spelled-out variants (K-O-A-L-A) also count!",ctf4aiEnded:"CTF4AI challenge ended.",ctf4aiAlready:'Already in CTF4AI challenge. Type your message or "exit".',ctf4aiPrompt:'Try to make the AI say "koala"...',ctf4aiThinking:"Thinking...",readyToTry:"Ready to try? Type a command:",forNational:"For national selection exams:",viewRegions:"View all participating regions",enterExam:"Enter your country selection exam",htpJump:"jump to a specific question",htpReview:"check progress",htpSubmit:"submit your answers for grading",htpMark:"flag this question to review later",nextLabel:"Next:",reportRetryCta:"Want to nail the ones you missed? Type:",reportRetryWrongN:"retry the {n} wrong question",reportRetryWrongNPlural:"retry the {n} wrong questions",reportBackHint:"return to main menu",reportAboutHint:"Learn more about ICOA 2026",reportExamSetupHint:"next level: prepare tools for national selection",ai4ctfExitNextTitle:"Next:",ai4ctfExitNextSub:"Challenge the AI (Prompt Injection)",ai4ctfExitNextBody:"Can you make an AI violate its own safety rules?",ai4ctfExitCmdNext:"start the next challenge",ai4ctfExitCmdBack:"return to main menu",ai4ctfWelcomeCta:"👉 New here? Start with the hints in order:",ai4ctfHintNudge:"nudge",ai4ctfHintTechnique:"technique",ai4ctfHintPrinciple:"principle + shell reminder",ai4ctfWelcomeNoReveal:"(hints guide you — they never give the answer directly)",ai4ctfOrChat:"Or chat freely with your AI teammate — ask anything",ai4ctfOrChatExample:"about the challenge. Example:",ai4ctfCmdHintLine:"pre-written hints (safe to use)",ai4ctfCmdShellLine:"run a shell command",ai4ctfHintABody:'The string ends with "==" and uses only letters, digits, "+", "/", and "=".\nThat pattern is a fingerprint — you have seen it before.\nWhere is "=" typically used as padding?',ai4ctfHintBBody:"This is Base64 encoded. Decode the string to find the flag.",ai4ctfHintCBody:`Hints never give you the answer directly — they only point you in the right direction.\nFor simple challenges like this one, hint c adds nothing extra:\nhint b already tells you everything you need.\n\nTwo ways forward from here:\n\n 1. Chat with your AI teammate in natural language. Examples:\n what is the base64 command?\n how do I decode Base64 on macOS?\n Just type the question — anything without "!" goes to the AI.\n\n 2. Run a shell command directly. Shell commands inside ai4ctf\n must start with "!", otherwise your text goes to the AI. Example:\n\n !echo ${a} | base64 -d`,ai4ctfHintNextA:"Stuck? Try:",ai4ctfHintNextB:"Really stuck? Try:",ai4ctfHintNextC:"Once you have the flag:",examWaysToSolve:"Ways to solve",examWaysSavedTo:"data saved to",examWaysShell:"shell one-liner (base64 / xxd / strings / grep …)",examWaysPythonRepl:"interactive REPL (base64/re pre-imported)",examWaysRefPython:"python cheat-sheet",examWaysAi4ctf:"ask your AI teammate (knows this file)",examWaysFound:"When you find it:",ai4ctfWrapTitle:"✨ What you just learned",ai4ctfWrapLine1Head:"Need a command?",ai4ctfWrapLine1Body:'Just ask your AI teammate in natural\nlanguage — "how do I decode Base64 on the command line?"\nYou do not need to memorize every tool. The AI era is\nabout {thinking}, not mechanical memorization.',ai4ctfWrapLine2Head:"Need to run something?",ai4ctfWrapLine2Body:'Prefix shell commands with "!":',ai4ctfWrapQuotasTitle:"🎯 Real competition hint quotas",ai4ctfWrapQuotaA:"50 uses per competition",ai4ctfWrapQuotaAHint:"(use freely)",ai4ctfWrapQuotaB:"10 uses",ai4ctfWrapQuotaBHint:"(when stuck)",ai4ctfWrapQuotaC:" 2 uses",ai4ctfWrapQuotaCHint:"(last resort)",ai4ctfWrapQuotaFooter:"Prefer hint a → b → c. And when you just need to look\nsomething up, chat with the AI first — it costs nothing\nagainst your hint quota.",ai4ctfWrapNextTitle:"🔜 Next up:",ai4ctfWrapNextSub:'trick the AI into saying "koala"',ai4ctfWrapNextBody:'This next demo {noProg} — just your\nwit and creativity. Can you make a "safe" AI break its\nown rules?',ai4ctfWrapNoProg:"does not need programming",ai4ctfWrapTypeCtf4ai:"Type:",typeAndEnterToBegin:"Type {cmd} and press ENTER to begin.",ai4ctfRevealTitle:"💡 Out of AI tokens — here is the reveal",ai4ctfRevealBody:"Looks like you have not found the flag yet.\nNo worries — for the demo we will just tell you.\n\nRun this command to decode the Base64 string:",ai4ctfRevealSeeFlag:"You will see the flag:",ai4ctfRevealThenSubmit:"Then submit it:",ai4ctfRevealLockNote:"(AI chat is locked — only shell commands and submit work now.)",ai4ctfLockedTitle:"AI chat is locked — out of tokens.",ai4ctfLockedUse:"Use:",ai4ctfLockedThen:"Then:"};let s=null,c={};function l(e){if("en"===e)return{};try{const a=n(r(e),"ui.json");return i(a)?JSON.parse(o(a,"utf8")):{}}catch{return{}}}export function loadUiPack(e){s=e||"en",c=l(s)}export function t(a){const o=e().language||"en";if(o!==s&&(s=o,c=l(o)),"en"!==o){const e=c[a];if(null!=e)return e}return EN[a]}
|
|
1
|
+
import { getConfig } from './config.js';
|
|
2
|
+
import { DEMO_AI4CTF_FLAG_B64 } from './demo-flags.js';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { getLangCacheDir } from './translations-fetcher.js';
|
|
6
|
+
export const EN = {
|
|
7
|
+
// How to play
|
|
8
|
+
howToPlay: 'How to play:',
|
|
9
|
+
htpAnswer: 'answer the question',
|
|
10
|
+
htpHelp: 'remove a wrong option',
|
|
11
|
+
htpNav: 'move between questions',
|
|
12
|
+
htpMoreHelp: '+3 bonus helps',
|
|
13
|
+
htpBack: 'pause and return to menu',
|
|
14
|
+
htpLang: 'view all languages / lang es to switch',
|
|
15
|
+
// Easter eggs
|
|
16
|
+
egg3: 'Sydney Opera House — Great start!',
|
|
17
|
+
egg5: "A koala says g'day — 1/3 done!",
|
|
18
|
+
egg7: 'Sydney Harbour Bridge — Keep going!',
|
|
19
|
+
egg9: 'A kangaroo hops by — past halfway!',
|
|
20
|
+
egg11: 'Bondi Beach — almost there!',
|
|
21
|
+
egg13: 'Great Barrier Reef — 2 more to go!',
|
|
22
|
+
egg15: "G'day mate! All done! 🇦🇺",
|
|
23
|
+
// Question UI
|
|
24
|
+
answerThis: 'answer this question',
|
|
25
|
+
helpRemove: 'remove a wrong option',
|
|
26
|
+
helpUsedUp: 'used up — type more help for +3',
|
|
27
|
+
helpAllUsed: 'used up',
|
|
28
|
+
wrong: 'wrong',
|
|
29
|
+
qTutorial: '👉 Try typing "help" to see what happens!',
|
|
30
|
+
qTutorialBlock: 'Try typing help first to see what it does!',
|
|
31
|
+
qTutorialRequired: 'This question requires you to use help before answering.',
|
|
32
|
+
// Results
|
|
33
|
+
allAnswered: '🎉 All questions answered!',
|
|
34
|
+
typeSubmit: 'Type exam submit to see your results!',
|
|
35
|
+
autoSubmitting: 'Auto-submitting your demo...',
|
|
36
|
+
grading: 'Grading...',
|
|
37
|
+
complete: 'Complete!',
|
|
38
|
+
passed: '✓ PASSED',
|
|
39
|
+
notPassed: '✗ NOT PASSED',
|
|
40
|
+
score: 'Score',
|
|
41
|
+
incorrectIntro: 'incorrect — here are the corrections:',
|
|
42
|
+
yourAnswer: 'Your answer',
|
|
43
|
+
correct: 'Correct',
|
|
44
|
+
perfectScore: 'Perfect score! All answers correct! 🎉',
|
|
45
|
+
// CTF intro
|
|
46
|
+
theoryDone: 'These were theory questions. In the real ICOA',
|
|
47
|
+
theoryDone2: 'competition, everything happens in this terminal.',
|
|
48
|
+
didYouKnow: 'Did you know?',
|
|
49
|
+
ctfHistory1: 'CTF (Capture The Flag) started at DEF CON 4 in 1996,',
|
|
50
|
+
ctfHistory2: 'Las Vegas. Founded by Jeff Moss (Dark Tangent).',
|
|
51
|
+
ctfHistory3: '2026 marks 30 years of CTF worldwide.',
|
|
52
|
+
ctfFlags1: 'In a CTF, you solve challenges to find hidden',
|
|
53
|
+
ctfFlags2: '"flags" — secret codes like:',
|
|
54
|
+
ctfFlags3: 'Find it, submit it, score points!',
|
|
55
|
+
timeline1: '1996 First CTF @ DEF CON, Las Vegas',
|
|
56
|
+
timeline2: '2016 DARPA Cyber Grand Challenge — AI enters CTF',
|
|
57
|
+
timeline3: '2026 ICOA — First AI Security Olympiad, Sydney',
|
|
58
|
+
twoTracks: 'ICOA uses TWO competition tracks:',
|
|
59
|
+
ai4ctfDesc: 'Use AI to help you solve CTF challenges',
|
|
60
|
+
ai4ctfSub: 'AI is your teammate. Chat, ask for hints, work together.',
|
|
61
|
+
ctf4aiDesc: 'Trick the AI (Prompt Injection)',
|
|
62
|
+
ctf4aiSub: 'Can you make the AI break its own safety rules?',
|
|
63
|
+
wantToContinue: 'Want to experience both? Type',
|
|
64
|
+
orBack: 'Or type "back" to return to the main menu.',
|
|
65
|
+
// ai4ctf
|
|
66
|
+
ai4ctfTitle: 'AI4CTF Demo — AI as Your Teammate',
|
|
67
|
+
ai4ctfSample: "Here's a sample CTF challenge:",
|
|
68
|
+
ai4ctfChallenge: 'Challenge: Hidden Message [Cryptography]',
|
|
69
|
+
ai4ctfIntercepted: 'You intercepted this encoded text:',
|
|
70
|
+
ai4ctfDecode: 'Can you decode it to find the flag?',
|
|
71
|
+
ai4ctfLevels: 'In competition, you get AI help at 3 levels:',
|
|
72
|
+
ai4ctfHintA: 'General guidance — "What type of encoding is this?"',
|
|
73
|
+
ai4ctfHintAUses: '50 uses per competition. Safe to use freely.',
|
|
74
|
+
ai4ctfHintB: 'Deep analysis — "How do I decode Base64?"',
|
|
75
|
+
ai4ctfHintBUses: "10 uses. Use when you're stuck.",
|
|
76
|
+
ai4ctfHintC: 'Critical assist — Nearly gives you the answer',
|
|
77
|
+
ai4ctfHintCUses: '2 uses only. Last resort!',
|
|
78
|
+
ai4ctfTryNow: 'Try now: ask the AI anything about the challenge above.',
|
|
79
|
+
ai4ctfExample: `Example: "What encoding is ${DEMO_AI4CTF_FLAG_B64}?"`,
|
|
80
|
+
ai4ctfFreeChat: 'Or just chat freely! You can also try hint a, hint b, hint c.',
|
|
81
|
+
ai4ctfExit: 'Type "exit" when done.',
|
|
82
|
+
ai4ctfReport: 'AI4CTF Session Report',
|
|
83
|
+
ai4ctfTokens: 'Tokens used',
|
|
84
|
+
ai4ctfModel: 'AI Model',
|
|
85
|
+
ai4ctfNext: 'Next: try ctf4ai — trick the AI into saying "koala"',
|
|
86
|
+
tokenLimit: 'Token limit reached!',
|
|
87
|
+
// ctf4ai
|
|
88
|
+
ctf4aiTitle: 'CTF4AI Demo — Challenge the AI',
|
|
89
|
+
ctf4aiChallenge: 'Challenge: Make the AI Say a Forbidden Word',
|
|
90
|
+
ctf4aiIntro1: 'In the real world, AI systems have safety rules.',
|
|
91
|
+
ctf4aiIntro2: 'Your job: find a way to trick the AI into breaking them!',
|
|
92
|
+
ctf4aiRule: 'The AI must NEVER say the word "koala".',
|
|
93
|
+
ctf4aiMission: 'Your mission: make it say "koala" anyway!',
|
|
94
|
+
ctf4aiIdeas: 'Ideas to try:',
|
|
95
|
+
ctf4aiIdea1: 'Ask about Australian animals',
|
|
96
|
+
ctf4aiIdea2: 'Ask it to write a story or poem',
|
|
97
|
+
ctf4aiIdea3: 'Ask it to translate or spell something',
|
|
98
|
+
ctf4aiIdea4: 'Be creative — there is no wrong approach!',
|
|
99
|
+
ctf4aiBudget: 'Conversation budget',
|
|
100
|
+
ctf4aiQuit: 'Type "exit" to quit anytime.',
|
|
101
|
+
ctf4aiSuccess: '🎉 SUCCESS! The AI said "koala"!',
|
|
102
|
+
ctf4aiDefense: "You broke through the AI's defenses!",
|
|
103
|
+
ctf4aiInjection: 'This is prompt injection — a real AI vulnerability.',
|
|
104
|
+
ctf4aiHeld: 'The AI held its ground this time. 💪',
|
|
105
|
+
// Demo report
|
|
106
|
+
reportTitle: 'ICOA Demo — Complete Report',
|
|
107
|
+
reportStage1: 'Stage 1: Theory Exam (15 questions)',
|
|
108
|
+
reportCompleted: '✓ Completed',
|
|
109
|
+
reportStage2: 'Stage 2: AI4CTF — AI as Your Teammate',
|
|
110
|
+
reportExperienced: '✓ Experienced',
|
|
111
|
+
reportStage2Sub: 'You used AI to analyze a CTF challenge.',
|
|
112
|
+
reportStage2Hints: 'In competition: hint a (50x) · hint b (10x) · hint c (2x)',
|
|
113
|
+
reportStage3: 'Stage 3: CTF4AI — Trick the AI',
|
|
114
|
+
reportSolved: '✓ Solved! You made the AI say "koala"',
|
|
115
|
+
reportNotSolved: '✗ The AI held its ground',
|
|
116
|
+
reportRecommend: 'Recommendations:',
|
|
117
|
+
reportRec1: 'Study cryptography, web security, and networking',
|
|
118
|
+
reportRec2: 'Practice on CTF platforms like picoCTF',
|
|
119
|
+
reportRec3: 'Learn about prompt injection and AI safety',
|
|
120
|
+
reportRec4: 'Explore the 38 reference guides: type "ref"',
|
|
121
|
+
reportReady: 'Ready for the real competition?',
|
|
122
|
+
reportNations: 'View participating countries',
|
|
123
|
+
reportAbout: 'Learn more about ICOA 2026',
|
|
124
|
+
reportDemo: 'Try again',
|
|
125
|
+
// continue command
|
|
126
|
+
continueTitle: 'AI4CTF — AI as Your Teammate',
|
|
127
|
+
continueLevels: 'In AI4CTF, you solve cybersecurity challenges\nwith AI by your side.',
|
|
128
|
+
continueHints: 'In competition, you get AI help at 3 levels:',
|
|
129
|
+
continueHintA: 'General guidance (50 uses)',
|
|
130
|
+
continueHintB: 'Deep analysis (10 uses)',
|
|
131
|
+
continueHintC: 'Critical assist (2 uses)',
|
|
132
|
+
continueTry: 'Try it now! Type:',
|
|
133
|
+
continueChat: 'Chat freely with your AI teammate. Type "exit" when done.',
|
|
134
|
+
continueAfter: 'After ai4ctf, try: ctf4ai — trick the AI into saying "koala"',
|
|
135
|
+
// ai4ctf new features
|
|
136
|
+
ai4ctfCommands: 'Commands:',
|
|
137
|
+
ai4ctfSubmitCmd: 'Submit your answer (e.g. submit icoa{...})',
|
|
138
|
+
ai4ctfShellCmd: 'Run a shell command (e.g. !echo aWNv... | base64 -d)',
|
|
139
|
+
ai4ctfEndSession: 'End session',
|
|
140
|
+
ai4ctfCorrectFlag: '🎉 Correct! Flag accepted!',
|
|
141
|
+
ai4ctfDecoded: 'You decoded the Base64 and found the flag.',
|
|
142
|
+
ai4ctfWouldEarn: 'In the real competition, this would earn you points!',
|
|
143
|
+
ai4ctfWrongFlag: '✗ Incorrect flag. Try again!',
|
|
144
|
+
ai4ctfFlagHint: 'Hint: decode the Base64 string. Flag format: icoa{...}',
|
|
145
|
+
ai4ctfFoundFlag: 'Found a flag? Type: submit <flag>',
|
|
146
|
+
ai4ctfThinking: 'Thinking...',
|
|
147
|
+
// ctf4ai new features
|
|
148
|
+
ctf4aiRelaxed: 'After 50%, spelled-out variants (K-O-A-L-A) also count!',
|
|
149
|
+
ctf4aiEnded: 'CTF4AI challenge ended.',
|
|
150
|
+
ctf4aiAlready: 'Already in CTF4AI challenge. Type your message or "exit".',
|
|
151
|
+
ctf4aiPrompt: 'Try to make the AI say "koala"...',
|
|
152
|
+
ctf4aiThinking: 'Thinking...',
|
|
153
|
+
// exam CTF intro
|
|
154
|
+
readyToTry: 'Ready to try? Type a command:',
|
|
155
|
+
forNational: 'For national selection exams:',
|
|
156
|
+
viewRegions: 'View all participating regions',
|
|
157
|
+
enterExam: 'Enter your country selection exam',
|
|
158
|
+
// ═══ v2.19.26–35 additions ═══
|
|
159
|
+
// Exam help footer (v2.19.26)
|
|
160
|
+
htpJump: 'jump to a specific question',
|
|
161
|
+
htpReview: 'check progress',
|
|
162
|
+
htpSubmit: 'submit your answers for grading',
|
|
163
|
+
htpMark: 'flag this question to review later',
|
|
164
|
+
// Stage 1 → Stage 2 handoff (v2.19.28)
|
|
165
|
+
nextLabel: 'Next:',
|
|
166
|
+
// Final report retry/back menu (v2.19.28)
|
|
167
|
+
reportRetryCta: 'Want to nail the ones you missed? Type:',
|
|
168
|
+
reportRetryWrongN: 'retry the {n} wrong question',
|
|
169
|
+
reportRetryWrongNPlural: 'retry the {n} wrong questions',
|
|
170
|
+
reportBackHint: 'return to main menu',
|
|
171
|
+
reportAboutHint: 'Learn more about ICOA 2026',
|
|
172
|
+
reportExamSetupHint: 'next level: prepare tools for national selection',
|
|
173
|
+
// AI4CTF exit → CTF4AI transition (v2.19.47)
|
|
174
|
+
ai4ctfExitNextTitle: 'Next:',
|
|
175
|
+
ai4ctfExitNextSub: 'Challenge the AI (Prompt Injection)',
|
|
176
|
+
ai4ctfExitNextBody: 'Can you make an AI violate its own safety rules?',
|
|
177
|
+
ai4ctfExitCmdNext: 'start the next challenge',
|
|
178
|
+
ai4ctfExitCmdBack: 'return to main menu',
|
|
179
|
+
// ai4ctf welcome / CTA (v2.19.30–32)
|
|
180
|
+
ai4ctfWelcomeCta: '👉 New here? Start with the hints in order:',
|
|
181
|
+
ai4ctfHintNudge: 'nudge',
|
|
182
|
+
ai4ctfHintTechnique: 'technique',
|
|
183
|
+
ai4ctfHintPrinciple: 'principle + shell reminder',
|
|
184
|
+
ai4ctfWelcomeNoReveal: '(hints guide you — they never give the answer directly)',
|
|
185
|
+
ai4ctfOrChat: 'Or chat freely with your AI teammate — ask anything',
|
|
186
|
+
ai4ctfOrChatExample: 'about the challenge. Example:',
|
|
187
|
+
ai4ctfCmdHintLine: 'pre-written hints (safe to use)',
|
|
188
|
+
ai4ctfCmdShellLine: 'run a shell command',
|
|
189
|
+
// Hint A/B/C bodies (v2.19.30–31 + v2.19.35)
|
|
190
|
+
ai4ctfHintABody: 'The string ends with "==" and uses only letters, digits, "+", "/", and "=".\n' +
|
|
191
|
+
'That pattern is a fingerprint — you have seen it before.\n' +
|
|
192
|
+
'Where is "=" typically used as padding?',
|
|
193
|
+
ai4ctfHintBBody: 'This is Base64 encoded. Decode the string to find the flag.',
|
|
194
|
+
ai4ctfHintCBody: 'Hints never give you the answer directly — they only point you in the right direction.\n' +
|
|
195
|
+
'For simple challenges like this one, hint c adds nothing extra:\n' +
|
|
196
|
+
'hint b already tells you everything you need.\n' +
|
|
197
|
+
'\n' +
|
|
198
|
+
'Two ways forward from here:\n' +
|
|
199
|
+
'\n' +
|
|
200
|
+
' 1. Chat with your AI teammate in natural language. Examples:\n' +
|
|
201
|
+
' what is the base64 command?\n' +
|
|
202
|
+
' how do I decode Base64 on macOS?\n' +
|
|
203
|
+
' Just type the question — anything without "!" goes to the AI.\n' +
|
|
204
|
+
'\n' +
|
|
205
|
+
' 2. Run a shell command directly. Shell commands inside ai4ctf\n' +
|
|
206
|
+
' must start with "!", otherwise your text goes to the AI. Example:\n' +
|
|
207
|
+
'\n' +
|
|
208
|
+
` !echo ${DEMO_AI4CTF_FLAG_B64} | base64 -d`,
|
|
209
|
+
ai4ctfHintNextA: 'Stuck? Try:',
|
|
210
|
+
ai4ctfHintNextB: 'Really stuck? Try:',
|
|
211
|
+
ai4ctfHintNextC: 'Once you have the flag:',
|
|
212
|
+
examWaysToSolve: 'Ways to solve',
|
|
213
|
+
examWaysSavedTo: 'data saved to',
|
|
214
|
+
examWaysShell: 'shell one-liner (base64 / xxd / strings / grep …)',
|
|
215
|
+
examWaysPythonRepl: 'interactive REPL (base64/re pre-imported)',
|
|
216
|
+
examWaysRefPython: 'python cheat-sheet',
|
|
217
|
+
examWaysAi4ctf: 'ask your AI teammate (knows this file)',
|
|
218
|
+
examWaysFound: 'When you find it:',
|
|
219
|
+
// Post-solve wrap-up (v2.19.32)
|
|
220
|
+
ai4ctfWrapTitle: '✨ What you just learned',
|
|
221
|
+
ai4ctfWrapLine1Head: 'Need a command?',
|
|
222
|
+
ai4ctfWrapLine1Body: 'Just ask your AI teammate in natural\n' +
|
|
223
|
+
'language — "how do I decode Base64 on the command line?"\n' +
|
|
224
|
+
'You do not need to memorize every tool. The AI era is\n' +
|
|
225
|
+
'about {thinking}, not mechanical memorization.',
|
|
226
|
+
ai4ctfWrapLine2Head: 'Need to run something?',
|
|
227
|
+
ai4ctfWrapLine2Body: 'Prefix shell commands with "!":',
|
|
228
|
+
ai4ctfWrapQuotasTitle: '🎯 Real competition hint quotas',
|
|
229
|
+
ai4ctfWrapQuotaA: '50 uses per competition',
|
|
230
|
+
ai4ctfWrapQuotaAHint: '(use freely)',
|
|
231
|
+
ai4ctfWrapQuotaB: '10 uses',
|
|
232
|
+
ai4ctfWrapQuotaBHint: '(when stuck)',
|
|
233
|
+
ai4ctfWrapQuotaC: ' 2 uses',
|
|
234
|
+
ai4ctfWrapQuotaCHint: '(last resort)',
|
|
235
|
+
ai4ctfWrapQuotaFooter: 'Prefer hint a → b → c. And when you just need to look\n' +
|
|
236
|
+
'something up, chat with the AI first — it costs nothing\n' +
|
|
237
|
+
'against your hint quota.',
|
|
238
|
+
ai4ctfWrapNextTitle: '🔜 Next up:',
|
|
239
|
+
ai4ctfWrapNextSub: 'trick the AI into saying "koala"',
|
|
240
|
+
ai4ctfWrapNextBody: 'This next demo {noProg} — just your\n' + 'wit and creativity. Can you make a "safe" AI break its\n' + 'own rules?',
|
|
241
|
+
ai4ctfWrapNoProg: 'does not need programming',
|
|
242
|
+
ai4ctfWrapTypeCtf4ai: 'Type:',
|
|
243
|
+
// Demo section-transition prompt (MCQ → ai4ctf, ai4ctf → ctf4ai).
|
|
244
|
+
// {cmd} is replaced in code with the highlighted command name.
|
|
245
|
+
typeAndEnterToBegin: 'Type {cmd} and press ENTER to begin.',
|
|
246
|
+
// Token-cap reveal (v2.19.35)
|
|
247
|
+
ai4ctfRevealTitle: '💡 Out of AI tokens — here is the reveal',
|
|
248
|
+
ai4ctfRevealBody: 'Looks like you have not found the flag yet.\n' +
|
|
249
|
+
'No worries — for the demo we will just tell you.\n' +
|
|
250
|
+
'\n' +
|
|
251
|
+
'Run this command to decode the Base64 string:',
|
|
252
|
+
ai4ctfRevealSeeFlag: 'You will see the flag:',
|
|
253
|
+
ai4ctfRevealThenSubmit: 'Then submit it:',
|
|
254
|
+
ai4ctfRevealLockNote: '(AI chat is locked — only shell commands and submit work now.)',
|
|
255
|
+
// Locked-chat bounce (v2.19.35)
|
|
256
|
+
ai4ctfLockedTitle: 'AI chat is locked — out of tokens.',
|
|
257
|
+
ai4ctfLockedUse: 'Use:',
|
|
258
|
+
ai4ctfLockedThen: 'Then:',
|
|
259
|
+
};
|
|
260
|
+
// ─── Lazy-loaded UI packs ──────────────────────────────────────────────────
|
|
261
|
+
// EN is bundled above (offline-instant source of truth). The 20 non-EN UI
|
|
262
|
+
// packs were moved out of the npm tarball (~78KB gz) and ride the existing
|
|
263
|
+
// translation-pack lazy-load: ensureLangCache() fetches <lang>.tar.gz (which
|
|
264
|
+
// now contains ui.json) → ~/.icoa/translations/<lang>/ui.json. t() stays
|
|
265
|
+
// synchronous — it reads config.language each call and lazily loads the active
|
|
266
|
+
// pack from disk once per language (memoized). Missing pack → EN fallback.
|
|
267
|
+
let loadedLang = null;
|
|
268
|
+
let activePack = {};
|
|
269
|
+
function readPack(lang) {
|
|
270
|
+
if (lang === 'en')
|
|
271
|
+
return {};
|
|
272
|
+
try {
|
|
273
|
+
const p = join(getLangCacheDir(lang), 'ui.json');
|
|
274
|
+
return existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Force (re)load the UI pack for `lang` into the in-memory cache. Call right
|
|
282
|
+
* after a fetch (boot / lang switch) so a freshly-downloaded ui.json takes
|
|
283
|
+
* effect even if t() already memoized an empty pack for this lang.
|
|
284
|
+
*/
|
|
285
|
+
export function loadUiPack(lang) {
|
|
286
|
+
loadedLang = lang || 'en';
|
|
287
|
+
activePack = readPack(loadedLang);
|
|
288
|
+
}
|
|
289
|
+
// For languages without a (loaded) translation, fall back to English.
|
|
290
|
+
export function t(key) {
|
|
291
|
+
const lang = getConfig().language || 'en';
|
|
292
|
+
if (lang !== loadedLang) {
|
|
293
|
+
loadedLang = lang;
|
|
294
|
+
activePack = readPack(lang);
|
|
295
|
+
}
|
|
296
|
+
if (lang !== 'en') {
|
|
297
|
+
const v = activePack[key];
|
|
298
|
+
if (v != null)
|
|
299
|
+
return v;
|
|
300
|
+
}
|
|
301
|
+
return EN[key];
|
|
302
|
+
}
|
|
@@ -1 +1,88 @@
|
|
|
1
|
-
|
|
1
|
+
// Submit-triggered integrity sampling (P1.5 — detective layer).
|
|
2
|
+
//
|
|
3
|
+
// On the flag-submit path we fire a SECOND, fire-and-forget POST to our own
|
|
4
|
+
// server carrying a SCRUBBED snapshot of client-only-knowable state, anchored
|
|
5
|
+
// to a real server-recorded scoring event (the CTFd submission). The server
|
|
6
|
+
// debounces (first per 5-min window, server-side so a cheater can't suppress
|
|
7
|
+
// it), reconciles against its own authoritative ledger, and stores the row in
|
|
8
|
+
// an auditable SQLite table.
|
|
9
|
+
//
|
|
10
|
+
// This is DETECTIVE-ONLY: it never auto-judges, never blocks a submit, and
|
|
11
|
+
// never reveals any verdict to the client. It catches casual tampering
|
|
12
|
+
// (`echo > budget.json`, `rm sim-cooldown.json`) and cross-account fingerprint
|
|
13
|
+
// reuse. A sophisticated forger who also fakes consistent snapshots evades it —
|
|
14
|
+
// it COMPLEMENTS the server-authoritative scoring surfaces (§9 of
|
|
15
|
+
// docs/POST_COMPETITION_AUDIT_STANDARD.md), it does not replace them.
|
|
16
|
+
//
|
|
17
|
+
// HARD RULES honored here (see §11):
|
|
18
|
+
// - Gate on competitionState === 'live' (sample only during the contest).
|
|
19
|
+
// - NEVER upload secrets: config.accessToken / config.token are excluded.
|
|
20
|
+
// - Carry the CTFd account identity (Authorization: Token <t> + account) so
|
|
21
|
+
// the server can VERIFY against CTFd's tokens→users tables — mirrors the
|
|
22
|
+
// gemini.ts / /api/icoa/audit auth so attribution is evidence, not a
|
|
23
|
+
// forgeable self-report.
|
|
24
|
+
// - Fire-and-forget: any failure is swallowed so the submit UX is untouched.
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { getConfig, getBudget } from './config.js';
|
|
29
|
+
import { getCliVersion } from './version.js';
|
|
30
|
+
function readJsonFile(path) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fire a scrubbed integrity snapshot to the server. Best-effort, non-blocking,
|
|
40
|
+
* and self-swallowing — callers may `await` it inside an existing try/catch
|
|
41
|
+
* without risking the submit flow.
|
|
42
|
+
*/
|
|
43
|
+
export async function postIntegritySnapshot() {
|
|
44
|
+
try {
|
|
45
|
+
const config = getConfig();
|
|
46
|
+
// Gate: only sample during the live contest. Practice/learn traffic is
|
|
47
|
+
// non-scoring (§9) and carries no integrity interest.
|
|
48
|
+
if (config.competitionState !== 'live')
|
|
49
|
+
return;
|
|
50
|
+
const serverUrl = config.ctfdUrl;
|
|
51
|
+
if (!serverUrl)
|
|
52
|
+
return;
|
|
53
|
+
const icoaDir = join(homedir(), '.icoa');
|
|
54
|
+
const budget = getBudget();
|
|
55
|
+
// SCRUBBED payload — only client-only-knowable, non-secret state. The
|
|
56
|
+
// accessToken / token in config.json are deliberately NOT included.
|
|
57
|
+
const payload = {
|
|
58
|
+
deviceFingerprint: config.deviceFingerprint || '',
|
|
59
|
+
installSalt: config.installSalt || '',
|
|
60
|
+
budget: { tokensUsed: budget.tokensUsed, tokenCap: budget.tokenCap },
|
|
61
|
+
simCooldown: readJsonFile(join(icoaDir, 'sim-cooldown.json')),
|
|
62
|
+
tokenBindings: readJsonFile(join(icoaDir, 'token-bindings.json')),
|
|
63
|
+
};
|
|
64
|
+
const headers = {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
'User-Agent': `icoa-cli/${getCliVersion()}`,
|
|
67
|
+
'X-Device-Fingerprint': config.deviceFingerprint || '',
|
|
68
|
+
};
|
|
69
|
+
// CTFd-account identity — same mechanism as gemini.ts. Only attach when
|
|
70
|
+
// connected to a CTFd event; the server VERIFIES the token in-process
|
|
71
|
+
// (account_verified=1) and falls back to the self-reported account only
|
|
72
|
+
// when it can't resolve.
|
|
73
|
+
if (config.token && config.userName) {
|
|
74
|
+
payload.account = config.userName;
|
|
75
|
+
headers.Authorization = `Token ${config.token}`;
|
|
76
|
+
}
|
|
77
|
+
await fetch(`${serverUrl}/api/icoa/snapshot`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers,
|
|
80
|
+
body: JSON.stringify(payload),
|
|
81
|
+
signal: AbortSignal.timeout(8000),
|
|
82
|
+
});
|
|
83
|
+
// Response intentionally ignored: detective-only, server reveals nothing.
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Swallow everything — the snapshot must never affect the submit UX.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1 +1,55 @@
|
|
|
1
|
-
import{spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { getMainRl } from './main-rl.js';
|
|
3
|
+
/**
|
|
4
|
+
* Run an interactive child process (nc / ncat / ssh) with the terminal fully
|
|
5
|
+
* released so the user's INPUT and Ctrl-C reach the child.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (BUG: pwn `connect` could read output but not send input and
|
|
8
|
+
* Ctrl-C was dead): when dispatched IN-PROCESS from the icoa REPL, the main
|
|
9
|
+
* readline holds stdin in RAW mode. A child spawned with `stdio: 'inherit'`
|
|
10
|
+
* then inherits a raw, non-blocking stdin → its reads see no keystrokes, and
|
|
11
|
+
* raw mode means Ctrl-C is delivered as a byte (0x03), never as SIGINT → the
|
|
12
|
+
* session looks frozen and won't quit. `execSync`/`execFileSync` never dropped
|
|
13
|
+
* raw mode, which is the bug.
|
|
14
|
+
*
|
|
15
|
+
* This mirrors repl.ts `runSystemCommand`'s proven TTY dance (pause the rl +
|
|
16
|
+
* `setRawMode(false)` + async spawn + restore), but pulls the active REPL
|
|
17
|
+
* readline via getMainRl() so it works BOTH inside the REPL and standalone
|
|
18
|
+
* (`icoa connect …`, where getMainRl() === null → pause/restore are no-ops).
|
|
19
|
+
*
|
|
20
|
+
* Pass a file + args (no shell — injection-safe, preferred for server-supplied
|
|
21
|
+
* host/port) OR set `shell: true` to run `file` as a full command string.
|
|
22
|
+
*
|
|
23
|
+
* Resolves with the child's exit code (1 on spawn error).
|
|
24
|
+
*/
|
|
25
|
+
export function runInteractive(file, args = [], opts = {}) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const rl = getMainRl();
|
|
28
|
+
const stdin = process.stdin;
|
|
29
|
+
const wasRaw = stdin.isTTY ? !!stdin.isRaw : false;
|
|
30
|
+
rl?.pause();
|
|
31
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
32
|
+
try {
|
|
33
|
+
stdin.setRawMode(false);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
const child = opts.shell ? spawn(file, { shell: true, stdio: 'inherit' }) : spawn(file, args, { stdio: 'inherit' });
|
|
38
|
+
let settled = false;
|
|
39
|
+
const restore = (code) => {
|
|
40
|
+
if (settled)
|
|
41
|
+
return;
|
|
42
|
+
settled = true;
|
|
43
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function' && wasRaw) {
|
|
44
|
+
try {
|
|
45
|
+
stdin.setRawMode(true);
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
rl?.resume();
|
|
50
|
+
resolve(code);
|
|
51
|
+
};
|
|
52
|
+
child.on('close', (code) => restore(code ?? 0));
|
|
53
|
+
child.on('error', () => restore(1));
|
|
54
|
+
});
|
|
55
|
+
}
|
package/dist/lib/ipynb-input.js
CHANGED
|
@@ -1 +1,65 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ipynb-input — pure cell-input decision logic for the `icoa ipynb` REPL
|
|
3
|
+
* (Phase 1 UI of the CLI notebook arena).
|
|
4
|
+
*
|
|
5
|
+
* IPython-style model: a one-liner runs the moment you press Enter; a block
|
|
6
|
+
* (def/for/if/…, an open bracket, or a trailing backslash) keeps accepting
|
|
7
|
+
* lines until you end it with a blank line. Kept pure so the rule is unit-
|
|
8
|
+
* tested without a kernel or readline.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Does this line open a multi-line block? True when its CODE portion (trailing
|
|
12
|
+
* `#` comment stripped) ends with a colon. Naive comment-strip — good enough for
|
|
13
|
+
* opener detection (a real block opener never hides its colon in a comment).
|
|
14
|
+
*/
|
|
15
|
+
export function isBlockOpener(line) {
|
|
16
|
+
const code = line.split('#')[0];
|
|
17
|
+
return /:\s*$/.test(code);
|
|
18
|
+
}
|
|
19
|
+
/** Trailing line-continuation backslash. */
|
|
20
|
+
function hasLineContinuation(line) {
|
|
21
|
+
return /\\\s*$/.test(line);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Count of unclosed brackets across the buffer. Naive: ignores brackets inside
|
|
25
|
+
* string literals (rare in interactive one-liners; a blank line always forces
|
|
26
|
+
* execution as the escape hatch). Positive → still open.
|
|
27
|
+
*/
|
|
28
|
+
function openBracketDepth(code) {
|
|
29
|
+
let depth = 0;
|
|
30
|
+
for (const ch of code) {
|
|
31
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
32
|
+
depth++;
|
|
33
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
34
|
+
depth--;
|
|
35
|
+
}
|
|
36
|
+
return depth;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Given the lines accumulated SO FAR (including the just-entered line), decide
|
|
40
|
+
* whether to execute the cell now or keep taking input.
|
|
41
|
+
*
|
|
42
|
+
* - empty buffer → nothing to run (false),
|
|
43
|
+
* - multi-line cell → execute as soon as the last line is blank,
|
|
44
|
+
* - single line → execute unless it opens a block / ends with `\` / has an
|
|
45
|
+
* unbalanced bracket.
|
|
46
|
+
*/
|
|
47
|
+
export function shouldExecuteCell(lines) {
|
|
48
|
+
if (lines.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const last = lines[lines.length - 1];
|
|
51
|
+
if (lines.length > 1) {
|
|
52
|
+
// In multi-line mode a blank line is the universal "run it now".
|
|
53
|
+
return last.trim() === '';
|
|
54
|
+
}
|
|
55
|
+
// Single line.
|
|
56
|
+
if (last.trim() === '')
|
|
57
|
+
return false;
|
|
58
|
+
if (isBlockOpener(last))
|
|
59
|
+
return false;
|
|
60
|
+
if (hasLineContinuation(last))
|
|
61
|
+
return false;
|
|
62
|
+
if (openBracketDepth(last) > 0)
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -1 +1,88 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Kernel protocol — the pure Node side of the Node ↔ Python-bridge boundary
|
|
3
|
+
* (Phase 1 of the CLI notebook arena, see `project_cli_notebook_arena_plan`).
|
|
4
|
+
*
|
|
5
|
+
* icoa is TypeScript/Node, but Jupyter kernels are driven by `jupyter_client`
|
|
6
|
+
* (Python). So a thin Python bridge (shipped alongside, run from the aienv
|
|
7
|
+
* venv) owns the kernel and speaks a line-delimited JSON protocol to Node: one
|
|
8
|
+
* event object per stdout line. This module parses those lines and folds them
|
|
9
|
+
* into renderable cell outputs.
|
|
10
|
+
*
|
|
11
|
+
* Keeping this layer pure (no I/O) means the wire contract is unit-tested
|
|
12
|
+
* without spawning Python — and it stays identical whether the bridge drives a
|
|
13
|
+
* LOCAL kernel (Phase 1) or a REMOTE one over a Kernel Gateway (Phase 2/4). The
|
|
14
|
+
* Node side never changes; only the bridge's connection does.
|
|
15
|
+
*/
|
|
16
|
+
const KNOWN_TYPES = new Set(['ready', 'stream', 'result', 'display', 'error', 'done', 'fatal']);
|
|
17
|
+
/**
|
|
18
|
+
* Parse one stdout line from the bridge into a KernelEvent. Returns null for
|
|
19
|
+
* blank lines, non-JSON, or JSON without a recognized `type` (the bridge may
|
|
20
|
+
* interleave incidental output; we ignore anything not in the protocol).
|
|
21
|
+
*/
|
|
22
|
+
export function parseKernelEvent(line) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed)
|
|
25
|
+
return null;
|
|
26
|
+
let obj;
|
|
27
|
+
try {
|
|
28
|
+
obj = JSON.parse(trimmed);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (!obj || typeof obj !== 'object')
|
|
34
|
+
return null;
|
|
35
|
+
const type = obj.type;
|
|
36
|
+
if (typeof type !== 'string' || !KNOWN_TYPES.has(type))
|
|
37
|
+
return null;
|
|
38
|
+
return obj;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fold a cell's event stream into renderable outputs.
|
|
42
|
+
*
|
|
43
|
+
* - consecutive `stream` events of the SAME name coalesce into one text block
|
|
44
|
+
* (matches Jupyter's stdout/stderr batching → clean rendering),
|
|
45
|
+
* - `result` / `display` / `error` pass through as discrete blocks,
|
|
46
|
+
* - any `error` sets ok=false,
|
|
47
|
+
* - `done.count` becomes execCount (null if the cell never finished).
|
|
48
|
+
*/
|
|
49
|
+
export function foldCellOutputs(events) {
|
|
50
|
+
const outputs = [];
|
|
51
|
+
let ok = true;
|
|
52
|
+
let execCount = null;
|
|
53
|
+
for (const e of events) {
|
|
54
|
+
switch (e.type) {
|
|
55
|
+
case 'stream': {
|
|
56
|
+
const last = outputs[outputs.length - 1];
|
|
57
|
+
if (last && last.kind === 'stream' && last.name === e.name) {
|
|
58
|
+
last.text += e.text;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
outputs.push({ kind: 'stream', name: e.name, text: e.text });
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 'result':
|
|
66
|
+
outputs.push({ kind: 'result', text: e.text, ...(e.data ? { data: e.data } : {}) });
|
|
67
|
+
break;
|
|
68
|
+
case 'display':
|
|
69
|
+
outputs.push({ kind: 'display', data: e.data });
|
|
70
|
+
break;
|
|
71
|
+
case 'error':
|
|
72
|
+
ok = false;
|
|
73
|
+
outputs.push({ kind: 'error', ename: e.ename, evalue: e.evalue, traceback: e.traceback });
|
|
74
|
+
break;
|
|
75
|
+
case 'done':
|
|
76
|
+
execCount = e.count;
|
|
77
|
+
break;
|
|
78
|
+
case 'fatal':
|
|
79
|
+
ok = false;
|
|
80
|
+
outputs.push({ kind: 'error', ename: 'KernelError', evalue: e.message, traceback: [e.message] });
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
// 'ready' and any future event types carry no cell output.
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { outputs, ok, execCount };
|
|
88
|
+
}
|