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,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, formatDateTime, mdKeyValue, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const getBrandTool = {
5
6
  name: 'get_brand',
@@ -16,19 +17,18 @@ export const getBrandTool = {
16
17
  const result = await client.getBrand(input.brand_id);
17
18
  const brand = result.brand as Record<string, any>;
18
19
 
19
- const lines = [
20
- `Brand: ${brand.name}`,
21
- `• ID: ${brand.id}`,
22
- `• Source: ${brand.source}`,
23
- `• Workspace: ${brand.workspace_id || 'N/A'}`,
24
- `• Accounts assigned: ${brand.account_count ?? 0}`,
25
- ];
26
-
27
- if (brand.workspace_client_id) lines.push(`• Workspace client ID: ${brand.workspace_client_id}`);
28
- if (brand.legacy_brand_group_id) lines.push(`• Legacy brand group ID: ${brand.legacy_brand_group_id}`);
29
- if (brand.created_at) lines.push(`• Created: ${brand.created_at}`);
30
- if (brand.updated_at) lines.push(`• Updated: ${brand.updated_at}`);
31
-
32
- return lines.join('\n');
20
+ return [
21
+ mdTitle(`Brand: ${brand.name}`),
22
+ mdKeyValue([
23
+ ['Brand ID', code(brand.id)],
24
+ ['Source', brand.source],
25
+ ['Workspace', brand.workspace_id ? code(brand.workspace_id) : 'n/a'],
26
+ ['Accounts assigned', brand.account_count ?? 0],
27
+ ['Workspace client ID', brand.workspace_client_id ? code(brand.workspace_client_id) : undefined],
28
+ ['Legacy brand group ID', brand.legacy_brand_group_id ? code(brand.legacy_brand_group_id) : undefined],
29
+ ['Created', formatDateTime(brand.created_at)],
30
+ ['Updated', formatDateTime(brand.updated_at)],
31
+ ]),
32
+ ].join('\n\n');
33
33
  },
34
34
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { ConnectOption, PosterlyClient } from '../lib/api-client.js';
3
3
  import { CONNECT_INPUTS, PLANNED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
+ import { code, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
4
5
 
5
6
  export const getConnectLinkTool = {
6
7
  name: 'get_connect_link',
@@ -31,8 +32,21 @@ export const getConnectLinkTool = {
31
32
  }
32
33
 
33
34
  const options = result.connect_options || [];
34
- const lines = options.map((option) => formatConnectOption(option, false));
35
- return `Posterly connection options (${options.length}):\n${lines.join('\n\n')}`;
35
+ return [
36
+ mdTitle(`Posterly connection options (${options.length})`),
37
+ mdTable(
38
+ ['Provider', 'Target', 'Status', 'Method', 'Connected', 'Readiness'],
39
+ options.map((option) => [
40
+ option.label,
41
+ code(option.platform),
42
+ statusLabel(option.status),
43
+ option.method,
44
+ option.connected_count ?? option.connected_accounts?.length ?? 0,
45
+ option.missing_env?.length ? 'missing config' : 'configured',
46
+ ]),
47
+ ),
48
+ '**Tip:** Call this tool with a specific `platform` to get the direct dashboard handoff URL.',
49
+ ].join('\n\n');
36
50
  },
37
51
  };
38
52
 
@@ -45,12 +59,20 @@ function formatConnectOption(option: ConnectOption, includeRaw: boolean): string
45
59
  ? `; credential fields: ${option.credential_fields.map((field: any) => field.key).join(', ')}`
46
60
  : '';
47
61
  const scopes = option.scopes?.length ? `; scopes: ${option.scopes.join(', ')}` : '';
48
- const raw = includeRaw ? `\n\nRaw connect option:\n${JSON.stringify(option, null, 2)}` : '';
62
+ const raw = includeRaw ? mdJson('Raw connect option', option) : '';
49
63
 
50
64
  return [
51
- `${option.label} (${option.platform})`,
52
- `Status: ${option.status}; method: ${option.method}; ${missing}; connected accounts: ${accounts}`,
53
- `Connection URL: ${option.connection_url || 'not available'}${fields}${scopes}`,
65
+ mdTitle(`${option.label} connection`, code(option.platform)),
66
+ mdKeyValue([
67
+ ['Status', statusLabel(option.status)],
68
+ ['Method', option.method],
69
+ ['Readiness', missing],
70
+ ['Connected accounts', accounts],
71
+ ['Connection URL', option.connection_url || 'not available'],
72
+ ['Credential fields', fields.replace(/^; credential fields: /, '') || undefined],
73
+ ['Scopes', scopes.replace(/^; scopes: /, '') || undefined],
74
+ ]),
75
+ option.notes?.length ? mdSection('Notes', option.notes.map((note) => `- ${note}`).join('\n')) : '',
54
76
  raw,
55
- ].join('\n');
77
+ ].filter(Boolean).join('\n\n');
56
78
  }
@@ -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 getGoogleBusinessReviewLinkTool = {
5
6
  name: 'get_google_business_review_link',
@@ -20,11 +21,10 @@ export const getGoogleBusinessReviewLinkTool = {
20
21
  },
21
22
  ) {
22
23
  const result = await client.getGoogleBusinessReviewLink(input);
23
- return [
24
- `Review link for ${result.businessName}:`,
25
- result.reviewLink,
26
- `Place ID: ${result.placeId}`,
27
- `Account ID: ${result.account_id}`,
28
- ].join('\n');
24
+ return mdSuccess(`Review link for ${result.businessName}`, [
25
+ ['Review link', result.reviewLink],
26
+ ['Place ID', code(result.placeId)],
27
+ ['Account ID', code(result.account_id)],
28
+ ]);
29
29
  },
30
30
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
3
  import { ALL_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
4
+ import { code, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
4
5
 
5
6
  export const getPlatformSchemaTool = {
6
7
  name: 'get_platform_schema',
@@ -24,29 +25,41 @@ export const getPlatformSchemaTool = {
24
25
  ) {
25
26
  const result = await client.getPlatformSchema(input);
26
27
  const schema = result.schema || result.platform || result.platforms?.[0];
27
- if (!schema) return JSON.stringify(result, null, 2);
28
+ if (!schema) return mdJson('Platform schema response', result);
28
29
 
29
30
  const fields = schema.settings_schema?.fields || [];
30
31
  const helpers = schema.helper_tools || [];
31
- const accountLine = result.account
32
- ? `Account: ${(result.account.username as string) || result.account.id} (${result.account.platform})\n`
33
- : '';
34
32
 
35
33
  return [
36
- `${schema.label} (${schema.id}) schema`,
37
- accountLine.trim(),
38
- `Status: ${schema.status}`,
39
- `Post types: ${schema.post_types.join(', ') || 'n/a'}`,
40
- `Content limit: ${schema.content_limit ?? 'n/a'}`,
41
- `Media: ${JSON.stringify(schema.media)}`,
42
- `Analytics: ${schema.analytics_supported ? 'yes' : 'no'}`,
34
+ mdTitle(`${schema.label} schema`, code(schema.id)),
35
+ mdKeyValue([
36
+ ['Account', result.account ? `${(result.account.username as string) || result.account.id} (${result.account.platform})` : undefined],
37
+ ['Status', statusLabel(schema.status)],
38
+ ['Post types', schema.post_types.join(', ') || 'n/a'],
39
+ ['Content limit', schema.content_limit ?? 'n/a'],
40
+ ['Analytics', schema.analytics_supported],
41
+ ]),
43
42
  fields.length
44
- ? `Settings:\n${fields.map((field: any) => `• ${field.key}${field.required ? ' (required)' : ''}: ${field.description}`).join('\n')}`
45
- : 'Settings: none',
43
+ ? mdSection('Settings', mdTable(
44
+ ['Field', 'Required', 'Description'],
45
+ fields.map((field: any) => [
46
+ code(field.key),
47
+ Boolean(field.required),
48
+ field.description || field.label || 'n/a',
49
+ ]),
50
+ ))
51
+ : mdSection('Settings', 'None'),
46
52
  helpers.length
47
- ? `Helpers:\n${helpers.map((helper: any) => `• ${helper.name}: ${helper.description}`).join('\n')}`
48
- : 'Helpers: none',
49
- `Example settings: ${JSON.stringify(schema.settings_schema?.example || {})}`,
53
+ ? mdSection('Helpers', mdTable(
54
+ ['Helper', 'Description'],
55
+ helpers.map((helper: any) => [
56
+ code(helper.name),
57
+ helper.description || 'n/a',
58
+ ]),
59
+ ))
60
+ : mdSection('Helpers', 'None'),
61
+ mdJson('Media rules', schema.media),
62
+ mdJson('Example settings', schema.settings_schema?.example || {}),
50
63
  ].filter(Boolean).join('\n');
51
64
  },
52
65
  };
@@ -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, formatNumber, mdEmpty, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const getPostAnalyticsTool = {
5
6
  name: 'get_post_analytics',
@@ -40,59 +41,65 @@ export const getPostAnalyticsTool = {
40
41
  const { account, range, posts, total } = result;
41
42
 
42
43
  if (posts.length === 0) {
43
- return `No analytics found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`;
44
+ return mdEmpty('post analytics', `No post analytics were found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`);
44
45
  }
45
46
 
46
- const lines: string[] = [
47
- `Post analytics for @${account.username} (${account.platform})`,
48
- `Range: ${range.from} ${range.to} • Showing ${posts.length} of ${total}`,
49
- '',
50
- ];
47
+ const totals = posts.reduce(
48
+ (acc, post) => ({
49
+ likes: acc.likes + post.likes,
50
+ comments: acc.comments + post.comments,
51
+ reach: acc.reach + post.reach,
52
+ impressions: acc.impressions + post.impressions,
53
+ shares: acc.shares + post.shares,
54
+ saves: acc.saves + post.saved,
55
+ }),
56
+ { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, saves: 0 },
57
+ );
51
58
 
52
- for (const p of posts) {
53
- const postedAt = p.posted_at
54
- ? new Date(p.posted_at).toLocaleString()
55
- : 'unknown date';
56
- const caption = p.caption_snippet
57
- ? p.caption_snippet.length > 60
58
- ? `${p.caption_snippet.slice(0, 60)}…`
59
- : p.caption_snippet
60
- : '(no caption)';
61
- const metrics = [
62
- `${p.likes} likes`,
63
- `${p.comments} comments`,
64
- `${p.reach.toLocaleString()} reach`,
65
- ];
66
- if (p.impressions) metrics.push(`${p.impressions.toLocaleString()} impressions`);
67
- if (p.saved) metrics.push(`${p.saved} saved`);
68
- if (p.shares) metrics.push(`${p.shares} shares`);
69
- if (p.plays) metrics.push(`${p.plays.toLocaleString()} plays`);
70
- if (account.platform === 'youtube' && p.total_watch_time_ms) {
71
- metrics.push(`${Math.round(p.total_watch_time_ms / 60000).toLocaleString()} watch minutes`);
72
- }
73
- if (account.platform === 'youtube' && p.avg_watch_time_ms) {
74
- metrics.push(`${Math.round(p.avg_watch_time_ms / 1000)}s avg view`);
75
- }
76
- if (p.url_link_clicks) {
77
- metrics.push(
78
- account.platform === 'pinterest'
79
- ? `${p.url_link_clicks.toLocaleString()} outbound clicks`
80
- : `${p.url_link_clicks.toLocaleString()} link clicks`
81
- );
82
- }
83
- if (p.user_profile_clicks) {
84
- metrics.push(
85
- account.platform === 'pinterest'
86
- ? `${p.user_profile_clicks.toLocaleString()} pin clicks`
87
- : `${p.user_profile_clicks.toLocaleString()} profile clicks`
88
- );
89
- }
90
-
91
- lines.push(`• [${postedAt}] ${caption}`);
92
- lines.push(` ${metrics.join(' • ')}`);
93
- if (p.permalink) lines.push(` ${p.permalink}`);
94
- }
95
-
96
- return lines.join('\n');
59
+ return [
60
+ mdTitle(`📈 Post analytics for @${account.username}`, `${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}`),
61
+ mdSection('Returned totals', mdTable(
62
+ ['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'],
63
+ [[totals.likes, totals.comments, totals.reach, totals.impressions, totals.shares, totals.saves]],
64
+ )),
65
+ mdSection('Posts', mdTable(
66
+ ['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'],
67
+ posts.map((post) => [
68
+ formatDateTime(post.posted_at),
69
+ post.id ? code(post.id) : code(post.platform_media_id),
70
+ compactText(post.caption_snippet, 70) || '(no caption)',
71
+ formatNumber(post.reach),
72
+ formatNumber(post.likes),
73
+ formatNumber(post.comments),
74
+ buildExtras(account.platform, post),
75
+ ]),
76
+ )),
77
+ ].join('\n\n');
97
78
  },
98
79
  };
80
+
81
+ function buildExtras(platform: string, post: any): string {
82
+ const metrics: string[] = [];
83
+ if (post.impressions) metrics.push(`${formatNumber(post.impressions)} impressions`);
84
+ if (post.saved) metrics.push(`${formatNumber(post.saved)} saved`);
85
+ if (post.shares) metrics.push(`${formatNumber(post.shares)} shares`);
86
+ if (post.plays) metrics.push(`${formatNumber(post.plays)} plays`);
87
+ if (platform === 'youtube' && post.total_watch_time_ms) {
88
+ metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
89
+ }
90
+ if (platform === 'youtube' && post.avg_watch_time_ms) {
91
+ metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
92
+ }
93
+ if (post.url_link_clicks) {
94
+ metrics.push(platform === 'pinterest'
95
+ ? `${formatNumber(post.url_link_clicks)} outbound clicks`
96
+ : `${formatNumber(post.url_link_clicks)} link clicks`);
97
+ }
98
+ if (post.user_profile_clicks) {
99
+ metrics.push(platform === 'pinterest'
100
+ ? `${formatNumber(post.user_profile_clicks)} pin clicks`
101
+ : `${formatNumber(post.user_profile_clicks)} profile clicks`);
102
+ }
103
+ if (post.permalink) metrics.push(post.permalink);
104
+ return metrics.join(' · ') || 'n/a';
105
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdJson, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const getPostMissingTool = {
5
6
  name: 'get_post_missing',
@@ -11,7 +12,9 @@ export const getPostMissingTool = {
11
12
 
12
13
  async execute(client: PosterlyClient, input: { post_id: number }) {
13
14
  const result = await client.getPostMissing(input.post_id);
14
- return JSON.stringify(result, null, 2);
15
+ return [
16
+ mdTitle(`Post readiness for ${code(input.post_id)}`),
17
+ mdJson('Missing fields response', result),
18
+ ].join('\n\n');
15
19
  },
16
20
  };
17
-
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, formatDateTime, mdKeyValue, mdQuote, mdSection, mdTitle, statusLabel } from '../lib/format.js';
3
4
 
4
5
  export const getPostTool = {
5
6
  name: 'get_post',
@@ -16,18 +17,20 @@ export const getPostTool = {
16
17
  const result = await client.getPost(input.post_id);
17
18
  const p = result.post as Record<string, any>;
18
19
 
19
- const lines = [
20
- `Post #${p.id}`,
21
- `• Status: ${p.status}`,
22
- `• Caption: ${p.content || '(empty)'}`,
23
- `• Type: ${p.post_type || 'unknown'}`,
24
- `• Scheduled: ${p.scheduled_at ? new Date(p.scheduled_at).toLocaleString() : 'N/A'}`,
25
- ];
26
-
27
- if (p.platform_post_url) lines.push(`• Published URL: ${p.platform_post_url}`);
28
- if (p.media_url) lines.push(`• Media: ${p.media_url}`);
29
- if (p.media_urls?.length) lines.push(`• Media (${p.media_urls.length}): ${p.media_urls.join(', ')}`);
30
-
31
- return lines.join('\n');
20
+ return [
21
+ mdTitle(`Post ${code(p.id)}`),
22
+ mdKeyValue([
23
+ ['Status', statusLabel(p.status)],
24
+ ['Type', p.post_type || 'unknown'],
25
+ ['Scheduled', formatDateTime(p.scheduled_at)],
26
+ ['Published URL', p.platform_post_url],
27
+ ['Media', p.media_url],
28
+ ['Media count', p.media_urls?.length],
29
+ ]),
30
+ mdSection('Caption', p.content ? mdQuote(p.content) : '_(empty)_'),
31
+ p.media_urls?.length
32
+ ? mdSection('Media URLs', p.media_urls.map((url: string, index: number) => `${index + 1}. ${url}`).join('\n'))
33
+ : '',
34
+ ].filter(Boolean).join('\n\n');
32
35
  },
33
36
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient, VideoJob } from '../lib/api-client.js';
3
+ import { code, compactText, formatDateTime, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
3
4
 
4
5
  export const getVideoJobTool = {
5
6
  name: 'get_video_job',
@@ -17,20 +18,39 @@ export const getVideoJobTool = {
17
18
 
18
19
  const jobs = result.jobs || [];
19
20
  return [
20
- `Video jobs (${jobs.length})`,
21
- jobs.map((job) => formatJob(job, false)).join('\n\n'),
21
+ mdTitle(`Video jobs (${jobs.length})`),
22
+ mdTable(
23
+ ['Job ID', 'Status', 'Prompt', 'Model', 'Created', 'Cost'],
24
+ jobs.map((job) => [
25
+ code(job.id),
26
+ statusLabel(job.status),
27
+ compactText(job.prompt, 70),
28
+ job.model,
29
+ formatDateTime(job.created_at),
30
+ job.credit_cost,
31
+ ]),
32
+ ),
22
33
  ].filter(Boolean).join('\n');
23
34
  },
24
35
  };
25
36
 
26
37
  function formatJob(job: VideoJob, includeRaw: boolean): string {
27
- const lines = [
28
- `${job.id} ${job.status}`,
29
- `Prompt: ${job.prompt}`,
30
- `Model: ${job.model}; duration: ${job.duration_seconds}s; resolution: ${job.resolution || 'n/a'}; cost: ${job.credit_cost}`,
31
- job.video_url ? `Video URL: ${job.video_url}` : null,
32
- job.error_message ? `Error: ${job.error_message}` : null,
33
- includeRaw ? `\nRaw job:\n${JSON.stringify(job, null, 2)}` : null,
34
- ];
35
- return lines.filter(Boolean).join('\n');
38
+ return [
39
+ mdTitle(`Video job ${code(job.id)}`),
40
+ mdKeyValue([
41
+ ['Status', statusLabel(job.status)],
42
+ ['Model', job.model],
43
+ ['Duration', `${job.duration_seconds}s`],
44
+ ['Aspect ratio', job.aspect_ratio],
45
+ ['Resolution', job.resolution || 'n/a'],
46
+ ['Credit cost', job.credit_cost],
47
+ ['Created', formatDateTime(job.created_at)],
48
+ ['Started', formatDateTime(job.started_at)],
49
+ ['Completed', formatDateTime(job.completed_at)],
50
+ ['Video URL', job.video_url || undefined],
51
+ ['Error', job.error_message || undefined],
52
+ ]),
53
+ mdSection('Prompt', job.prompt),
54
+ includeRaw ? mdJson('Raw job', job) : '',
55
+ ].filter(Boolean).join('\n\n');
36
56
  }
@@ -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, mdSection, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const getVideoOptionsTool = {
5
6
  name: 'get_video_options',
@@ -11,20 +12,31 @@ export const getVideoOptionsTool = {
11
12
  const options = await client.getVideoOptions();
12
13
  const models = Array.isArray(options.models) ? options.models : [];
13
14
  const inputModes = Array.isArray(options.input_modes) ? options.input_modes : [];
14
- const lines = [
15
- 'Video generation options',
16
- `Provider: ${options.provider || 'unknown'}`,
17
- `Public generation status: ${options.public_generation_status || 'unknown'}`,
18
- '',
19
- `Models: ${models.map((model: any) => model.id).filter(Boolean).join(', ') || 'none'}`,
20
- `Durations: ${(options.durations_seconds || []).join(', ')} seconds`,
21
- `Resolutions: ${(options.resolutions || []).join(', ')}`,
22
- `Aspect ratios: ${(options.aspect_ratios || []).join(', ')}`,
23
- `Input modes: ${inputModes.map((mode: any) => mode.id).filter(Boolean).join(', ') || 'none'}`,
24
- '',
25
- 'Raw options:',
26
- JSON.stringify(options, null, 2),
27
- ];
28
- return lines.join('\n');
15
+ return [
16
+ mdTitle('Video generation options'),
17
+ mdKeyValue([
18
+ ['Provider', options.provider || 'unknown'],
19
+ ['Public generation status', options.public_generation_status || 'unknown'],
20
+ ['Durations', `${(options.durations_seconds || []).join(', ')} seconds`],
21
+ ['Resolutions', (options.resolutions || []).join(', ')],
22
+ ['Aspect ratios', (options.aspect_ratios || []).join(', ')],
23
+ ]),
24
+ mdSection('Models', mdTable(
25
+ ['Model', 'Label', 'Credit cost'],
26
+ models.map((model: any) => [
27
+ code(model.id),
28
+ model.label || model.name || 'n/a',
29
+ model.credit_cost ?? model.cost ?? 'n/a',
30
+ ]),
31
+ )),
32
+ mdSection('Input modes', mdTable(
33
+ ['Mode', 'Label'],
34
+ inputModes.map((mode: any) => [
35
+ code(mode.id),
36
+ mode.label || mode.name || 'n/a',
37
+ ]),
38
+ )),
39
+ mdJson('Raw options', options),
40
+ ].filter(Boolean).join('\n\n');
29
41
  },
30
42
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdKeyValue, mdTitle, statusLabel } from '../lib/format.js';
3
4
 
4
5
  export const getXPostingQuotaTool = {
5
6
  name: 'get_x_posting_quota',
@@ -12,14 +13,16 @@ export const getXPostingQuotaTool = {
12
13
  async execute(client: PosterlyClient, input: { workspace_id?: string }) {
13
14
  const quota = await client.getXPostingQuota(input);
14
15
  return [
15
- 'X posting quota',
16
- `• Workspace: ${quota.workspace_id || 'default'}`,
17
- `• Tier: ${quota.tier || 'unknown'}`,
18
- `• Total posts: ${quota.total_posts}`,
19
- `• Used posts: ${quota.used_posts}`,
20
- `• Remaining posts: ${quota.remaining_posts}`,
21
- `• URL posting blocked: ${quota.urls_blocked ? 'yes' : 'no'}`,
22
- `• Period: ${quota.current_period_start} -> ${quota.current_period_end}`,
23
- ].join('\n');
16
+ mdTitle('X posting quota'),
17
+ mdKeyValue([
18
+ ['Workspace', quota.workspace_id ? code(quota.workspace_id) : 'default'],
19
+ ['Tier', quota.tier || 'unknown'],
20
+ ['Total posts', quota.total_posts],
21
+ ['Used posts', quota.used_posts],
22
+ ['Remaining posts', quota.remaining_posts],
23
+ ['URL posting', statusLabel(quota.urls_blocked ? 'blocked' : 'available')],
24
+ ['Period', `${quota.current_period_start} → ${quota.current_period_end}`],
25
+ ]),
26
+ ].join('\n\n');
24
27
  },
25
28
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const listAccountsTool = {
5
6
  name: 'list_accounts',
@@ -17,13 +18,21 @@ export const listAccountsTool = {
17
18
 
18
19
  if (accounts.length === 0) {
19
20
  return input.workspace_id
20
- ? 'No connected accounts found in this workspace.'
21
- : 'No connected accounts found. Connect accounts in the posterly dashboard first.';
21
+ ? mdEmpty('connected accounts', 'This workspace does not have any social accounts connected yet.')
22
+ : mdEmpty('connected accounts', 'Posterly does not see any connected social accounts yet.', 'Connect accounts in the Posterly dashboard, then run this again.');
22
23
  }
23
24
 
24
- const lines = accounts.map(
25
- (a) => `• ${a.platform} — @${a.username} (ID: ${a.id}${a.workspace_id ? `, ws: ${a.workspace_id}` : ''})`
26
- );
27
- return `Connected accounts (${accounts.length}):\n${lines.join('\n')}`;
25
+ return [
26
+ mdTitle(`Connected accounts (${accounts.length})`),
27
+ mdTable(
28
+ ['Platform', 'Account', 'Account ID', 'Workspace'],
29
+ accounts.map((account) => [
30
+ account.platform,
31
+ `@${account.username}`,
32
+ code(account.id),
33
+ account.workspace_id ? code(account.workspace_id) : 'default',
34
+ ]),
35
+ ),
36
+ ].join('\n\n');
28
37
  },
29
38
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { compactText, formatDateTime, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const listActivityTool = {
5
6
  name: 'list_activity',
@@ -25,27 +26,22 @@ export const listActivityTool = {
25
26
  ) {
26
27
  const result = await client.listActivity(input);
27
28
  if (result.items.length === 0) {
28
- return 'No activity found matching your filters.';
29
+ return mdEmpty('activity', 'No activity matched those filters.');
29
30
  }
30
31
 
31
- const lines = [
32
- `Activity (${result.items.length} of ${result.total}):`,
33
- ];
34
-
35
- for (const item of result.items) {
36
- const when = item.created_at ? new Date(item.created_at).toLocaleString() : 'unknown time';
37
- if (item.type === 'publish') {
38
- const outcome = item.outcome || 'publish';
39
- const platform = item.platform ? ` on ${item.platform}` : '';
40
- const detail = item.error_message ? ` - ${item.error_message}` : '';
41
- lines.push(`- [${when}] post #${item.post_id || '?'} ${outcome}${platform}${detail}`);
42
- } else {
43
- const label = item.description || item.action || 'activity';
44
- lines.push(`- [${when}] post #${item.post_id || '?'} ${label}`);
45
- }
46
- }
47
-
48
- return lines.join('\n');
32
+ return [
33
+ mdTitle(`Activity (${result.items.length} of ${result.total})`),
34
+ mdTable(
35
+ ['When', 'Source', 'Post', 'Platform', 'Event', 'Detail'],
36
+ result.items.map((item) => [
37
+ formatDateTime(item.created_at),
38
+ item.type,
39
+ item.post_id ?? '?',
40
+ item.platform || 'n/a',
41
+ item.type === 'publish' ? item.outcome || 'publish' : item.action || 'activity',
42
+ compactText(item.error_message || item.description, 120) || 'n/a',
43
+ ]),
44
+ ),
45
+ ].join('\n\n');
49
46
  },
50
47
  };
51
-
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ import { code, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
3
4
 
4
5
  export const listBrandAccountsTool = {
5
6
  name: 'list_brand_accounts',
@@ -16,13 +17,20 @@ export const listBrandAccountsTool = {
16
17
  const accounts = await client.listBrandAccounts(input.brand_id);
17
18
 
18
19
  if (accounts.length === 0) {
19
- return 'No social accounts are currently assigned to this brand.';
20
+ return mdEmpty('brand accounts', 'No social accounts are currently assigned to this brand.');
20
21
  }
21
22
 
22
- const lines = accounts.map(
23
- (account) => `• ${account.platform} — @${account.username} (ID: ${account.id}${account.workspace_id ? `, ws: ${account.workspace_id}` : ''})`
24
- );
25
-
26
- return `Brand accounts (${accounts.length}):\n${lines.join('\n')}`;
23
+ return [
24
+ mdTitle(`Brand accounts (${accounts.length})`),
25
+ mdTable(
26
+ ['Platform', 'Account', 'Account ID', 'Workspace'],
27
+ accounts.map((account) => [
28
+ account.platform,
29
+ `@${account.username}`,
30
+ code(account.id),
31
+ account.workspace_id ? code(account.workspace_id) : 'default',
32
+ ]),
33
+ ),
34
+ ].join('\n\n');
27
35
  },
28
36
  };