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, formatDateTime, mdKeyValue, mdTitle } from '../lib/format.js';
2
3
  export const getBrandTool = {
3
4
  name: 'get_brand',
4
5
  description: 'Get one brand/client by ID. Returns its workspace, source, linked legacy brand group if present, and the number of social accounts assigned to it.',
@@ -8,21 +9,18 @@ export const getBrandTool = {
8
9
  async execute(client, input) {
9
10
  const result = await client.getBrand(input.brand_id);
10
11
  const brand = result.brand;
11
- const lines = [
12
- `Brand: ${brand.name}`,
13
- `• ID: ${brand.id}`,
14
- `• Source: ${brand.source}`,
15
- `• Workspace: ${brand.workspace_id || 'N/A'}`,
16
- `• Accounts assigned: ${brand.account_count ?? 0}`,
17
- ];
18
- if (brand.workspace_client_id)
19
- lines.push(`• Workspace client ID: ${brand.workspace_client_id}`);
20
- if (brand.legacy_brand_group_id)
21
- lines.push(`• Legacy brand group ID: ${brand.legacy_brand_group_id}`);
22
- if (brand.created_at)
23
- lines.push(`• Created: ${brand.created_at}`);
24
- if (brand.updated_at)
25
- lines.push(`• Updated: ${brand.updated_at}`);
26
- return lines.join('\n');
12
+ return [
13
+ mdTitle(`Brand: ${brand.name}`),
14
+ mdKeyValue([
15
+ ['Brand ID', code(brand.id)],
16
+ ['Source', brand.source],
17
+ ['Workspace', brand.workspace_id ? code(brand.workspace_id) : 'n/a'],
18
+ ['Accounts assigned', brand.account_count ?? 0],
19
+ ['Workspace client ID', brand.workspace_client_id ? code(brand.workspace_client_id) : undefined],
20
+ ['Legacy brand group ID', brand.legacy_brand_group_id ? code(brand.legacy_brand_group_id) : undefined],
21
+ ['Created', formatDateTime(brand.created_at)],
22
+ ['Updated', formatDateTime(brand.updated_at)],
23
+ ]),
24
+ ].join('\n\n');
27
25
  },
28
26
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { CONNECT_INPUTS, PLANNED_PLATFORM_IDS } from '../generated/platform-manifest.js';
3
+ import { code, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
3
4
  export const getConnectLinkTool = {
4
5
  name: 'get_connect_link',
5
6
  description: 'List dashboard handoff links/readiness for connecting social accounts, or get one platform connection URL. Use connection_url in a logged-in browser. Direct OAuth URLs are intentionally not exposed because provider callbacks rely on browser state.',
@@ -23,8 +24,18 @@ export const getConnectLinkTool = {
23
24
  return formatConnectOption(result.connect, true);
24
25
  }
25
26
  const options = result.connect_options || [];
26
- const lines = options.map((option) => formatConnectOption(option, false));
27
- return `Posterly connection options (${options.length}):\n${lines.join('\n\n')}`;
27
+ return [
28
+ mdTitle(`Posterly connection options (${options.length})`),
29
+ mdTable(['Provider', 'Target', 'Status', 'Method', 'Connected', 'Readiness'], options.map((option) => [
30
+ option.label,
31
+ code(option.platform),
32
+ statusLabel(option.status),
33
+ option.method,
34
+ option.connected_count ?? option.connected_accounts?.length ?? 0,
35
+ option.missing_env?.length ? 'missing config' : 'configured',
36
+ ])),
37
+ '**Tip:** Call this tool with a specific `platform` to get the direct dashboard handoff URL.',
38
+ ].join('\n\n');
28
39
  },
29
40
  };
30
41
  function formatConnectOption(option, includeRaw) {
@@ -36,11 +47,19 @@ function formatConnectOption(option, includeRaw) {
36
47
  ? `; credential fields: ${option.credential_fields.map((field) => field.key).join(', ')}`
37
48
  : '';
38
49
  const scopes = option.scopes?.length ? `; scopes: ${option.scopes.join(', ')}` : '';
39
- const raw = includeRaw ? `\n\nRaw connect option:\n${JSON.stringify(option, null, 2)}` : '';
50
+ const raw = includeRaw ? mdJson('Raw connect option', option) : '';
40
51
  return [
41
- `${option.label} (${option.platform})`,
42
- `Status: ${option.status}; method: ${option.method}; ${missing}; connected accounts: ${accounts}`,
43
- `Connection URL: ${option.connection_url || 'not available'}${fields}${scopes}`,
52
+ mdTitle(`${option.label} connection`, code(option.platform)),
53
+ mdKeyValue([
54
+ ['Status', statusLabel(option.status)],
55
+ ['Method', option.method],
56
+ ['Readiness', missing],
57
+ ['Connected accounts', accounts],
58
+ ['Connection URL', option.connection_url || 'not available'],
59
+ ['Credential fields', fields.replace(/^; credential fields: /, '') || undefined],
60
+ ['Scopes', scopes.replace(/^; scopes: /, '') || undefined],
61
+ ]),
62
+ option.notes?.length ? mdSection('Notes', option.notes.map((note) => `- ${note}`).join('\n')) : '',
44
63
  raw,
45
- ].join('\n');
64
+ ].filter(Boolean).join('\n\n');
46
65
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdSuccess } from '../lib/format.js';
2
3
  export const getGoogleBusinessReviewLinkTool = {
3
4
  name: 'get_google_business_review_link',
4
5
  description: 'Get the direct public Google review link for a connected Google Business Profile location. Requires account_id or location_id.',
@@ -9,11 +10,10 @@ export const getGoogleBusinessReviewLinkTool = {
9
10
  }),
10
11
  async execute(client, input) {
11
12
  const result = await client.getGoogleBusinessReviewLink(input);
12
- return [
13
- `Review link for ${result.businessName}:`,
14
- result.reviewLink,
15
- `Place ID: ${result.placeId}`,
16
- `Account ID: ${result.account_id}`,
17
- ].join('\n');
13
+ return mdSuccess(`Review link for ${result.businessName}`, [
14
+ ['Review link', result.reviewLink],
15
+ ['Place ID', code(result.placeId)],
16
+ ['Account ID', code(result.account_id)],
17
+ ]);
18
18
  },
19
19
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ALL_PLATFORM_INPUTS, SUPPORTED_PLATFORM_IDS } from '../generated/platform-manifest.js';
3
+ import { code, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
3
4
  export const getPlatformSchemaTool = {
4
5
  name: 'get_platform_schema',
5
6
  description: 'Get the platform schema for a specific connected account or platform. Call this before creating posts with platform_settings so you know allowed post types, media limits, helper tools, and field names.',
@@ -18,27 +19,33 @@ export const getPlatformSchemaTool = {
18
19
  const result = await client.getPlatformSchema(input);
19
20
  const schema = result.schema || result.platform || result.platforms?.[0];
20
21
  if (!schema)
21
- return JSON.stringify(result, null, 2);
22
+ return mdJson('Platform schema response', result);
22
23
  const fields = schema.settings_schema?.fields || [];
23
24
  const helpers = schema.helper_tools || [];
24
- const accountLine = result.account
25
- ? `Account: ${result.account.username || result.account.id} (${result.account.platform})\n`
26
- : '';
27
25
  return [
28
- `${schema.label} (${schema.id}) schema`,
29
- accountLine.trim(),
30
- `Status: ${schema.status}`,
31
- `Post types: ${schema.post_types.join(', ') || 'n/a'}`,
32
- `Content limit: ${schema.content_limit ?? 'n/a'}`,
33
- `Media: ${JSON.stringify(schema.media)}`,
34
- `Analytics: ${schema.analytics_supported ? 'yes' : 'no'}`,
26
+ mdTitle(`${schema.label} schema`, code(schema.id)),
27
+ mdKeyValue([
28
+ ['Account', result.account ? `${result.account.username || result.account.id} (${result.account.platform})` : undefined],
29
+ ['Status', statusLabel(schema.status)],
30
+ ['Post types', schema.post_types.join(', ') || 'n/a'],
31
+ ['Content limit', schema.content_limit ?? 'n/a'],
32
+ ['Analytics', schema.analytics_supported],
33
+ ]),
35
34
  fields.length
36
- ? `Settings:\n${fields.map((field) => `• ${field.key}${field.required ? ' (required)' : ''}: ${field.description}`).join('\n')}`
37
- : 'Settings: none',
35
+ ? mdSection('Settings', mdTable(['Field', 'Required', 'Description'], fields.map((field) => [
36
+ code(field.key),
37
+ Boolean(field.required),
38
+ field.description || field.label || 'n/a',
39
+ ])))
40
+ : mdSection('Settings', 'None'),
38
41
  helpers.length
39
- ? `Helpers:\n${helpers.map((helper) => `• ${helper.name}: ${helper.description}`).join('\n')}`
40
- : 'Helpers: none',
41
- `Example settings: ${JSON.stringify(schema.settings_schema?.example || {})}`,
42
+ ? mdSection('Helpers', mdTable(['Helper', 'Description'], helpers.map((helper) => [
43
+ code(helper.name),
44
+ helper.description || 'n/a',
45
+ ])))
46
+ : mdSection('Helpers', 'None'),
47
+ mdJson('Media rules', schema.media),
48
+ mdJson('Example settings', schema.settings_schema?.example || {}),
42
49
  ].filter(Boolean).join('\n');
43
50
  },
44
51
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, compactText, formatDateTime, formatNumber, mdEmpty, mdSection, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const getPostAnalyticsTool = {
3
4
  name: 'get_post_analytics',
4
5
  description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first.',
@@ -26,56 +27,58 @@ export const getPostAnalyticsTool = {
26
27
  const result = await client.getPostAnalytics(input);
27
28
  const { account, range, posts, total } = result;
28
29
  if (posts.length === 0) {
29
- return `No analytics found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`;
30
+ return mdEmpty('post analytics', `No post analytics were found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`);
30
31
  }
31
- const lines = [
32
- `Post analytics for @${account.username} (${account.platform})`,
33
- `Range: ${range.from} ${range.to} • Showing ${posts.length} of ${total}`,
34
- '',
35
- ];
36
- for (const p of posts) {
37
- const postedAt = p.posted_at
38
- ? new Date(p.posted_at).toLocaleString()
39
- : 'unknown date';
40
- const caption = p.caption_snippet
41
- ? p.caption_snippet.length > 60
42
- ? `${p.caption_snippet.slice(0, 60)}…`
43
- : p.caption_snippet
44
- : '(no caption)';
45
- const metrics = [
46
- `${p.likes} likes`,
47
- `${p.comments} comments`,
48
- `${p.reach.toLocaleString()} reach`,
49
- ];
50
- if (p.impressions)
51
- metrics.push(`${p.impressions.toLocaleString()} impressions`);
52
- if (p.saved)
53
- metrics.push(`${p.saved} saved`);
54
- if (p.shares)
55
- metrics.push(`${p.shares} shares`);
56
- if (p.plays)
57
- metrics.push(`${p.plays.toLocaleString()} plays`);
58
- if (account.platform === 'youtube' && p.total_watch_time_ms) {
59
- metrics.push(`${Math.round(p.total_watch_time_ms / 60000).toLocaleString()} watch minutes`);
60
- }
61
- if (account.platform === 'youtube' && p.avg_watch_time_ms) {
62
- metrics.push(`${Math.round(p.avg_watch_time_ms / 1000)}s avg view`);
63
- }
64
- if (p.url_link_clicks) {
65
- metrics.push(account.platform === 'pinterest'
66
- ? `${p.url_link_clicks.toLocaleString()} outbound clicks`
67
- : `${p.url_link_clicks.toLocaleString()} link clicks`);
68
- }
69
- if (p.user_profile_clicks) {
70
- metrics.push(account.platform === 'pinterest'
71
- ? `${p.user_profile_clicks.toLocaleString()} pin clicks`
72
- : `${p.user_profile_clicks.toLocaleString()} profile clicks`);
73
- }
74
- lines.push(`• [${postedAt}] ${caption}`);
75
- lines.push(` ${metrics.join(' • ')}`);
76
- if (p.permalink)
77
- lines.push(` ${p.permalink}`);
78
- }
79
- return lines.join('\n');
32
+ const totals = posts.reduce((acc, post) => ({
33
+ likes: acc.likes + post.likes,
34
+ comments: acc.comments + post.comments,
35
+ reach: acc.reach + post.reach,
36
+ impressions: acc.impressions + post.impressions,
37
+ shares: acc.shares + post.shares,
38
+ saves: acc.saves + post.saved,
39
+ }), { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, saves: 0 });
40
+ return [
41
+ mdTitle(`📈 Post analytics for @${account.username}`, `${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}`),
42
+ mdSection('Returned totals', mdTable(['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'], [[totals.likes, totals.comments, totals.reach, totals.impressions, totals.shares, totals.saves]])),
43
+ mdSection('Posts', mdTable(['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'], posts.map((post) => [
44
+ formatDateTime(post.posted_at),
45
+ post.id ? code(post.id) : code(post.platform_media_id),
46
+ compactText(post.caption_snippet, 70) || '(no caption)',
47
+ formatNumber(post.reach),
48
+ formatNumber(post.likes),
49
+ formatNumber(post.comments),
50
+ buildExtras(account.platform, post),
51
+ ]))),
52
+ ].join('\n\n');
80
53
  },
81
54
  };
55
+ function buildExtras(platform, post) {
56
+ const metrics = [];
57
+ if (post.impressions)
58
+ metrics.push(`${formatNumber(post.impressions)} impressions`);
59
+ if (post.saved)
60
+ metrics.push(`${formatNumber(post.saved)} saved`);
61
+ if (post.shares)
62
+ metrics.push(`${formatNumber(post.shares)} shares`);
63
+ if (post.plays)
64
+ metrics.push(`${formatNumber(post.plays)} plays`);
65
+ if (platform === 'youtube' && post.total_watch_time_ms) {
66
+ metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
67
+ }
68
+ if (platform === 'youtube' && post.avg_watch_time_ms) {
69
+ metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
70
+ }
71
+ if (post.url_link_clicks) {
72
+ metrics.push(platform === 'pinterest'
73
+ ? `${formatNumber(post.url_link_clicks)} outbound clicks`
74
+ : `${formatNumber(post.url_link_clicks)} link clicks`);
75
+ }
76
+ if (post.user_profile_clicks) {
77
+ metrics.push(platform === 'pinterest'
78
+ ? `${formatNumber(post.user_profile_clicks)} pin clicks`
79
+ : `${formatNumber(post.user_profile_clicks)} profile clicks`);
80
+ }
81
+ if (post.permalink)
82
+ metrics.push(post.permalink);
83
+ return metrics.join(' · ') || 'n/a';
84
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdJson, mdTitle } from '../lib/format.js';
2
3
  export const getPostMissingTool = {
3
4
  name: 'get_post_missing',
4
5
  description: 'Inspect whether a post is missing required content, media, account, platform settings, or metadata before it can publish. Use this to repair failed or imported posts.',
@@ -7,6 +8,9 @@ export const getPostMissingTool = {
7
8
  }),
8
9
  async execute(client, input) {
9
10
  const result = await client.getPostMissing(input.post_id);
10
- return JSON.stringify(result, null, 2);
11
+ return [
12
+ mdTitle(`Post readiness for ${code(input.post_id)}`),
13
+ mdJson('Missing fields response', result),
14
+ ].join('\n\n');
11
15
  },
12
16
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, formatDateTime, mdKeyValue, mdQuote, mdSection, mdTitle, statusLabel } from '../lib/format.js';
2
3
  export const getPostTool = {
3
4
  name: 'get_post',
4
5
  description: 'Get details of a specific post by ID. Returns caption, status, scheduled time, media, and platform info.',
@@ -8,19 +9,20 @@ export const getPostTool = {
8
9
  async execute(client, input) {
9
10
  const result = await client.getPost(input.post_id);
10
11
  const p = result.post;
11
- const lines = [
12
- `Post #${p.id}`,
13
- `• Status: ${p.status}`,
14
- `• Caption: ${p.content || '(empty)'}`,
15
- `• Type: ${p.post_type || 'unknown'}`,
16
- `• Scheduled: ${p.scheduled_at ? new Date(p.scheduled_at).toLocaleString() : 'N/A'}`,
17
- ];
18
- if (p.platform_post_url)
19
- lines.push(`• Published URL: ${p.platform_post_url}`);
20
- if (p.media_url)
21
- lines.push(`• Media: ${p.media_url}`);
22
- if (p.media_urls?.length)
23
- lines.push(`• Media (${p.media_urls.length}): ${p.media_urls.join(', ')}`);
24
- return lines.join('\n');
12
+ return [
13
+ mdTitle(`Post ${code(p.id)}`),
14
+ mdKeyValue([
15
+ ['Status', statusLabel(p.status)],
16
+ ['Type', p.post_type || 'unknown'],
17
+ ['Scheduled', formatDateTime(p.scheduled_at)],
18
+ ['Published URL', p.platform_post_url],
19
+ ['Media', p.media_url],
20
+ ['Media count', p.media_urls?.length],
21
+ ]),
22
+ mdSection('Caption', p.content ? mdQuote(p.content) : '_(empty)_'),
23
+ p.media_urls?.length
24
+ ? mdSection('Media URLs', p.media_urls.map((url, index) => `${index + 1}. ${url}`).join('\n'))
25
+ : '',
26
+ ].filter(Boolean).join('\n\n');
25
27
  },
26
28
  };
@@ -8,11 +8,11 @@ export declare const getVideoJobTool: {
8
8
  status: z.ZodOptional<z.ZodEnum<["pending", "processing", "completed", "failed", "dismissed"]>>;
9
9
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
10
10
  }, "strip", z.ZodTypeAny, {
11
- status?: "pending" | "processing" | "completed" | "failed" | "dismissed" | undefined;
11
+ status?: "completed" | "pending" | "processing" | "failed" | "dismissed" | undefined;
12
12
  limit?: number | undefined;
13
13
  job_id?: string | undefined;
14
14
  }, {
15
- status?: "pending" | "processing" | "completed" | "failed" | "dismissed" | undefined;
15
+ status?: "completed" | "pending" | "processing" | "failed" | "dismissed" | undefined;
16
16
  limit?: number | undefined;
17
17
  job_id?: string | undefined;
18
18
  }>;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, compactText, formatDateTime, mdJson, mdKeyValue, mdSection, mdTable, mdTitle, statusLabel } from '../lib/format.js';
2
3
  export const getVideoJobTool = {
3
4
  name: 'get_video_job',
4
5
  description: 'Get one Veo video generation job by ID, or list recent video jobs when job_id is omitted. Use after generate_video to poll for completed video_url.',
@@ -13,19 +14,35 @@ export const getVideoJobTool = {
13
14
  return formatJob(result.job, true);
14
15
  const jobs = result.jobs || [];
15
16
  return [
16
- `Video jobs (${jobs.length})`,
17
- jobs.map((job) => formatJob(job, false)).join('\n\n'),
17
+ mdTitle(`Video jobs (${jobs.length})`),
18
+ mdTable(['Job ID', 'Status', 'Prompt', 'Model', 'Created', 'Cost'], jobs.map((job) => [
19
+ code(job.id),
20
+ statusLabel(job.status),
21
+ compactText(job.prompt, 70),
22
+ job.model,
23
+ formatDateTime(job.created_at),
24
+ job.credit_cost,
25
+ ])),
18
26
  ].filter(Boolean).join('\n');
19
27
  },
20
28
  };
21
29
  function formatJob(job, includeRaw) {
22
- const lines = [
23
- `${job.id} ${job.status}`,
24
- `Prompt: ${job.prompt}`,
25
- `Model: ${job.model}; duration: ${job.duration_seconds}s; resolution: ${job.resolution || 'n/a'}; cost: ${job.credit_cost}`,
26
- job.video_url ? `Video URL: ${job.video_url}` : null,
27
- job.error_message ? `Error: ${job.error_message}` : null,
28
- includeRaw ? `\nRaw job:\n${JSON.stringify(job, null, 2)}` : null,
29
- ];
30
- return lines.filter(Boolean).join('\n');
30
+ return [
31
+ mdTitle(`Video job ${code(job.id)}`),
32
+ mdKeyValue([
33
+ ['Status', statusLabel(job.status)],
34
+ ['Model', job.model],
35
+ ['Duration', `${job.duration_seconds}s`],
36
+ ['Aspect ratio', job.aspect_ratio],
37
+ ['Resolution', job.resolution || 'n/a'],
38
+ ['Credit cost', job.credit_cost],
39
+ ['Created', formatDateTime(job.created_at)],
40
+ ['Started', formatDateTime(job.started_at)],
41
+ ['Completed', formatDateTime(job.completed_at)],
42
+ ['Video URL', job.video_url || undefined],
43
+ ['Error', job.error_message || undefined],
44
+ ]),
45
+ mdSection('Prompt', job.prompt),
46
+ includeRaw ? mdJson('Raw job', job) : '',
47
+ ].filter(Boolean).join('\n\n');
31
48
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdJson, mdKeyValue, mdSection, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const getVideoOptionsTool = {
3
4
  name: 'get_video_options',
4
5
  description: 'List read-only Veo video generation options, input modes, durations, resolutions, aspect ratios, and credit-cost estimates. This does not generate video or spend credits.',
@@ -7,20 +8,25 @@ export const getVideoOptionsTool = {
7
8
  const options = await client.getVideoOptions();
8
9
  const models = Array.isArray(options.models) ? options.models : [];
9
10
  const inputModes = Array.isArray(options.input_modes) ? options.input_modes : [];
10
- const lines = [
11
- 'Video generation options',
12
- `Provider: ${options.provider || 'unknown'}`,
13
- `Public generation status: ${options.public_generation_status || 'unknown'}`,
14
- '',
15
- `Models: ${models.map((model) => model.id).filter(Boolean).join(', ') || 'none'}`,
16
- `Durations: ${(options.durations_seconds || []).join(', ')} seconds`,
17
- `Resolutions: ${(options.resolutions || []).join(', ')}`,
18
- `Aspect ratios: ${(options.aspect_ratios || []).join(', ')}`,
19
- `Input modes: ${inputModes.map((mode) => mode.id).filter(Boolean).join(', ') || 'none'}`,
20
- '',
21
- 'Raw options:',
22
- JSON.stringify(options, null, 2),
23
- ];
24
- return lines.join('\n');
11
+ return [
12
+ mdTitle('Video generation options'),
13
+ mdKeyValue([
14
+ ['Provider', options.provider || 'unknown'],
15
+ ['Public generation status', options.public_generation_status || 'unknown'],
16
+ ['Durations', `${(options.durations_seconds || []).join(', ')} seconds`],
17
+ ['Resolutions', (options.resolutions || []).join(', ')],
18
+ ['Aspect ratios', (options.aspect_ratios || []).join(', ')],
19
+ ]),
20
+ mdSection('Models', mdTable(['Model', 'Label', 'Credit cost'], models.map((model) => [
21
+ code(model.id),
22
+ model.label || model.name || 'n/a',
23
+ model.credit_cost ?? model.cost ?? 'n/a',
24
+ ]))),
25
+ mdSection('Input modes', mdTable(['Mode', 'Label'], inputModes.map((mode) => [
26
+ code(mode.id),
27
+ mode.label || mode.name || 'n/a',
28
+ ]))),
29
+ mdJson('Raw options', options),
30
+ ].filter(Boolean).join('\n\n');
25
31
  },
26
32
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdKeyValue, mdTitle, statusLabel } from '../lib/format.js';
2
3
  export const getXPostingQuotaTool = {
3
4
  name: 'get_x_posting_quota',
4
5
  description: 'Get managed X posting quota for a workspace, including total monthly allowance, used posts, remaining posts, and URL-blocking status.',
@@ -8,14 +9,16 @@ export const getXPostingQuotaTool = {
8
9
  async execute(client, input) {
9
10
  const quota = await client.getXPostingQuota(input);
10
11
  return [
11
- 'X posting quota',
12
- `• Workspace: ${quota.workspace_id || 'default'}`,
13
- `• Tier: ${quota.tier || 'unknown'}`,
14
- `• Total posts: ${quota.total_posts}`,
15
- `• Used posts: ${quota.used_posts}`,
16
- `• Remaining posts: ${quota.remaining_posts}`,
17
- `• URL posting blocked: ${quota.urls_blocked ? 'yes' : 'no'}`,
18
- `• Period: ${quota.current_period_start} -> ${quota.current_period_end}`,
19
- ].join('\n');
12
+ mdTitle('X posting quota'),
13
+ mdKeyValue([
14
+ ['Workspace', quota.workspace_id ? code(quota.workspace_id) : 'default'],
15
+ ['Tier', quota.tier || 'unknown'],
16
+ ['Total posts', quota.total_posts],
17
+ ['Used posts', quota.used_posts],
18
+ ['Remaining posts', quota.remaining_posts],
19
+ ['URL posting', statusLabel(quota.urls_blocked ? 'blocked' : 'available')],
20
+ ['Period', `${quota.current_period_start} → ${quota.current_period_end}`],
21
+ ]),
22
+ ].join('\n\n');
20
23
  },
21
24
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const listAccountsTool = {
3
4
  name: 'list_accounts',
4
5
  description: 'List connected social media accounts. Returns platform, username, account ID, and workspace ID for each. Pass workspace_id to filter to a specific workspace; otherwise returns all accounts the caller owns across every workspace.',
@@ -12,10 +13,17 @@ export const listAccountsTool = {
12
13
  const accounts = await client.listAccounts({ workspace_id: input.workspace_id });
13
14
  if (accounts.length === 0) {
14
15
  return input.workspace_id
15
- ? 'No connected accounts found in this workspace.'
16
- : 'No connected accounts found. Connect accounts in the posterly dashboard first.';
16
+ ? mdEmpty('connected accounts', 'This workspace does not have any social accounts connected yet.')
17
+ : mdEmpty('connected accounts', 'Posterly does not see any connected social accounts yet.', 'Connect accounts in the Posterly dashboard, then run this again.');
17
18
  }
18
- const lines = accounts.map((a) => `• ${a.platform} — @${a.username} (ID: ${a.id}${a.workspace_id ? `, ws: ${a.workspace_id}` : ''})`);
19
- return `Connected accounts (${accounts.length}):\n${lines.join('\n')}`;
19
+ return [
20
+ mdTitle(`Connected accounts (${accounts.length})`),
21
+ mdTable(['Platform', 'Account', 'Account ID', 'Workspace'], accounts.map((account) => [
22
+ account.platform,
23
+ `@${account.username}`,
24
+ code(account.id),
25
+ account.workspace_id ? code(account.workspace_id) : 'default',
26
+ ])),
27
+ ].join('\n\n');
20
28
  },
21
29
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { compactText, formatDateTime, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const listActivityTool = {
3
4
  name: 'list_activity',
4
5
  description: 'List recent activity and publish events for posts, including status changes, publish attempts, failures, and retries. Use this as the agent notifications feed.',
@@ -12,24 +13,18 @@ export const listActivityTool = {
12
13
  async execute(client, input) {
13
14
  const result = await client.listActivity(input);
14
15
  if (result.items.length === 0) {
15
- return 'No activity found matching your filters.';
16
+ return mdEmpty('activity', 'No activity matched those filters.');
16
17
  }
17
- const lines = [
18
- `Activity (${result.items.length} of ${result.total}):`,
19
- ];
20
- for (const item of result.items) {
21
- const when = item.created_at ? new Date(item.created_at).toLocaleString() : 'unknown time';
22
- if (item.type === 'publish') {
23
- const outcome = item.outcome || 'publish';
24
- const platform = item.platform ? ` on ${item.platform}` : '';
25
- const detail = item.error_message ? ` - ${item.error_message}` : '';
26
- lines.push(`- [${when}] post #${item.post_id || '?'} ${outcome}${platform}${detail}`);
27
- }
28
- else {
29
- const label = item.description || item.action || 'activity';
30
- lines.push(`- [${when}] post #${item.post_id || '?'} ${label}`);
31
- }
32
- }
33
- return lines.join('\n');
18
+ return [
19
+ mdTitle(`Activity (${result.items.length} of ${result.total})`),
20
+ mdTable(['When', 'Source', 'Post', 'Platform', 'Event', 'Detail'], result.items.map((item) => [
21
+ formatDateTime(item.created_at),
22
+ item.type,
23
+ item.post_id ?? '?',
24
+ item.platform || 'n/a',
25
+ item.type === 'publish' ? item.outcome || 'publish' : item.action || 'activity',
26
+ compactText(item.error_message || item.description, 120) || 'n/a',
27
+ ])),
28
+ ].join('\n\n');
34
29
  },
35
30
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { code, mdEmpty, mdTable, mdTitle } from '../lib/format.js';
2
3
  export const listBrandAccountsTool = {
3
4
  name: 'list_brand_accounts',
4
5
  description: 'List the connected social accounts assigned to a brand/client. Use this when a user refers to a brand name rather than a raw account handle.',
@@ -8,9 +9,16 @@ export const listBrandAccountsTool = {
8
9
  async execute(client, input) {
9
10
  const accounts = await client.listBrandAccounts(input.brand_id);
10
11
  if (accounts.length === 0) {
11
- return 'No social accounts are currently assigned to this brand.';
12
+ return mdEmpty('brand accounts', 'No social accounts are currently assigned to this brand.');
12
13
  }
13
- const lines = accounts.map((account) => `• ${account.platform} — @${account.username} (ID: ${account.id}${account.workspace_id ? `, ws: ${account.workspace_id}` : ''})`);
14
- return `Brand accounts (${accounts.length}):\n${lines.join('\n')}`;
14
+ return [
15
+ mdTitle(`Brand accounts (${accounts.length})`),
16
+ mdTable(['Platform', 'Account', 'Account ID', 'Workspace'], accounts.map((account) => [
17
+ account.platform,
18
+ `@${account.username}`,
19
+ code(account.id),
20
+ account.workspace_id ? code(account.workspace_id) : 'default',
21
+ ])),
22
+ ].join('\n\n');
15
23
  },
16
24
  };