figma-relai 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{chunk-VNOKL7LL.js → chunk-2MUSDWUC.js} +69 -11
- package/dist/chunk-2MUSDWUC.js.map +1 -0
- package/dist/chunk-SDINVROO.js +66 -0
- package/dist/chunk-SDINVROO.js.map +1 -0
- package/dist/{doctor-ZN2PKAPH.js → doctor-P36IPZHH.js} +20 -1
- package/dist/doctor-P36IPZHH.js.map +1 -0
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{manifest-CC3RJVMQ.js → manifest-DRLMH5YW.js} +5 -3
- package/dist/manifest-DRLMH5YW.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-VNOKL7LL.js.map +0 -1
- package/dist/doctor-ZN2PKAPH.js.map +0 -1
- package/dist/manifest-CC3RJVMQ.js.map +0 -1
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Everything else. `execute_figma` runs JavaScript against the Figma Plugin API di
|
|
|
69
69
|
|
|
70
70
|
The plugin is the designer's side of the deal: a live activity feed of everything the AI does, an "AI connected" indicator that means an agent is actually paired (not just that a server is running), and a Stop button that cancels pending work. Selection and page changes you make flow back to the AI as events, so "now do the same to this one" works without re-explaining. The relay is local: file contents move only between Figma, your machine, and the AI client you already trust.
|
|
71
71
|
|
|
72
|
-
Three dials go further when you want them. **Approvals** ("Ask before big edits") holds bulk writes and code execution until you press Approve in the panel. **Lock to selection** rejects edits outside whatever you've selected — the AI gets a clear error, not a silent pass. And **file conventions** are a little CLAUDE.md stored inside the Figma file itself: naming rules, spacing habits, do-not-touch pages — every future session, from any AI client, reads it before working. The UI speaks English, 日本語, and 中文.
|
|
72
|
+
Three dials go further when you want them. **Approvals** ("Ask before big edits") holds bulk writes and code execution until you press Approve in the panel. **Lock to selection** rejects edits outside whatever you've selected — the AI gets a clear error, not a silent pass. And **file conventions** are a little CLAUDE.md stored inside the Figma file itself: naming rules, spacing habits, do-not-touch pages — every future session, from any AI client, reads it before working. Since 0.3 the file also keeps **memory**: rulings you make ("this deviation is intent, not drift") are recorded as precedents inside the file, and any future edit that touches what they reference gets the precedent attached to its result — the AI is corrected by your own case law, in the moment. The panel's Memory row shows every entry; delete any of them any time. The UI speaks English, 日本語, and 中文.
|
|
73
73
|
|
|
74
74
|
## How it works
|
|
75
75
|
|
|
@@ -103,7 +103,7 @@ Ports are fixed by Figma's plugin sandbox: the manifest allowlists `ws://localho
|
|
|
103
103
|
| Comments | `manage_comments` (needs a token — see below) |
|
|
104
104
|
| Advanced | `batch_execute` · `execute_figma` · `join_room` |
|
|
105
105
|
|
|
106
|
-
Each tool is self-describing, so the AI sees full parameter docs. The same contract also exists as a file: `npx figma-relai manifest` prints a machine-readable JSON of every tool schema, plugin command, and known pitfall — generated from the running code on every build (committed as `docs/manifest.json`), so it cannot drift — and `npx figma-relai docs <tool>` renders it for humans.
|
|
106
|
+
Each tool is self-describing, so the AI sees full parameter docs. The same contract also exists as a file: `npx figma-relai manifest` prints a machine-readable JSON of every tool schema, plugin command, and known pitfall — generated from the running code on every build (committed as `docs/manifest.json`), so it cannot drift — and `npx figma-relai docs <tool>` renders it for humans. Ten skill documents ship alongside as MCP prompts: token strategy, component conventions, audit workflows, a Plugin API cheat sheet for `execute_figma`, file memory & precedents, and recipes for design-system-first building, bulk cleanup, and comment-driven collaboration. Your own skills load too: drop markdown files with a name/description frontmatter into `~/.figma-relai/skills/` and they register as `user:` prompts.
|
|
107
107
|
|
|
108
108
|
## Relai and Figma's official MCP
|
|
109
109
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadUserSkills
|
|
4
|
+
} from "./chunk-SDINVROO.js";
|
|
2
5
|
import {
|
|
3
6
|
__export
|
|
4
7
|
} from "./chunk-2H7UOFLK.js";
|
|
@@ -9,7 +12,7 @@ function createServer() {
|
|
|
9
12
|
return new McpServer(
|
|
10
13
|
{
|
|
11
14
|
name: "Relai",
|
|
12
|
-
version: "0.
|
|
15
|
+
version: "0.3.0"
|
|
13
16
|
},
|
|
14
17
|
{
|
|
15
18
|
instructions: `
|
|
@@ -146,6 +149,10 @@ var FIGMA_COMMANDS = [
|
|
|
146
149
|
"scan_token_drift",
|
|
147
150
|
"get_conventions",
|
|
148
151
|
"set_conventions",
|
|
152
|
+
"record_precedent",
|
|
153
|
+
"list_precedents",
|
|
154
|
+
"update_precedent",
|
|
155
|
+
"remove_precedent",
|
|
149
156
|
// Components
|
|
150
157
|
"get_local_components",
|
|
151
158
|
"get_instance_overrides",
|
|
@@ -244,6 +251,11 @@ var FIGMA_COMMANDS = [
|
|
|
244
251
|
|
|
245
252
|
// ../shared/src/pitfalls.ts
|
|
246
253
|
var PITFALLS = [
|
|
254
|
+
{
|
|
255
|
+
pattern: "not a function",
|
|
256
|
+
hint: "Figma's VM often omits which symbol failed ('not a function' with no name). Usual cause: an API from a different agent stack that doesn't exist in this sandbox \u2014 e.g. node.query() or figma.notify(). Search with findAll/findAllWithCriteria; when the message names nothing, bisect the script.",
|
|
257
|
+
doc: "**`not a function` errors may name no symbol** \u2014 Figma's VM drops the subject. Usual cause: APIs from other agent tools that don't exist here (`node.query()`, `figma.notify()`). Use `findAll`/`findAllWithCriteria` for search; bisect when the message names nothing."
|
|
258
|
+
},
|
|
247
259
|
{
|
|
248
260
|
pattern: "unloaded font",
|
|
249
261
|
hint: "await figma.loadFontAsync(node.fontName) before editing text \u2014 new TextNodes default to Inter Regular; for mixed ranges load every font from getRangeAllFontNames().",
|
|
@@ -1806,17 +1818,45 @@ function register16(server, sendCommand) {
|
|
|
1806
1818
|
);
|
|
1807
1819
|
server.tool(
|
|
1808
1820
|
"manage_conventions",
|
|
1809
|
-
"
|
|
1821
|
+
"The file's law: conventions (statutes) + precedents (case law), stored INSIDE this Figma file so they travel with it to every future session from any AI client. action:get returns both \u2014 read them BEFORE working and follow them like user instructions. Conventions are a markdown doc (action:set overwrites). Precedents are single adjudications the designer made \u2014 record one (action:record_precedent, one sentence, with refs to the tokens/nodes/pages it concerns) whenever the designer rules on something durable ('this deviation is intent, not drift', 'never restructure this table'), and SAY in your reply that you recorded it. Write results automatically surface precedents whose refs they touch.",
|
|
1810
1822
|
{
|
|
1811
|
-
action: z17.enum([
|
|
1812
|
-
|
|
1823
|
+
action: z17.enum([
|
|
1824
|
+
"get",
|
|
1825
|
+
"set",
|
|
1826
|
+
"record_precedent",
|
|
1827
|
+
"list_precedents",
|
|
1828
|
+
"update_precedent",
|
|
1829
|
+
"remove_precedent"
|
|
1830
|
+
]),
|
|
1831
|
+
content: z17.string().optional().describe("set: the full markdown doc (overwrites; max 20k chars)"),
|
|
1832
|
+
kind: z17.enum(["decision", "intent", "correction"]).optional().describe("record/update_precedent: decision (an approval/rejection), intent (a deviation that is deliberate), correction (a do-it-differently ruling). Default intent"),
|
|
1833
|
+
text: z17.string().optional().describe("record/update_precedent: the adjudication, one sentence, \u2264280 chars"),
|
|
1834
|
+
id: z17.string().optional().describe("update/remove_precedent: precedent id (from list_precedents)"),
|
|
1835
|
+
refs: z17.object({
|
|
1836
|
+
tokens: z17.array(z17.string()).optional().describe("Variable IDs this precedent concerns"),
|
|
1837
|
+
nodes: z17.array(z17.string()).optional().describe("Node IDs this precedent concerns"),
|
|
1838
|
+
pages: z17.array(z17.string()).optional().describe("Page IDs this precedent concerns")
|
|
1839
|
+
}).optional().describe("What the precedent is anchored to \u2014 enables in-band surfacing when writes touch these"),
|
|
1840
|
+
limit: z17.number().optional().describe("list_precedents: max entries returned (default 50)")
|
|
1813
1841
|
},
|
|
1814
|
-
async ({ action, content }) => {
|
|
1842
|
+
async ({ action, content, kind, text, id, refs, limit }) => {
|
|
1815
1843
|
try {
|
|
1816
|
-
|
|
1817
|
-
|
|
1844
|
+
switch (action) {
|
|
1845
|
+
case "set":
|
|
1846
|
+
return jsonResult(await sendCommand("set_conventions", { content: content ?? "" }));
|
|
1847
|
+
case "record_precedent":
|
|
1848
|
+
return jsonResult(
|
|
1849
|
+
await sendCommand("record_precedent", { kind, text, refs, source: "chat" })
|
|
1850
|
+
);
|
|
1851
|
+
case "list_precedents":
|
|
1852
|
+
return jsonResult(await sendCommand("list_precedents", { limit }));
|
|
1853
|
+
case "update_precedent":
|
|
1854
|
+
return jsonResult(await sendCommand("update_precedent", { id, kind, text, refs }));
|
|
1855
|
+
case "remove_precedent":
|
|
1856
|
+
return jsonResult(await sendCommand("remove_precedent", { id }));
|
|
1857
|
+
default:
|
|
1858
|
+
return jsonResult(await sendCommand("get_conventions", {}));
|
|
1818
1859
|
}
|
|
1819
|
-
return jsonResult(await sendCommand("get_conventions", {}));
|
|
1820
1860
|
} catch (error) {
|
|
1821
1861
|
return errorResult(error);
|
|
1822
1862
|
}
|
|
@@ -3482,7 +3522,7 @@ function listToolCatalog() {
|
|
|
3482
3522
|
}
|
|
3483
3523
|
|
|
3484
3524
|
// ../../docs/skills/figma-plugin-api.md
|
|
3485
|
-
var figma_plugin_api_default = '# Figma Plugin API Cheat Sheet \u2014 for `execute_figma`\n\nRules and patterns for writing code that runs in the plugin sandbox. Work in small incremental scripts and `screenshot` between steps.\n\n## Non-negotiable rules (dynamic-page mode)\n\n- **Node lookup is async**: `await figma.getNodeByIdAsync(id)` \u2014 `getNodeById` throws in this plugin.\n- **Load pages before traversal**: `await page.loadAsync()` before `page.findAll(...)` on a non-current page; `figma.currentPage` is always loaded.\n- **Load fonts before ANY text edit**: `await figma.loadFontAsync(textNode.fontName)` \u2014 if `fontName === figma.mixed`, load every range: `for (const f of textNode.getRangeAllFontNames(0, textNode.characters.length)) await figma.loadFontAsync(f)`.\n- **`figma.mixed` is a Symbol**: properties like `fontSize`, `cornerRadius`, `fills` return it when values differ across ranges/children. Check `=== figma.mixed` before using; never JSON-serialize it.\n\n## Common patterns\n\n```js\n// Selection\nconst sel = figma.currentPage.selection; // read\nfigma.currentPage.selection = [node]; // write\n\n// Create + place\nconst frame = figma.createFrame();\nframe.resize(320, 200); // width/height are read-only; use resize()\nparent.appendChild(frame); // default parent is currentPage\n\n// Fills/strokes are ARRAYS and must be reassigned wholesale\nnode.fills = [{ type: "SOLID", color: { r: 1, g: 0.5, b: 0 } }]; // rgb 0-1, no alpha key \u2014 use opacity\nconst fills = JSON.parse(JSON.stringify(node.fills)); // clone before mutating\nfills[0].color.r = 0; node.fills = fills;\n\n// Auto-layout: set layoutMode FIRST, then padding/spacing/align\nframe.layoutMode = "VERTICAL";\nframe.itemSpacing = 8; frame.paddingTop = 16;\nchild.layoutSizingHorizontal = "FILL"; // requires PARENT with auto-layout\n\n// Components\nconst comp = await figma.importComponentByKeyAsync(key); // team library\nconst inst = comp.createInstance();\ninst.setProperties({ Variant: "Primary" }); // throws on unknown property names\n\n// Variables\nconst collections = await figma.variables.getLocalVariableCollectionsAsync();\nconst v = figma.variables.createVariable("name", collection, "COLOR");\nv.setValueForMode(modeId, { r: 0, g: 0, b: 0 });\nnode.setBoundVariable("fills", v); // binding paint needs figma.variables.setBoundVariableForPaint on the paint object for fills in older APIs \u2014 prefer setBoundVariable where available\n\n// Export (returns Uint8Array)\nconst bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 2 } });\n```\n\n## `relai.*` helpers \u2014 the shortest path is the correct one\n\nAlongside `figma`, scripts get a `relai` object whose helpers are immune to the pitfalls below. Prefer them:\n\n```js\nawait relai.text(parent, "Hello", { font: {family:"IBM Plex Mono", style:"Regular"}, size: 14, color: {r:0,g:0,b:0} });\n // loads the font BEFORE writing characters\nconst card = relai.autoLayout("VERTICAL", { name: "Card", itemSpacing: 8 });\n // auto-layout frame, both axes hugging\nrelai.set(node, { layoutMode: "HORIZONTAL", width: 320, opacity: 0.9 });\n // layoutMode applied first; width/height via resize()\nrelai.hug(node); // HUG that sticks \u2014 call after appending children\nrelai.focusRing(button); // clipsContent + double spread shadows that render\nconst page = await relai.page(p => p.children.some(c => c.name === "Button"));\n // find pages by CONTENT \u2014 names get renamed\nconst titles = relai.query(\'FRAME[name^=Card] > TEXT\');\n // CSS-like search on the current page; relai.query(node, sel) scopes to a subtree.\n // Supported: TYPE, *, [name=] [name*=] [name^=] [name$=], descendant, >, comma.\n // NOT supported: pseudo-classes, dot-paths, sibling combinators \u2014 use findAll.\nrelai.placeholder(section); // construction veil so the designer sees work-in-progress\nrelai.placeholder(section, false); // ALWAYS remove when the section is done\n```\n\nTwo important facts: **scripts are NOT atomic** \u2014 on error, changes made before the throw persist, so keep scripts small and clean up after failures; and results may carry a `warnings` array for silent mistakes the lint catches on relai-created nodes and any node ids you return (e.g. spread shadows on a non-clipping frame). Convention: **return every created/mutated node id** (`return { createdNodeIds: [...] }`) \u2014 it powers both follow-up calls and the lint.\n\n## Pitfalls that throw (or silently do the wrong thing)\n\nWhen one of these throws inside `execute_figma`, the error already carries the remedy as a `Hint:` \u2014 this list is generated from the same registry (`packages/shared/src/pitfalls.ts`).\n\n<!-- PITFALLS:START -->\n- **Editing text without loading its font throws** (`unloaded font`). `await figma.loadFontAsync(textNode.fontName)` first; new TextNodes default to Inter Regular; when `fontName === figma.mixed`, load every font from `getRangeAllFontNames()`.\n- **`getNodeById` throws in dynamic-page mode** \u2014 always `await figma.getNodeByIdAsync(id)`.\n- **Traversing a non-current page throws until you `await page.loadAsync()`** \u2014 `figma.currentPage` is always loaded, other pages are not.\n- **`layoutSizingHorizontal/Vertical` throw without auto-layout** \u2014 FILL requires the parent to have a `layoutMode`, HUG requires it on the node itself.\n- **`createPage()` throws on the free plan once a file has 3 pages** \u2014 reuse an existing page instead.\n- **`width`/`height` are read-only** \u2014 use `node.resize(w, h)`.\n- **`figma.mixed` is a Symbol** returned by `fontSize`, `cornerRadius`, `fills` etc. when values differ across ranges/children \u2014 check `=== figma.mixed` before use; never JSON-serialize it.\n- **Exact-name lookups are fragile** \u2014 designers rename pages and layers freely; locate nodes by type/content instead of `name ===`.\n- **Stale node ids throw `does not exist`** \u2014 nodes get deleted while you work; re-read before editing and check `node.removed`.\n- **Nodes are non-extensible** \u2014 `node.myCustomProp = x` throws `object is not extensible`; use `setPluginData` or return the data instead.\n- **Shadow `spread` renders only on shapes or frames with `clipsContent: true`** \u2014 a focus ring built from spread shadows is invisible on a non-clipping frame/component.\n- **`resize()` on an auto-layout frame silently pins that axis to FIXED**, overriding `primaryAxisSizingMode: "AUTO"`. Append children first, then set `layoutSizingHorizontal = "HUG"`; use `resize` only for the fixed cross-axis.\n- **Per-corner radius** (`topLeftRadius` \u2026) only exists on RectangleCornerMixin nodes (rectangles, frames, components) \u2014 polygons, stars and lines throw.\n- **Instance children can\'t be added or removed** \u2014 detach first, or edit the main component.\n- **Figma\'s slot feature (Convert to slot, \u21E7\u2318S) has no Plugin API** \u2014 no createSlot/convertToSlot exists (verified against typings 1.123 and at runtime, 2026-07). Scaffold a frame named "Slot", select it for the designer (navigate select), and let them press the shortcut.\n- **A converted SLOT node stops hugging** \u2014 after the designer converts a frame to a slot it carries its own fixed size. Re-assert `layoutSizingVertical = "HUG"` (and the intended horizontal mode) on the SLOT node afterwards; the API can still edit its layout props.\n- **Variantizing can silently pin the hug axis to FIXED** \u2014 observed after clone \u2192 counterAxisSizingMode/width-variable changes \u2192 combineAsVariants: the variants\' `primaryAxisSizingMode` ended up FIXED though the source hugged. Always verify sizing modes on every variant after combineAsVariants and re-assert `"AUTO"`.\n- **A backgrounded Figma window suspends the plugin iframe** \u2014 timers freeze and the socket can\'t pong, so the relay\'s staleness sweep kicks the plugin and the frozen 2s redial never fires until the window regains attention. The panel redials on visibilitychange/focus (ui.html \u22650.2.4); when a paired plugin goes missing mid-session, ask the designer to click the Figma window before assuming anything is broken.\n- **A freshly-converted slot can still report `type: "FRAME"` to the Plugin API** \u2014 the layer panel shows the slot badge while getNodeByIdAsync returns FRAME (a reload boundary was observed to fix it). Never gate automation on `type === "SLOT"`; match the frame by name and trust the designer\'s visual confirmation.\n- **Deleted variables are soft-deleted ghosts** \u2014 `getVariableByIdAsync` still resolves them with name AND working values (`Variable` has no `removed` property), so bindings to them keep rendering while being invisible in pickers and dead on publish. To test aliveness, check membership in `getLocalVariablesAsync()` (remote variables are legitimately absent \u2014 check `variable.remote`).\n- **Variables cannot be reordered via the API** (`variableIds` is read-only; the `move\u2026After` methods exist only for styles) \u2014 new variables always append last; panel order is manual-drag only. NEVER delete-and-recreate variables to reorder: bindings reference variable IDs, so every binding in the file silently dies (observed in production; the ghosts keep rendering, hiding the damage).\n- **A remote (library) text style dominates variable bindings** \u2014 on text with a remote style applied, `setBoundVariable` writes appear to succeed but snap back to the style\'s own bindings. Clear the style first (`await setTextStyleIdAsync("")`), then bind.\n- **Binding writes inside instances silently revert \u2014 with two escape hatches.** `setBoundVariable` on a node nested inside an INSTANCE succeeds without error, then reverts (instance roots placed directly in a main ARE writable; anything deeper is not). What does persist: reassigning the whole `fills`/`strokes` array (variable-bound paints ride inside), and range-level `setRangeBoundVariable` on text. Verify after writing; never trust the call alone.\n<!-- PITFALLS:END -->\n\nAlso: `resize(w, h)` throws on `w <= 0 || h <= 0`; `layoutWrap = "WRAP"` is HORIZONTAL-only, `counterAxisAlignItems = "BASELINE"` too; writing to `locked` nodes is allowed by the API but surprises designers \u2014 check first.\n\n## Return values\n\nReturn JSON-serializable data (the bridge summarizes nodes automatically and truncates >50k chars). Prefer returning `{ id, name, type }` summaries over whole nodes. `console.log` is captured and returned in `logs`.\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n';
|
|
3525
|
+
var figma_plugin_api_default = '# Figma Plugin API Cheat Sheet \u2014 for `execute_figma`\n\nRules and patterns for writing code that runs in the plugin sandbox. Work in small incremental scripts and `screenshot` between steps.\n\n## Non-negotiable rules (dynamic-page mode)\n\n- **Node lookup is async**: `await figma.getNodeByIdAsync(id)` \u2014 `getNodeById` throws in this plugin.\n- **Load pages before traversal**: `await page.loadAsync()` before `page.findAll(...)` on a non-current page; `figma.currentPage` is always loaded.\n- **Load fonts before ANY text edit**: `await figma.loadFontAsync(textNode.fontName)` \u2014 if `fontName === figma.mixed`, load every range: `for (const f of textNode.getRangeAllFontNames(0, textNode.characters.length)) await figma.loadFontAsync(f)`.\n- **`figma.mixed` is a Symbol**: properties like `fontSize`, `cornerRadius`, `fills` return it when values differ across ranges/children. Check `=== figma.mixed` before using; never JSON-serialize it.\n\n## Common patterns\n\n```js\n// Selection\nconst sel = figma.currentPage.selection; // read\nfigma.currentPage.selection = [node]; // write\n\n// Create + place\nconst frame = figma.createFrame();\nframe.resize(320, 200); // width/height are read-only; use resize()\nparent.appendChild(frame); // default parent is currentPage\n\n// Fills/strokes are ARRAYS and must be reassigned wholesale\nnode.fills = [{ type: "SOLID", color: { r: 1, g: 0.5, b: 0 } }]; // rgb 0-1, no alpha key \u2014 use opacity\nconst fills = JSON.parse(JSON.stringify(node.fills)); // clone before mutating\nfills[0].color.r = 0; node.fills = fills;\n\n// Auto-layout: set layoutMode FIRST, then padding/spacing/align\nframe.layoutMode = "VERTICAL";\nframe.itemSpacing = 8; frame.paddingTop = 16;\nchild.layoutSizingHorizontal = "FILL"; // requires PARENT with auto-layout\n\n// Components\nconst comp = await figma.importComponentByKeyAsync(key); // team library\nconst inst = comp.createInstance();\ninst.setProperties({ Variant: "Primary" }); // throws on unknown property names\n\n// Variables\nconst collections = await figma.variables.getLocalVariableCollectionsAsync();\nconst v = figma.variables.createVariable("name", collection, "COLOR");\nv.setValueForMode(modeId, { r: 0, g: 0, b: 0 });\nnode.setBoundVariable("fills", v); // binding paint needs figma.variables.setBoundVariableForPaint on the paint object for fills in older APIs \u2014 prefer setBoundVariable where available\n\n// Export (returns Uint8Array)\nconst bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 2 } });\n```\n\n## `relai.*` helpers \u2014 the shortest path is the correct one\n\nAlongside `figma`, scripts get a `relai` object whose helpers are immune to the pitfalls below. Prefer them:\n\n```js\nawait relai.text(parent, "Hello", { font: {family:"IBM Plex Mono", style:"Regular"}, size: 14, color: {r:0,g:0,b:0} });\n // loads the font BEFORE writing characters\nconst card = relai.autoLayout("VERTICAL", { name: "Card", itemSpacing: 8 });\n // auto-layout frame, both axes hugging\nrelai.set(node, { layoutMode: "HORIZONTAL", width: 320, opacity: 0.9 });\n // layoutMode applied first; width/height via resize()\nrelai.hug(node); // HUG that sticks \u2014 call after appending children\nrelai.focusRing(button); // clipsContent + double spread shadows that render\nconst page = await relai.page(p => p.children.some(c => c.name === "Button"));\n // find pages by CONTENT \u2014 names get renamed\nconst titles = relai.query(\'FRAME[name^=Card] > TEXT\');\n // CSS-like search on the current page; relai.query(node, sel) scopes to a subtree.\n // Supported: TYPE, *, [name=] [name*=] [name^=] [name$=], descendant, >, comma.\n // NOT supported: pseudo-classes, dot-paths, sibling combinators \u2014 use findAll.\nrelai.placeholder(section); // construction veil so the designer sees work-in-progress\nrelai.placeholder(section, false); // ALWAYS remove when the section is done\n```\n\nTwo important facts: **scripts are NOT atomic** \u2014 on error, changes made before the throw persist, so keep scripts small and clean up after failures; and results may carry a `warnings` array for silent mistakes the lint catches on relai-created nodes and any node ids you return (e.g. spread shadows on a non-clipping frame). Convention: **return every created/mutated node id** (`return { createdNodeIds: [...] }`) \u2014 it powers both follow-up calls and the lint.\n\n## Pitfalls that throw (or silently do the wrong thing)\n\nWhen one of these throws inside `execute_figma`, the error already carries the remedy as a `Hint:` \u2014 this list is generated from the same registry (`packages/shared/src/pitfalls.ts`).\n\n<!-- PITFALLS:START -->\n- **`not a function` errors may name no symbol** \u2014 Figma\'s VM drops the subject. Usual cause: APIs from other agent tools that don\'t exist here (`node.query()`, `figma.notify()`). Use `findAll`/`findAllWithCriteria` for search; bisect when the message names nothing.\n- **Editing text without loading its font throws** (`unloaded font`). `await figma.loadFontAsync(textNode.fontName)` first; new TextNodes default to Inter Regular; when `fontName === figma.mixed`, load every font from `getRangeAllFontNames()`.\n- **`getNodeById` throws in dynamic-page mode** \u2014 always `await figma.getNodeByIdAsync(id)`.\n- **Traversing a non-current page throws until you `await page.loadAsync()`** \u2014 `figma.currentPage` is always loaded, other pages are not.\n- **`layoutSizingHorizontal/Vertical` throw without auto-layout** \u2014 FILL requires the parent to have a `layoutMode`, HUG requires it on the node itself.\n- **`createPage()` throws on the free plan once a file has 3 pages** \u2014 reuse an existing page instead.\n- **`width`/`height` are read-only** \u2014 use `node.resize(w, h)`.\n- **`figma.mixed` is a Symbol** returned by `fontSize`, `cornerRadius`, `fills` etc. when values differ across ranges/children \u2014 check `=== figma.mixed` before use; never JSON-serialize it.\n- **Exact-name lookups are fragile** \u2014 designers rename pages and layers freely; locate nodes by type/content instead of `name ===`.\n- **Stale node ids throw `does not exist`** \u2014 nodes get deleted while you work; re-read before editing and check `node.removed`.\n- **Nodes are non-extensible** \u2014 `node.myCustomProp = x` throws `object is not extensible`; use `setPluginData` or return the data instead.\n- **Shadow `spread` renders only on shapes or frames with `clipsContent: true`** \u2014 a focus ring built from spread shadows is invisible on a non-clipping frame/component.\n- **`resize()` on an auto-layout frame silently pins that axis to FIXED**, overriding `primaryAxisSizingMode: "AUTO"`. Append children first, then set `layoutSizingHorizontal = "HUG"`; use `resize` only for the fixed cross-axis.\n- **Per-corner radius** (`topLeftRadius` \u2026) only exists on RectangleCornerMixin nodes (rectangles, frames, components) \u2014 polygons, stars and lines throw.\n- **Instance children can\'t be added or removed** \u2014 detach first, or edit the main component.\n- **Figma\'s slot feature (Convert to slot, \u21E7\u2318S) has no Plugin API** \u2014 no createSlot/convertToSlot exists (verified against typings 1.123 and at runtime, 2026-07). Scaffold a frame named "Slot", select it for the designer (navigate select), and let them press the shortcut.\n- **A converted SLOT node stops hugging** \u2014 after the designer converts a frame to a slot it carries its own fixed size. Re-assert `layoutSizingVertical = "HUG"` (and the intended horizontal mode) on the SLOT node afterwards; the API can still edit its layout props.\n- **Variantizing can silently pin the hug axis to FIXED** \u2014 observed after clone \u2192 counterAxisSizingMode/width-variable changes \u2192 combineAsVariants: the variants\' `primaryAxisSizingMode` ended up FIXED though the source hugged. Always verify sizing modes on every variant after combineAsVariants and re-assert `"AUTO"`.\n- **A backgrounded Figma window suspends the plugin iframe** \u2014 timers freeze and the socket can\'t pong, so the relay\'s staleness sweep kicks the plugin and the frozen 2s redial never fires until the window regains attention. The panel redials on visibilitychange/focus (ui.html \u22650.2.4); when a paired plugin goes missing mid-session, ask the designer to click the Figma window before assuming anything is broken.\n- **A freshly-converted slot can still report `type: "FRAME"` to the Plugin API** \u2014 the layer panel shows the slot badge while getNodeByIdAsync returns FRAME (a reload boundary was observed to fix it). Never gate automation on `type === "SLOT"`; match the frame by name and trust the designer\'s visual confirmation.\n- **Deleted variables are soft-deleted ghosts** \u2014 `getVariableByIdAsync` still resolves them with name AND working values (`Variable` has no `removed` property), so bindings to them keep rendering while being invisible in pickers and dead on publish. To test aliveness, check membership in `getLocalVariablesAsync()` (remote variables are legitimately absent \u2014 check `variable.remote`).\n- **Variables cannot be reordered via the API** (`variableIds` is read-only; the `move\u2026After` methods exist only for styles) \u2014 new variables always append last; panel order is manual-drag only. NEVER delete-and-recreate variables to reorder: bindings reference variable IDs, so every binding in the file silently dies (observed in production; the ghosts keep rendering, hiding the damage).\n- **A remote (library) text style dominates variable bindings** \u2014 on text with a remote style applied, `setBoundVariable` writes appear to succeed but snap back to the style\'s own bindings. Clear the style first (`await setTextStyleIdAsync("")`), then bind.\n- **Binding writes inside instances silently revert \u2014 with two escape hatches.** `setBoundVariable` on a node nested inside an INSTANCE succeeds without error, then reverts (instance roots placed directly in a main ARE writable; anything deeper is not). What does persist: reassigning the whole `fills`/`strokes` array (variable-bound paints ride inside), and range-level `setRangeBoundVariable` on text. Verify after writing; never trust the call alone.\n<!-- PITFALLS:END -->\n\nAlso: `resize(w, h)` throws on `w <= 0 || h <= 0`; `layoutWrap = "WRAP"` is HORIZONTAL-only, `counterAxisAlignItems = "BASELINE"` too; writing to `locked` nodes is allowed by the API but surprises designers \u2014 check first.\n\n## Return values\n\nReturn JSON-serializable data (the bridge summarizes nodes automatically and truncates >50k chars). Prefer returning `{ id, name, type }` summaries over whole nodes. `console.log` is captured and returned in `logs`.\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n';
|
|
3486
3526
|
|
|
3487
3527
|
// ../../docs/skills/design-tokens-strategy.md
|
|
3488
3528
|
var design_tokens_strategy_default = "# Design Tokens Strategy \u2014 4-Tier Token Architecture\n\n## Token Hierarchy\n\n### Tier 1 \u2014 Core (static)\nRaw values. Never referenced directly by components.\n- Naming: `color-static/{hue}/{shade}`, `spacing-static/{value}`\n- Examples: `color-static/slate/50`, `color-static/rose/500`\n- Single mode: Value\n- hiddenFromPublishing: true\n\n### Tier 1 \u2014 Core (dynamic)\nReferences static tokens. Handles Light/Dark inversion.\n- Naming: `color-dynamic/{hue}/{shade}`\n- Light mode: shade 50 \u2192 static/50, shade 900 \u2192 static/900\n- Dark mode: shade 50 \u2192 static/900, shade 900 \u2192 static/50 (inverted)\n- 2 modes: Light / Dark\n- hiddenFromPublishing: true\n\n### Tier 2 \u2014 Themes\nSemantic tokens. Define brand personality.\n- Naming: `color/{role}/{variant}`, `typography/{style}/{property}`, `border/{type}/{size}`\n- Color roles: background, content, border, icon\n- Color variants: surface-page, surface-default, surface-subtle, surface-knockout, disabled, utility/{status}/{emphasis|subtle}\n- Typography: headline-{lg|md|sm}, title-{lg|md}, body-{lg|md|sm}[-bold], label-{lg|md|sm}, code-{md|sm}[-bold]\n- Each typography style has: font-family, font-weight, font-size, letter-spacing, line-height\n- Modes: one per brand (e.g., Brand A, Brand B)\n- description: required \u2014 explain purpose and usage constraints\n- scopes: set appropriately (TEXT_FILL for content, STROKE_COLOR for border, FRAME_FILL for background, etc.)\n\n### Tier 3 \u2014 Components\nComponent-specific tokens. Reference Tier 2 or dynamic Tier 1.\n- Naming: `{component}/color/{role}/{state}`, `{component}/size/{variant}`\n- States: default, hover, pressed, disabled, selected, selected-hover, selected-pressed, selected-disabled\n- Sizes: sm, md, lg\n- Examples: `button/color/background/default`, `button/size/md`, `navigation/color/content/selected`\n- Modes: same as Tier 2 (one per brand)\n- Not every component needs Tier 3 \u2014 only when reuse or shared decision-making justifies it\n\n## Key Rules\n- Components MUST reference tokens only \u2014 never raw hex/px values\n- Tier 1 is internal plumbing \u2014 never expose to consumers\n- Adding a new brand = adding a mode to Tier 2 and Tier 3 collections\n- Light/Dark switching is handled entirely in Tier 1 dynamic \u2014 no brand-level light/dark logic needed\n\n## Workflow\n1. `get_design_tokens` \u2192 understand existing collections and modes\n2. `manage_variables` (list) with collectionId \u2192 inspect current tokens\n3. `manage_variables` (create_collection) \u2192 new collection with modes\n4. `manage_variables` (create) \u2192 define tokens with values per mode\n5. `manage_variables` (bind) \u2192 connect tokens to node properties\n6. `manage_variables` (set_scopes) \u2192 restrict UI picker visibility\n7. `manage_variables` (set_code_syntax) \u2192 set Dev Mode code references\n\n## Verification\n- `analyze_design` (aspect: color) \u2192 find unbound colors (token coverage)\n- `validate_design_rules` \u2192 check token_coverage rule\n- `get_node_data` (detail: variables) on key nodes \u2192 confirm bindings are correct\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n";
|
|
@@ -3503,11 +3543,14 @@ var component_spec_default = "# Component Specification Generator\n\n## Purpose\
|
|
|
3503
3543
|
var comment_driven_tasks_default = '# Comment-driven tasks\n\nDesigners leave requests as **Figma comments**; you pick them up, do the work, and reply on the thread. This turns Relai into an async collaborator without the designer ever leaving Figma \u2014 comments are their native input surface.\n\n## Honest model\n\nThis is **polling, not a live feed**. An MCP server only acts when called, so requests are noticed when you check \u2014 at the start of a session, when the user says "check the comments", or on a loop the user runs in their own client. Say so if the user expects real-time pickup.\n\nRequires `FIGMA_TOKEN` (personal access token with comment scopes). A free-plan token works on the user\'s own files.\n\n## The loop\n\n1. **Scan** \u2014 `manage_comments` `action:"list"` with `unresolved:true`, plus `since:<checkedAt from the previous scan>` after the first pass. Keep the returned `checkedAt` as the next cursor.\n2. **Select** \u2014 treat a comment as a task when it reads as an instruction ("make this...", "fix...", "@relai ..."). Skip chatter between humans and threads you already replied to (your replies carry your token account\'s handle).\n3. **Anchor** \u2014 pinned comments carry a `nodeId`. Start there: `get_node_details` / `screenshot` for context. Unpinned comments apply to the file broadly; ask before guessing a target.\n4. **Claim** \u2014 reply on the thread *before* working: `action:"reply"` with a one-liner like `\u{1F916} On it \u2014 <what you understood>`. This prevents double-work across sessions and tells the designer they were heard.\n5. **Execute** \u2014 normal Relai flow: design-system first, verify with `verify_visual` / `screenshot`.\n6. **Report back** \u2014 reply on the same thread with what changed and the node ids touched, e.g. `\u{1F916} Done: CTA restyled to primary/600, radius token applied (2 nodes). Reply here if it\'s off.` The REST API cannot mark threads resolved \u2014 the designer resolves it, which doubles as their sign-off.\n\n## One-shot phrasing for users\n\nUsers can drive the whole loop with a single ask: *"Check the file\'s comments and handle anything addressed to you, then report back on each thread."* For a standing loop, they re-issue that ask periodically (or wire it to their client\'s scheduler if it has one).\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n';
|
|
3504
3544
|
|
|
3505
3545
|
// ../../docs/skills/design-system-first.md
|
|
3506
|
-
var design_system_first_default = '# Design-system-first building\n\nThe difference between "AI slop" and work a design team accepts is almost always this: **slop draws new shapes; good work reuses the file\'s own system**. Before building any UI in a file that has one, wire yourself to it.\n\n## The sequence\n\n1. **`get_design_system`** \u2014 one call, cached per session. Read what exists: components (sorted by usage \u2014 high-usage items are the file\'s vocabulary), variable collections, styles, and the remote items the file already consumes. If the team\'s library lives in another file and `FIGMA_TOKEN` is set, pass `libraryFileUrl` for the full catalog.\n2. **`manage_conventions` (get)** \u2014 if the file carries conventions, they outrank your defaults. (`get_document_overview` includes
|
|
3546
|
+
var design_system_first_default = '# Design-system-first building\n\nThe difference between "AI slop" and work a design team accepts is almost always this: **slop draws new shapes; good work reuses the file\'s own system**. Before building any UI in a file that has one, wire yourself to it.\n\n## The sequence\n\n1. **`get_design_system`** \u2014 one call, cached per session. Read what exists: components (sorted by usage \u2014 high-usage items are the file\'s vocabulary), variable collections, styles, and the remote items the file already consumes. If the team\'s library lives in another file and `FIGMA_TOKEN` is set, pass `libraryFileUrl` for the full catalog.\n2. **`manage_conventions` (get)** \u2014 if the file carries conventions or precedents, they outrank your defaults: conventions are the statutes, precedents are the designer\'s case law (a precedent saying a deviation is intent means you never "fix" it). (`get_document_overview` includes conventions automatically.)\n3. **Compose from instances** \u2014 `manage_components` `action:"instantiate"` with the component key (works for local AND enabled-library keys; remote keys import automatically). Set variants/text via `set_props`. Only draw raw shapes for genuinely new elements.\n4. **Bind, don\'t hardcode** \u2014 colors/spacing/radius come from the file\'s variables (`manage_variables` `action:"bind"`, or variable fields in `set_properties`). If you must hardcode during exploration, run `analyze_design aspect:"tokens"` afterward and fix with `manage_variables action:"tokenize" fix:true`.\n5. **Verify like a designer** \u2014 `screenshot` / `verify_visual` after each increment, not at the end.\n\n## When the file has no system\n\nDon\'t invent variables for a one-off request. But if the work IS system-shaped (a palette, a type scale, components), build variables first, then styles/components on top \u2014 and offer to record naming decisions in `manage_conventions`.\n\n## Signals you\'re drifting\n\n- You drew a rectangle that looks like an existing Button/Card/Badge \u2192 replace it with an instance.\n- A hex value appears in your code that `get_design_system` lists as a variable \u2192 bind instead.\n- You renamed or restructured library instances the designer didn\'t ask about \u2192 undo; instances belong to their component.\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n';
|
|
3507
3547
|
|
|
3508
3548
|
// ../../docs/skills/janitorial-cleanup.md
|
|
3509
3549
|
var janitorial_cleanup_default = '# Janitorial cleanup\n\nThe chores designers postpone for months \u2014 layer renaming, token drift, spacing normalization \u2014 are where an agent earns trust fastest. These jobs are big, mechanical, and easy to verify. Do them conservatively and loudly.\n\n## Ground rules\n\n- **Preview before bulk.** Use `dryRun:true` on `batch_execute` / `set_properties`, or report findings first (`analyze_design`), and tell the designer the scale ("about 140 renames on this page \u2014 go?"). If the plugin\'s approval gate is on, big writes will ask them anyway.\n- **Scope tightly.** Work page by page (or within the designer\'s selection). Never sweep the whole file unasked.\n- **Never rename or restyle library instances\' internals** \u2014 only top-level frames/groups the team owns.\n\n## Recipes\n\n### Rename layers by content\n`Frame 427` tells nobody anything. Walk the target scope with `execute_figma` + `relai.query`, and derive names:\n- A frame whose only text child says "Submit" \u2192 `Button/Submit` (match the file\'s existing naming pattern from `get_design_system`, don\'t invent one).\n- Text nodes \u2192 their content, truncated (`Heading: Pricing plans`).\n- Skip nodes that already have deliberate names (anything not matching `/^(Frame|Group|Rectangle|Ellipse|Vector|Line) \\d+$/`).\nReturn the rename list; apply via one `batch_execute` of `rename_node` commands.\n\n### Tokenize hardcoded values\n`analyze_design aspect:"tokens"` finds hardcoded colors/numbers that visually match existing variables; `manage_variables action:"tokenize" fix:true` binds them in one pass. Report anything close-but-not-matching (deltaE just over tolerance) instead of silently snapping it.\n\n### Normalize spacing\nIn auto-layout scopes, collect `itemSpacing`/padding values; flag off-scale values (not on the file\'s 4/8pt grid or spacing variables) and propose the nearest on-scale value. Apply only after the designer confirms the mapping.\n\n### Sweep detached instances\n`analyze_design aspect:"components"` lists likely-detached instances. Offer to re-link obvious ones (matching component still exists) and just report the rest \u2014 re-attaching wrong is worse than detached.\n\n## Report format\n\nEnd with counts and jump-points: what changed, what was skipped and why, node ids for spot-checking (the designer can click entries in the plugin feed).\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n';
|
|
3510
3550
|
|
|
3551
|
+
// ../../docs/skills/memory-and-precedents.md
|
|
3552
|
+
var memory_and_precedents_default = '# File memory & precedents\n\nEvery Figma file this plugin touches can carry its own case law: **precedents** \u2014 single adjudications the designer made, stored inside the file (shared plugin data) so they reach every future session from any AI client. Conventions are the statutes; precedents are the case law. Together they are the file\'s law, and `manage_conventions action:get` returns both.\n\n## Read before you work\n\nAt the start of any session that will modify the file, call `manage_conventions action:get`. Treat what comes back the way you treat user instructions:\n\n- `content` \u2014 the conventions doc (statutes).\n- `precedents` \u2014 recent adjudications (case law), newest first, with `precedentCount` for the total.\n\nA precedent outranks your own judgment about what "looks wrong". If a precedent says a deviation is intent, do not "fix" it \u2014 and do not re-ask about it.\n\n## When to record\n\nRecord a precedent (`action:record_precedent`) when the designer **rules** on something durable. The signal is adjudication language:\n\n- "This is intentional, not drift" \u2192 `kind: "intent"`\n- "Approved \u2014 but never restructure this table again" \u2192 `kind: "decision"`\n- "Don\'t do it that way; do X instead" \u2192 `kind: "correction"`\n\nOne precedent = one sentence (\u2264280 chars), written so a future session with zero context understands the ruling. Attach `refs` \u2014 the variable IDs, node IDs, or page IDs the ruling concerns \u2014 because refs power in-band surfacing: any future write that touches them gets the precedent attached to its result automatically.\n\n**Always say in your reply that you recorded it** ("\u5DF2\u8BB0\u5165\u6587\u4EF6\u8BB0\u5FC6: \u2026"). Recording silently breaks the designer\'s trust in what the file knows.\n\n## When NOT to record\n\n- Transient, one-off choices ("make this one red for the screenshot").\n- Anything already in the conventions doc (point to it instead; propose a conventions edit if it deserves statute status).\n- Secrets, personal information, or anything about people rather than the file.\n- Your own inferences the designer has not confirmed. Precedents are the DESIGNER\'s rulings, not your observations. When unsure, ask: "\u8BB0\u4E3A\u5224\u4F8B\u5417?"\n\n## Maintenance\n\n- The designer sees and can delete every entry in the plugin panel (Memory section). Their record, their rules.\n- Memory holds 200 entries / 64KB. When recording fails with a capacity error, offer a consolidation pass: `list_precedents`, merge superseded rulings into fewer summary entries (`record_precedent` the summaries, `remove_precedent` the merged ones), preserving every still-active ruling. Show the merge plan before applying it.\n- If a precedent contradicts current reality (the token it references is gone, the ruling was reversed), ask the designer whether to update or remove it \u2014 never silently drop case law.\n';
|
|
3553
|
+
|
|
3511
3554
|
// src/prompts.ts
|
|
3512
3555
|
var SKILLS = [
|
|
3513
3556
|
[
|
|
@@ -3554,6 +3597,11 @@ var SKILLS = [
|
|
|
3554
3597
|
"janitorial-cleanup",
|
|
3555
3598
|
"Bulk cleanup recipes: content-based layer renaming, tokenizing hardcoded values, spacing normalization, detached-instance sweeps \u2014 with preview-first discipline.",
|
|
3556
3599
|
janitorial_cleanup_default
|
|
3600
|
+
],
|
|
3601
|
+
[
|
|
3602
|
+
"memory-and-precedents",
|
|
3603
|
+
"The file's case law: when to record a designer adjudication as a precedent, when not to, and how memory is maintained. Load in any session that adjudicates or touches a file with existing precedents.",
|
|
3604
|
+
memory_and_precedents_default
|
|
3557
3605
|
]
|
|
3558
3606
|
];
|
|
3559
3607
|
function registerPrompts(server) {
|
|
@@ -3567,6 +3615,16 @@ function registerPrompts(server) {
|
|
|
3567
3615
|
]
|
|
3568
3616
|
}));
|
|
3569
3617
|
}
|
|
3618
|
+
for (const skill of loadUserSkills().skills) {
|
|
3619
|
+
server.prompt(`user:${skill.name}`, skill.description, () => ({
|
|
3620
|
+
messages: [
|
|
3621
|
+
{
|
|
3622
|
+
role: "user",
|
|
3623
|
+
content: { type: "text", text: skill.text }
|
|
3624
|
+
}
|
|
3625
|
+
]
|
|
3626
|
+
}));
|
|
3627
|
+
}
|
|
3570
3628
|
}
|
|
3571
3629
|
|
|
3572
3630
|
export {
|
|
@@ -3580,4 +3638,4 @@ export {
|
|
|
3580
3638
|
listToolCatalog,
|
|
3581
3639
|
registerPrompts
|
|
3582
3640
|
};
|
|
3583
|
-
//# sourceMappingURL=chunk-
|
|
3641
|
+
//# sourceMappingURL=chunk-2MUSDWUC.js.map
|