brand-intent 0.1.4 → 0.1.5
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 +4 -8
- package/cli/index.mjs +45 -347
- package/package.json +1 -1
- package/skills/brand-intent.md +103 -16
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@ Brand Intent is a set of five composable file types that encode brand knowledge
|
|
|
17
17
|
| Layer | File | Question |
|
|
18
18
|
|-------|------|----------|
|
|
19
19
|
| Identity | `.identity` | Who is this brand? |
|
|
20
|
-
| Brand | `.brand` |
|
|
21
|
-
| Format | `.format` | Where does
|
|
22
|
-
| Purpose | `.purpose` | What is this content
|
|
20
|
+
| Brand | `.brand` | How is it expressed? |
|
|
21
|
+
| Format | `.format` | Where does it appear? |
|
|
22
|
+
| Purpose | `.purpose` | What is this content for? |
|
|
23
23
|
| Composition | `.composition` | How are elements composed? |
|
|
24
24
|
|
|
25
25
|
## Get started
|
|
@@ -34,7 +34,7 @@ ext install studiostoll.brand-intent-lang
|
|
|
34
34
|
|
|
35
35
|
### 2. Run init
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Installs the Brand Intent skill for your AI agent. The skill handles everything else: onboard your brand through a guided interview, explore the Krume bakery reference, generate and validate on-brand content.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npx brand-intent init
|
|
@@ -46,10 +46,6 @@ npx brand-intent init
|
|
|
46
46
|
- [Design Decisions](DESIGN.md): reasoning behind the framework
|
|
47
47
|
- [intent.stoll.studio](https://intent.stoll.studio): project landing page
|
|
48
48
|
|
|
49
|
-
## Relationship to Impeccable
|
|
50
|
-
|
|
51
|
-
Brand Intent is complementary to [Impeccable](https://impeccable.style). Where Impeccable teaches agents *how to design well* (general craft), Brand Intent teaches them *what this brand's expression intent is* (specific identity). Together: execution quality and brand specificity.
|
|
52
|
-
|
|
53
49
|
## License
|
|
54
50
|
|
|
55
51
|
MIT
|
package/cli/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync,
|
|
4
|
-
import { join, dirname, resolve
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join, dirname, resolve } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ const PKG_ROOT = resolve(__dirname, '..');
|
|
|
11
11
|
|
|
12
12
|
const SKILL_FILE = join(PKG_ROOT, 'skills', 'brand-intent.md');
|
|
13
13
|
|
|
14
|
-
// ── Harness detection
|
|
14
|
+
// ── Harness detection ──
|
|
15
15
|
|
|
16
16
|
const HARNESSES = [
|
|
17
17
|
{ id: 'claude', label: 'Claude Code', dir: '.claude/commands' },
|
|
@@ -27,7 +27,7 @@ function detectHarnesses(cwd) {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// ── Readline
|
|
30
|
+
// ── Readline ──
|
|
31
31
|
|
|
32
32
|
let _rl;
|
|
33
33
|
function rl() {
|
|
@@ -44,45 +44,7 @@ function ask(q) {
|
|
|
44
44
|
return new Promise((res) => rl().question(q, res));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
console.log(` ${prompt}\n`);
|
|
49
|
-
options.forEach((opt, i) => {
|
|
50
|
-
console.log(` ${i + 1}) ${opt.label}`);
|
|
51
|
-
});
|
|
52
|
-
console.log('');
|
|
53
|
-
const answer = await ask(' Choose [1-' + options.length + ']: ');
|
|
54
|
-
const idx = parseInt(answer, 10) - 1;
|
|
55
|
-
if (idx >= 0 && idx < options.length) return options[idx].value;
|
|
56
|
-
return options[0].value;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function confirm(question, defaultYes = true) {
|
|
60
|
-
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
61
|
-
const answer = await ask(` ${question} (${hint}) `);
|
|
62
|
-
if (answer.trim() === '') return defaultYes;
|
|
63
|
-
return answer.trim().toLowerCase().startsWith('y');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function input(question, defaultValue) {
|
|
67
|
-
const hint = defaultValue ? ` (${defaultValue})` : '';
|
|
68
|
-
const answer = await ask(` ${question}${hint}: `);
|
|
69
|
-
return answer.trim() || defaultValue || '';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── File operations ──
|
|
73
|
-
|
|
74
|
-
function copyDirRecursive(src, dest) {
|
|
75
|
-
mkdirSync(dest, { recursive: true });
|
|
76
|
-
for (const entry of readdirSync(src)) {
|
|
77
|
-
const srcPath = join(src, entry);
|
|
78
|
-
const destPath = join(dest, entry);
|
|
79
|
-
if (statSync(srcPath).isDirectory()) {
|
|
80
|
-
copyDirRecursive(srcPath, destPath);
|
|
81
|
-
} else {
|
|
82
|
-
copyFileSync(srcPath, destPath);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
47
|
+
// ── Install skill ──
|
|
86
48
|
|
|
87
49
|
async function installSkills(cwd) {
|
|
88
50
|
if (!existsSync(SKILL_FILE)) {
|
|
@@ -94,7 +56,6 @@ async function installSkills(cwd) {
|
|
|
94
56
|
let harnesses = detectHarnesses(cwd);
|
|
95
57
|
|
|
96
58
|
if (harnesses.length === 0) {
|
|
97
|
-
// Nothing detected — ask which to install
|
|
98
59
|
console.log(' Which AI tools do you use?\n');
|
|
99
60
|
HARNESSES.forEach((h, i) => {
|
|
100
61
|
console.log(` ${i + 1}) ${h.label}`);
|
|
@@ -119,320 +80,57 @@ async function installSkills(cwd) {
|
|
|
119
80
|
writeFileSync(dest, skill, 'utf-8');
|
|
120
81
|
console.log(` \u2713 ${label} \u2014 ${dir}/brand-intent.md`);
|
|
121
82
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── Explore the example ──
|
|
125
|
-
|
|
126
|
-
function scaffoldExample(targetDir) {
|
|
127
|
-
const brandSrc = join(PKG_ROOT, 'brands', 'krume');
|
|
128
|
-
const exampleDest = join(targetDir, 'examples', 'krume');
|
|
129
|
-
|
|
130
|
-
// Copy brand files
|
|
131
|
-
copyDirRecursive(brandSrc, exampleDest);
|
|
132
|
-
console.log(' \u2713 examples/krume/krume.identity');
|
|
133
|
-
console.log(' \u2713 examples/krume/krume.brand');
|
|
134
|
-
|
|
135
|
-
// Copy one format, one purpose, one composition as starting examples
|
|
136
|
-
const examples = [
|
|
137
|
-
{ layer: 'formats', file: 'instagram-4-5-feed-portrait.format' },
|
|
138
|
-
{ layer: 'purposes', file: 'daily-bread.purpose' },
|
|
139
|
-
{ layer: 'compositions', file: 'editorial.composition' },
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
for (const { layer, file } of examples) {
|
|
143
|
-
const src = join(PKG_ROOT, layer, file);
|
|
144
|
-
if (existsSync(src)) {
|
|
145
|
-
const dest = join(exampleDest, layer, file);
|
|
146
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
147
|
-
copyFileSync(src, dest);
|
|
148
|
-
console.log(` \u2713 examples/krume/${layer}/${file}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
83
|
|
|
152
|
-
|
|
153
|
-
The Krume bakery is a fully commented reference implementation.
|
|
154
|
-
Read the .identity file first \u2014 it explains every decision.
|
|
155
|
-
Then look at how .brand derives expression intent from identity.
|
|
156
|
-
Formats, purposes, and compositions show the remaining three layers.`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── Create your own brand ──
|
|
160
|
-
|
|
161
|
-
async function scaffoldOwnBrand(targetDir) {
|
|
162
|
-
console.log('\n Let\u2019s define your brand. Answer what you can \u2014 skip the rest.\n');
|
|
163
|
-
|
|
164
|
-
const brandName = await input('Brand name');
|
|
165
|
-
if (!brandName) {
|
|
166
|
-
console.error('\n A brand needs a name. Try again.\n');
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const brandId = brandName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
171
|
-
const language = await input('Language', 'en');
|
|
172
|
-
const locale = await input('Locale', language === 'de' ? 'de-DE' : language === 'en' ? 'en-US' : `${language}-${language.toUpperCase()}`);
|
|
173
|
-
|
|
174
|
-
// Identity
|
|
175
|
-
const essence = await input('Essence \u2014 one sentence, what this brand is about');
|
|
176
|
-
const promise = await input('Promise \u2014 what people experience');
|
|
177
|
-
const tagline = await input('Tagline');
|
|
178
|
-
const archetype = await input('Archetype (e.g. craftsman, sage, rebel)');
|
|
179
|
-
|
|
180
|
-
// Voice
|
|
181
|
-
console.log('\n Voice \u2014 how the brand speaks.\n');
|
|
182
|
-
const register = await input('Register (e.g. informal/Du, formal/Sie, casual)');
|
|
183
|
-
const persona = await input('Persona \u2014 who is speaking?');
|
|
184
|
-
const rhythm = await input('Rhythm \u2014 sentence style');
|
|
185
|
-
|
|
186
|
-
console.log('\n Always rules \u2014 what the brand always does. One per line, empty line to stop.\n');
|
|
187
|
-
const always = await collectList();
|
|
188
|
-
|
|
189
|
-
console.log('\n Never rules \u2014 what the brand never does. One per line, empty line to stop.\n');
|
|
190
|
-
const never = await collectList();
|
|
191
|
-
|
|
192
|
-
// Pillars
|
|
193
|
-
console.log('');
|
|
194
|
-
const primaryPillars = await input('Primary pillars (comma-separated)');
|
|
195
|
-
const secondaryPillars = await input('Secondary pillars (comma-separated)');
|
|
196
|
-
const avoidPillars = await input('Avoid topics (comma-separated)');
|
|
197
|
-
|
|
198
|
-
// Audiences
|
|
199
|
-
console.log('\n Audiences \u2014 who the brand speaks to.\n');
|
|
200
|
-
const audiences = await collectAudiences();
|
|
201
|
-
|
|
202
|
-
// Colors
|
|
203
|
-
console.log('\n Colors \u2014 the brand palette.\n');
|
|
204
|
-
const primaryColor = await input('Primary color (hex)', '#1a1a1a');
|
|
205
|
-
const secondaryColor = await input('Secondary color (hex)', '#4a4a4a');
|
|
206
|
-
const accentColor = await input('Accent color (hex)', '#0066cc');
|
|
207
|
-
const whiteColor = await input('Background/white (hex)', '#ffffff');
|
|
208
|
-
|
|
209
|
-
// Typography
|
|
210
|
-
console.log('\n Typography\n');
|
|
211
|
-
const primaryFont = await input('Primary font name', 'Inter');
|
|
212
|
-
const primaryFontSource = await input('Font source (google/local)', 'google');
|
|
213
|
-
|
|
214
|
-
// ── Write .identity ──
|
|
215
|
-
|
|
216
|
-
let identity = `# ${brandId}.identity\n`;
|
|
217
|
-
if (essence) identity += `\nessence: ${essence}\n`;
|
|
218
|
-
if (promise) identity += `\npromise: ${promise}\n`;
|
|
219
|
-
if (tagline) identity += `\ntagline: ${tagline}\n`;
|
|
220
|
-
if (archetype) identity += `\narchetype: ${archetype}\n`;
|
|
221
|
-
|
|
222
|
-
if (register || persona || rhythm || always.length || never.length) {
|
|
223
|
-
identity += `\nvoice\n`;
|
|
224
|
-
if (register) identity += ` register: ${register}\n`;
|
|
225
|
-
if (persona) identity += ` persona: ${persona}\n`;
|
|
226
|
-
if (rhythm) identity += ` rhythm: ${rhythm}\n`;
|
|
227
|
-
if (always.length) {
|
|
228
|
-
identity += `\n always:\n`;
|
|
229
|
-
for (const rule of always) identity += ` - ${rule}\n`;
|
|
230
|
-
}
|
|
231
|
-
if (never.length) {
|
|
232
|
-
identity += `\n never:\n`;
|
|
233
|
-
for (const rule of never) identity += ` - ${rule}\n`;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (primaryPillars || secondaryPillars || avoidPillars) {
|
|
238
|
-
identity += `\npillars\n`;
|
|
239
|
-
if (primaryPillars) identity += ` primary: ${primaryPillars}\n`;
|
|
240
|
-
if (secondaryPillars) identity += ` secondary: ${secondaryPillars}\n`;
|
|
241
|
-
if (avoidPillars) identity += ` avoid: ${avoidPillars}\n`;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
for (const aud of audiences) {
|
|
245
|
-
identity += `\naudience ${aud.id}\n`;
|
|
246
|
-
identity += ` label: ${aud.label}\n`;
|
|
247
|
-
if (aud.profile) identity += ` profile: ${aud.profile}\n`;
|
|
248
|
-
if (aud.motivation) identity += ` motivation: ${aud.motivation}\n`;
|
|
249
|
-
if (aud.language) identity += ` language: ${aud.language}\n`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
writeFileSync(join(targetDir, `${brandId}.identity`), identity, 'utf-8');
|
|
253
|
-
console.log(`\n \u2713 ${brandId}.identity`);
|
|
254
|
-
|
|
255
|
-
// ── Write .brand ──
|
|
256
|
-
|
|
257
|
-
let brand = `# ${brandId}.brand \u2014 expression intent derived from ${brandId}.identity\n\n`;
|
|
258
|
-
brand += `id: ${brandId}\n`;
|
|
259
|
-
brand += `name: ${brandName}\n`;
|
|
260
|
-
brand += `language: ${language}\n`;
|
|
261
|
-
brand += `locale: ${locale}\n`;
|
|
262
|
-
|
|
263
|
-
brand += `\nbrand-colors\n`;
|
|
264
|
-
brand += ` primary: ${primaryColor}\n`;
|
|
265
|
-
brand += ` secondary: ${secondaryColor}\n`;
|
|
266
|
-
brand += ` accent1: ${accentColor}\n`;
|
|
267
|
-
brand += ` white: ${whiteColor}\n`;
|
|
268
|
-
|
|
269
|
-
brand += `\ntheme Default\n`;
|
|
270
|
-
brand += ` background: $white\n`;
|
|
271
|
-
brand += ` text-primary: $primary\n`;
|
|
272
|
-
brand += ` text-secondary: $secondary\n`;
|
|
273
|
-
brand += ` cta: $accent1\n`;
|
|
274
|
-
brand += ` divider: $secondary\n`;
|
|
275
|
-
|
|
276
|
-
brand += `\nfont primary\n`;
|
|
277
|
-
brand += ` name: ${primaryFont}\n`;
|
|
278
|
-
brand += ` fallback: sans-serif\n`;
|
|
279
|
-
brand += ` source: ${primaryFontSource}\n`;
|
|
280
|
-
|
|
281
|
-
brand += `\ntypography headline\n`;
|
|
282
|
-
brand += ` weight: 700\n`;
|
|
283
|
-
brand += ` size: 6\n`;
|
|
284
|
-
brand += ` lineHeight: 1.15\n`;
|
|
285
|
-
|
|
286
|
-
brand += `\ntypography body\n`;
|
|
287
|
-
brand += ` weight: 400\n`;
|
|
288
|
-
brand += ` size: 2.8\n`;
|
|
289
|
-
brand += ` lineHeight: 1.5\n`;
|
|
290
|
-
|
|
291
|
-
brand += `\ntypography caption\n`;
|
|
292
|
-
brand += ` weight: 400\n`;
|
|
293
|
-
brand += ` size: 2.2\n`;
|
|
294
|
-
brand += ` lineHeight: 1.4\n`;
|
|
295
|
-
|
|
296
|
-
brand += `\ntypography label\n`;
|
|
297
|
-
brand += ` weight: 600\n`;
|
|
298
|
-
brand += ` size: 2\n`;
|
|
299
|
-
brand += ` lineHeight: 1.2\n`;
|
|
300
|
-
brand += ` uppercase\n`;
|
|
301
|
-
|
|
302
|
-
brand += `\nspacing:\n`;
|
|
303
|
-
brand += ` unit: cqmin\n`;
|
|
304
|
-
brand += ` xs: 1.5\n`;
|
|
305
|
-
brand += ` s: 2\n`;
|
|
306
|
-
brand += ` m: 3\n`;
|
|
307
|
-
brand += ` l: 5\n`;
|
|
308
|
-
brand += ` xl: 6.5\n`;
|
|
309
|
-
|
|
310
|
-
writeFileSync(join(targetDir, `${brandId}.brand`), brand, 'utf-8');
|
|
311
|
-
console.log(` \u2713 ${brandId}.brand`);
|
|
312
|
-
|
|
313
|
-
// ── Write a starter .purpose ──
|
|
314
|
-
|
|
315
|
-
let purpose = `id: starter\n`;
|
|
316
|
-
purpose += `name: Starter Purpose\n`;
|
|
317
|
-
purpose += `density: light\n`;
|
|
318
|
-
purpose += `palette: dynamic\n\n`;
|
|
319
|
-
purpose += `slot primary\n`;
|
|
320
|
-
purpose += ` label: Headline\n`;
|
|
321
|
-
purpose += ` typography: headline\n`;
|
|
322
|
-
purpose += ` color: text-primary\n`;
|
|
323
|
-
purpose += ` maxLength: 60\n`;
|
|
324
|
-
purpose += ` samples:\n`;
|
|
325
|
-
purpose += ` - Your headline here\n\n`;
|
|
326
|
-
purpose += `slot secondary\n`;
|
|
327
|
-
purpose += ` label: Subline\n`;
|
|
328
|
-
purpose += ` typography: body\n`;
|
|
329
|
-
purpose += ` color: text-secondary\n`;
|
|
330
|
-
purpose += ` maxLength: 120\n`;
|
|
331
|
-
purpose += ` samples:\n`;
|
|
332
|
-
purpose += ` - Supporting text goes here\n`;
|
|
333
|
-
|
|
334
|
-
const purposesDir = join(targetDir, 'purposes');
|
|
335
|
-
mkdirSync(purposesDir, { recursive: true });
|
|
336
|
-
writeFileSync(join(purposesDir, 'starter.purpose'), purpose, 'utf-8');
|
|
337
|
-
console.log(` \u2713 purposes/starter.purpose`);
|
|
338
|
-
|
|
339
|
-
console.log(`
|
|
340
|
-
Your brand is scaffolded. Next steps:
|
|
341
|
-
- Edit ${brandId}.identity \u2014 add narratives, values, and audiences
|
|
342
|
-
- Edit ${brandId}.brand \u2014 refine colors, add themes, typography styles
|
|
343
|
-
- Replace purposes/starter.purpose with real content purposes
|
|
344
|
-
- Add formats/ and compositions/ when you\u2019re ready for layout`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function collectList() {
|
|
348
|
-
const items = [];
|
|
349
|
-
while (true) {
|
|
350
|
-
const line = await ask(' - ');
|
|
351
|
-
if (!line.trim()) break;
|
|
352
|
-
items.push(line.trim());
|
|
353
|
-
}
|
|
354
|
-
return items;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function collectAudiences() {
|
|
358
|
-
const audiences = [];
|
|
359
|
-
while (true) {
|
|
360
|
-
const id = await input('Audience ID (e.g. "regulars", empty to stop)');
|
|
361
|
-
if (!id) break;
|
|
362
|
-
const label = await input(` Label for "${id}"`);
|
|
363
|
-
const profile = await input(' Profile (demographics, behavior)');
|
|
364
|
-
const motivation = await input(' Motivation');
|
|
365
|
-
const language = await input(' Language style');
|
|
366
|
-
audiences.push({ id, label: label || id, profile, motivation, language });
|
|
367
|
-
console.log('');
|
|
368
|
-
}
|
|
369
|
-
return audiences;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// ── Interactive init flow ──
|
|
373
|
-
|
|
374
|
-
async function interactiveInit() {
|
|
375
|
-
console.log(' Brand Intent \u2014 What a brand means. Authored, structured, shared.\n');
|
|
376
|
-
|
|
377
|
-
const folder = await input('Project folder');
|
|
378
|
-
if (!folder) {
|
|
379
|
-
console.error('\n A project folder is required.\n');
|
|
380
|
-
process.exit(1);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const targetDir = resolve(process.cwd(), folder);
|
|
384
|
-
|
|
385
|
-
const mode = await choose('How do you want to start?', [
|
|
386
|
-
{ label: 'Explore the example', value: 'explore' },
|
|
387
|
-
{ label: 'Create your own brand', value: 'create' },
|
|
388
|
-
{ label: 'Just install the AI skill', value: 'skill-only' },
|
|
389
|
-
]);
|
|
390
|
-
|
|
391
|
-
// Create project directory
|
|
392
|
-
mkdirSync(targetDir, { recursive: true });
|
|
393
|
-
console.log('');
|
|
394
|
-
|
|
395
|
-
if (mode === 'explore') {
|
|
396
|
-
scaffoldExample(targetDir);
|
|
397
|
-
} else if (mode === 'create') {
|
|
398
|
-
await scaffoldOwnBrand(targetDir);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Always install skills — they're the infrastructure
|
|
402
|
-
console.log('');
|
|
403
|
-
await installSkills(targetDir);
|
|
404
|
-
|
|
405
|
-
closeRl();
|
|
406
|
-
console.log('\n Done. Happy branding.\n');
|
|
84
|
+
return harnesses;
|
|
407
85
|
}
|
|
408
86
|
|
|
409
87
|
// ── CLI entry point ──
|
|
410
88
|
|
|
411
89
|
const args = process.argv.slice(2);
|
|
412
90
|
const command = args[0];
|
|
91
|
+
const cwd = process.cwd();
|
|
413
92
|
|
|
414
93
|
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8'));
|
|
415
94
|
console.log(`Brand Intent v${pkg.version}\n`);
|
|
416
95
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
closeRl();
|
|
421
|
-
break;
|
|
96
|
+
if (command && command !== 'init' && command !== 'install-skills' && command !== '--help' && command !== '-h') {
|
|
97
|
+
console.log(` Unknown command: ${command}\n`);
|
|
98
|
+
}
|
|
422
99
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
100
|
+
if (!command || command === '--help' || command === '-h') {
|
|
101
|
+
console.log('Usage:');
|
|
102
|
+
console.log(' brand-intent init Install the Brand Intent skill for your AI agent');
|
|
103
|
+
console.log(' brand-intent install-skills Same as init (alias)');
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log('Example:');
|
|
106
|
+
console.log(' npx brand-intent init');
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
426
109
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
110
|
+
// Both "init" and "install-skills" do the same thing now
|
|
111
|
+
const installed = await installSkills(cwd);
|
|
112
|
+
closeRl();
|
|
113
|
+
|
|
114
|
+
// Show next steps with skill-specific commands
|
|
115
|
+
const hasClaudeCode = installed.some(h => h.id === 'claude');
|
|
116
|
+
const hasCursor = installed.some(h => h.id === 'cursor');
|
|
117
|
+
|
|
118
|
+
console.log('\n Done. Now open your AI agent and try:\n');
|
|
119
|
+
console.log(' "Onboard my brand"');
|
|
120
|
+
console.log(' "Show me the Krume bakery example"');
|
|
121
|
+
|
|
122
|
+
if (hasClaudeCode || hasCursor) {
|
|
123
|
+
console.log('\n Or use the skill directly:\n');
|
|
124
|
+
if (hasClaudeCode) {
|
|
125
|
+
console.log(' /brand-intent onboard Create your brand through a guided interview');
|
|
126
|
+
console.log(' /brand-intent example Scaffold the Krume reference implementation');
|
|
127
|
+
console.log(' /brand-intent validate Validate content against your brand rules');
|
|
128
|
+
console.log(' /brand-intent Discover brand files and available purposes');
|
|
129
|
+
}
|
|
130
|
+
if (hasCursor) {
|
|
131
|
+
console.log(' @brand-intent onboard Create your brand through a guided interview');
|
|
132
|
+
console.log(' @brand-intent example Scaffold the Krume reference implementation');
|
|
133
|
+
}
|
|
438
134
|
}
|
|
135
|
+
|
|
136
|
+
console.log('');
|
package/package.json
CHANGED
package/skills/brand-intent.md
CHANGED
|
@@ -6,6 +6,8 @@ description: Generate and validate on-brand content using Brand Intent files
|
|
|
6
6
|
|
|
7
7
|
**Scope:** `$ARGUMENTS`
|
|
8
8
|
|
|
9
|
+
> If `$ARGUMENTS` is "onboard": run the guided brand onboarding interview (see Onboarding section below).
|
|
10
|
+
> If `$ARGUMENTS` is "example": scaffold the Krume bakery reference implementation into `examples/krume/` and explain what each file does.
|
|
9
11
|
> If `$ARGUMENTS` names a purpose (e.g. "daily-bread", "event", "quote"): compose context from the five layers and generate content for that purpose.
|
|
10
12
|
> If `$ARGUMENTS` is "validate": read the most recent generated or provided content and run the validation checklist against the brand files.
|
|
11
13
|
> If `$ARGUMENTS` is empty: discover all `.identity`, `.brand`, `.purpose` files in the project, summarize the brand voice and list available purposes.
|
|
@@ -98,9 +100,6 @@ When generating content for a specific purpose, read all layers silently (do not
|
|
|
98
100
|
|
|
99
101
|
### Output format:
|
|
100
102
|
|
|
101
|
-
For each brand that has the requested purpose:
|
|
102
|
-
|
|
103
|
-
- If multiple brands exist, show the **brand name** as a heading
|
|
104
103
|
- Show each **slot** with its generated content — nothing else
|
|
105
104
|
- After the content, show a **compact validation summary** (pass/fail per rule, only call out failures in detail)
|
|
106
105
|
|
|
@@ -132,27 +131,115 @@ After generating or reviewing content, validate against these rules in order:
|
|
|
132
131
|
Brand Intent files follow this structure:
|
|
133
132
|
|
|
134
133
|
```
|
|
135
|
-
# Single brand
|
|
136
134
|
brandname.identity
|
|
137
135
|
brandname.brand
|
|
138
136
|
formats/*.format
|
|
139
137
|
purposes/*.purpose
|
|
140
138
|
compositions/*.composition
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
To find the active brand, look for the `.identity` and `.brand` files at the project root. There is exactly one of each per project.
|
|
142
|
+
|
|
143
|
+
## Onboarding: Create a New Brand
|
|
144
|
+
|
|
145
|
+
When `$ARGUMENTS` is "onboard", guide the user through a structured interview to create their brand files. Do NOT jump to file creation — the interview comes first.
|
|
146
|
+
|
|
147
|
+
### Interview Flow
|
|
148
|
+
|
|
149
|
+
Have a natural conversation. Don't dump all questions at once — adapt based on answers. Group questions into rounds of 2-4.
|
|
150
|
+
|
|
151
|
+
**Round 1: Identity**
|
|
152
|
+
- Brand name? (and brand ID — lowercase, no spaces, used in filenames)
|
|
153
|
+
- What does this brand do? One sentence.
|
|
154
|
+
- Language? (e.g., de, en)
|
|
155
|
+
- Essence — the irreducible truth in 3-8 words?
|
|
156
|
+
|
|
157
|
+
Listen for tone, confidence, specificity. A founder who says "we make artisan coffee" vs "we roast single-origin in a garage in Kreuzberg" gives you very different brands.
|
|
158
|
+
|
|
159
|
+
**Round 2: Voice & Personality**
|
|
160
|
+
- If this brand were a person talking to a customer, who would they be?
|
|
161
|
+
- Formal or informal?
|
|
162
|
+
- What does this brand ALWAYS do in communication? (2-3 concrete rules)
|
|
163
|
+
- What does this brand NEVER do? (2-3 concrete anti-patterns)
|
|
164
|
+
|
|
165
|
+
The user will often describe what the brand is NOT before what it IS. Capture both.
|
|
166
|
+
|
|
167
|
+
**Round 3: Visual Direction**
|
|
168
|
+
- Describe the color palette. Hex values, or a mood? (e.g., "warm earth tones", "cold Nordic blue")
|
|
169
|
+
- What fonts define this brand? (primary + body. If unknown: serif or sans-serif? Heavy or light?)
|
|
170
|
+
|
|
171
|
+
If the user describes a mood: propose a palette of 4-5 colors (primary, secondary, accent1, accent2, white/background) and confirm before proceeding.
|
|
172
|
+
|
|
173
|
+
**Round 4: Content & Audience**
|
|
174
|
+
- Who are the 1-2 main audiences? For each: who are they, what do they want, how should you talk to them?
|
|
175
|
+
- Anti-audience — someone this brand is explicitly NOT for?
|
|
176
|
+
- What content topics are on-brand? (primary pillars)
|
|
177
|
+
- What topics should the brand avoid?
|
|
178
|
+
|
|
179
|
+
### Synthesis
|
|
141
180
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
After the interview, present a Brand Brief for confirmation:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
## Brand Brief: [Brand Name]
|
|
185
|
+
ID: [brandid] | Language: [language]
|
|
186
|
+
Essence: [essence]
|
|
187
|
+
|
|
188
|
+
Voice: [persona], [register]
|
|
189
|
+
Always: [rules]
|
|
190
|
+
Never: [anti-patterns]
|
|
191
|
+
|
|
192
|
+
Colors: primary [hex], secondary [hex], accent [hex], background [hex]
|
|
193
|
+
Font: [primary font name]
|
|
194
|
+
|
|
195
|
+
Audiences: [summaries]
|
|
196
|
+
Pillars: [topics] | Avoid: [topics]
|
|
148
197
|
```
|
|
149
198
|
|
|
150
|
-
|
|
199
|
+
Ask the user to confirm or adjust. Iterate until they're happy.
|
|
200
|
+
|
|
201
|
+
### File Generation
|
|
202
|
+
|
|
203
|
+
Once confirmed, generate these files at the project root:
|
|
204
|
+
|
|
205
|
+
1. **`[brandid].identity`** — essence, promise, tagline, archetype, voice (register, persona, rhythm, always/never), pillars, audience blocks, anti-audience blocks. Follow the structure of the Krume reference.
|
|
151
206
|
|
|
152
|
-
|
|
207
|
+
2. **`[brandid].brand`** — id, name, language, locale, brand-colors, at least one theme (mapping semantic slots to color references), font blocks, typography blocks (headline, body, caption, label at minimum), spacing, dividers. Use `$name` references for theme colors.
|
|
153
208
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
3. **`purposes/starter.purpose`** — a starter purpose with slots that have on-brand sample content derived from the identity. Samples must be in the brand's language and respect voice rules.
|
|
210
|
+
|
|
211
|
+
### Rules
|
|
212
|
+
|
|
213
|
+
- **Every voice rule, value, and narrative must come from the user's answers.** Don't invent brand personality.
|
|
214
|
+
- **Sample content in purposes must match the brand's language.**
|
|
215
|
+
- **Don't skip the interview.** Even if the user says "just make a brand called X" — ask at least the essentials (essence, voice, colors, one audience).
|
|
216
|
+
- **Typography sizes use `cqmin` units.** Use these sensible defaults unless the user specified otherwise: headline 6, body 2.8, caption 2.2, label 2.
|
|
217
|
+
- **Spacing defaults:** unit cqmin, xs 1.5, s 2, m 3, l 5, xl 6.5.
|
|
218
|
+
|
|
219
|
+
## Example: Scaffold the Krume Reference
|
|
220
|
+
|
|
221
|
+
When `$ARGUMENTS` is "example", copy the Krume bakery reference files into `examples/krume/` in the current project. The Krume files are distributed with the npm package.
|
|
222
|
+
|
|
223
|
+
Run this shell command to copy them:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
node -e "
|
|
227
|
+
const pkg = require.resolve('brand-intent/package.json');
|
|
228
|
+
const root = require('path').dirname(pkg);
|
|
229
|
+
const { cpSync, mkdirSync } = require('fs');
|
|
230
|
+
const dest = 'examples/krume';
|
|
231
|
+
mkdirSync(dest, { recursive: true });
|
|
232
|
+
for (const d of ['brands/krume', 'formats', 'purposes', 'compositions']) {
|
|
233
|
+
const src = require('path').join(root, d);
|
|
234
|
+
try { cpSync(src, require('path').join(dest, d.replace('brands/krume', '')), { recursive: true }); } catch {}
|
|
235
|
+
}
|
|
236
|
+
console.log('Krume example scaffolded into examples/krume/');
|
|
237
|
+
"
|
|
238
|
+
```
|
|
157
239
|
|
|
158
|
-
|
|
240
|
+
Then explain to the user what each file does:
|
|
241
|
+
- **`.identity`** — the strategic layer: essence, voice, values, audiences. Read this first.
|
|
242
|
+
- **`.brand`** — derived expression: colors, themes, typography, spacing. Every decision traces back to identity.
|
|
243
|
+
- **`.format`** — canvas dimensions, safe zones, grid. Where content appears.
|
|
244
|
+
- **`.purpose`** — content type with semantic slots, samples, constraints. What content does.
|
|
245
|
+
- **`.composition`** — spatial arrangement of slots. How elements are placed.
|