@todoforai/figma-api 1.0.6 → 1.0.7
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/AGENTS.md +21 -9
- package/README.md +13 -5
- package/package.json +1 -1
- package/src/bridge.ts +31 -10
- package/src/index.ts +15 -7
package/AGENTS.md
CHANGED
|
@@ -7,7 +7,8 @@ Read this before using `figma-api`. It tells you exactly what to set up.
|
|
|
7
7
|
- Need to **read** designs, or **write** comments / dev-resources / webhooks?
|
|
8
8
|
→ REST is enough. Just set a token (step 1). Works headless / in CI.
|
|
9
9
|
- Need to **create canvas nodes** (frames, text, shapes)? → REST can't. Use the
|
|
10
|
-
**plugin bridge** (step 2). Requires the Figma desktop
|
|
10
|
+
**plugin bridge** (step 2). Requires a plugin connected in the Figma **desktop**
|
|
11
|
+
app (browser can't reach the localhost relay — see gotchas).
|
|
11
12
|
|
|
12
13
|
## 1. Auth (always required)
|
|
13
14
|
|
|
@@ -45,11 +46,15 @@ Setup (do all three, in order):
|
|
|
45
46
|
```bash
|
|
46
47
|
figma-api bridge # WebSocket relay on ws://localhost:3055
|
|
47
48
|
```
|
|
48
|
-
2. **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
2. **Connect a plugin** — a human step, you cannot automate it. Pick ONE:
|
|
50
|
+
- **Easy (recommended):** install **"Cursor Talk To Figma MCP"** from the
|
|
51
|
+
Figma Community (figma.com/community/plugin/1485687494525374295), run it in
|
|
52
|
+
the **desktop** app, set port `3055`, click **Connect**. It auto-picks a
|
|
53
|
+
random channel — that's fine: the relay bridges channels, so you do **not**
|
|
54
|
+
copy it or pass `--channel`.
|
|
55
|
+
- **Advanced:** in the Figma **desktop** app, Plugins → Development → Import
|
|
56
|
+
plugin from manifest → this package's `plugin/manifest.json`. Run it, set URL
|
|
57
|
+
`ws://localhost:3055`, channel `figma-api`, click **Connect**.
|
|
53
58
|
3. **Drive it** with named commands:
|
|
54
59
|
```bash
|
|
55
60
|
figma-api ping # round-trip check; prints page + selection
|
|
@@ -82,9 +87,16 @@ the plugin instead of localhost.
|
|
|
82
87
|
to `figma-api run` with `figma.variables.*` (off-Enterprise, via the bridge).
|
|
83
88
|
- **"create a frame/text/shape via REST"** is impossible — there is no endpoint.
|
|
84
89
|
Use the bridge (`create-frame`/`create-text`/…).
|
|
85
|
-
- **
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
- **Use the DESKTOP app for canvas writes.** Both plugins can be *opened* in
|
|
91
|
+
browser Figma, but a browser tab can't connect to `ws://localhost:3055`
|
|
92
|
+
(mixed-content: an https page blocks insecure `ws`), so it never reaches
|
|
93
|
+
"Connected" — this is the usual "plugin won't connect on Linux/browser" cause.
|
|
94
|
+
Desktop has no such block. Dev-manifest import is desktop-only regardless. If
|
|
95
|
+
desktop is truly impossible, expose the relay over TLS (`cloudflared tunnel
|
|
96
|
+
--url http://localhost:3055`) and paste the `wss://` URL into the plugin.
|
|
97
|
+
- **Channel names don't need to match.** The relay bridges across channels, so the
|
|
98
|
+
community plugin's random channel and the CLI's `figma-api` default interoperate
|
|
99
|
+
with no `--channel` flag.
|
|
88
100
|
- **`figma-api run` / the relay have no auth** and execute arbitrary code in the
|
|
89
101
|
user's document. Only run on a trusted machine; don't expose the tunnel URL
|
|
90
102
|
publicly; don't run untrusted code.
|
package/README.md
CHANGED
|
@@ -67,9 +67,14 @@ figma-api create-frame … ──ws──▶ relay (figma-api bridge) ◀─
|
|
|
67
67
|
Setup:
|
|
68
68
|
|
|
69
69
|
1. `figma-api bridge` — start the WebSocket relay (keep it running).
|
|
70
|
-
2.
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
2. Connect a plugin in the Figma **desktop** app — pick one:
|
|
71
|
+
- **Easy (recommended):** install [**Cursor Talk To Figma MCP**](https://www.figma.com/community/plugin/1485687494525374295)
|
|
72
|
+
from the Figma Community, run it, set port `3055`, click **Connect**. It picks
|
|
73
|
+
a random channel — that's fine, the relay bridges channels so you don't copy
|
|
74
|
+
it or pass `--channel`.
|
|
75
|
+
- **Advanced:** Plugins → Development → **Import plugin from manifest** →
|
|
76
|
+
`plugin/manifest.json`. Run it, set URL `ws://localhost:3055`, channel
|
|
77
|
+
`figma-api`, click **Connect**.
|
|
73
78
|
3. Drive it from the CLI with named commands:
|
|
74
79
|
|
|
75
80
|
```bash
|
|
@@ -98,8 +103,11 @@ Cross-machine: `cloudflared tunnel --url http://localhost:3055` and paste the
|
|
|
98
103
|
> Figma document. Only use it on a trusted machine/network; don't expose the tunnel
|
|
99
104
|
> URL publicly or run code you don't trust.
|
|
100
105
|
|
|
101
|
-
> Note:
|
|
102
|
-
>
|
|
106
|
+
> Note: use the **Figma desktop app**. A browser tab can't reach `ws://localhost:3055`
|
|
107
|
+
> (an https page blocks insecure `ws` as mixed content), so the plugin never reaches
|
|
108
|
+
> "Connected" — the usual "won't connect in the browser" cause. Dev-manifest import
|
|
109
|
+
> is desktop-only regardless. If desktop is impossible, expose the relay over TLS
|
|
110
|
+
> (`cloudflared tunnel --url http://localhost:3055`) and paste the `wss://` URL in.
|
|
103
111
|
|
|
104
112
|
## Why both REST and a plugin?
|
|
105
113
|
|
package/package.json
CHANGED
package/src/bridge.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
//
|
|
9
9
|
// figma-api create-frame … ──ws──▶ relay (channel broadcast) ◀──ws── plugin
|
|
10
10
|
//
|
|
11
|
-
// Protocol (channel-scoped
|
|
11
|
+
// Protocol (join/ack are channel-scoped; forwarding is cross-channel — see
|
|
12
|
+
// startBridge — so a plugin on its own random channel still reaches the CLI):
|
|
12
13
|
// join: { type:"join", channel } → { type:"system", message:{ id, result } }
|
|
13
14
|
// command: { type:"message", channel, message:{ id, command, params } }
|
|
14
15
|
// result: { type:"message", channel, message:{ id, result } | { id, error } }
|
|
@@ -33,6 +34,11 @@ function manifestPath(): string {
|
|
|
33
34
|
|
|
34
35
|
export function startBridge(port: number): void {
|
|
35
36
|
const channels = new Map<string, Set<ServerWebSocket<WsData>>>();
|
|
37
|
+
// Every connected peer, regardless of channel. The relay is single-user and
|
|
38
|
+
// local (one plugin + one CLI at a time), so we bridge across channels: the
|
|
39
|
+
// CLI joins "figma-api" while the community TTF plugin picks its own random
|
|
40
|
+
// channel — forwarding to all *other* peers spares the user copying it.
|
|
41
|
+
const all = new Set<ServerWebSocket<WsData>>();
|
|
36
42
|
|
|
37
43
|
Bun.serve<WsData, undefined>({
|
|
38
44
|
port,
|
|
@@ -45,6 +51,7 @@ export function startBridge(port: number): void {
|
|
|
45
51
|
// drop-in: welcome on open, two-part join ack, peer notices, broadcasts
|
|
46
52
|
// wrapped as {type:"broadcast", sender:"peer", message:<inner>}.
|
|
47
53
|
open(ws) {
|
|
54
|
+
all.add(ws);
|
|
48
55
|
ws.send(JSON.stringify({ type: "system", message: "Please join a channel to start chatting" }));
|
|
49
56
|
},
|
|
50
57
|
message(ws, raw) {
|
|
@@ -68,18 +75,21 @@ export function startBridge(port: number): void {
|
|
|
68
75
|
const ch = ws.data.channel;
|
|
69
76
|
if (!ch || !channels.get(ch)?.has(ws)) { ws.send(JSON.stringify({ type: "error", message: "You must join the channel first" })); return; }
|
|
70
77
|
|
|
78
|
+
// Forward across ALL peers (not just the sender's channel) so the CLI and
|
|
79
|
+
// the community plugin bridge even when their channel names differ.
|
|
71
80
|
// Forward progress updates verbatim to the other peers (upstream parity).
|
|
72
81
|
if (msg.type === "progress_update") {
|
|
73
|
-
for (const peer of
|
|
82
|
+
for (const peer of all) if (peer !== ws) peer.send(JSON.stringify(msg));
|
|
74
83
|
return;
|
|
75
84
|
}
|
|
76
85
|
if (msg.type !== "message") return;
|
|
77
86
|
|
|
78
87
|
// Broadcast the inner message to the OTHER peers (sender never gets its own).
|
|
79
88
|
const out = JSON.stringify({ type: "broadcast", sender: "peer", channel: ch, message: msg.message });
|
|
80
|
-
for (const peer of
|
|
89
|
+
for (const peer of all) if (peer !== ws) peer.send(out);
|
|
81
90
|
},
|
|
82
91
|
close(ws) {
|
|
92
|
+
all.delete(ws);
|
|
83
93
|
const ch = ws.data.channel;
|
|
84
94
|
if (!ch) return;
|
|
85
95
|
const peers = channels.get(ch);
|
|
@@ -92,7 +102,7 @@ export function startBridge(port: number): void {
|
|
|
92
102
|
});
|
|
93
103
|
|
|
94
104
|
console.log(`figma-api bridge (WebSocket) listening on ws://localhost:${port}`);
|
|
95
|
-
console.log(`
|
|
105
|
+
console.log(`Open a plugin, click Connect (any channel — the relay bridges them), then run figma-api commands.`);
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
/** Setup instructions shown only when a canvas command can't reach the plugin. */
|
|
@@ -102,14 +112,25 @@ the plugin bridge, which the user must install.
|
|
|
102
112
|
|
|
103
113
|
AGENT: be explicit with the user about this. The bridge plugin grants write access
|
|
104
114
|
to their Figma documents — confirm they want it, then walk them through (the plugin
|
|
105
|
-
runs in THEIR Figma, you can't do step
|
|
115
|
+
runs in THEIR Figma, you can't do the plugin step for them):
|
|
106
116
|
1. Start the relay (keep running): figma-api bridge
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
|
|
118
|
+
2. Install & connect a plugin — pick ONE:
|
|
119
|
+
EASY (recommended): install "Cursor Talk To Figma MCP" from the Figma
|
|
120
|
+
Community (figma.com/community/plugin/1485687494525374295), run it, set
|
|
121
|
+
port ${port}, click Connect. It picks its own random channel — that's fine,
|
|
122
|
+
the relay bridges channels so you do NOT need to copy it or pass --channel.
|
|
123
|
+
ADVANCED: in the Figma DESKTOP app, Plugins → Development → Import plugin
|
|
124
|
+
from manifest → ${manifestPath()}
|
|
125
|
+
run it, set URL ws://localhost:${port}, channel "${DEFAULT_CHANNEL}", Connect.
|
|
126
|
+
|
|
110
127
|
3. Retry. Verify the round-trip with: figma-api ping
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
|
|
129
|
+
USE THE DESKTOP APP. Both plugins can be *opened* in browser Figma, but a browser
|
|
130
|
+
tab can't reach ws://localhost:${port} (mixed-content: an https page blocks insecure
|
|
131
|
+
ws), so it never reaches "Connected". Desktop has no such block. If desktop is truly
|
|
132
|
+
impossible, expose the relay over TLS — 'cloudflared tunnel --url http://localhost:${port}'
|
|
133
|
+
— and paste that wss:// URL into the plugin. (Dev-manifest import is desktop-only.)`;
|
|
113
134
|
|
|
114
135
|
/** CLI peer: connect to the relay, send one command, return the plugin's result. */
|
|
115
136
|
export function sendCommand(
|
package/src/index.ts
CHANGED
|
@@ -640,13 +640,19 @@ ecosystem's MCP server / plugin.
|
|
|
640
640
|
|
|
641
641
|
Flow:
|
|
642
642
|
1. figma-api bridge # start this relay (keep running)
|
|
643
|
-
2.
|
|
644
|
-
|
|
645
|
-
|
|
643
|
+
2. Connect a plugin in the Figma DESKTOP app (pick one):
|
|
644
|
+
EASY: install "Cursor Talk To Figma MCP" from the Figma Community, run it,
|
|
645
|
+
set port ${DEFAULT_PORT}, Connect. Its random channel is fine — the relay
|
|
646
|
+
bridges channels, so no --channel needed.
|
|
647
|
+
ADVANCED: Plugins → Development → Import plugin from manifest →
|
|
648
|
+
plugin/manifest.json, set URL ws://localhost:${DEFAULT_PORT}, channel
|
|
649
|
+
"${DEFAULT_CHANNEL}", Connect.
|
|
646
650
|
3. figma-api ping # verify; then create-frame / create-text / …
|
|
647
651
|
|
|
648
|
-
|
|
649
|
-
|
|
652
|
+
Use the DESKTOP app: a browser tab can't reach ws://localhost:${DEFAULT_PORT} (an https
|
|
653
|
+
page blocks insecure ws), so it never connects. If desktop is impossible, expose the
|
|
654
|
+
relay over TLS with 'cloudflared tunnel --url http://localhost:${DEFAULT_PORT}' and paste the
|
|
655
|
+
wss:// URL into the plugin.
|
|
650
656
|
|
|
651
657
|
⚠️ Security: the relay has no auth and 'run' executes arbitrary code in your
|
|
652
658
|
Figma document. Only run it on a trusted machine/network.`)
|
|
@@ -654,8 +660,10 @@ Figma document. Only run it on a trusted machine/network.`)
|
|
|
654
660
|
|
|
655
661
|
canvas("ping")
|
|
656
662
|
.description("Ping the connected plugin to verify the bridge round-trip")
|
|
657
|
-
.addHelpText("after", `\nReturns the
|
|
658
|
-
|
|
663
|
+
.addHelpText("after", `\nReturns the current document info. Works with both the bundled plugin and the\ncommunity Cursor-Talk-To-Figma plugin.\nExample:\n figma-api ping`)
|
|
664
|
+
// get_document_info exists in both the bundled and the community plugin (unlike
|
|
665
|
+
// "ping", which is bundled-only), so verification works whichever is connected.
|
|
666
|
+
.action((o: any) => drive(o, "get_document_info"));
|
|
659
667
|
|
|
660
668
|
// ── canvas reads ──────────────────────────────────────────────────────────────
|
|
661
669
|
canvas("get-document-info")
|