stoa-mcp 0.1.2 → 0.1.4

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 CHANGED
@@ -32,48 +32,60 @@ This creates a `.stoa/` folder with everything Stoa needs:
32
32
  ```
33
33
  .stoa/
34
34
  moodboard/notes.md ← your design direction (colors, layout, style)
35
+ moodboard/tokens.json ← auto-generated machine-readable design tokens
35
36
  context.md ← dependencies, conventions, brand voice
36
37
  lessons.md ← project memory (grows automatically)
37
38
  guardrails/ ← rules the AI must follow
38
39
  roles/ ← AI personas (Builder, Fixer, Planner)
40
+ presets/ ← your saved custom style presets
39
41
  specs/ ← saved specifications
40
42
  ```
41
43
 
44
+ Every new project starts with the **Clean** style preset (white, minimal, Linear-style). Every spec you generate will include these design tokens automatically.
45
+
42
46
  ### 2. Set up your moodboard (optional but powerful)
43
47
 
44
- Open the moodboard file:
48
+ Your project already has a working design system. Check it:
45
49
 
46
50
  ```bash
47
- stoa edit moodboard
51
+ stoa moodboard
48
52
  ```
49
53
 
50
- Replace the template with your design preferences:
54
+ **Want a different style?** Pick a preset:
51
55
 
52
- ```markdown
53
- # Design Direction
54
- Minimal and dark like Linear
56
+ ```bash
57
+ stoa moodboard preset
58
+ ```
55
59
 
56
- # Colors
57
- Primary: #E8C872
58
- Background: #1A1A1A
59
- Text: #F5F5F5
60
+ Use arrow keys to browse 4 built-in presets with live previews:
61
+ - **Clean** — White, minimal, Linear/Vercel style
62
+ - **Dark** — Dark background, muted accents, GitHub/Raycast style
63
+ - **Warm** — Cream tones, friendly SaaS feel
64
+ - **Bold** — High contrast, sharp corners, brutalist
60
65
 
61
- # Layout
62
- Sidebar navigation left, main content right
66
+ Or apply directly: `stoa moodboard preset dark`
63
67
 
64
- # Typography
65
- Clean sans-serif, large headings
68
+ **Want to customize?** Edit interactively in the terminal:
69
+
70
+ ```bash
71
+ stoa moodboard edit
66
72
  ```
67
73
 
68
- That's it. Four lines. Every spec you generate will include these design tokens.
74
+ Walks you through each field (colors, typography, layout) one by one. Press Enter to keep, type a new value to change, or `q` to quit anytime.
69
75
 
70
- **Have a screenshot of a design you like?** Drop it in `.stoa/moodboard/` and run:
76
+ **Have a screenshot of a design you like?** Run:
71
77
 
72
78
  ```bash
73
79
  stoa moodboard describe
74
80
  ```
75
81
 
76
- If you have an Anthropic API key, Stoa will analyze the image and write the design system for you. If not, it prints a prompt you can paste into any Claude chat along with your screenshot.
82
+ This opens the moodboard folder in Finder — drop your screenshots in, press Enter. If you have an Anthropic API key, Stoa analyzes the images and writes the design system for you. If not, it prints a prompt you can paste into any Claude chat.
83
+
84
+ **Save your custom style** for reuse across projects:
85
+
86
+ ```bash
87
+ stoa moodboard save-preset my-brand
88
+ ```
77
89
 
78
90
  ### 3. Refine your idea
79
91
 
@@ -185,12 +197,11 @@ Stage 1 will reference your existing files, components, and design system. The s
185
197
 
186
198
  ## Changing the Design
187
199
 
188
- Update your moodboard, then refine:
200
+ Switch preset, edit interactively, or both:
189
201
 
190
202
  ```bash
191
- stoa edit moodboard
192
- # Change colors, layout, style...
193
-
203
+ stoa moodboard preset dark # switch to dark theme
204
+ stoa moodboard edit # tweak individual values
194
205
  stoa refine "Redesign the app to match the updated design system"
195
206
  ```
196
207
 
@@ -206,6 +217,7 @@ All files in `.stoa/` are optional. Use what you need, ignore what you don't.
206
217
  |------|-------------|----------|
207
218
  | `moodboard/notes.md` | Design direction: colors, layout, typography | Web apps, UI projects |
208
219
  | `moodboard/tokens.json` | Auto-generated machine-readable design values | Generated by `stoa moodboard sync` |
220
+ | `presets/*.json` | Custom saved style presets | Reuse across projects |
209
221
  | `context.md` | Dependencies, conventions, brand voice | All projects |
210
222
  | `lessons.md` | Past mistakes — auto-grows, prevents repeats | All projects (grows over time) |
211
223
  | `guardrails/*.md` | Rules the AI must follow (e.g. "don't delete code") | All projects |
@@ -253,62 +265,190 @@ Every future refine includes past lessons as failure modes to avoid. Your projec
253
265
 
254
266
  ## CLI Reference
255
267
 
268
+ ### Setup
269
+
270
+ ```bash
271
+ stoa init
272
+ ```
273
+ Creates `.stoa/` in the current directory with the Clean style preset, 5 guardrails, and 3 roles. Run this once per project.
274
+
275
+ ```bash
276
+ stoa edit moodboard # open moodboard in VS Code/Cursor
277
+ stoa edit context # open context.md
278
+ stoa edit lessons # open lessons.md
279
+ ```
280
+ Opens files in the best available editor. Detection order: Cursor → VS Code → `$EDITOR` → macOS default → nano.
281
+
282
+ ---
283
+
284
+ ### Moodboard
285
+
286
+ ```bash
287
+ stoa moodboard
288
+ ```
289
+ Shows current moodboard status: active style, color count, image count, and available commands.
290
+
291
+ ```bash
292
+ stoa moodboard preset
293
+ ```
294
+ **Interactive.** Browse 4 built-in presets (Clean, Dark, Warm, Bold) + any custom presets with arrow keys. Shows a live preview with colors, typography, and references. Press **Enter** to apply, **q** to cancel.
295
+
296
+ You can also apply directly without the picker:
297
+ ```bash
298
+ stoa moodboard preset dark
299
+ ```
300
+
301
+ ```bash
302
+ stoa moodboard edit
303
+ ```
304
+ **Interactive.** Walks through each field one by one: Design Direction → Colors (each individually) → Typography → Layout → Component Style → References. Press **Enter** to keep current value. Type a new value to replace. Type **q** to quit at any point.
305
+
306
+ ```bash
307
+ stoa moodboard describe
308
+ ```
309
+ **Interactive.** Opens the `.stoa/moodboard/` folder in Finder so you can drag screenshots in. Press **Enter** when ready. If you have an API key, Stoa analyzes the images with AI and writes the design system automatically. Without an API key, it prints a prompt you can paste into any Claude chat alongside your screenshot.
310
+
311
+ ```bash
312
+ stoa moodboard sync
313
+ ```
314
+ Regenerates `tokens.json` from `notes.md`. Usually happens automatically after preset or edit, but run this if you edited `notes.md` by hand.
315
+
316
+ ```bash
317
+ stoa moodboard save-preset my-brand
318
+ ```
319
+ Saves the current moodboard as `.stoa/presets/my-brand.json`. Reusable across projects — shows up in `stoa moodboard preset` picker.
320
+
321
+ ---
322
+
323
+ ### Refine
324
+
325
+ ```bash
326
+ stoa refine "Build a waitlist page with email signup and referral system"
327
+ ```
328
+ Runs the 5-stage pipeline. After completion, shows an interactive menu:
329
+
330
+ | Key | Action | Notes |
331
+ |-----|--------|-------|
332
+ | **b** | Build with Claude Code | Launches Claude Code with the spec |
333
+ | **c** | Copy spec to clipboard | Re-copy (Stage 1 is auto-copied on finish) |
334
+ | **e** | Export as markdown | Writes to `specs/<slug>/spec.md` in project root |
335
+ | **v** | View spec files | Opens spec directory in Finder |
336
+ | **q** | Done | Exits |
337
+
338
+ **Options:**
256
339
  ```bash
257
- # Setup
258
- stoa init # Create .stoa/ folder with templates
259
- stoa edit moodboard # Open moodboard in your editor
260
- stoa edit context # Open context.md in your editor
261
- stoa edit lessons # Open lessons.md in your editor
262
-
263
- # Moodboard
264
- stoa moodboard sync # Generate tokens.json from notes.md
265
- stoa moodboard describe # AI-analyze screenshots in moodboard/
266
- stoa moodboard describe --overwrite # Overwrite existing notes.md
267
-
268
- # Refine
269
- stoa refine "your task" # Run 5-stage pipeline
270
- stoa refine "task" --mode api # Force API mode (needs API key)
271
- stoa refine "task" --mode clipboard # Get prompts without AI calls
272
-
273
- # Specs
274
- stoa specs list # List all saved specs
275
- stoa specs show <name> # View a spec's contents
276
- # Export (also available as [e] in the post-refine menu)
277
- # Writes to specs/<slug>/spec.md in your project root
278
-
279
- # Scenarios
280
- stoa scenarios list # List scenarios for latest spec
340
+ stoa refine "task" --mode api # Force Anthropic API (needs key)
341
+ stoa refine "task" --mode claude-code # Force Claude Code CLI
342
+ stoa refine "task" --mode clipboard # Get prompts without AI calls (free)
343
+ stoa refine "task" --role planner # Use a specific role
344
+ stoa refine "task" --stages clarify,structure # Run specific stages only
345
+ ```
346
+
347
+ ---
348
+
349
+ ### Specs
350
+
351
+ ```bash
352
+ stoa specs list # List all saved specs with dates and stage count
353
+ stoa specs show <name> # Print a spec's contents to terminal
354
+ ```
355
+
356
+ Specs are saved in `.stoa/specs/<slug>/` with one markdown file per stage. The `[e]` export writes a combined `spec.md` to the visible `specs/` folder in your project root.
357
+
358
+ ---
359
+
360
+ ### Scenarios
361
+
362
+ ```bash
363
+ stoa scenarios list # List scenarios for the latest spec
364
+ stoa scenarios list <name> # List scenarios for a specific spec
281
365
  stoa scenarios run # Walk through scenarios interactively
282
366
  stoa scenarios run <name> # Run scenarios for a specific spec
367
+ ```
368
+
369
+ **Interactive.** Each scenario shows GIVEN (what to set up) and EXPECTED (what to check). Press **y** for pass, **n** for fail, **s** to skip. Shows a summary at the end.
370
+
371
+ ---
372
+
373
+ ### Review
374
+
375
+ ```bash
376
+ stoa review # Review the latest spec
377
+ stoa review <name> # Review a specific spec
378
+ ```
379
+
380
+ **Interactive.** Opens each stage for review. Accept, edit, or skip. After editing, optionally re-runs affected pipeline stages.
381
+
382
+ ---
383
+
384
+ ### Build & Verify
283
385
 
284
- # Guardrails & Roles
285
- stoa guardrails list # List active guardrails
286
- stoa guardrails show <name> # View a guardrail
287
- stoa roles list # List available roles
288
- stoa roles show <name> # View a role
386
+ ```bash
387
+ stoa build # Build the latest spec with Claude Code
388
+ stoa build <name> # Build a specific spec
389
+ stoa verify # Run blind test verification
390
+ stoa verify <name> # Verify a specific spec
391
+ ```
392
+
393
+ Build gives you a choice: build all at once or subtask by subtask. Verify runs the scenarios interactively after the build.
394
+
395
+ ---
396
+
397
+ ### Guardrails & Roles
398
+
399
+ ```bash
400
+ stoa guardrails list # List active guardrails
401
+ stoa guardrails show <name> # View a guardrail's content
402
+ stoa guardrails add <name> # Add a new guardrail
403
+ stoa guardrails remove <name> # Remove a guardrail
404
+ stoa roles list # List available roles
405
+ stoa roles show <name> # View a role's content
406
+ stoa roles add <name> # Add a new role
407
+ stoa roles remove <name> # Remove a role
408
+ ```
409
+
410
+ Guardrails are rules injected into every refine (e.g. "don't delete existing code"). Roles are AI personas used via `--role` flag.
411
+
412
+ ---
413
+
414
+ ### Config
289
415
 
290
- # Config
291
- stoa config # View current config
416
+ ```bash
417
+ stoa config # View current settings
292
418
  stoa config set apiKey <key> # Set Anthropic API key
293
- stoa config set model <model> # Set model (default: claude-sonnet-4-20250514)
294
- stoa config set mode <mode> # Set mode: api, claude-code, clipboard
419
+ stoa config set model <model> # Set model (default: claude-sonnet-4-6)
420
+ stoa config set mode <mode> # Set default mode: api, claude-code, clipboard
295
421
  ```
296
422
 
297
423
  ---
298
424
 
299
425
  ## Execution Modes
300
426
 
301
- Stoa has three ways to run the AI pipeline:
427
+ | Mode | How it works | You need | Cost |
428
+ |------|-------------|----------|------|
429
+ | `api` | Direct Anthropic API call | API key (`stoa config set apiKey`) | ~$0.05/refine |
430
+ | `claude-code` | Pipes to Claude Code CLI | Claude Code installed + subscription | Included in subscription |
431
+ | `clipboard` | Returns prompts — no AI calls | Nothing | Free |
302
432
 
303
- | Mode | How it works | You need |
304
- |------|-------------|----------|
305
- | `api` | Direct Anthropic API call | API key (`stoa config set apiKey`) |
306
- | `claude-code` | Pipes to Claude Code CLI | Claude Code installed + subscription |
307
- | `clipboard` | Returns prompts — no AI calls | Nothing (free) |
433
+ Stoa auto-detects: API key `api`, Claude Code in PATH → `claude-code`, otherwise → `clipboard`.
434
+
435
+ In clipboard mode, Stoa prints each stage's prompt. Paste into any AI chat (Claude, ChatGPT, Cursor) and copy the response back. Same pipeline, just manual.
436
+
437
+ ---
438
+
439
+ ## Keyboard Shortcuts
308
440
 
309
- Stoa auto-detects: if you have an API key it uses `api`, if Claude Code is installed it uses `claude-code`, otherwise `clipboard`.
441
+ All interactive commands support these:
310
442
 
311
- In clipboard mode, Stoa prints each stage's prompt. You paste it into any AI chat (Claude, ChatGPT, Cursor) and copy the response back. Everything works — just manually.
443
+ | Context | Key | Action |
444
+ |---------|-----|--------|
445
+ | Any prompt | `q` / `quit` / `exit` | Cancel and go back |
446
+ | Arrow key menus | `↑` `↓` or `k` `j` | Navigate |
447
+ | Arrow key menus | `Enter` | Select |
448
+ | Arrow key menus | `q` | Cancel |
449
+ | Post-refine menu | `b` `c` `e` `v` `q` | See table above |
450
+ | Scenario runner | `y` `n` `s` | Pass / Fail / Skip |
451
+ | Ctrl+C | Always | Force quit |
312
452
 
313
453
  ---
314
454
 
@@ -327,18 +467,24 @@ Add Stoa as an MCP server in Cursor. Create or edit `.cursor/mcp.json` in your p
327
467
  }
328
468
  ```
329
469
 
330
- Find the global path with:
331
-
470
+ Find the global path:
332
471
  ```bash
333
- npm root -g
472
+ echo "$(npm root -g)/stoa-mcp/dist/index.js"
334
473
  ```
335
474
 
336
475
  Then in Cursor's Agent chat:
337
-
338
476
  ```
339
477
  Use the refine_task tool with title: "My App" and description: "description of what I want"
340
478
  ```
341
479
 
480
+ ## Use with Claude Code
481
+
482
+ Stoa works directly with Claude Code. After refining:
483
+
484
+ 1. Press `[b]` in the post-refine menu — launches Claude Code automatically
485
+ 2. Or copy the spec and paste it: `claude "Read the spec in .stoa/specs/<name>/ and build it"`
486
+ 3. Or use `stoa build` for the full guided experience
487
+
342
488
  ---
343
489
 
344
490
  ## How It Works
@@ -389,6 +535,12 @@ The spec references existing files by name and says "add to" instead of "rebuild
389
535
  - `Fixer` — fixes bugs from failure context
390
536
  - `Planner` — breaks down large tasks
391
537
 
538
+ **4 Style Presets:**
539
+ - `Clean` — white, minimal, Linear/Vercel (applied by default)
540
+ - `Dark` — dark background, violet accents, GitHub/Raycast
541
+ - `Warm` — cream tones, amber accents, Cal.com/Stripe
542
+ - `Bold` — high contrast, sharp corners, brutalist
543
+
392
544
  ---
393
545
 
394
546
  ## The Stoa Desktop App
@@ -402,7 +554,7 @@ The CLI is the free version. The full loop lives in the Stoa desktop app:
402
554
  - Task hierarchy (parent → subtask → fix task)
403
555
  - Dashboard with spec scores across all tasks
404
556
 
405
- **Same pipeline, full GUI.** Coming soon at [stoafactory.com](https://stoafactory.com).
557
+ **Same pipeline, full GUI.** Coming soon at [stoafactory.com](https://stoafactory.dev).
406
558
 
407
559
  ---
408
560
 
@@ -0,0 +1 @@
1
+ export declare function runMoodboardEdit(projectDir: string): Promise<void>;
@@ -0,0 +1,245 @@
1
+ import chalk from "chalk";
2
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { syncMoodboard } from "../storage/moodboard-sync.js";
6
+ function writeln(text = "") {
7
+ process.stdout.write(text + "\n");
8
+ }
9
+ const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
10
+ function parseMoodboard(notesPath) {
11
+ const defaults = {
12
+ designDirection: "",
13
+ colors: {},
14
+ layout: "",
15
+ typography: "",
16
+ componentStyle: "",
17
+ references: "",
18
+ };
19
+ if (!existsSync(notesPath))
20
+ return defaults;
21
+ const raw = readFileSync(notesPath, "utf-8");
22
+ const sections = {};
23
+ const parts = raw.split(/^# /m);
24
+ for (const part of parts) {
25
+ const trimmed = part.trim();
26
+ if (!trimmed)
27
+ continue;
28
+ const newlineIdx = trimmed.indexOf("\n");
29
+ if (newlineIdx === -1)
30
+ continue;
31
+ const heading = trimmed.slice(0, newlineIdx).trim();
32
+ const body = trimmed.slice(newlineIdx + 1).replace(HTML_COMMENT_RE, "").trim();
33
+ if (body)
34
+ sections[heading] = body;
35
+ }
36
+ // Parse colors
37
+ const colors = {};
38
+ if (sections["Colors"]) {
39
+ const lines = sections["Colors"].split("\n");
40
+ for (const line of lines) {
41
+ const match = line.match(/^([A-Za-z][A-Za-z0-9 /]*?)\s*:\s*(#[0-9A-Fa-f]{6})\b/);
42
+ if (match) {
43
+ colors[match[1].trim()] = match[2];
44
+ }
45
+ }
46
+ }
47
+ return {
48
+ designDirection: sections["Design Direction"] ?? "",
49
+ colors,
50
+ layout: sections["Layout"] ?? "",
51
+ typography: sections["Typography"] ?? "",
52
+ componentStyle: sections["Component Style"] ?? "",
53
+ references: sections["References"] ?? "",
54
+ };
55
+ }
56
+ const QUIT_COMMANDS = new Set(["q", "quit", "exit"]);
57
+ function prompt(rl, question) {
58
+ return new Promise((resolve) => {
59
+ rl.question(question, (answer) => {
60
+ if (QUIT_COMMANDS.has(answer.trim().toLowerCase())) {
61
+ resolve(null);
62
+ }
63
+ else {
64
+ resolve(answer);
65
+ }
66
+ });
67
+ });
68
+ }
69
+ export async function runMoodboardEdit(projectDir) {
70
+ const notesPath = join(projectDir, ".stoa", "moodboard", "notes.md");
71
+ const current = parseMoodboard(notesPath);
72
+ const rl = createInterface({
73
+ input: process.stdin,
74
+ output: process.stdout,
75
+ });
76
+ writeln();
77
+ writeln(chalk.bold("Edit Moodboard"));
78
+ writeln(chalk.dim("Enter to keep current value. Type new value to replace. Type 'q' to quit."));
79
+ writeln();
80
+ let changed = false;
81
+ // Helper to handle a text field
82
+ async function editField(label, currentValue) {
83
+ writeln(chalk.bold(label));
84
+ if (currentValue) {
85
+ writeln(chalk.dim(` Current: ${currentValue}`));
86
+ }
87
+ const answer = await prompt(rl, chalk.cyan(" New: "));
88
+ if (answer === null)
89
+ return "quit";
90
+ if (answer.trim()) {
91
+ writeln(chalk.green(" ✓ Updated"));
92
+ return answer.trim();
93
+ }
94
+ writeln(chalk.dim(" ✓ Kept"));
95
+ return currentValue;
96
+ }
97
+ // Design Direction
98
+ const dir = await editField("Design Direction", current.designDirection);
99
+ if (dir === "quit") {
100
+ rl.close();
101
+ writeln(chalk.dim("\nCancelled."));
102
+ return;
103
+ }
104
+ if (dir !== current.designDirection) {
105
+ current.designDirection = dir;
106
+ changed = true;
107
+ }
108
+ writeln();
109
+ // Colors
110
+ writeln(chalk.bold("Colors"));
111
+ const colorKeys = Object.keys(current.colors);
112
+ if (colorKeys.length > 0) {
113
+ for (const key of colorKeys) {
114
+ const hex = current.colors[key];
115
+ const swatch = chalk.hex(hex)("██");
116
+ writeln(` ${chalk.dim(key)}: ${swatch} ${chalk.dim(hex)}`);
117
+ const newColor = await prompt(rl, chalk.cyan(` New ${key}: `));
118
+ if (newColor === null) {
119
+ rl.close();
120
+ writeln(chalk.dim("\nCancelled."));
121
+ return;
122
+ }
123
+ if (newColor.trim()) {
124
+ if (/^#[0-9A-Fa-f]{6}$/.test(newColor.trim())) {
125
+ current.colors[key] = newColor.trim();
126
+ changed = true;
127
+ writeln(chalk.green(" ✓ Updated"));
128
+ }
129
+ else {
130
+ writeln(chalk.yellow(" ✗ Invalid hex (use #RRGGBB). Kept original."));
131
+ }
132
+ }
133
+ else {
134
+ writeln(chalk.dim(" ✓ Kept"));
135
+ }
136
+ }
137
+ }
138
+ else {
139
+ writeln(chalk.dim(" No colors defined. Add them in format: Label: #HEXVAL"));
140
+ }
141
+ // Add new color?
142
+ const addColor = await prompt(rl, chalk.cyan(" Add new color? (name: #hex or Enter to skip): "));
143
+ if (addColor === null) {
144
+ rl.close();
145
+ writeln(chalk.dim("\nCancelled."));
146
+ return;
147
+ }
148
+ if (addColor.trim()) {
149
+ const match = addColor.match(/^([A-Za-z][A-Za-z0-9 ]*?)\s*:\s*(#[0-9A-Fa-f]{6})$/);
150
+ if (match) {
151
+ current.colors[match[1].trim()] = match[2];
152
+ changed = true;
153
+ writeln(chalk.green(` ✓ Added ${match[1].trim()}`));
154
+ }
155
+ else {
156
+ writeln(chalk.yellow(" ✗ Format: Name: #HEXVAL"));
157
+ }
158
+ }
159
+ writeln();
160
+ // Typography
161
+ const typo = await editField("Typography", current.typography);
162
+ if (typo === "quit") {
163
+ rl.close();
164
+ writeln(chalk.dim("\nCancelled."));
165
+ return;
166
+ }
167
+ if (typo !== current.typography) {
168
+ current.typography = typo;
169
+ changed = true;
170
+ }
171
+ writeln();
172
+ // Layout
173
+ const layout = await editField("Layout", current.layout);
174
+ if (layout === "quit") {
175
+ rl.close();
176
+ writeln(chalk.dim("\nCancelled."));
177
+ return;
178
+ }
179
+ if (layout !== current.layout) {
180
+ current.layout = layout;
181
+ changed = true;
182
+ }
183
+ writeln();
184
+ // Component Style
185
+ const style = await editField("Component Style", current.componentStyle);
186
+ if (style === "quit") {
187
+ rl.close();
188
+ writeln(chalk.dim("\nCancelled."));
189
+ return;
190
+ }
191
+ if (style !== current.componentStyle) {
192
+ current.componentStyle = style;
193
+ changed = true;
194
+ }
195
+ writeln();
196
+ // References
197
+ const refs = await editField("References", current.references);
198
+ if (refs === "quit") {
199
+ rl.close();
200
+ writeln(chalk.dim("\nCancelled."));
201
+ return;
202
+ }
203
+ if (refs !== current.references) {
204
+ current.references = refs;
205
+ changed = true;
206
+ }
207
+ rl.close();
208
+ writeln();
209
+ if (!changed) {
210
+ writeln(chalk.dim("No changes made."));
211
+ return;
212
+ }
213
+ // Write notes.md
214
+ const colorLines = Object.entries(current.colors)
215
+ .map(([key, value]) => `${key}: ${value}`)
216
+ .join("\n");
217
+ const markdown = `# Design Direction
218
+ ${current.designDirection || ""}
219
+
220
+ # Colors
221
+ ${colorLines || ""}
222
+
223
+ # Layout
224
+ ${current.layout || ""}
225
+
226
+ # Typography
227
+ ${current.typography || ""}
228
+
229
+ # Component Style
230
+ ${current.componentStyle || ""}
231
+
232
+ # References
233
+ ${current.references || ""}
234
+ `;
235
+ writeFileSync(notesPath, markdown, "utf-8");
236
+ writeln(chalk.green("✓ Saved to .stoa/moodboard/notes.md"));
237
+ // Auto-sync tokens
238
+ try {
239
+ syncMoodboard(projectDir);
240
+ writeln(chalk.green("✓ Synced tokens.json"));
241
+ }
242
+ catch {
243
+ // Non-critical
244
+ }
245
+ }
@@ -0,0 +1,2 @@
1
+ import type { PresetEntry } from "../storage/moodboard-presets.js";
2
+ export declare function pickPreset(entries: PresetEntry[]): Promise<PresetEntry | null>;
@@ -0,0 +1,125 @@
1
+ import chalk from "chalk";
2
+ function writeln(text = "") {
3
+ process.stdout.write(text + "\n");
4
+ }
5
+ function clearLines(count) {
6
+ for (let i = 0; i < count; i++) {
7
+ process.stdout.write("\x1B[1A\x1B[2K");
8
+ }
9
+ }
10
+ function renderPreview(entry) {
11
+ const { preset } = entry;
12
+ const lines = [];
13
+ const c = preset.colors;
14
+ // Color swatches line
15
+ const colorPairs = [
16
+ ["Bg", c.background],
17
+ ["Primary", c.primary],
18
+ ["Text", c.text],
19
+ ["Border", c.border],
20
+ ].filter(([, v]) => v);
21
+ const colorLine = colorPairs
22
+ .map(([label, hex]) => `${label}: ${chalk.hex(hex)("██")} ${chalk.dim(hex)}`)
23
+ .join(" ");
24
+ lines.push(` ${colorLine}`);
25
+ lines.push(` ${chalk.dim(preset.typography.split(".")[0])}`);
26
+ lines.push(` ${chalk.dim(preset.componentStyle.split(".")[0])}`);
27
+ lines.push(` ${chalk.dim(`Like: ${preset.references}`)}`);
28
+ return lines;
29
+ }
30
+ function render(entries, selected) {
31
+ const lines = [];
32
+ lines.push("");
33
+ lines.push(chalk.bold("Choose a style preset:"));
34
+ lines.push("");
35
+ let builtinDone = false;
36
+ for (let i = 0; i < entries.length; i++) {
37
+ const entry = entries[i];
38
+ // Separator between built-in and custom
39
+ if (entry.isCustom && !builtinDone) {
40
+ builtinDone = true;
41
+ lines.push(" " + chalk.dim("─".repeat(40)));
42
+ }
43
+ const isSelected = i === selected;
44
+ const marker = isSelected ? chalk.cyan("● ") : " ";
45
+ const name = isSelected ? chalk.cyan.bold(entry.preset.name) : entry.preset.name;
46
+ const desc = chalk.dim(entry.preset.description);
47
+ const tag = entry.isCustom ? chalk.dim(" (custom)") : "";
48
+ lines.push(` ${marker}${name} ${desc}${tag}`);
49
+ // Show preview for selected
50
+ if (isSelected) {
51
+ const preview = renderPreview(entry);
52
+ lines.push(...preview);
53
+ lines.push("");
54
+ }
55
+ }
56
+ lines.push(chalk.dim(" ↑↓ navigate Enter apply q cancel"));
57
+ lines.push("");
58
+ for (const line of lines) {
59
+ writeln(line);
60
+ }
61
+ return lines.length;
62
+ }
63
+ function waitForKey() {
64
+ return new Promise((resolve) => {
65
+ const { stdin } = process;
66
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
67
+ resolve("q");
68
+ return;
69
+ }
70
+ const wasRaw = stdin.isRaw;
71
+ stdin.setRawMode(true);
72
+ stdin.resume();
73
+ stdin.once("data", (data) => {
74
+ stdin.setRawMode(wasRaw);
75
+ stdin.pause();
76
+ resolve(data.toString());
77
+ });
78
+ });
79
+ }
80
+ export async function pickPreset(entries) {
81
+ if (entries.length === 0) {
82
+ writeln(chalk.red("No presets found."));
83
+ return null;
84
+ }
85
+ if (!process.stdin.isTTY) {
86
+ writeln(chalk.red("Interactive preset picker requires a TTY terminal."));
87
+ return null;
88
+ }
89
+ let selected = 0;
90
+ let lastLineCount = 0;
91
+ // Initial render
92
+ lastLineCount = render(entries, selected);
93
+ while (true) {
94
+ const key = await waitForKey();
95
+ // Ctrl+C
96
+ if (key === "\x03") {
97
+ return null;
98
+ }
99
+ // q to cancel
100
+ if (key === "q") {
101
+ return null;
102
+ }
103
+ // Enter to select
104
+ if (key === "\r" || key === "\n") {
105
+ return entries[selected];
106
+ }
107
+ // Arrow keys (escape sequences)
108
+ if (key === "\x1B[A" || key === "k") {
109
+ // Up
110
+ if (selected > 0) {
111
+ selected--;
112
+ clearLines(lastLineCount);
113
+ lastLineCount = render(entries, selected);
114
+ }
115
+ }
116
+ else if (key === "\x1B[B" || key === "j") {
117
+ // Down
118
+ if (selected < entries.length - 1) {
119
+ selected++;
120
+ clearLines(lastLineCount);
121
+ lastLineCount = render(entries, selected);
122
+ }
123
+ }
124
+ }
125
+ }
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@ import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import ora from "ora";
5
5
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
6
- import { basename, join } from "node:path";
6
+ import { basename, join, extname } from "node:path";
7
7
  import { homedir } from "node:os";
8
- import { existsSync, readFileSync, constants } from "node:fs";
8
+ import { existsSync, readFileSync, readdirSync, constants } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { refinePipeline } from "./core/refine.js";
11
11
  import { computeSpecScore } from "./core/spec-score.js";
@@ -28,7 +28,10 @@ import { scanProject } from "./storage/project-scan.js";
28
28
  import { syncMoodboard } from "./storage/moodboard-sync.js";
29
29
  import { describeMoodboard } from "./storage/moodboard-describe.js";
30
30
  import { runSpecScenarios } from "./cli/scenarios-runner.js";
31
- import { spawn } from "node:child_process";
31
+ import { spawn, execFileSync } from "node:child_process";
32
+ import { listPresets, loadPreset, applyPreset, savePreset } from "./storage/moodboard-presets.js";
33
+ import { pickPreset } from "./cli/moodboard-picker.js";
34
+ import { runMoodboardEdit } from "./cli/moodboard-edit.js";
32
35
  // ── Constants ─────────────────────────────────────────────────────────
33
36
  const STAGE_NAMES = ["clarify", "structure", "score", "harden", "finalize"];
34
37
  const STAGE_NAME_TO_NUMBER = {
@@ -120,6 +123,24 @@ function maskApiKey(value) {
120
123
  return value;
121
124
  return value.slice(0, 3) + "..." + value.slice(-4);
122
125
  }
126
+ function detectEditor() {
127
+ // Prefer cursor → code → $EDITOR → open (macOS) → nano
128
+ try {
129
+ execFileSync("which", ["cursor"], { stdio: "ignore" });
130
+ return "cursor";
131
+ }
132
+ catch { /* */ }
133
+ try {
134
+ execFileSync("which", ["code"], { stdio: "ignore" });
135
+ return "code";
136
+ }
137
+ catch { /* */ }
138
+ if (process.env.EDITOR)
139
+ return process.env.EDITOR;
140
+ if (process.platform === "darwin")
141
+ return "open";
142
+ return "nano";
143
+ }
123
144
  function copyToClipboard(text) {
124
145
  try {
125
146
  const child = spawn("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
@@ -568,7 +589,8 @@ program
568
589
  process.stderr.write(chalk.red(`File not found: ${relativePath}. Run 'stoa init' first.`) + "\n");
569
590
  process.exit(1);
570
591
  }
571
- const editor = process.env.EDITOR || "nano";
592
+ // Detect best editor: cursor → code → $EDITOR open (macOS) → nano
593
+ const editor = detectEditor();
572
594
  const child = spawn(editor, [fullPath], { stdio: "inherit" });
573
595
  child.on("exit", (code) => {
574
596
  process.exit(code ?? 0);
@@ -995,7 +1017,119 @@ scenariosCmd
995
1017
  // ── stoa moodboard ──────────────────────────────────────────────────
996
1018
  const moodboardCmd = program
997
1019
  .command("moodboard")
998
- .description("Manage project moodboard (.stoa/moodboard/)");
1020
+ .description("Manage project moodboard (.stoa/moodboard/)")
1021
+ .action(() => {
1022
+ // No subcommand → show status
1023
+ const cwd = process.cwd();
1024
+ const notesPath = join(cwd, ".stoa", "moodboard", "notes.md");
1025
+ const tokensPath = join(cwd, ".stoa", "moodboard", "tokens.json");
1026
+ if (!existsSync(join(cwd, ".stoa"))) {
1027
+ writeln(chalk.red("No .stoa/ directory. Run 'stoa init' first."));
1028
+ process.exit(1);
1029
+ }
1030
+ writeln();
1031
+ writeln(chalk.bold("Moodboard Status"));
1032
+ writeln();
1033
+ // Check notes.md
1034
+ if (existsSync(notesPath)) {
1035
+ const raw = readFileSync(notesPath, "utf-8");
1036
+ const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/^#.*$/gm, "").trim();
1037
+ if (stripped.length > 0) {
1038
+ // Extract design direction (first non-empty section)
1039
+ const dirMatch = raw.match(/# Design Direction\n([^\n#]+)/);
1040
+ if (dirMatch) {
1041
+ writeln(` Style: ${chalk.cyan(dirMatch[1].trim())}`);
1042
+ }
1043
+ }
1044
+ else {
1045
+ writeln(` Style: ${chalk.dim("(empty — run 'stoa moodboard preset' to set one)")}`);
1046
+ }
1047
+ }
1048
+ else {
1049
+ writeln(` Style: ${chalk.dim("(no notes.md)")}`);
1050
+ }
1051
+ // Check tokens
1052
+ if (existsSync(tokensPath)) {
1053
+ try {
1054
+ const tokens = JSON.parse(readFileSync(tokensPath, "utf-8"));
1055
+ const colorCount = tokens.colors ? Object.keys(tokens.colors).length : 0;
1056
+ writeln(` Colors: ${colorCount > 0 ? chalk.green(`${colorCount} defined`) : chalk.dim("none")}`);
1057
+ }
1058
+ catch {
1059
+ writeln(` Tokens: ${chalk.dim("invalid")}`);
1060
+ }
1061
+ }
1062
+ else {
1063
+ writeln(` Tokens: ${chalk.dim("not synced")}`);
1064
+ }
1065
+ // Check images
1066
+ const moodboardDir = join(cwd, ".stoa", "moodboard");
1067
+ if (existsSync(moodboardDir)) {
1068
+ try {
1069
+ const files = readdirSync(moodboardDir);
1070
+ const imageExts = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
1071
+ const imageCount = files.filter((f) => imageExts.has(extname(f).toLowerCase())).length;
1072
+ writeln(` Images: ${imageCount > 0 ? chalk.green(`${imageCount} screenshot(s)`) : chalk.dim("none")}`);
1073
+ }
1074
+ catch {
1075
+ // ignore
1076
+ }
1077
+ }
1078
+ writeln();
1079
+ writeln(chalk.bold("Commands:"));
1080
+ writeln(` ${chalk.cyan("stoa moodboard preset")} Switch style preset`);
1081
+ writeln(` ${chalk.cyan("stoa moodboard edit")} Edit interactively`);
1082
+ writeln(` ${chalk.cyan("stoa moodboard describe")} Extract design from screenshots`);
1083
+ writeln(` ${chalk.cyan("stoa moodboard sync")} Regenerate tokens.json`);
1084
+ writeln(` ${chalk.cyan("stoa moodboard save-preset")} Save current as reusable preset`);
1085
+ writeln(` ${chalk.cyan("stoa edit moodboard")} Open in editor`);
1086
+ writeln();
1087
+ });
1088
+ moodboardCmd
1089
+ .command("preset")
1090
+ .description("Choose a style preset for your moodboard")
1091
+ .argument("[name]", "Preset name (omit for interactive picker)")
1092
+ .action(async (name) => {
1093
+ const cwd = process.cwd();
1094
+ if (name) {
1095
+ // Direct apply by name
1096
+ const preset = loadPreset(cwd, name);
1097
+ if (!preset) {
1098
+ const available = listPresets(cwd).map((e) => e.id).join(", ");
1099
+ process.stderr.write(chalk.red(`Preset "${name}" not found. Available: ${available}`) + "\n");
1100
+ process.exit(1);
1101
+ }
1102
+ applyPreset(cwd, preset);
1103
+ writeln(chalk.green(`Applied "${preset.name}" preset.`));
1104
+ return;
1105
+ }
1106
+ // Interactive picker
1107
+ const entries = listPresets(cwd);
1108
+ if (entries.length === 0) {
1109
+ writeln(chalk.red("No presets found."));
1110
+ process.exit(1);
1111
+ }
1112
+ const selected = await pickPreset(entries);
1113
+ if (!selected) {
1114
+ writeln(chalk.dim("Cancelled."));
1115
+ return;
1116
+ }
1117
+ applyPreset(cwd, selected.preset);
1118
+ writeln(chalk.green(`Applied "${selected.preset.name}" preset.`));
1119
+ writeln(chalk.dim("Run 'stoa moodboard edit' to customize, or 'stoa refine' to use it."));
1120
+ });
1121
+ moodboardCmd
1122
+ .command("edit")
1123
+ .description("Edit moodboard interactively in the terminal")
1124
+ .action(async () => {
1125
+ try {
1126
+ await runMoodboardEdit(process.cwd());
1127
+ }
1128
+ catch (err) {
1129
+ process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
1130
+ process.exit(1);
1131
+ }
1132
+ });
999
1133
  moodboardCmd
1000
1134
  .command("sync")
1001
1135
  .description("Parse notes.md and generate tokens.json")
@@ -1048,6 +1182,28 @@ moodboardCmd
1048
1182
  else {
1049
1183
  mode = "clipboard";
1050
1184
  }
1185
+ // Open moodboard folder so user can drop screenshots
1186
+ const moodboardDir = join(process.cwd(), ".stoa", "moodboard");
1187
+ if (existsSync(moodboardDir)) {
1188
+ writeln(chalk.cyan("Opening moodboard folder — drop your screenshots in..."));
1189
+ if (process.platform === "darwin") {
1190
+ spawn("open", [moodboardDir], { stdio: "ignore", detached: true });
1191
+ }
1192
+ else {
1193
+ spawn("xdg-open", [moodboardDir], { stdio: "ignore", detached: true });
1194
+ }
1195
+ // Wait for user to confirm
1196
+ if (process.stdin.isTTY) {
1197
+ const { createInterface } = await import("node:readline");
1198
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1199
+ await new Promise((resolve) => {
1200
+ rl.question(chalk.dim("Press Enter when ready → "), () => {
1201
+ rl.close();
1202
+ resolve();
1203
+ });
1204
+ });
1205
+ }
1206
+ }
1051
1207
  const spinner = mode === "api" ? ora({ text: "Analyzing screenshots...", color: "cyan" }) : null;
1052
1208
  try {
1053
1209
  if (spinner)
@@ -1089,6 +1245,21 @@ moodboardCmd
1089
1245
  process.exit(1);
1090
1246
  }
1091
1247
  });
1248
+ moodboardCmd
1249
+ .command("save-preset")
1250
+ .description("Save current moodboard as a reusable preset")
1251
+ .argument("<name>", "Name for the preset")
1252
+ .action((name) => {
1253
+ try {
1254
+ const preset = savePreset(process.cwd(), name);
1255
+ writeln(chalk.green(`Saved preset "${preset.name}" to .stoa/presets/${name}.json`));
1256
+ writeln(chalk.dim("Use 'stoa moodboard preset' to apply it in other projects."));
1257
+ }
1258
+ catch (err) {
1259
+ process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
1260
+ process.exit(1);
1261
+ }
1262
+ });
1092
1263
  // ── stoa review ──────────────────────────────────────────────────────
1093
1264
  program
1094
1265
  .command("review")
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  /**
2
3
  * Stoa MCP Server entry point.
3
4
  * Registers refine_task, score_spec tools, guardrails resource, and refine prompt.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  /**
2
3
  * Stoa MCP Server entry point.
3
4
  * Registers refine_task, score_spec tools, guardrails resource, and refine prompt.
@@ -0,0 +1,30 @@
1
+ export interface PresetColors {
2
+ background: string;
3
+ surface: string;
4
+ border: string;
5
+ text: string;
6
+ text_secondary: string;
7
+ primary: string;
8
+ primary_hover: string;
9
+ [key: string]: string;
10
+ }
11
+ export interface MoodboardPreset {
12
+ name: string;
13
+ description: string;
14
+ designDirection: string;
15
+ colors: PresetColors;
16
+ typography: string;
17
+ layout: string;
18
+ componentStyle: string;
19
+ references: string;
20
+ }
21
+ export interface PresetEntry {
22
+ id: string;
23
+ preset: MoodboardPreset;
24
+ isCustom: boolean;
25
+ }
26
+ export declare function listPresets(projectDir: string): PresetEntry[];
27
+ export declare function loadPreset(projectDir: string, name: string): MoodboardPreset | null;
28
+ export declare function presetToNotesMarkdown(preset: MoodboardPreset): string;
29
+ export declare function applyPreset(projectDir: string, preset: MoodboardPreset): void;
30
+ export declare function savePreset(projectDir: string, name: string): MoodboardPreset;
@@ -0,0 +1,160 @@
1
+ import { readFileSync, readdirSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join, basename, extname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { syncMoodboard } from "./moodboard-sync.js";
5
+ function getBuiltinPresetsDir() {
6
+ return fileURLToPath(new URL("../../templates/moodboard-presets/", import.meta.url));
7
+ }
8
+ function getCustomPresetsDir(projectDir) {
9
+ return join(projectDir, ".stoa", "presets");
10
+ }
11
+ function loadPresetFile(filePath) {
12
+ try {
13
+ const raw = readFileSync(filePath, "utf-8");
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export function listPresets(projectDir) {
21
+ const entries = [];
22
+ // Built-in presets
23
+ const builtinDir = getBuiltinPresetsDir();
24
+ if (existsSync(builtinDir)) {
25
+ const files = readdirSync(builtinDir)
26
+ .filter((f) => extname(f) === ".json")
27
+ .sort();
28
+ for (const file of files) {
29
+ const preset = loadPresetFile(join(builtinDir, file));
30
+ if (preset) {
31
+ entries.push({
32
+ id: basename(file, ".json"),
33
+ preset,
34
+ isCustom: false,
35
+ });
36
+ }
37
+ }
38
+ }
39
+ // Custom presets
40
+ const customDir = getCustomPresetsDir(projectDir);
41
+ if (existsSync(customDir)) {
42
+ const files = readdirSync(customDir)
43
+ .filter((f) => extname(f) === ".json")
44
+ .sort();
45
+ for (const file of files) {
46
+ const preset = loadPresetFile(join(customDir, file));
47
+ if (preset) {
48
+ entries.push({
49
+ id: basename(file, ".json"),
50
+ preset,
51
+ isCustom: true,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+ export function loadPreset(projectDir, name) {
59
+ // Check custom first
60
+ const customPath = join(getCustomPresetsDir(projectDir), `${name}.json`);
61
+ if (existsSync(customPath)) {
62
+ return loadPresetFile(customPath);
63
+ }
64
+ // Then built-in
65
+ const builtinPath = join(getBuiltinPresetsDir(), `${name}.json`);
66
+ if (existsSync(builtinPath)) {
67
+ return loadPresetFile(builtinPath);
68
+ }
69
+ return null;
70
+ }
71
+ export function presetToNotesMarkdown(preset) {
72
+ const colorLines = Object.entries(preset.colors)
73
+ .map(([key, value]) => {
74
+ const label = key
75
+ .replace(/_/g, " ")
76
+ .replace(/\b\w/g, (c) => c.toUpperCase());
77
+ return `${label}: ${value}`;
78
+ })
79
+ .join("\n");
80
+ return `# Design Direction
81
+ ${preset.designDirection}
82
+
83
+ # Colors
84
+ ${colorLines}
85
+
86
+ # Layout
87
+ ${preset.layout}
88
+
89
+ # Typography
90
+ ${preset.typography}
91
+
92
+ # Component Style
93
+ ${preset.componentStyle}
94
+
95
+ # References
96
+ ${preset.references}
97
+ `;
98
+ }
99
+ export function applyPreset(projectDir, preset) {
100
+ const moodboardDir = join(projectDir, ".stoa", "moodboard");
101
+ mkdirSync(moodboardDir, { recursive: true });
102
+ const notesPath = join(moodboardDir, "notes.md");
103
+ const markdown = presetToNotesMarkdown(preset);
104
+ writeFileSync(notesPath, markdown, "utf-8");
105
+ // Auto-sync tokens
106
+ try {
107
+ syncMoodboard(projectDir);
108
+ }
109
+ catch {
110
+ // Non-critical
111
+ }
112
+ }
113
+ export function savePreset(projectDir, name) {
114
+ const moodboardDir = join(projectDir, ".stoa", "moodboard");
115
+ const notesPath = join(moodboardDir, "notes.md");
116
+ if (!existsSync(notesPath)) {
117
+ throw new Error("No moodboard/notes.md found. Run 'stoa init' first.");
118
+ }
119
+ // Try to load tokens.json for structured data
120
+ const tokensPath = join(moodboardDir, "tokens.json");
121
+ let tokens = {};
122
+ if (existsSync(tokensPath)) {
123
+ try {
124
+ tokens = JSON.parse(readFileSync(tokensPath, "utf-8"));
125
+ }
126
+ catch {
127
+ // ignore
128
+ }
129
+ }
130
+ // Parse notes.md sections
131
+ const raw = readFileSync(notesPath, "utf-8");
132
+ const sections = {};
133
+ const parts = raw.split(/^# /m);
134
+ for (const part of parts) {
135
+ const trimmed = part.trim();
136
+ if (!trimmed)
137
+ continue;
138
+ const newlineIdx = trimmed.indexOf("\n");
139
+ if (newlineIdx === -1)
140
+ continue;
141
+ const heading = trimmed.slice(0, newlineIdx).trim();
142
+ const body = trimmed.slice(newlineIdx + 1).replace(/<!--[\s\S]*?-->/g, "").trim();
143
+ if (body)
144
+ sections[heading] = body;
145
+ }
146
+ const preset = {
147
+ name: name.charAt(0).toUpperCase() + name.slice(1),
148
+ description: "Custom preset",
149
+ designDirection: sections["Design Direction"] ?? "",
150
+ colors: tokens.colors ?? {},
151
+ typography: sections["Typography"] ?? "",
152
+ layout: sections["Layout"] ?? "",
153
+ componentStyle: sections["Component Style"] ?? "",
154
+ references: sections["References"] ?? "",
155
+ };
156
+ const presetsDir = getCustomPresetsDir(projectDir);
157
+ mkdirSync(presetsDir, { recursive: true });
158
+ writeFileSync(join(presetsDir, `${name}.json`), JSON.stringify(preset, null, 2) + "\n", "utf-8");
159
+ return preset;
160
+ }
@@ -3,6 +3,7 @@ import { copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises"
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { toSlug } from "../utils/slug.js";
6
+ import { loadPreset, applyPreset } from "./moodboard-presets.js";
6
7
  function getTemplatesDir() {
7
8
  return fileURLToPath(new URL("../../templates/", import.meta.url));
8
9
  }
@@ -38,28 +39,39 @@ export async function initProject(name, type, noTemplates = false) {
38
39
  await mkdir(join(stoaDir, "scenarios"), { recursive: true });
39
40
  await mkdir(join(stoaDir, "specs"), { recursive: true });
40
41
  await mkdir(join(stoaDir, "moodboard"), { recursive: true });
41
- await writeFile(join(stoaDir, "moodboard", "notes.md"), `# Design Direction
42
- <!-- What should this feel like? E.g. "Minimal and fast like Linear" -->
42
+ await mkdir(join(stoaDir, "presets"), { recursive: true });
43
+ // Apply "clean" preset as default moodboard
44
+ const cleanPreset = loadPreset(process.cwd(), "clean");
45
+ if (cleanPreset) {
46
+ applyPreset(process.cwd(), cleanPreset);
47
+ }
48
+ else {
49
+ // Fallback if preset file missing
50
+ await writeFile(join(stoaDir, "moodboard", "notes.md"), `# Design Direction
51
+ Clean, modern, minimal — inspired by Linear and Vercel
43
52
 
44
53
  # Colors
45
- <!-- Hex values. E.g. Primary: #E8C872, Background: #1A1A1A -->
54
+ Background: #FFFFFF
55
+ Surface: #FAFAFA
56
+ Border: #E5E7EB
57
+ Text: #111827
58
+ Text Secondary: #6B7280
59
+ Primary: #6366F1
60
+ Primary Hover: #4F46E5
46
61
 
47
62
  # Layout
48
- <!-- E.g. "Sidebar navigation left, card-based content right" -->
63
+ Single column centered, max-width 768px. Cards for grouped content.
49
64
 
50
65
  # Typography
51
- <!-- E.g. "Sans-serif, large headings, compact body text" -->
66
+ Sans-serif (Inter or system font). Large bold headings, regular weight body.
52
67
 
53
68
  # Component Style
54
- <!-- E.g. "Rounded corners, subtle borders, no drop shadows" -->
69
+ Rounded corners (8px). Subtle borders, no heavy shadows. Filled primary buttons, ghost secondary buttons.
55
70
 
56
71
  # References
57
- <!-- Apps to emulate. E.g. "Linear task list density. Arc — sidebar tabs." -->
58
-
59
- # Images
60
- <!-- Drop files in moodboard/ folder, describe each here -->
61
- <!-- E.g. homepage-inspo.png — I want this hero layout -->
72
+ Linear, Notion, Vercel
62
73
  `, "utf-8");
74
+ }
63
75
  // Create context.md (brand voice + dependencies + conventions in one file)
64
76
  await writeFile(join(stoaDir, "context.md"), `# Project Context
65
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stoa-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The Specification Compiler for AI Agents. Transform vague tasks into executable specs with blind test scenarios.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "Bold",
3
+ "description": "High contrast, strong colors, brutalist",
4
+ "designDirection": "Bold, high-contrast, brutalist — unapologetically opinionated",
5
+ "colors": {
6
+ "background": "#FFFFFF",
7
+ "surface": "#F5F5F5",
8
+ "border": "#000000",
9
+ "text": "#000000",
10
+ "text_secondary": "#525252",
11
+ "primary": "#DC2626",
12
+ "primary_hover": "#B91C1C"
13
+ },
14
+ "typography": "Sans-serif (Helvetica, Arial, or system). Heavy weight headings, tight letter spacing. All-caps for labels.",
15
+ "layout": "Single column, full-width sections. Grid-based. Tight spacing, dense information.",
16
+ "componentStyle": "Sharp corners (2px radius). Thick borders (2px). No shadows. High contrast buttons. Monospace for data.",
17
+ "references": "Bloomberg terminal, Supreme, brutalist web design"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "Clean",
3
+ "description": "White, minimal, Linear-style",
4
+ "designDirection": "Clean, modern, minimal — inspired by Linear and Vercel",
5
+ "colors": {
6
+ "background": "#FFFFFF",
7
+ "surface": "#FAFAFA",
8
+ "border": "#E5E7EB",
9
+ "text": "#111827",
10
+ "text_secondary": "#6B7280",
11
+ "primary": "#6366F1",
12
+ "primary_hover": "#4F46E5"
13
+ },
14
+ "typography": "Sans-serif (Inter or system font). Large bold headings, regular weight body.",
15
+ "layout": "Single column centered, max-width 768px. Cards for grouped content.",
16
+ "componentStyle": "Rounded corners (8px). Subtle borders, no heavy shadows. Filled primary buttons, ghost secondary buttons.",
17
+ "references": "Linear, Notion, Vercel"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "Dark",
3
+ "description": "Dark background, muted accents, like GitHub/Raycast",
4
+ "designDirection": "Dark, focused, minimal — inspired by Raycast and GitHub",
5
+ "colors": {
6
+ "background": "#0A0A0B",
7
+ "surface": "#18181B",
8
+ "border": "#27272A",
9
+ "text": "#FAFAFA",
10
+ "text_secondary": "#A1A1AA",
11
+ "primary": "#8B5CF6",
12
+ "primary_hover": "#7C3AED"
13
+ },
14
+ "typography": "Sans-serif (Inter or system font). High contrast text on dark backgrounds.",
15
+ "layout": "Single column centered, max-width 768px. Cards with subtle borders on dark surfaces.",
16
+ "componentStyle": "Rounded corners (8px). Faint borders, no shadows, glow accents on focus. Filled primary buttons.",
17
+ "references": "GitHub dark, Raycast, Arc browser"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "Warm",
3
+ "description": "Cream tones, friendly SaaS feel",
4
+ "designDirection": "Warm, approachable, organic — inspired by Cal.com and Stripe docs",
5
+ "colors": {
6
+ "background": "#FFFBF5",
7
+ "surface": "#FFF7ED",
8
+ "border": "#E7D5C0",
9
+ "text": "#1C1917",
10
+ "text_secondary": "#78716C",
11
+ "primary": "#D97706",
12
+ "primary_hover": "#B45309"
13
+ },
14
+ "typography": "Serif headings (Georgia or Playfair Display), sans-serif body (Inter or system). Generous line height.",
15
+ "layout": "Single column centered, max-width 720px. Generous whitespace. Soft grouped sections.",
16
+ "componentStyle": "Rounded corners (12px). Soft shadows, warm borders. Filled buttons with slight rounding. Spacious padding.",
17
+ "references": "Cal.com, Stripe documentation, Notion in sepia"
18
+ }