blun-king-cli 9.1.6 → 9.1.8

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 (28) hide show
  1. package/bin/launcher-mode.js +1 -0
  2. package/bin/launcher-runtime.js +60 -29
  3. package/bin/standard-tools-bootstrap.js +218 -0
  4. package/blun.mjs +4 -2
  5. package/package.json +50 -47
  6. package/standard-skills/blun-app-design-system/SKILL.md +30 -0
  7. package/standard-skills/blun-app-design-system/provenance.json +17 -0
  8. package/standard-skills/blun-app-design-system/references/tokens.md +48 -0
  9. package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -0
  10. package/standard-tools/language-guard/blun_language_guard.py +686 -0
  11. package/standard-tools/language-guard/check_diacritics.py +353 -0
  12. package/standard-tools/language-guard/guard_service_client.py +82 -0
  13. package/standard-tools/language-guard/language_quality.py +172 -0
  14. package/standard-tools/language-guard/translation_guard.py +916 -0
  15. package/standard-tools/manifest.json +76 -0
  16. package/skills/design-taste-frontend/SKILL.md +0 -1206
  17. package/skills/full-output-enforcement/SKILL.md +0 -49
  18. package/skills/high-end-visual-design/SKILL.md +0 -98
  19. package/skills/image-to-code/SKILL.md +0 -1228
  20. package/skills/industrial-brutalist-ui/SKILL.md +0 -92
  21. package/skills/minimalist-ui/SKILL.md +0 -85
  22. package/skills/motion-design-taste/SKILL.md +0 -74
  23. package/skills/premortem/SKILL.md +0 -148
  24. package/skills/redesign-existing-projects/SKILL.md +0 -178
  25. package/skills/screenshot-lesen/SKILL.md +0 -52
  26. package/skills/stitch-design-taste/DESIGN.md +0 -121
  27. package/skills/stitch-design-taste/SKILL.md +0 -184
  28. package/skills/web-lesen/SKILL.md +0 -58
@@ -11,6 +11,7 @@ const LAUNCHER_HELP_TEXT = [
11
11
  ' blun Starts the CLI without automatically attaching Telegram.',
12
12
  ' king Starts the same CLI and automatically attaches the configured Telegram channel.',
13
13
  ' Both use the same version, account, model, and commands.',
14
+ ' blun tools list shows optional tools; enable writes only explicit choices.',
14
15
  '',
15
16
  ].join('\n');
16
17
 
@@ -16,13 +16,17 @@ const {
16
16
  launcherModeFromArgv,
17
17
  } = require('./launcher-mode');
18
18
  const { installManagedTelegramPlugin } = require('./plugin-bootstrap');
19
+ const {
20
+ availableStandardToolNames,
21
+ seedStandardDesignSkill,
22
+ seedStandardTools,
23
+ } = require('./standard-tools-bootstrap');
19
24
  const { CORE_LOADED_MESSAGE } = require('./core-bootstrap');
20
25
  const { tryAcquireUpdateLease } = require('./update-lease');
21
26
  const { runExplicitUpdate, runUpdateNotice } = require('./update-notice');
22
27
  const {
23
28
  ensurePrivateDirectory,
24
29
  securePrivateFile,
25
- securePrivateTree,
26
30
  writePrivateFile,
27
31
  } = require('./private-paths');
28
32
 
@@ -164,10 +168,25 @@ function exitCodeForChild(result, loaded) {
164
168
  return 1;
165
169
  }
166
170
 
171
+ function installTelegramForProfile(packageRoot, blunDir) {
172
+ installManagedTelegramPlugin({ packageRoot, blunDir });
173
+ const channelsDir = path.join(blunDir, 'channels');
174
+ const channelDir = path.join(channelsDir, 'telegram');
175
+ ensurePrivateDirectory(channelsDir);
176
+ ensurePrivateDirectory(channelDir);
177
+ const envFile = path.join(channelDir, '.env');
178
+ if (!fs.existsSync(envFile)) {
179
+ writePrivateFile(envFile, 'BLUN_TELEGRAM_BOT_TOKEN=HIER_DEIN_BOT_TOKEN_EINFUEGEN\n');
180
+ } else {
181
+ securePrivateFile(envFile);
182
+ }
183
+ }
184
+
167
185
  async function runLauncher(options = {}) {
168
186
  const callerCwd = process.cwd();
169
187
  const mode = options.mode || launcherModeFromArgv(process.argv);
170
188
  const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
189
+ const explicitToolsRequest = String(ARGS[0] || '').toLowerCase() === 'tools';
171
190
  const noticeResult = options.noticeLease
172
191
  ? { acquired: true, lease: options.noticeLease }
173
192
  : await tryAcquireUpdateLease({ packageRoot: PKG, role: 'notice' });
@@ -197,6 +216,40 @@ async function runLauncher(options = {}) {
197
216
  }
198
217
  await installProbe.lease.release();
199
218
 
219
+ if (explicitToolsRequest) {
220
+ const action = String(ARGS[1] || '').toLowerCase();
221
+ const available = availableStandardToolNames({ packageRoot: PKG });
222
+ if (action === 'list' && ARGS.length === 2) {
223
+ process.stdout.write(`${available.join('\n')}\n`);
224
+ return;
225
+ }
226
+ if (action === 'enable' && ARGS.length > 2) {
227
+ const privatePaths = resolveLauncherPrivatePaths();
228
+ const selected = [...new Set(ARGS.slice(2))];
229
+ const unknown = selected.find((name) => !available.includes(name));
230
+ if (unknown) {
231
+ process.stderr.write(`Unbekanntes Standard-Tool: ${unknown}\n`);
232
+ process.exitCode = 1;
233
+ return;
234
+ }
235
+ const mcpNames = selected.filter((name) => name !== 'telegram');
236
+ if (mcpNames.length > 0) {
237
+ seedStandardTools({
238
+ packageRoot: PKG,
239
+ blunDir: privatePaths.blunHome,
240
+ names: mcpNames,
241
+ });
242
+ }
243
+ if (selected.includes('telegram')) {
244
+ installTelegramForProfile(PKG, privatePaths.blunHome);
245
+ }
246
+ process.stdout.write(`${selected.join('\n')}\n`);
247
+ return;
248
+ }
249
+ process.stderr.write('Usage: blun tools list | blun tools enable <name...>\n');
250
+ process.exitCode = 1;
251
+ return;
252
+ }
200
253
  if (ARGS.some((arg) => arg === '--version' || arg === '-V')) {
201
254
  process.stdout.write(`${readPackageVersion()}\n`);
202
255
  return;
@@ -253,37 +306,15 @@ async function runLauncher(options = {}) {
253
306
  const cfgFile = path.join(blunDir, 'config.toml');
254
307
  if (fs.existsSync(cfgFile)) securePrivateFile(cfgFile);
255
308
 
256
- // --- 3. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
257
- if (mode === LAUNCHER_MODES.KING) {
258
- installManagedTelegramPlugin({ packageRoot: PKG, blunDir });
259
- const channelsDir = path.join(blunDir, 'channels');
260
- const channelDir = path.join(channelsDir, 'telegram');
261
- ensurePrivateDirectory(channelsDir);
262
- ensurePrivateDirectory(channelDir);
263
- const envFile = path.join(channelDir, '.env');
264
- if (!fs.existsSync(envFile)) {
265
- writePrivateFile(envFile, 'BLUN_TELEGRAM_BOT_TOKEN=HIER_DEIN_BOT_TOKEN_EINFUEGEN\n');
266
- } else {
267
- securePrivateFile(envFile);
268
- }
269
- }
309
+ // --- 3. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
310
+ seedStandardDesignSkill({ packageRoot: PKG, blunDir });
270
311
 
271
- // --- 3b. Built-in-Skills installieren (fehlende ergaenzen, nie
272
- // vorhandene ueberschreiben User-Anpassungen bleiben) -----------
273
- const skillsSrc = path.join(PKG, 'skills');
274
- const skillsDst = path.join(blunDir, 'skills');
275
- if (fs.existsSync(skillsSrc)) {
276
- if (fs.existsSync(skillsDst)) securePrivateTree(skillsDst);
277
- else ensurePrivateDirectory(skillsDst);
278
- for (const entry of fs.readdirSync(skillsSrc)) {
279
- const dst = path.join(skillsDst, entry);
280
- if (!fs.existsSync(dst)) {
281
- fs.cpSync(path.join(skillsSrc, entry), dst, { recursive: true });
282
- }
283
- }
312
+ // --- 4. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
313
+ if (mode === LAUNCHER_MODES.KING) {
314
+ installTelegramForProfile(PKG, blunDir);
284
315
  }
285
316
 
286
- // --- 4. Start -----------------------------------------------------------
317
+ // --- 5. Start -----------------------------------------------------------
287
318
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
288
319
  env.BLUN_HOME = blunDir;
289
320
  env.BLUN_MODEL_MAX_COMPLETION_TOKENS = env.BLUN_MODEL_MAX_COMPLETION_TOKENS || '32768';
@@ -0,0 +1,218 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const {
8
+ ensurePrivateDirectory,
9
+ securePrivateFile,
10
+ securePrivateTree,
11
+ writePrivateFile,
12
+ } = require('./private-paths');
13
+
14
+ function isObject(value) {
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ function readCatalogue(packageRoot) {
19
+ const manifestPath = path.join(packageRoot, 'standard-tools', 'manifest.json');
20
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
21
+ if (!isObject(parsed) || parsed.version !== 1 || !isObject(parsed.tools)) {
22
+ throw new Error(`Ungueltiger Standard-Tools-Katalog: ${manifestPath}`);
23
+ }
24
+ for (const [name, tool] of Object.entries(parsed.tools)) {
25
+ if (!isObject(tool) || !Array.isArray(tool.detectTokens)
26
+ || !tool.detectTokens.every((token) => typeof token === 'string' && token.length > 0)) {
27
+ throw new Error(`Ungueltiger Standard-Tool-Eintrag: ${name}`);
28
+ }
29
+ if (tool.platforms !== undefined
30
+ && (!Array.isArray(tool.platforms)
31
+ || !tool.platforms.every((platform) => platform === 'darwin' || platform === 'win32'))) {
32
+ throw new Error(`Ungueltige Standard-Tool-Plattform: ${name}`);
33
+ }
34
+ if (tool.owner === 'user-mcp'
35
+ && (!isObject(tool.config) || typeof tool.config.enabled !== 'boolean')) {
36
+ throw new Error(`Standard-Tool braucht einen Aktivierungszustand: ${name}`);
37
+ }
38
+ }
39
+ return parsed;
40
+ }
41
+
42
+ function readMcpDocument(configPath) {
43
+ if (!fs.existsSync(configPath)) return { mcpServers: {} };
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
47
+ } catch (error) {
48
+ throw new Error(`Ungueltige MCP-Konfiguration (JSON): ${configPath}`, { cause: error });
49
+ }
50
+ if (!isObject(parsed) || (parsed.mcpServers !== undefined && !isObject(parsed.mcpServers))) {
51
+ throw new Error(`Ungueltige MCP-Konfiguration: ${configPath}`);
52
+ }
53
+ return parsed.mcpServers === undefined ? { ...parsed, mcpServers: {} } : parsed;
54
+ }
55
+
56
+ function serverSearchText(name, config) {
57
+ const values = [name];
58
+ if (isObject(config)) {
59
+ if (typeof config.command === 'string') values.push(config.command);
60
+ if (Array.isArray(config.args)) {
61
+ values.push(...config.args.filter((value) => typeof value === 'string'));
62
+ }
63
+ if (typeof config.url === 'string') values.push(config.url);
64
+ }
65
+ return values.join('\n').toLowerCase();
66
+ }
67
+
68
+ function hasEquivalentServer(servers, name, tool) {
69
+ if (Object.hasOwn(servers, name)) return true;
70
+ const tokens = tool.detectTokens.map((token) => token.toLowerCase());
71
+ return Object.entries(servers).some(([serverName, config]) => {
72
+ const searchText = serverSearchText(serverName, config);
73
+ return tokens.some((token) => searchText.includes(token));
74
+ });
75
+ }
76
+
77
+ function writeMcpDocument(configPath, document) {
78
+ const temporaryPath = path.join(
79
+ path.dirname(configPath),
80
+ `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
81
+ );
82
+ try {
83
+ writePrivateFile(temporaryPath, `${JSON.stringify(document, null, 2)}\n`);
84
+ fs.renameSync(temporaryPath, configPath);
85
+ } catch (error) {
86
+ fs.rmSync(temporaryPath, { force: true });
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ function bundledToolConfig(name, tool, { packageRoot, blunDir, platform, env }) {
92
+ const config = structuredClone(tool.config);
93
+ config.enabled = true;
94
+ if (name === 'blun-language-guard') {
95
+ config.command = platform === 'win32' ? 'python' : 'python3';
96
+ config.args = [
97
+ path.join(
98
+ packageRoot,
99
+ 'standard-tools',
100
+ 'language-guard',
101
+ 'blun_language_guard.py',
102
+ ),
103
+ 'serve',
104
+ ];
105
+ config.env = {
106
+ ...(isObject(config.env) ? config.env : {}),
107
+ PYTHONDONTWRITEBYTECODE: '1',
108
+ PYTHONUTF8: '1',
109
+ BLUN_LANGUAGE_GUARD_KEY_FILE: path.join(
110
+ blunDir,
111
+ 'secrets',
112
+ 'blun-language-guard-signing.key',
113
+ ),
114
+ };
115
+ }
116
+ if (name === 'windows-mcp' && platform === 'win32') {
117
+ const localUvx = typeof env.USERPROFILE === 'string'
118
+ ? path.join(env.USERPROFILE, '.local', 'bin', 'uvx.exe')
119
+ : undefined;
120
+ config.command = localUvx !== undefined && fs.existsSync(localUvx) ? localUvx : 'uvx';
121
+ }
122
+ return config;
123
+ }
124
+
125
+ function availableStandardToolNames({
126
+ packageRoot,
127
+ platform = process.platform,
128
+ }) {
129
+ const catalogue = readCatalogue(packageRoot);
130
+ return Object.entries(catalogue.tools)
131
+ .filter(([, tool]) => tool.owner === 'user-mcp' || tool.owner === 'managed-plugin')
132
+ .filter(([, tool]) => !Array.isArray(tool.platforms) || tool.platforms.includes(platform))
133
+ .map(([name]) => name);
134
+ }
135
+
136
+ function seedStandardTools({
137
+ packageRoot,
138
+ blunDir,
139
+ names,
140
+ platform = process.platform,
141
+ env = process.env,
142
+ }) {
143
+ const catalogue = readCatalogue(packageRoot);
144
+ if (!Array.isArray(names) || names.length === 0) {
145
+ throw new Error('Mindestens ein Standard-Tool muss ausdruecklich ausgewaehlt werden.');
146
+ }
147
+ const selectedNames = [...new Set(names)];
148
+ for (const name of selectedNames) {
149
+ const tool = catalogue.tools[name];
150
+ if (!isObject(tool) || tool.owner !== 'user-mcp') {
151
+ throw new Error(`Unbekanntes Standard-Tool: ${name}`);
152
+ }
153
+ if (Array.isArray(tool.platforms) && !tool.platforms.includes(platform)) {
154
+ throw new Error(`Standard-Tool ist auf dieser Plattform nicht verfuegbar: ${name}`);
155
+ }
156
+ }
157
+ const configPath = path.join(blunDir, 'mcp.json');
158
+ ensurePrivateDirectory(blunDir);
159
+ if (fs.existsSync(configPath)) securePrivateFile(configPath);
160
+ const document = readMcpDocument(configPath);
161
+ const servers = { ...document.mcpServers };
162
+ const added = [];
163
+
164
+ for (const name of selectedNames) {
165
+ const tool = catalogue.tools[name];
166
+ if (hasEquivalentServer(servers, name, tool)) continue;
167
+ servers[name] = bundledToolConfig(name, tool, {
168
+ packageRoot,
169
+ blunDir,
170
+ platform,
171
+ env,
172
+ });
173
+ added.push(name);
174
+ }
175
+
176
+ if (added.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
177
+ return { added, configPath };
178
+ }
179
+
180
+ function seedStandardDesignSkill({ packageRoot, blunDir }) {
181
+ const sourcePath = path.join(
182
+ packageRoot,
183
+ 'standard-skills',
184
+ 'blun-app-design-system',
185
+ );
186
+ const sourceStat = fs.lstatSync(sourcePath);
187
+ if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
188
+ throw new Error(`Ungueltiger BLUN-Standard-Skill: ${sourcePath}`);
189
+ }
190
+
191
+ const skillsDir = path.join(blunDir, 'skills');
192
+ ensurePrivateDirectory(skillsDir);
193
+ const destinationPath = path.join(skillsDir, 'blun-app-design-system');
194
+ const installed = !fs.existsSync(destinationPath);
195
+ if (!installed) {
196
+ const destinationStat = fs.lstatSync(destinationPath);
197
+ if (!destinationStat.isDirectory() || destinationStat.isSymbolicLink()) {
198
+ throw new Error(`Ungueltiges Ziel fuer BLUN-Standard-Skill: ${destinationPath}`);
199
+ }
200
+ } else {
201
+ ensurePrivateDirectory(destinationPath);
202
+ }
203
+
204
+ fs.cpSync(sourcePath, destinationPath, {
205
+ recursive: true,
206
+ force: true,
207
+ errorOnExist: false,
208
+ });
209
+ securePrivateTree(destinationPath);
210
+ return { installed, updated: !installed, destinationPath };
211
+ }
212
+
213
+ module.exports = {
214
+ availableStandardToolNames,
215
+ readCatalogue,
216
+ seedStandardDesignSkill,
217
+ seedStandardTools,
218
+ };
package/blun.mjs CHANGED
@@ -492529,8 +492529,10 @@ var AuthFlowController = class {
492529
492529
  availableModels,
492530
492530
  availableProviders
492531
492531
  });
492532
- if (await this.refreshManagedAccountContext() === void 0) {
492533
- this.enterLoginRequiredStartupState();
492532
+ const accountContext = await this.refreshManagedAccountContextResult();
492533
+ if (accountContext.kind !== "ready") {
492534
+ if (accountContext.kind === "unauthenticated") this.enterLoginRequiredStartupState();
492535
+ else this.enterManagedAccountUnavailableStartupState(accountContext.kind);
492534
492536
  return;
492535
492537
  }
492536
492538
  if (selected === void 0) return;
package/package.json CHANGED
@@ -1,48 +1,51 @@
1
- {
2
- "name": "blun-king-cli",
3
- "version": "9.1.6",
4
- "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
- "license": "MIT",
6
- "bin": {
7
- "blun": "bin/blun.js",
8
- "king": "bin/king.js"
9
- },
10
- "scripts": {
11
- "test": "node --test test/*.test.js",
12
- "postinstall": "node scripts/fix-node-pty-perms.js"
13
- },
14
- "engines": {
15
- "node": ">=24.15.0"
16
- },
17
- "os": [
18
- "darwin",
19
- "win32"
20
- ],
21
- "cpu": [
22
- "x64",
23
- "arm64"
24
- ],
25
- "dependencies": {},
26
- "optionalDependencies": {
27
- "@mariozechner/clipboard": "^0.3.9",
28
- "node-pty": "^1.1.0"
29
- },
30
- "files": [
31
- "bin/",
32
- "blun.mjs",
33
- "dist-web/",
34
- "native/",
35
- "scripts/fix-node-pty-perms.js",
36
- "telegram-plugin/",
37
- "LIESMICH.txt",
38
- "skills/"
39
- ],
40
- "keywords": [
41
- "blun",
42
- "ai",
43
- "agent",
44
- "cli",
45
- "assistant"
46
- ],
47
- "author": "BLUN / Mayk Biletti"
1
+ {
2
+ "name": "blun-king-cli",
3
+ "version": "9.1.8",
4
+ "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "blun": "bin/blun.js",
8
+ "king": "bin/king.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/*.test.js",
12
+ "postinstall": "node scripts/fix-node-pty-perms.js"
13
+ },
14
+ "engines": {
15
+ "node": "\u003e=24.15.0"
16
+ },
17
+ "os": [
18
+ "darwin",
19
+ "win32"
20
+ ],
21
+ "cpu": [
22
+ "x64",
23
+ "arm64"
24
+ ],
25
+ "dependencies": {
26
+
27
+ },
28
+ "optionalDependencies": {
29
+ "@mariozechner/clipboard": "^0.3.9",
30
+ "node-pty": "^1.1.0"
31
+ },
32
+ "files": [
33
+ "bin/",
34
+ "blun.mjs",
35
+ "dist-web/",
36
+ "native/",
37
+ "scripts/fix-node-pty-perms.js",
38
+ "telegram-plugin/",
39
+ "LIESMICH.txt",
40
+ "standard-tools/",
41
+ "standard-skills/"
42
+ ],
43
+ "keywords": [
44
+ "blun",
45
+ "ai",
46
+ "agent",
47
+ "cli",
48
+ "assistant"
49
+ ],
50
+ "author": "BLUN / Mayk Biletti"
48
51
  }
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: blun-app-design-system
3
+ description: Apply the canonical BLUN application design language to desktop and web product interfaces while preserving the current product structure, tokens, accessibility, and runtime behavior.
4
+ ---
5
+
6
+ # BLUN App Design System
7
+
8
+ Use this skill for BLUN application screens, components, dialogs, navigation, chat, workspaces, settings, and related product UI.
9
+
10
+ ## Source order
11
+
12
+ 1. Read the target project's current design tokens and nearby components.
13
+ 2. Treat the implemented product as authoritative. Read [references/tokens.md](references/tokens.md) for the verified baseline.
14
+ 3. Use the archived visual reference only as inspiration. Never copy its old palette, typography, product names, or CSS into a runtime.
15
+ 4. Preserve the existing information architecture and interaction model unless the task explicitly changes them.
16
+
17
+ ## Build rules
18
+
19
+ - Use neutral dark and light surfaces with BLUN blue as the single primary accent.
20
+ - Use Geist for UI text and JetBrains Mono for code, values, and terminal-like content.
21
+ - Keep operational surfaces compact, predictable, and easy to scan. Use cards only for repeated items, dialogs, and genuinely framed tools.
22
+ - Prefer semantic tokens over literal component colors. Extend both themes when a new semantic color is required.
23
+ - Use familiar icons for actions, visible focus states, stable control dimensions, and reduced-motion fallbacks.
24
+ - Keep text within its container at desktop and mobile widths. Do not size type directly from viewport width.
25
+ - Preserve accessibility contrast, keyboard behavior, and native platform expectations.
26
+ - Do not overwrite a product stylesheet with a bundled reference. Make focused edits in the target's existing source.
27
+
28
+ ## Verification
29
+
30
+ Run `node scripts/check-blun-design.mjs <css-file>` from this skill directory for a token drift check. Then verify the actual interface in a real browser at representative desktop and mobile sizes. The token check is not a visual test.
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "blun-app-design-system",
3
+ "authority": "implemented-product",
4
+ "sources": {
5
+ "visualReference": {
6
+ "role": "historical-input-only",
7
+ "sha256": "F821EFEC89EB32628C3028F1FE34E83BF284B96E0FCFB6C647137DC93774DA64",
8
+ "normalizedCssSha256": "925260FBCC490B5C4EAC955ACD983BCCBF8FF35AFF8B1E374D1810C4D31FC83A"
9
+ },
10
+ "implementedProduct": {
11
+ "role": "authoritative",
12
+ "version": "1.0.110",
13
+ "file": "style.css",
14
+ "normalizedCssSha256": "956F156F9F944ED5AAB639C71C85A3989ABB6FC7505F17B3F7076E520781369F"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,48 @@
1
+ # Canonical BLUN Tokens
2
+
3
+ These values were verified against the current BLUN Code 1.0.110 product source. The target project's own newer implementation wins if it has intentionally advanced.
4
+
5
+ ## Core palette
6
+
7
+ ```css
8
+ :root {
9
+ --bg-sidebar: #0a0a0b;
10
+ --bg-primary: #17181a;
11
+ --bg-secondary: #1f2022;
12
+ --bg-card: #1c1d1f;
13
+ --border: #2a2c2f;
14
+ --text-primary: #f4f4f5;
15
+ --text-secondary: #a6a7ab;
16
+ --text-muted: #76777b;
17
+ --brand: #0261ff;
18
+ --brand-text-dark: #4d8bff;
19
+ --brand-text-light: #0051d6;
20
+ --font: "Geist", "Inter Tight", "Segoe UI Variable Text", "Segoe UI", sans-serif;
21
+ --mono: "JetBrains Mono Variable", "JetBrains Mono", "Cascadia Code", Consolas, monospace;
22
+ }
23
+
24
+ :root[data-theme="light"] {
25
+ --bg-sidebar: #ececee;
26
+ --bg-primary: #ffffff;
27
+ --bg-secondary: #f6f6f7;
28
+ --bg-card: #fbfbfc;
29
+ --border: #e2e2e5;
30
+ --text-primary: #17181a;
31
+ --text-secondary: #57585c;
32
+ --text-muted: #8a8b90;
33
+ }
34
+ ```
35
+
36
+ ## Shape and motion
37
+
38
+ - Compact controls normally use 5-8 px radii; larger overlays may use 8-12 px.
39
+ - Pills are reserved for statuses, compact toggles, and genuinely pill-shaped controls.
40
+ - Use restrained 120-200 ms transitions and disable nonessential movement under reduced motion.
41
+ - Use subtle borders and controlled shadows to establish hierarchy; avoid decorative gradients and floating decoration.
42
+
43
+ ## Layout
44
+
45
+ - Keep navigation, chat, and work output as stable application regions.
46
+ - Use `minmax(0, 1fr)` in flexible grid tracks so long content cannot force overflow.
47
+ - Give chat and composer a shared readable measure and stable gutters.
48
+ - Keep toolbars and icon controls dimensionally stable across hover, loading, and dynamic labels.
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { lstatSync, readFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+
6
+ const MAX_CSS_BYTES = 5 * 1024 * 1024;
7
+ const requiredColors = new Map([
8
+ ['--brand', '#0261ff'],
9
+ ['--bg-sidebar', '#0a0a0b'],
10
+ ['--bg-primary', '#17181a'],
11
+ ['--text-primary', '#f4f4f5'],
12
+ ]);
13
+
14
+ function fail(message) {
15
+ process.stderr.write(`BLUN design check failed: ${message}\n`);
16
+ process.exitCode = 1;
17
+ }
18
+
19
+ const input = process.argv[2];
20
+ if (input === undefined) {
21
+ fail('pass one CSS file');
22
+ } else {
23
+ const cssPath = resolve(input);
24
+ const stat = lstatSync(cssPath);
25
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_CSS_BYTES) {
26
+ fail('input must be a regular CSS file no larger than 5 MiB');
27
+ } else {
28
+ const css = readFileSync(cssPath, 'utf8');
29
+ const declarations = new Map();
30
+ for (const match of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;{}]+);/giu)) {
31
+ if (!declarations.has(match[1].toLowerCase())) {
32
+ declarations.set(match[1].toLowerCase(), match[2].trim().toLowerCase());
33
+ }
34
+ }
35
+
36
+ for (const [name, expected] of requiredColors) {
37
+ if (declarations.get(name) !== expected) fail(`${name} must be ${expected}`);
38
+ }
39
+ if (!/\bgeist\b/u.test(declarations.get('--font') ?? '')) {
40
+ fail('--font must include Geist');
41
+ }
42
+ if (!/jetbrains mono/u.test(declarations.get('--mono') ?? '')) {
43
+ fail('--mono must include JetBrains Mono');
44
+ }
45
+ if (/Fraunces|#2b6cff|#5b8dff/iu.test(css)) {
46
+ fail('legacy palette or typography detected');
47
+ }
48
+ }
49
+ }