blun-king-cli 9.1.5 → 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 (29) 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/bin/update-notice.js +37 -15
  5. package/blun.mjs +4 -2
  6. package/package.json +50 -47
  7. package/standard-skills/blun-app-design-system/SKILL.md +30 -0
  8. package/standard-skills/blun-app-design-system/provenance.json +17 -0
  9. package/standard-skills/blun-app-design-system/references/tokens.md +48 -0
  10. package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -0
  11. package/standard-tools/language-guard/blun_language_guard.py +686 -0
  12. package/standard-tools/language-guard/check_diacritics.py +353 -0
  13. package/standard-tools/language-guard/guard_service_client.py +82 -0
  14. package/standard-tools/language-guard/language_quality.py +172 -0
  15. package/standard-tools/language-guard/translation_guard.py +916 -0
  16. package/standard-tools/manifest.json +76 -0
  17. package/skills/design-taste-frontend/SKILL.md +0 -1206
  18. package/skills/full-output-enforcement/SKILL.md +0 -49
  19. package/skills/high-end-visual-design/SKILL.md +0 -98
  20. package/skills/image-to-code/SKILL.md +0 -1228
  21. package/skills/industrial-brutalist-ui/SKILL.md +0 -92
  22. package/skills/minimalist-ui/SKILL.md +0 -85
  23. package/skills/motion-design-taste/SKILL.md +0 -74
  24. package/skills/premortem/SKILL.md +0 -148
  25. package/skills/redesign-existing-projects/SKILL.md +0 -178
  26. package/skills/screenshot-lesen/SKILL.md +0 -52
  27. package/skills/stitch-design-taste/DESIGN.md +0 -121
  28. package/skills/stitch-design-taste/SKILL.md +0 -184
  29. 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
+ };
@@ -620,10 +620,19 @@ function createInstallInvocation(version, options = {}) {
620
620
  if (platform !== 'win32' && path.basename(installPrefix) === 'lib') {
621
621
  installPrefix = path.dirname(installPrefix);
622
622
  }
623
- let npmConfigPath;
624
- if (options.npmConfigPath) {
625
- npmConfigPath = fs.realpathSync(path.resolve(options.npmConfigPath));
626
- if (!fs.statSync(npmConfigPath).isFile()) throw new Error('INVALID_NPM_CONFIG_PATH');
623
+ let npmUserConfigPath;
624
+ let npmGlobalConfigPath;
625
+ if (options.npmUserConfigPath || options.npmGlobalConfigPath) {
626
+ if (!options.npmUserConfigPath || !options.npmGlobalConfigPath) {
627
+ throw new Error('INVALID_NPM_CONFIG_PATHS');
628
+ }
629
+ npmUserConfigPath = fs.realpathSync(path.resolve(options.npmUserConfigPath));
630
+ npmGlobalConfigPath = fs.realpathSync(path.resolve(options.npmGlobalConfigPath));
631
+ if (!fs.statSync(npmUserConfigPath).isFile()
632
+ || !fs.statSync(npmGlobalConfigPath).isFile()
633
+ || npmUserConfigPath === npmGlobalConfigPath) {
634
+ throw new Error('INVALID_NPM_CONFIG_PATHS');
635
+ }
627
636
  }
628
637
  return {
629
638
  command: execPath,
@@ -638,16 +647,21 @@ function createInstallInvocation(version, options = {}) {
638
647
  '--ignore-scripts=false',
639
648
  '--package-lock-only=false',
640
649
  `--prefix=${installPrefix}`,
641
- ...(npmConfigPath ? [
642
- `--userconfig=${npmConfigPath}`,
643
- `--globalconfig=${npmConfigPath}`,
650
+ ...(npmUserConfigPath ? [
651
+ `--userconfig=${npmUserConfigPath}`,
652
+ `--globalconfig=${npmGlobalConfigPath}`,
644
653
  ] : []),
645
654
  ],
646
655
  windowsVerbatimArguments: false,
647
656
  };
648
657
  }
649
658
 
650
- function createInstallEnvironment(sourceEnv, installDirectory, npmConfigPath) {
659
+ function createInstallEnvironment(
660
+ sourceEnv,
661
+ installDirectory,
662
+ npmUserConfigPath,
663
+ npmGlobalConfigPath,
664
+ ) {
651
665
  const allowedKeys = new Set([
652
666
  'appdata',
653
667
  'comspec',
@@ -685,8 +699,8 @@ function createInstallEnvironment(sourceEnv, installDirectory, npmConfigPath) {
685
699
  result.npm_config_dry_run = 'false';
686
700
  result.npm_config_ignore_scripts = 'false';
687
701
  result.npm_config_package_lock_only = 'false';
688
- result.npm_config_userconfig = npmConfigPath;
689
- result.npm_config_globalconfig = npmConfigPath;
702
+ result.npm_config_userconfig = npmUserConfigPath;
703
+ result.npm_config_globalconfig = npmGlobalConfigPath;
690
704
  return result;
691
705
  }
692
706
 
@@ -763,7 +777,8 @@ function preparePinnedInstaller(version, options = {}) {
763
777
  let closeSettled = false;
764
778
  let selectedAction;
765
779
  let installDirectory;
766
- let npmConfigPath;
780
+ let npmUserConfigPath;
781
+ let npmGlobalConfigPath;
767
782
  let disconnectTimer;
768
783
  const close = Promise.withResolvers();
769
784
  const closePromise = close.promise;
@@ -848,9 +863,15 @@ function preparePinnedInstaller(version, options = {}) {
848
863
  resolveInstallTempRoot(options),
849
864
  options.blunDir,
850
865
  );
851
- npmConfigPath = path.join(installDirectory, 'npmrc');
852
- writePrivateFile(npmConfigPath, '');
853
- invocation = createInstallInvocation(version, { ...options, npmConfigPath });
866
+ npmUserConfigPath = path.join(installDirectory, 'user-npmrc');
867
+ npmGlobalConfigPath = path.join(installDirectory, 'global-npmrc');
868
+ writePrivateFile(npmUserConfigPath, '');
869
+ writePrivateFile(npmGlobalConfigPath, '');
870
+ invocation = createInstallInvocation(version, {
871
+ ...options,
872
+ npmUserConfigPath,
873
+ npmGlobalConfigPath,
874
+ });
854
875
  child = forkImpl(path.join(__dirname, 'update-lease.js'), ['--installer-worker'], {
855
876
  detached: true,
856
877
  execPath: invocation.command,
@@ -858,7 +879,8 @@ function preparePinnedInstaller(version, options = {}) {
858
879
  env: createInstallEnvironment(
859
880
  options.env || process.env,
860
881
  installDirectory,
861
- npmConfigPath,
882
+ npmUserConfigPath,
883
+ npmGlobalConfigPath,
862
884
  ),
863
885
  stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
864
886
  windowsHide: true,
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.5",
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.