termi-kids 0.1.2 → 0.2.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.
package/README.md CHANGED
@@ -10,17 +10,15 @@
10
10
 
11
11
  # Termi
12
12
 
13
- Termi is a friendly robot that helps you build things on a real computer.
13
+ Termi is a friendly robot that helps you build browser games on a real computer.
14
14
 
15
- You type what you want. Termi writes the code with you. Your game opens in the browser right away.
15
+ You pick a game idea (or invent your own). You write a prompt, or ask Termi to help write one. Termi builds the game with you. Your game opens in the browser and updates after every prompt.
16
16
 
17
- You can make dodge games, platform games, pixel art, dance music, virtual pets, adventure stories, quizzes, your own web page, and talking characters. If you change your mind, type `undo` and the last change is gone. When something works, you earn badges.
17
+ There are 30 ready game ideas plus "Build my own idea". When you finish, Termi can run a short test pass and suggest one last improvement. Games save in your local library on this computer.
18
18
 
19
- Termi is a computer program, an AI tool. It is not a person. A grown-up sets it up first.
20
-
21
- Not sure what to say first? Type `/quest` inside any project. Termi walks you through a real build, step by step, with a ready idea at every step. Press Enter to use it, or type your own. Finish a quest and you earn the Quest Hero badge.
19
+ Want to get better at prompting? Open **Learn AI**. Six short lessons teach you how to boss your AI helper. Each one earns a badge, and you can replay them any time.
22
20
 
23
- Want to get good at building with AI? Type `termi learn`. Six short lessons teach you how to be the boss of your AI helper. Each one earns a badge, and you can replay them any time.
21
+ Termi is a computer program, an AI tool. It is not a person. A grown-up sets it up first.
24
22
 
25
23
  Ready? Ask a grown-up to read the next part.
26
24
 
@@ -37,7 +35,7 @@ Please read [SAFETY.md](SAFETY.md) before handing the keyboard to your kid. It e
37
35
  - Node.js 20.19 or newer: https://nodejs.org
38
36
  - macOS, Windows, or Linux
39
37
  - An AI account you own (one of: a ChatGPT account, a Claude API key, an OpenAI API key, or a Grok API key)
40
- - About 700 MB of free disk space for the on-device safety checker (recommended, on by default)
38
+ - About 700 MB of free disk space for the on-device safety checker (installed during setup)
41
39
 
42
40
  If Node is too old, Termi prints a plain message with the download link instead of starting.
43
41
 
@@ -58,16 +56,14 @@ To update later, run `termi update` (or `npm install -g termi-kids@latest`). On
58
56
  Run `termi`. The first run starts a setup wizard for a parent or guardian. It takes about five minutes:
59
57
 
60
58
  1. **Create a grown-up PIN** (at least 4 characters). It guards the grown-up zone. Your kid should not know it.
61
- 2. **Age band and consent.** You pick "Under 13" or "13 or older" and confirm that Termi uses an AI account you own and that you agree to watch how your kid uses it. This consent is recorded in the local safety log.
59
+ 2. **Consent.** You confirm that Termi uses an AI account you own and that you agree to watch how your kid uses it. One safety bar applies for every age (no under-13 / over-13 split). Consent is recorded in the local safety log.
62
60
  3. **Pick an AI provider.** The default is "ChatGPT sign-in": your browser opens, you sign in to your own ChatGPT account, and no API key is needed. The other options are a Claude API key, an OpenAI API key, or a Grok API key. Grok requires an extra confirmation because the xAI API terms are adults-only (details in SAFETY.md). You can add several providers and choose which one is active.
63
- 4. **Safety checker download.** Termi offers a small safety model (623 MB) that screens every message right on your computer, even with no internet. It is on by default and worth keeping. While it downloads, Termi tells you plainly that basic safety (the local filter plus the online checks) is already running, and asks whether you want to start building now or wait and watch the progress bar. Either way, the checker turns itself on the moment the verified file lands and Termi says so, even mid-session. The download is anonymous (no account or token needed) and resumes if interrupted. Details are in [SAFETY.md](SAFETY.md).
61
+ 4. **Safety checker install.** Setup always starts the on-device safety model download (about 623 MB). It screens every message on this computer, even offline. You can wait for the progress bar or keep setting up while it finishes in the background. Basic safety is already on during the download. The download is anonymous and resumes if interrupted. Details are in [SAFETY.md](SAFETY.md).
64
62
  5. **Hand the keyboard to your kid.** The kid picks a made-up nickname (Termi asks them not to use their real name) and sees a clear disclosure: "Termi is a computer program, an AI. It is a tool a grown-up set up for you. It is not a person."
65
63
  6. **Desktop shortcut.** Termi offers to write a double-clickable launcher on the Desktop (`Termi.command` on macOS, `Termi.bat` on Windows, `Termi.desktop` on Linux) so your kid can come back tomorrow without typing commands.
66
- 7. **Optional first game.** Two keypresses later there is a running game in the browser.
67
-
68
- If you skip the provider step, Termi runs in offline mode: your kid can still create projects, play them in the preview, undo changes, and browse ideas. The chat shows a kind "ask a grown-up to set up the AI helper" screen.
64
+ 7. **Optional first game.** The kid can jump straight into **Build a game** (pick an idea or invent one).
69
65
 
70
- **If your kid does not know where to start:** every project type has a Build Quest (`/quest` in the chat), a five-step guided build with a ready prompt at each step, and a large idea deck (`/ideas`). Quest steps go through the same safety pipeline as any other message.
66
+ If you skip the provider step, Termi runs in offline mode: your kid can still open the library and Learn AI. Building with the AI helper needs a provider.
71
67
 
72
68
  ### What keeps your kid safe
73
69
 
@@ -87,13 +83,13 @@ Parents get a PIN-gated grown-up zone, a tamper-evident log of every block and s
87
83
 
88
84
  | Command | What it does |
89
85
  |---|---|
90
- | `termi` | First run: setup wizard. After that: the home menu. |
91
- | `termi new` | Start a new project. |
92
- | `termi go [name]` | Open a project and build. With no name, pick from a list. |
93
- | `termi preview [name]` | Open a project in the browser without the chat. |
94
- | `termi ideas` | Print fun project ideas. |
95
- | `termi learn` | Six short, replayable lessons on building with AI. Fully offline, no AI calls. |
86
+ | `termi` | First run: setup wizard. After that: home (Build a game, My games, Learn AI). |
87
+ | `termi new` | Build a game (idea list + prompt loop). |
88
+ | `termi go [name]` | Open a game from your library. With no name, pick from a list. |
89
+ | `termi preview [name]` | Open a game in the browser without the chat. |
90
+ | `termi learn` | Learn AI: six short offline lessons on prompting. |
96
91
  | `termi grownups` | The grown-up zone. PIN required. |
92
+ | `termi update` | Update Termi to the latest npm version. |
97
93
  | `termi help` | Show the command list. |
98
94
  | `termi --version` | Show the version. |
99
95
 
package/SAFETY.md CHANGED
@@ -50,7 +50,7 @@ Grooming, personal info, and rule-breaking tricks block at a lower threshold tha
50
50
 
51
51
  Termi includes an on-device safety checker: a small AI model (Qwen3Guard 0.6B, published by the Qwen team under the Apache 2.0 license) that runs entirely on your computer. Setup offers the download (623 MB) with a yes as the default; you can decline and add it later from the grown-up zone (`termi grownups`, then **Safety checker**).
52
52
 
53
- The download never blocks anyone. Setup says, in plain words, that basic safety (the local filter plus the online checks) is already on, then asks whether you want to start building now or wait and watch the progress bar. If you start now, the download continues in the background while your kid builds, the home menu shows its bar, and the checker attaches itself to the running safety pipeline the moment the verified file is in place; Termi announces it on screen ("Your safety helper is on."), even in the middle of a build session. If the download is interrupted (a closed laptop, a dropped connection), it resumes from where it stopped the next time Termi starts. Until the file lands, the online checks carry the load exactly as they would with the checker turned off.
53
+ Setup always starts the download (it is part of install, not an easy skip). Setup says, in plain words, that basic safety (the local filter plus the online checks) is already on, then asks whether you want to wait and watch the progress bar or keep setting up while it finishes. Either way the download continues, the home menu can show its bar, and the checker attaches itself to the running safety pipeline the moment the verified file is in place; Termi announces it on screen ("Your safety helper is on."), even in the middle of a build session. If the download is interrupted (a closed laptop, a dropped connection), it resumes from where it stopped the next time Termi starts. Until the file lands, the online checks carry the load.
54
54
 
55
55
  The download itself is anonymous. The model file comes over HTTPS from a public model repository, with no account, sign-in, or token of any kind; nothing about you or your kid is sent with the request, and the file is checked against a pinned cryptographic fingerprint before it is used.
56
56
 
package/dist/cli.js CHANGED
@@ -19,7 +19,6 @@ import { installGlobalHandlers, renderCrash } from './ui/errors.js';
19
19
  import { mascot } from './ui/mascot.js';
20
20
  import { style } from './ui/theme.js';
21
21
  import { T } from './ui/text.js';
22
- import { executeIdeas } from './surfaces/commands.js';
23
22
  /** Pure boot branch logic: settings state in, actions out. */
24
23
  export function decideBoot(state) {
25
24
  return {
@@ -32,11 +31,10 @@ export function cliHelp() {
32
31
  return [
33
32
  'How to use Termi:',
34
33
  ' termi open the home menu',
35
- ' termi new start a new project',
36
- ' termi go [name] open a project and build',
37
- ' termi preview watch a project run',
38
- ' termi ideas get fun ideas',
39
- ' termi learn play six short lessons about AI',
34
+ ' termi new build a game',
35
+ ' termi go [name] open a game from your library',
36
+ ' termi preview watch a game run',
37
+ ' termi learn Learn AI (prompting lessons)',
40
38
  ' termi grownups grown-up zone (PIN needed)',
41
39
  ' termi update update Termi to the latest version',
42
40
  ' termi help show this help',
@@ -124,24 +122,6 @@ async function routePreview(nameArg, settings) {
124
122
  // Held open on purpose; Ctrl+C ends it through the global handler.
125
123
  });
126
124
  }
127
- async function routeIdeas(settings) {
128
- let scaffoldId = 'games';
129
- if (settings.lastProjectSlug !== null) {
130
- try {
131
- const store = await import('./projects/store.js');
132
- const last = store.openProject(settings.lastProjectSlug);
133
- if (last !== null) {
134
- scaffoldId = last.meta.scaffoldId;
135
- }
136
- }
137
- catch {
138
- // Fall back to game ideas.
139
- }
140
- }
141
- await executeIdeas(scaffoldId, (text) => {
142
- console.log(text);
143
- });
144
- }
145
125
  async function route(command, rest, settings) {
146
126
  const home = await import('./surfaces/home.js');
147
127
  switch (command) {
@@ -150,10 +130,8 @@ async function route(command, rest, settings) {
150
130
  return;
151
131
  }
152
132
  case 'new': {
153
- const project = await home.runNewProject(settings);
154
- if (project !== null) {
155
- await home.openChatLoop(project, settings);
156
- }
133
+ const build = await import('./surfaces/buildGame.js');
134
+ await build.runBuildGame(settings);
157
135
  return;
158
136
  }
159
137
  case 'go': {
@@ -168,7 +146,9 @@ async function route(command, rest, settings) {
168
146
  return;
169
147
  }
170
148
  case 'ideas': {
171
- await routeIdeas(settings);
149
+ // Ideas live inside Build a game now.
150
+ const build = await import('./surfaces/buildGame.js');
151
+ await build.runBuildGame(settings);
172
152
  return;
173
153
  }
174
154
  case 'learn': {
@@ -164,7 +164,7 @@ export async function runLesson(lesson, say = defaultSay) {
164
164
  */
165
165
  export async function runLearnMenu() {
166
166
  console.log(mascot('happy'));
167
- console.log('Welcome to Termi school! Six tiny lessons, zero homework.');
167
+ console.log('Learn AI: six tiny lessons on how to prompt. Zero homework.');
168
168
  for (;;) {
169
169
  const done = new Set(loadProgress().completed);
170
170
  const options = [
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Minimal blank game project: no stock playable game, just a local HTML
3
+ * shell the model fills in. Used by the Build a game flow.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { projectsDir } from '../config/paths.js';
8
+ import { slugifyName } from './create.js';
9
+ import { notesFileName, openProject, renderTermiMd, saveProjectMeta, } from './store.js';
10
+ const BLANK_HTML = `<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>__TITLE__</title>
16
+ <link rel="stylesheet" href="style.css" />
17
+ </head>
18
+ <body>
19
+ <main>
20
+ <h1 id="title">__TITLE__</h1>
21
+ <p id="hint">Your game will show up here after you build with Termi.</p>
22
+ <canvas id="game" width="480" height="320" aria-label="game"></canvas>
23
+ </main>
24
+ <script src="game.js"></script>
25
+ </body>
26
+ </html>
27
+ `;
28
+ const BLANK_CSS = `* { box-sizing: border-box; }
29
+ body {
30
+ margin: 0;
31
+ font-family: system-ui, sans-serif;
32
+ background: #0f172a;
33
+ color: #e2e8f0;
34
+ min-height: 100vh;
35
+ display: grid;
36
+ place-items: center;
37
+ }
38
+ main { text-align: center; padding: 1rem; }
39
+ h1 { font-size: 1.4rem; margin: 0 0 0.5rem; }
40
+ #hint { opacity: 0.8; margin-bottom: 1rem; }
41
+ canvas {
42
+ background: #1e293b;
43
+ border: 2px solid #38bdf8;
44
+ border-radius: 12px;
45
+ max-width: 100%;
46
+ }
47
+ `;
48
+ const BLANK_JS = `// Blank starter. Termi and the kid fill this in.
49
+ const canvas = document.getElementById('game');
50
+ const ctx = canvas.getContext('2d');
51
+ function frame() {
52
+ ctx.fillStyle = '#1e293b';
53
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
54
+ ctx.fillStyle = '#94a3b8';
55
+ ctx.font = '16px system-ui, sans-serif';
56
+ ctx.textAlign = 'center';
57
+ ctx.fillText('Ready to build!', canvas.width / 2, canvas.height / 2);
58
+ }
59
+ frame();
60
+ `;
61
+ function writeRel(dir, relPath, content) {
62
+ const target = path.join(dir, ...relPath.split('/'));
63
+ fs.mkdirSync(path.dirname(target), { recursive: true });
64
+ fs.writeFileSync(target, content, 'utf8');
65
+ }
66
+ /**
67
+ * Creates an empty local-browser game shell under the projects library.
68
+ * scaffoldId stays "games" so existing badges and quests still recognize it.
69
+ */
70
+ export function createBlankGameProject(prettyNameInput, ideaLabel = 'My game') {
71
+ const trimmed = prettyNameInput.trim();
72
+ const prettyName = trimmed.length > 0 ? trimmed : 'My Game';
73
+ const { slug } = slugifyName(prettyName);
74
+ const dir = path.join(projectsDir(), slug);
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ const html = BLANK_HTML.replaceAll('__TITLE__', prettyName);
77
+ writeRel(dir, 'index.html', html);
78
+ writeRel(dir, 'style.css', BLANK_CSS);
79
+ writeRel(dir, 'game.js', BLANK_JS);
80
+ writeRel(dir, notesFileName, renderTermiMd(prettyName, {
81
+ whatThisIs: `A browser game: ${ideaLabel}. Built with Termi.`,
82
+ files: ['index.html', 'style.css', 'game.js'],
83
+ builtSoFar: ['Blank game shell ready for the first prompt'],
84
+ recapLine: `We started ${prettyName}.`,
85
+ }));
86
+ const now = new Date().toISOString();
87
+ saveProjectMeta({
88
+ slug,
89
+ prettyName,
90
+ scaffoldId: 'games',
91
+ themeId: 'blank',
92
+ createdAt: now,
93
+ lastOpenedAt: now,
94
+ });
95
+ const project = openProject(slug);
96
+ if (project === null) {
97
+ throw new Error('Something went wrong while making the project. Try again.');
98
+ }
99
+ return project;
100
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Kid-facing Build-a-game idea bank.
3
+ *
4
+ * Exactly 31 entries: "Build my own idea" first, then 30 browser HTML games.
5
+ * Every idea is local-preview only (HTML/CSS/JS), no image generation, no
6
+ * native installs. Pure data for tests and the Build a game menu.
7
+ */
8
+ /** True for the custom entry that asks the kid for their own concept. */
9
+ export function isOwnIdea(idea) {
10
+ return idea.id === 'own';
11
+ }
12
+ /**
13
+ * Menu order is fixed. First entry is always Build my own idea.
14
+ * The remaining 30 are distinct local-browser game concepts.
15
+ */
16
+ export const GAME_IDEAS = [
17
+ {
18
+ id: 'own',
19
+ label: 'Build my own idea',
20
+ blurb: 'You invent the game. Termi helps you build it.',
21
+ seedPrompt: '',
22
+ },
23
+ {
24
+ id: 'dodge-rain',
25
+ label: 'Dodge the rain',
26
+ blurb: 'Move left and right. Catch drops or dodge them.',
27
+ seedPrompt: 'Make a simple canvas game where I move left and right to dodge falling rain drops. Add a score and a restart key.',
28
+ },
29
+ {
30
+ id: 'catch-stars',
31
+ label: 'Catch the stars',
32
+ blurb: 'Catch falling stars before they hit the ground.',
33
+ seedPrompt: 'Make a canvas catch game. Stars fall from the top. I move a basket to catch them. Show the score and lives.',
34
+ },
35
+ {
36
+ id: 'memory-cards',
37
+ label: 'Memory cards',
38
+ blurb: 'Flip cards and match pairs.',
39
+ seedPrompt: 'Make a memory match card game with 12 cards (6 pairs) using emoji. Click to flip. Count moves. Show a win message.',
40
+ },
41
+ {
42
+ id: 'click-targets',
43
+ label: 'Click the targets',
44
+ blurb: 'Click circles before they vanish.',
45
+ seedPrompt: 'Make a click target game. Circles appear for a short time. Click them for points. Add a 30 second timer and a final score.',
46
+ },
47
+ {
48
+ id: 'maze-escape',
49
+ label: 'Maze escape',
50
+ blurb: 'Find the exit in a simple maze.',
51
+ seedPrompt: 'Make a keyboard maze game on a canvas grid. Arrow keys move a square. Reach the green exit. Keep walls solid.',
52
+ },
53
+ {
54
+ id: 'snake-lite',
55
+ label: 'Snake lite',
56
+ blurb: 'Grow a snake by eating dots.',
57
+ seedPrompt: 'Make a simple snake game on a grid. Arrow keys move. Eat food to grow. Hitting a wall or yourself restarts.',
58
+ },
59
+ {
60
+ id: 'pong-solo',
61
+ label: 'Pong solo',
62
+ blurb: 'Bounce a ball with one paddle.',
63
+ seedPrompt: 'Make a one player pong game. Move the paddle with the mouse or arrows. Ball bounces. Score points. Restart on miss.',
64
+ },
65
+ {
66
+ id: 'whack-moles',
67
+ label: 'Whack a mole',
68
+ blurb: 'Tap moles as they pop up.',
69
+ seedPrompt: 'Make a whack a mole game with a 3 by 3 grid of holes. Moles pop up briefly. Click them for points. Add a timer.',
70
+ },
71
+ {
72
+ id: 'color-match',
73
+ label: 'Color match',
74
+ blurb: 'Match the word color to the right button.',
75
+ seedPrompt: 'Make a color word game. Show a color name in a random ink color. Kid clicks the button that matches the WORD meaning. Score streaks.',
76
+ },
77
+ {
78
+ id: 'typing-race',
79
+ label: 'Typing race',
80
+ blurb: 'Type the word before time runs out.',
81
+ seedPrompt: 'Make a typing race. Show a simple word. Kid types it and presses Enter. Track speed and accuracy for 10 words.',
82
+ },
83
+ {
84
+ id: 'platform-jump',
85
+ label: 'Platform jumper',
86
+ blurb: 'Jump across platforms to the flag.',
87
+ seedPrompt: 'Make a side view platformer on canvas. Arrow keys move and jump. Reach a flag on the right. Simple gravity and platforms.',
88
+ },
89
+ {
90
+ id: 'balloon-pop',
91
+ label: 'Balloon pop',
92
+ blurb: 'Pop balloons before they float away.',
93
+ seedPrompt: 'Make a balloon pop game. Balloons float up. Click to pop them. Missed balloons cost a life. Show score.',
94
+ },
95
+ {
96
+ id: 'quiz-sprint',
97
+ label: 'Quiz sprint',
98
+ blurb: 'Answer fun multiple choice questions.',
99
+ seedPrompt: 'Make a 5 question multiple choice quiz about animals. Show score at the end. Use big clear buttons.',
100
+ },
101
+ {
102
+ id: 'simon-says',
103
+ label: 'Simon says lights',
104
+ blurb: 'Repeat the light pattern.',
105
+ seedPrompt: 'Make a Simon style memory game with 4 colored buttons. Play a growing pattern. Kid repeats it. Score the longest streak.',
106
+ },
107
+ {
108
+ id: 'rock-paper',
109
+ label: 'Rock paper scissors',
110
+ blurb: 'Play against the computer. Pick rock, paper, or scissors.',
111
+ seedPrompt: 'Make rock paper scissors with three big buttons. Computer picks at random. Track wins, losses, and ties.',
112
+ },
113
+ {
114
+ id: 'number-guess',
115
+ label: 'Number guess',
116
+ blurb: 'Guess the secret number.',
117
+ seedPrompt: 'Make a number guess game from 1 to 50. Kid types a guess. Say higher or lower. Count tries until they win.',
118
+ },
119
+ {
120
+ id: 'breakout-lite',
121
+ label: 'Breakout lite',
122
+ blurb: 'Bounce a ball to break bricks.',
123
+ seedPrompt: 'Make a simple breakout game. Paddle at the bottom, ball, and one row of bricks. Clear the bricks to win.',
124
+ },
125
+ {
126
+ id: 'flappy-lite',
127
+ label: 'Flappy lite',
128
+ blurb: 'Tap to flap between gaps.',
129
+ seedPrompt: 'Make a flappy style game. Space or click flaps up. Gravity pulls down. Fly through gaps in pipes. Score each pipe.',
130
+ },
131
+ {
132
+ id: 'tic-tac-toe',
133
+ label: 'Tic tac toe',
134
+ blurb: 'Play X and O on a 3 by 3 board.',
135
+ seedPrompt: 'Make tic tac toe for two players on one keyboard. Click cells. Detect win and draw. Add a new game button.',
136
+ },
137
+ {
138
+ id: 'collect-coins',
139
+ label: 'Collect coins',
140
+ blurb: 'Run around and grab all the coins.',
141
+ seedPrompt: 'Make a top down collect game. Arrow keys move a player. Coins sit on a small map. Collect all coins to win.',
142
+ },
143
+ {
144
+ id: 'avoid-cars',
145
+ label: 'Avoid the cars',
146
+ blurb: 'Cross the road safely.',
147
+ seedPrompt: 'Make a cross the road game. Player moves up. Cars move left and right. Reach the top without hitting a car.',
148
+ },
149
+ {
150
+ id: 'reaction-test',
151
+ label: 'Reaction test',
152
+ blurb: 'Click when the screen turns green.',
153
+ seedPrompt: 'Make a reaction time test. Screen starts red, then turns green after a random wait. Click as fast as you can. Show ms time.',
154
+ },
155
+ {
156
+ id: 'word-scramble',
157
+ label: 'Word scramble',
158
+ blurb: 'Unscramble the letters.',
159
+ seedPrompt: 'Make a word scramble game with 8 easy words. Show mixed letters. Kid types the word. Track score out of 8.',
160
+ },
161
+ {
162
+ id: 'tower-stack',
163
+ label: 'Tower stack',
164
+ blurb: 'Stack blocks as high as you can.',
165
+ seedPrompt: 'Make a stack game. A block slides left and right. Click or press space to drop it. Stack as high as you can without missing.',
166
+ },
167
+ {
168
+ id: 'balloon-math',
169
+ label: 'Balloon math',
170
+ blurb: 'Pop the balloon with the right answer.',
171
+ seedPrompt: 'Make a math balloon game. Show a simple add or subtract problem. Three balloons show answers. Click the correct one.',
172
+ },
173
+ {
174
+ id: 'day-night',
175
+ label: 'Day and night runner',
176
+ blurb: 'Run while the world switches day and night.',
177
+ seedPrompt: 'Make a runner where the player auto runs right. Jump over rocks. Every 10 seconds switch day and night colors. Track distance.',
178
+ },
179
+ {
180
+ id: 'fruit-slice',
181
+ label: 'Fruit slice',
182
+ blurb: 'Swipe or click fruit, avoid bombs.',
183
+ seedPrompt: 'Make a fruit slice style game. Fruit flies up. Click fruit for points. Clicking a bomb ends the round. Show score.',
184
+ },
185
+ {
186
+ id: 'hide-seek',
187
+ label: 'Hide and seek dots',
188
+ blurb: 'Find the odd color in the crowd.',
189
+ seedPrompt: 'Make a find the odd one out game. Show many colored dots. One is a slightly different color. Click it. Levels get harder.',
190
+ },
191
+ {
192
+ id: 'space-lander',
193
+ label: 'Soft lander',
194
+ blurb: 'Land the ship gently on the pad.',
195
+ seedPrompt: 'Make a lander game. Ship falls with gravity. Arrow keys thrust and steer. Land softly on a pad. Crash if too fast.',
196
+ },
197
+ {
198
+ id: 'story-choice',
199
+ label: 'Choice adventure',
200
+ blurb: 'A short story with choices you click.',
201
+ seedPrompt: 'Make a short click adventure with 4 scenes. Each scene has text and 2 choice buttons. End with a happy finish screen.',
202
+ },
203
+ ];
204
+ /** Exactly one own-idea entry plus 30 game ideas. */
205
+ export const GAME_IDEA_COUNT = GAME_IDEAS.length;
206
+ /** Menu options for selects: value is idea id. */
207
+ export function gameIdeaMenuOptions() {
208
+ return GAME_IDEAS.map((idea) => ({
209
+ value: idea.id,
210
+ label: idea.label,
211
+ hint: idea.blurb,
212
+ }));
213
+ }
214
+ export function gameIdeaById(id) {
215
+ return GAME_IDEAS.find((idea) => idea.id === id);
216
+ }
@@ -16,7 +16,6 @@ import { appendAudit } from '../safety/audit.js';
16
16
  import { ensureGuardFetch, guardProgressBar } from '../safety/guarddownload.js';
17
17
  import { guardModelReady } from '../safety/modelstore.js';
18
18
  import { nameIsOkay } from '../safety/prefilter.js';
19
- import { scaffoldById } from '../projects/scaffolds/index.js';
20
19
  import { renderBanner } from '../ui/banner.js';
21
20
  import { mascot } from '../ui/mascot.js';
22
21
  import { style } from '../ui/theme.js';
@@ -262,14 +261,7 @@ async function createPinStep() {
262
261
  }
263
262
  }
264
263
  async function consentStep(settings) {
265
- const ageBand = ensure(await p.select({
266
- message: 'How old is your kid?',
267
- options: [
268
- { value: 'under13', label: 'Under 13' },
269
- { value: 'teen', label: '13 or older' },
270
- ],
271
- initialValue: 'under13',
272
- }));
264
+ // One safety bar for every age. No under-13 / over-13 split.
273
265
  const agreed = ensure(await p.confirm({ message: `${T.wizard.consentIntro} Do you agree?`, initialValue: true }));
274
266
  if (!agreed) {
275
267
  bail();
@@ -280,13 +272,13 @@ async function consentStep(settings) {
280
272
  ts: attestedAt,
281
273
  layer: 'system',
282
274
  event: 'consent',
283
- excerpt: `parent consent, age band ${ageBand}`,
275
+ excerpt: 'parent consent, one safety bar for all ages',
284
276
  });
285
277
  }
286
278
  catch {
287
279
  // Consent still counts; the audit line is best effort.
288
280
  }
289
- return { ...settings, ageBand, consentAttestedAt: attestedAt };
281
+ return { ...settings, ageBand: 'under13', consentAttestedAt: attestedAt };
290
282
  }
291
283
  async function providerLoop(settings) {
292
284
  let current = settings;
@@ -318,41 +310,27 @@ async function providerLoop(settings) {
318
310
  return { ...current, activeProvider: null };
319
311
  }
320
312
  /**
321
- * Offers the on-device safety checker download. Default is yes: the
322
- * classifier setting ships on, and this step starts fetching its model file
323
- * in the background. The parent hears, plainly, that basic safety (the
324
- * local filter plus the online checks) is already on and that the checker
325
- * strengthens it when the download lands, then chooses to start building
326
- * now or wait and watch the bar. Either way the pipeline hot-attaches the
327
- * checker the moment the verified file is in place. Declining turns the
328
- * setting off; a failed or interrupted download resumes on the next start.
313
+ * Installs the on-device safety checker as part of setup. Always on: the
314
+ * model download starts here (and resumes on later boots). Parent can wait
315
+ * for the bar or keep going; declining is not offered.
329
316
  */
330
317
  async function localGuardStep(settings) {
331
318
  if (guardModelReady()) {
332
- // Model already present: keep whatever the parent chose before. A
333
- // wizard re-run must not silently flip a deliberate off back to on.
334
- return settings;
335
- }
336
- const wants = ensure(await p.confirm({ message: T.wizard.guardOffer, initialValue: true }));
337
- if (!wants) {
338
- p.log.info(T.wizard.guardDeclined);
339
- audit('settings_change', 'local classifier off (declined in setup)');
340
- return { ...settings, localClassifier: false };
319
+ return { ...settings, localClassifier: true };
341
320
  }
321
+ p.log.info(T.wizard.guardOffer);
342
322
  const fetchDone = ensureGuardFetch();
343
323
  p.log.info(T.wizard.guardBackground);
324
+ audit('settings_change', 'local classifier install started in setup');
344
325
  const wait = ensure(await p.select({
345
326
  message: T.wizard.guardWaitPick,
346
327
  options: [
347
- { value: 'now', label: T.wizard.guardStartNow, hint: T.wizard.guardStartNowHint },
348
328
  { value: 'wait', label: T.wizard.guardWaitHere, hint: T.wizard.guardWaitHereHint },
329
+ { value: 'now', label: T.wizard.guardStartNow, hint: T.wizard.guardStartNowHint },
349
330
  ],
350
- initialValue: 'now',
331
+ initialValue: 'wait',
351
332
  }));
352
333
  if (wait === 'wait') {
353
- // The wait shows a live bar with periodic escape hatches (after one
354
- // minute, then every ten): a slow connection must not trap the parent
355
- // in setup when the download finishes fine in the background anyway.
356
334
  const escapeAfterMs = [60_000, 600_000];
357
335
  let escapeIndex = 0;
358
336
  for (;;) {
@@ -406,64 +384,12 @@ async function firstGameStep(settings) {
406
384
  if (p.isCancel(wants) || !wants) {
407
385
  return;
408
386
  }
409
- const games = scaffoldById('games');
410
- if (games === undefined) {
411
- return;
412
- }
413
- const themeId = await p.select({
414
- message: 'Pick a style for your game.',
415
- options: games.themes.map((t) => ({ value: t.id, label: `${t.emoji} ${t.label}` })),
416
- });
417
- if (p.isCancel(themeId)) {
418
- return;
419
- }
420
- const theme = games.themes.find((t) => t.id === themeId);
421
- if (theme === undefined) {
422
- return;
423
- }
424
- const CUSTOM = '__custom__';
425
- const suggestions = suggestProjectNames(theme.label);
426
- const namePick = await p.select({
427
- message: 'Pick a name for it.',
428
- options: [
429
- ...suggestions.map((n) => ({ value: n, label: n })),
430
- { value: CUSTOM, label: 'Type my own name' },
431
- ],
432
- });
433
- if (p.isCancel(namePick)) {
434
- return;
435
- }
436
- let prettyName = namePick;
437
- if (namePick === CUSTOM) {
438
- const typed = await p.text({
439
- message: 'What is its name?',
440
- validate: (value) => {
441
- const trimmed = (value ?? '').trim();
442
- if (trimmed.length === 0)
443
- return 'It needs a name.';
444
- if (!nameIsOkay(trimmed))
445
- return 'That name will not work. Try another one.';
446
- return undefined;
447
- },
448
- });
449
- if (p.isCancel(typed)) {
450
- return;
451
- }
452
- prettyName = typed.trim();
453
- }
454
387
  try {
455
- const create = await import('../projects/create.js');
456
- const home = await import('../surfaces/home.js');
457
- const made = create.createProject('games', theme.id, prettyName);
458
- await home.awardBadge('first-project');
459
- const starters = made.starterPrompts.slice(0, 2);
460
- if (starters.length > 0) {
461
- p.note(starters.map((line) => `- ${line}`).join('\n'), 'Try saying');
462
- }
463
- await home.openChatLoop(made.project, settings);
388
+ const build = await import('../surfaces/buildGame.js');
389
+ await build.runBuildGame(settings);
464
390
  }
465
391
  catch {
466
- p.log.warn('I could not start the game yet. Try "termi new" next time.');
392
+ p.log.warn('I could not start the game yet. Try Build a game next time.');
467
393
  }
468
394
  }
469
395
  /** Runs the full setup wizard, parent flow then kid flow. */
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Build a game: pick an idea, prompt loop, live preview, done/improve,
3
+ * final polish. Projects save in the local library under TERMI_PROJECTS_DIR.
4
+ */
5
+ import * as p from '@clack/prompts';
6
+ import { getSecret } from '../auth/keychain.js';
7
+ import { hasTokens } from '../auth/tokens.js';
8
+ import { startPreview } from '../preview/server.js';
9
+ import { createProviderClient, pickClassifierBackend, } from '../providers/index.js';
10
+ import { classifyProviderError } from '../providers/errors.js';
11
+ import { appendAudit } from '../safety/audit.js';
12
+ import { createSafetyPipeline } from '../safety/classifier.js';
13
+ import { ensureGuardFetch } from '../safety/guarddownload.js';
14
+ import { lazyGuardAccessor } from '../safety/guardrunner.js';
15
+ import { guardModelReady } from '../safety/modelstore.js';
16
+ import { createSessionState } from '../safety/session.js';
17
+ import { nameIsOkay } from '../safety/prefilter.js';
18
+ import { createBlankGameProject } from '../projects/blankGame.js';
19
+ import { GAME_IDEAS, gameIdeaById, gameIdeaMenuOptions, isOwnIdea, } from '../projects/gameIdeas.js';
20
+ import { providerLabel } from '../setup/wizard.js';
21
+ import { renderProviderError } from '../ui/errors.js';
22
+ import { style } from '../ui/theme.js';
23
+ import { T } from '../ui/text.js';
24
+ import { completenessHint, defaultNameForIdea, helpQuestions, parseDoneChoice, polishPrompt, seedPromptForIdea, suggestPromptFromAnswers, summarizeProjectFiles, } from './buildLoop.js';
25
+ import { awardBadge } from './home.js';
26
+ function audit(event) {
27
+ try {
28
+ appendAudit(event);
29
+ }
30
+ catch {
31
+ // Never break the kid flow for the log.
32
+ }
33
+ }
34
+ function providerAvailability(settings) {
35
+ return {
36
+ 'openai-chatgpt': hasTokens(),
37
+ 'openai-api': (getSecret('api-key-openai-api') ?? '').length > 0,
38
+ anthropic: (getSecret('api-key-anthropic') ?? '').length > 0,
39
+ xai: (getSecret('api-key-xai') ?? '').length > 0 && settings.xaiParentAck,
40
+ };
41
+ }
42
+ async function pickIdea() {
43
+ const pick = await p.select({
44
+ message: 'Build a game. What do you want to make?',
45
+ options: gameIdeaMenuOptions(),
46
+ initialValue: GAME_IDEAS[0].id,
47
+ });
48
+ if (p.isCancel(pick)) {
49
+ return null;
50
+ }
51
+ return gameIdeaById(pick) ?? null;
52
+ }
53
+ async function pickProjectName(idea) {
54
+ const suggested = defaultNameForIdea(idea);
55
+ const typed = await p.text({
56
+ message: 'Name your game.',
57
+ placeholder: suggested,
58
+ defaultValue: suggested,
59
+ validate: (value) => {
60
+ const trimmed = (value ?? '').trim() || suggested;
61
+ if (trimmed.length === 0)
62
+ return 'It needs a name.';
63
+ if (!nameIsOkay(trimmed))
64
+ return 'That name will not work. Try another one.';
65
+ return undefined;
66
+ },
67
+ });
68
+ if (p.isCancel(typed)) {
69
+ return null;
70
+ }
71
+ const name = typed.trim() || suggested;
72
+ return name;
73
+ }
74
+ async function gatherHelpAnswers(idea) {
75
+ const questions = helpQuestions(idea);
76
+ const answers = [];
77
+ for (const question of questions) {
78
+ const ans = await p.text({
79
+ message: question,
80
+ validate: (value) => (value ?? '').trim().length > 0 ? undefined : 'Say a little so I can help.',
81
+ });
82
+ if (p.isCancel(ans)) {
83
+ return null;
84
+ }
85
+ answers.push(ans.trim());
86
+ }
87
+ return answers;
88
+ }
89
+ async function obtainPrompt(idea) {
90
+ const path = await p.select({
91
+ message: 'How do you want to tell Termi what to build?',
92
+ options: [
93
+ { value: 'write', label: 'I will write my own prompt' },
94
+ { value: 'help', label: 'Help me with a prompt for my idea' },
95
+ ...(seedPromptForIdea(idea).length > 0
96
+ ? [{ value: 'seed', label: 'Use the ready idea prompt' }]
97
+ : []),
98
+ ],
99
+ });
100
+ if (p.isCancel(path)) {
101
+ return null;
102
+ }
103
+ let draft = '';
104
+ if (path === 'seed') {
105
+ draft = seedPromptForIdea(idea);
106
+ }
107
+ else if (path === 'help') {
108
+ const answers = await gatherHelpAnswers(idea);
109
+ if (answers === null) {
110
+ return null;
111
+ }
112
+ draft = suggestPromptFromAnswers(idea, answers);
113
+ p.note(draft, 'Suggested prompt');
114
+ }
115
+ const text = await p.text({
116
+ message: path === 'write' ? 'Type your prompt.' : 'Edit the prompt, or press Enter to run it.',
117
+ defaultValue: draft,
118
+ placeholder: draft || 'Make a fun browser game where...',
119
+ validate: (value) => (value ?? '').trim().length > 0 ? undefined : 'Need a prompt to build.',
120
+ });
121
+ if (p.isCancel(text)) {
122
+ return null;
123
+ }
124
+ return text.trim();
125
+ }
126
+ function listKidFileSummaries(project) {
127
+ const out = [];
128
+ for (const file of project.listKidFiles()) {
129
+ const content = project.readFile(file.relPath);
130
+ if (content !== null) {
131
+ out.push({ relPath: file.relPath, content });
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+ async function runOneTurn(kidMessage, deps) {
137
+ const loop = await import('../agent/loop.js');
138
+ const spin = p.spinner();
139
+ spin.start(T.chat.thinking);
140
+ let result;
141
+ try {
142
+ result = await loop.runTurn(kidMessage, {
143
+ provider: deps.provider,
144
+ modelAlias: deps.settings.modelAlias,
145
+ safety: deps.safety,
146
+ session: deps.session,
147
+ project: deps.project,
148
+ snapshots: deps.snapshots,
149
+ preview: deps.preview,
150
+ audit,
151
+ ui: {
152
+ onActivity(line) {
153
+ spin.message(line);
154
+ },
155
+ },
156
+ });
157
+ }
158
+ catch (err) {
159
+ spin.stop();
160
+ const label = deps.settings.activeProvider !== null
161
+ ? providerLabel(deps.settings.activeProvider)
162
+ : 'AI helper';
163
+ console.log(renderProviderError(classifyProviderError(err), label));
164
+ return 'error';
165
+ }
166
+ spin.stop(T.chat.working);
167
+ if (result.status === 'ok') {
168
+ if (result.replyText) {
169
+ console.log(result.replyText);
170
+ }
171
+ try {
172
+ deps.preview.notifyChange();
173
+ }
174
+ catch {
175
+ // Preview may already be closed.
176
+ }
177
+ return 'ok';
178
+ }
179
+ if (result.status === 'blocked') {
180
+ if (result.screen !== null) {
181
+ console.log(result.screen.body);
182
+ }
183
+ else {
184
+ console.log(T.blocks.generic);
185
+ }
186
+ return 'blocked';
187
+ }
188
+ if (result.screen !== null) {
189
+ console.log(result.screen.body);
190
+ }
191
+ else {
192
+ console.log(T.errors.oops);
193
+ }
194
+ return 'error';
195
+ }
196
+ /**
197
+ * Full Build a game session for one project from idea pick to library save.
198
+ * Returns the project when something was saved, null on cancel at the start.
199
+ */
200
+ export async function runBuildGame(settings) {
201
+ const idea = await pickIdea();
202
+ if (idea === null) {
203
+ return null;
204
+ }
205
+ const prettyName = await pickProjectName(idea);
206
+ if (prettyName === null) {
207
+ return null;
208
+ }
209
+ let project;
210
+ try {
211
+ project = createBlankGameProject(prettyName, idea.label);
212
+ }
213
+ catch (err) {
214
+ p.log.error(err instanceof Error ? err.message : 'Could not create the game folder.');
215
+ return null;
216
+ }
217
+ await awardBadge('first-project');
218
+ let provider = null;
219
+ if (settings.activeProvider !== null) {
220
+ try {
221
+ provider = createProviderClient(settings.activeProvider);
222
+ }
223
+ catch {
224
+ provider = null;
225
+ }
226
+ }
227
+ if (provider === null) {
228
+ p.log.warn(`${T.offline.noProvider} ${T.offline.stillWorks}`);
229
+ p.log.info(`Your game folder is ready: ${project.meta.prettyName}`);
230
+ return project;
231
+ }
232
+ if (settings.localClassifier && !guardModelReady()) {
233
+ void ensureGuardFetch();
234
+ }
235
+ const backend = pickClassifierBackend(settings, providerAvailability(settings));
236
+ const safety = createSafetyPipeline({
237
+ classifierModel: () => backend.classifierClient !== null
238
+ ? backend.classifierClient.languageModel('classifier', settings.modelAlias)
239
+ : null,
240
+ moderationKey: () => backend.moderationKey,
241
+ localGuard: lazyGuardAccessor(settings.localClassifier),
242
+ audit,
243
+ });
244
+ const session = createSessionState();
245
+ let snapshots = {
246
+ beginTurn() { },
247
+ undo() {
248
+ return false;
249
+ },
250
+ redo() {
251
+ return false;
252
+ },
253
+ };
254
+ try {
255
+ const snapMod = await import('../projects/snapshots.js');
256
+ snapshots = snapMod.createSnapshotStore(project);
257
+ }
258
+ catch {
259
+ // undo optional
260
+ }
261
+ const preview = await startPreview(project.dir, { openBrowser: true });
262
+ console.log(`${T.chat.previewOpened} ${style.dim(preview.url)}`);
263
+ console.log(style.dim('The browser will update after each build.'));
264
+ const turnDeps = { provider, settings, project, preview, safety, session, snapshots };
265
+ // First build
266
+ for (;;) {
267
+ const prompt = await obtainPrompt(idea);
268
+ if (prompt === null) {
269
+ break;
270
+ }
271
+ const status = await runOneTurn(prompt, turnDeps);
272
+ if (status === 'ok') {
273
+ try {
274
+ preview.notifyChange();
275
+ }
276
+ catch {
277
+ //
278
+ }
279
+ }
280
+ const next = await p.select({
281
+ message: 'Do you think you are done, or want to improve the idea?',
282
+ options: [
283
+ { value: 'improve', label: 'Improve' },
284
+ { value: 'done', label: 'I am done' },
285
+ ],
286
+ initialValue: 'improve',
287
+ });
288
+ if (p.isCancel(next)) {
289
+ break;
290
+ }
291
+ const choice = parseDoneChoice(next);
292
+ if (choice === 'improve') {
293
+ continue;
294
+ }
295
+ if (choice === 'done') {
296
+ console.log(style.title('Testing your game and making final fixes...'));
297
+ const summary = summarizeProjectFiles(listKidFileSummaries(project));
298
+ const hint = completenessHint(summary);
299
+ console.log(style.dim(`Suggestion: ${hint}`));
300
+ const approve = await p.confirm({
301
+ message: 'Apply this final improvement?',
302
+ initialValue: true,
303
+ });
304
+ if (!p.isCancel(approve) && approve) {
305
+ const finalPrompt = polishPrompt(`${hint}. Project: ${summary}`);
306
+ await runOneTurn(finalPrompt, turnDeps);
307
+ try {
308
+ preview.notifyChange();
309
+ }
310
+ catch {
311
+ //
312
+ }
313
+ console.log(style.good('Final fixes applied. Check the browser!'));
314
+ }
315
+ else {
316
+ console.log('Okay. Your game is saved as is.');
317
+ }
318
+ break;
319
+ }
320
+ }
321
+ p.log.success(`Saved in your game library: ${project.meta.prettyName}`);
322
+ // Keep preview open so the kid can keep playing; they Ctrl+C later or return home.
323
+ return project;
324
+ }
325
+ /** Exported for tests that assert the catalog is wired. */
326
+ export function buildGameIdeaCount() {
327
+ return GAME_IDEAS.length;
328
+ }
329
+ export function buildGameFirstLabel() {
330
+ return GAME_IDEAS[0]?.label ?? '';
331
+ }
332
+ export function buildGameIsOwnFirst() {
333
+ const first = GAME_IDEAS[0];
334
+ return first !== undefined && isOwnIdea(first);
335
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Pure helpers for the Build a game prompt loop.
3
+ * UI and network live in buildGame.ts; tests cover this module fully.
4
+ */
5
+ import { isOwnIdea } from '../projects/gameIdeas.js';
6
+ /** Clarifying questions when the kid picks Help me with a prompt. */
7
+ export function helpQuestions(idea) {
8
+ if (isOwnIdea(idea)) {
9
+ return [
10
+ 'What is your game about in one short line?',
11
+ 'How does the player win or finish?',
12
+ 'What keys or clicks should control it?',
13
+ ];
14
+ }
15
+ return [
16
+ `What should feel special about "${idea.label}"?`,
17
+ 'Do you want easy mode, normal, or hard?',
18
+ ];
19
+ }
20
+ /** Builds a starter prompt from a catalog idea (not own-idea). */
21
+ export function seedPromptForIdea(idea) {
22
+ if (isOwnIdea(idea) || idea.seedPrompt.trim().length === 0) {
23
+ return '';
24
+ }
25
+ return idea.seedPrompt.trim();
26
+ }
27
+ /**
28
+ * Turns help-question answers into a suggested build prompt the kid can
29
+ * edit. Works offline without a model call.
30
+ */
31
+ export function suggestPromptFromAnswers(idea, answers) {
32
+ const cleaned = answers.map((a) => a.trim()).filter((a) => a.length > 0);
33
+ if (isOwnIdea(idea)) {
34
+ const about = cleaned[0] ?? 'a fun browser game';
35
+ const win = cleaned[1] ?? 'reach a high score';
36
+ const controls = cleaned[2] ?? 'arrow keys or click';
37
+ return (`Make a complete local browser game about ${about}. ` +
38
+ `The player wins when they ${win}. ` +
39
+ `Controls: ${controls}. ` +
40
+ `Use only HTML, CSS, and JavaScript in index.html, style.css, and game.js. ` +
41
+ `No images from the internet. Keep it kid friendly and playable after one load.`);
42
+ }
43
+ const special = cleaned[0] ?? idea.blurb;
44
+ const difficulty = cleaned[1] ?? 'normal';
45
+ const base = seedPromptForIdea(idea) || `Make a ${idea.label} browser game.`;
46
+ return (`${base} ` +
47
+ `Make it feel special by: ${special}. ` +
48
+ `Difficulty: ${difficulty}. ` +
49
+ `Use only HTML, CSS, and JavaScript files. No outside images.`);
50
+ }
51
+ /** Final polish instruction after the kid says they are done. */
52
+ export function polishPrompt(projectSummary) {
53
+ const summary = projectSummary.trim().length > 0
54
+ ? projectSummary.trim().slice(0, 800)
55
+ : 'the current game files';
56
+ return (`You are doing final testing and fixes for this kid game. ` +
57
+ `Read the project files. Based on ${summary}, make ONE clear improvement ` +
58
+ `so the game feels more complete (for example a start screen, score, ` +
59
+ `restart key, win or lose message, or clearer controls). ` +
60
+ `Apply the change in the files. Keep it simple and kid friendly. ` +
61
+ `No internet images. Then briefly say what you fixed.`);
62
+ }
63
+ /** Summarize kid files for the polish prompt (pure). */
64
+ export function summarizeProjectFiles(files) {
65
+ if (files.length === 0) {
66
+ return 'no files yet';
67
+ }
68
+ return files
69
+ .map((f) => {
70
+ const lines = f.content.split('\n').length;
71
+ const hasCanvas = /canvas/i.test(f.content);
72
+ const hasScore = /score/i.test(f.content);
73
+ const flags = [
74
+ hasCanvas ? 'canvas' : null,
75
+ hasScore ? 'score' : null,
76
+ lines < 20 ? 'short' : null,
77
+ ]
78
+ .filter(Boolean)
79
+ .join(',');
80
+ return `${f.relPath}(${lines} lines${flags ? `; ${flags}` : ''})`;
81
+ })
82
+ .join('; ');
83
+ }
84
+ /** Maps select values to done/improve. */
85
+ export function parseDoneChoice(value) {
86
+ if (value === 'done' || value === 'improve') {
87
+ return value;
88
+ }
89
+ return null;
90
+ }
91
+ /** Default project name from an idea label. */
92
+ export function defaultNameForIdea(idea) {
93
+ if (isOwnIdea(idea)) {
94
+ return 'My Game';
95
+ }
96
+ return idea.label;
97
+ }
98
+ /**
99
+ * One completeness suggestion line for the terminal (before the kid
100
+ * approves a polish apply). Pure heuristic from file summary text.
101
+ */
102
+ export function completenessHint(summary) {
103
+ const s = summary.toLowerCase();
104
+ if (!s.includes('score')) {
105
+ return 'Add a clear score the player can see.';
106
+ }
107
+ if (!s.includes('canvas') && !s.includes('button')) {
108
+ return 'Add a start screen with a Play button.';
109
+ }
110
+ if (s.includes('short')) {
111
+ return 'Add a win or lose message and a restart key.';
112
+ }
113
+ return 'Add clearer control hints on the start screen.';
114
+ }
@@ -15,11 +15,10 @@ import { consumeGuardReadyNotice, guardFetchState, guardProgressBar, } from '../
15
15
  import { nameIsOkay } from '../safety/prefilter.js';
16
16
  import { scaffolds, scaffoldById } from '../projects/scaffolds/index.js';
17
17
  import { suggestProjectNames } from '../setup/wizard.js';
18
- import { BADGES, celebrate, confetti, renderBadgeShelf } from '../ui/celebrate.js';
18
+ import { BADGES, celebrate, confetti } from '../ui/celebrate.js';
19
19
  import { mascot } from '../ui/mascot.js';
20
20
  import { style } from '../ui/theme.js';
21
21
  import { T } from '../ui/text.js';
22
- import { executeIdeas } from './commands.js';
23
22
  /** Where the earned-badge list lives. */
24
23
  export function badgesFilePath() {
25
24
  return path.join(termiHome(), 'badges.json');
@@ -325,6 +324,31 @@ async function loadLastProject(settings) {
325
324
  return null;
326
325
  }
327
326
  }
327
+ /** Kid home menu values (simplified product surface). */
328
+ export const HOME_MENU_VALUES = [
329
+ 'build',
330
+ 'library',
331
+ 'learn',
332
+ 'continue',
333
+ 'grownups',
334
+ 'quit',
335
+ ];
336
+ /**
337
+ * Pure home option list for tests. Build a game and Learn AI are always
338
+ * present; continue and library depend on whether projects exist.
339
+ */
340
+ export function homeMenuOptions(hasLast, hasLibrary) {
341
+ const options = [];
342
+ if (hasLast) {
343
+ options.push({ value: 'continue', label: T.home.menuContinue });
344
+ }
345
+ options.push({ value: 'build', label: T.home.menuBuild }, { value: 'library', label: T.home.menuLibrary }, { value: 'learn', label: T.home.menuLearn }, { value: 'grownups', label: T.home.menuGrownups }, { value: 'quit', label: T.home.menuQuit });
346
+ if (!hasLibrary) {
347
+ // Library stays listed so kids discover where games are saved.
348
+ void hasLibrary;
349
+ }
350
+ return options;
351
+ }
328
352
  /** The home menu loop for a returning kid. */
329
353
  export async function showHome(settings) {
330
354
  const nickname = settings.kidNickname.trim();
@@ -348,15 +372,19 @@ export async function showHome(settings) {
348
372
  else if (consumeGuardReadyNotice()) {
349
373
  console.log(style.dim(T.home.guardOn));
350
374
  }
351
- const options = [];
352
- if (last !== null) {
353
- options.push({ value: 'continue', label: `Keep building ${last.context.meta.prettyName}` });
375
+ let hasLibrary = false;
376
+ try {
377
+ const store = await import('../projects/store.js');
378
+ hasLibrary = store.listProjects().length > 0;
379
+ }
380
+ catch {
381
+ hasLibrary = false;
354
382
  }
355
- options.push({ value: 'open', label: T.home.menuGo }, { value: 'new', label: T.home.menuNew }, { value: 'ideas', label: T.home.menuIdeas }, { value: 'learn', label: 'Learn AI tricks' }, { value: 'badges', label: T.home.menuBadges }, { value: 'grownups', label: T.home.menuGrownups }, { value: 'quit', label: T.home.menuQuit });
383
+ const options = homeMenuOptions(last !== null, hasLibrary);
356
384
  const pick = await p.select({
357
385
  message: 'What now?',
358
386
  options,
359
- initialValue: last !== null ? 'continue' : 'new',
387
+ initialValue: last !== null ? 'continue' : 'build',
360
388
  });
361
389
  if (p.isCancel(pick) || pick === 'quit') {
362
390
  p.outro(T.home.goodbye);
@@ -365,20 +393,26 @@ export async function showHome(settings) {
365
393
  if (pick === 'continue' && last !== null) {
366
394
  await openChatLoop(last.context, settings);
367
395
  }
368
- else if (pick === 'open') {
396
+ else if (pick === 'library') {
369
397
  await openPicked(settings);
370
398
  }
371
- else if (pick === 'new') {
372
- const made = await runNewProject(settings);
373
- if (made !== null) {
374
- await openChatLoop(made, settings);
399
+ else if (pick === 'build') {
400
+ try {
401
+ const build = await import('./buildGame.js');
402
+ const made = await build.runBuildGame(settings);
403
+ if (made !== null) {
404
+ settings.lastProjectSlug = made.meta.slug;
405
+ try {
406
+ saveSettings(settings);
407
+ }
408
+ catch {
409
+ //
410
+ }
411
+ }
412
+ }
413
+ catch {
414
+ p.log.warn('Build a game is taking a nap. Try again soon.');
375
415
  }
376
- }
377
- else if (pick === 'ideas') {
378
- const scaffoldId = last !== null ? last.context.meta.scaffoldId : 'games';
379
- await executeIdeas(scaffoldId, (text) => {
380
- console.log(text);
381
- });
382
416
  }
383
417
  else if (pick === 'learn') {
384
418
  try {
@@ -389,9 +423,6 @@ export async function showHome(settings) {
389
423
  p.log.warn('Learn mode is taking a nap. Try again soon.');
390
424
  }
391
425
  }
392
- else if (pick === 'badges') {
393
- console.log(renderBadgeShelf(loadBadges()));
394
- }
395
426
  else if (pick === 'grownups') {
396
427
  try {
397
428
  const panel = await import('../grownups/panel.js');
@@ -412,7 +443,7 @@ async function openPicked(settings) {
412
443
  }
413
444
  const metas = mods.store.listProjects();
414
445
  if (metas.length === 0) {
415
- p.log.info('No projects yet. Pick "Make something new"!');
446
+ p.log.info('No games yet. Pick "Build a game"!');
416
447
  return;
417
448
  }
418
449
  const first = metas[0];
package/dist/ui/text.js CHANGED
@@ -24,10 +24,14 @@ export const T = {
24
24
  welcomeBack: 'Welcome back, {name}!',
25
25
  recapIntro: 'Last time: {recap}',
26
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',
27
+ menuBuild: 'Build a game',
28
+ menuLibrary: 'My games',
29
+ menuLearn: 'Learn AI',
30
+ menuContinue: 'Keep building',
31
+ menuNew: 'Build a game',
32
+ menuGo: 'My games',
33
+ menuPreview: 'Watch a game run',
34
+ menuIdeas: 'Game ideas',
31
35
  menuBadges: 'See your badges',
32
36
  menuGrownups: 'Grown-up zone',
33
37
  menuHelp: 'Help',
@@ -43,25 +47,25 @@ export const T = {
43
47
  consentIntro: 'Termi uses an AI account you own. You agree to watch how your kid uses it.',
44
48
  providerPick: 'Pick the AI helper account Termi will use.',
45
49
  xaiAck: 'This provider is for adults only. A parent must own it and watch it.',
46
- guardOffer: 'Termi includes a safety checker that runs on this computer (a 623 MB download). ' +
47
- 'It screens every message even when the internet is down. Download it now?',
50
+ guardOffer: 'Termi will install a safety helper on this computer. ' +
51
+ 'It is about 623 MB. It checks messages even offline.',
48
52
  guardDownloading: 'Getting the safety file now...',
49
53
  guardBackground: 'Basic safety is on right now. It gets stronger when the download is done.',
50
- guardWaitPick: 'Start building now, or wait for it?',
51
- guardStartNow: 'Start now',
52
- guardStartNowHint: 'The checker turns on by itself later.',
54
+ guardWaitPick: 'Wait for the safety helper, or keep setting up?',
55
+ guardStartNow: 'Keep setting up',
56
+ guardStartNowHint: 'The helper turns on by itself when ready.',
53
57
  guardWaitHere: 'Wait here',
54
58
  guardWaitHereHint: 'Watch the download finish first.',
55
59
  guardKeepWaiting: 'Still going. Keep waiting?',
56
- guardReady: 'Your safety checker is set.',
57
- guardFailed: 'The safety checker download did not finish. Termi still works; ' +
58
- 'you can retry from the grown-up zone any time.',
59
- guardDeclined: 'Okay, skipping it. You can turn it on in the grown-up zone later.',
60
+ guardReady: 'Your safety helper is set.',
61
+ guardFailed: 'The safety download did not finish. Termi still works. ' +
62
+ 'It will keep trying in the background.',
63
+ guardDeclined: 'The safety helper stays on. Termi will download it now.',
60
64
  handToKid: 'All set! Now hand the keyboard to your kid.',
61
- kidHello: 'Hi! I am Termi. I help you build games and more.',
65
+ kidHello: 'Hi! I am Termi. I help you build games.',
62
66
  nicknamePrompt: 'Pick a fun made-up name. Not your real name!',
63
67
  aiDisclosure: 'Termi is a computer program, an AI. It is a tool a grown-up set up for you. It is not a person.',
64
- firstGameOffer: 'Want to make your first game right now?',
68
+ firstGameOffer: 'Want to build your first game right now?',
65
69
  launcherMade: 'I made a Termi shortcut so you can come back fast.',
66
70
  nodeTooOld: 'Termi needs a newer Node. Ask a grown-up to install it from nodejs.org.',
67
71
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "termi-kids",
3
- "version": "0.1.2",
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.",
3
+ "version": "0.2.0",
4
+ "description": "Termi is a friendly coding buddy for kids. Build browser games with AI prompts, Learn AI lessons, and strong local safety rails.",
5
5
  "keywords": [
6
6
  "kids",
7
7
  "coding",