posterly-mcp-server 0.19.1 → 0.19.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 (111) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +50 -49
  3. package/dist/lib/api-client.js +1 -1
  4. package/dist/lib/format.d.ts +21 -0
  5. package/dist/lib/format.js +125 -0
  6. package/dist/tools/audit-google-business-profile.js +15 -12
  7. package/dist/tools/create-oauth-client.js +7 -1
  8. package/dist/tools/create-post.js +20 -27
  9. package/dist/tools/create-posts-batch.js +23 -15
  10. package/dist/tools/create-signed-upload.js +11 -8
  11. package/dist/tools/create-webhook.js +7 -2
  12. package/dist/tools/delete-google-business-review-reply.d.ts +1 -1
  13. package/dist/tools/delete-google-business-review-reply.js +4 -1
  14. package/dist/tools/delete-oauth-client.js +4 -1
  15. package/dist/tools/delete-post-group.js +5 -1
  16. package/dist/tools/delete-post.js +4 -1
  17. package/dist/tools/delete-webhook.js +4 -1
  18. package/dist/tools/disconnect-account.js +7 -5
  19. package/dist/tools/find-slot.js +10 -3
  20. package/dist/tools/generate-image.js +17 -19
  21. package/dist/tools/generate-video.js +11 -9
  22. package/dist/tools/get-account-analytics.js +64 -45
  23. package/dist/tools/get-brand-profile.js +34 -40
  24. package/dist/tools/get-brand.js +14 -16
  25. package/dist/tools/get-connect-link.js +26 -7
  26. package/dist/tools/get-google-business-review-link.js +6 -6
  27. package/dist/tools/get-platform-schema.js +23 -16
  28. package/dist/tools/get-post-analytics.js +53 -50
  29. package/dist/tools/get-post-missing.js +5 -1
  30. package/dist/tools/get-post.js +16 -14
  31. package/dist/tools/get-video-job.d.ts +2 -2
  32. package/dist/tools/get-video-job.js +28 -11
  33. package/dist/tools/get-video-options.js +21 -15
  34. package/dist/tools/get-x-posting-quota.js +12 -9
  35. package/dist/tools/list-accounts.js +12 -4
  36. package/dist/tools/list-activity.js +13 -18
  37. package/dist/tools/list-brand-accounts.js +11 -3
  38. package/dist/tools/list-brands.js +12 -11
  39. package/dist/tools/list-google-business-reviews.js +13 -11
  40. package/dist/tools/list-oauth-clients.js +10 -4
  41. package/dist/tools/list-platforms.js +12 -5
  42. package/dist/tools/list-posts.js +12 -7
  43. package/dist/tools/list-webhooks.js +13 -11
  44. package/dist/tools/reply-google-business-review.d.ts +1 -1
  45. package/dist/tools/reply-google-business-review.js +4 -1
  46. package/dist/tools/run-video-function.js +4 -4
  47. package/dist/tools/suggest-google-business-review-reply.js +5 -4
  48. package/dist/tools/test-webhook.js +7 -1
  49. package/dist/tools/trigger-platform-helper.js +5 -1
  50. package/dist/tools/update-oauth-client.js +8 -1
  51. package/dist/tools/update-post-release-id.js +5 -1
  52. package/dist/tools/update-post-status.d.ts +2 -2
  53. package/dist/tools/update-post-status.js +7 -1
  54. package/dist/tools/update-post.js +10 -4
  55. package/dist/tools/update-webhook.js +7 -1
  56. package/dist/tools/upload-media-from-url.js +7 -9
  57. package/dist/tools/upload-media.js +6 -2
  58. package/dist/tools/whoami.js +18 -17
  59. package/package.json +1 -1
  60. package/src/index.ts +50 -49
  61. package/src/lib/api-client.ts +1 -1
  62. package/src/lib/format.ts +132 -0
  63. package/src/tools/audit-google-business-profile.ts +18 -12
  64. package/src/tools/create-oauth-client.ts +8 -2
  65. package/src/tools/create-post.ts +20 -28
  66. package/src/tools/create-posts-batch.ts +23 -18
  67. package/src/tools/create-signed-upload.ts +11 -8
  68. package/src/tools/create-webhook.ts +7 -3
  69. package/src/tools/delete-google-business-review-reply.ts +4 -1
  70. package/src/tools/delete-oauth-client.ts +4 -1
  71. package/src/tools/delete-post-group.ts +5 -1
  72. package/src/tools/delete-post.ts +4 -1
  73. package/src/tools/delete-webhook.ts +4 -2
  74. package/src/tools/disconnect-account.ts +7 -5
  75. package/src/tools/find-slot.ts +13 -5
  76. package/src/tools/generate-image.ts +20 -20
  77. package/src/tools/generate-video.ts +11 -9
  78. package/src/tools/get-account-analytics.ts +70 -49
  79. package/src/tools/get-brand-profile.ts +34 -38
  80. package/src/tools/get-brand.ts +14 -14
  81. package/src/tools/get-connect-link.ts +29 -7
  82. package/src/tools/get-google-business-review-link.ts +6 -6
  83. package/src/tools/get-platform-schema.ts +29 -16
  84. package/src/tools/get-post-analytics.ts +58 -51
  85. package/src/tools/get-post-missing.ts +5 -2
  86. package/src/tools/get-post.ts +16 -13
  87. package/src/tools/get-video-job.ts +31 -11
  88. package/src/tools/get-video-options.ts +27 -15
  89. package/src/tools/get-x-posting-quota.ts +12 -9
  90. package/src/tools/list-accounts.ts +15 -6
  91. package/src/tools/list-activity.ts +16 -20
  92. package/src/tools/list-brand-accounts.ts +14 -6
  93. package/src/tools/list-brands.ts +16 -13
  94. package/src/tools/list-google-business-reviews.ts +16 -12
  95. package/src/tools/list-oauth-clients.ts +13 -4
  96. package/src/tools/list-platforms.ts +15 -5
  97. package/src/tools/list-posts.ts +15 -9
  98. package/src/tools/list-webhooks.ts +16 -12
  99. package/src/tools/reply-google-business-review.ts +4 -1
  100. package/src/tools/run-video-function.ts +4 -4
  101. package/src/tools/suggest-google-business-review-reply.ts +5 -4
  102. package/src/tools/test-webhook.ts +7 -1
  103. package/src/tools/trigger-platform-helper.ts +5 -1
  104. package/src/tools/update-oauth-client.ts +9 -2
  105. package/src/tools/update-post-release-id.ts +5 -2
  106. package/src/tools/update-post-status.ts +7 -1
  107. package/src/tools/update-post.ts +10 -5
  108. package/src/tools/update-webhook.ts +7 -2
  109. package/src/tools/upload-media-from-url.ts +7 -9
  110. package/src/tools/upload-media.ts +6 -2
  111. package/src/tools/whoami.ts +21 -23
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdSuccess } from '../lib/format.js';
2
3
  const scopeSchema = z.enum(['accounts:read', 'accounts:write', 'posts:read', 'posts:write', 'media:write', 'analytics:read']);
3
4
  export const createOAuthClientTool = {
4
5
  name: 'create_oauth_client',
@@ -15,6 +16,11 @@ export const createOAuthClientTool = {
15
16
  async execute(client, input) {
16
17
  const { confirm: _confirm, ...payload } = input;
17
18
  const result = await client.createOAuthClient(payload);
18
- return `OAuth client created: ${result.client.client_name}\nclient_id: ${result.client.client_id}\nRedirect URIs: ${result.client.allowed_redirect_uris.join(', ')}`;
19
+ return mdSuccess('OAuth client created', [
20
+ ['Client', result.client.client_name],
21
+ ['Client ID', code(result.client.client_id)],
22
+ ['Redirect URIs', result.client.allowed_redirect_uris.join(', ')],
23
+ ['Default scopes', result.client.default_scopes.join(', ')],
24
+ ]);
19
25
  },
20
26
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
3
+ import { code, formatDateTime, mdKeyValue, mdSuccess, statusLabel } from '../lib/format.js';
3
4
  export const instagramSettingsSchema = z.object({
4
5
  __type: z.enum(['instagram', 'instagram-standalone']).optional(),
5
6
  post_type: z.enum(['post', 'feed', 'story', 'reel', 'carousel']).optional(),
@@ -159,32 +160,24 @@ export const createPostTool = {
159
160
  const result = await client.createPost(payload);
160
161
  const p = result.post;
161
162
  const ws = result.workspace;
162
- const when = p.scheduled_at
163
- ? new Date(p.scheduled_at).toLocaleString()
164
- : 'now';
165
- const lines = [
166
- 'Post created successfully!',
167
- `• ID: ${p.id}`,
168
- `• Platform: ${p.platform || input.platform || 'unknown'}`,
169
- `• Type: ${p.post_type}`,
170
- `• Status: ${p.status}`,
171
- `• Scheduled: ${when}`,
172
- ];
173
- if (thread_posts) {
174
- lines.push(`• Thread: ${thread_posts.length} posts`);
175
- }
176
- if (instagram_settings?.collaborators?.length) {
177
- lines.push(`• Instagram collaborators: ${instagram_settings.collaborators.length}`);
178
- }
179
- if (instagram_settings?.first_comment) {
180
- lines.push('• Instagram first comment: yes');
181
- }
182
- if (platform_settings) {
183
- lines.push('• Platform settings: yes');
184
- }
185
- if (ws) {
186
- lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
187
- }
188
- return lines.join('\n');
163
+ return [
164
+ mdSuccess('Post created', [
165
+ ['Post ID', code(p.id)],
166
+ ['Platform', p.platform || input.platform || 'unknown'],
167
+ ['Type', p.post_type],
168
+ ['Status', statusLabel(p.status)],
169
+ ['Scheduled', p.scheduled_at ? formatDateTime(p.scheduled_at) : 'now'],
170
+ ['Thread', thread_posts ? `${thread_posts.length} posts` : undefined],
171
+ ['Instagram collaborators', instagram_settings?.collaborators?.length],
172
+ ['Instagram first comment', Boolean(instagram_settings?.first_comment)],
173
+ ['Platform settings', Boolean(platform_settings)],
174
+ ], 'Use `get_post` if you want to review the saved post details.'),
175
+ ws
176
+ ? mdKeyValue([
177
+ ['Workspace', `${ws.name} (${code(ws.id)})`],
178
+ ['Workspace resolved from', ws.resolved_from],
179
+ ])
180
+ : '',
181
+ ].filter(Boolean).join('\n\n');
189
182
  },
190
183
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { buildCreatePostPayload, createPostInputSchema } from './create-post.js';
3
+ import { code, formatDateTime, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
3
4
  export const createPostsBatchTool = {
4
5
  name: 'create_posts_batch',
5
6
  description: 'Create 1-25 scheduled or immediate social posts in one API request. This is a DESTRUCTIVE WRITE that creates content on the user\'s real social accounts — once scheduled_at passes, posts may go public and cannot be un-posted.\n\n' +
@@ -18,21 +19,28 @@ export const createPostsBatchTool = {
18
19
  async execute(client, input) {
19
20
  const payloads = input.posts.map((post) => buildCreatePostPayload(post));
20
21
  const result = await client.createPostsBatch({ posts: payloads });
21
- const lines = [
22
- 'Batch create completed.',
23
- `• Total: ${result.total}`,
24
- `• Created: ${result.created}`,
25
- `• Replayed: ${result.replayed}`,
26
- `• Failed: ${result.failed}`,
27
- ];
28
- for (const item of result.posts.slice(0, 25)) {
22
+ const createdRows = result.posts.slice(0, 25).map((item) => {
29
23
  const post = item.post;
30
- const when = post.scheduled_at ? new Date(post.scheduled_at).toLocaleString() : 'now';
31
- lines.push(`• [${item.index}] ID ${post.id} ${post.platform || 'unknown'} ${post.post_type || 'post'} ${post.status || 'created'} at ${when}${item.replayed ? ' (replayed)' : ''}`);
32
- }
33
- for (const error of result.errors.slice(0, 25)) {
34
- lines.push(`• [${error.index}] ERROR ${error.status}: ${error.error}`);
35
- }
36
- return lines.join('\n');
24
+ return [
25
+ item.index,
26
+ code(post.id),
27
+ post.platform || 'unknown',
28
+ post.post_type || 'post',
29
+ statusLabel(post.status || 'created'),
30
+ post.scheduled_at ? formatDateTime(post.scheduled_at) : 'now',
31
+ item.replayed ? 'yes' : 'no',
32
+ ];
33
+ });
34
+ const errorRows = result.errors.slice(0, 25).map((error) => [
35
+ error.index,
36
+ error.status,
37
+ error.error,
38
+ ]);
39
+ return [
40
+ mdTitle(result.failed ? '⚠️ Batch create completed with issues' : '✅ Batch create completed'),
41
+ mdTable(['Total', 'Created', 'Replayed', 'Failed'], [[result.total, result.created, result.replayed, result.failed]]),
42
+ mdSection('Created posts', mdTable(['Index', 'Post ID', 'Platform', 'Type', 'Status', 'Scheduled', 'Replayed'], createdRows)),
43
+ mdSection('Errors', mdTable(['Index', 'Status', 'Error'], errorRows)),
44
+ ].filter(Boolean).join('\n\n');
37
45
  },
38
46
  };
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import { code, formatBytes, mdSuccess } from '../lib/format.js';
2
3
  export const createSignedUploadTool = {
3
4
  name: 'create_signed_upload',
4
- description: 'Create a signed direct-upload URL for a larger image or video. Upload the binary to upload_url, then use public_url with create_post.',
5
+ description: 'Create a signed upload URL for a larger image or video. Upload the binary to upload_url; the API validates actual bytes before storage, then public_url can be used with create_post.',
5
6
  inputSchema: z.object({
6
7
  filename: z.string().describe('Filename with extension, e.g. launch-video.mp4'),
7
8
  content_type: z.string().describe('MIME type, e.g. video/mp4'),
@@ -9,12 +10,14 @@ export const createSignedUploadTool = {
9
10
  }),
10
11
  async execute(client, input) {
11
12
  const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size);
12
- return [
13
- 'Signed upload created.',
14
- `• Upload URL: ${signed.upload_url}`,
15
- `• Token: ${signed.token}`,
16
- `• Storage path: ${signed.path}`,
17
- `• Public URL after upload: ${signed.public_url}`,
18
- ].join('\n');
13
+ return mdSuccess('Signed upload created', [
14
+ ['Filename', input.filename],
15
+ ['Content type', input.content_type],
16
+ ['Size', formatBytes(input.size)],
17
+ ['Upload URL', signed.upload_url],
18
+ ['Token', code(signed.token)],
19
+ ['Storage path', signed.path],
20
+ ['Public URL after upload', signed.public_url],
21
+ ]);
19
22
  },
20
23
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { webhookEventSchema } from './webhook-events.js';
3
+ import { code, mdSuccess } from '../lib/format.js';
3
4
  export const createWebhookTool = {
4
5
  name: 'create_webhook',
5
6
  description: 'Create a webhook subscription for post/account/analytics events. WRITE WITH OUTBOUND SIDE EFFECTS: show the user the target URL, workspace, events, and active state, then get explicit confirmation before calling. The response includes the signing secret once.',
@@ -14,7 +15,11 @@ export const createWebhookTool = {
14
15
  async execute(client, input) {
15
16
  const { confirm: _confirm, ...payload } = input;
16
17
  const result = await client.createWebhook(payload);
17
- const secret = result.secret ? `\nSigning secret: ${result.secret}` : '';
18
- return `Webhook created: ${result.webhook.id}\nURL: ${result.webhook.url}\nEvents: ${result.webhook.events.join(', ')}${secret}`;
18
+ return mdSuccess('Webhook created', [
19
+ ['Webhook ID', code(result.webhook.id)],
20
+ ['URL', result.webhook.url],
21
+ ['Events', result.webhook.events.join(', ')],
22
+ ['Signing secret', result.secret ? code(result.secret) : undefined],
23
+ ], 'Store the signing secret now; Posterly only returns it once.');
19
24
  },
20
25
  };
@@ -28,5 +28,5 @@ export declare const deleteGoogleBusinessReviewReplyTool: {
28
28
  location_id?: string;
29
29
  review_name: string;
30
30
  confirm: true;
31
- }): Promise<any>;
31
+ }): Promise<string>;
32
32
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { mdSuccess } from '../lib/format.js';
2
3
  export const deleteGoogleBusinessReviewReplyTool = {
3
4
  name: 'delete_google_business_review_reply',
4
5
  description: 'Delete the owner reply from a Google Business Profile review. DESTRUCTIVE: confirm the review and location with the user, then pass confirm=true only after explicit confirmation.',
@@ -12,6 +13,8 @@ export const deleteGoogleBusinessReviewReplyTool = {
12
13
  async execute(client, input) {
13
14
  const { confirm: _confirm, ...payload } = input;
14
15
  const result = await client.deleteGoogleBusinessReviewReply(payload);
15
- return result.message || 'Google Business review reply deleted successfully.';
16
+ return mdSuccess('Google Business review reply deleted', [
17
+ ['Message', result.message || 'Google Business review reply deleted successfully.'],
18
+ ]);
16
19
  },
17
20
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdSuccess } from '../lib/format.js';
2
3
  export const deleteOAuthClientTool = {
3
4
  name: 'delete_oauth_client',
4
5
  description: 'Delete a self-serve OAuth developer client. DESTRUCTIVE: prevents new authorizations for that client_id; existing access tokens remain revocable as API keys.',
@@ -8,6 +9,8 @@ export const deleteOAuthClientTool = {
8
9
  }),
9
10
  async execute(client, input) {
10
11
  const result = await client.deleteOAuthClient(input.client_id);
11
- return `OAuth client deleted: ${result.client_id}`;
12
+ return mdSuccess('OAuth client deleted', [
13
+ ['Client ID', code(result.client_id)],
14
+ ]);
12
15
  },
13
16
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { mdJson, mdTitle } from '../lib/format.js';
2
3
  export const deletePostGroupTool = {
3
4
  name: 'delete_post_group',
4
5
  description: 'Delete every draft/scheduled/failed/paused post matching a caller-defined group_id, post_group_id, api_group_id, or release_id. DESTRUCTIVE: inspect the group first, list the affected posts, and pass confirm=true only after explicit confirmation.',
@@ -8,6 +9,9 @@ export const deletePostGroupTool = {
8
9
  }),
9
10
  async execute(client, input) {
10
11
  const result = await client.deletePostGroup(input.group_id);
11
- return JSON.stringify(result, null, 2);
12
+ return [
13
+ mdTitle('✅ Post group delete completed', `Group: ${input.group_id}`),
14
+ mdJson('Result', result),
15
+ ].join('\n\n');
12
16
  },
13
17
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdSuccess } from '../lib/format.js';
2
3
  export const deletePostTool = {
3
4
  name: 'delete_post',
4
5
  description: 'Delete a scheduled or draft post. DESTRUCTIVE and IRREVERSIBLE — the post and its caption cannot be recovered. Cannot delete published or currently publishing posts.\n\n' +
@@ -8,6 +9,8 @@ export const deletePostTool = {
8
9
  }),
9
10
  async execute(client, input) {
10
11
  const result = await client.deletePost(input.post_id);
11
- return `Post #${result.id} deleted successfully.`;
12
+ return mdSuccess('Post deleted', [
13
+ ['Post ID', code(result.id)],
14
+ ]);
12
15
  },
13
16
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdSuccess } from '../lib/format.js';
2
3
  export const deleteWebhookTool = {
3
4
  name: 'delete_webhook',
4
5
  description: 'Delete a webhook subscription. DESTRUCTIVE: list the webhook first, show the user its URL/events/workspace, and get explicit confirmation before calling.',
@@ -8,6 +9,8 @@ export const deleteWebhookTool = {
8
9
  }),
9
10
  async execute(client, input) {
10
11
  const result = await client.deleteWebhook(input.webhook_id);
11
- return `Webhook deleted: ${result.id}`;
12
+ return mdSuccess('Webhook deleted', [
13
+ ['Webhook ID', code(result.id)],
14
+ ]);
12
15
  },
13
16
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdSuccess } from '../lib/format.js';
2
3
  export const disconnectAccountTool = {
3
4
  name: 'disconnect_account',
4
5
  description: 'Disconnect a connected social account from posterly. DESTRUCTIVE and IRREVERSIBLE: removes the account connection, emits account.disconnected webhooks, and may transfer Instagram scheduled posts to a replacement account.\n\n' +
@@ -13,10 +14,11 @@ export const disconnectAccountTool = {
13
14
  }
14
15
  const result = await client.disconnectAccount(input.account_id);
15
16
  const account = result.account || {};
16
- return [
17
- `Disconnected ${account.platform || 'account'}${account.username ? ` @${account.username}` : ''} (ID: ${account.id || input.account_id}).`,
18
- `Transferred posts: ${result.transferred_post_count ?? 0}.`,
19
- `Stamped posts: ${result.stamped_post_count ?? 0}.`,
20
- ].join('\n');
17
+ return mdSuccess('Account disconnected', [
18
+ ['Account', `${account.platform || 'account'}${account.username ? ` @${account.username}` : ''}`],
19
+ ['Account ID', code(account.id || input.account_id)],
20
+ ['Transferred posts', result.transferred_post_count ?? 0],
21
+ ['Stamped posts', result.stamped_post_count ?? 0],
22
+ ]);
21
23
  },
22
24
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const findSlotTool = {
3
4
  name: 'find_available_slot',
4
5
  description: 'Find available time slots for posting. Respects a 1-hour gap between posts and preferred hours (8am–10pm in the given timezone). Returns up to 10 slots. IMPORTANT: pass a timezone explicitly — default is America/New_York and slots will be off if the user is elsewhere. Pass workspace_id to only avoid collisions with posts in that workspace.',
@@ -25,9 +26,15 @@ export const findSlotTool = {
25
26
  async execute(client, input) {
26
27
  const slots = await client.findAvailableSlots(input);
27
28
  if (slots.length === 0) {
28
- return 'No available slots found in the next 14 days.';
29
+ return mdEmpty('available slots', 'No available posting slots were found in the next 14 days.');
29
30
  }
30
- const lines = slots.map((s, i) => `${i + 1}. ${s.local_time} — ${new Date(s.time).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}`);
31
- return `Available posting slots:\n${lines.join('\n')}`;
31
+ return [
32
+ mdTitle(`Available posting slots (${slots.length})`),
33
+ mdTable(['Option', 'Local time', 'Date'], slots.map((slot, index) => [
34
+ index + 1,
35
+ slot.local_time,
36
+ formatDate(slot.time),
37
+ ])),
38
+ ].join('\n\n');
32
39
  },
33
40
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { mdBullets, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const generateImageTool = {
3
4
  name: 'generate_image',
4
5
  description: 'Generate an AI image via Posterly\'s Nano Banana (Gemini) integration. The image is saved to the user\'s media storage and the returned URL can be passed to `create_post` as `media_url`.\n\n' +
@@ -47,24 +48,21 @@ export const generateImageTool = {
47
48
  }),
48
49
  async execute(client, input) {
49
50
  const result = await client.generateImage(input);
50
- const lines = [];
51
- lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
52
- lines.push('');
53
- result.images.forEach((img, i) => {
54
- lines.push(`${i + 1}. ${img.url}`);
55
- });
56
- lines.push('');
57
- lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
58
- if (result.usage.limit != null) {
59
- lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
60
- }
61
- if (result.warnings?.length) {
62
- lines.push('');
63
- lines.push('Warnings:');
64
- result.warnings.forEach((w) => lines.push(`• ${w}`));
65
- }
66
- lines.push('');
67
- lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
68
- return lines.join('\n');
51
+ return [
52
+ mdTitle(`✅ Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'}`, `Model: ${result.model}`),
53
+ mdTable(['#', 'Image URL', 'Filename'], result.images.map((img, index) => [
54
+ index + 1,
55
+ img.url,
56
+ img.filename,
57
+ ])),
58
+ mdKeyValue([
59
+ ['Billed from', result.usage.billed_from],
60
+ ['Credits used', result.credits_used],
61
+ ['Plan usage', result.usage.limit != null ? `${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}` : undefined],
62
+ ['Plan tier', result.usage.tier || undefined],
63
+ ]),
64
+ result.warnings?.length ? mdSection('Warnings', mdBullets(result.warnings)) : '',
65
+ '**Next step:** Pass any URL above to `create_post` as `media_url`, or use multiple as `media_urls` for a carousel.',
66
+ ].filter(Boolean).join('\n\n');
69
67
  },
70
68
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdJson, mdKeyValue, mdTitle, statusLabel } from '../lib/format.js';
2
3
  export const generateVideoTool = {
3
4
  name: 'generate_video',
4
5
  description: 'Queue a Veo AI video generation job. COSTS VEO CREDITS: confirm prompt, model, duration, resolution, aspect ratio, audio choice, and credit cost with the user before calling. Poll get_video_job for status and final video_url.',
@@ -21,14 +22,15 @@ export const generateVideoTool = {
21
22
  async execute(client, input) {
22
23
  const result = await client.generateVideo(input);
23
24
  return [
24
- 'Video generation job queued',
25
- `Job ID: ${result.job_id}`,
26
- `Status: ${result.status}`,
27
- `Credit cost: ${result.credit_cost} (${result.included_credit_cost || 0} included, ${result.purchased_credit_cost || 0} purchased)`,
28
- result.message,
29
- '',
30
- 'Raw response:',
31
- JSON.stringify(result, null, 2),
32
- ].filter(Boolean).join('\n');
25
+ mdTitle('🎬 Video generation job queued'),
26
+ mdKeyValue([
27
+ ['Job ID', code(result.job_id)],
28
+ ['Status', statusLabel(result.status)],
29
+ ['Credit cost', `${result.credit_cost} (${result.included_credit_cost || 0} included, ${result.purchased_credit_cost || 0} purchased)`],
30
+ ['Message', result.message],
31
+ ]),
32
+ '**Next step:** Poll `get_video_job` with the job ID until the video URL is ready.',
33
+ mdJson('Raw response', result),
34
+ ].filter(Boolean).join('\n\n');
33
35
  },
34
36
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { formatDelta, formatNumber, formatPercent, mdBullets, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const getAccountAnalyticsTool = {
3
4
  name: 'get_account_analytics',
4
5
  description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns follower growth, reach, views, engagement rate, and platform-specific metrics.',
@@ -18,54 +19,72 @@ export const getAccountAnalyticsTool = {
18
19
  async execute(client, input) {
19
20
  const result = await client.getAccountAnalytics(input);
20
21
  const { account, range, summary, snapshots } = result;
21
- const lines = [
22
- `Analytics for @${account.username} (${account.platform}, id ${account.id})`,
23
- `Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
24
- '',
25
- 'Summary:',
26
- ];
27
- if (account.platform === 'google_business') {
28
- const metrics = summary.platform_metrics || {};
29
- pushMetric(lines, 'Profile views', metrics.profile_views ?? summary.total_views);
30
- pushMetric(lines, 'Search views', metrics.search_views ?? summary.total_reach);
31
- pushMetric(lines, 'Maps views', metrics.maps_views ?? summary.total_profile_views);
32
- pushMetric(lines, 'Customer actions', metrics.customer_actions ?? summary.total_accounts_engaged);
33
- pushMetric(lines, 'Posts', metrics.posts ?? summary.current_media_count);
34
- lines.push(`• Engagement rate (actions / profile views): ${summary.engagement_rate}%`);
35
- pushMetric(lines, 'Website clicks', metrics.website_clicks ?? summary.total_website_clicks);
36
- pushMetric(lines, 'Call clicks', metrics.call_clicks ?? summary.total_call_clicks);
37
- pushMetric(lines, 'Direction requests', metrics.direction_requests ?? summary.total_direction_requests);
38
- pushMetric(lines, 'Conversations', metrics.conversations ?? summary.total_conversations);
39
- pushMetric(lines, 'Bookings', metrics.bookings ?? summary.total_bookings);
40
- }
41
- else if (account.platform === 'pinterest') {
42
- pushGenericSummary(lines, summary);
43
- pushMetric(lines, 'Outbound clicks', summary.total_website_clicks);
44
- }
45
- else if (account.platform === 'youtube') {
46
- pushGenericSummary(lines, summary);
47
- pushMetric(lines, 'Watch minutes', summary.total_watch_minutes);
48
- pushMetric(lines, 'Likes', summary.total_likes);
49
- }
50
- else {
51
- pushGenericSummary(lines, summary);
52
- }
53
- return lines.join('\n');
22
+ const metricRows = account.platform === 'google_business'
23
+ ? googleBusinessMetricRows(summary)
24
+ : genericMetricRows(summary, account.platform);
25
+ return [
26
+ mdTitle(`📊 Analytics for @${account.username}`, `${account.platform} · ${range.from} → ${range.to}`),
27
+ mdKeyValue([
28
+ ['Account ID', account.id],
29
+ ['Daily snapshots', snapshots.length],
30
+ ]),
31
+ mdSection('Summary', mdTable(['Metric', 'Value'], metricRows)),
32
+ mdSection('Quick read', mdBullets(buildInsights(summary, account.platform))),
33
+ ].filter(Boolean).join('\n\n');
54
34
  },
55
35
  };
56
- function formatDelta(n) {
57
- return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
36
+ function genericMetricRows(summary, platform) {
37
+ const rows = [
38
+ ['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
39
+ ['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
40
+ ['Total reach', formatNumber(summary.total_reach)],
41
+ ['Total views', formatNumber(summary.total_views)],
42
+ ['Profile views', formatNumber(summary.total_profile_views)],
43
+ ['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
44
+ ['Engagement rate by reach', formatPercent(summary.engagement_rate)],
45
+ ['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
46
+ ];
47
+ if (platform === 'pinterest') {
48
+ rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
49
+ }
50
+ if (platform === 'youtube') {
51
+ rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
52
+ rows.push(['Likes', formatNumber(summary.total_likes)]);
53
+ }
54
+ return rows;
58
55
  }
59
- function pushGenericSummary(lines, summary) {
60
- lines.push(`• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`, `• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`);
61
- pushMetric(lines, 'Total reach', summary.total_reach);
62
- pushMetric(lines, 'Total views', summary.total_views);
63
- pushMetric(lines, 'Profile views', summary.total_profile_views);
64
- pushMetric(lines, 'Total accounts engaged', summary.total_accounts_engaged);
65
- lines.push(`• Engagement rate (by reach): ${summary.engagement_rate}%`, `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`);
56
+ function googleBusinessMetricRows(summary) {
57
+ const metrics = summary.platform_metrics || {};
58
+ return [
59
+ ['Profile views', formatNumber(metrics.profile_views ?? summary.total_views)],
60
+ ['Search views', formatNumber(metrics.search_views ?? summary.total_reach)],
61
+ ['Maps views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
62
+ ['Customer actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
63
+ ['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
64
+ ['Engagement rate (actions / profile views)', formatPercent(summary.engagement_rate)],
65
+ ['Website clicks', formatNumber(metrics.website_clicks ?? summary.total_website_clicks)],
66
+ ['Call clicks', formatNumber(metrics.call_clicks ?? summary.total_call_clicks)],
67
+ ['Direction requests', formatNumber(metrics.direction_requests ?? summary.total_direction_requests)],
68
+ ['Conversations', formatNumber(metrics.conversations ?? summary.total_conversations)],
69
+ ['Bookings', formatNumber(metrics.bookings ?? summary.total_bookings)],
70
+ ];
66
71
  }
67
- function pushMetric(lines, label, value) {
68
- if (value != null) {
69
- lines.push(`• ${label}: ${value.toLocaleString()}`);
72
+ function buildInsights(summary, platform) {
73
+ const insights = [];
74
+ if (summary.followers_change > 0) {
75
+ insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
76
+ }
77
+ else if (summary.followers_change < 0) {
78
+ insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
79
+ }
80
+ if (summary.engagement_rate != null) {
81
+ insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
82
+ }
83
+ if (platform === 'google_business' && summary.total_website_clicks != null) {
84
+ insights.push(`${formatNumber(summary.total_website_clicks)} website clicks came from the profile.`);
85
+ }
86
+ if (platform === 'youtube' && summary.total_watch_minutes != null) {
87
+ insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
70
88
  }
89
+ return insights.length > 0 ? insights : ['No standout movement detected in the returned period.'];
71
90
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, compactText, formatDateTime, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
2
3
  export const getBrandProfileTool = {
3
4
  name: 'get_brand_profile',
4
5
  description: 'Get the extended brand profile for a brand/client. Returns voice/tone guidance, audience, keywords, dos and don’ts, visual notes, and other saved brand context.',
@@ -8,48 +9,41 @@ export const getBrandProfileTool = {
8
9
  async execute(client, input) {
9
10
  const result = await client.getBrandProfile(input.brand_id);
10
11
  const profile = result.brand_profile;
11
- const lines = [
12
- `Brand profile: ${profile.brand_name}`,
13
- `• Source: ${profile.source}`,
14
- ];
15
- if (profile.workspace_client_id)
16
- lines.push(`• Workspace client ID: ${profile.workspace_client_id}`);
17
- if (profile.brand_group_id)
18
- lines.push(`• Legacy brand group ID: ${profile.brand_group_id}`);
19
- if (profile.tone_of_voice)
20
- lines.push(`• Tone of voice: ${profile.tone_of_voice}`);
21
- if (profile.audience)
22
- lines.push(`• Audience: ${profile.audience}`);
23
- if (profile.brand_values)
24
- lines.push(`• Brand values: ${profile.brand_values}`);
25
- if (profile.custom_instructions)
26
- lines.push(`• Custom instructions: ${profile.custom_instructions}`);
27
- if (profile.logo_url)
28
- lines.push(`• Logo URL: ${profile.logo_url}`);
29
- addStructuredLine(lines, 'Keywords', profile.keywords);
30
- addStructuredLine(lines, 'Competitors', profile.competitors);
31
- addStructuredLine(lines, 'Do / Don’ts', profile.do_donts);
32
- addStructuredLine(lines, 'Visual guidelines', profile.visual_guidelines);
33
- addStructuredLine(lines, 'Brand story', profile.brand_story);
34
- addStructuredLine(lines, 'Example posts', profile.example_posts);
35
- addStructuredLine(lines, 'Topics to cover', profile.topics_to_cover);
36
- addStructuredLine(lines, 'Topics to avoid', profile.topics_to_avoid);
37
- addStructuredLine(lines, 'Voice examples', profile.voice_examples);
38
- if (profile.last_context_refresh_at) {
39
- lines.push(`• Last refreshed: ${profile.last_context_refresh_at}`);
40
- }
41
- if (lines.length === 2) {
42
- lines.push('No extended brand profile fields have been saved yet.');
43
- }
44
- return lines.join('\n');
12
+ const profileBody = [
13
+ mdKeyValue([
14
+ ['Source', profile.source],
15
+ ['Profile ID', profile.id ? code(profile.id) : undefined],
16
+ ['Workspace client ID', profile.workspace_client_id ? code(profile.workspace_client_id) : undefined],
17
+ ['Legacy brand group ID', profile.brand_group_id ? code(profile.brand_group_id) : undefined],
18
+ ['Logo', profile.logo_url],
19
+ ['Last refreshed', profile.last_context_refresh_at ? formatDateTime(profile.last_context_refresh_at) : undefined],
20
+ ]),
21
+ mdSection('Voice and audience', mdKeyValue([
22
+ ['Tone of voice', compactText(profile.tone_of_voice, 180)],
23
+ ['Audience', compactText(profile.audience, 180)],
24
+ ['Brand values', compactText(profile.brand_values, 180)],
25
+ ['Brand story', compactText(formatStructuredValue(profile.brand_story), 240)],
26
+ ])),
27
+ mdSection('Guidance', mdKeyValue([
28
+ ['Custom instructions', compactText(profile.custom_instructions, 240)],
29
+ ['Keywords', formatStructuredValue(profile.keywords)],
30
+ ['Competitors', formatStructuredValue(profile.competitors)],
31
+ ['Do / Don’ts', formatStructuredValue(profile.do_donts)],
32
+ ['Topics to cover', formatStructuredValue(profile.topics_to_cover)],
33
+ ['Topics to avoid', formatStructuredValue(profile.topics_to_avoid)],
34
+ ])),
35
+ mdSection('Creative references', mdKeyValue([
36
+ ['Visual guidelines', formatStructuredValue(profile.visual_guidelines)],
37
+ ['Example posts', formatStructuredValue(profile.example_posts)],
38
+ ['Voice examples', formatStructuredValue(profile.voice_examples)],
39
+ ])),
40
+ ].filter(Boolean).join('\n\n');
41
+ return [
42
+ mdTitle(`Brand profile: ${profile.brand_name}`),
43
+ profileBody || 'No extended brand profile fields have been saved yet.',
44
+ ].join('\n\n');
45
45
  },
46
46
  };
47
- function addStructuredLine(lines, label, value) {
48
- const formatted = formatStructuredValue(value);
49
- if (formatted) {
50
- lines.push(`• ${label}: ${formatted}`);
51
- }
52
- }
53
47
  function formatStructuredValue(value) {
54
48
  if (value == null)
55
49
  return null;