diff --git a/.gitignore b/.gitignore index 1750d0a..7ad0d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ public/dist/ .cursor/ .claude/ +# User preferences (only the example ships) +skills/excalidraw-skill/preferences.json + # Development artifacts *.excalidraw diff --git a/skills/excalidraw-skill/SKILL.md b/skills/excalidraw-skill/SKILL.md index 31a737d..8e723e6 100644 --- a/skills/excalidraw-skill/SKILL.md +++ b/skills/excalidraw-skill/SKILL.md @@ -15,6 +15,79 @@ Run these checks **in order**: See `references/cheatsheet.md` for the full MCP-vs-REST mapping and REST API gotchas. +## Step 1: Load User Preferences + +Before creating any elements, load the user's diagram preferences. These control default font, roughness, stroke width, etc. + +### Preference Resolution Order (most specific wins) + +| Priority | Scope | Location | Persists | +|----------|-------|----------|----------| +| 1 (highest) | Session | In-memory (set via prompt during this conversation) | No — current session only | +| 2 | Folder | `.claude/excalidraw-preferences.json` in the current project root | Yes — per-project | +| 3 | Global | `~/.claude/skills/excalidraw-skill/preferences.json` | Yes — all projects | +| 4 (lowest) | Hardcoded | Server defaults (fontFamily: 1, roughness: 0, fontSize: 20, strokeWidth: 2) | — | + +### How to Load + +1. **Check folder-level first**: Read `.claude/excalidraw-preferences.json` from the current working directory (or project root). If it exists and has `defaults`, use those values. +2. **Fall back to global**: Read `~/.claude/skills/excalidraw-skill/preferences.json`. If it exists and has `defaults`, use those values. +3. **If neither exists** → run the **First-Time Setup** prompt below. +4. **Merge**: Folder preferences override global; global overrides hardcoded. Only override fields that are explicitly set. + +### First-Time Setup (Interactive) + +If no preferences file exists at either location, **prompt the user before drawing anything**: + +> **Excalidraw Preferences Setup** +> +> I don't have any saved diagram preferences yet. Let me set up your defaults so every diagram looks the way you want. + +Ask these questions (use `AskUserQuestion` tool if available, otherwise ask inline): + +1. **Font family** — Which font for all text? + - Excalifont (hand-drawn) = 1 + - Helvetica (sans-serif) = 2 + - Cascadia (monospace) = 3 + - Comic Shanns = 4 + - Nunito = 6 + - Lilita One = 7 + +2. **Roughness** — Diagram style? + - Clean/professional (roughness: 0) — recommended + - Hand-drawn sketch (roughness: 1) + - Very rough (roughness: 2) + +3. **Scope** — Where to save? + - **This session only** — don't save to disk, just use for this conversation + - **This project** — save to `.claude/excalidraw-preferences.json` in project root + - **Global (all projects)** — save to `~/.claude/skills/excalidraw-skill/preferences.json` + +Then save the preferences JSON to the chosen location: + +```json +{ + "defaults": { + "fontFamily": , + "fontSize": 20, + "roughness": , + "strokeWidth": 2 + } +} +``` + +For session-only scope, just hold the values in memory and apply them to every element in this conversation. + +### Applying Preferences + +Once loaded, apply `defaults` to **every element** that supports the property: +- `fontFamily` → all text-containing elements (text, rectangles with labels, diamonds, ellipses, arrows with labels) +- `fontSize` → text elements and labels (unless the element explicitly overrides it) +- `roughness` → all elements +- `strokeWidth` → arrows and lines + +User-specified values in individual element calls always override preferences. + ## Core Principles (Read Before Any Diagram) These principles were learned through extensive iterative use. Violating them produces bad diagrams. diff --git a/skills/excalidraw-skill/preferences.example.json b/skills/excalidraw-skill/preferences.example.json new file mode 100644 index 0000000..8998b09 --- /dev/null +++ b/skills/excalidraw-skill/preferences.example.json @@ -0,0 +1,18 @@ +{ + "_comment": "Excalidraw MCP user preferences. Copy to preferences.json to activate.", + "_fontReference": { + "1": "Excalifont (hand-drawn)", + "2": "Helvetica (sans-serif)", + "3": "Cascadia (monospace)", + "4": "Comic Shanns", + "5": "Liberation Sans", + "6": "Nunito", + "7": "Lilita One" + }, + "defaults": { + "fontFamily": 1, + "fontSize": 20, + "roughness": 0, + "strokeWidth": 2 + } +} diff --git a/skills/excalidraw-skill/references/cheatsheet.md b/skills/excalidraw-skill/references/cheatsheet.md index 9e6286d..7a04810 100644 --- a/skills/excalidraw-skill/references/cheatsheet.md +++ b/skills/excalidraw-skill/references/cheatsheet.md @@ -94,16 +94,15 @@ |---------|----------------|-----------------| | Shape labels | `"text": "My Label"` (auto-converts) | `"label": {"text": "My Label"}` | | Arrow binding | `"startElementId": "id"` / `"endElementId": "id"` | `"start": {"id": "id"}` / `"end": {"id": "id"}` | -| `fontFamily` | String `"1"` or omit | String `"1"` or omit (never a number) | +| `fontFamily` | Number or string — use value from user preferences (see Step 1 in SKILL.md) | String — use value from user preferences | | Tenant scoping | Auto (uses active tenant) | Include `X-Tenant-Id` header on every request | ### Element Creation Best Practices -- **Always set `roughness: 0`** for clean, professional diagrams (default is hand-drawn). -- **Always set `strokeWidth: 2`** on arrows for visibility. +- **Always apply user preferences** — load from Step 1 in SKILL.md and apply `fontFamily`, `roughness`, `fontSize`, `strokeWidth` to every element. - **Create shapes first, arrows second** (two separate `batch_create_elements` calls). - **Assign custom `id`** to every shape so arrows can reference it. -- **Size shapes for their text** — Virgil font is ~30% wider than standard. Use sizing formulas from SKILL.md. +- **Size shapes for their text** — use sizing formulas from SKILL.md. - `points` accepts both `[[x,y]]` tuples and `[{x,y}]` objects — normalized automatically. - **Curved arrows**: Use `"roundness": {"type": 2}` with 3+ points. **Elbowed arrows**: Use `"elbowed": true`. diff --git a/src/index.ts b/src/index.ts index 36f6f80..1b3f0c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,47 @@ const CANVAS_PORT = process.env.CANVAS_PORT || process.env.PORT || '3000'; const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || `http://localhost:${CANVAS_PORT}`; const ENABLE_CANVAS_SYNC = true; +// User preferences for element defaults (font, roughness, etc.) +// Resolution: folder-level .claude/excalidraw-preferences.json > global ~/.claude/skills/excalidraw-skill/preferences.json > hardcoded +interface ExcalidrawPreferences { + fontFamily: number; + fontSize: number; + roughness: number; + strokeWidth: number; +} + +const HARDCODED_DEFAULTS: ExcalidrawPreferences = { + fontFamily: 1, + fontSize: 20, + roughness: 0, + strokeWidth: 2, +}; + +function loadPreferences(): ExcalidrawPreferences { + const locations = [ + path.join(process.cwd(), '.claude', 'excalidraw-preferences.json'), + path.join(process.env.HOME || '~', '.claude', 'skills', 'excalidraw-skill', 'preferences.json'), + ]; + + for (const loc of locations) { + try { + if (fs.existsSync(loc)) { + const raw = JSON.parse(fs.readFileSync(loc, 'utf-8')); + if (raw?.defaults) { + logger.info(`Loaded user preferences from ${loc}`); + return { ...HARDCODED_DEFAULTS, ...raw.defaults }; + } + } + } catch (e) { + logger.warn(`Failed to read preferences from ${loc}: ${e}`); + } + } + + return HARDCODED_DEFAULTS; +} + +const USER_PREFS = loadPreferences(); + // One-time tokens for clear_canvas confirmation (token → expiry timestamp) const pendingClearTokens = new Map(); const CLEAR_TOKEN_TTL_MS = 120_000; // 2 minutes @@ -2199,8 +2240,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) if (el.type === 'text') { base.text = text ?? ''; base.originalText = text ?? ''; - base.fontSize = rest.fontSize ?? 20; - base.fontFamily = rest.fontFamily ?? 1; + base.fontSize = rest.fontSize ?? USER_PREFS.fontSize; + base.fontFamily = rest.fontFamily ?? USER_PREFS.fontFamily; base.textAlign = rest.textAlign ?? 'center'; base.verticalAlign = rest.verticalAlign ?? 'middle'; base.autoResize = rest.autoResize ?? true; @@ -2294,8 +2335,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) locked: false, text: labelText, originalText: labelText, - fontSize: isArrow ? 14 : (rest.fontSize ?? 16), - fontFamily: rest.fontFamily ?? 1, + fontSize: isArrow ? 14 : (rest.fontSize ?? USER_PREFS.fontSize), + fontFamily: rest.fontFamily ?? USER_PREFS.fontFamily, textAlign: 'center', verticalAlign: 'middle', autoResize: true,