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,484 @@
1
+ /**
2
+ * Pixel Studio scaffold: a canvas pixel-art painter.
3
+ * Kid files: index.html, style.css, game.js. Vanilla JS, zero network.
4
+ */
5
+ const freeDraw = {
6
+ id: 'free-draw',
7
+ label: 'Free Draw',
8
+ emoji: '🌈',
9
+ palette: { bg: '#1b1f3b', fg: '#f7f7ff', accent: '#ffd166' },
10
+ glyphs: { brush: '🖌️', eraser: '🧽', fill: '🪣', clear: '✨', download: '💾' },
11
+ strings: {
12
+ studioName: 'Rainbow Studio',
13
+ hint: 'Pick a color. Then click or drag to paint!',
14
+ idea: 'Idea: paint a sunset over the sea.',
15
+ brushTip: 'Brush time! Drag to paint squares.',
16
+ eraserTip: 'Eraser ready. Rub out any square.',
17
+ fillTip: 'Bucket ready! Click a spot to fill it.',
18
+ cleared: 'Fresh paper! What will you make now?',
19
+ saved: 'Saved! Check your downloads folder.',
20
+ autoSaveNote: 'Your art saves itself while you paint.',
21
+ },
22
+ narrativeIntro: 'Welcome to your art studio. The paper is ready. What will you paint first?',
23
+ nonViolent: true,
24
+ nonCompetitive: true,
25
+ };
26
+ const petPortraits = {
27
+ id: 'pet-portraits',
28
+ label: 'Pet Portraits',
29
+ emoji: '🐶',
30
+ palette: { bg: '#2c1c12', fg: '#fff4e6', accent: '#ffb26b' },
31
+ glyphs: { brush: '🖌️', eraser: '🧽', fill: '🪣', clear: '✨', download: '💾' },
32
+ strings: {
33
+ studioName: 'Pet Portrait Studio',
34
+ hint: 'Paint a furry friend, square by square!',
35
+ idea: 'Idea: paint a cat with bright green eyes.',
36
+ brushTip: 'Brush time! Paint some soft fur.',
37
+ eraserTip: 'Eraser ready. Fix any stray fur.',
38
+ fillTip: 'Bucket ready! Fill the background fast.',
39
+ cleared: 'Fresh paper! Time for a new pet.',
40
+ saved: 'Saved! Check your downloads folder.',
41
+ autoSaveNote: 'Your art saves itself while you paint.',
42
+ },
43
+ narrativeIntro: 'Every pet deserves a portrait. Paint a furry friend today.',
44
+ nonViolent: true,
45
+ nonCompetitive: true,
46
+ };
47
+ const EXTRAS = {
48
+ 'free-draw': {
49
+ paper: '#fffdf6',
50
+ fileName: 'my-pixel-art',
51
+ colors: [
52
+ '#ff5d5d', '#ff9f43', '#ffd93d', '#6bcb77', '#4d96ff', '#9b5de5',
53
+ '#f15bb5', '#00c2a8', '#8d6e63', '#222831', '#b0bec5', '#ffffff',
54
+ ],
55
+ prompts: [
56
+ 'Add a hot pink color to my paint set.',
57
+ 'Make a rainbow button that paints with random colors.',
58
+ 'Add a big brush that paints four squares at once.',
59
+ 'Add a mirror mode that copies my left side to the right.',
60
+ 'Add a night mode with a dark paper color.',
61
+ ],
62
+ },
63
+ 'pet-portraits': {
64
+ paper: '#fdf6ec',
65
+ fileName: 'my-pet-portrait',
66
+ colors: [
67
+ '#fff3e0', '#f6d186', '#c98850', '#8d5a3c', '#5d4037', '#3e2723',
68
+ '#9e9e9e', '#37474f', '#ffffff', '#f48fb1', '#66bb6a', '#42a5f5',
69
+ ],
70
+ prompts: [
71
+ 'Add more fur colors, like golden and spotted gray.',
72
+ 'Add a stamp that draws a paw print.',
73
+ 'Add a fancy gold frame around my painting.',
74
+ 'Make a button that draws cat whiskers for me.',
75
+ 'Add a little bone sticker I can place anywhere.',
76
+ ],
77
+ },
78
+ };
79
+ function extrasFor(id) {
80
+ return EXTRAS[id] ?? EXTRAS['free-draw'];
81
+ }
82
+ function s(theme, key) {
83
+ return theme.strings[key] ?? '';
84
+ }
85
+ function g(theme, key) {
86
+ return theme.glyphs[key] ?? '';
87
+ }
88
+ function escapeHtml(text) {
89
+ return text
90
+ .replace(/&/g, '&')
91
+ .replace(/</g, '&lt;')
92
+ .replace(/>/g, '&gt;')
93
+ .replace(/"/g, '&quot;');
94
+ }
95
+ function buildHtml(theme, prettyName) {
96
+ const name = escapeHtml(prettyName);
97
+ return `<!DOCTYPE html>
98
+ <html lang="en">
99
+ <head>
100
+ <meta charset="utf-8">
101
+ <meta name="viewport" content="width=device-width, initial-scale=1">
102
+ <title>${name} ${theme.emoji}</title>
103
+ <link rel="stylesheet" href="style.css">
104
+ </head>
105
+ <body>
106
+ <main class="studio">
107
+ <header>
108
+ <h1>${theme.emoji} ${name}</h1>
109
+ <p class="subtitle">${escapeHtml(s(theme, 'studioName'))}</p>
110
+ <p class="intro">${escapeHtml(theme.narrativeIntro)}</p>
111
+ </header>
112
+
113
+ <section class="toolbar" aria-label="Tools">
114
+ <button data-tool="brush" class="tool active">${g(theme, 'brush')} Brush</button>
115
+ <button data-tool="eraser" class="tool">${g(theme, 'eraser')} Eraser</button>
116
+ <button data-tool="fill" class="tool">${g(theme, 'fill')} Fill</button>
117
+ <span class="divider"></span>
118
+ <button data-size="16" class="size active">16 x 16</button>
119
+ <button data-size="32" class="size">32 x 32</button>
120
+ <span class="divider"></span>
121
+ <button id="clear" class="action">${g(theme, 'clear')} Clear</button>
122
+ <button id="download" class="action">${g(theme, 'download')} Save PNG</button>
123
+ </section>
124
+
125
+ <div id="swatches" class="swatches" aria-label="Paint colors"></div>
126
+
127
+ <canvas id="paper" width="480" height="480"></canvas>
128
+
129
+ <p id="message" class="message"></p>
130
+ <footer>
131
+ <p class="idea">${escapeHtml(s(theme, 'idea'))}</p>
132
+ <p class="note">${escapeHtml(s(theme, 'autoSaveNote'))}</p>
133
+ </footer>
134
+ </main>
135
+ <script src="game.js"></script>
136
+ </body>
137
+ </html>
138
+ `;
139
+ }
140
+ function buildCss(theme) {
141
+ const extra = extrasFor(theme.id);
142
+ return `:root {
143
+ --bg: ${theme.palette.bg};
144
+ --fg: ${theme.palette.fg};
145
+ --accent: ${theme.palette.accent};
146
+ --paper: ${extra.paper};
147
+ }
148
+
149
+ * { box-sizing: border-box; }
150
+
151
+ body {
152
+ margin: 0;
153
+ min-height: 100vh;
154
+ background: var(--bg);
155
+ color: var(--fg);
156
+ font-family: "Avenir Next", "Trebuchet MS", Verdana, sans-serif;
157
+ }
158
+
159
+ .studio {
160
+ width: min(720px, 94vw);
161
+ margin: 0 auto;
162
+ padding: 18px 0 40px;
163
+ text-align: center;
164
+ }
165
+
166
+ header h1 { margin: 8px 0 2px; font-size: 2rem; }
167
+ .subtitle { margin: 0; color: var(--accent); font-weight: bold; }
168
+ .intro { margin: 6px 0 14px; opacity: 0.85; }
169
+
170
+ .toolbar {
171
+ display: flex;
172
+ flex-wrap: wrap;
173
+ gap: 8px;
174
+ justify-content: center;
175
+ align-items: center;
176
+ margin-bottom: 12px;
177
+ }
178
+
179
+ .toolbar button {
180
+ font: inherit;
181
+ color: var(--fg);
182
+ background: rgba(255, 255, 255, 0.08);
183
+ border: 2px solid transparent;
184
+ border-radius: 999px;
185
+ padding: 8px 14px;
186
+ cursor: pointer;
187
+ }
188
+
189
+ .toolbar button:hover { background: rgba(255, 255, 255, 0.16); }
190
+ .toolbar button.active { border-color: var(--accent); }
191
+
192
+ .divider {
193
+ width: 2px;
194
+ height: 24px;
195
+ background: rgba(255, 255, 255, 0.2);
196
+ border-radius: 2px;
197
+ }
198
+
199
+ .swatches {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ gap: 8px;
203
+ justify-content: center;
204
+ margin-bottom: 14px;
205
+ }
206
+
207
+ .swatch {
208
+ width: 34px;
209
+ height: 34px;
210
+ border-radius: 50%;
211
+ border: 3px solid rgba(255, 255, 255, 0.25);
212
+ cursor: pointer;
213
+ padding: 0;
214
+ }
215
+
216
+ .swatch.active {
217
+ border-color: var(--accent);
218
+ transform: scale(1.18);
219
+ }
220
+
221
+ #paper {
222
+ width: min(480px, 92vw);
223
+ aspect-ratio: 1 / 1;
224
+ background: var(--paper);
225
+ border: 4px solid var(--accent);
226
+ border-radius: 12px;
227
+ touch-action: none;
228
+ image-rendering: pixelated;
229
+ cursor: crosshair;
230
+ }
231
+
232
+ .message {
233
+ min-height: 1.4em;
234
+ margin: 12px 0 4px;
235
+ color: var(--accent);
236
+ font-weight: bold;
237
+ }
238
+
239
+ footer p { margin: 4px 0; opacity: 0.75; font-size: 0.95rem; }
240
+ `;
241
+ }
242
+ function buildJs(theme) {
243
+ const extra = extrasFor(theme.id);
244
+ const themeData = {
245
+ id: theme.id,
246
+ studioName: s(theme, 'studioName'),
247
+ hint: s(theme, 'hint'),
248
+ paper: extra.paper,
249
+ fileName: extra.fileName,
250
+ colors: extra.colors,
251
+ toolTips: {
252
+ brush: s(theme, 'brushTip'),
253
+ eraser: s(theme, 'eraserTip'),
254
+ fill: s(theme, 'fillTip'),
255
+ },
256
+ clearedMessage: s(theme, 'cleared'),
257
+ savedMessage: s(theme, 'saved'),
258
+ };
259
+ return `// ====================================================
260
+ // THEME SETTINGS
261
+ // These values set the colors and the words.
262
+ // Change one, save the file, and reload the page!
263
+ // ====================================================
264
+ const THEME = ${JSON.stringify(themeData, null, 2)};
265
+
266
+ // ----- find the page pieces -----
267
+ const canvas = document.getElementById("paper");
268
+ const ctx = canvas.getContext("2d");
269
+ const swatchBar = document.getElementById("swatches");
270
+ const messageBox = document.getElementById("message");
271
+
272
+ // ----- studio state -----
273
+ let gridSize = 16;
274
+ let cells = [];
275
+ let tool = "brush";
276
+ let paintColor = THEME.colors[0];
277
+ let painting = false;
278
+ let messageTimer = null;
279
+
280
+ // Each grid size keeps its own saved drawing.
281
+ function saveKey() {
282
+ return "pixel-studio-" + THEME.id + "-" + gridSize;
283
+ }
284
+
285
+ function freshCells() {
286
+ return new Array(gridSize * gridSize).fill("");
287
+ }
288
+
289
+ function loadCells() {
290
+ let saved = null;
291
+ try {
292
+ saved = JSON.parse(localStorage.getItem(saveKey()));
293
+ } catch (err) {
294
+ saved = null;
295
+ }
296
+ const wanted = gridSize * gridSize;
297
+ cells = Array.isArray(saved) && saved.length === wanted ? saved : freshCells();
298
+ }
299
+
300
+ function saveCells() {
301
+ localStorage.setItem(saveKey(), JSON.stringify(cells));
302
+ }
303
+
304
+ // ----- drawing the paper -----
305
+ function draw() {
306
+ const size = canvas.width / gridSize;
307
+ ctx.fillStyle = THEME.paper;
308
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
309
+ for (let i = 0; i < cells.length; i++) {
310
+ if (cells[i] === "") continue;
311
+ const x = (i % gridSize) * size;
312
+ const y = Math.floor(i / gridSize) * size;
313
+ ctx.fillStyle = cells[i];
314
+ ctx.fillRect(x, y, size, size);
315
+ }
316
+ // light grid lines so squares are easy to see
317
+ ctx.strokeStyle = "rgba(0, 0, 0, 0.15)";
318
+ ctx.lineWidth = 1;
319
+ for (let n = 1; n < gridSize; n++) {
320
+ const p = n * size;
321
+ ctx.beginPath();
322
+ ctx.moveTo(p, 0);
323
+ ctx.lineTo(p, canvas.height);
324
+ ctx.moveTo(0, p);
325
+ ctx.lineTo(canvas.width, p);
326
+ ctx.stroke();
327
+ }
328
+ }
329
+
330
+ // ----- painting tools -----
331
+ function cellAt(event) {
332
+ const rect = canvas.getBoundingClientRect();
333
+ const col = Math.floor(((event.clientX - rect.left) / rect.width) * gridSize);
334
+ const row = Math.floor(((event.clientY - rect.top) / rect.height) * gridSize);
335
+ if (col < 0 || row < 0 || col >= gridSize || row >= gridSize) return -1;
336
+ return row * gridSize + col;
337
+ }
338
+
339
+ function useToolAt(index) {
340
+ if (index < 0) return;
341
+ if (tool === "brush") cells[index] = paintColor;
342
+ if (tool === "eraser") cells[index] = "";
343
+ if (tool === "fill") fillArea(index);
344
+ draw();
345
+ }
346
+
347
+ // The bucket fills every touching square that matches.
348
+ function fillArea(start) {
349
+ const target = cells[start];
350
+ if (target === paintColor) return;
351
+ const spots = [start];
352
+ while (spots.length > 0) {
353
+ const i = spots.pop();
354
+ if (cells[i] !== target) continue;
355
+ cells[i] = paintColor;
356
+ const col = i % gridSize;
357
+ if (col > 0) spots.push(i - 1);
358
+ if (col < gridSize - 1) spots.push(i + 1);
359
+ if (i - gridSize >= 0) spots.push(i - gridSize);
360
+ if (i + gridSize < cells.length) spots.push(i + gridSize);
361
+ }
362
+ }
363
+
364
+ canvas.addEventListener("pointerdown", (event) => {
365
+ painting = true;
366
+ useToolAt(cellAt(event));
367
+ });
368
+
369
+ canvas.addEventListener("pointermove", (event) => {
370
+ if (painting && tool !== "fill") useToolAt(cellAt(event));
371
+ });
372
+
373
+ window.addEventListener("pointerup", () => {
374
+ if (!painting) return;
375
+ painting = false;
376
+ saveCells();
377
+ });
378
+
379
+ // ----- little helper messages -----
380
+ function toast(text) {
381
+ messageBox.textContent = text;
382
+ if (messageTimer) clearTimeout(messageTimer);
383
+ messageTimer = setTimeout(() => {
384
+ messageBox.textContent = THEME.hint;
385
+ }, 2500);
386
+ }
387
+
388
+ function selectButton(group, button) {
389
+ document.querySelectorAll(group).forEach((b) => b.classList.remove("active"));
390
+ button.classList.add("active");
391
+ }
392
+
393
+ // ----- build the color swatches -----
394
+ THEME.colors.forEach((color, i) => {
395
+ const b = document.createElement("button");
396
+ b.className = "swatch" + (i === 0 ? " active" : "");
397
+ b.style.background = color;
398
+ b.title = "Paint with " + color;
399
+ b.addEventListener("click", () => {
400
+ paintColor = color;
401
+ selectButton(".swatch", b);
402
+ if (tool === "eraser") {
403
+ tool = "brush";
404
+ const brushButton = document.querySelector('[data-tool="brush"]');
405
+ if (brushButton) selectButton("[data-tool]", brushButton);
406
+ }
407
+ });
408
+ swatchBar.appendChild(b);
409
+ });
410
+
411
+ // ----- wire up the toolbar -----
412
+ document.querySelectorAll("[data-tool]").forEach((b) => {
413
+ b.addEventListener("click", () => {
414
+ tool = b.dataset.tool;
415
+ selectButton("[data-tool]", b);
416
+ toast(THEME.toolTips[tool]);
417
+ });
418
+ });
419
+
420
+ document.querySelectorAll("[data-size]").forEach((b) => {
421
+ b.addEventListener("click", () => {
422
+ saveCells();
423
+ gridSize = Number(b.dataset.size);
424
+ selectButton("[data-size]", b);
425
+ loadCells();
426
+ draw();
427
+ toast("Now painting on " + gridSize + " by " + gridSize + " paper.");
428
+ });
429
+ });
430
+
431
+ document.getElementById("clear").addEventListener("click", () => {
432
+ cells = freshCells();
433
+ saveCells();
434
+ draw();
435
+ toast(THEME.clearedMessage);
436
+ });
437
+
438
+ // Turn the drawing into a PNG file, right on this computer.
439
+ document.getElementById("download").addEventListener("click", () => {
440
+ const art = document.createElement("canvas");
441
+ const scale = 16;
442
+ art.width = gridSize * scale;
443
+ art.height = gridSize * scale;
444
+ const artCtx = art.getContext("2d");
445
+ artCtx.fillStyle = THEME.paper;
446
+ artCtx.fillRect(0, 0, art.width, art.height);
447
+ for (let i = 0; i < cells.length; i++) {
448
+ if (cells[i] === "") continue;
449
+ const x = (i % gridSize) * scale;
450
+ const y = Math.floor(i / gridSize) * scale;
451
+ artCtx.fillStyle = cells[i];
452
+ artCtx.fillRect(x, y, scale, scale);
453
+ }
454
+ const link = document.createElement("a");
455
+ link.download = THEME.fileName + ".png";
456
+ link.href = art.toDataURL("image/png");
457
+ link.click();
458
+ toast(THEME.savedMessage);
459
+ });
460
+
461
+ // ----- start the studio -----
462
+ loadCells();
463
+ draw();
464
+ messageBox.textContent = THEME.hint;
465
+ `;
466
+ }
467
+ export const artScaffold = {
468
+ id: 'art',
469
+ label: 'Pixel Studio',
470
+ emoji: '🎨',
471
+ ageNote: 'Calm and creative. Make art at your own pace.',
472
+ themes: [freeDraw, petPortraits],
473
+ files(theme, prettyName) {
474
+ return {
475
+ 'index.html': buildHtml(theme, prettyName),
476
+ 'style.css': buildCss(theme),
477
+ 'game.js': buildJs(theme),
478
+ };
479
+ },
480
+ starterPrompts(theme) {
481
+ return [...extrasFor(theme.id).prompts];
482
+ },
483
+ };
484
+ export default artScaffold;