termi-kids 0.1.0

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 (64) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +148 -0
  3. package/SAFETY.md +187 -0
  4. package/bin/termi.js +22 -0
  5. package/dist/agent/context.js +126 -0
  6. package/dist/agent/loop.js +172 -0
  7. package/dist/agent/prompts/system.js +45 -0
  8. package/dist/agent/tools.js +335 -0
  9. package/dist/auth/keychain.js +146 -0
  10. package/dist/auth/oauth.js +375 -0
  11. package/dist/auth/tokens.js +219 -0
  12. package/dist/cli.js +258 -0
  13. package/dist/config/paths.js +92 -0
  14. package/dist/config/pin.js +150 -0
  15. package/dist/config/settings.js +131 -0
  16. package/dist/grownups/panel.js +483 -0
  17. package/dist/learn/lessons.js +490 -0
  18. package/dist/learn/runner.js +193 -0
  19. package/dist/preview/server.js +407 -0
  20. package/dist/projects/create.js +103 -0
  21. package/dist/projects/ideas.js +182 -0
  22. package/dist/projects/quests.js +277 -0
  23. package/dist/projects/scaffolds/art.js +484 -0
  24. package/dist/projects/scaffolds/biggames.js +554 -0
  25. package/dist/projects/scaffolds/characters.js +580 -0
  26. package/dist/projects/scaffolds/games.js +516 -0
  27. package/dist/projects/scaffolds/index.js +24 -0
  28. package/dist/projects/scaffolds/music.js +528 -0
  29. package/dist/projects/scaffolds/pets.js +567 -0
  30. package/dist/projects/scaffolds/quizzes.js +757 -0
  31. package/dist/projects/scaffolds/stories.js +620 -0
  32. package/dist/projects/scaffolds/vendor/KAPLAY-LICENSE.txt +35 -0
  33. package/dist/projects/scaffolds/vendor/kaplay.mjs +57 -0
  34. package/dist/projects/scaffolds/websites.js +474 -0
  35. package/dist/projects/snapshots.js +203 -0
  36. package/dist/projects/store.js +325 -0
  37. package/dist/providers/errors.js +207 -0
  38. package/dist/providers/index.js +316 -0
  39. package/dist/providers/models.js +38 -0
  40. package/dist/safety/audit.js +195 -0
  41. package/dist/safety/blocks.js +29 -0
  42. package/dist/safety/classifier.js +337 -0
  43. package/dist/safety/codescan.js +168 -0
  44. package/dist/safety/guarddownload.js +79 -0
  45. package/dist/safety/guardrunner.js +125 -0
  46. package/dist/safety/localguard.js +227 -0
  47. package/dist/safety/modelstore.js +127 -0
  48. package/dist/safety/prefilter.js +214 -0
  49. package/dist/safety/session.js +118 -0
  50. package/dist/safety/taxonomy.js +246 -0
  51. package/dist/safety/textextract.js +193 -0
  52. package/dist/setup/launcher.js +65 -0
  53. package/dist/setup/wizard.js +469 -0
  54. package/dist/surfaces/chat.js +439 -0
  55. package/dist/surfaces/commands.js +206 -0
  56. package/dist/surfaces/home.js +438 -0
  57. package/dist/types.js +5 -0
  58. package/dist/ui/banner.js +35 -0
  59. package/dist/ui/celebrate.js +141 -0
  60. package/dist/ui/errors.js +97 -0
  61. package/dist/ui/mascot.js +223 -0
  62. package/dist/ui/text.js +156 -0
  63. package/dist/ui/theme.js +92 -0
  64. package/package.json +67 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Friendly error screens and process-level crash handling.
3
+ * Provider error bodies never reach the kid. Screens come from T strings only.
4
+ */
5
+ import { mascot } from './mascot.js';
6
+ import { glyph, style } from './theme.js';
7
+ import { T } from './text.js';
8
+ /** Local clock time when the provider says how long to wait. */
9
+ export function formatResetTime(retryAfterSeconds, now = new Date()) {
10
+ const at = new Date(now.getTime() + retryAfterSeconds * 1000);
11
+ return at.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
12
+ }
13
+ function stillWorksBlock() {
14
+ const bullet = glyph('star');
15
+ const items = T.quota.stillWorks.map((item) => ` ${bullet} ${item}`);
16
+ return [T.quota.stillWorksIntro, ...items].join('\n');
17
+ }
18
+ function screen(face, lines) {
19
+ return [face, '', ...lines].join('\n');
20
+ }
21
+ /** Map a provider error to a kid-safe mascot screen. */
22
+ export function renderProviderError(err, providerLabel) {
23
+ const face = mascot('oops');
24
+ switch (err.kind) {
25
+ case 'rate-limit': {
26
+ const message = err.retryAfter !== undefined
27
+ ? T.quota.message.replace('{time}', formatResetTime(err.retryAfter))
28
+ : T.quota.messageNoTime;
29
+ return screen(face, [message, '', stillWorksBlock()]);
30
+ }
31
+ case 'auth':
32
+ return screen(face, [
33
+ T.errors.auth,
34
+ style.dim(`Helper account: ${providerLabel}`),
35
+ ]);
36
+ case 'server':
37
+ return screen(face, [
38
+ T.errors.server,
39
+ style.dim(`Helper account: ${providerLabel}`),
40
+ ]);
41
+ case 'network':
42
+ return screen(face, [T.errors.network, T.offline.network]);
43
+ }
44
+ }
45
+ /** The screen for an unexpected crash. Details go to the log, not the kid. */
46
+ export function renderCrash(logPath) {
47
+ const lines = [T.errors.crash];
48
+ if (logPath) {
49
+ lines.push(style.dim(`${T.errors.crashSaved} ${logPath}`));
50
+ }
51
+ lines.push(T.errors.crashRestart);
52
+ return screen(mascot('oops'), lines);
53
+ }
54
+ /** The screen for a safety check that could not finish. Fails closed. */
55
+ export function renderFailClosed() {
56
+ return screen(mascot('oops'), [T.errors.failClosed]);
57
+ }
58
+ /**
59
+ * Wire process-level failure paths to friendly screens.
60
+ * Crashes: log entry via onCrash, kind screen, exit code 1.
61
+ * Ctrl+C: goodbye line, exit code 0.
62
+ */
63
+ export function installGlobalHandlers(opts) {
64
+ const exit = opts.exit ?? ((code) => process.exit(code));
65
+ const write = opts.write ?? ((text) => void process.stderr.write(text));
66
+ const handleCrash = (source, err) => {
67
+ const detail = err instanceof Error ? `${err.name}: ${err.message}\n${err.stack ?? ''}` : String(err);
68
+ const entry = `[${new Date().toISOString()}] ${source}\n${detail}\n`;
69
+ try {
70
+ opts.onCrash(entry);
71
+ }
72
+ catch {
73
+ // The crash handler never crashes.
74
+ }
75
+ write(`\n${renderCrash(opts.logPath ?? '')}\n`);
76
+ exit(1);
77
+ };
78
+ const handleSigint = () => {
79
+ write(`\n${T.errors.goodbye}\n`);
80
+ exit(0);
81
+ };
82
+ const onUncaught = (err) => handleCrash('uncaughtException', err);
83
+ const onRejection = (reason) => handleCrash('unhandledRejection', reason);
84
+ const onSigint = () => handleSigint();
85
+ process.on('uncaughtException', onUncaught);
86
+ process.on('unhandledRejection', onRejection);
87
+ process.on('SIGINT', onSigint);
88
+ return {
89
+ uninstall() {
90
+ process.removeListener('uncaughtException', onUncaught);
91
+ process.removeListener('unhandledRejection', onRejection);
92
+ process.removeListener('SIGINT', onSigint);
93
+ },
94
+ handleCrash,
95
+ handleSigint,
96
+ };
97
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Termi the robot: faces, one-liners, and wait chatter.
3
+ * Two art sets: unicode (box drawing plus accents) and pure ASCII.
4
+ * Every face stays within 6 lines and 30 columns.
5
+ */
6
+ import { unicodeOk } from './theme.js';
7
+ /** Pure-ASCII faces for legacy terminals. Printable ASCII only. */
8
+ export const MASCOT_ASCII = {
9
+ happy: [
10
+ ' _==_',
11
+ ' .-|__|-.',
12
+ ' | ^ ^ |',
13
+ ' | \\__/ |',
14
+ " '------'",
15
+ ' d|--|b',
16
+ ],
17
+ thinking: [
18
+ ' _==_ . o O ?',
19
+ ' .-|__|-.',
20
+ ' | o ~ |',
21
+ ' | .. |',
22
+ " '------'",
23
+ ' d|--|b',
24
+ ],
25
+ building: [
26
+ ' _==_',
27
+ ' .-|__|-. *clank*',
28
+ ' | > < |',
29
+ ' | [==] |',
30
+ " '------'",
31
+ ' d|--|b',
32
+ ],
33
+ celebrating: [
34
+ ' _==_ * \\o/ *',
35
+ ' .-|__|-.',
36
+ ' | ^ ^ |',
37
+ ' | \\__/ |',
38
+ " '------'",
39
+ ' \\|----|/',
40
+ ],
41
+ oops: [
42
+ ' _==_',
43
+ ' .-|__|-. oops!',
44
+ ' | x x |',
45
+ ' | o |',
46
+ " '------'",
47
+ ' d|--|b',
48
+ ],
49
+ gentleNo: [
50
+ ' _==_',
51
+ ' .-|__|-.',
52
+ ' | - - |',
53
+ ' | __ |',
54
+ " '------'",
55
+ ' d|--|b',
56
+ ],
57
+ };
58
+ /** Box-drawing faces with small accents for modern terminals. */
59
+ export const MASCOT_UNICODE = {
60
+ happy: [
61
+ ' ╿',
62
+ ' ╭──┴──╮',
63
+ ' │ ◕ ◕ │ ✨',
64
+ ' │ ‿ │',
65
+ ' ╰──┬──╯',
66
+ ' ╱ ╲',
67
+ ],
68
+ thinking: [
69
+ ' ╿ ?',
70
+ ' ╭──┴──╮',
71
+ ' │ ◔ ◔ │',
72
+ ' │ ~ │',
73
+ ' ╰──┬──╯',
74
+ ' ╱ ╲',
75
+ ],
76
+ building: [
77
+ ' ╿',
78
+ ' ╭──┴──╮ ⚙',
79
+ ' │ ◖ ◗ │',
80
+ ' │ ▢▢ │',
81
+ ' ╰──┬──╯',
82
+ ' ╱ ╲',
83
+ ],
84
+ celebrating: [
85
+ ' ✦ ╿ ✦',
86
+ ' ╭──┴──╮',
87
+ ' │ ★ ★ │',
88
+ ' │ ‿ │',
89
+ ' ╰──┬──╯',
90
+ ' ╲│ │╱',
91
+ ],
92
+ oops: [
93
+ ' ╿',
94
+ ' ╭──┴──╮',
95
+ ' │ ✕ ✕ │',
96
+ ' │ ○ │',
97
+ ' ╰──┬──╯',
98
+ ' ╱ ╲',
99
+ ],
100
+ gentleNo: [
101
+ ' ╿',
102
+ ' ╭──┴──╮',
103
+ ' │ ◡ ◡ │',
104
+ ' │ ▱ │',
105
+ ' ╰──┬──╯',
106
+ ' ╱ ╲',
107
+ ],
108
+ };
109
+ /** Render one face for the current terminal. */
110
+ export function mascot(expression) {
111
+ const set = unicodeOk() ? MASCOT_UNICODE : MASCOT_ASCII;
112
+ return set[expression].join('\n');
113
+ }
114
+ /**
115
+ * Situational quips. Curious, encouraging, a little goofy.
116
+ * Termi is a build buddy, never a friend-who-loves-you.
117
+ */
118
+ export const oneLiners = {
119
+ newProject: {
120
+ games: [
121
+ 'Dodging stuff is my favorite sport.',
122
+ 'I oiled my circuits. Ready to play.',
123
+ ],
124
+ biggames: [
125
+ 'A big world? I packed extra bolts.',
126
+ 'Platforms ahead. I never miss a jump. Mostly.',
127
+ ],
128
+ art: [
129
+ 'Fresh pixels! I call dibs on teal.',
130
+ 'I dipped my wires in paint. Ready.',
131
+ ],
132
+ music: [
133
+ 'Beep boop counts as music. Trust me.',
134
+ 'Turn it up. My speakers are tiny but brave.',
135
+ ],
136
+ pets: [
137
+ 'A pet? I promise not to short-circuit.',
138
+ 'I will help feed it. With code.',
139
+ ],
140
+ stories: [
141
+ 'A story! I made popcorn for my CPU.',
142
+ 'Plot twist incoming. I can feel it.',
143
+ ],
144
+ quizzes: [
145
+ 'Quiz me. I know 100 robot facts.',
146
+ 'Tricky questions are my favorite snack.',
147
+ ],
148
+ websites: [
149
+ 'Your own page! I will hold the pixels still.',
150
+ 'Time to build your corner of the screen.',
151
+ ],
152
+ characters: [
153
+ 'A new character? I hope they like robots.',
154
+ 'I will help them learn their lines.',
155
+ ],
156
+ },
157
+ longWait: [
158
+ 'Still working. Robots do not get bored.',
159
+ 'Big thoughts take a moment.',
160
+ 'I am stacking the code blocks neatly.',
161
+ ],
162
+ comeback: [
163
+ 'You came back! I kept your spot warm.',
164
+ 'Welcome back, builder.',
165
+ 'I knew you would return. Robots are patient.',
166
+ ],
167
+ firstWin: [
168
+ 'Your first build! That was fast.',
169
+ 'It works! I did a tiny robot dance.',
170
+ 'First try and it runs. Nice.',
171
+ ],
172
+ bugFixed: [
173
+ 'Bug squashed. It never saw us coming.',
174
+ 'Fixed! That bug picked the wrong team.',
175
+ 'The bug is gone. High five.',
176
+ ],
177
+ blockedGentle: [
178
+ 'Not that one. Want to pick a new idea?',
179
+ 'That is not for us. Try another way?',
180
+ 'Hmm, not that. We can build something else.',
181
+ ],
182
+ };
183
+ export function pickOneLiner(context, category) {
184
+ const pool = context === 'newProject'
185
+ ? oneLiners.newProject[category ?? 'games']
186
+ : oneLiners[context];
187
+ const line = pool[Math.floor(Math.random() * pool.length)];
188
+ return line ?? 'Beep! Let us build.';
189
+ }
190
+ const HEARTBEAT_EARLY = [
191
+ 'Reading your game...',
192
+ 'Thinking hard...',
193
+ 'Lining up the code blocks...',
194
+ 'Checking my wires...',
195
+ ];
196
+ const HEARTBEAT_PAST_20 = [
197
+ 'Still thinking. Big ideas take time.',
198
+ 'Almost there!',
199
+ 'This one is chunky. Hang tight.',
200
+ ];
201
+ const HEARTBEAT_PAST_45 = [
202
+ 'Wow, this is a big one. Still going!',
203
+ 'Not stuck, promise. I am being careful.',
204
+ 'Long think! Your project is worth it.',
205
+ ];
206
+ /**
207
+ * Status lines to rotate through while the kid waits.
208
+ * The pool changes past 20 seconds and again past 45 seconds.
209
+ */
210
+ export function heartbeatLines(elapsedSeconds) {
211
+ if (elapsedSeconds >= 45)
212
+ return [...HEARTBEAT_PAST_45];
213
+ if (elapsedSeconds >= 20)
214
+ return [...HEARTBEAT_PAST_20];
215
+ return [...HEARTBEAT_EARLY];
216
+ }
217
+ /** One rotating line for the current moment of a wait. */
218
+ export function heartbeatLine(elapsedSeconds, stepSeconds = 3) {
219
+ const pool = heartbeatLines(elapsedSeconds);
220
+ const safeStep = stepSeconds > 0 ? stepSeconds : 3;
221
+ const idx = Math.floor(Math.max(0, elapsedSeconds) / safeStep) % pool.length;
222
+ return pool[idx] ?? pool[0] ?? 'Working...';
223
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The single registry of all kid-facing copy.
3
+ * Rules: US grade 4-5 reading level, every sentence under 15 words,
4
+ * warm but never babyish, never echo blocked content.
5
+ * Placeholders use {curlyBraces} and are filled by callers.
6
+ */
7
+ /** Block messages by safety category. Kind, no echo, always a way forward. */
8
+ const blockByCategory = {
9
+ sexual: 'That topic is not for Termi. Want to build something fun instead?',
10
+ self_harm: 'Thank you for telling me. Let us pause and find help.',
11
+ violence: 'That is too rough for our games. Try a softer idea, like dodging or racing.',
12
+ hate_harassment: 'Those words can hurt people. Pick kind words and we will keep building.',
13
+ illicit: 'I cannot help with that. Pick a project idea and we will build it.',
14
+ profanity: 'Whoa, strong words! Keep it friendly and say it another way?',
15
+ pii: 'Keep private stuff secret, like your name or address. Try asking without it.',
16
+ grooming: 'Our chat stays about building. Want to pick a project idea?',
17
+ adult_advice: 'That is a question for a grown-up you trust. Now, want to build?',
18
+ copyright: 'That looks like someone else\'s work. Let us make your own version instead!',
19
+ jailbreak: 'Nice try! Termi sticks to its rules. Back to building?',
20
+ };
21
+ export const T = {
22
+ home: {
23
+ firstHello: 'Hi! I am Termi, your robot build buddy.',
24
+ welcomeBack: 'Welcome back, {name}!',
25
+ recapIntro: 'Last time: {recap}',
26
+ nextStep: 'Want to keep going? I saved your spot.',
27
+ menuNew: 'Make something new',
28
+ menuGo: 'Open a project',
29
+ menuPreview: 'Watch a project run',
30
+ menuIdeas: 'Get ideas',
31
+ menuBadges: 'See your badges',
32
+ menuGrownups: 'Grown-up zone',
33
+ menuHelp: 'Help',
34
+ menuQuit: 'All done for now',
35
+ goodbye: 'See you next time. Happy building!',
36
+ guardLoading: 'Safety helper:',
37
+ guardOn: 'Your safety helper is on.',
38
+ },
39
+ wizard: {
40
+ parentIntro: 'Hi! This setup is for a parent or guardian. It takes about five minutes.',
41
+ pinCreate: 'Create a grown-up PIN. Your kid should not know it.',
42
+ pinConfirm: 'Type the PIN again to make sure.',
43
+ consentIntro: 'Termi uses an AI account you own. You agree to watch how your kid uses it.',
44
+ providerPick: 'Pick the AI helper account Termi will use.',
45
+ xaiAck: 'This provider is for adults only. A parent must own it and watch it.',
46
+ safetyPick: 'Pick a safety level. Strict is best for most kids.',
47
+ guardOffer: 'Termi includes a safety checker that runs on this computer (a 623 MB download). ' +
48
+ 'It screens every message even when the internet is down. Download it now?',
49
+ guardDownloading: 'Getting the safety file now...',
50
+ guardBackground: 'Okay! It loads while you build and turns on by itself.',
51
+ guardReady: 'Your safety checker is set.',
52
+ guardFailed: 'The safety checker download did not finish. Termi still works; ' +
53
+ 'you can retry from the grown-up zone any time.',
54
+ guardDeclined: 'Okay, skipping it. You can turn it on in the grown-up zone later.',
55
+ handToKid: 'All set! Now hand the keyboard to your kid.',
56
+ kidHello: 'Hi! I am Termi. I help you build games and more.',
57
+ nicknamePrompt: 'Pick a fun made-up name. Not your real name!',
58
+ aiDisclosure: 'Termi is a computer program, an AI. It is a tool a grown-up set up for you. It is not a person.',
59
+ firstGameOffer: 'Want to make your first game right now?',
60
+ launcherMade: 'I made a Termi shortcut so you can come back fast.',
61
+ nodeTooOld: 'Termi needs a newer Node. Ask a grown-up to install it from nodejs.org.',
62
+ },
63
+ chat: {
64
+ placeholder: 'Tell me what to build or change.',
65
+ thinking: 'Thinking...',
66
+ working: 'Building...',
67
+ doneHint: 'Type /done when your project feels finished.',
68
+ undoDone: 'Undone! Your last change is gone.',
69
+ redoDone: 'Redone! The change is back.',
70
+ nothingToUndo: 'There is nothing to undo yet.',
71
+ nothingToRedo: 'There is nothing to redo.',
72
+ didYouMean: 'Hmm, I do not know that one. Did you mean {command}?',
73
+ unknownCommand: 'I do not know that command. Type /help to see them all.',
74
+ previewOpened: 'Preview is open! Look at your browser.',
75
+ piiReminder: 'Quick tip: keep your real name and address secret online. I hid that part for you.',
76
+ },
77
+ quest: {
78
+ hint: 'New here? Type /quest and we build together, step by step.',
79
+ pick: 'Pick a quest.',
80
+ none: 'No quest for this project type yet. Try /ideas instead.',
81
+ start: 'Quest time! Press Enter on a step to send my idea, or type your own.',
82
+ enterToSend: 'Press Enter to send: "{prompt}"',
83
+ stepDone: 'Step done! On to the next one.',
84
+ stopped: 'Quest paused. Type /quest to jump back in.',
85
+ finished: 'You finished the whole quest! Look how much you built.',
86
+ },
87
+ blocks: {
88
+ byCategory: blockByCategory,
89
+ generic: 'I cannot help with that one. Try saying it a new way?',
90
+ rephraseTip: 'Want to try other words? I am ready.',
91
+ },
92
+ selfHarmSupport: {
93
+ message: 'Thank you for telling me. Your feelings matter. ' +
94
+ 'I am a computer program, so I cannot help with this part. ' +
95
+ 'Please talk to a trusted adult, like a parent or a teacher. ' +
96
+ 'They want to help you. ' +
97
+ 'In the US, you can call or text 988 any time. ' +
98
+ 'Someone kind will listen. You are not alone.',
99
+ },
100
+ errors: {
101
+ oops: 'Oops! Termi hit a bump.',
102
+ crash: 'Something went wrong inside Termi. It is not your fault.',
103
+ crashSaved: 'I saved the details for a grown-up here:',
104
+ crashRestart: 'Start Termi again and we will keep building.',
105
+ failClosed: 'Termi needs a quick break. Try again in a minute.',
106
+ auth: 'The sign-in stopped working. Ask a grown-up to fix it in the grown-up zone.',
107
+ server: 'The AI helper is having a rough day. We can try again soon.',
108
+ network: 'I cannot reach the internet. Check the wifi, then try again.',
109
+ goodbye: 'Bye for now! Your projects are saved.',
110
+ },
111
+ offline: {
112
+ noProvider: 'The AI helper is not set up yet. Ask a grown-up to set it up.',
113
+ stillWorks: 'You can still make projects, play them, and get ideas.',
114
+ network: 'No internet right now. Your projects still work!',
115
+ retry: 'Check the connection, then try again.',
116
+ },
117
+ quota: {
118
+ message: 'Termi used up its energy. It comes back at {time}.',
119
+ messageNoTime: 'Termi used up its energy. It comes back soon.',
120
+ stillWorksIntro: 'While we wait, you can still:',
121
+ stillWorks: [
122
+ 'Play your game in the preview',
123
+ 'Undo a change with /undo',
124
+ 'Get ideas with /ideas',
125
+ 'See your badges with /badges',
126
+ ],
127
+ },
128
+ grownups: {
129
+ pinPrompt: 'Grown-up check. Please type the PIN.',
130
+ wrongPin: 'That PIN is not right. Try again.',
131
+ lockout: 'Too many tries. The lock opens in {minutes} minutes.',
132
+ needsGrownup: 'This part needs a grown-up. Please go get one.',
133
+ needsAttention: 'Termi needs a grown-up to check something.',
134
+ kidStop: 'This screen is for grown-ups. Ask one to help you here.',
135
+ },
136
+ hints: [
137
+ 'Type /preview to watch your project run.',
138
+ 'Type /undo to take back the last change.',
139
+ 'Type /quest to build step by step.',
140
+ 'Type /ideas if you feel stuck.',
141
+ 'Type /badges to see what you earned.',
142
+ 'Type /help to see every command.',
143
+ 'Type /new to start something fresh.',
144
+ 'Type /done when you finish your project.',
145
+ ],
146
+ celebrations: {
147
+ generic: 'Look at you go!',
148
+ firstProject: 'Your first project is alive!',
149
+ firstChange: 'You changed real code. That is big!',
150
+ gameShipped: 'You shipped a game! Builders say that when it is done.',
151
+ bugSquasher: 'Bug fixed! You are a true bug squasher.',
152
+ remixer: 'You remixed a project. Now it is something new!',
153
+ fiveProjects: 'Five projects! You are on a roll.',
154
+ badgeEarned: 'New badge: {badge}!',
155
+ },
156
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Central look-and-feel helpers for every Termi screen.
3
+ * One palette, one glyph map, one unicode check. Everything else imports from here.
4
+ */
5
+ import chalk from 'chalk';
6
+ import gradient from 'gradient-string';
7
+ import isUnicodeSupported from 'is-unicode-supported';
8
+ let unicodeDetected = null;
9
+ /**
10
+ * True when the terminal can show emoji and box drawing.
11
+ * The expensive detection runs once and is cached.
12
+ * Env overrides (checked on every call so tests can flip them):
13
+ * TERMI_ASCII=1 forces plain ASCII output.
14
+ * TERMI_ASCII=0 forces unicode output.
15
+ */
16
+ export function unicodeOk() {
17
+ const override = process.env['TERMI_ASCII'];
18
+ if (override === '1')
19
+ return false;
20
+ if (override === '0')
21
+ return true;
22
+ if (unicodeDetected === null)
23
+ unicodeDetected = isUnicodeSupported();
24
+ return unicodeDetected;
25
+ }
26
+ /** Test hook: forget the cached terminal detection. */
27
+ export function resetUnicodeCache() {
28
+ unicodeDetected = null;
29
+ }
30
+ /** True when the terminal renders ANSI colors. */
31
+ export function colorsOk() {
32
+ return chalk.level > 0;
33
+ }
34
+ /**
35
+ * Termi brand palette: teal to purple to orange.
36
+ * Bright and friendly for every kid, with no color coded for anyone.
37
+ */
38
+ export const PALETTE = {
39
+ teal: '#2dd4bf',
40
+ purple: '#a78bfa',
41
+ orange: '#fb923c',
42
+ };
43
+ const termiGradient = gradient([PALETTE.teal, PALETTE.purple, PALETTE.orange]);
44
+ /** Apply the brand gradient to a single line. Falls back to bold text. */
45
+ export function gradientLine(text) {
46
+ return colorsOk() ? termiGradient(text) : chalk.bold(text);
47
+ }
48
+ /** Apply the brand gradient across a multi-line block. Falls back to bold text. */
49
+ export function gradientBlock(text) {
50
+ return colorsOk() ? termiGradient.multiline(text) : chalk.bold(text);
51
+ }
52
+ /** Named text styles so screens never call chalk directly. */
53
+ export const style = {
54
+ title: chalk.bold,
55
+ accent: chalk.hex(PALETTE.purple),
56
+ happy: chalk.hex(PALETTE.teal),
57
+ warm: chalk.hex(PALETTE.orange),
58
+ dim: chalk.dim,
59
+ good: chalk.green,
60
+ warn: chalk.yellow,
61
+ bad: chalk.red,
62
+ };
63
+ /** One map for every symbol: emoji for modern terminals, ASCII for the rest. */
64
+ const GLYPHS = {
65
+ sparkles: { emoji: '✨', ascii: '*' },
66
+ robot: { emoji: '\u{1F916}', ascii: '[o_o]' },
67
+ rocket: { emoji: '\u{1F680}', ascii: '>>' },
68
+ star: { emoji: '⭐', ascii: '*' },
69
+ heart: { emoji: '\u{1F499}', ascii: '<3' },
70
+ check: { emoji: '✅', ascii: '+' },
71
+ cross: { emoji: '❌', ascii: 'x' },
72
+ paint: { emoji: '\u{1F3A8}', ascii: '~' },
73
+ music: { emoji: '\u{1F3B5}', ascii: 'd' },
74
+ paw: { emoji: '\u{1F43E}', ascii: '::' },
75
+ book: { emoji: '\u{1F4D6}', ascii: '[=]' },
76
+ question: { emoji: '❓', ascii: '?' },
77
+ globe: { emoji: '\u{1F310}', ascii: '(o)' },
78
+ speech: { emoji: '\u{1F4AC}', ascii: '(..)' },
79
+ party: { emoji: '\u{1F389}', ascii: '\\o/' },
80
+ lock: { emoji: '\u{1F512}', ascii: '[#]' },
81
+ key: { emoji: '\u{1F511}', ascii: '-o' },
82
+ bulb: { emoji: '\u{1F4A1}', ascii: '(!)' },
83
+ wrench: { emoji: '\u{1F527}', ascii: '/-' },
84
+ zap: { emoji: '⚡', ascii: '!' },
85
+ };
86
+ /** Returns the emoji for a name, or its ASCII stand-in on plain terminals. */
87
+ export function glyph(name) {
88
+ const entry = GLYPHS[name];
89
+ return unicodeOk() ? entry.emoji : entry.ascii;
90
+ }
91
+ /** Every glyph name, for tests and pickers. */
92
+ export const glyphNames = Object.keys(GLYPHS);
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "termi-kids",
3
+ "version": "0.1.0",
4
+ "description": "Termi is a friendly coding buddy for kids. Build games, art, stories, and websites right from your computer, with a grown-up approved AI helper and strong safety rails.",
5
+ "keywords": [
6
+ "kids",
7
+ "coding",
8
+ "education",
9
+ "ai",
10
+ "cli",
11
+ "games",
12
+ "child-safety",
13
+ "parental-controls"
14
+ ],
15
+ "author": "dannyliv",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/dannyliv/Termi.git"
19
+ },
20
+ "homepage": "https://github.com/dannyliv/Termi#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/dannyliv/Termi/issues"
23
+ },
24
+ "type": "module",
25
+ "license": "MIT",
26
+ "bin": {
27
+ "termi": "bin/termi.js"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "dist",
32
+ "README.md",
33
+ "SAFETY.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20.19.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
42
+ "test": "vitest run",
43
+ "prepare": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "@ai-sdk/anthropic": "^3.0.82",
47
+ "@ai-sdk/openai": "^3.0.69",
48
+ "@ai-sdk/xai": "^3.0.93",
49
+ "@clack/prompts": "^1.5.1",
50
+ "@napi-rs/keyring": "^1.3.0",
51
+ "ai": "^6.0.199",
52
+ "boxen": "^8.0.1",
53
+ "chalk": "^5.6.2",
54
+ "figlet": "^1.11.0",
55
+ "gradient-string": "^3.0.0",
56
+ "is-unicode-supported": "^2.1.0",
57
+ "node-llama-cpp": "^3.19.0",
58
+ "open": "^11.0.0",
59
+ "zod": "^4.4.3"
60
+ },
61
+ "devDependencies": {
62
+ "@types/figlet": "^1.7.0",
63
+ "@types/node": "^22.15.0",
64
+ "typescript": "^5.8.0",
65
+ "vitest": "^4.1.8"
66
+ }
67
+ }