@atomixstudio/mcp 1.0.33 → 1.0.35

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/dist/index.js CHANGED
@@ -1,8 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ FIGMA_DESIGN_CATALOG,
4
+ FIGMA_DESIGN_SKILL_MD,
5
+ buildFigmaPayloadsFromDS,
6
+ buildResolvers,
7
+ formatCatalogForMCP,
8
+ getDesignMethodNames,
9
+ getQueryMethodNames,
3
10
  isAllowedMethod,
4
- normalizeBridgeMethod
5
- } from "./chunk-CE6J5MJX.js";
11
+ normalizeBridgeMethod,
12
+ resolveStepParams
13
+ } from "./chunk-426RNS3G.js";
6
14
 
7
15
  // src/index.ts
8
16
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -17,8 +25,6 @@ import {
17
25
  } from "@modelcontextprotocol/sdk/types.js";
18
26
 
19
27
  // ../atomix-sync-core/dist/index.js
20
- import * as fs from "fs";
21
- import * as path from "path";
22
28
  import * as path3 from "path";
23
29
  function generateETag(meta) {
24
30
  const hash = `${meta.version}-${meta.updatedAt}`;
@@ -224,7 +230,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
224
230
  return changes;
225
231
  }
226
232
  async function fetchDesignSystem(options) {
227
- const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomixstudio.eu", etag, forceRefresh = false } = options;
233
+ const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomix.studio", etag, forceRefresh = false } = options;
228
234
  if (!dsId2) {
229
235
  throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
230
236
  }
@@ -337,74 +343,6 @@ function diffTokens(oldContent, newCssVars, format, newDarkVars) {
337
343
  }
338
344
  return { added, modified, removed, addedDark, modifiedDark, removedDark };
339
345
  }
340
- async function syncRulesFiles(options) {
341
- const { dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2 = "https://atomixstudio.eu", rulesDir = process.cwd() } = options;
342
- const rulesDirResolved = path.resolve(process.cwd(), rulesDir);
343
- const toolsToSync = [
344
- { tool: "cursor", filename: ".cursorrules" },
345
- { tool: "windsurf", filename: ".windsurfrules" },
346
- { tool: "cline", filename: ".clinerules" },
347
- { tool: "continue", filename: ".continuerules" },
348
- { tool: "copilot", filename: "copilot-instructions.md", dir: ".github" },
349
- { tool: "generic", filename: "AI_GUIDELINES.md" }
350
- ];
351
- const existingTools = toolsToSync.filter((t) => {
352
- const filePath = t.dir ? path.join(rulesDirResolved, t.dir, t.filename) : path.join(rulesDirResolved, t.filename);
353
- return fs.existsSync(filePath);
354
- });
355
- const toolsToWrite = existingTools.length > 0 ? existingTools : [{ tool: "cursor", filename: ".cursorrules" }];
356
- const results = [];
357
- for (const { tool, filename, dir } of toolsToWrite) {
358
- try {
359
- const rulesUrl = `${apiBase2}/api/ds/${dsId2}/rules?format=${tool}`;
360
- const headers = { "Content-Type": "application/json" };
361
- if (apiKey2) headers["x-api-key"] = apiKey2;
362
- const response = await fetch(rulesUrl, { headers });
363
- if (!response.ok) {
364
- results.push({
365
- tool,
366
- filename,
367
- path: dir ? `${dir}/${filename}` : filename,
368
- success: false,
369
- error: `Failed to fetch ${tool} rules: ${response.status}`
370
- });
371
- continue;
372
- }
373
- const rulesData = await response.json();
374
- if (!rulesData.content) {
375
- results.push({
376
- tool,
377
- filename,
378
- path: dir ? `${dir}/${filename}` : filename,
379
- success: false,
380
- error: `No content for ${tool} rules`
381
- });
382
- continue;
383
- }
384
- const targetDir = dir ? path.join(rulesDirResolved, dir) : rulesDirResolved;
385
- if (!fs.existsSync(targetDir)) {
386
- fs.mkdirSync(targetDir, { recursive: true });
387
- }
388
- const filePath = path.join(targetDir, filename);
389
- fs.writeFileSync(filePath, rulesData.content);
390
- results.push({
391
- tool,
392
- filename,
393
- path: dir ? `${dir}/${filename}` : filename,
394
- success: true
395
- });
396
- } catch (error) {
397
- results.push({
398
- tool,
399
- filename,
400
- path: dir ? `${dir}/${filename}` : filename,
401
- success: false,
402
- error: error instanceof Error ? error.message : String(error)
403
- });
404
- }
405
- }
406
- return results;
407
- }
408
346
  function generateCSSOutput(cssVariables, darkModeColors, deprecatedTokens = /* @__PURE__ */ new Map()) {
409
347
  const lines = [
410
348
  "/* Atomix Design System Tokens",
@@ -1163,9 +1101,7 @@ function formatSyncResponse(options) {
1163
1101
  `;
1164
1102
  });
1165
1103
  response += `
1166
- These changes are reflected in your AI tool rules files (e.g., .cursorrules).
1167
- `;
1168
- response += `Review the updated rules files to see the new guidance.
1104
+ These changes are reflected in the design system governance; run /--sync to update the skill and call getRules for the latest rules.
1169
1105
  `;
1170
1106
  }
1171
1107
  if (diff) {
@@ -1288,712 +1224,9 @@ function getTokenStats(data) {
1288
1224
  };
1289
1225
  }
1290
1226
 
1291
- // ../figma-tools/dist/index.js
1292
- var LEGACY_GROUP_PREFIX = {
1293
- background: "bg",
1294
- text: "text",
1295
- icon: "icon",
1296
- border: "border",
1297
- brand: "brand",
1298
- action: "action",
1299
- feedback: "feedback"
1300
- };
1301
- var RESERVED_COLOR_KEYS = /* @__PURE__ */ new Set(["customScales", "neutral"]);
1302
- function getColorGroupOrder(storedColors) {
1303
- if (Array.isArray(storedColors._groupOrder) && storedColors._groupOrder.length > 0) {
1304
- return storedColors._groupOrder;
1305
- }
1306
- return Object.keys(storedColors).filter(
1307
- (k) => !k.startsWith("_") && !RESERVED_COLOR_KEYS.has(k) && typeof storedColors[k] === "object"
1308
- );
1309
- }
1310
- function getGroupPrefix(storedColors, groupName) {
1311
- const custom = storedColors._groupPrefix;
1312
- if (custom && typeof custom[groupName] === "string") return custom[groupName];
1313
- return LEGACY_GROUP_PREFIX[groupName] ?? groupName;
1314
- }
1315
- function refScaleToFigmaDisplayName(scaleFromRef) {
1316
- const s = scaleFromRef.trim();
1317
- if (!s) return s;
1318
- return s.charAt(0).toUpperCase() + s.slice(1);
1319
- }
1320
- function referenceToFigmaPrimitiveName(ref, primitiveNames) {
1321
- if (!ref || typeof ref !== "string") return null;
1322
- const r = ref.trim();
1323
- const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
1324
- if (scaleStep) {
1325
- const [, scale, step] = scaleStep;
1326
- const scaleDisplay = refScaleToFigmaDisplayName(scale);
1327
- const name = `${scaleDisplay} / ${step}`;
1328
- return primitiveNames.has(name) ? name : null;
1329
- }
1330
- const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
1331
- if (radixMatch) {
1332
- const [, family, step] = radixMatch;
1333
- const scaleName = refScaleToFigmaDisplayName(family);
1334
- const name = `${scaleName} / ${step}`;
1335
- return primitiveNames.has(name) ? name : null;
1336
- }
1337
- const oneOffMatch = /^(?:new|oneOff)\.(.+)$/.exec(r);
1338
- if (oneOffMatch) {
1339
- const name = `One-off / ${oneOffMatch[1]}`;
1340
- return primitiveNames.has(name) ? name : null;
1341
- }
1342
- const whiteAlpha = /^whiteAlpha\.(.+)$/.exec(r);
1343
- if (whiteAlpha) {
1344
- const name = `WhiteAlpha / ${whiteAlpha[1]}`;
1345
- return primitiveNames.has(name) ? name : null;
1346
- }
1347
- const blackAlpha = /^blackAlpha\.(.+)$/.exec(r);
1348
- if (blackAlpha) {
1349
- const name = `BlackAlpha / ${blackAlpha[1]}`;
1350
- return primitiveNames.has(name) ? name : null;
1351
- }
1352
- return null;
1353
- }
1354
- function referenceToScaleName(ref) {
1355
- if (!ref || typeof ref !== "string") return null;
1356
- const r = ref.trim();
1357
- const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
1358
- if (scaleStep) return refScaleToFigmaDisplayName(scaleStep[1]);
1359
- const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
1360
- if (radixMatch) return refScaleToFigmaDisplayName(radixMatch[1]);
1361
- return null;
1362
- }
1363
- function getReferencedScaleNames(storedColors) {
1364
- const names = /* @__PURE__ */ new Set();
1365
- const groupOrder = getColorGroupOrder(storedColors);
1366
- for (const groupName of groupOrder) {
1367
- const group = storedColors[groupName];
1368
- if (!group || typeof group !== "object") continue;
1369
- const keys = Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
1370
- for (const key of keys) {
1371
- const value = group[key];
1372
- if (value === void 0 || typeof value !== "object" || value === null) continue;
1373
- const theme = value;
1374
- const lightScale = theme.light?.reference ? referenceToScaleName(theme.light.reference) : null;
1375
- const darkScale = theme.dark?.reference ? referenceToScaleName(theme.dark.reference) : null;
1376
- if (lightScale) names.add(lightScale);
1377
- if (darkScale) names.add(darkScale);
1378
- }
1379
- }
1380
- return names;
1381
- }
1382
- function getFullScaleFromStored(storedColors, scaleName) {
1383
- const key = scaleName.toLowerCase();
1384
- if (key === "neutral" || key === "gray") {
1385
- const neutral = storedColors.neutral;
1386
- if (!neutral?.steps || typeof neutral.steps !== "object") return null;
1387
- const out = {};
1388
- for (const [step, stepValue] of Object.entries(neutral.steps)) {
1389
- const hex = stepValue?.hex;
1390
- if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
1391
- out[step] = hex.startsWith("#") ? hex : `#${hex}`;
1392
- }
1393
- }
1394
- return Object.keys(out).length > 0 ? out : null;
1395
- }
1396
- const customScales = storedColors.customScales;
1397
- if (Array.isArray(customScales)) {
1398
- const keyNorm = key.replace(/\s+/g, "");
1399
- const scale = customScales.find(
1400
- (s) => s.name?.toLowerCase() === key || s.name?.toLowerCase().replace(/\s+/g, "") === keyNorm
1401
- );
1402
- if (scale?.steps && typeof scale.steps === "object") {
1403
- const out = {};
1404
- for (const [step, stepValue] of Object.entries(scale.steps)) {
1405
- const hex = stepValue?.hex;
1406
- if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
1407
- out[step] = hex.startsWith("#") ? hex : `#${hex}`;
1408
- }
1409
- }
1410
- if (Object.keys(out).length > 0) return out;
1411
- }
1412
- }
1413
- return null;
1414
- }
1415
- function buildSemanticRefMap(storedColors, primitiveNames) {
1416
- const out = {};
1417
- const groupOrder = getColorGroupOrder(storedColors);
1418
- for (const groupName of groupOrder) {
1419
- const group = storedColors[groupName];
1420
- if (!group || typeof group !== "object") continue;
1421
- const prefix = getGroupPrefix(storedColors, groupName);
1422
- const rowOrder = Array.isArray(group._rowOrder) ? group._rowOrder : void 0;
1423
- const keys = Array.isArray(rowOrder) ? rowOrder : Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
1424
- const toKebab = prefix === "action" || prefix === "brand" || prefix === "feedback";
1425
- for (const key of keys) {
1426
- const value = group[key];
1427
- if (value === void 0 || typeof value !== "object" || value === null) continue;
1428
- const theme = value;
1429
- let cssKey = key === "app" ? "page" : key;
1430
- if (toKebab) {
1431
- cssKey = String(key).replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1432
- }
1433
- const fullKey = `${prefix}-${cssKey}`;
1434
- const lightRef = theme.light?.reference ? referenceToFigmaPrimitiveName(theme.light.reference, primitiveNames) : null;
1435
- const darkRef = theme.dark?.reference ? referenceToFigmaPrimitiveName(theme.dark.reference, primitiveNames) : null;
1436
- if (lightRef || darkRef) {
1437
- out[fullKey] = {};
1438
- if (lightRef) out[fullKey].Light = lightRef;
1439
- if (darkRef) out[fullKey].Dark = darkRef;
1440
- }
1441
- }
1442
- }
1443
- return out;
1444
- }
1445
- function figmaColorNameWithGroup(key) {
1446
- if (key.includes("/")) {
1447
- const [group2, ...rest] = key.split("/");
1448
- const name2 = rest.join("/").trim();
1449
- if (!name2) return key;
1450
- const groupDisplay2 = group2.charAt(0).toUpperCase() + group2.slice(1).toLowerCase();
1451
- return `${groupDisplay2} / ${name2}`;
1452
- }
1453
- const firstDash = key.indexOf("-");
1454
- if (firstDash <= 0) return key;
1455
- const group = key.slice(0, firstDash);
1456
- const name = key.slice(firstDash + 1);
1457
- const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
1458
- return `${groupDisplay} / ${name}`;
1459
- }
1460
- var FIGMA_SHADOW_ORDER = {
1461
- none: 0,
1462
- xs: 1,
1463
- sm: 2,
1464
- md: 3,
1465
- lg: 4,
1466
- xl: 5,
1467
- "2xl": 6,
1468
- focus: 7
1469
- };
1470
- function tokenValueToNumber(s) {
1471
- if (typeof s !== "string" || !s.trim()) return 0;
1472
- const t = s.trim();
1473
- if (t.endsWith("rem")) {
1474
- const n2 = parseFloat(t.replace(/rem$/, ""));
1475
- return Number.isFinite(n2) ? Math.round(n2 * 16) : 0;
1476
- }
1477
- if (t.endsWith("px")) {
1478
- const n2 = parseFloat(t.replace(/px$/, ""));
1479
- return Number.isFinite(n2) ? Math.round(n2) : 0;
1480
- }
1481
- const n = parseFloat(t);
1482
- return Number.isFinite(n) ? n : 0;
1483
- }
1484
- function parseBoxShadowToFigmaEffect(shadowStr) {
1485
- const s = shadowStr.trim();
1486
- if (!s || s.toLowerCase() === "none") return null;
1487
- const parsePx = (x) => typeof x === "string" ? parseFloat(x.replace(/px$/i, "")) : NaN;
1488
- const colorMatch = s.match(/(rgba?\s*\([^)]+\)|#[0-9A-Fa-f]{3,8})\s*$/i);
1489
- const colorStr = colorMatch ? colorMatch[1].trim() : void 0;
1490
- const rest = (colorMatch ? s.slice(0, colorMatch.index) : s).trim();
1491
- const parts = rest ? rest.split(/\s+/) : [];
1492
- if (parts.length < 3) return null;
1493
- const offsetX = parsePx(parts[0]);
1494
- const offsetY = parsePx(parts[1]);
1495
- const blur = parsePx(parts[2]);
1496
- let spread = 0;
1497
- if (parts.length >= 4) spread = parsePx(parts[3]);
1498
- let r = 0, g = 0, b = 0, a = 0.1;
1499
- if (colorStr) {
1500
- const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
1501
- if (rgbaMatch) {
1502
- r = Number(rgbaMatch[1]) / 255;
1503
- g = Number(rgbaMatch[2]) / 255;
1504
- b = Number(rgbaMatch[3]) / 255;
1505
- a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1;
1506
- } else {
1507
- const hexMatch = colorStr.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/);
1508
- if (hexMatch) {
1509
- r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
1510
- g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
1511
- b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
1512
- a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 0.1;
1513
- }
1514
- }
1515
- }
1516
- if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) return null;
1517
- return {
1518
- type: "DROP_SHADOW",
1519
- offset: { x: offsetX, y: offsetY },
1520
- radius: Math.max(0, blur),
1521
- spread: Number.isFinite(spread) ? spread : 0,
1522
- color: { r, g, b, a },
1523
- visible: true,
1524
- blendMode: "NORMAL"
1525
- };
1526
- }
1527
- function parseBoxShadowToFigmaEffects(shadowStr) {
1528
- const s = (shadowStr || "").trim();
1529
- if (!s || s.toLowerCase() === "none") return [];
1530
- const out = [];
1531
- const segments = s.split(/\s*,\s*/);
1532
- for (const seg of segments) {
1533
- const effect = parseBoxShadowToFigmaEffect(seg.trim());
1534
- if (effect) out.push(effect);
1535
- }
1536
- return out;
1537
- }
1538
- function colorTo8DigitHex(color) {
1539
- if (!color || typeof color !== "string") return null;
1540
- const s = color.trim();
1541
- const rgbaMatch = s.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
1542
- if (rgbaMatch) {
1543
- const r = Math.max(0, Math.min(255, parseInt(rgbaMatch[1], 10)));
1544
- const g = Math.max(0, Math.min(255, parseInt(rgbaMatch[2], 10)));
1545
- const b = Math.max(0, Math.min(255, parseInt(rgbaMatch[3], 10)));
1546
- const a = rgbaMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4]))) : 1;
1547
- const aByte = Math.round(a * 255);
1548
- const hex = "#" + [r, g, b, aByte].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
1549
- return hex;
1550
- }
1551
- const hexMatch = s.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/i);
1552
- if (hexMatch) {
1553
- const rgb = hexMatch[1];
1554
- const aa = hexMatch[2] ?? "FF";
1555
- return `#${rgb}${aa}`.toUpperCase();
1556
- }
1557
- return null;
1558
- }
1559
- function typesetKeyToFontFamilyRole(key) {
1560
- const prefix = (key.split("-")[0] ?? key).toLowerCase();
1561
- if (prefix === "display" || prefix.startsWith("display")) return "display";
1562
- if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
1563
- if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
1564
- if (prefix.startsWith("body")) return "body";
1565
- return "body";
1566
- }
1567
- function buildFigmaPayloadsFromDS(data) {
1568
- const tokens = data.tokens;
1569
- const colors = tokens?.colors;
1570
- const typography = tokens?.typography;
1571
- const hexRe = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
1572
- const modes = [];
1573
- if (colors?.modes) {
1574
- const light = colors.modes.light ?? {};
1575
- const dark = colors.modes.dark ?? {};
1576
- if (Object.keys(light).length > 0) modes.push("Light");
1577
- if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
1578
- }
1579
- if (modes.length === 0) modes.push("Light");
1580
- const primitiveModes = ["Value"];
1581
- const dsName = data.meta?.name;
1582
- const collectionPrefix = dsName ? `${dsName} ` : "";
1583
- const referencedScaleNames = data.storedColors ? getReferencedScaleNames(data.storedColors) : /* @__PURE__ */ new Set();
1584
- const primitiveVariables = [];
1585
- const primitiveNames = /* @__PURE__ */ new Set();
1586
- const oneOffHexToFigmaName = /* @__PURE__ */ new Map();
1587
- const alphaScales = colors?.alphaScales;
1588
- if (alphaScales?.whiteAlpha && typeof alphaScales.whiteAlpha === "object") {
1589
- for (const [step, value] of Object.entries(alphaScales.whiteAlpha)) {
1590
- if (typeof value !== "string") continue;
1591
- const hex = colorTo8DigitHex(value);
1592
- if (!hex || !hexRe.test(hex)) continue;
1593
- const figmaName = `WhiteAlpha / ${step}`;
1594
- if (primitiveNames.has(figmaName)) continue;
1595
- primitiveNames.add(figmaName);
1596
- const values = {};
1597
- for (const m of primitiveModes) values[m] = hex;
1598
- primitiveVariables.push({ name: figmaName, values });
1599
- }
1600
- }
1601
- if (alphaScales?.blackAlpha && typeof alphaScales.blackAlpha === "object") {
1602
- for (const [step, value] of Object.entries(alphaScales.blackAlpha)) {
1603
- if (typeof value !== "string") continue;
1604
- const hex = colorTo8DigitHex(value);
1605
- if (!hex || !hexRe.test(hex)) continue;
1606
- const figmaName = `BlackAlpha / ${step}`;
1607
- if (primitiveNames.has(figmaName)) continue;
1608
- primitiveNames.add(figmaName);
1609
- const values = {};
1610
- for (const m of primitiveModes) values[m] = hex;
1611
- primitiveVariables.push({ name: figmaName, values });
1612
- }
1613
- }
1614
- if (colors?.scales && typeof colors.scales === "object") {
1615
- for (const [scaleName, steps] of Object.entries(colors.scales)) {
1616
- if (!steps || typeof steps !== "object") continue;
1617
- let groupDisplay = scaleName.charAt(0).toUpperCase() + scaleName.slice(1);
1618
- if (scaleName.toLowerCase() === "neutral" && data.storedColors) {
1619
- const neutral = data.storedColors.neutral;
1620
- if (neutral?.baseHueFamily) {
1621
- groupDisplay = refScaleToFigmaDisplayName(neutral.baseHueFamily);
1622
- }
1623
- }
1624
- for (const [step, hexVal] of Object.entries(steps)) {
1625
- if (typeof hexVal !== "string") continue;
1626
- const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
1627
- if (!hex || !hexRe.test(hex)) continue;
1628
- const figmaName = `${groupDisplay} / ${step}`;
1629
- if (primitiveNames.has(figmaName)) continue;
1630
- primitiveNames.add(figmaName);
1631
- const values = {};
1632
- for (const m of primitiveModes) values[m] = hex;
1633
- primitiveVariables.push({ name: figmaName, values });
1634
- }
1635
- }
1636
- }
1637
- const radixVariables = [];
1638
- const radixCollectionName = `${collectionPrefix}Colors Radix`;
1639
- for (const scaleName of referencedScaleNames) {
1640
- if (primitiveNames.has(`${scaleName} / 50`) || primitiveNames.has(`${scaleName} / 100`)) continue;
1641
- const fromStored = data.storedColors ? getFullScaleFromStored(data.storedColors, scaleName) : null;
1642
- const scaleData = fromStored ?? data.getRadixScale?.(scaleName) ?? null;
1643
- if (!scaleData) continue;
1644
- for (const [step, hexVal] of Object.entries(scaleData)) {
1645
- const figmaName = `${scaleName} / ${step}`;
1646
- if (primitiveNames.has(figmaName)) continue;
1647
- const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
1648
- if (!hex || !hexRe.test(hex)) continue;
1649
- primitiveNames.add(figmaName);
1650
- const values = {};
1651
- for (const m of primitiveModes) values[m] = hex;
1652
- primitiveVariables.push({ name: figmaName, values });
1653
- }
1654
- }
1655
- if (colors?.oneOffs && Array.isArray(colors.oneOffs)) {
1656
- for (const oneOff of colors.oneOffs) {
1657
- if (!oneOff || typeof oneOff !== "object" || typeof oneOff.hex !== "string") continue;
1658
- const hex = colorTo8DigitHex(oneOff.hex) ?? (hexRe.test(oneOff.hex) ? oneOff.hex : null);
1659
- if (!hex || !hexRe.test(hex)) continue;
1660
- const name = typeof oneOff.name === "string" && oneOff.name ? oneOff.name : oneOff.id ?? "unnamed";
1661
- const figmaName = `One-off / ${name}`;
1662
- if (primitiveNames.has(figmaName)) continue;
1663
- primitiveNames.add(figmaName);
1664
- const values = {};
1665
- for (const m of primitiveModes) values[m] = hex;
1666
- primitiveVariables.push({ name: figmaName, values });
1667
- const normalizedHex = hex.replace(/^#/, "").toUpperCase();
1668
- const key8 = normalizedHex.length === 6 ? normalizedHex + "FF" : normalizedHex;
1669
- oneOffHexToFigmaName.set(key8, figmaName);
1670
- }
1671
- }
1672
- const semanticRefMap = data.storedColors && primitiveNames.size > 0 ? buildSemanticRefMap(data.storedColors, primitiveNames) : {};
1673
- const semanticVariables = [];
1674
- const semanticNames = /* @__PURE__ */ new Set();
1675
- const primitivesCollectionName = `${collectionPrefix}Colors Primitives`;
1676
- if (colors?.modes) {
1677
- const light = colors.modes.light ?? {};
1678
- const dark = colors.modes.dark ?? {};
1679
- const orderedKeys = [...Object.keys(light)];
1680
- for (const k of Object.keys(dark)) {
1681
- if (!orderedKeys.includes(k)) orderedKeys.push(k);
1682
- }
1683
- for (const key of orderedKeys) {
1684
- const lightVal = light[key];
1685
- const darkVal = dark[key];
1686
- const lightHex = typeof lightVal === "string" ? colorTo8DigitHex(lightVal) ?? (hexRe.test(lightVal) ? lightVal : null) : null;
1687
- if (lightHex && hexRe.test(lightHex)) {
1688
- const figmaName = figmaColorNameWithGroup(key);
1689
- if (semanticNames.has(figmaName)) continue;
1690
- semanticNames.add(figmaName);
1691
- const darkHex = typeof darkVal === "string" ? colorTo8DigitHex(darkVal) ?? (hexRe.test(darkVal) ? darkVal : null) : null;
1692
- const refs = semanticRefMap[key];
1693
- const values = {
1694
- ...modes.includes("Light") && { Light: lightHex },
1695
- ...modes.includes("Dark") && { Dark: darkHex && hexRe.test(darkHex) ? darkHex : lightHex }
1696
- };
1697
- const aliasByMode = {};
1698
- for (const m of modes) {
1699
- const aliasFromRef = m === "Light" ? refs?.Light : refs?.Dark;
1700
- if (aliasFromRef && primitiveNames.has(aliasFromRef)) {
1701
- aliasByMode[m] = aliasFromRef;
1702
- continue;
1703
- }
1704
- const hexForMode = m === "Light" ? lightHex : darkHex && hexRe.test(darkHex) ? darkHex : lightHex;
1705
- const norm = hexForMode.replace(/^#/, "").toUpperCase();
1706
- const key8 = norm.length === 6 ? norm + "FF" : norm;
1707
- const oneOffAlias = oneOffHexToFigmaName.get(key8);
1708
- if (oneOffAlias) {
1709
- aliasByMode[m] = oneOffAlias;
1710
- }
1711
- }
1712
- semanticVariables.push({
1713
- name: figmaName,
1714
- values,
1715
- ...Object.keys(aliasByMode).length > 0 ? { aliasByMode } : {}
1716
- });
1717
- }
1718
- }
1719
- }
1720
- if (colors?.static?.brand && typeof colors.static.brand === "object") {
1721
- for (const [key, hex] of Object.entries(colors.static.brand)) {
1722
- if (typeof hex !== "string" || !hexRe.test(hex)) continue;
1723
- const figmaName = figmaColorNameWithGroup(`brand/${key}`);
1724
- if (semanticNames.has(figmaName)) continue;
1725
- semanticNames.add(figmaName);
1726
- const values = {};
1727
- for (const m of modes) values[m] = hex;
1728
- semanticVariables.push({ name: figmaName, values });
1729
- }
1730
- }
1731
- const colorVariableCollections = [];
1732
- if (primitiveVariables.length > 0) {
1733
- colorVariableCollections.push({
1734
- collectionName: primitivesCollectionName,
1735
- modes: primitiveModes,
1736
- variables: primitiveVariables,
1737
- applyScopes: false
1738
- });
1739
- }
1740
- if (radixVariables.length > 0) {
1741
- colorVariableCollections.push({
1742
- collectionName: radixCollectionName,
1743
- modes,
1744
- variables: radixVariables,
1745
- applyScopes: false
1746
- });
1747
- }
1748
- if (semanticVariables.length > 0) {
1749
- const primitiveCollections = [];
1750
- if (primitiveVariables.length > 0) primitiveCollections.push(primitivesCollectionName);
1751
- if (radixVariables.length > 0) primitiveCollections.push(radixCollectionName);
1752
- colorVariableCollections.push({
1753
- collectionName: `${collectionPrefix}Colors Semantic`,
1754
- modes,
1755
- variables: semanticVariables,
1756
- applyScopes: true,
1757
- ...primitiveCollections.length > 1 ? { primitiveCollectionNames: primitiveCollections } : primitiveCollections.length === 1 ? { primitiveCollectionName: primitiveCollections[0] } : {}
1758
- });
1759
- }
1760
- const paintStyles = [];
1761
- const textStyles = [];
1762
- const sizeToPx = (val, basePx = 16) => {
1763
- if (typeof val === "number") return Math.round(val);
1764
- const s = String(val).trim();
1765
- const pxMatch = s.match(/^([\d.]+)\s*px$/i);
1766
- if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
1767
- const remMatch = s.match(/^([\d.]+)\s*rem$/i);
1768
- if (remMatch) return Math.round(parseFloat(remMatch[1]) * basePx);
1769
- const n = parseFloat(s);
1770
- if (Number.isFinite(n)) return n <= 0 ? basePx : n < 50 ? Math.round(n * basePx) : Math.round(n);
1771
- return basePx;
1772
- };
1773
- const letterSpacingToPx = (val, fontSizePx) => {
1774
- if (val === void 0 || val === null) return void 0;
1775
- if (typeof val === "number") return Math.round(val);
1776
- const s = String(val).trim();
1777
- const pxMatch = s.match(/^([-\d.]+)\s*px$/i);
1778
- if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
1779
- const emMatch = s.match(/^([-\d.]+)\s*em$/i);
1780
- if (emMatch) return Math.round(parseFloat(emMatch[1]) * fontSizePx);
1781
- const n = parseFloat(s);
1782
- return Number.isFinite(n) ? Math.round(n) : void 0;
1783
- };
1784
- const firstFont = (obj) => {
1785
- if (typeof obj === "string") {
1786
- const primary = obj.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
1787
- return primary || "Inter";
1788
- }
1789
- if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1790
- const v = obj.body ?? obj.heading ?? obj.display ?? Object.values(obj)[0];
1791
- return firstFont(v);
1792
- }
1793
- return "Inter";
1794
- };
1795
- const toFontFamilyString = (val) => {
1796
- if (typeof val === "string") {
1797
- const s = val.trim().replace(/^["']|["']$/g, "");
1798
- return s || "Inter";
1799
- }
1800
- return firstFont(val);
1801
- };
1802
- const fontFamilyMap = typography?.fontFamily ?? {};
1803
- const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
1804
- const fontSizeMap = typography?.fontSize;
1805
- const fontWeightMap = typography?.fontWeight;
1806
- const lineHeightMap = typography?.lineHeight;
1807
- const letterSpacingMap = typography?.letterSpacing;
1808
- const textTransformMap = typography?.textTransform;
1809
- const textDecorationMap = typography?.textDecoration;
1810
- if (fontSizeMap && typeof fontSizeMap === "object" && Object.keys(fontSizeMap).length > 0) {
1811
- for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
1812
- const fontSize = sizeToPx(sizeVal);
1813
- if (fontSize <= 0) continue;
1814
- const role = typesetKeyToFontFamilyRole(key);
1815
- const fontFamily = toFontFamilyString(
1816
- fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
1817
- );
1818
- const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
1819
- const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
1820
- const fontWeight = weight != null ? String(weight) : "400";
1821
- const letterSpacingPx = letterSpacingToPx(
1822
- letterSpacingMap && typeof letterSpacingMap === "object" ? letterSpacingMap[key] : void 0,
1823
- fontSize
1824
- );
1825
- const textTransform = textTransformMap && typeof textTransformMap === "object" ? textTransformMap[key] : void 0;
1826
- const textDecoration = textDecorationMap && typeof textDecorationMap === "object" ? textDecorationMap[key] : void 0;
1827
- const namePart = key.replace(/-/g, " / ");
1828
- const style = {
1829
- name: namePart.startsWith("Typography") ? namePart : `Typography / ${namePart}`,
1830
- fontFamily,
1831
- fontWeight,
1832
- fontSize,
1833
- lineHeightUnit: "PERCENT",
1834
- letterSpacingUnit: "PIXELS",
1835
- ...letterSpacingPx !== void 0 && letterSpacingPx !== 0 ? { letterSpacingValue: letterSpacingPx } : {}
1836
- };
1837
- if (lh != null && typeof lh === "number" && lh > 0) {
1838
- style.lineHeightValue = lh >= 10 ? Math.round(lh / fontSize * 100) : Math.round(lh * 100);
1839
- } else {
1840
- style.lineHeightValue = 150;
1841
- }
1842
- if (textTransform === "uppercase") style.textCase = "UPPER";
1843
- else if (textTransform === "lowercase") style.textCase = "LOWER";
1844
- else if (textTransform === "capitalize") style.textCase = "TITLE";
1845
- else style.textCase = "ORIGINAL";
1846
- if (textDecoration === "underline") style.textDecoration = "UNDERLINE";
1847
- else style.textDecoration = "NONE";
1848
- textStyles.push(style);
1849
- }
1850
- }
1851
- const textStylesMap = typography?.textStyles;
1852
- if (textStyles.length === 0 && textStylesMap && typeof textStylesMap === "object") {
1853
- for (const [styleName, style] of Object.entries(textStylesMap)) {
1854
- if (!style || typeof style !== "object") continue;
1855
- const fontSize = sizeToPx(style.fontSize ?? "1rem");
1856
- const lhStr = style.lineHeight;
1857
- const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
1858
- const payload = {
1859
- name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
1860
- fontFamily: defaultFontFamily,
1861
- fontWeight: String(style.fontWeight ?? "400"),
1862
- fontSize,
1863
- lineHeightUnit: "PERCENT",
1864
- lineHeightValue: Math.round((Number.isFinite(lineHeightUnitless) ? lineHeightUnitless : 1.5) * 100),
1865
- letterSpacingUnit: "PIXELS",
1866
- textCase: "ORIGINAL",
1867
- textDecoration: "NONE"
1868
- };
1869
- textStyles.push(payload);
1870
- }
1871
- }
1872
- textStyles.sort((a, b) => {
1873
- if (a.fontSize !== b.fontSize) return a.fontSize - b.fontSize;
1874
- return (a.name || "").localeCompare(b.name || "");
1875
- });
1876
- const numberVariableCollections = [];
1877
- const spacing = tokens?.spacing;
1878
- if (spacing?.scale && typeof spacing.scale === "object") {
1879
- const vars = [];
1880
- for (const [key, val] of Object.entries(spacing.scale)) {
1881
- const n = tokenValueToNumber(val);
1882
- if (n >= 0) vars.push({ name: `Spacing / ${key}`, value: n });
1883
- }
1884
- vars.sort((a, b) => a.value - b.value);
1885
- if (vars.length > 0)
1886
- numberVariableCollections.push({
1887
- collectionName: `${collectionPrefix}Spacing`,
1888
- categoryKey: "Spacing",
1889
- variables: vars,
1890
- scopes: ["GAP"]
1891
- });
1892
- }
1893
- const radius = tokens?.radius;
1894
- if (radius?.scale && typeof radius.scale === "object") {
1895
- const vars = [];
1896
- for (const [key, val] of Object.entries(radius.scale)) {
1897
- const n = tokenValueToNumber(val);
1898
- if (n >= 0) vars.push({ name: `Radius / ${key}`, value: n });
1899
- }
1900
- vars.sort((a, b) => a.value - b.value);
1901
- if (vars.length > 0)
1902
- numberVariableCollections.push({
1903
- collectionName: `${collectionPrefix}Radius`,
1904
- categoryKey: "Radius",
1905
- variables: vars,
1906
- scopes: ["CORNER_RADIUS"]
1907
- });
1908
- }
1909
- const borders = tokens?.borders;
1910
- if (borders?.width && typeof borders.width === "object") {
1911
- const vars = [];
1912
- for (const [key, val] of Object.entries(borders.width)) {
1913
- const n = tokenValueToNumber(val);
1914
- if (n >= 0) vars.push({ name: `Borders / ${key}`, value: n });
1915
- }
1916
- vars.sort((a, b) => a.value - b.value);
1917
- if (vars.length > 0)
1918
- numberVariableCollections.push({
1919
- collectionName: `${collectionPrefix}Borders`,
1920
- categoryKey: "Borders",
1921
- variables: vars,
1922
- scopes: ["STROKE_FLOAT"]
1923
- });
1924
- }
1925
- const sizing = tokens?.sizing;
1926
- const sizingVariables = [];
1927
- if (sizing?.height && typeof sizing.height === "object") {
1928
- for (const [key, val] of Object.entries(sizing.height)) {
1929
- const n = tokenValueToNumber(val);
1930
- if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
1931
- }
1932
- }
1933
- if (sizing?.icon && typeof sizing.icon === "object") {
1934
- for (const [key, val] of Object.entries(sizing.icon)) {
1935
- const n = tokenValueToNumber(val);
1936
- if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
1937
- }
1938
- }
1939
- sizingVariables.sort((a, b) => a.value - b.value);
1940
- if (sizingVariables.length > 0) {
1941
- numberVariableCollections.push({
1942
- collectionName: `${collectionPrefix}Sizing`,
1943
- categoryKey: "Sizing",
1944
- variables: sizingVariables,
1945
- scopes: ["WIDTH_HEIGHT"]
1946
- });
1947
- }
1948
- const layout = tokens?.layout;
1949
- if (layout?.breakpoint && typeof layout.breakpoint === "object") {
1950
- const vars = [];
1951
- for (const [key, val] of Object.entries(layout.breakpoint)) {
1952
- const n = tokenValueToNumber(val);
1953
- if (n >= 0) vars.push({ name: `Breakpoint / ${key}`, value: n });
1954
- }
1955
- vars.sort((a, b) => a.value - b.value);
1956
- if (vars.length > 0)
1957
- numberVariableCollections.push({
1958
- collectionName: `${collectionPrefix}Layout`,
1959
- categoryKey: "Layout",
1960
- variables: vars,
1961
- scopes: ["WIDTH_HEIGHT"]
1962
- });
1963
- }
1964
- const effectStyles = [];
1965
- const shadows = tokens?.shadows;
1966
- if (shadows?.elevation && typeof shadows.elevation === "object") {
1967
- for (const [key, val] of Object.entries(shadows.elevation)) {
1968
- if (typeof val !== "string") continue;
1969
- const effects = parseBoxShadowToFigmaEffects(val);
1970
- if (effects.length > 0) effectStyles.push({ name: `Shadow / ${key}`, effects });
1971
- }
1972
- }
1973
- if (shadows?.focus && typeof shadows.focus === "string") {
1974
- const effects = parseBoxShadowToFigmaEffects(shadows.focus);
1975
- if (effects.length > 0) effectStyles.push({ name: "Shadow / focus", effects });
1976
- }
1977
- effectStyles.sort((a, b) => {
1978
- const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
1979
- const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
1980
- const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
1981
- const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
1982
- if (orderA !== orderB) return orderA - orderB;
1983
- return nameA.localeCompare(nameB);
1984
- });
1985
- return {
1986
- colorVariableCollections,
1987
- paintStyles,
1988
- textStyles,
1989
- numberVariableCollections,
1990
- effectStyles
1991
- };
1992
- }
1993
-
1994
1227
  // src/index.ts
1995
- import * as path2 from "path";
1996
- import * as fs2 from "fs";
1228
+ import * as path from "path";
1229
+ import * as fs from "fs";
1997
1230
  import { execSync } from "child_process";
1998
1231
  import { platform } from "os";
1999
1232
  import WebSocket, { WebSocketServer } from "ws";
@@ -2152,13 +1385,13 @@ function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS)
2152
1385
  );
2153
1386
  }
2154
1387
  const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2155
- return new Promise((resolve4, reject) => {
1388
+ return new Promise((resolve3, reject) => {
2156
1389
  const timeout = setTimeout(() => {
2157
1390
  if (pendingBridgeRequests.delete(id)) {
2158
1391
  reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
2159
1392
  }
2160
1393
  }, timeoutMs);
2161
- pendingBridgeRequests.set(id, { resolve: resolve4, reject, timeout });
1394
+ pendingBridgeRequests.set(id, { resolve: resolve3, reject, timeout });
2162
1395
  try {
2163
1396
  ws.send(JSON.stringify({ id, method: normalized, params }));
2164
1397
  } catch (e) {
@@ -2194,10 +1427,25 @@ function parseArgs() {
2194
1427
  var cliArgs = parseArgs();
2195
1428
  var { dsId, apiKey, accessToken } = cliArgs;
2196
1429
  var apiBase = cliArgs.apiBase || "https://atomix.studio";
1430
+ var MCP_VERSION = "1.0.34";
2197
1431
  var cachedData = null;
2198
1432
  var cachedETag = null;
2199
1433
  var cachedMcpTier = null;
2200
1434
  var authFailedNoTools = false;
1435
+ var mcpUpdateNotice = null;
1436
+ var mcpLatestVersion = null;
1437
+ function isVersionNewer(latest, current) {
1438
+ const toParts = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
1439
+ const a = toParts(latest);
1440
+ const b = toParts(current);
1441
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
1442
+ const x = a[i] ?? 0;
1443
+ const y = b[i] ?? 0;
1444
+ if (x > y) return true;
1445
+ if (x < y) return false;
1446
+ }
1447
+ return false;
1448
+ }
2201
1449
  function hasValidAuthConfig() {
2202
1450
  return !!(dsId && accessToken);
2203
1451
  }
@@ -2218,10 +1466,10 @@ ${changes.summary}`);
2218
1466
  }
2219
1467
  }
2220
1468
  function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
2221
- if (!fs2.existsSync(outputPath)) {
1469
+ if (!fs.existsSync(outputPath)) {
2222
1470
  return { path: outputPath, status: "FAIL", detail: "File not found after write." };
2223
1471
  }
2224
- const content = fs2.readFileSync(outputPath, "utf-8");
1472
+ const content = fs.readFileSync(outputPath, "utf-8");
2225
1473
  if (!content || content.trim().length === 0) {
2226
1474
  return { path: outputPath, status: "FAIL", detail: "File is empty after write." };
2227
1475
  }
@@ -2243,7 +1491,7 @@ function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
2243
1491
  }
2244
1492
  function formatValidationBlock(entries) {
2245
1493
  if (entries.length === 0) return "";
2246
- const displayPath = (p) => p.startsWith("(") ? p : path2.relative(process.cwd(), p);
1494
+ const displayPath = (p) => p.startsWith("(") ? p : path.relative(process.cwd(), p);
2247
1495
  const lines = [
2248
1496
  "",
2249
1497
  "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
@@ -2273,11 +1521,19 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
2273
1521
  cachedData = result.data;
2274
1522
  cachedETag = result.etag;
2275
1523
  cachedMcpTier = result.data.meta.mcpTier ?? null;
1524
+ const latest = result.data.meta.mcpLatestVersion;
1525
+ if (latest && isVersionNewer(latest, MCP_VERSION)) {
1526
+ mcpLatestVersion = latest;
1527
+ mcpUpdateNotice = `**MCP update available:** A new Atomix MCP server (v${latest}) is available. You're on v${MCP_VERSION}. To use the new version in Cursor: 1) Quit Cursor completely and reopen, 2) Clear npx cache: \`rm -rf ~/.npm/_npx\` (macOS/Linux), 3) Ensure your MCP config uses \`@atomixstudio/mcp@latest\` or \`@atomixstudio/mcp@${latest}\`. To test a local build before publishing: point MCP to \`node /path/to/Atom/packages/mcp-user/dist/index.js\` with \`--ds-id\` and \`--atomix-token\`.`;
1528
+ } else {
1529
+ mcpUpdateNotice = null;
1530
+ mcpLatestVersion = null;
1531
+ }
2276
1532
  await updateChangeSummary(result.data);
2277
1533
  return result.data;
2278
1534
  }
2279
1535
  var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
2280
- function typesetKeyToFontFamilyRole2(key) {
1536
+ function typesetKeyToFontFamilyRole(key) {
2281
1537
  const prefix = (key.split("-")[0] ?? key).toLowerCase();
2282
1538
  if (prefix === "display" || prefix.startsWith("display")) return "display";
2283
1539
  if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
@@ -2297,7 +1553,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
2297
1553
  const p = cssPrefix ? `${cssPrefix}-` : "";
2298
1554
  const typesets = [];
2299
1555
  for (const key of Object.keys(fontSize)) {
2300
- const role = typesetKeyToFontFamilyRole2(key);
1556
+ const role = typesetKeyToFontFamilyRole(key);
2301
1557
  const familyName = fontFamily[role] ?? fontFamily.body;
2302
1558
  const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
2303
1559
  const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
@@ -2323,7 +1579,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
2323
1579
  var server = new Server(
2324
1580
  {
2325
1581
  name: "atomix-mcp-user",
2326
- version: "1.0.30"
1582
+ version: MCP_VERSION
2327
1583
  },
2328
1584
  {
2329
1585
  capabilities: {
@@ -2441,18 +1697,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2441
1697
  }
2442
1698
  },
2443
1699
  {
2444
- name: "getAIToolRules",
2445
- description: "Generate design system rules for AI coding tools (Cursor, Copilot, Windsurf, etc.).",
1700
+ name: "getRules",
1701
+ description: "Get design system governance rules. Optionally filter by topic (colors, typo, motion, icons, layout, visual). Call at session start or before writing visual code.",
2446
1702
  inputSchema: {
2447
1703
  type: "object",
2448
1704
  properties: {
2449
- tool: {
1705
+ topic: {
2450
1706
  type: "string",
2451
- enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic", "all"],
2452
- description: "AI tool to generate rules for. Use 'all' to get rules for all tools."
1707
+ enum: ["colors", "typo", "typography", "motion", "icons", "layout", "visual", "style"],
1708
+ description: "Optional. Filter rules by topic: colors, typo/typography, motion, icons, layout, or visual/style (color, border, radius, shadows, icons). Omit for all rules."
2453
1709
  }
2454
- },
2455
- required: ["tool"]
1710
+ }
2456
1711
  }
2457
1712
  },
2458
1713
  {
@@ -2487,10 +1742,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2487
1742
  },
2488
1743
  {
2489
1744
  name: "syncAll",
2490
- description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Response includes a VALIDATION section\u2014agent must check it to confirm success. Optional: output (default ./tokens.css), format (default css), skipTokens (if true, only skills and manifest), dryRun (if true, report only; no files written).",
1745
+ description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
2491
1746
  inputSchema: {
2492
1747
  type: "object",
2493
1748
  properties: {
1749
+ workspaceRoot: {
1750
+ type: "string",
1751
+ description: "Absolute path to the project/repo root. Skills and manifest are written under this path so they can be committed. If omitted, uses ATOMIX_PROJECT_ROOT env var, then process.cwd()."
1752
+ },
2494
1753
  output: {
2495
1754
  type: "string",
2496
1755
  description: "Token file path (e.g. ./tokens.css). Default: ./tokens.css. Ignored if skipTokens is true."
@@ -2531,6 +1790,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2531
1790
  required: []
2532
1791
  }
2533
1792
  },
1793
+ {
1794
+ name: "getMcpVersion",
1795
+ description: "Return the current Atomix MCP server version (e.g. 1.0.33) and, if known, the latest available from the API. Call this whenever the user asks about MCP version, Atomix MCP version, what version of the MCP server they are using, or whether an update is available. Prefer this over explaining the MCP protocol spec version.",
1796
+ inputSchema: {
1797
+ type: "object",
1798
+ properties: {},
1799
+ required: []
1800
+ }
1801
+ },
2534
1802
  {
2535
1803
  name: "syncToFigma",
2536
1804
  description: "Push the owner's design system to Figma: creates color variable collection (Light/Dark), color and paint styles, number variables (spacing, radius, borders, sizing, breakpoints), text styles, and shadow effect styles. Uses local WebSocket bridge and Atomix Figma plugin (no Figma REST API). No arguments. If the bridge is not running, the response includes agentInstruction to start it; only if that fails should the user start the bridge and connect the plugin. Call this when the user asks to 'sync to Figma' or 'push DS to Figma'.",
@@ -2541,6 +1809,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2541
1809
  }
2542
1810
  }
2543
1811
  ];
1812
+ if (cachedMcpTier === "pro") {
1813
+ toolsList.push({
1814
+ name: "designInFigma",
1815
+ description: "Design UI in the connected Figma file using the design system tokens. Call with action:'catalog' to discover available bridge methods, their parameters, and the file's variables/styles. Call with action:'query' to read from Figma: get_selection (current selection), get_node_info (nodeId), get_design_screenshot (frameId; returns PNG image for review). Call with action:'execute' and an array of steps to create the design on canvas. Requires the Atomix Figma plugin to be connected.",
1816
+ inputSchema: {
1817
+ type: "object",
1818
+ properties: {
1819
+ action: {
1820
+ type: "string",
1821
+ enum: ["catalog", "query", "execute"],
1822
+ description: "catalog = discover methods + file context; query = read selection/node/screenshot; execute = run design steps"
1823
+ },
1824
+ queryMethod: {
1825
+ type: "string",
1826
+ description: "Required when action is 'query'. One of: get_selection, get_node_info, get_document_info, get_design_screenshot, get_figma_variables_and_styles, list_local_components, get_component_catalog, get_variable_collection_modes, get_frame_variable_mode."
1827
+ },
1828
+ queryParams: {
1829
+ type: "object",
1830
+ description: "Optional params for query. get_node_info needs { nodeId }. get_design_screenshot needs { frameId } and optional { scale }."
1831
+ },
1832
+ steps: {
1833
+ type: "array",
1834
+ description: "Required when action is 'execute'. Array of { method, params } design commands.",
1835
+ items: {
1836
+ type: "object",
1837
+ properties: {
1838
+ method: { type: "string" },
1839
+ params: { type: "object" }
1840
+ },
1841
+ required: ["method"]
1842
+ }
1843
+ }
1844
+ },
1845
+ required: ["action"]
1846
+ }
1847
+ });
1848
+ }
2544
1849
  return { tools: toolsList };
2545
1850
  });
2546
1851
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -2554,18 +1859,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2554
1859
  isError: true
2555
1860
  };
2556
1861
  }
1862
+ if (name === "getMcpVersion") {
1863
+ const out = {
1864
+ version: MCP_VERSION,
1865
+ name: "atomix-mcp-user"
1866
+ };
1867
+ if (mcpLatestVersion) {
1868
+ out.latestVersion = mcpLatestVersion;
1869
+ out.updateAvailable = true;
1870
+ }
1871
+ return {
1872
+ content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
1873
+ };
1874
+ }
2557
1875
  try {
2558
1876
  const shouldForceRefresh = name === "syncAll";
2559
1877
  const data = await fetchDesignSystemForMCP(shouldForceRefresh);
2560
- async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun) {
1878
+ async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun, projectRoot) {
2561
1879
  const output = tokenOutput;
2562
1880
  const format = tokenFormat;
2563
- const outputPath = path2.resolve(process.cwd(), output);
2564
- const fileExists = fs2.existsSync(outputPath);
1881
+ const outputPath = path.resolve(projectRoot, output);
1882
+ const fileExists = fs.existsSync(outputPath);
2565
1883
  const deprecatedTokens = /* @__PURE__ */ new Map();
2566
1884
  const existingTokens = /* @__PURE__ */ new Map();
2567
1885
  if (fileExists && ["css", "scss", "less"].includes(format)) {
2568
- const oldContent = fs2.readFileSync(outputPath, "utf-8");
1886
+ const oldContent = fs.readFileSync(outputPath, "utf-8");
2569
1887
  const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
2570
1888
  let match;
2571
1889
  while ((match = oldVarPattern.exec(oldContent)) !== null) {
@@ -2615,7 +1933,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2615
1933
  let changes = [];
2616
1934
  let diff;
2617
1935
  if (fileExists && ["css", "scss", "less"].includes(format)) {
2618
- const oldContent = fs2.readFileSync(outputPath, "utf-8");
1936
+ const oldContent = fs.readFileSync(outputPath, "utf-8");
2619
1937
  diff = diffTokens(oldContent, mergedCssVariables, format, darkModeColors?.dark);
2620
1938
  const lightChanges = diff.added.length + diff.modified.length;
2621
1939
  const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
@@ -2670,7 +1988,7 @@ Version: ${designSystemData.meta.version}`,
2670
1988
  ` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
2671
1989
  changeLine,
2672
1990
  "",
2673
- "Rules: .cursorrules (or existing rules files in project)",
1991
+ "Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md",
2674
1992
  "",
2675
1993
  "Call syncAll again with dryRun: false to apply."
2676
1994
  ].filter(Boolean).join("\n");
@@ -2680,30 +1998,12 @@ Version: ${designSystemData.meta.version}`,
2680
1998
  validation: [{ path: "(dry run)", status: "OK", detail: "No files written." }]
2681
1999
  };
2682
2000
  }
2683
- const outputDir = path2.dirname(outputPath);
2684
- if (!fs2.existsSync(outputDir)) fs2.mkdirSync(outputDir, { recursive: true });
2685
- fs2.writeFileSync(outputPath, newContent);
2001
+ const outputDir = path.dirname(outputPath);
2002
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
2003
+ fs.writeFileSync(outputPath, newContent);
2686
2004
  const validation = [];
2687
2005
  validation.push(validateTokenFileAfterWrite(outputPath, format, tokenCount));
2688
- let rulesResults = [];
2689
- try {
2690
- rulesResults = await syncRulesFiles({
2691
- dsId,
2692
- apiKey: apiKey ?? void 0,
2693
- apiBase: apiBase ?? void 0,
2694
- rulesDir: process.cwd()
2695
- });
2696
- for (const r of rulesResults) {
2697
- const fullPath = path2.resolve(process.cwd(), r.path);
2698
- validation.push({
2699
- path: fullPath,
2700
- status: r.success && fs2.existsSync(fullPath) ? "OK" : "FAIL",
2701
- detail: r.success ? "Written." : r.error || "Write failed."
2702
- });
2703
- }
2704
- } catch (error) {
2705
- console.error(`[syncAll] Failed to sync rules: ${error}`);
2706
- }
2006
+ const rulesResults = [];
2707
2007
  const governanceChanges = cachedData ? detectGovernanceChangesByFoundation(cachedData, designSystemData) : [];
2708
2008
  const response = formatSyncResponse({
2709
2009
  data: designSystemData,
@@ -2721,31 +2021,32 @@ Version: ${designSystemData.meta.version}`,
2721
2021
  hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
2722
2022
  deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
2723
2023
  });
2724
- return { responseText: response, rulesResults, validation };
2024
+ const tokenSyncChangeSummary = fileExists && ["css", "scss", "less"].includes(format) ? { fileExisted: true, previousVarCount: existingTokens.size, currentVarCount: tokenCount } : { fileExisted: false, previousVarCount: void 0, currentVarCount: tokenCount };
2025
+ return { responseText: response, rulesResults, validation, tokenSyncChangeSummary };
2725
2026
  }
2726
2027
  switch (name) {
2727
2028
  case "getToken": {
2728
- const path4 = args?.path;
2729
- const value = getTokenByPath(data.tokens, path4);
2029
+ const path2 = args?.path;
2030
+ const value = getTokenByPath(data.tokens, path2);
2730
2031
  if (value === void 0) {
2731
2032
  return {
2732
2033
  content: [{
2733
2034
  type: "text",
2734
2035
  text: JSON.stringify({
2735
- error: `Token not found: ${path4}`,
2036
+ error: `Token not found: ${path2}`,
2736
2037
  suggestion: "Use listTokens or searchTokens to find available tokens.",
2737
2038
  availableCategories: TOKEN_CATEGORIES
2738
2039
  }, null, 2)
2739
2040
  }]
2740
2041
  };
2741
2042
  }
2742
- const cssVarKey = `--atmx-${path4.replace(/\./g, "-")}`;
2043
+ const cssVarKey = `--atmx-${path2.replace(/\./g, "-")}`;
2743
2044
  const cssVar = data.cssVariables[cssVarKey];
2744
2045
  return {
2745
2046
  content: [{
2746
2047
  type: "text",
2747
2048
  text: JSON.stringify({
2748
- path: path4,
2049
+ path: path2,
2749
2050
  value,
2750
2051
  cssVariable: cssVar || `var(${cssVarKey})`,
2751
2052
  usage: `style={{ property: "var(${cssVarKey})" }}`
@@ -2772,13 +2073,13 @@ Version: ${designSystemData.meta.version}`,
2772
2073
  };
2773
2074
  }
2774
2075
  const flat = flattenTokens(tokensToList);
2775
- const tokensWithCssVars = flat.map(({ path: path4, value }) => {
2776
- const fullPath = subcategory ? `${category}.${subcategory}.${path4}` : `${category}.${path4}`;
2076
+ const tokensWithCssVars = flat.map(({ path: path2, value }) => {
2077
+ const fullPath = subcategory ? `${category}.${subcategory}.${path2}` : `${category}.${path2}`;
2777
2078
  let cssVar;
2778
2079
  if (category === "colors" && subcategory === "static.brand") {
2779
- cssVar = data.cssVariables[`--atmx-color-brand-${path4}`];
2080
+ cssVar = data.cssVariables[`--atmx-color-brand-${path2}`];
2780
2081
  } else if (category === "colors" && subcategory?.startsWith("modes.")) {
2781
- cssVar = data.cssVariables[`--atmx-color-${path4}`];
2082
+ cssVar = data.cssVariables[`--atmx-color-${path2}`];
2782
2083
  } else {
2783
2084
  const cssVarKey = `--atmx-${fullPath.replace(/\./g, "-")}`;
2784
2085
  cssVar = data.cssVariables[cssVarKey];
@@ -2887,30 +2188,63 @@ Version: ${designSystemData.meta.version}`,
2887
2188
  }]
2888
2189
  };
2889
2190
  }
2890
- case "getAIToolRules": {
2891
- const tool = args?.tool;
2892
- const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool === "all" ? "all" : tool}`;
2893
- console.error(`[getAIToolRules] Fetching: ${rulesUrl}`);
2191
+ case "getRules": {
2192
+ const topicRaw = args?.topic;
2193
+ const topic = topicRaw?.toLowerCase().trim();
2194
+ const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
2195
+ console.error(`[getRules] Fetching: ${rulesUrl}${topic ? ` topic=${topic}` : ""}`);
2894
2196
  const headers = { "Content-Type": "application/json" };
2895
2197
  if (apiKey) headers["x-api-key"] = apiKey;
2896
2198
  try {
2897
2199
  const response = await fetch(rulesUrl, { headers });
2898
- console.error(`[getAIToolRules] Response status: ${response.status}`);
2200
+ console.error(`[getRules] Response status: ${response.status}`);
2899
2201
  if (!response.ok) {
2900
2202
  const errorText = await response.text();
2901
- console.error(`[getAIToolRules] Error response: ${errorText}`);
2203
+ console.error(`[getRules] Error response: ${errorText}`);
2902
2204
  throw new Error(`Failed to fetch rules: ${response.status} - ${errorText}`);
2903
2205
  }
2904
- const rules = await response.json();
2905
- console.error(`[getAIToolRules] Got ${rules.rules?.length || 0} rules`);
2206
+ const payload = await response.json();
2207
+ const categories = payload.categories ?? {};
2208
+ const allRules = payload.rules ?? [];
2209
+ if (!topic) {
2210
+ return {
2211
+ content: [{ type: "text", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
2212
+ };
2213
+ }
2214
+ const topicToCategories = {
2215
+ colors: ["general", "colors"],
2216
+ typo: ["general", "typography"],
2217
+ typography: ["general", "typography"],
2218
+ motion: ["general", "motion"],
2219
+ icons: ["general", "icons"],
2220
+ layout: ["general", "spacing", "sizing", "layout"],
2221
+ visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
2222
+ style: ["general", "colors", "borders", "radius", "shadows", "icons"]
2223
+ };
2224
+ const categoryKeys = topicToCategories[topic];
2225
+ if (!categoryKeys) {
2226
+ return {
2227
+ content: [{ type: "text", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
2228
+ };
2229
+ }
2230
+ const filteredCategories = {};
2231
+ const filteredRules = [];
2232
+ for (const key of categoryKeys) {
2233
+ const list = categories[key];
2234
+ if (list && list.length > 0) {
2235
+ filteredCategories[key] = list;
2236
+ filteredRules.push(...list);
2237
+ }
2238
+ }
2239
+ console.error(`[getRules] Got ${filteredRules.length} rules for topic=${topic}`);
2906
2240
  return {
2907
2241
  content: [{
2908
2242
  type: "text",
2909
- text: JSON.stringify(rules, null, 2)
2243
+ text: JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2)
2910
2244
  }]
2911
2245
  };
2912
2246
  } catch (fetchError) {
2913
- console.error(`[getAIToolRules] Fetch error:`, fetchError);
2247
+ console.error(`[getRules] Fetch error:`, fetchError);
2914
2248
  throw fetchError;
2915
2249
  }
2916
2250
  }
@@ -2995,7 +2329,7 @@ Version: ${designSystemData.meta.version}`,
2995
2329
  copilot: `# Copilot Setup
2996
2330
 
2997
2331
  1. Create a Copilot instructions file in your project (e.g. \`.github/copilot-instructions.md\`)
2998
- 2. Use getAIToolRules({ tool: "copilot" }) to get the content
2332
+ 2. Run /--sync to write the skill; call getRules() when you need governance rules
2999
2333
  3. Enable custom instructions in your editor (e.g. \`github.copilot.chat.codeGeneration.useInstructionFiles\`: true in settings)
3000
2334
 
3001
2335
  ## File Structure
@@ -3009,7 +2343,7 @@ Version: ${designSystemData.meta.version}`,
3009
2343
  windsurf: `# Windsurf Setup
3010
2344
 
3011
2345
  1. Create \`.windsurf/mcp.json\` in your project root
3012
- 2. Create \`.windsurfrules\` with rules from getAIToolRules
2346
+ 2. Run /--sync to write the skill; call getRules() when you need governance rules
3013
2347
  3. Restart Windsurf Editor
3014
2348
 
3015
2349
  ## File Structure
@@ -3050,7 +2384,7 @@ Version: ${designSystemData.meta.version}`,
3050
2384
  zed: `# Zed Setup
3051
2385
 
3052
2386
  1. Create \`.zed/assistant/rules.md\` in your project
3053
- 2. Use getAIToolRules({ tool: "zed" }) for content
2387
+ 2. Run /--sync to write the skill; call getRules() for governance rules
3054
2388
 
3055
2389
  ## File Structure
3056
2390
 
@@ -3078,9 +2412,8 @@ Version: ${designSystemData.meta.version}`,
3078
2412
  **Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
3079
2413
  generic: `# Generic AI Tool Setup
3080
2414
 
3081
- 1. Create AI_GUIDELINES.md in your project root
3082
- 2. Use getAIToolRules({ tool: "generic" }) for content
3083
- 3. Reference in your prompts or context
2415
+ 1. Run /--sync to write the skill (.cursor/skills/atomix-ds/SKILL.md)
2416
+ 2. Call getRules() when you need governance rules; reference in your prompts or context
3084
2417
 
3085
2418
  ## File Structure
3086
2419
 
@@ -3115,16 +2448,18 @@ Version: ${designSystemData.meta.version}`,
3115
2448
  const dryRun = args?.dryRun === true;
3116
2449
  const output = args?.output || "./tokens.css";
3117
2450
  const format = args?.format || "css";
2451
+ const projectRoot = path.resolve(
2452
+ args?.workspaceRoot || process.env.ATOMIX_PROJECT_ROOT || process.cwd()
2453
+ );
3118
2454
  const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
3119
2455
  let tokenResponseText = "";
3120
2456
  const allValidation = [];
2457
+ let tokenSyncChangeSummary;
3121
2458
  if (!skipTokens) {
3122
- const result = await performTokenSyncAndRules(data, output, format, dryRun);
2459
+ const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
3123
2460
  tokenResponseText = result.responseText;
3124
2461
  allValidation.push(...result.validation);
3125
- if (!dryRun && result.rulesResults.length > 0) {
3126
- parts.push(`Rules: ${result.rulesResults.map((r) => r.path).join(", ")}`);
3127
- }
2462
+ tokenSyncChangeSummary = result.tokenSyncChangeSummary;
3128
2463
  if (dryRun) {
3129
2464
  parts.push(`Would write tokens: ${output} (${format})`);
3130
2465
  } else {
@@ -3133,11 +2468,12 @@ Version: ${designSystemData.meta.version}`,
3133
2468
  }
3134
2469
  const dsVersion = String(data.meta.version ?? "1.0.0");
3135
2470
  const dsExportedAt = data.meta.exportedAt;
3136
- const skillsDir = path2.resolve(process.cwd(), ".cursor/skills/atomix-ds");
3137
- const skillPath1 = path2.join(skillsDir, "SKILL.md");
3138
- const manifestPath = path2.resolve(process.cwd(), "atomix-dependencies.json");
2471
+ const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
2472
+ const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
2473
+ const dependencySkills = getSyncDependencySkills(data, dsVersion, dsExportedAt);
3139
2474
  if (dryRun) {
3140
- parts.push("Would write skills: .cursor/skills/atomix-ds/SKILL.md");
2475
+ const skillList = dependencySkills.map((s) => s.path).join(", ");
2476
+ parts.push(`Would write skills: ${skillList}`);
3141
2477
  parts.push("Would write manifest: atomix-dependencies.json");
3142
2478
  const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
3143
2479
  return {
@@ -3146,11 +2482,19 @@ Version: ${designSystemData.meta.version}`,
3146
2482
  ${reportText}` }]
3147
2483
  };
3148
2484
  }
3149
- if (!fs2.existsSync(skillsDir)) fs2.mkdirSync(skillsDir, { recursive: true });
3150
- const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
3151
- fs2.writeFileSync(skillPath1, genericWithVersion);
3152
- allValidation.push({ path: skillPath1, status: fs2.existsSync(skillPath1) ? "OK" : "FAIL", detail: "Written." });
3153
- parts.push("Skills: .cursor/skills/atomix-ds/SKILL.md (synced at DS v" + dsVersion + ")");
2485
+ if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
2486
+ const skillsWritten = [];
2487
+ for (const sk of dependencySkills) {
2488
+ const absPath = path.resolve(projectRoot, sk.path);
2489
+ const existed = fs.existsSync(absPath);
2490
+ const prevVersion = existed ? readSkillVersionFromFile(absPath) : null;
2491
+ const fromLabel = existed ? prevVersion ? `DS v${prevVersion}` : "existing" : "missing";
2492
+ fs.writeFileSync(absPath, sk.content);
2493
+ allValidation.push({ path: absPath, status: fs.existsSync(absPath) ? "OK" : "FAIL", detail: "Written." });
2494
+ skillsWritten.push({ path: sk.path, shortName: sk.shortName, from: fromLabel });
2495
+ }
2496
+ const skillShortNames = skillsWritten.map((s) => s.shortName).join(" + ");
2497
+ parts.push(`Skills: ${skillShortNames} (DS v${dsVersion})`);
3154
2498
  const tokens = data.tokens;
3155
2499
  const typography = tokens?.typography;
3156
2500
  const fontFamily = typography?.fontFamily;
@@ -3169,6 +2513,10 @@ ${reportText}` }]
3169
2513
  };
3170
2514
  const lib = icons?.library || "lucide";
3171
2515
  const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
2516
+ const skillsManifestEntry = { skill: ".cursor/skills/atomix-ds/SKILL.md", syncedAtVersion: String(data.meta.version ?? "1.0.0") };
2517
+ if (dependencySkills.some((s) => s.shortName === "figma-design-SKILL.md")) {
2518
+ skillsManifestEntry.figmaDesignSkill = ".cursor/skills/atomix-ds/figma-design-SKILL.md";
2519
+ }
3172
2520
  const manifest = {
3173
2521
  designSystem: { name: data.meta.name, version: data.meta.version },
3174
2522
  tokenFile: skipTokens ? void 0 : output,
@@ -3179,23 +2527,67 @@ ${reportText}` }]
3179
2527
  strokeWidthValue: icons?.strokeWidth
3180
2528
  },
3181
2529
  fonts: { families: fontNames },
3182
- skills: {
3183
- skill: ".cursor/skills/atomix-ds/SKILL.md",
3184
- syncedAtVersion: data.meta.version ?? "1.0.0"
3185
- }
2530
+ skills: skillsManifestEntry
3186
2531
  };
3187
- fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3188
- allValidation.push({ path: manifestPath, status: fs2.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
2532
+ const manifestExisted = fs.existsSync(manifestPath);
2533
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
2534
+ allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
3189
2535
  parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
2536
+ const changeRows = [];
2537
+ if (!skipTokens && tokenSyncChangeSummary) {
2538
+ const fromStr = tokenSyncChangeSummary.fileExisted && tokenSyncChangeSummary.previousVarCount != null ? `${tokenSyncChangeSummary.previousVarCount} tokens in ${output}` : "missing";
2539
+ changeRows.push({
2540
+ what: "Tokens",
2541
+ from: fromStr,
2542
+ to: `${tokenSyncChangeSummary.currentVarCount} tokens in ${output} (${format})`
2543
+ });
2544
+ }
2545
+ const skillsFrom = skillsWritten.length ? skillsWritten.every((s) => s.from === "missing") ? "missing" : "updated" : "";
2546
+ if (skillsWritten.length > 0) {
2547
+ changeRows.push({
2548
+ what: "Skills",
2549
+ from: skillsFrom,
2550
+ to: `${skillShortNames} (DS v${dsVersion})`
2551
+ });
2552
+ }
2553
+ changeRows.push({
2554
+ what: "Manifest",
2555
+ from: manifestExisted ? "existing" : "missing",
2556
+ to: "atomix-dependencies.json"
2557
+ });
2558
+ if (lastSyncAffectedTokens && (lastSyncAffectedTokens.modified.length > 0 || lastSyncAffectedTokens.added.length > 0 || lastSyncAffectedTokens.removed.length > 0)) {
2559
+ const mod = lastSyncAffectedTokens.modified;
2560
+ const add = lastSyncAffectedTokens.added;
2561
+ const rem = lastSyncAffectedTokens.removed;
2562
+ const tokenNames = [
2563
+ ...mod.map((m) => m.token),
2564
+ ...add,
2565
+ ...rem.map((r) => r.token)
2566
+ ];
2567
+ const fromParts = [];
2568
+ if (mod.length) fromParts.push(`${mod.length} modified`);
2569
+ if (add.length) fromParts.push(`${add.length} added`);
2570
+ if (rem.length) fromParts.push(`${rem.length} removed`);
2571
+ const toDetail = tokenNames.length <= 8 ? tokenNames.join(", ") : `${tokenNames.length} tokens (${tokenNames.slice(0, 4).join(", ")}...)`;
2572
+ changeRows.push({
2573
+ what: "Token changes (this run)",
2574
+ from: fromParts.join(", ") || "\u2014",
2575
+ to: toDetail
2576
+ });
2577
+ if (mod.length > 0 && mod.length <= 15) {
2578
+ mod.forEach((m) => {
2579
+ changeRows.push({ what: ` ${m.token}`, from: m.oldValue, to: m.newValue });
2580
+ });
2581
+ }
2582
+ }
2583
+ const changesList = "Report the following changes to the user (From \u2192 To). Do not mention what did not change.\n\n**Changes applied**\n\n" + changeRows.map((r) => `\u2022 **${r.what}:** ${r.from} \u2192 ${r.to}`).join("\n");
3190
2584
  const summary = parts.join("\n");
3191
2585
  const validationBlock = formatValidationBlock(allValidation);
3192
2586
  const hasFailure = allValidation.some((e) => e.status === "FAIL");
3193
2587
  const resultLine = hasFailure ? "syncAllResult: FAIL \u2014 Check VALIDATION section below. Do not report success to the user.\n\n" : "syncAllResult: OK\n\n";
3194
- const fullText = resultLine + (tokenResponseText ? `${summary}
3195
-
3196
- ---
2588
+ const fullText = resultLine + changesList + "\n\n" + (tokenResponseText ? `---
3197
2589
 
3198
- ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
2590
+ ${tokenResponseText}` : "") + validationBlock;
3199
2591
  return {
3200
2592
  content: [{ type: "text", text: fullText }]
3201
2593
  };
@@ -3233,10 +2625,17 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3233
2625
  families: fontNames,
3234
2626
  performanceHint: "Link fonts via URL (e.g. Google Fonts <link> or CSS @import); no need to download font files or add them to the repo. Prefer font-display: swap when possible. You must also build a complete typeset CSS: call listTypesets to get every typeset from the design system, then emit one CSS class per typeset (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too so the result is 1:1 with the DS. Use the CSS variable names returned by listTypesets. Do not create a file that only contains a font import."
3235
2627
  },
3236
- skill: {
3237
- path: ".cursor/skills/atomix-ds/SKILL.md",
3238
- content: GENERIC_SKILL_MD
3239
- },
2628
+ skill: (() => {
2629
+ const list = getSyncDependencySkills(data, String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
2630
+ const generic = list.find((s) => s.shortName === "SKILL.md");
2631
+ return generic ? { path: generic.path, content: GENERIC_SKILL_MD } : { path: ".cursor/skills/atomix-ds/SKILL.md", content: GENERIC_SKILL_MD };
2632
+ })(),
2633
+ ...getEffectiveTier() === "pro" ? {
2634
+ skillFigmaDesign: {
2635
+ path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
2636
+ content: FIGMA_DESIGN_SKILL_MD
2637
+ }
2638
+ } : {},
3240
2639
  tokenFiles: {
3241
2640
  files: ["tokens.css", "tokens.json"],
3242
2641
  copyInstructions: "Call the syncAll MCP tool to create the token file, skills, and atomix-dependencies.json; do not only suggest the user run sync later."
@@ -3244,7 +2643,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3244
2643
  showcase: platform2 === "web" || !platform2 ? {
3245
2644
  path: "atomix-setup-showcase.html",
3246
2645
  template: SHOWCASE_HTML_TEMPLATE,
3247
- substitutionInstructions: 'Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{TYPESETS_LINK}} = if a typeset CSS file was created (e.g. typesets.css), the full tag e.g. <link rel="stylesheet" href="typesets.css">, otherwise empty string. {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl or typeset-display-bold), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = typeset class for body text from listTypesets (e.g. typeset-body-md), or empty string if no typeset file. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export.'
2646
+ substitutionInstructions: 'The synced token file (from syncAll) always uses the --atmx- prefix for every CSS variable. Keep all var(--atmx-*) references in the template; do not remove or change the prefix. Replace placeholders with values from the synced token file. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css). {{TYPESETS_LINK}} = if a typeset CSS file was created, the full <link rel=\\"stylesheet\\" href=\\"typesets.css\\"> tag, otherwise empty string. {{DS_NAME}} = design system name. {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl), or empty string if no typeset file. {{LARGEST_BODY_TYPESET_CLASS}} = largest body typeset class from listTypesets (body role, largest font size; e.g. typeset-body-lg), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = default body typeset class from listTypesets (e.g. typeset-body-md), or empty string. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). Icon on circle uses luminance of brand primary (script sets white or black); no semantic foreground var. {{BUTTON_PADDING_VAR}} = var(--atmx-spacing-scale-md) or closest spacing token for button padding. {{BUTTON_HEIGHT_VAR}} = var(--atmx-sizing-height-md) or closest height token. {{BUTTON_RADIUS_VAR}} = var(--atmx-radius-scale-md) or var(--atmx-radius-scale-lg). {{CIRCLE_PADDING_VAR}} = var(--atmx-spacing-scale-md) or var(--atmx-spacing-scale-sm) for icon circle padding. {{ICON_SIZE_VAR}} = var(--atmx-sizing-icon-md) or var(--atmx-sizing-icon-lg). {{CHECK_ICON_SVG}} = inline SVG for Check icon from the design system icon library (getDependencies.iconLibrary.package: lucide-react, @heroicons/react, or phosphor-react). Use 24x24 viewBox; stroke=\\"currentColor\\" for Lucide/Heroicons, fill=\\"currentColor\\" for Phosphor so the script can set icon color by luminance. If unavailable, use: <svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M20 6L9 17l-5-5\\"/></svg>. Do not invent CSS variable names; use only vars that exist in the export.'
3248
2647
  } : void 0,
3249
2648
  meta: {
3250
2649
  dsName: data.meta.name,
@@ -3461,13 +2860,282 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3461
2860
  ...out.error ? { isError: true } : {}
3462
2861
  };
3463
2862
  }
2863
+ case "designInFigma": {
2864
+ if (cachedMcpTier !== "pro") {
2865
+ return {
2866
+ content: [{ type: "text", text: JSON.stringify({ error: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio" }, null, 2) }],
2867
+ isError: true
2868
+ };
2869
+ }
2870
+ const action = args?.action;
2871
+ if (action === "catalog") {
2872
+ try {
2873
+ const reachable = await isBridgeReachable();
2874
+ let fileCtx;
2875
+ if (reachable) {
2876
+ try {
2877
+ const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
2878
+ fileCtx = raw;
2879
+ } catch {
2880
+ }
2881
+ }
2882
+ const localVars = [];
2883
+ const libVars = [];
2884
+ const textStyleNames = [];
2885
+ const effectStyleNames = [];
2886
+ if (fileCtx) {
2887
+ for (const coll of fileCtx.variableCollections ?? []) {
2888
+ for (const v of coll.variables) localVars.push(v.name);
2889
+ }
2890
+ for (const coll of fileCtx.variableCollectionsLibrary ?? []) {
2891
+ for (const v of coll.variables) libVars.push(v.name);
2892
+ }
2893
+ for (const s of fileCtx.textStyles ?? []) textStyleNames.push(s.name);
2894
+ for (const s of fileCtx.effectStyles ?? []) effectStyleNames.push(s.name);
2895
+ }
2896
+ const catalogPayload = formatCatalogForMCP(FIGMA_DESIGN_CATALOG, {
2897
+ localVariables: localVars,
2898
+ libraryVariables: libVars,
2899
+ textStyles: textStyleNames,
2900
+ effectStyles: effectStyleNames
2901
+ });
2902
+ let designAssets = void 0;
2903
+ if (reachable) {
2904
+ try {
2905
+ const raw = await sendBridgeRequest("get_component_catalog", {});
2906
+ if (raw && typeof raw === "object") {
2907
+ designAssets = raw;
2908
+ }
2909
+ } catch {
2910
+ }
2911
+ }
2912
+ return {
2913
+ content: [{
2914
+ type: "text",
2915
+ text: JSON.stringify({
2916
+ ...catalogPayload,
2917
+ pluginConnected: reachable,
2918
+ ...designAssets ? { designAssets } : {},
2919
+ ...reachable ? {
2920
+ queryMethods: getQueryMethodNames(),
2921
+ queryHint: "Use action:'query' with queryMethod (e.g. get_selection, get_node_info, get_design_screenshot) and optional queryParams to read selection, node details, or a frame screenshot for review."
2922
+ } : {},
2923
+ ...reachable ? {} : { hint: "Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect." }
2924
+ }, null, 2)
2925
+ }]
2926
+ };
2927
+ } catch (e) {
2928
+ const msg = e instanceof Error ? e.message : String(e);
2929
+ return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
2930
+ }
2931
+ }
2932
+ if (action === "query") {
2933
+ const queryMethodRaw = args?.queryMethod;
2934
+ const queryParams = args?.queryParams ?? {};
2935
+ if (!queryMethodRaw || typeof queryMethodRaw !== "string") {
2936
+ return {
2937
+ content: [{ type: "text", text: JSON.stringify({ error: "query requires queryMethod (e.g. get_selection, get_node_info, get_design_screenshot)" }, null, 2) }],
2938
+ isError: true
2939
+ };
2940
+ }
2941
+ const queryMethod = queryMethodRaw.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
2942
+ const allowedQueryMethods = new Set(getQueryMethodNames());
2943
+ if (!allowedQueryMethods.has(queryMethod)) {
2944
+ return {
2945
+ content: [{ type: "text", text: JSON.stringify({ error: `Unknown query method "${queryMethod}". Allowed: ${[...allowedQueryMethods].join(", ")}` }, null, 2) }],
2946
+ isError: true
2947
+ };
2948
+ }
2949
+ try {
2950
+ const reachable = await isBridgeReachable();
2951
+ if (!reachable) {
2952
+ return {
2953
+ content: [{
2954
+ type: "text",
2955
+ text: JSON.stringify({
2956
+ error: "Figma plugin not connected.",
2957
+ bridgeNotRunning: true,
2958
+ agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma with action:'query' again."
2959
+ }, null, 2)
2960
+ }],
2961
+ isError: true
2962
+ };
2963
+ }
2964
+ const result = await sendBridgeRequest(queryMethod, queryParams);
2965
+ const content = [];
2966
+ const summary = { method: queryMethod, success: true };
2967
+ if (result && typeof result === "object" && "imageBase64" in result && typeof result.imageBase64 === "string") {
2968
+ summary.screenshot = "included below (base64 PNG)";
2969
+ summary.format = result.format ?? "PNG";
2970
+ summary.scale = result.scale;
2971
+ content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2972
+ content.push({
2973
+ type: "image",
2974
+ data: result.imageBase64,
2975
+ mimeType: "image/png"
2976
+ });
2977
+ } else {
2978
+ summary.result = result;
2979
+ content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2980
+ }
2981
+ return { content };
2982
+ } catch (e) {
2983
+ const msg = e instanceof Error ? e.message : String(e);
2984
+ return { content: [{ type: "text", text: JSON.stringify({ method: queryMethod, success: false, error: msg }, null, 2) }], isError: true };
2985
+ }
2986
+ }
2987
+ if (action === "execute") {
2988
+ const rawSteps = args?.steps;
2989
+ if (!rawSteps || !Array.isArray(rawSteps) || rawSteps.length === 0) {
2990
+ return {
2991
+ content: [{ type: "text", text: JSON.stringify({ error: "steps array is required for action:'execute'" }, null, 2) }],
2992
+ isError: true
2993
+ };
2994
+ }
2995
+ try {
2996
+ const reachable = await isBridgeReachable();
2997
+ if (!reachable) {
2998
+ return {
2999
+ content: [{
3000
+ type: "text",
3001
+ text: JSON.stringify({
3002
+ error: "Figma plugin not connected.",
3003
+ bridgeNotRunning: true,
3004
+ agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma again."
3005
+ }, null, 2)
3006
+ }],
3007
+ isError: true
3008
+ };
3009
+ }
3010
+ const designMethods = /* @__PURE__ */ new Set([
3011
+ ...getDesignMethodNames(),
3012
+ "list_local_components",
3013
+ "get_component_catalog",
3014
+ "get_design_screenshot",
3015
+ "get_variable_collection_modes",
3016
+ "get_frame_variable_mode"
3017
+ ]);
3018
+ const warnings = [];
3019
+ let fileCtx;
3020
+ try {
3021
+ const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
3022
+ fileCtx = raw;
3023
+ } catch {
3024
+ }
3025
+ const resolvers = fileCtx ? buildResolvers(fileCtx) : null;
3026
+ const isFigmaNodeId = (s) => /^\d+:\d+$/.test(s);
3027
+ let rootFrameId = null;
3028
+ let lastCreatedFrameNodeId = null;
3029
+ const namedNodeIds = /* @__PURE__ */ new Map();
3030
+ const results = [];
3031
+ for (let i = 0; i < rawSteps.length; i++) {
3032
+ const step = rawSteps[i];
3033
+ const method = (step.method ?? "").replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
3034
+ if (!designMethods.has(method)) {
3035
+ warnings.push({ step: i, method, issue: `Unknown method "${method}" \u2014 skipped` });
3036
+ continue;
3037
+ }
3038
+ let params = { ...step.params ?? {} };
3039
+ if (resolvers) {
3040
+ params = resolveStepParams(params, resolvers);
3041
+ }
3042
+ if (rootFrameId) {
3043
+ const parentId = params.parentId;
3044
+ const frameId = params.frameId;
3045
+ if (typeof parentId === "string" && !isFigmaNodeId(parentId)) {
3046
+ params.parentId = namedNodeIds.get(parentId) ?? rootFrameId;
3047
+ }
3048
+ if (typeof frameId === "string" && !isFigmaNodeId(frameId)) {
3049
+ params.frameId = namedNodeIds.get(frameId) ?? rootFrameId;
3050
+ }
3051
+ if (method === "finalize_design_frame" && !params.frameId) {
3052
+ params.frameId = rootFrameId;
3053
+ }
3054
+ const needsParent = method.startsWith("design_create_") || method === "design_create_component";
3055
+ if (needsParent && !params.parentId) {
3056
+ params.parentId = rootFrameId;
3057
+ }
3058
+ }
3059
+ const nodeId = params.nodeId;
3060
+ if (typeof nodeId === "string" && nodeId && !isFigmaNodeId(nodeId)) {
3061
+ const resolved = namedNodeIds.get(nodeId) ?? lastCreatedFrameNodeId ?? rootFrameId;
3062
+ params.nodeId = resolved ?? nodeId;
3063
+ }
3064
+ const needsNode = [
3065
+ "design_set_auto_layout",
3066
+ "design_set_layout_sizing",
3067
+ "design_set_effects",
3068
+ "design_set_strokes",
3069
+ "design_resize_node",
3070
+ "design_set_resize_constraints",
3071
+ "design_set_layout_constraints",
3072
+ "design_set_node_position",
3073
+ "design_set_text_properties"
3074
+ ].includes(method);
3075
+ if (needsNode && !params.nodeId) {
3076
+ params.nodeId = lastCreatedFrameNodeId ?? rootFrameId;
3077
+ }
3078
+ const childId = params.childId;
3079
+ if (typeof childId === "string" && childId && !isFigmaNodeId(childId)) {
3080
+ params.childId = namedNodeIds.get(childId) ?? childId;
3081
+ }
3082
+ try {
3083
+ const response = await sendBridgeRequest(method, params);
3084
+ const res = response;
3085
+ if (method === "create_design_placeholder" && typeof res?.frameId === "string") {
3086
+ rootFrameId = res.frameId;
3087
+ }
3088
+ const isCreateMethod = method.startsWith("design_create_") || method === "design_convert_to_component" || method === "design_combine_as_variants" || method === "design_create_frame_from_preset" || method === "design_group_nodes";
3089
+ if (isCreateMethod && typeof res?.nodeId === "string") {
3090
+ const createdNodeId = res.nodeId;
3091
+ if (method === "design_create_frame" || method === "design_create_frame_from_preset") {
3092
+ lastCreatedFrameNodeId = createdNodeId;
3093
+ if (!rootFrameId && res?.isRoot === true) {
3094
+ rootFrameId = createdNodeId;
3095
+ }
3096
+ }
3097
+ const stepName = step.params?.name ?? "";
3098
+ if (stepName) namedNodeIds.set(stepName, createdNodeId);
3099
+ }
3100
+ results.push({ step: i, method, result: response });
3101
+ } catch (e) {
3102
+ const errMsg = e instanceof Error ? e.message : String(e);
3103
+ results.push({ step: i, method, error: errMsg });
3104
+ if (!rootFrameId && (method === "design_create_frame" || method === "create_design_placeholder")) break;
3105
+ }
3106
+ }
3107
+ const errorCount = results.filter((r) => r.error).length;
3108
+ const success = errorCount === 0;
3109
+ return {
3110
+ content: [{
3111
+ type: "text",
3112
+ text: JSON.stringify({
3113
+ success,
3114
+ stepsExecuted: results.length,
3115
+ errors: errorCount,
3116
+ warnings,
3117
+ results
3118
+ }, null, 2)
3119
+ }],
3120
+ ...errorCount > 0 ? { isError: true } : {}
3121
+ };
3122
+ } catch (e) {
3123
+ const msg = e instanceof Error ? e.message : String(e);
3124
+ return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
3125
+ }
3126
+ }
3127
+ return {
3128
+ content: [{ type: "text", text: JSON.stringify({ error: "action must be 'catalog', 'query', or 'execute'" }, null, 2) }],
3129
+ isError: true
3130
+ };
3131
+ }
3464
3132
  default:
3465
3133
  return {
3466
3134
  content: [{
3467
3135
  type: "text",
3468
3136
  text: JSON.stringify({
3469
3137
  error: `Unknown tool: ${name}`,
3470
- availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma"]
3138
+ availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma", "designInFigma"]
3471
3139
  }, null, 2)
3472
3140
  }]
3473
3141
  };
@@ -3509,6 +3177,43 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3509
3177
  };
3510
3178
  }
3511
3179
  });
3180
+ function getEffectiveTier() {
3181
+ return cachedMcpTier;
3182
+ }
3183
+ function getSyncDependencySkills(data, dsVersion, dsExportedAt) {
3184
+ const tier = getEffectiveTier();
3185
+ const skills = [];
3186
+ const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
3187
+ skills.push({
3188
+ path: ".cursor/skills/atomix-ds/SKILL.md",
3189
+ content: genericWithVersion,
3190
+ shortName: "SKILL.md"
3191
+ });
3192
+ if (tier === "pro") {
3193
+ const figmaSkillWithFrontmatter = `---
3194
+ name: atomix-figma-design
3195
+ description: Figma design skill \u2014 create high-quality, token-bound designs in Figma from any reference (screenshots, images, code snippets, URLs). Covers mobile, web, token application, and component/variant creation. Use with syncToFigma and designInFigma MCP tools when the Atomix plugin is connected.
3196
+ ---
3197
+ ${FIGMA_DESIGN_SKILL_MD}`;
3198
+ const figmaSkillWithVersion = injectSkillVersion(figmaSkillWithFrontmatter, dsVersion, dsExportedAt);
3199
+ skills.push({
3200
+ path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
3201
+ content: figmaSkillWithVersion,
3202
+ shortName: "figma-design-SKILL.md"
3203
+ });
3204
+ }
3205
+ return skills;
3206
+ }
3207
+ function readSkillVersionFromFile(filePath) {
3208
+ if (!fs.existsSync(filePath)) return null;
3209
+ try {
3210
+ const raw = fs.readFileSync(filePath, "utf-8");
3211
+ const match = raw.match(/atomixDsVersion:\s*["']([^"']+)["']/);
3212
+ return match ? match[1] : null;
3213
+ } catch {
3214
+ return null;
3215
+ }
3216
+ }
3512
3217
  function injectSkillVersion(content, version, exportedAt) {
3513
3218
  const endOfFrontmatter = content.indexOf("\n---\n", 3);
3514
3219
  if (endOfFrontmatter === -1) return content;
@@ -3521,51 +3226,124 @@ atomixDsExportedAt: "${exportedAt ?? ""}"
3521
3226
  }
3522
3227
  var GENERIC_SKILL_MD = `---
3523
3228
  name: atomix-ds
3524
- description: Use the Atomix design system for UI, tokens, and styles. Fetch rules and tokens via MCP tools; never hardcode design values.
3229
+ description: Use the project's design system tokens semantically when building or editing any UI. Covers everyday tasks \u2014 creating components, building pages, styling forms, theming, dark mode, responsive layouts, adding icons, choosing colors, setting type, adjusting spacing, and applying motion. Works with any platform or framework (web, React Native, iOS, Android, Flutter, etc.). Fetch token values and governance rules via MCP tools (getToken, listTokens, searchTokens, getRules, validateUsage). Tokens include colors, typography, spacing, sizing, radius, shadows, borders, motion, z-index, and icons. Never hardcode visual values \u2014 always resolve from the design system first.
3525
3230
  ---
3526
3231
 
3527
3232
  # Atomix Design System
3528
3233
 
3529
- Use this skill when editing UI, design system files, or when the user asks to follow the project's design system. Your job is to **get the relevant data from the design system via MCP** and apply it\u2014not to guess or invent values.
3234
+ **Prerequisite:** Load rules first before anything else \u2014 call getRules() at the start of the session or before writing any visual code.
3530
3235
 
3531
- ## Goal
3532
-
3533
- Complete the user's task using the design system as the single source of truth. Every color, spacing, typography, radius, shadow, or sizing value must come from the MCP tools (tokens or governance rules). Do not hardcode hex codes, pixel values, or font names.
3236
+ This project uses a design system with semantic tokens. **Before writing any visual code**, fetch the relevant tokens and governance rules from the MCP tools below. The design system is the single source of truth \u2014 do not guess, invent, or hardcode visual values.
3534
3237
 
3535
3238
  ## When to use
3536
3239
 
3537
- - Building or editing UI components, pages, or styles
3538
- - Working in design system or token files (e.g. \`tokens.css\`, theme files)
3539
- - User asks to "follow the design system", "use tokens", or "match the DS"
3540
- - Validating or refactoring existing code, UI or visual design for token compliance
3240
+ Use this skill for everyday design and frontend tasks:
3241
+
3242
+ **Building UI**
3243
+ - Creating a component (button, card, input, modal, nav, sidebar, table, list, badge, tooltip\u2026)
3244
+ - Laying out a page, screen, or view
3245
+ - Building a form, dialog, popover, or overlay
3246
+ - Adding a header, footer, hero section, or onboarding flow
3247
+
3248
+ **Styling & theming**
3249
+ - Choosing colors for backgrounds, text, borders, icons, or interactive states
3250
+ - Setting typography \u2014 font family, size, weight, line height, letter spacing
3251
+ - Adjusting spacing, padding, margins, or gaps between elements
3252
+ - Applying border radius, shadows, or elevation to surfaces
3253
+ - Adding hover, focus, active, disabled, or loading states
3254
+ - Setting up dark mode, light mode, or theme switching
3255
+ - Configuring responsive or adaptive layouts
3256
+
3257
+ **Working with assets**
3258
+ - Rendering icons (sizing and stroke width are defined by the design system)
3259
+ - Implementing designs from Figma, mockups, screenshots, or design specs
3260
+ - Translating a design handoff into code
3261
+
3262
+ **Maintenance**
3263
+ - Refactoring hardcoded values to use tokens
3264
+ - Auditing code for design system compliance
3265
+ - Updating styles after a design system version change
3266
+
3267
+ If the task has no visual output (pure logic, data, APIs, DevOps), this skill is not needed.
3268
+
3269
+ ## Semantic token usage
3270
+
3271
+ Tokens have two layers \u2014 **primitives** (raw scales) and **semantic** (purpose-driven). Always prefer semantic tokens because they adapt to themes and modes automatically.
3272
+
3273
+ | Intent | Use semantic token | Avoid raw primitive |
3274
+ |--------|-------------------|---------------------|
3275
+ | Page background | \`bg-page\` | \`neutral.50\` |
3276
+ | Card surface | \`bg-surface\` | \`white\` |
3277
+ | Primary text | \`text-primary\` | \`neutral.900\` |
3278
+ | Muted text | \`text-muted\` | \`neutral.500\` |
3279
+ | Default border | \`border-default\` | \`neutral.200\` |
3280
+ | Brand action | \`brand.primary\` | \`green.600\` |
3281
+ | Error state | \`status.error\` | \`red.500\` |
3282
+
3283
+ Call \`getRules\` for the full mapping of semantic tokens to primitives in each theme.
3284
+
3285
+ When a semantic token doesn't exist for your use case, use the closest primitive from \`listTokens\` \u2014 but document why so the team can promote it to a semantic token later.
3286
+
3287
+ ## How to fetch design system data
3288
+
3289
+ ### 1. Governance rules \u2014 always fetch first
3541
3290
 
3542
- ## How to get design system data
3291
+ \`getRules()\` \u2014 optionally with a topic: \`colors\`, \`typo\`, \`motion\`, \`icons\`, \`layout\`, or \`visual\` (color, border, radius, shadows, icons). Returns how tokens should be applied \u2014 naming conventions, variable format, and semantic mappings.
3543
3292
 
3544
- **1. Governance rules (how to use tokens in code)**
3545
- Call **getAIToolRules** with the tool id for your current environment: \`cursor\`, \`windsurf\`, \`copilot\`, \`cline\`, \`continue\`, \`zed\`, or \`generic\`.
3546
- Example: \`getAIToolRules({ tool: "cursor" })\`.
3547
- Alternatively use the **/--rules** prompt or the resource \`atomix://rules/<tool>\`.
3293
+ ### 2. Token values \u2014 by task
3548
3294
 
3549
- **2. Token values (what to use in code)**
3550
- - **getToken(path)** \u2014 One token by path (e.g. \`colors.brand.primary\`, \`typography.fontSize.lg\`, \`sizing.icon.sm\`, \`icons.strokeWidth\`). Icon stroke width is at \`icons.strokeWidth\` when the design system defines it.
3551
- - **listTokens(category)** \u2014 All tokens in a category: \`colors\`, \`typography\`, \`spacing\`, \`sizing\`, \`shadows\`, \`radius\`, \`borders\`, \`motion\`, \`zIndex\`. For icon config (e.g. stroke width), use \`getToken("icons.strokeWidth")\` since \`icons\` is not a list category.
3552
- - **searchTokens(query)** \u2014 Find tokens by name or value.
3295
+ | I need\u2026 | MCP call |
3296
+ |---------|----------|
3297
+ | A specific token | \`getToken("colors.brand.primary")\` or \`getToken("spacing.scale.md")\` |
3298
+ | All tokens in a category | \`listTokens("colors")\` \u2014 categories: \`colors\`, \`typography\`, \`spacing\`, \`sizing\`, \`shadows\`, \`radius\`, \`borders\`, \`motion\`, \`zIndex\` |
3299
+ | Search by name or value | \`searchTokens("primary")\` or \`searchTokens("bold")\` |
3300
+ | Icon size or stroke | \`getToken("sizing.icon.sm")\` for dimensions, \`getToken("icons.strokeWidth")\` for stroke |
3301
+ | Typeset classes | \`listTypesets()\` \u2014 emit one class per typeset; include text-transform and text-decoration for 1:1 match |
3553
3302
 
3554
- **3. Validation**
3555
- - **validateUsage(value, context)** \u2014 Check if a CSS/value should use a token instead (e.g. \`validateUsage("#007061", "color")\`).
3303
+ ### 3. Validation
3556
3304
 
3557
- **4. Syncing tokens to a file**
3558
- - **syncAll({ output?, format?, skipTokens? })** \u2014 Syncs tokens to a file, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Default output \`./tokens.css\`, format \`css\`. Use \`skipTokens: true\` to only write skills and manifest.
3305
+ \`validateUsage("#007061", "color")\` \u2014 checks if a raw value should be a token. Run this on any value you suspect is hardcoded.
3559
3306
 
3560
- Use the returned rules and token paths/values when generating or editing code. Prefer CSS variables (e.g. \`var(--atmx-*)\`) or the exact token references from the tools.
3307
+ ### 4. Syncing tokens to a file
3308
+
3309
+ \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
3310
+
3311
+ ## Workflow
3312
+
3313
+ 1. **Fetch rules** \u2014 call \`getRules\` once per session (or with a topic when working on a specific area).
3314
+ 2. **Fetch tokens** \u2014 call \`getToken\`, \`listTokens\`, or \`searchTokens\` for the values you need.
3315
+ 3. **Apply semantically** \u2014 use token references or CSS variables (\`var(--atmx-*)\`) depending on your platform. Choose the semantic token that matches the *purpose*, not just the visual appearance.
3316
+ 4. **Self-check** \u2014 scan your output for hardcoded hex codes, pixel values, rem/em literals, duration strings, or font names. If found, replace with the matching token.
3317
+
3318
+ ## Common mistakes
3319
+
3320
+ Do not hardcode visual values. Always resolve from the design system:
3321
+
3322
+ - A hex color (\`#007061\`, \`#333\`) \u2192 call \`getToken\` or \`searchTokens\` for the matching color token
3323
+ - A pixel/rem value (\`16px\`, \`1.5rem\`) \u2192 use the spacing, sizing, or radius token
3324
+ - A font name (\`"Inter"\`, \`"SF Pro"\`) \u2192 use the typography font-family token
3325
+ - A duration (\`200ms\`, \`0.3s\`) \u2192 use the motion duration token
3326
+ - A numeric weight (\`600\`, \`700\`) \u2192 use the typography font-weight token
3327
+ - A shadow string (\`0 4px 6px rgba(\u2026)\`) \u2192 use the shadow elevation token
3328
+
3329
+ If no token matches, call \`searchTokens\` to find the closest option. Never invent a token path.
3561
3330
 
3562
3331
  ## Best practices
3563
3332
 
3564
- - **Fetch first:** Before writing UI or styles, call getAIToolRules and/or getToken/listTokens so you know the exact tokens and conventions.
3565
- - **Icons:** Apply the design system's icon tokens when rendering icons: sizing via \`getToken("sizing.icon.sm")\` or \`listTokens("sizing")\`, and stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it; do not use hardcoded sizes or stroke widths.
3566
- - **Typography:** Use typography tokens from the DS for any text. When creating global typeset CSS, call **listTypesets** and emit one CSS class per typeset (do not skip any); include text-transform and text-decoration when present for 1:1 match.
3567
- - **No guessing:** If a value is not in the rules or token list, use searchTokens or listTokens to find the closest match rather than inventing a value.
3568
- - **Version check:** If this skill file has frontmatter \`atomixDsVersion\`, compare it to the design system version from **getDependencies** (\`meta.designSystemVersion\`). If the design system is newer, suggest the user run **syncAll** to update skills and tokens.
3333
+ - **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
3334
+ - **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
3335
+ - **Icons:** Size via \`getToken("sizing.icon.sm")\`; stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it.
3336
+ - **Typography:** Use **typeset classes** (e.g. from \`typesets.css\` or \`listTypesets()\` output \u2014 \`typeset-heading-h1\`, \`typeset-body-md-regular\`, etc.) for all text; do not re-declare font-family, font-size, font-weight, line-height, or letter-spacing in component CSS, which duplicates the design system. For global typeset output, call **listTypesets** and emit every entry; include text-transform and text-decoration for 1:1 match.
3337
+ - **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
3338
+ - **Platform agnostic:** Token values work across CSS, Tailwind, React Native, SwiftUI, Compose, Flutter, and any style system. Use the output format appropriate to your platform.
3339
+ - **Version check:** If this file has frontmatter \`atomixDsVersion\`, compare to the version from **getDependencies** (\`meta.designSystemVersion\`). If the DS is newer, suggest running **syncAll** to update.
3340
+
3341
+ ## Strict mode and patterns
3342
+
3343
+ - **Strict mode:** NO arbitrary values (e.g. \`bg-[#ff0000]\` forbidden). NO hardcoded colors \u2014 use CSS variables and semantic tokens only. NO hardcoded pixel values \u2014 use spacing/sizing tokens only. NO hardcoded typography \u2014 use typography typeset tokens only. Token vocabulary only \u2014 if a value is not in the design system, do not use it.
3344
+ - **CSS variables:** All tokens follow \`--atmx-{category}-{subcategory}-{token}\`. Correct: \`backgroundColor: "var(--atmx-color-bg-surface)"\`, \`borderRadius: "var(--atmx-radius-scale-md)"\`. Wrong: hex, raw px.
3345
+ - **Dark mode:** Colors switch in dark mode when using CSS variables; \`.dark\` on root toggles color variables.
3346
+ - **Button pattern:** Height and padding use sizing/spacing tokens; typography from typeset tokens; radius and border from tokens; transition from motion tokens; primary/secondary/ghost use action color tokens (e.g. \`--atmx-color-action-primary\`, \`--atmx-color-action-on-primary\`).
3569
3347
  `;
3570
3348
  var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3571
3349
  <html lang="en">
@@ -3581,52 +3359,153 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3581
3359
  body {
3582
3360
  margin: 0;
3583
3361
  font-family: {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3584
- background: {{BRAND_PRIMARY_VAR}};
3585
- color: {{BRAND_PRIMARY_FOREGROUND_VAR}};
3362
+ background: var(--atmx-color-bg-page);
3363
+ color: var(--atmx-color-text-primary);
3586
3364
  min-height: 100vh;
3587
- padding: 2rem;
3365
+ padding: var(--atmx-spacing-scale-2xl);
3588
3366
  display: flex;
3589
3367
  justify-content: center;
3590
3368
  align-items: center;
3591
3369
  }
3592
3370
  .wrap { width: 375px; max-width: 100%; }
3593
- .icon { width: 2rem; height: 2rem; margin: 0 0 1rem; }
3371
+ .top-row {
3372
+ display: flex;
3373
+ justify-content: space-between;
3374
+ align-items: flex-start;
3375
+ margin-bottom: var(--atmx-spacing-scale-lg);
3376
+ }
3377
+ .top-row .mode-toggle { margin-bottom: 0; }
3378
+ .icon-circle {
3379
+ display: inline-flex;
3380
+ align-items: center;
3381
+ justify-content: center;
3382
+ width: calc({{ICON_SIZE_VAR}} + 2 * {{CIRCLE_PADDING_VAR}});
3383
+ height: calc({{ICON_SIZE_VAR}} + 2 * {{CIRCLE_PADDING_VAR}});
3384
+ padding: {{CIRCLE_PADDING_VAR}};
3385
+ background: {{BRAND_PRIMARY_VAR}};
3386
+ border-radius: 50%;
3387
+ }
3388
+ .icon-circle.light-icon { color: #fff; }
3389
+ .icon-circle.dark-icon { color: #000; }
3390
+ .icon-circle svg {
3391
+ width: {{ICON_SIZE_VAR}};
3392
+ height: {{ICON_SIZE_VAR}};
3393
+ flex-shrink: 0;
3394
+ }
3395
+ .mode-toggle {
3396
+ display: inline-flex;
3397
+ align-items: center;
3398
+ justify-content: center;
3399
+ padding: {{BUTTON_PADDING_VAR}};
3400
+ height: {{BUTTON_HEIGHT_VAR}};
3401
+ border-radius: {{BUTTON_RADIUS_VAR}};
3402
+ background: var(--atmx-color-bg-surface);
3403
+ color: var(--atmx-color-text-primary);
3404
+ border: 1px solid var(--atmx-color-border-default);
3405
+ font-family: inherit;
3406
+ font-size: inherit;
3407
+ font-weight: inherit;
3408
+ cursor: pointer;
3409
+ margin-bottom: var(--atmx-spacing-scale-xl);
3410
+ }
3411
+ .mode-toggle:hover {
3412
+ background: var(--atmx-color-bg-muted);
3413
+ }
3594
3414
  h1 {
3595
3415
  font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3596
- font-size: clamp(3rem, 4vw, 5rem);
3597
- font-weight: 700;
3598
- margin: 0 0 0.75rem;
3416
+ margin: 0 0 var(--atmx-spacing-scale-md);
3599
3417
  line-height: 1.2;
3600
3418
  }
3601
- .lead { margin: 0 0 1.5rem; font-size: 1rem; line-height: 1.5; opacity: 0.95; }
3602
- .now { margin: 1.5rem 0 0; font-size: 0.875rem; line-height: 1.6; opacity: 0.95; text-align: left; }
3603
- .now strong { display: block; margin-bottom: 0.5rem; }
3604
- .now ul { margin: 0; padding-left: 1.25rem; }
3605
- .tips { margin-top: 1.5rem; font-size: 0.875rem; line-height: 1.6; opacity: 0.9; }
3419
+ .lead {
3420
+ margin: 0 0 var(--atmx-spacing-scale-xl);
3421
+ opacity: 0.95;
3422
+ }
3423
+ .now { margin: var(--atmx-spacing-scale-xl) 0 0; opacity: 0.95; text-align: left; }
3424
+ .now strong { display: block; margin-bottom: var(--atmx-spacing-scale-sm); }
3425
+ .now ul { margin: 0; padding-left: var(--atmx-spacing-scale-xl); }
3426
+ .tips { margin-top: var(--atmx-spacing-scale-xl); opacity: 0.9; }
3606
3427
  .tips a { color: inherit; text-decoration: underline; }
3428
+ code {
3429
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
3430
+ font-size: 0.8125rem;
3431
+ padding: 0.125rem 0.375rem;
3432
+ border-radius: 0.25rem;
3433
+ background: var(--atmx-color-bg-muted);
3434
+ }
3607
3435
  </style>
3608
3436
  </head>
3609
3437
  <body class="{{BODY_TYPESET_CLASS}}">
3610
3438
  <div class="wrap">
3611
- <div class="icon" aria-hidden="true">
3612
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="44" height="44"><path d="M20 6L9 17l-5-5"/></svg>
3439
+ <div class="top-row">
3440
+ <div class="icon-circle" aria-hidden="true">
3441
+ {{CHECK_ICON_SVG}}
3442
+ </div>
3443
+ <button type="button" class="mode-toggle" id="mode-toggle" aria-label="Toggle light or dark mode">Dark</button>
3613
3444
  </div>
3614
3445
  <h1 class="{{LARGEST_DISPLAY_TYPESET_CLASS}}">You're all set with {{DS_NAME}}</h1>
3615
- <p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon.</p>
3616
- <div class="now">
3446
+ <p class="lead {{LARGEST_BODY_TYPESET_CLASS}}">This page uses your design system: semantic colors (mode-aware), headline and body typesets, and an icon.</p>
3447
+ <div class="now {{LARGEST_BODY_TYPESET_CLASS}}">
3617
3448
  <strong>What you can do now:</strong>
3618
3449
  <ul>
3619
3450
  <li>Ask your agent to build your designs using the design system tokens</li>
3620
3451
  <li>Build components and pages that use <code>var(--atmx-*)</code> for colors, spacing, and typography</li>
3621
- <li>Run <code>/--rules</code> to load governance rules; run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
3452
+ <li>Run <code>/--rules</code> to load governance rules (or call getRules); run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
3622
3453
  </ul>
3623
3454
  </div>
3624
- <p class="tips">Keep the source of truth at <a href="https://atomix.studio" target="_blank" rel="noopener">atomix.studio</a> \u2014 avoid editing token values in this repo.</p>
3455
+ <p class="tips {{LARGEST_BODY_TYPESET_CLASS}}">Keep the source of truth at <a href="https://atomix.studio" target="_blank" rel="noopener">atomix.studio</a> \u2014 avoid editing token values in this repo.</p>
3625
3456
  </div>
3457
+ <script>
3458
+ (function() {
3459
+ var root = document.documentElement;
3460
+ var btn = document.getElementById('mode-toggle');
3461
+ var circle = document.querySelector('.icon-circle');
3462
+ function hexToRgb(hex) {
3463
+ var m = hex.slice(1).match(/.{2}/g);
3464
+ return m ? m.map(function(x) { return parseInt(x, 16) / 255; }) : [0, 0, 0];
3465
+ }
3466
+ function relativeLuminance(r, g, b) {
3467
+ var srgb = function(x) { return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); };
3468
+ return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
3469
+ }
3470
+ function setIconContrast() {
3471
+ if (!circle) return;
3472
+ var bg = getComputedStyle(circle).backgroundColor;
3473
+ var r = 0, g = 0, b = 0;
3474
+ var rgbMatch = bg.match(/rgb\\(?\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)?/);
3475
+ if (rgbMatch) {
3476
+ r = parseInt(rgbMatch[1], 10) / 255;
3477
+ g = parseInt(rgbMatch[2], 10) / 255;
3478
+ b = parseInt(rgbMatch[3], 10) / 255;
3479
+ } else if (bg.indexOf('#') === 0) {
3480
+ var parts = hexToRgb(bg);
3481
+ r = parts[0]; g = parts[1]; b = parts[2];
3482
+ } else return;
3483
+ var L = relativeLuminance(r, g, b);
3484
+ circle.classList.remove('light-icon', 'dark-icon');
3485
+ circle.classList.add(L > 0.179 ? 'dark-icon' : 'light-icon');
3486
+ }
3487
+ function updateLabel() {
3488
+ var isDark = root.classList.contains('dark');
3489
+ btn.textContent = isDark ? 'Light' : 'Dark';
3490
+ btn.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode');
3491
+ }
3492
+ btn.addEventListener('click', function() {
3493
+ root.classList.toggle('dark');
3494
+ root.setAttribute('data-theme', root.classList.contains('dark') ? 'dark' : 'light');
3495
+ updateLabel();
3496
+ setIconContrast();
3497
+ });
3498
+ if (root.classList.contains('dark') || root.getAttribute('data-theme') === 'dark') {
3499
+ root.classList.add('dark');
3500
+ root.setAttribute('data-theme', 'dark');
3501
+ }
3502
+ updateLabel();
3503
+ setIconContrast();
3504
+ })();
3505
+ </script>
3626
3506
  </body>
3627
3507
  </html>
3628
3508
  `;
3629
- var AI_TOOLS = ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic"];
3630
3509
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
3631
3510
  if (!hasValidAuthConfig() || authFailedNoTools) {
3632
3511
  throw new Error(AUTH_REQUIRED_MESSAGE);
@@ -3681,26 +3560,44 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
3681
3560
  }]
3682
3561
  };
3683
3562
  }
3684
- const rulesMatch = uri.match(/^atomix:\/\/rules\/(.+)$/);
3563
+ const rulesMatch = uri.match(/^atomix:\/\/rules(?:\/(.+))?$/);
3685
3564
  if (rulesMatch) {
3686
- const tool = rulesMatch[1];
3687
- if (!AI_TOOLS.includes(tool)) {
3688
- throw new Error(`Unknown tool: ${tool}. Available: ${AI_TOOLS.join(", ")}`);
3689
- }
3690
- const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool}`;
3565
+ const topicRaw = rulesMatch[1]?.toLowerCase().trim();
3566
+ const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3691
3567
  const headers = { "Content-Type": "application/json" };
3692
3568
  if (apiKey) headers["x-api-key"] = apiKey;
3693
3569
  const response = await fetch(rulesUrl, { headers });
3694
- if (!response.ok) {
3695
- throw new Error(`Failed to fetch rules: ${response.status}`);
3570
+ if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
3571
+ const payload = await response.json();
3572
+ const categories = payload.categories ?? {};
3573
+ const allRules = payload.rules ?? [];
3574
+ const topicToCategories = {
3575
+ colors: ["general", "colors"],
3576
+ typo: ["general", "typography"],
3577
+ typography: ["general", "typography"],
3578
+ motion: ["general", "motion"],
3579
+ icons: ["general", "icons"],
3580
+ layout: ["general", "spacing", "sizing", "layout"],
3581
+ visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
3582
+ style: ["general", "colors", "borders", "radius", "shadows", "icons"]
3583
+ };
3584
+ if (!topicRaw || !topicToCategories[topicRaw]) {
3585
+ return {
3586
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
3587
+ };
3588
+ }
3589
+ const categoryKeys = topicToCategories[topicRaw];
3590
+ const filteredCategories = {};
3591
+ const filteredRules = [];
3592
+ for (const key of categoryKeys) {
3593
+ const list = categories[key];
3594
+ if (list?.length) {
3595
+ filteredCategories[key] = list;
3596
+ filteredRules.push(...list);
3597
+ }
3696
3598
  }
3697
- const rulesData = await response.json();
3698
3599
  return {
3699
- contents: [{
3700
- uri,
3701
- mimeType: "text/markdown",
3702
- text: rulesData.content || JSON.stringify(rulesData, null, 2)
3703
- }]
3600
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2) }]
3704
3601
  };
3705
3602
  }
3706
3603
  throw new Error(`Unknown resource: ${uri}`);
@@ -3723,11 +3620,14 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
3723
3620
  const prompts = [
3724
3621
  { name: "--hello", description: "Get started with this design system - overview, tokens, and tools. Run this first!" },
3725
3622
  { name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
3726
- { name: "--rules", description: "Get the design system governance rules for your AI coding tool (default: cursor)." },
3623
+ { name: "--rules", description: "Get design system governance rules (optionally by topic: colors, typo, motion, icons, layout, visual)." },
3727
3624
  { name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
3728
3625
  { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
3729
3626
  { name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
3730
3627
  ];
3628
+ if (cachedMcpTier === "pro") {
3629
+ prompts.push({ name: "--design-in-figma", description: "Load Figma design skill and use designInFigma tool to design UI in the connected Figma file. Pro only." });
3630
+ }
3731
3631
  return { prompts };
3732
3632
  });
3733
3633
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
@@ -3744,8 +3644,18 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3744
3644
  }]
3745
3645
  };
3746
3646
  }
3747
- const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name;
3748
- const shouldForceRefresh = canonicalName === "sync" || canonicalName === "refactor";
3647
+ const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name === "--design-in-figma" || name === "designInFigma" ? "design-in-figma" : name;
3648
+ const shouldForceRefresh = [
3649
+ "hello",
3650
+ "atomix-setup",
3651
+ // --get-started
3652
+ "design-system-rules",
3653
+ // --rules
3654
+ "sync",
3655
+ "refactor",
3656
+ "sync-to-figma",
3657
+ "design-in-figma"
3658
+ ].includes(canonicalName);
3749
3659
  let data = null;
3750
3660
  let stats = null;
3751
3661
  try {
@@ -3848,6 +3758,20 @@ Both are required. Configure the MCP server in your AI tool's MCP settings, then
3848
3758
  lines.push(instructions);
3849
3759
  return lines.join("\n");
3850
3760
  };
3761
+ function withMcpNotice(res) {
3762
+ if (!mcpUpdateNotice || res.messages.length === 0) return res;
3763
+ const first = res.messages[0];
3764
+ if (first?.content?.type === "text" && typeof first.content.text === "string") {
3765
+ return {
3766
+ ...res,
3767
+ messages: [
3768
+ { ...first, content: { ...first.content, text: first.content.text + "\n\n---\n\n" + mcpUpdateNotice } },
3769
+ ...res.messages.slice(1)
3770
+ ]
3771
+ };
3772
+ }
3773
+ return res;
3774
+ }
3851
3775
  switch (canonicalName) {
3852
3776
  case "hello": {
3853
3777
  const welcome = generateWelcomeMessage(data, stats);
@@ -3860,7 +3784,7 @@ Do not add any introduction or commentary before the ASCII art. The ASCII art mu
3860
3784
 
3861
3785
  ---
3862
3786
  ${welcome}`;
3863
- return {
3787
+ return withMcpNotice({
3864
3788
  description: `Hello \u2014 ${data.meta.name} Design System`,
3865
3789
  messages: [
3866
3790
  {
@@ -3871,41 +3795,55 @@ ${welcome}`;
3871
3795
  }
3872
3796
  }
3873
3797
  ]
3874
- };
3798
+ });
3875
3799
  }
3876
3800
  case "design-system-rules": {
3877
- const tool = args?.tool || "cursor";
3878
- const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool}`;
3801
+ const topic = args?.topic?.toLowerCase().trim();
3802
+ const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3879
3803
  const headers = { "Content-Type": "application/json" };
3880
3804
  if (apiKey) headers["x-api-key"] = apiKey;
3881
3805
  const response = await fetch(rulesUrl, { headers });
3882
- if (!response.ok) {
3883
- throw new Error(`Failed to fetch rules: ${response.status}`);
3806
+ if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
3807
+ const payload = await response.json();
3808
+ const categories = payload.categories ?? {};
3809
+ const allRules = payload.rules ?? [];
3810
+ const topicToCategories = {
3811
+ colors: ["general", "colors"],
3812
+ typo: ["general", "typography"],
3813
+ typography: ["general", "typography"],
3814
+ motion: ["general", "motion"],
3815
+ icons: ["general", "icons"],
3816
+ layout: ["general", "spacing", "sizing", "layout"],
3817
+ visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
3818
+ style: ["general", "colors", "borders", "radius", "shadows", "icons"]
3819
+ };
3820
+ let rulesText;
3821
+ if (topic && topicToCategories[topic]) {
3822
+ const categoryKeys = topicToCategories[topic];
3823
+ const filteredCategories = {};
3824
+ const filteredRules = [];
3825
+ for (const key of categoryKeys) {
3826
+ const list = categories[key];
3827
+ if (list?.length) {
3828
+ filteredCategories[key] = list;
3829
+ filteredRules.push(...list);
3830
+ }
3831
+ }
3832
+ rulesText = JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2);
3833
+ } else {
3834
+ rulesText = JSON.stringify({ rules: allRules, categories }, null, 2);
3884
3835
  }
3885
- const rulesData = await response.json();
3886
- return {
3887
- description: `Design system rules for ${tool}`,
3836
+ return withMcpNotice({
3837
+ description: topic ? `Design system rules (topic: ${topic})` : "Design system rules",
3888
3838
  messages: [
3889
- {
3890
- role: "user",
3891
- content: {
3892
- type: "text",
3893
- text: `Show me the design system rules for ${tool}.`
3894
- }
3895
- },
3896
- {
3897
- role: "assistant",
3898
- content: {
3899
- type: "text",
3900
- text: rulesData.content || JSON.stringify(rulesData, null, 2)
3901
- }
3902
- }
3839
+ { role: "user", content: { type: "text", text: topic ? `Show me the design system rules for topic: ${topic}.` : "Show me the design system rules." } },
3840
+ { role: "assistant", content: { type: "text", text: rulesText } }
3903
3841
  ]
3904
- };
3842
+ });
3905
3843
  }
3906
3844
  case "spacing": {
3907
3845
  const instructions = `List all spacing tokens in a table format. Use the listTokens tool with category "spacing" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "spacing.xs" instead of "spacing.scale.xs").`;
3908
- const response = {
3846
+ return withMcpNotice({
3909
3847
  description: "List all spacing tokens",
3910
3848
  messages: [
3911
3849
  {
@@ -3916,12 +3854,11 @@ ${welcome}`;
3916
3854
  }
3917
3855
  }
3918
3856
  ]
3919
- };
3920
- return response;
3857
+ });
3921
3858
  }
3922
3859
  case "radius": {
3923
3860
  const instructions = `List all border radius tokens in a table format. Use the listTokens tool with category "radius" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "radius.sm" instead of "radius.scale.sm").`;
3924
- return {
3861
+ return withMcpNotice({
3925
3862
  description: "List all border radius tokens",
3926
3863
  messages: [
3927
3864
  {
@@ -3932,7 +3869,7 @@ ${welcome}`;
3932
3869
  }
3933
3870
  }
3934
3871
  ]
3935
- };
3872
+ });
3936
3873
  }
3937
3874
  case "color": {
3938
3875
  const instructions = `List all color tokens in a table format showing both light and dark mode values.
@@ -3949,7 +3886,7 @@ For semantic colors (from modes.light/modes.dark), match tokens by name and show
3949
3886
  The Token Name should be in short format:
3950
3887
  - Brand colors: "colors.brand.primary" (not "colors.static.brand.primary")
3951
3888
  - Semantic colors: "colors.bgSurface" (not "colors.modes.light.bgSurface")`;
3952
- return {
3889
+ return withMcpNotice({
3953
3890
  description: "List all color tokens with light/dark mode",
3954
3891
  messages: [
3955
3892
  {
@@ -3960,11 +3897,11 @@ The Token Name should be in short format:
3960
3897
  }
3961
3898
  }
3962
3899
  ]
3963
- };
3900
+ });
3964
3901
  }
3965
3902
  case "typography": {
3966
3903
  const instructions = `List all typography tokens in a table format. Use the listTokens tool with category "typography" (no subcategory needed). Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group tokens by type (fontSize, fontWeight, lineHeight, etc.) with section headers.`;
3967
- return {
3904
+ return withMcpNotice({
3968
3905
  description: "List all typography tokens",
3969
3906
  messages: [
3970
3907
  {
@@ -3975,11 +3912,11 @@ The Token Name should be in short format:
3975
3912
  }
3976
3913
  }
3977
3914
  ]
3978
- };
3915
+ });
3979
3916
  }
3980
3917
  case "shadow": {
3981
3918
  const instructions = `List all shadow/elevation tokens in a table format. Use the listTokens tool with category "shadows" and subcategory "elevation". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "shadows.elevation.md" is fine as-is).`;
3982
- return {
3919
+ return withMcpNotice({
3983
3920
  description: "List all shadow/elevation tokens",
3984
3921
  messages: [
3985
3922
  {
@@ -3990,11 +3927,11 @@ The Token Name should be in short format:
3990
3927
  }
3991
3928
  }
3992
3929
  ]
3993
- };
3930
+ });
3994
3931
  }
3995
3932
  case "border": {
3996
3933
  const instructions = `List all border width tokens in a table format. Use the listTokens tool with category "borders" and subcategory "width". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "borders.width.sm" is fine as-is).`;
3997
- return {
3934
+ return withMcpNotice({
3998
3935
  description: "List all border width tokens",
3999
3936
  messages: [
4000
3937
  {
@@ -4005,7 +3942,7 @@ The Token Name should be in short format:
4005
3942
  }
4006
3943
  }
4007
3944
  ]
4008
- };
3945
+ });
4009
3946
  }
4010
3947
  case "sizing": {
4011
3948
  const instructions = `List all sizing tokens in a table format. Call listTokens twice:
@@ -4013,7 +3950,7 @@ The Token Name should be in short format:
4013
3950
  2. category "sizing" and subcategory "icon" for icon sizes
4014
3951
 
4015
3952
  Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (height vs icon) with section headers.`;
4016
- return {
3953
+ return withMcpNotice({
4017
3954
  description: "List all sizing tokens",
4018
3955
  messages: [
4019
3956
  {
@@ -4024,7 +3961,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4024
3961
  }
4025
3962
  }
4026
3963
  ]
4027
- };
3964
+ });
4028
3965
  }
4029
3966
  case "motion": {
4030
3967
  const instructions = `List all motion tokens in a table format. Call listTokens twice:
@@ -4032,7 +3969,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4032
3969
  2. category "motion" and subcategory "easing" for easing tokens
4033
3970
 
4034
3971
  Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (duration vs easing) with section headers.`;
4035
- return {
3972
+ return withMcpNotice({
4036
3973
  description: "List all motion tokens",
4037
3974
  messages: [
4038
3975
  {
@@ -4043,26 +3980,28 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4043
3980
  }
4044
3981
  }
4045
3982
  ]
4046
- };
3983
+ });
4047
3984
  }
4048
3985
  case "sync": {
4049
3986
  const output = args?.output || "./tokens.css";
4050
3987
  const format = args?.format || "css";
4051
- return {
3988
+ const workspaceRoot = args?.workspaceRoot;
3989
+ const rootHint = workspaceRoot ? ` Use workspaceRoot: "${workspaceRoot}" so files are written inside the repo.` : " If the project root is known, pass workspaceRoot with its absolute path so skills and manifest are written inside the repo (committable).";
3990
+ return withMcpNotice({
4052
3991
  description: "Sync tokens, rules, skills, and dependencies manifest",
4053
3992
  messages: [
4054
3993
  {
4055
3994
  role: "user",
4056
3995
  content: {
4057
3996
  type: "text",
4058
- text: `Call the syncAll tool now. Use output="${output}" and format="${format}". This syncs tokens, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Execute immediately - do not search or ask questions.`
3997
+ text: `Call the syncAll tool now. Use output="${output}" and format="${format}".${rootHint} This syncs tokens, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Execute immediately - do not search or ask questions.`
4059
3998
  }
4060
3999
  }
4061
4000
  ]
4062
- };
4001
+ });
4063
4002
  }
4064
4003
  case "sync-to-figma": {
4065
- return {
4004
+ return withMcpNotice({
4066
4005
  description: "Push design system to Figma via MCP tool",
4067
4006
  messages: [
4068
4007
  {
@@ -4073,34 +4012,108 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4073
4012
  }
4074
4013
  }
4075
4014
  ]
4076
- };
4015
+ });
4016
+ }
4017
+ case "design-in-figma": {
4018
+ if (getEffectiveTier() !== "pro") {
4019
+ return withMcpNotice({
4020
+ description: "Design in Figma (Pro required)",
4021
+ messages: [
4022
+ {
4023
+ role: "user",
4024
+ content: {
4025
+ type: "text",
4026
+ text: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio to use Design in Figma."
4027
+ }
4028
+ }
4029
+ ]
4030
+ });
4031
+ }
4032
+ if (!data) {
4033
+ return withMcpNotice({
4034
+ description: "Design in Figma",
4035
+ messages: [
4036
+ {
4037
+ role: "user",
4038
+ content: {
4039
+ type: "text",
4040
+ text: "Failed to fetch design system. Check your --ds-id and --atomix-token configuration, then try /--design-in-figma again."
4041
+ }
4042
+ }
4043
+ ]
4044
+ });
4045
+ }
4046
+ const figmaSkillPath = path.resolve(process.cwd(), ".cursor/skills/atomix-ds/figma-design-SKILL.md");
4047
+ const skillExists = fs.existsSync(figmaSkillPath);
4048
+ const currentVersion = String(data.meta.version ?? "1.0.0");
4049
+ const fileVersion = skillExists ? readSkillVersionFromFile(figmaSkillPath) : null;
4050
+ const isOutdated = !skillExists || fileVersion === null || fileVersion !== currentVersion;
4051
+ if (isOutdated) {
4052
+ return withMcpNotice({
4053
+ description: "Figma design skill missing or outdated",
4054
+ messages: [
4055
+ {
4056
+ role: "user",
4057
+ content: {
4058
+ type: "text",
4059
+ text: skillExists ? `The Figma design skill at \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` is outdated (design system version ${currentVersion}). Run **/--sync** to update the skill and other design system files, then run **/--design-in-figma** again.` : `The Figma design skill is missing. Run **/--sync** to write \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` (and other design system files), then run **/--design-in-figma** again.`
4060
+ }
4061
+ }
4062
+ ]
4063
+ });
4064
+ }
4065
+ return withMcpNotice({
4066
+ description: "Load Figma design skill and use designInFigma tool",
4067
+ messages: [
4068
+ {
4069
+ role: "user",
4070
+ content: {
4071
+ type: "text",
4072
+ text: `You are using **/--design-in-figma**: load the Figma design skill below and use the **designInFigma** MCP tool to design UI in the connected Figma file.
4073
+
4074
+ **Instructions:**
4075
+ 1. Treat the following as the Figma design skill (advisory and generative UI on the Figma canvas). Follow it when creating or editing designs.
4076
+ 2. Call **designInFigma** with \`action: "catalog"\` first to discover available bridge methods, file variables/styles, and query/execute capabilities.
4077
+ 3. Use \`action: "query"\` to read from Figma (e.g. get_selection, get_node_info, get_design_screenshot).
4078
+ 4. Use \`action: "execute"\` with an array of steps to create or modify the design on the canvas.
4079
+ 5. If the response includes \`bridgeNotRunning\` or \`pluginConnected: false\`, tell the user to run the Atomix plugin in Figma and tap Connect, then try again.
4080
+
4081
+ ---
4082
+
4083
+ ## Figma design skill
4084
+
4085
+ ${FIGMA_DESIGN_SKILL_MD}`
4086
+ }
4087
+ }
4088
+ ]
4089
+ });
4077
4090
  }
4078
4091
  case "refactor": {
4079
4092
  const refactorOutput = args?.output || "./tokens.css";
4080
4093
  const refactorFormat = args?.format || "css";
4081
- const refactorOutputPath = path2.resolve(process.cwd(), refactorOutput);
4082
- const refactorFileExists = fs2.existsSync(refactorOutputPath);
4094
+ const refactorOutputPath = path.resolve(process.cwd(), refactorOutput);
4095
+ const refactorFileExists = fs.existsSync(refactorOutputPath);
4083
4096
  if (!data) {
4084
- return {
4097
+ return withMcpNotice({
4085
4098
  description: "Refactor codebase for deprecated tokens",
4086
4099
  messages: [{
4087
4100
  role: "user",
4088
4101
  content: { type: "text", text: `Failed to fetch design system from DB. Check your --ds-id and --atomix-token configuration.` }
4089
4102
  }]
4090
- };
4103
+ });
4091
4104
  }
4092
4105
  if (!refactorFileExists) {
4093
- return {
4106
+ return withMcpNotice({
4094
4107
  description: "Refactor codebase for deprecated tokens",
4095
4108
  messages: [{
4096
4109
  role: "user",
4097
4110
  content: { type: "text", text: `No token file found at \`${refactorOutput}\`. Please run \`/--sync\` first to create your token file, then run \`/--refactor\` to scan your codebase for deprecated token usage.` }
4098
4111
  }]
4099
- };
4112
+ });
4100
4113
  }
4101
4114
  const deprecatedTokens = /* @__PURE__ */ new Map();
4102
4115
  if (["css", "scss", "less"].includes(refactorFormat)) {
4103
- const oldContent = fs2.readFileSync(refactorOutputPath, "utf-8");
4116
+ const oldContent = fs.readFileSync(refactorOutputPath, "utf-8");
4104
4117
  const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
4105
4118
  let match;
4106
4119
  while ((match = oldVarPattern.exec(oldContent)) !== null) {
@@ -4112,7 +4125,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4112
4125
  const dsVersion = data.meta.version ?? "?";
4113
4126
  const dsExportedAt = data.meta.exportedAt ? new Date(data.meta.exportedAt).toLocaleString() : "N/A";
4114
4127
  if (deprecatedTokens.size === 0) {
4115
- return {
4128
+ return withMcpNotice({
4116
4129
  description: "Refactor codebase for deprecated tokens",
4117
4130
  messages: [{
4118
4131
  role: "user",
@@ -4120,7 +4133,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4120
4133
 
4121
4134
  Your token file \`${refactorOutput}\` is aligned with the design system (v${dsVersion}, exported ${dsExportedAt}). No tokens need migration.` }
4122
4135
  }]
4123
- };
4136
+ });
4124
4137
  }
4125
4138
  const format = refactorFormat;
4126
4139
  const isNativeFormat = ["swift", "kotlin", "dart"].includes(format);
@@ -4221,8 +4234,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4221
4234
 
4222
4235
  - Resolve platform/stack: infer from the project (e.g. package.json, build.gradle, Xcode) or ask once: "Which platform? (e.g. web, Android, iOS)" and if relevant "Which stack? (e.g. React, Vue, Next, Swift, Kotlin)." Do not assume a default.
4223
4236
  - Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
4224
- - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
4225
- - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4237
+ - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
4238
+ - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skills (SKILL.md and figma-design-SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4226
4239
  - Do not write, create, or add anything in Phase 1.
4227
4240
 
4228
4241
  ## Phase 2 \u2013 Report and ask
@@ -4235,18 +4248,18 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4235
4248
 
4236
4249
  - Run only when the user has said yes (all or specific items).
4237
4250
  - For each approved item:
4238
- - **Skill:** Write the skill content from getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md).
4239
- - **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css"). syncAll also writes skills and atomix-dependencies.json. You must call syncAll; do not only suggest the user run it later.
4251
+ - **Skills:** Prefer calling **syncAll** (it writes both skills into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` and \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` under the project root.
4252
+ - **Token file and skills in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md, and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
4240
4253
  - **Icon package:** Install per getDependencies. When rendering icons, apply the design system's icon tokens: use getToken(\`sizing.icon.*\`) or listTokens(\`sizing\`) for size, and getToken(\`icons.strokeWidth\`) for stroke width when the DS defines it; do not use hardcoded sizes or stroke widths.
4241
4254
  - **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
4242
- - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, TYPESETS_LINK (if you created a typeset CSS file, use the full \`<link rel="stylesheet" href="\u2026">\` tag; otherwise empty string), DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1 fallback), FONT_FAMILY_VAR (body fallback), LARGEST_DISPLAY_TYPESET_CLASS (largest display typeset from listTypesets) and BODY_TYPESET_CLASS (e.g. typeset-body-md; leave empty if no typeset file), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4255
+ - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, TYPESETS_LINK, DS_NAME, HEADING_FONT_VAR, FONT_FAMILY_VAR, LARGEST_DISPLAY_TYPESET_CLASS, LARGEST_BODY_TYPESET_CLASS, BODY_TYPESET_CLASS, FONT_LINK_TAG, BRAND_PRIMARY_VAR, BUTTON_PADDING_VAR, BUTTON_HEIGHT_VAR, BUTTON_RADIUS_VAR, CIRCLE_PADDING_VAR, ICON_SIZE_VAR, CHECK_ICON_SVG (inline SVG from the design system icon library). The page uses semantic colors (mode-aware) and a Dark/Light toggle. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4243
4256
  - Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
4244
4257
  - **After reporting \u2013 styles/theme:**
4245
4258
  - **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014call **listTypesets** and emit **one CSS class per typeset** (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too for a 1:1 match. Use the CSS variable names returned by listTypesets. The output must not be only a font import; it must define every typeset with every style detail from the design system.
4246
4259
  - **iOS/Android:** If the project already has theme/style files: recommend how to integrate Atomix tokens. Do not suggest a new global theme. Only if there is **no** theme/style at all, ask once: "There's no theme/style setup yet. Do you want a minimal token-based theme?" and add only if the user says yes.
4247
4260
 
4248
4261
  Create your todo list first, then Phase 1 (resolve platform/stack, call getDependencies, scan, build lists), then Phase 2 (report and ask). Do not perform Phase 3 until the user replies.`;
4249
- return {
4262
+ return withMcpNotice({
4250
4263
  description: "Get started with design system in project (/--get-started). Create todo list; Phase 1 scan, Phase 2 report and ask, Phase 3 create only after user approval.",
4251
4264
  messages: [
4252
4265
  {
@@ -4257,7 +4270,7 @@ Create your todo list first, then Phase 1 (resolve platform/stack, call getDepen
4257
4270
  }
4258
4271
  }
4259
4272
  ]
4260
- };
4273
+ });
4261
4274
  }
4262
4275
  default:
4263
4276
  throw new Error(`Unknown prompt: ${name}`);
@@ -4322,6 +4335,7 @@ ${tokenSummary}
4322
4335
  | **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
4323
4336
  | **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
4324
4337
  | **/--sync-to-figma** | Push design system to Figma (variables, paint/text/effect styles). Uses built-in bridge + Atomix plugin; connect plugin in Figma then run. Available on all tiers. |
4338
+ | **/--design-in-figma** | Design in Figma: loads Figma design skill and use designInFigma tool (catalog, query, execute). Pro only. |
4325
4339
  | **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
4326
4340
 
4327
4341
  **Suggested next step:** Run **/--get-started** to set up global styles, icons, fonts, and token files; the AI will list options and ask before adding anything.