figmanage 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/dist/auth/client.d.ts +15 -0
  4. package/dist/auth/client.js +29 -0
  5. package/dist/auth/health.d.ts +5 -0
  6. package/dist/auth/health.js +93 -0
  7. package/dist/clients/internal-api.d.ts +4 -0
  8. package/dist/clients/internal-api.js +50 -0
  9. package/dist/clients/public-api.d.ts +4 -0
  10. package/dist/clients/public-api.js +47 -0
  11. package/dist/index.d.ts +19 -0
  12. package/dist/index.js +100 -0
  13. package/dist/setup.d.ts +3 -0
  14. package/dist/setup.js +565 -0
  15. package/dist/tools/analytics.d.ts +2 -0
  16. package/dist/tools/analytics.js +58 -0
  17. package/dist/tools/branching.d.ts +2 -0
  18. package/dist/tools/branching.js +103 -0
  19. package/dist/tools/comments.d.ts +2 -0
  20. package/dist/tools/comments.js +147 -0
  21. package/dist/tools/components.d.ts +2 -0
  22. package/dist/tools/components.js +104 -0
  23. package/dist/tools/compound.d.ts +2 -0
  24. package/dist/tools/compound.js +334 -0
  25. package/dist/tools/export.d.ts +2 -0
  26. package/dist/tools/export.js +70 -0
  27. package/dist/tools/files.d.ts +2 -0
  28. package/dist/tools/files.js +241 -0
  29. package/dist/tools/libraries.d.ts +2 -0
  30. package/dist/tools/libraries.js +31 -0
  31. package/dist/tools/navigate.d.ts +2 -0
  32. package/dist/tools/navigate.js +436 -0
  33. package/dist/tools/org.d.ts +2 -0
  34. package/dist/tools/org.js +311 -0
  35. package/dist/tools/permissions.d.ts +2 -0
  36. package/dist/tools/permissions.js +246 -0
  37. package/dist/tools/projects.d.ts +2 -0
  38. package/dist/tools/projects.js +160 -0
  39. package/dist/tools/reading.d.ts +2 -0
  40. package/dist/tools/reading.js +60 -0
  41. package/dist/tools/register.d.ts +32 -0
  42. package/dist/tools/register.js +76 -0
  43. package/dist/tools/teams.d.ts +2 -0
  44. package/dist/tools/teams.js +81 -0
  45. package/dist/tools/variables.d.ts +2 -0
  46. package/dist/tools/variables.js +102 -0
  47. package/dist/tools/versions.d.ts +2 -0
  48. package/dist/tools/versions.js +69 -0
  49. package/dist/tools/webhooks.d.ts +2 -0
  50. package/dist/tools/webhooks.js +126 -0
  51. package/dist/types/figma.d.ts +58 -0
  52. package/dist/types/figma.js +2 -0
  53. package/package.json +55 -0
@@ -0,0 +1,334 @@
1
+ import { z } from 'zod';
2
+ import { internalClient } from '../clients/internal-api.js';
3
+ import { publicClient } from '../clients/public-api.js';
4
+ import { hasPat, hasCookie } from '../auth/client.js';
5
+ import { defineTool, toolResult, toolError, figmaId, requireOrgId } from './register.js';
6
+ // -- file_summary --
7
+ defineTool({
8
+ toolset: 'compound',
9
+ auth: 'pat',
10
+ register(server, config) {
11
+ server.registerTool('file_summary', {
12
+ description: 'Quick overview of a Figma file. Fetches pages, components, styles, and comment counts in parallel.',
13
+ inputSchema: {
14
+ file_key: figmaId.describe('File key'),
15
+ },
16
+ }, async ({ file_key }) => {
17
+ try {
18
+ const api = publicClient(config);
19
+ const [fileResult, componentsResult, stylesResult, commentsResult] = await Promise.allSettled([
20
+ api.get(`/v1/files/${file_key}`, { params: { depth: '1' } }),
21
+ api.get(`/v1/files/${file_key}/components`),
22
+ api.get(`/v1/files/${file_key}/styles`),
23
+ api.get(`/v1/files/${file_key}/comments`),
24
+ ]);
25
+ if (fileResult.status === 'rejected') {
26
+ return toolError(`Failed to fetch file: ${fileResult.reason?.response?.status || fileResult.reason?.message}`);
27
+ }
28
+ const fileData = fileResult.value.data;
29
+ const pages = (fileData.document?.children || []).map((c) => c.name);
30
+ const components = componentsResult.status === 'fulfilled' ? componentsResult.value.data?.meta?.components || [] : [];
31
+ const styles = stylesResult.status === 'fulfilled' ? stylesResult.value.data?.meta?.styles || [] : [];
32
+ const comments = commentsResult.status === 'fulfilled' ? commentsResult.value.data?.comments || [] : [];
33
+ const unresolved = comments.filter((c) => !c.resolved_at);
34
+ const summary = {
35
+ name: fileData.name,
36
+ last_modified: fileData.lastModified,
37
+ version: fileData.version,
38
+ pages,
39
+ component_count: components.length,
40
+ style_count: styles.length,
41
+ comment_count: comments.length,
42
+ unresolved_comment_count: unresolved.length,
43
+ };
44
+ return toolResult(JSON.stringify(summary, null, 2));
45
+ }
46
+ catch (e) {
47
+ return toolError(`Failed to summarize file: ${e.response?.status || e.message}`);
48
+ }
49
+ });
50
+ },
51
+ });
52
+ // -- workspace_overview --
53
+ defineTool({
54
+ toolset: 'compound',
55
+ auth: 'cookie',
56
+ register(server, config) {
57
+ server.registerTool('workspace_overview', {
58
+ description: 'Full org snapshot: teams with member/project counts, seat breakdown, and billing summary.',
59
+ inputSchema: {
60
+ org_id: figmaId.optional().describe('Org ID override (defaults to current workspace)'),
61
+ },
62
+ }, async ({ org_id }) => {
63
+ try {
64
+ let orgId;
65
+ try {
66
+ orgId = requireOrgId(config, org_id);
67
+ }
68
+ catch (e) {
69
+ return toolError(e.message);
70
+ }
71
+ const api = internalClient(config);
72
+ const [teamsResult, seatsResult, billingResult] = await Promise.allSettled([
73
+ api.get(`/api/orgs/${orgId}/teams`, {
74
+ params: { include_member_count: true, include_project_count: true, include_top_members: true },
75
+ }),
76
+ api.get(`/api/orgs/${orgId}/org_users/filter_counts`),
77
+ api.get(`/api/orgs/${orgId}/billing_data`),
78
+ ]);
79
+ const teamsData = teamsResult.status === 'fulfilled' ? (teamsResult.value.data?.meta || teamsResult.value.data) : null;
80
+ const teamsRaw = teamsData ? (Array.isArray(teamsData) ? teamsData : (teamsData?.teams || [])) : [];
81
+ const teams = teamsRaw.map((t) => ({
82
+ id: String(t.id),
83
+ name: t.name,
84
+ members: t.member_count || 0,
85
+ projects: t.project_count || 0,
86
+ }));
87
+ const seats = seatsResult.status === 'fulfilled' ? (seatsResult.value.data?.meta || seatsResult.value.data) : null;
88
+ const rawBilling = billingResult.status === 'fulfilled' ? (billingResult.value.data?.meta || billingResult.value.data) : null;
89
+ // Strip PII from billing data
90
+ const billing = rawBilling ? { ...rawBilling } : null;
91
+ if (billing)
92
+ delete billing.shipping_address;
93
+ const errors = [];
94
+ if (teamsResult.status === 'rejected')
95
+ errors.push(`teams: ${teamsResult.reason?.response?.status || teamsResult.reason?.message}`);
96
+ if (seatsResult.status === 'rejected')
97
+ errors.push(`seats: ${seatsResult.reason?.response?.status || seatsResult.reason?.message}`);
98
+ if (billingResult.status === 'rejected')
99
+ errors.push(`billing: ${billingResult.reason?.response?.status || billingResult.reason?.message}`);
100
+ const overview = { teams, seats, billing };
101
+ if (errors.length > 0)
102
+ overview.errors = errors;
103
+ return toolResult(JSON.stringify(overview, null, 2));
104
+ }
105
+ catch (e) {
106
+ return toolError(`Failed to fetch workspace overview: ${e.response?.status || e.message}`);
107
+ }
108
+ });
109
+ },
110
+ });
111
+ // -- open_comments --
112
+ defineTool({
113
+ toolset: 'compound',
114
+ auth: 'pat',
115
+ register(server, config) {
116
+ server.registerTool('open_comments', {
117
+ description: 'Aggregated unresolved comments across all files in a project. Checks up to 20 files.',
118
+ inputSchema: {
119
+ project_id: figmaId.describe('Project ID'),
120
+ },
121
+ }, async ({ project_id }) => {
122
+ try {
123
+ const api = publicClient(config);
124
+ const filesRes = await api.get(`/v1/projects/${project_id}/files`);
125
+ const allFiles = filesRes.data?.files || [];
126
+ const capped = allFiles.length > 20;
127
+ const files = allFiles.slice(0, 20);
128
+ const commentResults = [];
129
+ for (const f of files) {
130
+ try {
131
+ const commentsRes = await api.get(`/v1/files/${f.key}/comments`);
132
+ const comments = (commentsRes.data?.comments || [])
133
+ .filter((c) => !c.resolved_at);
134
+ commentResults.push({ file_key: f.key, file_name: f.name, comments });
135
+ }
136
+ catch (e) {
137
+ commentResults.push({ file_key: f.key, file_name: f.name, comments: [], error: e.response?.status || e.message });
138
+ }
139
+ }
140
+ const filesWithComments = commentResults
141
+ .filter(f => f.comments.length > 0)
142
+ .map(f => ({
143
+ file_key: f.file_key,
144
+ file_name: f.file_name,
145
+ comments: f.comments.map((c) => ({
146
+ id: c.id,
147
+ author: c.user?.handle || c.user?.email || 'unknown',
148
+ message: c.message,
149
+ created_at: c.created_at,
150
+ })),
151
+ }));
152
+ const totalUnresolved = filesWithComments.reduce((sum, f) => sum + f.comments.length, 0);
153
+ const errors = commentResults
154
+ .filter((f) => f.error)
155
+ .map((f) => ({ file_key: f.file_key, file_name: f.file_name, error: f.error }));
156
+ const result = {
157
+ total_unresolved: totalUnresolved,
158
+ files: filesWithComments,
159
+ };
160
+ if (errors.length > 0) {
161
+ result.errors = errors;
162
+ }
163
+ if (capped) {
164
+ result.note = `Project has ${allFiles.length} files; only the first 20 were checked.`;
165
+ }
166
+ return toolResult(JSON.stringify(result, null, 2));
167
+ }
168
+ catch (e) {
169
+ return toolError(`Failed to fetch open comments: ${e.response?.status || e.message}`);
170
+ }
171
+ });
172
+ },
173
+ });
174
+ // -- cleanup_stale_files --
175
+ defineTool({
176
+ toolset: 'compound',
177
+ auth: 'either',
178
+ mutates: true,
179
+ destructive: true,
180
+ register(server, config) {
181
+ server.registerTool('cleanup_stale_files', {
182
+ description: 'Find files not modified in N days and optionally trash them. Defaults to dry run.',
183
+ inputSchema: {
184
+ project_id: figmaId.describe('Project ID'),
185
+ days_stale: z.number().optional().default(90).describe('Days since last modification (default: 90)'),
186
+ dry_run: z.boolean().optional().default(true).describe('Preview only, no deletion (default: true)'),
187
+ },
188
+ }, async ({ project_id, days_stale, dry_run: rawDryRun }) => {
189
+ try {
190
+ const dry_run = rawDryRun ?? true;
191
+ if (!dry_run && !hasCookie(config)) {
192
+ return toolError('Cookie auth required to trash files. Run with dry_run=true to preview, or configure cookie auth.');
193
+ }
194
+ let files;
195
+ if (hasPat(config)) {
196
+ const res = await publicClient(config).get(`/v1/projects/${project_id}/files`);
197
+ files = res.data?.files || [];
198
+ }
199
+ else {
200
+ const res = await internalClient(config).get(`/api/folders/${project_id}/paginated_files`, { params: { folderId: project_id, sort_column: 'touched_at', sort_order: 'desc', page_size: 100, file_type: '' } });
201
+ const meta = res.data?.meta || res.data;
202
+ files = meta?.files || meta || [];
203
+ }
204
+ const cutoff = Date.now() - days_stale * 86400000;
205
+ const staleFiles = files.filter((f) => {
206
+ const raw = f.last_modified || f.touched_at;
207
+ if (!raw)
208
+ return false;
209
+ const modified = new Date(raw).getTime();
210
+ return !isNaN(modified) && modified < cutoff;
211
+ });
212
+ const MAX_TRASH_BATCH = 25;
213
+ const result = {
214
+ stale_files: staleFiles.map((f) => ({
215
+ key: f.key,
216
+ name: f.name,
217
+ last_modified: f.last_modified || f.touched_at,
218
+ })),
219
+ total_stale: staleFiles.length,
220
+ dry_run,
221
+ trashed: false,
222
+ };
223
+ if (!dry_run) {
224
+ if (staleFiles.length > MAX_TRASH_BATCH) {
225
+ return toolError(`${staleFiles.length} stale files exceeds safety limit of ${MAX_TRASH_BATCH}. ` +
226
+ `Run with dry_run=true to review, then trash in smaller batches using trash_files.`);
227
+ }
228
+ if (staleFiles.length > 0) {
229
+ await internalClient(config).delete('/api/files_batch', {
230
+ data: { files: staleFiles.map((f) => ({ key: f.key })), trashed: true },
231
+ });
232
+ result.trashed = true;
233
+ }
234
+ }
235
+ return toolResult(JSON.stringify(result, null, 2));
236
+ }
237
+ catch (e) {
238
+ return toolError(`Failed to cleanup stale files: ${e.response?.status || e.message}`);
239
+ }
240
+ });
241
+ },
242
+ });
243
+ // -- organize_project --
244
+ defineTool({
245
+ toolset: 'compound',
246
+ auth: 'cookie',
247
+ mutates: true,
248
+ register(server, config) {
249
+ server.registerTool('organize_project', {
250
+ description: 'Move multiple files into a target project in a single batch.',
251
+ inputSchema: {
252
+ file_keys: z.array(figmaId).min(1).describe('File keys to move'),
253
+ target_project_id: figmaId.describe('Destination project ID'),
254
+ },
255
+ }, async ({ file_keys, target_project_id }) => {
256
+ try {
257
+ const payload = {
258
+ files: file_keys.map(key => ({
259
+ key,
260
+ folder_id: target_project_id,
261
+ is_multi_move: true,
262
+ restore_files: false,
263
+ })),
264
+ };
265
+ const res = await internalClient(config).put('/api/files_batch', payload);
266
+ const data = res.data?.meta || res.data;
267
+ const moved = Object.keys(data?.success || {}).length;
268
+ const failed = Object.keys(data?.errors || {}).length;
269
+ const result = {
270
+ moved: moved || (failed === 0 ? file_keys.length : 0),
271
+ failed,
272
+ errors: data?.errors || {},
273
+ };
274
+ return toolResult(JSON.stringify(result, null, 2));
275
+ }
276
+ catch (e) {
277
+ return toolError(`Failed to organize project: ${e.response?.status || e.message}`);
278
+ }
279
+ });
280
+ },
281
+ });
282
+ // -- setup_project_structure --
283
+ defineTool({
284
+ toolset: 'compound',
285
+ auth: 'cookie',
286
+ mutates: true,
287
+ register(server, config) {
288
+ server.registerTool('setup_project_structure', {
289
+ description: 'Create multiple projects in a team from a plan. Optionally set descriptions.',
290
+ inputSchema: {
291
+ team_id: figmaId.describe('Team ID'),
292
+ projects: z.array(z.object({
293
+ name: z.string().describe('Project name'),
294
+ description: z.string().optional().describe('Project description'),
295
+ })).min(1).describe('Projects to create'),
296
+ },
297
+ }, async ({ team_id, projects }) => {
298
+ try {
299
+ const api = internalClient(config);
300
+ const created = [];
301
+ const failed = [];
302
+ for (const project of projects) {
303
+ try {
304
+ const createRes = await api.post('/api/folders', {
305
+ team_id,
306
+ path: project.name,
307
+ sharing_audience_control: 'org_view',
308
+ team_access: 'team_edit',
309
+ });
310
+ const meta = createRes.data?.meta;
311
+ const p = Array.isArray(meta) ? meta[0] : (meta?.folder || meta || createRes.data);
312
+ const folderId = p?.id != null ? String(p.id) : null;
313
+ if (!folderId) {
314
+ failed.push({ name: project.name, error: 'Could not extract project ID from response' });
315
+ continue;
316
+ }
317
+ if (project.description) {
318
+ await api.put(`/api/folders/${folderId}`, { description: project.description });
319
+ }
320
+ created.push({ id: folderId, name: project.name, description: project.description || null });
321
+ }
322
+ catch (e) {
323
+ failed.push({ name: project.name, error: e.response?.status || e.message });
324
+ }
325
+ }
326
+ return toolResult(JSON.stringify({ created, failed }, null, 2));
327
+ }
328
+ catch (e) {
329
+ return toolError(`Failed to setup project structure: ${e.response?.status || e.message}`);
330
+ }
331
+ });
332
+ },
333
+ });
334
+ //# sourceMappingURL=compound.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ import { publicClient } from '../clients/public-api.js';
3
+ import { defineTool, toolResult, toolError, figmaId } from './register.js';
4
+ // -- export_nodes --
5
+ defineTool({
6
+ toolset: 'export',
7
+ auth: 'pat',
8
+ register(server, config) {
9
+ server.registerTool('export_nodes', {
10
+ description: 'Export specific nodes from a file as images. Returns temporary URLs (valid ~14 days).',
11
+ inputSchema: {
12
+ file_key: figmaId.describe('File key'),
13
+ node_ids: z.array(figmaId).min(1).describe('Node IDs to export (e.g. ["1:2", "3:4"])'),
14
+ format: z.enum(['png', 'svg', 'pdf', 'jpg']).optional().describe('Image format (default: png)'),
15
+ scale: z.number().optional().describe('Scale factor, 0.01-4 (default: 1)'),
16
+ },
17
+ }, async ({ file_key, node_ids, format, scale }) => {
18
+ try {
19
+ const params = {
20
+ ids: node_ids.join(','),
21
+ format: format || 'png',
22
+ };
23
+ if (scale)
24
+ params.scale = String(Math.min(Math.max(scale, 0.01), 4));
25
+ const res = await publicClient(config).get(`/v1/images/${file_key}`, { params });
26
+ const images = res.data?.images || {};
27
+ const err = res.data?.err;
28
+ if (err)
29
+ return toolError(`Export error: ${err}`);
30
+ const results = Object.entries(images).map(([nodeId, url]) => ({
31
+ node_id: nodeId,
32
+ url,
33
+ }));
34
+ return toolResult(JSON.stringify(results, null, 2));
35
+ }
36
+ catch (e) {
37
+ return toolError(`Failed to export nodes: ${e.response?.status || e.message}`);
38
+ }
39
+ });
40
+ },
41
+ });
42
+ // -- get_image_fills --
43
+ defineTool({
44
+ toolset: 'export',
45
+ auth: 'pat',
46
+ register(server, config) {
47
+ server.registerTool('get_image_fills', {
48
+ description: 'Get download URLs for all images used as fills in a file.',
49
+ inputSchema: {
50
+ file_key: figmaId.describe('File key'),
51
+ },
52
+ }, async ({ file_key }) => {
53
+ try {
54
+ const res = await publicClient(config).get(`/v1/files/${file_key}/images`);
55
+ const images = res.data?.meta?.images || res.data?.images || {};
56
+ const results = Object.entries(images).map(([ref, url]) => ({
57
+ image_ref: ref,
58
+ url,
59
+ }));
60
+ if (results.length === 0)
61
+ return toolResult('No image fills in this file.');
62
+ return toolResult(JSON.stringify(results, null, 2));
63
+ }
64
+ catch (e) {
65
+ return toolError(`Failed to get image fills: ${e.response?.status || e.message}`);
66
+ }
67
+ });
68
+ },
69
+ });
70
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1,241 @@
1
+ import { z } from 'zod';
2
+ import { internalClient } from '../clients/internal-api.js';
3
+ import { defineTool, toolResult, toolError, figmaId, resolveOrgId } from './register.js';
4
+ // -- create_file --
5
+ defineTool({
6
+ toolset: 'files',
7
+ auth: 'cookie',
8
+ mutates: true,
9
+ register(server, config) {
10
+ server.registerTool('create_file', {
11
+ description: 'Create a new Figma file in a project. Supports design, whiteboard, slides, sites.',
12
+ inputSchema: {
13
+ project_id: figmaId.describe('Project (folder) ID to create the file in'),
14
+ editor_type: z.enum(['design', 'whiteboard', 'slides', 'sites']).optional().describe('File type (default: design)'),
15
+ org_id: figmaId.optional().describe('Org ID override (defaults to current workspace)'),
16
+ },
17
+ }, async ({ project_id, editor_type, org_id }) => {
18
+ try {
19
+ const res = await internalClient(config).post('/api/files/create', {
20
+ folder_id: project_id,
21
+ org_id: resolveOrgId(config, org_id),
22
+ editor_type: editor_type || 'design',
23
+ });
24
+ const f = res.data?.meta?.fig_file || res.data?.meta || res.data;
25
+ return toolResult(JSON.stringify({
26
+ key: f.key,
27
+ name: f.name,
28
+ editor_type: f.editor_type,
29
+ url: `https://www.figma.com/design/${f.key}`,
30
+ }, null, 2));
31
+ }
32
+ catch (e) {
33
+ return toolError(`Failed to create file: ${e.response?.status || e.message}`);
34
+ }
35
+ });
36
+ },
37
+ });
38
+ // -- rename_file --
39
+ defineTool({
40
+ toolset: 'files',
41
+ auth: 'cookie',
42
+ mutates: true,
43
+ register(server, config) {
44
+ server.registerTool('rename_file', {
45
+ description: 'Rename a Figma file.',
46
+ inputSchema: {
47
+ file_key: figmaId.describe('File key'),
48
+ name: z.string().describe('New name'),
49
+ },
50
+ }, async ({ file_key, name }) => {
51
+ try {
52
+ await internalClient(config).put(`/api/files/${file_key}`, {
53
+ key: file_key,
54
+ name,
55
+ });
56
+ return toolResult(`Renamed to "${name}"`);
57
+ }
58
+ catch (e) {
59
+ return toolError(`Failed to rename file: ${e.response?.status || e.message}`);
60
+ }
61
+ });
62
+ },
63
+ });
64
+ // -- move_files --
65
+ defineTool({
66
+ toolset: 'files',
67
+ auth: 'cookie',
68
+ mutates: true,
69
+ register(server, config) {
70
+ server.registerTool('move_files', {
71
+ description: 'Move one or more files to a different project. Supports batch moves.',
72
+ inputSchema: {
73
+ file_keys: z.array(figmaId).min(1).describe('Array of file keys to move'),
74
+ destination_project_id: figmaId.describe('Destination project (folder) ID'),
75
+ },
76
+ }, async ({ file_keys, destination_project_id }) => {
77
+ try {
78
+ const files = file_keys.map(key => ({
79
+ key,
80
+ folder_id: destination_project_id,
81
+ is_multi_move: file_keys.length > 1,
82
+ restore_files: false,
83
+ }));
84
+ const res = await internalClient(config).put('/api/files_batch', { files });
85
+ const data = res.data?.meta || res.data;
86
+ const succeeded = Object.keys(data.success || {}).length;
87
+ const failed = Object.keys(data.errors || {}).length;
88
+ let msg = `Moved ${succeeded} file(s) to project ${destination_project_id}`;
89
+ if (failed > 0)
90
+ msg += `. ${failed} failed: ${JSON.stringify(data.errors)}`;
91
+ return toolResult(msg);
92
+ }
93
+ catch (e) {
94
+ return toolError(`Failed to move files: ${e.response?.status || e.message}`);
95
+ }
96
+ });
97
+ },
98
+ });
99
+ // -- duplicate_file --
100
+ defineTool({
101
+ toolset: 'files',
102
+ auth: 'cookie',
103
+ mutates: true,
104
+ register(server, config) {
105
+ server.registerTool('duplicate_file', {
106
+ description: 'Duplicate a file. Optionally specify a destination project.',
107
+ inputSchema: {
108
+ file_key: figmaId.describe('File key to duplicate'),
109
+ project_id: figmaId.optional().describe('Destination project ID (optional, defaults to same project)'),
110
+ },
111
+ }, async ({ file_key, project_id }) => {
112
+ try {
113
+ const res = await internalClient(config).post(`/api/multiplayer/${file_key}/copy`, null, {
114
+ headers: { 'Content-Length': '0' },
115
+ params: project_id ? { folder_id: project_id } : undefined,
116
+ });
117
+ const f = res.data?.meta?.fig_file || res.data?.meta || res.data;
118
+ return toolResult(JSON.stringify({
119
+ key: f.key,
120
+ name: f.name,
121
+ url: `https://www.figma.com/design/${f.key}`,
122
+ }, null, 2));
123
+ }
124
+ catch (e) {
125
+ return toolError(`Failed to duplicate file: ${e.response?.status || e.message}`);
126
+ }
127
+ });
128
+ },
129
+ });
130
+ // -- trash_files --
131
+ defineTool({
132
+ toolset: 'files',
133
+ auth: 'cookie',
134
+ mutates: true,
135
+ destructive: true,
136
+ register(server, config) {
137
+ server.registerTool('trash_files', {
138
+ description: 'Move files to trash. Supports batch operations.',
139
+ inputSchema: {
140
+ file_keys: z.array(figmaId).min(1).describe('Array of file keys to trash'),
141
+ },
142
+ }, async ({ file_keys }) => {
143
+ try {
144
+ const files = file_keys.map(key => ({ key }));
145
+ const res = await internalClient(config).delete('/api/files_batch', {
146
+ data: { files, trashed: true },
147
+ });
148
+ const data = res.data?.meta || res.data;
149
+ const succeeded = Object.keys(data.success || {}).length;
150
+ return toolResult(`Trashed ${succeeded} file(s)`);
151
+ }
152
+ catch (e) {
153
+ return toolError(`Failed to trash files: ${e.response?.status || e.message}`);
154
+ }
155
+ });
156
+ },
157
+ });
158
+ // -- restore_files --
159
+ defineTool({
160
+ toolset: 'files',
161
+ auth: 'cookie',
162
+ mutates: true,
163
+ register(server, config) {
164
+ server.registerTool('restore_files', {
165
+ description: 'Restore files from trash.',
166
+ inputSchema: {
167
+ file_keys: z.array(figmaId).min(1).describe('Array of file keys to restore'),
168
+ },
169
+ }, async ({ file_keys }) => {
170
+ try {
171
+ const files = file_keys.map(key => ({ key }));
172
+ const res = await internalClient(config).post('/api/files_batch/restore', { files });
173
+ const data = res.data?.meta || res.data;
174
+ const succeeded = Object.keys(data?.success || {}).length || file_keys.length;
175
+ const failed = Object.keys(data?.errors || {}).length;
176
+ let msg = `Restored ${succeeded} file(s)`;
177
+ if (failed > 0)
178
+ msg += `. ${failed} failed: ${JSON.stringify(data.errors)}`;
179
+ return toolResult(msg);
180
+ }
181
+ catch (e) {
182
+ return toolError(`Failed to restore files: ${e.response?.status || e.message}`);
183
+ }
184
+ });
185
+ },
186
+ });
187
+ // -- favorite_file --
188
+ defineTool({
189
+ toolset: 'files',
190
+ auth: 'cookie',
191
+ mutates: true,
192
+ register(server, config) {
193
+ server.registerTool('favorite_file', {
194
+ description: 'Add or remove a file from your sidebar favorites.',
195
+ inputSchema: {
196
+ file_key: figmaId.describe('File key'),
197
+ favorited: z.boolean().optional().describe('true to favorite, false to unfavorite (default: true)'),
198
+ },
199
+ }, async ({ file_key, favorited }) => {
200
+ try {
201
+ const isFavorited = favorited !== false;
202
+ await internalClient(config).put('/api/favorited_resources', {
203
+ resource_type: 'file',
204
+ resource_id_or_key: file_key,
205
+ is_favorited: isFavorited,
206
+ });
207
+ return toolResult(`${isFavorited ? 'Favorited' : 'Unfavorited'} file ${file_key}`);
208
+ }
209
+ catch (e) {
210
+ return toolError(`Failed to toggle favorite: ${e.response?.status || e.message}`);
211
+ }
212
+ });
213
+ },
214
+ });
215
+ // -- set_link_access --
216
+ defineTool({
217
+ toolset: 'files',
218
+ auth: 'cookie',
219
+ mutates: true,
220
+ register(server, config) {
221
+ server.registerTool('set_link_access', {
222
+ description: 'Set link access on a file. Use "inherit" to remove custom access and fall back to project/team defaults.',
223
+ inputSchema: {
224
+ file_key: figmaId.describe('File key'),
225
+ link_access: z.enum(['inherit', 'view', 'edit', 'org_view', 'org_edit']).optional().describe('Link access level (default: inherit)'),
226
+ },
227
+ }, async ({ file_key, link_access }) => {
228
+ try {
229
+ const res = await internalClient(config).put(`/api/files/${file_key}`, {
230
+ link_access: link_access || 'inherit',
231
+ });
232
+ const newAccess = res.data?.meta?.link_access || link_access || 'inherit';
233
+ return toolResult(`Set link access to "${newAccess}" on file ${file_key}`);
234
+ }
235
+ catch (e) {
236
+ return toolError(`Failed to set link access: ${e.response?.status || e.message}`);
237
+ }
238
+ });
239
+ },
240
+ });
241
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=libraries.d.ts.map