@grampro/kit 2.5.0 → 2.6.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/CHANGELOG.md +88 -0
- package/README.md +25 -5
- package/index.cjs +32 -1
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +1 -1
- package/source/components/combobox/passport.json +1 -1
- package/source/components/data-grid/passport.json +1 -1
- package/source/components/date-picker/passport.json +1 -1
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +1 -1
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +1 -1
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +1 -1
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +1 -1
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +1 -1
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/mcp/README.md +37 -2
- package/tools/mcp/design.cjs +154 -0
- package/tools/mcp/figma.cjs +52 -2
- package/tools/mcp/server.cjs +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
**A whole Figma design, exported once and built screen by screen.**
|
|
8
|
+
|
|
9
|
+
In Figma, **Grampro Kit → Export all screens for an agent** walks every page
|
|
10
|
+
and takes each top-level frame. Then:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @grampro/kit design ./design.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
Wrote 3 screen(s) to design/
|
|
18
|
+
purchase-orders.json Purchase orders (Product) — 22 nodes
|
|
19
|
+
suppliers.json Suppliers (Product) — 16 nodes
|
|
20
|
+
settings.json Settings (Admin) — 12 nodes
|
|
21
|
+
index.json the plan: every screen, shared theme
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The accuracy is in the theme rather than the convenience. Derived one
|
|
25
|
+
frame at a time it disagrees with itself — a screen that is mostly white
|
|
26
|
+
names a different background from one that is mostly grey — and the agent
|
|
27
|
+
then themes each page differently. Read across every screen, once, it is
|
|
28
|
+
well founded.
|
|
29
|
+
|
|
30
|
+
`index.json` is the plan: every screen with its size, node count and the
|
|
31
|
+
regions whose names repeat, plus that shared theme. It is the cheap place
|
|
32
|
+
to be wrong. A design drawn without the component library names nothing
|
|
33
|
+
directly, so every component is inferred, and a guess that `Action` is a
|
|
34
|
+
Card rather than a Button costs one sentence there and five screens of
|
|
35
|
+
rework later.
|
|
36
|
+
|
|
37
|
+
**`figma_frame` reads a file.** Pass `designFile` with a path instead of
|
|
38
|
+
`design` with the JSON. A chat input caps at around fifty thousand
|
|
39
|
+
characters and a whole screen passes that; and a pasted export sits in the
|
|
40
|
+
conversation twice, once as the argument and again inside the tool call,
|
|
41
|
+
where a file appears only in the answer.
|
|
42
|
+
|
|
43
|
+
**Every prop the library declares now carries a description.** 117 did
|
|
44
|
+
not, and the passports are what an agent reads instead of the source, so
|
|
45
|
+
an undescribed prop is one it guesses at. `DataGrid`'s `columns` and
|
|
46
|
+
`data` had none at all. See 2.5.0's notes — this is that work, reaching
|
|
47
|
+
npm for the first time.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
**The Figma export costs a third of what it did.** The copy button takes
|
|
52
|
+
minified JSON: `JSON.parse` cannot tell the difference, and indentation
|
|
53
|
+
was most of the payload. Three fields that said nothing went with it —
|
|
54
|
+
`radius: 0` on every square frame, `padding: [0,0,0,0]` on every frame
|
|
55
|
+
without padding, and `align`/`justify` restating Figma's own defaults —
|
|
56
|
+
along with the child frame Figma wraps around every icon, whose entire
|
|
57
|
+
content was the word "Vector" when the frame's name (`search`, `plus`)
|
|
58
|
+
was the whole signal.
|
|
59
|
+
|
|
60
|
+
**It also says more about what it exports.** Node size and stroke colour
|
|
61
|
+
are now included, because most designs are not built from the kit's Figma
|
|
62
|
+
library and an agent has to infer every component from what it can see. A
|
|
63
|
+
36px pill with a label is a button; the same shape at 200px is a card. A
|
|
64
|
+
white box with a border is a secondary button; without one it is a
|
|
65
|
+
surface. Neither was visible before.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
**The design system generator laid out over itself.** Variant grids were
|
|
70
|
+
placed by column count without measuring whether the row fitted, so 14 of
|
|
71
|
+
38 component sets overflowed the card holding them — Alert reached 2444px
|
|
72
|
+
inside a 1320px section.
|
|
73
|
+
|
|
74
|
+
**Text that was told a width kept its own.** Figma creates a text node
|
|
75
|
+
auto-width, and setting it to fill is quietly ignored, so 23 layers across
|
|
76
|
+
the file measured themselves from their own characters. NumberInput's
|
|
77
|
+
value layer, created empty and filled in later by a component property,
|
|
78
|
+
wrapped one glyph per line.
|
|
79
|
+
|
|
80
|
+
**Alert's coloured edge did not match its card**, being sized before the
|
|
81
|
+
card had any content and never clipped to its rounded corners. Toast drew
|
|
82
|
+
the same edge with the same fault.
|
|
83
|
+
|
|
84
|
+
**The uploader thumbnails and the dialog's intent badge rendered at full
|
|
85
|
+
saturation**, which also hid the icon drawn on them in the same colour.
|
|
86
|
+
|
|
87
|
+
**The plugin's name displayed as mojibake** in Figma's menu, and its
|
|
88
|
+
build was never committed despite its README saying so, so a clone could
|
|
89
|
+
not run it.
|
|
90
|
+
|
|
3
91
|
## 2.5.0
|
|
4
92
|
|
|
5
93
|
### Added
|
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ React components you own — styled, dependency-free, and machine-readable. Each
|
|
|
6
6
|
component arrives in your project as source, with its stylesheet, and with a
|
|
7
7
|
contract describing what it is and what can safely be done to it.
|
|
8
8
|
|
|
9
|
+
It ships an **MCP server**, so Claude Code, Codex and Cursor can read that
|
|
10
|
+
contract instead of guessing at it — and build a screen straight from a
|
|
11
|
+
Figma design.
|
|
12
|
+
|
|
9
13
|
```bash
|
|
10
14
|
npx @grampro/kit add DataGrid
|
|
11
15
|
```
|
|
@@ -92,15 +96,31 @@ In this repository:
|
|
|
92
96
|
|
|
93
97
|
## MCP server (beta)
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
The catalogue, served to a coding agent on demand over the Model Context
|
|
100
|
+
Protocol:
|
|
96
101
|
|
|
97
102
|
```bash
|
|
98
|
-
|
|
103
|
+
npx @grampro/kit mcp install
|
|
99
104
|
```
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
That configures **Claude Code** (`.mcp.json`), **Codex**
|
|
107
|
+
(`.codex/config.toml`) and **Cursor** (`.cursor/mcp.json`) in one command,
|
|
108
|
+
merging into whatever is already there.
|
|
109
|
+
|
|
110
|
+
Its seven tools answer what components exist (`list_components`), what a
|
|
111
|
+
component's props actually are (`get_component`), how it is used
|
|
112
|
+
(`get_example`, `get_guidance`), what a Figma frame contains
|
|
113
|
+
(`figma_frame`), and — the one that earns its place — whether the JSX an
|
|
114
|
+
agent just wrote matches the real API (`validate_usage`).
|
|
115
|
+
|
|
116
|
+
`install_component` copies what the design needs into your project, so an
|
|
117
|
+
agent can go from a Figma link to a working screen without you installing
|
|
118
|
+
anything first.
|
|
119
|
+
|
|
120
|
+
No dependencies, and it starts in about the time an empty Node process
|
|
121
|
+
takes. Full walkthrough, including a prompt to paste:
|
|
122
|
+
[kit.gramproindia.com/mcp-server](https://kit.gramproindia.com/mcp-server).
|
|
123
|
+
Details in [`tools/mcp/README.md`](tools/mcp/README.md).
|
|
104
124
|
|
|
105
125
|
## Agent skill
|
|
106
126
|
|
package/index.cjs
CHANGED
|
@@ -1190,7 +1190,7 @@ const validateComponents = (components, beta = false) => {
|
|
|
1190
1190
|
* projects a week install this; breaking their scripts to tidy an argument
|
|
1191
1191
|
* parser is not a trade worth making.
|
|
1192
1192
|
*/
|
|
1193
|
-
const COMMANDS = new Set(["add", "list", "skill", "passport", "mcp", "secret"]);
|
|
1193
|
+
const COMMANDS = new Set(["add", "list", "skill", "passport", "mcp", "secret", "design"]);
|
|
1194
1194
|
|
|
1195
1195
|
/**
|
|
1196
1196
|
* Flags that take a separate value, so the token after one is that value and
|
|
@@ -1297,6 +1297,7 @@ const main = async () => {
|
|
|
1297
1297
|
.example("$0 mcp install", "Register the MCP server with every agent found")
|
|
1298
1298
|
.example("$0 mcp install --for codex", "Only write the Codex config")
|
|
1299
1299
|
.example("$0 secret set figma", "Store a Figma token in the OS credential store")
|
|
1300
|
+
.example("$0 design ./design.json", "Split a whole-file Figma export into design/")
|
|
1300
1301
|
.example("$0 -i", "Interactive selection")
|
|
1301
1302
|
.help().argv;
|
|
1302
1303
|
|
|
@@ -1333,6 +1334,36 @@ const main = async () => {
|
|
|
1333
1334
|
return;
|
|
1334
1335
|
}
|
|
1335
1336
|
|
|
1337
|
+
if (parsed && parsed.command === "design") {
|
|
1338
|
+
const [file, dest] = parsed.operands;
|
|
1339
|
+
if (!file) {
|
|
1340
|
+
console.log(`Which export? Try: ${CLI_NAME} design ./design.json`);
|
|
1341
|
+
console.log("");
|
|
1342
|
+
console.log("Save one from Figma first: Grampro Kit -> Export all screens for an agent.");
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const { splitDesign } = require("./tools/mcp/design.cjs");
|
|
1346
|
+
const result = splitDesign(file, dest || "design");
|
|
1347
|
+
if (result.error) {
|
|
1348
|
+
console.log(result.error);
|
|
1349
|
+
process.exitCode = 1;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const rel = path.relative(process.cwd(), result.dest) || result.dest;
|
|
1353
|
+
console.log(`Wrote ${result.written.length} screen(s) to ${rel}/`);
|
|
1354
|
+
for (const screen of result.written) {
|
|
1355
|
+
const where = screen.page ? ` (${screen.page})` : "";
|
|
1356
|
+
console.log(` ${screen.file.padEnd(28)} ${screen.name}${where} — ${screen.nodes} nodes`);
|
|
1357
|
+
}
|
|
1358
|
+
console.log(` index.json${" ".repeat(18)} the plan: every screen, shared theme`);
|
|
1359
|
+
console.log("");
|
|
1360
|
+
console.log(
|
|
1361
|
+
`Point an agent at it: "read ${rel}/index.json and build these screens`
|
|
1362
|
+
+ ` with the grampro MCP server".`,
|
|
1363
|
+
);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1336
1367
|
if (parsed) {
|
|
1337
1368
|
if (parsed.command === "list") argv.list = true;
|
|
1338
1369
|
if (parsed.command === "skill") argv.skill = true;
|
package/package.json
CHANGED
package/tools/mcp/README.md
CHANGED
|
@@ -108,7 +108,7 @@ document pasted into the prompt.
|
|
|
108
108
|
| `get_component` | Props with types and allowed values, which are required, handler signatures, slots, states, composition rules |
|
|
109
109
|
| `get_example` | Working usage from the component's README |
|
|
110
110
|
| `get_guidance` | Authored notes on forms, overlays, the grid, pickers, styling, accessibility |
|
|
111
|
-
| `figma_frame` | A design's structure, text, repeated regions and theme variables |
|
|
111
|
+
| `figma_frame` | A design's structure, text, repeated regions and theme variables, from a `designFile` path, a pasted `design`, or a Figma `url` |
|
|
112
112
|
| `install_component` | Copies components into the project being worked in |
|
|
113
113
|
| `validate_usage` | Whether the JSX an agent wrote matches the passports |
|
|
114
114
|
|
|
@@ -125,9 +125,38 @@ invention, and this catches it:
|
|
|
125
125
|
dismissible — not a prop of this component. Did you mean: onDismiss
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
## A whole design at once
|
|
129
|
+
|
|
130
|
+
In Figma, **Grampro Kit → Export all screens for an agent** walks every
|
|
131
|
+
page and exports each top-level frame, with one set of theme values read
|
|
132
|
+
across the whole design rather than guessed per frame. Save it, then:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx @grampro/kit design ./design.json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
Wrote 3 screen(s) to design/
|
|
140
|
+
purchase-orders.json Purchase orders (Product) — 22 nodes
|
|
141
|
+
suppliers.json Suppliers (Product) — 16 nodes
|
|
142
|
+
settings.json Settings (Admin) — 12 nodes
|
|
143
|
+
index.json the plan: every screen, shared theme
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A browser cannot write a directory and a plugin is an iframe, so the
|
|
147
|
+
plugin hands over one file and the CLI does the filesystem work. Each
|
|
148
|
+
screen is written in the shape a single-frame export has, so `figma_frame`
|
|
149
|
+
reads one with `designFile` and no special case.
|
|
150
|
+
|
|
151
|
+
`index.json` is the plan: every screen with its size, node count and
|
|
152
|
+
repeated regions, plus the shared theme. Have an agent map regions to
|
|
153
|
+
components from that and correct it before any code exists — a guess that
|
|
154
|
+
`Action` is a Card rather than a Button costs one sentence there and five
|
|
155
|
+
screens of rework later. Then build one screen at a time, validating each.
|
|
156
|
+
|
|
128
157
|
## From a Figma design
|
|
129
158
|
|
|
130
|
-
|
|
159
|
+
Three routes, and only the first needs credentials.
|
|
131
160
|
|
|
132
161
|
**By URL.** Pass a `figma.com/design/...` link. Called with just the URL it
|
|
133
162
|
lists the frames in the file; called again with `frame` it returns that
|
|
@@ -140,6 +169,12 @@ agent**, then pass the JSON as `design`. The plugin reads the scene graph
|
|
|
140
169
|
directly, so this works on any file you can open, asks for no token, and
|
|
141
170
|
never touches the REST API — which also means it cannot be rate limited.
|
|
142
171
|
|
|
172
|
+
**By file.** Press **Save .json** in that panel and pass the path as
|
|
173
|
+
`designFile`. Prefer this for anything but a small frame: a chat input
|
|
174
|
+
caps at around fifty thousand characters, and a pasted export sits in the
|
|
175
|
+
conversation twice — once as the argument, again inside the tool call —
|
|
176
|
+
where a file appears only in the answer.
|
|
177
|
+
|
|
143
178
|
Either way the answer is the same shape: the structure, the text, the
|
|
144
179
|
regions whose names repeat — eight `Status cell` nodes are a column — and
|
|
145
180
|
the `--gbs-*` variables the design's own colours and radii map onto.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `grampro-kit design <file>` — a whole-file Figma export, split into a folder.
|
|
3
|
+
*
|
|
4
|
+
* The plugin downloads one file because a browser cannot write a directory.
|
|
5
|
+
* Splitting it is filesystem work, which belongs in the CLI rather than in a
|
|
6
|
+
* plugin iframe, and it is what makes the result usable: an agent reads the
|
|
7
|
+
* index to plan, then opens one screen at a time, instead of loading every
|
|
8
|
+
* screen to build any of them.
|
|
9
|
+
*
|
|
10
|
+
* Each screen is written in the same shape a single-frame export has, so
|
|
11
|
+
* `figma_frame` reads one with no special case.
|
|
12
|
+
*/
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const { themeFrom } = require("./figma.cjs");
|
|
18
|
+
|
|
19
|
+
/** A file name from a screen name: lowercase, dashes, nothing exotic. */
|
|
20
|
+
const slug = (name) =>
|
|
21
|
+
String(name)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-|-$/g, "") || "screen";
|
|
25
|
+
|
|
26
|
+
/** Names a designer repeated are the ones that mean something. */
|
|
27
|
+
function countNames(node, counts) {
|
|
28
|
+
counts.set(node.name, (counts.get(node.name) || 0) + 1);
|
|
29
|
+
for (const child of node.children || []) countNames(child, counts);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const repeatedIn = (nodes) => {
|
|
33
|
+
const counts = new Map();
|
|
34
|
+
for (const node of nodes) countNames(node, counts);
|
|
35
|
+
return [...counts.entries()]
|
|
36
|
+
.filter(([, n]) => n > 2)
|
|
37
|
+
.sort((a, b) => b[1] - a[1])
|
|
38
|
+
.slice(0, 12)
|
|
39
|
+
.map(([name, times]) => ({ name, times }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Component instances found anywhere in a screen. */
|
|
43
|
+
function componentsIn(nodes) {
|
|
44
|
+
const found = new Set();
|
|
45
|
+
const visit = (node) => {
|
|
46
|
+
if (node.component) found.add(node.component);
|
|
47
|
+
for (const child of node.children || []) visit(child);
|
|
48
|
+
};
|
|
49
|
+
for (const node of nodes) visit(node);
|
|
50
|
+
return [...found];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const countNodes = (nodes) =>
|
|
54
|
+
nodes.reduce((n, node) => n + 1 + countNodes(node.children || []), 0);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Split an export into `dest`.
|
|
58
|
+
*
|
|
59
|
+
* Returns what it wrote rather than printing, so the CLI owns the words and
|
|
60
|
+
* this stays testable.
|
|
61
|
+
*/
|
|
62
|
+
function splitDesign(file, dest) {
|
|
63
|
+
const source = path.resolve(file);
|
|
64
|
+
if (!fs.existsSync(source)) return { error: `No file at ${source}.` };
|
|
65
|
+
|
|
66
|
+
let design;
|
|
67
|
+
try {
|
|
68
|
+
design = JSON.parse(fs.readFileSync(source, "utf8").replace(/^/, ""));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return { error: `${source} is not JSON: ${error.message}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* A single-frame export is accepted too, as one screen. Someone who ran the
|
|
75
|
+
* wrong menu item should get a folder, not a lecture.
|
|
76
|
+
*/
|
|
77
|
+
const screens = Array.isArray(design.screens)
|
|
78
|
+
? design.screens
|
|
79
|
+
: Array.isArray(design.nodes)
|
|
80
|
+
? [{ name: design.frame || "screen", page: "", nodes: design.nodes }]
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
if (!screens) {
|
|
84
|
+
return {
|
|
85
|
+
error:
|
|
86
|
+
"Expected the output of the Figma plugin's 'Export all screens for an agent' " +
|
|
87
|
+
"(or 'Export selection'). This file has neither `screens` nor `nodes`.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (screens.length === 0) return { error: "That export contains no screens." };
|
|
91
|
+
|
|
92
|
+
const out = path.resolve(dest);
|
|
93
|
+
fs.mkdirSync(out, { recursive: true });
|
|
94
|
+
|
|
95
|
+
const tokens = design.tokens || {};
|
|
96
|
+
const colours = new Map((tokens.colours || []).map((c) => [c.value, c.uses]));
|
|
97
|
+
const radii = new Map((tokens.radii || []).map((r) => [r.value, r.uses]));
|
|
98
|
+
const fontSizes = new Map((tokens.fontSizes || []).map((f) => [f.value, f.uses]));
|
|
99
|
+
|
|
100
|
+
const taken = new Set();
|
|
101
|
+
const written = [];
|
|
102
|
+
|
|
103
|
+
for (const screen of screens) {
|
|
104
|
+
// Two screens may share a name across pages; the second gets a suffix
|
|
105
|
+
// rather than quietly overwriting the first.
|
|
106
|
+
let name = slug(screen.name);
|
|
107
|
+
if (taken.has(name)) {
|
|
108
|
+
let n = 2;
|
|
109
|
+
while (taken.has(`${name}-${n}`)) n += 1;
|
|
110
|
+
name = `${name}-${n}`;
|
|
111
|
+
}
|
|
112
|
+
taken.add(name);
|
|
113
|
+
|
|
114
|
+
const fileName = `${name}.json`;
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(out, fileName),
|
|
117
|
+
`${JSON.stringify({ frame: screen.name, nodes: screen.nodes, tokens }, null, 2)}\n`,
|
|
118
|
+
"utf8",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
written.push({
|
|
122
|
+
name: screen.name,
|
|
123
|
+
page: screen.page || undefined,
|
|
124
|
+
file: fileName,
|
|
125
|
+
size: screen.size,
|
|
126
|
+
nodes: countNodes(screen.nodes),
|
|
127
|
+
repeatedRegions: repeatedIn(screen.nodes),
|
|
128
|
+
componentsNamed: componentsIn(screen.nodes),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* The index is what an agent reads first: every screen, how big it is, the
|
|
134
|
+
* regions whose names repeat, and one theme for all of them. Enough to
|
|
135
|
+
* propose a component for each region before any code is written, which is
|
|
136
|
+
* where a wrong guess is cheap to correct.
|
|
137
|
+
*/
|
|
138
|
+
const index = {
|
|
139
|
+
screens: written,
|
|
140
|
+
theme: themeFrom(colours, radii, fontSizes),
|
|
141
|
+
builtFromLibrary: written.some((s) => s.componentsNamed.length > 0),
|
|
142
|
+
howToUse:
|
|
143
|
+
"Read this index first and say which kit component each repeated region maps to, " +
|
|
144
|
+
"so the mapping can be corrected before any screen is built. Then take one screen " +
|
|
145
|
+
"at a time: figma_frame with designFile set to its file, get_component for each " +
|
|
146
|
+
"component before writing it, install_component for what is missing, and " +
|
|
147
|
+
"validate_usage on the JSX before moving to the next screen.",
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(path.join(out, "index.json"), `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
150
|
+
|
|
151
|
+
return { dest: out, index, written };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { splitDesign, slug };
|
package/tools/mcp/figma.cjs
CHANGED
|
@@ -323,11 +323,61 @@ function fromPluginExport(design) {
|
|
|
323
323
|
builtFromLibrary: instances.length > 0,
|
|
324
324
|
componentsNamed: [...new Set(instances)],
|
|
325
325
|
tree: parsed.nodes,
|
|
326
|
+
/*
|
|
327
|
+
* The same closing instruction the URL route gives. Without it an agent
|
|
328
|
+
* arriving by the plugin — now the route the documentation leads with —
|
|
329
|
+
* got the tree and no word on what to do when nothing names a component,
|
|
330
|
+
* which is exactly when it starts approximating.
|
|
331
|
+
*/
|
|
332
|
+
howToUse: instances.length
|
|
333
|
+
? "This design uses component instances; `component` names the master for each."
|
|
334
|
+
: "This design was drawn without the component library, so nothing names a component directly. Use the region names and structure to decide which catalogue component each part is, then call get_component before writing it.",
|
|
326
335
|
};
|
|
327
336
|
}
|
|
328
337
|
|
|
329
|
-
|
|
330
|
-
|
|
338
|
+
/**
|
|
339
|
+
* The plugin's export, read from a file.
|
|
340
|
+
*
|
|
341
|
+
* Worth preferring over `design` for anything but a small frame, because a
|
|
342
|
+
* chat input caps at around fifty thousand characters and a whole screen
|
|
343
|
+
* passes that.
|
|
344
|
+
*
|
|
345
|
+
* It halves the context too, though not more: pasted, the export is in the
|
|
346
|
+
* conversation twice — once as the argument and again inside the tool call
|
|
347
|
+
* — where from a file it appears only in the answer. The tree still comes
|
|
348
|
+
* back in full, because the tree is what the agent builds from. The plugin
|
|
349
|
+
* has already condensed it; there is nothing further to drop that the
|
|
350
|
+
* screen does not need.
|
|
351
|
+
*/
|
|
352
|
+
function fromFile(file) {
|
|
353
|
+
let resolved;
|
|
354
|
+
try {
|
|
355
|
+
resolved = path.resolve(file);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { error: `Not a usable path: ${file}` };
|
|
358
|
+
}
|
|
359
|
+
if (!fs.existsSync(resolved)) {
|
|
360
|
+
return {
|
|
361
|
+
error:
|
|
362
|
+
`No file at ${resolved}. Save the plugin's export first — in Figma, ` +
|
|
363
|
+
"Grampro Kit -> Export selection for an agent, then Save .json.",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
let body;
|
|
367
|
+
try {
|
|
368
|
+
body = fs.readFileSync(resolved, "utf8");
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return { error: `Could not read ${resolved}: ${error.message}` };
|
|
371
|
+
}
|
|
372
|
+
// A byte-order mark survives a round trip through some editors and is not
|
|
373
|
+
// valid JSON, so it goes before parsing rather than after a confusing throw.
|
|
374
|
+
return fromPluginExport(body.replace(/^\uFEFF/, ""));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function figmaFrame({ url, design, designFile, frame, depth }) {
|
|
378
|
+
// Both export routes need no token, so they are offered before one is
|
|
379
|
+
// demanded. The file comes first: it is the one that scales.
|
|
380
|
+
if (designFile) return fromFile(designFile);
|
|
331
381
|
if (design) return fromPluginExport(design);
|
|
332
382
|
|
|
333
383
|
const token = figmaToken();
|
package/tools/mcp/server.cjs
CHANGED
|
@@ -371,7 +371,7 @@ const TOOLS = [
|
|
|
371
371
|
{
|
|
372
372
|
name: "figma_frame",
|
|
373
373
|
description:
|
|
374
|
-
"BETA. Read a Figma design
|
|
374
|
+
"BETA. Read a Figma design: from a URL (needs FIGMA_TOKEN), from a file the kit's Figma plugin saved (`designFile`, no token, and the one to use for a whole screen), or from that export pasted in (`design`). Called with just a URL it lists the frames in the file; called with `frame` it returns that screen's structure, text, the repeated regions that indicate lists and tables, and the theme variables its colours and radii map onto.",
|
|
375
375
|
inputSchema: {
|
|
376
376
|
type: "object",
|
|
377
377
|
properties: {
|
|
@@ -379,7 +379,12 @@ const TOOLS = [
|
|
|
379
379
|
design: {
|
|
380
380
|
type: "string",
|
|
381
381
|
description:
|
|
382
|
-
"Alternative to url, needing no token: JSON from the kit's Figma plugin, 'Export selection for an agent'.",
|
|
382
|
+
"Alternative to url, needing no token: JSON from the kit's Figma plugin, 'Export selection for an agent'. For anything but a small frame, prefer designFile.",
|
|
383
|
+
},
|
|
384
|
+
designFile: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description:
|
|
387
|
+
"Path to a .json saved by the plugin's 'Save .json' button. Preferred over `design`: the file is read here, so the raw export never passes through the conversation.",
|
|
383
388
|
},
|
|
384
389
|
frame: { type: "string", description: "Which frame to read, from the listing." },
|
|
385
390
|
depth: { type: "number", description: "How deep to descend. Lower it if the payload is large." },
|