figmanage 1.3.0 → 1.3.2

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 (56) hide show
  1. package/dist/cli/analytics.js +3 -2
  2. package/dist/cli/branching.js +9 -3
  3. package/dist/cli/comments.js +10 -4
  4. package/dist/cli/components.js +21 -4
  5. package/dist/cli/compound-commands.js +13 -12
  6. package/dist/cli/export.js +3 -2
  7. package/dist/cli/files.js +14 -8
  8. package/dist/cli/helpers.d.ts +1 -0
  9. package/dist/cli/helpers.js +10 -0
  10. package/dist/cli/libraries.js +2 -1
  11. package/dist/cli/navigate.js +11 -10
  12. package/dist/cli/org.js +13 -12
  13. package/dist/cli/permissions.js +13 -7
  14. package/dist/cli/projects.js +12 -6
  15. package/dist/cli/reading.js +3 -2
  16. package/dist/cli/teams.js +9 -3
  17. package/dist/cli/variables.js +29 -7
  18. package/dist/cli/versions.js +3 -2
  19. package/dist/cli/webhooks.js +14 -4
  20. package/dist/helpers.d.ts +11 -0
  21. package/dist/helpers.js +41 -0
  22. package/dist/operations/analytics.js +1 -1
  23. package/dist/operations/components.d.ts +8 -2
  24. package/dist/operations/components.js +4 -2
  25. package/dist/operations/compound-manager.js +8 -9
  26. package/dist/operations/compound.d.ts +3 -0
  27. package/dist/operations/compound.js +14 -8
  28. package/dist/operations/files.js +1 -1
  29. package/dist/operations/libraries.js +1 -1
  30. package/dist/operations/org.js +1 -1
  31. package/dist/operations/reading.js +2 -0
  32. package/dist/operations/teams.js +1 -1
  33. package/dist/operations/variables.js +11 -0
  34. package/dist/operations/webhooks.d.ts +4 -1
  35. package/dist/operations/webhooks.js +2 -1
  36. package/dist/tools/analytics.js +6 -5
  37. package/dist/tools/branching.js +7 -6
  38. package/dist/tools/comments.js +14 -9
  39. package/dist/tools/components.js +24 -15
  40. package/dist/tools/compound-manager.js +10 -7
  41. package/dist/tools/compound.js +34 -22
  42. package/dist/tools/export.js +6 -5
  43. package/dist/tools/files.js +13 -12
  44. package/dist/tools/libraries.js +6 -3
  45. package/dist/tools/navigate.js +29 -18
  46. package/dist/tools/org.js +25 -24
  47. package/dist/tools/permissions.js +14 -11
  48. package/dist/tools/projects.js +10 -9
  49. package/dist/tools/reading.js +11 -7
  50. package/dist/tools/register.d.ts +8 -2
  51. package/dist/tools/register.js +9 -14
  52. package/dist/tools/teams.js +7 -6
  53. package/dist/tools/variables.js +12 -8
  54. package/dist/tools/versions.js +6 -5
  55. package/dist/tools/webhooks.js +15 -12
  56. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { internalClient } from '../clients/internal-api.js';
2
- import { resolveOrgId } from '../tools/register.js';
2
+ import { resolveOrgId } from '../helpers.js';
3
3
  export async function createFile(config, params) {
4
4
  const res = await internalClient(config).post('/api/files/create', {
5
5
  folder_id: params.project_id,
@@ -1,5 +1,5 @@
1
1
  import { internalClient } from '../clients/internal-api.js';
2
- import { requireOrgId } from '../tools/register.js';
2
+ import { requireOrgId } from '../helpers.js';
3
3
  export async function listOrgLibraries(config, params) {
4
4
  const orgId = requireOrgId(config, params.org_id);
5
5
  const res = await internalClient(config).get('/api/design_systems/libraries', {
@@ -1,5 +1,5 @@
1
1
  import { internalClient } from '../clients/internal-api.js';
2
- import { requireOrgId } from '../tools/register.js';
2
+ import { requireOrgId } from '../helpers.js';
3
3
  // -- Seat constants --
4
4
  export const SEAT_HIERARCHY = {
5
5
  view: 0,
@@ -1,4 +1,6 @@
1
1
  import { publicClient } from '../clients/public-api.js';
2
+ // WARNING: Response can be very large for complex files. Callers should
3
+ // encourage use of depth and node_id params to limit payload size.
2
4
  export async function getFile(config, params) {
3
5
  const queryParams = {};
4
6
  if (params.depth !== undefined)
@@ -1,5 +1,5 @@
1
1
  import { internalClient } from '../clients/internal-api.js';
2
- import { requireOrgId } from '../tools/register.js';
2
+ import { requireOrgId } from '../helpers.js';
3
3
  export async function createTeam(config, params) {
4
4
  const orgId = requireOrgId(config, params.org_id);
5
5
  const res = await internalClient(config).post('/api/teams/create', { team_name: params.name, org_id: orgId, sharing_audience_control: 'org_view' });
@@ -24,6 +24,17 @@ export async function updateVariables(config, params) {
24
24
  !params.variable_mode_values?.length) {
25
25
  throw new Error('At least one operation array is required.');
26
26
  }
27
+ const allOps = [
28
+ ...(params.variable_collections || []),
29
+ ...(params.variable_modes || []),
30
+ ...(params.variables || []),
31
+ ...(params.variable_mode_values || []),
32
+ ];
33
+ for (const op of allOps) {
34
+ if (!op.action || !['CREATE', 'UPDATE', 'DELETE'].includes(op.action)) {
35
+ throw new Error(`Each operation must have an action field (CREATE, UPDATE, or DELETE). Got: ${JSON.stringify(op.action)}`);
36
+ }
37
+ }
27
38
  const body = {};
28
39
  if (params.variable_collections)
29
40
  body.variableCollections = params.variable_collections;
@@ -3,7 +3,10 @@ export type WebhookEventType = 'FILE_UPDATE' | 'FILE_DELETE' | 'FILE_VERSION_UPD
3
3
  export type WebhookStatus = 'ACTIVE' | 'PAUSED';
4
4
  export declare function listWebhooks(config: AuthConfig, params: {
5
5
  team_id: string;
6
- }): Promise<any[]>;
6
+ }): Promise<{
7
+ count: number;
8
+ webhooks: any[];
9
+ }>;
7
10
  export declare function createWebhook(config: AuthConfig, params: {
8
11
  team_id: string;
9
12
  event_type: WebhookEventType;
@@ -1,7 +1,8 @@
1
1
  import { publicClient } from '../clients/public-api.js';
2
2
  export async function listWebhooks(config, params) {
3
3
  const res = await publicClient(config).get(`/v2/teams/${params.team_id}/webhooks`);
4
- return res.data?.webhooks || [];
4
+ const webhooks = res.data?.webhooks || [];
5
+ return { count: webhooks.length, webhooks };
5
6
  }
6
7
  export async function createWebhook(config, params) {
7
8
  const body = {
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { libraryUsage, componentUsage } from '../operations/analytics.js';
4
5
  // -- library_usage --
5
6
  defineTool({
@@ -15,10 +16,10 @@ defineTool({
15
16
  }, async ({ library_file_key, days }) => {
16
17
  try {
17
18
  const result = await libraryUsage(config, { library_file_key, days });
18
- return toolResult(JSON.stringify(result, null, 2));
19
+ return toolSummary('Library usage:', result);
19
20
  }
20
21
  catch (e) {
21
- return toolError(`Failed to fetch library usage: ${e.response?.status || e.message}`);
22
+ return toolError(`Failed to fetch library usage: ${formatApiError(e)}`);
22
23
  }
23
24
  });
24
25
  },
@@ -37,10 +38,10 @@ defineTool({
37
38
  }, async ({ component_key, org_id }) => {
38
39
  try {
39
40
  const result = await componentUsage(config, { component_key, org_id });
40
- return toolResult(JSON.stringify(result, null, 2));
41
+ return toolSummary('Component usage:', result);
41
42
  }
42
43
  catch (e) {
43
- return toolError(`Failed to fetch component usage: ${e.response?.status || e.message}`);
44
+ return toolError(`Failed to fetch component usage: ${formatApiError(e)}`);
44
45
  }
45
46
  });
46
47
  },
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolResult, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { listBranches, createBranch, deleteBranch } from '../operations/branching.js';
4
5
  // -- list_branches --
5
6
  defineTool({
@@ -16,10 +17,10 @@ defineTool({
16
17
  const result = await listBranches(config, { file_key });
17
18
  if (result.length === 0)
18
19
  return toolResult('No branches found.');
19
- return toolResult(JSON.stringify(result, null, 2));
20
+ return toolSummary(`${result.length} branch(es).`, result, 'Use create_branch to add a branch, or delete_branch to archive one.');
20
21
  }
21
22
  catch (e) {
22
- return toolError(`Failed to list branches: ${e.response?.status || e.message}`);
23
+ return toolError(`Failed to list branches: ${formatApiError(e)}`);
23
24
  }
24
25
  });
25
26
  },
@@ -39,10 +40,10 @@ defineTool({
39
40
  }, async ({ file_key, name }) => {
40
41
  try {
41
42
  const result = await createBranch(config, { file_key, name });
42
- return toolResult(JSON.stringify(result, null, 2));
43
+ return toolSummary('Created branch.', result, 'Use get_file with the branch file key to read the branch.');
43
44
  }
44
45
  catch (e) {
45
- return toolError(`Failed to create branch: ${e.response?.status || e.message}`);
46
+ return toolError(`Failed to create branch: ${formatApiError(e)}`);
46
47
  }
47
48
  });
48
49
  },
@@ -66,7 +67,7 @@ defineTool({
66
67
  return toolResult(`Archived branch ${branch_key}`);
67
68
  }
68
69
  catch (e) {
69
- return toolError(`Failed to delete branch: ${e.response?.status || e.message}`);
70
+ return toolError(`Failed to delete branch: ${formatApiError(e)}`);
70
71
  }
71
72
  });
72
73
  },
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolResult, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { listComments, formatCommentsAsMarkdown, postComment, deleteComment, listCommentReactions, } from '../operations/comments.js';
4
5
  // -- list_comments --
5
6
  defineTool({
@@ -18,10 +19,12 @@ defineTool({
18
19
  if (as_md) {
19
20
  return toolResult(formatCommentsAsMarkdown(comments));
20
21
  }
21
- return toolResult(JSON.stringify(comments, null, 2));
22
+ if (comments.length === 0)
23
+ return toolResult('No comments on this file.');
24
+ return toolSummary(`${comments.length} comment(s).`, comments, 'Use post_comment to reply, or delete_comment to remove.');
22
25
  }
23
26
  catch (e) {
24
- return toolError(`Failed to list comments: ${e.response?.status || e.message}`);
27
+ return toolError(`Failed to list comments: ${formatApiError(e)}`);
25
28
  }
26
29
  });
27
30
  },
@@ -43,10 +46,10 @@ defineTool({
43
46
  }, async ({ file_key, message, comment_id, node_id }) => {
44
47
  try {
45
48
  const result = await postComment(config, { file_key, message, comment_id, node_id });
46
- return toolResult(JSON.stringify(result, null, 2));
49
+ return toolSummary('Posted comment.', result);
47
50
  }
48
51
  catch (e) {
49
- return toolError(`Failed to post comment: ${e.response?.status || e.message}`);
52
+ return toolError(`Failed to post comment: ${formatApiError(e)}`);
50
53
  }
51
54
  });
52
55
  },
@@ -59,7 +62,7 @@ defineTool({
59
62
  destructive: true,
60
63
  register(server, config) {
61
64
  server.registerTool('delete_comment', {
62
- description: 'Permanently delete a comment. For top-level comments, the entire thread is removed.',
65
+ description: 'Permanently delete a comment. For top-level comments, the entire thread is removed. Cannot be undone.',
63
66
  inputSchema: {
64
67
  file_key: figmaId.describe('File key'),
65
68
  comment_id: figmaId.describe('Comment ID to delete'),
@@ -70,7 +73,7 @@ defineTool({
70
73
  return toolResult(`Deleted comment ${comment_id}`);
71
74
  }
72
75
  catch (e) {
73
- return toolError(`Failed to delete comment: ${e.response?.status || e.message}`);
76
+ return toolError(`Failed to delete comment: ${formatApiError(e)}`);
74
77
  }
75
78
  });
76
79
  },
@@ -89,10 +92,12 @@ defineTool({
89
92
  }, async ({ file_key, comment_id }) => {
90
93
  try {
91
94
  const reactions = await listCommentReactions(config, { file_key, comment_id });
92
- return toolResult(JSON.stringify(reactions, null, 2));
95
+ if (reactions.length === 0)
96
+ return toolResult('No reactions on this comment.');
97
+ return toolSummary(`${reactions.length} reaction(s).`, reactions);
93
98
  }
94
99
  catch (e) {
95
- return toolError(`Failed to list reactions: ${e.response?.status || e.message}`);
100
+ return toolError(`Failed to list reactions: ${formatApiError(e)}`);
96
101
  }
97
102
  });
98
103
  },
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolResult, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { listFileComponents, listFileStyles, listTeamComponents, listTeamStyles, } from '../operations/components.js';
4
5
  // -- list_file_components --
5
6
  defineTool({
@@ -7,17 +8,19 @@ defineTool({
7
8
  auth: 'pat',
8
9
  register(server, config) {
9
10
  server.registerTool('list_file_components', {
10
- description: 'List components published from a specific file.',
11
+ description: 'List components published from a file. Only published library components appear -- local unpublished components are not included.',
11
12
  inputSchema: {
12
13
  file_key: figmaId.describe('File key'),
13
14
  },
14
15
  }, async ({ file_key }) => {
15
16
  try {
16
- const components = await listFileComponents(config, { file_key });
17
- return toolResult(JSON.stringify(components, null, 2));
17
+ const result = await listFileComponents(config, { file_key });
18
+ if (result.count === 0)
19
+ return toolResult('No published components in this file. Components must be published to appear.');
20
+ return toolSummary(`${result.count} published component(s).`, result);
18
21
  }
19
22
  catch (e) {
20
- return toolError(`Failed to list file components: ${e.response?.status || e.message}`);
23
+ return toolError(`Failed to list file components: ${formatApiError(e)}`);
21
24
  }
22
25
  });
23
26
  },
@@ -28,17 +31,19 @@ defineTool({
28
31
  auth: 'pat',
29
32
  register(server, config) {
30
33
  server.registerTool('list_file_styles', {
31
- description: 'List styles in a specific file.',
34
+ description: 'List styles published from a file. Only published library styles appear.',
32
35
  inputSchema: {
33
36
  file_key: figmaId.describe('File key'),
34
37
  },
35
38
  }, async ({ file_key }) => {
36
39
  try {
37
- const styles = await listFileStyles(config, { file_key });
38
- return toolResult(JSON.stringify(styles, null, 2));
40
+ const result = await listFileStyles(config, { file_key });
41
+ if (result.count === 0)
42
+ return toolResult('No published styles in this file.');
43
+ return toolSummary(`${result.count} published style(s).`, result);
39
44
  }
40
45
  catch (e) {
41
- return toolError(`Failed to list file styles: ${e.response?.status || e.message}`);
46
+ return toolError(`Failed to list file styles: ${formatApiError(e)}`);
42
47
  }
43
48
  });
44
49
  },
@@ -49,7 +54,7 @@ defineTool({
49
54
  auth: 'pat',
50
55
  register(server, config) {
51
56
  server.registerTool('list_team_components', {
52
- description: 'List published components across a team. Supports pagination.',
57
+ description: 'List published components across a team. Pass the cursor from the response\'s pagination object to fetch the next page.',
53
58
  inputSchema: {
54
59
  team_id: figmaId.describe('Team ID'),
55
60
  page_size: z.number().optional().default(30).describe('Max items per page (default: 30)'),
@@ -58,10 +63,12 @@ defineTool({
58
63
  }, async ({ team_id, page_size, cursor }) => {
59
64
  try {
60
65
  const result = await listTeamComponents(config, { team_id, page_size, cursor });
61
- return toolResult(JSON.stringify(result, null, 2));
66
+ const count = result.components?.length || 0;
67
+ const hasMore = !!result.pagination?.cursor;
68
+ return toolSummary(`${count} component(s)${hasMore ? ' (more pages available)' : ''}.`, result, hasMore ? 'Pass the cursor from pagination to fetch the next page.' : undefined);
62
69
  }
63
70
  catch (e) {
64
- return toolError(`Failed to list team components: ${e.response?.status || e.message}`);
71
+ return toolError(`Failed to list team components: ${formatApiError(e)}`);
65
72
  }
66
73
  });
67
74
  },
@@ -72,7 +79,7 @@ defineTool({
72
79
  auth: 'pat',
73
80
  register(server, config) {
74
81
  server.registerTool('list_team_styles', {
75
- description: 'List published styles across a team. Supports pagination.',
82
+ description: 'List published styles across a team. Pass the cursor from the response\'s pagination object to fetch the next page.',
76
83
  inputSchema: {
77
84
  team_id: figmaId.describe('Team ID'),
78
85
  page_size: z.number().optional().default(30).describe('Max items per page (default: 30)'),
@@ -81,10 +88,12 @@ defineTool({
81
88
  }, async ({ team_id, page_size, cursor }) => {
82
89
  try {
83
90
  const result = await listTeamStyles(config, { team_id, page_size, cursor });
84
- return toolResult(JSON.stringify(result, null, 2));
91
+ const count = result.styles?.length || 0;
92
+ const hasMore = !!result.pagination?.cursor;
93
+ return toolSummary(`${count} style(s)${hasMore ? ' (more pages available)' : ''}.`, result, hasMore ? 'Pass the cursor from pagination to fetch the next page.' : undefined);
85
94
  }
86
95
  catch (e) {
87
- return toolError(`Failed to list team styles: ${e.response?.status || e.message}`);
96
+ return toolError(`Failed to list team styles: ${formatApiError(e)}`);
88
97
  }
89
98
  });
90
99
  },
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { offboardUser, onboardUser, quarterlyDesignOpsReport, } from '../operations/compound-manager.js';
4
5
  // -- offboard_user --
5
6
  defineTool({
@@ -24,10 +25,11 @@ defineTool({
24
25
  transfer_to,
25
26
  org_id,
26
27
  });
27
- return toolResult(JSON.stringify(result, null, 2));
28
+ const mode = (execute ?? false) ? 'Offboarding complete.' : 'Offboarding audit (read-only). Set execute=true to proceed.';
29
+ return toolSummary(mode, result);
28
30
  }
29
31
  catch (e) {
30
- return toolError(`Failed to audit user for offboarding: ${e.response?.status || e.message}`);
32
+ return toolError(`Failed to audit user for offboarding: ${formatApiError(e)}`);
31
33
  }
32
34
  });
33
35
  },
@@ -60,10 +62,10 @@ defineTool({
60
62
  confirm,
61
63
  org_id,
62
64
  });
63
- return toolResult(JSON.stringify(result, null, 2));
65
+ return toolSummary(`Onboarding complete for ${email}.`, result);
64
66
  }
65
67
  catch (e) {
66
- return toolError(`Failed to onboard user: ${e.response?.status || e.message}`);
68
+ return toolError(`Failed to onboard user: ${formatApiError(e)}`);
67
69
  }
68
70
  });
69
71
  },
@@ -85,10 +87,11 @@ defineTool({
85
87
  org_id,
86
88
  days: days ?? 90,
87
89
  });
88
- return toolResult(JSON.stringify(result, null, 2));
90
+ const highlightText = result.highlights?.join(' ') || 'Report generated.';
91
+ return toolSummary(highlightText, result);
89
92
  }
90
93
  catch (e) {
91
- return toolError(`Failed to generate design ops report: ${e.response?.status || e.message}`);
94
+ return toolError(`Failed to generate design ops report: ${formatApiError(e)}`);
92
95
  }
93
96
  });
94
97
  },
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { fileSummary, workspaceOverview, openComments, cleanupStaleFiles, organizeProject, setupProjectStructure, seatOptimization, permissionAudit, branchCleanup, } from '../operations/compound.js';
4
5
  // -- file_summary --
5
6
  defineTool({
@@ -14,10 +15,10 @@ defineTool({
14
15
  }, async ({ file_key }) => {
15
16
  try {
16
17
  const result = await fileSummary(config, { file_key });
17
- return toolResult(JSON.stringify(result, null, 2));
18
+ return toolSummary(`${result.name}: ${result.pages?.length || 0} pages, ${result.component_count} components, ${result.unresolved_comment_count} unresolved comments.`, result);
18
19
  }
19
20
  catch (e) {
20
- return toolError(`Failed to summarize file: ${e.response?.status || e.message}`);
21
+ return toolError(`Failed to summarize file: ${formatApiError(e)}`);
21
22
  }
22
23
  });
23
24
  },
@@ -35,10 +36,11 @@ defineTool({
35
36
  }, async ({ org_id }) => {
36
37
  try {
37
38
  const result = await workspaceOverview(config, { org_id });
38
- return toolResult(JSON.stringify(result, null, 2));
39
+ const teamCount = result.teams?.length || 0;
40
+ return toolSummary(`${teamCount} team(s). Seats and billing included.`, result);
39
41
  }
40
42
  catch (e) {
41
- return toolError(`Failed to fetch workspace overview: ${e.response?.status || e.message}`);
43
+ return toolError(`Failed to fetch workspace overview: ${formatApiError(e)}`);
42
44
  }
43
45
  });
44
46
  },
@@ -56,10 +58,12 @@ defineTool({
56
58
  }, async ({ project_id }) => {
57
59
  try {
58
60
  const result = await openComments(config, { project_id });
59
- return toolResult(JSON.stringify(result, null, 2));
61
+ const total = result.total_unresolved || 0;
62
+ const fileCount = result.files?.length || 0;
63
+ return toolSummary(`${total} unresolved comment(s) across ${fileCount} file(s).`, result, 'Use post_comment with file_key and parent_id to reply.');
60
64
  }
61
65
  catch (e) {
62
- return toolError(`Failed to fetch open comments: ${e.response?.status || e.message}`);
66
+ return toolError(`Failed to fetch open comments: ${formatApiError(e)}`);
63
67
  }
64
68
  });
65
69
  },
@@ -72,7 +76,7 @@ defineTool({
72
76
  destructive: true,
73
77
  register(server, config) {
74
78
  server.registerTool('cleanup_stale_files', {
75
- description: 'Find files not modified in N days and optionally trash them. Defaults to dry run.',
79
+ description: 'Find files not modified in N days and optionally trash them. dry_run=true (default) previews which files would be trashed without trashing them. Set dry_run=false to execute.',
76
80
  inputSchema: {
77
81
  project_id: figmaId.describe('Project ID'),
78
82
  days_stale: z.number().optional().default(90).describe('Days since last modification (default: 90)'),
@@ -85,10 +89,12 @@ defineTool({
85
89
  days_stale: days_stale ?? 90,
86
90
  dry_run: rawDryRun ?? true,
87
91
  });
88
- return toolResult(JSON.stringify(result, null, 2));
92
+ const count = result.stale_files?.length || 0;
93
+ const action = result.dry_run ? `${count} stale file(s) found. Set dry_run=false to trash them.` : `Trashed ${result.trashed_count || 0} file(s).`;
94
+ return toolSummary(action, result);
89
95
  }
90
96
  catch (e) {
91
- return toolError(`Failed to cleanup stale files: ${e.response?.status || e.message}`);
97
+ return toolError(`Failed to cleanup stale files: ${formatApiError(e)}`);
92
98
  }
93
99
  });
94
100
  },
@@ -100,7 +106,7 @@ defineTool({
100
106
  mutates: true,
101
107
  register(server, config) {
102
108
  server.registerTool('organize_project', {
103
- description: 'Move multiple files into a target project in a single batch.',
109
+ description: 'Move files into a target project in a single batch. Files are moved (not copied) from their current project.',
104
110
  inputSchema: {
105
111
  file_keys: z.array(figmaId).min(1).describe('File keys to move'),
106
112
  target_project_id: figmaId.describe('Destination project ID'),
@@ -108,10 +114,10 @@ defineTool({
108
114
  }, async ({ file_keys, target_project_id }) => {
109
115
  try {
110
116
  const result = await organizeProject(config, { file_keys, target_project_id });
111
- return toolResult(JSON.stringify(result, null, 2));
117
+ return toolSummary(`Moved ${result.moved} file(s) to project ${result.target_project_id}.`, result);
112
118
  }
113
119
  catch (e) {
114
- return toolError(`Failed to organize project: ${e.response?.status || e.message}`);
120
+ return toolError(`Failed to organize project: ${formatApiError(e)}`);
115
121
  }
116
122
  });
117
123
  },
@@ -134,10 +140,10 @@ defineTool({
134
140
  }, async ({ team_id, projects }) => {
135
141
  try {
136
142
  const result = await setupProjectStructure(config, { team_id, projects });
137
- return toolResult(JSON.stringify(result, null, 2));
143
+ return toolSummary(`Created ${result.created?.length || 0} project(s).`, result);
138
144
  }
139
145
  catch (e) {
140
- return toolError(`Failed to setup project structure: ${e.response?.status || e.message}`);
146
+ return toolError(`Failed to setup project structure: ${formatApiError(e)}`);
141
147
  }
142
148
  });
143
149
  },
@@ -161,10 +167,13 @@ defineTool({
161
167
  days_inactive: days_inactive ?? 90,
162
168
  include_cost: include_cost ?? true,
163
169
  });
164
- return toolResult(JSON.stringify(result, null, 2));
170
+ const inactive = result.summary?.inactive_paid || 0;
171
+ const savingsCents = result.summary?.monthly_waste_cents || 0;
172
+ const savingsStr = savingsCents > 0 ? ` Potential savings: $${(savingsCents / 100).toFixed(2)}/mo.` : '';
173
+ return toolSummary(`${inactive} inactive paid seat(s).${savingsStr}`, result, 'Use change_seat to downgrade inactive users, or offboard_user for full removal.');
165
174
  }
166
175
  catch (e) {
167
- return toolError(`Failed to analyze seat optimization: ${e.response?.status || e.message}`);
176
+ return toolError(`Failed to analyze seat optimization: ${formatApiError(e)}`);
168
177
  }
169
178
  });
170
179
  },
@@ -190,10 +199,11 @@ defineTool({
190
199
  flag_external: flag_external ?? true,
191
200
  org_id,
192
201
  });
193
- return toolResult(JSON.stringify(result, null, 2));
202
+ const flagCount = result.flags?.length || 0;
203
+ return toolSummary(`${flagCount} issue(s) flagged.`, result, flagCount > 0 ? 'Use revoke_access or set_permissions to address flagged issues.' : undefined);
194
204
  }
195
205
  catch (e) {
196
- return toolError(`Failed to audit permissions: ${e.response?.status || e.message}`);
206
+ return toolError(`Failed to audit permissions: ${formatApiError(e)}`);
197
207
  }
198
208
  });
199
209
  },
@@ -206,7 +216,7 @@ defineTool({
206
216
  destructive: true,
207
217
  register(server, config) {
208
218
  server.registerTool('branch_cleanup', {
209
- description: 'Find stale branches across a project and optionally archive them. Defaults to dry run.',
219
+ description: 'Find stale branches across a project and optionally archive them. dry_run=true (default) previews which branches would be archived. Archives move branch files to trash (recoverable).',
210
220
  inputSchema: {
211
221
  project_id: figmaId.describe('Project ID'),
212
222
  days_stale: z.number().min(1).max(365).optional().default(60).describe('Days since last modification to flag as stale (default: 60)'),
@@ -219,10 +229,12 @@ defineTool({
219
229
  days_stale: days_stale ?? 60,
220
230
  dry_run: rawDryRun ?? true,
221
231
  });
222
- return toolResult(JSON.stringify(result, null, 2));
232
+ const stale = result.stale_branches?.length || 0;
233
+ const action = result.dry_run ? `${stale} stale branch(es) found. Set dry_run=false to archive.` : `Archived ${result.archived_count || 0} branch(es).`;
234
+ return toolSummary(action, result);
223
235
  }
224
236
  catch (e) {
225
- return toolError(`Failed to cleanup branches: ${e.response?.status || e.message}`);
237
+ return toolError(`Failed to cleanup branches: ${formatApiError(e)}`);
226
238
  }
227
239
  });
228
240
  },
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { defineTool, toolResult, toolError, figmaId } from './register.js';
2
+ import { defineTool, toolResult, toolError, toolSummary, figmaId } from './register.js';
3
+ import { formatApiError } from '../helpers.js';
3
4
  import { exportNodes, getImageFills } from '../operations/export.js';
4
5
  // -- export_nodes --
5
6
  defineTool({
@@ -17,10 +18,10 @@ defineTool({
17
18
  }, async ({ file_key, node_ids, format, scale }) => {
18
19
  try {
19
20
  const result = await exportNodes(config, { file_key, node_ids, format, scale });
20
- return toolResult(JSON.stringify(result, null, 2));
21
+ return toolSummary(`Exported ${result.length} node(s). URLs valid ~14 days.`, result);
21
22
  }
22
23
  catch (e) {
23
- return toolError(`Failed to export nodes: ${e.response?.status || e.message}`);
24
+ return toolError(`Failed to export nodes: ${formatApiError(e)}`);
24
25
  }
25
26
  });
26
27
  },
@@ -40,10 +41,10 @@ defineTool({
40
41
  const results = await getImageFills(config, { file_key });
41
42
  if (results.length === 0)
42
43
  return toolResult('No image fills in this file.');
43
- return toolResult(JSON.stringify(results, null, 2));
44
+ return toolSummary(`${results.length} image fill(s).`, results);
44
45
  }
45
46
  catch (e) {
46
- return toolError(`Failed to get image fills: ${e.response?.status || e.message}`);
47
+ return toolError(`Failed to get image fills: ${formatApiError(e)}`);
47
48
  }
48
49
  });
49
50
  },