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,193 @@
1
+ /**
2
+ * L4b: pulls the human-visible text out of project files so the output
3
+ * classifier can judge what a kid would actually read on screen.
4
+ *
5
+ * HTML: text nodes plus title, alt, and aria-label attribute values.
6
+ * JS: string literals (single, double, template) plus comments.
7
+ * Markdown and plain text: the prose as-is.
8
+ * CSS: the values of content: properties.
9
+ */
10
+ import path from 'node:path';
11
+ /** Output cap in characters. Past this we truncate with a note. */
12
+ export const EXTRACT_CHAR_CAP = 6000;
13
+ export const TRUNCATION_NOTE = '[text truncated for review]';
14
+ function decodeEntities(text) {
15
+ return text
16
+ .replace(/ /gi, ' ')
17
+ .replace(/&/gi, '&')
18
+ .replace(/&lt;/gi, '<')
19
+ .replace(/&gt;/gi, '>')
20
+ .replace(/&quot;/gi, '"')
21
+ .replace(/&#0?39;/g, "'")
22
+ .replace(/&apos;/gi, "'");
23
+ }
24
+ function extractFromHtml(content) {
25
+ const pieces = [];
26
+ // Attribute values a kid can see or hear: tooltips, image text, labels.
27
+ const attrRe = /\b(?:title|alt|aria-label)\s*=\s*("([^"]*)"|'([^']*)')/gi;
28
+ let attrMatch;
29
+ while ((attrMatch = attrRe.exec(content)) !== null) {
30
+ const value = attrMatch[2] ?? attrMatch[3] ?? '';
31
+ if (value.trim()) {
32
+ pieces.push(decodeEntities(value.trim()));
33
+ }
34
+ }
35
+ // Drop script and style bodies, then comments, then all tags.
36
+ let stripped = content
37
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, '\n')
38
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, '\n')
39
+ .replace(/<!--[\s\S]*?-->/g, '\n');
40
+ stripped = stripped.replace(/<[^>]+>/g, '\n');
41
+ for (const line of stripped.split('\n')) {
42
+ const trimmed = decodeEntities(line).trim();
43
+ if (trimmed) {
44
+ pieces.push(trimmed);
45
+ }
46
+ }
47
+ return pieces;
48
+ }
49
+ /**
50
+ * A small scanner over JS source. Collects string literal contents and
51
+ * comment text. Template literals keep their text parts; ${...} expressions
52
+ * are skipped. Not a full parser, but robust for kid-project code.
53
+ */
54
+ function extractFromJs(content) {
55
+ const pieces = [];
56
+ const push = (s) => {
57
+ const trimmed = s.trim();
58
+ if (trimmed) {
59
+ pieces.push(trimmed);
60
+ }
61
+ };
62
+ let i = 0;
63
+ const n = content.length;
64
+ while (i < n) {
65
+ const ch = content[i];
66
+ const next = i + 1 < n ? content[i + 1] : '';
67
+ if (ch === '/' && next === '/') {
68
+ const end = content.indexOf('\n', i + 2);
69
+ const stop = end === -1 ? n : end;
70
+ push(content.slice(i + 2, stop));
71
+ i = stop;
72
+ continue;
73
+ }
74
+ if (ch === '/' && next === '*') {
75
+ const end = content.indexOf('*/', i + 2);
76
+ const stop = end === -1 ? n : end;
77
+ push(content.slice(i + 2, stop).replace(/^\s*\*+/gm, ' '));
78
+ i = end === -1 ? n : end + 2;
79
+ continue;
80
+ }
81
+ if (ch === '"' || ch === "'") {
82
+ const quote = ch;
83
+ let j = i + 1;
84
+ let buf = '';
85
+ while (j < n) {
86
+ const c = content[j];
87
+ if (c === '\\' && j + 1 < n) {
88
+ buf += content[j + 1];
89
+ j += 2;
90
+ continue;
91
+ }
92
+ if (c === quote || c === '\n') {
93
+ break;
94
+ }
95
+ buf += c;
96
+ j++;
97
+ }
98
+ push(buf);
99
+ i = j + 1;
100
+ continue;
101
+ }
102
+ if (ch === '`') {
103
+ let j = i + 1;
104
+ let buf = '';
105
+ while (j < n) {
106
+ const c = content[j];
107
+ if (c === '\\' && j + 1 < n) {
108
+ buf += content[j + 1];
109
+ j += 2;
110
+ continue;
111
+ }
112
+ if (c === '$' && j + 1 < n && content[j + 1] === '{') {
113
+ // Skip the interpolation expression, tracking nested braces.
114
+ let depth = 1;
115
+ let k = j + 2;
116
+ while (k < n && depth > 0) {
117
+ if (content[k] === '{')
118
+ depth++;
119
+ else if (content[k] === '}')
120
+ depth--;
121
+ k++;
122
+ }
123
+ buf += ' ';
124
+ j = k;
125
+ continue;
126
+ }
127
+ if (c === '`') {
128
+ break;
129
+ }
130
+ buf += c;
131
+ j++;
132
+ }
133
+ push(buf);
134
+ i = j + 1;
135
+ continue;
136
+ }
137
+ i++;
138
+ }
139
+ return pieces;
140
+ }
141
+ function extractFromCss(content) {
142
+ const pieces = [];
143
+ const re = /\bcontent\s*:\s*(?:"([^"]*)"|'([^']*)')/gi;
144
+ let match;
145
+ while ((match = re.exec(content)) !== null) {
146
+ const value = (match[1] ?? match[2] ?? '').trim();
147
+ if (value) {
148
+ pieces.push(value);
149
+ }
150
+ }
151
+ return pieces;
152
+ }
153
+ /**
154
+ * Extracts the kid-visible text from a project file. Lines are deduplicated
155
+ * and the result is capped at 6,000 characters with a truncation note.
156
+ */
157
+ export function extractVisibleText(relPath, content) {
158
+ const ext = path.extname(relPath).toLowerCase();
159
+ let pieces;
160
+ switch (ext) {
161
+ case '.html':
162
+ case '.htm':
163
+ pieces = extractFromHtml(content);
164
+ break;
165
+ case '.js':
166
+ case '.mjs':
167
+ case '.cjs':
168
+ pieces = extractFromJs(content);
169
+ break;
170
+ case '.css':
171
+ pieces = extractFromCss(content);
172
+ break;
173
+ case '.md':
174
+ case '.txt':
175
+ default:
176
+ // Prose and unknown files: classify everything.
177
+ pieces = content.split('\n').map((l) => l.trim());
178
+ break;
179
+ }
180
+ const seen = new Set();
181
+ const lines = [];
182
+ for (const piece of pieces) {
183
+ if (piece && !seen.has(piece)) {
184
+ seen.add(piece);
185
+ lines.push(piece);
186
+ }
187
+ }
188
+ let joined = lines.join('\n');
189
+ if (joined.length > EXTRACT_CHAR_CAP) {
190
+ joined = `${joined.slice(0, EXTRACT_CHAR_CAP)}\n${TRUNCATION_NOTE}`;
191
+ }
192
+ return joined;
193
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Day-2 launcher: a double-clickable "Termi" file on the Desktop so a kid
3
+ * can get back in without typing a command. One file per platform:
4
+ * macOS Termi.command, Windows Termi.bat, Linux Termi.desktop.
5
+ * Everything is best effort and silent on failure.
6
+ */
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ /** The launcher file contents for a platform string (process.platform). */
11
+ export function launcherFileFor(platform) {
12
+ if (platform === 'darwin') {
13
+ return {
14
+ fileName: 'Termi.command',
15
+ content: '#!/bin/zsh\nexec termi\n',
16
+ mode: 0o755,
17
+ };
18
+ }
19
+ if (platform === 'win32') {
20
+ return {
21
+ fileName: 'Termi.bat',
22
+ content: '@echo off\r\ntermi\r\n',
23
+ mode: 0o644,
24
+ };
25
+ }
26
+ return {
27
+ fileName: 'Termi.desktop',
28
+ content: [
29
+ '[Desktop Entry]',
30
+ 'Type=Application',
31
+ 'Name=Termi',
32
+ 'Comment=Build games with your robot buddy',
33
+ 'Exec=termi',
34
+ 'Terminal=true',
35
+ '',
36
+ ].join('\n'),
37
+ mode: 0o755,
38
+ };
39
+ }
40
+ /**
41
+ * Writes the launcher onto the Desktop. Returns what it wrote, or null when
42
+ * there is no Desktop directory or the write failed. Never throws.
43
+ */
44
+ export function writeLauncher(opts = {}) {
45
+ const platform = opts.platform ?? process.platform;
46
+ const desktopDir = opts.desktopDir ?? path.join(os.homedir(), 'Desktop');
47
+ try {
48
+ if (!fs.existsSync(desktopDir) || !fs.statSync(desktopDir).isDirectory()) {
49
+ return null;
50
+ }
51
+ const file = launcherFileFor(platform);
52
+ const fullPath = path.join(desktopDir, file.fileName);
53
+ fs.writeFileSync(fullPath, file.content, { mode: file.mode });
54
+ try {
55
+ fs.chmodSync(fullPath, file.mode);
56
+ }
57
+ catch {
58
+ // chmod is best effort, mostly a no-op on Windows.
59
+ }
60
+ return { path: fullPath, fileName: file.fileName, content: file.content };
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }