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,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
3
  import { SUPPORTED_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
+ import { code, formatDateTime, mdKeyValue, mdSuccess, statusLabel } from '../lib/format.js';
4
5
 
5
6
  export const instagramSettingsSchema = z.object({
6
7
  __type: z.enum(['instagram', 'instagram-standalone']).optional(),
@@ -179,33 +180,24 @@ export const createPostTool = {
179
180
  const p = result.post as Record<string, any>;
180
181
  const ws = result.workspace;
181
182
 
182
- const when = p.scheduled_at
183
- ? new Date(p.scheduled_at).toLocaleString()
184
- : 'now';
185
-
186
- const lines = [
187
- 'Post created successfully!',
188
- `• ID: ${p.id}`,
189
- `• Platform: ${p.platform || input.platform || 'unknown'}`,
190
- `• Type: ${p.post_type}`,
191
- `• Status: ${p.status}`,
192
- `• Scheduled: ${when}`,
193
- ];
194
- if (thread_posts) {
195
- lines.push(`• Thread: ${thread_posts.length} posts`);
196
- }
197
- if (instagram_settings?.collaborators?.length) {
198
- lines.push(`• Instagram collaborators: ${instagram_settings.collaborators.length}`);
199
- }
200
- if (instagram_settings?.first_comment) {
201
- lines.push('• Instagram first comment: yes');
202
- }
203
- if (platform_settings) {
204
- lines.push('• Platform settings: yes');
205
- }
206
- if (ws) {
207
- lines.push(`• Workspace: ${ws.name} (${ws.id}) — resolved from ${ws.resolved_from}`);
208
- }
209
- return lines.join('\n');
183
+ return [
184
+ mdSuccess('Post created', [
185
+ ['Post ID', code(p.id)],
186
+ ['Platform', p.platform || input.platform || 'unknown'],
187
+ ['Type', p.post_type],
188
+ ['Status', statusLabel(p.status)],
189
+ ['Scheduled', p.scheduled_at ? formatDateTime(p.scheduled_at) : 'now'],
190
+ ['Thread', thread_posts ? `${thread_posts.length} posts` : undefined],
191
+ ['Instagram collaborators', instagram_settings?.collaborators?.length],
192
+ ['Instagram first comment', Boolean(instagram_settings?.first_comment)],
193
+ ['Platform settings', Boolean(platform_settings)],
194
+ ], 'Use `get_post` if you want to review the saved post details.'),
195
+ ws
196
+ ? mdKeyValue([
197
+ ['Workspace', `${ws.name} (${code(ws.id)})`],
198
+ ['Workspace resolved from', ws.resolved_from],
199
+ ])
200
+ : '',
201
+ ].filter(Boolean).join('\n\n');
210
202
  },
211
203
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
3
  import { buildCreatePostPayload, createPostInputSchema, type CreatePostInput } from './create-post.js';
4
+ import { code, formatDateTime, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
4
5
 
5
6
  export const createPostsBatchTool = {
6
7
  name: 'create_posts_batch',
@@ -23,26 +24,30 @@ export const createPostsBatchTool = {
23
24
  const payloads = input.posts.map((post) => buildCreatePostPayload(post));
24
25
  const result = await client.createPostsBatch({ posts: payloads });
25
26
 
26
- const lines = [
27
- 'Batch create completed.',
28
- `• Total: ${result.total}`,
29
- `• Created: ${result.created}`,
30
- `• Replayed: ${result.replayed}`,
31
- `• Failed: ${result.failed}`,
32
- ];
33
-
34
- for (const item of result.posts.slice(0, 25)) {
27
+ const createdRows = result.posts.slice(0, 25).map((item) => {
35
28
  const post = item.post as Record<string, any>;
36
- const when = post.scheduled_at ? new Date(post.scheduled_at).toLocaleString() : 'now';
37
- lines.push(
38
- `• [${item.index}] ID ${post.id} ${post.platform || 'unknown'} ${post.post_type || 'post'} ${post.status || 'created'} at ${when}${item.replayed ? ' (replayed)' : ''}`,
39
- );
40
- }
29
+ return [
30
+ item.index,
31
+ code(post.id),
32
+ post.platform || 'unknown',
33
+ post.post_type || 'post',
34
+ statusLabel(post.status || 'created'),
35
+ post.scheduled_at ? formatDateTime(post.scheduled_at) : 'now',
36
+ item.replayed ? 'yes' : 'no',
37
+ ];
38
+ });
41
39
 
42
- for (const error of result.errors.slice(0, 25)) {
43
- lines.push(`• [${error.index}] ERROR ${error.status}: ${error.error}`);
44
- }
40
+ const errorRows = result.errors.slice(0, 25).map((error) => [
41
+ error.index,
42
+ error.status,
43
+ error.error,
44
+ ]);
45
45
 
46
- return lines.join('\n');
46
+ return [
47
+ mdTitle(result.failed ? '⚠️ Batch create completed with issues' : '✅ Batch create completed'),
48
+ mdTable(['Total', 'Created', 'Replayed', 'Failed'], [[result.total, result.created, result.replayed, result.failed]]),
49
+ mdSection('Created posts', mdTable(['Index', 'Post ID', 'Platform', 'Type', 'Status', 'Scheduled', 'Replayed'], createdRows)),
50
+ mdSection('Errors', mdTable(['Index', 'Status', 'Error'], errorRows)),
51
+ ].filter(Boolean).join('\n\n');
47
52
  },
48
53
  };
@@ -1,10 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, formatBytes, mdSuccess } from '../lib/format.js';
3
4
 
4
5
  export const createSignedUploadTool = {
5
6
  name: 'create_signed_upload',
6
7
  description:
7
- '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.',
8
+ '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.',
8
9
  inputSchema: z.object({
9
10
  filename: z.string().describe('Filename with extension, e.g. launch-video.mp4'),
10
11
  content_type: z.string().describe('MIME type, e.g. video/mp4'),
@@ -16,12 +17,14 @@ export const createSignedUploadTool = {
16
17
  input: { filename: string; content_type: string; size: number },
17
18
  ) {
18
19
  const signed = await client.getSignedUploadUrl(input.filename, input.content_type, input.size);
19
- return [
20
- 'Signed upload created.',
21
- `• Upload URL: ${signed.upload_url}`,
22
- `• Token: ${signed.token}`,
23
- `• Storage path: ${signed.path}`,
24
- `• Public URL after upload: ${signed.public_url}`,
25
- ].join('\n');
20
+ return mdSuccess('Signed upload created', [
21
+ ['Filename', input.filename],
22
+ ['Content type', input.content_type],
23
+ ['Size', formatBytes(input.size)],
24
+ ['Upload URL', signed.upload_url],
25
+ ['Token', code(signed.token)],
26
+ ['Storage path', signed.path],
27
+ ['Public URL after upload', signed.public_url],
28
+ ]);
26
29
  },
27
30
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
3
  import { webhookEventSchema } from './webhook-events.js';
4
+ import { code, mdSuccess } from '../lib/format.js';
4
5
 
5
6
  export const createWebhookTool = {
6
7
  name: 'create_webhook',
@@ -28,8 +29,11 @@ export const createWebhookTool = {
28
29
  ) {
29
30
  const { confirm: _confirm, ...payload } = input;
30
31
  const result = await client.createWebhook(payload);
31
- const secret = result.secret ? `\nSigning secret: ${result.secret}` : '';
32
- return `Webhook created: ${result.webhook.id}\nURL: ${result.webhook.url}\nEvents: ${result.webhook.events.join(', ')}${secret}`;
32
+ return mdSuccess('Webhook created', [
33
+ ['Webhook ID', code(result.webhook.id)],
34
+ ['URL', result.webhook.url],
35
+ ['Events', result.webhook.events.join(', ')],
36
+ ['Signing secret', result.secret ? code(result.secret) : undefined],
37
+ ], 'Store the signing secret now; Posterly only returns it once.');
33
38
  },
34
39
  };
35
-
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdSuccess } from '../lib/format.js';
3
4
 
4
5
  export const deleteGoogleBusinessReviewReplyTool = {
5
6
  name: 'delete_google_business_review_reply',
@@ -25,6 +26,8 @@ export const deleteGoogleBusinessReviewReplyTool = {
25
26
  ) {
26
27
  const { confirm: _confirm, ...payload } = input;
27
28
  const result = await client.deleteGoogleBusinessReviewReply(payload);
28
- return result.message || 'Google Business review reply deleted successfully.';
29
+ return mdSuccess('Google Business review reply deleted', [
30
+ ['Message', result.message || 'Google Business review reply deleted successfully.'],
31
+ ]);
29
32
  },
30
33
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdSuccess } from '../lib/format.js';
3
4
 
4
5
  export const deleteOAuthClientTool = {
5
6
  name: 'delete_oauth_client',
@@ -18,6 +19,8 @@ export const deleteOAuthClientTool = {
18
19
  },
19
20
  ) {
20
21
  const result = await client.deleteOAuthClient(input.client_id);
21
- return `OAuth client deleted: ${result.client_id}`;
22
+ return mdSuccess('OAuth client deleted', [
23
+ ['Client ID', code(result.client_id)],
24
+ ]);
22
25
  },
23
26
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdJson, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const deletePostGroupTool = {
5
6
  name: 'delete_post_group',
@@ -12,6 +13,9 @@ export const deletePostGroupTool = {
12
13
 
13
14
  async execute(client: PosterlyClient, input: { group_id: string; confirm: true }) {
14
15
  const result = await client.deletePostGroup(input.group_id);
15
- return JSON.stringify(result, null, 2);
16
+ return [
17
+ mdTitle('✅ Post group delete completed', `Group: ${input.group_id}`),
18
+ mdJson('Result', result),
19
+ ].join('\n\n');
16
20
  },
17
21
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdSuccess } from '../lib/format.js';
3
4
 
4
5
  export const deletePostTool = {
5
6
  name: 'delete_post',
@@ -15,6 +16,8 @@ export const deletePostTool = {
15
16
  input: { post_id: number },
16
17
  ) {
17
18
  const result = await client.deletePost(input.post_id);
18
- return `Post #${result.id} deleted successfully.`;
19
+ return mdSuccess('Post deleted', [
20
+ ['Post ID', code(result.id)],
21
+ ]);
19
22
  },
20
23
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdSuccess } from '../lib/format.js';
3
4
 
4
5
  export const deleteWebhookTool = {
5
6
  name: 'delete_webhook',
@@ -12,7 +13,8 @@ export const deleteWebhookTool = {
12
13
 
13
14
  async execute(client: PosterlyClient, input: { webhook_id: string; confirm: true }) {
14
15
  const result = await client.deleteWebhook(input.webhook_id);
15
- return `Webhook deleted: ${result.id}`;
16
+ return mdSuccess('Webhook deleted', [
17
+ ['Webhook ID', code(result.id)],
18
+ ]);
16
19
  },
17
20
  };
18
-
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdSuccess } from '../lib/format.js';
3
4
 
4
5
  export const disconnectAccountTool = {
5
6
  name: 'disconnect_account',
@@ -21,10 +22,11 @@ export const disconnectAccountTool = {
21
22
 
22
23
  const result = await client.disconnectAccount(input.account_id);
23
24
  const account = result.account || {};
24
- return [
25
- `Disconnected ${account.platform || 'account'}${account.username ? ` @${account.username}` : ''} (ID: ${account.id || input.account_id}).`,
26
- `Transferred posts: ${result.transferred_post_count ?? 0}.`,
27
- `Stamped posts: ${result.stamped_post_count ?? 0}.`,
28
- ].join('\n');
25
+ return mdSuccess('Account disconnected', [
26
+ ['Account', `${account.platform || 'account'}${account.username ? ` @${account.username}` : ''}`],
27
+ ['Account ID', code(account.id || input.account_id)],
28
+ ['Transferred posts', result.transferred_post_count ?? 0],
29
+ ['Stamped posts', result.stamped_post_count ?? 0],
30
+ ]);
29
31
  },
30
32
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { formatDate, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const findSlotTool = {
5
6
  name: 'find_available_slot',
@@ -38,12 +39,19 @@ export const findSlotTool = {
38
39
  const slots = await client.findAvailableSlots(input);
39
40
 
40
41
  if (slots.length === 0) {
41
- return 'No available slots found in the next 14 days.';
42
+ return mdEmpty('available slots', 'No available posting slots were found in the next 14 days.');
42
43
  }
43
44
 
44
- const lines = slots.map(
45
- (s, i) => `${i + 1}. ${s.local_time} — ${new Date(s.time).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}`
46
- );
47
- return `Available posting slots:\n${lines.join('\n')}`;
45
+ return [
46
+ mdTitle(`Available posting slots (${slots.length})`),
47
+ mdTable(
48
+ ['Option', 'Local time', 'Date'],
49
+ slots.map((slot, index) => [
50
+ index + 1,
51
+ slot.local_time,
52
+ formatDate(slot.time),
53
+ ]),
54
+ ),
55
+ ].join('\n\n');
48
56
  },
49
57
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { mdBullets, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const generateImageTool = {
5
6
  name: 'generate_image',
@@ -62,25 +63,24 @@ export const generateImageTool = {
62
63
  ) {
63
64
  const result = await client.generateImage(input);
64
65
 
65
- const lines: string[] = [];
66
- lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
67
- lines.push('');
68
- result.images.forEach((img, i) => {
69
- lines.push(`${i + 1}. ${img.url}`);
70
- });
71
- lines.push('');
72
- lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
73
- if (result.usage.limit != null) {
74
- lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
75
- }
76
- if (result.warnings?.length) {
77
- lines.push('');
78
- lines.push('Warnings:');
79
- result.warnings.forEach((w) => lines.push(`• ${w}`));
80
- }
81
- lines.push('');
82
- lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
83
-
84
- return lines.join('\n');
66
+ return [
67
+ mdTitle(`✅ Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'}`, `Model: ${result.model}`),
68
+ mdTable(
69
+ ['#', 'Image URL', 'Filename'],
70
+ result.images.map((img, index) => [
71
+ index + 1,
72
+ img.url,
73
+ img.filename,
74
+ ]),
75
+ ),
76
+ mdKeyValue([
77
+ ['Billed from', result.usage.billed_from],
78
+ ['Credits used', result.credits_used],
79
+ ['Plan usage', result.usage.limit != null ? `${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}` : undefined],
80
+ ['Plan tier', result.usage.tier || undefined],
81
+ ]),
82
+ result.warnings?.length ? mdSection('Warnings', mdBullets(result.warnings)) : '',
83
+ '**Next step:** Pass any URL above to `create_post` as `media_url`, or use multiple as `media_urls` for a carousel.',
84
+ ].filter(Boolean).join('\n\n');
85
85
  },
86
86
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdJson, mdKeyValue, mdTitle, statusLabel } from '../lib/format.js';
3
4
 
4
5
  export const generateVideoTool = {
5
6
  name: 'generate_video',
@@ -25,14 +26,15 @@ export const generateVideoTool = {
25
26
  async execute(client: PosterlyClient, input: any) {
26
27
  const result = await client.generateVideo(input);
27
28
  return [
28
- 'Video generation job queued',
29
- `Job ID: ${result.job_id}`,
30
- `Status: ${result.status}`,
31
- `Credit cost: ${result.credit_cost} (${result.included_credit_cost || 0} included, ${result.purchased_credit_cost || 0} purchased)`,
32
- result.message,
33
- '',
34
- 'Raw response:',
35
- JSON.stringify(result, null, 2),
36
- ].filter(Boolean).join('\n');
29
+ mdTitle('🎬 Video generation job queued'),
30
+ mdKeyValue([
31
+ ['Job ID', code(result.job_id)],
32
+ ['Status', statusLabel(result.status)],
33
+ ['Credit cost', `${result.credit_cost} (${result.included_credit_cost || 0} included, ${result.purchased_credit_cost || 0} purchased)`],
34
+ ['Message', result.message],
35
+ ]),
36
+ '**Next step:** Poll `get_video_job` with the job ID until the video URL is ready.',
37
+ mdJson('Raw response', result),
38
+ ].filter(Boolean).join('\n\n');
37
39
  },
38
40
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { AccountAnalyticsSummary, PosterlyClient } from '../lib/api-client.js';
3
+ import { formatDelta, formatNumber, formatPercent, mdBullets, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const getAccountAnalyticsTool = {
5
6
  name: 'get_account_analytics',
@@ -26,62 +27,82 @@ export const getAccountAnalyticsTool = {
26
27
  const result = await client.getAccountAnalytics(input);
27
28
  const { account, range, summary, snapshots } = result;
28
29
 
29
- const lines: string[] = [
30
- `Analytics for @${account.username} (${account.platform}, id ${account.id})`,
31
- `Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
32
- '',
33
- 'Summary:',
34
- ];
30
+ const metricRows = account.platform === 'google_business'
31
+ ? googleBusinessMetricRows(summary)
32
+ : genericMetricRows(summary, account.platform);
35
33
 
36
- if (account.platform === 'google_business') {
37
- const metrics = summary.platform_metrics || {};
38
- pushMetric(lines, 'Profile views', metrics.profile_views ?? summary.total_views);
39
- pushMetric(lines, 'Search views', metrics.search_views ?? summary.total_reach);
40
- pushMetric(lines, 'Maps views', metrics.maps_views ?? summary.total_profile_views);
41
- pushMetric(lines, 'Customer actions', metrics.customer_actions ?? summary.total_accounts_engaged);
42
- pushMetric(lines, 'Posts', metrics.posts ?? summary.current_media_count);
43
- lines.push(`• Engagement rate (actions / profile views): ${summary.engagement_rate}%`);
44
- pushMetric(lines, 'Website clicks', metrics.website_clicks ?? summary.total_website_clicks);
45
- pushMetric(lines, 'Call clicks', metrics.call_clicks ?? summary.total_call_clicks);
46
- pushMetric(lines, 'Direction requests', metrics.direction_requests ?? summary.total_direction_requests);
47
- pushMetric(lines, 'Conversations', metrics.conversations ?? summary.total_conversations);
48
- pushMetric(lines, 'Bookings', metrics.bookings ?? summary.total_bookings);
49
- } else if (account.platform === 'pinterest') {
50
- pushGenericSummary(lines, summary);
51
- pushMetric(lines, 'Outbound clicks', summary.total_website_clicks);
52
- } else if (account.platform === 'youtube') {
53
- pushGenericSummary(lines, summary);
54
- pushMetric(lines, 'Watch minutes', summary.total_watch_minutes);
55
- pushMetric(lines, 'Likes', summary.total_likes);
56
- } else {
57
- pushGenericSummary(lines, summary);
58
- }
59
-
60
- return lines.join('\n');
34
+ return [
35
+ mdTitle(`📊 Analytics for @${account.username}`, `${account.platform} · ${range.from} → ${range.to}`),
36
+ mdKeyValue([
37
+ ['Account ID', account.id],
38
+ ['Daily snapshots', snapshots.length],
39
+ ]),
40
+ mdSection('Summary', mdTable(['Metric', 'Value'], metricRows)),
41
+ mdSection('Quick read', mdBullets(buildInsights(summary, account.platform))),
42
+ ].filter(Boolean).join('\n\n');
61
43
  },
62
44
  };
63
45
 
64
- function formatDelta(n: number): string {
65
- return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
46
+ function genericMetricRows(summary: AccountAnalyticsSummary, platform: string) {
47
+ const rows: Array<[string, string]> = [
48
+ ['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
49
+ ['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
50
+ ['Total reach', formatNumber(summary.total_reach)],
51
+ ['Total views', formatNumber(summary.total_views)],
52
+ ['Profile views', formatNumber(summary.total_profile_views)],
53
+ ['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
54
+ ['Engagement rate by reach', formatPercent(summary.engagement_rate)],
55
+ ['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
56
+ ];
57
+
58
+ if (platform === 'pinterest') {
59
+ rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
60
+ }
61
+ if (platform === 'youtube') {
62
+ rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
63
+ rows.push(['Likes', formatNumber(summary.total_likes)]);
64
+ }
65
+
66
+ return rows;
66
67
  }
67
68
 
68
- function pushGenericSummary(lines: string[], summary: AccountAnalyticsSummary) {
69
- lines.push(
70
- `• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`,
71
- `• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`
72
- );
73
- pushMetric(lines, 'Total reach', summary.total_reach);
74
- pushMetric(lines, 'Total views', summary.total_views);
75
- pushMetric(lines, 'Profile views', summary.total_profile_views);
76
- pushMetric(lines, 'Total accounts engaged', summary.total_accounts_engaged);
77
- lines.push(
78
- `• Engagement rate (by reach): ${summary.engagement_rate}%`,
79
- `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`
80
- );
69
+ function googleBusinessMetricRows(summary: AccountAnalyticsSummary) {
70
+ const metrics = summary.platform_metrics || {};
71
+ return [
72
+ ['Profile views', formatNumber(metrics.profile_views ?? summary.total_views)],
73
+ ['Search views', formatNumber(metrics.search_views ?? summary.total_reach)],
74
+ ['Maps views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
75
+ ['Customer actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
76
+ ['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
77
+ ['Engagement rate (actions / profile views)', formatPercent(summary.engagement_rate)],
78
+ ['Website clicks', formatNumber(metrics.website_clicks ?? summary.total_website_clicks)],
79
+ ['Call clicks', formatNumber(metrics.call_clicks ?? summary.total_call_clicks)],
80
+ ['Direction requests', formatNumber(metrics.direction_requests ?? summary.total_direction_requests)],
81
+ ['Conversations', formatNumber(metrics.conversations ?? summary.total_conversations)],
82
+ ['Bookings', formatNumber(metrics.bookings ?? summary.total_bookings)],
83
+ ];
81
84
  }
82
85
 
83
- function pushMetric(lines: string[], label: string, value: number | null | undefined) {
84
- if (value != null) {
85
- lines.push(`• ${label}: ${value.toLocaleString()}`);
86
+ function buildInsights(summary: AccountAnalyticsSummary, platform: string): string[] {
87
+ const insights: string[] = [];
88
+
89
+ if (summary.followers_change > 0) {
90
+ insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
91
+ } else if (summary.followers_change < 0) {
92
+ insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
86
93
  }
94
+
95
+ if (summary.engagement_rate != null) {
96
+ insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
97
+ }
98
+
99
+ if (platform === 'google_business' && summary.total_website_clicks != null) {
100
+ insights.push(`${formatNumber(summary.total_website_clicks)} website clicks came from the profile.`);
101
+ }
102
+
103
+ if (platform === 'youtube' && summary.total_watch_minutes != null) {
104
+ insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
105
+ }
106
+
107
+ return insights.length > 0 ? insights : ['No standout movement detected in the returned period.'];
87
108
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, compactText, formatDateTime, mdKeyValue, mdSection, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const getBrandProfileTool = {
5
6
  name: 'get_brand_profile',
@@ -16,48 +17,43 @@ export const getBrandProfileTool = {
16
17
  const result = await client.getBrandProfile(input.brand_id);
17
18
  const profile = result.brand_profile as Record<string, any>;
18
19
 
19
- const lines = [
20
- `Brand profile: ${profile.brand_name}`,
21
- `• Source: ${profile.source}`,
22
- ];
20
+ const profileBody = [
21
+ mdKeyValue([
22
+ ['Source', profile.source],
23
+ ['Profile ID', profile.id ? code(profile.id) : undefined],
24
+ ['Workspace client ID', profile.workspace_client_id ? code(profile.workspace_client_id) : undefined],
25
+ ['Legacy brand group ID', profile.brand_group_id ? code(profile.brand_group_id) : undefined],
26
+ ['Logo', profile.logo_url],
27
+ ['Last refreshed', profile.last_context_refresh_at ? formatDateTime(profile.last_context_refresh_at) : undefined],
28
+ ]),
29
+ mdSection('Voice and audience', mdKeyValue([
30
+ ['Tone of voice', compactText(profile.tone_of_voice, 180)],
31
+ ['Audience', compactText(profile.audience, 180)],
32
+ ['Brand values', compactText(profile.brand_values, 180)],
33
+ ['Brand story', compactText(formatStructuredValue(profile.brand_story), 240)],
34
+ ])),
35
+ mdSection('Guidance', mdKeyValue([
36
+ ['Custom instructions', compactText(profile.custom_instructions, 240)],
37
+ ['Keywords', formatStructuredValue(profile.keywords)],
38
+ ['Competitors', formatStructuredValue(profile.competitors)],
39
+ ['Do / Don’ts', formatStructuredValue(profile.do_donts)],
40
+ ['Topics to cover', formatStructuredValue(profile.topics_to_cover)],
41
+ ['Topics to avoid', formatStructuredValue(profile.topics_to_avoid)],
42
+ ])),
43
+ mdSection('Creative references', mdKeyValue([
44
+ ['Visual guidelines', formatStructuredValue(profile.visual_guidelines)],
45
+ ['Example posts', formatStructuredValue(profile.example_posts)],
46
+ ['Voice examples', formatStructuredValue(profile.voice_examples)],
47
+ ])),
48
+ ].filter(Boolean).join('\n\n');
23
49
 
24
- if (profile.workspace_client_id) lines.push(`• Workspace client ID: ${profile.workspace_client_id}`);
25
- if (profile.brand_group_id) lines.push(`• Legacy brand group ID: ${profile.brand_group_id}`);
26
- if (profile.tone_of_voice) lines.push(`• Tone of voice: ${profile.tone_of_voice}`);
27
- if (profile.audience) lines.push(`• Audience: ${profile.audience}`);
28
- if (profile.brand_values) lines.push(`• Brand values: ${profile.brand_values}`);
29
- if (profile.custom_instructions) lines.push(`• Custom instructions: ${profile.custom_instructions}`);
30
- if (profile.logo_url) lines.push(`• Logo URL: ${profile.logo_url}`);
31
-
32
- addStructuredLine(lines, 'Keywords', profile.keywords);
33
- addStructuredLine(lines, 'Competitors', profile.competitors);
34
- addStructuredLine(lines, 'Do / Don’ts', profile.do_donts);
35
- addStructuredLine(lines, 'Visual guidelines', profile.visual_guidelines);
36
- addStructuredLine(lines, 'Brand story', profile.brand_story);
37
- addStructuredLine(lines, 'Example posts', profile.example_posts);
38
- addStructuredLine(lines, 'Topics to cover', profile.topics_to_cover);
39
- addStructuredLine(lines, 'Topics to avoid', profile.topics_to_avoid);
40
- addStructuredLine(lines, 'Voice examples', profile.voice_examples);
41
-
42
- if (profile.last_context_refresh_at) {
43
- lines.push(`• Last refreshed: ${profile.last_context_refresh_at}`);
44
- }
45
-
46
- if (lines.length === 2) {
47
- lines.push('• No extended brand profile fields have been saved yet.');
48
- }
49
-
50
- return lines.join('\n');
50
+ return [
51
+ mdTitle(`Brand profile: ${profile.brand_name}`),
52
+ profileBody || 'No extended brand profile fields have been saved yet.',
53
+ ].join('\n\n');
51
54
  },
52
55
  };
53
56
 
54
- function addStructuredLine(lines: string[], label: string, value: unknown) {
55
- const formatted = formatStructuredValue(value);
56
- if (formatted) {
57
- lines.push(`• ${label}: ${formatted}`);
58
- }
59
- }
60
-
61
57
  function formatStructuredValue(value: unknown): string | null {
62
58
  if (value == null) return null;
63
59
  if (typeof value === 'string') {