pi-mono-figma 0.1.1
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 +34 -0
- package/LICENCE.md +7 -0
- package/README.md +171 -0
- package/index.ts +6 -0
- package/package.json +27 -0
- package/skills/figma/SKILL.md +113 -0
- package/src/figma-cache.ts +6 -0
- package/src/figma-client.ts +335 -0
- package/src/figma-schemas.ts +84 -0
- package/src/figma-summarizer.ts +606 -0
- package/src/figma-tools.ts +269 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { registerAuthConfigurator, runWithAuthRetry, type AuthConfiguratorOptions } from "pi-common/auth-config";
|
|
3
|
+
import { jsonToolResult, textToolResult } from "pi-common/tool-result";
|
|
4
|
+
import { FigmaClient, parseFigmaUrl } from "./figma-client.js";
|
|
5
|
+
import {
|
|
6
|
+
FigmaGetDesignContextParams,
|
|
7
|
+
FigmaGetFileParams,
|
|
8
|
+
FigmaGetNodesParams,
|
|
9
|
+
FigmaProcessedNodeParams,
|
|
10
|
+
FigmaProcessedNodeWithRenderParams,
|
|
11
|
+
FigmaParseUrlParams,
|
|
12
|
+
FigmaRenderNodesParams,
|
|
13
|
+
FigmaSearchComponentsParams,
|
|
14
|
+
FigmaSingleFileParams,
|
|
15
|
+
} from "./figma-schemas.js";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PROCESSED_MAX_CHARS = 20_000;
|
|
18
|
+
const DEFAULT_RAW_MAX_CHARS = 40_000;
|
|
19
|
+
|
|
20
|
+
interface ProcessedNodeParams {
|
|
21
|
+
fileKey: string;
|
|
22
|
+
nodeId: string;
|
|
23
|
+
depth?: number;
|
|
24
|
+
includeHidden?: boolean;
|
|
25
|
+
includeVectors?: boolean;
|
|
26
|
+
includeComponentInternals?: boolean;
|
|
27
|
+
renderImage?: boolean;
|
|
28
|
+
outputDir?: string;
|
|
29
|
+
format?: "png" | "jpg" | "svg" | "pdf";
|
|
30
|
+
scale?: number;
|
|
31
|
+
maxResponseChars?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FIGMA_AUTH: AuthConfiguratorOptions = {
|
|
35
|
+
service: "figma",
|
|
36
|
+
displayName: "Figma",
|
|
37
|
+
envName: "FIGMA_TOKEN",
|
|
38
|
+
authPath: ["figma", "token"],
|
|
39
|
+
commandName: "figma-auth",
|
|
40
|
+
toolName: "figma_configure_auth",
|
|
41
|
+
tokenUrl: "https://www.figma.com/settings/tokens",
|
|
42
|
+
scopeInstructions: ["Enable File content/read access for the files, projects, or team you want pi to inspect.", "No write/admin scopes are required."],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function withFigmaAuth<T>(ctx: ExtensionContext, operation: () => Promise<T>): Promise<T> {
|
|
46
|
+
return runWithAuthRetry(ctx, FIGMA_AUTH, operation);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerFigmaTools(pi: ExtensionAPI): void {
|
|
50
|
+
const client = new FigmaClient();
|
|
51
|
+
registerAuthConfigurator(pi, FIGMA_AUTH);
|
|
52
|
+
|
|
53
|
+
pi.registerTool({
|
|
54
|
+
name: "figma_parse_url",
|
|
55
|
+
label: "Figma Parse URL",
|
|
56
|
+
description: "Parse a Figma URL into fileKey and nodeId values for the other figma_* tools.",
|
|
57
|
+
promptSnippet: "Parse Figma URLs into file key and node ID.",
|
|
58
|
+
parameters: FigmaParseUrlParams,
|
|
59
|
+
async execute(_toolCallId, params) {
|
|
60
|
+
return jsonToolResult(parseFigmaUrl(params.url));
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
pi.registerTool({
|
|
65
|
+
name: "figma_get_design_context",
|
|
66
|
+
label: "Figma Design Context",
|
|
67
|
+
description: "Fetch compact LLM-ready Figma context. With nodeId returns target node summary, ancestors/page, and sibling names; without nodeId returns canvases and top-level frames only.",
|
|
68
|
+
promptSnippet: "Explore compact Figma file structure and a target node summary without full raw JSON.",
|
|
69
|
+
promptGuidelines: [
|
|
70
|
+
"Use figma_configure_auth only when Figma auth is missing, invalid, expired, or the user asks to update the token; never ask the user to paste tokens in chat.",
|
|
71
|
+
"Use figma_parse_url, figma_render_nodes, and figma_explain_node or figma_get_node_summary as the default workflow.",
|
|
72
|
+
"Use figma_get_implementation_context when translating a design into code.",
|
|
73
|
+
"Do not call figma_get_nodes by default; use it only when raw Figma JSON is explicitly needed or when debugging the extension.",
|
|
74
|
+
"Use figma_render_nodes when screenshots or visual assets are needed.",
|
|
75
|
+
],
|
|
76
|
+
parameters: FigmaGetDesignContextParams,
|
|
77
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
78
|
+
const result = await withFigmaAuth(ctx, () => client.getDesignContext(params.fileKey, params.nodeId));
|
|
79
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.registerTool({
|
|
84
|
+
name: "figma_get_node_summary",
|
|
85
|
+
label: "Figma Node Summary",
|
|
86
|
+
description: "Fetch a compact structured summary of a Figma node: dimensions, layout, spacing, styles, visible text, component properties, and shallow child hierarchy. Default depth is 2; hidden nodes, vectors, and component internals are omitted by default.",
|
|
87
|
+
promptSnippet: "Get LLM-ready structured summaries of Figma frames/components.",
|
|
88
|
+
parameters: FigmaProcessedNodeParams,
|
|
89
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
90
|
+
const result = await withFigmaAuth(ctx, () => client.getNodeSummary(params.fileKey, params.nodeId, processedOptions(params)));
|
|
91
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pi.registerTool({
|
|
96
|
+
name: "figma_extract_text",
|
|
97
|
+
label: "Figma Extract Text",
|
|
98
|
+
description: "Extract visible text nodes from a Figma node without raw JSON. Hidden text is excluded by default and results are capped for LLM readability.",
|
|
99
|
+
promptSnippet: "Extract visible text from Figma designs.",
|
|
100
|
+
parameters: FigmaProcessedNodeParams,
|
|
101
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
102
|
+
const result = await withFigmaAuth(ctx, () => client.extractText(params.fileKey, params.nodeId, processedOptions(params)));
|
|
103
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.registerTool({
|
|
108
|
+
name: "figma_explain_node",
|
|
109
|
+
label: "Figma Explain Node",
|
|
110
|
+
description: "Explain a Figma node in human-readable Markdown using compact summary, visible text, shallow hierarchy, and optional rendered image asset. Primary tool for questions like 'Explain this component'.",
|
|
111
|
+
promptSnippet: "Explain a Figma component/frame in Markdown without raw JSON.",
|
|
112
|
+
parameters: FigmaProcessedNodeWithRenderParams,
|
|
113
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
114
|
+
const assets = params.renderImage ? await withFigmaAuth(ctx, () => renderAssets(client, ctx, params)) : undefined;
|
|
115
|
+
const result = await withFigmaAuth(ctx, () => client.explainNode(params.fileKey, params.nodeId, { ...processedOptions(params), assets }));
|
|
116
|
+
return limitedTextToolResult(result, params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
pi.registerTool({
|
|
121
|
+
name: "figma_get_implementation_context",
|
|
122
|
+
label: "Figma Implementation Context",
|
|
123
|
+
description: "Return concise design-to-code context for a Figma node: purpose, sections, fields/buttons, measurements, typography, colors, spacing, assets, and React-friendly component hierarchy.",
|
|
124
|
+
promptSnippet: "Get coding-ready Figma implementation context.",
|
|
125
|
+
parameters: FigmaProcessedNodeWithRenderParams,
|
|
126
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
127
|
+
const assets = params.renderImage ? await withFigmaAuth(ctx, () => renderAssets(client, ctx, params)) : undefined;
|
|
128
|
+
const result = await withFigmaAuth(ctx, () => client.getImplementationContext(params.fileKey, params.nodeId, { ...processedOptions(params), assets }));
|
|
129
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.registerTool({
|
|
134
|
+
name: "figma_get_file",
|
|
135
|
+
label: "Figma File",
|
|
136
|
+
description: "Fetch a raw Figma file JSON document. Use only when raw Figma JSON is explicitly needed or when debugging the extension; prefer figma_get_node_summary, figma_explain_node, or figma_get_design_context.",
|
|
137
|
+
parameters: FigmaGetFileParams,
|
|
138
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
139
|
+
const result = await withFigmaAuth(ctx, () => client.getFile(params.fileKey, params.depth));
|
|
140
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_RAW_MAX_CHARS });
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
pi.registerTool({
|
|
145
|
+
name: "figma_get_nodes",
|
|
146
|
+
label: "Figma Nodes",
|
|
147
|
+
description: "Fetch raw Figma JSON for one or more nodes/frames/components by node ID. Use only when raw Figma JSON is explicitly needed or when debugging the extension; do not use by default.",
|
|
148
|
+
parameters: FigmaGetNodesParams,
|
|
149
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
150
|
+
const result = await withFigmaAuth(ctx, () => client.getNodes(params.fileKey, params.nodeIds));
|
|
151
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_RAW_MAX_CHARS });
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
pi.registerTool({
|
|
156
|
+
name: "figma_get_node_metadata",
|
|
157
|
+
label: "Figma Node Metadata",
|
|
158
|
+
description: "Fetch compact spatial/layout metadata for one or more Figma nodes.",
|
|
159
|
+
parameters: FigmaGetNodesParams,
|
|
160
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
161
|
+
const result = await withFigmaAuth(ctx, () => client.getNodeMetadata(params.fileKey, params.nodeIds));
|
|
162
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
pi.registerTool({
|
|
167
|
+
name: "figma_get_styles",
|
|
168
|
+
label: "Figma Styles",
|
|
169
|
+
description: "Fetch named styles from a Figma file, including colors, text, effects, and grids.",
|
|
170
|
+
parameters: FigmaSingleFileParams,
|
|
171
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
172
|
+
const result = await withFigmaAuth(ctx, () => client.getStyles(params.fileKey));
|
|
173
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
pi.registerTool({
|
|
178
|
+
name: "figma_get_variables",
|
|
179
|
+
label: "Figma Variables",
|
|
180
|
+
description: "Fetch local Figma variables and collections for design tokens.",
|
|
181
|
+
parameters: FigmaSingleFileParams,
|
|
182
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
183
|
+
const result = await withFigmaAuth(ctx, () => client.getVariables(params.fileKey));
|
|
184
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.registerTool({
|
|
189
|
+
name: "figma_get_components",
|
|
190
|
+
label: "Figma Components",
|
|
191
|
+
description: "Fetch Figma component metadata for a file.",
|
|
192
|
+
parameters: FigmaSingleFileParams,
|
|
193
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
194
|
+
const result = await withFigmaAuth(ctx, () => client.getComponents(params.fileKey));
|
|
195
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
pi.registerTool({
|
|
200
|
+
name: "figma_get_component_sets",
|
|
201
|
+
label: "Figma Component Sets",
|
|
202
|
+
description: "Fetch Figma component set metadata for a file.",
|
|
203
|
+
parameters: FigmaSingleFileParams,
|
|
204
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
205
|
+
const result = await withFigmaAuth(ctx, () => client.getComponentSets(params.fileKey));
|
|
206
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
pi.registerTool({
|
|
211
|
+
name: "figma_search_components",
|
|
212
|
+
label: "Figma Search Components",
|
|
213
|
+
description: "Search Figma components in a file by name or description.",
|
|
214
|
+
parameters: FigmaSearchComponentsParams,
|
|
215
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
216
|
+
const result = await withFigmaAuth(ctx, () => client.searchComponents(params.fileKey, params.query));
|
|
217
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pi.registerTool({
|
|
222
|
+
name: "figma_render_nodes",
|
|
223
|
+
label: "Figma Render Nodes",
|
|
224
|
+
description: "Render one or more Figma nodes to image URLs and optionally download them as local assets.",
|
|
225
|
+
parameters: FigmaRenderNodesParams,
|
|
226
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
227
|
+
const result = await withFigmaAuth(ctx, () =>
|
|
228
|
+
client.renderNodes(params.fileKey, params.nodeIds, {
|
|
229
|
+
cwd: ctx.cwd,
|
|
230
|
+
outputDir: params.outputDir,
|
|
231
|
+
format: params.format,
|
|
232
|
+
scale: params.scale,
|
|
233
|
+
download: params.download,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function processedOptions(params: ProcessedNodeParams) {
|
|
242
|
+
return {
|
|
243
|
+
depth: params.depth,
|
|
244
|
+
includeHidden: params.includeHidden,
|
|
245
|
+
includeVectors: params.includeVectors,
|
|
246
|
+
includeComponentInternals: params.includeComponentInternals,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function renderAssets(client: FigmaClient, ctx: ExtensionContext, params: ProcessedNodeParams): Promise<Array<{ nodeId: string; url?: string | null; path?: string }>> {
|
|
251
|
+
const rendered = await client.renderNodes(params.fileKey, [params.nodeId], {
|
|
252
|
+
cwd: ctx.cwd,
|
|
253
|
+
outputDir: params.outputDir,
|
|
254
|
+
format: params.format,
|
|
255
|
+
scale: params.scale,
|
|
256
|
+
download: true,
|
|
257
|
+
});
|
|
258
|
+
return Object.entries(rendered.images).map(([nodeId, url]) => ({
|
|
259
|
+
nodeId,
|
|
260
|
+
url,
|
|
261
|
+
path: rendered.savedFiles.find((file) => file.nodeId === nodeId)?.path,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function limitedTextToolResult(text: string, maxChars: number) {
|
|
266
|
+
const truncated = text.length > maxChars;
|
|
267
|
+
const output = truncated ? `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters; call figma_get_node_summary on a narrower child node]` : text;
|
|
268
|
+
return textToolResult(output, { truncated, characters: text.length });
|
|
269
|
+
}
|